diff --git a/daemons/attrd/attrd_messages.c b/daemons/attrd/attrd_messages.c
index 4ea06f0471..5d31c16733 100644
--- a/daemons/attrd/attrd_messages.c
+++ b/daemons/attrd/attrd_messages.c
@@ -1,219 +1,262 @@
 /*
  * Copyright 2022 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <glib.h>
 
 #include <crm/common/messages_internal.h>
+#include <crm/msg_xml.h>
 
 #include "pacemaker-attrd.h"
 
+int minimum_protocol_version = -1;
+
 static GHashTable *attrd_handlers = NULL;
 
 static xmlNode *
 handle_unknown_request(pcmk__request_t *request)
 {
     crm_err("Unknown IPC request %s from %s %s",
             request->op, pcmk__request_origin_type(request),
             pcmk__request_origin(request));
     pcmk__format_result(&request->result, CRM_EX_PROTOCOL, PCMK_EXEC_INVALID,
                         "Unknown request type '%s' (bug?)", request->op);
     return NULL;
 }
 
 static xmlNode *
 handle_clear_failure_request(pcmk__request_t *request)
 {
     if (request->peer != NULL) {
         /* It is not currently possible to receive this as a peer command,
          * but will be, if we one day enable propagating this operation.
          */
         attrd_peer_clear_failure(request);
         pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL);
         return NULL;
     } else {
         return attrd_client_clear_failure(request);
     }
 }
 
 static xmlNode *
 handle_flush_request(pcmk__request_t *request)
 {
     if (request->peer != NULL) {
         /* Ignore. The flush command was removed in 2.0.0 but may be
          * received from peers running older versions.
          */
         pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL);
         return NULL;
     } else {
         return handle_unknown_request(request);
     }
 }
 
 static xmlNode *
 handle_query_request(pcmk__request_t *request)
 {
     if (request->peer != NULL) {
         return handle_unknown_request(request);
     } else {
         return attrd_client_query(request);
     }
 }
 
 static xmlNode *
 handle_remove_request(pcmk__request_t *request)
 {
     if (request->peer != NULL) {
         const char *host = crm_element_value(request->xml, PCMK__XA_ATTR_NODE_NAME);
         attrd_peer_remove(host, true, request->peer);
         pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL);
         return NULL;
     } else {
         return attrd_client_peer_remove(request);
     }
 }
 
 static xmlNode *
 handle_refresh_request(pcmk__request_t *request)
 {
     if (request->peer != NULL) {
         return handle_unknown_request(request);
     } else {
         return attrd_client_refresh(request);
     }
 }
 
 static xmlNode *
 handle_sync_request(pcmk__request_t *request)
 {
     if (request->peer != NULL) {
         crm_node_t *peer = crm_get_peer(0, request->peer);
 
         attrd_peer_sync(peer, request->xml);
         pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL);
         return NULL;
     } else {
         return handle_unknown_request(request);
     }
 }
 
 static xmlNode *
 handle_sync_response_request(pcmk__request_t *request)
 {
     if (request->ipc_client != NULL) {
         return handle_unknown_request(request);
     } else {
         if (request->peer != NULL) {
             crm_node_t *peer = crm_get_peer(0, request->peer);
             bool peer_won = attrd_check_for_new_writer(peer, request->xml);
 
             if (!pcmk__str_eq(peer->uname, attrd_cluster->uname, pcmk__str_casei)) {
                 attrd_peer_sync_response(peer, peer_won, request->xml);
             }
         }
 
         pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL);
         return NULL;
     }
 }
 
 static xmlNode *
 handle_update_request(pcmk__request_t *request)
 {
     if (request->peer != NULL) {
         const char *host = crm_element_value(request->xml, PCMK__XA_ATTR_NODE_NAME);
         crm_node_t *peer = crm_get_peer(0, request->peer);
 
         attrd_peer_update(peer, request->xml, host, false);
         pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL);
         return NULL;
     } else {
         /* Because attrd_client_update can be called recursively, we send the ACK
          * here to ensure that the client only ever receives one.
          */
         attrd_send_ack(request->ipc_client, request->ipc_id,
                        request->flags|crm_ipc_client_response);
         return attrd_client_update(request);
     }
 }
 
 static void
 attrd_register_handlers(void)
 {
     pcmk__server_command_t handlers[] = {
         { PCMK__ATTRD_CMD_CLEAR_FAILURE, handle_clear_failure_request },
         { PCMK__ATTRD_CMD_FLUSH, handle_flush_request },
         { PCMK__ATTRD_CMD_PEER_REMOVE, handle_remove_request },
         { PCMK__ATTRD_CMD_QUERY, handle_query_request },
         { PCMK__ATTRD_CMD_REFRESH, handle_refresh_request },
         { PCMK__ATTRD_CMD_SYNC, handle_sync_request },
         { PCMK__ATTRD_CMD_SYNC_RESPONSE, handle_sync_response_request },
         { PCMK__ATTRD_CMD_UPDATE, handle_update_request },
         { PCMK__ATTRD_CMD_UPDATE_DELAY, handle_update_request },
         { PCMK__ATTRD_CMD_UPDATE_BOTH, handle_update_request },
         { NULL, handle_unknown_request },
     };
 
     attrd_handlers = pcmk__register_handlers(handlers);
 }
 
 void
 attrd_unregister_handlers(void)
 {
     if (attrd_handlers != NULL) {
         g_hash_table_destroy(attrd_handlers);
         attrd_handlers = NULL;
     }
 }
 
 void
 attrd_handle_request(pcmk__request_t *request)
 {
     xmlNode *reply = NULL;
     char *log_msg = NULL;
     const char *reason = NULL;
 
     if (attrd_handlers == NULL) {
         attrd_register_handlers();
     }
 
     reply = pcmk__process_request(request, attrd_handlers);
 
     if (reply != NULL) {
         crm_log_xml_trace(reply, "Reply");
 
         if (request->ipc_client != NULL) {
             pcmk__ipc_send_xml(request->ipc_client, request->ipc_id, reply,
                                request->ipc_flags);
         } else {
             crm_err("Not sending CPG reply to client");
         }
 
         free_xml(reply);
     }
 
     reason = request->result.exit_reason;
     log_msg = crm_strdup_printf("Processed %s request from %s %s: %s%s%s%s",
                                 request->op, pcmk__request_origin_type(request),
                                 pcmk__request_origin(request),
                                 pcmk_exec_status_str(request->result.execution_status),
                                 (reason == NULL)? "" : " (",
                                 pcmk__s(reason, ""),
                                 (reason == NULL)? "" : ")");
 
     if (!pcmk__result_ok(&request->result)) {
         crm_warn("%s", log_msg);
     } else {
         crm_debug("%s", log_msg);
     }
 
     free(log_msg);
     pcmk__reset_request(request);
 }
