diff --git a/daemons/based/based_callbacks.c b/daemons/based/based_callbacks.c
index 4e184de578..5e695eefff 100644
--- a/daemons/based/based_callbacks.c
+++ b/daemons/based/based_callbacks.c
@@ -1,1390 +1,1391 @@
 /*
  * Copyright 2004-2025 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 <sys/param.h>
 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 
 #include <stdlib.h>
 #include <stdint.h>     // uint32_t, uint64_t, UINT64_C()
 #include <errno.h>
 #include <fcntl.h>
 #include <inttypes.h>   // PRIu64
 
 #include <glib.h>
 #include <libxml/tree.h>
 #include <libxml/xpath.h>               // xmlXPathObject, etc.
 
 #include <crm/crm.h>
 #include <crm/cib.h>
 #include <crm/cluster/internal.h>
 
 #include <crm/common/xml.h>
 #include <crm/common/remote_internal.h>
 
 #include <pacemaker-based.h>
 
 #define EXIT_ESCALATION_MS 10000
 
 qb_ipcs_service_t *ipcs_ro = NULL;
 qb_ipcs_service_t *ipcs_rw = NULL;
 qb_ipcs_service_t *ipcs_shm = NULL;
 
 static int cib_process_command(xmlNode *request,
                                const cib__operation_t *operation,
                                cib__op_fn_t op_function, xmlNode **reply,
                                xmlNode **cib_diff, bool privileged);
 
 static gboolean cib_common_callback(qb_ipcs_connection_t *c, void *data,
                                     size_t size, gboolean privileged);
 
 static int32_t
 cib_ipc_accept(qb_ipcs_connection_t * c, uid_t uid, gid_t gid)
 {
     if (cib_shutdown_flag) {
         crm_info("Ignoring new IPC client [%d] during shutdown",
                  pcmk__client_pid(c));
         return -ECONNREFUSED;
     }
 
     if (pcmk__new_client(c, uid, gid) == NULL) {
         return -ENOMEM;
     }
     return 0;
 }
 
 static int32_t
 cib_ipc_dispatch_rw(qb_ipcs_connection_t * c, void *data, size_t size)
 {
     pcmk__client_t *client = pcmk__find_client(c);
 
     crm_trace("%p message from %s", c, client->id);
     return cib_common_callback(c, data, size, TRUE);
 }
 
 static int32_t
 cib_ipc_dispatch_ro(qb_ipcs_connection_t * c, void *data, size_t size)
 {
     pcmk__client_t *client = pcmk__find_client(c);
 
     crm_trace("%p message from %s", c, client->id);
     return cib_common_callback(c, data, size, FALSE);
 }
 
 /* Error code means? */
 static int32_t
 cib_ipc_closed(qb_ipcs_connection_t * c)
 {
     pcmk__client_t *client = pcmk__find_client(c);
 
     if (client == NULL) {
         return 0;
     }
     crm_trace("Connection %p", c);
     pcmk__free_client(client);
     return 0;
 }
 
 static void
 cib_ipc_destroy(qb_ipcs_connection_t * c)
 {
     crm_trace("Connection %p", c);
     cib_ipc_closed(c);
     if (cib_shutdown_flag) {
         cib_shutdown(0);
     }
 }
 
 struct qb_ipcs_service_handlers ipc_ro_callbacks = {
     .connection_accept = cib_ipc_accept,
     .connection_created = NULL,
     .msg_process = cib_ipc_dispatch_ro,
     .connection_closed = cib_ipc_closed,
     .connection_destroyed = cib_ipc_destroy
 };
 
 struct qb_ipcs_service_handlers ipc_rw_callbacks = {
     .connection_accept = cib_ipc_accept,
     .connection_created = NULL,
     .msg_process = cib_ipc_dispatch_rw,
     .connection_closed = cib_ipc_closed,
     .connection_destroyed = cib_ipc_destroy
 };
 
 /*!
  * \internal
  * \brief Create reply XML for a CIB request
  *
  * \param[in] op            CIB operation type
  * \param[in] call_id       CIB call ID
  * \param[in] client_id     CIB client ID
  * \param[in] call_options  Group of <tt>enum cib_call_options</tt> flags
  * \param[in] rc            Request return code
  * \param[in] call_data     Request output data
  *
  * \return Reply XML (guaranteed not to be \c NULL)
  *
  * \note The caller is responsible for freeing the return value using
  *       \p pcmk__xml_free().
  */
 static xmlNode *
 create_cib_reply(const char *op, const char *call_id, const char *client_id,
                  uint32_t call_options, int rc, xmlNode *call_data)
 {
     xmlNode *reply = pcmk__xe_create(NULL, PCMK__XE_CIB_REPLY);
 
     pcmk__xe_set(reply, PCMK__XA_T, PCMK__VALUE_CIB);
     pcmk__xe_set(reply, PCMK__XA_CIB_OP, op);
     pcmk__xe_set(reply, PCMK__XA_CIB_CALLID, call_id);
     pcmk__xe_set(reply, PCMK__XA_CIB_CLIENTID, client_id);
     pcmk__xe_set_int(reply, PCMK__XA_CIB_CALLOPT, call_options);
     pcmk__xe_set_int(reply, PCMK__XA_CIB_RC, rc);
 
     if (call_data != NULL) {
         xmlNode *wrapper = pcmk__xe_create(reply, PCMK__XE_CIB_CALLDATA);
 
         crm_trace("Attaching reply output");
         pcmk__xml_copy(wrapper, call_data);
     }
 
     crm_log_xml_explicit(reply, "cib:reply");
     return reply;
 }
 
 static void
 do_local_notify(const xmlNode *notify_src, const char *client_id,
                 bool sync_reply, bool from_peer)
 {
     int msg_id = 0;
     int rc = pcmk_rc_ok;
     pcmk__client_t *client_obj = NULL;
     uint32_t flags = crm_ipc_server_event;
 
     CRM_CHECK((notify_src != NULL) && (client_id != NULL), return);
 
     pcmk__xe_get_int(notify_src, PCMK__XA_CIB_CALLID, &msg_id);
 
     client_obj = pcmk__find_client_by_id(client_id);
     if (client_obj == NULL) {
         crm_debug("Could not notify client %s%s %s of call %d result: "
                   "client no longer exists", client_id,
                   (from_peer? " (originator of delegated request)" : ""),
                   (sync_reply? "synchronously" : "asynchronously"), msg_id);
         return;
     }
 
     if (sync_reply) {
         flags = crm_ipc_flags_none;
         if (client_obj->ipcs != NULL) {
             msg_id = client_obj->request_id;
             client_obj->request_id = 0;
         }
     }
 
     switch (PCMK__CLIENT_TYPE(client_obj)) {
         case pcmk__client_ipc:
             rc = pcmk__ipc_send_xml(client_obj, msg_id, notify_src, flags);
             break;
         case pcmk__client_tls:
         case pcmk__client_tcp:
             rc = pcmk__remote_send_xml(client_obj->remote, notify_src);
             break;
         default:
             rc = EPROTONOSUPPORT;
             break;
     }
     if (rc == pcmk_rc_ok) {
         crm_trace("Notified %s client %s%s %s of call %d result",
                   pcmk__client_type_str(PCMK__CLIENT_TYPE(client_obj)),
                   pcmk__client_name(client_obj),
                   (from_peer? " (originator of delegated request)" : ""),
                   (sync_reply? "synchronously" : "asynchronously"), msg_id);
     } else {
         crm_warn("Could not notify %s client %s%s %s of call %d result: %s",
                  pcmk__client_type_str(PCMK__CLIENT_TYPE(client_obj)),
                  pcmk__client_name(client_obj),
                  (from_peer? " (originator of delegated request)" : ""),
                  (sync_reply? "synchronously" : "asynchronously"), msg_id,
                  pcmk_rc_str(rc));
     }
 }
 
 void
 cib_common_callback_worker(uint32_t id, uint32_t flags, xmlNode * op_request,
                            pcmk__client_t *cib_client, gboolean privileged)
 {
     const char *op = pcmk__xe_get(op_request, PCMK__XA_CIB_OP);
     uint32_t call_options = cib_none;
     int rc = pcmk_rc_ok;
 
     rc = pcmk__xe_get_flags(op_request, PCMK__XA_CIB_CALLOPT, &call_options,
                             cib_none);
     if (rc != pcmk_rc_ok) {
         crm_warn("Couldn't parse options from request: %s", pcmk_rc_str(rc));
     }
 
     /* Requests with cib_transaction set should not be sent to based directly
      * (outside of a commit-transaction request)
      */
     if (pcmk__is_set(call_options, cib_transaction)) {
         return;
     }
 
     if (pcmk__str_eq(op, CRM_OP_REGISTER, pcmk__str_none)) {
         if (flags & crm_ipc_client_response) {
             xmlNode *ack = pcmk__xe_create(NULL, __func__);
 
             pcmk__xe_set(ack, PCMK__XA_CIB_OP, CRM_OP_REGISTER);
             pcmk__xe_set(ack, PCMK__XA_CIB_CLIENTID, cib_client->id);
             pcmk__ipc_send_xml(cib_client, id, ack, flags);
             cib_client->request_id = 0;
             pcmk__xml_free(ack);
         }
         return;
 
     } else if (pcmk__str_eq(op, PCMK__VALUE_CIB_NOTIFY, pcmk__str_none)) {
         /* Update the notify filters for this client */
         int on_off = 0;
         crm_exit_t status = CRM_EX_OK;
         uint64_t bit = UINT64_C(0);
         const char *type = pcmk__xe_get(op_request, PCMK__XA_CIB_NOTIFY_TYPE);
 
         pcmk__xe_get_int(op_request, PCMK__XA_CIB_NOTIFY_ACTIVATE, &on_off);
 
         crm_debug("Setting %s callbacks %s for client %s",
                   type, (on_off? "on" : "off"), pcmk__client_name(cib_client));
 
         if (pcmk__str_eq(type, PCMK__VALUE_CIB_POST_NOTIFY, pcmk__str_none)) {
             bit = cib_notify_post;
 
         } else if (pcmk__str_eq(type, PCMK__VALUE_CIB_PRE_NOTIFY,
                                 pcmk__str_none)) {
             bit = cib_notify_pre;
 
         } else if (pcmk__str_eq(type, PCMK__VALUE_CIB_UPDATE_CONFIRMATION,
                                 pcmk__str_none)) {
             bit = cib_notify_confirm;
 
         } else if (pcmk__str_eq(type, PCMK__VALUE_CIB_DIFF_NOTIFY,
                                 pcmk__str_none)) {
             bit = cib_notify_diff;
 
         } else {
             status = CRM_EX_INVALID_PARAM;
         }
 
         if (bit != 0) {
             if (on_off) {
                 pcmk__set_client_flags(cib_client, bit);
             } else {
                 pcmk__clear_client_flags(cib_client, bit);
             }
         }
 
         pcmk__ipc_send_ack(cib_client, id, flags, PCMK__XE_ACK, NULL, status);
         return;
     }
 
     cib_process_request(op_request, privileged, cib_client);
 }
 
 int32_t
 cib_common_callback(qb_ipcs_connection_t * c, void *data, size_t size, gboolean privileged)
 {
     uint32_t id = 0;
     uint32_t flags = 0;
     uint32_t call_options = cib_none;
     pcmk__client_t *cib_client = pcmk__find_client(c);
     xmlNode *op_request = pcmk__client_data2xml(cib_client, data, &id, &flags);
 
     if (op_request) {
         int rc = pcmk_rc_ok;
 
         rc = pcmk__xe_get_flags(op_request, PCMK__XA_CIB_CALLOPT, &call_options,
                                 cib_none);
         if (rc != pcmk_rc_ok) {
             crm_warn("Couldn't parse options from request: %s",
                      pcmk_rc_str(rc));
         }
     }
 
     if (op_request == NULL) {
         crm_trace("Invalid message from %p", c);
         pcmk__ipc_send_ack(cib_client, id, flags, PCMK__XE_NACK, NULL,
                            CRM_EX_PROTOCOL);
         return 0;
 
     } else if(cib_client == NULL) {
         crm_trace("Invalid client %p", c);
         return 0;
     }
 
     if (pcmk__is_set(call_options, cib_sync_call)) {
         CRM_LOG_ASSERT(flags & crm_ipc_client_response);
         CRM_LOG_ASSERT(cib_client->request_id == 0);    /* This means the client has two synchronous events in-flight */
         cib_client->request_id = id;    /* Reply only to the last one */
     }
 
     if (cib_client->name == NULL) {
         const char *value = pcmk__xe_get(op_request, PCMK__XA_CIB_CLIENTNAME);
 
         if (value == NULL) {
             cib_client->name = pcmk__itoa(cib_client->pid);
         } else {
             cib_client->name = pcmk__str_copy(value);
             if (pcmk__parse_server(value) != pcmk_ipc_unknown) {
                 pcmk__set_client_flags(cib_client, cib_is_daemon);
             }
         }
     }
 
     /* Allow cluster daemons more leeway before being evicted */
     if (pcmk__is_set(cib_client->flags, cib_is_daemon)) {
         const char *qmax = cib_config_lookup(PCMK_OPT_CLUSTER_IPC_LIMIT);
 
         pcmk__set_client_queue_max(cib_client, qmax);
     }
 
     pcmk__xe_set(op_request, PCMK__XA_CIB_CLIENTID, cib_client->id);
     pcmk__xe_set(op_request, PCMK__XA_CIB_CLIENTNAME, cib_client->name);
 
     CRM_LOG_ASSERT(cib_client->user != NULL);
     pcmk__update_acl_user(op_request, PCMK__XA_CIB_USER, cib_client->user);
 
     cib_common_callback_worker(id, flags, op_request, cib_client, privileged);
     pcmk__xml_free(op_request);
 
     return 0;
 }
 
 static uint64_t ping_seq = 0;
 static char *ping_digest = NULL;
 static bool ping_modified_since = FALSE;
 
 static gboolean
 cib_digester_cb(gpointer data)
 {
     if (based_is_primary) {
         char buffer[32];
         xmlNode *ping = pcmk__xe_create(NULL, PCMK__XE_PING);
 
         ping_seq++;
         free(ping_digest);
         ping_digest = NULL;
         ping_modified_since = FALSE;
         snprintf(buffer, 32, "%" PRIu64, ping_seq);
         crm_trace("Requesting peer digests (%s)", buffer);
 
         pcmk__xe_set(ping, PCMK__XA_T, PCMK__VALUE_CIB);
         pcmk__xe_set(ping, PCMK__XA_CIB_OP, CRM_OP_PING);
         pcmk__xe_set(ping, PCMK__XA_CIB_PING_ID, buffer);
 
         pcmk__xe_set(ping, PCMK_XA_CRM_FEATURE_SET, CRM_FEATURE_SET);
         pcmk__cluster_send_message(NULL, pcmk_ipc_based, ping);
 
         pcmk__xml_free(ping);
     }
     return FALSE;
 }
 
 static void
 process_ping_reply(xmlNode *reply) 
 {
     uint64_t seq = 0;
     const char *host = pcmk__xe_get(reply, PCMK__XA_SRC);
 
     xmlNode *wrapper = pcmk__xe_first_child(reply, PCMK__XE_CIB_CALLDATA, NULL,
                                             NULL);
     xmlNode *pong = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
 
     const char *seq_s = pcmk__xe_get(pong, PCMK__XA_CIB_PING_ID);
     const char *digest = pcmk__xe_get(pong, PCMK__XA_DIGEST);
 
     if (seq_s == NULL) {
         crm_debug("Ignoring ping reply with no " PCMK__XA_CIB_PING_ID);
         return;
 
     } else {
         long long seq_ll;
         int rc = pcmk__scan_ll(seq_s, &seq_ll, 0LL);
 
         if (rc != pcmk_rc_ok) {
             crm_debug("Ignoring ping reply with invalid " PCMK__XA_CIB_PING_ID
                       " '%s': %s", seq_s, pcmk_rc_str(rc));
             return;
         }
         seq = (uint64_t) seq_ll;
     }
 
     if(digest == NULL) {
         crm_trace("Ignoring ping reply %s from %s with no digest", seq_s, host);
 
     } else if(seq != ping_seq) {
         crm_trace("Ignoring out of sequence ping reply %s from %s", seq_s, host);
 
     } else if(ping_modified_since) {
         crm_trace("Ignoring ping reply %s from %s: cib updated since", seq_s, host);
 
     } else {
         if(ping_digest == NULL) {
             crm_trace("Calculating new digest");
             ping_digest = pcmk__digest_xml(the_cib, true);
         }
 
         crm_trace("Processing ping reply %s from %s (%s)", seq_s, host, digest);
         if (!pcmk__str_eq(ping_digest, digest, pcmk__str_casei)) {
             xmlNode *wrapper = pcmk__xe_first_child(pong, PCMK__XE_CIB_CALLDATA,
                                                     NULL, NULL);
             xmlNode *remote_cib = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
 
             const char *admin_epoch_s = NULL;
             const char *epoch_s = NULL;
             const char *num_updates_s = NULL;
 
             if (remote_cib != NULL) {
                 admin_epoch_s = pcmk__xe_get(remote_cib, PCMK_XA_ADMIN_EPOCH);
                 epoch_s = pcmk__xe_get(remote_cib, PCMK_XA_EPOCH);
                 num_updates_s = pcmk__xe_get(remote_cib, PCMK_XA_NUM_UPDATES);
             }
 
             crm_notice("Local CIB %s.%s.%s.%s differs from %s: %s.%s.%s.%s %p",
                        pcmk__xe_get(the_cib, PCMK_XA_ADMIN_EPOCH),
                        pcmk__xe_get(the_cib, PCMK_XA_EPOCH),
                        pcmk__xe_get(the_cib, PCMK_XA_NUM_UPDATES),
                        ping_digest, host,
                        pcmk__s(admin_epoch_s, "_"),
                        pcmk__s(epoch_s, "_"),
                        pcmk__s(num_updates_s, "_"),
                        digest, remote_cib);
 
             if(remote_cib && remote_cib->children) {
                 // Additional debug
                 pcmk__xml_mark_changes(the_cib, remote_cib);
                 pcmk__log_xml_changes(LOG_INFO, remote_cib);
                 crm_trace("End of differences");
             }
 
             pcmk__xml_free(remote_cib);
             sync_our_cib(reply, FALSE);
         }
     }
 }
 
 static void
 parse_local_options(const pcmk__client_t *cib_client,
                     const cib__operation_t *operation,
                     const char *host, const char *op, gboolean *local_notify,
                     gboolean *needs_reply, gboolean *process,
                     gboolean *needs_forward)
 {
     // Process locally and notify local client
     *process = TRUE;
     *needs_reply = FALSE;
     *local_notify = TRUE;
     *needs_forward = FALSE;
 
     if (pcmk__is_set(operation->flags, cib__op_attr_local)) {
         /* Always process locally if cib__op_attr_local is set.
          *
          * @COMPAT: Currently host is ignored. At a compatibility break, throw
          * an error (from cib_process_request() or earlier) if host is not NULL or
          * OUR_NODENAME.
          */
         crm_trace("Processing always-local %s op from client %s",
                   op, pcmk__client_name(cib_client));
 
         if (!pcmk__str_eq(host, OUR_NODENAME,
                           pcmk__str_casei|pcmk__str_null_matches)) {
 
             crm_warn("Operation '%s' is always local but its target host is "
                      "set to '%s'",
                      op, host);
         }
         return;
     }
 
     if (pcmk__is_set(operation->flags, cib__op_attr_modifies)
         || !pcmk__str_eq(host, OUR_NODENAME,
                          pcmk__str_casei|pcmk__str_null_matches)) {
 
         // Forward modifying and non-local requests via cluster
         *process = FALSE;
         *needs_reply = FALSE;
         *local_notify = FALSE;
         *needs_forward = TRUE;
 
         crm_trace("%s op from %s needs to be forwarded to %s",
                   op, pcmk__client_name(cib_client),
                   pcmk__s(host, "all nodes"));
         return;
     }
 
     if (stand_alone) {
         crm_trace("Processing %s op from client %s (stand-alone)",
                   op, pcmk__client_name(cib_client));
 
     } else {
         crm_trace("Processing %saddressed %s op from client %s",
                   ((host != NULL)? "locally " : "un"),
                   op, pcmk__client_name(cib_client));
     }
 }
 
 static gboolean
 parse_peer_options(const cib__operation_t *operation, xmlNode *request,
                    gboolean *local_notify, gboolean *needs_reply,
                    gboolean *process)
 {
     /* TODO: What happens when an update comes in after node A
      * requests the CIB from node B, but before it gets the reply (and
      * sends out the replace operation)?
      *
      * (This may no longer be relevant since legacy mode was dropped; need to
      * trace code more closely to check.)
      */
     const char *host = NULL;
     const char *delegated = pcmk__xe_get(request, PCMK__XA_CIB_DELEGATED_FROM);
     const char *op = pcmk__xe_get(request, PCMK__XA_CIB_OP);
     const char *originator = pcmk__xe_get(request, PCMK__XA_SRC);
     const char *reply_to = pcmk__xe_get(request, PCMK__XA_CIB_ISREPLYTO);
 
     gboolean is_reply = pcmk__str_eq(reply_to, OUR_NODENAME, pcmk__str_casei);
 
     if (originator == NULL) { // Shouldn't be possible
         originator = "peer";
     }
 
     if (pcmk__str_eq(op, PCMK__CIB_REQUEST_REPLACE, pcmk__str_none)) {
         // sync_our_cib() sets PCMK__XA_CIB_ISREPLYTO
         if (reply_to) {
             delegated = reply_to;
         }
         goto skip_is_reply;
 
     } else if (pcmk__str_eq(op, PCMK__CIB_REQUEST_SYNC_TO_ALL,
                             pcmk__str_none)) {
         // Nothing to do
 
     } else if (is_reply && pcmk__str_eq(op, CRM_OP_PING, pcmk__str_casei)) {
         process_ping_reply(request);
         return FALSE;
 
     } else if (pcmk__str_eq(op, PCMK__CIB_REQUEST_UPGRADE, pcmk__str_none)) {
         /* Only the DC (node with the oldest software) should process
          * this operation if PCMK__XA_CIB_SCHEMA_MAX is unset.
          *
          * If the DC is happy it will then send out another
          * PCMK__CIB_REQUEST_UPGRADE which will tell all nodes to do the actual
          * upgrade.
          *
          * Except this time PCMK__XA_CIB_SCHEMA_MAX will be set which puts a
          * limit on how far newer nodes will go
          */
         const char *max = pcmk__xe_get(request, PCMK__XA_CIB_SCHEMA_MAX);
         const char *upgrade_rc = pcmk__xe_get(request, PCMK__XA_CIB_UPGRADE_RC);
 
         crm_trace("Parsing upgrade %s for %s with max=%s and upgrade_rc=%s",
                   (is_reply? "reply" : "request"),
                   (based_is_primary? "primary" : "secondary"),
                   pcmk__s(max, "none"), pcmk__s(upgrade_rc, "none"));
 
         if (upgrade_rc != NULL) {
             // Our upgrade request was rejected by DC, notify clients of result
             pcmk__xe_set(request, PCMK__XA_CIB_RC, upgrade_rc);
 
         } else if ((max == NULL) && based_is_primary) {
             /* We are the DC, check if this upgrade is allowed */
             goto skip_is_reply;
 
         } else if(max) {
             /* Ok, go ahead and upgrade to 'max' */
             goto skip_is_reply;
 
         } else {
             // Ignore broadcast client requests when we're not primary
             return FALSE;
         }
 
     } else if (pcmk__xe_attr_is_true(request, PCMK__XA_CIB_UPDATE)) {
         crm_info("Detected legacy %s global update from %s", op, originator);
         send_sync_request(NULL);
         return FALSE;
 
     } else if (is_reply
                && pcmk__is_set(operation->flags, cib__op_attr_modifies)) {
         crm_trace("Ignoring legacy %s reply sent from %s to local clients", op, originator);
         return FALSE;
 
     } else if (pcmk__str_eq(op, PCMK__CIB_REQUEST_SHUTDOWN, pcmk__str_none)) {
         *local_notify = FALSE;
         if (reply_to == NULL) {
             *process = TRUE;
         } else { // Not possible?
             crm_debug("Ignoring shutdown request from %s because reply_to=%s",
                       originator, reply_to);
         }
         return *process;
     }
 
     if (is_reply) {
         crm_trace("Will notify local clients for %s reply from %s",
                   op, originator);
         *process = FALSE;
         *needs_reply = FALSE;
         *local_notify = TRUE;
         return TRUE;
     }
 
   skip_is_reply:
     *process = TRUE;
     *needs_reply = FALSE;
 
     *local_notify = pcmk__str_eq(delegated, OUR_NODENAME, pcmk__str_casei);
 
     host = pcmk__xe_get(request, PCMK__XA_CIB_HOST);
     if (pcmk__str_eq(host, OUR_NODENAME, pcmk__str_casei)) {
         crm_trace("Processing %s request sent to us from %s", op, originator);
         *needs_reply = TRUE;
         return TRUE;
 
     } else if (host != NULL) {
         crm_trace("Ignoring %s request intended for CIB manager on %s",
                   op, host);
         return FALSE;
 
     } else if(is_reply == FALSE && pcmk__str_eq(op, CRM_OP_PING, pcmk__str_casei)) {
         *needs_reply = TRUE;
     }
 
     crm_trace("Processing %s request broadcast by %s call %s on %s "
               "(local clients will%s be notified)", op,
               pcmk__s(pcmk__xe_get(request, PCMK__XA_CIB_CLIENTNAME), "client"),
               pcmk__s(pcmk__xe_get(request, PCMK__XA_CIB_CALLID), "without ID"),
               originator, (*local_notify? "" : "not"));
     return TRUE;
 }
 
 /*!
  * \internal
  * \brief Forward a CIB request to the appropriate target host(s)
  *
  * \param[in] request  CIB request to forward
  */
 static void
 forward_request(xmlNode *request)
 {
     const char *op = pcmk__xe_get(request, PCMK__XA_CIB_OP);
     const char *section = pcmk__xe_get(request, PCMK__XA_CIB_SECTION);
     const char *host = pcmk__xe_get(request, PCMK__XA_CIB_HOST);
     const char *originator = pcmk__xe_get(request, PCMK__XA_SRC);
     const char *client_name = pcmk__xe_get(request, PCMK__XA_CIB_CLIENTNAME);
     const char *call_id = pcmk__xe_get(request, PCMK__XA_CIB_CALLID);
     pcmk__node_status_t *peer = NULL;
 
     int log_level = LOG_INFO;
 
     if (pcmk__str_eq(op, PCMK__CIB_REQUEST_NOOP, pcmk__str_none)) {
         log_level = LOG_DEBUG;
     }
 
     do_crm_log(log_level,
                "Forwarding %s operation for section %s to %s (origin=%s/%s/%s)",
                pcmk__s(op, "invalid"),
                pcmk__s(section, "all"),
                pcmk__s(host, "all"),
                pcmk__s(originator, "local"),
                pcmk__s(client_name, "unspecified"),
                pcmk__s(call_id, "unspecified"));
 
     pcmk__xe_set(request, PCMK__XA_CIB_DELEGATED_FROM, OUR_NODENAME);
 
     if (host != NULL) {
         peer = pcmk__get_node(0, host, NULL, pcmk__node_search_cluster_member);
     }
     pcmk__cluster_send_message(peer, pcmk_ipc_based, request);
 
     // Return the request to its original state
     pcmk__xe_remove_attr(request, PCMK__XA_CIB_DELEGATED_FROM);
 }
 
 static void
 send_peer_reply(xmlNode *msg, const char *originator)
 {
     const pcmk__node_status_t *node = NULL;
 
     if ((msg == NULL) || (originator == NULL)) {
         return;
     }
 
     // Send reply via cluster to originating node
     node = pcmk__get_node(0, originator, NULL,
                           pcmk__node_search_cluster_member);
 
     crm_trace("Sending request result to %s only", originator);
     pcmk__xe_set(msg, PCMK__XA_CIB_ISREPLYTO, originator);
     pcmk__cluster_send_message(node, pcmk_ipc_based, msg);
 }
 
 /*!
  * \internal
  * \brief Handle an IPC or CPG message containing a request
  *
  * \param[in,out] request        Request XML
  * \param[in] privileged         Whether privileged commands may be run
  *                               (see cib_server_ops[] definition)
  * \param[in] cib_client         IPC client that sent request (or NULL if CPG)
  *
  * \return Legacy Pacemaker return code
  */
 int
 cib_process_request(xmlNode *request, gboolean privileged,
                     const pcmk__client_t *cib_client)
 {
     // @TODO: Break into multiple smaller functions
     uint32_t call_options = cib_none;
 
     gboolean process = TRUE;        // Whether to process request locally now
     gboolean is_update = TRUE;      // Whether request would modify CIB
     gboolean needs_reply = TRUE;    // Whether to build a reply
     gboolean local_notify = FALSE;  // Whether to notify (local) requester
     gboolean needs_forward = FALSE; // Whether to forward request somewhere else
 
     xmlNode *op_reply = NULL;
     xmlNode *result_diff = NULL;
 
     int rc = pcmk_ok;
     const char *op = pcmk__xe_get(request, PCMK__XA_CIB_OP);
     const char *originator = pcmk__xe_get(request, PCMK__XA_SRC);
     const char *host = pcmk__xe_get(request, PCMK__XA_CIB_HOST);
     const char *call_id = pcmk__xe_get(request, PCMK__XA_CIB_CALLID);
     const char *client_id = pcmk__xe_get(request, PCMK__XA_CIB_CLIENTID);
     const char *client_name = pcmk__xe_get(request, PCMK__XA_CIB_CLIENTNAME);
     const char *reply_to = pcmk__xe_get(request, PCMK__XA_CIB_ISREPLYTO);
 
     const cib__operation_t *operation = NULL;
     cib__op_fn_t op_function = NULL;
 
     rc = pcmk__xe_get_flags(request, PCMK__XA_CIB_CALLOPT, &call_options,
                             cib_none);
     if (rc != pcmk_rc_ok) {
         crm_warn("Couldn't parse options from request: %s", pcmk_rc_str(rc));
     }
 
     if ((host != NULL) && (*host == '\0')) {
         host = NULL;
     }
 
     if (cib_client == NULL) {
         crm_trace("Processing peer %s operation from %s/%s on %s intended for %s (reply=%s)",
                   op, pcmk__s(client_name, "client"), call_id, originator,
                   pcmk__s(host, "all"), reply_to);
     } else {
         pcmk__xe_set(request, PCMK__XA_SRC, OUR_NODENAME);
         crm_trace("Processing local %s operation from %s/%s intended for %s",
                   op, pcmk__s(client_name, "client"), call_id,
                   pcmk__s(host, "all"));
     }
 
     rc = cib__get_operation(op, &operation);
     rc = pcmk_rc2legacy(rc);
     if (rc != pcmk_ok) {
         /* TODO: construct error reply? */
         crm_err("Pre-processing of command failed: %s", pcmk_strerror(rc));
         return rc;
     }
 
     op_function = based_get_op_function(operation);
     if (op_function == NULL) {
         crm_err("Operation %s not supported by CIB manager", op);
         return -EOPNOTSUPP;
     }
 
     if (cib_client != NULL) {
         parse_local_options(cib_client, operation, host, op,
                             &local_notify, &needs_reply, &process,
                             &needs_forward);
 
     } else if (!parse_peer_options(operation, request, &local_notify,
                                    &needs_reply, &process)) {
         return rc;
     }
 
     if (pcmk__is_set(call_options, cib_transaction)) {
         /* All requests in a transaction are processed locally against a working
          * CIB copy, and we don't notify for individual requests because the
          * entire transaction is atomic.
          *
          * We still call the option parser functions above, for the sake of log
          * messages and checking whether we're the target for peer requests.
          */
         process = TRUE;
         needs_reply = FALSE;
         local_notify = FALSE;
         needs_forward = FALSE;
     }
 
     is_update = pcmk__is_set(operation->flags, cib__op_attr_modifies);
 
     if (pcmk__is_set(call_options, cib_discard_reply)) {
         /* If the request will modify the CIB, and we are in legacy mode, we
          * need to build a reply so we can broadcast a diff, even if the
          * requester doesn't want one.
          */
         needs_reply = FALSE;
         local_notify = FALSE;
         crm_trace("Client is not interested in the reply");
     }
 
     if (needs_forward) {
         forward_request(request);
         return rc;
     }
 
     if (cib_status != pcmk_ok) {
         rc = cib_status;
         crm_err("Ignoring request because cluster configuration is invalid "
                 "(please repair and restart): %s", pcmk_strerror(rc));
         op_reply = create_cib_reply(op, call_id, client_id, call_options, rc,
                                     the_cib);
 
     } else if (process) {
         time_t finished = 0;
         time_t now = time(NULL);
         int level = LOG_INFO;
         const char *section = pcmk__xe_get(request, PCMK__XA_CIB_SECTION);
         const char *admin_epoch_s = NULL;
         const char *epoch_s = NULL;
         const char *num_updates_s = NULL;
 
         rc = cib_process_command(request, operation, op_function, &op_reply,
                                  &result_diff, privileged);
 
         if (!is_update) {
             level = LOG_TRACE;
 
         } else if (pcmk__xe_attr_is_true(request, PCMK__XA_CIB_UPDATE)) {
             switch (rc) {
                 case pcmk_ok:
                     level = LOG_INFO;
                     break;
                 case -pcmk_err_old_data:
                 case -pcmk_err_diff_resync:
                 case -pcmk_err_diff_failed:
                     level = LOG_TRACE;
                     break;
                 default:
                     level = LOG_ERR;
             }
 
         } else if (rc != pcmk_ok) {
             level = LOG_WARNING;
         }
 
         if (the_cib != NULL) {
             admin_epoch_s = pcmk__xe_get(the_cib, PCMK_XA_ADMIN_EPOCH);
             epoch_s = pcmk__xe_get(the_cib, PCMK_XA_EPOCH);
             num_updates_s = pcmk__xe_get(the_cib, PCMK_XA_NUM_UPDATES);
         }
 
         do_crm_log(level,
                    "Completed %s operation for section %s: %s (rc=%d, origin=%s/%s/%s, version=%s.%s.%s)",
                    op, section ? section : "'all'", pcmk_strerror(rc), rc,
                    originator ? originator : "local",
                    pcmk__s(client_name, "client"), call_id,
                    pcmk__s(admin_epoch_s, "0"),
                    pcmk__s(epoch_s, "0"),
                    pcmk__s(num_updates_s, "0"));
 
         finished = time(NULL);
         if ((finished - now) > 3) {
             crm_trace("%s operation took %lds to complete", op, (long)(finished - now));
             crm_write_blackbox(0, NULL);
         }
 
         if (op_reply == NULL && (needs_reply || local_notify)) {
             crm_err("Unexpected NULL reply to message");
             crm_log_xml_err(request, "null reply");
             needs_reply = FALSE;
             local_notify = FALSE;
         }
     }
 
     if (is_update) {
         crm_trace("Completed pre-sync update from %s/%s/%s%s",
                   originator ? originator : "local",
                   pcmk__s(client_name, "client"), call_id,
                   local_notify?" with local notification":"");
 
     } else if (!needs_reply || stand_alone) {
         // This was a non-originating secondary update
         crm_trace("Completed update as secondary");
 
     } else if ((cib_client == NULL)
                && !pcmk__is_set(call_options, cib_discard_reply)) {
 
         if (is_update == FALSE || result_diff == NULL) {
             crm_trace("Request not broadcast: R/O call");
 
         } else if (rc != pcmk_ok) {
             crm_trace("Request not broadcast: call failed: %s", pcmk_strerror(rc));
 
         } else {
             crm_trace("Directing reply to %s", originator);
         }
 
         send_peer_reply(op_reply, originator);
     }
 
     if (local_notify && client_id) {
         crm_trace("Performing local %ssync notification for %s",
                   (pcmk__is_set(call_options, cib_sync_call)? "" : "a"),
                   client_id);
         if (process == FALSE) {
             do_local_notify(request, client_id,
                             pcmk__is_set(call_options, cib_sync_call),
                             (cib_client == NULL));
         } else {
             do_local_notify(op_reply, client_id,
                             pcmk__is_set(call_options, cib_sync_call),
                             (cib_client == NULL));
         }
     }
 
     pcmk__xml_free(op_reply);
     pcmk__xml_free(result_diff);
 
     return rc;
 }
 
 /*!
  * \internal
  * \brief Get a CIB operation's input from the request XML
  *
  * \param[in]  request  CIB request XML
  * \param[in]  type     CIB operation type
  * \param[out] section  Where to store CIB section name
  *
  * \return Input XML for CIB operation
  *
  * \note If not \c NULL, the return value is a non-const pointer to part of
  *       \p request. The caller should not free it directly.
  */
 static xmlNode *
 prepare_input(const xmlNode *request, enum cib__op_type type,
               const char **section)
 {
     xmlNode *wrapper = pcmk__xe_first_child(request, PCMK__XE_CIB_CALLDATA,
                                             NULL, NULL);
     xmlNode *input = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
 
     if (type == cib__op_apply_patch) {
         *section = NULL;
     } else {
         *section = pcmk__xe_get(request, PCMK__XA_CIB_SECTION);
     }
 
     // Grab the specified section
     if ((*section != NULL) && pcmk__xe_is(input, PCMK_XE_CIB)) {
         input = pcmk_find_cib_element(input, *section);
     }
 
     return input;
 }
 
 #define XPATH_CONFIG_CHANGE         \
     "//" PCMK_XE_CHANGE             \
     "[contains(@" PCMK_XA_PATH ",'/" PCMK_XE_CRM_CONFIG "/')]"
 
 static bool
 contains_config_change(xmlNode *diff)
 {
     bool changed = false;
 
     if (diff) {
         xmlXPathObject *xpathObj = pcmk__xpath_search(diff->doc,
                                                       XPATH_CONFIG_CHANGE);
 
         if (pcmk__xpath_num_results(xpathObj) > 0) {
             changed = true;
         }
         xmlXPathFreeObject(xpathObj);
     }
     return changed;
 }
 
 static int
 cib_process_command(xmlNode *request, const cib__operation_t *operation,
                     cib__op_fn_t op_function, xmlNode **reply,
                     xmlNode **cib_diff, bool privileged)
 {
     xmlNode *input = NULL;
     xmlNode *output = NULL;
     xmlNode *result_cib = NULL;
 
     uint32_t call_options = cib_none;
 
     const char *op = NULL;
     const char *section = NULL;
     const char *call_id = pcmk__xe_get(request, PCMK__XA_CIB_CALLID);
     const char *client_id = pcmk__xe_get(request, PCMK__XA_CIB_CLIENTID);
     const char *client_name = pcmk__xe_get(request, PCMK__XA_CIB_CLIENTNAME);
     const char *originator = pcmk__xe_get(request, PCMK__XA_SRC);
 
     int rc = pcmk_ok;
 
     bool config_changed = false;
     bool manage_counters = true;
 
     static mainloop_timer_t *digest_timer = NULL;
 
     pcmk__assert(cib_status == pcmk_ok);
 
     if(digest_timer == NULL) {
         digest_timer = mainloop_timer_add("digester", 5000, FALSE, cib_digester_cb, NULL);
     }
 
     *reply = NULL;
     *cib_diff = NULL;
 
     /* Start processing the request... */
     op = pcmk__xe_get(request, PCMK__XA_CIB_OP);
     rc = pcmk__xe_get_flags(request, PCMK__XA_CIB_CALLOPT, &call_options,
                             cib_none);
     if (rc != pcmk_rc_ok) {
         crm_warn("Couldn't parse options from request: %s", pcmk_rc_str(rc));
     }
 
     if (!privileged
         && pcmk__is_set(operation->flags, cib__op_attr_privileged)) {
         rc = -EACCES;
         crm_trace("Failed due to lack of privileges: %s", pcmk_strerror(rc));
         goto done;
     }
 
     input = prepare_input(request, operation->type, &section);
 
     if (!pcmk__is_set(operation->flags, cib__op_attr_modifies)) {
         rc = cib_perform_op(NULL, op, call_options, op_function, true, section,
                             request, input, false, &config_changed, &the_cib,
                             &result_cib, NULL, &output);
 
         CRM_CHECK(result_cib == NULL, pcmk__xml_free(result_cib));
         goto done;
     }
 
     /* @COMPAT: Handle a valid write action (legacy)
      *
      * @TODO: Re-evaluate whether this is all truly legacy. The cib_force_diff
      * portion is. However, PCMK__XA_CIB_UPDATE may be set by a sync operation
      * even in non-legacy mode, and manage_counters tells xml_create_patchset()
      * whether to update version/epoch info.
      */
     if (pcmk__xe_attr_is_true(request, PCMK__XA_CIB_UPDATE)) {
         manage_counters = false;
         cib__set_call_options(call_options, "call", cib_force_diff);
         crm_trace("Global update detected");
 
         CRM_LOG_ASSERT(pcmk__str_any_of(op,
                                         PCMK__CIB_REQUEST_APPLY_PATCH,
                                         PCMK__CIB_REQUEST_REPLACE,
                                         NULL));
     }
 
     ping_modified_since = TRUE;
 
     // result_cib must not be modified after cib_perform_op() returns
     rc = cib_perform_op(NULL, op, call_options, op_function, false, section,
                         request, input, manage_counters, &config_changed,
                         &the_cib, &result_cib, cib_diff, &output);
 
     /* Always write to disk for successful ops with the flag set. This also
      * negates the need to detect ordering changes.
      */
     if ((rc == pcmk_ok)
         && pcmk__is_set(operation->flags, cib__op_attr_writes_through)) {
 
         config_changed = true;
     }
 
     if ((rc == pcmk_ok)
         && !pcmk__any_flags_set(call_options, cib_dryrun|cib_transaction)) {
 
         if (result_cib != the_cib) {
             if (pcmk__is_set(operation->flags, cib__op_attr_writes_through)) {
                 config_changed = true;
             }
 
             crm_trace("Activating %s->%s%s",
                       pcmk__xe_get(the_cib, PCMK_XA_NUM_UPDATES),
                       pcmk__xe_get(result_cib, PCMK_XA_NUM_UPDATES),
                       (config_changed? " changed" : ""));
 
             rc = activateCibXml(result_cib, config_changed, op);
             if (rc != pcmk_ok) {
                 crm_err("Failed to activate new CIB: %s", pcmk_strerror(rc));
             }
         }
 
         if ((rc == pcmk_ok) && contains_config_change(*cib_diff)) {
             cib_read_config(config_hash, result_cib);
         }
 
         /* @COMPAT Nodes older than feature set 3.19.0 don't support
          * transactions. In a mixed-version cluster with nodes <3.19.0, we must
          * sync the updated CIB, so that the older nodes receive the changes.
          * Any node that has already applied the transaction will ignore the
          * synced CIB.
          *
          * To ensure the updated CIB is synced from only one node, we sync it
          * from the originator.
          */
         if ((operation->type == cib__op_commit_transact)
             && pcmk__str_eq(originator, OUR_NODENAME, pcmk__str_casei)
-            && compare_version(pcmk__xe_get(the_cib, PCMK_XA_CRM_FEATURE_SET),
-                               "3.19.0") < 0) {
+            && (pcmk__compare_versions(pcmk__xe_get(the_cib,
+                                                    PCMK_XA_CRM_FEATURE_SET),
+                                       "3.19.0") < 0)) {
 
             sync_our_cib(request, TRUE);
         }
 
         mainloop_timer_stop(digest_timer);
         mainloop_timer_start(digest_timer);
 
     } else if (rc == -pcmk_err_schema_validation) {
         pcmk__assert(result_cib != the_cib);
 
         if (output != NULL) {
             crm_log_xml_info(output, "cib:output");
             pcmk__xml_free(output);
         }
 
         output = result_cib;
 
     } else {
         crm_trace("Not activating %d %d %s", rc,
                   pcmk__is_set(call_options, cib_dryrun),
                   pcmk__xe_get(result_cib, PCMK_XA_NUM_UPDATES));
 
         if (result_cib != the_cib) {
             pcmk__xml_free(result_cib);
         }
     }
 
     if (!pcmk__any_flags_set(call_options,
                              cib_dryrun|cib_inhibit_notify|cib_transaction)) {
         crm_trace("Sending notifications %d",
                   pcmk__is_set(call_options, cib_dryrun));
         cib_diff_notify(op, rc, call_id, client_id, client_name, originator,
                         input, *cib_diff);
     }
 
     pcmk__log_xml_patchset(LOG_TRACE, *cib_diff);
 
   done:
     if (!pcmk__is_set(call_options, cib_discard_reply)) {
         *reply = create_cib_reply(op, call_id, client_id, call_options, rc,
                                   output);
     }
 
     if (output != the_cib) {
         pcmk__xml_free(output);
     }
     crm_trace("done");
     return rc;
 }
 
 void
 cib_peer_callback(xmlNode * msg, void *private_data)
 {
     const char *reason = NULL;
     const char *originator = pcmk__xe_get(msg, PCMK__XA_SRC);
 
     if (pcmk__peer_cache == NULL) {
         reason = "membership not established";
         goto bail;
     }
 
     if (pcmk__xe_get(msg, PCMK__XA_CIB_CLIENTNAME) == NULL) {
         pcmk__xe_set(msg, PCMK__XA_CIB_CLIENTNAME, originator);
     }
 
     /* crm_log_xml_trace(msg, "Peer[inbound]"); */
     cib_process_request(msg, TRUE, NULL);
     return;
 
   bail:
     if (reason) {
         const char *op = pcmk__xe_get(msg, PCMK__XA_CIB_OP);
 
         crm_warn("Discarding %s message from %s: %s", op, originator, reason);
     }
 }
 
 static gboolean
 cib_force_exit(gpointer data)
 {
     crm_notice("Exiting immediately after %s without shutdown acknowledgment",
                pcmk__readable_interval(EXIT_ESCALATION_MS));
     terminate_cib(CRM_EX_ERROR);
     return FALSE;
 }
 
 static void
 disconnect_remote_client(gpointer key, gpointer value, gpointer user_data)
 {
     pcmk__client_t *a_client = value;
 
     crm_err("Can't disconnect client %s: Not implemented",
             pcmk__client_name(a_client));
 }
 
 static void
 initiate_exit(void)
 {
     int active = 0;
     xmlNode *leaving = NULL;
 
     active = pcmk__cluster_num_active_nodes();
     if (active < 2) { // This is the last active node
         crm_info("Exiting without sending shutdown request (no active peers)");
         terminate_cib(CRM_EX_OK);
         return;
     }
 
     crm_info("Sending shutdown request to %d peers", active);
 
     leaving = pcmk__xe_create(NULL, PCMK__XE_EXIT_NOTIFICATION);
     pcmk__xe_set(leaving, PCMK__XA_T, PCMK__VALUE_CIB);
     pcmk__xe_set(leaving, PCMK__XA_CIB_OP, PCMK__CIB_REQUEST_SHUTDOWN);
 
     pcmk__cluster_send_message(NULL, pcmk_ipc_based, leaving);
     pcmk__xml_free(leaving);
 
     pcmk__create_timer(EXIT_ESCALATION_MS, cib_force_exit, NULL);
 }
 
 void
 cib_shutdown(int nsig)
 {
     struct qb_ipcs_stats srv_stats;
 
     if (cib_shutdown_flag == FALSE) {
         int disconnects = 0;
         qb_ipcs_connection_t *c = NULL;
 
         cib_shutdown_flag = TRUE;
 
         c = qb_ipcs_connection_first_get(ipcs_rw);
         while (c != NULL) {
             qb_ipcs_connection_t *last = c;
 
             c = qb_ipcs_connection_next_get(ipcs_rw, last);
 
             crm_debug("Disconnecting r/w client %p...", last);
             qb_ipcs_disconnect(last);
             qb_ipcs_connection_unref(last);
             disconnects++;
         }
 
         c = qb_ipcs_connection_first_get(ipcs_ro);
         while (c != NULL) {
             qb_ipcs_connection_t *last = c;
 
             c = qb_ipcs_connection_next_get(ipcs_ro, last);
 
             crm_debug("Disconnecting r/o client %p...", last);
             qb_ipcs_disconnect(last);
             qb_ipcs_connection_unref(last);
             disconnects++;
         }
 
         c = qb_ipcs_connection_first_get(ipcs_shm);
         while (c != NULL) {
             qb_ipcs_connection_t *last = c;
 
             c = qb_ipcs_connection_next_get(ipcs_shm, last);
 
             crm_debug("Disconnecting non-blocking r/w client %p...", last);
             qb_ipcs_disconnect(last);
             qb_ipcs_connection_unref(last);
             disconnects++;
         }
 
         disconnects += pcmk__ipc_client_count();
 
         crm_debug("Disconnecting %d remote clients", pcmk__ipc_client_count());
         pcmk__foreach_ipc_client(disconnect_remote_client, NULL);
         crm_info("Disconnected %d clients", disconnects);
     }
 
     qb_ipcs_stats_get(ipcs_rw, &srv_stats, QB_FALSE);
 
     if (pcmk__ipc_client_count() == 0) {
         crm_info("All clients disconnected (%d)", srv_stats.active_connections);
         initiate_exit();
 
     } else {
         crm_info("Waiting on %d clients to disconnect (%d)",
                  pcmk__ipc_client_count(), srv_stats.active_connections);
     }
 }
 
 extern int remote_fd;
 extern int remote_tls_fd;
 
 /*!
  * \internal
  * \brief Close remote sockets, free the global CIB and quit
  *
  * \param[in] exit_status  What exit status to use (if -1, use CRM_EX_OK, but
  *                         skip disconnecting from the cluster layer)
  */
 void
 terminate_cib(int exit_status)
 {
     if (remote_fd > 0) {
         close(remote_fd);
         remote_fd = 0;
     }
     if (remote_tls_fd > 0) {
         close(remote_tls_fd);
         remote_tls_fd = 0;
     }
 
     uninitializeCib();
 
     // Exit immediately on error
     if (exit_status > CRM_EX_OK) {
         pcmk__stop_based_ipc(ipcs_ro, ipcs_rw, ipcs_shm);
         crm_exit(exit_status);
         return;
     }
 
     if ((mainloop != NULL) && g_main_loop_is_running(mainloop)) {
         /* Quit via returning from the main loop. If exit_status has the special
          * value -1, we skip the disconnect here, and it will be done when the
          * main loop returns (this allows the peer status callback to avoid
          * messing with the peer caches).
          */
         if (exit_status == CRM_EX_OK) {
             pcmk_cluster_disconnect(crm_cluster);
         }
         g_main_loop_quit(mainloop);
         return;
     }
 
     /* Exit cleanly. Even the peer status callback can disconnect here, because
      * we're not returning control to the caller.
      */
     pcmk_cluster_disconnect(crm_cluster);
     pcmk__stop_based_ipc(ipcs_ro, ipcs_rw, ipcs_shm);
     crm_exit(CRM_EX_OK);
 }
diff --git a/daemons/controld/controld_cib.c b/daemons/controld/controld_cib.c
index eff4800aa3..8690b086f7 100644
--- a/daemons/controld/controld_cib.c
+++ b/daemons/controld/controld_cib.c
@@ -1,1060 +1,1061 @@
 /*
  * Copyright 2004-2025 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 <unistd.h>  /* sleep */
 
 #include <crm/common/alerts_internal.h>
 #include <crm/common/xml.h>
 #include <crm/crm.h>
 #include <crm/lrmd_internal.h>
 
 #include <pacemaker-controld.h>
 
 // Call ID of the most recent in-progress CIB resource update (or 0 if none)
 static int pending_rsc_update = 0;
 
 /*!
  * \internal
  * \brief Respond to a dropped CIB connection
  *
  * \param[in] user_data  CIB connection that dropped
  */
 static void
 handle_cib_disconnect(gpointer user_data)
 {
     CRM_LOG_ASSERT(user_data == controld_globals.cib_conn);
 
     controld_trigger_fsa();
     controld_globals.cib_conn->state = cib_disconnected;
 
     if (pcmk__is_set(controld_globals.fsa_input_register, R_CIB_CONNECTED)) {
         // @TODO This should trigger a reconnect, not a shutdown
         crm_crit("Lost connection to the CIB manager, shutting down");
         register_fsa_input(C_FSA_INTERNAL, I_ERROR, NULL);
         controld_clear_fsa_input_flags(R_CIB_CONNECTED);
 
     } else { // Expected
         crm_info("Disconnected from the CIB manager");
     }
 }
 
 static void
 do_cib_updated(const char *event, xmlNode * msg)
 {
     const xmlNode *patchset = NULL;
     const char *client_name = NULL;
 
     crm_debug("Received CIB diff notification: DC=%s", pcmk__btoa(AM_I_DC));
 
     if (cib__get_notify_patchset(msg, &patchset) != pcmk_rc_ok) {
         return;
     }
 
     if (pcmk__cib_element_in_patchset(patchset, PCMK_XE_ALERTS)
         || pcmk__cib_element_in_patchset(patchset, PCMK_XE_CRM_CONFIG)) {
 
         controld_trigger_config();
     }
 
     if (!AM_I_DC) {
         // We're not in control of the join sequence
         return;
     }
 
     client_name = pcmk__xe_get(msg, PCMK__XA_CIB_CLIENTNAME);
     if (!cib__client_triggers_refresh(client_name)) {
         // The CIB is still accurate
         return;
     }
 
     if (pcmk__cib_element_in_patchset(patchset, PCMK_XE_NODES)
         || pcmk__cib_element_in_patchset(patchset, PCMK_XE_STATUS)) {
 
         /* An unsafe client modified the PCMK_XE_NODES or PCMK_XE_STATUS
          * section. Ensure the node list is up-to-date, and start the join
          * process again so we get everyone's current resource history.
          */
         if (client_name == NULL) {
             client_name = pcmk__xe_get(msg, PCMK__XA_CIB_CLIENTID);
         }
         crm_notice("Populating nodes and starting an election after %s event "
                    "triggered by %s",
                    event, pcmk__s(client_name, "(unidentified client)"));
 
         populate_cib_nodes(node_update_quick|node_update_all, __func__);
         register_fsa_input(C_FSA_INTERNAL, I_ELECTION, NULL);
     }
 }
 
 void
 controld_disconnect_cib_manager(void)
 {
     cib_t *cib_conn = controld_globals.cib_conn;
 
     pcmk__assert(cib_conn != NULL);
 
     crm_debug("Disconnecting from the CIB manager");
 
     controld_clear_fsa_input_flags(R_CIB_CONNECTED);
 
     cib_conn->cmds->del_notify_callback(cib_conn, PCMK__VALUE_CIB_DIFF_NOTIFY,
                                         do_cib_updated);
     cib_free_callbacks(cib_conn);
 
     if (cib_conn->state != cib_disconnected) {
         cib_conn->cmds->set_secondary(cib_conn, cib_discard_reply);
         cib_conn->cmds->signoff(cib_conn);
     }
 }
 
 /* A_CIB_STOP, A_CIB_START, O_CIB_RESTART */
 void
 do_cib_control(long long action,
                enum crmd_fsa_cause cause,
                enum crmd_fsa_state cur_state,
                enum crmd_fsa_input current_input, fsa_data_t * msg_data)
 {
     static int cib_retries = 0;
 
     cib_t *cib_conn = controld_globals.cib_conn;
 
     void (*dnotify_fn) (gpointer user_data) = handle_cib_disconnect;
     void (*update_cb) (const char *event, xmlNodePtr msg) = do_cib_updated;
 
     int rc = pcmk_ok;
 
     pcmk__assert(cib_conn != NULL);
 
     if (pcmk__is_set(action, A_CIB_STOP)) {
         if ((cib_conn->state != cib_disconnected)
             && (pending_rsc_update != 0)) {
 
             crm_info("Waiting for resource update %d to complete",
                      pending_rsc_update);
             crmd_fsa_stall(FALSE);
             return;
         }
         controld_disconnect_cib_manager();
     }
 
     if (!pcmk__is_set(action, A_CIB_START)) {
         return;
     }
 
     if (cur_state == S_STOPPING) {
         crm_err("Ignoring request to connect to the CIB manager after "
                 "shutdown");
         return;
     }
 
     rc = cib_conn->cmds->signon(cib_conn, crm_system_name,
                                 cib_command_nonblocking);
 
     if (rc != pcmk_ok) {
         // A short wait that usually avoids stalling the FSA
         sleep(1);
         rc = cib_conn->cmds->signon(cib_conn, crm_system_name,
                                     cib_command_nonblocking);
     }
 
     if (rc != pcmk_ok) {
         crm_info("Could not connect to the CIB manager: %s", pcmk_strerror(rc));
 
     } else if (cib_conn->cmds->set_connection_dnotify(cib_conn,
                                                       dnotify_fn) != pcmk_ok) {
         crm_err("Could not set dnotify callback");
 
     } else if (cib_conn->cmds->add_notify_callback(cib_conn,
                                                    PCMK__VALUE_CIB_DIFF_NOTIFY,
                                                    update_cb) != pcmk_ok) {
         crm_err("Could not set CIB notification callback (update)");
 
     } else {
         controld_set_fsa_input_flags(R_CIB_CONNECTED);
         cib_retries = 0;
     }
 
     if (!pcmk__is_set(controld_globals.fsa_input_register, R_CIB_CONNECTED)) {
         cib_retries++;
 
         if (cib_retries < 30) {
             crm_warn("Couldn't complete CIB registration %d times... "
                      "pause and retry", cib_retries);
             controld_start_wait_timer();
             crmd_fsa_stall(FALSE);
 
         } else {
             crm_err("Could not complete CIB registration %d times... "
                     "hard error", cib_retries);
             register_fsa_error(C_FSA_INTERNAL, I_ERROR, NULL);
         }
     }
 }
 
 #define MIN_CIB_OP_TIMEOUT (30)
 
 /*!
  * \internal
  * \brief Get the timeout (in seconds) that should be used with CIB operations
  *
  * \return The maximum of 30 seconds, the value of the PCMK_cib_timeout
  *         environment variable, or 10 seconds times one more than the number of
  *         nodes in the cluster.
  */
 unsigned int
 cib_op_timeout(void)
 {
     unsigned int calculated_timeout = 10U * (pcmk__cluster_num_active_nodes()
                                              + pcmk__cluster_num_remote_nodes()
                                              + 1U);
 
     calculated_timeout = QB_MAX(calculated_timeout, MIN_CIB_OP_TIMEOUT);
     crm_trace("Calculated timeout: %s",
               pcmk__readable_interval(calculated_timeout * 1000));
 
     if (controld_globals.cib_conn) {
         controld_globals.cib_conn->call_timeout = calculated_timeout;
     }
     return calculated_timeout;
 }
 
 /*!
  * \internal
  * \brief Get CIB call options to use local scope if primary is unavailable
  *
  * \return CIB call options
  */
 int
 crmd_cib_smart_opt(void)
 {
     int call_opt = cib_none;
 
     if ((controld_globals.fsa_state == S_ELECTION)
         || (controld_globals.fsa_state == S_PENDING)) {
         crm_info("Sending update to local CIB in state: %s",
                  fsa_state2string(controld_globals.fsa_state));
         cib__set_call_options(call_opt, "update", cib_none);
     }
     return call_opt;
 }
 
 static void
 cib_delete_callback(xmlNode *msg, int call_id, int rc, xmlNode *output,
                     void *user_data)
 {
     char *desc = user_data;
 
     if (rc == 0) {
         crm_debug("Deletion of %s (via CIB call %d) succeeded", desc, call_id);
     } else {
         crm_warn("Deletion of %s (via CIB call %d) failed: %s " QB_XS " rc=%d",
                  desc, call_id, pcmk_strerror(rc), rc);
     }
 }
 
 // Searches for various portions of PCMK__XE_NODE_STATE to delete
 
 // Match a particular node's PCMK__XE_NODE_STATE (takes node name 1x)
 #define XPATH_NODE_STATE "//" PCMK__XE_NODE_STATE "[@" PCMK_XA_UNAME "='%s']"
 
 // Node's lrm section (name 1x)
 #define XPATH_NODE_LRM XPATH_NODE_STATE "/" PCMK__XE_LRM
 
 /* Node's PCMK__XE_LRM_RSC_OP entries and PCMK__XE_LRM_RESOURCE entries without
  * unexpired lock
  * (name 2x, (seconds_since_epoch - PCMK_OPT_SHUTDOWN_LOCK_LIMIT) 1x)
  */
 #define XPATH_NODE_LRM_UNLOCKED XPATH_NODE_STATE "//" PCMK__XE_LRM_RSC_OP   \
                                 "|" XPATH_NODE_STATE                        \
                                 "//" PCMK__XE_LRM_RESOURCE                  \
                                 "[not(@" PCMK_OPT_SHUTDOWN_LOCK ") "        \
                                     "or " PCMK_OPT_SHUTDOWN_LOCK "<%lld]"
 
 // Node's PCMK__XE_TRANSIENT_ATTRIBUTES section (name 1x)
 #define XPATH_NODE_ATTRS XPATH_NODE_STATE "/" PCMK__XE_TRANSIENT_ATTRIBUTES
 
 // Everything under PCMK__XE_NODE_STATE (name 1x)
 #define XPATH_NODE_ALL          XPATH_NODE_STATE "/*"
 
 /* Unlocked history + transient attributes
  * (name 2x, (seconds_since_epoch - PCMK_OPT_SHUTDOWN_LOCK_LIMIT) 1x, name 1x)
  */
 #define XPATH_NODE_ALL_UNLOCKED XPATH_NODE_LRM_UNLOCKED "|" XPATH_NODE_ATTRS
 
 /*!
  * \internal
  * \brief Get the XPath and description of a node state section to be deleted
  *
  * \param[in]  uname    Desired node
  * \param[in]  section  Subsection of \c PCMK__XE_NODE_STATE to be deleted
  * \param[out] xpath    Where to store XPath of \p section
  * \param[out] desc     If not \c NULL, where to store description of \p section
  */
 void
 controld_node_state_deletion_strings(const char *uname,
                                      enum controld_section_e section,
                                      char **xpath, char **desc)
 {
     const char *desc_pre = NULL;
 
     // Shutdown locks that started before this time are expired
     long long expire = (long long) time(NULL)
                        - controld_globals.shutdown_lock_limit;
 
     switch (section) {
         case controld_section_lrm:
             *xpath = pcmk__assert_asprintf(XPATH_NODE_LRM, uname);
             desc_pre = "resource history";
             break;
         case controld_section_lrm_unlocked:
             *xpath = pcmk__assert_asprintf(XPATH_NODE_LRM_UNLOCKED, uname,
                                            uname, expire);
             desc_pre = "resource history (other than shutdown locks)";
             break;
         case controld_section_attrs:
             *xpath = pcmk__assert_asprintf(XPATH_NODE_ATTRS, uname);
             desc_pre = "transient attributes";
             break;
         case controld_section_all:
             *xpath = pcmk__assert_asprintf(XPATH_NODE_ALL, uname);
             desc_pre = "all state";
             break;
         case controld_section_all_unlocked:
             *xpath = pcmk__assert_asprintf(XPATH_NODE_ALL_UNLOCKED, uname,
                                            uname, expire, uname);
             desc_pre = "all state (other than shutdown locks)";
             break;
         default:
             // We called this function incorrectly
             pcmk__assert(false);
             break;
     }
 
     if (desc != NULL) {
         *desc = pcmk__assert_asprintf("%s for node %s", desc_pre, uname);
     }
 }
 
 /*!
  * \internal
  * \brief Delete subsection of a node's CIB \c PCMK__XE_NODE_STATE
  *
  * \param[in] uname    Desired node
  * \param[in] section  Subsection of \c PCMK__XE_NODE_STATE to delete
  * \param[in] options  CIB call options to use
  */
 void
 controld_delete_node_state(const char *uname, enum controld_section_e section,
                            int options)
 {
     cib_t *cib = controld_globals.cib_conn;
     char *xpath = NULL;
     char *desc = NULL;
     int cib_rc = pcmk_ok;
 
     pcmk__assert((uname != NULL) && (cib != NULL));
 
     controld_node_state_deletion_strings(uname, section, &xpath, &desc);
 
     cib__set_call_options(options, "node state deletion",
                           cib_xpath|cib_multiple);
     cib_rc = cib->cmds->remove(cib, xpath, NULL, options);
     fsa_register_cib_callback(cib_rc, desc, cib_delete_callback);
     crm_info("Deleting %s (via CIB call %d) " QB_XS " xpath=%s",
              desc, cib_rc, xpath);
 
     // CIB library handles freeing desc
     free(xpath);
 }
 
 // Takes node name and resource ID
 #define XPATH_RESOURCE_HISTORY "//" PCMK__XE_NODE_STATE                 \
                                "[@" PCMK_XA_UNAME "='%s']/"             \
                                PCMK__XE_LRM "/" PCMK__XE_LRM_RESOURCES  \
                                "/" PCMK__XE_LRM_RESOURCE                \
                                "[@" PCMK_XA_ID "='%s']"
 // @TODO could add "and @PCMK_OPT_SHUTDOWN_LOCK" to limit to locks
 
 /*!
  * \internal
  * \brief Clear resource history from CIB for a given resource and node
  *
  * \param[in]  rsc_id        ID of resource to be cleared
  * \param[in]  node          Node whose resource history should be cleared
  * \param[in]  user_name     ACL user name to use
  * \param[in]  call_options  CIB call options
  *
  * \return Standard Pacemaker return code
  */
 int
 controld_delete_resource_history(const char *rsc_id, const char *node,
                                  const char *user_name, int call_options)
 {
     char *desc = NULL;
     char *xpath = NULL;
     int rc = pcmk_rc_ok;
     cib_t *cib = controld_globals.cib_conn;
 
     CRM_CHECK((rsc_id != NULL) && (node != NULL), return EINVAL);
 
     desc = pcmk__assert_asprintf("resource history for %s on %s", rsc_id, node);
     if (cib == NULL) {
         crm_err("Unable to clear %s: no CIB connection", desc);
         free(desc);
         return ENOTCONN;
     }
 
     // Ask CIB to delete the entry
     xpath = pcmk__assert_asprintf(XPATH_RESOURCE_HISTORY, node, rsc_id);
 
     cib->cmds->set_user(cib, user_name);
     rc = cib->cmds->remove(cib, xpath, NULL, call_options|cib_xpath);
     cib->cmds->set_user(cib, NULL);
 
     if (rc < 0) {
         rc = pcmk_legacy2rc(rc);
         crm_err("Could not delete resource status of %s on %s%s%s: %s "
                 QB_XS " rc=%d", rsc_id, node,
                 (user_name? " for user " : ""), (user_name? user_name : ""),
                 pcmk_rc_str(rc), rc);
         free(desc);
         free(xpath);
         return rc;
     }
 
     if (pcmk__is_set(call_options, cib_sync_call)) {
         if (pcmk__is_set(call_options, cib_dryrun)) {
             crm_debug("Deletion of %s would succeed", desc);
         } else {
             crm_debug("Deletion of %s succeeded", desc);
         }
         free(desc);
 
     } else {
         crm_info("Clearing %s (via CIB call %d) " QB_XS " xpath=%s",
                  desc, rc, xpath);
         fsa_register_cib_callback(rc, desc, cib_delete_callback);
         // CIB library handles freeing desc
     }
 
     free(xpath);
     return pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Build XML and string of parameters meeting some criteria, for digest
  *
  * \param[in]  op          Executor event with parameter table to use
  * \param[in]  metadata    Parsed meta-data for executed resource agent
  * \param[in]  param_type  Flag used for selection criteria
  * \param[out] result      Will be set to newly created XML with selected
  *                         parameters as attributes
  *
  * \return Newly allocated space-separated string of parameter names
  * \note Selection criteria varies by param_type: for the restart digest, we
  *       want parameters that are *not* marked reloadable (OCF 1.1) or that
  *       *are* marked unique (pre-1.1), for both string and XML results; for the
  *       secure digest, we want parameters that *are* marked private for the
  *       string, but parameters that are *not* marked private for the XML.
  * \note It is the caller's responsibility to free the string return value with
  *       \p g_string_free() and the XML result with \p pcmk__xml_free().
  */
 static GString *
 build_parameter_list(const lrmd_event_data_t *op,
                      const struct ra_metadata_s *metadata,
                      enum ra_param_flags_e param_type, xmlNode **result)
 {
     GString *list = NULL;
 
     *result = pcmk__xe_create(NULL, PCMK_XE_PARAMETERS);
 
     /* Consider all parameters only except private ones to be consistent with
      * what scheduler does with calculate_secure_digest().
      */
-    if (param_type == ra_param_private
-        && compare_version(controld_globals.dc_version, "3.16.0") >= 0) {
+    if ((param_type == ra_param_private)
+        && (pcmk__compare_versions(controld_globals.dc_version,
+                                   "3.16.0") >= 0)) {
         g_hash_table_foreach(op->params, hash2field, *result);
         pcmk__filter_op_for_digest(*result);
     }
 
     for (GList *iter = metadata->ra_params; iter != NULL; iter = iter->next) {
         struct ra_param_s *param = (struct ra_param_s *) iter->data;
 
         bool accept_for_list = false;
         bool accept_for_xml = false;
 
         switch (param_type) {
             case ra_param_reloadable:
                 accept_for_list = !pcmk__is_set(param->rap_flags, param_type);
                 accept_for_xml = accept_for_list;
                 break;
 
             case ra_param_unique:
                 accept_for_list = pcmk__is_set(param->rap_flags, param_type);
                 accept_for_xml = accept_for_list;
                 break;
 
             case ra_param_private:
                 accept_for_list = pcmk__is_set(param->rap_flags, param_type);
                 accept_for_xml = !accept_for_list;
                 break;
         }
 
         if (accept_for_list) {
             crm_trace("Attr %s is %s", param->rap_name, ra_param_flag2text(param_type));
 
             if (list == NULL) {
                 // We will later search for " WORD ", so start list with a space
                 pcmk__add_word(&list, 256, " ");
             }
             pcmk__add_word(&list, 0, param->rap_name);
 
         } else {
             crm_trace("Rejecting %s for %s", param->rap_name, ra_param_flag2text(param_type));
         }
 
         if (accept_for_xml) {
             const char *v = g_hash_table_lookup(op->params, param->rap_name);
 
             if (v != NULL) {
                 crm_trace("Adding attr %s=%s to the xml result", param->rap_name, v);
                 pcmk__xe_set(*result, param->rap_name, v);
             }
 
         } else {
             crm_trace("Removing attr %s from the xml result", param->rap_name);
             pcmk__xe_remove_attr(*result, param->rap_name);
         }
     }
 
     if (list != NULL) {
         // We will later search for " WORD ", so end list with a space
         pcmk__add_word(&list, 0, " ");
     }
     return list;
 }
 
 static void
 append_restart_list(lrmd_event_data_t *op, struct ra_metadata_s *metadata,
                     xmlNode *update, const char *version)
 {
     GString *list = NULL;
     char *digest = NULL;
     xmlNode *restart = NULL;
 
     CRM_LOG_ASSERT(op->params != NULL);
 
     if (op->interval_ms > 0) {
         /* monitors are not reloadable */
         return;
     }
 
     if (pcmk__is_set(metadata->ra_flags, ra_supports_reload_agent)) {
         /* Add parameters not marked reloadable to the PCMK__XA_OP_FORCE_RESTART
          * list
          */
         list = build_parameter_list(op, metadata, ra_param_reloadable,
                                     &restart);
 
     } else if (pcmk__is_set(metadata->ra_flags, ra_supports_legacy_reload)) {
         /* @COMPAT pre-OCF-1.1 resource agents
          *
          * Before OCF 1.1, Pacemaker abused "unique=0" to indicate
          * reloadability. Add any parameters with unique="1" to the
          * PCMK__XA_OP_FORCE_RESTART list.
          */
         list = build_parameter_list(op, metadata, ra_param_unique, &restart);
 
     } else {
         // Resource does not support agent reloads
         return;
     }
 
     digest = pcmk__digest_operation(restart);
     /* Add PCMK__XA_OP_FORCE_RESTART and PCMK__XA_OP_RESTART_DIGEST to indicate
      * the resource supports reload, no matter if it actually supports any
      * reloadable parameters
      */
     pcmk__xe_set(update, PCMK__XA_OP_FORCE_RESTART,
                  (list == NULL)? "" : (const char *) list->str);
     pcmk__xe_set(update, PCMK__XA_OP_RESTART_DIGEST, digest);
 
     if ((list != NULL) && (list->len > 0)) {
         crm_trace("%s: %s, %s", op->rsc_id, digest, (const char *) list->str);
     } else {
         crm_trace("%s: %s", op->rsc_id, digest);
     }
 
     if (list != NULL) {
         g_string_free(list, TRUE);
     }
     pcmk__xml_free(restart);
     free(digest);
 }
 
 static void
 append_secure_list(lrmd_event_data_t *op, struct ra_metadata_s *metadata,
                    xmlNode *update, const char *version)
 {
     GString *list = NULL;
     char *digest = NULL;
     xmlNode *secure = NULL;
 
     CRM_LOG_ASSERT(op->params != NULL);
 
     /* To keep PCMK__XA_OP_SECURE_PARAMS short, we want it to contain the secure
      * parameters but PCMK__XA_OP_SECURE_DIGEST to be based on the insecure ones
      */
     list = build_parameter_list(op, metadata, ra_param_private, &secure);
 
     if (list != NULL) {
         digest = pcmk__digest_operation(secure);
         pcmk__xe_set(update, PCMK__XA_OP_SECURE_PARAMS,
                      (const char *) list->str);
         pcmk__xe_set(update, PCMK__XA_OP_SECURE_DIGEST, digest);
 
         crm_trace("%s: %s, %s", op->rsc_id, digest, (const char *) list->str);
         g_string_free(list, TRUE);
     } else {
         crm_trace("%s: no secure parameters", op->rsc_id);
     }
 
     pcmk__xml_free(secure);
     free(digest);
 }
 
 /*!
  * \internal
  * \brief Create XML for a resource history entry
  *
  * \param[in]     func       Function name of caller
  * \param[in,out] parent     XML to add entry to
  * \param[in]     rsc        Affected resource
  * \param[in,out] op         Action to add an entry for (or NULL to do nothing)
  * \param[in]     node_name  Node where action occurred
  */
 void
 controld_add_resource_history_xml_as(const char *func, xmlNode *parent,
                                      const lrmd_rsc_info_t *rsc,
                                      lrmd_event_data_t *op,
                                      const char *node_name)
 {
     int target_rc = 0;
     xmlNode *xml_op = NULL;
     struct ra_metadata_s *metadata = NULL;
     const char *caller_version = NULL;
     lrm_state_t *lrm_state = NULL;
 
     if (op == NULL) {
         return;
     }
 
     target_rc = rsc_op_expected_rc(op);
 
     caller_version = g_hash_table_lookup(op->params, PCMK_XA_CRM_FEATURE_SET);
     CRM_CHECK(caller_version != NULL, caller_version = CRM_FEATURE_SET);
 
     xml_op = pcmk__create_history_xml(parent, op, caller_version, target_rc,
                                       controld_globals.cluster->priv->node_name,
                                       func);
     if (xml_op == NULL) {
         return;
     }
 
     if ((rsc == NULL) || (op->params == NULL)
         || !crm_op_needs_metadata(rsc->standard, op->op_type)) {
 
         crm_trace("No digests needed for %s action on %s (params=%p rsc=%p)",
                   op->op_type, op->rsc_id, op->params, rsc);
         return;
     }
 
     lrm_state = controld_get_executor_state(node_name, false);
     if (lrm_state == NULL) {
         crm_warn("Cannot calculate digests for operation " PCMK__OP_FMT
                  " because we have no connection to executor for %s",
                  op->rsc_id, op->op_type, op->interval_ms, node_name);
         return;
     }
 
     /* Ideally the metadata is cached, and the agent is just a fallback.
      *
      * @TODO Go through all callers and ensure they get metadata asynchronously
      * first.
      */
     metadata = controld_get_rsc_metadata(lrm_state, rsc,
                                          controld_metadata_from_agent
                                          |controld_metadata_from_cache);
     if (metadata == NULL) {
         return;
     }
 
     crm_trace("Including additional digests for %s:%s:%s",
               rsc->standard, rsc->provider, rsc->type);
     append_restart_list(op, metadata, xml_op, caller_version);
     append_secure_list(op, metadata, xml_op, caller_version);
 
     return;
 }
 
 /*!
  * \internal
  * \brief Record an action as pending in the CIB, if appropriate
  *
  * \param[in]     node_name  Node where the action is pending
  * \param[in]     rsc        Resource that action is for
  * \param[in,out] op         Pending action
  *
  * \return true if action was recorded in CIB, otherwise false
  */
 bool
 controld_record_pending_op(const char *node_name, const lrmd_rsc_info_t *rsc,
                            lrmd_event_data_t *op)
 {
     const char *record_pending = NULL;
 
     CRM_CHECK((node_name != NULL) && (rsc != NULL) && (op != NULL),
               return false);
 
     // Never record certain operation types as pending
     if ((op->op_type == NULL) || (op->params == NULL)
         || !controld_action_is_recordable(op->op_type)) {
         return false;
     }
 
     // Check action's PCMK_META_RECORD_PENDING meta-attribute (defaults to true)
     record_pending = crm_meta_value(op->params, PCMK_META_RECORD_PENDING);
     if ((record_pending != NULL) && !pcmk__is_true(record_pending)) {
         pcmk__warn_once(pcmk__wo_record_pending,
                         "The " PCMK_META_RECORD_PENDING " option (for example, "
                         "for the %s resource's %s operation) is deprecated and "
                         "will be removed in a future release",
                         rsc->id, op->op_type);
         return false;
     }
 
     op->call_id = -1;
     op->t_run = time(NULL);
     op->t_rcchange = op->t_run;
 
     lrmd__set_result(op, PCMK_OCF_UNKNOWN, PCMK_EXEC_PENDING, NULL);
 
     crm_debug("Recording pending %s-interval %s for %s on %s in the CIB",
               pcmk__readable_interval(op->interval_ms), op->op_type, op->rsc_id,
               node_name);
     controld_update_resource_history(node_name, rsc, op, 0);
     return true;
 }
 
 static void
 cib_rsc_callback(xmlNode * msg, int call_id, int rc, xmlNode * output, void *user_data)
 {
     switch (rc) {
         case pcmk_ok:
         case -pcmk_err_diff_failed:
         case -pcmk_err_diff_resync:
             crm_trace("Resource history update completed (call=%d rc=%d)",
                       call_id, rc);
             break;
         default:
             if (call_id > 0) {
                 crm_warn("Resource history update %d failed: %s "
                          QB_XS " rc=%d", call_id, pcmk_strerror(rc), rc);
             } else {
                 crm_warn("Resource history update failed: %s " QB_XS " rc=%d",
                          pcmk_strerror(rc), rc);
             }
     }
 
     if (call_id == pending_rsc_update) {
         pending_rsc_update = 0;
         controld_trigger_fsa();
     }
 }
 
 /* Only successful stops, and probes that found the resource inactive, get locks
  * recorded in the history. This ensures the resource stays locked to the node
  * until it is active there again after the node comes back up.
  */
 static bool
 should_preserve_lock(lrmd_event_data_t *op)
 {
     if (!pcmk__is_set(controld_globals.flags, controld_shutdown_lock_enabled)) {
         return false;
     }
     if (!strcmp(op->op_type, PCMK_ACTION_STOP) && (op->rc == PCMK_OCF_OK)) {
         return true;
     }
     if (!strcmp(op->op_type, PCMK_ACTION_MONITOR)
         && (op->rc == PCMK_OCF_NOT_RUNNING)) {
         return true;
     }
     return false;
 }
 
 /*!
  * \internal
  * \brief Request a CIB update
  *
  * \param[in]     section    Section of CIB to update
  * \param[in]     data       New XML of CIB section to update
  * \param[in]     options    CIB call options
  * \param[in]     callback   If not \c NULL, set this as the operation callback
  *
  * \return Standard Pacemaker return code
  *
  * \note If \p callback is \p cib_rsc_callback(), the CIB update's call ID is
  *       stored in \p pending_rsc_update on success.
  */
 int
 controld_update_cib(const char *section, xmlNode *data, int options,
                     void (*callback)(xmlNode *, int, int, xmlNode *, void *))
 {
     cib_t *cib = controld_globals.cib_conn;
     int cib_rc = -ENOTCONN;
 
     pcmk__assert(data != NULL);
 
     if (cib != NULL) {
         cib_rc = cib->cmds->modify(cib, section, data, options);
         if (cib_rc >= 0) {
             crm_debug("Submitted CIB update %d for %s section",
                       cib_rc, section);
         }
     }
 
     if (callback == NULL) {
         if (cib_rc < 0) {
             crm_err("Failed to update CIB %s section: %s",
                     section, pcmk_rc_str(pcmk_legacy2rc(cib_rc)));
         }
 
     } else {
         if ((cib_rc >= 0) && (callback == cib_rsc_callback)) {
             /* Checking for a particular callback is a little hacky, but it
              * didn't seem worth adding an output argument for cib_rc for just
              * one use case.
              */
             pending_rsc_update = cib_rc;
         }
         fsa_register_cib_callback(cib_rc, NULL, callback);
     }
 
     return (cib_rc >= 0)? pcmk_rc_ok : pcmk_legacy2rc(cib_rc);
 }
 
 /*!
  * \internal
  * \brief Update resource history entry in CIB
  *
  * \param[in]     node_name  Node where action occurred
  * \param[in]     rsc        Resource that action is for
  * \param[in,out] op         Action to record
  * \param[in]     lock_time  If nonzero, when resource was locked to node
  *
  * \note On success, the CIB update's call ID will be stored in
  *       pending_rsc_update.
  */
 void
 controld_update_resource_history(const char *node_name,
                                  const lrmd_rsc_info_t *rsc,
                                  lrmd_event_data_t *op, time_t lock_time)
 {
     xmlNode *update = NULL;
     xmlNode *xml = NULL;
     int call_opt = crmd_cib_smart_opt();
     const char *node_id = NULL;
     const char *container = NULL;
 
     CRM_CHECK((node_name != NULL) && (op != NULL), return);
 
     if (rsc == NULL) {
         crm_warn("Resource %s no longer exists in the executor", op->rsc_id);
         controld_ack_event_directly(NULL, NULL, rsc, op, op->rsc_id);
         return;
     }
 
     // <status>
     update = pcmk__xe_create(NULL, PCMK_XE_STATUS);
 
     //   <node_state ...>
     xml = pcmk__xe_create(update, PCMK__XE_NODE_STATE);
     if (controld_is_local_node(node_name)) {
         node_id = controld_globals.our_uuid;
     } else {
         node_id = node_name;
         pcmk__xe_set_bool_attr(xml, PCMK_XA_REMOTE_NODE, true);
     }
     pcmk__xe_set(xml, PCMK_XA_ID, node_id);
     pcmk__xe_set(xml, PCMK_XA_UNAME, node_name);
     pcmk__xe_set(xml, PCMK_XA_CRM_DEBUG_ORIGIN, __func__);
 
     //     <lrm ...>
     xml = pcmk__xe_create(xml, PCMK__XE_LRM);
     pcmk__xe_set(xml, PCMK_XA_ID, node_id);
 
     //       <lrm_resources>
     xml = pcmk__xe_create(xml, PCMK__XE_LRM_RESOURCES);
 
     //         <lrm_resource ...>
     xml = pcmk__xe_create(xml, PCMK__XE_LRM_RESOURCE);
     pcmk__xe_set(xml, PCMK_XA_ID, op->rsc_id);
     pcmk__xe_set(xml, PCMK_XA_CLASS, rsc->standard);
     pcmk__xe_set(xml, PCMK_XA_PROVIDER, rsc->provider);
     pcmk__xe_set(xml, PCMK_XA_TYPE, rsc->type);
     if (lock_time != 0) {
         /* Actions on a locked resource should either preserve the lock by
          * recording it with the action result, or clear it.
          */
         if (!should_preserve_lock(op)) {
             lock_time = 0;
         }
         pcmk__xe_set_time(xml, PCMK_OPT_SHUTDOWN_LOCK, lock_time);
     }
     if (op->params != NULL) {
         container = g_hash_table_lookup(op->params,
                                         CRM_META "_" PCMK__META_CONTAINER);
         if (container != NULL) {
             crm_trace("Resource %s is a part of container resource %s",
                       op->rsc_id, container);
             pcmk__xe_set(xml, PCMK__META_CONTAINER, container);
         }
     }
 
     //           <lrm_resource_op ...> (possibly more than one)
     controld_add_resource_history_xml(xml, rsc, op, node_name);
 
     /* Update CIB asynchronously. Even if it fails, the resource state should be
      * discovered during the next election. Worst case, the node is wrongly
      * fenced for running a resource it isn't.
      */
     crm_log_xml_trace(update, __func__);
     controld_update_cib(PCMK_XE_STATUS, update, call_opt, cib_rsc_callback);
     pcmk__xml_free(update);
 }
 
 /*!
  * \internal
  * \brief Erase an LRM history entry from the CIB, given the operation data
  *
  * \param[in] op         Operation whose history should be deleted
  */
 void
 controld_delete_action_history(const lrmd_event_data_t *op)
 {
     xmlNode *xml_top = NULL;
 
     CRM_CHECK(op != NULL, return);
 
     xml_top = pcmk__xe_create(NULL, PCMK__XE_LRM_RSC_OP);
     pcmk__xe_set_int(xml_top, PCMK__XA_CALL_ID, op->call_id);
     pcmk__xe_set(xml_top, PCMK__XA_TRANSITION_KEY, op->user_data);
 
     if (op->interval_ms > 0) {
         char *op_id = pcmk__op_key(op->rsc_id, op->op_type, op->interval_ms);
 
         /* Avoid deleting last_failure too (if it was a result of this recurring op failing) */
         pcmk__xe_set(xml_top, PCMK_XA_ID, op_id);
         free(op_id);
     }
 
     crm_debug("Erasing resource operation history for " PCMK__OP_FMT " (call=%d)",
               op->rsc_id, op->op_type, op->interval_ms, op->call_id);
 
     controld_globals.cib_conn->cmds->remove(controld_globals.cib_conn,
                                             PCMK_XE_STATUS, xml_top, cib_none);
     crm_log_xml_trace(xml_top, "op:cancel");
     pcmk__xml_free(xml_top);
 }
 
 /* Define xpath to find LRM resource history entry by node and resource */
 #define XPATH_HISTORY                                   \
     "/" PCMK_XE_CIB "/" PCMK_XE_STATUS                  \
     "/" PCMK__XE_NODE_STATE "[@" PCMK_XA_UNAME "='%s']" \
     "/" PCMK__XE_LRM "/" PCMK__XE_LRM_RESOURCES         \
     "/" PCMK__XE_LRM_RESOURCE "[@" PCMK_XA_ID "='%s']"  \
     "/" PCMK__XE_LRM_RSC_OP
 
 /* ... and also by operation key */
 #define XPATH_HISTORY_ID XPATH_HISTORY "[@" PCMK_XA_ID "='%s']"
 
 /* ... and also by operation key and operation call ID */
 #define XPATH_HISTORY_CALL XPATH_HISTORY \
     "[@" PCMK_XA_ID "='%s' and @" PCMK__XA_CALL_ID "='%d']"
 
 /* ... and also by operation key and original operation key */
 #define XPATH_HISTORY_ORIG XPATH_HISTORY \
     "[@" PCMK_XA_ID "='%s' and @" PCMK__XA_OPERATION_KEY "='%s']"
 
 /*!
  * \internal
  * \brief Delete a last_failure resource history entry from the CIB
  *
  * \param[in] rsc_id       Name of resource to clear history for
  * \param[in] node         Name of node to clear history for
  * \param[in] action       If specified, delete only if this was failed action
  * \param[in] interval_ms  If \p action is specified, it has this interval
  */
 void
 controld_cib_delete_last_failure(const char *rsc_id, const char *node,
                                  const char *action, guint interval_ms)
 {
     char *xpath = NULL;
     char *last_failure_key = NULL;
     CRM_CHECK((rsc_id != NULL) && (node != NULL), return);
 
     // Generate XPath to match desired entry
     last_failure_key = pcmk__op_key(rsc_id, "last_failure", 0);
     if (action == NULL) {
         xpath = pcmk__assert_asprintf(XPATH_HISTORY_ID, node, rsc_id,
                                       last_failure_key);
     } else {
         char *action_key = pcmk__op_key(rsc_id, action, interval_ms);
 
         xpath = pcmk__assert_asprintf(XPATH_HISTORY_ORIG, node, rsc_id,
                                       last_failure_key, action_key);
         free(action_key);
     }
     free(last_failure_key);
 
     controld_globals.cib_conn->cmds->remove(controld_globals.cib_conn, xpath,
                                             NULL, cib_xpath);
     free(xpath);
 }
 
 /*!
  * \internal
  * \brief Delete resource history entry from the CIB, given operation key
  *
  * \param[in] rsc_id     Name of resource to clear history for
  * \param[in] node       Name of node to clear history for
  * \param[in] key        Operation key of operation to clear history for
  * \param[in] call_id    If specified, delete entry only if it has this call ID
  */
 void
 controld_delete_action_history_by_key(const char *rsc_id, const char *node,
                                       const char *key, int call_id)
 {
     char *xpath = NULL;
 
     CRM_CHECK((rsc_id != NULL) && (node != NULL) && (key != NULL), return);
 
     if (call_id > 0) {
         xpath = pcmk__assert_asprintf(XPATH_HISTORY_CALL, node, rsc_id, key,
                                      call_id);
     } else {
         xpath = pcmk__assert_asprintf(XPATH_HISTORY_ID, node, rsc_id, key);
     }
     controld_globals.cib_conn->cmds->remove(controld_globals.cib_conn, xpath,
                                             NULL, cib_xpath);
     free(xpath);
 }
diff --git a/daemons/controld/controld_execd.c b/daemons/controld/controld_execd.c
index ea950cf48b..b0a092419c 100644
--- a/daemons/controld/controld_execd.c
+++ b/daemons/controld/controld_execd.c
@@ -1,2405 +1,2405 @@
 /*
  * Copyright 2004-2025 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 <regex.h>
 #include <sys/param.h>
 #include <sys/types.h>
 #include <sys/wait.h>
 
 #include <crm/crm.h>
 #include <crm/lrmd.h>           // lrmd_event_data_t, lrmd_rsc_info_t, etc.
 #include <crm/services.h>
 #include <crm/common/xml.h>
 #include <crm/lrmd_internal.h>
 
 #include <pacemaker-internal.h>
 #include <pacemaker-controld.h>
 
 #define START_DELAY_THRESHOLD 5 * 60 * 1000
 #define MAX_LRM_REG_FAILS 30
 
 struct delete_event_s {
     int rc;
     const char *rsc;
     lrm_state_t *lrm_state;
 };
 
 static gboolean is_rsc_active(lrm_state_t * lrm_state, const char *rsc_id);
 static gboolean build_active_RAs(lrm_state_t * lrm_state, xmlNode * rsc_list);
 static gboolean stop_recurring_actions(gpointer key, gpointer value, gpointer user_data);
 
 static lrmd_event_data_t *construct_op(const lrm_state_t *lrm_state,
                                        const xmlNode *rsc_op,
                                        const char *rsc_id,
                                        const char *operation);
 static void do_lrm_rsc_op(lrm_state_t *lrm_state, lrmd_rsc_info_t *rsc,
                           xmlNode *msg, struct ra_metadata_s *md);
 
 static gboolean lrm_state_verify_stopped(lrm_state_t * lrm_state, enum crmd_fsa_state cur_state,
                                          int log_level);
 
 static void
 lrm_connection_destroy(void)
 {
     if (pcmk__is_set(controld_globals.fsa_input_register, R_LRM_CONNECTED)) {
         crm_crit("Lost connection to local executor");
         register_fsa_input(C_FSA_INTERNAL, I_ERROR, NULL);
         controld_clear_fsa_input_flags(R_LRM_CONNECTED);
     }
 }
 
 static char *
 make_stop_id(const char *rsc, int call_id)
 {
     return pcmk__assert_asprintf("%s:%d", rsc, call_id);
 }
 
 static void
 copy_instance_keys(gpointer key, gpointer value, gpointer user_data)
 {
     if (strstr(key, CRM_META "_") == NULL) {
         pcmk__insert_dup(user_data, (const char *) key, (const char *) value);
     }
 }
 
 static void
 copy_meta_keys(gpointer key, gpointer value, gpointer user_data)
 {
     if (strstr(key, CRM_META "_") != NULL) {
         pcmk__insert_dup(user_data, (const char *) key, (const char *) value);
     }
 }
 
 /*!
  * \internal
  * \brief Remove a recurring operation from a resource's history
  *
  * \param[in,out] history  Resource history to modify
  * \param[in]     op       Operation to remove
  *
  * \return TRUE if the operation was found and removed, FALSE otherwise
  */
 static gboolean
 history_remove_recurring_op(rsc_history_t *history, const lrmd_event_data_t *op)
 {
     GList *iter;
 
     for (iter = history->recurring_op_list; iter != NULL; iter = iter->next) {
         lrmd_event_data_t *existing = iter->data;
 
         if ((op->interval_ms == existing->interval_ms)
             && pcmk__str_eq(op->rsc_id, existing->rsc_id, pcmk__str_none)
             && pcmk__str_eq(op->op_type, existing->op_type, pcmk__str_casei)) {
 
             history->recurring_op_list = g_list_delete_link(history->recurring_op_list, iter);
             lrmd_free_event(existing);
             return TRUE;
         }
     }
     return FALSE;
 }
 
 /*!
  * \internal
  * \brief Free all recurring operations in resource history
  *
  * \param[in,out] history  Resource history to modify
  */
 static void
 history_free_recurring_ops(rsc_history_t *history)
 {
     GList *iter;
 
     for (iter = history->recurring_op_list; iter != NULL; iter = iter->next) {
         lrmd_free_event(iter->data);
     }
     g_list_free(history->recurring_op_list);
     history->recurring_op_list = NULL;
 }
 
 /*!
  * \internal
  * \brief Free resource history
  *
  * \param[in,out] history  Resource history to free
  */
 void
 history_free(gpointer data)
 {
     rsc_history_t *history = (rsc_history_t*)data;
 
     if (history->stop_params) {
         g_hash_table_destroy(history->stop_params);
     }
 
     /* Don't need to free history->rsc.id because it's set to history->id */
     free(history->rsc.type);
     free(history->rsc.standard);
     free(history->rsc.provider);
 
     lrmd_free_event(history->failed);
     lrmd_free_event(history->last);
     free(history->id);
     history_free_recurring_ops(history);
     free(history);
 }
 
 static void
 update_history_cache(lrm_state_t * lrm_state, lrmd_rsc_info_t * rsc, lrmd_event_data_t * op)
 {
     int target_rc = 0;
     rsc_history_t *entry = NULL;
 
     if (op->rsc_deleted) {
         crm_debug("Purged history for '%s' after %s", op->rsc_id, op->op_type);
         controld_delete_resource_history(op->rsc_id, lrm_state->node_name,
                                          NULL, crmd_cib_smart_opt());
         return;
     }
 
     if (pcmk__str_eq(op->op_type, PCMK_ACTION_NOTIFY, pcmk__str_casei)) {
         return;
     }
 
     crm_debug("Updating history for '%s' with %s op", op->rsc_id, op->op_type);
 
     entry = g_hash_table_lookup(lrm_state->resource_history, op->rsc_id);
     if (entry == NULL && rsc) {
         entry = pcmk__assert_alloc(1, sizeof(rsc_history_t));
         entry->id = pcmk__str_copy(op->rsc_id);
         g_hash_table_insert(lrm_state->resource_history, entry->id, entry);
 
         entry->rsc.id = entry->id;
         entry->rsc.type = pcmk__str_copy(rsc->type);
         entry->rsc.standard = pcmk__str_copy(rsc->standard);
         entry->rsc.provider = pcmk__str_copy(rsc->provider);
 
     } else if (entry == NULL) {
         crm_info("Resource %s no longer exists, not updating cache", op->rsc_id);
         return;
     }
 
     entry->last_callid = op->call_id;
     target_rc = rsc_op_expected_rc(op);
     if (op->op_status == PCMK_EXEC_CANCELLED) {
         if (op->interval_ms > 0) {
             crm_trace("Removing cancelled recurring op: " PCMK__OP_FMT,
                       op->rsc_id, op->op_type, op->interval_ms);
             history_remove_recurring_op(entry, op);
             return;
         } else {
             crm_trace("Skipping " PCMK__OP_FMT " rc=%d, status=%d",
                       op->rsc_id, op->op_type, op->interval_ms, op->rc,
                       op->op_status);
         }
 
     } else if (did_rsc_op_fail(op, target_rc)) {
         /* Store failed monitors here, otherwise the block below will cause them
          * to be forgotten when a stop happens.
          */
         if (entry->failed) {
             lrmd_free_event(entry->failed);
         }
         entry->failed = lrmd_copy_event(op);
 
     } else if (op->interval_ms == 0) {
         if (entry->last) {
             lrmd_free_event(entry->last);
         }
         entry->last = lrmd_copy_event(op);
 
         if (op->params && pcmk__strcase_any_of(op->op_type, PCMK_ACTION_START,
                                                PCMK_ACTION_RELOAD,
                                                PCMK_ACTION_RELOAD_AGENT,
                                                PCMK_ACTION_MONITOR, NULL)) {
             if (entry->stop_params) {
                 g_hash_table_destroy(entry->stop_params);
             }
             entry->stop_params = pcmk__strkey_table(free, free);
 
             g_hash_table_foreach(op->params, copy_instance_keys, entry->stop_params);
         }
     }
 
     if (op->interval_ms > 0) {
         /* Ensure there are no duplicates */
         history_remove_recurring_op(entry, op);
 
         crm_trace("Adding recurring op: " PCMK__OP_FMT,
                   op->rsc_id, op->op_type, op->interval_ms);
         entry->recurring_op_list = g_list_prepend(entry->recurring_op_list, lrmd_copy_event(op));
 
     } else if ((entry->recurring_op_list != NULL)
                 && !pcmk__str_eq(op->op_type, PCMK_ACTION_MONITOR,
                                  pcmk__str_casei)) {
         crm_trace("Dropping %d recurring ops because of: " PCMK__OP_FMT,
                   g_list_length(entry->recurring_op_list), op->rsc_id,
                   op->op_type, op->interval_ms);
         history_free_recurring_ops(entry);
     }
 }
 
 /*!
  * \internal
  * \brief Send a direct OK ack for a resource task
  *
  * \param[in] lrm_state  LRM connection
  * \param[in] input      Input message being ack'ed
  * \param[in] rsc_id     ID of affected resource
  * \param[in] rsc        Affected resource (if available)
  * \param[in] task       Operation task being ack'ed
  * \param[in] ack_host   Name of host to send ack to
  * \param[in] ack_sys    IPC system name to ack
  */
 static void
 send_task_ok_ack(const lrm_state_t *lrm_state, const ha_msg_input_t *input,
                  const char *rsc_id, const lrmd_rsc_info_t *rsc,
                  const char *task, const char *ack_host, const char *ack_sys)
 {
     lrmd_event_data_t *op = construct_op(lrm_state, input->xml, rsc_id, task);
 
     lrmd__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL);
     controld_ack_event_directly(ack_host, ack_sys, rsc, op, rsc_id);
     lrmd_free_event(op);
 }
 
 static inline const char *
 op_node_name(lrmd_event_data_t *op)
 {
     return pcmk__s(op->remote_nodename,
                    controld_globals.cluster->priv->node_name);
 }
 
 void
 lrm_op_callback(lrmd_event_data_t * op)
 {
     CRM_CHECK(op != NULL, return);
     switch (op->type) {
         case lrmd_event_disconnect:
             if (op->remote_nodename == NULL) {
                 /* If this is the local executor IPC connection, set the right
                  * bits in the controller when the connection goes down.
                  */
                 lrm_connection_destroy();
             }
             break;
 
         case lrmd_event_exec_complete:
             {
                 lrm_state_t *lrm_state =
                     controld_get_executor_state(op_node_name(op), false);
 
                 pcmk__assert(lrm_state != NULL);
                 process_lrm_event(lrm_state, op, NULL, NULL);
             }
             break;
 
         default:
             break;
     }
 }
 
 static void
 try_local_executor_connect(long long action, fsa_data_t *msg_data,
                            lrm_state_t *lrm_state)
 {
     int rc = pcmk_rc_ok;
 
     crm_debug("Connecting to the local executor");
 
     // If we can connect, great
     rc = controld_connect_local_executor(lrm_state);
     if (rc == pcmk_rc_ok) {
         controld_set_fsa_input_flags(R_LRM_CONNECTED);
         crm_info("Connection to the local executor established");
         return;
     }
 
     // Otherwise, if we can try again, set a timer to do so
     if (lrm_state->num_lrm_register_fails < MAX_LRM_REG_FAILS) {
         crm_warn("Failed to connect to the local executor %d time%s "
                  "(%d max): %s", lrm_state->num_lrm_register_fails,
                  pcmk__plural_s(lrm_state->num_lrm_register_fails),
                  MAX_LRM_REG_FAILS, pcmk_rc_str(rc));
         controld_start_wait_timer();
         crmd_fsa_stall(FALSE);
         return;
     }
 
     // Otherwise give up
     crm_err("Failed to connect to the executor the max allowed "
             "%d time%s: %s", lrm_state->num_lrm_register_fails,
             pcmk__plural_s(lrm_state->num_lrm_register_fails),
             pcmk_rc_str(rc));
     register_fsa_error(C_FSA_INTERNAL, I_ERROR, NULL);
 }
 
 /*	 A_LRM_CONNECT	*/
 void
 do_lrm_control(long long action,
                enum crmd_fsa_cause cause,
                enum crmd_fsa_state cur_state,
                enum crmd_fsa_input current_input, fsa_data_t * msg_data)
 {
     /* This only pertains to local executor connections. Remote connections are
      * handled as resources within the scheduler. Connecting and disconnecting
      * from remote executor instances is handled differently.
      */
 
     lrm_state_t *lrm_state = NULL;
 
     if (controld_globals.cluster->priv->node_name == NULL) {
         return; // Shouldn't be possible
     }
     lrm_state = controld_get_executor_state(NULL, true);
     if (lrm_state == NULL) {
         register_fsa_error(C_FSA_INTERNAL, I_ERROR, NULL);
         return;
     }
 
     if (action & A_LRM_DISCONNECT) {
         if (lrm_state_verify_stopped(lrm_state, cur_state, LOG_INFO) == FALSE) {
             if (action == A_LRM_DISCONNECT) {
                 crmd_fsa_stall(FALSE);
                 return;
             }
         }
 
         controld_clear_fsa_input_flags(R_LRM_CONNECTED);
         lrm_state_disconnect(lrm_state);
         lrm_state_reset_tables(lrm_state, FALSE);
     }
 
     if (action & A_LRM_CONNECT) {
         try_local_executor_connect(action, msg_data, lrm_state);
     }
 
     if (action & ~(A_LRM_CONNECT | A_LRM_DISCONNECT)) {
         crm_err("Unexpected action %s in %s", fsa_action2string(action),
                 __func__);
     }
 }
 
 static gboolean
 lrm_state_verify_stopped(lrm_state_t * lrm_state, enum crmd_fsa_state cur_state, int log_level)
 {
     int counter = 0;
     gboolean rc = TRUE;
     const char *when = "lrm disconnect";
 
     GHashTableIter gIter;
     const char *key = NULL;
     rsc_history_t *entry = NULL;
     active_op_t *pending = NULL;
 
     crm_debug("Checking for active resources before exit");
 
     if (cur_state == S_TERMINATE) {
         log_level = LOG_ERR;
         when = "shutdown";
 
     } else if (pcmk__is_set(controld_globals.fsa_input_register, R_SHUTDOWN)) {
         when = "shutdown... waiting";
     }
 
     if ((lrm_state->active_ops != NULL) && lrm_state_is_connected(lrm_state)) {
         guint removed = g_hash_table_foreach_remove(lrm_state->active_ops,
                                                     stop_recurring_actions,
                                                     lrm_state);
         guint nremaining = g_hash_table_size(lrm_state->active_ops);
 
         if (removed || nremaining) {
             crm_notice("Stopped %u recurring operation%s at %s (%u remaining)",
                        removed, pcmk__plural_s(removed), when, nremaining);
         }
     }
 
     if (lrm_state->active_ops != NULL) {
         g_hash_table_iter_init(&gIter, lrm_state->active_ops);
         while (g_hash_table_iter_next(&gIter, NULL, (void **)&pending)) {
             /* Ignore recurring actions in the shutdown calculations */
             if (pending->interval_ms == 0) {
                 counter++;
             }
         }
     }
 
     if (counter > 0) {
         do_crm_log(log_level, "%d pending executor operation%s at %s",
                    counter, pcmk__plural_s(counter), when);
 
         if ((cur_state == S_TERMINATE)
             || !pcmk__is_set(controld_globals.fsa_input_register,
                              R_SENT_RSC_STOP)) {
             g_hash_table_iter_init(&gIter, lrm_state->active_ops);
             while (g_hash_table_iter_next(&gIter, (gpointer*)&key, (gpointer*)&pending)) {
                 do_crm_log(log_level, "Pending action: %s (%s)", key, pending->op_key);
             }
 
         } else {
             rc = FALSE;
         }
         return rc;
     }
 
     if (lrm_state->resource_history == NULL) {
         return rc;
     }
 
     if (pcmk__is_set(controld_globals.fsa_input_register, R_SHUTDOWN)) {
         /* At this point we're not waiting, we're just shutting down */
         when = "shutdown";
     }
 
     counter = 0;
     g_hash_table_iter_init(&gIter, lrm_state->resource_history);
     while (g_hash_table_iter_next(&gIter, NULL, (gpointer*)&entry)) {
         if (is_rsc_active(lrm_state, entry->id) == FALSE) {
             continue;
         }
 
         counter++;
         if (log_level == LOG_ERR) {
             crm_info("Found %s active at %s", entry->id, when);
         } else {
             crm_trace("Found %s active at %s", entry->id, when);
         }
         if (lrm_state->active_ops != NULL) {
             GHashTableIter hIter;
 
             g_hash_table_iter_init(&hIter, lrm_state->active_ops);
             while (g_hash_table_iter_next(&hIter, (gpointer*)&key, (gpointer*)&pending)) {
                 if (pcmk__str_eq(entry->id, pending->rsc_id, pcmk__str_none)) {
                     crm_notice("%sction %s (%s) incomplete at %s",
                                pending->interval_ms == 0 ? "A" : "Recurring a",
                                key, pending->op_key, when);
                 }
             }
         }
     }
 
     if (counter) {
         crm_err("%d resource%s active at %s",
                 counter, (counter == 1)? " was" : "s were", when);
     }
 
     return rc;
 }
 
 static gboolean
 is_rsc_active(lrm_state_t * lrm_state, const char *rsc_id)
 {
     rsc_history_t *entry = NULL;
 
     entry = g_hash_table_lookup(lrm_state->resource_history, rsc_id);
     if (entry == NULL || entry->last == NULL) {
         return FALSE;
     }
 
     crm_trace("Processing %s: %s.%d=%d", rsc_id, entry->last->op_type,
               entry->last->interval_ms, entry->last->rc);
     if ((entry->last->rc == PCMK_OCF_OK)
         && pcmk__str_eq(entry->last->op_type, PCMK_ACTION_STOP,
                         pcmk__str_casei)) {
         return FALSE;
 
     } else if (entry->last->rc == PCMK_OCF_OK
                && pcmk__str_eq(entry->last->op_type, PCMK_ACTION_MIGRATE_TO,
                                pcmk__str_casei)) {
         // A stricter check is too complex ... leave that to the scheduler
         return FALSE;
 
     } else if (entry->last->rc == PCMK_OCF_NOT_RUNNING) {
         return FALSE;
 
     } else if ((entry->last->interval_ms == 0)
                && (entry->last->rc == PCMK_OCF_NOT_CONFIGURED)) {
         /* Badly configured resources can't be reliably stopped */
         return FALSE;
     }
 
     return TRUE;
 }
 
 static gboolean
 build_active_RAs(lrm_state_t * lrm_state, xmlNode * rsc_list)
 {
     GHashTableIter iter;
     rsc_history_t *entry = NULL;
 
     g_hash_table_iter_init(&iter, lrm_state->resource_history);
     while (g_hash_table_iter_next(&iter, NULL, (void **)&entry)) {
 
         GList *gIter = NULL;
         xmlNode *xml_rsc = pcmk__xe_create(rsc_list, PCMK__XE_LRM_RESOURCE);
 
         pcmk__xe_set(xml_rsc, PCMK_XA_ID, entry->id);
         pcmk__xe_set(xml_rsc, PCMK_XA_TYPE, entry->rsc.type);
         pcmk__xe_set(xml_rsc, PCMK_XA_CLASS, entry->rsc.standard);
         pcmk__xe_set(xml_rsc, PCMK_XA_PROVIDER, entry->rsc.provider);
 
         if (entry->last && entry->last->params) {
             static const char *name = CRM_META "_" PCMK__META_CONTAINER;
             const char *container = g_hash_table_lookup(entry->last->params,
                                                         name);
 
             if (container) {
                 crm_trace("Resource %s is a part of container resource %s", entry->id, container);
                 pcmk__xe_set(xml_rsc, PCMK__META_CONTAINER, container);
             }
         }
         controld_add_resource_history_xml(xml_rsc, &(entry->rsc), entry->failed,
                                           lrm_state->node_name);
         controld_add_resource_history_xml(xml_rsc, &(entry->rsc), entry->last,
                                           lrm_state->node_name);
         for (gIter = entry->recurring_op_list; gIter != NULL; gIter = gIter->next) {
             controld_add_resource_history_xml(xml_rsc, &(entry->rsc), gIter->data,
                                               lrm_state->node_name);
         }
     }
 
     return FALSE;
 }
 
 xmlNode *
 controld_query_executor_state(void)
 {
     // @TODO Ensure all callers handle NULL returns
     xmlNode *xml_state = NULL;
     xmlNode *xml_data = NULL;
     xmlNode *rsc_list = NULL;
     pcmk__node_status_t *peer = NULL;
     lrm_state_t *lrm_state = controld_get_executor_state(NULL, false);
 
     if (!lrm_state) {
         crm_err("Could not get executor state for local node");
         return NULL;
     }
 
     peer = pcmk__get_node(0, lrm_state->node_name, NULL, pcmk__node_search_any);
     CRM_CHECK(peer != NULL, return NULL);
 
     xml_state = create_node_state_update(peer,
                                          node_update_cluster|node_update_peer,
                                          NULL, __func__);
     if (xml_state == NULL) {
         return NULL;
     }
 
     xml_data = pcmk__xe_create(xml_state, PCMK__XE_LRM);
     pcmk__xe_set(xml_data, PCMK_XA_ID, peer->xml_id);
     rsc_list = pcmk__xe_create(xml_data, PCMK__XE_LRM_RESOURCES);
 
     // Build a list of active (not necessarily running) resources
     build_active_RAs(lrm_state, rsc_list);
 
     crm_log_xml_trace(xml_state, "Current executor state");
 
     return xml_state;
 }
 
 /*!
  * \internal
  * \brief Map standard Pacemaker return code to operation status and OCF code
  *
  * \param[out] event  Executor event whose status and return code should be set
  * \param[in]  rc     Standard Pacemaker return code
  */
 void
 controld_rc2event(lrmd_event_data_t *event, int rc)
 {
     /* This is called for cleanup requests from controller peers/clients, not
      * for resource actions, so no exit reason is needed.
      */
     switch (rc) {
         case pcmk_rc_ok:
             lrmd__set_result(event, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL);
             break;
         case EACCES:
             lrmd__set_result(event, PCMK_OCF_INSUFFICIENT_PRIV,
                              PCMK_EXEC_ERROR, NULL);
             break;
         default:
             lrmd__set_result(event, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR,
                              NULL);
             break;
     }
 }
 
 /*!
  * \internal
  * \brief Trigger a new transition after CIB status was deleted
  *
  * If a CIB status delete was not expected (as part of the transition graph),
  * trigger a new transition by updating the (arbitrary) "last-lrm-refresh"
  * cluster property.
  *
  * \param[in] from_sys  IPC name that requested the delete
  * \param[in] rsc_id    Resource whose status was deleted (for logging only)
  */
 void
 controld_trigger_delete_refresh(const char *from_sys, const char *rsc_id)
 {
     if (!pcmk__str_eq(from_sys, CRM_SYSTEM_TENGINE, pcmk__str_casei)) {
         char *now_s = pcmk__assert_asprintf("%lld", (long long) time(NULL));
 
         crm_debug("Triggering a refresh after %s cleaned %s", from_sys, rsc_id);
         cib__update_node_attr(controld_globals.logger_out,
                               controld_globals.cib_conn, cib_none,
                               PCMK_XE_CRM_CONFIG, NULL, NULL, NULL, NULL,
                               "last-lrm-refresh", now_s, NULL, NULL);
         free(now_s);
     }
 }
 
 static void
 notify_deleted(lrm_state_t * lrm_state, ha_msg_input_t * input, const char *rsc_id, int rc)
 {
     lrmd_event_data_t *op = NULL;
     const char *from_sys = pcmk__xe_get(input->msg, PCMK__XA_CRM_SYS_FROM);
     const char *from_host = pcmk__xe_get(input->msg, PCMK__XA_SRC);
 
     crm_info("Notifying %s on %s that %s was%s deleted",
              from_sys, (from_host? from_host : "localhost"), rsc_id,
              ((rc == pcmk_ok)? "" : " not"));
     op = construct_op(lrm_state, input->xml, rsc_id, PCMK_ACTION_DELETE);
     controld_rc2event(op, pcmk_legacy2rc(rc));
     controld_ack_event_directly(from_host, from_sys, NULL, op, rsc_id);
     lrmd_free_event(op);
     controld_trigger_delete_refresh(from_sys, rsc_id);
 }
 
 static gboolean
 lrm_remove_deleted_rsc(gpointer key, gpointer value, gpointer user_data)
 {
     struct delete_event_s *event = user_data;
     struct pending_deletion_op_s *op = value;
 
     if (pcmk__str_eq(event->rsc, op->rsc, pcmk__str_none)) {
         notify_deleted(event->lrm_state, op->input, event->rsc, event->rc);
         return TRUE;
     }
     return FALSE;
 }
 
 static gboolean
 lrm_remove_deleted_op(gpointer key, gpointer value, gpointer user_data)
 {
     const char *rsc = user_data;
     active_op_t *pending = value;
 
     if (pcmk__str_eq(rsc, pending->rsc_id, pcmk__str_none)) {
         crm_info("Removing op %s:%d for deleted resource %s",
                  pending->op_key, pending->call_id, rsc);
         return TRUE;
     }
     return FALSE;
 }
 
 static void
 delete_rsc_entry(lrm_state_t *lrm_state, ha_msg_input_t *input,
                  const char *rsc_id, GHashTableIter *rsc_iter, int rc,
                  const char *user_name, bool from_cib)
 {
     struct delete_event_s event;
 
     CRM_CHECK(rsc_id != NULL, return);
 
     if (rc == pcmk_ok) {
         char *rsc_id_copy = pcmk__str_copy(rsc_id);
 
         if (rsc_iter) {
             g_hash_table_iter_remove(rsc_iter);
         } else {
             g_hash_table_remove(lrm_state->resource_history, rsc_id_copy);
         }
 
         if (from_cib) {
             controld_delete_resource_history(rsc_id_copy, lrm_state->node_name,
                                              user_name, crmd_cib_smart_opt());
         }
         g_hash_table_foreach_remove(lrm_state->active_ops,
                                     lrm_remove_deleted_op, rsc_id_copy);
         free(rsc_id_copy);
     }
 
     if (input) {
         notify_deleted(lrm_state, input, rsc_id, rc);
     }
 
     event.rc = rc;
     event.rsc = rsc_id;
     event.lrm_state = lrm_state;
     g_hash_table_foreach_remove(lrm_state->deletion_ops, lrm_remove_deleted_rsc, &event);
 }
 
 static inline gboolean
 last_failed_matches_op(rsc_history_t *entry, const char *op, guint interval_ms)
 {
     if (entry == NULL) {
         return FALSE;
     }
     if (op == NULL) {
         return TRUE;
     }
     return (pcmk__str_eq(op, entry->failed->op_type, pcmk__str_casei)
             && (interval_ms == entry->failed->interval_ms));
 }
 
 /*!
  * \internal
  * \brief Clear a resource's last failure
  *
  * Erase a resource's last failure on a particular node from both the
  * LRM resource history in the CIB, and the resource history remembered
  * for the LRM state.
  *
  * \param[in] rsc_id      Resource name
  * \param[in] node_name   Node name
  * \param[in] operation   If specified, only clear if matching this operation
  * \param[in] interval_ms If operation is specified, it has this interval
  */
 void
 lrm_clear_last_failure(const char *rsc_id, const char *node_name,
                        const char *operation, guint interval_ms)
 {
     lrm_state_t *lrm_state = controld_get_executor_state(node_name, false);
 
     if (lrm_state == NULL) {
         return;
     }
     if (lrm_state->resource_history != NULL) {
         rsc_history_t *entry = g_hash_table_lookup(lrm_state->resource_history,
                                                    rsc_id);
 
         if (last_failed_matches_op(entry, operation, interval_ms)) {
             lrmd_free_event(entry->failed);
             entry->failed = NULL;
         }
     }
 }
 
 /* Returns: gboolean - cancellation is in progress */
 static gboolean
 cancel_op(lrm_state_t * lrm_state, const char *rsc_id, const char *key, int op, gboolean remove)
 {
     int rc = pcmk_ok;
     char *local_key = NULL;
     active_op_t *pending = NULL;
 
     CRM_CHECK(op != 0, return FALSE);
     CRM_CHECK(rsc_id != NULL, return FALSE);
     if (key == NULL) {
         local_key = make_stop_id(rsc_id, op);
         key = local_key;
     }
     pending = g_hash_table_lookup(lrm_state->active_ops, key);
 
     if (pending) {
         if (remove && !pcmk__is_set(pending->flags, active_op_remove)) {
             controld_set_active_op_flags(pending, active_op_remove);
             crm_debug("Scheduling %s for removal", key);
         }
 
         if (pcmk__is_set(pending->flags, active_op_cancelled)) {
             crm_debug("Operation %s already cancelled", key);
             free(local_key);
             return FALSE;
         }
         controld_set_active_op_flags(pending, active_op_cancelled);
 
     } else {
         crm_info("No pending op found for %s", key);
         free(local_key);
         return FALSE;
     }
 
     crm_debug("Cancelling op %d for %s (%s)", op, rsc_id, key);
     rc = lrm_state_cancel(lrm_state, pending->rsc_id, pending->op_type,
                           pending->interval_ms);
     if (rc == pcmk_ok) {
         crm_debug("Op %d for %s (%s): cancelled", op, rsc_id, key);
         free(local_key);
         return TRUE;
     }
 
     crm_debug("Op %d for %s (%s): Nothing to cancel", op, rsc_id, key);
     /* The caller needs to make sure the entry is
      * removed from the active operations list
      *
      * Usually by returning TRUE inside the worker function
      * supplied to g_hash_table_foreach_remove()
      *
      * Not removing the entry from active operations will block
      * the node from shutting down
      */
     free(local_key);
     return FALSE;
 }
 
 struct cancel_data {
     gboolean done;
     gboolean remove;
     const char *key;
     lrmd_rsc_info_t *rsc;
     lrm_state_t *lrm_state;
 };
 
 static gboolean
 cancel_action_by_key(gpointer key, gpointer value, gpointer user_data)
 {
     gboolean remove = FALSE;
     struct cancel_data *data = user_data;
     active_op_t *op = value;
 
     if (pcmk__str_eq(op->op_key, data->key, pcmk__str_none)) {
         data->done = TRUE;
         remove = !cancel_op(data->lrm_state, data->rsc->id, key, op->call_id, data->remove);
     }
     return remove;
 }
 
 static gboolean
 cancel_op_key(lrm_state_t * lrm_state, lrmd_rsc_info_t * rsc, const char *key, gboolean remove)
 {
     guint removed = 0;
     struct cancel_data data;
 
     CRM_CHECK(rsc != NULL, return FALSE);
     CRM_CHECK(key != NULL, return FALSE);
 
     data.key = key;
     data.rsc = rsc;
     data.done = FALSE;
     data.remove = remove;
     data.lrm_state = lrm_state;
 
     removed = g_hash_table_foreach_remove(lrm_state->active_ops,
                                           cancel_action_by_key, &data);
     crm_trace("Removed %u op cache entries, new size: %u",
               removed, g_hash_table_size(lrm_state->active_ops));
     return data.done;
 }
 
 /*!
  * \internal
  * \brief Retrieve resource information from LRM
  *
  * \param[in,out]  lrm_state  Executor connection state to use
  * \param[in]      rsc_xml    XML containing resource configuration
  * \param[in]      do_create  If true, register resource if not already
  * \param[out]     rsc_info   Where to store information obtained from executor
  *
  * \retval pcmk_ok   Success (and rsc_info holds newly allocated result)
  * \retval -EINVAL   Required information is missing from arguments
  * \retval -ENOTCONN No active connection to LRM
  * \retval -ENODEV   Resource not found
  * \retval -errno    Error communicating with executor when registering resource
  *
  * \note Caller is responsible for freeing result on success.
  */
 static int
 get_lrm_resource(lrm_state_t *lrm_state, const xmlNode *rsc_xml,
                  gboolean do_create, lrmd_rsc_info_t **rsc_info)
 {
     const char *id = pcmk__xe_id(rsc_xml);
 
     CRM_CHECK(lrm_state && rsc_xml && rsc_info, return -EINVAL);
     CRM_CHECK(id, return -EINVAL);
 
     if (lrm_state_is_connected(lrm_state) == FALSE) {
         return -ENOTCONN;
     }
 
     crm_trace("Retrieving resource information for %s from the executor", id);
     *rsc_info = lrm_state_get_rsc_info(lrm_state, id, 0);
 
     // If resource isn't known by ID, try clone name, if provided
     if (!*rsc_info) {
         const char *long_id = pcmk__xe_get(rsc_xml, PCMK__XA_LONG_ID);
 
         if (long_id) {
             *rsc_info = lrm_state_get_rsc_info(lrm_state, long_id, 0);
         }
     }
 
     if ((*rsc_info == NULL) && do_create) {
         const char *class = pcmk__xe_get(rsc_xml, PCMK_XA_CLASS);
         const char *provider = pcmk__xe_get(rsc_xml, PCMK_XA_PROVIDER);
         const char *type = pcmk__xe_get(rsc_xml, PCMK_XA_TYPE);
         int rc;
 
         crm_trace("Registering resource %s with the executor", id);
         rc = lrm_state_register_rsc(lrm_state, id, class, provider, type,
                                     lrmd_opt_drop_recurring);
         if (rc != pcmk_ok) {
             fsa_data_t *msg_data = NULL;
 
             crm_err("Could not register resource %s with the executor on %s: %s "
                     QB_XS " rc=%d",
                     id, lrm_state->node_name, pcmk_strerror(rc), rc);
 
             /* Register this as an internal error if this involves the local
              * executor. Otherwise, we're likely dealing with an unresponsive
              * remote node, which is not an FSA failure.
              */
             if (lrm_state_is_local(lrm_state) == TRUE) {
                 register_fsa_error(C_FSA_INTERNAL, I_FAIL, NULL);
             }
             return rc;
         }
 
         *rsc_info = lrm_state_get_rsc_info(lrm_state, id, 0);
     }
     return *rsc_info? pcmk_ok : -ENODEV;
 }
 
 static void
 delete_resource(lrm_state_t *lrm_state, const char *id, lrmd_rsc_info_t *rsc,
                 GHashTableIter *iter, const char *sys, const char *user,
                 ha_msg_input_t *request, bool unregister, bool from_cib)
 {
     int rc = pcmk_ok;
 
     crm_info("Removing resource %s from executor for %s%s%s",
              id, sys, (user? " as " : ""), (user? user : ""));
 
     if (rsc && unregister) {
         rc = lrm_state_unregister_rsc(lrm_state, id, 0);
     }
 
     if (rc == pcmk_ok) {
         crm_trace("Resource %s deleted from executor", id);
     } else if (rc == -EINPROGRESS) {
         crm_info("Deletion of resource '%s' from executor is pending", id);
         if (request) {
             struct pending_deletion_op_s *op = NULL;
             char *ref = pcmk__xe_get_copy(request->msg, PCMK_XA_REFERENCE);
 
             op = pcmk__assert_alloc(1, sizeof(struct pending_deletion_op_s));
             op->rsc = pcmk__str_copy(rsc->id);
             op->input = copy_ha_msg_input(request);
             g_hash_table_insert(lrm_state->deletion_ops, ref, op);
         }
         return;
     } else {
         crm_warn("Could not delete '%s' from executor for %s%s%s: %s "
                  QB_XS " rc=%d", id, sys, (user? " as " : ""),
                  (user? user : ""), pcmk_strerror(rc), rc);
     }
 
     delete_rsc_entry(lrm_state, request, id, iter, rc, user, from_cib);
 }
 
 static int
 get_fake_call_id(lrm_state_t *lrm_state, const char *rsc_id)
 {
     int call_id = 999999999;
     rsc_history_t *entry = NULL;
 
     if(lrm_state) {
         entry = g_hash_table_lookup(lrm_state->resource_history, rsc_id);
     }
 
     /* Make sure the call id is greater than the last successful operation,
      * otherwise the failure will not result in a possible recovery of the resource
      * as it could appear the failure occurred before the successful start */
     if (entry) {
         call_id = entry->last_callid + 1;
     }
 
     if (call_id < 0) {
         call_id = 1;
     }
     return call_id;
 }
 
 static void
 fake_op_status(lrm_state_t *lrm_state, lrmd_event_data_t *op, int op_status,
                enum ocf_exitcode op_exitcode, const char *exit_reason)
 {
     op->call_id = get_fake_call_id(lrm_state, op->rsc_id);
     op->t_run = time(NULL);
     op->t_rcchange = op->t_run;
     lrmd__set_result(op, op_exitcode, op_status, exit_reason);
 }
 
 static void
 force_reprobe(lrm_state_t *lrm_state, const char *from_sys,
               const char *from_host, const char *user_name,
               gboolean is_remote_node, bool reprobe_all_nodes)
 {
     GHashTableIter gIter;
     rsc_history_t *entry = NULL;
 
     crm_info("Clearing resource history on node %s", lrm_state->node_name);
     g_hash_table_iter_init(&gIter, lrm_state->resource_history);
     while (g_hash_table_iter_next(&gIter, NULL, (void **)&entry)) {
         /* only unregister the resource during a reprobe if it is not a remote connection
          * resource. otherwise unregistering the connection will terminate remote-node
          * membership */
         bool unregister = true;
 
         if (is_remote_lrmd_ra(NULL, NULL, entry->id)) {
             unregister = false;
 
             if (reprobe_all_nodes) {
                 lrm_state_t *remote_lrm_state =
                     controld_get_executor_state(entry->id, false);
 
                 if (remote_lrm_state != NULL) {
                     /* If reprobing all nodes, be sure to reprobe the remote
                      * node before clearing its connection resource
                      */
                     force_reprobe(remote_lrm_state, from_sys, from_host,
                                   user_name, TRUE, reprobe_all_nodes);
                 }
             }
         }
 
         /* Don't delete from the CIB, since we'll delete the whole node's LRM
          * state from the CIB soon
          */
         delete_resource(lrm_state, entry->id, &entry->rsc, &gIter, from_sys,
                         user_name, NULL, unregister, false);
     }
 
     /* Now delete the copy in the CIB */
     controld_delete_node_state(lrm_state->node_name, controld_section_lrm,
                                cib_none);
 }
 
 /*!
  * \internal
  * \brief Fail a requested action without actually executing it
  *
  * For an action that can't be executed, process it similarly to an actual
  * execution result, with specified error status (except for notify actions,
  * which will always be treated as successful).
  *
  * \param[in,out] lrm_state    Executor connection that action is for
  * \param[in]     action       Action XML from request
  * \param[in]     rc           Desired return code to use
  * \param[in]     op_status    Desired operation status to use
  * \param[in]     exit_reason  Human-friendly detail, if error
  */
 static void
 synthesize_lrmd_failure(lrm_state_t *lrm_state, const xmlNode *action,
                         int op_status, enum ocf_exitcode rc,
                         const char *exit_reason)
 {
     lrmd_event_data_t *op = NULL;
     const char *operation = pcmk__xe_get(action, PCMK_XA_OPERATION);
     const char *target_node = pcmk__xe_get(action, PCMK__META_ON_NODE);
     xmlNode *xml_rsc = pcmk__xe_first_child(action, PCMK_XE_PRIMITIVE, NULL,
                                             NULL);
 
     if ((xml_rsc == NULL) || (pcmk__xe_id(xml_rsc) == NULL)) {
         /* @TODO Should we do something else, like direct ack? */
         crm_info("Can't fake %s failure (%d) on %s without resource configuration",
                  pcmk__xe_get(action, PCMK__XA_OPERATION_KEY), rc, target_node);
         return;
 
     } else if(operation == NULL) {
         /* This probably came from crm_resource -C, nothing to do */
         crm_info("Can't fake %s failure (%d) on %s without operation",
                  pcmk__xe_id(xml_rsc), rc, target_node);
         return;
     }
 
     op = construct_op(lrm_state, action, pcmk__xe_id(xml_rsc), operation);
 
     if (pcmk__str_eq(operation, PCMK_ACTION_NOTIFY, pcmk__str_casei)) {
         // Notifications can't fail
         fake_op_status(lrm_state, op, PCMK_EXEC_DONE, PCMK_OCF_OK, NULL);
     } else {
         fake_op_status(lrm_state, op, op_status, rc, exit_reason);
     }
 
     crm_info("Faking " PCMK__OP_FMT " result (%d) on %s",
              op->rsc_id, op->op_type, op->interval_ms, op->rc, target_node);
 
     // Process the result as if it came from the LRM
     process_lrm_event(lrm_state, op, NULL, action);
     lrmd_free_event(op);
 }
 
 /*!
  * \internal
  * \brief Get target of an LRM operation (replacing \p NULL with local node
  *        name)
  *
  * \param[in] xml  LRM operation data XML
  *
  * \return LRM operation target node name (local node or Pacemaker Remote node)
  */
 static const char *
 lrm_op_target(const xmlNode *xml)
 {
     const char *target = NULL;
 
     if (xml) {
         target = pcmk__xe_get(xml, PCMK__META_ON_NODE);
     }
     if (target == NULL) {
         target = controld_globals.cluster->priv->node_name;
     }
     return target;
 }
 
 static void
 fail_lrm_resource(xmlNode *xml, lrm_state_t *lrm_state, const char *user_name,
                   const char *from_host, const char *from_sys)
 {
     lrmd_event_data_t *op = NULL;
     lrmd_rsc_info_t *rsc = NULL;
     xmlNode *xml_rsc = pcmk__xe_first_child(xml, PCMK_XE_PRIMITIVE, NULL, NULL);
 
     CRM_CHECK(xml_rsc != NULL, return);
 
     /* The executor simply executes operations and reports the results, without
      * any concept of success or failure, so to fail a resource, we must fake
      * what a failure looks like.
      *
      * To do this, we create a fake executor operation event for the resource,
      * and pass that event to the executor client callback so it will be
      * processed as if it came from the executor.
      */
     op = construct_op(lrm_state, xml, pcmk__xe_id(xml_rsc), "asyncmon");
 
     free((char*) op->user_data);
     op->user_data = NULL;
     op->interval_ms = 0;
 
     if (user_name && !pcmk__is_privileged(user_name)) {
         crm_err("%s does not have permission to fail %s",
                 user_name, pcmk__xe_id(xml_rsc));
         fake_op_status(lrm_state, op, PCMK_EXEC_ERROR,
                        PCMK_OCF_INSUFFICIENT_PRIV,
                        "Unprivileged user cannot fail resources");
         controld_ack_event_directly(from_host, from_sys, NULL, op,
                                     pcmk__xe_id(xml_rsc));
         lrmd_free_event(op);
         return;
     }
 
 
     if (get_lrm_resource(lrm_state, xml_rsc, TRUE, &rsc) == pcmk_ok) {
         crm_info("Failing resource %s...", rsc->id);
         fake_op_status(lrm_state, op, PCMK_EXEC_DONE, PCMK_OCF_UNKNOWN_ERROR,
                        "Simulated failure");
         process_lrm_event(lrm_state, op, NULL, xml);
         op->rc = PCMK_OCF_OK; // The request to fail the resource succeeded
         lrmd_free_rsc_info(rsc);
 
     } else {
         crm_info("Cannot find/create resource in order to fail it...");
         crm_log_xml_warn(xml, "bad input");
         fake_op_status(lrm_state, op, PCMK_EXEC_ERROR, PCMK_OCF_UNKNOWN_ERROR,
                        "Cannot fail unknown resource");
     }
 
     controld_ack_event_directly(from_host, from_sys, NULL, op,
                                 pcmk__xe_id(xml_rsc));
     lrmd_free_event(op);
 }
 
 static void
 handle_reprobe_op(lrm_state_t *lrm_state, xmlNode *msg, const char *from_sys,
                   const char *from_host, const char *user_name,
                   gboolean is_remote_node, bool reprobe_all_nodes)
 {
     crm_notice("Forcing the status of all resources to be redetected");
     force_reprobe(lrm_state, from_sys, from_host, user_name, is_remote_node,
                   reprobe_all_nodes);
 
     if (!pcmk__strcase_any_of(from_sys, CRM_SYSTEM_PENGINE, CRM_SYSTEM_TENGINE, NULL)) {
         xmlNode *reply = pcmk__new_reply(msg, NULL);
 
         crm_debug("ACK'ing re-probe from %s (%s)", from_sys, from_host);
 
         if (relay_message(reply, TRUE) == FALSE) {
             crm_log_xml_err(reply, "Unable to route reply");
         }
         pcmk__xml_free(reply);
     }
 }
 
 static bool do_lrm_cancel(ha_msg_input_t *input, lrm_state_t *lrm_state,
               lrmd_rsc_info_t *rsc, const char *from_host, const char *from_sys)
 {
     char *op_key = NULL;
     char *meta_key = NULL;
     int call = 0;
     const char *call_id = NULL;
     const char *op_task = NULL;
     guint interval_ms = 0;
     gboolean in_progress = FALSE;
     xmlNode *params = pcmk__xe_first_child(input->xml, PCMK__XE_ATTRIBUTES,
                                            NULL, NULL);
 
     CRM_CHECK(params != NULL, return FALSE);
 
     meta_key = crm_meta_name(PCMK_XA_OPERATION);
     op_task = pcmk__xe_get(params, meta_key);
     free(meta_key);
     CRM_CHECK(op_task != NULL, return FALSE);
 
     meta_key = crm_meta_name(PCMK_META_INTERVAL);
     if (pcmk__xe_get_guint(params, meta_key, &interval_ms) != pcmk_rc_ok) {
         free(meta_key);
         return FALSE;
     }
     free(meta_key);
 
     op_key = pcmk__op_key(rsc->id, op_task, interval_ms);
 
     meta_key = crm_meta_name(PCMK__XA_CALL_ID);
     call_id = pcmk__xe_get(params, meta_key);
     free(meta_key);
 
     crm_debug("Scheduler requested op %s (call=%s) be cancelled",
               op_key, (call_id? call_id : "NA"));
     pcmk__scan_min_int(call_id, &call, 0);
     if (call == 0) {
         // Normal case when the scheduler cancels a recurring op
         in_progress = cancel_op_key(lrm_state, rsc, op_key, TRUE);
 
     } else {
         // Normal case when the scheduler cancels an orphan op
         in_progress = cancel_op(lrm_state, rsc->id, NULL, call, TRUE);
     }
 
     // Acknowledge cancellation operation if for a remote connection resource
     if (!in_progress || is_remote_lrmd_ra(NULL, NULL, rsc->id)) {
         char *op_id = make_stop_id(rsc->id, call);
 
         if (is_remote_lrmd_ra(NULL, NULL, rsc->id) == FALSE) {
             crm_info("Nothing known about operation %d for %s", call, op_key);
         }
         controld_delete_action_history_by_key(rsc->id, lrm_state->node_name,
                                               op_key, call);
         send_task_ok_ack(lrm_state, input, rsc->id, rsc, op_task,
                          from_host, from_sys);
 
         /* needed at least for cancellation of a remote operation */
         if (lrm_state->active_ops != NULL) {
             g_hash_table_remove(lrm_state->active_ops, op_id);
         }
         free(op_id);
     }
 
     free(op_key);
     return TRUE;
 }
 
 static void
 do_lrm_delete(ha_msg_input_t *input, lrm_state_t *lrm_state,
               lrmd_rsc_info_t *rsc, const char *from_sys, const char *from_host,
               bool crm_rsc_delete, const char *user_name)
 {
     bool unregister = true;
     int cib_rc = controld_delete_resource_history(rsc->id, lrm_state->node_name,
                                                   user_name,
                                                   cib_dryrun|cib_sync_call);
 
     if (cib_rc != pcmk_rc_ok) {
         lrmd_event_data_t *op = NULL;
 
         op = construct_op(lrm_state, input->xml, rsc->id, PCMK_ACTION_DELETE);
 
         /* These are resource clean-ups, not actions, so no exit reason is
          * needed.
          */
         lrmd__set_result(op, pcmk_rc2ocf(cib_rc), PCMK_EXEC_ERROR, NULL);
         controld_ack_event_directly(from_host, from_sys, NULL, op, rsc->id);
         lrmd_free_event(op);
         return;
     }
 
     if (crm_rsc_delete && is_remote_lrmd_ra(NULL, NULL, rsc->id)) {
         unregister = false;
     }
 
     delete_resource(lrm_state, rsc->id, rsc, NULL, from_sys,
                     user_name, input, unregister, true);
 }
 
 // User data for asynchronous metadata execution
 struct metadata_cb_data {
     lrmd_rsc_info_t *rsc;   // Copy of resource information
     xmlNode *input_xml;     // Copy of FSA input XML
 };
 
 static struct metadata_cb_data *
 new_metadata_cb_data(lrmd_rsc_info_t *rsc, xmlNode *input_xml)
 {
     struct metadata_cb_data *data = NULL;
 
     data = pcmk__assert_alloc(1, sizeof(struct metadata_cb_data));
     data->input_xml = pcmk__xml_copy(NULL, input_xml);
     data->rsc = lrmd_copy_rsc_info(rsc);
     return data;
 }
 
 static void
 free_metadata_cb_data(struct metadata_cb_data *data)
 {
     lrmd_free_rsc_info(data->rsc);
     pcmk__xml_free(data->input_xml);
     free(data);
 }
 
 /*!
  * \internal
  * \brief Execute an action after metadata has been retrieved
  *
  * \param[in] pid        Ignored
  * \param[in] result     Result of metadata action
  * \param[in] user_data  Metadata callback data
  */
 static void
 metadata_complete(int pid, const pcmk__action_result_t *result, void *user_data)
 {
     struct metadata_cb_data *data = (struct metadata_cb_data *) user_data;
 
     struct ra_metadata_s *md = NULL;
     lrm_state_t *lrm_state =
         controld_get_executor_state(lrm_op_target(data->input_xml), false);
 
     if ((lrm_state != NULL) && pcmk__result_ok(result)) {
         md = controld_cache_metadata(lrm_state->metadata_cache, data->rsc,
                                      result->action_stdout);
     }
     if (!pcmk__is_set(controld_globals.fsa_input_register, R_HA_DISCONNECTED)) {
         do_lrm_rsc_op(lrm_state, data->rsc, data->input_xml, md);
     }
     free_metadata_cb_data(data);
 }
 
 /*	 A_LRM_INVOKE	*/
 void
 do_lrm_invoke(long long action,
               enum crmd_fsa_cause cause,
               enum crmd_fsa_state cur_state,
               enum crmd_fsa_input current_input, fsa_data_t * msg_data)
 {
     lrm_state_t *lrm_state = NULL;
     const char *crm_op = NULL;
     const char *from_sys = NULL;
     const char *from_host = NULL;
     const char *operation = NULL;
     ha_msg_input_t *input = fsa_typed_data(fsa_dt_ha_msg);
     const char *user_name = NULL;
     const char *target_node = lrm_op_target(input->xml);
     gboolean is_remote_node = FALSE;
     bool crm_rsc_delete = FALSE;
 
     // Message routed to the local node is targeting a specific, non-local node
     is_remote_node = !controld_is_local_node(target_node);
 
     lrm_state = controld_get_executor_state(target_node, false);
     if ((lrm_state == NULL) && is_remote_node) {
         crm_err("Failing action because local node has never had connection to remote node %s",
                 target_node);
         synthesize_lrmd_failure(NULL, input->xml, PCMK_EXEC_NOT_CONNECTED,
                                 PCMK_OCF_UNKNOWN_ERROR,
                                 "Local node has no connection to remote");
         return;
     }
     pcmk__assert(lrm_state != NULL);
 
     user_name = pcmk__update_acl_user(input->msg, PCMK__XA_CRM_USER, NULL);
     crm_op = pcmk__xe_get(input->msg, PCMK__XA_CRM_TASK);
     from_sys = pcmk__xe_get(input->msg, PCMK__XA_CRM_SYS_FROM);
     if (!pcmk__str_eq(from_sys, CRM_SYSTEM_TENGINE, pcmk__str_none)) {
         from_host = pcmk__xe_get(input->msg, PCMK__XA_SRC);
     }
 
     if (pcmk__str_eq(crm_op, PCMK_ACTION_LRM_DELETE, pcmk__str_none)) {
         if (!pcmk__str_eq(from_sys, CRM_SYSTEM_TENGINE, pcmk__str_none)) {
             crm_rsc_delete = TRUE; // from crm_resource
         }
         operation = PCMK_ACTION_DELETE;
 
     } else if (input->xml != NULL) {
         operation = pcmk__xe_get(input->xml, PCMK_XA_OPERATION);
     }
 
     CRM_CHECK(!pcmk__str_empty(crm_op) || !pcmk__str_empty(operation), return);
 
     crm_trace("'%s' execution request from %s as %s user",
               pcmk__s(crm_op, operation),
               pcmk__s(from_sys, "unknown subsystem"),
               pcmk__s(user_name, "current"));
 
     if (pcmk__str_eq(crm_op, CRM_OP_LRM_FAIL, pcmk__str_none)) {
         fail_lrm_resource(input->xml, lrm_state, user_name, from_host,
                           from_sys);
 
     } else if (pcmk__str_eq(crm_op, CRM_OP_REPROBE, pcmk__str_none)
                || pcmk__str_eq(operation, CRM_OP_REPROBE, pcmk__str_none)) {
         const char *raw_target = NULL;
 
         if (input->xml != NULL) {
             // For CRM_OP_REPROBE, a NULL target means we're targeting all nodes
             raw_target = pcmk__xe_get(input->xml, PCMK__META_ON_NODE);
         }
         handle_reprobe_op(lrm_state, input->msg, from_sys, from_host, user_name,
                           is_remote_node, (raw_target == NULL));
 
     } else if (operation != NULL) {
         lrmd_rsc_info_t *rsc = NULL;
         xmlNode *xml_rsc = pcmk__xe_first_child(input->xml, PCMK_XE_PRIMITIVE,
                                                 NULL, NULL);
         gboolean create_rsc = !pcmk__str_eq(operation, PCMK_ACTION_DELETE,
                                             pcmk__str_none);
         int rc;
 
         // We can't return anything meaningful without a resource ID
         CRM_CHECK((xml_rsc != NULL) && (pcmk__xe_id(xml_rsc) != NULL), return);
 
         rc = get_lrm_resource(lrm_state, xml_rsc, create_rsc, &rsc);
         if (rc == -ENOTCONN) {
             synthesize_lrmd_failure(lrm_state, input->xml,
                                     PCMK_EXEC_NOT_CONNECTED,
                                     PCMK_OCF_UNKNOWN_ERROR,
                                     "Not connected to remote executor");
             return;
 
         } else if ((rc < 0) && !create_rsc) {
             /* Delete of malformed or nonexistent resource
              * (deleting something that does not exist is a success)
              */
             crm_debug("Not registering resource '%s' for a %s event "
                       QB_XS " get-rc=%d (%s) transition-key=%s",
                       pcmk__xe_id(xml_rsc), operation,
                       rc, pcmk_strerror(rc), pcmk__xe_id(input->xml));
             delete_rsc_entry(lrm_state, input, pcmk__xe_id(xml_rsc), NULL,
                              pcmk_ok, user_name, true);
             return;
 
         } else if (rc == -EINVAL) {
             // Resource operation on malformed resource
             crm_err("Invalid resource definition for %s", pcmk__xe_id(xml_rsc));
             crm_log_xml_warn(input->msg, "invalid resource");
             synthesize_lrmd_failure(lrm_state, input->xml, PCMK_EXEC_ERROR,
                                     PCMK_OCF_NOT_CONFIGURED, // fatal error
                                     "Invalid resource definition");
             return;
 
         } else if (rc < 0) {
             // Error communicating with the executor
             crm_err("Could not register resource '%s' with executor: %s "
                     QB_XS " rc=%d",
                     pcmk__xe_id(xml_rsc), pcmk_strerror(rc), rc);
             crm_log_xml_warn(input->msg, "failed registration");
             synthesize_lrmd_failure(lrm_state, input->xml, PCMK_EXEC_ERROR,
                                     PCMK_OCF_INVALID_PARAM, // hard error
                                     "Could not register resource with executor");
             return;
         }
 
         if (pcmk__str_eq(operation, PCMK_ACTION_CANCEL, pcmk__str_none)) {
             if (!do_lrm_cancel(input, lrm_state, rsc, from_host, from_sys)) {
                 crm_log_xml_warn(input->xml, "Bad command");
             }
 
         } else if (pcmk__str_eq(operation, PCMK_ACTION_DELETE,
                                 pcmk__str_none)) {
             do_lrm_delete(input, lrm_state, rsc, from_sys, from_host,
                           crm_rsc_delete, user_name);
 
         } else {
             struct ra_metadata_s *md = NULL;
 
             /* Getting metadata from cache is OK except for start actions --
              * always refresh from the agent for those, in case the resource
              * agent was updated.
              *
              * @TODO Only refresh metadata for starts if the agent actually
              * changed (using something like inotify, or a hash or modification
              * time of the agent executable).
              */
             if (strcmp(operation, PCMK_ACTION_START) != 0) {
                 md = controld_get_rsc_metadata(lrm_state, rsc,
                                                controld_metadata_from_cache);
             }
 
             if ((md == NULL) && crm_op_needs_metadata(rsc->standard,
                                                       operation)) {
                 /* Most likely, we'll need the agent metadata to record the
                  * pending operation and the operation result. Get it now rather
                  * than wait until then, so the metadata action doesn't eat into
                  * the real action's timeout.
                  *
                  * @TODO Metadata is retrieved via direct execution of the
                  * agent, which has a couple of related issues: the executor
                  * should execute agents, not the controller; and metadata for
                  * Pacemaker Remote nodes should be collected on those nodes,
                  * not locally.
                  */
                 struct metadata_cb_data *data = NULL;
 
                 data = new_metadata_cb_data(rsc, input->xml);
                 crm_info("Retrieving metadata for %s (%s%s%s:%s) asynchronously",
                          rsc->id, rsc->standard,
                          ((rsc->provider == NULL)? "" : ":"),
                          ((rsc->provider == NULL)? "" : rsc->provider),
                          rsc->type);
                 (void) lrmd__metadata_async(rsc, metadata_complete,
                                             (void *) data);
             } else {
                 do_lrm_rsc_op(lrm_state, rsc, input->xml, md);
             }
         }
 
         lrmd_free_rsc_info(rsc);
 
     } else {
         crm_err("Invalid execution request: unknown command '%s' (bug?)",
                 crm_op);
         register_fsa_error(C_FSA_INTERNAL, I_ERROR, NULL);
     }
 }
 
 static lrmd_event_data_t *
 construct_op(const lrm_state_t *lrm_state, const xmlNode *rsc_op,
              const char *rsc_id, const char *operation)
 {
     lrmd_event_data_t *op = NULL;
     const char *op_delay = NULL;
     const char *op_timeout = NULL;
     GHashTable *params = NULL;
 
     xmlNode *primitive = NULL;
     const char *class = NULL;
 
     const char *transition = NULL;
 
     pcmk__assert((rsc_id != NULL) && (operation != NULL));
 
     op = lrmd_new_event(rsc_id, operation, 0);
     op->type = lrmd_event_exec_complete;
     op->timeout = 0;
     op->start_delay = 0;
     lrmd__set_result(op, PCMK_OCF_UNKNOWN, PCMK_EXEC_PENDING, NULL);
 
     if (rsc_op == NULL) {
         CRM_LOG_ASSERT(pcmk__str_eq(operation, PCMK_ACTION_STOP,
                                     pcmk__str_casei));
         op->user_data = NULL;
         /* the stop_all_resources() case
          * by definition there is no DC (or they'd be shutting
          *   us down).
          * So we should put our version here.
          */
         op->params = pcmk__strkey_table(free, free);
 
         pcmk__insert_dup(op->params, PCMK_XA_CRM_FEATURE_SET, CRM_FEATURE_SET);
 
         crm_trace("Constructed %s op for %s", operation, rsc_id);
         return op;
     }
 
     params = xml2list(rsc_op);
     g_hash_table_remove(params, CRM_META "_" PCMK__META_OP_TARGET_RC);
 
     op_delay = crm_meta_value(params, PCMK_META_START_DELAY);
     pcmk__scan_min_int(op_delay, &op->start_delay, 0);
 
     op_timeout = crm_meta_value(params, PCMK_META_TIMEOUT);
     pcmk__scan_min_int(op_timeout, &op->timeout, 0);
 
     if (pcmk__guint_from_hash(params, CRM_META "_" PCMK_META_INTERVAL, 0,
                               &(op->interval_ms)) != pcmk_rc_ok) {
         op->interval_ms = 0;
     }
 
     /* Use pcmk_monitor_timeout instead of meta timeout for stonith
        recurring monitor, if set */
     primitive = pcmk__xe_first_child(rsc_op, PCMK_XE_PRIMITIVE, NULL, NULL);
     class = pcmk__xe_get(primitive, PCMK_XA_CLASS);
 
     if (pcmk__is_set(pcmk_get_ra_caps(class), pcmk_ra_cap_fence_params)
         && pcmk__str_eq(operation, PCMK_ACTION_MONITOR, pcmk__str_casei)
         && (op->interval_ms > 0)) {
 
         op_timeout = g_hash_table_lookup(params, "pcmk_monitor_timeout");
         if (op_timeout != NULL) {
             long long timeout_ms = 0;
 
             if ((pcmk__parse_ms(op_timeout, &timeout_ms) == pcmk_rc_ok)
                 && (timeout_ms >= 0)) {
 
                 op->timeout = (int) QB_MIN(timeout_ms, INT_MAX);
             }
         }
     }
 
     if (!pcmk__str_eq(operation, PCMK_ACTION_STOP, pcmk__str_casei)) {
         op->params = params;
 
     } else {
         rsc_history_t *entry = NULL;
 
         if (lrm_state) {
             entry = g_hash_table_lookup(lrm_state->resource_history, rsc_id);
         }
 
         /* If we do not have stop parameters cached, use
          * whatever we are given */
         if (!entry || !entry->stop_params) {
             op->params = params;
         } else {
             /* Copy the cached parameter list so that we stop the resource
              * with the old attributes, not the new ones */
             op->params = pcmk__strkey_table(free, free);
 
             g_hash_table_foreach(params, copy_meta_keys, op->params);
             g_hash_table_foreach(entry->stop_params, copy_instance_keys, op->params);
             g_hash_table_destroy(params);
             params = NULL;
         }
     }
 
     /* sanity */
     if (op->timeout <= 0) {
         op->timeout = op->interval_ms;
     }
     if (op->start_delay < 0) {
         op->start_delay = 0;
     }
 
     transition = pcmk__xe_get(rsc_op, PCMK__XA_TRANSITION_KEY);
     CRM_CHECK(transition != NULL, return op);
 
     op->user_data = pcmk__str_copy(transition);
 
     if (op->interval_ms != 0) {
         if (pcmk__strcase_any_of(operation, PCMK_ACTION_START, PCMK_ACTION_STOP,
                                  NULL)) {
             crm_err("Start and Stop actions cannot have an interval: %u",
                     op->interval_ms);
             op->interval_ms = 0;
         }
     }
 
     crm_trace("Constructed %s op for %s: interval=%u",
               operation, rsc_id, op->interval_ms);
 
     return op;
 }
 
 /*!
  * \internal
  * \brief Send a (synthesized) event result
  *
  * Reply with a synthesized event result directly, as opposed to going through
  * the executor.
  *
  * \param[in]     to_host  Host to send result to
  * \param[in]     to_sys   IPC name to send result (NULL for transition engine)
  * \param[in]     rsc      Type information about resource the result is for
  * \param[in,out] op       Event with result to send
  * \param[in]     rsc_id   ID of resource the result is for
  */
 void
 controld_ack_event_directly(const char *to_host, const char *to_sys,
                             const lrmd_rsc_info_t *rsc, lrmd_event_data_t *op,
                             const char *rsc_id)
 {
     xmlNode *reply = NULL;
     xmlNode *update, *iter;
     pcmk__node_status_t *peer = NULL;
 
     CRM_CHECK(op != NULL, return);
     if (op->rsc_id == NULL) {
         // op->rsc_id is a (const char *) but lrmd_free_event() frees it
         pcmk__assert(rsc_id != NULL);
         op->rsc_id = pcmk__str_copy(rsc_id);
     }
     if (to_sys == NULL) {
         to_sys = CRM_SYSTEM_TENGINE;
     }
 
     peer = controld_get_local_node_status();
     update = create_node_state_update(peer, node_update_none, NULL,
                                       __func__);
 
     iter = pcmk__xe_create(update, PCMK__XE_LRM);
     pcmk__xe_set(iter, PCMK_XA_ID, controld_globals.our_uuid);
     iter = pcmk__xe_create(iter, PCMK__XE_LRM_RESOURCES);
     iter = pcmk__xe_create(iter, PCMK__XE_LRM_RESOURCE);
 
     pcmk__xe_set(iter, PCMK_XA_ID, op->rsc_id);
 
     controld_add_resource_history_xml(iter, rsc, op,
                                       controld_globals.cluster->priv->node_name);
 
     /* We don't have the original message ID, so use "direct-ack" (we just need
      * something non-NULL for this to create a reply)
      *
      * @TODO It would be better to use the server, message ID, and task from the
      * original request when callers have it available
      */
     reply = pcmk__new_message(pcmk_ipc_controld, "direct-ack", CRM_SYSTEM_LRMD,
                               to_host, to_sys, CRM_OP_INVOKE_LRM, update);
 
     crm_log_xml_trace(update, "[direct ACK]");
 
     crm_debug("ACK'ing resource op " PCMK__OP_FMT " from %s: %s",
               op->rsc_id, op->op_type, op->interval_ms, op->user_data,
               pcmk__xe_get(reply, PCMK_XA_REFERENCE));
 
     if (relay_message(reply, TRUE) == FALSE) {
         crm_log_xml_err(reply, "Unable to route reply");
     }
 
     pcmk__xml_free(update);
     pcmk__xml_free(reply);
 }
 
 gboolean
 verify_stopped(enum crmd_fsa_state cur_state, int log_level)
 {
     gboolean res = TRUE;
     GList *lrm_state_list = lrm_state_get_list();
     GList *state_entry;
 
     for (state_entry = lrm_state_list; state_entry != NULL; state_entry = state_entry->next) {
         lrm_state_t *lrm_state = state_entry->data;
 
         if (!lrm_state_verify_stopped(lrm_state, cur_state, log_level)) {
             /* keep iterating through all even when false is returned */
             res = FALSE;
         }
     }
 
     controld_set_fsa_input_flags(R_SENT_RSC_STOP);
     g_list_free(lrm_state_list); lrm_state_list = NULL;
     return res;
 }
 
 struct stop_recurring_action_s {
     lrmd_rsc_info_t *rsc;
     lrm_state_t *lrm_state;
 };
 
 static gboolean
 stop_recurring_action_by_rsc(gpointer key, gpointer value, gpointer user_data)
 {
     gboolean remove = FALSE;
     struct stop_recurring_action_s *event = user_data;
     active_op_t *op = value;
 
     if ((op->interval_ms != 0)
         && pcmk__str_eq(op->rsc_id, event->rsc->id, pcmk__str_none)) {
 
         crm_debug("Cancelling op %d for %s (%s)", op->call_id, op->rsc_id, (char*)key);
         remove = !cancel_op(event->lrm_state, event->rsc->id, key, op->call_id, FALSE);
     }
 
     return remove;
 }
 
 static gboolean
 stop_recurring_actions(gpointer key, gpointer value, gpointer user_data)
 {
     gboolean remove = FALSE;
     lrm_state_t *lrm_state = user_data;
     active_op_t *op = value;
 
     if (op->interval_ms != 0) {
         crm_info("Cancelling op %d for %s (%s)", op->call_id, op->rsc_id,
                  (const char *) key);
         remove = !cancel_op(lrm_state, op->rsc_id, key, op->call_id, FALSE);
     }
 
     return remove;
 }
 
 /*!
  * \internal
  * \brief Check whether recurring actions should be cancelled before an action
  *
  * \param[in] rsc_id       Resource that action is for
  * \param[in] action       Action being performed
  * \param[in] interval_ms  Operation interval of \p action (in milliseconds)
  *
  * \return true if recurring actions should be cancelled, otherwise false
  */
 static bool
 should_cancel_recurring(const char *rsc_id, const char *action, guint interval_ms)
 {
     if (is_remote_lrmd_ra(NULL, NULL, rsc_id) && (interval_ms == 0)
         && (strcmp(action, PCMK_ACTION_MIGRATE_TO) == 0)) {
         /* Don't stop monitoring a migrating Pacemaker Remote connection
          * resource until the entire migration has completed. We must detect if
          * the connection is unexpectedly severed, even during a migration.
          */
         return false;
     }
 
     // Cancel recurring actions before changing resource state
     return (interval_ms == 0)
             && !pcmk__str_any_of(action, PCMK_ACTION_MONITOR,
                                  PCMK_ACTION_NOTIFY, NULL);
 }
 
 /*!
  * \internal
  * \brief Check whether an action should not be performed at this time
  *
  * \param[in] operation  Action to be performed
  *
  * \return Readable description of why action should not be performed,
  *         or NULL if it should be performed
  */
 static const char *
 should_nack_action(const char *action)
 {
     if (pcmk__is_set(controld_globals.fsa_input_register, R_SHUTDOWN)
         && pcmk__str_eq(action, PCMK_ACTION_START, pcmk__str_none)) {
 
         register_fsa_input(C_SHUTDOWN, I_SHUTDOWN, NULL);
         return "Not attempting start due to shutdown in progress";
     }
 
     switch (controld_globals.fsa_state) {
         case S_NOT_DC:
         case S_POLICY_ENGINE:   // Recalculating
         case S_TRANSITION_ENGINE:
             break;
         default:
             if (!pcmk__str_eq(action, PCMK_ACTION_STOP, pcmk__str_none)) {
                 return "Controller cannot attempt actions at this time";
             }
             break;
     }
     return NULL;
 }
 
 static void
 do_lrm_rsc_op(lrm_state_t *lrm_state, lrmd_rsc_info_t *rsc, xmlNode *msg,
               struct ra_metadata_s *md)
 {
     int rc;
     int call_id = 0;
     char *op_id = NULL;
     lrmd_event_data_t *op = NULL;
     fsa_data_t *msg_data = NULL;
     const char *transition = NULL;
     const char *operation = NULL;
     const char *nack_reason = NULL;
 
     CRM_CHECK((rsc != NULL) && (msg != NULL), return);
 
     operation = pcmk__xe_get(msg, PCMK_XA_OPERATION);
     CRM_CHECK(!pcmk__str_empty(operation), return);
 
     transition = pcmk__xe_get(msg, PCMK__XA_TRANSITION_KEY);
     if (pcmk__str_empty(transition)) {
         crm_log_xml_err(msg, "Missing transition number");
     }
 
     if (lrm_state == NULL) {
         // This shouldn't be possible, but provide a failsafe just in case
         crm_err("Cannot execute %s of %s: No executor connection "
                 QB_XS " transition_key=%s",
                 operation, rsc->id, pcmk__s(transition, ""));
         synthesize_lrmd_failure(NULL, msg, PCMK_EXEC_INVALID,
                                 PCMK_OCF_UNKNOWN_ERROR,
                                 "No executor connection");
         return;
     }
 
     if (pcmk__str_any_of(operation, PCMK_ACTION_RELOAD,
                          PCMK_ACTION_RELOAD_AGENT, NULL)) {
         /* Pre-2.1.0 DCs will schedule reload actions only, and 2.1.0+ DCs
          * will schedule reload-agent actions only. In either case, we need
          * to map that to whatever the resource agent actually supports.
          * Default to the OCF 1.1 name.
          */
         if ((md != NULL)
             && pcmk__is_set(md->ra_flags, ra_supports_legacy_reload)) {
             operation = PCMK_ACTION_RELOAD;
         } else {
             operation = PCMK_ACTION_RELOAD_AGENT;
         }
     }
 
     op = construct_op(lrm_state, msg, rsc->id, operation);
     CRM_CHECK(op != NULL, return);
 
     if (should_cancel_recurring(rsc->id, operation, op->interval_ms)) {
         guint removed = 0;
         struct stop_recurring_action_s data;
 
         data.rsc = rsc;
         data.lrm_state = lrm_state;
         removed = g_hash_table_foreach_remove(lrm_state->active_ops,
                                               stop_recurring_action_by_rsc,
                                               &data);
 
         if (removed) {
             crm_debug("Stopped %u recurring operation%s in preparation for "
                       PCMK__OP_FMT, removed, pcmk__plural_s(removed),
                       rsc->id, operation, op->interval_ms);
         }
     }
 
     nack_reason = should_nack_action(operation);
     if (nack_reason != NULL) {
         crm_notice("Not requesting local execution of %s operation for %s on %s"
                    " in state %s: %s",
                    pcmk__readable_action(op->op_type, op->interval_ms), rsc->id,
                    lrm_state->node_name,
                    fsa_state2string(controld_globals.fsa_state), nack_reason);
 
         lrmd__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_INVALID,
                          nack_reason);
         controld_ack_event_directly(NULL, NULL, rsc, op, rsc->id);
         lrmd_free_event(op);
         free(op_id);
         return;
     }
 
     crm_notice("Requesting local execution of %s operation for %s on %s "
                QB_XS " transition %s",
                pcmk__readable_action(op->op_type, op->interval_ms), rsc->id,
                lrm_state->node_name, pcmk__s(transition, ""));
 
     controld_record_pending_op(lrm_state->node_name, rsc, op);
 
     op_id = pcmk__op_key(rsc->id, op->op_type, op->interval_ms);
 
     if (op->interval_ms > 0) {
         /* cancel it so we can then restart it without conflict */
         cancel_op_key(lrm_state, rsc, op_id, FALSE);
     }
 
     rc = controld_execute_resource_agent(lrm_state, rsc->id, op->op_type,
                                          op->user_data, op->interval_ms,
                                          op->timeout, op->start_delay,
                                          op->params, &call_id);
     if (rc == pcmk_rc_ok) {
         /* record all operations so we can wait
          * for them to complete during shutdown
          */
         char *call_id_s = make_stop_id(rsc->id, call_id);
         active_op_t *pending = NULL;
 
         pending = pcmk__assert_alloc(1, sizeof(active_op_t));
         crm_trace("Recording pending op: %d - %s %s", call_id, op_id, call_id_s);
 
         pending->call_id = call_id;
         pending->interval_ms = op->interval_ms;
         pending->op_type = pcmk__str_copy(operation);
         pending->op_key = pcmk__str_copy(op_id);
         pending->rsc_id = pcmk__str_copy(rsc->id);
         pending->start_time = time(NULL);
         pending->user_data = pcmk__str_copy(op->user_data);
         pcmk__xe_get_time(msg, PCMK_OPT_SHUTDOWN_LOCK, &(pending->lock_time));
         g_hash_table_replace(lrm_state->active_ops, call_id_s, pending);
 
         if ((op->interval_ms > 0)
             && (op->start_delay > START_DELAY_THRESHOLD)) {
             int target_rc = PCMK_OCF_OK;
 
             crm_info("Faking confirmation of %s: execution postponed for over 5 minutes", op_id);
             decode_transition_key(op->user_data, NULL, NULL, NULL, &target_rc);
             lrmd__set_result(op, target_rc, PCMK_EXEC_DONE, NULL);
             controld_ack_event_directly(NULL, NULL, rsc, op, rsc->id);
         }
 
         pending->params = op->params;
         op->params = NULL;
 
     } else if (lrm_state_is_local(lrm_state)) {
         crm_err("Could not initiate %s action for resource %s locally: %s "
                 QB_XS " rc=%d", operation, rsc->id, pcmk_rc_str(rc), rc);
         fake_op_status(lrm_state, op, PCMK_EXEC_NOT_CONNECTED,
                        PCMK_OCF_UNKNOWN_ERROR, pcmk_rc_str(rc));
         process_lrm_event(lrm_state, op, NULL, NULL);
         register_fsa_error(C_FSA_INTERNAL, I_FAIL, NULL);
 
     } else {
         crm_err("Could not initiate %s action for resource %s remotely on %s: "
                 "%s " QB_XS " rc=%d",
                 operation, rsc->id, lrm_state->node_name, pcmk_rc_str(rc), rc);
         fake_op_status(lrm_state, op, PCMK_EXEC_NOT_CONNECTED,
                        PCMK_OCF_UNKNOWN_ERROR, pcmk_rc_str(rc));
         process_lrm_event(lrm_state, op, NULL, NULL);
     }
 
     free(op_id);
     lrmd_free_event(op);
 }
 
 static char *
 unescape_newlines(const char *string)
 {
     char *pch = NULL;
     char *ret = NULL;
     static const char *escaped_newline = "\\n";
 
     if (!string) {
         return NULL;
     }
 
     ret = pcmk__str_copy(string);
     pch = strstr(ret, escaped_newline);
     while (pch != NULL) {
         /* Replace newline escape pattern with actual newline (and a space so we
          * don't have to shuffle the rest of the buffer)
          */
         pch[0] = '\n';
         pch[1] = ' ';
         pch = strstr(pch, escaped_newline);
     }
 
     return ret;
 }
 
 static bool
 did_lrm_rsc_op_fail(lrm_state_t *lrm_state, const char * rsc_id,
                     const char * op_type, guint interval_ms)
 {
     rsc_history_t *entry = NULL;
 
     CRM_CHECK(lrm_state != NULL, return FALSE);
     CRM_CHECK(rsc_id != NULL, return FALSE);
     CRM_CHECK(op_type != NULL, return FALSE);
 
     entry = g_hash_table_lookup(lrm_state->resource_history, rsc_id);
     if (entry == NULL || entry->failed == NULL) {
         return FALSE;
     }
 
     if (pcmk__str_eq(entry->failed->rsc_id, rsc_id, pcmk__str_none)
         && pcmk__str_eq(entry->failed->op_type, op_type, pcmk__str_casei)
         && entry->failed->interval_ms == interval_ms) {
         return TRUE;
     }
 
     return FALSE;
 }
 
 /*!
  * \internal
  * \brief Log the result of an executor action (actual or synthesized)
  *
  * \param[in] op         Executor action to log result for
  * \param[in] op_key     Operation key for action
  * \param[in] node_name  Name of node action was performed on, if known
  * \param[in] confirmed  Whether to log that graph action was confirmed
  */
 static void
 log_executor_event(const lrmd_event_data_t *op, const char *op_key,
                    const char *node_name, gboolean confirmed)
 {
     int log_level = LOG_ERR;
     GString *str = g_string_sized_new(100); // reasonable starting size
 
     pcmk__g_strcat(str,
                    "Result of ",
                    pcmk__readable_action(op->op_type, op->interval_ms),
                    " operation for ", op->rsc_id, NULL);
 
     if (node_name != NULL) {
         pcmk__g_strcat(str, " on ", node_name, NULL);
     }
 
     switch (op->op_status) {
         case PCMK_EXEC_DONE:
             log_level = LOG_NOTICE;
             pcmk__g_strcat(str, ": ", crm_exit_str((crm_exit_t) op->rc), NULL);
             break;
 
         case PCMK_EXEC_TIMEOUT:
             pcmk__g_strcat(str,
                            ": ", pcmk_exec_status_str(op->op_status), " after ",
                            pcmk__readable_interval(op->timeout), NULL);
             break;
 
         case PCMK_EXEC_CANCELLED:
             log_level = LOG_INFO;
             pcmk__g_strcat(str, ": ", pcmk_exec_status_str(op->op_status),
                            NULL);
             break;
 
         default:
             pcmk__g_strcat(str, ": ", pcmk_exec_status_str(op->op_status),
                            NULL);
             break;
     }
 
     if ((op->exit_reason != NULL)
         && ((op->op_status != PCMK_EXEC_DONE) || (op->rc != PCMK_OCF_OK))) {
 
         pcmk__g_strcat(str, " (", op->exit_reason, ")", NULL);
     }
 
     g_string_append(str, " " QB_XS);
     g_string_append_printf(str, " graph action %sconfirmed; call=%d key=%s",
                            (confirmed? "" : "un"), op->call_id, op_key);
     if (op->op_status == PCMK_EXEC_DONE) {
         g_string_append_printf(str, " rc=%d", op->rc);
     }
 
     do_crm_log(log_level, "%s", str->str);
     g_string_free(str, TRUE);
 
     /* The services library has already logged the output at info or debug
      * level, so just raise to notice if it looks like a failure.
      */
     if ((op->output != NULL) && (op->rc != PCMK_OCF_OK)) {
         char *prefix = pcmk__assert_asprintf(PCMK__OP_FMT "@%s output",
                                              op->rsc_id, op->op_type,
                                              op->interval_ms, node_name);
 
         crm_log_output(LOG_NOTICE, prefix, op->output);
         free(prefix);
     }
 }
 
 void
 process_lrm_event(lrm_state_t *lrm_state, lrmd_event_data_t *op,
                   active_op_t *pending, const xmlNode *action_xml)
 {
     char *op_id = NULL;
     char *op_key = NULL;
 
     gboolean remove = FALSE;
     gboolean removed = FALSE;
     bool need_direct_ack = FALSE;
     lrmd_rsc_info_t *rsc = NULL;
     const char *node_name = NULL;
 
     CRM_CHECK(op != NULL, return);
     CRM_CHECK(op->rsc_id != NULL, return);
 
     // Remap new status codes for older DCs
-    if (compare_version(controld_globals.dc_version, "3.2.0") < 0) {
+    if (pcmk__compare_versions(controld_globals.dc_version, "3.2.0") < 0) {
         switch (op->op_status) {
             case PCMK_EXEC_NOT_CONNECTED:
                 lrmd__set_result(op, PCMK_OCF_CONNECTION_DIED,
                                  PCMK_EXEC_ERROR, op->exit_reason);
                 break;
             case PCMK_EXEC_INVALID:
                 lrmd__set_result(op, CRM_DIRECT_NACK_RC, PCMK_EXEC_ERROR,
                                  op->exit_reason);
                 break;
             default:
                 break;
         }
     }
 
     op_id = make_stop_id(op->rsc_id, op->call_id);
     op_key = pcmk__op_key(op->rsc_id, op->op_type, op->interval_ms);
 
     // Get resource info if available (from executor state or action XML)
     if (lrm_state) {
         rsc = lrm_state_get_rsc_info(lrm_state, op->rsc_id, 0);
     }
     if ((rsc == NULL) && action_xml) {
         xmlNode *xml = pcmk__xe_first_child(action_xml, PCMK_XE_PRIMITIVE, NULL,
                                             NULL);
 
         const char *standard = pcmk__xe_get(xml, PCMK_XA_CLASS);
         const char *provider = pcmk__xe_get(xml, PCMK_XA_PROVIDER);
         const char *type = pcmk__xe_get(xml, PCMK_XA_TYPE);
 
         if (standard && type) {
             crm_info("%s agent information not cached, using %s%s%s:%s from action XML",
                      op->rsc_id, standard,
                      (provider? ":" : ""), (provider? provider : ""), type);
             rsc = lrmd_new_rsc_info(op->rsc_id, standard, provider, type);
         } else {
             crm_err("Can't process %s result because %s agent information not cached or in XML",
                     op_key, op->rsc_id);
         }
     }
 
     // Get node name if available (from executor state or action XML)
     if (lrm_state) {
         node_name = lrm_state->node_name;
     } else if (action_xml) {
         node_name = pcmk__xe_get(action_xml, PCMK__META_ON_NODE);
     }
 
     if(pending == NULL) {
         remove = TRUE;
         if (lrm_state) {
             pending = g_hash_table_lookup(lrm_state->active_ops, op_id);
         }
     }
 
     if (op->op_status == PCMK_EXEC_ERROR) {
         switch(op->rc) {
             case PCMK_OCF_NOT_RUNNING:
             case PCMK_OCF_RUNNING_PROMOTED:
             case PCMK_OCF_DEGRADED:
             case PCMK_OCF_DEGRADED_PROMOTED:
                 // Leave it to the TE/scheduler to decide if this is an error
                 op->op_status = PCMK_EXEC_DONE;
                 break;
             default:
                 /* Nothing to do */
                 break;
         }
     }
 
     if (op->op_status != PCMK_EXEC_CANCELLED) {
         /* We might not record the result, so directly acknowledge it to the
          * originator instead, so it doesn't time out waiting for the result
          * (especially important if part of a transition).
          */
         need_direct_ack = TRUE;
 
         if (controld_action_is_recordable(op->op_type)) {
             if (node_name && rsc) {
                 // We should record the result, and happily, we can
                 time_t lock_time = (pending == NULL)? 0 : pending->lock_time;
 
                 controld_update_resource_history(node_name, rsc, op, lock_time);
                 need_direct_ack = FALSE;
 
             } else if (op->rsc_deleted) {
                 /* We shouldn't record the result (likely the resource was
                  * refreshed, cleaned, or removed while this operation was
                  * in flight).
                  */
                 crm_notice("Not recording %s result in CIB because "
                            "resource information was removed since it was initiated",
                            op_key);
             } else {
                 /* This shouldn't be possible; the executor didn't consider the
                  * resource deleted, but we couldn't find resource or node
                  * information.
                  */
                 crm_err("Unable to record %s result in CIB: %s", op_key,
                         (node_name? "No resource information" : "No node name"));
             }
         }
 
     } else if (op->interval_ms == 0) {
         /* A non-recurring operation was cancelled. Most likely, the
          * never-initiated action was removed from the executor's pending
          * operations list upon resource removal.
          */
         need_direct_ack = TRUE;
 
     } else if (pending == NULL) {
         /* This recurring operation was cancelled, but was not pending. No
          * transition actions are waiting on it, nothing needs to be done.
          */
 
     } else if (op->user_data == NULL) {
         /* This recurring operation was cancelled and pending, but we don't
          * have a transition key. This should never happen.
          */
         crm_err("Recurring operation %s was cancelled without transition information",
                 op_key);
 
     } else if (pcmk__is_set(pending->flags, active_op_remove)) {
         /* This recurring operation was cancelled (by us) and pending, and we
          * have been waiting for it to finish.
          */
         if (lrm_state) {
             controld_delete_action_history(op);
         }
 
         /* Directly acknowledge failed recurring actions here. The above call to
          * controld_delete_action_history() will not erase any corresponding
          * last_failure entry, which means that the DC won't confirm the
          * cancellation via process_op_deletion(), and the transition would
          * otherwise wait for the action timer to pop.
          */
         if (did_lrm_rsc_op_fail(lrm_state, pending->rsc_id,
                                 pending->op_type, pending->interval_ms)) {
             need_direct_ack = TRUE;
         }
 
     } else if (op->rsc_deleted) {
         /* This recurring operation was cancelled (but not by us, and the
          * executor does not have resource information, likely due to resource
          * cleanup, refresh, or removal) and pending.
          */
         crm_debug("Recurring op %s was cancelled due to resource deletion",
                   op_key);
         need_direct_ack = TRUE;
 
     } else {
         /* This recurring operation was cancelled (but not by us, likely by the
          * executor before stopping the resource) and pending. We don't need to
          * do anything special.
          */
     }
 
     if (need_direct_ack) {
         controld_ack_event_directly(NULL, NULL, NULL, op, op->rsc_id);
     }
 
     if(remove == FALSE) {
         /* The caller will do this afterwards, but keep the logging consistent */
         removed = TRUE;
 
     } else if (lrm_state && ((op->interval_ms == 0)
                              || (op->op_status == PCMK_EXEC_CANCELLED))) {
 
         gboolean found = g_hash_table_remove(lrm_state->active_ops, op_id);
 
         if (op->interval_ms != 0) {
             removed = TRUE;
         } else if (found) {
             removed = TRUE;
             crm_trace("Op %s (call=%d, stop-id=%s, remaining=%u): Confirmed",
                       op_key, op->call_id, op_id,
                       g_hash_table_size(lrm_state->active_ops));
         }
     }
 
     log_executor_event(op, op_key, node_name, removed);
 
     if (lrm_state) {
         if (!pcmk__str_eq(op->op_type, PCMK_ACTION_META_DATA,
                           pcmk__str_casei)) {
             crmd_alert_resource_op(lrm_state->node_name, op);
         } else if (rsc && (op->rc == PCMK_OCF_OK)) {
             char *metadata = unescape_newlines(op->output);
 
             controld_cache_metadata(lrm_state->metadata_cache, rsc, metadata);
             free(metadata);
         }
     }
 
     if (op->rsc_deleted) {
         crm_info("Deletion of resource '%s' complete after %s", op->rsc_id, op_key);
         if (lrm_state) {
             delete_rsc_entry(lrm_state, NULL, op->rsc_id, NULL, pcmk_ok, NULL,
                              true);
         }
     }
 
     /* If a shutdown was escalated while operations were pending,
      * then the FSA will be stalled right now... allow it to continue
      */
     controld_trigger_fsa();
     if (lrm_state && rsc) {
         update_history_cache(lrm_state, rsc, op);
     }
 
     lrmd_free_rsc_info(rsc);
     free(op_key);
     free(op_id);
 }
diff --git a/daemons/controld/controld_membership.c b/daemons/controld/controld_membership.c
index c0fbd68bba..e5175f093a 100644
--- a/daemons/controld/controld_membership.c
+++ b/daemons/controld/controld_membership.c
@@ -1,471 +1,473 @@
 /*
  * Copyright 2004-2025 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.
  */
 
 /* put these first so that uuid_t is defined without conflicts */
 #include <crm_internal.h>
 
 #include <string.h>
 
 #include <crm/crm.h>
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>
 #include <crm/cluster/internal.h>
 
 #include <pacemaker-controld.h>
 
 void post_cache_update(int instance);
 
 extern gboolean check_join_state(enum crmd_fsa_state cur_state, const char *source);
 
 static void
 reap_dead_nodes(gpointer key, gpointer value, gpointer user_data)
 {
     pcmk__node_status_t *node = value;
 
     if (pcmk__cluster_is_node_active(node)) {
         return;
     }
 
     crm_update_peer_join(__func__, node, controld_join_none);
 
     if ((node != NULL) && (node->name != NULL)) {
         if (controld_is_local_node(node->name)) {
             crm_err("We're not part of the cluster anymore");
             register_fsa_input(C_FSA_INTERNAL, I_ERROR, NULL);
 
         } else if (!AM_I_DC
                    && pcmk__str_eq(node->name, controld_globals.dc_name,
                                    pcmk__str_casei)) {
             crm_warn("Our DC node (%s) left the cluster", node->name);
             register_fsa_input(C_FSA_INTERNAL, I_ELECTION, NULL);
         }
     }
 
     if ((controld_globals.fsa_state == S_INTEGRATION)
         || (controld_globals.fsa_state == S_FINALIZE_JOIN)) {
         check_join_state(controld_globals.fsa_state, __func__);
     }
     if ((node != NULL) && (node->xml_id != NULL)) {
         fail_incompletable_actions(controld_globals.transition_graph,
                                    node->xml_id);
     }
 }
 
 void
 post_cache_update(int instance)
 {
     xmlNode *no_op = NULL;
 
     controld_globals.peer_seq = instance;
     crm_debug("Updated cache after membership event %d.", instance);
 
     g_hash_table_foreach(pcmk__peer_cache, reap_dead_nodes, NULL);
     controld_set_fsa_input_flags(R_MEMBERSHIP);
 
     if (AM_I_DC) {
         populate_cib_nodes(node_update_quick | node_update_cluster | node_update_peer |
                            node_update_expected, __func__);
     }
 
     /*
      * If we lost nodes, we should re-check the election status
      * Safe to call outside of an election
      */
     controld_set_fsa_action_flags(A_ELECTION_CHECK);
     controld_trigger_fsa();
 
     /* Membership changed, remind everyone we're here.
      * This will aid detection of duplicate DCs
      */
     no_op = pcmk__new_request(pcmk_ipc_controld,
                               (AM_I_DC? CRM_SYSTEM_DC : CRM_SYSTEM_CRMD), NULL,
                               CRM_SYSTEM_CRMD, CRM_OP_NOOP, NULL);
     pcmk__cluster_send_message(NULL, pcmk_ipc_controld, no_op);
     pcmk__xml_free(no_op);
 }
 
 static void
 crmd_node_update_complete(xmlNode * msg, int call_id, int rc, xmlNode * output, void *user_data)
 {
     fsa_data_t *msg_data = NULL;
 
     if (rc == pcmk_ok) {
         crm_trace("Node update %d complete", call_id);
 
     } else if(call_id < pcmk_ok) {
         crm_err("Node update failed: %s (%d)", pcmk_strerror(call_id), call_id);
         crm_log_xml_debug(msg, "failed");
         register_fsa_error(C_FSA_INTERNAL, I_ERROR, NULL);
 
     } else {
         crm_err("Node update %d failed: %s (%d)", call_id, pcmk_strerror(rc), rc);
         crm_log_xml_debug(msg, "failed");
         register_fsa_error(C_FSA_INTERNAL, I_ERROR, NULL);
     }
 }
 
 /*!
  * \internal
  * \brief Create an XML node state tag with updates
  *
  * \param[in,out] node    Node whose state will be used for update
  * \param[in]     flags   Bitmask of node_update_flags indicating what to update
  * \param[in,out] parent  XML node to contain update (or NULL)
  * \param[in]     source  Who requested the update (only used for logging)
  *
  * \return Pointer to created node state tag
  */
 xmlNode *
 create_node_state_update(pcmk__node_status_t *node, int flags,
                          xmlNode *parent, const char *source)
 {
     // @TODO Ensure all callers handle NULL returns
     const char *id = NULL;
     const char *value = NULL;
     xmlNode *node_state;
 
     if (!node->state) {
         crm_info("Node update for %s cancelled: no state, not seen yet",
                  node->name);
        return NULL;
     }
 
     node_state = pcmk__xe_create(parent, PCMK__XE_NODE_STATE);
 
     if (pcmk__is_set(node->flags, pcmk__node_status_remote)) {
         pcmk__xe_set_bool_attr(node_state, PCMK_XA_REMOTE_NODE, true);
     }
 
     id = pcmk__cluster_get_xml_id(node);
     if ((id == NULL)
         || (pcmk__xe_set(node_state, PCMK_XA_ID, id) != pcmk_rc_ok)) {
 
         crm_info("Node update for %s cancelled: no ID", node->name);
         pcmk__xml_free(node_state);
         return NULL;
     }
 
     pcmk__xe_set(node_state, PCMK_XA_UNAME, node->name);
 
     if ((flags & node_update_cluster) && node->state) {
-        if (compare_version(controld_globals.dc_version, "3.18.0") >= 0) {
+        if (pcmk__compare_versions(controld_globals.dc_version,
+                                   "3.18.0") >= 0) {
             // A value 0 means the node is not a cluster member.
             pcmk__xe_set_ll(node_state, PCMK__XA_IN_CCM, node->when_member);
 
         } else {
             pcmk__xe_set_bool_attr(node_state, PCMK__XA_IN_CCM,
                                    pcmk__str_eq(node->state, PCMK_VALUE_MEMBER,
                                                 pcmk__str_none));
         }
     }
 
     if (!pcmk__is_set(node->flags, pcmk__node_status_remote)) {
         if (flags & node_update_peer) {
-            if (compare_version(controld_globals.dc_version, "3.18.0") >= 0) {
+            if (pcmk__compare_versions(controld_globals.dc_version,
+                                       "3.18.0") >= 0) {
                 // A value 0 means the peer is offline in CPG.
                 pcmk__xe_set_ll(node_state, PCMK_XA_CRMD, node->when_online);
 
             } else {
                 // @COMPAT DCs < 2.1.7 use online/offline rather than timestamp
                 value = PCMK_VALUE_OFFLINE;
                 if (pcmk__is_set(node->processes, crm_get_cluster_proc())) {
                     value = PCMK_VALUE_ONLINE;
                 }
                 pcmk__xe_set(node_state, PCMK_XA_CRMD, value);
             }
         }
 
         if (flags & node_update_join) {
             if (controld_get_join_phase(node) <= controld_join_none) {
                 value = CRMD_JOINSTATE_DOWN;
             } else {
                 value = CRMD_JOINSTATE_MEMBER;
             }
             pcmk__xe_set(node_state, PCMK__XA_JOIN, value);
         }
 
         if (flags & node_update_expected) {
             pcmk__xe_set(node_state, PCMK_XA_EXPECTED, node->expected);
         }
     }
 
     pcmk__xe_set(node_state, PCMK_XA_CRM_DEBUG_ORIGIN, source);
 
     return node_state;
 }
 
 static void
 remove_conflicting_node_callback(xmlNode * msg, int call_id, int rc,
                                  xmlNode * output, void *user_data)
 {
     char *node_uuid = user_data;
 
     do_crm_log_unlikely(rc == 0 ? LOG_DEBUG : LOG_NOTICE,
                         "Deletion of the unknown conflicting node \"%s\": %s (rc=%d)",
                         node_uuid, pcmk_strerror(rc), rc);
 }
 
 static void
 search_conflicting_node_callback(xmlNode * msg, int call_id, int rc,
                                  xmlNode * output, void *user_data)
 {
     char *new_node_uuid = user_data;
     xmlNode *node_xml = NULL;
 
     if (rc != pcmk_ok) {
         if (rc != -ENXIO) {
             crm_notice("Searching conflicting nodes for %s failed: %s (%d)",
                        new_node_uuid, pcmk_strerror(rc), rc);
         }
         return;
 
     } else if (output == NULL) {
         return;
     }
 
     if (pcmk__xe_is(output, PCMK_XE_NODE)) {
         node_xml = output;
 
     } else {
         node_xml = pcmk__xe_first_child(output, PCMK_XE_NODE, NULL, NULL);
     }
 
     for (; node_xml != NULL; node_xml = pcmk__xe_next(node_xml, PCMK_XE_NODE)) {
         const char *node_uuid = NULL;
         const char *node_uname = NULL;
         GHashTableIter iter;
         pcmk__node_status_t *node = NULL;
         gboolean known = FALSE;
 
         node_uuid = pcmk__xe_get(node_xml, PCMK_XA_ID);
         node_uname = pcmk__xe_get(node_xml, PCMK_XA_UNAME);
 
         if (node_uuid == NULL || node_uname == NULL) {
             continue;
         }
 
         g_hash_table_iter_init(&iter, pcmk__peer_cache);
         while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) {
             if ((node != NULL)
                 && pcmk__str_eq(node->xml_id, node_uuid, pcmk__str_casei)
                 && pcmk__str_eq(node->name, node_uname, pcmk__str_casei)) {
 
                 known = TRUE;
                 break;
             }
         }
 
         if (known == FALSE) {
             cib_t *cib_conn = controld_globals.cib_conn;
             int delete_call_id = 0;
             xmlNode *node_state_xml = NULL;
 
             crm_notice("Deleting unknown node %s/%s which has conflicting uname with %s",
                        node_uuid, node_uname, new_node_uuid);
 
             delete_call_id = cib_conn->cmds->remove(cib_conn, PCMK_XE_NODES,
                                                     node_xml, cib_none);
             fsa_register_cib_callback(delete_call_id, pcmk__str_copy(node_uuid),
                                       remove_conflicting_node_callback);
 
             node_state_xml = pcmk__xe_create(NULL, PCMK__XE_NODE_STATE);
             pcmk__xe_set(node_state_xml, PCMK_XA_ID, node_uuid);
             pcmk__xe_set(node_state_xml, PCMK_XA_UNAME, node_uname);
 
             delete_call_id = cib_conn->cmds->remove(cib_conn, PCMK_XE_STATUS,
                                                     node_state_xml, cib_none);
             fsa_register_cib_callback(delete_call_id, pcmk__str_copy(node_uuid),
                                       remove_conflicting_node_callback);
             pcmk__xml_free(node_state_xml);
         }
     }
 }
 
 static void
 node_list_update_callback(xmlNode * msg, int call_id, int rc, xmlNode * output, void *user_data)
 {
     fsa_data_t *msg_data = NULL;
 
     if(call_id < pcmk_ok) {
         crm_err("Node list update failed: %s (%d)", pcmk_strerror(call_id), call_id);
         crm_log_xml_debug(msg, "update:failed");
         register_fsa_error(C_FSA_INTERNAL, I_ERROR, NULL);
 
     } else if(rc < pcmk_ok) {
         crm_err("Node update %d failed: %s (%d)", call_id, pcmk_strerror(rc), rc);
         crm_log_xml_debug(msg, "update:failed");
         register_fsa_error(C_FSA_INTERNAL, I_ERROR, NULL);
     }
 }
 
 void
 populate_cib_nodes(enum node_update_flags flags, const char *source)
 {
     cib_t *cib_conn = controld_globals.cib_conn;
 
     int call_id = 0;
     gboolean from_hashtable = TRUE;
     xmlNode *node_list = pcmk__xe_create(NULL, PCMK_XE_NODES);
 
 #if SUPPORT_COROSYNC
     if (!pcmk__is_set(flags, node_update_quick)
         && (pcmk_get_cluster_layer() == pcmk_cluster_layer_corosync)) {
 
         from_hashtable = pcmk__corosync_add_nodes(node_list);
     }
 #endif
 
     if (from_hashtable) {
         GHashTableIter iter;
         pcmk__node_status_t *node = NULL;
         GString *xpath = NULL;
 
         g_hash_table_iter_init(&iter, pcmk__peer_cache);
         while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) {
             xmlNode *new_node = NULL;
 
             if ((node->xml_id != NULL) && (node->name != NULL)) {
                 crm_trace("Creating node entry for %s/%s",
                           node->name, node->xml_id);
                 if (xpath == NULL) {
                     xpath = g_string_sized_new(512);
                 } else {
                     g_string_truncate(xpath, 0);
                 }
 
                 /* We need both to be valid */
                 new_node = pcmk__xe_create(node_list, PCMK_XE_NODE);
                 pcmk__xe_set(new_node, PCMK_XA_ID, node->xml_id);
                 pcmk__xe_set(new_node, PCMK_XA_UNAME, node->name);
 
                 /* Search and remove unknown nodes with the conflicting uname from CIB */
                 pcmk__g_strcat(xpath,
                                "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION
                                "/" PCMK_XE_NODES "/" PCMK_XE_NODE
                                "[@" PCMK_XA_UNAME "='", node->name, "']"
                                "[@" PCMK_XA_ID "!='", node->xml_id, "']", NULL);
 
                 call_id = cib_conn->cmds->query(cib_conn,
                                                 (const char *) xpath->str, NULL,
                                                 cib_xpath);
                 fsa_register_cib_callback(call_id, pcmk__str_copy(node->xml_id),
                                           search_conflicting_node_callback);
             }
         }
 
         if (xpath != NULL) {
             g_string_free(xpath, TRUE);
         }
     }
 
     crm_trace("Populating <nodes> section from %s", from_hashtable ? "hashtable" : "cluster");
 
     if ((controld_update_cib(PCMK_XE_NODES, node_list, cib_none,
                              node_list_update_callback) == pcmk_rc_ok)
          && (pcmk__peer_cache != NULL) && AM_I_DC) {
         /*
          * There is no need to update the local CIB with our values if
          * we've not seen valid membership data
          */
         GHashTableIter iter;
         pcmk__node_status_t *node = NULL;
 
         pcmk__xml_free(node_list);
         node_list = pcmk__xe_create(NULL, PCMK_XE_STATUS);
 
         g_hash_table_iter_init(&iter, pcmk__peer_cache);
         while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) {
             create_node_state_update(node, flags, node_list, source);
         }
 
         if (pcmk__remote_peer_cache != NULL) {
             g_hash_table_iter_init(&iter, pcmk__remote_peer_cache);
             while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) {
                 create_node_state_update(node, flags, node_list, source);
             }
         }
 
         controld_update_cib(PCMK_XE_STATUS, node_list, cib_none,
                             crmd_node_update_complete);
     }
     pcmk__xml_free(node_list);
 }
 
 static void
 cib_quorum_update_complete(xmlNode * msg, int call_id, int rc, xmlNode * output, void *user_data)
 {
     fsa_data_t *msg_data = NULL;
 
     if (rc == pcmk_ok) {
         crm_trace("Quorum update %d complete", call_id);
 
     } else {
         crm_err("Quorum update %d failed: %s (%d)", call_id, pcmk_strerror(rc), rc);
         crm_log_xml_debug(msg, "failed");
         register_fsa_error(C_FSA_INTERNAL, I_ERROR, NULL);
     }
 }
 
 void
 crm_update_quorum(gboolean quorum, gboolean force_update)
 {
     bool has_quorum = pcmk__is_set(controld_globals.flags, controld_has_quorum);
 
     if (quorum) {
         controld_set_global_flags(controld_ever_had_quorum);
 
     } else if (pcmk__all_flags_set(controld_globals.flags,
                                    controld_ever_had_quorum
                                    |controld_no_quorum_panic)) {
         pcmk__panic("Quorum lost");
     }
 
     if (AM_I_DC
         && ((has_quorum && !quorum) || (!has_quorum && quorum)
             || force_update)) {
         xmlNode *update = NULL;
 
         update = pcmk__xe_create(NULL, PCMK_XE_CIB);
         pcmk__xe_set_int(update, PCMK_XA_HAVE_QUORUM, quorum);
         pcmk__xe_set(update, PCMK_XA_DC_UUID, controld_globals.our_uuid);
 
         crm_debug("Updating quorum status to %s", pcmk__btoa(quorum));
         controld_update_cib(PCMK_XE_CIB, update, cib_none,
                             cib_quorum_update_complete);
         pcmk__xml_free(update);
 
         /* Quorum changes usually cause a new transition via other activity:
          * quorum gained via a node joining will abort via the node join,
          * and quorum lost via a node leaving will usually abort via resource
          * activity and/or fencing.
          *
          * However, it is possible that nothing else causes a transition (e.g.
          * someone forces quorum via corosync-cmaptcl, or quorum is lost due to
          * a node in standby shutting down cleanly), so here ensure a new
          * transition is triggered.
          */
         if (quorum) {
             /* If quorum was gained, abort after a short delay, in case multiple
              * nodes are joining around the same time, so the one that brings us
              * to quorum doesn't cause all the remaining ones to be fenced.
              */
             abort_after_delay(PCMK_SCORE_INFINITY, pcmk__graph_restart,
                               "Quorum gained", 5000);
         } else {
             abort_transition(PCMK_SCORE_INFINITY, pcmk__graph_restart,
                              "Quorum lost", NULL);
         }
     }
 
     if (quorum) {
         controld_set_global_flags(controld_has_quorum);
     } else {
         controld_clear_global_flags(controld_has_quorum);
     }
 }
diff --git a/daemons/controld/controld_metadata.c b/daemons/controld/controld_metadata.c
index 8b5aa0db84..8c6f380509 100644
--- a/daemons/controld/controld_metadata.c
+++ b/daemons/controld/controld_metadata.c
@@ -1,311 +1,312 @@
 /*
  * Copyright 2017-2025 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 <glib.h>
 #include <regex.h>
 
 #include <crm/crm.h>
 #include <crm/lrmd.h>
 
 #include <pacemaker-controld.h>
 
 static void
 ra_param_free(void *param)
 {
     if (param) {
         struct ra_param_s *p = (struct ra_param_s *) param;
 
         if (p->rap_name) {
             free(p->rap_name);
         }
         free(param);
     }
 }
 
 static void
 metadata_free(void *metadata)
 {
     if (metadata) {
         struct ra_metadata_s *md = (struct ra_metadata_s *) metadata;
 
         g_list_free_full(md->ra_params, ra_param_free);
         free(metadata);
     }
 }
 
 GHashTable *
 metadata_cache_new(void)
 {
     return pcmk__strkey_table(free, metadata_free);
 }
 
 void
 metadata_cache_free(GHashTable *mdc)
 {
     if (mdc) {
         crm_trace("Destroying metadata cache with %d members", g_hash_table_size(mdc));
         g_hash_table_destroy(mdc);
     }
 }
 
 void
 metadata_cache_reset(GHashTable *mdc)
 {
     if (mdc) {
         crm_trace("Resetting metadata cache with %d members",
                   g_hash_table_size(mdc));
         g_hash_table_remove_all(mdc);
     }
 }
 
 static struct ra_param_s *
 ra_param_from_xml(xmlNode *param_xml)
 {
     const char *param_name = pcmk__xe_get(param_xml, PCMK_XA_NAME);
     struct ra_param_s *p;
 
     p = pcmk__assert_alloc(1, sizeof(struct ra_param_s));
 
     p->rap_name = pcmk__str_copy(param_name);
 
     if (pcmk__xe_attr_is_true(param_xml, PCMK_XA_RELOADABLE)) {
         controld_set_ra_param_flags(p, ra_param_reloadable);
     }
 
     if (pcmk__xe_attr_is_true(param_xml, PCMK_XA_UNIQUE)) {
         controld_set_ra_param_flags(p, ra_param_unique);
     }
 
     if (pcmk__xe_attr_is_true(param_xml, "private")) {
         controld_set_ra_param_flags(p, ra_param_private);
     }
     return p;
 }
 
 static void
 log_ra_ocf_version(const char *ra_key, const char *ra_ocf_version)
 {
     if (pcmk__str_empty(ra_ocf_version)) {
         crm_warn("%s does not advertise OCF version supported", ra_key);
 
-    } else if (compare_version(ra_ocf_version, "2") >= 0) {
+    } else if (pcmk__compare_versions(ra_ocf_version, "2") >= 0) {
         crm_warn("%s supports OCF version %s (this Pacemaker version supports "
                  PCMK_OCF_VERSION " and might not work properly with agent)",
                  ra_key, ra_ocf_version);
 
-    } else if (compare_version(ra_ocf_version, PCMK_OCF_VERSION) > 0) {
+    } else if (pcmk__compare_versions(ra_ocf_version, PCMK_OCF_VERSION) > 0) {
         crm_info("%s supports OCF version %s (this Pacemaker version supports "
                  PCMK_OCF_VERSION " and might not use all agent features)",
                  ra_key, ra_ocf_version);
 
     } else {
         crm_debug("%s supports OCF version %s", ra_key, ra_ocf_version);
     }
 }
 
 struct ra_metadata_s *
 controld_cache_metadata(GHashTable *mdc, const lrmd_rsc_info_t *rsc,
                         const char *metadata_str)
 {
     char *key = NULL;
     const char *reason = NULL;
     xmlNode *metadata = NULL;
     xmlNode *match = NULL;
     struct ra_metadata_s *md = NULL;
     bool any_private_params = false;
     bool ocf1_1 = false;
 
     CRM_CHECK(mdc && rsc && metadata_str, return NULL);
 
     key = crm_generate_ra_key(rsc->standard, rsc->provider, rsc->type);
     if (!key) {
         reason = "Invalid resource agent standard or type";
         goto err;
     }
 
     metadata = pcmk__xml_parse(metadata_str);
     if (!metadata) {
         reason = "Metadata is not valid XML";
         goto err;
     }
 
     md = pcmk__assert_alloc(1, sizeof(struct ra_metadata_s));
 
     if (strcmp(rsc->standard, PCMK_RESOURCE_CLASS_OCF) == 0) {
         xmlChar *content = NULL;
         xmlNode *version_element = pcmk__xe_first_child(metadata,
                                                         PCMK_XE_VERSION, NULL,
                                                         NULL);
 
         if (version_element != NULL) {
             content = xmlNodeGetContent(version_element);
         }
         log_ra_ocf_version(key, (const char *) content);
         if (content != NULL) {
-            ocf1_1 = (compare_version((const char *) content, "1.1") >= 0);
+            ocf1_1 = (pcmk__compare_versions((const char *) content,
+                                             "1.1") >= 0);
             xmlFree(content);
         }
     }
 
     // Check supported actions
     match = pcmk__xe_first_child(metadata, PCMK_XE_ACTIONS, NULL, NULL);
     for (match = pcmk__xe_first_child(match, PCMK_XE_ACTION, NULL, NULL);
          match != NULL; match = pcmk__xe_next(match, PCMK_XE_ACTION)) {
 
         const char *action_name = pcmk__xe_get(match, PCMK_XA_NAME);
 
         if (pcmk__str_eq(action_name, PCMK_ACTION_RELOAD_AGENT,
                          pcmk__str_none)) {
             if (ocf1_1) {
                 controld_set_ra_flags(md, key, ra_supports_reload_agent);
             } else {
                 crm_notice("reload-agent action will not be used with %s "
                            "because it does not support OCF 1.1 or later", key);
             }
 
         } else if (!ocf1_1 && pcmk__str_eq(action_name, PCMK_ACTION_RELOAD,
                                            pcmk__str_casei)) {
             controld_set_ra_flags(md, key, ra_supports_legacy_reload);
         }
     }
 
     // Build a parameter list
     match = pcmk__xe_first_child(metadata, PCMK_XE_PARAMETERS, NULL, NULL);
     for (match = pcmk__xe_first_child(match, PCMK_XE_PARAMETER, NULL, NULL);
          match != NULL; match = pcmk__xe_next(match, PCMK_XE_PARAMETER)) {
 
         const char *param_name = pcmk__xe_get(match, PCMK_XA_NAME);
 
         if (param_name == NULL) {
             crm_warn("Metadata for %s:%s:%s has parameter without a "
                      PCMK_XA_NAME, rsc->standard, rsc->provider, rsc->type);
         } else {
             struct ra_param_s *p = ra_param_from_xml(match);
 
             if (p == NULL) {
                 reason = "Could not allocate memory";
                 goto err;
             }
             if (pcmk__is_set(p->rap_flags, ra_param_private)) {
                 any_private_params = true;
             }
             md->ra_params = g_list_prepend(md->ra_params, p);
         }
     }
 
     /* Newer resource agents support the "private" parameter attribute to
      * indicate sensitive parameters. For backward compatibility with older
      * agents, implicitly treat a few common names as private when the agent
      * doesn't specify any explicitly.
      */
     if (!any_private_params) {
         for (GList *iter = md->ra_params; iter != NULL; iter = iter->next) {
             struct ra_param_s *p = iter->data;
 
             if (pcmk__str_any_of(p->rap_name, "password", "passwd", "user",
                                  NULL)) {
                 controld_set_ra_param_flags(p, ra_param_private);
             }
         }
     }
 
     g_hash_table_replace(mdc, key, md);
     pcmk__xml_free(metadata);
     return md;
 
 err:
     crm_warn("Unable to update metadata for %s (%s%s%s:%s): %s",
              rsc->id, rsc->standard, ((rsc->provider == NULL)? "" : ":"),
              pcmk__s(rsc->provider, ""), rsc->type, reason);
     free(key);
     pcmk__xml_free(metadata);
     metadata_free(md);
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Get meta-data for a resource
  *
  * \param[in,out] lrm_state  Use meta-data cache from this executor connection
  * \param[in]     rsc        Resource to get meta-data for
  * \param[in]     source     Allowed meta-data sources (bitmask of
  *                           enum controld_metadata_source_e values)
  *
  * \return Meta-data cache entry for given resource, or NULL if not available
  */
 struct ra_metadata_s *
 controld_get_rsc_metadata(lrm_state_t *lrm_state, const lrmd_rsc_info_t *rsc,
                           uint32_t source)
 {
     struct ra_metadata_s *metadata = NULL;
     char *metadata_str = NULL;
     char *key = NULL;
     int rc = pcmk_ok;
 
     CRM_CHECK((lrm_state != NULL) && (rsc != NULL), return NULL);
 
     if (pcmk__is_set(source, controld_metadata_from_cache)) {
         key = crm_generate_ra_key(rsc->standard, rsc->provider, rsc->type);
         if (key != NULL) {
             metadata = g_hash_table_lookup(lrm_state->metadata_cache, key);
             free(key);
         }
         if (metadata != NULL) {
             crm_debug("Retrieved metadata for %s (%s%s%s:%s) from cache",
                       rsc->id, rsc->standard,
                       ((rsc->provider == NULL)? "" : ":"),
                       ((rsc->provider == NULL)? "" : rsc->provider),
                       rsc->type);
             return metadata;
         }
     }
 
     if (!pcmk__is_set(source, controld_metadata_from_agent)) {
         return NULL;
     }
 
     /* For most actions, metadata was cached asynchronously before action
      * execution (via metadata_complete()).
      *
      * However if that failed, and for other actions, retrieve the metadata now
      * via a local, synchronous, direct execution of the agent.
      *
      * This has multiple issues, which is why this is just a fallback: the
      * executor should execute agents, not the controller; metadata for
      * Pacemaker Remote nodes should be collected on those nodes, not locally;
      * the metadata call shouldn't eat into the timeout of the real action being
      * performed; and the synchronous call blocks the controller (which also
      * means that if the metadata action tries to contact the controller,
      * everything will hang until the timeout).
      */
     crm_debug("Retrieving metadata for %s (%s%s%s:%s) synchronously",
               rsc->id, rsc->standard,
               ((rsc->provider == NULL)? "" : ":"),
               ((rsc->provider == NULL)? "" : rsc->provider),
               rsc->type);
     rc = lrm_state_get_metadata(lrm_state, rsc->standard, rsc->provider,
                                 rsc->type, &metadata_str, 0);
     if (rc != pcmk_ok) {
         crm_warn("Failed to get metadata for %s (%s%s%s:%s): %s",
                  rsc->id, rsc->standard,
                  ((rsc->provider == NULL)? "" : ":"),
                  ((rsc->provider == NULL)? "" : rsc->provider),
                  rsc->type, pcmk_strerror(rc));
         return NULL;
     }
 
     metadata = controld_cache_metadata(lrm_state->metadata_cache, rsc,
                                        metadata_str);
     free(metadata_str);
     return metadata;
 }
diff --git a/daemons/execd/execd_commands.c b/daemons/execd/execd_commands.c
index 336fa3caf9..cd34021f61 100644
--- a/daemons/execd/execd_commands.c
+++ b/daemons/execd/execd_commands.c
@@ -1,2001 +1,2002 @@
 /*
  * Copyright 2012-2025 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 #include <crm/fencing/internal.h>
 
 #include <glib.h>
 #include <libxml/tree.h>                // xmlNode
 
 // Check whether we have a high-resolution monotonic clock
 #undef PCMK__TIME_USE_CGT
 #if HAVE_DECL_CLOCK_MONOTONIC && defined(CLOCK_MONOTONIC)
 #  define PCMK__TIME_USE_CGT
 #  include <time.h>  /* clock_gettime */
 #endif
 
 #include <unistd.h>
 
 #include <crm/crm.h>
 #include <crm/fencing/internal.h>
 #include <crm/services.h>
 #include <crm/services_internal.h>
 #include <crm/common/mainloop.h>
 #include <crm/common/ipc.h>
 #include <crm/common/ipc_internal.h>
 #include <crm/common/xml.h>
 
 #include "pacemaker-execd.h"
 
 GHashTable *rsc_list = NULL;
 
 typedef struct lrmd_cmd_s {
     int timeout;
     guint interval_ms;
     int start_delay;
     int timeout_orig;
 
     int call_id;
 
     int call_opts;
     /* Timer ids, must be removed on cmd destruction. */
     int delay_id;
     int stonith_recurring_id;
 
     int rsc_deleted;
 
     int service_flags;
 
     char *client_id;
     char *origin;
     char *rsc_id;
     char *action;
     char *real_action;
     char *userdata_str;
 
     pcmk__action_result_t result;
 
     /* We can track operation queue time and run time, to be saved with the CIB
      * resource history (and displayed in cluster status). We need
      * high-resolution monotonic time for this purpose, so we use
      * clock_gettime(CLOCK_MONOTONIC, ...) (if available, otherwise this feature
      * is disabled).
      *
      * However, we also need epoch timestamps for recording the time the command
      * last ran and the time its return value last changed, for use in time
      * displays (as opposed to interval calculations). We keep time_t values for
      * this purpose.
      *
      * The last run time is used for both purposes, so we keep redundant
      * monotonic and epoch values for this. Technically the two could represent
      * different times, but since time_t has only second resolution and the
      * values are used for distinct purposes, that is not significant.
      */
 #ifdef PCMK__TIME_USE_CGT
     /* Recurring and systemd operations may involve more than one executor
      * command per operation, so they need info about the original and the most
      * recent.
      */
     struct timespec t_first_run;    // When op first ran
     struct timespec t_run;          // When op most recently ran
     struct timespec t_first_queue;  // When op was first queued
     struct timespec t_queue;        // When op was most recently queued
 #endif
     time_t epoch_last_run;          // Epoch timestamp of when op last ran
     time_t epoch_rcchange;          // Epoch timestamp of when rc last changed
 
     bool first_notify_sent;
     int last_notify_rc;
     int last_notify_op_status;
     int last_pid;
 
     GHashTable *params;
 } lrmd_cmd_t;
 
 static void cmd_finalize(lrmd_cmd_t * cmd, lrmd_rsc_t * rsc);
 static gboolean execute_resource_action(gpointer user_data);
 static void cancel_all_recurring(lrmd_rsc_t * rsc, const char *client_id);
 
 #ifdef PCMK__TIME_USE_CGT
 
 /*!
  * \internal
  * \brief Check whether a struct timespec has been set
  *
  * \param[in] timespec  Time to check
  *
  * \return true if timespec has been set (i.e. is nonzero), false otherwise
  */
 static inline bool
 time_is_set(const struct timespec *timespec)
 {
     return (timespec != NULL) &&
            ((timespec->tv_sec != 0) || (timespec->tv_nsec != 0));
 }
 
 /*
  * \internal
  * \brief Set a timespec (and its original if unset) to the current time
  *
  * \param[out] t_current  Where to store current time
  * \param[out] t_orig     Where to copy t_current if unset
  */
 static void
 get_current_time(struct timespec *t_current, struct timespec *t_orig)
 {
     clock_gettime(CLOCK_MONOTONIC, t_current);
     if ((t_orig != NULL) && !time_is_set(t_orig)) {
         *t_orig = *t_current;
     }
 }
 
 /*!
  * \internal
  * \brief Return difference between two times in milliseconds
  *
  * \param[in] now  More recent time (or NULL to use current time)
  * \param[in] old  Earlier time
  *
  * \return milliseconds difference (or 0 if old is NULL or unset)
  *
  * \note Can overflow on 32bit machines when the differences is around
  *       24 days or more.
  */
 static int
 time_diff_ms(const struct timespec *now, const struct timespec *old)
 {
     int diff_ms = 0;
 
     if (time_is_set(old)) {
         struct timespec local_now = { 0, };
 
         if (now == NULL) {
             clock_gettime(CLOCK_MONOTONIC, &local_now);
             now = &local_now;
         }
         diff_ms = (now->tv_sec - old->tv_sec) * 1000
                   + (now->tv_nsec - old->tv_nsec) / 1000000;
     }
     return diff_ms;
 }
 
 /*!
  * \internal
  * \brief Reset a command's operation times to their original values.
  *
  * Reset a command's run and queued timestamps to the timestamps of the original
  * command, so we report the entire time since then and not just the time since
  * the most recent command (for recurring and systemd operations).
  *
  * \param[in,out] cmd  Executor command object to reset
  *
  * \note It's not obvious what the queued time should be for a systemd
  *       start/stop operation, which might go like this:
  *         initial command queued 5ms, runs 3s
  *         monitor command queued 10ms, runs 10s
  *         monitor command queued 10ms, runs 10s
  *       Is the queued time for that operation 5ms, 10ms or 25ms? The current
  *       implementation will report 5ms. If it's 25ms, then we need to
  *       subtract 20ms from the total exec time so as not to count it twice.
  *       We can implement that later if it matters to anyone ...
  */
 static void
 cmd_original_times(lrmd_cmd_t * cmd)
 {
     cmd->t_run = cmd->t_first_run;
     cmd->t_queue = cmd->t_first_queue;
 }
 #endif
 
 static inline bool
 action_matches(const lrmd_cmd_t *cmd, const char *action, guint interval_ms)
 {
     return (cmd->interval_ms == interval_ms)
            && pcmk__str_eq(cmd->action, action, pcmk__str_casei);
 }
 
 /*!
  * \internal
  * \brief Log the result of an asynchronous command
  *
  * \param[in] cmd            Command to log result for
  * \param[in] exec_time_ms   Execution time in milliseconds, if known
  * \param[in] queue_time_ms  Queue time in milliseconds, if known
  */
 static void
 log_finished(const lrmd_cmd_t *cmd, int exec_time_ms, int queue_time_ms)
 {
     int log_level = LOG_INFO;
     GString *str = g_string_sized_new(100); // reasonable starting size
 
     if (pcmk__str_eq(cmd->action, PCMK_ACTION_MONITOR, pcmk__str_casei)) {
         log_level = LOG_DEBUG;
     }
 
     g_string_append_printf(str, "%s %s (call %d",
                            cmd->rsc_id, cmd->action, cmd->call_id);
     if (cmd->last_pid != 0) {
         g_string_append_printf(str, ", PID %d", cmd->last_pid);
     }
     switch (cmd->result.execution_status) {
         case PCMK_EXEC_DONE:
             g_string_append_printf(str, ") exited with status %d",
                                    cmd->result.exit_status);
             break;
         case PCMK_EXEC_CANCELLED:
             g_string_append_printf(str, ") cancelled");
             break;
         default:
             pcmk__g_strcat(str, ") could not be executed: ",
                            pcmk_exec_status_str(cmd->result.execution_status),
                            NULL);
             break;
     }
     if (cmd->result.exit_reason != NULL) {
         pcmk__g_strcat(str, " (", cmd->result.exit_reason, ")", NULL);
     }
 
 #ifdef PCMK__TIME_USE_CGT
     pcmk__g_strcat(str, " (execution time ",
                    pcmk__readable_interval(exec_time_ms), NULL);
     if (queue_time_ms > 0) {
         pcmk__g_strcat(str, " after being queued ",
                        pcmk__readable_interval(queue_time_ms), NULL);
     }
     g_string_append_c(str, ')');
 #endif
 
     do_crm_log(log_level, "%s", str->str);
     g_string_free(str, TRUE);
 }
 
 static void
 log_execute(lrmd_cmd_t * cmd)
 {
     int log_level = LOG_INFO;
 
     if (pcmk__str_eq(cmd->action, PCMK_ACTION_MONITOR, pcmk__str_casei)) {
         log_level = LOG_DEBUG;
     }
 
     do_crm_log(log_level, "executing - rsc:%s action:%s call_id:%d",
                cmd->rsc_id, cmd->action, cmd->call_id);
 }
 
 static const char *
 normalize_action_name(lrmd_rsc_t * rsc, const char *action)
 {
     if (pcmk__str_eq(action, PCMK_ACTION_MONITOR, pcmk__str_casei) &&
         pcmk__is_set(pcmk_get_ra_caps(rsc->class), pcmk_ra_cap_status)) {
         return PCMK_ACTION_STATUS;
     }
     return action;
 }
 
 static lrmd_rsc_t *
 build_rsc_from_xml(xmlNode * msg)
 {
     xmlNode *rsc_xml = pcmk__xpath_find_one(msg->doc, "//" PCMK__XE_LRMD_RSC,
                                             LOG_ERR);
     lrmd_rsc_t *rsc = NULL;
 
     rsc = pcmk__assert_alloc(1, sizeof(lrmd_rsc_t));
 
     pcmk__xe_get_int(msg, PCMK__XA_LRMD_CALLOPT, &rsc->call_opts);
 
     rsc->rsc_id = pcmk__xe_get_copy(rsc_xml, PCMK__XA_LRMD_RSC_ID);
     rsc->class = pcmk__xe_get_copy(rsc_xml, PCMK__XA_LRMD_CLASS);
     rsc->provider = pcmk__xe_get_copy(rsc_xml, PCMK__XA_LRMD_PROVIDER);
     rsc->type = pcmk__xe_get_copy(rsc_xml, PCMK__XA_LRMD_TYPE);
     rsc->work = mainloop_add_trigger(G_PRIORITY_HIGH, execute_resource_action,
                                      rsc);
 
     // Initialize fence device probes (to return "not running")
     pcmk__set_result(&rsc->fence_probe_result, CRM_EX_ERROR,
                      PCMK_EXEC_NO_FENCE_DEVICE, NULL);
     return rsc;
 }
 
 static lrmd_cmd_t *
 create_lrmd_cmd(xmlNode *msg, pcmk__client_t *client)
 {
     int call_options = 0;
     xmlNode *rsc_xml = pcmk__xpath_find_one(msg->doc, "//" PCMK__XE_LRMD_RSC,
                                             LOG_ERR);
     lrmd_cmd_t *cmd = NULL;
 
     cmd = pcmk__assert_alloc(1, sizeof(lrmd_cmd_t));
 
     pcmk__xe_get_int(msg, PCMK__XA_LRMD_CALLOPT, &call_options);
     cmd->call_opts = call_options;
     cmd->client_id = pcmk__str_copy(client->id);
 
     pcmk__xe_get_int(msg, PCMK__XA_LRMD_CALLID, &cmd->call_id);
     pcmk__xe_get_guint(rsc_xml, PCMK__XA_LRMD_RSC_INTERVAL, &cmd->interval_ms);
     pcmk__xe_get_int(rsc_xml, PCMK__XA_LRMD_TIMEOUT, &cmd->timeout);
     pcmk__xe_get_int(rsc_xml, PCMK__XA_LRMD_RSC_START_DELAY, &cmd->start_delay);
     cmd->timeout_orig = cmd->timeout;
 
     cmd->origin = pcmk__xe_get_copy(rsc_xml, PCMK__XA_LRMD_ORIGIN);
     cmd->action = pcmk__xe_get_copy(rsc_xml, PCMK__XA_LRMD_RSC_ACTION);
     cmd->userdata_str = pcmk__xe_get_copy(rsc_xml,
                                           PCMK__XA_LRMD_RSC_USERDATA_STR);
     cmd->rsc_id = pcmk__xe_get_copy(rsc_xml, PCMK__XA_LRMD_RSC_ID);
 
     cmd->params = xml2list(rsc_xml);
 
     if (pcmk__str_eq(g_hash_table_lookup(cmd->params, "CRM_meta_on_fail"),
                      PCMK_VALUE_BLOCK, pcmk__str_casei)) {
         crm_debug("Setting flag to leave pid group on timeout and "
                   "only kill action pid for " PCMK__OP_FMT,
                   cmd->rsc_id, cmd->action, cmd->interval_ms);
         cmd->service_flags = pcmk__set_flags_as(__func__, __LINE__,
                                                 LOG_TRACE, "Action",
                                                 cmd->action, 0,
                                                 SVC_ACTION_LEAVE_GROUP,
                                                 "SVC_ACTION_LEAVE_GROUP");
     }
     return cmd;
 }
 
 static void
 stop_recurring_timer(lrmd_cmd_t *cmd)
 {
     if (cmd) {
         if (cmd->stonith_recurring_id) {
             g_source_remove(cmd->stonith_recurring_id);
         }
         cmd->stonith_recurring_id = 0;
     }
 }
 
 static void
 free_lrmd_cmd(lrmd_cmd_t * cmd)
 {
     stop_recurring_timer(cmd);
     if (cmd->delay_id) {
         g_source_remove(cmd->delay_id);
     }
     if (cmd->params) {
         g_hash_table_destroy(cmd->params);
     }
     pcmk__reset_result(&(cmd->result));
     free(cmd->origin);
     free(cmd->action);
     free(cmd->real_action);
     free(cmd->userdata_str);
     free(cmd->rsc_id);
     free(cmd->client_id);
     free(cmd);
 }
 
 static gboolean
 stonith_recurring_op_helper(gpointer data)
 {
     lrmd_cmd_t *cmd = data;
     lrmd_rsc_t *rsc;
 
     cmd->stonith_recurring_id = 0;
 
     if (!cmd->rsc_id) {
         return FALSE;
     }
 
     rsc = g_hash_table_lookup(rsc_list, cmd->rsc_id);
 
     pcmk__assert(rsc != NULL);
     /* take it out of recurring_ops list, and put it in the pending ops
      * to be executed */
     rsc->recurring_ops = g_list_remove(rsc->recurring_ops, cmd);
     rsc->pending_ops = g_list_append(rsc->pending_ops, cmd);
 #ifdef PCMK__TIME_USE_CGT
     get_current_time(&(cmd->t_queue), &(cmd->t_first_queue));
 #endif
     mainloop_set_trigger(rsc->work);
 
     return FALSE;
 }
 
 static inline void
 start_recurring_timer(lrmd_cmd_t *cmd)
 {
     if (!cmd || (cmd->interval_ms <= 0)) {
         return;
     }
 
     cmd->stonith_recurring_id = pcmk__create_timer(cmd->interval_ms,
                                                    stonith_recurring_op_helper,
                                                    cmd);
 }
 
 static gboolean
 start_delay_helper(gpointer data)
 {
     lrmd_cmd_t *cmd = data;
     lrmd_rsc_t *rsc = NULL;
 
     cmd->delay_id = 0;
     rsc = cmd->rsc_id ? g_hash_table_lookup(rsc_list, cmd->rsc_id) : NULL;
 
     if (rsc) {
         mainloop_set_trigger(rsc->work);
     }
 
     return FALSE;
 }
 
 /*!
  * \internal
  * \brief Check whether a list already contains the equivalent of a given action
  *
  * \param[in] action_list  List to search
  * \param[in] cmd          Action to search for
  */
 static lrmd_cmd_t *
 find_duplicate_action(const GList *action_list, const lrmd_cmd_t *cmd)
 {
     for (const GList *item = action_list; item != NULL; item = item->next) {
         lrmd_cmd_t *dup = item->data;
 
         if (action_matches(cmd, dup->action, dup->interval_ms)) {
             return dup;
         }
     }
     return NULL;
 }
 
 static bool
 merge_recurring_duplicate(lrmd_rsc_t * rsc, lrmd_cmd_t * cmd)
 {
     lrmd_cmd_t * dup = NULL;
     bool dup_pending = true;
 
     if (cmd->interval_ms == 0) {
         return false;
     }
 
     // Search for a duplicate of this action (in-flight or not)
     dup = find_duplicate_action(rsc->pending_ops, cmd);
     if (dup == NULL) {
         dup_pending = false;
         dup = find_duplicate_action(rsc->recurring_ops, cmd);
         if (dup == NULL) {
             return false;
         }
     }
 
     /* Do not merge fencing monitors marked for cancellation, so we can reply to
      * the cancellation separately.
      */
     if (pcmk__str_eq(rsc->class, PCMK_RESOURCE_CLASS_STONITH,
                      pcmk__str_casei)
         && (dup->result.execution_status == PCMK_EXEC_CANCELLED)) {
         return false;
     }
 
     /* This should not occur. If it does, we need to investigate how something
      * like this is possible in the controller.
      */
     crm_warn("Duplicate recurring op entry detected (" PCMK__OP_FMT
              "), merging with previous op entry",
              rsc->rsc_id, normalize_action_name(rsc, dup->action),
              dup->interval_ms);
 
     // Merge new action's call ID and user data into existing action
     dup->first_notify_sent = false;
     free(dup->userdata_str);
     dup->userdata_str = cmd->userdata_str;
     cmd->userdata_str = NULL;
     dup->call_id = cmd->call_id;
     free_lrmd_cmd(cmd);
     cmd = NULL;
 
     /* If dup is not pending, that means it has already executed at least once
      * and is waiting in the interval. In that case, stop waiting and initiate
      * a new instance now.
      */
     if (!dup_pending) {
         if (pcmk__str_eq(rsc->class, PCMK_RESOURCE_CLASS_STONITH,
                          pcmk__str_casei)) {
             stop_recurring_timer(dup);
             stonith_recurring_op_helper(dup);
         } else {
             services_action_kick(rsc->rsc_id,
                                  normalize_action_name(rsc, dup->action),
                                  dup->interval_ms);
         }
     }
     return true;
 }
 
 static void
 schedule_lrmd_cmd(lrmd_rsc_t * rsc, lrmd_cmd_t * cmd)
 {
     CRM_CHECK(cmd != NULL, return);
     CRM_CHECK(rsc != NULL, return);
 
     crm_trace("Scheduling %s on %s", cmd->action, rsc->rsc_id);
 
     if (merge_recurring_duplicate(rsc, cmd)) {
         // Equivalent of cmd has already been scheduled
         return;
     }
 
     /* The controller expects the executor to automatically cancel
      * recurring operations before a resource stops.
      */
     if (pcmk__str_eq(cmd->action, PCMK_ACTION_STOP, pcmk__str_casei)) {
         cancel_all_recurring(rsc, NULL);
     }
 
     rsc->pending_ops = g_list_append(rsc->pending_ops, cmd);
 #ifdef PCMK__TIME_USE_CGT
     get_current_time(&(cmd->t_queue), &(cmd->t_first_queue));
 #endif
     mainloop_set_trigger(rsc->work);
 
     if (cmd->start_delay) {
         cmd->delay_id = pcmk__create_timer(cmd->start_delay, start_delay_helper, cmd);
     }
 }
 
 static xmlNode *
 create_lrmd_reply(const char *origin, int rc, int call_id)
 {
     xmlNode *reply = pcmk__xe_create(NULL, PCMK__XE_LRMD_REPLY);
 
     pcmk__xe_set(reply, PCMK__XA_LRMD_ORIGIN, origin);
     pcmk__xe_set_int(reply, PCMK__XA_LRMD_RC, rc);
     pcmk__xe_set_int(reply, PCMK__XA_LRMD_CALLID, call_id);
     return reply;
 }
 
 static void
 send_client_notify(gpointer key, gpointer value, gpointer user_data)
 {
     xmlNode *update_msg = user_data;
     pcmk__client_t *client = value;
     int rc;
     int log_level = LOG_WARNING;
     const char *msg = NULL;
 
     CRM_CHECK(client != NULL, return);
     if (client->name == NULL) {
         crm_trace("Skipping notification to client without name");
         return;
     }
     if (pcmk__is_set(client->flags, pcmk__client_to_proxy)) {
         /* We only want to notify clients of the executor IPC API. If we are
          * running as Pacemaker Remote, we may have clients proxied to other
          * IPC services in the cluster, so skip those.
          */
         crm_trace("Skipping executor API notification to client %s",
                   pcmk__client_name(client));
         return;
     }
 
     rc = lrmd_server_send_notify(client, update_msg);
     if (rc == pcmk_rc_ok) {
         return;
     }
 
     switch (rc) {
         case ENOTCONN:
         case EPIPE: // Client exited without waiting for notification
             log_level = LOG_INFO;
             msg = "Disconnected";
             break;
 
         default:
             msg = pcmk_rc_str(rc);
             break;
     }
     do_crm_log(log_level, "Could not notify client %s: %s " QB_XS " rc=%d",
                pcmk__client_name(client), msg, rc);
 }
 
 static void
 send_cmd_complete_notify(lrmd_cmd_t * cmd)
 {
     xmlNode *notify = NULL;
     int exec_time = 0;
     int queue_time = 0;
 
 #ifdef PCMK__TIME_USE_CGT
     exec_time = time_diff_ms(NULL, &(cmd->t_run));
     queue_time = time_diff_ms(&cmd->t_run, &(cmd->t_queue));
 #endif
     log_finished(cmd, exec_time, queue_time);
 
     /* If the originator requested to be notified only for changes in recurring
      * operation results, skip the notification if the result hasn't changed.
      */
     if (cmd->first_notify_sent
         && pcmk__is_set(cmd->call_opts, lrmd_opt_notify_changes_only)
         && (cmd->last_notify_rc == cmd->result.exit_status)
         && (cmd->last_notify_op_status == cmd->result.execution_status)) {
         return;
     }
 
     cmd->first_notify_sent = true;
     cmd->last_notify_rc = cmd->result.exit_status;
     cmd->last_notify_op_status = cmd->result.execution_status;
 
     notify = pcmk__xe_create(NULL, PCMK__XE_LRMD_NOTIFY);
 
     pcmk__xe_set(notify, PCMK__XA_LRMD_ORIGIN, __func__);
     pcmk__xe_set_int(notify, PCMK__XA_LRMD_TIMEOUT, cmd->timeout);
     pcmk__xe_set_guint(notify, PCMK__XA_LRMD_RSC_INTERVAL, cmd->interval_ms);
     pcmk__xe_set_int(notify, PCMK__XA_LRMD_RSC_START_DELAY, cmd->start_delay);
     pcmk__xe_set_int(notify, PCMK__XA_LRMD_EXEC_RC, cmd->result.exit_status);
     pcmk__xe_set_int(notify, PCMK__XA_LRMD_EXEC_OP_STATUS,
                      cmd->result.execution_status);
     pcmk__xe_set_int(notify, PCMK__XA_LRMD_CALLID, cmd->call_id);
     pcmk__xe_set_int(notify, PCMK__XA_LRMD_RSC_DELETED, cmd->rsc_deleted);
 
     pcmk__xe_set_time(notify, PCMK__XA_LRMD_RUN_TIME, cmd->epoch_last_run);
     pcmk__xe_set_time(notify, PCMK__XA_LRMD_RCCHANGE_TIME, cmd->epoch_rcchange);
 #ifdef PCMK__TIME_USE_CGT
     pcmk__xe_set_int(notify, PCMK__XA_LRMD_EXEC_TIME, exec_time);
     pcmk__xe_set_int(notify, PCMK__XA_LRMD_QUEUE_TIME, queue_time);
 #endif
 
     pcmk__xe_set(notify, PCMK__XA_LRMD_OP, LRMD_OP_RSC_EXEC);
     pcmk__xe_set(notify, PCMK__XA_LRMD_RSC_ID, cmd->rsc_id);
     if(cmd->real_action) {
         pcmk__xe_set(notify, PCMK__XA_LRMD_RSC_ACTION, cmd->real_action);
     } else {
         pcmk__xe_set(notify, PCMK__XA_LRMD_RSC_ACTION, cmd->action);
     }
     pcmk__xe_set(notify, PCMK__XA_LRMD_RSC_USERDATA_STR, cmd->userdata_str);
     pcmk__xe_set(notify, PCMK__XA_LRMD_RSC_EXIT_REASON, cmd->result.exit_reason);
 
     if (cmd->result.action_stderr != NULL) {
         pcmk__xe_set(notify, PCMK__XA_LRMD_RSC_OUTPUT,
                      cmd->result.action_stderr);
 
     } else if (cmd->result.action_stdout != NULL) {
         pcmk__xe_set(notify, PCMK__XA_LRMD_RSC_OUTPUT,
                      cmd->result.action_stdout);
     }
 
     if (cmd->params) {
         char *key = NULL;
         char *value = NULL;
         GHashTableIter iter;
 
         xmlNode *args = pcmk__xe_create(notify, PCMK__XE_ATTRIBUTES);
 
         g_hash_table_iter_init(&iter, cmd->params);
         while (g_hash_table_iter_next(&iter, (gpointer *) & key, (gpointer *) & value)) {
             hash2smartfield((gpointer) key, (gpointer) value, args);
         }
     }
     if ((cmd->client_id != NULL)
         && pcmk__is_set(cmd->call_opts, lrmd_opt_notify_orig_only)) {
 
         pcmk__client_t *client = pcmk__find_client_by_id(cmd->client_id);
 
         if (client != NULL) {
             send_client_notify(client->id, client, notify);
         }
     } else {
         pcmk__foreach_ipc_client(send_client_notify, notify);
     }
 
     pcmk__xml_free(notify);
 }
 
 static void
 send_generic_notify(int rc, xmlNode * request)
 {
     if (pcmk__ipc_client_count() != 0) {
         int call_id = 0;
         xmlNode *notify = NULL;
         xmlNode *rsc_xml = pcmk__xpath_find_one(request->doc,
                                                 "//" PCMK__XE_LRMD_RSC,
                                                 LOG_ERR);
         const char *rsc_id = pcmk__xe_get(rsc_xml, PCMK__XA_LRMD_RSC_ID);
         const char *op = pcmk__xe_get(request, PCMK__XA_LRMD_OP);
 
         pcmk__xe_get_int(request, PCMK__XA_LRMD_CALLID, &call_id);
 
         notify = pcmk__xe_create(NULL, PCMK__XE_LRMD_NOTIFY);
         pcmk__xe_set(notify, PCMK__XA_LRMD_ORIGIN, __func__);
         pcmk__xe_set_int(notify, PCMK__XA_LRMD_RC, rc);
         pcmk__xe_set_int(notify, PCMK__XA_LRMD_CALLID, call_id);
         pcmk__xe_set(notify, PCMK__XA_LRMD_OP, op);
         pcmk__xe_set(notify, PCMK__XA_LRMD_RSC_ID, rsc_id);
 
         pcmk__foreach_ipc_client(send_client_notify, notify);
 
         pcmk__xml_free(notify);
     }
 }
 
 static void
 cmd_reset(lrmd_cmd_t * cmd)
 {
     cmd->last_pid = 0;
 #ifdef PCMK__TIME_USE_CGT
     memset(&cmd->t_run, 0, sizeof(cmd->t_run));
     memset(&cmd->t_queue, 0, sizeof(cmd->t_queue));
 #endif
     cmd->epoch_last_run = 0;
 
     pcmk__reset_result(&(cmd->result));
     cmd->result.execution_status = PCMK_EXEC_DONE;
 }
 
 static void
 cmd_finalize(lrmd_cmd_t * cmd, lrmd_rsc_t * rsc)
 {
     crm_trace("Resource operation rsc:%s action:%s completed (%p %p)", cmd->rsc_id, cmd->action,
               rsc ? rsc->active : NULL, cmd);
 
     if (rsc && (rsc->active == cmd)) {
         rsc->active = NULL;
         mainloop_set_trigger(rsc->work);
     }
 
     if (!rsc) {
         cmd->rsc_deleted = 1;
     }
 
     /* reset original timeout so client notification has correct information */
     cmd->timeout = cmd->timeout_orig;
 
     send_cmd_complete_notify(cmd);
 
     if ((cmd->interval_ms != 0)
         && (cmd->result.execution_status == PCMK_EXEC_CANCELLED)) {
 
         if (rsc) {
             rsc->recurring_ops = g_list_remove(rsc->recurring_ops, cmd);
             rsc->pending_ops = g_list_remove(rsc->pending_ops, cmd);
         }
         free_lrmd_cmd(cmd);
     } else if (cmd->interval_ms == 0) {
         if (rsc) {
             rsc->pending_ops = g_list_remove(rsc->pending_ops, cmd);
         }
         free_lrmd_cmd(cmd);
     } else {
         /* Clear all the values pertaining just to the last iteration of a recurring op. */
         cmd_reset(cmd);
     }
 }
 
 struct notify_new_client_data {
     xmlNode *notify;
     pcmk__client_t *new_client;
 };
 
 static void
 notify_one_client(gpointer key, gpointer value, gpointer user_data)
 {
     pcmk__client_t *client = value;
     struct notify_new_client_data *data = user_data;
 
     if (!pcmk__str_eq(client->id, data->new_client->id, pcmk__str_casei)) {
         send_client_notify(key, (gpointer) client, (gpointer) data->notify);
     }
 }
 
 void
 notify_of_new_client(pcmk__client_t *new_client)
 {
     struct notify_new_client_data data;
 
     data.new_client = new_client;
     data.notify = pcmk__xe_create(NULL, PCMK__XE_LRMD_NOTIFY);
     pcmk__xe_set(data.notify, PCMK__XA_LRMD_ORIGIN, __func__);
     pcmk__xe_set(data.notify, PCMK__XA_LRMD_OP, LRMD_OP_NEW_CLIENT);
     pcmk__foreach_ipc_client(notify_one_client, &data);
     pcmk__xml_free(data.notify);
 }
 
 void
 client_disconnect_cleanup(const char *client_id)
 {
     GHashTableIter iter;
     lrmd_rsc_t *rsc = NULL;
     char *key = NULL;
 
     g_hash_table_iter_init(&iter, rsc_list);
     while (g_hash_table_iter_next(&iter, (gpointer *) & key, (gpointer *) & rsc)) {
         if (pcmk__is_set(rsc->call_opts, lrmd_opt_drop_recurring)) {
             /* This client is disconnecting, drop any recurring operations
              * it may have initiated on the resource */
             cancel_all_recurring(rsc, client_id);
         }
     }
 }
 
 static void
 action_complete(svc_action_t * action)
 {
     lrmd_rsc_t *rsc;
     lrmd_cmd_t *cmd = action->cb_data;
     enum ocf_exitcode code;
 
 #ifdef PCMK__TIME_USE_CGT
     const char *rclass = NULL;
     bool goagain = false;
     int time_sum = 0;
     int timeout_left = 0;
     int delay = 0;
 #endif
 
     if (!cmd) {
         crm_err("Completed executor action (%s) does not match any known operations",
                 action->id);
         return;
     }
 
 #ifdef PCMK__TIME_USE_CGT
     if (cmd->result.exit_status != action->rc) {
         cmd->epoch_rcchange = time(NULL);
     }
 #endif
 
     cmd->last_pid = action->pid;
 
     // Cast variable instead of function return to keep compilers happy
     code = services_result2ocf(action->standard, cmd->action, action->rc);
     pcmk__set_result(&(cmd->result), (int) code,
                      action->status, services__exit_reason(action));
 
     rsc = cmd->rsc_id ? g_hash_table_lookup(rsc_list, cmd->rsc_id) : NULL;
 
 #ifdef PCMK__TIME_USE_CGT
     if (rsc != NULL) {
         rclass = rsc->class;
 #if PCMK__ENABLE_SERVICE
         if (pcmk__str_eq(rclass, PCMK_RESOURCE_CLASS_SERVICE,
                          pcmk__str_casei)) {
             rclass = resources_find_service_class(rsc->type);
         }
 #endif
     }
 
     if (!pcmk__str_eq(rclass, PCMK_RESOURCE_CLASS_SYSTEMD, pcmk__str_casei)) {
         goto finalize;
     }
 
     if (pcmk__result_ok(&(cmd->result))
         && pcmk__strcase_any_of(cmd->action, PCMK_ACTION_START,
                                 PCMK_ACTION_STOP, NULL)) {
         /* Getting results for when a start or stop action completes is now
          * handled by watching for JobRemoved() signals from systemd and
          * reacting to them. So, we can bypass the rest of the code in this
          * function for those actions, and simply finalize cmd.
          *
          * @TODO When monitors are handled in the same way, this function
          * can either be drastically simplified or done away with entirely.
          */
         services__copy_result(action, &(cmd->result));
         goto finalize;
 
     } else if (cmd->result.execution_status == PCMK_EXEC_PENDING &&
                pcmk__str_any_of(cmd->action, PCMK_ACTION_MONITOR, PCMK_ACTION_STATUS, NULL) &&
                cmd->interval_ms == 0 &&
                cmd->real_action == NULL) {
         /* If the state is Pending at the time of probe, execute follow-up monitor. */
         goagain = true;
         cmd->real_action = cmd->action;
         cmd->action = pcmk__str_copy(PCMK_ACTION_MONITOR);
     } else if (cmd->real_action != NULL) {
         // This is follow-up monitor to check whether start/stop/probe(monitor) completed
         if (cmd->result.execution_status == PCMK_EXEC_PENDING) {
             goagain = true;
 
         } else if (pcmk__result_ok(&(cmd->result))
                    && pcmk__str_eq(cmd->real_action, PCMK_ACTION_STOP,
                                    pcmk__str_casei)) {
             goagain = true;
 
         } else {
             int time_sum = time_diff_ms(NULL, &(cmd->t_first_run));
             int timeout_left = cmd->timeout_orig - time_sum;
 
             crm_debug("%s systemd %s is now complete (elapsed=%dms, "
                       "remaining=%dms): %s (%d)",
                       cmd->rsc_id, cmd->real_action, time_sum, timeout_left,
                       crm_exit_str(cmd->result.exit_status),
                       cmd->result.exit_status);
             cmd_original_times(cmd);
 
             // Monitors may return "not running", but start/stop shouldn't
             if ((cmd->result.execution_status == PCMK_EXEC_DONE)
                 && (cmd->result.exit_status == PCMK_OCF_NOT_RUNNING)) {
 
                 if (pcmk__str_eq(cmd->real_action, PCMK_ACTION_START,
                                  pcmk__str_casei)) {
                     cmd->result.exit_status = PCMK_OCF_UNKNOWN_ERROR;
                 } else if (pcmk__str_eq(cmd->real_action, PCMK_ACTION_STOP,
                                         pcmk__str_casei)) {
                     cmd->result.exit_status = PCMK_OCF_OK;
                 }
             }
         }
     } else if (pcmk__str_any_of(cmd->action, PCMK_ACTION_MONITOR, PCMK_ACTION_STATUS, NULL)
                && (cmd->interval_ms > 0)) {
         /* For monitors, excluding follow-up monitors,                                  */
         /* if the pending state persists from the first notification until its timeout, */
         /* it will be treated as a timeout.                                             */
 
         if ((cmd->result.execution_status == PCMK_EXEC_PENDING) &&
             (cmd->last_notify_op_status == PCMK_EXEC_PENDING)) {
             int time_left = time(NULL) - (cmd->epoch_rcchange + (cmd->timeout_orig/1000));
 
             if (time_left >= 0) {
                 crm_notice("Giving up on %s %s (rc=%d): monitor pending timeout "
                            "(first pending notification=%s timeout=%ds)",
                            cmd->rsc_id, cmd->action, cmd->result.exit_status,
                            pcmk__trim(ctime(&cmd->epoch_rcchange)), cmd->timeout_orig);
                 pcmk__set_result(&(cmd->result), PCMK_OCF_UNKNOWN_ERROR,
                                  PCMK_EXEC_TIMEOUT,
                                  "Investigate reason for timeout, and adjust "
                                  "configured operation timeout if necessary");
                 cmd_original_times(cmd);
             }
         }
     }
 
     if (!goagain) {
         goto finalize;
     }
 
     time_sum = time_diff_ms(NULL, &(cmd->t_first_run));
     timeout_left = cmd->timeout_orig - time_sum;
     delay = cmd->timeout_orig / 10;
 
     if (delay >= timeout_left && timeout_left > 20) {
         delay = timeout_left/2;
     }
 
     delay = QB_MIN(2000, delay);
     if (delay < timeout_left) {
         cmd->start_delay = delay;
         cmd->timeout = timeout_left;
 
         if (pcmk__result_ok(&(cmd->result))) {
             crm_debug("%s %s may still be in progress: re-scheduling (elapsed=%dms, remaining=%dms, start_delay=%dms)",
                       cmd->rsc_id, cmd->real_action, time_sum, timeout_left, delay);
 
         } else if (cmd->result.execution_status == PCMK_EXEC_PENDING) {
             crm_info("%s %s is still in progress: re-scheduling (elapsed=%dms, remaining=%dms, start_delay=%dms)",
                      cmd->rsc_id, cmd->action, time_sum, timeout_left, delay);
 
         } else {
             crm_notice("%s %s failed: %s: Re-scheduling (remaining "
                        "timeout %s) " QB_XS
                        " exitstatus=%d elapsed=%dms start_delay=%dms)",
                        cmd->rsc_id, cmd->action,
                        crm_exit_str(cmd->result.exit_status),
                        pcmk__readable_interval(timeout_left),
                        cmd->result.exit_status, time_sum, delay);
         }
 
         cmd_reset(cmd);
         if (rsc) {
             rsc->active = NULL;
         }
         schedule_lrmd_cmd(rsc, cmd);
 
         /* Don't finalize cmd, we're not done with it yet */
         return;
 
     } else {
         crm_notice("Giving up on %s %s (rc=%d): timeout (elapsed=%dms, remaining=%dms)",
                    cmd->rsc_id,
                    (cmd->real_action? cmd->real_action : cmd->action),
                    cmd->result.exit_status, time_sum, timeout_left);
         pcmk__set_result(&(cmd->result), PCMK_OCF_UNKNOWN_ERROR,
                          PCMK_EXEC_TIMEOUT,
                          "Investigate reason for timeout, and adjust "
                          "configured operation timeout if necessary");
         cmd_original_times(cmd);
     }
 #endif
 
 finalize:
     pcmk__set_result_output(&(cmd->result), services__grab_stdout(action),
                             services__grab_stderr(action));
     cmd_finalize(cmd, rsc);
 }
 
 /*!
  * \internal
  * \brief Process the result of a fence device action (start, stop, or monitor)
  *
  * \param[in,out] cmd               Fence device action that completed
  * \param[in]     exit_status       Fencer API exit status for action
  * \param[in]     execution_status  Fencer API execution status for action
  * \param[in]     exit_reason       Human-friendly detail, if action failed
  */
 static void
 stonith_action_complete(lrmd_cmd_t *cmd, int exit_status,
                         enum pcmk_exec_status execution_status,
                         const char *exit_reason)
 {
     // This can be NULL if resource was removed before command completed
     lrmd_rsc_t *rsc = g_hash_table_lookup(rsc_list, cmd->rsc_id);
 
     // Simplify fencer exit status to uniform exit status
     if (exit_status != CRM_EX_OK) {
         exit_status = PCMK_OCF_UNKNOWN_ERROR;
     }
 
     if (cmd->result.execution_status == PCMK_EXEC_CANCELLED) {
         /* An in-flight fence action was cancelled. The execution status is
          * already correct, so don't overwrite it.
          */
         execution_status = PCMK_EXEC_CANCELLED;
 
     } else {
         /* Some execution status codes have specific meanings for the fencer
          * that executor clients may not expect, so map them to a simple error
          * status.
          */
         switch (execution_status) {
             case PCMK_EXEC_NOT_CONNECTED:
             case PCMK_EXEC_INVALID:
                 execution_status = PCMK_EXEC_ERROR;
                 break;
 
             case PCMK_EXEC_NO_FENCE_DEVICE:
                 /* This should be possible only for probes in practice, but
                  * interpret for all actions to be safe.
                  */
                 if (pcmk__str_eq(cmd->action, PCMK_ACTION_MONITOR,
                                  pcmk__str_none)) {
                     exit_status = PCMK_OCF_NOT_RUNNING;
 
                 } else if (pcmk__str_eq(cmd->action, PCMK_ACTION_STOP,
                                         pcmk__str_none)) {
                     exit_status = PCMK_OCF_OK;
 
                 } else {
                     exit_status = PCMK_OCF_NOT_INSTALLED;
                 }
                 execution_status = PCMK_EXEC_ERROR;
                 break;
 
             case PCMK_EXEC_NOT_SUPPORTED:
                 exit_status = PCMK_OCF_UNIMPLEMENT_FEATURE;
                 break;
 
             default:
                 break;
         }
     }
 
     pcmk__set_result(&cmd->result, exit_status, execution_status, exit_reason);
 
     // Certain successful actions change the known state of the resource
     if ((rsc != NULL) && pcmk__result_ok(&(cmd->result))) {
 
         if (pcmk__str_eq(cmd->action, PCMK_ACTION_START, pcmk__str_casei)) {
             pcmk__set_result(&rsc->fence_probe_result, CRM_EX_OK,
                              PCMK_EXEC_DONE, NULL); // "running"
 
         } else if (pcmk__str_eq(cmd->action, PCMK_ACTION_STOP,
                                 pcmk__str_casei)) {
             pcmk__set_result(&rsc->fence_probe_result, CRM_EX_ERROR,
                              PCMK_EXEC_NO_FENCE_DEVICE, NULL); // "not running"
         }
     }
 
     /* The recurring timer should not be running at this point in any case, but
      * as a failsafe, stop it if it is.
      */
     stop_recurring_timer(cmd);
 
     /* Reschedule this command if appropriate. If a recurring command is *not*
      * rescheduled, its status must be PCMK_EXEC_CANCELLED, otherwise it will
      * not be removed from recurring_ops by cmd_finalize().
      */
     if (rsc && (cmd->interval_ms > 0)
         && (cmd->result.execution_status != PCMK_EXEC_CANCELLED)) {
         start_recurring_timer(cmd);
     }
 
     cmd_finalize(cmd, rsc);
 }
 
 static void
 lrmd_stonith_callback(stonith_t * stonith, stonith_callback_data_t * data)
 {
     if ((data == NULL) || (data->userdata == NULL)) {
         crm_err("Ignoring fence action result: "
                 "Invalid callback arguments (bug?)");
     } else {
         stonith_action_complete((lrmd_cmd_t *) data->userdata,
                                 stonith__exit_status(data),
                                 stonith__execution_status(data),
                                 stonith__exit_reason(data));
     }
 }
 
 void
 stonith_connection_failed(void)
 {
     GHashTableIter iter;
     lrmd_rsc_t *rsc = NULL;
 
     crm_warn("Connection to fencer lost (any pending operations for "
              "fence devices will be considered failed)");
 
     g_hash_table_iter_init(&iter, rsc_list);
     while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &rsc)) {
         if (!pcmk__str_eq(rsc->class, PCMK_RESOURCE_CLASS_STONITH,
                           pcmk__str_none)) {
             continue;
         }
 
         /* If we registered this fence device, we don't know whether the
          * fencer still has the registration or not. Cause future probes to
          * return an error until the resource is stopped or started
          * successfully. This is especially important if the controller also
          * went away (possibly due to a cluster layer restart) and won't
          * receive our client notification of any monitors finalized below.
          */
         if (rsc->fence_probe_result.execution_status == PCMK_EXEC_DONE) {
             pcmk__set_result(&rsc->fence_probe_result, CRM_EX_ERROR,
                              PCMK_EXEC_NOT_CONNECTED,
                              "Lost connection to fencer");
         }
 
         // Consider any active, pending, or recurring operations as failed
 
         for (GList *op = rsc->recurring_ops; op != NULL; op = op->next) {
             lrmd_cmd_t *cmd = op->data;
 
             /* This won't free a recurring op but instead restart its timer.
              * If cmd is rsc->active, this will set rsc->active to NULL, so we
              * don't have to worry about finalizing it a second time below.
              */
             stonith_action_complete(cmd,
                                     CRM_EX_ERROR, PCMK_EXEC_NOT_CONNECTED,
                                     "Lost connection to fencer");
         }
 
         if (rsc->active != NULL) {
             rsc->pending_ops = g_list_prepend(rsc->pending_ops, rsc->active);
         }
         while (rsc->pending_ops != NULL) {
             // This will free the op and remove it from rsc->pending_ops
             stonith_action_complete((lrmd_cmd_t *) rsc->pending_ops->data,
                                     CRM_EX_ERROR, PCMK_EXEC_NOT_CONNECTED,
                                     "Lost connection to fencer");
         }
     }
 }
 
 /*!
  * \internal
  * \brief Execute a stonith resource "start" action
  *
  * Start a stonith resource by registering it with the fencer.
  * (Stonith agents don't have a start command.)
  *
  * \param[in,out] stonith_api  Connection to fencer
  * \param[in]     rsc          Stonith resource to start
  * \param[in]     cmd          Start command to execute
  *
  * \return pcmk_ok on success, -errno otherwise
  */
 static int
 execd_stonith_start(stonith_t *stonith_api, const lrmd_rsc_t *rsc,
                     const lrmd_cmd_t *cmd)
 {
     char *key = NULL;
     char *value = NULL;
     stonith_key_value_t *device_params = NULL;
     int rc = pcmk_ok;
 
     // Convert command parameters to stonith API key/values
     if (cmd->params) {
         GHashTableIter iter;
 
         g_hash_table_iter_init(&iter, cmd->params);
         while (g_hash_table_iter_next(&iter, (gpointer *) & key, (gpointer *) & value)) {
             device_params = stonith_key_value_add(device_params, key, value);
         }
     }
 
     /* The fencer will automatically register devices via CIB notifications
      * when the CIB changes, but to avoid a possible race condition between
      * the fencer receiving the notification and the executor requesting that
      * resource, the executor registers the device as well. The fencer knows how
      * to handle duplicate registrations.
      */
     rc = stonith_api->cmds->register_device(stonith_api, st_opt_sync_call,
                                             cmd->rsc_id, rsc->provider,
                                             rsc->type, device_params);
 
     stonith_key_value_freeall(device_params, 1, 1);
     return rc;
 }
 
 /*!
  * \internal
  * \brief Execute a stonith resource "stop" action
  *
  * Stop a stonith resource by unregistering it with the fencer.
  * (Stonith agents don't have a stop command.)
  *
  * \param[in,out] stonith_api  Connection to fencer
  * \param[in]     rsc          Stonith resource to stop
  *
  * \return pcmk_ok on success, -errno otherwise
  */
 static inline int
 execd_stonith_stop(stonith_t *stonith_api, const lrmd_rsc_t *rsc)
 {
     /* @TODO Failure would indicate a problem communicating with fencer;
      * perhaps we should try reconnecting and retrying a few times?
      */
     return stonith_api->cmds->remove_device(stonith_api, st_opt_sync_call,
                                             rsc->rsc_id);
 }
 
 /*!
  * \internal
  * \brief Initiate a stonith resource agent recurring "monitor" action
  *
  * \param[in,out] stonith_api  Connection to fencer
  * \param[in,out] rsc          Stonith resource to monitor
  * \param[in]     cmd          Monitor command being executed
  *
  * \return pcmk_ok if monitor was successfully initiated, -errno otherwise
  */
 static inline int
 execd_stonith_monitor(stonith_t *stonith_api, lrmd_rsc_t *rsc, lrmd_cmd_t *cmd)
 {
     int rc = stonith_api->cmds->monitor(stonith_api, 0, cmd->rsc_id,
                                         pcmk__timeout_ms2s(cmd->timeout));
 
     rc = stonith_api->cmds->register_callback(stonith_api, rc, 0, 0, cmd,
                                               "lrmd_stonith_callback",
                                               lrmd_stonith_callback);
     if (rc == TRUE) {
         rsc->active = cmd;
         rc = pcmk_ok;
     } else {
         rc = -pcmk_err_generic;
     }
     return rc;
 }
 
 static void
 execute_stonith_action(lrmd_rsc_t *rsc, lrmd_cmd_t *cmd)
 {
     int rc = 0;
     bool do_monitor = FALSE;
 
     stonith_t *stonith_api = get_stonith_connection();
 
     if (pcmk__str_eq(cmd->action, PCMK_ACTION_MONITOR, pcmk__str_casei)
         && (cmd->interval_ms == 0)) {
         // Probes don't require a fencer connection
         stonith_action_complete(cmd, rsc->fence_probe_result.exit_status,
                                 rsc->fence_probe_result.execution_status,
                                 rsc->fence_probe_result.exit_reason);
         return;
 
     } else if (stonith_api == NULL) {
         stonith_action_complete(cmd, PCMK_OCF_UNKNOWN_ERROR,
                                 PCMK_EXEC_NOT_CONNECTED,
                                 "No connection to fencer");
         return;
 
     } else if (pcmk__str_eq(cmd->action, PCMK_ACTION_START, pcmk__str_casei)) {
         rc = execd_stonith_start(stonith_api, rsc, cmd);
         if (rc == pcmk_ok) {
             do_monitor = TRUE;
         }
 
     } else if (pcmk__str_eq(cmd->action, PCMK_ACTION_STOP, pcmk__str_casei)) {
         rc = execd_stonith_stop(stonith_api, rsc);
 
     } else if (pcmk__str_eq(cmd->action, PCMK_ACTION_MONITOR,
                             pcmk__str_casei)) {
         do_monitor = TRUE;
 
     } else {
         stonith_action_complete(cmd, PCMK_OCF_UNIMPLEMENT_FEATURE,
                                 PCMK_EXEC_ERROR,
                                 "Invalid fence device action (bug?)");
         return;
     }
 
     if (do_monitor) {
         rc = execd_stonith_monitor(stonith_api, rsc, cmd);
         if (rc == pcmk_ok) {
             // Don't clean up yet, we will find out result of the monitor later
             return;
         }
     }
 
     stonith_action_complete(cmd,
                             ((rc == pcmk_ok)? CRM_EX_OK : CRM_EX_ERROR),
                             stonith__legacy2status(rc),
                             ((rc == -pcmk_err_generic)? NULL : pcmk_strerror(rc)));
 }
 
 static void
 execute_nonstonith_action(lrmd_rsc_t *rsc, lrmd_cmd_t *cmd)
 {
     svc_action_t *action = NULL;
     GHashTable *params_copy = NULL;
 
     pcmk__assert((rsc != NULL) && (cmd != NULL));
 
     crm_trace("Creating action, resource:%s action:%s class:%s provider:%s agent:%s",
               rsc->rsc_id, cmd->action, rsc->class, rsc->provider, rsc->type);
 
     params_copy = pcmk__str_table_dup(cmd->params);
 
     action = services__create_resource_action(rsc->rsc_id, rsc->class, rsc->provider,
                                      rsc->type,
                                      normalize_action_name(rsc, cmd->action),
                                      cmd->interval_ms, cmd->timeout,
                                      params_copy, cmd->service_flags);
 
     if (action == NULL) {
         pcmk__set_result(&(cmd->result), PCMK_OCF_UNKNOWN_ERROR,
                          PCMK_EXEC_ERROR, strerror(ENOMEM));
         cmd_finalize(cmd, rsc);
         return;
     }
 
     if (action->rc != PCMK_OCF_UNKNOWN) {
         services__copy_result(action, &(cmd->result));
         services_action_free(action);
         cmd_finalize(cmd, rsc);
         return;
     }
 
     action->cb_data = cmd;
 
     if (services_action_async(action, action_complete)) {
         /* The services library has taken responsibility for the action. It
          * could be pending, blocked, or merged into a duplicate recurring
          * action, in which case the action callback (action_complete())
          * will be called when the action completes, otherwise the callback has
          * already been called.
          *
          * action_complete() calls cmd_finalize() which can free cmd, so cmd
          * cannot be used here.
          */
     } else {
         /* This is a recurring action that is not being cancelled and could not
          * be initiated. It has been rescheduled, and the action callback
          * (action_complete()) has been called, which in this case has already
          * called cmd_finalize(), which in this case should only reset (not
          * free) cmd.
          */
         services__copy_result(action, &(cmd->result));
         services_action_free(action);
     }
 }
 
 static gboolean
 execute_resource_action(gpointer user_data)
 {
     lrmd_rsc_t *rsc = (lrmd_rsc_t *) user_data;
     lrmd_cmd_t *cmd = NULL;
 
     CRM_CHECK(rsc != NULL, return FALSE);
 
     if (rsc->active) {
         crm_trace("%s is still active", rsc->rsc_id);
         return TRUE;
     }
 
     if (rsc->pending_ops) {
         GList *first = rsc->pending_ops;
 
         cmd = first->data;
         if (cmd->delay_id) {
             crm_trace
                 ("Command %s %s was asked to run too early, waiting for start_delay timeout of %dms",
                  cmd->rsc_id, cmd->action, cmd->start_delay);
             return TRUE;
         }
         rsc->pending_ops = g_list_remove_link(rsc->pending_ops, first);
         g_list_free_1(first);
 
 #ifdef PCMK__TIME_USE_CGT
         get_current_time(&(cmd->t_run), &(cmd->t_first_run));
 #endif
         cmd->epoch_last_run = time(NULL);
     }
 
     if (!cmd) {
         crm_trace("Nothing further to do for %s", rsc->rsc_id);
         return TRUE;
     }
 
     rsc->active = cmd;          /* only one op at a time for a rsc */
     if (cmd->interval_ms) {
         rsc->recurring_ops = g_list_append(rsc->recurring_ops, cmd);
     }
 
     log_execute(cmd);
 
     if (pcmk__str_eq(rsc->class, PCMK_RESOURCE_CLASS_STONITH, pcmk__str_casei)) {
         execute_stonith_action(rsc, cmd);
     } else {
         execute_nonstonith_action(rsc, cmd);
     }
 
     return TRUE;
 }
 
 void
 free_rsc(gpointer data)
 {
     GList *gIter = NULL;
     lrmd_rsc_t *rsc = data;
     int is_stonith = pcmk__str_eq(rsc->class, PCMK_RESOURCE_CLASS_STONITH,
                                   pcmk__str_casei);
 
     gIter = rsc->pending_ops;
     while (gIter != NULL) {
         GList *next = gIter->next;
         lrmd_cmd_t *cmd = gIter->data;
 
         /* command was never executed */
         cmd->result.execution_status = PCMK_EXEC_CANCELLED;
         cmd_finalize(cmd, NULL);
 
         gIter = next;
     }
     /* frees list, but not list elements. */
     g_list_free(rsc->pending_ops);
 
     gIter = rsc->recurring_ops;
     while (gIter != NULL) {
         GList *next = gIter->next;
         lrmd_cmd_t *cmd = gIter->data;
 
         if (is_stonith) {
             cmd->result.execution_status = PCMK_EXEC_CANCELLED;
             /* If a stonith command is in-flight, just mark it as cancelled;
              * it is not safe to finalize/free the cmd until the stonith api
              * says it has either completed or timed out.
              */
             if (rsc->active != cmd) {
                 cmd_finalize(cmd, NULL);
             }
         } else {
             /* This command is already handed off to service library,
              * let service library cancel it and tell us via the callback
              * when it is cancelled. The rsc can be safely destroyed
              * even if we are waiting for the cancel result */
             services_action_cancel(rsc->rsc_id,
                                    normalize_action_name(rsc, cmd->action),
                                    cmd->interval_ms);
         }
 
         gIter = next;
     }
     /* frees list, but not list elements. */
     g_list_free(rsc->recurring_ops);
 
     free(rsc->rsc_id);
     free(rsc->class);
     free(rsc->provider);
     free(rsc->type);
     mainloop_destroy_trigger(rsc->work);
 
     free(rsc);
 }
 
 static int
 process_lrmd_signon(pcmk__client_t *client, xmlNode *request, int call_id,
                     xmlNode **reply)
 {
     int rc = pcmk_ok;
     time_t now = time(NULL);
     const char *protocol_version = pcmk__xe_get(request,
                                                 PCMK__XA_LRMD_PROTOCOL_VERSION);
     const char *start_state = pcmk__env_option(PCMK__ENV_NODE_START_STATE);
 
-    if (compare_version(protocol_version, LRMD_COMPATIBLE_PROTOCOL) < 0) {
+    if (pcmk__compare_versions(protocol_version,
+                               LRMD_COMPATIBLE_PROTOCOL) < 0) {
         crm_err("Cluster API version must be greater than or equal to %s, not %s",
                 LRMD_COMPATIBLE_PROTOCOL, protocol_version);
         rc = -EPROTO;
     }
 
     if (pcmk__xe_attr_is_true(request, PCMK__XA_LRMD_IS_IPC_PROVIDER)) {
 #ifdef PCMK__COMPILE_REMOTE
         if ((client->remote != NULL)
             && pcmk__is_set(client->flags,
                             pcmk__client_tls_handshake_complete)) {
             const char *op = pcmk__xe_get(request, PCMK__XA_LRMD_OP);
 
             // This is a remote connection from a cluster node's controller
             ipc_proxy_add_provider(client);
 
             /* @TODO Allowing multiple proxies makes no sense given that clients
              * have no way to choose between them. Maybe always use the most
              * recent one and switch any existing IPC connections to use it,
              * by iterating over ipc_clients here, and if client->id doesn't
              * match the client's userdata, replace the userdata with the new
              * ID. After the iteration, call lrmd_remote_client_destroy() on any
              * of the replaced values in ipc_providers.
              */
 
             /* If this was a register operation, also ask for new schema files but
              * only if it's supported by the protocol version.
              */
             if (pcmk__str_eq(op, CRM_OP_REGISTER, pcmk__str_none) &&
                 LRMD_SUPPORTS_SCHEMA_XFER(protocol_version)) {
                 remoted_request_cib_schema_files();
             }
         } else {
             rc = -EACCES;
         }
 #else
         rc = -EPROTONOSUPPORT;
 #endif
     }
 
     *reply = create_lrmd_reply(__func__, rc, call_id);
     pcmk__xe_set(*reply, PCMK__XA_LRMD_OP, CRM_OP_REGISTER);
     pcmk__xe_set(*reply, PCMK__XA_LRMD_CLIENTID, client->id);
     pcmk__xe_set(*reply, PCMK__XA_LRMD_PROTOCOL_VERSION, LRMD_PROTOCOL_VERSION);
     pcmk__xe_set_time(*reply, PCMK__XA_UPTIME, now - start_time);
 
     if (start_state) {
         pcmk__xe_set(*reply, PCMK__XA_NODE_START_STATE, start_state);
     }
 
     return rc;
 }
 
 static int
 process_lrmd_rsc_register(pcmk__client_t *client, uint32_t id, xmlNode *request)
 {
     int rc = pcmk_ok;
     lrmd_rsc_t *rsc = build_rsc_from_xml(request);
     lrmd_rsc_t *dup = g_hash_table_lookup(rsc_list, rsc->rsc_id);
 
     if (dup &&
         pcmk__str_eq(rsc->class, dup->class, pcmk__str_casei) &&
         pcmk__str_eq(rsc->provider, dup->provider, pcmk__str_casei) && pcmk__str_eq(rsc->type, dup->type, pcmk__str_casei)) {
 
         crm_notice("Ignoring duplicate registration of '%s'", rsc->rsc_id);
         free_rsc(rsc);
         return rc;
     }
 
     g_hash_table_replace(rsc_list, rsc->rsc_id, rsc);
     crm_info("Cached agent information for '%s'", rsc->rsc_id);
     return rc;
 }
 
 static xmlNode *
 process_lrmd_get_rsc_info(xmlNode *request, int call_id)
 {
     int rc = pcmk_ok;
     xmlNode *rsc_xml = pcmk__xpath_find_one(request->doc,
                                             "//" PCMK__XE_LRMD_RSC,
                                             LOG_ERR);
     const char *rsc_id = pcmk__xe_get(rsc_xml, PCMK__XA_LRMD_RSC_ID);
     xmlNode *reply = NULL;
     lrmd_rsc_t *rsc = NULL;
 
     if (rsc_id == NULL) {
         rc = -ENODEV;
     } else {
         rsc = g_hash_table_lookup(rsc_list, rsc_id);
         if (rsc == NULL) {
             crm_info("Agent information for '%s' not in cache", rsc_id);
             rc = -ENODEV;
         }
     }
 
     reply = create_lrmd_reply(__func__, rc, call_id);
     if (rsc) {
         pcmk__xe_set(reply, PCMK__XA_LRMD_RSC_ID, rsc->rsc_id);
         pcmk__xe_set(reply, PCMK__XA_LRMD_CLASS, rsc->class);
         pcmk__xe_set(reply, PCMK__XA_LRMD_PROVIDER, rsc->provider);
         pcmk__xe_set(reply, PCMK__XA_LRMD_TYPE, rsc->type);
     }
     return reply;
 }
 
 static int
 process_lrmd_rsc_unregister(pcmk__client_t *client, uint32_t id,
                             xmlNode *request)
 {
     int rc = pcmk_ok;
     lrmd_rsc_t *rsc = NULL;
     xmlNode *rsc_xml = pcmk__xpath_find_one(request->doc,
                                             "//" PCMK__XE_LRMD_RSC,
                                             LOG_ERR);
     const char *rsc_id = pcmk__xe_get(rsc_xml, PCMK__XA_LRMD_RSC_ID);
 
     if (!rsc_id) {
         return -ENODEV;
     }
 
     rsc = g_hash_table_lookup(rsc_list, rsc_id);
     if (rsc == NULL) {
         crm_info("Ignoring unregistration of resource '%s', which is not registered",
                  rsc_id);
         return pcmk_ok;
     }
 
     if (rsc->active) {
         /* let the caller know there are still active ops on this rsc to watch for */
         crm_trace("Operation (%p) still in progress for unregistered resource %s",
                   rsc->active, rsc_id);
         rc = -EINPROGRESS;
     }
 
     g_hash_table_remove(rsc_list, rsc_id);
 
     return rc;
 }
 
 static int
 process_lrmd_rsc_exec(pcmk__client_t *client, uint32_t id, xmlNode *request)
 {
     lrmd_rsc_t *rsc = NULL;
     lrmd_cmd_t *cmd = NULL;
     xmlNode *rsc_xml = pcmk__xpath_find_one(request->doc,
                                             "//" PCMK__XE_LRMD_RSC,
                                             LOG_ERR);
     const char *rsc_id = pcmk__xe_get(rsc_xml, PCMK__XA_LRMD_RSC_ID);
     int call_id;
 
     if (!rsc_id) {
         return -EINVAL;
     }
     if (!(rsc = g_hash_table_lookup(rsc_list, rsc_id))) {
         crm_info("Resource '%s' not found (%d active resources)",
                  rsc_id, g_hash_table_size(rsc_list));
         return -ENODEV;
     }
 
     cmd = create_lrmd_cmd(request, client);
     call_id = cmd->call_id;
 
     /* Don't reference cmd after handing it off to be scheduled.
      * The cmd could get merged and freed. */
     schedule_lrmd_cmd(rsc, cmd);
 
     return call_id;
 }
 
 static int
 cancel_op(const char *rsc_id, const char *action, guint interval_ms)
 {
     GList *gIter = NULL;
     lrmd_rsc_t *rsc = g_hash_table_lookup(rsc_list, rsc_id);
 
     /* How to cancel an action.
      * 1. Check pending ops list, if it hasn't been handed off
      *    to the service library or stonith recurring list remove
      *    it there and that will stop it.
      * 2. If it isn't in the pending ops list, then it's either a
      *    recurring op in the stonith recurring list, or the service
      *    library's recurring list.  Stop it there
      * 3. If not found in any lists, then this operation has either
      *    been executed already and is not a recurring operation, or
      *    never existed.
      */
     if (!rsc) {
         return -ENODEV;
     }
 
     for (gIter = rsc->pending_ops; gIter != NULL; gIter = gIter->next) {
         lrmd_cmd_t *cmd = gIter->data;
 
         if (action_matches(cmd, action, interval_ms)) {
             cmd->result.execution_status = PCMK_EXEC_CANCELLED;
             cmd_finalize(cmd, rsc);
             return pcmk_ok;
         }
     }
 
     if (pcmk__str_eq(rsc->class, PCMK_RESOURCE_CLASS_STONITH, pcmk__str_casei)) {
         /* The service library does not handle stonith operations.
          * We have to handle recurring stonith operations ourselves. */
         for (gIter = rsc->recurring_ops; gIter != NULL; gIter = gIter->next) {
             lrmd_cmd_t *cmd = gIter->data;
 
             if (action_matches(cmd, action, interval_ms)) {
                 cmd->result.execution_status = PCMK_EXEC_CANCELLED;
                 if (rsc->active != cmd) {
                     cmd_finalize(cmd, rsc);
                 }
                 return pcmk_ok;
             }
         }
     } else if (services_action_cancel(rsc_id,
                                       normalize_action_name(rsc, action),
                                       interval_ms) == TRUE) {
         /* The service library will tell the action_complete callback function
          * this action was cancelled, which will destroy the cmd and remove
          * it from the recurring_op list. Do not do that in this function
          * if the service library says it cancelled it. */
         return pcmk_ok;
     }
 
     return -EOPNOTSUPP;
 }
 
 static void
 cancel_all_recurring(lrmd_rsc_t * rsc, const char *client_id)
 {
     GList *cmd_list = NULL;
     GList *cmd_iter = NULL;
 
     /* Notice a copy of each list is created when concat is called.
      * This prevents odd behavior from occurring when the cmd_list
      * is iterated through later on.  It is possible the cancel_op
      * function may end up modifying the recurring_ops and pending_ops
      * lists.  If we did not copy those lists, our cmd_list iteration
      * could get messed up.*/
     if (rsc->recurring_ops) {
         cmd_list = g_list_concat(cmd_list, g_list_copy(rsc->recurring_ops));
     }
     if (rsc->pending_ops) {
         cmd_list = g_list_concat(cmd_list, g_list_copy(rsc->pending_ops));
     }
     if (!cmd_list) {
         return;
     }
 
     for (cmd_iter = cmd_list; cmd_iter; cmd_iter = cmd_iter->next) {
         lrmd_cmd_t *cmd = cmd_iter->data;
 
         if (cmd->interval_ms == 0) {
             continue;
         }
 
         if (client_id && !pcmk__str_eq(cmd->client_id, client_id, pcmk__str_casei)) {
             continue;
         }
 
         cancel_op(rsc->rsc_id, cmd->action, cmd->interval_ms);
     }
     /* frees only the copied list data, not the cmds */
     g_list_free(cmd_list);
 }
 
 static int
 process_lrmd_rsc_cancel(pcmk__client_t *client, uint32_t id, xmlNode *request)
 {
     xmlNode *rsc_xml = pcmk__xpath_find_one(request->doc,
                                             "//" PCMK__XE_LRMD_RSC,
                                             LOG_ERR);
     const char *rsc_id = pcmk__xe_get(rsc_xml, PCMK__XA_LRMD_RSC_ID);
     const char *action = pcmk__xe_get(rsc_xml, PCMK__XA_LRMD_RSC_ACTION);
     guint interval_ms = 0;
 
     pcmk__xe_get_guint(rsc_xml, PCMK__XA_LRMD_RSC_INTERVAL, &interval_ms);
 
     if (!rsc_id || !action) {
         return -EINVAL;
     }
 
     return cancel_op(rsc_id, action, interval_ms);
 }
 
 static void
 add_recurring_op_xml(xmlNode *reply, lrmd_rsc_t *rsc)
 {
     xmlNode *rsc_xml = pcmk__xe_create(reply, PCMK__XE_LRMD_RSC);
 
     pcmk__xe_set(rsc_xml, PCMK__XA_LRMD_RSC_ID, rsc->rsc_id);
     for (GList *item = rsc->recurring_ops; item != NULL; item = item->next) {
         lrmd_cmd_t *cmd = item->data;
         xmlNode *op_xml = pcmk__xe_create(rsc_xml, PCMK__XE_LRMD_RSC_OP);
 
         pcmk__xe_set(op_xml, PCMK__XA_LRMD_RSC_ACTION,
                      pcmk__s(cmd->real_action, cmd->action));
         pcmk__xe_set_guint(op_xml, PCMK__XA_LRMD_RSC_INTERVAL,
                            cmd->interval_ms);
         pcmk__xe_set_int(op_xml, PCMK__XA_LRMD_TIMEOUT, cmd->timeout_orig);
     }
 }
 
 static xmlNode *
 process_lrmd_get_recurring(xmlNode *request, int call_id)
 {
     int rc = pcmk_ok;
     const char *rsc_id = NULL;
     lrmd_rsc_t *rsc = NULL;
     xmlNode *reply = NULL;
     xmlNode *rsc_xml = NULL;
 
     // Resource ID is optional
     rsc_xml = pcmk__xe_first_child(request, PCMK__XE_LRMD_CALLDATA, NULL, NULL);
     if (rsc_xml) {
         rsc_xml = pcmk__xe_first_child(rsc_xml, PCMK__XE_LRMD_RSC, NULL, NULL);
     }
     if (rsc_xml) {
         rsc_id = pcmk__xe_get(rsc_xml, PCMK__XA_LRMD_RSC_ID);
     }
 
     // If resource ID is specified, resource must exist
     if (rsc_id != NULL) {
         rsc = g_hash_table_lookup(rsc_list, rsc_id);
         if (rsc == NULL) {
             crm_info("Resource '%s' not found (%d active resources)",
                      rsc_id, g_hash_table_size(rsc_list));
             rc = -ENODEV;
         }
     }
 
     reply = create_lrmd_reply(__func__, rc, call_id);
 
     // If resource ID is not specified, check all resources
     if (rsc_id == NULL) {
         GHashTableIter iter;
         char *key = NULL;
 
         g_hash_table_iter_init(&iter, rsc_list);
         while (g_hash_table_iter_next(&iter, (gpointer *) &key,
                                       (gpointer *) &rsc)) {
             add_recurring_op_xml(reply, rsc);
         }
     } else if (rsc) {
         add_recurring_op_xml(reply, rsc);
     }
     return reply;
 }
 
 void
 process_lrmd_message(pcmk__client_t *client, uint32_t id, xmlNode *request)
 {
     int rc = pcmk_ok;
     int call_id = 0;
     const char *op = pcmk__xe_get(request, PCMK__XA_LRMD_OP);
     int do_reply = 0;
     int do_notify = 0;
     xmlNode *reply = NULL;
 
     /* Certain IPC commands may be done only by privileged users (i.e. root or
      * hacluster), because they would otherwise provide a means of bypassing
      * ACLs.
      */
     bool allowed = pcmk__is_set(client->flags, pcmk__client_privileged);
 
     crm_trace("Processing %s operation from %s", op, client->id);
     pcmk__xe_get_int(request, PCMK__XA_LRMD_CALLID, &call_id);
 
     if (pcmk__str_eq(op, CRM_OP_IPC_FWD, pcmk__str_none)) {
 #ifdef PCMK__COMPILE_REMOTE
         if (allowed) {
             ipc_proxy_forward_client(client, request);
         } else {
             rc = -EACCES;
         }
 #else
         rc = -EPROTONOSUPPORT;
 #endif
         do_reply = 1;
     } else if (pcmk__str_eq(op, CRM_OP_REGISTER, pcmk__str_none)) {
         rc = process_lrmd_signon(client, request, call_id, &reply);
         do_reply = 1;
     } else if (pcmk__str_eq(op, LRMD_OP_RSC_REG, pcmk__str_none)) {
         if (allowed) {
             rc = process_lrmd_rsc_register(client, id, request);
             do_notify = 1;
         } else {
             rc = -EACCES;
         }
         do_reply = 1;
     } else if (pcmk__str_eq(op, LRMD_OP_RSC_INFO, pcmk__str_none)) {
         if (allowed) {
             reply = process_lrmd_get_rsc_info(request, call_id);
         } else {
             rc = -EACCES;
         }
         do_reply = 1;
     } else if (pcmk__str_eq(op, LRMD_OP_RSC_UNREG, pcmk__str_none)) {
         if (allowed) {
             rc = process_lrmd_rsc_unregister(client, id, request);
             /* don't notify anyone about failed un-registers */
             if (rc == pcmk_ok || rc == -EINPROGRESS) {
                 do_notify = 1;
             }
         } else {
             rc = -EACCES;
         }
         do_reply = 1;
     } else if (pcmk__str_eq(op, LRMD_OP_RSC_EXEC, pcmk__str_none)) {
         if (allowed) {
             rc = process_lrmd_rsc_exec(client, id, request);
         } else {
             rc = -EACCES;
         }
         do_reply = 1;
     } else if (pcmk__str_eq(op, LRMD_OP_RSC_CANCEL, pcmk__str_none)) {
         if (allowed) {
             rc = process_lrmd_rsc_cancel(client, id, request);
         } else {
             rc = -EACCES;
         }
         do_reply = 1;
     } else if (pcmk__str_eq(op, LRMD_OP_POKE, pcmk__str_none)) {
         do_notify = 1;
         do_reply = 1;
     } else if (pcmk__str_eq(op, LRMD_OP_CHECK, pcmk__str_none)) {
         if (allowed) {
             xmlNode *wrapper = pcmk__xe_first_child(request,
                                                     PCMK__XE_LRMD_CALLDATA,
                                                     NULL, NULL);
             xmlNode *data = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
 
             const char *timeout = NULL;
 
             CRM_LOG_ASSERT(data != NULL);
             timeout = pcmk__xe_get(data, PCMK__XA_LRMD_WATCHDOG);
             pcmk__valid_stonith_watchdog_timeout(timeout);
         } else {
             rc = -EACCES;
         }
     } else if (pcmk__str_eq(op, LRMD_OP_ALERT_EXEC, pcmk__str_none)) {
         if (allowed) {
             rc = process_lrmd_alert_exec(client, id, request);
         } else {
             rc = -EACCES;
         }
         do_reply = 1;
     } else if (pcmk__str_eq(op, LRMD_OP_GET_RECURRING, pcmk__str_none)) {
         if (allowed) {
             reply = process_lrmd_get_recurring(request, call_id);
         } else {
             rc = -EACCES;
         }
         do_reply = 1;
     } else {
         rc = -EOPNOTSUPP;
         do_reply = 1;
         crm_err("Unknown IPC request '%s' from client %s",
                 op, pcmk__client_name(client));
     }
 
     if (rc == -EACCES) {
         crm_warn("Rejecting IPC request '%s' from unprivileged client %s",
                  op, pcmk__client_name(client));
     }
 
     crm_debug("Processed %s operation from %s: rc=%d, reply=%d, notify=%d",
               op, client->id, rc, do_reply, do_notify);
 
     if (do_reply) {
         int send_rc = pcmk_rc_ok;
 
         if (reply == NULL) {
             reply = create_lrmd_reply(__func__, rc, call_id);
         }
         send_rc = lrmd_server_send_reply(client, id, reply);
         pcmk__xml_free(reply);
         if (send_rc != pcmk_rc_ok) {
             crm_warn("Reply to client %s failed: %s " QB_XS " rc=%d",
                      pcmk__client_name(client), pcmk_rc_str(send_rc), send_rc);
         }
     }
 
     if (do_notify) {
         send_generic_notify(rc, request);
     }
 }
diff --git a/include/crm/cib.h b/include/crm/cib.h
index 8fcc257d08..e2c44764a0 100644
--- a/include/crm/cib.h
+++ b/include/crm/cib.h
@@ -1,66 +1,66 @@
 /*
- * Copyright 2004-2024 the Pacemaker project contributors
+ * Copyright 2004-2025 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #ifndef PCMK__CRM_CIB__H
 #  define PCMK__CRM_CIB__H
 
 #  include <glib.h>             // gboolean
 #  include <crm/common/ipc.h>
 #  include <crm/common/xml.h>
 #  include <crm/cib/cib_types.h>
 #  include <crm/cib/util.h>
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 /**
  * \file
  * \brief Cluster Configuration
  * \ingroup cib
  */
 
-// Use compare_version() for doing comparisons
+// Use pcmk__compare_versions() for doing comparisons
 #  define CIB_FEATURE_SET "2.0"
 
 /* Core functions */
 
 // NOTE: sbd (as of at least 1.5.2) uses this
 cib_t *cib_new(void);
 
 cib_t *cib_native_new(void);
 cib_t *cib_file_new(const char *filename);
 cib_t *cib_remote_new(const char *server, const char *user, const char *passwd, int port,
                       gboolean encrypted);
 
 cib_t *cib_new_no_shadow(void);
 char *get_shadow_file(const char *name);
 cib_t *cib_shadow_new(const char *name);
 
 void cib_free_notify(cib_t *cib);
 void cib_free_callbacks(cib_t *cib);
 
 // NOTE: sbd (as of at least 1.5.2) uses this
 void cib_delete(cib_t * cib);
 
 void cib_dump_pending_callbacks(void);
 int num_cib_op_callbacks(void);
 void remove_cib_op_callback(int call_id, gboolean all_callbacks);
 
 #define CIB_LIBRARY "libcib.so.54"
 
 #ifdef __cplusplus
 }
 #endif
 
 #if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1)
 #include <crm/cib_compat.h>
 #endif
 
 #endif
diff --git a/include/crm/common/internal.h b/include/crm/common/internal.h
index 09e5e3a091..0e56cb61ad 100644
--- a/include/crm/common/internal.h
+++ b/include/crm/common/internal.h
@@ -1,408 +1,409 @@
 /*
  * Copyright 2015-2025 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #ifndef PCMK__CRM_COMMON_INTERNAL__H
 #define PCMK__CRM_COMMON_INTERNAL__H
 
 #include <pwd.h>                // struct passwd
 #include <unistd.h>             // getpid()
 #include <stdbool.h>            // bool
 #include <stdint.h>             // uint8_t, uint64_t
 #include <sys/types.h>          // pid_t, uid_t, gid_t
 #include <inttypes.h>           // PRIu64
 
 #include <glib.h>               // guint, GList, GHashTable
 #include <libxml/tree.h>        // xmlNode
 
 #include <crm/common/logging.h>  // do_crm_log_unlikely(), etc.
 #include <crm/common/mainloop.h> // mainloop_io_t, struct ipc_client_callbacks
 #include <crm/common/actions_internal.h>
 #include <crm/common/digest_internal.h>
 #include <crm/common/health_internal.h>
 #include <crm/common/io_internal.h>
 #include <crm/common/iso8601_internal.h>
 #include <crm/common/results_internal.h>
 #include <crm/common/messages_internal.h>
 #include <crm/common/nvpair_internal.h>
 #include <crm/common/scores_internal.h>
 #include <crm/common/strings_internal.h>    // pcmk__assert_asprintf()
 #include <crm/common/acl_internal.h>
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 /* This says whether the current application is a Pacemaker daemon or not,
  * and is used to change default logging settings such as whether to log to
  * stderr, etc., as well as a few other details such as whether blackbox signal
  * handling is enabled.
  *
  * It is set when logging is initialized, and does not need to be set directly.
  */
 extern bool pcmk__is_daemon;
 
 // Number of elements in a statically defined array
 #define PCMK__NELEM(a) ((int) (sizeof(a)/sizeof(a[0])) )
 
 #if PCMK__ENABLE_CIBSECRETS
 /* internal CIB utilities (from cib_secrets.c) */
 
 int pcmk__substitute_secrets(const char *rsc_id, GHashTable *params);
 #endif
 
 
 /* internal main loop utilities (from mainloop.c) */
 
 int pcmk__add_mainloop_ipc(crm_ipc_t *ipc, int priority, void *userdata,
                            const struct ipc_client_callbacks *callbacks,
                            mainloop_io_t **source);
 guint pcmk__mainloop_timer_get_period(const mainloop_timer_t *timer);
 
 
 /* internal name/value utilities (from nvpair.c) */
 
 int pcmk__scan_nvpair(const gchar *input, gchar **name, gchar **value);
 char *pcmk__format_nvpair(const char *name, const char *value,
                           const char *units);
 
 /* internal procfs utilities (from procfs.c) */
 
 pid_t pcmk__procfs_pid_of(const char *name);
 unsigned int pcmk__procfs_num_cores(void);
 int pcmk__procfs_pid2path(pid_t pid, char path[], size_t path_size);
 bool pcmk__procfs_has_pids(void);
 DIR *pcmk__procfs_fd_dir(void);
 void pcmk__sysrq_trigger(char t);
 bool pcmk__throttle_cib_load(const char *server, float *load);
 bool pcmk__throttle_load_avg(float *load);
 
 /* internal functions related to process IDs (from pid.c) */
 
 /*!
  * \internal
  * \brief Check whether process exists (by PID and optionally executable path)
  *
  * \param[in] pid     PID of process to check
  * \param[in] daemon  If not NULL, path component to match with procfs entry
  *
  * \return Standard Pacemaker return code
  * \note Particular return codes of interest include pcmk_rc_ok for alive,
  *       ESRCH for process is not alive (verified by kill and/or executable path
  *       match), EACCES for caller unable or not allowed to check. A result of
  *       "alive" is less reliable when \p daemon is not provided or procfs is
  *       not available, since there is no guarantee that the PID has not been
  *       recycled for another process.
  * \note This function cannot be used to verify \e authenticity of the process.
  */
 int pcmk__pid_active(pid_t pid, const char *daemon);
 
 int pcmk__read_pidfile(const char *filename, pid_t *pid);
 int pcmk__pidfile_matches(const char *filename, pid_t expected_pid,
                           const char *expected_name, pid_t *pid);
 int pcmk__lock_pidfile(const char *filename, const char *name);
 
 
 // bitwise arithmetic utilities
 
 /*!
  * \internal
  * \brief Set specified flags in a flag group
  *
  * \param[in] function    Function name of caller
  * \param[in] line        Line number of caller
  * \param[in] log_level   Log a message at this level
  * \param[in] flag_type   Label describing this flag group (for logging)
  * \param[in] target      Name of object whose flags these are (for logging)
  * \param[in] flag_group  Flag group being manipulated
  * \param[in] flags       Which flags in the group should be set
  * \param[in] flags_str   Readable equivalent of \p flags (for logging)
  *
  * \return Possibly modified flag group
  */
 static inline uint64_t
 pcmk__set_flags_as(const char *function, int line, uint8_t log_level,
                    const char *flag_type, const char *target,
                    uint64_t flag_group, uint64_t flags, const char *flags_str)
 {
     uint64_t result = flag_group | flags;
 
     if (result != flag_group) {
         do_crm_log_unlikely(log_level,
                             "%s flags %#.8" PRIx64 " (%s) for %s set by %s:%d",
                             pcmk__s(flag_type, "Group of"), flags,
                             pcmk__s(flags_str, "flags"),
                             pcmk__s(target, "target"), function, line);
     }
     return result;
 }
 
 /*!
  * \internal
  * \brief Clear specified flags in a flag group
  *
  * \param[in] function    Function name of caller
  * \param[in] line        Line number of caller
  * \param[in] log_level   Log a message at this level
  * \param[in] flag_type   Label describing this flag group (for logging)
  * \param[in] target      Name of object whose flags these are (for logging)
  * \param[in] flag_group  Flag group being manipulated
  * \param[in] flags       Which flags in the group should be cleared
  * \param[in] flags_str   Readable equivalent of \p flags (for logging)
  *
  * \return Possibly modified flag group
  */
 static inline uint64_t
 pcmk__clear_flags_as(const char *function, int line, uint8_t log_level,
                      const char *flag_type, const char *target,
                      uint64_t flag_group, uint64_t flags, const char *flags_str)
 {
     uint64_t result = flag_group & ~flags;
 
     if (result != flag_group) {
         do_crm_log_unlikely(log_level,
                             "%s flags %#.8" PRIx64
                             " (%s) for %s cleared by %s:%d",
                             pcmk__s(flag_type, "Group of"), flags,
                             pcmk__s(flags_str, "flags"),
                             pcmk__s(target, "target"), function, line);
     }
     return result;
 }
 
 /*!
  * \internal
  * \brief Check whether any of specified flags are set in a flag group
  *
  * \param[in] flag_group      Flag group to check whether \p flags_to_check are
  *                            set
  * \param[in] flags_to_check  Flags to check whether set in \p flag_group
  *
  * \retval \c true   if \p flags_to_check is nonzero and any of its flags are
  *                   set in \p flag_group
  * \retval \c false  otherwise
  */
 static inline bool
 pcmk__any_flags_set(uint64_t flag_group, uint64_t flags_to_check)
 {
     return (flag_group & flags_to_check) != 0;
 }
 
 /*!
  * \internal
  * \brief Check whether all of specified flags are set in a flag group
  *
  * \param[in] flag_group      Flag group to check whether \p flags_to_check are
  *                            set
  * \param[in] flags_to_check  Flags to check whether set in \p flag_group
  *
  * \retval \c true   if all flags in \p flags_to_check are set in \p flag_group
  *                   or if \p flags_to_check is 0
  * \retval \c false  otherwise
  */
 static inline bool
 pcmk__all_flags_set(uint64_t flag_group, uint64_t flags_to_check)
 {
     return (flag_group & flags_to_check) == flags_to_check;
 }
 
 /*!
  * \internal
  * \brief Convenience alias for \c pcmk__all_flags_set(), to check single flag
  *
  * This is truly identical to \c pcmk__all_flags_set() but allows a call that's
  * shorter and semantically clearer for checking a single flag.
  *
  * \param[in] flag_group  Flag group (check whether \p flag is set in this)
  * \param[in] flag        Flag (check whether this is set in \p flag_group)
  *
  * \retval \c true   if \p flag is set in \p flag_group or if \p flag is 0
  * \retval \c false  otherwise
  */
 static inline bool
 pcmk__is_set(uint64_t flag_group, uint64_t flag)
 {
     return pcmk__all_flags_set(flag_group, flag);
 }
 
 /*!
  * \internal
  * \brief Get readable string for whether specified flags are set
  *
  * \param[in] flag_group    Group of flags to check
  * \param[in] flags         Which flags in \p flag_group should be checked
  *
  * \return "true" if all \p flags are set in \p flag_group, otherwise "false"
  */
 static inline const char *
 pcmk__flag_text(uint64_t flag_group, uint64_t flags)
 {
     return pcmk__btoa(pcmk__all_flags_set(flag_group, flags));
 }
 
 
 // miscellaneous utilities (from utils.c)
 
+int pcmk__compare_versions(const char *version1, const char *version2);
 int pcmk__daemon_user(uid_t *uid, gid_t *gid);
 void pcmk__daemonize(const char *name, const char *pidfile);
 char *pcmk__generate_uuid(void);
 int pcmk__lookup_user(const char *name, uid_t *uid, gid_t *gid);
 void pcmk__panic(const char *reason);
 pid_t pcmk__locate_sbd(void);
 void pcmk__sleep_ms(unsigned int ms);
 guint pcmk__create_timer(guint interval_ms, GSourceFunc fn, gpointer data);
 guint pcmk__timeout_ms2s(guint timeout_ms);
 
 extern int pcmk__score_red;
 extern int pcmk__score_green;
 extern int pcmk__score_yellow;
 
 /*!
  * \internal
  * \brief Allocate new zero-initialized memory, asserting on failure
  *
  * \param[in] file      File where \p function is located
  * \param[in] function  Calling function
  * \param[in] line      Line within \p file
  * \param[in] nmemb     Number of elements to allocate memory for
  * \param[in] size      Size of each element
  *
  * \return Newly allocated memory of of size <tt>nmemb * size</tt> (guaranteed
  *         not to be \c NULL)
  *
  * \note The caller is responsible for freeing the return value using \c free().
  */
 static inline void *
 pcmk__assert_alloc_as(const char *file, const char *function, uint32_t line,
                       size_t nmemb, size_t size)
 {
     void *ptr = calloc(nmemb, size);
 
     if (ptr == NULL) {
         crm_abort(file, function, line, "Out of memory", FALSE, TRUE);
         crm_exit(CRM_EX_OSERR);
     }
     return ptr;
 }
 
 /*!
  * \internal
  * \brief Allocate new zero-initialized memory, asserting on failure
  *
  * \param[in] nmemb  Number of elements to allocate memory for
  * \param[in] size   Size of each element
  *
  * \return Newly allocated memory of of size <tt>nmemb * size</tt> (guaranteed
  *         not to be \c NULL)
  *
  * \note The caller is responsible for freeing the return value using \c free().
  */
 #define pcmk__assert_alloc(nmemb, size) \
     pcmk__assert_alloc_as(__FILE__, __func__, __LINE__, nmemb, size)
 
 /*!
  * \internal
  * \brief Resize a dynamically allocated memory block
  *
  * \param[in] ptr   Memory block to resize (or NULL to allocate new memory)
  * \param[in] size  New size of memory block in bytes (must be > 0)
  *
  * \return Pointer to resized memory block
  *
  * \note This asserts on error, so the result is guaranteed to be non-NULL
  *       (which is the main advantage of this over directly using realloc()).
  */
 static inline void *
 pcmk__realloc(void *ptr, size_t size)
 {
     void *new_ptr;
 
     // realloc(p, 0) can replace free(p) but this wrapper can't
     pcmk__assert(size > 0);
 
     new_ptr = realloc(ptr, size);
     if (new_ptr == NULL) {
         free(ptr);
         abort();
     }
     return new_ptr;
 }
 
 static inline char *
 pcmk__getpid_s(void)
 {
     return pcmk__assert_asprintf("%lu", (unsigned long) getpid());
 }
 
 // More efficient than g_list_length(list) == 1
 static inline bool
 pcmk__list_of_1(GList *list)
 {
     return list && (list->next == NULL);
 }
 
 // More efficient than g_list_length(list) > 1
 static inline bool
 pcmk__list_of_multiple(GList *list)
 {
     return list && (list->next != NULL);
 }
 
 /* convenience functions for failure-related node attributes */
 
 #define PCMK__FAIL_COUNT_PREFIX   "fail-count"
 #define PCMK__LAST_FAILURE_PREFIX "last-failure"
 
 /*!
  * \internal
  * \brief Generate a failure-related node attribute name for a resource
  *
  * \param[in] prefix       Start of attribute name
  * \param[in] rsc_id       Resource name
  * \param[in] op           Operation name
  * \param[in] interval_ms  Operation interval
  *
  * \return Newly allocated string with attribute name
  *
  * \note Failure attributes are named like PREFIX-RSC#OP_INTERVAL (for example,
  *       "fail-count-myrsc#monitor_30000"). The '#' is used because it is not
  *       a valid character in a resource ID, to reliably distinguish where the
  *       operation name begins. The '_' is used simply to be more comparable to
  *       action labels like "myrsc_monitor_30000".
  */
 static inline char *
 pcmk__fail_attr_name(const char *prefix, const char *rsc_id, const char *op,
                    guint interval_ms)
 {
     CRM_CHECK(prefix && rsc_id && op, return NULL);
     return pcmk__assert_asprintf("%s-%s#%s_%u", prefix, rsc_id, op,
                                  interval_ms);
 }
 
 static inline char *
 pcmk__failcount_name(const char *rsc_id, const char *op, guint interval_ms)
 {
     return pcmk__fail_attr_name(PCMK__FAIL_COUNT_PREFIX, rsc_id, op,
                                 interval_ms);
 }
 
 static inline char *
 pcmk__lastfailure_name(const char *rsc_id, const char *op, guint interval_ms)
 {
     return pcmk__fail_attr_name(PCMK__LAST_FAILURE_PREFIX, rsc_id, op,
                                 interval_ms);
 }
 
 // internal resource agent functions (from agents.c)
 int pcmk__effective_rc(int rc);
 
 #ifdef __cplusplus
 }
 #endif
 
 #endif // PCMK__CRM_COMMON_INTERNAL__H
diff --git a/include/crm/lrmd.h b/include/crm/lrmd.h
index 4c1f8eed1e..5d666fd022 100644
--- a/include/crm/lrmd.h
+++ b/include/crm/lrmd.h
@@ -1,504 +1,506 @@
 /*
- * Copyright 2012-2024 the Pacemaker project contributors
+ * Copyright 2012-2025 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #ifndef PCMK__CRM_LRMD__H
 #  define PCMK__CRM_LRMD__H
 
 #include <stdbool.h>      // bool
 #include <glib.h>         // guint, GList
 #include <crm_config.h>
 #include <crm/lrmd_events.h>
 #include <crm/services.h>
 
+#include <crm/common/internal.h>    // pcmk__compare_versions()
+
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 /**
  * \file
  * \brief Resource agent executor
  * \ingroup lrmd
  */
 
 typedef struct lrmd_s lrmd_t;
 typedef struct lrmd_key_value_s {
     char *key;
     char *value;
     struct lrmd_key_value_s *next;
 } lrmd_key_value_t;
 
 /* The major version should be bumped every time there is an incompatible
  * change that prevents older clients from connecting to this version of
  * the server.  The minor version indicates feature support.
  *
  * Protocol  Pacemaker  Significant changes
  * --------  ---------  -------------------
  *   1.2       2.1.8    PCMK__CIB_REQUEST_SCHEMAS
  */
 #define LRMD_PROTOCOL_VERSION "1.2"
 
-#define LRMD_SUPPORTS_SCHEMA_XFER(x) (compare_version((x), "1.2") >= 0)
+#define LRMD_SUPPORTS_SCHEMA_XFER(x) (pcmk__compare_versions((x), "1.2") >= 0)
 
 /* The major protocol version the client and server both need to support for
  * the connection to be successful.  This should only ever be the major
  * version - not a complete version number.
  */
 #define LRMD_COMPATIBLE_PROTOCOL "1"
 
 /* *INDENT-OFF* */
 #define DEFAULT_REMOTE_KEY_LOCATION PACEMAKER_CONFIG_DIR "/authkey"
 #define DEFAULT_REMOTE_PORT 3121
 #define DEFAULT_REMOTE_USERNAME "lrmd"
 
 #define LRMD_OP_RSC_REG           "lrmd_rsc_register"
 #define LRMD_OP_RSC_EXEC          "lrmd_rsc_exec"
 #define LRMD_OP_RSC_CANCEL        "lrmd_rsc_cancel"
 #define LRMD_OP_RSC_UNREG         "lrmd_rsc_unregister"
 #define LRMD_OP_RSC_INFO          "lrmd_rsc_info"
 #define LRMD_OP_RSC_METADATA      "lrmd_rsc_metadata"
 #define LRMD_OP_POKE              "lrmd_rsc_poke"
 #define LRMD_OP_NEW_CLIENT        "lrmd_rsc_new_client"
 #define LRMD_OP_CHECK             "lrmd_check"
 #define LRMD_OP_ALERT_EXEC        "lrmd_alert_exec"
 #define LRMD_OP_GET_RECURRING     "lrmd_get_recurring"
 
 #define LRMD_IPC_OP_NEW           "new"
 #define LRMD_IPC_OP_DESTROY       "destroy"
 #define LRMD_IPC_OP_EVENT         "event"
 #define LRMD_IPC_OP_REQUEST       "request"
 #define LRMD_IPC_OP_RESPONSE      "response"
 #define LRMD_IPC_OP_SHUTDOWN_REQ  "shutdown_req"
 #define LRMD_IPC_OP_SHUTDOWN_ACK  "shutdown_ack"
 #define LRMD_IPC_OP_SHUTDOWN_NACK "shutdown_nack"
 /* *INDENT-ON* */
 
 /*!
  * \brief Create a new connection to the local executor
  */
 lrmd_t *lrmd_api_new(void);
 
 /*!
  * \brief Create a new TLS connection to a remote executor
  *
  * \param[in] nodename  Name of remote node identified with this connection
  * \param[in] server    Hostname to connect to
  * \param[in] port      Port number to connect to (or 0 to use default)
  *
  * \return Newly created executor connection object
  * \note If only one of \p nodename and \p server is non-NULL, it will be used
  *       for both purposes. If both are NULL, a local IPC connection will be
  *       created instead.
  */
 lrmd_t *lrmd_remote_api_new(const char *nodename, const char *server, int port);
 
 /*!
  * \brief Use after lrmd_poll returns 1 to read and dispatch a message
  *
  * \param[in,out] lrmd  Executor connection object
  *
  * \return TRUE if connection is still up, FALSE if disconnected
  */
 bool lrmd_dispatch(lrmd_t *lrmd);
 
 /*!
  * \brief Check whether a message is available on an executor connection
  *
  * \param[in,out] lrmd     Executor connection object to check
  * \param[in]     timeout  Currently ignored
  *
  * \retval 1               Message is ready
  * \retval 0               Timeout occurred
  * \retval negative errno  Error occurred
  *
  * \note This is intended for callers that do not use a main loop.
  */
 int lrmd_poll(lrmd_t *lrmd, int timeout);
 
 /*!
  * \brief Destroy executor connection object
  *
  * \param[in,out] lrmd     Executor connection object to destroy
  */
 void lrmd_api_delete(lrmd_t *lrmd);
 
 lrmd_key_value_t *lrmd_key_value_add(lrmd_key_value_t * kvp, const char *key, const char *value);
 
 enum lrmd_call_options {
     lrmd_opt_none                   = 0,
 
     /*!
      * Drop recurring operations initiated by a client when the client
      * disconnects. This option is only valid when registering a resource. When
      * used with a connection to a remote executor, recurring operations will be
      * dropped once all remote connections disconnect.
      */
     lrmd_opt_drop_recurring         = (1 << 0),
 
     //! Notify only the client that made the request (rather than all clients)
     lrmd_opt_notify_orig_only       = (1 << 1),
 
     //! Send notifications for recurring operations only when the result changes
     lrmd_opt_notify_changes_only    = (1 << 2),
 };
 
 typedef struct lrmd_rsc_info_s {
     char *id;
     char *type;
     char *standard;
     char *provider;
 } lrmd_rsc_info_t;
 
 typedef struct lrmd_op_info_s {
     char *rsc_id;
     char *action;
     char *interval_ms_s;
     char *timeout_ms_s;
 } lrmd_op_info_t;
 
 lrmd_rsc_info_t *lrmd_new_rsc_info(const char *rsc_id, const char *standard,
                                    const char *provider, const char *type);
 lrmd_rsc_info_t *lrmd_copy_rsc_info(lrmd_rsc_info_t * rsc_info);
 void lrmd_free_rsc_info(lrmd_rsc_info_t * rsc_info);
 void lrmd_free_op_info(lrmd_op_info_t *op_info);
 
 typedef void (*lrmd_event_callback) (lrmd_event_data_t * event);
 
 typedef struct lrmd_list_s {
     const char *val;
     struct lrmd_list_s *next;
 } lrmd_list_t;
 
 void lrmd_list_freeall(lrmd_list_t * head);
 void lrmd_key_value_freeall(lrmd_key_value_t * head);
 
 typedef struct lrmd_api_operations_s {
     /*!
      * \brief Connect to an executor
      *
      * \param[in,out] lrmd         Executor connection object
      * \param[in]     client_name  Arbitrary identifier to pass to server
      * \param[out]    fd           If not NULL, where to store file descriptor
      *                             for connection's socket
      *
      * \return Legacy Pacemaker return code
      */
     int (*connect) (lrmd_t *lrmd, const char *client_name, int *fd);
 
     /*!
      * \brief Initiate an executor connection without blocking
      *
      * \param[in,out] lrmd         Executor connection object
      * \param[in]     client_name  Arbitrary identifier to pass to server
      * \param[in]     timeout      Error if not connected within this time
      *                             (milliseconds)
      *
      * \return Legacy Pacemaker return code (if pcmk_ok, the event callback will
      *         be called later with the result)
      * \note This function requires a mainloop.
      */
     int (*connect_async) (lrmd_t *lrmd, const char *client_name,
                           int timeout /*ms */ );
 
     /*!
      * \brief Check whether connection to executor daemon is (still) active
      *
      * \param[in,out] lrmd  Executor connection object to check
      *
      * \return 1 if the executor connection is active, 0 otherwise
      */
     int (*is_connected) (lrmd_t *lrmd);
 
     /*!
      * \brief Poke executor connection to verify it is still active
      *
      * \param[in,out] lrmd  Executor connection object to check
      *
      * \return Legacy Pacemaker return code (if pcmk_ok, the event callback will
      *         be called later with the result)
      * \note The response comes in the form of a poke event to the callback.
      *
      */
     int (*poke_connection) (lrmd_t *lrmd);
 
     /*!
      * \brief Disconnect from the executor.
      *
      * \param[in,out] lrmd  Executor connection object to disconnect
      *
      * \return Legacy Pacemaker return code
      */
     int (*disconnect) (lrmd_t *lrmd);
 
     /*!
      * \brief Register a resource with the executor
      *
      * \param[in,out] lrmd      Executor connection object
      * \param[in]     rsc_id    ID of resource to register
      * \param[in]     standard  Resource's resource agent standard
      * \param[in]     provider  Resource's resource agent provider (or NULL)
      * \param[in]     agent     Resource's resource agent name
      * \param[in]     options   Group of enum lrmd_call_options flags
      *
      * \note Synchronous, guaranteed to occur in daemon before function returns.
      *
      * \return Legacy Pacemaker return code
      */
     int (*register_rsc) (lrmd_t *lrmd, const char *rsc_id, const char *standard,
                          const char *provider, const char *agent,
                          enum lrmd_call_options options);
 
     /*!
      * \brief Retrieve a resource's registration information
      *
      * \param[in,out] lrmd      Executor connection object
      * \param[in]     rsc_id    ID of resource to check
      * \param[in]     options   Group of enum lrmd_call_options flags
      *
      * \return Resource information on success, otherwise NULL
      */
     lrmd_rsc_info_t *(*get_rsc_info) (lrmd_t *lrmd, const char *rsc_id,
                                       enum lrmd_call_options options);
 
     /*!
      * \brief Retrieve recurring operations registered for a resource
      *
      * \param[in,out] lrmd        Executor connection object
      * \param[in]     rsc_id      ID of resource to check
      * \param[in]     timeout_ms  Error if not completed within this time
      * \param[in]     options     Group of enum lrmd_call_options flags
      * \param[out]    output      Where to store list of lrmd_op_info_t
      *
      * \return Legacy Pacemaker return code
      */
     int (*get_recurring_ops) (lrmd_t *lrmd, const char *rsc_id, int timeout_ms,
                               enum lrmd_call_options options, GList **output);
 
     /*!
      * \brief Unregister a resource from the executor
      *
      * \param[in,out] lrmd     Executor connection object
      * \param[in]     rsc_id   ID of resource to unregister
      * \param[in]     options  Group of enum lrmd_call_options flags
      *
      * \return Legacy Pacemaker return code (of particular interest, EINPROGRESS
      *         means that operations are in progress for the resource, and the
      *         unregistration will be done when they complete)
      * \note Pending and recurring operations will be cancelled.
      * \note Synchronous, guaranteed to occur in daemon before function returns.
      *
      */
     int (*unregister_rsc) (lrmd_t *lrmd, const char *rsc_id,
                            enum lrmd_call_options options);
 
     /*!
      * \brief Set a callback for executor events
      *
      * \param[in,out] lrmd      Executor connection object
      * \param[in]     callback  Callback to set
      */
     void (*set_callback) (lrmd_t *lrmd, lrmd_event_callback callback);
 
     /*!
      * \brief Request execution of a resource action
      *
      * \param[in,out] lrmd         Executor connection object
      * \param[in]     rsc_id       ID of resource
      * \param[in]     action       Name of resource action to execute
      * \param[in]     userdata     Arbitrary string to pass to event callback
      * \param[in]     interval_ms  If 0, execute action once, otherwise
      *                             recurring at this interval (in milliseconds)
      * \param[in]     timeout      Error if not complete within this time (in
      *                             milliseconds)
      * \param[in]     start_delay  Wait this long before execution (in
      *                             milliseconds)
      * \param[in]     options      Group of enum lrmd_call_options flags
      * \param[in,out] params       Parameters to pass to agent (will be freed)
      *
      * \return A call ID for the action on success (in which case the action is
      *         queued in the executor, and the event callback will be called
      *         later with the result), otherwise a negative legacy Pacemaker
      *         return code
      * \note exec() and cancel() operations on an individual resource are
      *       guaranteed to occur in the order the client API is called. However,
      *       operations on different resources are not guaranteed to occur in
      *       any specific order.
      */
     int (*exec) (lrmd_t *lrmd, const char *rsc_id, const char *action,
                  const char *userdata, guint interval_ms, int timeout,
                  int start_delay, enum lrmd_call_options options,
                  lrmd_key_value_t *params);
 
     /*!
      * \brief Cancel a recurring resource action
      *
      * \param[in,out] lrmd         Executor connection object
      * \param[in]     rsc_id       ID of resource
      * \param[in]     action       Name of resource action to cancel
      * \param[in]     interval_ms  Action's interval (in milliseconds)
      *
      * \return Legacy Pacemaker return code (if pcmk_ok, cancellation is queued
      *         on function return, and the event callback will be called later
      *         with an exec_complete event with an lrmd_op_status signifying
      *         that the operation is cancelled)
      *
      * \note exec() and cancel() operations on an individual resource are
      *       guaranteed to occur in the order the client API is called. However,
      *       operations on different resources are not guaranteed to occur in
      *       any specific order.
      */
     int (*cancel) (lrmd_t *lrmd, const char *rsc_id, const char *action,
                    guint interval_ms);
 
     /*!
      * \brief Retrieve resource agent metadata synchronously
      *
      * \param[in]  lrmd      Executor connection (unused)
      * \param[in]  standard  Resource agent class
      * \param[in]  provider  Resource agent provider
      * \param[in]  agent     Resource agent type
      * \param[out] output    Where to store metadata (must not be NULL)
      * \param[in]  options   Group of enum lrmd_call_options flags (unused)
      *
      * \return Legacy Pacemaker return code
      *
      * \note Caller is responsible for freeing output. This call is always
      *       synchronous (blocking), and always done directly by the library
      *       (not via the executor connection). This means that it is based on
      *       the local host environment, even if the executor connection is to a
      *       remote node, so this may fail if the agent is not installed
      *       locally. This also means that, if an external agent must be
      *       executed, it will be executed by the caller's user, not the
      *       executor's.
      */
     int (*get_metadata) (lrmd_t *lrmd, const char *standard,
                          const char *provider, const char *agent,
                          char **output, enum lrmd_call_options options);
 
     /*!
      * \brief Retrieve a list of installed resource agents
      *
      * \param[in]  lrmd      Executor connection (unused)
      * \param[out] agents    Where to store agent list (must not be NULL)
      * \param[in]  standard  Resource agent standard to list
      * \param[in]  provider  Resource agent provider to list (or NULL)
      *
      * \return Number of items in list on success, negative legacy Pacemaker
      *         return code otherwise
      *
      * \note if standard is not provided, all known agents will be returned
      * \note list must be freed using lrmd_list_freeall()
      */
     int (*list_agents) (lrmd_t *lrmd, lrmd_list_t **agents,
                         const char *standard, const char *provider);
 
     /*!
      * \brief Retrieve a list of resource agent providers
      *
      * \param[in]  lrmd       Executor connection (unused)
      * \param[in]  agent      If not NULL, list providers for this agent only
      * \param[out] providers  Where to store provider list
      *
      * \return Number of items in list on success, negative legacy Pacemaker
      *         return code otherwise
      * \note The caller is responsible for freeing *providers with
      *       lrmd_list_freeall().
      */
     int (*list_ocf_providers) (lrmd_t *lrmd, const char *agent,
                                lrmd_list_t **providers);
 
     /*!
      * \brief Retrieve a list of supported standards
      *
      * \param[in]  lrmd       Executor connection (unused)
      * \param[out] standards  Where to store standards list
      *
      * \return Number of items in list on success, negative legacy Pacemaker
      *         return code otherwise
      * \note The caller is responsible for freeing *standards with
      *       lrmd_list_freeall().
      */
     int (*list_standards) (lrmd_t *lrmd, lrmd_list_t **standards);
 
     /*!
      * \brief Execute an alert agent
      *
      * \param[in,out] lrmd        Executor connection
      * \param[in]     alert_id    Name of alert to execute
      * \param[in]     alert_path  Full path to alert executable
      * \param[in]     timeout     Error if not complete within this many
      *                            milliseconds
      * \param[in,out] params      Parameters to pass to agent (will be freed)
      *
      * \return Legacy Pacemaker return code (if pcmk_ok, the alert is queued in
      *         the executor, and the event callback will be called later with
      *         the result)
      *
      * \note Operations on individual alerts (by ID) are guaranteed to occur in
      *       the order the client API is called. Operations on different alerts
      *       are not guaranteed to occur in any specific order.
      */
     int (*exec_alert) (lrmd_t *lrmd, const char *alert_id,
                        const char *alert_path, int timeout,
                        lrmd_key_value_t *params);
 
     /*!
      * \brief Retrieve resource agent metadata synchronously with parameters
      *
      * \param[in]     lrmd      Executor connection (unused)
      * \param[in]     standard  Resource agent class
      * \param[in]     provider  Resource agent provider
      * \param[in]     agent     Resource agent type
      * \param[out]    output    Where to store metadata (must not be NULL)
      * \param[in]     options   Group of enum lrmd_call_options flags (unused)
      * \param[in,out] params    Parameters to pass to agent (will be freed)
      *
      * \return Legacy Pacemaker return code
      *
      * \note This is identical to the get_metadata() API call, except parameters
      *       will be passed to the resource agent via environment variables.
      */
     int (*get_metadata_params) (lrmd_t *lrmd, const char *standard,
                                 const char *provider, const char *agent,
                                 char **output, enum lrmd_call_options options,
                                 lrmd_key_value_t *params);
 
 } lrmd_api_operations_t;
 
 struct lrmd_s {
     lrmd_api_operations_t *cmds;
     void *lrmd_private;
 };
 
 static inline const char *
 lrmd_event_type2str(enum lrmd_callback_event type)
 {
     switch (type) {
         case lrmd_event_register:
             return "register";
         case lrmd_event_unregister:
             return "unregister";
         case lrmd_event_exec_complete:
             return "exec_complete";
         case lrmd_event_disconnect:
             return "disconnect";
         case lrmd_event_connect:
             return "connect";
         case lrmd_event_poke:
             return "poke";
         case lrmd_event_new_client:
             return "new_client";
     }
     return "unknown";
 }
 
 #ifdef __cplusplus
 }
 #endif
 
 #endif
diff --git a/lib/cluster/election.c b/lib/cluster/election.c
index 66df579236..b3c2c7fd24 100644
--- a/lib/cluster/election.c
+++ b/lib/cluster/election.c
@@ -1,743 +1,744 @@
 /*
  * Copyright 2004-2025 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <sys/time.h>
 #include <sys/resource.h>
 
 #include <crm/crm.h>
 #include <crm/common/mainloop.h>
 #include <crm/common/xml.h>
 
 #include <crm/cluster/internal.h>
 #include <crm/cluster/election_internal.h>
 #include "crmcluster_private.h"
 
 #define STORM_INTERVAL   2      /* in seconds */
 
 struct pcmk__election {
     enum election_result state;     // Current state of election
     guint count;                    // How many times local node has voted
     void (*cb)(pcmk_cluster_t *);   // Function to call if election is won
     GHashTable *voted;  // Key = node name, value = how node voted
     mainloop_timer_t *timeout; // When to abort if all votes not received
     int election_wins;         // Track wins, for storm detection
     bool wrote_blackbox;       // Write a storm blackbox at most once
     time_t expires;            // When storm detection period ends
     time_t last_election_loss; // When dampening period ends
 };
 
 static void
 election_complete(pcmk_cluster_t *cluster)
 {
     pcmk__assert((cluster != NULL) && (cluster->priv->election != NULL));
     cluster->priv->election->state = election_won;
     if (cluster->priv->election->cb != NULL) {
         cluster->priv->election->cb(cluster);
     }
     election_reset(cluster);
 }
 
 static gboolean
 election_timer_cb(gpointer user_data)
 {
     pcmk_cluster_t *cluster = user_data;
 
     crm_info("Declaring local node as winner after election timed out");
     election_complete(cluster);
     return FALSE;
 }
 
 /*!
  * \internal
  * \brief Get current state of an election
  *
  * \param[in] cluster  Cluster with election
  *
  * \return Current state of \e
  */
 enum election_result
 election_state(const pcmk_cluster_t *cluster)
 {
     if ((cluster == NULL) || (cluster->priv->election == NULL)) {
         return election_error;
     }
     return cluster->priv->election->state;
 }
 
 /* The local node will be declared the winner if missing votes are not received
  * within this time. The value is chosen to be the same as the default for the
  * election-timeout cluster option.
  */
 #define ELECTION_TIMEOUT_MS 120000
 
 /*!
  * \internal
  * \brief Track election state in a cluster
  *
  * Every node that wishes to participate in an election must initialize the
  * election once, typically at start-up.
  *
  * \param[in] cluster    Cluster that election is for
  * \param[in] cb         Function to call if local node wins election
  */
 void
 election_init(pcmk_cluster_t *cluster, void (*cb)(pcmk_cluster_t *))
 {
     const char *name = pcmk__s(crm_system_name, "election");
 
     CRM_CHECK(cluster->priv->election == NULL, return);
 
     cluster->priv->election = pcmk__assert_alloc(1, sizeof(pcmk__election_t));
     cluster->priv->election->cb = cb;
     cluster->priv->election->timeout = mainloop_timer_add(name,
                                                           ELECTION_TIMEOUT_MS,
                                                           FALSE,
                                                           election_timer_cb,
                                                           cluster);
 }
 
 /*!
  * \internal
  * \brief Disregard any previous vote by specified peer
  *
  * This discards any recorded vote from a specified peer. Election users should
  * call this whenever a voting peer becomes inactive.
  *
  * \param[in,out] cluster  Cluster with election
  * \param[in]     uname    Name of peer to disregard
  */
 void
 election_remove(pcmk_cluster_t *cluster, const char *uname)
 {
     if ((cluster != NULL) && (cluster->priv->election != NULL)
         && (uname != NULL) && (cluster->priv->election->voted != NULL)) {
         crm_trace("Discarding (no-)vote from lost peer %s", uname);
         g_hash_table_remove(cluster->priv->election->voted, uname);
     }
 }
 
 /*!
  * \internal
  * \brief Stop election timer and disregard all votes
  *
  * \param[in,out] cluster  Cluster with election
  */
 void
 election_reset(pcmk_cluster_t *cluster)
 {
     if ((cluster != NULL) && (cluster->priv->election != NULL)) {
         crm_trace("Resetting election");
         mainloop_timer_stop(cluster->priv->election->timeout);
         if (cluster->priv->election->voted != NULL) {
             g_hash_table_destroy(cluster->priv->election->voted);
             cluster->priv->election->voted = NULL;
         }
     }
 }
 
 /*!
  * \internal
  * \brief Free an election object
  *
  * Free all memory associated with an election object, stopping its
  * election timer (if running).
  *
  * \param[in,out] cluster  Cluster with election
  */
 void
 election_fini(pcmk_cluster_t *cluster)
 {
     if ((cluster != NULL) && (cluster->priv->election != NULL)) {
         election_reset(cluster);
         crm_trace("Destroying election");
         mainloop_timer_del(cluster->priv->election->timeout);
         free(cluster->priv->election);
         cluster->priv->election = NULL;
     }
 }
 
 static void
 election_timeout_start(pcmk_cluster_t *cluster)
 {
     mainloop_timer_start(cluster->priv->election->timeout);
 }
 
 /*!
  * \internal
  * \brief Stop an election's timer, if running
  *
  * \param[in,out] cluster  Cluster with election
  */
 void
 election_timeout_stop(pcmk_cluster_t *cluster)
 {
     if ((cluster != NULL) && (cluster->priv->election != NULL)) {
         mainloop_timer_stop(cluster->priv->election->timeout);
     }
 }
 
 /*!
  * \internal
  * \brief Change an election's timeout (restarting timer if running)
  *
  * \param[in,out] cluster  Cluster with election
  * \param[in]     period   New timeout
  */
 void
 election_timeout_set_period(pcmk_cluster_t *cluster, guint period)
 {
     CRM_CHECK((cluster != NULL) && (cluster->priv->election != NULL), return);
     mainloop_timer_set_period(cluster->priv->election->timeout, period);
 }
 
 static int
 get_uptime(struct timeval *output)
 {
     static time_t expires = 0;
     static struct rusage info;
 
     time_t tm_now = time(NULL);
 
     if (expires < tm_now) {
         int rc = 0;
 
         info.ru_utime.tv_sec = 0;
         info.ru_utime.tv_usec = 0;
         rc = getrusage(RUSAGE_SELF, &info);
 
         output->tv_sec = 0;
         output->tv_usec = 0;
 
         if (rc < 0) {
             crm_perror(LOG_ERR, "Could not calculate the current uptime");
             expires = 0;
             return -1;
         }
 
         crm_debug("Current CPU usage is: %lds, %ldus", (long)info.ru_utime.tv_sec,
                   (long)info.ru_utime.tv_usec);
     }
 
     expires = tm_now + STORM_INTERVAL;  /* N seconds after the last _access_ */
     output->tv_sec = info.ru_utime.tv_sec;
     output->tv_usec = info.ru_utime.tv_usec;
 
     return 1;
 }
 
 static int
 compare_age(struct timeval your_age)
 {
     struct timeval our_age;
 
     get_uptime(&our_age); /* If an error occurred, our_age will be compared as {0,0} */
 
     if (our_age.tv_sec > your_age.tv_sec) {
         crm_debug("Win: %ld vs %ld (seconds)", (long)our_age.tv_sec, (long)your_age.tv_sec);
         return 1;
     } else if (our_age.tv_sec < your_age.tv_sec) {
         crm_debug("Lose: %ld vs %ld (seconds)", (long)our_age.tv_sec, (long)your_age.tv_sec);
         return -1;
     } else if (our_age.tv_usec > your_age.tv_usec) {
         crm_debug("Win: %ld.%06ld vs %ld.%06ld (usec)",
                   (long)our_age.tv_sec, (long)our_age.tv_usec, (long)your_age.tv_sec, (long)your_age.tv_usec);
         return 1;
     } else if (our_age.tv_usec < your_age.tv_usec) {
         crm_debug("Lose: %ld.%06ld vs %ld.%06ld (usec)",
                   (long)our_age.tv_sec, (long)our_age.tv_usec, (long)your_age.tv_sec, (long)your_age.tv_usec);
         return -1;
     }
 
     return 0;
 }
 
 /*!
  * \internal
  * \brief Start a new election by offering local node's candidacy
  *
  * Broadcast a "vote" election message containing the local node's ID,
  * (incremented) election counter, and uptime, and start the election timer.
  *
  * \param[in,out] cluster  Cluster with election
  *
  * \note Any nodes agreeing to the candidacy will send a "no-vote" reply, and if
  *       all active peers do so, or if the election times out, the local node
  *       wins the election. (If we lose to any peer vote, we will stop the
  *       timer, so a timeout means we did not lose -- either some peer did not
  *       vote, or we did not call election_check() in time.)
  */
 void
 election_vote(pcmk_cluster_t *cluster)
 {
     struct timeval age;
     xmlNode *vote = NULL;
     pcmk__node_status_t *our_node = NULL;
     const char *message_type = NULL;
 
     CRM_CHECK((cluster != NULL) && (cluster->priv->election != NULL), return);
 
     if (cluster->priv->node_name == NULL) {
         crm_err("Cannot start an election: Local node name unknown");
         return;
     }
 
     our_node = pcmk__get_node(0, cluster->priv->node_name, NULL,
                               pcmk__node_search_cluster_member);
     if (!pcmk__cluster_is_node_active(our_node)) {
         crm_trace("Cannot vote yet: local node not connected to cluster");
         return;
     }
 
     election_reset(cluster);
     cluster->priv->election->state = election_in_progress;
     message_type = pcmk__server_message_type(cluster->priv->server);
 
     /* @COMPAT We use message_type as the sender and recipient system for
      * backward compatibility (see T566).
      */
     vote = pcmk__new_request(cluster->priv->server, message_type,
                              NULL, message_type, CRM_OP_VOTE, NULL);
 
     cluster->priv->election->count++;
     pcmk__xe_set(vote, PCMK__XA_ELECTION_OWNER,
                  pcmk__cluster_get_xml_id(our_node));
     pcmk__xe_set_int(vote, PCMK__XA_ELECTION_ID,
                      cluster->priv->election->count);
 
     // Warning: PCMK__XA_ELECTION_AGE_NANO_SEC value is actually microseconds
     get_uptime(&age);
     pcmk__xe_set_timeval(vote, PCMK__XA_ELECTION_AGE_SEC,
                          PCMK__XA_ELECTION_AGE_NANO_SEC, &age);
 
     pcmk__cluster_send_message(NULL, cluster->priv->server, vote);
     pcmk__xml_free(vote);
 
     crm_debug("Started election round %u", cluster->priv->election->count);
     election_timeout_start(cluster);
     return;
 }
 
 /*!
  * \internal
  * \brief Check whether local node has won an election
  *
  * If all known peers have sent no-vote messages, stop the election timer, set
  * the election state to won, and call any registered win callback.
  *
  * \param[in,out] cluster  Cluster with election
  *
  * \return TRUE if local node has won, FALSE otherwise
  * \note If all known peers have sent no-vote messages, but the election owner
  *       does not call this function, the election will not be won (and the
  *       callback will not be called) until the election times out.
  * \note This should be called when election_count_vote() returns
  *       \c election_in_progress.
  */
 bool
 election_check(pcmk_cluster_t *cluster)
 {
     int voted_size = 0;
     int num_members = 0;
 
     CRM_CHECK((cluster != NULL) && (cluster->priv->election != NULL),
               return false);
 
     if (cluster->priv->election->voted == NULL) {
         crm_trace("Election check requested, but no votes received yet");
         return FALSE;
     }
 
     voted_size = g_hash_table_size(cluster->priv->election->voted);
     num_members = pcmk__cluster_num_active_nodes();
 
     /* in the case of #voted > #members, it is better to
      *   wait for the timeout and give the cluster time to
      *   stabilize
      */
     if (voted_size >= num_members) {
         /* we won and everyone has voted */
         election_timeout_stop(cluster);
         if (voted_size > num_members) {
             GHashTableIter gIter;
             const pcmk__node_status_t *node = NULL;
             char *key = NULL;
 
             crm_warn("Received too many votes in election");
             g_hash_table_iter_init(&gIter, pcmk__peer_cache);
             while (g_hash_table_iter_next(&gIter, NULL, (gpointer *) & node)) {
                 if (pcmk__cluster_is_node_active(node)) {
                     crm_warn("* expected vote: %s", node->name);
                 }
             }
 
             g_hash_table_iter_init(&gIter, cluster->priv->election->voted);
             while (g_hash_table_iter_next(&gIter, (gpointer *) & key, NULL)) {
                 crm_warn("* actual vote: %s", key);
             }
 
         }
 
         crm_info("Election won by local node");
         election_complete(cluster);
         return TRUE;
 
     } else {
         crm_debug("Election still waiting on %d of %d vote%s",
                   num_members - voted_size, num_members,
                   pcmk__plural_s(num_members));
     }
 
     return FALSE;
 }
 
 #define LOSS_DAMPEN 2           /* in seconds */
 
 struct vote {
     const char *op;
     const char *from;
     const char *version;
     const char *election_owner;
     int election_id;
     struct timeval age;
 };
 
 /*!
  * \internal
  * \brief Unpack an election message
  *
  * \param[in] message  Election message XML
  * \param[out] vote    Parsed fields from message
  *
  * \return TRUE if election message and election are valid, FALSE otherwise
  * \note The parsed struct's pointer members are valid only for the lifetime of
  *       the message argument.
  */
 static bool
 parse_election_message(const xmlNode *message, struct vote *vote)
 {
     CRM_CHECK(message && vote, return FALSE);
 
     vote->election_id = -1;
     vote->age.tv_sec = -1;
     vote->age.tv_usec = -1;
 
     vote->op = pcmk__xe_get(message, PCMK__XA_CRM_TASK);
     vote->from = pcmk__xe_get(message, PCMK__XA_SRC);
     vote->version = pcmk__xe_get(message, PCMK_XA_VERSION);
     vote->election_owner = pcmk__xe_get(message, PCMK__XA_ELECTION_OWNER);
 
     pcmk__xe_get_int(message, PCMK__XA_ELECTION_ID, &(vote->election_id));
 
     if ((vote->op == NULL) || (vote->from == NULL) || (vote->version == NULL)
         || (vote->election_owner == NULL) || (vote->election_id < 0)) {
 
         crm_warn("Invalid %s message from %s",
                  pcmk__s(vote->op, "election"),
                  pcmk__s(vote->from, "unspecified node"));
         crm_log_xml_trace(message, "bad-vote");
         return FALSE;
     }
 
     // Op-specific validation
 
     if (pcmk__str_eq(vote->op, CRM_OP_VOTE, pcmk__str_none)) {
         /* Only vote ops have uptime.
            Warning: PCMK__XA_ELECTION_AGE_NANO_SEC value is in microseconds.
          */
         if ((pcmk__xe_get_timeval(message, PCMK__XA_ELECTION_AGE_SEC,
                                    PCMK__XA_ELECTION_AGE_NANO_SEC,
                                    &(vote->age)) != pcmk_rc_ok)
             || (vote->age.tv_sec < 0) || (vote->age.tv_usec < 0)) {
 
             crm_warn("Cannot count election %s from %s because uptime is "
                      "missing or invalid",
                      vote->op, vote->from);
             return FALSE;
         }
 
     } else if (!pcmk__str_eq(vote->op, CRM_OP_NOVOTE, pcmk__str_none)) {
         crm_info("Cannot process election message from %s "
                  "because %s is not a known election op", vote->from, vote->op);
         return FALSE;
     }
 
     /* If the membership cache is NULL, we REALLY shouldn't be voting --
      * the question is how we managed to get here.
      */
     if (pcmk__peer_cache == NULL) {
         crm_info("Cannot count election %s from %s "
                  "because no peer information available", vote->op, vote->from);
         return FALSE;
     }
     return TRUE;
 }
 
 static void
 record_vote(pcmk_cluster_t *cluster, struct vote *vote)
 {
     pcmk__assert((vote->from != NULL) && (vote->op != NULL));
 
     if (cluster->priv->election->voted == NULL) {
         cluster->priv->election->voted = pcmk__strkey_table(free, free);
     }
     pcmk__insert_dup(cluster->priv->election->voted, vote->from, vote->op);
 }
 
 static void
 send_no_vote(pcmk_cluster_t *cluster, pcmk__node_status_t *peer,
              struct vote *vote)
 {
     const char *message_type = NULL;
     xmlNode *novote = NULL;
 
     message_type = pcmk__server_message_type(cluster->priv->server);
     novote = pcmk__new_request(cluster->priv->server, message_type,
                                vote->from, message_type, CRM_OP_NOVOTE, NULL);
     pcmk__xe_set(novote, PCMK__XA_ELECTION_OWNER, vote->election_owner);
     pcmk__xe_set_int(novote, PCMK__XA_ELECTION_ID, vote->election_id);
 
     pcmk__cluster_send_message(peer, cluster->priv->server, novote);
     pcmk__xml_free(novote);
 }
 
 /*!
  * \internal
  * \brief Process an election message (vote or no-vote) from a peer
  *
  * \param[in,out] cluster  Cluster with election
  * \param[in]     message  Election message XML from peer
  * \param[in]     can_win  Whether local node is eligible to win
  *
  * \return Election state after new vote is considered
  * \note If the peer message is a vote, and we prefer the peer to win, this will
  *       send a no-vote reply to the peer.
  * \note The situations "we lost to this vote" from "this is a late no-vote
  *       after we've already lost" both return election_lost. If a caller needs
  *       to distinguish them, it should save the current state before calling
  *       this function, and then compare the result.
  */
 enum election_result
 election_count_vote(pcmk_cluster_t *cluster, const xmlNode *message,
                     bool can_win)
 {
     int log_level = LOG_INFO;
     gboolean done = FALSE;
     gboolean we_lose = FALSE;
     const char *reason = "unknown";
     bool we_are_owner = FALSE;
     pcmk__node_status_t *our_node = NULL;
     pcmk__node_status_t *your_node = NULL;
     time_t tm_now = time(NULL);
     struct vote vote;
 
     CRM_CHECK((cluster != NULL) && (cluster->priv->election != NULL)
               && (message != NULL) && (cluster->priv->node_name != NULL),
               return election_error);
 
     if (!parse_election_message(message, &vote)) {
         return election_error;
     }
 
     your_node = pcmk__get_node(0, vote.from, NULL,
                                pcmk__node_search_cluster_member);
     our_node = pcmk__get_node(0, cluster->priv->node_name, NULL,
                               pcmk__node_search_cluster_member);
     we_are_owner = (our_node != NULL)
                    && pcmk__str_eq(pcmk__cluster_get_xml_id(our_node),
                                    vote.election_owner, pcmk__str_none);
 
     if (!can_win) {
         reason = "Not eligible";
         we_lose = TRUE;
 
     } else if (!pcmk__cluster_is_node_active(our_node)) {
         reason = "We are not part of the cluster";
         log_level = LOG_ERR;
         we_lose = TRUE;
 
     } else if (we_are_owner
                && (vote.election_id != cluster->priv->election->count)) {
         log_level = LOG_TRACE;
         reason = "Superseded";
         done = TRUE;
 
     } else if (!pcmk__cluster_is_node_active(your_node)) {
         /* Possibly we cached the message in the FSA queue at a point that it wasn't */
         reason = "Peer is not part of our cluster";
         log_level = LOG_WARNING;
         done = TRUE;
 
     } else if (pcmk__str_eq(vote.op, CRM_OP_NOVOTE, pcmk__str_none)
                || pcmk__str_eq(vote.from, cluster->priv->node_name,
                                pcmk__str_casei)) {
         /* Receiving our own broadcast vote, or a no-vote from peer, is a vote
          * for us to win
          */
         if (!we_are_owner) {
             crm_warn("Cannot count election round %d %s from %s "
                      "because we did not start election (node ID %s did)",
                      vote.election_id, vote.op, vote.from,
                      vote.election_owner);
             return election_error;
         }
         if (cluster->priv->election->state != election_in_progress) {
             // Should only happen if we already lost
             crm_debug("Not counting election round %d %s from %s "
                       "because no election in progress",
                       vote.election_id, vote.op, vote.from);
             return cluster->priv->election->state;
         }
         record_vote(cluster, &vote);
         reason = "Recorded";
         done = TRUE;
 
     } else {
         // A peer vote requires a comparison to determine which node is better
         int age_result = compare_age(vote.age);
-        int version_result = compare_version(vote.version, CRM_FEATURE_SET);
+        int version_result = pcmk__compare_versions(vote.version,
+                                                    CRM_FEATURE_SET);
 
         if (version_result < 0) {
             reason = "Version";
             we_lose = TRUE;
 
         } else if (version_result > 0) {
             reason = "Version";
 
         } else if (age_result < 0) {
             reason = "Uptime";
             we_lose = TRUE;
 
         } else if (age_result > 0) {
             reason = "Uptime";
 
         } else if (strcasecmp(cluster->priv->node_name, vote.from) > 0) {
             reason = "Host name";
             we_lose = TRUE;
 
         } else {
             reason = "Host name";
         }
     }
 
     if (cluster->priv->election->expires < tm_now) {
         cluster->priv->election->election_wins = 0;
         cluster->priv->election->expires = tm_now + STORM_INTERVAL;
 
     } else if (done == FALSE && we_lose == FALSE) {
         int peers = 1 + g_hash_table_size(pcmk__peer_cache);
 
         /* If every node has to vote down every other node, thats N*(N-1) total elections
          * Allow some leeway before _really_ complaining
          */
         cluster->priv->election->election_wins++;
         if (cluster->priv->election->election_wins > (peers * peers)) {
             crm_warn("Election storm detected: %d wins in %d seconds",
                      cluster->priv->election->election_wins, STORM_INTERVAL);
             cluster->priv->election->election_wins = 0;
             cluster->priv->election->expires = tm_now + STORM_INTERVAL;
             if (!(cluster->priv->election->wrote_blackbox)) {
                 /* It's questionable whether a black box (from every node in the
                  * cluster) would be truly helpful in diagnosing an election
                  * storm. It's also highly doubtful a production environment
                  * would get multiple election storms from distinct causes, so
                  * saving one blackbox per process lifetime should be
                  * sufficient. Alternatives would be to save a timestamp of the
                  * last blackbox write instead of a boolean, and write a new one
                  * if some amount of time has passed; or to save a storm count,
                  * write a blackbox on every Nth occurrence.
                  */
                 crm_write_blackbox(0, NULL);
                 cluster->priv->election->wrote_blackbox = true;
             }
         }
     }
 
     if (done) {
         do_crm_log(log_level + 1,
                    "Processed election round %u %s (current round %d) "
                    "from %s (%s)",
                    vote.election_id, vote.op, cluster->priv->election->count,
                    vote.from, reason);
         return cluster->priv->election->state;
 
     } else if (we_lose == FALSE) {
         /* We track the time of the last election loss to implement an election
          * dampening period, reducing the likelihood of an election storm. If
          * this node has lost within the dampening period, don't start a new
          * election, even if we win against a peer's vote -- the peer we lost to
          * should win again.
          *
          * @TODO This has a problem case: if an election winner immediately
          * leaves the cluster, and a new election is immediately called, all
          * nodes could lose, with no new winner elected. The ideal solution
          * would be to tie the election structure with the peer caches, which
          * would allow us to clear the dampening when the previous winner
          * leaves (and would allow other improvements as well).
          */
         if ((cluster->priv->election->last_election_loss == 0)
             || ((tm_now - cluster->priv->election->last_election_loss)
                 > (time_t) LOSS_DAMPEN)) {
 
             do_crm_log(log_level,
                        "Election round %d (started by node ID %s) pass: "
                        "%s from %s (%s)",
                        vote.election_id, vote.election_owner, vote.op,
                        vote.from, reason);
 
             cluster->priv->election->last_election_loss = 0;
             election_timeout_stop(cluster);
 
             /* Start a new election by voting down this, and other, peers */
             cluster->priv->election->state = election_start;
             return cluster->priv->election->state;
         } else {
             char *loss_time = NULL;
 
             loss_time = ctime(&(cluster->priv->election->last_election_loss));
             if (loss_time) {
                 // Show only HH:MM:SS
                 loss_time += 11;
                 loss_time[8] = '\0';
             }
             crm_info("Ignoring election round %d (started by node ID %s) pass "
                      "vs %s because we lost less than %ds ago at %s",
                      vote.election_id, vote.election_owner, vote.from,
                      LOSS_DAMPEN, (loss_time? loss_time : "unknown"));
         }
     }
 
     cluster->priv->election->last_election_loss = tm_now;
 
     do_crm_log(log_level,
                "Election round %d (started by node ID %s) lost: "
                "%s from %s (%s)",
                vote.election_id, vote.election_owner, vote.op,
                vote.from, reason);
 
     election_reset(cluster);
     send_no_vote(cluster, your_node, &vote);
     cluster->priv->election->state = election_lost;
     return cluster->priv->election->state;
 }
 
 /*!
  * \internal
  * \brief Reset any election dampening currently in effect
  *
  * \param[in,out] cluster  Cluster with election
  */
 void
 election_clear_dampening(pcmk_cluster_t *cluster)
 {
     if ((cluster != NULL) && (cluster->priv->election != NULL)) {
         cluster->priv->election->last_election_loss = 0;
     }
 }
diff --git a/lib/common/cib.c b/lib/common/cib.c
index fc29b0df64..58fcceb969 100644
--- a/lib/common/cib.c
+++ b/lib/common/cib.c
@@ -1,192 +1,193 @@
 /*
  * Original copyright 2004 International Business Machines
  * Later changes copyright 2008-2025 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <stdio.h>
 #include <libxml/tree.h>    // xmlNode
 
 #include <crm/common/xml.h>
 #include <crm/common/cib.h>
 #include <crm/common/cib_internal.h>
 
 /*
  * Functions to help find particular sections of the CIB
  */
 
 // Map CIB element names to their parent elements and XPath searches
 static struct {
     const char *name;   // Name of this CIB element
     const char *parent; // CIB element that this element is a child of
     const char *path;   // XPath to find this CIB element
 } cib_sections[] = {
     {
         // This first entry is also the default if a NULL is compared
         PCMK_XE_CIB,
         NULL,
         "//" PCMK_XE_CIB
     },
     {
         PCMK_XE_STATUS,
         "/" PCMK_XE_CIB,
         "//" PCMK_XE_CIB "/" PCMK_XE_STATUS
     },
     {
         PCMK_XE_CONFIGURATION,
         "/" PCMK_XE_CIB,
         "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION
     },
     {
         PCMK_XE_CRM_CONFIG,
         "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION,
         "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_CRM_CONFIG
     },
     {
         PCMK_XE_NODES,
         "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION,
         "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_NODES
     },
     {
         PCMK_XE_RESOURCES,
         "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION,
         "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_RESOURCES
     },
     {
         PCMK_XE_CONSTRAINTS,
         "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION,
         "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_CONSTRAINTS
     },
     {
         PCMK_XE_OP_DEFAULTS,
         "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION,
         "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_OP_DEFAULTS
     },
     {
         PCMK_XE_RSC_DEFAULTS,
         "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION,
         "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_RSC_DEFAULTS
     },
     {
         PCMK_XE_ACLS,
         "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION,
         "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_ACLS
     },
     {
         PCMK_XE_FENCING_TOPOLOGY,
         "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION,
         "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_FENCING_TOPOLOGY
     },
     {
         PCMK_XE_TAGS,
         "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION,
         "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_TAGS
     },
     {
         PCMK_XE_ALERTS,
         "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION,
         "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_ALERTS
     },
     {
         PCMK__XE_ALL,
         NULL,
         "//" PCMK_XE_CIB
     },
 };
 
 /*!
  * \brief Get the relative XPath needed to find a specified CIB element name
  *
  * \param[in] element_name  Name of CIB element
  *
  * \return XPath for finding \p element_name in CIB XML (or NULL if unknown)
  * \note The return value is constant and should not be freed.
  */
 const char *
 pcmk_cib_xpath_for(const char *element_name)
 {
     for (int lpc = 0; lpc < PCMK__NELEM(cib_sections); lpc++) {
         // A NULL element_name will match the first entry
         if (pcmk__str_eq(element_name, cib_sections[lpc].name,
                          pcmk__str_null_matches)) {
             return cib_sections[lpc].path;
         }
     }
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Get the absolute XPath needed to find a specified CIB element name
  *
  * \param[in] element  Name of CIB element
  *
  * \return XPath for finding \p element in CIB XML (or \c NULL if unknown)
  */
 const char *
 pcmk__cib_abs_xpath_for(const char *element)
 {
     const char *xpath = pcmk_cib_xpath_for(element);
 
     // XPaths returned by pcmk_cib_xpath_for() are relative (starting with "//")
     return ((xpath != NULL)? (xpath + 1) : NULL);
 }
 
 /*!
  * \brief Get the parent element name of a given CIB element name
  *
  * \param[in] element_name  Name of CIB element
  *
  * \return Parent element of \p element_name (or NULL if none or unknown)
  * \note The return value is constant and should not be freed.
  */
 const char *
 pcmk_cib_parent_name_for(const char *element_name)
 {
     for (int lpc = 0; lpc < PCMK__NELEM(cib_sections); lpc++) {
         // A NULL element_name will match the first entry
         if (pcmk__str_eq(element_name, cib_sections[lpc].name,
                          pcmk__str_null_matches)) {
             return cib_sections[lpc].parent;
         }
     }
     return NULL;
 }
 
 /*!
  * \brief Find an element in the CIB
  *
  * \param[in,out] cib           Top-level CIB XML to search
  * \param[in]     element_name  Name of CIB element to search for
  *
  * \return XML element in \p cib corresponding to \p element_name
  *         (or \p cib itself if element is unknown or not found)
  */
 xmlNode *
 pcmk_find_cib_element(xmlNode *cib, const char *element_name)
 {
     return pcmk__xpath_find_one(cib->doc, pcmk_cib_xpath_for(element_name),
                                 LOG_TRACE);
 }
 
 /*!
  * \internal
  * \brief Check that the feature set in the CIB is supported on this node
  *
  * \param[in] new_version   PCMK_XA_CRM_FEATURE_SET attribute from the CIB
  */
 int
 pcmk__check_feature_set(const char *cib_version)
 {
-    if (cib_version && compare_version(cib_version, CRM_FEATURE_SET) > 0) {
+    if ((cib_version != NULL)
+        && (pcmk__compare_versions(cib_version, CRM_FEATURE_SET) > 0)) {
         return EPROTONOSUPPORT;
     }
 
     return pcmk_rc_ok;
 }
diff --git a/lib/common/digest.c b/lib/common/digest.c
index f54e5b8337..657544b525 100644
--- a/lib/common/digest.c
+++ b/lib/common/digest.c
@@ -1,437 +1,437 @@
 /*
  * Copyright 2015-2025 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <stdbool.h>
 #include <stdio.h>
 #include <unistd.h>
 #include <string.h>
 #include <stdlib.h>
 
 #include <glib.h>               // GString, etc.
 #include <gnutls/crypto.h>      // gnutls_hash_fast(), gnutls_hash_get_len()
 #include <gnutls/gnutls.h>      // gnutls_strerror()
 
 #include <crm/crm.h>
 #include <crm/common/xml.h>
 #include "crmcommon_private.h"
 
 #define BEST_EFFORT_STATUS 0
 
 /*
  * Pacemaker uses digests (MD5 hashes) of stringified XML to detect changes in
  * the CIB as a whole, a particular resource's agent parameters, and the device
  * parameters last used to unfence a particular node.
  *
  * "v2" digests hash pcmk__xml_string() directly, while less efficient "v1"
  * digests do the same with a prefixed space, suffixed newline, and optional
  * pre-sorting.
  *
  * On-disk CIB digests use v1 without sorting.
  *
  * Operation digests use v1 with sorting, and are stored in a resource's
  * operation history in the CIB status section. They come in three flavors:
  * - a digest of (nearly) all resource parameters and options, used to detect
  *   any resource configuration change;
  * - a digest of resource parameters marked as nonreloadable, used to decide
  *   whether a reload or full restart is needed after a configuration change;
  * - and a digest of resource parameters not marked as private, used in
  *   simulations where private parameters have been removed from the input.
  *
  * Unfencing digests are set as node attributes, and are used to require
  * that nodes be unfenced again after a device's configuration changes.
  */
 
 /*!
  * \internal
  * \brief Dump XML in a format used with v1 digests
  *
  * \param[in] xml  Root of XML to dump
  *
  * \return Newly allocated buffer containing dumped XML
  */
 static GString *
 dump_xml_for_digest(const xmlNode *xml)
 {
     GString *buffer = g_string_sized_new(1024);
 
     /* for compatibility with the old result which is used for v1 digests */
     g_string_append_c(buffer, ' ');
     pcmk__xml_string(xml, 0, buffer, 0);
     g_string_append_c(buffer, '\n');
 
     return buffer;
 }
 
 /*!
  * \internal
  * \brief Compute an MD5 checksum for a given input string
  *
  * \param[in] input  Input string (can be \c NULL)
  *
  * \return Newly allocated string containing MD5 checksum for \p input, or
  *         \c NULL on error or if \p input is \c NULL
  *
  * \note The caller is responsible for freeing the return value using \c free().
  */
 char *
 pcmk__md5sum(const char *input)
 {
     /* This function makes two copies of the same string: one inside
      * g_compute_checksum_for_string() and one from pcmk__str_copy(). The first
      * one gets freed before return.
      *
      * We could avoid this by calling g_checksum_new(), g_checksum_get_string()
      * (copying the return value with pcmk__str_copy(), and g_checksum_free()
      * directly; or by updating our own call chains to use (gchar *) strings.
      * However, the below is much more readable.
      */
     char *checksum = NULL;
     gchar *checksum_g = NULL;
 
     if (input == NULL) {
         return NULL;
     }
 
     checksum_g = g_compute_checksum_for_string(G_CHECKSUM_MD5, input, -1);
     if (checksum_g == NULL) {
         crm_err("Failed to compute MD5 checksum for %s", input);
     }
 
     checksum = pcmk__str_copy(checksum_g);
     g_free(checksum_g);
     return checksum;
 }
 
 /*!
  * \internal
  * \brief Calculate and return v1 digest of XML tree
  *
  * \param[in] input  Root of XML to digest
  *
  * \return Newly allocated string containing digest
  *
  * \note Example return value: "c048eae664dba840e1d2060f00299e9d"
  */
 static char *
 calculate_xml_digest_v1(const xmlNode *input)
 {
     GString *buffer = dump_xml_for_digest(input);
     gchar *digest_g = NULL;
     char *digest = NULL;
 
     // buffer->len > 2 for initial space and trailing newline
     CRM_CHECK(buffer->len > 2,
               g_string_free(buffer, TRUE);
               return NULL);
 
     digest_g = pcmk__md5sum(buffer->str);
     digest = pcmk__str_copy(digest_g);
 
     g_string_free(buffer, TRUE);
     g_free(digest_g);
     return digest;
 }
 
 /*!
  * \internal
  * \brief Calculate and return the digest of a CIB, suitable for storing on disk
  *
  * \param[in] input  Root of XML to digest
  *
  * \return Newly allocated string containing digest
  */
 char *
 pcmk__digest_on_disk_cib(const xmlNode *input)
 {
     /* Always use the v1 format for on-disk digests.
      * * Switching to v2 affects even full-restart upgrades, so it would be a
      *   compatibility nightmare.
      * * We only use this once at startup. All other invocations are in a
      *   separate child process.
      */
     return calculate_xml_digest_v1(input);
 }
 
 /*!
  * \internal
  * \brief Calculate and return digest of an operation XML element
  *
  * The digest is invariant to changes in the order of XML attributes.
  *
  * \param[in] input  Root of XML to digest (must have no children)
  *
  * \return Newly allocated string containing digest
  */
 char *
 pcmk__digest_operation(const xmlNode *input)
 {
     /* Switching to v2 digests would likely cause restarts during rolling
      * upgrades.
      *
      * @TODO Confirm this. Switch to v2 if safe, or drop this TODO otherwise.
      */
     char *digest = NULL;
     xmlNode *sorted = NULL;
 
     pcmk__assert(input->children == NULL);
 
     sorted = pcmk__xe_create(NULL, (const char *) input->name);
     pcmk__xe_copy_attrs(sorted, input, pcmk__xaf_none);
     pcmk__xe_sort_attrs(sorted);
 
     digest = calculate_xml_digest_v1(sorted);
 
     pcmk__xml_free(sorted);
     return digest;
 }
 
 /*!
  * \internal
  * \brief Calculate and return the digest of an XML tree
  *
  * \param[in] xml     XML tree to digest
  * \param[in] filter  Whether to filter certain XML attributes
  *
  * \return Newly allocated string containing digest
  */
 char *
 pcmk__digest_xml(const xmlNode *xml, bool filter)
 {
     /* @TODO Filtering accounts for significant CPU usage. Consider removing if
      * possible.
      */
     GString *buf = g_string_sized_new(1024);
     gchar *digest_g = NULL;
     char *digest = NULL;
 
     pcmk__xml_string(xml, (filter? pcmk__xml_fmt_filtered : 0), buf, 0);
     digest_g = pcmk__md5sum(buf->str);
     if (digest_g == NULL) {
         goto done;
     }
 
     digest = pcmk__str_copy(digest_g);
 
     pcmk__if_tracing(
         {
             char *trace_file = pcmk__assert_asprintf("digest-%s", digest);
 
             crm_trace("Saving %s.%s.%s to %s",
                       pcmk__xe_get(xml, PCMK_XA_ADMIN_EPOCH),
                       pcmk__xe_get(xml, PCMK_XA_EPOCH),
                       pcmk__xe_get(xml, PCMK_XA_NUM_UPDATES),
                       trace_file);
             pcmk__xml_write_temp_file(xml, "digest input", trace_file);
             free(trace_file);
         },
         {}
     );
 
 done:
     g_string_free(buf, TRUE);
     g_free(digest_g);
     return digest;
 }
 
 /*!
  * \internal
  * \brief Check whether calculated digest of given XML matches expected digest
  *
  * \param[in] input     Root of XML tree to digest
  * \param[in] expected  Expected digest in on-disk format
  *
  * \return true if digests match, false on mismatch or error
  */
 bool
 pcmk__verify_digest(const xmlNode *input, const char *expected)
 {
     char *calculated = NULL;
     bool passed;
 
     if (input != NULL) {
         calculated = pcmk__digest_on_disk_cib(input);
         if (calculated == NULL) {
             crm_perror(LOG_ERR, "Could not calculate digest for comparison");
             return false;
         }
     }
     passed = pcmk__str_eq(expected, calculated, pcmk__str_casei);
     if (passed) {
         crm_trace("Digest comparison passed: %s", calculated);
     } else {
         crm_err("Digest comparison failed: expected %s, calculated %s",
                 expected, calculated);
     }
     free(calculated);
     return passed;
 }
 
 /*!
  * \internal
  * \brief Check whether an XML attribute should be excluded from CIB digests
  *
  * \param[in] name  XML attribute name
  *
  * \return true if XML attribute should be excluded from CIB digest calculation
  */
 bool
 pcmk__xa_filterable(const char *name)
 {
     static const char *filter[] = {
         PCMK_XA_CRM_DEBUG_ORIGIN,
         PCMK_XA_CIB_LAST_WRITTEN,
         PCMK_XA_UPDATE_ORIGIN,
         PCMK_XA_UPDATE_CLIENT,
         PCMK_XA_UPDATE_USER,
     };
 
     for (int i = 0; i < PCMK__NELEM(filter); i++) {
         if (strcmp(name, filter[i]) == 0) {
             return true;
         }
     }
     return false;
 }
 
 // Return true if a is an attribute that should be filtered
 static bool
 should_filter_for_digest(xmlAttrPtr a, void *user_data)
 {
     if (strncmp((const char *) a->name, CRM_META "_",
                 sizeof(CRM_META " ") - 1) == 0) {
         return true;
     }
     return pcmk__str_any_of((const char *) a->name,
                             PCMK_XA_ID,
                             PCMK_XA_CRM_FEATURE_SET,
                             PCMK__XA_OP_DIGEST,
                             PCMK__META_ON_NODE,
                             PCMK__META_ON_NODE_UUID,
                             "pcmk_external_ip",
                             NULL);
 }
 
 /*!
  * \internal
  * \brief Remove XML attributes not needed for operation digest
  *
  * \param[in,out] param_set  XML with operation parameters
  */
 void
 pcmk__filter_op_for_digest(xmlNode *param_set)
 {
     char *key = NULL;
     char *timeout = NULL;
     guint interval_ms = 0;
 
     if (param_set == NULL) {
         return;
     }
 
     /* Timeout is useful for recurring operation digests, so grab it before
      * removing meta-attributes
      */
     key = crm_meta_name(PCMK_META_INTERVAL);
     pcmk__xe_get_guint(param_set, key, &interval_ms);
     free(key);
     key = NULL;
     if (interval_ms != 0) {
         key = crm_meta_name(PCMK_META_TIMEOUT);
         timeout = pcmk__xe_get_copy(param_set, key);
     }
 
     // Remove all CRM_meta_* attributes and certain other attributes
     pcmk__xe_remove_matching_attrs(param_set, false, should_filter_for_digest,
                                    NULL);
 
     // Add timeout back for recurring operation digests
     if (timeout != NULL) {
         pcmk__xe_set(param_set, key, timeout);
     }
     free(timeout);
     free(key);
 }
 
 // Deprecated functions kept only for backward API compatibility
 // LCOV_EXCL_START
 
 #include <crm/common/util_compat.h>         // crm_md5sum()
 #include <crm/common/xml_compat.h>
 #include <crm/common/xml_element_compat.h>
 
 char *
 calculate_on_disk_digest(xmlNode *input)
 {
     return calculate_xml_digest_v1(input);
 }
 
 char *
 calculate_operation_digest(xmlNode *input, const char *version)
 {
     xmlNode *sorted = sorted_xml(input, NULL, true);
     char *digest = calculate_xml_digest_v1(sorted);
 
     pcmk__xml_free(sorted);
     return digest;
 }
 
 char *
 calculate_xml_versioned_digest(xmlNode *input, gboolean sort,
                                gboolean do_filter, const char *version)
 {
-    if ((version == NULL) || (compare_version("3.0.5", version) > 0)) {
+    if ((version == NULL) || (pcmk__compare_versions("3.0.5", version) > 0)) {
         xmlNode *sorted = NULL;
         char *digest = NULL;
 
         if (sort) {
             xmlNode *sorted = sorted_xml(input, NULL, true);
 
             input = sorted;
         }
 
         crm_trace("Using v1 digest algorithm for %s",
                   pcmk__s(version, "unknown feature set"));
 
         digest = calculate_xml_digest_v1(input);
 
         pcmk__xml_free(sorted);
         return digest;
     }
     crm_trace("Using v2 digest algorithm for %s", version);
     return pcmk__digest_xml(input, do_filter);
 }
 
 char *
 crm_md5sum(const char *buffer)
 {
     char *digest = NULL;
     gchar *raw_digest = NULL;
 
     if (buffer == NULL) {
         return NULL;
     }
 
     raw_digest = g_compute_checksum_for_string(G_CHECKSUM_MD5, buffer, -1);
 
     if (raw_digest == NULL) {
         crm_err("Failed to calculate hash");
         return NULL;
     }
 
     digest = pcmk__str_copy(raw_digest);
     g_free(raw_digest);
 
     crm_trace("Digest %s.", digest);
     return digest;
 }
 
 // LCOV_EXCL_STOP
 // End deprecated API
diff --git a/lib/common/rules.c b/lib/common/rules.c
index e97ce52cda..66891c1c9c 100644
--- a/lib/common/rules.c
+++ b/lib/common/rules.c
@@ -1,1375 +1,1375 @@
 /*
  * Copyright 2004-2025 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <stdio.h>                          // NULL, size_t
 #include <stdbool.h>                        // bool
 #include <ctype.h>                          // isdigit()
 #include <regex.h>                          // regmatch_t
 #include <stdint.h>                         // uint32_t
 #include <inttypes.h>                       // PRIu32
 #include <glib.h>                           // gboolean, FALSE
 #include <libxml/tree.h>                    // xmlNode
 
 #include <crm/common/scheduler.h>
 
 #include <crm/common/iso8601_internal.h>
 #include <crm/common/nvpair_internal.h>
 #include <crm/common/rules_internal.h>
 #include <crm/common/scheduler_internal.h>
 #include "crmcommon_private.h"
 
 /*!
  * \internal
  * \brief Get the condition type corresponding to given condition XML
  *
  * \param[in] condition  Rule condition XML
  *
  * \return Condition type corresponding to \p condition
  */
 enum expression_type
 pcmk__condition_type(const xmlNode *condition)
 {
     const char *name = NULL;
 
     // Expression types based on element name
 
     if (pcmk__xe_is(condition, PCMK_XE_DATE_EXPRESSION)) {
         return pcmk__condition_datetime;
 
     } else if (pcmk__xe_is(condition, PCMK_XE_RSC_EXPRESSION)) {
         return pcmk__condition_resource;
 
     } else if (pcmk__xe_is(condition, PCMK_XE_OP_EXPRESSION)) {
         return pcmk__condition_operation;
 
     } else if (pcmk__xe_is(condition, PCMK_XE_RULE)) {
         return pcmk__condition_rule;
 
     } else if (!pcmk__xe_is(condition, PCMK_XE_EXPRESSION)) {
         return pcmk__condition_unknown;
     }
 
     // Expression types based on node attribute name
 
     name = pcmk__xe_get(condition, PCMK_XA_ATTRIBUTE);
 
     if (pcmk__str_any_of(name, CRM_ATTR_UNAME, CRM_ATTR_KIND, CRM_ATTR_ID,
                          NULL)) {
         return pcmk__condition_location;
     }
 
     return pcmk__condition_attribute;
 }
 
 /*!
  * \internal
  * \brief Get parent XML element's ID for logging purposes
  *
  * \param[in] xml  XML of a subelement
  *
  * \return ID of \p xml's parent for logging purposes (guaranteed non-NULL)
  */
 static const char *
 loggable_parent_id(const xmlNode *xml)
 {
     // Default if called without parent (likely for unit testing)
     const char *parent_id = "implied";
 
     if ((xml != NULL) && (xml->parent != NULL)) {
         parent_id = pcmk__xe_id(xml->parent);
         if (parent_id == NULL) { // Not possible with schema validation enabled
             parent_id = "without ID";
         }
     }
     return parent_id;
 }
 
 /*!
  * \internal
  * \brief Check an integer value against a range from a date specification
  *
  * \param[in] date_spec  XML of PCMK_XE_DATE_SPEC element to check
  * \param[in] id         XML ID of parent date expression for logging purposes
  * \param[in] attr       Name of XML attribute with range to check against
  * \param[in] value      Value to compare against range
  *
  * \return Standard Pacemaker return code (specifically, pcmk_rc_before_range,
  *         pcmk_rc_after_range, or pcmk_rc_ok to indicate that result is either
  *         within range or undetermined)
  * \note We return pcmk_rc_ok for an undetermined result so we can continue
  *       checking the next range attribute.
  */
 static int
 check_range(const xmlNode *date_spec, const char *id, const char *attr,
             uint32_t value)
 {
     int rc = pcmk_rc_ok;
     const char *range = pcmk__xe_get(date_spec, attr);
     long long low, high;
 
     if (range == NULL) {
         // Attribute not present
 
     } else if (pcmk__parse_ll_range(range, &low, &high) != pcmk_rc_ok) {
         // Invalid range
         pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s "
                          "as not passing because '%s' is not a valid range "
                          "for " PCMK_XE_DATE_SPEC " attribute %s",
                          id, range, attr);
         rc = pcmk_rc_unpack_error;
 
     } else if ((low != -1) && (value < low)) {
         rc = pcmk_rc_before_range;
 
     } else if ((high != -1) && (value > high)) {
         rc = pcmk_rc_after_range;
     }
 
     crm_trace(PCMK_XE_DATE_EXPRESSION " %s: " PCMK_XE_DATE_SPEC
               " %s='%s' for %" PRIu32 ": %s",
               id, attr, pcmk__s(range, ""), value, pcmk_rc_str(rc));
     return rc;
 }
 
 /*!
  * \internal
  * \brief Evaluate a date specification for a given date/time
  *
  * \param[in] date_spec  XML of PCMK_XE_DATE_SPEC element to evaluate
  * \param[in] now        Time to check
  *
  * \return Standard Pacemaker return code (specifically, EINVAL for NULL
  *         arguments, pcmk_rc_unpack_error if the specification XML is invalid,
  *         \c pcmk_rc_ok if \p now is within the specification's ranges, or
  *         \c pcmk_rc_before_range or \c pcmk_rc_after_range as appropriate)
  */
 int
 pcmk__evaluate_date_spec(const xmlNode *date_spec, const crm_time_t *now)
 {
     const char *id = NULL;
     const char *parent_id = loggable_parent_id(date_spec);
 
     // Range attributes that can be specified for a PCMK_XE_DATE_SPEC element
     struct range {
         const char *attr;
         uint32_t value;
     } ranges[] = {
         { PCMK_XA_YEARS, 0U },
         { PCMK_XA_MONTHS, 0U },
         { PCMK_XA_MONTHDAYS, 0U },
         { PCMK_XA_HOURS, 0U },
         { PCMK_XA_MINUTES, 0U },
         { PCMK_XA_SECONDS, 0U },
         { PCMK_XA_YEARDAYS, 0U },
         { PCMK_XA_WEEKYEARS, 0U },
         { PCMK_XA_WEEKS, 0U },
         { PCMK_XA_WEEKDAYS, 0U },
     };
 
     if ((date_spec == NULL) || (now == NULL)) {
         return EINVAL;
     }
 
     // Get specification ID (for logging)
     id = pcmk__xe_id(date_spec);
     if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
         pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
                          "passing because " PCMK_XE_DATE_SPEC
                          " subelement has no " PCMK_XA_ID, parent_id);
         return pcmk_rc_unpack_error;
     }
 
     // Year, month, day
     crm_time_get_gregorian(now, &(ranges[0].value), &(ranges[1].value),
                            &(ranges[2].value));
 
     // Hour, minute, second
     crm_time_get_timeofday(now, &(ranges[3].value), &(ranges[4].value),
                            &(ranges[5].value));
 
     // Year (redundant) and day of year
     crm_time_get_ordinal(now, &(ranges[0].value), &(ranges[6].value));
 
     // Week year, week of week year, day of week
     crm_time_get_isoweek(now, &(ranges[7].value), &(ranges[8].value),
                          &(ranges[9].value));
 
     for (int i = 0; i < PCMK__NELEM(ranges); ++i) {
         int rc = check_range(date_spec, parent_id, ranges[i].attr,
                              ranges[i].value);
 
         if (rc != pcmk_rc_ok) {
             return rc;
         }
     }
 
     // All specified ranges passed, or none were given (also considered a pass)
     return pcmk_rc_ok;
 }
 
 #define ADD_COMPONENT(component) do {                                       \
         int rc = pcmk__add_time_from_xml(*end, component, duration);        \
         if (rc != pcmk_rc_ok) {                                             \
             pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s "     \
                              "as not passing because " PCMK_XE_DURATION     \
                              " %s attribute %s is invalid: %s",             \
                              parent_id, id,                                 \
                              pcmk__time_component_attr(component),          \
                              pcmk_rc_str(rc));                              \
             crm_time_free(*end);                                            \
             *end = NULL;                                                    \
             return rc;                                                      \
         }                                                                   \
     } while (0)
 
 /*!
  * \internal
  * \brief Given a duration and a start time, calculate the end time
  *
  * \param[in]  duration  XML of PCMK_XE_DURATION element
  * \param[in]  start     Start time
  * \param[out] end       Where to store end time (\p *end must be NULL
  *                       initially)
  *
  * \return Standard Pacemaker return code
  * \note The caller is responsible for freeing \p *end using crm_time_free().
  */
 int
 pcmk__unpack_duration(const xmlNode *duration, const crm_time_t *start,
                       crm_time_t **end)
 {
     const char *id = NULL;
     const char *parent_id = loggable_parent_id(duration);
 
     if ((start == NULL) || (duration == NULL)
         || (end == NULL) || (*end != NULL)) {
         return EINVAL;
     }
 
     // Get duration ID (for logging)
     id = pcmk__xe_id(duration);
     if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
         pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s "
                          "as not passing because " PCMK_XE_DURATION
                          " subelement has no " PCMK_XA_ID, parent_id);
         return pcmk_rc_unpack_error;
     }
 
     *end = pcmk_copy_time(start);
 
     ADD_COMPONENT(pcmk__time_years);
     ADD_COMPONENT(pcmk__time_months);
     ADD_COMPONENT(pcmk__time_weeks);
     ADD_COMPONENT(pcmk__time_days);
     ADD_COMPONENT(pcmk__time_hours);
     ADD_COMPONENT(pcmk__time_minutes);
     ADD_COMPONENT(pcmk__time_seconds);
 
     return pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Evaluate a range check for a given date/time
  *
  * \param[in]     date_expression  XML of PCMK_XE_DATE_EXPRESSION element
  * \param[in]     id               Expression ID for logging purposes
  * \param[in]     now              Date/time to compare
  * \param[in,out] next_change      If not NULL, set this to when the evaluation
  *                                 will change, if known and earlier than the
  *                                 original value
  *
  * \return Standard Pacemaker return code
  */
 static int
 evaluate_in_range(const xmlNode *date_expression, const char *id,
                   const crm_time_t *now, crm_time_t *next_change)
 {
     crm_time_t *start = NULL;
     crm_time_t *end = NULL;
 
     if (pcmk__xe_get_datetime(date_expression, PCMK_XA_START,
                               &start) != pcmk_rc_ok) {
         pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
                          "passing because " PCMK_XA_START " is invalid", id);
         return pcmk_rc_unpack_error;
     }
 
     if (pcmk__xe_get_datetime(date_expression, PCMK_XA_END,
                               &end) != pcmk_rc_ok) {
         pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
                          "passing because " PCMK_XA_END " is invalid", id);
         crm_time_free(start);
         return pcmk_rc_unpack_error;
     }
 
     if ((start == NULL) && (end == NULL)) {
         // Not possible with schema validation enabled
         pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
                          "passing because " PCMK_VALUE_IN_RANGE
                          " requires at least one of " PCMK_XA_START " or "
                          PCMK_XA_END, id);
         return pcmk_rc_unpack_error;
     }
 
     if (end == NULL) {
         xmlNode *duration = pcmk__xe_first_child(date_expression,
                                                  PCMK_XE_DURATION, NULL, NULL);
 
         if (duration != NULL) {
             int rc = pcmk__unpack_duration(duration, start, &end);
 
             if (rc != pcmk_rc_ok) {
                 pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION
                                  " %s as not passing because duration "
                                  "is invalid", id);
                 crm_time_free(start);
                 return rc;
             }
         }
     }
 
     if ((start != NULL) && (crm_time_compare(now, start) < 0)) {
         pcmk__set_time_if_earlier(next_change, start);
         crm_time_free(start);
         crm_time_free(end);
         return pcmk_rc_before_range;
     }
 
     if (end != NULL) {
         if (crm_time_compare(now, end) > 0) {
             crm_time_free(start);
             crm_time_free(end);
             return pcmk_rc_after_range;
         }
 
         // Evaluation doesn't change until second after end
         if (next_change != NULL) {
             crm_time_add_seconds(end, 1);
             pcmk__set_time_if_earlier(next_change, end);
         }
     }
 
     crm_time_free(start);
     crm_time_free(end);
     return pcmk_rc_within_range;
 }
 
 /*!
  * \internal
  * \brief Evaluate a greater-than check for a given date/time
  *
  * \param[in]     date_expression  XML of PCMK_XE_DATE_EXPRESSION element
  * \param[in]     id               Expression ID for logging purposes
  * \param[in]     now              Date/time to compare
  * \param[in,out] next_change      If not NULL, set this to when the evaluation
  *                                 will change, if known and earlier than the
  *                                 original value
  *
  * \return Standard Pacemaker return code
  */
 static int
 evaluate_gt(const xmlNode *date_expression, const char *id,
             const crm_time_t *now, crm_time_t *next_change)
 {
     crm_time_t *start = NULL;
 
     if (pcmk__xe_get_datetime(date_expression, PCMK_XA_START,
                               &start) != pcmk_rc_ok) {
         pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
                          "passing because " PCMK_XA_START " is invalid",
                          id);
         return pcmk_rc_unpack_error;
     }
 
     if (start == NULL) { // Not possible with schema validation enabled
         pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
                          "passing because " PCMK_VALUE_GT " requires "
                          PCMK_XA_START, id);
         return pcmk_rc_unpack_error;
     }
 
     if (crm_time_compare(now, start) > 0) {
         crm_time_free(start);
         return pcmk_rc_within_range;
     }
 
     // Evaluation doesn't change until second after start time
     crm_time_add_seconds(start, 1);
     pcmk__set_time_if_earlier(next_change, start);
     crm_time_free(start);
     return pcmk_rc_before_range;
 }
 
 /*!
  * \internal
  * \brief Evaluate a less-than check for a given date/time
  *
  * \param[in]     date_expression  XML of PCMK_XE_DATE_EXPRESSION element
  * \param[in]     id               Expression ID for logging purposes
  * \param[in]     now              Date/time to compare
  * \param[in,out] next_change      If not NULL, set this to when the evaluation
  *                                 will change, if known and earlier than the
  *                                 original value
  *
  * \return Standard Pacemaker return code
  */
 static int
 evaluate_lt(const xmlNode *date_expression, const char *id,
             const crm_time_t *now, crm_time_t *next_change)
 {
     crm_time_t *end = NULL;
 
     if (pcmk__xe_get_datetime(date_expression, PCMK_XA_END,
                               &end) != pcmk_rc_ok) {
         pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
                          "passing because " PCMK_XA_END " is invalid", id);
         return pcmk_rc_unpack_error;
     }
 
     if (end == NULL) { // Not possible with schema validation enabled
         pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
                          "passing because " PCMK_VALUE_GT " requires "
                          PCMK_XA_END, id);
         return pcmk_rc_unpack_error;
     }
 
     if (crm_time_compare(now, end) < 0) {
         pcmk__set_time_if_earlier(next_change, end);
         crm_time_free(end);
         return pcmk_rc_within_range;
     }
 
     crm_time_free(end);
     return pcmk_rc_after_range;
 }
 
 /*!
  * \internal
  * \brief Evaluate a rule's date expression for a given date/time
  *
  * \param[in]     date_expression  XML of a PCMK_XE_DATE_EXPRESSION element
  * \param[in]     now              Time to use for evaluation
  * \param[in,out] next_change      If not NULL, set this to when the evaluation
  *                                 will change, if known and earlier than the
  *                                 original value
  *
  * \return Standard Pacemaker return code (unlike most other evaluation
  *         functions, this can return either pcmk_rc_ok or pcmk_rc_within_range
  *         on success)
  */
 int
 pcmk__evaluate_date_expression(const xmlNode *date_expression,
                                const crm_time_t *now, crm_time_t *next_change)
 {
     const char *id = NULL;
     const char *op = NULL;
     int rc = pcmk_rc_ok;
 
     if ((date_expression == NULL) || (now == NULL)) {
         return EINVAL;
     }
 
     // Get expression ID (for logging)
     id = pcmk__xe_id(date_expression);
     if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
         pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " without "
                          PCMK_XA_ID " as not passing");
         return pcmk_rc_unpack_error;
     }
 
     op = pcmk__xe_get(date_expression, PCMK_XA_OPERATION);
     if (pcmk__str_eq(op, PCMK_VALUE_IN_RANGE,
                      pcmk__str_null_matches|pcmk__str_casei)) {
         rc = evaluate_in_range(date_expression, id, now, next_change);
 
     } else if (pcmk__str_eq(op, PCMK_VALUE_DATE_SPEC, pcmk__str_casei)) {
         xmlNode *date_spec = pcmk__xe_first_child(date_expression,
                                                   PCMK_XE_DATE_SPEC, NULL,
                                                   NULL);
 
         if (date_spec == NULL) { // Not possible with schema validation enabled
             pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s "
                              "as not passing because " PCMK_VALUE_DATE_SPEC
                              " operations require a " PCMK_XE_DATE_SPEC
                              " subelement", id);
             return pcmk_rc_unpack_error;
         }
 
         // @TODO set next_change appropriately
         rc = pcmk__evaluate_date_spec(date_spec, now);
 
     } else if (pcmk__str_eq(op, PCMK_VALUE_GT, pcmk__str_casei)) {
         rc = evaluate_gt(date_expression, id, now, next_change);
 
     } else if (pcmk__str_eq(op, PCMK_VALUE_LT, pcmk__str_casei)) {
         rc = evaluate_lt(date_expression, id, now, next_change);
 
     } else { // Not possible with schema validation enabled
         pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION
                          " %s as not passing because '%s' is not a valid "
                          PCMK_XE_OPERATION, id, op);
         return pcmk_rc_unpack_error;
     }
 
     crm_trace(PCMK_XE_DATE_EXPRESSION " %s (%s): %s (%d)",
               id, op, pcmk_rc_str(rc), rc);
     return rc;
 }
 
 /*!
  * \internal
  * \brief Go through submatches in a string, either counting how many bytes
  *        would be needed for the expansion, or performing the expansion,
  *        as requested
  *
  * \param[in]  string      String possibly containing submatch variables
  * \param[in]  match       String that matched the regular expression
  * \param[in]  submatches  Regular expression submatches (as set by regexec())
  * \param[in]  nmatches    Number of entries in \p submatches[]
  * \param[out] expansion   If not NULL, expand string here (must be
  *                         pre-allocated to appropriate size)
  * \param[out] nbytes      If not NULL, set to size needed for expansion
  *
  * \return true if any expansion is needed, otherwise false
  */
 static bool
 process_submatches(const char *string, const char *match,
                    const regmatch_t submatches[], int nmatches,
                    char *expansion, size_t *nbytes)
 {
     bool expanded = false;
     const char *src = string;
 
     if (nbytes != NULL) {
         *nbytes = 1; // Include space for terminator
     }
 
     while (*src != '\0') {
         int submatch = 0;
         size_t match_len = 0;
 
         if ((src[0] != '%') || !isdigit(src[1])) {
             /* src does not point to the first character of a %N sequence,
              * so expand this character as-is
              */
             if (expansion != NULL) {
                 *expansion++ = *src;
             }
             if (nbytes != NULL) {
                 ++(*nbytes);
             }
             ++src;
             continue;
         }
 
         submatch = src[1] - '0';
         src += 2; // Skip over %N sequence in source string
         expanded = true; // Expansion will be different from source
 
         // Omit sequence from expansion unless it has a non-empty match
         if ((nmatches <= submatch)                // Not enough submatches
             || (submatches[submatch].rm_so < 0)   // Pattern did not match
             || (submatches[submatch].rm_eo
                 <= submatches[submatch].rm_so)) { // Match was empty
             continue;
         }
 
         match_len = submatches[submatch].rm_eo - submatches[submatch].rm_so;
         if (nbytes != NULL) {
             *nbytes += match_len;
         }
         if (expansion != NULL) {
             memcpy(expansion, match + submatches[submatch].rm_so,
                    match_len);
             expansion += match_len;
         }
     }
 
     return expanded;
 }
 
 /*!
  * \internal
  * \brief Expand any regular expression submatches (%0-%9) in a string
  *
  * \param[in] string      String possibly containing submatch variables
  * \param[in] match       String that matched the regular expression
  * \param[in] submatches  Regular expression submatches (as set by regexec())
  * \param[in] nmatches    Number of entries in \p submatches[]
  *
  * \return Newly allocated string identical to \p string with submatches
  *         expanded on success, or NULL if no expansions were needed
  * \note The caller is responsible for freeing the result with free()
  */
 char *
 pcmk__replace_submatches(const char *string, const char *match,
                          const regmatch_t submatches[], int nmatches)
 {
     size_t nbytes = 0;
     char *result = NULL;
 
     if (pcmk__str_empty(string) || pcmk__str_empty(match)) {
         return NULL; // Nothing to expand
     }
 
     // Calculate how much space will be needed for expanded string
     if (!process_submatches(string, match, submatches, nmatches, NULL,
                             &nbytes)) {
         return NULL; // No expansions needed
     }
 
     // Allocate enough space for expanded string
     result = pcmk__assert_alloc(nbytes, sizeof(char));
 
     // Expand submatches
     (void) process_submatches(string, match, submatches, nmatches, result,
                               NULL);
     return result;
 }
 
 /*!
  * \internal
  * \brief Parse a comparison type from a string
  *
  * \param[in] op  String with comparison type (valid values are
  *                \c PCMK_VALUE_DEFINED, \c PCMK_VALUE_NOT_DEFINED,
  *                \c PCMK_VALUE_EQ, \c PCMK_VALUE_NE,
  *                \c PCMK_VALUE_LT, \c PCMK_VALUE_LTE,
  *                \c PCMK_VALUE_GT, or \c PCMK_VALUE_GTE)
  *
  * \return Comparison type corresponding to \p op
  */
 enum pcmk__comparison
 pcmk__parse_comparison(const char *op)
 {
     if (pcmk__str_eq(op, PCMK_VALUE_DEFINED, pcmk__str_casei)) {
         return pcmk__comparison_defined;
 
     } else if (pcmk__str_eq(op, PCMK_VALUE_NOT_DEFINED, pcmk__str_casei)) {
         return pcmk__comparison_undefined;
 
     } else if (pcmk__str_eq(op, PCMK_VALUE_EQ, pcmk__str_casei)) {
         return pcmk__comparison_eq;
 
     } else if (pcmk__str_eq(op, PCMK_VALUE_NE, pcmk__str_casei)) {
         return pcmk__comparison_ne;
 
     } else if (pcmk__str_eq(op, PCMK_VALUE_LT, pcmk__str_casei)) {
         return pcmk__comparison_lt;
 
     } else if (pcmk__str_eq(op, PCMK_VALUE_LTE, pcmk__str_casei)) {
         return pcmk__comparison_lte;
 
     } else if (pcmk__str_eq(op, PCMK_VALUE_GT, pcmk__str_casei)) {
         return pcmk__comparison_gt;
 
     } else if (pcmk__str_eq(op, PCMK_VALUE_GTE, pcmk__str_casei)) {
         return pcmk__comparison_gte;
     }
 
     return pcmk__comparison_unknown;
 }
 
 /*!
  * \internal
  * \brief Parse a value type from a string
  *
  * \param[in] type    String with value type (valid values are NULL,
  *                    \c PCMK_VALUE_STRING, \c PCMK_VALUE_INTEGER,
  *                    \c PCMK_VALUE_NUMBER, and \c PCMK_VALUE_VERSION)
  * \param[in] op      Operation type (used only to select default)
  * \param[in] value1  First value being compared (used only to select default)
  * \param[in] value2  Second value being compared (used only to select default)
  */
 enum pcmk__type
 pcmk__parse_type(const char *type, enum pcmk__comparison op,
                  const char *value1, const char *value2)
 {
     if (type == NULL) {
         switch (op) {
             case pcmk__comparison_lt:
             case pcmk__comparison_lte:
             case pcmk__comparison_gt:
             case pcmk__comparison_gte:
                 if (((value1 != NULL) && (strchr(value1, '.') != NULL))
                     || ((value2 != NULL) && (strchr(value2, '.') != NULL))) {
                     return pcmk__type_number;
                 }
                 return pcmk__type_integer;
 
             default:
                 return pcmk__type_string;
         }
     }
 
     if (pcmk__str_eq(type, PCMK_VALUE_STRING, pcmk__str_casei)) {
         return pcmk__type_string;
 
     } else if (pcmk__str_eq(type, PCMK_VALUE_INTEGER, pcmk__str_casei)) {
         return pcmk__type_integer;
 
     } else if (pcmk__str_eq(type, PCMK_VALUE_NUMBER, pcmk__str_casei)) {
         return pcmk__type_number;
 
     } else if (pcmk__str_eq(type, PCMK_VALUE_VERSION, pcmk__str_casei)) {
         return pcmk__type_version;
     }
 
     return pcmk__type_unknown;
 }
 
 /*!
  * \internal
  * \brief Compare two strings according to a given type
  *
  * \param[in] value1  String with first value to compare
  * \param[in] value2  String with second value to compare
  * \param[in] type    How to interpret the values
  *
  * \return Standard comparison result (a negative integer if \p value1 is
  *         lesser, 0 if the values are equal, and a positive integer if
  *         \p value1 is greater)
  */
 int
 pcmk__cmp_by_type(const char *value1, const char *value2, enum pcmk__type type)
 {
     //  NULL compares as less than non-NULL
     if (value2 == NULL) {
         return (value1 == NULL)? 0 : 1;
     }
     if (value1 == NULL) {
         return -1;
     }
 
     switch (type) {
         case pcmk__type_string:
             return strcasecmp(value1, value2);
 
         case pcmk__type_integer:
             {
                 long long integer1;
                 long long integer2;
 
                 if ((pcmk__scan_ll(value1, &integer1, 0LL) != pcmk_rc_ok)
                     || (pcmk__scan_ll(value2, &integer2, 0LL) != pcmk_rc_ok)) {
                     crm_warn("Comparing '%s' and '%s' as strings because "
                              "invalid as integers", value1, value2);
                     return strcasecmp(value1, value2);
                 }
                 return (integer1 < integer2)? -1 : (integer1 > integer2)? 1 : 0;
             }
             break;
 
         case pcmk__type_number:
             {
                 double num1;
                 double num2;
 
                 if ((pcmk__scan_double(value1, &num1, NULL, NULL) != pcmk_rc_ok)
                     || (pcmk__scan_double(value2, &num2, NULL,
                                           NULL) != pcmk_rc_ok)) {
                     crm_warn("Comparing '%s' and '%s' as strings because invalid as "
                              "numbers", value1, value2);
                     return strcasecmp(value1, value2);
                 }
                 return (num1 < num2)? -1 : (num1 > num2)? 1 : 0;
             }
             break;
 
         case pcmk__type_version:
-            return compare_version(value1, value2);
+            return pcmk__compare_versions(value1, value2);
 
         default: // Invalid type
             return 0;
     }
 }
 
 /*!
  * \internal
  * \brief Parse a reference value source from a string
  *
  * \param[in] source  String indicating reference value source
  *
  * \return Reference value source corresponding to \p source
  */
 enum pcmk__reference_source
 pcmk__parse_source(const char *source)
 {
     if (pcmk__str_eq(source, PCMK_VALUE_LITERAL,
                      pcmk__str_casei|pcmk__str_null_matches)) {
         return pcmk__source_literal;
 
     } else if (pcmk__str_eq(source, PCMK_VALUE_PARAM, pcmk__str_casei)) {
         return pcmk__source_instance_attrs;
 
     } else if (pcmk__str_eq(source, PCMK_VALUE_META, pcmk__str_casei)) {
         return pcmk__source_meta_attrs;
 
     } else {
         return pcmk__source_unknown;
     }
 }
 
 /*!
  * \internal
  * \brief Parse a boolean operator from a string
  *
  * \param[in] combine  String indicating boolean operator
  *
  * \return Enumeration value corresponding to \p combine
  */
 enum pcmk__combine
 pcmk__parse_combine(const char *combine)
 {
     if (pcmk__str_eq(combine, PCMK_VALUE_AND,
                      pcmk__str_null_matches|pcmk__str_casei)) {
         return pcmk__combine_and;
 
     } else if (pcmk__str_eq(combine, PCMK_VALUE_OR, pcmk__str_casei)) {
         return pcmk__combine_or;
 
     } else {
         return pcmk__combine_unknown;
     }
 }
 
 /*!
  * \internal
  * \brief Get the result of a node attribute comparison for rule evaluation
  *
  * \param[in] actual      Actual node attribute value
  * \param[in] reference   Node attribute value from rule (ignored for
  *                        \p comparison of \c pcmk__comparison_defined or
  *                        \c pcmk__comparison_undefined)
  * \param[in] type        How to interpret the values
  * \param[in] comparison  How to compare the values
  *
  * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok if the
  *         comparison passes, and some other value if it does not)
  */
 static int
 evaluate_attr_comparison(const char *actual, const char *reference,
                          enum pcmk__type type, enum pcmk__comparison comparison)
 {
     int cmp = 0;
 
     switch (comparison) {
         case pcmk__comparison_defined:
             return (actual != NULL)? pcmk_rc_ok : pcmk_rc_op_unsatisfied;
 
         case pcmk__comparison_undefined:
             return (actual == NULL)? pcmk_rc_ok : pcmk_rc_op_unsatisfied;
 
         default:
             break;
     }
 
     cmp = pcmk__cmp_by_type(actual, reference, type);
 
     switch (comparison) {
         case pcmk__comparison_eq:
             return (cmp == 0)? pcmk_rc_ok : pcmk_rc_op_unsatisfied;
 
         case pcmk__comparison_ne:
             return (cmp != 0)? pcmk_rc_ok : pcmk_rc_op_unsatisfied;
 
         default:
             break;
     }
 
     if ((actual == NULL) || (reference == NULL)) {
         return pcmk_rc_op_unsatisfied; // Comparison would be meaningless
     }
 
     switch (comparison) {
         case pcmk__comparison_lt:
             return (cmp < 0)? pcmk_rc_ok : pcmk_rc_after_range;
 
         case pcmk__comparison_lte:
             return (cmp <= 0)? pcmk_rc_ok : pcmk_rc_after_range;
 
         case pcmk__comparison_gt:
             return (cmp > 0)? pcmk_rc_ok : pcmk_rc_before_range;
 
         case pcmk__comparison_gte:
             return (cmp >= 0)? pcmk_rc_ok : pcmk_rc_before_range;
 
         default: // Not possible with schema validation enabled
             return pcmk_rc_op_unsatisfied;
     }
 }
 
 /*!
  * \internal
  * \brief Get a reference value from a configured source
  *
  * \param[in] value       Value given in rule expression
  * \param[in] source      Reference value source
  * \param[in] rule_input  Values used to evaluate rule criteria
  */
 static const char *
 value_from_source(const char *value, enum pcmk__reference_source source,
                   const pcmk_rule_input_t *rule_input)
 {
     GHashTable *table = NULL;
 
     switch (source) {
         case pcmk__source_literal:
             return value;
 
         case pcmk__source_instance_attrs:
             table = rule_input->rsc_params;
             break;
 
         case pcmk__source_meta_attrs:
             table = rule_input->rsc_meta;
             break;
 
         default:
             return NULL; // Not possible
     }
 
     if (table == NULL) {
         return NULL;
     }
     return (const char *) g_hash_table_lookup(table, value);
 }
 
 /*!
  * \internal
  * \brief Evaluate a node attribute rule expression
  *
  * \param[in] expression  XML of a rule's PCMK_XE_EXPRESSION subelement
  * \param[in] rule_input  Values used to evaluate rule criteria
  *
  * \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression
  *         passes, some other value if it does not)
  */
 int
 pcmk__evaluate_attr_expression(const xmlNode *expression,
                                const pcmk_rule_input_t *rule_input)
 {
     const char *id = NULL;
     const char *op = NULL;
     const char *attr = NULL;
     const char *type_s = NULL;
     const char *value = NULL;
     const char *actual = NULL;
     const char *source_s = NULL;
     const char *reference = NULL;
     char *expanded_attr = NULL;
     int rc = pcmk_rc_ok;
 
     enum pcmk__type type = pcmk__type_unknown;
     enum pcmk__reference_source source = pcmk__source_unknown;
     enum pcmk__comparison comparison = pcmk__comparison_unknown;
 
     if ((expression == NULL) || (rule_input == NULL)) {
         return EINVAL;
     }
 
     // Get expression ID (for logging)
     id = pcmk__xe_id(expression);
     if (pcmk__str_empty(id)) {
         pcmk__config_err("Treating " PCMK_XE_EXPRESSION " without " PCMK_XA_ID
                          " as not passing");
         return pcmk_rc_unpack_error;
     }
 
     /* Get name of node attribute to compare (expanding any %0-%9 to
      * regular expression submatches)
      */
     attr = pcmk__xe_get(expression, PCMK_XA_ATTRIBUTE);
     if (attr == NULL) {
         pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not passing "
                          "because " PCMK_XA_ATTRIBUTE " was not specified", id);
         return pcmk_rc_unpack_error;
     }
     expanded_attr = pcmk__replace_submatches(attr, rule_input->rsc_id,
                                              rule_input->rsc_id_submatches,
                                              rule_input->rsc_id_nmatches);
     if (expanded_attr != NULL) {
         attr = expanded_attr;
     }
 
     // Get and validate operation
     op = pcmk__xe_get(expression, PCMK_XA_OPERATION);
     comparison = pcmk__parse_comparison(op);
     if (comparison == pcmk__comparison_unknown) {
         // Not possible with schema validation enabled
         if (op == NULL) {
             pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not "
                              "passing because it has no " PCMK_XA_OPERATION,
                              id);
         } else {
             pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not "
                              "passing because '%s' is not a valid "
                              PCMK_XA_OPERATION, id, op);
         }
         rc = pcmk_rc_unpack_error;
         goto done;
     }
 
     // How reference value is obtained (literal, resource meta-attribute, etc.)
     source_s = pcmk__xe_get(expression, PCMK_XA_VALUE_SOURCE);
     source = pcmk__parse_source(source_s);
     if (source == pcmk__source_unknown) {
         // Not possible with schema validation enabled
         pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not passing "
                          "because '%s' is not a valid " PCMK_XA_VALUE_SOURCE,
                          id, source_s);
         rc = pcmk_rc_unpack_error;
         goto done;
     }
 
     // Get and validate reference value
     value = pcmk__xe_get(expression, PCMK_XA_VALUE);
     switch (comparison) {
         case pcmk__comparison_defined:
         case pcmk__comparison_undefined:
             if (value != NULL) {
                 pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not "
                                  "passing because " PCMK_XA_VALUE " is not "
                                  "allowed when " PCMK_XA_OPERATION " is %s",
                                  id, op);
                 rc = pcmk_rc_unpack_error;
                 goto done;
             }
             break;
 
         default:
             if (value == NULL) {
                 pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not "
                                  "passing because " PCMK_XA_VALUE " is "
                                  "required when " PCMK_XA_OPERATION " is %s",
                                  id, op);
                 rc = pcmk_rc_unpack_error;
                 goto done;
             }
             reference = value_from_source(value, source, rule_input);
             break;
     }
 
     // Get actual value of node attribute
     if (rule_input->node_attrs != NULL) {
         actual = g_hash_table_lookup(rule_input->node_attrs, attr);
     }
 
     // Get and validate value type (after expanding reference value)
     type_s = pcmk__xe_get(expression, PCMK_XA_TYPE);
     type = pcmk__parse_type(type_s, comparison, actual, reference);
     if (type == pcmk__type_unknown) {
         // Not possible with schema validation enabled
         pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not passing "
                          "because '%s' is not a valid type", id, type_s);
         rc = pcmk_rc_unpack_error;
         goto done;
     }
 
     rc = evaluate_attr_comparison(actual, reference, type, comparison);
     switch (comparison) {
         case pcmk__comparison_defined:
         case pcmk__comparison_undefined:
             crm_trace(PCMK_XE_EXPRESSION " %s result: %s (for attribute %s %s)",
                       id, pcmk_rc_str(rc), attr, op);
             break;
 
         default:
             crm_trace(PCMK_XE_EXPRESSION " %s result: "
                       "%s (attribute %s %s '%s' via %s source as %s type)",
                       id, pcmk_rc_str(rc), attr, op, pcmk__s(reference, ""),
                       pcmk__s(source_s, "default"), pcmk__s(type_s, "default"));
             break;
     }
 
 done:
     free(expanded_attr);
     return rc;
 }
 
 /*!
  * \internal
  * \brief Evaluate a resource rule expression
  *
  * \param[in] rsc_expression  XML of rule's \c PCMK_XE_RSC_EXPRESSION subelement
  * \param[in] rule_input      Values used to evaluate rule criteria
  *
  * \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression
  *         passes, some other value if it does not)
  */
 int
 pcmk__evaluate_rsc_expression(const xmlNode *rsc_expression,
                               const pcmk_rule_input_t *rule_input)
 {
     const char *id = NULL;
     const char *standard = NULL;
     const char *provider = NULL;
     const char *type = NULL;
 
     if ((rsc_expression == NULL) || (rule_input == NULL)) {
         return EINVAL;
     }
 
     // Validate XML ID
     id = pcmk__xe_id(rsc_expression);
     if (pcmk__str_empty(id)) {
         // Not possible with schema validation enabled
         pcmk__config_err("Treating " PCMK_XE_RSC_EXPRESSION " without "
                          PCMK_XA_ID " as not passing");
         return pcmk_rc_unpack_error;
     }
 
     // Compare resource standard
     standard = pcmk__xe_get(rsc_expression, PCMK_XA_CLASS);
     if ((standard != NULL)
         && !pcmk__str_eq(standard, rule_input->rsc_standard, pcmk__str_none)) {
         crm_trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because "
                   "actual standard '%s' doesn't match '%s'",
                   id, pcmk__s(rule_input->rsc_standard, ""), standard);
         return pcmk_rc_op_unsatisfied;
     }
 
     // Compare resource provider
     provider = pcmk__xe_get(rsc_expression, PCMK_XA_PROVIDER);
     if ((provider != NULL)
         && !pcmk__str_eq(provider, rule_input->rsc_provider, pcmk__str_none)) {
         crm_trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because "
                   "actual provider '%s' doesn't match '%s'",
                   id, pcmk__s(rule_input->rsc_provider, ""), provider);
         return pcmk_rc_op_unsatisfied;
     }
 
     // Compare resource agent type
     type = pcmk__xe_get(rsc_expression, PCMK_XA_TYPE);
     if ((type != NULL)
         && !pcmk__str_eq(type, rule_input->rsc_agent, pcmk__str_none)) {
         crm_trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because "
                   "actual agent '%s' doesn't match '%s'",
                   id, pcmk__s(rule_input->rsc_agent, ""), type);
         return pcmk_rc_op_unsatisfied;
     }
 
     crm_trace(PCMK_XE_RSC_EXPRESSION " %s is satisfied by %s%s%s:%s",
               id, pcmk__s(standard, ""),
               ((provider == NULL)? "" : ":"), pcmk__s(provider, ""),
               pcmk__s(type, ""));
     return pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Evaluate an operation rule expression
  *
  * \param[in] op_expression  XML of a rule's \c PCMK_XE_OP_EXPRESSION subelement
  * \param[in] rule_input     Values used to evaluate rule criteria
  *
  * \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression
  *         is satisfied, some other value if it is not)
  */
 int
 pcmk__evaluate_op_expression(const xmlNode *op_expression,
                              const pcmk_rule_input_t *rule_input)
 {
     const char *id = NULL;
     const char *name = NULL;
     const char *interval_s = NULL;
     guint interval_ms = 0U;
 
     if ((op_expression == NULL) || (rule_input == NULL)) {
         return EINVAL;
     }
 
     // Get operation expression ID (for logging)
     id = pcmk__xe_id(op_expression);
     if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
         pcmk__config_err("Treating " PCMK_XE_OP_EXPRESSION " without "
                          PCMK_XA_ID " as not passing");
         return pcmk_rc_unpack_error;
     }
 
     // Validate operation name
     name = pcmk__xe_get(op_expression, PCMK_XA_NAME);
     if (name == NULL) { // Not possible with schema validation enabled
         pcmk__config_err("Treating " PCMK_XE_OP_EXPRESSION " %s as not "
                          "passing because it has no " PCMK_XA_NAME, id);
         return pcmk_rc_unpack_error;
     }
 
     // Validate operation interval
     interval_s = pcmk__xe_get(op_expression, PCMK_META_INTERVAL);
     if (pcmk_parse_interval_spec(interval_s, &interval_ms) != pcmk_rc_ok) {
         pcmk__config_err("Treating " PCMK_XE_OP_EXPRESSION " %s as not "
                          "passing because '%s' is not a valid "
                          PCMK_META_INTERVAL,
                          id, interval_s);
         return pcmk_rc_unpack_error;
     }
 
     // Compare operation name
     if (!pcmk__str_eq(name, rule_input->op_name, pcmk__str_none)) {
         crm_trace(PCMK_XE_OP_EXPRESSION " %s is unsatisfied because "
                   "actual name '%s' doesn't match '%s'",
                   id, pcmk__s(rule_input->op_name, ""), name);
         return pcmk_rc_op_unsatisfied;
     }
 
     // Compare operation interval (unspecified interval matches all)
     if ((interval_s != NULL) && (interval_ms != rule_input->op_interval_ms)) {
         crm_trace(PCMK_XE_OP_EXPRESSION " %s is unsatisfied because "
                   "actual interval %s doesn't match %s",
                   id, pcmk__readable_interval(rule_input->op_interval_ms),
                   pcmk__readable_interval(interval_ms));
         return pcmk_rc_op_unsatisfied;
     }
 
     crm_trace(PCMK_XE_OP_EXPRESSION " %s is satisfied (name %s, interval %s)",
               id, name, pcmk__readable_interval(rule_input->op_interval_ms));
     return pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Evaluate a rule condition
  *
  * \param[in,out] condition    XML containing a rule condition (a subrule, or an
  *                             expression of any type)
  * \param[in]     rule_input   Values used to evaluate rule criteria
  * \param[out]    next_change  If not NULL, set to when evaluation will change
  *
  * \return Standard Pacemaker return code (\c pcmk_rc_ok if the condition
  *         passes, some other value if it does not)
  */
 int
 pcmk__evaluate_condition(xmlNode *condition,
                          const pcmk_rule_input_t *rule_input,
                          crm_time_t *next_change)
 {
 
     if ((condition == NULL) || (rule_input == NULL)) {
         return EINVAL;
     }
 
     switch (pcmk__condition_type(condition)) {
         case pcmk__condition_rule:
             return pcmk_evaluate_rule(condition, rule_input, next_change);
 
         case pcmk__condition_attribute:
         case pcmk__condition_location:
             return pcmk__evaluate_attr_expression(condition, rule_input);
 
         case pcmk__condition_datetime:
             {
                 int rc = pcmk__evaluate_date_expression(condition,
                                                         rule_input->now,
                                                         next_change);
 
                 return (rc == pcmk_rc_within_range)? pcmk_rc_ok : rc;
             }
 
         case pcmk__condition_resource:
             return pcmk__evaluate_rsc_expression(condition, rule_input);
 
         case pcmk__condition_operation:
             return pcmk__evaluate_op_expression(condition, rule_input);
 
         default: // Not possible with schema validation enabled
             pcmk__config_err("Treating rule condition %s as not passing "
                              "because %s is not a valid condition type",
                              pcmk__s(pcmk__xe_id(condition), "without ID"),
                              (const char *) condition->name);
             return pcmk_rc_unpack_error;
     }
 }
 
 /*!
  * \brief Evaluate a single rule, including all its conditions
  *
  * \param[in,out] rule         XML containing a rule definition or its id-ref
  * \param[in]     rule_input   Values used to evaluate rule criteria
  * \param[out]    next_change  If not NULL, set to when evaluation will change
  *
  * \return Standard Pacemaker return code (\c pcmk_rc_ok if the rule is
  *         satisfied, some other value if it is not)
  */
 int
 pcmk_evaluate_rule(xmlNode *rule, const pcmk_rule_input_t *rule_input,
                    crm_time_t *next_change)
 {
     bool empty = true;
     int rc = pcmk_rc_ok;
     const char *id = NULL;
     const char *value = NULL;
     enum pcmk__combine combine = pcmk__combine_unknown;
 
     if ((rule == NULL) || (rule_input == NULL)) {
         return EINVAL;
     }
 
     rule = pcmk__xe_resolve_idref(rule, NULL);
     if (rule == NULL) {
         // Not possible with schema validation enabled; message already logged
         return pcmk_rc_unpack_error;
     }
 
     // Validate XML ID
     id = pcmk__xe_id(rule);
     if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
         pcmk__config_err("Treating " PCMK_XE_RULE " without " PCMK_XA_ID
                          " as not passing");
         return pcmk_rc_unpack_error;
     }
 
     value = pcmk__xe_get(rule, PCMK_XA_BOOLEAN_OP);
     combine = pcmk__parse_combine(value);
     switch (combine) {
         case pcmk__combine_and:
             // For "and", rc defaults to success (reset on failure below)
             break;
 
         case pcmk__combine_or:
             // For "or", rc defaults to failure (reset on success below)
             rc = pcmk_rc_op_unsatisfied;
             break;
 
         default: // Not possible with schema validation enabled
             pcmk__config_err("Treating " PCMK_XE_RULE " %s as not passing "
                              "because '%s' is not a valid " PCMK_XA_BOOLEAN_OP,
                              id, value);
             return pcmk_rc_unpack_error;
     }
 
     // Evaluate each condition
     for (xmlNode *condition = pcmk__xe_first_child(rule, NULL, NULL, NULL);
          condition != NULL; condition = pcmk__xe_next(condition, NULL)) {
 
         empty = false;
         if (pcmk__evaluate_condition(condition, rule_input,
                                      next_change) == pcmk_rc_ok) {
             if (combine == pcmk__combine_or) {
                 rc = pcmk_rc_ok; // Any pass is final for "or"
                 break;
             }
         } else if (combine == pcmk__combine_and) {
             rc = pcmk_rc_op_unsatisfied; // Any failure is final for "and"
             break;
         }
     }
 
     if (empty) { // Not possible with schema validation enabled
         pcmk__config_warn("Ignoring rule %s because it contains no conditions",
                           id);
         rc = pcmk_rc_ok;
     }
 
     crm_trace("Rule %s is %ssatisfied", id, ((rc == pcmk_rc_ok)? "" : "not "));
     return rc;
 }
diff --git a/lib/common/tests/utils/Makefile.am b/lib/common/tests/utils/Makefile.am
index 9e78afe01d..682ca21bab 100644
--- a/lib/common/tests/utils/Makefile.am
+++ b/lib/common/tests/utils/Makefile.am
@@ -1,25 +1,25 @@
 #
 # Copyright 2020-2025 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 $(top_srcdir)/mk/common.mk
 include $(top_srcdir)/mk/tap.mk
 include $(top_srcdir)/mk/unittest.mk
 
 # Add "_test" to the end of all test program names to simplify .gitignore.
-check_PROGRAMS = compare_version_test			\
+check_PROGRAMS = pcmk__compare_versions_test		\
 		 pcmk__daemon_user_test			\
 		 pcmk__fail_attr_name_test 		\
 		 pcmk__failcount_name_test 		\
 		 pcmk__getpid_s_test 			\
 		 pcmk__lastfailure_name_test 		\
 		 pcmk__lookup_user_test			\
 		 pcmk__realloc_test 			\
 		 pcmk__timeout_ms2s_test
 
 TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/utils/compare_version_test.c b/lib/common/tests/utils/pcmk__compare_versions_test.c
similarity index 68%
rename from lib/common/tests/utils/compare_version_test.c
rename to lib/common/tests/utils/pcmk__compare_versions_test.c
index 31d1a034a6..f9da064791 100644
--- a/lib/common/tests/utils/compare_version_test.c
+++ b/lib/common/tests/utils/pcmk__compare_versions_test.c
@@ -1,328 +1,318 @@
 /*
  * Copyright 2022-2025 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 <crm/common/unittest_internal.h>
 
 /*!
  * \internal
  * \brief Compare two version strings in both directions
  *
- * \param[in] v1           First argument for \c compare_version()
- * \param[in] v2           Second argument for \c compare_version()
+ * \param[in] v1           First argument for \c pcmk__compare_versions()
+ * \param[in] v2           Second argument for \c pcmk__compare_versions()
  * \param[in] expected_rc  Expected return code from
- *                         <tt>compare_version(v1, v2)</tt>
+ *                         <tt>pcmk__compare_versions(v1, v2)</tt>
  */
 static void
 assert_compare_version(const char *v1, const char *v2, int expected_rc)
 {
-    assert_int_equal(compare_version(v1, v2), expected_rc);
+    assert_int_equal(pcmk__compare_versions(v1, v2), expected_rc);
 
     if (v1 != v2) {
         /* Try reverse order even if expected_rc == 0, if v1 and v2 are
          * different strings
          */
-        assert_int_equal(compare_version(v2, v1), -expected_rc);
+        assert_int_equal(pcmk__compare_versions(v2, v1), -expected_rc);
     }
 }
 
 static void
 empty_params(void **state)
 {
-    // @FIXME Treat empty string the same as NULL
     assert_compare_version(NULL, NULL, 0);
-    assert_compare_version(NULL, "", -1);   // Should be 0
+    assert_compare_version(NULL, "", 0);
     assert_compare_version("", "", 0);
 
     assert_compare_version(NULL, "1.0.1", -1);
     assert_compare_version("", "1.0.1", -1);
 
     // @FIXME NULL/empty should be equal to an invalid version
     assert_compare_version(NULL, "abc", -1);    // Should be 0
     assert_compare_version("", "abc", -1);      // Should be 0
 }
 
 static void
 equal_versions(void **state)
 {
     assert_compare_version("0.4.7", "0.4.7", 0);
     assert_compare_version("1.0", "1.0", 0);
 }
 
 static void
 unequal_versions(void **state)
 {
     assert_compare_version("0.4.7", "0.4.8", -1);
     assert_compare_version("0.2.3", "0.3", -1);
     assert_compare_version("0.99", "1.0", -1);
 }
 
 static void
 shorter_versions(void **state)
 {
     assert_compare_version("1.0", "1.0.1", -1);
     assert_compare_version("1.0", "1", 0);
     assert_compare_version("1", "1.2", -1);
     assert_compare_version("1.0.0", "1.0", 0);
     assert_compare_version("1.0.0", "1.2", -1);
     assert_compare_version("0.99", "1", -1);
 }
 
 static void
 leading_zeros(void **state)
 {
     // Equal to self
     assert_compare_version("00001.0", "00001.0", 0);
 
     // Leading zeros in each segment are ignored
     assert_compare_version("0001.0", "1", 0);
     assert_compare_version("0.0001", "0.1", 0);
     assert_compare_version("0001.1", "1.0001", 0);
 }
 
 static void
 negative_sign(void **state)
 {
     // Equal to self
     assert_compare_version("-1", "-1", 0);
     assert_compare_version("1.-1.5", "1.-1.5", 0);
 
-    // @FIXME Treat negative version as 0 (invalid)
-    assert_compare_version("-1", "0", -1);          // Should be 0
-    assert_compare_version("-1", "0.0", -1);        // Should be 0
+    // Negative version is treated as 0 (invalid)
+    assert_compare_version("-1", "0", 0);
+    assert_compare_version("-1", "0.0", 0);
     assert_compare_version("-1", "0.1", -1);
     assert_compare_version("-1", "1.0", -1);
 
-    assert_compare_version("-1", "-0", -1);         // Should be 0
-    assert_compare_version("-1", "-0.0", -1);       // Should be 0
-    assert_compare_version("-1", "-0.1", -1);       // Should be 0
+    assert_compare_version("-1", "-0", 0);
+    assert_compare_version("-1", "-0.0", 0);
+    assert_compare_version("-1", "-0.1", 0);
     assert_compare_version("-1", "-1.0", 0);
-    assert_compare_version("-1", "-2.0", 1);        // Should be 0
+    assert_compare_version("-1", "-2.0", 0);
 
-    // @FIXME Treat negative sign inside version as garbage
-    assert_compare_version("1.-1.5", "1.0", -1);    // Should be 0
+    // Negative sign inside version is treated as garbage
+    assert_compare_version("1.-1.5", "1.0", 0);
     assert_compare_version("1.-1.5", "1.0.5", -1);
 
-    assert_compare_version("1.-1.5", "1.-0", -1);   // Should be 0
-    assert_compare_version("1.-1.5", "1.-0.5", -1); // Should be 0
+    assert_compare_version("1.-1.5", "1.-0", 0);
+    assert_compare_version("1.-1.5", "1.-0.5", 0);
 
-    assert_compare_version("1.-1.5", "1.-1", 1);    // Should be 0
-    assert_compare_version("1.-1.5", "1.-1.9", -1); // Should be 0
+    assert_compare_version("1.-1.5", "1.-1", 0);
+    assert_compare_version("1.-1.5", "1.-1.9", 0);
 
-    assert_compare_version("1.-1.5", "1.-2", 1);    // Should be 0
-    assert_compare_version("1.-1.5", "1.-2.5", 1);  // Should be 0
+    assert_compare_version("1.-1.5", "1.-2", 0);
+    assert_compare_version("1.-1.5", "1.-2.5", 0);
 
     assert_compare_version("1.-1.5", "2.0.5", -1);
     assert_compare_version("1.-1.5", "0.0.5", 1);
 }
 
 static void
 positive_sign(void **state)
 {
     // Equal to self
     assert_compare_version("+1", "+1", 0);
     assert_compare_version("1.+1.5", "1.+1.5", 0);
 
     // @FIXME Treat version with explicit positive sign as 0 (invalid)
-    assert_compare_version("+1", "0", 1);           // Should be 0
-    assert_compare_version("+1", "0.0", 1);         // Should be 0
-    assert_compare_version("+1", "0.1", 1);         // Should be -1
-    assert_compare_version("+1", "1.0", 0);         // Should be -1
-    assert_compare_version("+1", "2.0", -1);        // Should be -1
-
-    assert_compare_version("+1", "+0", 1);          // Should be 0
-    assert_compare_version("+1", "+0.0", 1);        // Should be 0
-    assert_compare_version("+1", "+0.1", 1);        // Should be 0
+    assert_compare_version("+1", "0", 0);
+    assert_compare_version("+1", "0.0", 0);
+    assert_compare_version("+1", "0.1", -1);
+    assert_compare_version("+1", "1.0", -1);
+    assert_compare_version("+1", "2.0", -1);
+
+    assert_compare_version("+1", "+0", 0);
+    assert_compare_version("+1", "+0.0", 0);
+    assert_compare_version("+1", "+0.1", 0);
     assert_compare_version("+1", "+1.0", 0);
-    assert_compare_version("+1", "+2.0", -1);       // Should be 0
+    assert_compare_version("+1", "+2.0", 0);
 
     // @FIXME Treat positive sign inside version as garbage
-    assert_compare_version("1.+1.5", "1.0", 1);     // Should be 0
-    assert_compare_version("1.+1.5", "1.0.5", 1);   // Should be -1
+    assert_compare_version("1.+1.5", "1.0", 0);
+    assert_compare_version("1.+1.5", "1.0.5", -1);
 
-    assert_compare_version("1.+1.5", "1.+0", 1);    // Should be 0
-    assert_compare_version("1.+1.5", "1.+0.5", 1);  // Should be 0
+    assert_compare_version("1.+1.5", "1.+0", 0);
+    assert_compare_version("1.+1.5", "1.+0.5", 0);
 
-    assert_compare_version("1.+1.5", "1.+1", 1);    // Should be 0
-    assert_compare_version("1.+1.5", "1.+1.9", -1); // Should be 0
+    assert_compare_version("1.+1.5", "1.+1", 0);
+    assert_compare_version("1.+1.5", "1.+1.9", 0);
 
-    assert_compare_version("1.+1.5", "1.+2", -1);   // Should be 0
-    assert_compare_version("1.+1.5", "1.+2.5", -1); // Should be 0
+    assert_compare_version("1.+1.5", "1.+2", 0);
+    assert_compare_version("1.+1.5", "1.+2.5", 0);
 
     assert_compare_version("1.+1.5", "2.0.5", -1);
     assert_compare_version("1.+1.5", "0.0.5", 1);
 }
 
-/*
 static void
 hex_digits(void **state)
 {
     // Equal to self
     assert_compare_version("a", "a", 0);
 
     // Hex digits > 9 are garbage
     assert_compare_version("a", "0", 0);
     assert_compare_version("a111", "0", 0);
     assert_compare_version("a", "1", -1);
     assert_compare_version("a111", "1", -1);
 
     assert_compare_version("1a", "1", 0);
     assert_compare_version("1a111", "1", 0);
     assert_compare_version("1a", "2", -1);
     assert_compare_version("1a111", "2", -1);
     assert_compare_version("1a", "0", 1);
     assert_compare_version("1a111", "0", 1);
 }
-*/
 
 static void
 bare_dot(void **state)
 {
     // Equal to self
     assert_compare_version(".", ".", 0);
 
     // Bare dot is treated as 0
     assert_compare_version(".", "0", 0);
     assert_compare_version(".", "0.1", -1);
     assert_compare_version(".", "1.0", -1);
 }
 
 static void
 leading_dot(void **state)
 {
     // Equal to self
     assert_compare_version(".0", ".0", 0);
     assert_compare_version(".1", ".1", 0);
 
-    // Leading dot is treated as 0
+    // Version with leading dot is treated as 0
     assert_compare_version(".0", "0", 0);
     assert_compare_version(".0", "0.0", 0);
     assert_compare_version(".0", "0.0.0", 0);
     assert_compare_version(".0", "0.1", -1);
 
-    // @FIXME .1 should equal 0, not 0.1
-    assert_compare_version(".1", "0", 1);
-    assert_compare_version(".1", "0.0", 1);
-    assert_compare_version(".1", "0.0.0", 1);
-    assert_compare_version(".1", "0.1", 0);
-    assert_compare_version(".1", "0.1.0", 0);
-    assert_compare_version(".1", "0.2", -1);
+    assert_compare_version(".1", "0", 0);
+    assert_compare_version(".1", "0.0", 0);
+    assert_compare_version(".1", "0.0.0", 0);
+    assert_compare_version(".1", "0.1", -1);
+    assert_compare_version(".1", "0.1.0", -1);
 }
 
 static void
 trailing_dot(void **state)
 {
     // Equal to self
     assert_compare_version("0.", "0.", 0);
     assert_compare_version("0.1.", "0.1.", 0);
 
     // Trailing dot is ignored
     assert_compare_version("0.", "0", 0);
     assert_compare_version("0.", "0.0", 0);
     assert_compare_version("0.", "0.1", -1);
     assert_compare_version("0.1.", "0.1", 0);
     assert_compare_version("0.1.", "0.1.0", 0);
     assert_compare_version("0.1.", "0.2", -1);
     assert_compare_version("0.1.", "0", 1);
 }
 
 static void
 leading_spaces(void **state)
 {
     // Equal to self
     assert_compare_version("    ", "    ", 0);
     assert_compare_version("   1", "   1", 0);
 
     // Leading spaces are ignored
     assert_compare_version("   1", "1.0", 0);
     assert_compare_version("1", "   1.0", 0);
     assert_compare_version("   1", "   1.0", 0);
     assert_compare_version("   1", "1.1", -1);
     assert_compare_version("1", "   1.1", -1);
     assert_compare_version("   1", "   1.1", -1);
 }
 
-/*
 static void
 trailing_spaces(void **state)
 {
     // Equal to self
     assert_compare_version("1   ", "1   ", 0);
 
     // Trailing spaces are ignored
     assert_compare_version("1   ", "1.0", 0);
     assert_compare_version("1", "1.0   ", 0);
     assert_compare_version("1   ", "1.0   ", 0);
     assert_compare_version("1   ", "1.1", -1);
     assert_compare_version("1", "1.1   ", -1);
     assert_compare_version("1   ", "1.1   ", -1);
 }
 
 static void
 leading_garbage(void **state)
 {
     // Equal to self
     assert_compare_version("@1", "@1", 0);
 
-    // Leading garbage means rest of string is ignored
+    // Version with leading garbage is treated as 0
     assert_compare_version("@1", "0", 0);
     assert_compare_version("@1", "1", -1);
 
     assert_compare_version("@0.1", "0", 0);
     assert_compare_version("@0.1", "1", -1);
 }
 
 static void
 trailing_garbage(void **state)
 {
     // Equal to self
     assert_compare_version("0.1@", "0.1@", 0);
 
     // Trailing garbage is ignored
     assert_compare_version("0.1@", "0.1", 0);
     assert_compare_version("0.1.@", "0.1", 0);
     assert_compare_version("0.1    @", "0.1", 0);
     assert_compare_version("0.1.    @", "0.1", 0);
     assert_compare_version("0.1    .@", "0.1", 0);
 
     // This includes more numbers after spaces
     assert_compare_version("0.1    1", "0.1", 0);
     assert_compare_version("0.1.    1", "0.1", 0);
     assert_compare_version("0.1    .1", "0.1", 0);
 
-    // Second consecutive dot is treated as garbage (end of valid input)
+    // Second consecutive dot is treated as garbage
     assert_compare_version("1..", "1", 0);
     assert_compare_version("1..1", "1", 0);
     assert_compare_version("1..", "1.0.0", 0);
     assert_compare_version("1..1", "1.0.0", 0);
     assert_compare_version("1..", "1.0.1", -1);
     assert_compare_version("1..1", "1.0.1", -1);
 }
-*/
 
-// @FIXME Commented-out tests cause infinite loops
 PCMK__UNIT_TEST(NULL, NULL,
                 cmocka_unit_test(empty_params),
                 cmocka_unit_test(equal_versions),
                 cmocka_unit_test(unequal_versions),
                 cmocka_unit_test(shorter_versions),
                 cmocka_unit_test(leading_zeros),
                 cmocka_unit_test(negative_sign),
                 cmocka_unit_test(positive_sign),
-                //cmocka_unit_test(hex_digits),
+                cmocka_unit_test(hex_digits),
                 cmocka_unit_test(bare_dot),
                 cmocka_unit_test(leading_dot),
                 cmocka_unit_test(trailing_dot),
-                cmocka_unit_test(leading_spaces))
-                /*
-                cmocka_unit_test(trailing_spaces))
-                cmocka_unit_test(leading_garbage))
+                cmocka_unit_test(leading_spaces),
+                cmocka_unit_test(trailing_spaces),
+                cmocka_unit_test(leading_garbage),
                 cmocka_unit_test(trailing_garbage))
-                */
diff --git a/lib/common/utils.c b/lib/common/utils.c
index 7b19d634d8..acf3d8d603 100644
--- a/lib/common/utils.c
+++ b/lib/common/utils.c
@@ -1,594 +1,710 @@
 /*
  * Copyright 2004-2025 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <sys/stat.h>
 #include <sys/utsname.h>
 
 #include <stdio.h>
 #include <unistd.h>
 #include <string.h>
 #include <stdlib.h>
 #include <limits.h>
 #include <pwd.h>
 #include <time.h>
 #include <libgen.h>
 #include <signal.h>
 #include <grp.h>
 
 #include <qb/qbdefs.h>
 
 #include <crm/crm.h>
 #include <crm/services.h>
 #include <crm/cib/internal.h>
 #include <crm/common/xml.h>
 #include <crm/common/util.h>
 #include <crm/common/ipc.h>
 #include <crm/common/iso8601.h>
 #include <crm/common/mainloop.h>
 #include <libxml2/libxml/relaxng.h>
 
 #include "crmcommon_private.h"
 
 CRM_TRACE_INIT_DATA(common);
 
 bool pcmk__config_has_error = false;
 bool pcmk__config_has_warning = false;
 char *crm_system_name = NULL;
 
 /*!
  * \brief Free all memory used by libcrmcommon
  *
  * Free all global memory allocated by the libcrmcommon library. This should be
  * called before exiting a process that uses the library, and the process should
  * not call any libcrmcommon or libxml2 APIs after calling this one.
  */
 void
 pcmk_common_cleanup(void)
 {
     // @TODO This isn't really everything, move all cleanup here
     mainloop_cleanup();
     pcmk__xml_cleanup();
     pcmk__free_common_logger();
     qb_log_fini(); // Don't log anything after this point
 
     free(crm_system_name);
     crm_system_name = NULL;
 }
 
 bool
 pcmk__is_user_in_group(const char *user, const char *group)
 {
     struct group *grent;
     char **gr_mem;
 
     if (user == NULL || group == NULL) {
         return false;
     }
     
     setgrent();
     while ((grent = getgrent()) != NULL) {
         if (grent->gr_mem == NULL) {
             continue;
         }
 
         if(strcmp(group, grent->gr_name) != 0) {
             continue;
         }
 
         gr_mem = grent->gr_mem;
         while (*gr_mem != NULL) {
             if (!strcmp(user, *gr_mem++)) {
                 endgrent();
                 return true;
             }
         }
     }
     endgrent();
     return false;
 }
 
 int
 pcmk__lookup_user(const char *name, uid_t *uid, gid_t *gid)
 {
     struct passwd *pwentry = NULL;
 
     CRM_CHECK(name != NULL, return EINVAL);
 
     // getpwnam() is not thread-safe, but Pacemaker is single-threaded
     errno = 0;
     pwentry = getpwnam(name);
     if (pwentry == NULL) {
         /* Either an error occurred or no passwd entry was found.
          *
          * The value of errno is implementation-dependent if no passwd entry is
          * found. The POSIX specification does not consider it an error.
          * POSIX.1-2008 specifies that errno shall not be changed in this case,
          * while POSIX.1-2001 does not specify the value of errno in this case.
          * The man page on Linux notes that a variety of values have been
          * observed in practice. So an implementation may set errno to an
          * arbitrary value, despite the POSIX specification.
          *
          * However, if pwentry == NULL and errno == 0, then we know that no
          * matching entry was found and there was no error. So we default to
          * ENOENT as our return code.
          */
         return ((errno != 0)? errno : ENOENT);
     }
 
     if (uid != NULL) {
         *uid = pwentry->pw_uid;
     }
     if (gid != NULL) {
         *gid = pwentry->pw_gid;
     }
     crm_trace("User %s has uid=%lld gid=%lld", name,
               (long long) pwentry->pw_uid, (long long) pwentry->pw_gid);
 
     return pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Get user and group IDs of Pacemaker daemon user
  *
  * \param[out] uid  Where to store daemon user ID (can be \c NULL)
  * \param[out] gid  Where to store daemon group ID (can be \c NULL)
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__daemon_user(uid_t *uid, gid_t *gid)
 {
     static uid_t daemon_uid = 0;
     static gid_t daemon_gid = 0;
     static bool found = false;
 
     if (!found) {
         int rc = pcmk__lookup_user(CRM_DAEMON_USER, &daemon_uid, &daemon_gid);
 
         if (rc != pcmk_rc_ok) {
             return rc;
         }
         found = true;
     }
 
     if (uid != NULL) {
         *uid = daemon_uid;
     }
     if (gid != NULL) {
         *gid = daemon_gid;
     }
     return pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Return the integer equivalent of a portion of a string
  *
  * \param[in]  text      Pointer to beginning of string portion
  * \param[out] end_text  This will point to next character after integer
  */
 static int
 version_helper(const char *text, const char **end_text)
 {
     int atoi_result = -1;
 
     pcmk__assert(end_text != NULL);
 
     errno = 0;
 
     if (text != NULL && text[0] != 0) {
         /* seemingly sacrificing const-correctness -- because while strtol
            doesn't modify the input, it doesn't want to artificially taint the
            "end_text" pointer-to-pointer-to-first-char-in-string with constness
            in case the input wasn't actually constant -- by semantic definition
            not a single character will get modified so it shall be perfectly
            safe to make compiler happy with dropping "const" qualifier here */
         atoi_result = (int) strtol(text, (char **) end_text, 10);
 
         if (errno == EINVAL) {
             crm_err("Conversion of '%s' %c failed", text, text[0]);
             atoi_result = -1;
         }
     }
     return atoi_result;
 }
 
+/*!
+ * \internal
+ * \brief Parse a version segment from an input and advance the input pointer
+ *
+ * \param[in,out] version_segment  Pointer to a version string segment
+ *
+ * \return Nonnegative integer value parsed from \p *version_segment on success,
+ *         or 0 on failure
+ *
+ * \note Upon return, \p *version_segment points to the (possibly invalid) next
+ *       segment on success, or to the terminating null byte on failure.
+ */
+static long
+parse_version_segment(const char **version_segment)
+{
+    char *endptr = NULL;
+    long rc = 0;
+
+    if (pcmk__str_empty(*version_segment)) {
+        return 0;
+    }
+
+    /* '+', '-', and whitespace are invalid in version strings. strtol() won't
+     * complain, so catch them here. If the first character is not a digit,
+     * advance to end of string.
+     */
+    if (!isdigit(**version_segment)) {
+        *version_segment += strlen(*version_segment);
+        return 0;
+    }
+
+    /* Negative return code or unparsable input should be impossible, since we
+     * checked with isdigit() first. If it happens somehow, advance to end of
+     * string.
+     */
+    rc = strtol(*version_segment, &endptr, 10);
+    CRM_CHECK((rc >= 0) && (endptr != *version_segment),
+              *version_segment = endptr + strlen(endptr); return 0);
+
+    // Skip one dot immediately after a series of digits
+    if (*endptr == '.') {
+        endptr++;
+    }
+
+    // Advance to next version segment
+    *version_segment = endptr;
+    return rc;
+}
+
+/*!
+ * \internal
+ * \brief Compare two version strings to determine which one is higher
+ *
+ * A valid version string is of the form specified by the regex
+ * <tt>[0-9]+(\.[0-9]+)*</tt>.
+ *
+ * Leading whitespace is allowed and ignored. The two strings are compared
+ * segment by segment, until either the terminating null byte or an invalid
+ * character has been reached in both strings. A segment is a series of digits
+ * followed by a single dot or by the terminating null byte.
+ *
+ * After the terminating null byte or an invalid character is reached in one
+ * string, parsing of that string stops. All further comparisons are as if that
+ * string has an infinite number of trailing \c "0." segments. This continues
+ * until the terminating null byte or an invalid character is reached in the
+ * other string.
+ *
+ * Segments are compared by calling \c strtol() to parse them to long integers,
+ * and then performing standard integer comparison.
+ *
+ * \param[in] version1  First version to compare
+ * \param[in] version2  Second version to compare
+ *
+ * \retval -1  if \p version1 evaluates to a lower version than \p version2
+ * \retval  1  if \p version1 evaluates to a higher version than \p version2
+ * \retval  0  if \p version1 and \p version2 evaluate to an equal version
+ */
+int
+pcmk__compare_versions(const char *version1, const char *version2)
+{
+    if (version1 == version2) {
+        return 0;
+    }
+    if (pcmk__str_empty(version1) && pcmk__str_empty(version2)) {
+        return 0;
+    }
+    if (pcmk__str_empty(version1)) {
+        return -1;
+    }
+    if (pcmk__str_empty(version2)) {
+        return 1;
+    }
+
+    // Skip leading whitespace
+    for (; isspace(*version1); version1++);
+    for (; isspace(*version2); version2++);
+
+    for (const char *v1 = version1, *v2 = version2;
+         ((*v1 != '\0') || (*v2 != '\0')); ) {
+
+        long digit1 = parse_version_segment(&v1);
+        long digit2 = parse_version_segment(&v2);
+
+        if (digit1 < digit2) {
+            crm_trace("%s < %s", version1, version2);
+            return -1;
+        }
+        if (digit1 > digit2) {
+            crm_trace("%s > %s", version1, version2);
+            return 1;
+        }
+    }
+    crm_trace("%s == %s", version1, version2);
+    return 0;
+}
+
 /*
  * version1 < version2 : -1
  * version1 = version2 :  0
  * version1 > version2 :  1
  */
 int
 compare_version(const char *version1, const char *version2)
 {
     int rc = 0;
     int lpc = 0;
     const char *ver1_iter, *ver2_iter;
 
     if (version1 == NULL && version2 == NULL) {
         return 0;
     } else if (version1 == NULL) {
         return -1;
     } else if (version2 == NULL) {
         return 1;
     }
 
     ver1_iter = version1;
     ver2_iter = version2;
 
     while (1) {
         int digit1 = 0;
         int digit2 = 0;
 
         lpc++;
 
         if (ver1_iter == ver2_iter) {
             break;
         }
 
         if (ver1_iter != NULL) {
             digit1 = version_helper(ver1_iter, &ver1_iter);
         }
 
         if (ver2_iter != NULL) {
             digit2 = version_helper(ver2_iter, &ver2_iter);
         }
 
         if (digit1 < digit2) {
             rc = -1;
             break;
 
         } else if (digit1 > digit2) {
             rc = 1;
             break;
         }
 
         if (ver1_iter != NULL && *ver1_iter == '.') {
             ver1_iter++;
         }
         if (ver1_iter != NULL && *ver1_iter == '\0') {
             ver1_iter = NULL;
         }
 
         if (ver2_iter != NULL && *ver2_iter == '.') {
             ver2_iter++;
         }
         if (ver2_iter != NULL && *ver2_iter == 0) {
             ver2_iter = NULL;
         }
     }
 
     if (rc == 0) {
         crm_trace("%s == %s (%d)", version1, version2, lpc);
     } else if (rc < 0) {
         crm_trace("%s < %s (%d)", version1, version2, lpc);
     } else if (rc > 0) {
         crm_trace("%s > %s (%d)", version1, version2, lpc);
     }
 
     return rc;
 }
 
 /*!
  * \internal
  * \brief Convert the current process to a daemon process
  *
  * Fork a child process, exit the parent, create a PID file with the current
  * process ID, and close the standard input/output/error file descriptors.
  * Exit instead if a daemon is already running and using the PID file.
  *
  * \param[in] name     Daemon executable name
  * \param[in] pidfile  File name to use as PID file
  */
 void
 pcmk__daemonize(const char *name, const char *pidfile)
 {
     int rc;
     pid_t pid;
 
     /* Check before we even try... */
     rc = pcmk__pidfile_matches(pidfile, 1, name, &pid);
     if ((rc != pcmk_rc_ok) && (rc != ENOENT)) {
         crm_err("%s: already running [pid %lld in %s]",
                 name, (long long) pid, pidfile);
         printf("%s: already running [pid %lld in %s]\n",
                name, (long long) pid, pidfile);
         crm_exit(CRM_EX_ERROR);
     }
 
     pid = fork();
     if (pid < 0) {
         fprintf(stderr, "%s: could not start daemon\n", name);
         crm_perror(LOG_ERR, "fork");
         crm_exit(CRM_EX_OSERR);
 
     } else if (pid > 0) {
         crm_exit(CRM_EX_OK);
     }
 
     rc = pcmk__lock_pidfile(pidfile, name);
     if (rc != pcmk_rc_ok) {
         crm_err("Could not lock '%s' for %s: %s " QB_XS " rc=%d",
                 pidfile, name, pcmk_rc_str(rc), rc);
         printf("Could not lock '%s' for %s: %s (%d)\n",
                pidfile, name, pcmk_rc_str(rc), rc);
         crm_exit(CRM_EX_ERROR);
     }
 
     umask(S_IWGRP | S_IWOTH | S_IROTH);
 
     close(STDIN_FILENO);
     pcmk__open_devnull(O_RDONLY);   // stdin (fd 0)
 
     close(STDOUT_FILENO);
     pcmk__open_devnull(O_WRONLY);   // stdout (fd 1)
 
     close(STDERR_FILENO);
     pcmk__open_devnull(O_WRONLY);   // stderr (fd 2)
 }
 
 /* @FIXME uuid.h is an optional header per configure.ac, and we include it
  * conditionally above. But uuid_generate() and uuid_unparse() depend on it, on
  * many or perhaps all systems with libuuid. So it's not clear how it would ever
  * be optional in practice.
  *
  * Note that these functions are not POSIX, although there is probably no good
  * portable alternative.
  *
  * We do list libuuid as a build dependency in INSTALL.md already.
  */
 
 #ifdef HAVE_UUID_UUID_H
 #include <uuid/uuid.h>
 #endif  // HAVE_UUID_UUID_H
 
 /*!
  * \internal
  * \brief Generate a 37-byte (36 bytes plus null terminator) UUID string
  *
  * \return Newly allocated UUID string
  *
  * \note The caller is responsible for freeing the return value using \c free().
  */
 char *
 pcmk__generate_uuid(void)
 {
     uuid_t uuid;
 
     // uuid_unparse() converts a UUID to a 37-byte string (including null byte)
     char *buffer = pcmk__assert_alloc(37, sizeof(char));
 
     uuid_generate(uuid);
     uuid_unparse(uuid, buffer);
     return buffer;
 }
 
 /*!
  * \internal
  * \brief Sleep for given milliseconds
  *
  * \param[in] ms  Time to sleep
  *
  * \note The full time might not be slept if a signal is received.
  */
 void
 pcmk__sleep_ms(unsigned int ms)
 {
     // @TODO Impose a sane maximum sleep to avoid hanging a process for long
     //CRM_CHECK(ms <= MAX_SLEEP, ms = MAX_SLEEP);
 
     // Use sleep() for any whole seconds
     if (ms >= 1000) {
         sleep(ms / 1000);
         ms -= ms / 1000;
     }
 
     if (ms == 0) {
         return;
     }
 
 #if defined(HAVE_NANOSLEEP)
     // nanosleep() is POSIX-2008, so prefer that
     {
         struct timespec req = { .tv_sec = 0, .tv_nsec = (long) (ms * 1000000) };
 
         nanosleep(&req, NULL);
     }
 #elif defined(HAVE_USLEEP)
     // usleep() is widely available, though considered obsolete
     usleep((useconds_t) ms);
 #else
     // Otherwise use a trick with select() timeout
     {
         struct timeval tv = { .tv_sec = 0, .tv_usec = (suseconds_t) ms };
 
         select(0, NULL, NULL, NULL, &tv);
     }
 #endif
 }
 
 /*!
  * \internal
  * \brief Add a timer
  *
  * \param[in] interval_ms The interval for the function to be called, in ms
  * \param[in] fn          The function to be called
  * \param[in] data        Data to be passed to fn (can be NULL)
  *
  * \return The ID of the event source
  */
 guint
 pcmk__create_timer(guint interval_ms, GSourceFunc fn, gpointer data)
 {
     pcmk__assert(interval_ms != 0 && fn != NULL);
 
     if (interval_ms % 1000 == 0) {
         /* In case interval_ms is 0, the call to pcmk__timeout_ms2s ensures
          * an interval of one second.
          */
         return g_timeout_add_seconds(pcmk__timeout_ms2s(interval_ms), fn, data);
     } else {
         return g_timeout_add(interval_ms, fn, data);
     }
 }
 
 /*!
  * \internal
  * \brief Convert milliseconds to seconds
  *
  * \param[in] timeout_ms The interval, in ms
  *
  * \return If \p timeout_ms is 0, return 0.  Otherwise, return the number of
  *         seconds, rounded to the nearest integer, with a minimum of 1.
  */
 guint
 pcmk__timeout_ms2s(guint timeout_ms)
 {
     guint quot, rem;
 
     if (timeout_ms == 0) {
         return 0;
     } else if (timeout_ms < 1000) {
         return 1;
     }
 
     quot = timeout_ms / 1000;
     rem = timeout_ms % 1000;
 
     if (rem >= 500) {
         quot += 1;
     }
 
     return quot;
 }
 
 // Deprecated functions kept only for backward API compatibility
 // LCOV_EXCL_START
 
 #include <crm/common/util_compat.h>
 
 static void
 _gnutls_log_func(int level, const char *msg)
 {
     crm_trace("%s", msg);
 }
 
 void
 crm_gnutls_global_init(void)
 {
     signal(SIGPIPE, SIG_IGN);
     gnutls_global_init();
     gnutls_global_set_log_level(8);
     gnutls_global_set_log_function(_gnutls_log_func);
 }
 
 /*!
  * \brief Check whether string represents a client name used by cluster daemons
  *
  * \param[in] name  String to check
  *
  * \return true if name is standard client name used by daemons, false otherwise
  *
  * \note This is provided by the client, and so cannot be used by itself as a
  *       secure means of authentication.
  */
 bool
 crm_is_daemon_name(const char *name)
 {
     return pcmk__str_any_of(name,
                             "attrd",
                             CRM_SYSTEM_CIB,
                             CRM_SYSTEM_CRMD,
                             CRM_SYSTEM_DC,
                             CRM_SYSTEM_LRMD,
                             CRM_SYSTEM_MCP,
                             CRM_SYSTEM_PENGINE,
                             CRM_SYSTEM_TENGINE,
                             "pacemaker-attrd",
                             "pacemaker-based",
                             "pacemaker-controld",
                             "pacemaker-execd",
                             "pacemaker-fenced",
                             "pacemaker-remoted",
                             "pacemaker-schedulerd",
                             "stonith-ng",
                             "stonithd",
                             NULL);
 }
 
 char *
 crm_generate_uuid(void)
 {
     return pcmk__generate_uuid();
 }
 
 #define PW_BUFFER_LEN 500
 
 int
 crm_user_lookup(const char *name, uid_t * uid, gid_t * gid)
 {
     int rc = pcmk_ok;
     char *buffer = NULL;
     struct passwd pwd;
     struct passwd *pwentry = NULL;
 
     buffer = calloc(1, PW_BUFFER_LEN);
     if (buffer == NULL) {
         return -ENOMEM;
     }
 
     rc = getpwnam_r(name, &pwd, buffer, PW_BUFFER_LEN, &pwentry);
     if (pwentry) {
         if (uid) {
             *uid = pwentry->pw_uid;
         }
         if (gid) {
             *gid = pwentry->pw_gid;
         }
         crm_trace("User %s has uid=%d gid=%d", name, pwentry->pw_uid, pwentry->pw_gid);
 
     } else {
         rc = rc? -rc : -EINVAL;
         crm_info("User %s lookup: %s", name, pcmk_strerror(rc));
     }
 
     free(buffer);
     return rc;
 }
 
 int
 pcmk_daemon_user(uid_t *uid, gid_t *gid)
 {
     static uid_t daemon_uid;
     static gid_t daemon_gid;
     static bool found = false;
     int rc = pcmk_ok;
 
     if (!found) {
         rc = crm_user_lookup(CRM_DAEMON_USER, &daemon_uid, &daemon_gid);
         if (rc == pcmk_ok) {
             found = true;
         }
     }
     if (found) {
         if (uid) {
             *uid = daemon_uid;
         }
         if (gid) {
             *gid = daemon_gid;
         }
     }
     return rc;
 }
 
 // LCOV_EXCL_STOP
 // End deprecated API
diff --git a/lib/pengine/pe_digest.c b/lib/pengine/pe_digest.c
index 24361ceac2..39e9b07bc4 100644
--- a/lib/pengine/pe_digest.c
+++ b/lib/pengine/pe_digest.c
@@ -1,612 +1,612 @@
 /*
  * Copyright 2004-2025 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <glib.h>
 #include <stdbool.h>
 
 #include <crm/crm.h>
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>
 #include <crm/pengine/internal.h>
 #include "pe_status_private.h"
 
 extern bool pcmk__is_daemon;
 
 /*!
  * \internal
  * \brief Free an operation digest cache entry
  *
  * \param[in,out] ptr  Pointer to cache entry to free
  *
  * \note The argument is a gpointer so this can be used as a hash table
  *       free function.
  */
 void
 pe__free_digests(gpointer ptr)
 {
     pcmk__op_digest_t *data = ptr;
 
     if (data != NULL) {
         pcmk__xml_free(data->params_all);
         pcmk__xml_free(data->params_secure);
         pcmk__xml_free(data->params_restart);
 
         free(data->digest_all_calc);
         free(data->digest_restart_calc);
         free(data->digest_secure_calc);
 
         free(data);
     }
 }
 
 // Return true if XML attribute name is not substring of a given string
 static bool
 attr_not_in_string(xmlAttrPtr a, void *user_data)
 {
     bool filter = false;
     char *name = pcmk__assert_asprintf(" %s ", (const char *) a->name);
 
     if (strstr((const char *) user_data, name) == NULL) {
         crm_trace("Filtering %s (not found in '%s')",
                   (const char *) a->name, (const char *) user_data);
         filter = true;
     }
     free(name);
     return filter;
 }
 
 // Return true if XML attribute name is substring of a given string
 static bool
 attr_in_string(xmlAttrPtr a, void *user_data)
 {
     bool filter = false;
     char *name = pcmk__assert_asprintf(" %s ", (const char *) a->name);
 
     if (strstr((const char *) user_data, name) != NULL) {
         crm_trace("Filtering %s (found in '%s')",
                   (const char *) a->name, (const char *) user_data);
         filter = true;
     }
     free(name);
     return filter;
 }
 
 /*!
  * \internal
  * \brief Add digest of all parameters to a digest cache entry
  *
  * \param[out]    data         Digest cache entry to modify
  * \param[in,out] rsc          Resource that action was for
  * \param[in]     node         Node action was performed on
  * \param[in]     params       Resource parameters evaluated for node
  * \param[in]     task         Name of action performed
  * \param[in,out] interval_ms  Action's interval (will be reset if in overrides)
  * \param[in]     xml_op       Unused
  * \param[in]     op_version   CRM feature set to use for digest calculation
  * \param[in]     overrides    Key/value table to override resource parameters
  * \param[in,out] scheduler    Scheduler data
  */
 static void
 calculate_main_digest(pcmk__op_digest_t *data, pcmk_resource_t *rsc,
                       const pcmk_node_t *node, GHashTable *params,
                       const char *task, guint *interval_ms,
                       const xmlNode *xml_op, const char *op_version,
                       GHashTable *overrides, pcmk_scheduler_t *scheduler)
 {
     xmlNode *action_config = NULL;
 
     data->params_all = pcmk__xe_create(NULL, PCMK_XE_PARAMETERS);
 
     /* REMOTE_CONTAINER_HACK: Allow Pacemaker Remote nodes to run containers
      * that themselves are Pacemaker Remote nodes
      */
     (void) pe__add_bundle_remote_name(rsc, data->params_all,
                                       PCMK_REMOTE_RA_ADDR);
 
     if (overrides != NULL) {
         // If interval was overridden, reset it
         const char *meta_name = CRM_META "_" PCMK_META_INTERVAL;
         const char *interval_s = g_hash_table_lookup(overrides, meta_name);
 
         if (interval_s != NULL) {
             long long value_ll;
 
             if ((pcmk__scan_ll(interval_s, &value_ll, 0LL) == pcmk_rc_ok)
                 && (value_ll >= 0) && (value_ll <= G_MAXUINT)) {
                 *interval_ms = (guint) value_ll;
             }
         }
 
         // Add overrides to list of all parameters
         g_hash_table_foreach(overrides, hash2field, data->params_all);
     }
 
     // Add provided instance parameters
     g_hash_table_foreach(params, hash2field, data->params_all);
 
     // Find action configuration XML in CIB
     action_config = pcmk__find_action_config(rsc, task, *interval_ms, true);
 
     /* Add action-specific resource instance attributes to the digest list.
      *
      * If this is a one-time action with action-specific instance attributes,
      * enforce a restart instead of reload-agent in case the main digest doesn't
      * match, even if the restart digest does. This ensures any changes of the
      * action-specific parameters get applied for this specific action, and
      * digests calculated for the resulting history will be correct. Default the
      * result to RSC_DIGEST_RESTART for the case where the main digest doesn't
      * match.
      */
     params = pcmk__unpack_action_rsc_params(action_config, node->priv->attrs,
                                             scheduler);
     if ((*interval_ms == 0) && (g_hash_table_size(params) > 0)) {
         data->rc = pcmk__digest_restart;
     }
     g_hash_table_foreach(params, hash2field, data->params_all);
     g_hash_table_destroy(params);
 
     // Add action meta-attributes
     params = pcmk__unpack_action_meta(rsc, node, task, *interval_ms,
                                       action_config);
     g_hash_table_foreach(params, hash2metafield, data->params_all);
     g_hash_table_destroy(params);
 
     pcmk__filter_op_for_digest(data->params_all);
 
     data->digest_all_calc = pcmk__digest_operation(data->params_all);
 }
 
 // Return true if XML attribute name is a Pacemaker-defined fencing parameter
 static bool
 is_fence_param(xmlAttrPtr attr, void *user_data)
 {
     return pcmk_stonith_param((const char *) attr->name);
 }
 
 /*!
  * \internal
  * \brief Add secure digest to a digest cache entry
  *
  * \param[out] data        Digest cache entry to modify
  * \param[in]  rsc         Resource that action was for
  * \param[in]  params      Resource parameters evaluated for node
  * \param[in]  xml_op      XML of operation in CIB status (if available)
  * \param[in]  op_version  CRM feature set to use for digest calculation
  * \param[in]  overrides   Key/value hash table to override resource parameters
  */
 static void
 calculate_secure_digest(pcmk__op_digest_t *data, const pcmk_resource_t *rsc,
                         GHashTable *params, const xmlNode *xml_op,
                         const char *op_version, GHashTable *overrides)
 {
     const char *class = pcmk__xe_get(rsc->priv->xml, PCMK_XA_CLASS);
     const char *secure_list = NULL;
-    bool old_version = (compare_version(op_version, "3.16.0") < 0);
+    bool old_version = (pcmk__compare_versions(op_version, "3.16.0") < 0);
 
     if (xml_op == NULL) {
         secure_list = " passwd password user ";
     } else {
         secure_list = pcmk__xe_get(xml_op, PCMK__XA_OP_SECURE_PARAMS);
     }
 
     if (old_version) {
         data->params_secure = pcmk__xe_create(NULL, PCMK_XE_PARAMETERS);
         if (overrides != NULL) {
             g_hash_table_foreach(overrides, hash2field, data->params_secure);
         }
 
         g_hash_table_foreach(params, hash2field, data->params_secure);
 
     } else {
         // Start with a copy of all parameters
         data->params_secure = pcmk__xml_copy(NULL, data->params_all);
     }
 
     if (secure_list != NULL) {
         pcmk__xe_remove_matching_attrs(data->params_secure, false,
                                        attr_in_string, (void *) secure_list);
     }
     if (old_version
         && pcmk__is_set(pcmk_get_ra_caps(class),
                         pcmk_ra_cap_fence_params)) {
         /* For stonith resources, Pacemaker adds special parameters,
          * but these are not listed in fence agent meta-data, so with older
          * versions of DC, the controller will not hash them. That means we have
          * to filter them out before calculating our hash for comparison.
          */
         pcmk__xe_remove_matching_attrs(data->params_secure, false,
                                        is_fence_param, NULL);
     }
     pcmk__filter_op_for_digest(data->params_secure);
 
     /* CRM_meta_timeout *should* be part of a digest for recurring operations.
      * However, with older versions of DC, the controller does not add timeout
      * to secure digests, because it only includes parameters declared by the
      * resource agent.
      * Remove any timeout that made it this far, to match.
      */
     if (old_version) {
         pcmk__xe_remove_attr(data->params_secure,
                              CRM_META "_" PCMK_META_TIMEOUT);
     }
 
     data->digest_secure_calc = pcmk__digest_operation(data->params_secure);
 }
 
 /*!
  * \internal
  * \brief Add restart digest to a digest cache entry
  *
  * \param[out] data        Digest cache entry to modify
  * \param[in]  xml_op      XML of operation in CIB status (if available)
  * \param[in]  op_version  CRM feature set to use for digest calculation
  *
  * \note This function doesn't need to handle overrides because it starts with
  *       data->params_all, which already has overrides applied.
  */
 static void
 calculate_restart_digest(pcmk__op_digest_t *data, const xmlNode *xml_op,
                          const char *op_version)
 {
     const char *value = NULL;
 
     // We must have XML of resource operation history
     if (xml_op == NULL) {
         return;
     }
 
     // And the history must have a restart digest to compare against
     if (pcmk__xe_get(xml_op, PCMK__XA_OP_RESTART_DIGEST) == NULL) {
         return;
     }
 
     // Start with a copy of all parameters
     data->params_restart = pcmk__xml_copy(NULL, data->params_all);
 
     // Then filter out reloadable parameters, if any
     value = pcmk__xe_get(xml_op, PCMK__XA_OP_FORCE_RESTART);
     if (value != NULL) {
         pcmk__xe_remove_matching_attrs(data->params_restart, false,
                                        attr_not_in_string, (void *) value);
     }
 
     value = pcmk__xe_get(xml_op, PCMK_XA_CRM_FEATURE_SET);
     data->digest_restart_calc = pcmk__digest_operation(data->params_restart);
 }
 
 /*!
  * \internal
  * \brief Create a new digest cache entry with calculated digests
  *
  * \param[in,out] rsc          Resource that action was for
  * \param[in]     task         Name of action performed
  * \param[in,out] interval_ms  Action's interval (will be reset if in overrides)
  * \param[in]     node         Node action was performed on
  * \param[in]     xml_op       XML of operation in CIB status (if available)
  * \param[in]     overrides    Key/value table to override resource parameters
  * \param[in]     calc_secure  Whether to calculate secure digest
  * \param[in,out] scheduler    Scheduler data
  *
  * \return Pointer to new digest cache entry (or NULL on memory error)
  * \note It is the caller's responsibility to free the result using
  *       pe__free_digests().
  */
 pcmk__op_digest_t *
 pe__calculate_digests(pcmk_resource_t *rsc, const char *task,
                       guint *interval_ms, const pcmk_node_t *node,
                       const xmlNode *xml_op, GHashTable *overrides,
                       bool calc_secure, pcmk_scheduler_t *scheduler)
 {
     pcmk__op_digest_t *data = NULL;
     const char *op_version = NULL;
     GHashTable *params = NULL;
 
     CRM_CHECK(scheduler != NULL, return NULL);
 
     data = calloc(1, sizeof(pcmk__op_digest_t));
     if (data == NULL) {
         pcmk__sched_err(scheduler,
                         "Could not allocate memory for operation digest");
         return NULL;
     }
 
     data->rc = pcmk__digest_match;
 
     if (xml_op != NULL) {
         op_version = pcmk__xe_get(xml_op, PCMK_XA_CRM_FEATURE_SET);
     }
 
     if ((op_version == NULL) && (scheduler->input != NULL)) {
         op_version = pcmk__xe_get(scheduler->input, PCMK_XA_CRM_FEATURE_SET);
     }
 
     if (op_version == NULL) {
         op_version = CRM_FEATURE_SET;
     }
 
     params = pe_rsc_params(rsc, node, scheduler);
     calculate_main_digest(data, rsc, node, params, task, interval_ms, xml_op,
                           op_version, overrides, scheduler);
     if (calc_secure) {
         calculate_secure_digest(data, rsc, params, xml_op, op_version,
                                 overrides);
     }
     calculate_restart_digest(data, xml_op, op_version);
     return data;
 }
 
 /*!
  * \internal
  * \brief Calculate action digests and store in node's digest cache
  *
  * \param[in,out] rsc          Resource that action was for
  * \param[in]     task         Name of action performed
  * \param[in]     interval_ms  Action's interval
  * \param[in,out] node         Node action was performed on
  * \param[in]     xml_op       XML of operation in CIB status (if available)
  * \param[in]     calc_secure  Whether to calculate secure digest
  * \param[in,out] scheduler    Scheduler data
  *
  * \return Pointer to node's digest cache entry
  */
 static pcmk__op_digest_t *
 rsc_action_digest(pcmk_resource_t *rsc, const char *task, guint interval_ms,
                   pcmk_node_t *node, const xmlNode *xml_op,
                   bool calc_secure, pcmk_scheduler_t *scheduler)
 {
     pcmk__op_digest_t *data = NULL;
     char *key = pcmk__op_key(rsc->id, task, interval_ms);
 
     data = g_hash_table_lookup(node->priv->digest_cache, key);
     if (data == NULL) {
         data = pe__calculate_digests(rsc, task, &interval_ms, node, xml_op,
                                      NULL, calc_secure, scheduler);
         pcmk__assert(data != NULL);
         g_hash_table_insert(node->priv->digest_cache, strdup(key), data);
     }
     free(key);
     return data;
 }
 
 /*!
  * \internal
  * \brief Calculate operation digests and compare against an XML history entry
  *
  * \param[in,out] rsc        Resource to check
  * \param[in]     xml_op     Resource history XML
  * \param[in,out] node       Node to use for digest calculation
  * \param[in,out] scheduler  Scheduler data
  *
  * \return Pointer to node's digest cache entry, with comparison result set
  */
 pcmk__op_digest_t *
 rsc_action_digest_cmp(pcmk_resource_t *rsc, const xmlNode *xml_op,
                       pcmk_node_t *node, pcmk_scheduler_t *scheduler)
 {
     pcmk__op_digest_t *data = NULL;
     guint interval_ms = 0;
 
     const char *op_version;
     const char *task = pcmk__xe_get(xml_op, PCMK_XA_OPERATION);
     const char *digest_all;
     const char *digest_restart;
 
     pcmk__assert(node != NULL);
 
     op_version = pcmk__xe_get(xml_op, PCMK_XA_CRM_FEATURE_SET);
     digest_all = pcmk__xe_get(xml_op, PCMK__XA_OP_DIGEST);
     digest_restart = pcmk__xe_get(xml_op, PCMK__XA_OP_RESTART_DIGEST);
 
     pcmk__xe_get_guint(xml_op, PCMK_META_INTERVAL, &interval_ms);
     data = rsc_action_digest(rsc, task, interval_ms, node, xml_op,
                              pcmk__is_set(scheduler->flags,
                                           pcmk__sched_sanitized),
                              scheduler);
 
     if (!pcmk__str_eq(data->digest_restart_calc, digest_restart,
                       pcmk__str_null_matches)) {
         pcmk__rsc_info(rsc,
                        "Parameter change for %s-interval %s of %s on %s "
                        "requires restart (hash now %s vs. %s "
                        "with op feature set %s for transition %s)",
                        pcmk__readable_interval(interval_ms), task, rsc->id,
                        pcmk__node_name(node), data->digest_restart_calc,
                        pcmk__s(digest_restart, "missing"), op_version,
                        pcmk__xe_get(xml_op, PCMK__XA_TRANSITION_MAGIC));
         data->rc = pcmk__digest_restart;
 
     } else if (digest_all == NULL) {
         /* it is unknown what the previous op digest was */
         data->rc = pcmk__digest_unknown;
 
     } else if (strcmp(digest_all, data->digest_all_calc) != 0) {
         /* Given a non-recurring operation with extra parameters configured,
          * in case that the main digest doesn't match, even if the restart
          * digest matches, enforce a restart rather than a reload-agent anyway.
          * So that it ensures any changes of the extra parameters get applied
          * for this specific operation, and the digests calculated for the
          * resulting PCMK__XE_LRM_RSC_OP will be correct.
          * Preserve the implied rc pcmk__digest_restart for the case that the
          * main digest doesn't match.
          */
         if ((interval_ms == 0) && (data->rc == pcmk__digest_restart)) {
             pcmk__rsc_info(rsc,
                            "Parameters containing extra ones to %ums-interval"
                            " %s action for %s on %s "
                            "changed: hash was %s vs. now %s (restart:%s) %s",
                            interval_ms, task, rsc->id, pcmk__node_name(node),
                            pcmk__s(digest_all, "missing"),
                            data->digest_all_calc, op_version,
                            pcmk__xe_get(xml_op, PCMK__XA_TRANSITION_MAGIC));
 
         } else {
             pcmk__rsc_info(rsc,
                            "Parameters to %ums-interval %s action for %s on %s "
                            "changed: hash was %s vs. now %s (%s:%s) %s",
                            interval_ms, task, rsc->id, pcmk__node_name(node),
                            pcmk__s(digest_all, "missing"),
                            data->digest_all_calc,
                            (interval_ms > 0)? "reschedule" : "reload",
                            op_version,
                            pcmk__xe_get(xml_op, PCMK__XA_TRANSITION_MAGIC));
             data->rc = pcmk__digest_mismatch;
         }
 
     } else {
         data->rc = pcmk__digest_match;
     }
     return data;
 }
 
 /*!
  * \internal
  * \brief Create an unfencing summary for use in special node attribute
  *
  * Create a string combining a fence device's resource ID, agent type, and
  * parameter digest (whether for all parameters or just non-private parameters).
  * This can be stored in a special node attribute, allowing us to detect changes
  * in either the agent type or parameters, to know whether unfencing must be
  * redone or can be safely skipped when the device's history is cleaned.
  *
  * \param[in] rsc_id        Fence device resource ID
  * \param[in] agent_type    Fence device agent
  * \param[in] param_digest  Fence device parameter digest
  *
  * \return Newly allocated string with unfencing digest
  * \note The caller is responsible for freeing the result.
  */
 static inline char *
 create_unfencing_summary(const char *rsc_id, const char *agent_type,
                          const char *param_digest)
 {
     return pcmk__assert_asprintf("%s:%s:%s", rsc_id, agent_type, param_digest);
 }
 
 /*!
  * \internal
  * \brief Check whether a node can skip unfencing
  *
  * Check whether a fence device's current definition matches a node's
  * stored summary of when it was last unfenced by the device.
  *
  * \param[in] rsc_id        Fence device's resource ID
  * \param[in] agent         Fence device's agent type
  * \param[in] digest_calc   Fence device's current parameter digest
  * \param[in] node_summary  Value of node's special unfencing node attribute
  *                          (a comma-separated list of unfencing summaries for
  *                          all devices that have unfenced this node)
  *
  * \return TRUE if digest matches, FALSE otherwise
  */
 static bool
 unfencing_digest_matches(const char *rsc_id, const char *agent,
                          const char *digest_calc, const char *node_summary)
 {
     bool matches = FALSE;
 
     if (rsc_id && agent && digest_calc && node_summary) {
         char *search_secure = create_unfencing_summary(rsc_id, agent,
                                                        digest_calc);
 
         /* The digest was calculated including the device ID and agent,
          * so there is no risk of collision using strstr().
          */
         matches = (strstr(node_summary, search_secure) != NULL);
         crm_trace("Calculated unfencing digest '%s' %sfound in '%s'",
                   search_secure, matches? "" : "not ", node_summary);
         free(search_secure);
     }
     return matches;
 }
 
 /* Magic string to use as action name for digest cache entries used for
  * unfencing checks. This is not a real action name (i.e. "on"), so
  * pcmk__check_action_config() won't confuse these entries with real actions.
  */
 #define STONITH_DIGEST_TASK "stonith-on"
 
 /*!
  * \internal
  * \brief Calculate fence device digests and digest comparison result
  *
  * \param[in,out] rsc        Fence device resource
  * \param[in]     agent      Fence device's agent type
  * \param[in,out] node       Node with digest cache to use
  * \param[in,out] scheduler  Scheduler data
  *
  * \return Node's digest cache entry
  */
 pcmk__op_digest_t *
 pe__compare_fencing_digest(pcmk_resource_t *rsc, const char *agent,
                            pcmk_node_t *node, pcmk_scheduler_t *scheduler)
 {
     const char *node_summary = NULL;
 
     // Calculate device's current parameter digests
     pcmk__op_digest_t *data = rsc_action_digest(rsc, STONITH_DIGEST_TASK, 0U,
                                                 node, NULL, TRUE, scheduler);
 
     // Check whether node has special unfencing summary node attribute
     node_summary = pcmk__node_attr(node, CRM_ATTR_DIGESTS_ALL, NULL,
                                    pcmk__rsc_node_current);
     if (node_summary == NULL) {
         data->rc = pcmk__digest_unknown;
         return data;
     }
 
     // Check whether full parameter digest matches
     if (unfencing_digest_matches(rsc->id, agent, data->digest_all_calc,
                                  node_summary)) {
         data->rc = pcmk__digest_match;
         return data;
     }
 
     // Check whether secure parameter digest matches
     node_summary = pcmk__node_attr(node, CRM_ATTR_DIGESTS_SECURE, NULL,
                                    pcmk__rsc_node_current);
     if (unfencing_digest_matches(rsc->id, agent, data->digest_secure_calc,
                                  node_summary)) {
         data->rc = pcmk__digest_match;
         if (!pcmk__is_daemon && (scheduler->priv->out != NULL)) {
             pcmk__output_t *out = scheduler->priv->out;
 
             out->info(out, "Only 'private' parameters to %s "
                       "for unfencing %s changed", rsc->id,
                       pcmk__node_name(node));
         }
         return data;
     }
 
     // Parameters don't match
     data->rc = pcmk__digest_mismatch;
     if (pcmk__is_set(scheduler->flags, pcmk__sched_sanitized)
         && (data->digest_secure_calc != NULL)) {
 
         if (scheduler->priv->out != NULL) {
             pcmk__output_t *out = scheduler->priv->out;
             char *digest = create_unfencing_summary(rsc->id, agent,
                                                     data->digest_secure_calc);
 
             out->info(out, "Parameters to %s for unfencing "
                       "%s changed, try '%s'", rsc->id,
                       pcmk__node_name(node), digest);
             free(digest);
         } else if (!pcmk__is_daemon) {
             char *digest = create_unfencing_summary(rsc->id, agent,
                                                     data->digest_secure_calc);
 
             printf("Parameters to %s for unfencing %s changed, try '%s'\n",
                    rsc->id, pcmk__node_name(node), digest);
             free(digest);
         }
     }
     return data;
 }