+
+/*!
+    \internal
+    \brief Broadcast private attribute for local node with protocol version
+*/
+void
+attrd_broadcast_protocol(void)
+{
+    char *host = strdup(attrd_cluster->uname);
+
+    xmlNode *attrd_op = create_xml_node(NULL, __func__);
+
+    CRM_ASSERT(host != NULL);
+
+    crm_xml_add(attrd_op, F_TYPE, T_ATTRD);
+    crm_xml_add(attrd_op, F_ORIG, crm_system_name);
+    crm_xml_add(attrd_op, PCMK__XA_TASK, PCMK__ATTRD_CMD_UPDATE);
+    crm_xml_add(attrd_op, PCMK__XA_ATTR_NAME, CRM_ATTR_PROTOCOL);
+    crm_xml_add(attrd_op, PCMK__XA_ATTR_VALUE, ATTRD_PROTOCOL_VERSION);
+    crm_xml_add_int(attrd_op, PCMK__XA_ATTR_IS_PRIVATE, 1);
+    crm_xml_add(attrd_op, PCMK__XA_ATTR_NODE_NAME, host);
+    crm_xml_add_int(attrd_op, PCMK__XA_ATTR_NODE_ID, attrd_cluster->nodeid);
+
+    crm_debug("Broadcasting attrd protocol version %s for node %s",
+              ATTRD_PROTOCOL_VERSION, host);
+
+    attrd_send_message(NULL, attrd_op); /* ends up at attrd_peer_message() */
+
+    free(host);
+    free_xml(attrd_op);
+}
+
+gboolean
+attrd_send_message(crm_node_t * node, xmlNode * data)
+{
+    crm_xml_add(data, F_TYPE, T_ATTRD);
+    crm_xml_add(data, PCMK__XA_ATTR_VERSION, ATTRD_PROTOCOL_VERSION);
+    attrd_xml_add_writer(data);
+    return send_cluster_message(node, crm_msg_attrd, data, TRUE);
+}
diff --git a/daemons/attrd/attrd_utils.c b/daemons/attrd/attrd_utils.c
index abee50293c..6a1900997b 100644
--- a/daemons/attrd/attrd_utils.c
+++ b/daemons/attrd/attrd_utils.c
@@ -1,328 +1,286 @@
 /*
  * Copyright 2004-2022 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <stdio.h>
 #include <stdbool.h>
 #include <errno.h>
 #include <glib.h>
 #include <regex.h>
 #include <sys/types.h>
 
 #include <crm/crm.h>
 #include <crm/common/ipc_internal.h>
 #include <crm/common/mainloop.h>
 #include <crm/msg_xml.h>
 
 #include "pacemaker-attrd.h"
 
 cib_t *the_cib = NULL;
 
 static bool requesting_shutdown = false;
 static bool shutting_down = false;
 static GMainLoop *mloop = NULL;
 
-int minimum_protocol_version = -1;
-
-/*!
-    \internal
-    \brief Broadcast private attribute for local node with protocol version
-*/
-void
-attrd_broadcast_protocol(void)
-{
-    char *host = strdup(attrd_cluster->uname);
-
-    xmlNode *attrd_op = create_xml_node(NULL, __func__);
-
-    CRM_ASSERT(host != NULL);
-
-    crm_xml_add(attrd_op, F_TYPE, T_ATTRD);
-    crm_xml_add(attrd_op, F_ORIG, crm_system_name);
-    crm_xml_add(attrd_op, PCMK__XA_TASK, PCMK__ATTRD_CMD_UPDATE);
-    crm_xml_add(attrd_op, PCMK__XA_ATTR_NAME, CRM_ATTR_PROTOCOL);
-    crm_xml_add(attrd_op, PCMK__XA_ATTR_VALUE, ATTRD_PROTOCOL_VERSION);
-    crm_xml_add_int(attrd_op, PCMK__XA_ATTR_IS_PRIVATE, 1);
-    crm_xml_add(attrd_op, PCMK__XA_ATTR_NODE_NAME, host);
-    crm_xml_add_int(attrd_op, PCMK__XA_ATTR_NODE_ID, attrd_cluster->nodeid);
-
-    crm_debug("Broadcasting attrd protocol version %s for node %s",
-              ATTRD_PROTOCOL_VERSION, host);
-
-    attrd_send_message(NULL, attrd_op); /* ends up at attrd_peer_message() */
-
-    free(host);
-    free_xml(attrd_op);
-}
-
 /*!
  * \internal
  * \brief  Set requesting_shutdown state
  */
 void
 attrd_set_requesting_shutdown(void)
 {
     requesting_shutdown = true;
 }
 
 /*!
  * \internal
  * \brief  Clear requesting_shutdown state
  */
 void
 attrd_clear_requesting_shutdown(void)
 {
     requesting_shutdown = false;
 }
 
 /*!
  * \internal
  * \brief Check whether we're currently requesting shutdown
  *
  * \return true if requesting shutdown, false otherwise
  */
 bool
 attrd_requesting_shutdown(void)
 {
     return requesting_shutdown;
 }
 
 /*!
  * \internal
  * \brief Check whether we're currently shutting down
  *
  * \return true if shutting down, false otherwise
  */
 bool
 attrd_shutting_down(void)
 {
     return shutting_down;
 }
 
 /*!
  * \internal
  * \brief  Exit (using mainloop or not, as appropriate)
  *
  * \param[in] nsig  Ignored
  */
 void
 attrd_shutdown(int nsig)
 {
     // Tell various functions not to do anthing
     shutting_down = true;
 
     // Don't respond to signals while shutting down
     mainloop_destroy_signal(SIGTERM);
     mainloop_destroy_signal(SIGCHLD);
     mainloop_destroy_signal(SIGPIPE);
     mainloop_destroy_signal(SIGUSR1);
     mainloop_destroy_signal(SIGUSR2);
     mainloop_destroy_signal(SIGTRAP);
 
     if ((mloop == NULL) || !g_main_loop_is_running(mloop)) {
         /* If there's no main loop active, just exit. This should be possible
          * only if we get SIGTERM in brief windows at start-up and shutdown.
          */
         crm_exit(CRM_EX_OK);
     } else {
         g_main_loop_quit(mloop);
         g_main_loop_unref(mloop);
     }
 }
 
 /*!
  * \internal
  * \brief Create a main loop for attrd
  */
 void
 attrd_init_mainloop(void)
 {
     mloop = g_main_loop_new(NULL, FALSE);
 }
 
 /*!
  * \internal
  * \brief Run attrd main loop
  */
 void
 attrd_run_mainloop(void)
 {
     g_main_loop_run(mloop);
 }
 
 void
 attrd_cib_disconnect(void)
 {
     CRM_CHECK(the_cib != NULL, return);
     the_cib->cmds->del_notify_callback(the_cib, T_CIB_REPLACE_NOTIFY, attrd_cib_replaced_cb);
     the_cib->cmds->del_notify_callback(the_cib, T_CIB_DIFF_NOTIFY, attrd_cib_updated_cb);
     cib__clean_up_connection(&the_cib);
 }
 
 void
 attrd_cib_replaced_cb(const char *event, xmlNode * msg)
 {
     int change_section = cib_change_section_nodes | cib_change_section_status | cib_change_section_alerts;
 
     if (attrd_requesting_shutdown() || attrd_shutting_down()) {
         return;
     }
 
     crm_element_value_int(msg, F_CIB_CHANGE_SECTION, &change_section);
 
     if (attrd_election_won()) {
         if (change_section & (cib_change_section_nodes | cib_change_section_status)) {
             crm_notice("Updating all attributes after %s event", event);
             attrd_write_attributes(true, false);
         }
     }
 
     if (change_section & cib_change_section_alerts) {
         // Check for changes in alerts
         mainloop_set_trigger(attrd_config_read);
     }
 }
 
 /* strlen("value") */
 #define plus_plus_len (5)
 
 /*!
  * \internal
  * \brief  Check whether an attribute value should be expanded
  *
  * \param[in] value  Attribute value to check
  *
  * \return true if value needs expansion, false otherwise
  */
 bool
 attrd_value_needs_expansion(const char *value)
 {
     return ((strlen(value) >= (plus_plus_len + 2))
            && (value[plus_plus_len] == '+')
            && ((value[plus_plus_len + 1] == '+')
                || (value[plus_plus_len + 1] == '=')));
 }
 
 /*!
  * \internal
  * \brief Expand an increment expression into an integer
  *
  * \param[in] value      Attribute increment expression to expand
  * \param[in] old_value  Previous value of attribute
  *
  * \return Expanded value
  */
 int
 attrd_expand_value(const char *value, const char *old_value)
 {
     int offset = 1;
     int int_value = char2score(old_value);
 
     if (value[plus_plus_len + 1] != '+') {
         const char *offset_s = value + (plus_plus_len + 2);
 
         offset = char2score(offset_s);
     }
     int_value += offset;
 
     if (int_value > INFINITY) {
         int_value = INFINITY;
     }
     return int_value;
 }
 
 /*!
  * \internal
  * \brief Create regular expression matching failure-related attributes
  *
  * \param[out] regex  Where to store created regular expression
  * \param[in]  rsc    Name of resource to clear (or NULL for all)
  * \param[in]  op     Operation to clear if rsc is specified (or NULL for all)
  * \param[in]  interval_ms  Interval of operation to clear if op is specified
  *
  * \return pcmk_ok on success, -EINVAL if arguments are invalid
  *
  * \note The caller is responsible for freeing the result with regfree().
  */
 int
 attrd_failure_regex(regex_t *regex, const char *rsc, const char *op,
                     guint interval_ms)
 {
     char *pattern = NULL;
     int rc;
 
     /* Create a pattern that matches desired attributes */
 
     if (rsc == NULL) {
         pattern = strdup(ATTRD_RE_CLEAR_ALL);
     } else if (op == NULL) {
         pattern = crm_strdup_printf(ATTRD_RE_CLEAR_ONE, rsc);
     } else {
         pattern = crm_strdup_printf(ATTRD_RE_CLEAR_OP, rsc, op, interval_ms);
     }
 
     /* Compile pattern into regular expression */
     crm_trace("Clearing attributes matching %s", pattern);
     rc = regcomp(regex, pattern, REG_EXTENDED|REG_NOSUB);
     free(pattern);
 
     return (rc == 0)? pcmk_ok : -EINVAL;
 }
 
 void
 attrd_free_attribute_value(gpointer data)
 {
     attribute_value_t *v = data;
 
     free(v->nodename);
     free(v->current);
     free(v->requested);
     free(v);
 }
 
 void
 attrd_free_attribute(gpointer data)
 {
     attribute_t *a = data;
     if(a) {
         free(a->id);
         free(a->set);
         free(a->uuid);
         free(a->user);
 
         mainloop_timer_del(a->timer);
         g_hash_table_destroy(a->values);
 
         free(a);
     }
 }
 
-gboolean
-attrd_send_message(crm_node_t * node, xmlNode * data)
-{
-    crm_xml_add(data, F_TYPE, T_ATTRD);
-    crm_xml_add(data, PCMK__XA_ATTR_VERSION, ATTRD_PROTOCOL_VERSION);
-    attrd_xml_add_writer(data);
-    return send_cluster_message(node, crm_msg_attrd, data, TRUE);
-}
-
 void
 attrd_update_minimum_protocol_ver(const char *value)
 {
     int ver;
 
     pcmk__scan_min_int(value, &ver, 0);
 
     if (ver > 0 && (minimum_protocol_version == -1 || ver < minimum_protocol_version)) {
         minimum_protocol_version = ver;
         crm_trace("Set minimum attrd protocol version to %d",
                   minimum_protocol_version);
     }
 }