diff --git a/daemons/based/based_callbacks.c b/daemons/based/based_callbacks.c
index 1774e5c882..6bcc34bd4a 100644
--- a/daemons/based/based_callbacks.c
+++ b/daemons/based/based_callbacks.c
@@ -1,1675 +1,1677 @@
 /*
  * Copyright 2004-2023 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 <crm/crm.h>
 #include <crm/cib.h>
 #include <crm/msg_xml.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
 #define OUR_NODENAME (stand_alone? "localhost" : crm_cluster->uname)
 
 static unsigned long cib_local_bcast_num = 0;
 
 typedef struct cib_local_notify_s {
     xmlNode *notify_src;
     char *client_id;
     gboolean from_peer;
     gboolean sync_reply;
 } cib_local_notify_t;
 
 int next_client_id = 0;
 
 gboolean legacy_mode = FALSE;
 
 qb_ipcs_service_t *ipcs_ro = NULL;
 qb_ipcs_service_t *ipcs_rw = NULL;
 qb_ipcs_service_t *ipcs_shm = NULL;
 
 void send_cib_replace(const xmlNode * sync_request, const char *host);
 static void cib_process_request(xmlNode *request, gboolean privileged,
                                 const pcmk__client_t *cib_client);
 
 
 static int cib_process_command(xmlNode *request, xmlNode **reply,
                                xmlNode **cib_diff, gboolean privileged);
 
 gboolean cib_common_callback(qb_ipcs_connection_t * c, void *data, size_t size,
                              gboolean privileged);
 
 gboolean cib_legacy_mode(void)
 {
     return legacy_mode;
 }
 
 
 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 -EPERM;
     }
 
     if (pcmk__new_client(c, uid, gid) == NULL) {
         return -EIO;
     }
     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
 };
 
 void
 cib_common_callback_worker(uint32_t id, uint32_t flags, xmlNode * op_request,
                            pcmk__client_t *cib_client, gboolean privileged)
 {
     const char *op = crm_element_value(op_request, F_CIB_OPERATION);
 
     if (pcmk__str_eq(op, CRM_OP_REGISTER, pcmk__str_none)) {
         if (flags & crm_ipc_client_response) {
             xmlNode *ack = create_xml_node(NULL, __func__);
 
             crm_xml_add(ack, F_CIB_OPERATION, CRM_OP_REGISTER);
             crm_xml_add(ack, F_CIB_CLIENTID, cib_client->id);
             pcmk__ipc_send_xml(cib_client, id, ack, flags);
             cib_client->request_id = 0;
             free_xml(ack);
         }
         return;
 
     } else if (pcmk__str_eq(op, T_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 = crm_element_value(op_request, F_CIB_NOTIFY_TYPE);
 
         crm_element_value_int(op_request, F_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, T_CIB_POST_NOTIFY, pcmk__str_casei)) {
             bit = cib_notify_post;
 
         } else if (pcmk__str_eq(type, T_CIB_PRE_NOTIFY, pcmk__str_casei)) {
             bit = cib_notify_pre;
 
         } else if (pcmk__str_eq(type, T_CIB_UPDATE_CONFIRM, pcmk__str_casei)) {
             bit = cib_notify_confirm;
 
         } else if (pcmk__str_eq(type, T_CIB_DIFF_NOTIFY, pcmk__str_casei)) {
             bit = cib_notify_diff;
 
         } else if (pcmk__str_eq(type, T_CIB_REPLACE_NOTIFY, pcmk__str_casei)) {
             bit = cib_notify_replace;
 
         } 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, "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;
     int call_options = 0;
     pcmk__client_t *cib_client = pcmk__find_client(c);
     xmlNode *op_request = pcmk__client_data2xml(cib_client, data, &id, &flags);
 
     if (op_request) {
         crm_element_value_int(op_request, F_CIB_CALLOPTS, &call_options);
     }
 
     if (op_request == NULL) {
         crm_trace("Invalid message from %p", c);
         pcmk__ipc_send_ack(cib_client, id, flags, "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 = crm_element_value(op_request, F_CIB_CLIENTNAME);
 
         if (value == NULL) {
             cib_client->name = pcmk__itoa(cib_client->pid);
         } else {
             cib_client->name = strdup(value);
             if (crm_is_daemon_name(value)) {
                 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("cluster-ipc-limit");
 
         if (pcmk__set_client_queue_max(cib_client, qmax)) {
             crm_trace("IPC threshold for client %s[%u] is now %u",
                       pcmk__client_name(cib_client), cib_client->pid,
                       cib_client->queue_max);
         }
     }
 
     crm_xml_add(op_request, F_CIB_CLIENTID, cib_client->id);
     crm_xml_add(op_request, F_CIB_CLIENTNAME, cib_client->name);
 
     CRM_LOG_ASSERT(cib_client->user != NULL);
     pcmk__update_acl_user(op_request, F_CIB_USER, cib_client->user);
 
     cib_common_callback_worker(id, flags, op_request, cib_client, privileged);
     free_xml(op_request);
 
     return 0;
 }
 
 static uint64_t ping_seq = 0;
 static char *ping_digest = NULL;
 static bool ping_modified_since = FALSE;
 int sync_our_cib(xmlNode * request, gboolean all);
 
 static gboolean
 cib_digester_cb(gpointer data)
 {
     if (based_is_primary) {
         char buffer[32];
         xmlNode *ping = create_xml_node(NULL, "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);
 
         crm_xml_add(ping, F_TYPE, "cib");
         crm_xml_add(ping, F_CIB_OPERATION, CRM_OP_PING);
         crm_xml_add(ping, F_CIB_PING_ID, buffer);
 
         crm_xml_add(ping, XML_ATTR_CRM_VERSION, CRM_FEATURE_SET);
         send_cluster_message(NULL, crm_msg_cib, ping, TRUE);
 
         free_xml(ping);
     }
     return FALSE;
 }
 
 static void
 process_ping_reply(xmlNode *reply) 
 {
     uint64_t seq = 0;
     const char *host = crm_element_value(reply, F_ORIG);
 
     xmlNode *pong = get_message_xml(reply, F_CIB_CALLDATA);
     const char *seq_s = crm_element_value(pong, F_CIB_PING_ID);
     const char *digest = crm_element_value(pong, XML_ATTR_DIGEST);
 
     if (seq_s == NULL) {
         crm_debug("Ignoring ping reply with no " F_CIB_PING_ID);
         return;
 
     } else {
         long long seq_ll;
 
         if (pcmk__scan_ll(seq_s, &seq_ll, 0LL) != pcmk_rc_ok) {
             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 {
         const char *version = crm_element_value(pong, XML_ATTR_CRM_VERSION);
 
         if(ping_digest == NULL) {
             crm_trace("Calculating new digest");
             ping_digest = calculate_xml_versioned_digest(the_cib, FALSE, TRUE, version);
         }
 
         crm_trace("Processing ping reply %s from %s (%s)", seq_s, host, digest);
         if (!pcmk__str_eq(ping_digest, digest, pcmk__str_casei)) {
             xmlNode *remote_cib = get_message_xml(pong, F_CIB_CALLDATA);
 
             crm_notice("Local CIB %s.%s.%s.%s differs from %s: %s.%s.%s.%s %p",
                        crm_element_value(the_cib, XML_ATTR_GENERATION_ADMIN),
                        crm_element_value(the_cib, XML_ATTR_GENERATION),
                        crm_element_value(the_cib, XML_ATTR_NUMUPDATES),
                        ping_digest, host,
                        remote_cib?crm_element_value(remote_cib, XML_ATTR_GENERATION_ADMIN):"_",
                        remote_cib?crm_element_value(remote_cib, XML_ATTR_GENERATION):"_",
                        remote_cib?crm_element_value(remote_cib, XML_ATTR_NUMUPDATES):"_",
                        digest, remote_cib);
 
             if(remote_cib && remote_cib->children) {
                 // Additional debug
                 xml_calculate_changes(the_cib, remote_cib);
 
                 pcmk__output_set_log_level(logger_out, LOG_INFO);
                 pcmk__xml_show_changes(logger_out, remote_cib);
                 crm_trace("End of differences");
             }
 
             free_xml(remote_cib);
             sync_our_cib(reply, FALSE);
         }
     }
 }
 
 static void
 do_local_notify(xmlNode * notify_src, const char *client_id,
                 gboolean sync_reply, gboolean from_peer)
 {
     int rid = 0;
     int call_id = 0;
     pcmk__client_t *client_obj = NULL;
 
     CRM_ASSERT(notify_src && client_id);
 
     crm_element_value_int(notify_src, F_CIB_CALLID, &call_id);
 
     client_obj = pcmk__find_client_by_id(client_id);
     if (client_obj == NULL) {
         crm_debug("Could not send response %d: client %s not found",
                   call_id, client_id);
         return;
     }
 
     if (sync_reply) {
         if (client_obj->ipcs) {
             CRM_LOG_ASSERT(client_obj->request_id);
 
             rid = client_obj->request_id;
             client_obj->request_id = 0;
 
             crm_trace("Sending response %d to client %s%s",
                       rid, pcmk__client_name(client_obj),
                       (from_peer? " (originator of delegated request)" : ""));
         } else {
             crm_trace("Sending response (call %d) to client %s%s",
                       call_id, pcmk__client_name(client_obj),
                       (from_peer? " (originator of delegated request)" : ""));
         }
 
     } else {
         crm_trace("Sending event %d to client %s%s",
                   call_id, pcmk__client_name(client_obj),
                   (from_peer? " (originator of delegated request)" : ""));
     }
 
     switch (PCMK__CLIENT_TYPE(client_obj)) {
         case pcmk__client_ipc:
             {
                 int rc = pcmk__ipc_send_xml(client_obj, rid, notify_src,
                                             (sync_reply? crm_ipc_flags_none
                                              : crm_ipc_server_event));
 
                 if (rc != pcmk_rc_ok) {
                     crm_warn("%s reply to client %s failed: %s " CRM_XS " rc=%d",
                              (sync_reply? "Synchronous" : "Asynchronous"),
                              pcmk__client_name(client_obj), pcmk_rc_str(rc),
                              rc);
                 }
             }
             break;
 #ifdef HAVE_GNUTLS_GNUTLS_H
         case pcmk__client_tls:
 #endif
         case pcmk__client_tcp:
             pcmk__remote_send_xml(client_obj->remote, notify_src);
             break;
         default:
             crm_err("Unknown transport for client %s "
                     CRM_XS " flags=%#016" PRIx64,
                     pcmk__client_name(client_obj), client_obj->flags);
     }
 }
 
 static void
 local_notify_destroy_callback(gpointer data)
 {
     cib_local_notify_t *notify = data;
 
     free_xml(notify->notify_src);
     free(notify->client_id);
     free(notify);
 }
 
 static void
 check_local_notify(int bcast_id)
 {
     cib_local_notify_t *notify = NULL;
 
     if (!local_notify_queue) {
         return;
     }
 
     notify = pcmk__intkey_table_lookup(local_notify_queue, bcast_id);
 
     if (notify) {
         do_local_notify(notify->notify_src, notify->client_id, notify->sync_reply,
                         notify->from_peer);
         pcmk__intkey_table_remove(local_notify_queue, bcast_id);
     }
 }
 
 static void
 queue_local_notify(xmlNode * notify_src, const char *client_id, gboolean sync_reply,
                    gboolean from_peer)
 {
     cib_local_notify_t *notify = calloc(1, sizeof(cib_local_notify_t));
 
     notify->notify_src = notify_src;
     notify->client_id = strdup(client_id);
     notify->sync_reply = sync_reply;
     notify->from_peer = from_peer;
 
     if (!local_notify_queue) {
         local_notify_queue = pcmk__intkey_table(local_notify_destroy_callback);
     }
     pcmk__intkey_table_insert(local_notify_queue, cib_local_bcast_num, notify);
     // cppcheck doesn't know notify will get freed when hash table is destroyed
     // cppcheck-suppress memleak
 }
 
 static void
 parse_local_options_v1(const pcmk__client_t *cib_client, int call_type,
                        int call_options, const char *host, const char *op,
                        gboolean *local_notify, gboolean *needs_reply,
                        gboolean *process, gboolean *needs_forward)
 {
     if (cib_op_modifies(call_type)
         && !(call_options & cib_inhibit_bcast)) {
         /* we need to send an update anyway */
         *needs_reply = TRUE;
     } else {
         *needs_reply = FALSE;
     }
 
     if (host == NULL && (call_options & cib_scope_local)) {
         crm_trace("Processing locally scoped %s op from client %s",
                   op, pcmk__client_name(cib_client));
         *local_notify = TRUE;
 
     } else if ((host == NULL) && based_is_primary) {
         crm_trace("Processing %s op locally from client %s as primary",
                   op, pcmk__client_name(cib_client));
         *local_notify = TRUE;
 
     } else if (pcmk__str_eq(host, OUR_NODENAME, pcmk__str_casei)) {
         crm_trace("Processing locally addressed %s op from client %s",
                   op, pcmk__client_name(cib_client));
         *local_notify = TRUE;
 
     } else if (stand_alone) {
         *needs_forward = FALSE;
         *local_notify = TRUE;
         *process = TRUE;
 
     } else {
         crm_trace("%s op from %s needs to be forwarded to client %s",
                   op, pcmk__client_name(cib_client),
                   pcmk__s(host, "the primary instance"));
         *needs_forward = TRUE;
         *process = FALSE;
     }
 }
 
 static void
 parse_local_options_v2(const pcmk__client_t *cib_client, int call_type,
                        int call_options, const char *host, const char *op,
                        gboolean *local_notify, gboolean *needs_reply,
                        gboolean *process, gboolean *needs_forward)
 {
     if (cib_op_modifies(call_type)) {
         if (pcmk__str_any_of(op, PCMK__CIB_REQUEST_PRIMARY,
                              PCMK__CIB_REQUEST_SECONDARY, NULL)) {
             /* Always handle these locally */
             *process = TRUE;
             *needs_reply = FALSE;
             *local_notify = TRUE;
             *needs_forward = FALSE;
             return;
 
         } else {
             /* Redirect all other updates via CPG */
             *needs_reply = TRUE;
             *needs_forward = TRUE;
             *process = FALSE;
             crm_trace("%s op from %s needs to be forwarded to client %s",
                       op, pcmk__client_name(cib_client),
                       pcmk__s(host, "the primary instance"));
             return;
         }
     }
 
 
     *process = TRUE;
     *needs_reply = FALSE;
     *local_notify = TRUE;
     *needs_forward = FALSE;
 
     if (stand_alone) {
         crm_trace("Processing %s op from client %s (stand-alone)",
                   op, pcmk__client_name(cib_client));
 
     } else if (host == NULL) {
         crm_trace("Processing unaddressed %s op from client %s",
                   op, pcmk__client_name(cib_client));
 
     } else if (pcmk__str_eq(host, OUR_NODENAME, pcmk__str_casei)) {
         crm_trace("Processing locally addressed %s op from client %s",
                   op, pcmk__client_name(cib_client));
 
     } else {
         crm_trace("%s op from %s needs to be forwarded to client %s",
                   op, pcmk__client_name(cib_client), host);
         *needs_forward = TRUE;
         *process = FALSE;
     }
 }
 
 static void
 parse_local_options(const pcmk__client_t *cib_client, int call_type,
                     int call_options, const char *host, const char *op,
                     gboolean *local_notify, gboolean *needs_reply,
                     gboolean *process, gboolean *needs_forward)
 {
     if(cib_legacy_mode()) {
         parse_local_options_v1(cib_client, call_type, call_options, host,
                                op, local_notify, needs_reply, process, needs_forward);
     } else {
         parse_local_options_v2(cib_client, call_type, call_options, host,
                                op, local_notify, needs_reply, process, needs_forward);
     }
 }
 
 static gboolean
 parse_peer_options_v1(int call_type, xmlNode * request,
                    gboolean * local_notify, gboolean * needs_reply, gboolean * process,
                    gboolean * needs_forward)
 {
     const char *op = NULL;
     const char *host = NULL;
     const char *delegated = NULL;
     const char *originator = crm_element_value(request, F_ORIG);
     const char *reply_to = crm_element_value(request, F_CIB_ISREPLY);
 
     gboolean is_reply = pcmk__str_eq(reply_to, OUR_NODENAME, pcmk__str_casei);
 
     if (pcmk__xe_attr_is_true(request, F_CIB_GLOBAL_UPDATE)) {
         *needs_reply = FALSE;
         if (is_reply) {
             *local_notify = TRUE;
             crm_trace("Processing global/peer update from %s"
                       " that originated from us", originator);
         } else {
             crm_trace("Processing global/peer update from %s", originator);
         }
         return TRUE;
     }
 
     op = crm_element_value(request, F_CIB_OPERATION);
     crm_trace("Processing %s request sent by %s", op, originator);
     if (pcmk__str_eq(op, PCMK__CIB_REQUEST_SHUTDOWN, pcmk__str_none)) {
         /* Always process these */
         *local_notify = FALSE;
         if (reply_to == NULL || is_reply) {
             *process = TRUE;
         }
         if (is_reply) {
             *needs_reply = FALSE;
         }
         return *process;
     }
 
     if (is_reply && pcmk__str_eq(op, CRM_OP_PING, pcmk__str_casei)) {
         process_ping_reply(request);
         return FALSE;
     }
 
     if (is_reply) {
         crm_trace("Forward reply sent from %s to local clients", originator);
         *process = FALSE;
         *needs_reply = FALSE;
         *local_notify = TRUE;
         return TRUE;
     }
 
     host = crm_element_value(request, F_CIB_HOST);
     if (pcmk__str_eq(host, OUR_NODENAME, pcmk__str_casei)) {
         crm_trace("Processing %s request sent to us from %s", op, originator);
         return TRUE;
 
     } else if(is_reply == FALSE && pcmk__str_eq(op, CRM_OP_PING, pcmk__str_casei)) {
         crm_trace("Processing %s request sent to %s by %s", op, host?host:"everyone", originator);
         *needs_reply = TRUE;
         return TRUE;
 
     } else if ((host == NULL) && based_is_primary) {
         crm_trace("Processing %s request sent to primary instance from %s",
                   op, originator);
         return TRUE;
     }
 
     delegated = crm_element_value(request, F_CIB_DELEGATED);
     if (delegated != NULL) {
         crm_trace("Ignoring message for primary instance");
 
     } else if (host != NULL) {
         /* this is for a specific instance and we're not it */
         crm_trace("Ignoring msg for instance on %s", host);
 
     } else if ((reply_to == NULL) && !based_is_primary) {
         // This is for the primary instance, and we're not it
         crm_trace("Ignoring reply for primary instance");
 
     } else if (pcmk__str_eq(op, PCMK__CIB_REQUEST_SHUTDOWN, pcmk__str_none)) {
         if (reply_to != NULL) {
             crm_debug("Processing %s from %s", op, originator);
             *needs_reply = FALSE;
 
         } else {
             crm_debug("Processing %s reply from %s", op, originator);
         }
         return TRUE;
 
     } else {
         crm_err("Nothing for us to do?");
         crm_log_xml_err(request, "Peer[inbound]");
     }
 
     return FALSE;
 }
 
 static gboolean
 parse_peer_options_v2(int call_type, xmlNode * request,
                    gboolean * local_notify, gboolean * needs_reply, gboolean * process,
                    gboolean * needs_forward)
 {
     const char *host = NULL;
     const char *delegated = crm_element_value(request, F_CIB_DELEGATED);
     const char *op = crm_element_value(request, F_CIB_OPERATION);
     const char *originator = crm_element_value(request, F_ORIG);
     const char *reply_to = crm_element_value(request, F_CIB_ISREPLY);
 
     gboolean is_reply = pcmk__str_eq(reply_to, OUR_NODENAME, pcmk__str_casei);
 
     if (pcmk__str_eq(op, PCMK__CIB_REQUEST_REPLACE, pcmk__str_none)) {
         /* sync_our_cib() sets F_CIB_ISREPLY */
         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 F_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 F_CIB_SCHEMA_MAX will be set which puts a
          * limit on how far newer nodes will go
          */
         const char *max = crm_element_value(request, F_CIB_SCHEMA_MAX);
         const char *upgrade_rc = crm_element_value(request, F_CIB_UPGRADE_RC);
 
         crm_trace("Parsing %s operation%s for %s with max=%s and upgrade_rc=%s",
                   op, (is_reply? " reply" : ""),
                   (based_is_primary? "primary" : "secondary"),
                   (max? max : "none"), (upgrade_rc? upgrade_rc : "none"));
 
         if (upgrade_rc != NULL) {
             // Our upgrade request was rejected by DC, notify clients of result
             crm_xml_add(request, F_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 DC
             return FALSE;
         }
 
     } else if (pcmk__xe_attr_is_true(request, F_CIB_GLOBAL_UPDATE)) {
         crm_info("Detected legacy %s global update from %s", op, originator);
         send_sync_request(NULL);
         legacy_mode = TRUE;
         return FALSE;
 
     } else if (is_reply && cib_op_modifies(call_type)) {
         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)) {
         /* Legacy handling */
         crm_debug("Legacy handling of %s message from %s", op, originator);
         *local_notify = FALSE;
         if (reply_to == NULL) {
             *process = TRUE;
         }
         return *process;
     }
 
     if(is_reply) {
         crm_trace("Handling %s reply sent from %s to local clients", 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 = crm_element_value(request, F_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) {
         /* this is for a specific instance and we're not it */
         crm_trace("Ignoring %s operation for instance 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 sent to everyone by %s/%s on %s %s", op,
               crm_element_value(request, F_CIB_CLIENTNAME),
               crm_element_value(request, F_CIB_CALLID),
               originator, (*local_notify)?"(notify)":"");
     return TRUE;
 }
 
 static gboolean
 parse_peer_options(int call_type, xmlNode * request,
                    gboolean * local_notify, gboolean * needs_reply, gboolean * process,
                    gboolean * needs_forward)
 {
     /* 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)
      */
     if(cib_legacy_mode()) {
         return parse_peer_options_v1(
             call_type, request, local_notify, needs_reply, process, needs_forward);
     } else {
         return parse_peer_options_v2(
             call_type, request, local_notify, needs_reply, process, needs_forward);
     }
 }
 
 static void
 forward_request(xmlNode *request, int call_options)
 {
     const char *op = crm_element_value(request, F_CIB_OPERATION);
     const char *host = crm_element_value(request, F_CIB_HOST);
 
     crm_xml_add(request, F_CIB_DELEGATED, OUR_NODENAME);
 
     if (host != NULL) {
         crm_trace("Forwarding %s op to %s", op, host);
         send_cluster_message(crm_get_peer(0, host), crm_msg_cib, request, FALSE);
 
     } else {
         crm_trace("Forwarding %s op to primary instance", op);
         send_cluster_message(NULL, crm_msg_cib, request, FALSE);
     }
 
     /* Return the request to its original state */
     xml_remove_prop(request, F_CIB_DELEGATED);
 
     if (call_options & cib_discard_reply) {
         crm_trace("Client not interested in reply");
     }
 }
 
 static gboolean
 send_peer_reply(xmlNode * msg, xmlNode * result_diff, const char *originator, gboolean broadcast)
 {
     CRM_ASSERT(msg != NULL);
 
     if (broadcast) {
         /* this (successful) call modified the CIB _and_ the
          * change needs to be broadcast...
          *   send via HA to other nodes
          */
         int diff_add_updates = 0;
         int diff_add_epoch = 0;
         int diff_add_admin_epoch = 0;
 
         int diff_del_updates = 0;
         int diff_del_epoch = 0;
         int diff_del_admin_epoch = 0;
 
         const char *digest = NULL;
         int format = 1;
 
         CRM_LOG_ASSERT(result_diff != NULL);
         digest = crm_element_value(result_diff, XML_ATTR_DIGEST);
         crm_element_value_int(result_diff, "format", &format);
 
         cib_diff_version_details(result_diff,
                                  &diff_add_admin_epoch, &diff_add_epoch, &diff_add_updates,
                                  &diff_del_admin_epoch, &diff_del_epoch, &diff_del_updates);
 
         crm_trace("Sending update diff %d.%d.%d -> %d.%d.%d %s",
                   diff_del_admin_epoch, diff_del_epoch, diff_del_updates,
                   diff_add_admin_epoch, diff_add_epoch, diff_add_updates, digest);
 
         crm_xml_add(msg, F_CIB_ISREPLY, originator);
         pcmk__xe_set_bool_attr(msg, F_CIB_GLOBAL_UPDATE, true);
         crm_xml_add(msg, F_CIB_OPERATION, PCMK__CIB_REQUEST_APPLY_PATCH);
         crm_xml_add(msg, F_CIB_USER, CRM_DAEMON_USER);
 
         if (format == 1) {
             CRM_ASSERT(digest != NULL);
         }
 
         add_message_xml(msg, F_CIB_UPDATE_DIFF, result_diff);
         crm_log_xml_explicit(msg, "copy");
         return send_cluster_message(NULL, crm_msg_cib, msg, TRUE);
 
     } else if (originator != NULL) {
         /* send reply via HA to originating node */
         crm_trace("Sending request result to %s only", originator);
         crm_xml_add(msg, F_CIB_ISREPLY, originator);
         return send_cluster_message(crm_get_peer(0, originator), crm_msg_cib, msg, FALSE);
     }
 
     return FALSE;
 }
 
 /*!
  * \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)
  */
 static void
 cib_process_request(xmlNode *request, gboolean privileged,
                     const pcmk__client_t *cib_client)
 {
     int call_type = 0;
     int call_options = 0;
 
     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 = crm_element_value(request, F_CIB_OPERATION);
     const char *originator = crm_element_value(request, F_ORIG);
     const char *host = crm_element_value(request, F_CIB_HOST);
     const char *target = NULL;
     const char *call_id = crm_element_value(request, F_CIB_CALLID);
     const char *client_id = crm_element_value(request, F_CIB_CLIENTID);
     const char *client_name = crm_element_value(request, F_CIB_CLIENTNAME);
     const char *reply_to = crm_element_value(request, F_CIB_ISREPLY);
 
     crm_element_value_int(request, F_CIB_CALLOPTS, &call_options);
 
     if ((host != NULL) && (*host == '\0')) {
         host = NULL;
     }
 
     if (host) {
         target = host;
 
     } else if (call_options & cib_scope_local) {
         target = "local host";
 
     } else {
         target = "primary";
     }
 
     if (cib_client == NULL) {
         crm_trace("Processing peer %s operation from %s/%s on %s intended for %s (reply=%s)",
                   op, client_name, call_id, originator, target, reply_to);
     } else {
         crm_xml_add(request, F_ORIG, OUR_NODENAME);
         crm_trace("Processing local %s operation from %s/%s intended for %s", op, client_name, call_id, target);
     }
 
     rc = cib_get_operation_id(op, &call_type);
     if (rc != pcmk_ok) {
         /* TODO: construct error reply? */
         crm_err("Pre-processing of command failed: %s", pcmk_strerror(rc));
         return;
     }
 
     if (cib_client != NULL) {
         parse_local_options(cib_client, call_type, call_options, host, op,
                             &local_notify, &needs_reply, &process, &needs_forward);
 
     } else if (parse_peer_options(call_type, request, &local_notify,
                                   &needs_reply, &process, &needs_forward) == FALSE) {
         return;
     }
 
     is_update = cib_op_modifies(call_type);
 
     if (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 = is_update && cib_legacy_mode();
         local_notify = FALSE;
     }
 
     if (needs_forward) {
         const char *section = crm_element_value(request, F_CIB_SECTION);
         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)",
                    op,
                    section ? section : "'all'",
                    pcmk__s(host, (cib_legacy_mode() ? "primary" : "all")),
                    originator ? originator : "local",
                    client_name, call_id);
 
         forward_request(request, call_options);
         return;
     }
 
     if (cib_status != pcmk_ok) {
         const char *call = crm_element_value(request, F_CIB_CALLID);
 
         rc = cib_status;
         crm_err("Operation ignored, cluster configuration is invalid."
                 " Please repair and restart: %s", pcmk_strerror(cib_status));
 
         op_reply = create_xml_node(NULL, "cib-reply");
         crm_xml_add(op_reply, F_TYPE, T_CIB);
         crm_xml_add(op_reply, F_CIB_OPERATION, op);
         crm_xml_add(op_reply, F_CIB_CALLID, call);
         crm_xml_add(op_reply, F_CIB_CLIENTID, client_id);
         crm_xml_add_int(op_reply, F_CIB_CALLOPTS, call_options);
         crm_xml_add_int(op_reply, F_CIB_RC, rc);
 
         crm_trace("Attaching reply output");
         add_message_xml(op_reply, F_CIB_CALLDATA, the_cib);
 
         crm_log_xml_explicit(op_reply, "cib:reply");
 
     } else if (process) {
         time_t finished = 0;
         time_t now = time(NULL);
         int level = LOG_INFO;
         const char *section = crm_element_value(request, F_CIB_SECTION);
 
         rc = cib_process_command(request, &op_reply, &result_diff, privileged);
 
         if (!is_update) {
             level = LOG_TRACE;
 
         } else if (pcmk__xe_attr_is_true(request, F_CIB_GLOBAL_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;
         }
 
         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", client_name, call_id,
                    the_cib ? crm_element_value(the_cib, XML_ATTR_GENERATION_ADMIN) : "0",
                    the_cib ? crm_element_value(the_cib, XML_ATTR_GENERATION) : "0",
                    the_cib ? crm_element_value(the_cib, XML_ATTR_NUMUPDATES) : "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 && !cib_legacy_mode()) {
         crm_trace("Completed pre-sync update from %s/%s/%s%s",
                   originator ? originator : "local", client_name, 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_legacy_mode() &&
                rc == pcmk_ok && result_diff != NULL && !(call_options & cib_inhibit_bcast)) {
         gboolean broadcast = FALSE;
 
         cib_local_bcast_num++;
         crm_xml_add_int(request, F_CIB_LOCAL_NOTIFY_ID, cib_local_bcast_num);
         broadcast = send_peer_reply(request, result_diff, originator, TRUE);
 
         if (broadcast && client_id && local_notify && op_reply) {
 
             /* If we have been asked to sync the reply,
              * and a bcast msg has gone out, we queue the local notify
              * until we know the bcast message has been received */
             local_notify = FALSE;
             crm_trace("Queuing local %ssync notification for %s",
                       (call_options & cib_sync_call) ? "" : "a-", client_id);
 
             queue_local_notify(op_reply, client_id,
                                pcmk_is_set(call_options, cib_sync_call),
                                (cib_client == NULL));
             op_reply = NULL;    /* the reply is queued, so don't free here */
         }
 
     } else if (call_options & cib_discard_reply) {
         crm_trace("Caller isn't interested in reply");
 
     } else if (cib_client == NULL) {
         if (is_update == FALSE || result_diff == NULL) {
             crm_trace("Request not broadcast: R/O call");
 
         } else if (call_options & cib_inhibit_bcast) {
             crm_trace("Request not broadcast: inhibited");
 
         } 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, result_diff, originator, FALSE);
     }
 
     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));
         }
     }
 
     free_xml(op_reply);
     free_xml(result_diff);
 
     return;
 }
 
 static char *
 calculate_section_digest(const char *xpath, xmlNode * xml_obj)
 {
     xmlNode *xml_section = NULL;
 
     if (xml_obj == NULL) {
         return NULL;
     }
 
     xml_section = get_xpath_object(xpath, xml_obj, LOG_TRACE);
     if (xml_section == NULL) {
         return NULL;
     }
     return calculate_xml_versioned_digest(xml_section, FALSE, TRUE, CRM_FEATURE_SET); 
 
 }
 
 static int
 cib_process_command(xmlNode * request, xmlNode ** reply, xmlNode ** cib_diff, gboolean privileged)
 {
     xmlNode *input = NULL;
     xmlNode *output = NULL;
     xmlNode *result_cib = NULL;
     xmlNode *current_cib = NULL;
 
     int call_type = 0;
     int call_options = 0;
 
     const char *op = NULL;
     const char *section = NULL;
     const char *call_id = crm_element_value(request, F_CIB_CALLID);
 
     int rc = pcmk_ok;
     int rc2 = pcmk_ok;
 
     gboolean send_r_notify = FALSE;
     gboolean global_update = FALSE;
     gboolean config_changed = FALSE;
     gboolean manage_counters = TRUE;
 
     static mainloop_timer_t *digest_timer = NULL;
 
     char *current_nodes_digest = NULL;
     char *current_alerts_digest = NULL;
     char *current_status_digest = NULL;
     uint32_t change_section = cib_change_section_nodes
                               |cib_change_section_alerts
                               |cib_change_section_status;
 
     CRM_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;
     current_cib = the_cib;
 
     /* Start processing the request... */
     op = crm_element_value(request, F_CIB_OPERATION);
     crm_element_value_int(request, F_CIB_CALLOPTS, &call_options);
     rc = cib_get_operation_id(op, &call_type);
 
     if (rc == pcmk_ok && privileged == FALSE) {
         rc = cib_op_can_run(call_type, call_options, privileged, global_update);
     }
 
     rc2 = cib_op_prepare(call_type, request, &input, &section);
     if (rc == pcmk_ok) {
         rc = rc2;
     }
 
     if (rc != pcmk_ok) {
         crm_trace("Call setup failed: %s", pcmk_strerror(rc));
         goto done;
 
     } else if (cib_op_modifies(call_type) == FALSE) {
         rc = cib_perform_op(op, call_options, cib_op_func(call_type), TRUE,
                             section, request, input, FALSE, &config_changed,
                             current_cib, &result_cib, NULL, &output);
 
         CRM_CHECK(result_cib == NULL, free_xml(result_cib));
         goto done;
     }
 
     /* Handle a valid write action */
     if (pcmk__xe_attr_is_true(request, F_CIB_GLOBAL_UPDATE)) {
         /* legacy code */
         manage_counters = FALSE;
         cib__set_call_options(call_options, "call", cib_force_diff);
         crm_trace("Global update detected");
 
         CRM_CHECK(call_type == 3 || call_type == 4, crm_err("Call type: %d", call_type);
                   crm_log_xml_err(request, "bad op"));
     }
 
     ping_modified_since = TRUE;
     if (pcmk_is_set(call_options, cib_inhibit_bcast)) {
         crm_trace("Skipping update: inhibit broadcast");
         manage_counters = FALSE;
     }
 
     if (!pcmk_is_set(call_options, cib_dryrun)
         && pcmk__str_eq(section, XML_CIB_TAG_STATUS, pcmk__str_casei)) {
         // Copying large CIBs accounts for a huge percentage of our CIB usage
         cib__set_call_options(call_options, "call", cib_zero_copy);
     } else {
         cib__clear_call_options(call_options, "call", cib_zero_copy);
     }
 
 #define XPATH_CONFIG    "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION
 #define XPATH_NODES     XPATH_CONFIG "/" XML_CIB_TAG_NODES
 #define XPATH_ALERTS    XPATH_CONFIG "/" XML_CIB_TAG_ALERTS
 #define XPATH_STATUS    "//" XML_TAG_CIB "/" XML_CIB_TAG_STATUS
 
     // Calculate the hash value of the section before the change
     if (pcmk__str_eq(PCMK__CIB_REQUEST_REPLACE, op, pcmk__str_none)) {
         current_nodes_digest = calculate_section_digest(XPATH_NODES,
                                                         current_cib);
         current_alerts_digest = calculate_section_digest(XPATH_ALERTS,
                                                          current_cib);
         current_status_digest = calculate_section_digest(XPATH_STATUS,
                                                          current_cib);
         crm_trace("current-digest %s:%s:%s", current_nodes_digest,
                   current_alerts_digest, current_status_digest);
     }
 
     // result_cib must not be modified after cib_perform_op() returns
     rc = cib_perform_op(op, call_options, cib_op_func(call_type), FALSE,
                         section, request, input, manage_counters,
                         &config_changed, current_cib, &result_cib, cib_diff,
                         &output);
 
     if (!manage_counters) {
         int format = 1;
 
         /* Legacy code
          * If the diff is NULL at this point, it's because nothing changed
          */
         if (*cib_diff != NULL) {
             crm_element_value_int(*cib_diff, "format", &format);
         }
 
         if (format == 1) {
             config_changed = cib_config_changed(NULL, NULL, cib_diff);
         }
     }
 
     /* Always write to disk for successful replace and upgrade ops. This also
      * negates the need to detect ordering changes.
      */
     if ((rc == pcmk_ok)
         && pcmk__str_any_of(op,
                             PCMK__CIB_REQUEST_REPLACE,
                             PCMK__CIB_REQUEST_UPGRADE,
                             NULL)) {
         config_changed = TRUE;
     }
 
     if (rc == pcmk_ok && !pcmk_is_set(call_options, cib_dryrun)) {
         crm_trace("Activating %s->%s%s%s",
                   crm_element_value(current_cib, XML_ATTR_NUMUPDATES),
                   crm_element_value(result_cib, XML_ATTR_NUMUPDATES),
                   (pcmk_is_set(call_options, cib_zero_copy)? " zero-copy" : ""),
                   (config_changed? " changed" : ""));
         if (!pcmk_is_set(call_options, cib_zero_copy)) {
             rc = activateCibXml(result_cib, config_changed, op);
             crm_trace("Activated %s (%d)",
                       crm_element_value(current_cib, XML_ATTR_NUMUPDATES), rc);
         }
 
         if (rc == pcmk_ok && cib_internal_config_changed(*cib_diff)) {
             cib_read_config(config_hash, result_cib);
         }
 
         if (pcmk__str_eq(PCMK__CIB_REQUEST_REPLACE, op, pcmk__str_none)) {
             char *result_nodes_digest = NULL;
             char *result_alerts_digest = NULL;
             char *result_status_digest = NULL;
 
             /* Calculate the hash value of the changed section. */
             result_nodes_digest = calculate_section_digest(XPATH_NODES,
                                                            result_cib);
             result_alerts_digest = calculate_section_digest(XPATH_ALERTS,
                                                             result_cib);
             result_status_digest = calculate_section_digest(XPATH_STATUS,
                                                             result_cib);
             crm_trace("result-digest %s:%s:%s", result_nodes_digest,
                       result_alerts_digest, result_status_digest);
 
             if (pcmk__str_eq(current_nodes_digest, result_nodes_digest,
                              pcmk__str_none)) {
                 change_section =
                     pcmk__clear_flags_as(__func__, __LINE__, LOG_TRACE,
                                          "CIB change section",
                                          "change_section", change_section,
                                          cib_change_section_nodes, "nodes");
             }
 
             if (pcmk__str_eq(current_alerts_digest, result_alerts_digest,
                              pcmk__str_none)) {
                 change_section =
                     pcmk__clear_flags_as(__func__, __LINE__, LOG_TRACE,
                                          "CIB change section",
                                          "change_section", change_section,
                                          cib_change_section_alerts, "alerts");
             }
 
             if (pcmk__str_eq(current_status_digest, result_status_digest,
                              pcmk__str_none)) {
                 change_section =
                     pcmk__clear_flags_as(__func__, __LINE__, LOG_TRACE,
                                          "CIB change section",
                                          "change_section", change_section,
                                          cib_change_section_status, "status");
             }
 
             if (change_section != cib_change_section_none) {
                 send_r_notify = TRUE;
             }
             
             free(result_nodes_digest);
             free(result_alerts_digest);
             free(result_status_digest);
 
         } else if (pcmk__str_eq(PCMK__CIB_REQUEST_ERASE, op, pcmk__str_none)) {
             send_r_notify = TRUE;
         }
 
         mainloop_timer_stop(digest_timer);
         mainloop_timer_start(digest_timer);
 
     } else if (rc == -pcmk_err_schema_validation) {
         CRM_ASSERT(!pcmk_is_set(call_options, cib_zero_copy));
 
         if (output != NULL) {
             crm_log_xml_info(output, "cib:output");
             free_xml(output);
         }
 
         output = result_cib;
 
     } else {
         crm_trace("Not activating %d %d %s", rc,
                   pcmk_is_set(call_options, cib_dryrun),
                   crm_element_value(result_cib, XML_ATTR_NUMUPDATES));
         if (!pcmk_is_set(call_options, cib_zero_copy)) {
             free_xml(result_cib);
         }
     }
 
     if ((call_options & (cib_inhibit_notify|cib_dryrun)) == 0) {
         const char *client = crm_element_value(request, F_CIB_CLIENTNAME);
 
         crm_trace("Sending notifications %d",
                   pcmk_is_set(call_options, cib_dryrun));
         cib_diff_notify(call_options, client, call_id, op, input, rc, *cib_diff);
     }
 
     if (send_r_notify) {
         const char *origin = crm_element_value(request, F_ORIG);
 
         cib_replace_notify(origin, the_cib, rc, *cib_diff, change_section);
     }
 
-    pcmk__xml_log_patchset(LOG_TRACE, *cib_diff);
+    pcmk__output_set_log_level(logger_out, LOG_TRACE);
+    logger_out->message(logger_out, "xml-patchset", *cib_diff);
+
   done:
     if (!pcmk_is_set(call_options, cib_discard_reply) || cib_legacy_mode()) {
         const char *caller = crm_element_value(request, F_CIB_CLIENTID);
 
         *reply = create_xml_node(NULL, "cib-reply");
         crm_xml_add(*reply, F_TYPE, T_CIB);
         crm_xml_add(*reply, F_CIB_OPERATION, op);
         crm_xml_add(*reply, F_CIB_CALLID, call_id);
         crm_xml_add(*reply, F_CIB_CLIENTID, caller);
         crm_xml_add_int(*reply, F_CIB_CALLOPTS, call_options);
         crm_xml_add_int(*reply, F_CIB_RC, rc);
 
         if (output != NULL) {
             crm_trace("Attaching reply output");
             add_message_xml(*reply, F_CIB_CALLDATA, output);
         }
 
         crm_log_xml_explicit(*reply, "cib:reply");
     }
 
     crm_trace("cleanup");
 
     if (cib_op_modifies(call_type) == FALSE && output != current_cib) {
         free_xml(output);
         output = NULL;
     }
 
     if (call_type >= 0) {
         cib_op_cleanup(call_type, call_options, &input, &output);
     }
 
     free(current_nodes_digest);
     free(current_alerts_digest);
     free(current_status_digest);
 
     crm_trace("done");
     return rc;
 }
 
 void
 cib_peer_callback(xmlNode * msg, void *private_data)
 {
     const char *reason = NULL;
     const char *originator = crm_element_value(msg, F_ORIG);
 
     if (cib_legacy_mode()
         && pcmk__str_eq(originator, OUR_NODENAME,
                         pcmk__str_casei|pcmk__str_null_matches)) {
         /* message is from ourselves */
         int bcast_id = 0;
 
         if (!(crm_element_value_int(msg, F_CIB_LOCAL_NOTIFY_ID, &bcast_id))) {
             check_local_notify(bcast_id);
         }
         return;
 
     } else if (crm_peer_cache == NULL) {
         reason = "membership not established";
         goto bail;
     }
 
     if (crm_element_value(msg, F_CIB_CLIENTNAME) == NULL) {
         crm_xml_add(msg, F_CIB_CLIENTNAME, originator);
     }
 
     /* crm_log_xml_trace(msg, "Peer[inbound]"); */
     cib_process_request(msg, TRUE, NULL);
     return;
 
   bail:
     if (reason) {
         const char *seq = crm_element_value(msg, F_SEQ);
         const char *op = crm_element_value(msg, F_CIB_OPERATION);
 
         crm_warn("Discarding %s message (%s) from %s: %s", op, seq, originator, reason);
     }
 }
 
 static gboolean
 cib_force_exit(gpointer data)
 {
     crm_notice("Forcing exit!");
     terminate_cib(__func__, 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));
 }
 
 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);
     }
 }
 
 void
 initiate_exit(void)
 {
     int active = 0;
     xmlNode *leaving = NULL;
 
     active = crm_active_peers();
     if (active < 2) {
         terminate_cib(__func__, 0);
         return;
     }
 
     crm_info("Sending disconnect notification to %d peers...", active);
 
     leaving = create_xml_node(NULL, "exit-notification");
     crm_xml_add(leaving, F_TYPE, "cib");
     crm_xml_add(leaving, F_CIB_OPERATION, PCMK__CIB_REQUEST_SHUTDOWN);
 
     send_cluster_message(NULL, crm_msg_cib, leaving, TRUE);
     free_xml(leaving);
 
     g_timeout_add(EXIT_ESCALATION_MS, cib_force_exit, NULL);
 }
 
 extern int remote_fd;
 extern int remote_tls_fd;
 
 /*!
  * \internal
  * \brief Close remote sockets, free the global CIB and quit
  *
  * \param[in] caller           Name of calling function (for log message)
  * \param[in] fast             If -1, skip disconnect; if positive, exit that
  */
 void
 terminate_cib(const char *caller, int fast)
 {
     crm_info("%s: Exiting%s...", caller,
              (fast > 0)? " fast" : mainloop ? " from mainloop" : "");
 
     if (remote_fd > 0) {
         close(remote_fd);
         remote_fd = 0;
     }
     if (remote_tls_fd > 0) {
         close(remote_tls_fd);
         remote_tls_fd = 0;
     }
 
     uninitializeCib();
 
     if (logger_out != NULL) {
         logger_out->finish(logger_out, CRM_EX_OK, true, NULL);
         pcmk__output_free(logger_out);
         logger_out = NULL;
     }
 
     if (fast > 0) {
         /* Quit fast on error */
         pcmk__stop_based_ipc(ipcs_ro, ipcs_rw, ipcs_shm);
         crm_exit(fast);
 
     } else if ((mainloop != NULL) && g_main_loop_is_running(mainloop)) {
         /* Quit via returning from the main loop. If fast == -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 (fast == 0) {
             crm_cluster_disconnect(crm_cluster);
         }
         g_main_loop_quit(mainloop);
 
     } else {
         /* Quit via clean exit. Even the peer status callback can disconnect
          * here, because we're not returning control to the caller. */
         crm_cluster_disconnect(crm_cluster);
         pcmk__stop_based_ipc(ipcs_ro, ipcs_rw, ipcs_shm);
         crm_exit(CRM_EX_OK);
     }
 }
diff --git a/daemons/based/based_messages.c b/daemons/based/based_messages.c
index 4414f3cd4c..4c6b978886 100644
--- a/daemons/based/based_messages.c
+++ b/daemons/based/based_messages.c
@@ -1,436 +1,438 @@
 /*
  * Copyright 2004-2023 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 <unistd.h>
 #include <stdlib.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <time.h>
 
 #include <sys/param.h>
 #include <sys/types.h>
 
 #include <crm/crm.h>
 #include <crm/cib/internal.h>
 #include <crm/msg_xml.h>
 
 #include <crm/common/xml.h>
 #include <crm/common/ipc_internal.h>
 #include <crm/common/xml_internal.h>
 #include <crm/cluster/internal.h>
 
 #include <pacemaker-based.h>
 
 /* Maximum number of diffs to ignore while waiting for a resync */
 #define MAX_DIFF_RETRY 5
 
 bool based_is_primary = false;
 
 xmlNode *the_cib = NULL;
 int revision_check(xmlNode * cib_update, xmlNode * cib_copy, int flags);
 int get_revision(xmlNode * xml_obj, int cur_revision);
 
 int updateList(xmlNode * local_cib, xmlNode * update_command, xmlNode * failed,
                int operation, const char *section);
 
 gboolean update_results(xmlNode * failed, xmlNode * target, const char *operation, int return_code);
 
 int cib_update_counter(xmlNode * xml_obj, const char *field, gboolean reset);
 
 int sync_our_cib(xmlNode * request, gboolean all);
 
 int
 cib_process_shutdown_req(const char *op, int options, const char *section, xmlNode * req,
                          xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib,
                          xmlNode ** answer)
 {
     const char *host = crm_element_value(req, F_ORIG);
 
     *answer = NULL;
 
     if (crm_element_value(req, F_CIB_ISREPLY) == NULL) {
         crm_info("Peer %s is requesting to shut down", host);
         return pcmk_ok;
     }
 
     if (cib_shutdown_flag == FALSE) {
         crm_err("Peer %s mistakenly thinks we wanted to shut down", host);
         return -EINVAL;
     }
 
     crm_info("Peer %s has acknowledged our shutdown request", host);
     terminate_cib(__func__, 0);
     return pcmk_ok;
 }
 
 int
 cib_process_default(const char *op, int options, const char *section, xmlNode * req,
                     xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib,
                     xmlNode ** answer)
 {
     int result = pcmk_ok;
 
     crm_trace("Processing \"%s\" event", op);
     *answer = NULL;
 
     if (op == NULL) {
         result = -EINVAL;
         crm_err("No operation specified");
 
     } else if (strcmp(PCMK__CIB_REQUEST_NOOP, op) != 0) {
         result = -EPROTONOSUPPORT;
         crm_err("Action [%s] is not supported by the CIB manager", op);
     }
     return result;
 }
 
 int
 cib_process_readwrite(const char *op, int options, const char *section, xmlNode * req,
                       xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib,
                       xmlNode ** answer)
 {
     int result = pcmk_ok;
 
     crm_trace("Processing \"%s\" event", op);
 
     if (pcmk__str_eq(op, PCMK__CIB_REQUEST_IS_PRIMARY, pcmk__str_none)) {
         if (based_is_primary) {
             result = pcmk_ok;
         } else {
             result = -EPERM;
         }
         return result;
     }
 
     if (pcmk__str_eq(op, PCMK__CIB_REQUEST_PRIMARY, pcmk__str_none)) {
         if (!based_is_primary) {
             crm_info("We are now in R/W mode");
             based_is_primary = true;
         } else {
             crm_debug("We are still in R/W mode");
         }
 
     } else if (based_is_primary) {
         crm_info("We are now in R/O mode");
         based_is_primary = false;
     }
 
     return result;
 }
 
 /* Set to 1 when a sync is requested, incremented when a diff is ignored,
  * reset to 0 when a sync is received
  */
 static int sync_in_progress = 0;
 
 void
 send_sync_request(const char *host)
 {
     xmlNode *sync_me = create_xml_node(NULL, "sync-me");
 
     crm_info("Requesting re-sync from %s", (host? host : "all peers"));
     sync_in_progress = 1;
 
     crm_xml_add(sync_me, F_TYPE, "cib");
     crm_xml_add(sync_me, F_CIB_OPERATION, PCMK__CIB_REQUEST_SYNC_TO_ONE);
     crm_xml_add(sync_me, F_CIB_DELEGATED,
                 stand_alone? "localhost" : crm_cluster->uname);
 
     send_cluster_message(host ? crm_get_peer(0, host) : NULL, crm_msg_cib, sync_me, FALSE);
     free_xml(sync_me);
 }
 
 int
 cib_process_ping(const char *op, int options, const char *section, xmlNode * req, xmlNode * input,
                  xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer)
 {
     const char *host = crm_element_value(req, F_ORIG);
     const char *seq = crm_element_value(req, F_CIB_PING_ID);
     char *digest = calculate_xml_versioned_digest(the_cib, FALSE, TRUE, CRM_FEATURE_SET);
 
     crm_trace("Processing \"%s\" event %s from %s", op, seq, host);
     *answer = create_xml_node(NULL, XML_CRM_TAG_PING);
 
     crm_xml_add(*answer, XML_ATTR_CRM_VERSION, CRM_FEATURE_SET);
     crm_xml_add(*answer, XML_ATTR_DIGEST, digest);
     crm_xml_add(*answer, F_CIB_PING_ID, seq);
 
     pcmk__if_tracing(
         {
             // Append additional detail so the receiver can log the differences
             add_message_xml(*answer, F_CIB_CALLDATA, the_cib);
         },
         {
             // Always include at least the version details
             const char *tag = TYPE(the_cib);
             xmlNode *shallow = create_xml_node(NULL, tag);
 
             copy_in_properties(shallow, the_cib);
             add_message_xml(*answer, F_CIB_CALLDATA, shallow);
             free_xml(shallow);
         }
     );
 
     crm_info("Reporting our current digest to %s: %s for %s.%s.%s",
              host, digest,
              crm_element_value(existing_cib, XML_ATTR_GENERATION_ADMIN),
              crm_element_value(existing_cib, XML_ATTR_GENERATION),
              crm_element_value(existing_cib, XML_ATTR_NUMUPDATES));
 
     free(digest);
 
     return pcmk_ok;
 }
 
 int
 cib_process_sync(const char *op, int options, const char *section, xmlNode * req, xmlNode * input,
                  xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer)
 {
     return sync_our_cib(req, TRUE);
 }
 
 int
 cib_process_upgrade_server(const char *op, int options, const char *section, xmlNode * req, xmlNode * input,
                            xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer)
 {
     int rc = pcmk_ok;
 
     *answer = NULL;
 
     if(crm_element_value(req, F_CIB_SCHEMA_MAX)) {
         /* The originator of an upgrade request sends it to the DC, without
          * F_CIB_SCHEMA_MAX. If an upgrade is needed, the DC re-broadcasts the
          * request with F_CIB_SCHEMA_MAX, and each node performs the upgrade
          * (and notifies its local clients) here.
          */
         return cib_process_upgrade(
             op, options, section, req, input, existing_cib, result_cib, answer);
 
     } else {
         int new_version = 0;
         int current_version = 0;
         xmlNode *scratch = copy_xml(existing_cib);
         const char *host = crm_element_value(req, F_ORIG);
         const char *value = crm_element_value(existing_cib, XML_ATTR_VALIDATION);
         const char *client_id = crm_element_value(req, F_CIB_CLIENTID);
         const char *call_opts = crm_element_value(req, F_CIB_CALLOPTS);
         const char *call_id = crm_element_value(req, F_CIB_CALLID);
 
         crm_trace("Processing \"%s\" event", op);
         if (value != NULL) {
             current_version = get_schema_version(value);
         }
 
         rc = update_validation(&scratch, &new_version, 0, TRUE, TRUE);
         if (new_version > current_version) {
             xmlNode *up = create_xml_node(NULL, __func__);
 
             rc = pcmk_ok;
             crm_notice("Upgrade request from %s verified", host);
 
             crm_xml_add(up, F_TYPE, "cib");
             crm_xml_add(up, F_CIB_OPERATION, PCMK__CIB_REQUEST_UPGRADE);
             crm_xml_add(up, F_CIB_SCHEMA_MAX, get_schema_name(new_version));
             crm_xml_add(up, F_CIB_DELEGATED, host);
             crm_xml_add(up, F_CIB_CLIENTID, client_id);
             crm_xml_add(up, F_CIB_CALLOPTS, call_opts);
             crm_xml_add(up, F_CIB_CALLID, call_id);
 
             if (cib_legacy_mode() && based_is_primary) {
                 rc = cib_process_upgrade(
                     op, options, section, up, input, existing_cib, result_cib, answer);
 
             } else {
                 send_cluster_message(NULL, crm_msg_cib, up, FALSE);
             }
 
             free_xml(up);
 
         } else if(rc == pcmk_ok) {
             rc = -pcmk_err_schema_unchanged;
         }
 
         if (rc != pcmk_ok) {
             // Notify originating peer so it can notify its local clients
             crm_node_t *origin = pcmk__search_cluster_node_cache(0, host);
 
             crm_info("Rejecting upgrade request from %s: %s "
                      CRM_XS " rc=%d peer=%s", host, pcmk_strerror(rc), rc,
                      (origin? origin->uname : "lost"));
 
             if (origin) {
                 xmlNode *up = create_xml_node(NULL, __func__);
 
                 crm_xml_add(up, F_TYPE, "cib");
                 crm_xml_add(up, F_CIB_OPERATION, PCMK__CIB_REQUEST_UPGRADE);
                 crm_xml_add(up, F_CIB_DELEGATED, host);
                 crm_xml_add(up, F_CIB_ISREPLY, host);
                 crm_xml_add(up, F_CIB_CLIENTID, client_id);
                 crm_xml_add(up, F_CIB_CALLOPTS, call_opts);
                 crm_xml_add(up, F_CIB_CALLID, call_id);
                 crm_xml_add_int(up, F_CIB_UPGRADE_RC, rc);
                 if (send_cluster_message(origin, crm_msg_cib, up, TRUE)
                     == FALSE) {
                     crm_warn("Could not send CIB upgrade result to %s", host);
                 }
                 free_xml(up);
             }
         }
         free_xml(scratch);
     }
     return rc;
 }
 
 int
 cib_process_sync_one(const char *op, int options, const char *section, xmlNode * req,
                      xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib,
                      xmlNode ** answer)
 {
     return sync_our_cib(req, FALSE);
 }
 
 int
 cib_server_process_diff(const char *op, int options, const char *section, xmlNode * req,
                         xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib,
                         xmlNode ** answer)
 {
     int rc = pcmk_ok;
 
     if (sync_in_progress > MAX_DIFF_RETRY) {
         /* Don't ignore diffs forever; the last request may have been lost.
          * If the diff fails, we'll ask for another full resync.
          */
         sync_in_progress = 0;
     }
 
     // The primary instance should never ignore a diff
     if (sync_in_progress && !based_is_primary) {
         int diff_add_updates = 0;
         int diff_add_epoch = 0;
         int diff_add_admin_epoch = 0;
 
         int diff_del_updates = 0;
         int diff_del_epoch = 0;
         int diff_del_admin_epoch = 0;
 
         cib_diff_version_details(input,
                                  &diff_add_admin_epoch, &diff_add_epoch, &diff_add_updates,
                                  &diff_del_admin_epoch, &diff_del_epoch, &diff_del_updates);
 
         sync_in_progress++;
         crm_notice("Not applying diff %d.%d.%d -> %d.%d.%d (sync in progress)",
                    diff_del_admin_epoch, diff_del_epoch, diff_del_updates,
                    diff_add_admin_epoch, diff_add_epoch, diff_add_updates);
         return -pcmk_err_diff_resync;
     }
 
     rc = cib_process_diff(op, options, section, req, input, existing_cib, result_cib, answer);
     crm_trace("result: %s (%d), %s", pcmk_strerror(rc), rc,
               (based_is_primary? "primary": "secondary"));
 
     if ((rc == -pcmk_err_diff_resync) && !based_is_primary) {
         free_xml(*result_cib);
         *result_cib = NULL;
         send_sync_request(NULL);
 
     } else if (rc == -pcmk_err_diff_resync) {
         rc = -pcmk_err_diff_failed;
         if (options & cib_force_diff) {
             crm_warn("Not requesting full refresh in R/W mode");
         }
 
     } else if ((rc != pcmk_ok) && !based_is_primary && cib_legacy_mode()) {
         crm_warn("Requesting full CIB refresh because update failed: %s"
                  CRM_XS " rc=%d", pcmk_strerror(rc), rc);
-        pcmk__xml_log_patchset(LOG_INFO, input);
+
+        pcmk__output_set_log_level(logger_out, LOG_INFO);
+        logger_out->message(logger_out, "xml-patchset", input);
         free_xml(*result_cib);
         *result_cib = NULL;
         send_sync_request(NULL);
     }
 
     return rc;
 }
 
 int
 cib_process_replace_svr(const char *op, int options, const char *section, xmlNode * req,
                         xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib,
                         xmlNode ** answer)
 {
     const char *tag = crm_element_name(input);
     int rc =
         cib_process_replace(op, options, section, req, input, existing_cib, result_cib, answer);
     if (rc == pcmk_ok && pcmk__str_eq(tag, XML_TAG_CIB, pcmk__str_casei)) {
         sync_in_progress = 0;
     }
     return rc;
 }
 
 int
 cib_process_delete_absolute(const char *op, int options, const char *section, xmlNode * req,
                             xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib,
                             xmlNode ** answer)
 {
     return -EINVAL;
 }
 
 int
 sync_our_cib(xmlNode * request, gboolean all)
 {
     int result = pcmk_ok;
     char *digest = NULL;
     const char *host = crm_element_value(request, F_ORIG);
     const char *op = crm_element_value(request, F_CIB_OPERATION);
 
     xmlNode *replace_request = NULL;
 
     CRM_CHECK(the_cib != NULL, return -EINVAL);
 
     replace_request = cib_msg_copy(request, FALSE);
     CRM_CHECK(replace_request != NULL, return -EINVAL);
 
     crm_debug("Syncing CIB to %s", all ? "all peers" : host);
     if (all == FALSE && host == NULL) {
         crm_log_xml_err(request, "bad sync");
     }
 
     /* remove the "all == FALSE" condition
      *
      * sync_from was failing, the local client wasn't being notified
      *    because it didn't know it was a reply
      * setting this does not prevent the other nodes from applying it
      *    if all == TRUE
      */
     if (host != NULL) {
         crm_xml_add(replace_request, F_CIB_ISREPLY, host);
     }
     if (all) {
         xml_remove_prop(replace_request, F_CIB_HOST);
     }
 
     crm_xml_add(replace_request, F_CIB_OPERATION, PCMK__CIB_REQUEST_REPLACE);
     crm_xml_add(replace_request, "original_" F_CIB_OPERATION, op);
     pcmk__xe_set_bool_attr(replace_request, F_CIB_GLOBAL_UPDATE, true);
 
     crm_xml_add(replace_request, XML_ATTR_CRM_VERSION, CRM_FEATURE_SET);
     digest = calculate_xml_versioned_digest(the_cib, FALSE, TRUE, CRM_FEATURE_SET);
     crm_xml_add(replace_request, XML_ATTR_DIGEST, digest);
 
     add_message_xml(replace_request, F_CIB_CALLDATA, the_cib);
 
     if (send_cluster_message
         (all ? NULL : crm_get_peer(0, host), crm_msg_cib, replace_request, FALSE) == FALSE) {
         result = -ENOTCONN;
     }
     free_xml(replace_request);
     free(digest);
     return result;
 }
diff --git a/daemons/controld/controld_te_callbacks.c b/daemons/controld/controld_te_callbacks.c
index 101c03012f..471a5fe722 100644
--- a/daemons/controld/controld_te_callbacks.c
+++ b/daemons/controld/controld_te_callbacks.c
@@ -1,686 +1,689 @@
 /*
  * Copyright 2004-2023 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/stat.h>
 
 #include <crm/crm.h>
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>
 #include <crm/msg_xml.h>
 #include <crm/cluster.h>        /* For ONLINESTATUS etc */
 
 #include <pacemaker-controld.h>
 
 void te_update_confirm(const char *event, xmlNode * msg);
 
 #define RSC_OP_PREFIX "//" XML_TAG_DIFF_ADDED "//" XML_TAG_CIB \
                       "//" XML_LRM_TAG_RSC_OP "[@" XML_ATTR_ID "='"
 
 // An explicit shutdown-lock of 0 means the lock has been cleared
 static bool
 shutdown_lock_cleared(xmlNode *lrm_resource)
 {
     time_t shutdown_lock = 0;
 
     return (crm_element_value_epoch(lrm_resource, XML_CONFIG_ATTR_SHUTDOWN_LOCK,
                                     &shutdown_lock) == pcmk_ok)
            && (shutdown_lock == 0);
 }
 
 static void
 te_update_diff_v1(const char *event, xmlNode *diff)
 {
     int lpc, max;
     xmlXPathObject *xpathObj = NULL;
     GString *rsc_op_xpath = NULL;
 
     CRM_CHECK(diff != NULL, return);
 
-    pcmk__xml_log_patchset(LOG_TRACE, diff);
+    pcmk__output_set_log_level(controld_globals.logger_out, LOG_TRACE);
+    controld_globals.logger_out->message(controld_globals.logger_out,
+                                         "xml-patchset", diff);
+
     if (cib_config_changed(NULL, NULL, &diff)) {
         abort_transition(INFINITY, pcmk__graph_restart, "Non-status change",
                          diff);
         goto bail;              /* configuration changed */
     }
 
     /* Tickets Attributes - Added/Updated */
     xpathObj =
         xpath_search(diff,
                      "//" F_CIB_UPDATE_RESULT "//" XML_TAG_DIFF_ADDED "//" XML_CIB_TAG_TICKETS);
     if (numXpathResults(xpathObj) > 0) {
         xmlNode *aborted = getXpathResult(xpathObj, 0);
 
         abort_transition(INFINITY, pcmk__graph_restart,
                          "Ticket attribute: update", aborted);
         goto bail;
 
     }
     freeXpathObject(xpathObj);
 
     /* Tickets Attributes - Removed */
     xpathObj =
         xpath_search(diff,
                      "//" F_CIB_UPDATE_RESULT "//" XML_TAG_DIFF_REMOVED "//" XML_CIB_TAG_TICKETS);
     if (numXpathResults(xpathObj) > 0) {
         xmlNode *aborted = getXpathResult(xpathObj, 0);
 
         abort_transition(INFINITY, pcmk__graph_restart,
                          "Ticket attribute: removal", aborted);
         goto bail;
     }
     freeXpathObject(xpathObj);
 
     /* Transient Attributes - Removed */
     xpathObj =
         xpath_search(diff,
                      "//" F_CIB_UPDATE_RESULT "//" XML_TAG_DIFF_REMOVED "//"
                      XML_TAG_TRANSIENT_NODEATTRS);
     if (numXpathResults(xpathObj) > 0) {
         xmlNode *aborted = getXpathResult(xpathObj, 0);
 
         abort_transition(INFINITY, pcmk__graph_restart,
                          "Transient attribute: removal", aborted);
         goto bail;
 
     }
     freeXpathObject(xpathObj);
 
     // Check for lrm_resource entries
     xpathObj = xpath_search(diff,
                             "//" F_CIB_UPDATE_RESULT
                             "//" XML_TAG_DIFF_ADDED
                             "//" XML_LRM_TAG_RESOURCE);
     max = numXpathResults(xpathObj);
 
     /*
      * Updates by, or in response to, graph actions will never affect more than
      * one resource at a time, so such updates indicate an LRM refresh. In that
      * case, start a new transition rather than check each result individually,
      * which can result in _huge_ speedups in large clusters.
      *
      * Unfortunately, we can only do so when there are no pending actions.
      * Otherwise, we could mistakenly throw away those results here, and
      * the cluster will stall waiting for them and time out the operation.
      */
     if ((controld_globals.transition_graph->pending == 0) && (max > 1)) {
         crm_debug("Ignoring resource operation updates due to history refresh of %d resources",
                   max);
         crm_log_xml_trace(diff, "lrm-refresh");
         abort_transition(INFINITY, pcmk__graph_restart, "History refresh",
                          NULL);
         goto bail;
     }
 
     if (max == 1) {
         xmlNode *lrm_resource = getXpathResult(xpathObj, 0);
 
         if (shutdown_lock_cleared(lrm_resource)) {
             // @TODO would be more efficient to abort once after transition done
             abort_transition(INFINITY, pcmk__graph_restart,
                              "Shutdown lock cleared", lrm_resource);
             // Still process results, so we stop timers and update failcounts
         }
     }
     freeXpathObject(xpathObj);
 
     /* Process operation updates */
     xpathObj =
         xpath_search(diff,
                      "//" F_CIB_UPDATE_RESULT "//" XML_TAG_DIFF_ADDED "//" XML_LRM_TAG_RSC_OP);
     max = numXpathResults(xpathObj);
     if (max > 0) {
         int lpc = 0;
 
         for (lpc = 0; lpc < max; lpc++) {
             xmlNode *rsc_op = getXpathResult(xpathObj, lpc);
             const char *node = get_node_id(rsc_op);
 
             process_graph_event(rsc_op, node);
         }
     }
     freeXpathObject(xpathObj);
 
     /* Detect deleted (as opposed to replaced or added) actions - eg. crm_resource -C */
     xpathObj = xpath_search(diff, "//" XML_TAG_DIFF_REMOVED "//" XML_LRM_TAG_RSC_OP);
     max = numXpathResults(xpathObj);
     for (lpc = 0; lpc < max; lpc++) {
         const char *op_id = NULL;
         xmlXPathObject *op_match = NULL;
         xmlNode *match = getXpathResult(xpathObj, lpc);
 
         CRM_LOG_ASSERT(match != NULL);
         if(match == NULL) { continue; };
 
         op_id = ID(match);
 
         if (rsc_op_xpath == NULL) {
             rsc_op_xpath = g_string_new(RSC_OP_PREFIX);
         } else {
             g_string_truncate(rsc_op_xpath, sizeof(RSC_OP_PREFIX) - 1);
         }
         pcmk__g_strcat(rsc_op_xpath, op_id, "']", NULL);
 
         op_match = xpath_search(diff, (const char *) rsc_op_xpath->str);
         if (numXpathResults(op_match) == 0) {
             /* Prevent false positives by matching cancelations too */
             const char *node = get_node_id(match);
             pcmk__graph_action_t *cancelled = get_cancel_action(op_id, node);
 
             if (cancelled == NULL) {
                 crm_debug("No match for deleted action %s (%s on %s)",
                           (const char *) rsc_op_xpath->str, op_id, node);
                 abort_transition(INFINITY, pcmk__graph_restart,
                                  "Resource op removal", match);
                 freeXpathObject(op_match);
                 goto bail;
 
             } else {
                 crm_debug("Deleted lrm_rsc_op %s on %s was for graph event %d",
                           op_id, node, cancelled->id);
             }
         }
 
         freeXpathObject(op_match);
     }
 
   bail:
     freeXpathObject(xpathObj);
     if (rsc_op_xpath != NULL) {
         g_string_free(rsc_op_xpath, TRUE);
     }
 }
 
 static void
 process_lrm_resource_diff(xmlNode *lrm_resource, const char *node)
 {
     for (xmlNode *rsc_op = pcmk__xml_first_child(lrm_resource); rsc_op != NULL;
          rsc_op = pcmk__xml_next(rsc_op)) {
         process_graph_event(rsc_op, node);
     }
     if (shutdown_lock_cleared(lrm_resource)) {
         // @TODO would be more efficient to abort once after transition done
         abort_transition(INFINITY, pcmk__graph_restart, "Shutdown lock cleared",
                          lrm_resource);
     }
 }
 
 static void
 process_resource_updates(const char *node, xmlNode *xml, xmlNode *change,
                          const char *op, const char *xpath)
 {
     xmlNode *rsc = NULL;
 
     if (xml == NULL) {
         return;
     }
 
     if (strcmp(TYPE(xml), XML_CIB_TAG_LRM) == 0) {
         xml = first_named_child(xml, XML_LRM_TAG_RESOURCES);
         CRM_CHECK(xml != NULL, return);
     }
 
     CRM_CHECK(strcmp(TYPE(xml), XML_LRM_TAG_RESOURCES) == 0, return);
 
     /*
      * Updates by, or in response to, TE actions will never contain updates
      * for more than one resource at a time, so such updates indicate an
      * LRM refresh.
      *
      * In that case, start a new transition rather than check each result
      * individually, which can result in _huge_ speedups in large clusters.
      *
      * Unfortunately, we can only do so when there are no pending actions.
      * Otherwise, we could mistakenly throw away those results here, and
      * the cluster will stall waiting for them and time out the operation.
      */
     if ((controld_globals.transition_graph->pending == 0)
         && (xml->children != NULL) && (xml->children->next != NULL)) {
 
         crm_log_xml_trace(change, "lrm-refresh");
         abort_transition(INFINITY, pcmk__graph_restart, "History refresh",
                          NULL);
         return;
     }
 
     for (rsc = pcmk__xml_first_child(xml); rsc != NULL;
          rsc = pcmk__xml_next(rsc)) {
         crm_trace("Processing %s", ID(rsc));
         process_lrm_resource_diff(rsc, node);
     }
 }
 
 static char *extract_node_uuid(const char *xpath) 
 {
     char *mutable_path = strdup(xpath);
     char *node_uuid = NULL;
     char *search = NULL;
     char *match = NULL;
 
     match = strstr(mutable_path, "node_state[@" XML_ATTR_ID "=\'");
     if (match == NULL) {
         free(mutable_path);
         return NULL;
     }
     match += strlen("node_state[@" XML_ATTR_ID "=\'");
 
     search = strchr(match, '\'');
     if (search == NULL) {
         free(mutable_path);
         return NULL;
     }
     search[0] = 0;
 
     node_uuid = strdup(match);
     free(mutable_path);
     return node_uuid;
 }
 
 static void
 abort_unless_down(const char *xpath, const char *op, xmlNode *change,
                   const char *reason)
 {
     char *node_uuid = NULL;
     pcmk__graph_action_t *down = NULL;
 
     if(!pcmk__str_eq(op, "delete", pcmk__str_casei)) {
         abort_transition(INFINITY, pcmk__graph_restart, reason, change);
         return;
     }
 
     node_uuid = extract_node_uuid(xpath);
     if(node_uuid == NULL) {
         crm_err("Could not extract node ID from %s", xpath);
         abort_transition(INFINITY, pcmk__graph_restart, reason, change);
         return;
     }
 
     down = match_down_event(node_uuid);
     if (down == NULL) {
         crm_trace("Not expecting %s to be down (%s)", node_uuid, xpath);
         abort_transition(INFINITY, pcmk__graph_restart, reason, change);
     } else {
         crm_trace("Expecting changes to %s (%s)", node_uuid, xpath);
     }
     free(node_uuid);
 }
 
 static void
 process_op_deletion(const char *xpath, xmlNode *change)
 {
     char *mutable_key = strdup(xpath);
     char *key;
     char *node_uuid;
 
     // Extract the part of xpath between last pair of single quotes
     key = strrchr(mutable_key, '\'');
     if (key != NULL) {
         *key = '\0';
         key = strrchr(mutable_key, '\'');
     }
     if (key == NULL) {
         crm_warn("Ignoring malformed CIB update (resource deletion of %s)",
                  xpath);
         free(mutable_key);
         return;
     }
     ++key;
 
     node_uuid = extract_node_uuid(xpath);
     if (confirm_cancel_action(key, node_uuid) == FALSE) {
         abort_transition(INFINITY, pcmk__graph_restart,
                          "Resource operation removal", change);
     }
     free(mutable_key);
     free(node_uuid);
 }
 
 static void
 process_delete_diff(const char *xpath, const char *op, xmlNode *change)
 {
     if (strstr(xpath, "/" XML_LRM_TAG_RSC_OP "[")) {
         process_op_deletion(xpath, change);
 
     } else if (strstr(xpath, "/" XML_CIB_TAG_LRM "[")) {
         abort_unless_down(xpath, op, change, "Resource state removal");
 
     } else if (strstr(xpath, "/" XML_CIB_TAG_STATE "[")) {
         abort_unless_down(xpath, op, change, "Node state removal");
 
     } else {
         crm_trace("Ignoring delete of %s", xpath);
     }
 }
 
 static void
 process_node_state_diff(xmlNode *state, xmlNode *change, const char *op,
                         const char *xpath)
 {
     xmlNode *lrm = first_named_child(state, XML_CIB_TAG_LRM);
 
     process_resource_updates(ID(state), lrm, change, op, xpath);
 }
 
 static void
 process_status_diff(xmlNode *status, xmlNode *change, const char *op,
                     const char *xpath)
 {
     for (xmlNode *state = pcmk__xml_first_child(status); state != NULL;
          state = pcmk__xml_next(state)) {
         process_node_state_diff(state, change, op, xpath);
     }
 }
 
 static void
 process_cib_diff(xmlNode *cib, xmlNode *change, const char *op,
                  const char *xpath)
 {
     xmlNode *status = first_named_child(cib, XML_CIB_TAG_STATUS);
     xmlNode *config = first_named_child(cib, XML_CIB_TAG_CONFIGURATION);
 
     if (status) {
         process_status_diff(status, change, op, xpath);
     }
     if (config) {
         abort_transition(INFINITY, pcmk__graph_restart,
                          "Non-status-only change", change);
     }
 }
 
 static void
 te_update_diff_v2(xmlNode *diff)
 {
     crm_log_xml_trace(diff, "Patch:Raw");
 
     for (xmlNode *change = pcmk__xml_first_child(diff); change != NULL;
          change = pcmk__xml_next(change)) {
 
         xmlNode *match = NULL;
         const char *name = NULL;
         const char *xpath = crm_element_value(change, XML_DIFF_PATH);
 
         // Possible ops: create, modify, delete, move
         const char *op = crm_element_value(change, XML_DIFF_OP);
 
         // Ignore uninteresting updates
         if (op == NULL) {
             continue;
 
         } else if (xpath == NULL) {
             crm_trace("Ignoring %s change for version field", op);
             continue;
 
         } else if ((strcmp(op, "move") == 0)
                    && (strstr(xpath,
                               "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION
                               "/" XML_CIB_TAG_RESOURCES) == NULL)) {
             /* We still need to consider moves within the resources section,
              * since they affect placement order.
              */
             crm_trace("Ignoring move change at %s", xpath);
             continue;
         }
 
         // Find the result of create/modify ops
         if (strcmp(op, "create") == 0) {
             match = change->children;
 
         } else if (strcmp(op, "modify") == 0) {
             match = first_named_child(change, XML_DIFF_RESULT);
             if(match) {
                 match = match->children;
             }
 
         } else if (!pcmk__str_any_of(op, "delete", "move", NULL)) {
             crm_warn("Ignoring malformed CIB update (%s operation on %s is unrecognized)",
                      op, xpath);
             continue;
         }
 
         if (match) {
             if (match->type == XML_COMMENT_NODE) {
                 crm_trace("Ignoring %s operation for comment at %s", op, xpath);
                 continue;
             }
             name = (const char *)match->name;
         }
 
         crm_trace("Handling %s operation for %s%s%s",
                   op, (xpath? xpath : "CIB"),
                   (name? " matched by " : ""), (name? name : ""));
 
         if (strstr(xpath, "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION)) {
             abort_transition(INFINITY, pcmk__graph_restart,
                              "Configuration change", change);
             break; // Won't be packaged with operation results we may be waiting for
 
         } else if (strstr(xpath, "/" XML_CIB_TAG_TICKETS)
                    || pcmk__str_eq(name, XML_CIB_TAG_TICKETS, pcmk__str_none)) {
             abort_transition(INFINITY, pcmk__graph_restart,
                              "Ticket attribute change", change);
             break; // Won't be packaged with operation results we may be waiting for
 
         } else if (strstr(xpath, "/" XML_TAG_TRANSIENT_NODEATTRS "[")
                    || pcmk__str_eq(name, XML_TAG_TRANSIENT_NODEATTRS,
                                    pcmk__str_none)) {
             abort_unless_down(xpath, op, change, "Transient attribute change");
             break; // Won't be packaged with operation results we may be waiting for
 
         } else if (strcmp(op, "delete") == 0) {
             process_delete_diff(xpath, op, change);
 
         } else if (name == NULL) {
             crm_warn("Ignoring malformed CIB update (%s at %s has no result)",
                      op, xpath);
 
         } else if (strcmp(name, XML_TAG_CIB) == 0) {
             process_cib_diff(match, change, op, xpath);
 
         } else if (strcmp(name, XML_CIB_TAG_STATUS) == 0) {
             process_status_diff(match, change, op, xpath);
 
         } else if (strcmp(name, XML_CIB_TAG_STATE) == 0) {
             process_node_state_diff(match, change, op, xpath);
 
         } else if (strcmp(name, XML_CIB_TAG_LRM) == 0) {
             process_resource_updates(ID(match), match, change, op, xpath);
 
         } else if (strcmp(name, XML_LRM_TAG_RESOURCES) == 0) {
             char *local_node = pcmk__xpath_node_id(xpath, "lrm");
 
             process_resource_updates(local_node, match, change, op, xpath);
             free(local_node);
 
         } else if (strcmp(name, XML_LRM_TAG_RESOURCE) == 0) {
             char *local_node = pcmk__xpath_node_id(xpath, "lrm");
 
             process_lrm_resource_diff(match, local_node);
             free(local_node);
 
         } else if (strcmp(name, XML_LRM_TAG_RSC_OP) == 0) {
             char *local_node = pcmk__xpath_node_id(xpath, "lrm");
 
             process_graph_event(match, local_node);
             free(local_node);
 
         } else {
             crm_warn("Ignoring malformed CIB update (%s at %s has unrecognized result %s)",
                      op, xpath, name);
         }
     }
 }
 
 void
 te_update_diff(const char *event, xmlNode * msg)
 {
     xmlNode *diff = NULL;
     const char *op = NULL;
     int rc = -EINVAL;
     int format = 1;
     int p_add[] = { 0, 0, 0 };
     int p_del[] = { 0, 0, 0 };
 
     CRM_CHECK(msg != NULL, return);
     crm_element_value_int(msg, F_CIB_RC, &rc);
 
     if (controld_globals.transition_graph == NULL) {
         crm_trace("No graph");
         return;
 
     } else if (rc < pcmk_ok) {
         crm_trace("Filter rc=%d (%s)", rc, pcmk_strerror(rc));
         return;
 
     } else if (controld_globals.transition_graph->complete
                && (controld_globals.fsa_state != S_IDLE)
                && (controld_globals.fsa_state != S_TRANSITION_ENGINE)
                && (controld_globals.fsa_state != S_POLICY_ENGINE)) {
         crm_trace("Filter state=%s (complete)",
                   fsa_state2string(controld_globals.fsa_state));
         return;
     }
 
     op = crm_element_value(msg, F_CIB_OPERATION);
     diff = get_message_xml(msg, F_CIB_UPDATE_RESULT);
 
     xml_patch_versions(diff, p_add, p_del);
     crm_debug("Processing (%s) diff: %d.%d.%d -> %d.%d.%d (%s)", op,
               p_del[0], p_del[1], p_del[2], p_add[0], p_add[1], p_add[2],
               fsa_state2string(controld_globals.fsa_state));
 
     crm_element_value_int(diff, "format", &format);
     switch (format) {
         case 1:
             te_update_diff_v1(event, diff);
             break;
         case 2:
             te_update_diff_v2(diff);
             break;
         default:
             crm_warn("Ignoring malformed CIB update (unknown patch format %d)",
                      format);
     }
     controld_destroy_outside_event_table();
 }
 
 void
 process_te_message(xmlNode * msg, xmlNode * xml_data)
 {
     const char *value = NULL;
     xmlXPathObject *xpathObj = NULL;
     int nmatches = 0;
 
     CRM_CHECK(msg != NULL, return);
 
     // Transition requests must specify transition engine as subsystem
     value = crm_element_value(msg, F_CRM_SYS_TO);
     if (pcmk__str_empty(value)
         || !pcmk__str_eq(value, CRM_SYSTEM_TENGINE, pcmk__str_none)) {
         crm_info("Received invalid transition request: subsystem '%s' not '"
                  CRM_SYSTEM_TENGINE "'", pcmk__s(value, ""));
         return;
     }
 
     // Only the lrm_invoke command is supported as a transition request
     value = crm_element_value(msg, F_CRM_TASK);
     if (!pcmk__str_eq(value, CRM_OP_INVOKE_LRM, pcmk__str_none)) {
         crm_info("Received invalid transition request: command '%s' not '"
                  CRM_OP_INVOKE_LRM "'", pcmk__s(value, ""));
         return;
     }
 
     // Transition requests must be marked as coming from the executor
     value = crm_element_value(msg, F_CRM_SYS_FROM);
     if (!pcmk__str_eq(value, CRM_SYSTEM_LRMD, pcmk__str_none)) {
         crm_info("Received invalid transition request: from '%s' not '"
                  CRM_SYSTEM_LRMD "'", pcmk__s(value, ""));
         return;
     }
 
     crm_debug("Processing transition request with ref='%s' origin='%s'",
               pcmk__s(crm_element_value(msg, F_CRM_REFERENCE), ""),
               pcmk__s(crm_element_value(msg, F_ORIG), ""));
 
     xpathObj = xpath_search(xml_data, "//" XML_LRM_TAG_RSC_OP);
     nmatches = numXpathResults(xpathObj);
     if (nmatches == 0) {
         crm_err("Received transition request with no results (bug?)");
     } else {
         for (int lpc = 0; lpc < nmatches; lpc++) {
             xmlNode *rsc_op = getXpathResult(xpathObj, lpc);
             const char *node = get_node_id(rsc_op);
 
             process_graph_event(rsc_op, node);
         }
     }
     freeXpathObject(xpathObj);
 }
 
 void
 cib_action_updated(xmlNode * msg, int call_id, int rc, xmlNode * output, void *user_data)
 {
     if (rc < pcmk_ok) {
         crm_err("Update %d FAILED: %s", call_id, pcmk_strerror(rc));
     }
 }
 
 /*!
  * \brief Handle a timeout in node-to-node communication
  *
  * \param[in,out] data  Pointer to graph action
  *
  * \return FALSE (indicating that source should be not be re-added)
  */
 gboolean
 action_timer_callback(gpointer data)
 {
     pcmk__graph_action_t *action = (pcmk__graph_action_t *) data;
     const char *task = NULL;
     const char *on_node = NULL;
     const char *via_node = NULL;
 
     CRM_CHECK(data != NULL, return FALSE);
 
     stop_te_timer(action);
 
     task = crm_element_value(action->xml, XML_LRM_ATTR_TASK);
     on_node = crm_element_value(action->xml, XML_LRM_ATTR_TARGET);
     via_node = crm_element_value(action->xml, XML_LRM_ATTR_ROUTER_NODE);
 
     if (controld_globals.transition_graph->complete) {
         crm_notice("Node %s did not send %s result (via %s) within %dms "
                    "(ignoring because transition not in progress)",
                    (on_node? on_node : ""), (task? task : "unknown action"),
                    (via_node? via_node : "controller"), action->timeout);
     } else {
         /* fail the action */
 
         crm_err("Node %s did not send %s result (via %s) within %dms "
                 "(action timeout plus cluster-delay)",
                 (on_node? on_node : ""), (task? task : "unknown action"),
                 (via_node? via_node : "controller"),
                 (action->timeout
                  + controld_globals.transition_graph->network_delay));
         pcmk__log_graph_action(LOG_ERR, action);
 
         pcmk__set_graph_action_flags(action, pcmk__graph_action_failed);
 
         te_action_confirmed(action, controld_globals.transition_graph);
         abort_transition(INFINITY, pcmk__graph_restart, "Action lost", NULL);
 
         // Record timeout in the CIB if appropriate
         if ((action->type == pcmk__rsc_graph_action)
             && controld_action_is_recordable(task)) {
             controld_record_action_timeout(action);
         }
     }
 
     return FALSE;
 }
diff --git a/daemons/fenced/pacemaker-fenced.c b/daemons/fenced/pacemaker-fenced.c
index 6b14dbb491..cadd733f0f 100644
--- a/daemons/fenced/pacemaker-fenced.c
+++ b/daemons/fenced/pacemaker-fenced.c
@@ -1,1743 +1,1751 @@
 /*
  * Copyright 2009-2023 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 <sys/stat.h>
 #include <unistd.h>
 #include <sys/utsname.h>
 
 #include <stdlib.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <inttypes.h>  // PRIu32, PRIx32
 
 #include <crm/crm.h>
 #include <crm/msg_xml.h>
 #include <crm/common/cmdline_internal.h>
 #include <crm/common/ipc.h>
 #include <crm/common/ipc_internal.h>
 #include <crm/common/output_internal.h>
 #include <crm/cluster/internal.h>
 
 #include <crm/stonith-ng.h>
 #include <crm/fencing/internal.h>
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>
 
 #include <crm/common/mainloop.h>
 
 #include <crm/cib/internal.h>
 #include <crm/pengine/status.h>
 #include <pacemaker-internal.h>
 
 #include <pacemaker-fenced.h>
 
 #define SUMMARY "daemon for executing fencing devices in a Pacemaker cluster"
 
 char *stonith_our_uname = NULL;
 long stonith_watchdog_timeout_ms = 0;
 GList *stonith_watchdog_targets = NULL;
 
 static GMainLoop *mainloop = NULL;
 
 gboolean stand_alone = FALSE;
 static gboolean stonith_shutdown_flag = FALSE;
 
 static qb_ipcs_service_t *ipcs = NULL;
 static xmlNode *local_cib = NULL;
 static pe_working_set_t *fenced_data_set = NULL;
 static const unsigned long long data_set_flags = pe_flag_quick_location
                                                  | pe_flag_no_compat
                                                  | pe_flag_no_counts;
 
 static cib_t *cib_api = NULL;
 
 static pcmk__output_t *logger_out = NULL;
 static pcmk__output_t *out = NULL;
 
 pcmk__supported_format_t formats[] = {
     PCMK__SUPPORTED_FORMAT_NONE,
     PCMK__SUPPORTED_FORMAT_TEXT,
     PCMK__SUPPORTED_FORMAT_XML,
     { NULL, NULL, NULL }
 };
 
 static struct {
     bool no_cib_connect;
     gchar **log_files;
 } options;
 
 static crm_exit_t exit_code = CRM_EX_OK;
 
 static void stonith_shutdown(int nsig);
 static void stonith_cleanup(void);
 
 static int32_t
 st_ipc_accept(qb_ipcs_connection_t * c, uid_t uid, gid_t gid)
 {
     if (stonith_shutdown_flag) {
         crm_info("Ignoring new client [%d] during shutdown",
                  pcmk__client_pid(c));
         return -EPERM;
     }
 
     if (pcmk__new_client(c, uid, gid) == NULL) {
         return -EIO;
     }
     return 0;
 }
 
 /* Exit code means? */
 static int32_t
 st_ipc_dispatch(qb_ipcs_connection_t * qbc, void *data, size_t size)
 {
     uint32_t id = 0;
     uint32_t flags = 0;
     int call_options = 0;
     xmlNode *request = NULL;
     pcmk__client_t *c = pcmk__find_client(qbc);
     const char *op = NULL;
 
     if (c == NULL) {
         crm_info("Invalid client: %p", qbc);
         return 0;
     }
 
     request = pcmk__client_data2xml(c, data, &id, &flags);
     if (request == NULL) {
         pcmk__ipc_send_ack(c, id, flags, "nack", NULL, CRM_EX_PROTOCOL);
         return 0;
     }
 
 
     op = crm_element_value(request, F_CRM_TASK);
     if(pcmk__str_eq(op, CRM_OP_RM_NODE_CACHE, pcmk__str_casei)) {
         crm_xml_add(request, F_TYPE, T_STONITH_NG);
         crm_xml_add(request, F_STONITH_OPERATION, op);
         crm_xml_add(request, F_STONITH_CLIENTID, c->id);
         crm_xml_add(request, F_STONITH_CLIENTNAME, pcmk__client_name(c));
         crm_xml_add(request, F_STONITH_CLIENTNODE, stonith_our_uname);
 
         send_cluster_message(NULL, crm_msg_stonith_ng, request, FALSE);
         free_xml(request);
         return 0;
     }
 
     if (c->name == NULL) {
         const char *value = crm_element_value(request, F_STONITH_CLIENTNAME);
 
         if (value == NULL) {
             value = "unknown";
         }
         c->name = crm_strdup_printf("%s.%u", value, c->pid);
     }
 
     crm_element_value_int(request, F_STONITH_CALLOPTS, &call_options);
     crm_trace("Flags %#08" PRIx32 "/%#08x for command %" PRIu32
               " from client %s", flags, call_options, id, pcmk__client_name(c));
 
     if (pcmk_is_set(call_options, st_opt_sync_call)) {
         CRM_ASSERT(flags & crm_ipc_client_response);
         CRM_LOG_ASSERT(c->request_id == 0);     /* This means the client has two synchronous events in-flight */
         c->request_id = id;     /* Reply only to the last one */
     }
 
     crm_xml_add(request, F_STONITH_CLIENTID, c->id);
     crm_xml_add(request, F_STONITH_CLIENTNAME, pcmk__client_name(c));
     crm_xml_add(request, F_STONITH_CLIENTNODE, stonith_our_uname);
 
     crm_log_xml_trace(request, "ipc-received");
     stonith_command(c, id, flags, request, NULL);
 
     free_xml(request);
     return 0;
 }
 
 /* Error code means? */
 static int32_t
 st_ipc_closed(qb_ipcs_connection_t * c)
 {
     pcmk__client_t *client = pcmk__find_client(c);
 
     if (client == NULL) {
         return 0;
     }
 
     crm_trace("Connection %p closed", c);
     pcmk__free_client(client);
 
     /* 0 means: yes, go ahead and destroy the connection */
     return 0;
 }
 
 static void
 st_ipc_destroy(qb_ipcs_connection_t * c)
 {
     crm_trace("Connection %p destroyed", c);
     st_ipc_closed(c);
 }
 
 static void
 stonith_peer_callback(xmlNode * msg, void *private_data)
 {
     const char *remote_peer = crm_element_value(msg, F_ORIG);
     const char *op = crm_element_value(msg, F_STONITH_OPERATION);
 
     if (pcmk__str_eq(op, "poke", pcmk__str_none)) {
         return;
     }
 
     crm_log_xml_trace(msg, "Peer[inbound]");
     stonith_command(NULL, 0, 0, msg, remote_peer);
 }
 
 #if SUPPORT_COROSYNC
 static void
 stonith_peer_ais_callback(cpg_handle_t handle,
                           const struct cpg_name *groupName,
                           uint32_t nodeid, uint32_t pid, void *msg, size_t msg_len)
 {
     uint32_t kind = 0;
     xmlNode *xml = NULL;
     const char *from = NULL;
     char *data = pcmk_message_common_cs(handle, nodeid, pid, msg, &kind, &from);
 
     if(data == NULL) {
         return;
     }
     if (kind == crm_class_cluster) {
         xml = string2xml(data);
         if (xml == NULL) {
             crm_err("Invalid XML: '%.120s'", data);
             free(data);
             return;
         }
         crm_xml_add(xml, F_ORIG, from);
         /* crm_xml_add_int(xml, F_SEQ, wrapper->id); */
         stonith_peer_callback(xml, NULL);
     }
 
     free_xml(xml);
     free(data);
     return;
 }
 
 static void
 stonith_peer_cs_destroy(gpointer user_data)
 {
     crm_crit("Lost connection to cluster layer, shutting down");
     stonith_shutdown(0);
 }
 #endif
 
 void
 do_local_reply(xmlNode *notify_src, pcmk__client_t *client, int call_options)
 {
     /* send callback to originating child */
     int local_rc = pcmk_rc_ok;
     int rid = 0;
     uint32_t ipc_flags = crm_ipc_server_event;
 
     if (pcmk_is_set(call_options, st_opt_sync_call)) {
         CRM_LOG_ASSERT(client->request_id);
         rid = client->request_id;
         client->request_id = 0;
         ipc_flags = crm_ipc_flags_none;
     }
 
     local_rc = pcmk__ipc_send_xml(client, rid, notify_src, ipc_flags);
     if (local_rc == pcmk_rc_ok) {
         crm_trace("Sent response %d to client %s",
                   rid, pcmk__client_name(client));
     } else {
         crm_warn("%synchronous reply to client %s failed: %s",
                  (pcmk_is_set(call_options, st_opt_sync_call)? "S" : "As"),
                  pcmk__client_name(client), pcmk_rc_str(local_rc));
     }
 }
 
 uint64_t
 get_stonith_flag(const char *name)
 {
     if (pcmk__str_eq(name, T_STONITH_NOTIFY_FENCE, pcmk__str_casei)) {
         return st_callback_notify_fence;
 
     } else if (pcmk__str_eq(name, STONITH_OP_DEVICE_ADD, pcmk__str_casei)) {
         return st_callback_device_add;
 
     } else if (pcmk__str_eq(name, STONITH_OP_DEVICE_DEL, pcmk__str_casei)) {
         return st_callback_device_del;
 
     } else if (pcmk__str_eq(name, T_STONITH_NOTIFY_HISTORY, pcmk__str_casei)) {
         return st_callback_notify_history;
 
     } else if (pcmk__str_eq(name, T_STONITH_NOTIFY_HISTORY_SYNCED, pcmk__str_casei)) {
         return st_callback_notify_history_synced;
 
     }
     return st_callback_unknown;
 }
 
 static void
 stonith_notify_client(gpointer key, gpointer value, gpointer user_data)
 {
 
     xmlNode *update_msg = user_data;
     pcmk__client_t *client = value;
     const char *type = NULL;
 
     CRM_CHECK(client != NULL, return);
     CRM_CHECK(update_msg != NULL, return);
 
     type = crm_element_value(update_msg, F_SUBTYPE);
     CRM_CHECK(type != NULL, crm_log_xml_err(update_msg, "notify"); return);
 
     if (client->ipcs == NULL) {
         crm_trace("Skipping client with NULL channel");
         return;
     }
 
     if (pcmk_is_set(client->flags, get_stonith_flag(type))) {
         int rc = pcmk__ipc_send_xml(client, 0, update_msg,
                                     crm_ipc_server_event);
 
         if (rc != pcmk_rc_ok) {
             crm_warn("%s notification of client %s failed: %s "
                      CRM_XS " id=%.8s rc=%d", type, pcmk__client_name(client),
                      pcmk_rc_str(rc), client->id, rc);
         } else {
             crm_trace("Sent %s notification to client %s",
                       type, pcmk__client_name(client));
         }
     }
 }
 
 void
 do_stonith_async_timeout_update(const char *client_id, const char *call_id, int timeout)
 {
     pcmk__client_t *client = NULL;
     xmlNode *notify_data = NULL;
 
     if (!timeout || !call_id || !client_id) {
         return;
     }
 
     client = pcmk__find_client_by_id(client_id);
     if (!client) {
         return;
     }
 
     notify_data = create_xml_node(NULL, T_STONITH_TIMEOUT_VALUE);
     crm_xml_add(notify_data, F_TYPE, T_STONITH_TIMEOUT_VALUE);
     crm_xml_add(notify_data, F_STONITH_CALLID, call_id);
     crm_xml_add_int(notify_data, F_STONITH_TIMEOUT, timeout);
 
     crm_trace("timeout update is %d for client %s and call id %s", timeout, client_id, call_id);
 
     if (client) {
         pcmk__ipc_send_xml(client, 0, notify_data, crm_ipc_server_event);
     }
 
     free_xml(notify_data);
 }
 
 /*!
  * \internal
  * \brief Notify relevant IPC clients of a fencing operation result
  *
  * \param[in] type     Notification type
  * \param[in] result   Result of fencing operation (assume success if NULL)
  * \param[in] data     If not NULL, add to notification as call data
  */
 void
 fenced_send_notification(const char *type, const pcmk__action_result_t *result,
                          xmlNode *data)
 {
     /* TODO: Standardize the contents of data */
     xmlNode *update_msg = create_xml_node(NULL, "notify");
 
     CRM_LOG_ASSERT(type != NULL);
 
     crm_xml_add(update_msg, F_TYPE, T_STONITH_NOTIFY);
     crm_xml_add(update_msg, F_SUBTYPE, type);
     crm_xml_add(update_msg, F_STONITH_OPERATION, type);
     stonith__xe_set_result(update_msg, result);
 
     if (data != NULL) {
         add_message_xml(update_msg, F_STONITH_CALLDATA, data);
     }
 
     crm_trace("Notifying clients");
     pcmk__foreach_ipc_client(stonith_notify_client, update_msg);
     free_xml(update_msg);
     crm_trace("Notify complete");
 }
 
 /*!
  * \internal
  * \brief Send notifications for a configuration change to subscribed clients
  *
  * \param[in] op      Notification type (STONITH_OP_DEVICE_ADD,
  *                    STONITH_OP_DEVICE_DEL, STONITH_OP_LEVEL_ADD, or
  *                    STONITH_OP_LEVEL_DEL)
  * \param[in] result  Operation result
  * \param[in] desc    Description of what changed
  * \param[in] active  Current number of devices or topologies in use
  */
 static void
 send_config_notification(const char *op, const pcmk__action_result_t *result,
                          const char *desc, int active)
 {
     xmlNode *notify_data = create_xml_node(NULL, op);
 
     CRM_CHECK(notify_data != NULL, return);
 
     crm_xml_add(notify_data, F_STONITH_DEVICE, desc);
     crm_xml_add_int(notify_data, F_STONITH_ACTIVE, active);
 
     fenced_send_notification(op, result, notify_data);
     free_xml(notify_data);
 }
 
 /*!
  * \internal
  * \brief Send notifications for a device change to subscribed clients
  *
  * \param[in] op      Notification type (STONITH_OP_DEVICE_ADD or
  *                    STONITH_OP_DEVICE_DEL)
  * \param[in] result  Operation result
  * \param[in] desc    ID of device that changed
  */
 void
 fenced_send_device_notification(const char *op,
                                 const pcmk__action_result_t *result,
                                 const char *desc)
 {
     send_config_notification(op, result, desc, g_hash_table_size(device_list));
 }
 
 /*!
  * \internal
  * \brief Send notifications for a topology level change to subscribed clients
  *
  * \param[in] op      Notification type (STONITH_OP_LEVEL_ADD or
  *                    STONITH_OP_LEVEL_DEL)
  * \param[in] result  Operation result
  * \param[in] desc    String representation of level (<target>[<level_index>])
  */
 void
 fenced_send_level_notification(const char *op,
                                const pcmk__action_result_t *result,
                                const char *desc)
 {
     send_config_notification(op, result, desc, g_hash_table_size(topology));
 }
 
 static void
 topology_remove_helper(const char *node, int level)
 {
     char *desc = NULL;
     pcmk__action_result_t result = PCMK__UNKNOWN_RESULT;
     xmlNode *data = create_xml_node(NULL, XML_TAG_FENCING_LEVEL);
 
     crm_xml_add(data, F_STONITH_ORIGIN, __func__);
     crm_xml_add_int(data, XML_ATTR_STONITH_INDEX, level);
     crm_xml_add(data, XML_ATTR_STONITH_TARGET, node);
 
     fenced_unregister_level(data, &desc, &result);
     fenced_send_level_notification(STONITH_OP_LEVEL_DEL, &result, desc);
     pcmk__reset_result(&result);
     free_xml(data);
     free(desc);
 }
 
 static void
 remove_cib_device(xmlXPathObjectPtr xpathObj)
 {
     int max = numXpathResults(xpathObj), lpc = 0;
 
     for (lpc = 0; lpc < max; lpc++) {
         const char *rsc_id = NULL;
         const char *standard = NULL;
         xmlNode *match = getXpathResult(xpathObj, lpc);
 
         CRM_LOG_ASSERT(match != NULL);
         if(match != NULL) {
             standard = crm_element_value(match, XML_AGENT_ATTR_CLASS);
         }
 
         if (!pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_STONITH, pcmk__str_casei)) {
             continue;
         }
 
         rsc_id = crm_element_value(match, XML_ATTR_ID);
 
         stonith_device_remove(rsc_id, true);
     }
 }
 
 static void
 remove_topology_level(xmlNode *match)
 {
     int index = 0;
     char *key = NULL;
 
     CRM_CHECK(match != NULL, return);
 
     key = stonith_level_key(match, fenced_target_by_unknown);
     crm_element_value_int(match, XML_ATTR_STONITH_INDEX, &index);
     topology_remove_helper(key, index);
     free(key);
 }
 
 static void
 add_topology_level(xmlNode *match)
 {
     char *desc = NULL;
     pcmk__action_result_t result = PCMK__UNKNOWN_RESULT;
 
     CRM_CHECK(match != NULL, return);
 
     fenced_register_level(match, &desc, &result);
     fenced_send_level_notification(STONITH_OP_LEVEL_ADD, &result, desc);
     pcmk__reset_result(&result);
     free(desc);
 }
 
 static void
 remove_fencing_topology(xmlXPathObjectPtr xpathObj)
 {
     int max = numXpathResults(xpathObj), lpc = 0;
 
     for (lpc = 0; lpc < max; lpc++) {
         xmlNode *match = getXpathResult(xpathObj, lpc);
 
         CRM_LOG_ASSERT(match != NULL);
         if (match && crm_element_value(match, XML_DIFF_MARKER)) {
             /* Deletion */
             int index = 0;
             char *target = stonith_level_key(match, fenced_target_by_unknown);
 
             crm_element_value_int(match, XML_ATTR_STONITH_INDEX, &index);
             if (target == NULL) {
                 crm_err("Invalid fencing target in element %s", ID(match));
 
             } else if (index <= 0) {
                 crm_err("Invalid level for %s in element %s", target, ID(match));
 
             } else {
                 topology_remove_helper(target, index);
             }
             /* } else { Deal with modifications during the 'addition' stage */
         }
     }
 }
 
 static void
 register_fencing_topology(xmlXPathObjectPtr xpathObj)
 {
     int max = numXpathResults(xpathObj), lpc = 0;
 
     for (lpc = 0; lpc < max; lpc++) {
         xmlNode *match = getXpathResult(xpathObj, lpc);
 
         remove_topology_level(match);
         add_topology_level(match);
     }
 }
 
 /* Fencing
 <diff crm_feature_set="3.0.6">
   <diff-removed>
     <fencing-topology>
       <fencing-level id="f-p1.1" target="pcmk-1" index="1" devices="poison-pill" __crm_diff_marker__="removed:top"/>
       <fencing-level id="f-p1.2" target="pcmk-1" index="2" devices="power" __crm_diff_marker__="removed:top"/>
       <fencing-level devices="disk,network" id="f-p2.1"/>
     </fencing-topology>
   </diff-removed>
   <diff-added>
     <fencing-topology>
       <fencing-level id="f-p.1" target="pcmk-1" index="1" devices="poison-pill" __crm_diff_marker__="added:top"/>
       <fencing-level id="f-p2.1" target="pcmk-2" index="1" devices="disk,something"/>
       <fencing-level id="f-p3.1" target="pcmk-2" index="2" devices="power" __crm_diff_marker__="added:top"/>
     </fencing-topology>
   </diff-added>
 </diff>
 */
 
 static void
 fencing_topology_init(void)
 {
     xmlXPathObjectPtr xpathObj = NULL;
     const char *xpath = "//" XML_TAG_FENCING_LEVEL;
 
     crm_trace("Full topology refresh");
     free_topology_list();
     init_topology_list();
 
     /* Grab everything */
     xpathObj = xpath_search(local_cib, xpath);
     register_fencing_topology(xpathObj);
 
     freeXpathObject(xpathObj);
 }
 
 #define rsc_name(x) x->clone_name?x->clone_name:x->id
 
 /*!
  * \internal
  * \brief Check whether our uname is in a resource's allowed node list
  *
  * \param[in] rsc  Resource to check
  *
  * \return Pointer to node object if found, NULL otherwise
  */
 static pe_node_t *
 our_node_allowed_for(const pe_resource_t *rsc)
 {
     GHashTableIter iter;
     pe_node_t *node = NULL;
 
     if (rsc && stonith_our_uname) {
         g_hash_table_iter_init(&iter, rsc->allowed_nodes);
         while (g_hash_table_iter_next(&iter, NULL, (void **)&node)) {
             if (node && strcmp(node->details->uname, stonith_our_uname) == 0) {
                 break;
             }
             node = NULL;
         }
     }
     return node;
 }
 
 static void
 watchdog_device_update(void)
 {
     if (stonith_watchdog_timeout_ms > 0) {
         if (!g_hash_table_lookup(device_list, STONITH_WATCHDOG_ID) &&
             !stonith_watchdog_targets) {
             /* getting here watchdog-fencing enabled, no device there yet
                and reason isn't stonith_watchdog_targets preventing that
              */
             int rc;
             xmlNode *xml;
 
             xml = create_device_registration_xml(
                     STONITH_WATCHDOG_ID,
                     st_namespace_internal,
                     STONITH_WATCHDOG_AGENT,
                     NULL, /* stonith_device_register will add our
                              own name as PCMK_STONITH_HOST_LIST param
                              so we can skip that here
                            */
                     NULL);
             rc = stonith_device_register(xml, TRUE);
             free_xml(xml);
             if (rc != pcmk_ok) {
                 rc = pcmk_legacy2rc(rc);
                 exit_code = CRM_EX_FATAL;
                 crm_crit("Cannot register watchdog pseudo fence agent: %s",
                          pcmk_rc_str(rc));
                 stonith_shutdown(0);
             }
         }
 
     } else if (g_hash_table_lookup(device_list, STONITH_WATCHDOG_ID) != NULL) {
         /* be silent if no device - todo parameter to stonith_device_remove */
         stonith_device_remove(STONITH_WATCHDOG_ID, true);
     }
 }
 
 static void
 update_stonith_watchdog_timeout_ms(xmlNode *cib)
 {
     long timeout_ms = 0;
     xmlNode *stonith_watchdog_xml = NULL;
     const char *value = NULL;
 
     stonith_watchdog_xml = get_xpath_object("//nvpair[@name='stonith-watchdog-timeout']",
 					    cib, LOG_NEVER);
     if (stonith_watchdog_xml) {
         value = crm_element_value(stonith_watchdog_xml, XML_NVPAIR_ATTR_VALUE);
     }
     if (value) {
         timeout_ms = crm_get_msec(value);
     }
 
     if (timeout_ms < 0) {
         timeout_ms = pcmk__auto_watchdog_timeout();
     }
 
     stonith_watchdog_timeout_ms = timeout_ms;
 }
 
 /*!
  * \internal
  * \brief If a resource or any of its children are STONITH devices, update their
  *        definitions given a cluster working set.
  *
  * \param[in,out] rsc       Resource to check
  * \param[in,out] data_set  Cluster working set with device information
  */
 static void
 cib_device_update(pe_resource_t *rsc, pe_working_set_t *data_set)
 {
     pe_node_t *node = NULL;
     const char *value = NULL;
     const char *rclass = NULL;
     pe_node_t *parent = NULL;
 
     /* If this is a complex resource, check children rather than this resource itself. */
     if(rsc->children) {
         GList *gIter = NULL;
         for (gIter = rsc->children; gIter != NULL; gIter = gIter->next) {
             cib_device_update(gIter->data, data_set);
             if(pe_rsc_is_clone(rsc)) {
                 crm_trace("Only processing one copy of the clone %s", rsc->id);
                 break;
             }
         }
         return;
     }
 
     /* We only care about STONITH resources. */
     rclass = crm_element_value(rsc->xml, XML_AGENT_ATTR_CLASS);
     if (!pcmk__str_eq(rclass, PCMK_RESOURCE_CLASS_STONITH, pcmk__str_casei)) {
         return;
     }
 
     /* If this STONITH resource is disabled, remove it. */
     if (pe__resource_is_disabled(rsc)) {
         crm_info("Device %s has been disabled", rsc->id);
         return;
     }
 
     /* if watchdog-fencing is disabled handle any watchdog-fence
        resource as if it was disabled
      */
     if ((stonith_watchdog_timeout_ms <= 0) &&
         pcmk__str_eq(rsc->id, STONITH_WATCHDOG_ID, pcmk__str_none)) {
         crm_info("Watchdog-fencing disabled thus handling "
                  "device %s as disabled", rsc->id);
         return;
     }
 
     /* Check whether our node is allowed for this resource (and its parent if in a group) */
     node = our_node_allowed_for(rsc);
     if (rsc->parent && (rsc->parent->variant == pe_group)) {
         parent = our_node_allowed_for(rsc->parent);
     }
 
     if(node == NULL) {
         /* Our node is disallowed, so remove the device */
         GHashTableIter iter;
 
         crm_info("Device %s has been disabled on %s: unknown", rsc->id, stonith_our_uname);
         g_hash_table_iter_init(&iter, rsc->allowed_nodes);
         while (g_hash_table_iter_next(&iter, NULL, (void **)&node)) {
             crm_trace("Available: %s = %d", pe__node_name(node), node->weight);
         }
 
         return;
 
     } else if(node->weight < 0 || (parent && parent->weight < 0)) {
         /* Our node (or its group) is disallowed by score, so remove the device */
         int score = (node->weight < 0)? node->weight : parent->weight;
 
         crm_info("Device %s has been disabled on %s: score=%s",
                  rsc->id, stonith_our_uname, pcmk_readable_score(score));
         return;
 
     } else {
         /* Our node is allowed, so update the device information */
         int rc;
         xmlNode *data;
         GHashTable *rsc_params = NULL;
         GHashTableIter gIter;
         stonith_key_value_t *params = NULL;
 
         const char *name = NULL;
         const char *agent = crm_element_value(rsc->xml, XML_EXPR_ATTR_TYPE);
         const char *rsc_provides = NULL;
 
         crm_debug("Device %s is allowed on %s: score=%d", rsc->id, stonith_our_uname, node->weight);
         rsc_params = pe_rsc_params(rsc, node, data_set);
         get_meta_attributes(rsc->meta, rsc, node, data_set);
 
         rsc_provides = g_hash_table_lookup(rsc->meta, PCMK_STONITH_PROVIDES);
 
         g_hash_table_iter_init(&gIter, rsc_params);
         while (g_hash_table_iter_next(&gIter, (gpointer *) & name, (gpointer *) & value)) {
             if (!name || !value) {
                 continue;
             }
             params = stonith_key_value_add(params, name, value);
             crm_trace(" %s=%s", name, value);
         }
 
         data = create_device_registration_xml(rsc_name(rsc), st_namespace_any,
                                               agent, params, rsc_provides);
         stonith_key_value_freeall(params, 1, 1);
         rc = stonith_device_register(data, TRUE);
         CRM_ASSERT(rc == pcmk_ok);
         free_xml(data);
     }
 }
 
 /*!
  * \internal
  * \brief Update all STONITH device definitions based on current CIB
  */
 static void
 cib_devices_update(void)
 {
     GHashTableIter iter;
     stonith_device_t *device = NULL;
 
     crm_info("Updating devices to version %s.%s.%s",
              crm_element_value(local_cib, XML_ATTR_GENERATION_ADMIN),
              crm_element_value(local_cib, XML_ATTR_GENERATION),
              crm_element_value(local_cib, XML_ATTR_NUMUPDATES));
 
     if (fenced_data_set->now != NULL) {
         crm_time_free(fenced_data_set->now);
         fenced_data_set->now = NULL;
     }
     fenced_data_set->localhost = stonith_our_uname;
     pcmk__schedule_actions(local_cib, data_set_flags, fenced_data_set);
 
     g_hash_table_iter_init(&iter, device_list);
     while (g_hash_table_iter_next(&iter, NULL, (void **)&device)) {
         if (device->cib_registered) {
             device->dirty = TRUE;
         }
     }
 
     /* have list repopulated if cib has a watchdog-fencing-resource
        TODO: keep a cached list for queries happening while we are refreshing
      */
     g_list_free_full(stonith_watchdog_targets, free);
     stonith_watchdog_targets = NULL;
     g_list_foreach(fenced_data_set->resources, (GFunc) cib_device_update, fenced_data_set);
 
     g_hash_table_iter_init(&iter, device_list);
     while (g_hash_table_iter_next(&iter, NULL, (void **)&device)) {
         if (device->dirty) {
             g_hash_table_iter_remove(&iter);
         }
     }
 
     fenced_data_set->input = NULL; // Wasn't a copy, so don't let API free it
     pe_reset_working_set(fenced_data_set);
 }
 
 static void
 update_cib_stonith_devices_v2(const char *event, xmlNode * msg)
 {
     xmlNode *change = NULL;
     char *reason = NULL;
     bool needs_update = FALSE;
     xmlNode *patchset = get_message_xml(msg, F_CIB_UPDATE_RESULT);
 
     for (change = pcmk__xml_first_child(patchset); change != NULL;
          change = pcmk__xml_next(change)) {
         const char *op = crm_element_value(change, XML_DIFF_OP);
         const char *xpath = crm_element_value(change, XML_DIFF_PATH);
         const char *shortpath = NULL;
 
         if ((op == NULL) ||
             (strcmp(op, "move") == 0) ||
             strstr(xpath, "/"XML_CIB_TAG_STATUS)) {
             continue;
         } else if (pcmk__str_eq(op, "delete", pcmk__str_casei) && strstr(xpath, "/"XML_CIB_TAG_RESOURCE)) {
             const char *rsc_id = NULL;
             char *search = NULL;
             char *mutable = NULL;
 
             if (strstr(xpath, XML_TAG_ATTR_SETS) ||
                 strstr(xpath, XML_TAG_META_SETS)) {
                 needs_update = TRUE;
                 pcmk__str_update(&reason,
                                  "(meta) attribute deleted from resource");
                 break;
             }
             pcmk__str_update(&mutable, xpath);
             rsc_id = strstr(mutable, "primitive[@" XML_ATTR_ID "=\'");
             if (rsc_id != NULL) {
                 rsc_id += strlen("primitive[@" XML_ATTR_ID "=\'");
                 search = strchr(rsc_id, '\'');
             }
             if (search != NULL) {
                 *search = 0;
                 stonith_device_remove(rsc_id, true);
                 /* watchdog_device_update called afterwards
                    to fall back to implicit definition if needed */
             } else {
                 crm_warn("Ignoring malformed CIB update (resource deletion)");
             }
             free(mutable);
 
         } else if (strstr(xpath, "/"XML_CIB_TAG_RESOURCES) ||
                    strstr(xpath, "/"XML_CIB_TAG_CONSTRAINTS) ||
                    strstr(xpath, "/"XML_CIB_TAG_RSCCONFIG)) {
             shortpath = strrchr(xpath, '/'); CRM_ASSERT(shortpath);
             reason = crm_strdup_printf("%s %s", op, shortpath+1);
             needs_update = TRUE;
             break;
         }
     }
 
     if(needs_update) {
         crm_info("Updating device list from CIB: %s", reason);
         cib_devices_update();
     } else {
         crm_trace("No updates for device list found in CIB");
     }
     free(reason);
 }
 
 
 static void
 update_cib_stonith_devices_v1(const char *event, xmlNode * msg)
 {
     const char *reason = "none";
     gboolean needs_update = FALSE;
     xmlXPathObjectPtr xpath_obj = NULL;
 
     /* process new constraints */
     xpath_obj = xpath_search(msg, "//" F_CIB_UPDATE_RESULT "//" XML_CONS_TAG_RSC_LOCATION);
     if (numXpathResults(xpath_obj) > 0) {
         int max = numXpathResults(xpath_obj), lpc = 0;
 
         /* Safest and simplest to always recompute */
         needs_update = TRUE;
         reason = "new location constraint";
 
         for (lpc = 0; lpc < max; lpc++) {
             xmlNode *match = getXpathResult(xpath_obj, lpc);
 
             crm_log_xml_trace(match, "new constraint");
         }
     }
     freeXpathObject(xpath_obj);
 
     /* process deletions */
     xpath_obj = xpath_search(msg, "//" F_CIB_UPDATE_RESULT "//" XML_TAG_DIFF_REMOVED "//" XML_CIB_TAG_RESOURCE);
     if (numXpathResults(xpath_obj) > 0) {
         remove_cib_device(xpath_obj);
     }
     freeXpathObject(xpath_obj);
 
     /* process additions */
     xpath_obj = xpath_search(msg, "//" F_CIB_UPDATE_RESULT "//" XML_TAG_DIFF_ADDED "//" XML_CIB_TAG_RESOURCE);
     if (numXpathResults(xpath_obj) > 0) {
         int max = numXpathResults(xpath_obj), lpc = 0;
 
         for (lpc = 0; lpc < max; lpc++) {
             const char *rsc_id = NULL;
             const char *standard = NULL;
             xmlNode *match = getXpathResult(xpath_obj, lpc);
 
             rsc_id = crm_element_value(match, XML_ATTR_ID);
             standard = crm_element_value(match, XML_AGENT_ATTR_CLASS);
 
             if (!pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_STONITH, pcmk__str_casei)) {
                 continue;
             }
 
             crm_trace("Fencing resource %s was added or modified", rsc_id);
             reason = "new resource";
             needs_update = TRUE;
         }
     }
     freeXpathObject(xpath_obj);
 
     if(needs_update) {
         crm_info("Updating device list from CIB: %s", reason);
         cib_devices_update();
     }
 }
 
 static void
 update_cib_stonith_devices(const char *event, xmlNode * msg)
 {
     int format = 1;
     xmlNode *patchset = get_message_xml(msg, F_CIB_UPDATE_RESULT);
 
     CRM_ASSERT(patchset);
     crm_element_value_int(patchset, "format", &format);
     switch(format) {
         case 1:
             update_cib_stonith_devices_v1(event, msg);
             break;
         case 2:
             update_cib_stonith_devices_v2(event, msg);
             break;
         default:
             crm_warn("Unknown patch format: %d", format);
     }
 }
 
 /*!
  * \internal
  * \brief Check whether a node has a specific attribute name/value
  *
  * \param[in] node    Name of node to check
  * \param[in] name    Name of an attribute to look for
  * \param[in] value   The value the named attribute needs to be set to in order to be considered a match
  *
  * \return TRUE if the locally cached CIB has the specified node attribute
  */
 gboolean
 node_has_attr(const char *node, const char *name, const char *value)
 {
     GString *xpath = NULL;
     xmlNode *match;
 
     CRM_CHECK((local_cib != NULL) && (node != NULL) && (name != NULL)
               && (value != NULL), return FALSE);
 
     /* Search for the node's attributes in the CIB. While the schema allows
      * multiple sets of instance attributes, and allows instance attributes to
      * use id-ref to reference values elsewhere, that is intended for resources,
      * so we ignore that here.
      */
     xpath = g_string_sized_new(256);
     pcmk__g_strcat(xpath,
                    "//" XML_CIB_TAG_NODES "/" XML_CIB_TAG_NODE
                    "[@" XML_ATTR_UNAME "='", node, "']/" XML_TAG_ATTR_SETS
                    "/" XML_CIB_TAG_NVPAIR
                    "[@" XML_NVPAIR_ATTR_NAME "='", name, "' "
                    "and @" XML_NVPAIR_ATTR_VALUE "='", value, "']", NULL);
 
     match = get_xpath_object((const char *) xpath->str, local_cib, LOG_NEVER);
 
     g_string_free(xpath, TRUE);
     return (match != NULL);
 }
 
 /*!
  * \internal
  * \brief Check whether a node does watchdog-fencing
  *
  * \param[in] node    Name of node to check
  *
  * \return TRUE if node found in stonith_watchdog_targets
  *         or stonith_watchdog_targets is empty indicating
  *         all nodes are doing watchdog-fencing
  */
 gboolean
 node_does_watchdog_fencing(const char *node)
 {
     return ((stonith_watchdog_targets == NULL) ||
             pcmk__str_in_list(node, stonith_watchdog_targets, pcmk__str_casei));
 }
 
 
 static void
 update_fencing_topology(const char *event, xmlNode * msg)
 {
     int format = 1;
     const char *xpath;
     xmlXPathObjectPtr xpathObj = NULL;
     xmlNode *patchset = get_message_xml(msg, F_CIB_UPDATE_RESULT);
 
     CRM_ASSERT(patchset);
     crm_element_value_int(patchset, "format", &format);
 
     if(format == 1) {
         /* Process deletions (only) */
         xpath = "//" F_CIB_UPDATE_RESULT "//" XML_TAG_DIFF_REMOVED "//" XML_TAG_FENCING_LEVEL;
         xpathObj = xpath_search(msg, xpath);
 
         remove_fencing_topology(xpathObj);
         freeXpathObject(xpathObj);
 
         /* Process additions and changes */
         xpath = "//" F_CIB_UPDATE_RESULT "//" XML_TAG_DIFF_ADDED "//" XML_TAG_FENCING_LEVEL;
         xpathObj = xpath_search(msg, xpath);
 
         register_fencing_topology(xpathObj);
         freeXpathObject(xpathObj);
 
     } else if(format == 2) {
         xmlNode *change = NULL;
         int add[] = { 0, 0, 0 };
         int del[] = { 0, 0, 0 };
 
         xml_patch_versions(patchset, add, del);
 
         for (change = pcmk__xml_first_child(patchset); change != NULL;
              change = pcmk__xml_next(change)) {
             const char *op = crm_element_value(change, XML_DIFF_OP);
             const char *xpath = crm_element_value(change, XML_DIFF_PATH);
 
             if(op == NULL) {
                 continue;
 
             } else if(strstr(xpath, "/" XML_TAG_FENCING_LEVEL) != NULL) {
                 /* Change to a specific entry */
 
                 crm_trace("Handling %s operation %d.%d.%d for %s", op, add[0], add[1], add[2], xpath);
                 if(strcmp(op, "move") == 0) {
                     continue;
 
                 } else if(strcmp(op, "create") == 0) {
                     add_topology_level(change->children);
 
                 } else if(strcmp(op, "modify") == 0) {
                     xmlNode *match = first_named_child(change, XML_DIFF_RESULT);
 
                     if(match) {
                         remove_topology_level(match->children);
                         add_topology_level(match->children);
                     }
 
                 } else if(strcmp(op, "delete") == 0) {
                     /* Nuclear option, all we have is the path and an id... not enough to remove a specific entry */
                     crm_info("Re-initializing fencing topology after %s operation %d.%d.%d for %s",
                              op, add[0], add[1], add[2], xpath);
                     fencing_topology_init();
                     return;
                 }
 
             } else if (strstr(xpath, "/" XML_TAG_FENCING_TOPOLOGY) != NULL) {
                 /* Change to the topology in general */
                 crm_info("Re-initializing fencing topology after top-level %s operation  %d.%d.%d for %s",
                          op, add[0], add[1], add[2], xpath);
                 fencing_topology_init();
                 return;
 
             } else if (strstr(xpath, "/" XML_CIB_TAG_CONFIGURATION)) {
                 /* Changes to the whole config section, possibly including the topology as a whild */
                 if(first_named_child(change, XML_TAG_FENCING_TOPOLOGY) == NULL) {
                     crm_trace("Nothing for us in %s operation %d.%d.%d for %s.",
                               op, add[0], add[1], add[2], xpath);
 
                 } else if(strcmp(op, "delete") == 0 || strcmp(op, "create") == 0) {
                     crm_info("Re-initializing fencing topology after top-level %s operation %d.%d.%d for %s.",
                              op, add[0], add[1], add[2], xpath);
                     fencing_topology_init();
                     return;
                 }
 
             } else {
                 crm_trace("Nothing for us in %s operation %d.%d.%d for %s",
                           op, add[0], add[1], add[2], xpath);
             }
         }
 
     } else {
         crm_warn("Unknown patch format: %d", format);
     }
 }
 static bool have_cib_devices = FALSE;
 
 static void
 update_cib_cache_cb(const char *event, xmlNode * msg)
 {
     int rc = pcmk_ok;
     long timeout_ms_saved = stonith_watchdog_timeout_ms;
     bool need_full_refresh = false;
 
     if(!have_cib_devices) {
         crm_trace("Skipping updates until we get a full dump");
         return;
 
     } else if(msg == NULL) {
         crm_trace("Missing %s update", event);
         return;
     }
 
     /* Maintain a local copy of the CIB so that we have full access
      * to device definitions, location constraints, and node attributes
      */
     if (local_cib != NULL) {
         int rc = pcmk_ok;
         xmlNode *patchset = NULL;
 
         crm_element_value_int(msg, F_CIB_RC, &rc);
         if (rc != pcmk_ok) {
             return;
         }
 
         patchset = get_message_xml(msg, F_CIB_UPDATE_RESULT);
-        pcmk__xml_log_patchset(LOG_TRACE, patchset);
+        pcmk__output_set_log_level(logger_out, LOG_TRACE);
+        out->message(out, "xml-patchset", patchset);
         rc = xml_apply_patchset(local_cib, patchset, TRUE);
         switch (rc) {
             case pcmk_ok:
             case -pcmk_err_old_data:
                 break;
             case -pcmk_err_diff_resync:
             case -pcmk_err_diff_failed:
                 crm_notice("[%s] Patch aborted: %s (%d)", event, pcmk_strerror(rc), rc);
                 free_xml(local_cib);
                 local_cib = NULL;
                 break;
             default:
                 crm_warn("[%s] ABORTED: %s (%d)", event, pcmk_strerror(rc), rc);
                 free_xml(local_cib);
                 local_cib = NULL;
         }
     }
 
     if (local_cib == NULL) {
         crm_trace("Re-requesting full CIB");
         rc = cib_api->cmds->query(cib_api, NULL, &local_cib, cib_scope_local | cib_sync_call);
         if(rc != pcmk_ok) {
             crm_err("Couldn't retrieve the CIB: %s (%d)", pcmk_strerror(rc), rc);
             return;
         }
         CRM_ASSERT(local_cib != NULL);
         need_full_refresh = true;
     }
 
     pcmk__refresh_node_caches_from_cib(local_cib);
     update_stonith_watchdog_timeout_ms(local_cib);
 
     if (timeout_ms_saved != stonith_watchdog_timeout_ms) {
         need_full_refresh = true;
     }
 
     if (need_full_refresh) {
         fencing_topology_init();
         cib_devices_update();
     } else {
         // Partial refresh
         update_fencing_topology(event, msg);
         update_cib_stonith_devices(event, msg);
     }
 
     watchdog_device_update();
 }
 
 static void
 init_cib_cache_cb(xmlNode * msg, int call_id, int rc, xmlNode * output, void *user_data)
 {
     crm_info("Updating device list from CIB");
     have_cib_devices = TRUE;
     local_cib = copy_xml(output);
 
     pcmk__refresh_node_caches_from_cib(local_cib);
     update_stonith_watchdog_timeout_ms(local_cib);
 
     fencing_topology_init();
     cib_devices_update();
     watchdog_device_update();
 }
 
 static void
 stonith_shutdown(int nsig)
 {
     crm_info("Terminating with %d clients", pcmk__ipc_client_count());
     stonith_shutdown_flag = TRUE;
     if (mainloop != NULL && g_main_loop_is_running(mainloop)) {
         g_main_loop_quit(mainloop);
     }
 }
 
 static void
 cib_connection_destroy(gpointer user_data)
 {
     if (stonith_shutdown_flag) {
         crm_info("Connection to the CIB manager closed");
         return;
     } else {
         crm_crit("Lost connection to the CIB manager, shutting down");
     }
     if (cib_api) {
         cib_api->cmds->signoff(cib_api);
     }
     stonith_shutdown(0);
 }
 
 static void
 stonith_cleanup(void)
 {
     if (cib_api) {
         cib_api->cmds->del_notify_callback(cib_api, T_CIB_DIFF_NOTIFY, update_cib_cache_cb);
         cib_api->cmds->signoff(cib_api);
     }
 
     if (ipcs) {
         qb_ipcs_destroy(ipcs);
     }
 
     crm_peer_destroy();
     pcmk__client_cleanup();
     free_stonith_remote_op_list();
     free_topology_list();
     free_device_list();
     free_metadata_cache();
     fenced_unregister_handlers();
 
     free(stonith_our_uname);
     stonith_our_uname = NULL;
 
     free_xml(local_cib);
     local_cib = NULL;
 }
 
 static gboolean
 stand_alone_cpg_cb(const gchar *option_name, const gchar *optarg, gpointer data,
                    GError **error)
 {
     stand_alone = FALSE;
     options.no_cib_connect = true;
     return TRUE;
 }
 
 static void
 setup_cib(void)
 {
     int rc, retries = 0;
 
     cib_api = cib_new();
     if (cib_api == NULL) {
         crm_err("No connection to the CIB manager");
         return;
     }
 
     do {
         sleep(retries);
         rc = cib_api->cmds->signon(cib_api, CRM_SYSTEM_STONITHD, cib_command);
     } while (rc == -ENOTCONN && ++retries < 5);
 
     if (rc != pcmk_ok) {
         crm_err("Could not connect to the CIB manager: %s (%d)", pcmk_strerror(rc), rc);
 
     } else if (pcmk_ok !=
                cib_api->cmds->add_notify_callback(cib_api, T_CIB_DIFF_NOTIFY, update_cib_cache_cb)) {
         crm_err("Could not set CIB notification callback");
 
     } else {
         rc = cib_api->cmds->query(cib_api, NULL, NULL, cib_scope_local);
         cib_api->cmds->register_callback(cib_api, rc, 120, FALSE, NULL, "init_cib_cache_cb",
                                          init_cib_cache_cb);
         cib_api->cmds->set_connection_dnotify(cib_api, cib_connection_destroy);
         crm_info("Watching for fencing topology changes");
     }
 }
 
 struct qb_ipcs_service_handlers ipc_callbacks = {
     .connection_accept = st_ipc_accept,
     .connection_created = NULL,
     .msg_process = st_ipc_dispatch,
     .connection_closed = st_ipc_closed,
     .connection_destroyed = st_ipc_destroy
 };
 
 /*!
  * \internal
  * \brief Callback for peer status changes
  *
  * \param[in] type  What changed
  * \param[in] node  What peer had the change
  * \param[in] data  Previous value of what changed
  */
 static void
 st_peer_update_callback(enum crm_status_type type, crm_node_t * node, const void *data)
 {
     if ((type != crm_status_processes)
         && !pcmk_is_set(node->flags, crm_remote_node)) {
         /*
          * This is a hack until we can send to a nodeid and/or we fix node name lookups
          * These messages are ignored in stonith_peer_callback()
          */
         xmlNode *query = create_xml_node(NULL, "stonith_command");
 
         crm_xml_add(query, F_XML_TAGNAME, "stonith_command");
         crm_xml_add(query, F_TYPE, T_STONITH_NG);
         crm_xml_add(query, F_STONITH_OPERATION, "poke");
 
         crm_debug("Broadcasting our uname because of node %u", node->id);
         send_cluster_message(NULL, crm_msg_stonith_ng, query, FALSE);
 
         free_xml(query);
     }
 }
 
 static pcmk__cluster_option_t fencer_options[] = {
     /* name, old name, type, allowed values,
      * default value, validator,
      * short description,
      * long description
      */
     {
         PCMK_STONITH_HOST_ARGUMENT, NULL, "string", NULL, "port", NULL,
         N_("Advanced use only: An alternate parameter to supply instead of 'port'"),
         N_("some devices do not support the "
            "standard 'port' parameter or may provide additional ones. Use "
            "this to specify an alternate, device-specific, parameter "
            "that should indicate the machine to be fenced. A value of "
            "none can be used to tell the cluster not to supply any "
            "additional parameters.")
     },
     {
         PCMK_STONITH_HOST_MAP,NULL, "string", NULL, "", NULL,
         N_("A mapping of host names to ports numbers for devices that do not support host names."),
         N_("Eg. node1:1;node2:2,3 would tell the cluster to use port 1 for node1 and ports 2 and 3 for node2")
     },
     {
         PCMK_STONITH_HOST_LIST,NULL, "string", NULL, "", NULL,
         N_("Eg. node1,node2,node3"),
         N_("A list of machines controlled by "
                "this device (Optional unless pcmk_host_list=static-list)")
     },
     {
         PCMK_STONITH_HOST_CHECK,NULL, "string", NULL, "dynamic-list", NULL,
         N_("How to determine which machines are controlled by the device."),
         N_("Allowed values: dynamic-list "
                "(query the device via the 'list' command), static-list "
                "(check the pcmk_host_list attribute), status "
                "(query the device via the 'status' command), "
                "none (assume every device can fence every "
                "machine)")
     },
     {
         PCMK_STONITH_DELAY_MAX,NULL, "time", NULL, "0s", NULL,
         N_("Enable a base delay for fencing actions and specify base delay value."),
         N_("Enable a delay of no more than the "
                "time specified before executing fencing actions. Pacemaker "
                "derives the overall delay by taking the value of "
                "pcmk_delay_base and adding a random delay value such "
                "that the sum is kept below this maximum.")
     },
     {
         PCMK_STONITH_DELAY_BASE,NULL, "string", NULL, "0s", NULL,
         N_("Enable a base delay for "
                "fencing actions and specify base delay value."),
         N_("This enables a static delay for "
                "fencing actions, which can help avoid \"death matches\" where "
                "two nodes try to fence each other at the same time. If "
                "pcmk_delay_max  is also used, a random delay will be "
                "added such that the total delay is kept below that value."
                "This can be set to a single time value to apply to any node "
                "targeted by this device (useful if a separate device is "
                "configured for each target), or to a node map (for example, "
                "\"node1:1s;node2:5\") to set a different value per target.")
     },
     {
         PCMK_STONITH_ACTION_LIMIT,NULL, "integer", NULL, "1", NULL,
         N_("The maximum number of actions can be performed in parallel on this device"),
         N_("Cluster property concurrent-fencing=true needs to be configured first."
              "Then use this to specify the maximum number of actions can be performed in parallel on this device. -1 is unlimited.")
     },
     {
 	"pcmk_reboot_action",NULL, "string", NULL, "reboot", NULL,
 	N_("Advanced use only: An alternate command to run instead of 'reboot'"),
         N_("Some devices do not support the standard commands or may provide additional ones.\n"
                  "Use this to specify an alternate, device-specific, command that implements the \'reboot\' action.")
     },
     {
 	"pcmk_reboot_timeout",NULL, "time", NULL, "60s", NULL,
 	N_("Advanced use only: Specify an alternate timeout to use for reboot actions instead of stonith-timeout"),
         N_("Some devices need much more/less time to complete than normal."
 	   "Use this to specify an alternate, device-specific, timeout for \'reboot\' actions.")
     },
     {
 	"pcmk_reboot_retries",NULL, "integer", NULL, "2", NULL,
 	N_("Advanced use only: The maximum number of times to retry the 'reboot' command within the timeout period"),
         N_("Some devices do not support multiple connections."
            " Operations may 'fail' if the device is busy with another task so Pacemaker will automatically retry the operation,      if there is time remaining."
            " Use this option to alter the number of times Pacemaker retries \'reboot\' actions before giving up.")
     },
     {
 	"pcmk_off_action",NULL, "string", NULL, "off", NULL,
 	N_("Advanced use only: An alternate command to run instead of \'off\'"),
         N_("Some devices do not support the standard commands or may provide additional ones."
                  "Use this to specify an alternate, device-specific, command that implements the \'off\' action.")
     },
     {
 	"pcmk_off_timeout",NULL, "time", NULL, "60s", NULL,
 	N_("Advanced use only: Specify an alternate timeout to use for off actions instead of stonith-timeout"),
         N_("Some devices need much more/less time to complete than normal."
 	   "Use this to specify an alternate, device-specific, timeout for \'off\' actions.")
     },
     {
 	"pcmk_off_retries",NULL, "integer", NULL, "2", NULL,
 	N_("Advanced use only: The maximum number of times to retry the 'off' command within the timeout period"),
         N_("Some devices do not support multiple connections."
            " Operations may 'fail' if the device is busy with another task so Pacemaker will automatically retry the operation,      if there is time remaining."
            " Use this option to alter the number of times Pacemaker retries \'off\' actions before giving up.")
     },
     {
 	"pcmk_on_action",NULL, "string", NULL, "on", NULL,
 	N_("Advanced use only: An alternate command to run instead of 'on'"),
         N_("Some devices do not support the standard commands or may provide additional ones."
                  "Use this to specify an alternate, device-specific, command that implements the \'on\' action.")
     },
     {
 	"pcmk_on_timeout",NULL, "time", NULL, "60s", NULL,
 	N_("Advanced use only: Specify an alternate timeout to use for on actions instead of stonith-timeout"),
         N_("Some devices need much more/less time to complete than normal."
 	   "Use this to specify an alternate, device-specific, timeout for \'on\' actions.")
     },
     {
 	"pcmk_on_retries",NULL, "integer", NULL, "2", NULL,
 	N_("Advanced use only: The maximum number of times to retry the 'on' command within the timeout period"),
         N_("Some devices do not support multiple connections."
            " Operations may 'fail' if the device is busy with another task so Pacemaker will automatically retry the operation,      if there is time remaining."
            " Use this option to alter the number of times Pacemaker retries \'on\' actions before giving up.")
     },
     {
 	"pcmk_list_action",NULL, "string", NULL, "list", NULL,
 	N_("Advanced use only: An alternate command to run instead of \'list\'"),
         N_("Some devices do not support the standard commands or may provide additional ones."
                  "Use this to specify an alternate, device-specific, command that implements the \'list\' action.")
     },
     {
 	"pcmk_list_timeout",NULL, "time", NULL, "60s", NULL,
 	N_("Advanced use only: Specify an alternate timeout to use for list actions instead of stonith-timeout"),
         N_("Some devices need much more/less time to complete than normal."
 	   "Use this to specify an alternate, device-specific, timeout for \'list\' actions.")
     },
     {
 	"pcmk_list_retries",NULL, "integer", NULL, "2", NULL,
 	N_("Advanced use only: The maximum number of times to retry the \'list\' command within the timeout period"),
         N_("Some devices do not support multiple connections."
            " Operations may 'fail' if the device is busy with another task so Pacemaker will automatically retry the operation,      if there is time remaining."
            " Use this option to alter the number of times Pacemaker retries \'list\' actions before giving up.")
     },
     {
 	"pcmk_monitor_action",NULL, "string", NULL, "monitor", NULL,
 	N_("Advanced use only: An alternate command to run instead of \'monitor\'"),
         N_("Some devices do not support the standard commands or may provide additional ones."
                  "Use this to specify an alternate, device-specific, command that implements the \'monitor\' action.")
     },
     {
 	"pcmk_monitor_timeout",NULL, "time", NULL, "60s", NULL,
 	N_("Advanced use only: Specify an alternate timeout to use for monitor actions instead of stonith-timeout"),
         N_("Some devices need much more/less time to complete than normal.\n"
 	   "Use this to specify an alternate, device-specific, timeout for \'monitor\' actions.")
     },
     {
 	"pcmk_monitor_retries",NULL, "integer", NULL, "2", NULL,
 	N_("Advanced use only: The maximum number of times to retry the \'monitor\' command within the timeout period"),
         N_("Some devices do not support multiple connections."
            " Operations may 'fail' if the device is busy with another task so Pacemaker will automatically retry the operation,      if there is time remaining."
            " Use this option to alter the number of times Pacemaker retries \'monitor\' actions before giving up.")
     },
     {
 	"pcmk_status_action",NULL, "string", NULL, "status", NULL,
 	N_("Advanced use only: An alternate command to run instead of \'status\'"),
         N_("Some devices do not support the standard commands or may provide additional ones."
                  "Use this to specify an alternate, device-specific, command that implements the \'status\' action.")
     },
     {
 	"pcmk_status_timeout",NULL, "time", NULL, "60s", NULL,
 	N_("Advanced use only: Specify an alternate timeout to use for status actions instead of stonith-timeout"),
         N_("Some devices need much more/less time to complete than normal."
 	   "Use this to specify an alternate, device-specific, timeout for \'status\' actions.")
     },
     {
 	"pcmk_status_retries",NULL, "integer", NULL, "2", NULL,
 	N_("Advanced use only: The maximum number of times to retry the \'status\' command within the timeout period"),
         N_("Some devices do not support multiple connections."
            " Operations may 'fail' if the device is busy with another task so Pacemaker will automatically retry the operation,      if there is time remaining."
            " Use this option to alter the number of times Pacemaker retries \'status\' actions before giving up.")
     },
 };
 
 void
 fencer_metadata(void)
 {
     const char *desc_short = N_("Instance attributes available for all "
                              "\"stonith\"-class resources");
     const char *desc_long = N_("Instance attributes available for all \"stonith\"-"
                             "class resources and used by Pacemaker's fence "
                             "daemon, formerly known as stonithd");
 
     gchar *s = pcmk__format_option_metadata("pacemaker-fenced", desc_short,
                                             desc_long, fencer_options,
                                             PCMK__NELEM(fencer_options));
     printf("%s", s);
     g_free(s);
 }
 
 static GOptionEntry entries[] = {
     { "stand-alone", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &stand_alone,
       "Deprecated (will be removed in a future release)", NULL },
 
     { "stand-alone-w-cpg", 'c', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
       stand_alone_cpg_cb, "Intended for use in regression testing only", NULL },
 
     { "logfile", 'l', G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME_ARRAY,
       &options.log_files, "Send logs to the additional named logfile", NULL },
 
     { NULL }
 };
 
 static GOptionContext *
 build_arg_context(pcmk__common_args_t *args, GOptionGroup **group)
 {
     GOptionContext *context = NULL;
 
     context = pcmk__build_arg_context(args, "text (default), xml", group,
                                       "[metadata]");
     pcmk__add_main_args(context, entries);
     return context;
 }
 
 int
 main(int argc, char **argv)
 {
     int rc = pcmk_rc_ok;
     crm_cluster_t *cluster = NULL;
     crm_ipc_t *old_instance = NULL;
 
     GError *error = NULL;
 
     GOptionGroup *output_group = NULL;
     pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
     gchar **processed_args = pcmk__cmdline_preproc(argv, "l");
     GOptionContext *context = build_arg_context(args, &output_group);
 
     crm_log_preinit(NULL, argc, argv);
 
     pcmk__register_formats(output_group, formats);
     if (!g_option_context_parse_strv(context, &processed_args, &error)) {
         exit_code = CRM_EX_USAGE;
         goto done;
     }
 
     rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
     if (rc != pcmk_rc_ok) {
         exit_code = CRM_EX_ERROR;
         g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                     "Error creating output format %s: %s",
                     args->output_ty, pcmk_rc_str(rc));
         goto done;
     }
 
     if (args->version) {
         out->version(out, false);
         goto done;
     }
 
     if ((g_strv_length(processed_args) >= 2)
         && pcmk__str_eq(processed_args[1], "metadata", pcmk__str_none)) {
         fencer_metadata();
         goto done;
     }
 
     // Open additional log files
     pcmk__add_logfiles(options.log_files, out);
 
     crm_log_init(NULL, LOG_INFO + args->verbosity, TRUE,
                  (args->verbosity > 0), argc, argv, FALSE);
 
     crm_notice("Starting Pacemaker fencer");
 
     old_instance = crm_ipc_new("stonith-ng", 0);
     if (old_instance == NULL) {
         /* crm_ipc_new() will have already logged an error message with
          * crm_err()
          */
         exit_code = CRM_EX_FATAL;
         goto done;
     }
 
     if (crm_ipc_connect(old_instance)) {
         // IPC endpoint already up
         crm_ipc_close(old_instance);
         crm_ipc_destroy(old_instance);
         crm_err("pacemaker-fenced is already active, aborting startup");
         goto done;
     } else {
         // Not up or not authentic, we'll proceed either way
         crm_ipc_destroy(old_instance);
         old_instance = NULL;
     }
 
     mainloop_add_signal(SIGTERM, stonith_shutdown);
 
     crm_peer_init();
 
     fenced_data_set = pe_new_working_set();
     CRM_ASSERT(fenced_data_set != NULL);
 
     cluster = pcmk_cluster_new();
 
+    /* Initialize the logger prior to setup_cib(). update_cib_cache_cb() may
+     * call the "xml-patchset" message function, which needs the logger, after
+     * setup_cib() has run.
+     */
+    rc = pcmk__log_output_new(&logger_out) != pcmk_rc_ok;
+    if (rc != pcmk_rc_ok) {
+        exit_code = CRM_EX_FATAL;
+        g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+                    "Error creating output format log: %s", pcmk_rc_str(rc));
+        goto done;
+    }
+    pe__register_messages(logger_out);
+    pcmk__register_lib_messages(logger_out);
+    pcmk__output_set_log_level(logger_out, LOG_TRACE);
+    fenced_data_set->priv = logger_out;
+
     if (!stand_alone) {
         if (is_corosync_cluster()) {
 #if SUPPORT_COROSYNC
             cluster->destroy = stonith_peer_cs_destroy;
             cluster->cpg.cpg_deliver_fn = stonith_peer_ais_callback;
             cluster->cpg.cpg_confchg_fn = pcmk_cpg_membership;
 #endif
         }
 
         crm_set_status_callback(&st_peer_update_callback);
 
         if (crm_cluster_connect(cluster) == FALSE) {
             exit_code = CRM_EX_FATAL;
             crm_crit("Cannot sign in to the cluster... terminating");
             goto done;
         }
         pcmk__str_update(&stonith_our_uname, cluster->uname);
 
         if (!options.no_cib_connect) {
             setup_cib();
         }
 
     } else {
         pcmk__str_update(&stonith_our_uname, "localhost");
         crm_warn("Stand-alone mode is deprecated and will be removed "
                  "in a future release");
     }
 
     init_device_list();
     init_topology_list();
 
     pcmk__serve_fenced_ipc(&ipcs, &ipc_callbacks);
 
-    if (pcmk__log_output_new(&logger_out) != pcmk_rc_ok) {
-        exit_code = CRM_EX_FATAL;
-        goto done;
-    }
-    pe__register_messages(logger_out);
-    pcmk__register_lib_messages(logger_out);
-    pcmk__output_set_log_level(logger_out, LOG_TRACE);
-    fenced_data_set->priv = logger_out;
-
     // Create the mainloop and run it...
     mainloop = g_main_loop_new(NULL, FALSE);
     crm_notice("Pacemaker fencer successfully started and accepting connections");
     g_main_loop_run(mainloop);
 
 done:
     g_strfreev(processed_args);
     pcmk__free_arg_context(context);
 
     g_strfreev(options.log_files);
 
     stonith_cleanup();
     pcmk_cluster_free(cluster);
     pe_free_working_set(fenced_data_set);
 
     pcmk__output_and_clear_error(error, out);
 
     if (logger_out != NULL) {
         logger_out->finish(logger_out, exit_code, true, NULL);
         pcmk__output_free(logger_out);
     }
 
     if (out != NULL) {
         out->finish(out, exit_code, true, NULL);
         pcmk__output_free(out);
     }
 
     pcmk__unregister_formats();
     crm_exit(exit_code);
 }
diff --git a/include/crm/common/xml_internal.h b/include/crm/common/xml_internal.h
index 4283ef69b9..43b3b8c163 100644
--- a/include/crm/common/xml_internal.h
+++ b/include/crm/common/xml_internal.h
@@ -1,415 +1,414 @@
 /*
  * Copyright 2017-2023 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__XML_INTERNAL__H
 #  define PCMK__XML_INTERNAL__H
 
 /*
  * Internal-only wrappers for and extensions to libxml2 (libxslt)
  */
 
 #  include <stdlib.h>
 #  include <stdio.h>
 #  include <string.h>
 
 #  include <crm/crm.h>  /* transitively imports qblog.h */
 #  include <crm/common/output_internal.h>
 
 
 /*!
  * \brief Base for directing lib{xml2,xslt} log into standard libqb backend
  *
  * This macro implements the core of what can be needed for directing
  * libxml2 or libxslt error messaging into standard, preconfigured
  * libqb-backed log stream.
  *
  * It's a bit unfortunate that libxml2 (and more sparsely, also libxslt)
  * emits a single message by chunks (location is emitted separatedly from
  * the message itself), so we have to take the effort to combine these
  * chunks back to single message.  Whether to do this or not is driven
  * with \p dechunk toggle.
  *
  * The form of a macro was chosen for implicit deriving of __FILE__, etc.
  * and also because static dechunking buffer should be differentiated per
  * library (here we assume different functions referring to this macro
  * will not ever be using both at once), preferably also per-library
  * context of use to avoid clashes altogether.
  *
  * Note that we cannot use qb_logt, because callsite data have to be known
  * at the moment of compilation, which it is not always the case -- xml_log
  * (and unfortunately there's no clear explanation of the fail to compile).
  *
  * Also note that there's no explicit guard against said libraries producing
  * never-newline-terminated chunks (which would just keep consuming memory),
  * as it's quite improbable.  Termination of the program in between the
  * same-message chunks will raise a flag with valgrind and the likes, though.
  *
  * And lastly, regarding how dechunking combines with other non-message
  * parameters -- for \p priority, most important running specification
  * wins (possibly elevated to LOG_ERR in case of nonconformance with the
  * newline-termination "protocol"), \p dechunk is expected to always be
  * on once it was at the start, and the rest (\p postemit and \p prefix)
  * are picked directly from the last chunk entry finalizing the message
  * (also reasonable to always have it the same with all related entries).
  *
  * \param[in] priority Syslog priority for the message to be logged
  * \param[in] dechunk  Whether to dechunk new-line terminated message
  * \param[in] postemit Code to be executed once message is sent out
  * \param[in] prefix   How to prefix the message or NULL for raw passing
  * \param[in] fmt      Format string as with printf-like functions
  * \param[in] ap       Variable argument list to supplement \p fmt format string
  */
 #define PCMK__XML_LOG_BASE(priority, dechunk, postemit, prefix, fmt, ap)        \
 do {                                                                            \
     if (!(dechunk) && (prefix) == NULL) {  /* quick pass */                     \
         qb_log_from_external_source_va(__func__, __FILE__, (fmt),               \
                                        (priority), __LINE__, 0, (ap));          \
         (void) (postemit);                                                      \
     } else {                                                                    \
         int CXLB_len = 0;                                                       \
         char *CXLB_buf = NULL;                                                  \
         static int CXLB_buffer_len = 0;                                         \
         static char *CXLB_buffer = NULL;                                        \
         static uint8_t CXLB_priority = 0;                                       \
                                                                                 \
         CXLB_len = vasprintf(&CXLB_buf, (fmt), (ap));                           \
                                                                                 \
         if (CXLB_len <= 0 || CXLB_buf[CXLB_len - 1] == '\n' || !(dechunk)) {    \
             if (CXLB_len < 0) {                                                 \
                 CXLB_buf = (char *) "LOG CORRUPTION HAZARD"; /*we don't modify*/\
                 CXLB_priority = QB_MIN(CXLB_priority, LOG_ERR);                 \
             } else if (CXLB_len > 0 /* && (dechunk) */                          \
                        && CXLB_buf[CXLB_len - 1] == '\n') {                     \
                 CXLB_buf[CXLB_len - 1] = '\0';                                  \
             }                                                                   \
             if (CXLB_buffer) {                                                  \
                 qb_log_from_external_source(__func__, __FILE__, "%s%s%s",       \
                                             CXLB_priority, __LINE__, 0,         \
                                             (prefix) != NULL ? (prefix) : "",   \
                                             CXLB_buffer, CXLB_buf);             \
                 free(CXLB_buffer);                                              \
             } else {                                                            \
                 qb_log_from_external_source(__func__, __FILE__, "%s%s",         \
                                             (priority), __LINE__, 0,            \
                                             (prefix) != NULL ? (prefix) : "",   \
                                             CXLB_buf);                          \
             }                                                                   \
             if (CXLB_len < 0) {                                                 \
                 CXLB_buf = NULL;  /* restore temporary override */              \
             }                                                                   \
             CXLB_buffer = NULL;                                                 \
             CXLB_buffer_len = 0;                                                \
             (void) (postemit);                                                  \
                                                                                 \
         } else if (CXLB_buffer == NULL) {                                       \
             CXLB_buffer_len = CXLB_len;                                         \
             CXLB_buffer = CXLB_buf;                                             \
             CXLB_buf = NULL;                                                    \
             CXLB_priority = (priority);  /* remember as a running severest */   \
                                                                                 \
         } else {                                                                \
             CXLB_buffer = realloc(CXLB_buffer, 1 + CXLB_buffer_len + CXLB_len); \
             memcpy(CXLB_buffer + CXLB_buffer_len, CXLB_buf, CXLB_len);          \
             CXLB_buffer_len += CXLB_len;                                        \
             CXLB_buffer[CXLB_buffer_len] = '\0';                                \
             CXLB_priority = QB_MIN(CXLB_priority, (priority));  /* severest? */ \
         }                                                                       \
         free(CXLB_buf);                                                         \
     }                                                                           \
 } while (0)
 
 /*
  * \enum pcmk__xml_fmt_options
  * \brief Bit flags to control format in XML logs and dumps
  */
 enum pcmk__xml_fmt_options {
     //! Exclude certain XML attributes (for calculating digests)
     pcmk__xml_fmt_filtered   = (1 << 0),
 
     //! Include indentation and newlines
     pcmk__xml_fmt_pretty     = (1 << 1),
 
     //! Include full XML subtree (with any text), using libxml serialization
     pcmk__xml_fmt_full       = (1 << 2),
 
     //! Include the opening tag of an XML element, and include XML comments
     pcmk__xml_fmt_open       = (1 << 3),
 
     //! Include the children of an XML element
     pcmk__xml_fmt_children   = (1 << 4),
 
     //! Include the closing tag of an XML element
     pcmk__xml_fmt_close      = (1 << 5),
 
     // @COMPAT Remove when log_data_element() is removed
     //! Include XML text nodes
     pcmk__xml_fmt_text       = (1 << 6),
 
     // @COMPAT Remove when v1 patchsets are removed
     //! Log a created XML subtree
     pcmk__xml_fmt_diff_plus  = (1 << 7),
 
     // @COMPAT Remove when v1 patchsets are removed
     //! Log a removed XML subtree
     pcmk__xml_fmt_diff_minus = (1 << 8),
 
     // @COMPAT Remove when v1 patchsets are removed
     //! Log a minimal version of an XML diff (only showing the changes)
     pcmk__xml_fmt_diff_short = (1 << 9),
 };
 
 int pcmk__xml_show(pcmk__output_t *out, const char *prefix, const xmlNode *data,
                    int depth, uint32_t options);
 int pcmk__xml_show_changes(pcmk__output_t *out, const xmlNode *xml);
-int pcmk__xml_log_patchset(uint8_t log_level, const xmlNode *patchset);
 
 /* XML search strings for guest, remote and pacemaker_remote nodes */
 
 /* search string to find CIB resources entries for cluster nodes */
 #define PCMK__XP_MEMBER_NODE_CONFIG \
     "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_NODES \
     "/" XML_CIB_TAG_NODE "[not(@type) or @type='member']"
 
 /* search string to find CIB resources entries for guest nodes */
 #define PCMK__XP_GUEST_NODE_CONFIG \
     "//" XML_TAG_CIB "//" XML_CIB_TAG_CONFIGURATION "//" XML_CIB_TAG_RESOURCE \
     "//" XML_TAG_META_SETS "//" XML_CIB_TAG_NVPAIR \
     "[@name='" XML_RSC_ATTR_REMOTE_NODE "']"
 
 /* search string to find CIB resources entries for remote nodes */
 #define PCMK__XP_REMOTE_NODE_CONFIG \
     "//" XML_TAG_CIB "//" XML_CIB_TAG_CONFIGURATION "//" XML_CIB_TAG_RESOURCE \
     "[@type='remote'][@provider='pacemaker']"
 
 /* search string to find CIB node status entries for pacemaker_remote nodes */
 #define PCMK__XP_REMOTE_NODE_STATUS \
     "//" XML_TAG_CIB "//" XML_CIB_TAG_STATUS "//" XML_CIB_TAG_STATE \
     "[@" XML_NODE_IS_REMOTE "='true']"
 
 enum pcmk__xml_artefact_ns {
     pcmk__xml_artefact_ns_legacy_rng = 1,
     pcmk__xml_artefact_ns_legacy_xslt,
     pcmk__xml_artefact_ns_base_rng,
     pcmk__xml_artefact_ns_base_xslt,
 };
 
 void pcmk__strip_xml_text(xmlNode *xml);
 const char *pcmk__xe_add_last_written(xmlNode *xe);
 
 xmlNode *pcmk__xe_match(const xmlNode *parent, const char *node_name,
                         const char *attr_n, const char *attr_v);
 
 void pcmk__xe_remove_matching_attrs(xmlNode *element,
                                     bool (*match)(xmlAttrPtr, void *),
                                     void *user_data);
 
 GString *pcmk__element_xpath(const xmlNode *xml);
 
 /*!
  * \internal
  * \brief Get the root directory to scan XML artefacts of given kind for
  *
  * \param[in] ns governs the hierarchy nesting against the inherent root dir
  *
  * \return root directory to scan XML artefacts of given kind for
  */
 char *
 pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns);
 
 /*!
  * \internal
  * \brief Get the fully unwrapped path to particular XML artifact (RNG/XSLT)
  *
  * \param[in] ns       denotes path forming details (parent dir, suffix)
  * \param[in] filespec symbolic file specification to be combined with
  *                     #artefact_ns to form the final path
  * \return unwrapped path to particular XML artifact (RNG/XSLT)
  */
 char *pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns,
                               const char *filespec);
 
 /*!
  * \internal
  * \brief Return first non-text child node of an XML node
  *
  * \param[in] parent  XML node to check
  *
  * \return First non-text child node of \p parent (or NULL if none)
  */
 static inline xmlNode *
 pcmk__xml_first_child(const xmlNode *parent)
 {
     xmlNode *child = (parent? parent->children : NULL);
 
     while (child && (child->type == XML_TEXT_NODE)) {
         child = child->next;
     }
     return child;
 }
 
 /*!
  * \internal
  * \brief Return next non-text sibling node of an XML node
  *
  * \param[in] child  XML node to check
  *
  * \return Next non-text sibling of \p child (or NULL if none)
  */
 static inline xmlNode *
 pcmk__xml_next(const xmlNode *child)
 {
     xmlNode *next = (child? child->next : NULL);
 
     while (next && (next->type == XML_TEXT_NODE)) {
         next = next->next;
     }
     return next;
 }
 
 /*!
  * \internal
  * \brief Return first non-text child element of an XML node
  *
  * \param[in] parent  XML node to check
  *
  * \return First child element of \p parent (or NULL if none)
  */
 static inline xmlNode *
 pcmk__xe_first_child(const xmlNode *parent)
 {
     xmlNode *child = (parent? parent->children : NULL);
 
     while (child && (child->type != XML_ELEMENT_NODE)) {
         child = child->next;
     }
     return child;
 }
 
 /*!
  * \internal
  * \brief Return next non-text sibling element of an XML element
  *
  * \param[in] child  XML element to check
  *
  * \return Next sibling element of \p child (or NULL if none)
  */
 static inline xmlNode *
 pcmk__xe_next(const xmlNode *child)
 {
     xmlNode *next = child? child->next : NULL;
 
     while (next && (next->type != XML_ELEMENT_NODE)) {
         next = next->next;
     }
     return next;
 }
 
 /*!
  * \internal
  * \brief Like pcmk__xe_set_props, but takes a va_list instead of
  *        arguments directly.
  *
  * \param[in,out] node   XML to add attributes to
  * \param[in]     pairs  NULL-terminated list of name/value pairs to add
  */
 void
 pcmk__xe_set_propv(xmlNodePtr node, va_list pairs);
 
 /*!
  * \internal
  * \brief Add a NULL-terminated list of name/value pairs to the given
  *        XML node as properties.
  *
  * \param[in,out] node XML node to add properties to
  * \param[in]     ...  NULL-terminated list of name/value pairs
  *
  * \note A NULL name terminates the arguments; a NULL value will be skipped.
  */
 void
 pcmk__xe_set_props(xmlNodePtr node, ...)
 G_GNUC_NULL_TERMINATED;
 
 /*!
  * \internal
  * \brief Get first attribute of an XML element
  *
  * \param[in] xe  XML element to check
  *
  * \return First attribute of \p xe (or NULL if \p xe is NULL or has none)
  */
 static inline xmlAttr *
 pcmk__xe_first_attr(const xmlNode *xe)
 {
     return (xe == NULL)? NULL : xe->properties;
 }
 
 /*!
  * \internal
  * \brief Extract the ID attribute from an XML element
  *
  * \param[in] xpath String to search
  * \param[in] node  Node to get the ID for
  *
  * \return ID attribute of \p node in xpath string \p xpath
  */
 char *
 pcmk__xpath_node_id(const char *xpath, const char *node);
 
 /* internal XML-related utilities */
 
 enum xml_private_flags {
      pcmk__xf_none        = 0x0000,
      pcmk__xf_dirty       = 0x0001,
      pcmk__xf_deleted     = 0x0002,
      pcmk__xf_created     = 0x0004,
      pcmk__xf_modified    = 0x0008,
 
      pcmk__xf_tracking    = 0x0010,
      pcmk__xf_processed   = 0x0020,
      pcmk__xf_skip        = 0x0040,
      pcmk__xf_moved       = 0x0080,
 
      pcmk__xf_acl_enabled = 0x0100,
      pcmk__xf_acl_read    = 0x0200,
      pcmk__xf_acl_write   = 0x0400,
      pcmk__xf_acl_deny    = 0x0800,
 
      pcmk__xf_acl_create  = 0x1000,
      pcmk__xf_acl_denied  = 0x2000,
      pcmk__xf_lazy        = 0x4000,
 };
 
 void pcmk__set_xml_doc_flag(xmlNode *xml, enum xml_private_flags flag);
 
 /*!
  * \internal
  * \brief Iterate over child elements of \p xml
  *
  * This function iterates over the children of \p xml, performing the
  * callback function \p handler on each node.  If the callback returns
  * a value other than pcmk_rc_ok, the iteration stops and the value is
  * returned.  It is therefore possible that not all children will be
  * visited.
  *
  * \param[in,out] xml                 The starting XML node.  Can be NULL.
  * \param[in]     child_element_name  The name that the node must match in order
  *                                    for \p handler to be run.  If NULL, all
  *                                    child elements will match.
  * \param[in]     handler             The callback function.
  * \param[in,out] userdata            User data to pass to the callback function.
  *                                    Can be NULL.
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name,
                        int (*handler)(xmlNode *xml, void *userdata),
                        void *userdata);
 
 #endif // PCMK__XML_INTERNAL__H
diff --git a/lib/cib/cib_file.c b/lib/cib/cib_file.c
index cbc8dea7c4..5e5e1bdaa4 100644
--- a/lib/cib/cib_file.c
+++ b/lib/cib/cib_file.c
@@ -1,897 +1,907 @@
 /*
  * Original copyright 2004 International Business Machines
  * Later changes copyright 2008-2023 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 <unistd.h>
 #include <limits.h>
 #include <stdlib.h>
 #include <stdint.h>
 #include <stdio.h>
 #include <stdarg.h>
 #include <string.h>
 #include <pwd.h>
 
 #include <sys/stat.h>
 #include <sys/types.h>
 #include <glib.h>
 
 #include <crm/crm.h>
 #include <crm/cib/internal.h>
 #include <crm/msg_xml.h>
 #include <crm/common/ipc.h>
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>
 
 enum cib_file_flags {
     cib_file_flag_dirty = (1 << 0),
     cib_file_flag_live  = (1 << 1),
 };
 
 typedef struct cib_file_opaque_s {
     uint32_t flags; // Group of enum cib_file_flags
     char *filename;
 } cib_file_opaque_t;
 
 #define cib_set_file_flags(cibfile, flags_to_set) do {                  \
         (cibfile)->flags = pcmk__set_flags_as(__func__, __LINE__,       \
                                               LOG_TRACE, "CIB file",    \
                                               cibfile->filename,        \
                                               (cibfile)->flags,         \
                                               (flags_to_set),           \
                                               #flags_to_set);           \
     } while (0)
 
 #define cib_clear_file_flags(cibfile, flags_to_clear) do {              \
         (cibfile)->flags = pcmk__clear_flags_as(__func__, __LINE__,     \
                                                 LOG_TRACE, "CIB file",  \
                                                 cibfile->filename,      \
                                                 (cibfile)->flags,       \
                                                 (flags_to_clear),       \
                                                 #flags_to_clear);       \
     } while (0)
 
 int cib_file_perform_op(cib_t * cib, const char *op, const char *host, const char *section,
                         xmlNode * data, xmlNode ** output_data, int call_options);
 
 int cib_file_perform_op_delegate(cib_t * cib, const char *op, const char *host, const char *section,
                                  xmlNode * data, xmlNode ** output_data, int call_options,
                                  const char *user_name);
 
 int cib_file_signon(cib_t * cib, const char *name, enum cib_conn_type type);
 int cib_file_signoff(cib_t * cib);
 int cib_file_free(cib_t * cib);
 
 static int
 cib_file_inputfd(cib_t * cib)
 {
     return -EPROTONOSUPPORT;
 }
 
 static int
 cib_file_set_connection_dnotify(cib_t * cib, void (*dnotify) (gpointer user_data))
 {
     return -EPROTONOSUPPORT;
 }
 
 static int
 cib_file_register_notification(cib_t * cib, const char *callback, int enabled)
 {
     return -EPROTONOSUPPORT;
 }
 
 /*!
  * \internal
  * \brief Compare the calculated digest of an XML tree against a signature file
  *
  * \param[in] root     Root of XML tree to compare
  * \param[in] sigfile  Name of signature file containing digest to compare
  *
  * \return TRUE if digests match or signature file does not exist, else FALSE
  */
 static gboolean
 cib_file_verify_digest(xmlNode *root, const char *sigfile)
 {
     gboolean passed = FALSE;
     char *expected;
     int rc = pcmk__file_contents(sigfile, &expected);
 
     switch (rc) {
         case pcmk_rc_ok:
             if (expected == NULL) {
                 crm_err("On-disk digest at %s is empty", sigfile);
                 return FALSE;
             }
             break;
         case ENOENT:
             crm_warn("No on-disk digest present at %s", sigfile);
             return TRUE;
         default:
             crm_err("Could not read on-disk digest from %s: %s",
                     sigfile, pcmk_rc_str(rc));
             return FALSE;
     }
     passed = pcmk__verify_digest(root, expected);
     free(expected);
     return passed;
 }
 
 /*!
  * \internal
  * \brief Read an XML tree from a file and verify its digest
  *
  * \param[in]  filename  Name of XML file to read
  * \param[in]  sigfile   Name of signature file containing digest to compare
  * \param[out] root      If non-NULL, will be set to pointer to parsed XML tree
  *
  * \return 0 if file was successfully read, parsed and verified, otherwise:
  *         -errno on stat() failure,
  *         -pcmk_err_cib_corrupt if file size is 0 or XML is not parseable, or
  *         -pcmk_err_cib_modified if digests do not match
  * \note If root is non-NULL, it is the caller's responsibility to free *root on
  *       successful return.
  */
 int
 cib_file_read_and_verify(const char *filename, const char *sigfile, xmlNode **root)
 {
     int s_res;
     struct stat buf;
     char *local_sigfile = NULL;
     xmlNode *local_root = NULL;
 
     CRM_ASSERT(filename != NULL);
     if (root) {
         *root = NULL;
     }
 
     /* Verify that file exists and its size is nonzero */
     s_res = stat(filename, &buf);
     if (s_res < 0) {
         crm_perror(LOG_WARNING, "Could not verify cluster configuration file %s", filename);
         return -errno;
     } else if (buf.st_size == 0) {
         crm_warn("Cluster configuration file %s is corrupt (size is zero)", filename);
         return -pcmk_err_cib_corrupt;
     }
 
     /* Parse XML */
     local_root = filename2xml(filename);
     if (local_root == NULL) {
         crm_warn("Cluster configuration file %s is corrupt (unparseable as XML)", filename);
         return -pcmk_err_cib_corrupt;
     }
 
     /* If sigfile is not specified, use original file name plus .sig */
     if (sigfile == NULL) {
         sigfile = local_sigfile = crm_strdup_printf("%s.sig", filename);
     }
 
     /* Verify that digests match */
     if (cib_file_verify_digest(local_root, sigfile) == FALSE) {
         free(local_sigfile);
         free_xml(local_root);
         return -pcmk_err_cib_modified;
     }
 
     free(local_sigfile);
     if (root) {
         *root = local_root;
     } else {
         free_xml(local_root);
     }
     return pcmk_ok;
 }
 
 #define CIB_SERIES "cib"
 #define CIB_SERIES_MAX 100
 #define CIB_SERIES_BZIP FALSE /* Must be false because archived copies are
                                  created with hard links
                                */
 
 #define CIB_LIVE_NAME CIB_SERIES ".xml"
 
 /*!
  * \internal
  * \brief Check whether a file is the live CIB
  *
  * \param[in] filename Name of file to check
  *
  * \return TRUE if file exists and its real path is same as live CIB's
  */
 static gboolean
 cib_file_is_live(const char *filename)
 {
     gboolean same = FALSE;
 
     if (filename != NULL) {
         // Canonicalize file names for true comparison
         char *real_filename = NULL;
 
         if (pcmk__real_path(filename, &real_filename) == pcmk_rc_ok) {
             char *real_livename = NULL;
 
             if (pcmk__real_path(CRM_CONFIG_DIR "/" CIB_LIVE_NAME,
                                 &real_livename) == pcmk_rc_ok) {
                 same = !strcmp(real_filename, real_livename);
                 free(real_livename);
             }
             free(real_filename);
         }
     }
     return same;
 }
 
 /* cib_file_backup() and cib_file_write_with_digest() need to chown the
  * written files only in limited circumstances, so these variables allow
  * that to be indicated without affecting external callers
  */
 static uid_t cib_file_owner = 0;
 static uid_t cib_file_group = 0;
 static gboolean cib_do_chown = FALSE;
 
 /*!
  * \internal
  * \brief Back up a CIB
  *
  * \param[in] cib_dirname Directory containing CIB file and backups
  * \param[in] cib_filename Name (relative to cib_dirname) of CIB file to back up
  *
  * \return 0 on success, -1 on error
  */
 static int
 cib_file_backup(const char *cib_dirname, const char *cib_filename)
 {
     int rc = 0;
     unsigned int seq;
     char *cib_path = crm_strdup_printf("%s/%s", cib_dirname, cib_filename);
     char *cib_digest = crm_strdup_printf("%s.sig", cib_path);
     char *backup_path;
     char *backup_digest;
 
     // Determine backup and digest file names
     if (pcmk__read_series_sequence(cib_dirname, CIB_SERIES,
                                    &seq) != pcmk_rc_ok) {
         // @TODO maybe handle errors better ...
         seq = 0;
     }
     backup_path = pcmk__series_filename(cib_dirname, CIB_SERIES, seq,
                                         CIB_SERIES_BZIP);
     backup_digest = crm_strdup_printf("%s.sig", backup_path);
 
     /* Remove the old backups if they exist */
     unlink(backup_path);
     unlink(backup_digest);
 
     /* Back up the CIB, by hard-linking it to the backup name */
     if ((link(cib_path, backup_path) < 0) && (errno != ENOENT)) {
         crm_perror(LOG_ERR, "Could not archive %s by linking to %s",
                    cib_path, backup_path);
         rc = -1;
 
     /* Back up the CIB signature similarly */
     } else if ((link(cib_digest, backup_digest) < 0) && (errno != ENOENT)) {
         crm_perror(LOG_ERR, "Could not archive %s by linking to %s",
                    cib_digest, backup_digest);
         rc = -1;
 
     /* Update the last counter and ensure everything is sync'd to media */
     } else {
         pcmk__write_series_sequence(cib_dirname, CIB_SERIES, ++seq,
                                     CIB_SERIES_MAX);
         if (cib_do_chown) {
             int rc2;
 
             if ((chown(backup_path, cib_file_owner, cib_file_group) < 0)
                     && (errno != ENOENT)) {
                 crm_perror(LOG_ERR, "Could not set owner of %s", backup_path);
                 rc = -1;
             }
             if ((chown(backup_digest, cib_file_owner, cib_file_group) < 0)
                     && (errno != ENOENT)) {
                 crm_perror(LOG_ERR, "Could not set owner of %s", backup_digest);
                 rc = -1;
             }
             rc2 = pcmk__chown_series_sequence(cib_dirname, CIB_SERIES,
                                               cib_file_owner, cib_file_group);
             if (rc2 != pcmk_rc_ok) {
                 crm_err("Could not set owner of sequence file in %s: %s",
                         cib_dirname, pcmk_rc_str(rc2));
                 rc = -1;
             }
         }
         pcmk__sync_directory(cib_dirname);
         crm_info("Archived previous version as %s", backup_path);
     }
 
     free(cib_path);
     free(cib_digest);
     free(backup_path);
     free(backup_digest);
     return rc;
 }
 
 /*!
  * \internal
  * \brief Prepare CIB XML to be written to disk
  *
  * Set num_updates to 0, set cib-last-written to the current timestamp,
  * and strip out the status section.
  *
  * \param[in,out] root  Root of CIB XML tree
  *
  * \return void
  */
 static void
 cib_file_prepare_xml(xmlNode *root)
 {
     xmlNode *cib_status_root = NULL;
 
     /* Always write out with num_updates=0 and current last-written timestamp */
     crm_xml_add(root, XML_ATTR_NUMUPDATES, "0");
     pcmk__xe_add_last_written(root);
 
     /* Delete status section before writing to file, because
      * we discard it on startup anyway, and users get confused by it */
     cib_status_root = find_xml_node(root, XML_CIB_TAG_STATUS, TRUE);
     CRM_LOG_ASSERT(cib_status_root != NULL);
     if (cib_status_root != NULL) {
         free_xml(cib_status_root);
     }
 }
 
 /*!
  * \internal
  * \brief Write CIB to disk, along with a signature file containing its digest
  *
  * \param[in,out] cib_root      Root of XML tree to write
  * \param[in]     cib_dirname   Directory containing CIB and signature files
  * \param[in]     cib_filename  Name (relative to cib_dirname) of file to write
  *
  * \return pcmk_ok on success,
  *         pcmk_err_cib_modified if existing cib_filename doesn't match digest,
  *         pcmk_err_cib_backup if existing cib_filename couldn't be backed up,
  *         or pcmk_err_cib_save if new cib_filename couldn't be saved
  */
 int
 cib_file_write_with_digest(xmlNode *cib_root, const char *cib_dirname,
                            const char *cib_filename)
 {
     int exit_rc = pcmk_ok;
     int rc, fd;
     char *digest = NULL;
 
     /* Detect CIB version for diagnostic purposes */
     const char *epoch = crm_element_value(cib_root, XML_ATTR_GENERATION);
     const char *admin_epoch = crm_element_value(cib_root,
                                                 XML_ATTR_GENERATION_ADMIN);
 
     /* Determine full CIB and signature pathnames */
     char *cib_path = crm_strdup_printf("%s/%s", cib_dirname, cib_filename);
     char *digest_path = crm_strdup_printf("%s.sig", cib_path);
 
     /* Create temporary file name patterns for writing out CIB and signature */
     char *tmp_cib = crm_strdup_printf("%s/cib.XXXXXX", cib_dirname);
     char *tmp_digest = crm_strdup_printf("%s/cib.XXXXXX", cib_dirname);
 
     CRM_ASSERT((cib_path != NULL) && (digest_path != NULL)
                && (tmp_cib != NULL) && (tmp_digest != NULL));
 
     /* Ensure the admin didn't modify the existing CIB underneath us */
     crm_trace("Reading cluster configuration file %s", cib_path);
     rc = cib_file_read_and_verify(cib_path, NULL, NULL);
     if ((rc != pcmk_ok) && (rc != -ENOENT)) {
         crm_err("%s was manually modified while the cluster was active!",
                 cib_path);
         exit_rc = pcmk_err_cib_modified;
         goto cleanup;
     }
 
     /* Back up the existing CIB */
     if (cib_file_backup(cib_dirname, cib_filename) < 0) {
         exit_rc = pcmk_err_cib_backup;
         goto cleanup;
     }
 
     crm_debug("Writing CIB to disk");
     umask(S_IWGRP | S_IWOTH | S_IROTH);
     cib_file_prepare_xml(cib_root);
 
     /* Write the CIB to a temporary file, so we can deploy (near) atomically */
     fd = mkstemp(tmp_cib);
     if (fd < 0) {
         crm_perror(LOG_ERR, "Couldn't open temporary file %s for writing CIB",
                    tmp_cib);
         exit_rc = pcmk_err_cib_save;
         goto cleanup;
     }
 
     /* Protect the temporary file */
     if (fchmod(fd, S_IRUSR | S_IWUSR) < 0) {
         crm_perror(LOG_ERR, "Couldn't protect temporary file %s for writing CIB",
                    tmp_cib);
         exit_rc = pcmk_err_cib_save;
         goto cleanup;
     }
     if (cib_do_chown && (fchown(fd, cib_file_owner, cib_file_group) < 0)) {
         crm_perror(LOG_ERR, "Couldn't protect temporary file %s for writing CIB",
                    tmp_cib);
         exit_rc = pcmk_err_cib_save;
         goto cleanup;
     }
 
     /* Write out the CIB */
     if (write_xml_fd(cib_root, tmp_cib, fd, FALSE) <= 0) {
         crm_err("Changes couldn't be written to %s", tmp_cib);
         exit_rc = pcmk_err_cib_save;
         goto cleanup;
     }
 
     /* Calculate CIB digest */
     digest = calculate_on_disk_digest(cib_root);
     CRM_ASSERT(digest != NULL);
     crm_info("Wrote version %s.%s.0 of the CIB to disk (digest: %s)",
              (admin_epoch ? admin_epoch : "0"), (epoch ? epoch : "0"), digest);
 
     /* Write the CIB digest to a temporary file */
     fd = mkstemp(tmp_digest);
     if (fd < 0) {
         crm_perror(LOG_ERR, "Could not create temporary file for CIB digest");
         exit_rc = pcmk_err_cib_save;
         goto cleanup;
     }
     if (cib_do_chown && (fchown(fd, cib_file_owner, cib_file_group) < 0)) {
         crm_perror(LOG_ERR, "Couldn't protect temporary file %s for writing CIB",
                    tmp_cib);
         exit_rc = pcmk_err_cib_save;
         close(fd);
         goto cleanup;
     }
     rc = pcmk__write_sync(fd, digest);
     if (rc != pcmk_rc_ok) {
         crm_err("Could not write digest to %s: %s",
                 tmp_digest, pcmk_rc_str(rc));
         exit_rc = pcmk_err_cib_save;
         close(fd);
         goto cleanup;
     }
     close(fd);
     crm_debug("Wrote digest %s to disk", digest);
 
     /* Verify that what we wrote is sane */
     crm_info("Reading cluster configuration file %s (digest: %s)",
              tmp_cib, tmp_digest);
     rc = cib_file_read_and_verify(tmp_cib, tmp_digest, NULL);
     CRM_ASSERT(rc == 0);
 
     /* Rename temporary files to live, and sync directory changes to media */
     crm_debug("Activating %s", tmp_cib);
     if (rename(tmp_cib, cib_path) < 0) {
         crm_perror(LOG_ERR, "Couldn't rename %s as %s", tmp_cib, cib_path);
         exit_rc = pcmk_err_cib_save;
     }
     if (rename(tmp_digest, digest_path) < 0) {
         crm_perror(LOG_ERR, "Couldn't rename %s as %s", tmp_digest,
                    digest_path);
         exit_rc = pcmk_err_cib_save;
     }
     pcmk__sync_directory(cib_dirname);
 
   cleanup:
     free(cib_path);
     free(digest_path);
     free(digest);
     free(tmp_digest);
     free(tmp_cib);
     return exit_rc;
 }
 
 cib_t *
 cib_file_new(const char *cib_location)
 {
     cib_file_opaque_t *private = NULL;
     cib_t *cib = cib_new_variant();
 
     if (cib == NULL) {
         return NULL;
     }
 
     private = calloc(1, sizeof(cib_file_opaque_t));
 
     if (private == NULL) {
         free(cib);
         return NULL;
     }
 
     cib->variant = cib_file;
     cib->variant_opaque = private;
 
     if (cib_location == NULL) {
         cib_location = getenv("CIB_file");
         CRM_CHECK(cib_location != NULL, return NULL); // Shouldn't be possible
     }
     private->flags = 0;
     if (cib_file_is_live(cib_location)) {
         cib_set_file_flags(private, cib_file_flag_live);
         crm_trace("File %s detected as live CIB", cib_location);
     }
     private->filename = strdup(cib_location);
 
     /* assign variant specific ops */
     cib->delegate_fn = cib_file_perform_op_delegate;
     cib->cmds->signon = cib_file_signon;
     cib->cmds->signoff = cib_file_signoff;
     cib->cmds->free = cib_file_free;
     cib->cmds->inputfd = cib_file_inputfd;
 
     cib->cmds->register_notification = cib_file_register_notification;
     cib->cmds->set_connection_dnotify = cib_file_set_connection_dnotify;
 
     return cib;
 }
 
 static xmlNode *in_mem_cib = NULL;
 
 /*!
  * \internal
  * \brief Read CIB from disk and validate it against XML schema
  *
  * \param[in] filename Name of file to read CIB from
  *
  * \return pcmk_ok on success,
  *         -ENXIO if file does not exist (or stat() otherwise fails), or
  *         -pcmk_err_schema_validation if XML doesn't parse or validate
  * \note If filename is the live CIB, this will *not* verify its digest,
  *       though that functionality would be trivial to add here.
  *       Also, this will *not* verify that the file is writable,
  *       because some callers might not need to write.
  */
 static int
 load_file_cib(const char *filename)
 {
     struct stat buf;
     xmlNode *root = NULL;
 
     /* Ensure file is readable */
     if (strcmp(filename, "-") && (stat(filename, &buf) < 0)) {
         return -ENXIO;
     }
 
     /* Parse XML from file */
     root = filename2xml(filename);
     if (root == NULL) {
         return -pcmk_err_schema_validation;
     }
 
     /* Add a status section if not already present */
     if (find_xml_node(root, XML_CIB_TAG_STATUS, FALSE) == NULL) {
         create_xml_node(root, XML_CIB_TAG_STATUS);
     }
 
     /* Validate XML against its specified schema */
     if (validate_xml(root, NULL, TRUE) == FALSE) {
         const char *schema = crm_element_value(root, XML_ATTR_VALIDATION);
 
         crm_err("CIB does not validate against %s", schema);
         free_xml(root);
         return -pcmk_err_schema_validation;
     }
 
     /* Remember the parsed XML for later use */
     in_mem_cib = root;
     return pcmk_ok;
 }
 
 int
 cib_file_signon(cib_t * cib, const char *name, enum cib_conn_type type)
 {
     int rc = pcmk_ok;
     cib_file_opaque_t *private = cib->variant_opaque;
 
     if (private->filename == NULL) {
         rc = -EINVAL;
     } else {
         rc = load_file_cib(private->filename);
     }
 
     if (rc == pcmk_ok) {
         crm_debug("Opened connection to local file '%s' for %s",
                   private->filename, name);
         cib->state = cib_connected_command;
         cib->type = cib_command;
 
     } else {
         crm_info("Connection to local file '%s' for %s failed: %s\n",
                  private->filename, name, pcmk_strerror(rc));
     }
     return rc;
 }
 
 /*!
  * \internal
  * \brief Write out the in-memory CIB to a live CIB file
  *
  * param[in,out] path  Full path to file to write
  *
  * \return 0 on success, -1 on failure
  */
 static int
 cib_file_write_live(char *path)
 {
     uid_t uid = geteuid();
     struct passwd *daemon_pwent;
     char *sep = strrchr(path, '/');
     const char *cib_dirname, *cib_filename;
     int rc = 0;
 
     /* Get the desired uid/gid */
     errno = 0;
     daemon_pwent = getpwnam(CRM_DAEMON_USER);
     if (daemon_pwent == NULL) {
         crm_perror(LOG_ERR, "Could not find %s user", CRM_DAEMON_USER);
         return -1;
     }
 
     /* If we're root, we can change the ownership;
      * if we're daemon, anything we create will be OK;
      * otherwise, block access so we don't create wrong owner
      */
     if ((uid != 0) && (uid != daemon_pwent->pw_uid)) {
         crm_perror(LOG_ERR, "Must be root or %s to modify live CIB",
                    CRM_DAEMON_USER);
         return 0;
     }
 
     /* fancy footwork to separate dirname from filename
      * (we know the canonical name maps to the live CIB,
      * but the given name might be relative, or symlinked)
      */
     if (sep == NULL) { /* no directory component specified */
         cib_dirname = "./";
         cib_filename = path;
     } else if (sep == path) { /* given name is in / */
         cib_dirname = "/";
         cib_filename = path + 1;
     } else { /* typical case; split given name into parts */
         *sep = '\0';
         cib_dirname = path;
         cib_filename = sep + 1;
     }
 
     /* if we're root, we want to update the file ownership */
     if (uid == 0) {
         cib_file_owner = daemon_pwent->pw_uid;
         cib_file_group = daemon_pwent->pw_gid;
         cib_do_chown = TRUE;
     }
 
     /* write the file */
     if (cib_file_write_with_digest(in_mem_cib, cib_dirname,
                                    cib_filename) != pcmk_ok) {
         rc = -1;
     }
 
     /* turn off file ownership changes, for other callers */
     if (uid == 0) {
         cib_do_chown = FALSE;
     }
 
     /* undo fancy stuff */
     if ((sep != NULL) && (*sep == '\0')) {
         *sep = '/';
     }
 
     return rc;
 }
 
 /*!
  * \internal
  * \brief Sign-off method for CIB file variants
  *
  * This will write the file to disk if needed, and free the in-memory CIB. If
  * the file is the live CIB, it will compute and write a signature as well.
  *
  * \param[in,out] cib  CIB object to sign off
  *
  * \return pcmk_ok on success, pcmk_err_generic on failure
  * \todo This method should refuse to write the live CIB if the CIB manager is
  *       running.
  */
 int
 cib_file_signoff(cib_t * cib)
 {
     int rc = pcmk_ok;
     cib_file_opaque_t *private = cib->variant_opaque;
 
     crm_debug("Disconnecting from the CIB manager");
     cib->state = cib_disconnected;
     cib->type = cib_no_connection;
 
     /* If the in-memory CIB has been changed, write it to disk */
     if (pcmk_is_set(private->flags, cib_file_flag_dirty)) {
 
         /* If this is the live CIB, write it out with a digest */
         if (pcmk_is_set(private->flags, cib_file_flag_live)) {
             if (cib_file_write_live(private->filename) < 0) {
                 rc = pcmk_err_generic;
             }
 
         /* Otherwise, it's a simple write */
         } else {
             gboolean do_bzip = pcmk__ends_with_ext(private->filename, ".bz2");
 
             if (write_xml_file(in_mem_cib, private->filename, do_bzip) <= 0) {
                 rc = pcmk_err_generic;
             }
         }
 
         if (rc == pcmk_ok) {
             crm_info("Wrote CIB to %s", private->filename);
             cib_clear_file_flags(private, cib_file_flag_dirty);
         } else {
             crm_err("Could not write CIB to %s", private->filename);
         }
     }
 
     /* Free the in-memory CIB */
     free_xml(in_mem_cib);
     in_mem_cib = NULL;
     return rc;
 }
 
 int
 cib_file_free(cib_t * cib)
 {
     int rc = pcmk_ok;
 
     if (cib->state != cib_disconnected) {
         rc = cib_file_signoff(cib);
     }
 
     if (rc == pcmk_ok) {
         cib_file_opaque_t *private = cib->variant_opaque;
 
         free(private->filename);
         free(cib->cmds);
         free(private);
         free(cib);
 
     } else {
         fprintf(stderr, "Couldn't sign off: %d\n", rc);
     }
 
     return rc;
 }
 
 struct cib_func_entry {
     const char *op;
     gboolean read_only;
     cib_op_t fn;
 };
 
 /* *INDENT-OFF* */
 static struct cib_func_entry cib_file_ops[] = {
     { PCMK__CIB_REQUEST_QUERY,      TRUE,   cib_process_query},
     { PCMK__CIB_REQUEST_MODIFY,     FALSE,  cib_process_modify},
     { PCMK__CIB_REQUEST_APPLY_PATCH,FALSE,  cib_process_diff},
     { PCMK__CIB_REQUEST_BUMP,       FALSE,  cib_process_bump },
     { PCMK__CIB_REQUEST_REPLACE,    FALSE,  cib_process_replace},
     { PCMK__CIB_REQUEST_CREATE,     FALSE,  cib_process_create },
     { PCMK__CIB_REQUEST_DELETE,     FALSE,  cib_process_delete},
     { PCMK__CIB_REQUEST_ERASE,      FALSE,  cib_process_erase},
     { PCMK__CIB_REQUEST_UPGRADE,    FALSE,  cib_process_upgrade},
 };
 /* *INDENT-ON* */
 
 int
 cib_file_perform_op(cib_t * cib, const char *op, const char *host, const char *section,
                     xmlNode * data, xmlNode ** output_data, int call_options)
 {
     return cib_file_perform_op_delegate(cib, op, host, section, data, output_data, call_options,
                                         NULL);
 }
 
 int
 cib_file_perform_op_delegate(cib_t * cib, const char *op, const char *host, const char *section,
                              xmlNode * data, xmlNode ** output_data, int call_options,
                              const char *user_name)
 {
     int rc = pcmk_ok;
     char *effective_user = NULL;
     gboolean query = FALSE;
     gboolean changed = FALSE;
     xmlNode *request = NULL;
     xmlNode *output = NULL;
     xmlNode *cib_diff = NULL;
     xmlNode *result_cib = NULL;
     cib_op_t *fn = NULL;
     int lpc = 0;
     static int max_msg_types = PCMK__NELEM(cib_file_ops);
     cib_file_opaque_t *private = cib->variant_opaque;
 
     crm_info("Handling %s operation for %s as %s",
              (op? op : "invalid"), (section? section : "entire CIB"),
              (user_name? user_name : "default user"));
 
     cib__set_call_options(call_options, "file operation",
                           cib_no_mtime|cib_inhibit_bcast|cib_scope_local);
 
     if (cib->state == cib_disconnected) {
         return -ENOTCONN;
     }
 
     if (output_data != NULL) {
         *output_data = NULL;
     }
 
     if (op == NULL) {
         return -EINVAL;
     }
 
     for (lpc = 0; lpc < max_msg_types; lpc++) {
         if (pcmk__str_eq(op, cib_file_ops[lpc].op, pcmk__str_casei)) {
             fn = &(cib_file_ops[lpc].fn);
             query = cib_file_ops[lpc].read_only;
             break;
         }
     }
 
     if (fn == NULL) {
         return -EPROTONOSUPPORT;
     }
 
     cib->call_id++;
     request = cib_create_op(cib->call_id, "dummy-token", op, host, section, data, call_options, user_name);
     if(user_name) {
         crm_xml_add(request, XML_ACL_TAG_USER, user_name);
     }
 
     /* Mirror the logic in cib_prepare_common() */
     if (section != NULL && data != NULL && pcmk__str_eq(crm_element_name(data), XML_TAG_CIB, pcmk__str_none)) {
         data = pcmk_find_cib_element(data, section);
     }
 
     rc = cib_perform_op(op, call_options, fn, query,
                         section, request, data, TRUE, &changed, in_mem_cib, &result_cib, &cib_diff,
                         &output);
 
     free_xml(request);
     if (rc == -pcmk_err_schema_validation) {
         validate_xml_verbose(result_cib);
     }
 
     if (rc != pcmk_ok) {
         free_xml(result_cib);
 
     } else if (query == FALSE) {
-        pcmk__xml_log_patchset(LOG_DEBUG, cib_diff);
+        pcmk__output_t *out = NULL;
+
+        rc = pcmk_rc2legacy(pcmk__log_output_new(&out));
+        CRM_CHECK(rc == pcmk_ok, goto done);
+
+        pcmk__output_set_log_level(out, LOG_DEBUG);
+        rc = out->message(out, "xml-patchset", cib_diff);
+        out->finish(out, pcmk_rc2exitc(rc), true, NULL);
+        pcmk__output_free(out);
+        rc = pcmk_ok;
+
         free_xml(in_mem_cib);
         in_mem_cib = result_cib;
         cib_set_file_flags(private, cib_file_flag_dirty);
     }
 
-    free_xml(cib_diff);
-
     if (cib->op_callback != NULL) {
         cib->op_callback(NULL, cib->call_id, rc, output);
     }
 
-    if (output_data && output) {
-        if(output == in_mem_cib) {
-            *output_data = copy_xml(output);
-        } else {
-            *output_data = output;
-        }
+    if ((output_data != NULL) && (output != NULL)) {
+        *output_data = (output == in_mem_cib)? copy_xml(output) : output;
+    }
 
-    } else if(output != in_mem_cib) {
+done:
+    free_xml(cib_diff);
+
+    if ((output_data == NULL) && (output != in_mem_cib)) {
+        /* Don't free output if we're still using it. (output_data != NULL)
+         * means we may have assigned *output_data = output above.
+         */
         free_xml(output);
     }
-
     free(effective_user);
     return rc;
 }
diff --git a/lib/cib/cib_utils.c b/lib/cib/cib_utils.c
index 7bd5b46bc8..5f4bd93923 100644
--- a/lib/cib/cib_utils.c
+++ b/lib/cib/cib_utils.c
@@ -1,837 +1,859 @@
 /*
  * Original copyright 2004 International Business Machines
  * Later changes copyright 2008-2023 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 <unistd.h>
 #include <stdlib.h>
 #include <stdio.h>
 #include <stdarg.h>
 #include <string.h>
 #include <sys/utsname.h>
 
 #include <glib.h>
 
 #include <crm/crm.h>
 #include <crm/cib/internal.h>
 #include <crm/msg_xml.h>
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>
 #include <crm/pengine/rules.h>
 
 xmlNode *
 cib_get_generation(cib_t * cib)
 {
     xmlNode *the_cib = NULL;
     xmlNode *generation = create_xml_node(NULL, XML_CIB_TAG_GENERATION_TUPPLE);
 
     cib->cmds->query(cib, NULL, &the_cib, cib_scope_local | cib_sync_call);
     if (the_cib != NULL) {
         copy_in_properties(generation, the_cib);
         free_xml(the_cib);
     }
 
     return generation;
 }
 
 gboolean
 cib_version_details(xmlNode * cib, int *admin_epoch, int *epoch, int *updates)
 {
     *epoch = -1;
     *updates = -1;
     *admin_epoch = -1;
 
     if (cib == NULL) {
         return FALSE;
 
     } else {
         crm_element_value_int(cib, XML_ATTR_GENERATION, epoch);
         crm_element_value_int(cib, XML_ATTR_NUMUPDATES, updates);
         crm_element_value_int(cib, XML_ATTR_GENERATION_ADMIN, admin_epoch);
     }
     return TRUE;
 }
 
 gboolean
 cib_diff_version_details(xmlNode * diff, int *admin_epoch, int *epoch, int *updates,
                          int *_admin_epoch, int *_epoch, int *_updates)
 {
     int add[] = { 0, 0, 0 };
     int del[] = { 0, 0, 0 };
 
     xml_patch_versions(diff, add, del);
 
     *admin_epoch = add[0];
     *epoch = add[1];
     *updates = add[2];
 
     *_admin_epoch = del[0];
     *_epoch = del[1];
     *_updates = del[2];
 
     return TRUE;
 }
 
 /*!
  * \brief Create XML for a new (empty) CIB
  *
  * \param[in] cib_epoch   What to use as "epoch" CIB property
  *
  * \return Newly created XML for empty CIB
  * \note It is the caller's responsibility to free the result with free_xml().
  */
 xmlNode *
 createEmptyCib(int cib_epoch)
 {
     xmlNode *cib_root = NULL, *config = NULL;
 
     cib_root = create_xml_node(NULL, XML_TAG_CIB);
     crm_xml_add(cib_root, XML_ATTR_CRM_VERSION, CRM_FEATURE_SET);
     crm_xml_add(cib_root, XML_ATTR_VALIDATION, xml_latest_schema());
 
     crm_xml_add_int(cib_root, XML_ATTR_GENERATION, cib_epoch);
     crm_xml_add_int(cib_root, XML_ATTR_NUMUPDATES, 0);
     crm_xml_add_int(cib_root, XML_ATTR_GENERATION_ADMIN, 0);
 
     config = create_xml_node(cib_root, XML_CIB_TAG_CONFIGURATION);
     create_xml_node(cib_root, XML_CIB_TAG_STATUS);
 
     create_xml_node(config, XML_CIB_TAG_CRMCONFIG);
     create_xml_node(config, XML_CIB_TAG_NODES);
     create_xml_node(config, XML_CIB_TAG_RESOURCES);
     create_xml_node(config, XML_CIB_TAG_CONSTRAINTS);
 
 #if PCMK__RESOURCE_STICKINESS_DEFAULT != 0
     {
         xmlNode *rsc_defaults = create_xml_node(config, XML_CIB_TAG_RSCCONFIG);
         xmlNode *meta = create_xml_node(rsc_defaults, XML_TAG_META_SETS);
         xmlNode *nvpair = create_xml_node(meta, XML_CIB_TAG_NVPAIR);
 
         crm_xml_add(meta, XML_ATTR_ID, "build-resource-defaults");
         crm_xml_add(nvpair, XML_ATTR_ID, "build-" XML_RSC_ATTR_STICKINESS);
         crm_xml_add(nvpair, XML_NVPAIR_ATTR_NAME, XML_RSC_ATTR_STICKINESS);
         crm_xml_add_int(nvpair, XML_NVPAIR_ATTR_VALUE,
                         PCMK__RESOURCE_STICKINESS_DEFAULT);
     }
 #endif
     return cib_root;
 }
 
 static bool
 cib_acl_enabled(xmlNode *xml, const char *user)
 {
     bool rc = FALSE;
 
     if(pcmk_acl_required(user)) {
         const char *value = NULL;
         GHashTable *options = pcmk__strkey_table(free, free);
 
         cib_read_config(options, xml);
         value = cib_pref(options, "enable-acl");
         rc = crm_is_true(value);
         g_hash_table_destroy(options);
     }
 
     crm_trace("CIB ACL is %s", rc ? "enabled" : "disabled");
     return rc;
 }
 
 int
 cib_perform_op(const char *op, int call_options, cib_op_t * fn, gboolean is_query,
                const char *section, xmlNode * req, xmlNode * input,
                gboolean manage_counters, gboolean * config_changed,
                xmlNode * current_cib, xmlNode ** result_cib, xmlNode ** diff, xmlNode ** output)
 {
     int rc = pcmk_ok;
     gboolean check_schema = TRUE;
     xmlNode *top = NULL;
     xmlNode *scratch = NULL;
     xmlNode *local_diff = NULL;
 
     const char *new_version = NULL;
     const char *user = crm_element_value(req, F_CIB_USER);
     bool with_digest = FALSE;
 
+    pcmk__output_t *out = NULL;
+    int out_rc = pcmk_rc_no_output;
+
     crm_trace("Begin %s%s%s op",
               (pcmk_is_set(call_options, cib_dryrun)? "dry run of " : ""),
               (is_query? "read-only " : ""), op);
 
     CRM_CHECK(output != NULL, return -ENOMSG);
     CRM_CHECK(result_cib != NULL, return -ENOMSG);
     CRM_CHECK(config_changed != NULL, return -ENOMSG);
 
     if(output) {
         *output = NULL;
     }
 
     *result_cib = NULL;
     *config_changed = FALSE;
 
     if (fn == NULL) {
         return -EINVAL;
     }
 
     if (is_query) {
         xmlNode *cib_ro = current_cib;
         xmlNode *cib_filtered = NULL;
 
         if(cib_acl_enabled(cib_ro, user)) {
             if(xml_acl_filtered_copy(user, current_cib, current_cib, &cib_filtered)) {
                 if (cib_filtered == NULL) {
                     crm_debug("Pre-filtered the entire cib");
                     return -EACCES;
                 }
                 cib_ro = cib_filtered;
                 crm_log_xml_trace(cib_ro, "filtered");
             }
         }
 
         rc = (*fn) (op, call_options, section, req, input, cib_ro, result_cib, output);
 
         if(output == NULL || *output == NULL) {
             /* nothing */
 
         } else if(cib_filtered == *output) {
             cib_filtered = NULL; /* Let them have this copy */
 
         } else if(*output == current_cib) {
             /* They already know not to free it */
 
         } else if(cib_filtered && (*output)->doc == cib_filtered->doc) {
             /* We're about to free the document of which *output is a part */
             *output = copy_xml(*output);
 
         } else if((*output)->doc == current_cib->doc) {
             /* Give them a copy they can free */
             *output = copy_xml(*output);
         }
 
         free_xml(cib_filtered);
         return rc;
     }
 
 
     if (pcmk_is_set(call_options, cib_zero_copy)) {
         /* Conditional on v2 patch style */
 
         scratch = current_cib;
 
         /* Create a shallow copy of current_cib for the version details */
         current_cib = create_xml_node(NULL, (const char *)scratch->name);
         copy_in_properties(current_cib, scratch);
         top = current_cib;
 
         xml_track_changes(scratch, user, NULL, cib_acl_enabled(scratch, user));
         rc = (*fn) (op, call_options, section, req, input, scratch, &scratch, output);
 
     } else {
         scratch = copy_xml(current_cib);
         xml_track_changes(scratch, user, NULL, cib_acl_enabled(scratch, user));
         rc = (*fn) (op, call_options, section, req, input, current_cib, &scratch, output);
 
         if(scratch && xml_tracking_changes(scratch) == FALSE) {
             crm_trace("Inferring changes after %s op", op);
             xml_track_changes(scratch, user, current_cib, cib_acl_enabled(current_cib, user));
             xml_calculate_changes(current_cib, scratch);
         }
         CRM_CHECK(current_cib != scratch, return -EINVAL);
     }
 
     xml_acl_disable(scratch); /* Allow the system to make any additional changes */
 
     if (rc == pcmk_ok && scratch == NULL) {
         rc = -EINVAL;
         goto done;
 
     } else if(rc == pcmk_ok && xml_acl_denied(scratch)) {
         crm_trace("ACL rejected part or all of the proposed changes");
         rc = -EACCES;
         goto done;
 
     } else if (rc != pcmk_ok) {
         goto done;
     }
 
     if (scratch) {
         new_version = crm_element_value(scratch, XML_ATTR_CRM_VERSION);
 
         if (new_version && compare_version(new_version, CRM_FEATURE_SET) > 0) {
             crm_err("Discarding update with feature set '%s' greater than our own '%s'",
                     new_version, CRM_FEATURE_SET);
             rc = -EPROTONOSUPPORT;
             goto done;
         }
     }
 
     if (current_cib) {
         int old = 0;
         int new = 0;
 
         crm_element_value_int(scratch, XML_ATTR_GENERATION_ADMIN, &new);
         crm_element_value_int(current_cib, XML_ATTR_GENERATION_ADMIN, &old);
 
         if (old > new) {
             crm_err("%s went backwards: %d -> %d (Opts: %#x)",
                     XML_ATTR_GENERATION_ADMIN, old, new, call_options);
             crm_log_xml_warn(req, "Bad Op");
             crm_log_xml_warn(input, "Bad Data");
             rc = -pcmk_err_old_data;
 
         } else if (old == new) {
             crm_element_value_int(scratch, XML_ATTR_GENERATION, &new);
             crm_element_value_int(current_cib, XML_ATTR_GENERATION, &old);
             if (old > new) {
                 crm_err("%s went backwards: %d -> %d (Opts: %#x)",
                         XML_ATTR_GENERATION, old, new, call_options);
                 crm_log_xml_warn(req, "Bad Op");
                 crm_log_xml_warn(input, "Bad Data");
                 rc = -pcmk_err_old_data;
             }
         }
     }
 
     crm_trace("Massaging CIB contents");
     pcmk__strip_xml_text(scratch);
     fix_plus_plus_recursive(scratch);
 
     if (pcmk_is_set(call_options, cib_zero_copy)) {
         /* At this point, current_cib is just the 'cib' tag and its properties,
          *
          * The v1 format would barf on this, but we know the v2 patch
          * format only needs it for the top-level version fields
          */
         local_diff = xml_create_patchset(2, current_cib, scratch, (bool*)config_changed, manage_counters);
 
     } else {
         static time_t expires = 0;
         time_t tm_now = time(NULL);
 
         if (expires < tm_now) {
             expires = tm_now + 60;  /* Validate clients are correctly applying v2-style diffs at most once a minute */
             with_digest = TRUE;
         }
 
         local_diff = xml_create_patchset(0, current_cib, scratch, (bool*)config_changed, manage_counters);
     }
 
     // Create a log output object only if we're going to use it
     pcmk__if_tracing(
         {
-            pcmk__output_t *out = NULL;
             rc = pcmk_rc2legacy(pcmk__log_output_new(&out));
-
             CRM_CHECK(rc == pcmk_ok, goto done);
 
             pcmk__output_set_log_level(out, LOG_TRACE);
-            rc = pcmk__xml_show_changes(out, scratch);
-            out->finish(out, pcmk_rc2exitc(rc), true, NULL);
-            pcmk__output_free(out);
-            rc = pcmk_ok;
+            out_rc = pcmk__xml_show_changes(out, scratch);
         },
         {}
     );
     xml_accept_changes(scratch);
 
     if(local_diff) {
+        int temp_rc = pcmk_rc_no_output;
+
         patchset_process_digest(local_diff, current_cib, scratch, with_digest);
 
-        pcmk__xml_log_patchset(LOG_INFO, local_diff);
+        if (out == NULL) {
+            rc = pcmk_rc2legacy(pcmk__log_output_new(&out));
+            CRM_CHECK(rc == pcmk_ok, goto done);
+        }
+        pcmk__output_set_log_level(out, LOG_INFO);
+        temp_rc = out->message(out, "xml-patchset", local_diff);
+        out_rc = pcmk__output_select_rc(rc, temp_rc);
+
         crm_log_xml_trace(local_diff, "raw patch");
     }
 
+    if (out != NULL) {
+        out->finish(out, pcmk_rc2exitc(out_rc), true, NULL);
+        pcmk__output_free(out);
+        out = NULL;
+    }
+
     if (!pcmk_is_set(call_options, cib_zero_copy) && (local_diff != NULL)) {
         // Original to compare against doesn't exist
         pcmk__if_tracing(
             {
                 // Validate the calculated patch set
                 int test_rc = pcmk_ok;
                 int format = 1;
                 xmlNode *cib_copy = copy_xml(current_cib);
 
                 crm_element_value_int(local_diff, "format", &format);
                 test_rc = xml_apply_patchset(cib_copy, local_diff,
                                              manage_counters);
 
                 if (test_rc != pcmk_ok) {
                     save_xml_to_file(cib_copy, "PatchApply:calculated", NULL);
                     save_xml_to_file(current_cib, "PatchApply:input", NULL);
                     save_xml_to_file(scratch, "PatchApply:actual", NULL);
                     save_xml_to_file(local_diff, "PatchApply:diff", NULL);
                     crm_err("v%d patchset error, patch failed to apply: %s "
                             "(%d)",
                             format, pcmk_rc_str(pcmk_legacy2rc(test_rc)),
                             test_rc);
                 }
                 free_xml(cib_copy);
             },
             {}
         );
     }
 
     if (pcmk__str_eq(section, XML_CIB_TAG_STATUS, pcmk__str_casei)) {
         /* Throttle the amount of costly validation we perform due to status updates
          * a) we don't really care whats in the status section
          * b) we don't validate any of its contents at the moment anyway
          */
         check_schema = FALSE;
     }
 
     /* === scratch must not be modified after this point ===
      * Exceptions, anything in:
 
      static filter_t filter[] = {
      { 0, XML_ATTR_ORIGIN },
      { 0, XML_CIB_ATTR_WRITTEN },
      { 0, XML_ATTR_UPDATE_ORIG },
      { 0, XML_ATTR_UPDATE_CLIENT },
      { 0, XML_ATTR_UPDATE_USER },
      };
      */
 
     if (*config_changed && !pcmk_is_set(call_options, cib_no_mtime)) {
         const char *schema = crm_element_value(scratch, XML_ATTR_VALIDATION);
 
         pcmk__xe_add_last_written(scratch);
         if (schema) {
             static int minimum_schema = 0;
             int current_schema = get_schema_version(schema);
 
             if (minimum_schema == 0) {
                 minimum_schema = get_schema_version("pacemaker-1.2");
             }
 
             /* Does the CIB support the "update-*" attributes... */
             if (current_schema >= minimum_schema) {
                 const char *origin = crm_element_value(req, F_ORIG);
 
                 CRM_LOG_ASSERT(origin != NULL);
                 crm_xml_replace(scratch, XML_ATTR_UPDATE_ORIG, origin);
                 crm_xml_replace(scratch, XML_ATTR_UPDATE_CLIENT,
                                 crm_element_value(req, F_CIB_CLIENTNAME));
                 crm_xml_replace(scratch, XML_ATTR_UPDATE_USER, crm_element_value(req, F_CIB_USER));
             }
         }
     }
 
     crm_trace("Perform validation: %s", pcmk__btoa(check_schema));
     if ((rc == pcmk_ok) && check_schema && !validate_xml(scratch, NULL, TRUE)) {
         const char *current_schema = crm_element_value(scratch,
                                                        XML_ATTR_VALIDATION);
 
         crm_warn("Updated CIB does not validate against %s schema",
                  pcmk__s(current_schema, "unspecified"));
         rc = -pcmk_err_schema_validation;
     }
 
   done:
 
     *result_cib = scratch;
     if(rc != pcmk_ok && cib_acl_enabled(current_cib, user)) {
         if(xml_acl_filtered_copy(user, current_cib, scratch, result_cib)) {
             if (*result_cib == NULL) {
                 crm_debug("Pre-filtered the entire cib result");
             }
             free_xml(scratch);
         }
     }
 
     if(diff) {
         *diff = local_diff;
     } else {
         free_xml(local_diff);
     }
 
     free_xml(top);
     crm_trace("Done");
     return rc;
 }
 
 xmlNode *
 cib_create_op(int call_id, const char *token, const char *op, const char *host, const char *section,
               xmlNode * data, int call_options, const char *user_name)
 {
     xmlNode *op_msg = create_xml_node(NULL, "cib_command");
 
     CRM_CHECK(op_msg != NULL, return NULL);
     CRM_CHECK(token != NULL, return NULL);
 
     crm_xml_add(op_msg, F_XML_TAGNAME, "cib_command");
 
     crm_xml_add(op_msg, F_TYPE, T_CIB);
     crm_xml_add(op_msg, F_CIB_CALLBACK_TOKEN, token);
     crm_xml_add(op_msg, F_CIB_OPERATION, op);
     crm_xml_add(op_msg, F_CIB_HOST, host);
     crm_xml_add(op_msg, F_CIB_SECTION, section);
     crm_xml_add_int(op_msg, F_CIB_CALLID, call_id);
     if (user_name) {
         crm_xml_add(op_msg, F_CIB_USER, user_name);
     }
     crm_trace("Sending call options: %.8lx, %d", (long)call_options, call_options);
     crm_xml_add_int(op_msg, F_CIB_CALLOPTS, call_options);
 
     if (data != NULL) {
         add_message_xml(op_msg, F_CIB_CALLDATA, data);
     }
 
     if (call_options & cib_inhibit_bcast) {
         CRM_CHECK((call_options & cib_scope_local), return NULL);
     }
     return op_msg;
 }
 
 void
 cib_native_callback(cib_t * cib, xmlNode * msg, int call_id, int rc)
 {
     xmlNode *output = NULL;
     cib_callback_client_t *blob = NULL;
 
     if (msg != NULL) {
         crm_element_value_int(msg, F_CIB_RC, &rc);
         crm_element_value_int(msg, F_CIB_CALLID, &call_id);
         output = get_message_xml(msg, F_CIB_CALLDATA);
     }
 
     blob = cib__lookup_id(call_id);
 
     if (blob == NULL) {
         crm_trace("No callback found for call %d", call_id);
     }
 
     if (cib == NULL) {
         crm_debug("No cib object supplied");
     }
 
     if (rc == -pcmk_err_diff_resync) {
         /* This is an internal value that clients do not and should not care about */
         rc = pcmk_ok;
     }
 
     if (blob && blob->callback && (rc == pcmk_ok || blob->only_success == FALSE)) {
         crm_trace("Invoking callback %s for call %d",
                   pcmk__s(blob->id, "without ID"), call_id);
         blob->callback(msg, call_id, rc, output, blob->user_data);
 
     } else if (cib && cib->op_callback == NULL && rc != pcmk_ok) {
         crm_warn("CIB command failed: %s", pcmk_strerror(rc));
         crm_log_xml_debug(msg, "Failed CIB Update");
     }
 
     /* This may free user_data, so do it after the callback */
     if (blob) {
         remove_cib_op_callback(call_id, FALSE);
     }
 
     if (cib && cib->op_callback != NULL) {
         crm_trace("Invoking global callback for call %d", call_id);
         cib->op_callback(msg, call_id, rc, output);
     }
     crm_trace("OP callback activated for %d", call_id);
 }
 
 void
 cib_native_notify(gpointer data, gpointer user_data)
 {
     xmlNode *msg = user_data;
     cib_notify_client_t *entry = data;
     const char *event = NULL;
 
     if (msg == NULL) {
         crm_warn("Skipping callback - NULL message");
         return;
     }
 
     event = crm_element_value(msg, F_SUBTYPE);
 
     if (entry == NULL) {
         crm_warn("Skipping callback - NULL callback client");
         return;
 
     } else if (entry->callback == NULL) {
         crm_warn("Skipping callback - NULL callback");
         return;
 
     } else if (!pcmk__str_eq(entry->event, event, pcmk__str_casei)) {
         crm_trace("Skipping callback - event mismatch %p/%s vs. %s", entry, entry->event, event);
         return;
     }
 
     crm_trace("Invoking callback for %p/%s event...", entry, event);
     entry->callback(event, msg);
     crm_trace("Callback invoked...");
 }
 
 static pcmk__cluster_option_t cib_opts[] = {
     /* name, legacy name, type, allowed values,
      * default value, validator,
      * short description,
      * long description
      */
     {
         "enable-acl", NULL, "boolean", NULL,
         "false", pcmk__valid_boolean,
         N_("Enable Access Control Lists (ACLs) for the CIB"),
         NULL
     },
     {
         "cluster-ipc-limit", NULL, "integer", NULL,
         "500", pcmk__valid_positive_number,
         N_("Maximum IPC message backlog before disconnecting a cluster daemon"),
         N_("Raise this if log has \"Evicting client\" messages for cluster daemon"
             " PIDs (a good value is the number of resources in the cluster"
             " multiplied by the number of nodes).")
     },
 };
 
 void
 cib_metadata(void)
 {
     const char *desc_short = "Cluster Information Base manager options";
     const char *desc_long = "Cluster options used by Pacemaker's Cluster "
                             "Information Base manager";
 
     gchar *s = pcmk__format_option_metadata("pacemaker-based", desc_short,
                                             desc_long, cib_opts,
                                             PCMK__NELEM(cib_opts));
     printf("%s", s);
     g_free(s);
 }
 
 void
 verify_cib_options(GHashTable * options)
 {
     pcmk__validate_cluster_options(options, cib_opts, PCMK__NELEM(cib_opts));
 }
 
 const char *
 cib_pref(GHashTable * options, const char *name)
 {
     return pcmk__cluster_option(options, cib_opts, PCMK__NELEM(cib_opts),
                                 name);
 }
 
 gboolean
 cib_read_config(GHashTable * options, xmlNode * current_cib)
 {
     xmlNode *config = NULL;
     crm_time_t *now = NULL;
 
     if (options == NULL || current_cib == NULL) {
         return FALSE;
     }
 
     now = crm_time_new(NULL);
 
     g_hash_table_remove_all(options);
 
     config = pcmk_find_cib_element(current_cib, XML_CIB_TAG_CRMCONFIG);
     if (config) {
         pe_unpack_nvpairs(current_cib, config, XML_CIB_TAG_PROPSET, NULL,
                           options, CIB_OPTIONS_FIRST, TRUE, now, NULL);
     }
 
     verify_cib_options(options);
 
     crm_time_free(now);
 
     return TRUE;
 }
 
 /* v2 and v2 patch formats */
 #define XPATH_CONFIG_CHANGE \
     "//" XML_CIB_TAG_CRMCONFIG " | " \
     "//" XML_DIFF_CHANGE "[contains(@" XML_DIFF_PATH ",'/" XML_CIB_TAG_CRMCONFIG "/')]"
 
 gboolean
 cib_internal_config_changed(xmlNode *diff)
 {
     gboolean changed = FALSE;
 
     if (diff) {
         xmlXPathObject *xpathObj = xpath_search(diff, XPATH_CONFIG_CHANGE);
 
         if (numXpathResults(xpathObj) > 0) {
             changed = TRUE;
         }
         freeXpathObject(xpathObj);
     }
     return changed;
 }
 
 int
 cib_internal_op(cib_t * cib, const char *op, const char *host,
                 const char *section, xmlNode * data,
                 xmlNode ** output_data, int call_options, const char *user_name)
 {
     int (*delegate) (cib_t * cib, const char *op, const char *host,
                      const char *section, xmlNode * data,
                      xmlNode ** output_data, int call_options, const char *user_name) =
         cib->delegate_fn;
 
     if(user_name == NULL) {
         user_name = getenv("CIB_user");
     }
 
     return delegate(cib, op, host, section, data, output_data, call_options, user_name);
 }
 
 /*!
  * \brief Apply a CIB update patch to a given CIB
  *
  * \param[in]  event   CIB update patch
  * \param[in]  input   CIB to patch
  * \param[out] output  Resulting CIB after patch
  * \param[in]  level   Log the patch at this log level (unless LOG_CRIT)
  *
  * \return Legacy Pacemaker return code
  * \note sbd calls this function
  */
 int
 cib_apply_patch_event(xmlNode *event, xmlNode *input, xmlNode **output,
                       int level)
 {
     int rc = pcmk_err_generic;
 
     xmlNode *diff = NULL;
 
     CRM_ASSERT(event);
     CRM_ASSERT(input);
     CRM_ASSERT(output);
 
     crm_element_value_int(event, F_CIB_RC, &rc);
     diff = get_message_xml(event, F_CIB_UPDATE_RESULT);
 
     if (rc < pcmk_ok || diff == NULL) {
         return rc;
     }
 
     if (level > LOG_CRIT) {
-        pcmk__xml_log_patchset(level, diff);
+        pcmk__output_t *out = NULL;
+
+        rc = pcmk_rc2legacy(pcmk__log_output_new(&out));
+        CRM_CHECK(rc == pcmk_ok, return rc);
+
+        pcmk__output_set_log_level(out, level);
+        rc = out->message(out, "xml-patchset", diff);
+        out->finish(out, pcmk_rc2exitc(rc), true, NULL);
+        pcmk__output_free(out);
+        rc = pcmk_ok;
     }
 
     if (input != NULL) {
         rc = cib_process_diff(NULL, cib_none, NULL, event, diff, input, output,
                               NULL);
 
         if (rc != pcmk_ok) {
             crm_debug("Update didn't apply: %s (%d) %p",
                       pcmk_strerror(rc), rc, *output);
 
             if (rc == -pcmk_err_old_data) {
                 crm_trace("Masking error, we already have the supplied update");
                 return pcmk_ok;
             }
             free_xml(*output);
             *output = NULL;
             return rc;
         }
     }
     return rc;
 }
 
 #define log_signon_query_err(out, fmt, args...) do {    \
         if (out != NULL) {                              \
             out->err(out, fmt, ##args);                 \
         } else {                                        \
             crm_err(fmt, ##args);                       \
         }                                               \
     } while (0)
 
 int
 cib__signon_query(pcmk__output_t *out, cib_t **cib, xmlNode **cib_object)
 {
     int rc = pcmk_rc_ok;
     cib_t *cib_conn = NULL;
 
     CRM_ASSERT(cib_object != NULL);
 
     if (cib == NULL) {
         cib_conn = cib_new();
     } else {
         if (*cib == NULL) {
             *cib = cib_new();
         }
         cib_conn = *cib;
     }
 
     if (cib_conn == NULL) {
         return ENOMEM;
     }
 
     if (cib_conn->state == cib_disconnected) {
         rc = cib_conn->cmds->signon(cib_conn, crm_system_name, cib_command);
         rc = pcmk_legacy2rc(rc);
     }
 
     if (rc != pcmk_rc_ok) {
         log_signon_query_err(out, "Could not connect to the CIB: %s",
                              pcmk_rc_str(rc));
         goto done;
     }
 
     if (out != NULL) {
         out->transient(out, "Querying CIB...");
     }
     rc = cib_conn->cmds->query(cib_conn, NULL, cib_object,
                                cib_scope_local|cib_sync_call);
     rc = pcmk_legacy2rc(rc);
 
     if (rc != pcmk_rc_ok) {
         log_signon_query_err(out, "CIB query failed: %s", pcmk_rc_str(rc));
     }
 
 done:
     if (cib == NULL) {
         cib__clean_up_connection(&cib_conn);
     }
 
     if ((rc == pcmk_rc_ok) && (*cib_object == NULL)) {
         return pcmk_rc_no_input;
     }
     return rc;
 }
 
 int
 cib__clean_up_connection(cib_t **cib)
 {
     int rc;
 
     if (*cib == NULL) {
         return pcmk_rc_ok;
     }
 
     rc = (*cib)->cmds->signoff(*cib);
     cib_delete(*cib);
     *cib = NULL;
     return pcmk_legacy2rc(rc);
 }
 
 // Deprecated functions kept only for backward API compatibility
 // LCOV_EXCL_START
 
 #include <crm/cib/util_compat.h>
 
 const char *
 get_object_path(const char *object_type)
 {
     return pcmk_cib_xpath_for(object_type);
 }
 
 const char *
 get_object_parent(const char *object_type)
 {
     return pcmk_cib_parent_name_for(object_type);
 }
 
 xmlNode *
 get_object_root(const char *object_type, xmlNode *the_root)
 {
     return pcmk_find_cib_element(the_root, object_type);
 }
 
 // LCOV_EXCL_STOP
 // End deprecated API
diff --git a/lib/common/patchset.c b/lib/common/patchset.c
index bf302e0bf2..c5e851ceaa 100644
--- a/lib/common/patchset.c
+++ b/lib/common/patchset.c
@@ -1,1548 +1,1563 @@
 /*
  * Copyright 2004-2023 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 <sys/types.h>
 #include <unistd.h>
 #include <time.h>
 #include <string.h>
 #include <stdlib.h>
 #include <stdarg.h>
 #include <bzlib.h>
 
 #include <libxml/tree.h>
 
 #include <crm/crm.h>
 #include <crm/msg_xml.h>
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>  // CRM_XML_LOG_BASE, etc.
 #include "crmcommon_private.h"
 
 static xmlNode *subtract_xml_comment(xmlNode *parent, xmlNode *left,
                                      xmlNode *right, gboolean *changed);
 
 /*
 <diff format="2.0">
   <version>
     <source admin_epoch="1" epoch="2" num_updates="3"/>
     <target admin_epoch="1" epoch="3" num_updates="0"/>
   </version>
   <change operation="add" xpath="/cib/configuration/nodes">
     <node id="node2" uname="node2" description="foo"/>
   </change>
   <change operation="add" xpath="/cib/configuration/nodes/node[node2]">
     <instance_attributes id="nodes-node"><!-- NOTE: can be a full tree -->
       <nvpair id="nodes-node2-ram" name="ram" value="1024M"/>
     </instance_attributes>
   </change>
   <change operation="update" xpath="/cib/configuration/nodes[@id='node2']">
     <change-list>
       <change-attr operation="set" name="type" value="member"/>
       <change-attr operation="unset" name="description"/>
     </change-list>
     <change-result>
       <node id="node2" uname="node2" type="member"/><!-- NOTE: not recursive -->
     </change-result>
   </change>
   <change operation="delete" xpath="/cib/configuration/nodes/node[@id='node3'] /">
   <change operation="update" xpath="/cib/configuration/resources/group[@id='g1']">
     <change-list>
       <change-attr operation="set" name="description" value="some garbage here"/>
     </change-list>
     <change-result>
       <group id="g1" description="some garbage here"/><!-- NOTE: not recursive -->
     </change-result>
   </change>
   <change operation="update" xpath="/cib/status/node_state[@id='node2]/lrm[@id='node2']/lrm_resources/lrm_resource[@id='Fence']">
     <change-list>
       <change-attr operation="set" name="oper" value="member"/>
       <change-attr operation="set" name="operation_key" value="Fence_start_0"/>
       <change-attr operation="set" name="operation" value="start"/>
       <change-attr operation="set" name="transition-key" value="2:-1:0:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"/>
       <change-attr operation="set" name="transition-magic" value="0:0;2:-1:0:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"/>
       <change-attr operation="set" name="call-id" value="2"/>
       <change-attr operation="set" name="rc-code" value="0"/>
     </change-list>
     <change-result>
       <lrm_rsc_op id="Fence_last_0" operation_key="Fence_start_0" operation="start" crm-debug-origin="crm_simulate"  transition-key="2:-1:0:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" transition-magic="0:0;2:-1:0:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" call-id="2" rc-code="0" op-status="0" interval="0" exec-time="0" queue-time="0" op-digest="f2317cad3d54cec5d7d7aa7d0bf35cf8"/>
     </change-result>
   </change>
 </diff>
  */
 
 // Add changes for specified XML to patchset
 static void
 add_xml_changes_to_patchset(xmlNode *xml, xmlNode *patchset)
 {
     xmlNode *cIter = NULL;
     xmlAttr *pIter = NULL;
     xmlNode *change = NULL;
     xml_node_private_t *nodepriv = xml->_private;
     const char *value = NULL;
 
     // If this XML node is new, just report that
     if (patchset && pcmk_is_set(nodepriv->flags, pcmk__xf_created)) {
         GString *xpath = pcmk__element_xpath(xml->parent);
 
         if (xpath != NULL) {
             int position = pcmk__xml_position(xml, pcmk__xf_deleted);
 
             change = create_xml_node(patchset, XML_DIFF_CHANGE);
 
             crm_xml_add(change, XML_DIFF_OP, "create");
             crm_xml_add(change, XML_DIFF_PATH, (const char *) xpath->str);
             crm_xml_add_int(change, XML_DIFF_POSITION, position);
             add_node_copy(change, xml);
             g_string_free(xpath, TRUE);
         }
 
         return;
     }
 
     // Check each of the XML node's attributes for changes
     for (pIter = pcmk__xe_first_attr(xml); pIter != NULL;
          pIter = pIter->next) {
         xmlNode *attr = NULL;
 
         nodepriv = pIter->_private;
         if (!pcmk_any_flags_set(nodepriv->flags, pcmk__xf_deleted|pcmk__xf_dirty)) {
             continue;
         }
 
         if (change == NULL) {
             GString *xpath = pcmk__element_xpath(xml);
 
             if (xpath != NULL) {
                 change = create_xml_node(patchset, XML_DIFF_CHANGE);
 
                 crm_xml_add(change, XML_DIFF_OP, "modify");
                 crm_xml_add(change, XML_DIFF_PATH, (const char *) xpath->str);
 
                 change = create_xml_node(change, XML_DIFF_LIST);
                 g_string_free(xpath, TRUE);
             }
         }
 
         attr = create_xml_node(change, XML_DIFF_ATTR);
 
         crm_xml_add(attr, XML_NVPAIR_ATTR_NAME, (const char *)pIter->name);
         if (nodepriv->flags & pcmk__xf_deleted) {
             crm_xml_add(attr, XML_DIFF_OP, "unset");
 
         } else {
             crm_xml_add(attr, XML_DIFF_OP, "set");
 
             value = crm_element_value(xml, (const char *) pIter->name);
             crm_xml_add(attr, XML_NVPAIR_ATTR_VALUE, value);
         }
     }
 
     if (change) {
         xmlNode *result = NULL;
 
         change = create_xml_node(change->parent, XML_DIFF_RESULT);
         result = create_xml_node(change, (const char *)xml->name);
 
         for (pIter = pcmk__xe_first_attr(xml); pIter != NULL;
              pIter = pIter->next) {
             nodepriv = pIter->_private;
             if (!pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) {
                 value = crm_element_value(xml, (const char *) pIter->name);
                 crm_xml_add(result, (const char *)pIter->name, value);
             }
         }
     }
 
     // Now recursively do the same for each child node of this node
     for (cIter = pcmk__xml_first_child(xml); cIter != NULL;
          cIter = pcmk__xml_next(cIter)) {
         add_xml_changes_to_patchset(cIter, patchset);
     }
 
     nodepriv = xml->_private;
     if (patchset && pcmk_is_set(nodepriv->flags, pcmk__xf_moved)) {
         GString *xpath = pcmk__element_xpath(xml);
 
         crm_trace("%s.%s moved to position %d",
                   xml->name, ID(xml), pcmk__xml_position(xml, pcmk__xf_skip));
 
         if (xpath != NULL) {
             change = create_xml_node(patchset, XML_DIFF_CHANGE);
 
             crm_xml_add(change, XML_DIFF_OP, "move");
             crm_xml_add(change, XML_DIFF_PATH, (const char *) xpath->str);
             crm_xml_add_int(change, XML_DIFF_POSITION,
                             pcmk__xml_position(xml, pcmk__xf_deleted));
             g_string_free(xpath, TRUE);
         }
     }
 }
 
 static bool
 is_config_change(xmlNode *xml)
 {
     GList *gIter = NULL;
     xml_node_private_t *nodepriv = NULL;
     xml_doc_private_t *docpriv;
     xmlNode *config = first_named_child(xml, XML_CIB_TAG_CONFIGURATION);
 
     if (config) {
         nodepriv = config->_private;
     }
     if ((nodepriv != NULL) && pcmk_is_set(nodepriv->flags, pcmk__xf_dirty)) {
         return TRUE;
     }
 
     if ((xml->doc != NULL) && (xml->doc->_private != NULL)) {
         docpriv = xml->doc->_private;
         for (gIter = docpriv->deleted_objs; gIter; gIter = gIter->next) {
             pcmk__deleted_xml_t *deleted_obj = gIter->data;
 
             if (strstr(deleted_obj->path,
                        "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION) != NULL) {
                 return TRUE;
             }
         }
     }
     return FALSE;
 }
 
 static void
 xml_repair_v1_diff(xmlNode *last, xmlNode *next, xmlNode *local_diff,
                    gboolean changed)
 {
     int lpc = 0;
     xmlNode *cib = NULL;
     xmlNode *diff_child = NULL;
 
     const char *tag = NULL;
 
     const char *vfields[] = {
         XML_ATTR_GENERATION_ADMIN,
         XML_ATTR_GENERATION,
         XML_ATTR_NUMUPDATES,
     };
 
     if (local_diff == NULL) {
         crm_trace("Nothing to do");
         return;
     }
 
     tag = "diff-removed";
     diff_child = find_xml_node(local_diff, tag, FALSE);
     if (diff_child == NULL) {
         diff_child = create_xml_node(local_diff, tag);
     }
 
     tag = XML_TAG_CIB;
     cib = find_xml_node(diff_child, tag, FALSE);
     if (cib == NULL) {
         cib = create_xml_node(diff_child, tag);
     }
 
     for (lpc = 0; (last != NULL) && (lpc < PCMK__NELEM(vfields)); lpc++) {
         const char *value = crm_element_value(last, vfields[lpc]);
 
         crm_xml_add(diff_child, vfields[lpc], value);
         if (changed || lpc == 2) {
             crm_xml_add(cib, vfields[lpc], value);
         }
     }
 
     tag = "diff-added";
     diff_child = find_xml_node(local_diff, tag, FALSE);
     if (diff_child == NULL) {
         diff_child = create_xml_node(local_diff, tag);
     }
 
     tag = XML_TAG_CIB;
     cib = find_xml_node(diff_child, tag, FALSE);
     if (cib == NULL) {
         cib = create_xml_node(diff_child, tag);
     }
 
     for (lpc = 0; next && lpc < PCMK__NELEM(vfields); lpc++) {
         const char *value = crm_element_value(next, vfields[lpc]);
 
         crm_xml_add(diff_child, vfields[lpc], value);
     }
 
     for (xmlAttrPtr a = pcmk__xe_first_attr(next); a != NULL; a = a->next) {
         const char *p_value = crm_element_value(next, (const char *) a->name);
 
         xmlSetProp(cib, a->name, (pcmkXmlStr) p_value);
     }
 
     crm_log_xml_explicit(local_diff, "Repaired-diff");
 }
 
 static xmlNode *
 xml_create_patchset_v1(xmlNode *source, xmlNode *target, bool config,
                        bool suppress)
 {
     xmlNode *patchset = diff_xml_object(source, target, suppress);
 
     if (patchset) {
         CRM_LOG_ASSERT(xml_document_dirty(target));
         xml_repair_v1_diff(source, target, patchset, config);
         crm_xml_add(patchset, "format", "1");
     }
     return patchset;
 }
 
 static xmlNode *
 xml_create_patchset_v2(xmlNode *source, xmlNode *target)
 {
     int lpc = 0;
     GList *gIter = NULL;
     xml_doc_private_t *docpriv;
 
     xmlNode *v = NULL;
     xmlNode *version = NULL;
     xmlNode *patchset = NULL;
     const char *vfields[] = {
         XML_ATTR_GENERATION_ADMIN,
         XML_ATTR_GENERATION,
         XML_ATTR_NUMUPDATES,
     };
 
     CRM_ASSERT(target);
     if (!xml_document_dirty(target)) {
         return NULL;
     }
 
     CRM_ASSERT(target->doc);
     docpriv = target->doc->_private;
 
     patchset = create_xml_node(NULL, XML_TAG_DIFF);
     crm_xml_add_int(patchset, "format", 2);
 
     version = create_xml_node(patchset, XML_DIFF_VERSION);
 
     v = create_xml_node(version, XML_DIFF_VSOURCE);
     for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
         const char *value = crm_element_value(source, vfields[lpc]);
 
         if (value == NULL) {
             value = "1";
         }
         crm_xml_add(v, vfields[lpc], value);
     }
 
     v = create_xml_node(version, XML_DIFF_VTARGET);
     for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
         const char *value = crm_element_value(target, vfields[lpc]);
 
         if (value == NULL) {
             value = "1";
         }
         crm_xml_add(v, vfields[lpc], value);
     }
 
     for (gIter = docpriv->deleted_objs; gIter; gIter = gIter->next) {
         pcmk__deleted_xml_t *deleted_obj = gIter->data;
         xmlNode *change = create_xml_node(patchset, XML_DIFF_CHANGE);
 
         crm_xml_add(change, XML_DIFF_OP, "delete");
         crm_xml_add(change, XML_DIFF_PATH, deleted_obj->path);
         if (deleted_obj->position >= 0) {
             crm_xml_add_int(change, XML_DIFF_POSITION, deleted_obj->position);
         }
     }
 
     add_xml_changes_to_patchset(target, patchset);
     return patchset;
 }
 
 xmlNode *
 xml_create_patchset(int format, xmlNode *source, xmlNode *target,
                     bool *config_changed, bool manage_version)
 {
     int counter = 0;
     bool config = FALSE;
     xmlNode *patch = NULL;
     const char *version = crm_element_value(source, XML_ATTR_CRM_VERSION);
 
     xml_acl_disable(target);
     if (!xml_document_dirty(target)) {
         crm_trace("No change %d", format);
         return NULL; /* No change */
     }
 
     config = is_config_change(target);
     if (config_changed) {
         *config_changed = config;
     }
 
     if (manage_version && config) {
         crm_trace("Config changed %d", format);
         crm_xml_add(target, XML_ATTR_NUMUPDATES, "0");
 
         crm_element_value_int(target, XML_ATTR_GENERATION, &counter);
         crm_xml_add_int(target, XML_ATTR_GENERATION, counter+1);
 
     } else if (manage_version) {
         crm_element_value_int(target, XML_ATTR_NUMUPDATES, &counter);
         crm_trace("Status changed %d - %d %s", format, counter,
                   crm_element_value(source, XML_ATTR_NUMUPDATES));
         crm_xml_add_int(target, XML_ATTR_NUMUPDATES, (counter + 1));
     }
 
     if (format == 0) {
         if (compare_version("3.0.8", version) < 0) {
             format = 2;
         } else {
             format = 1;
         }
         crm_trace("Using patch format %d for version: %s", format, version);
     }
 
     switch (format) {
         case 1:
             patch = xml_create_patchset_v1(source, target, config, FALSE);
             break;
         case 2:
             patch = xml_create_patchset_v2(source, target);
             break;
         default:
             crm_err("Unknown patch format: %d", format);
             return NULL;
     }
     return patch;
 }
 
 void
 patchset_process_digest(xmlNode *patch, xmlNode *source, xmlNode *target,
                         bool with_digest)
 {
     int format = 1;
     const char *version = NULL;
     char *digest = NULL;
 
     if ((patch == NULL) || (source == NULL) || (target == NULL)) {
         return;
     }
 
     /* We should always call xml_accept_changes() before calculating a digest.
      * Otherwise, with an on-tracking dirty target, we could get a wrong digest.
      */
     CRM_LOG_ASSERT(!xml_document_dirty(target));
 
     crm_element_value_int(patch, "format", &format);
     if ((format > 1) && !with_digest) {
         return;
     }
 
     version = crm_element_value(source, XML_ATTR_CRM_VERSION);
     digest = calculate_xml_versioned_digest(target, FALSE, TRUE, version);
 
     crm_xml_add(patch, XML_ATTR_DIGEST, digest);
     free(digest);
 
     return;
 }
 
 // Return true if attribute name is not "id"
 static bool
 not_id(xmlAttrPtr attr, void *user_data)
 {
     return strcmp((const char *) attr->name, XML_ATTR_ID) != 0;
 }
 
 // Apply the removals section of an v1 patchset to an XML node
 static void
 process_v1_removals(xmlNode *target, xmlNode *patch)
 {
     xmlNode *patch_child = NULL;
     xmlNode *cIter = NULL;
 
     char *id = NULL;
     const char *name = NULL;
     const char *value = NULL;
 
     if ((target == NULL) || (patch == NULL)) {
         return;
     }
 
     if (target->type == XML_COMMENT_NODE) {
         gboolean dummy;
 
         subtract_xml_comment(target->parent, target, patch, &dummy);
     }
 
     name = crm_element_name(target);
     CRM_CHECK(name != NULL, return);
     CRM_CHECK(pcmk__str_eq(crm_element_name(target), crm_element_name(patch),
                            pcmk__str_casei),
               return);
     CRM_CHECK(pcmk__str_eq(ID(target), ID(patch), pcmk__str_casei), return);
 
     // Check for XML_DIFF_MARKER in a child
     id = crm_element_value_copy(target, XML_ATTR_ID);
     value = crm_element_value(patch, XML_DIFF_MARKER);
     if ((value != NULL) && (strcmp(value, "removed:top") == 0)) {
         crm_trace("We are the root of the deletion: %s.id=%s", name, id);
         free_xml(target);
         free(id);
         return;
     }
 
     // Removing then restoring id would change ordering of properties
     pcmk__xe_remove_matching_attrs(patch, not_id, NULL);
 
     // Changes to child objects
     cIter = pcmk__xml_first_child(target);
     while (cIter) {
         xmlNode *target_child = cIter;
 
         cIter = pcmk__xml_next(cIter);
         patch_child = pcmk__xml_match(patch, target_child, false);
         process_v1_removals(target_child, patch_child);
     }
     free(id);
 }
 
 // Apply the additions section of an v1 patchset to an XML node
 static void
 process_v1_additions(xmlNode *parent, xmlNode *target, xmlNode *patch)
 {
     xmlNode *patch_child = NULL;
     xmlNode *target_child = NULL;
     xmlAttrPtr xIter = NULL;
 
     const char *id = NULL;
     const char *name = NULL;
     const char *value = NULL;
 
     if (patch == NULL) {
         return;
     } else if ((parent == NULL) && (target == NULL)) {
         return;
     }
 
     // Check for XML_DIFF_MARKER in a child
     value = crm_element_value(patch, XML_DIFF_MARKER);
     if ((target == NULL) && (value != NULL)
         && (strcmp(value, "added:top") == 0)) {
         id = ID(patch);
         name = crm_element_name(patch);
         crm_trace("We are the root of the addition: %s.id=%s", name, id);
         add_node_copy(parent, patch);
         return;
 
     } else if (target == NULL) {
         id = ID(patch);
         name = crm_element_name(patch);
         crm_err("Could not locate: %s.id=%s", name, id);
         return;
     }
 
     if (target->type == XML_COMMENT_NODE) {
         pcmk__xc_update(parent, target, patch);
     }
 
     name = crm_element_name(target);
     CRM_CHECK(name != NULL, return);
     CRM_CHECK(pcmk__str_eq(crm_element_name(target), crm_element_name(patch),
                            pcmk__str_casei),
               return);
     CRM_CHECK(pcmk__str_eq(ID(target), ID(patch), pcmk__str_casei), return);
 
     for (xIter = pcmk__xe_first_attr(patch); xIter != NULL;
          xIter = xIter->next) {
         const char *p_name = (const char *) xIter->name;
         const char *p_value = crm_element_value(patch, p_name);
 
         xml_remove_prop(target, p_name); // Preserve patch order
         crm_xml_add(target, p_name, p_value);
     }
 
     // Changes to child objects
     for (patch_child = pcmk__xml_first_child(patch); patch_child != NULL;
          patch_child = pcmk__xml_next(patch_child)) {
 
         target_child = pcmk__xml_match(target, patch_child, false);
         process_v1_additions(target, target_child, patch_child);
     }
 }
 
 /*!
  * \internal
  * \brief Find additions or removals in a patch set
  *
  * \param[in]     patchset   XML of patch
  * \param[in]     format     Patch version
  * \param[in]     added      TRUE if looking for additions, FALSE if removals
  * \param[in,out] patch_node Will be set to node if found
  *
  * \return TRUE if format is valid, FALSE if invalid
  */
 static bool
 find_patch_xml_node(const xmlNode *patchset, int format, bool added,
                     xmlNode **patch_node)
 {
     xmlNode *cib_node;
     const char *label;
 
     switch (format) {
         case 1:
             label = added? "diff-added" : "diff-removed";
             *patch_node = find_xml_node(patchset, label, FALSE);
             cib_node = find_xml_node(*patch_node, "cib", FALSE);
             if (cib_node != NULL) {
                 *patch_node = cib_node;
             }
             break;
         case 2:
             label = added? "target" : "source";
             *patch_node = find_xml_node(patchset, "version", FALSE);
             *patch_node = find_xml_node(*patch_node, label, FALSE);
             break;
         default:
             crm_warn("Unknown patch format: %d", format);
             *patch_node = NULL;
             return FALSE;
     }
     return TRUE;
 }
 
 // Get CIB versions used for additions and deletions in a patchset
 bool
 xml_patch_versions(const xmlNode *patchset, int add[3], int del[3])
 {
     int lpc = 0;
     int format = 1;
     xmlNode *tmp = NULL;
 
     const char *vfields[] = {
         XML_ATTR_GENERATION_ADMIN,
         XML_ATTR_GENERATION,
         XML_ATTR_NUMUPDATES,
     };
 
 
     crm_element_value_int(patchset, "format", &format);
 
     /* Process removals */
     if (!find_patch_xml_node(patchset, format, FALSE, &tmp)) {
         return -EINVAL;
     }
     if (tmp != NULL) {
         for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
             crm_element_value_int(tmp, vfields[lpc], &(del[lpc]));
             crm_trace("Got %d for del[%s]", del[lpc], vfields[lpc]);
         }
     }
 
     /* Process additions */
     if (!find_patch_xml_node(patchset, format, TRUE, &tmp)) {
         return -EINVAL;
     }
     if (tmp != NULL) {
         for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
             crm_element_value_int(tmp, vfields[lpc], &(add[lpc]));
             crm_trace("Got %d for add[%s]", add[lpc], vfields[lpc]);
         }
     }
     return pcmk_ok;
 }
 
 /*!
  * \internal
  * \brief Check whether patchset can be applied to current CIB
  *
  * \param[in] xml       Root of current CIB
  * \param[in] patchset  Patchset to check
  * \param[in] format    Patchset version
  *
  * \return Standard Pacemaker return code
  */
 static int
 xml_patch_version_check(const xmlNode *xml, const xmlNode *patchset, int format)
 {
     int lpc = 0;
     bool changed = FALSE;
 
     int this[] = { 0, 0, 0 };
     int add[] = { 0, 0, 0 };
     int del[] = { 0, 0, 0 };
 
     const char *vfields[] = {
         XML_ATTR_GENERATION_ADMIN,
         XML_ATTR_GENERATION,
         XML_ATTR_NUMUPDATES,
     };
 
     for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
         crm_element_value_int(xml, vfields[lpc], &(this[lpc]));
         crm_trace("Got %d for this[%s]", this[lpc], vfields[lpc]);
         if (this[lpc] < 0) {
             this[lpc] = 0;
         }
     }
 
     /* Set some defaults in case nothing is present */
     add[0] = this[0];
     add[1] = this[1];
     add[2] = this[2] + 1;
     for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
         del[lpc] = this[lpc];
     }
 
     xml_patch_versions(patchset, add, del);
 
     for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
         if (this[lpc] < del[lpc]) {
             crm_debug("Current %s is too low (%d.%d.%d < %d.%d.%d --> %d.%d.%d)",
                       vfields[lpc], this[0], this[1], this[2],
                       del[0], del[1], del[2], add[0], add[1], add[2]);
             return pcmk_rc_diff_resync;
 
         } else if (this[lpc] > del[lpc]) {
             crm_info("Current %s is too high (%d.%d.%d > %d.%d.%d --> %d.%d.%d) %p",
                      vfields[lpc], this[0], this[1], this[2],
                      del[0], del[1], del[2], add[0], add[1], add[2], patchset);
             crm_log_xml_info(patchset, "OldPatch");
             return pcmk_rc_old_data;
         }
     }
 
     for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
         if (add[lpc] > del[lpc]) {
             changed = TRUE;
         }
     }
 
     if (!changed) {
         crm_notice("Versions did not change in patch %d.%d.%d",
                    add[0], add[1], add[2]);
         return pcmk_rc_old_data;
     }
 
     crm_debug("Can apply patch %d.%d.%d to %d.%d.%d",
               add[0], add[1], add[2], this[0], this[1], this[2]);
     return pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Apply a version 1 patchset to an XML node
  *
  * \param[in,out] xml       XML to apply patchset to
  * \param[in]     patchset  Patchset to apply
  *
  * \return Standard Pacemaker return code
  */
 static int
 apply_v1_patchset(xmlNode *xml, const xmlNode *patchset)
 {
     int rc = pcmk_rc_ok;
     int root_nodes_seen = 0;
 
     xmlNode *child_diff = NULL;
     xmlNode *added = find_xml_node(patchset, "diff-added", FALSE);
     xmlNode *removed = find_xml_node(patchset, "diff-removed", FALSE);
     xmlNode *old = copy_xml(xml);
 
     crm_trace("Subtraction Phase");
     for (child_diff = pcmk__xml_first_child(removed); child_diff != NULL;
          child_diff = pcmk__xml_next(child_diff)) {
         CRM_CHECK(root_nodes_seen == 0, rc = FALSE);
         if (root_nodes_seen == 0) {
             process_v1_removals(xml, child_diff);
         }
         root_nodes_seen++;
     }
 
     if (root_nodes_seen > 1) {
         crm_err("(-) Diffs cannot contain more than one change set... saw %d",
                 root_nodes_seen);
         rc = ENOTUNIQ;
     }
 
     root_nodes_seen = 0;
     crm_trace("Addition Phase");
     if (rc == pcmk_rc_ok) {
         xmlNode *child_diff = NULL;
 
         for (child_diff = pcmk__xml_first_child(added); child_diff != NULL;
              child_diff = pcmk__xml_next(child_diff)) {
             CRM_CHECK(root_nodes_seen == 0, rc = FALSE);
             if (root_nodes_seen == 0) {
                 process_v1_additions(NULL, xml, child_diff);
             }
             root_nodes_seen++;
         }
     }
 
     if (root_nodes_seen > 1) {
         crm_err("(+) Diffs cannot contain more than one change set... saw %d",
                 root_nodes_seen);
         rc = ENOTUNIQ;
     }
 
     purge_diff_markers(xml); // Purge prior to checking digest
 
     free_xml(old);
     return rc;
 }
 
 // Return first child matching element name and optionally id or position
 static xmlNode *
 first_matching_xml_child(const xmlNode *parent, const char *name,
                          const char *id, int position)
 {
     xmlNode *cIter = NULL;
 
     for (cIter = pcmk__xml_first_child(parent); cIter != NULL;
          cIter = pcmk__xml_next(cIter)) {
         if (strcmp((const char *) cIter->name, name) != 0) {
             continue;
         } else if (id) {
             const char *cid = ID(cIter);
 
             if ((cid == NULL) || (strcmp(cid, id) != 0)) {
                 continue;
             }
         }
 
         // "position" makes sense only for XML comments for now
         if ((cIter->type == XML_COMMENT_NODE)
             && (position >= 0)
             && (pcmk__xml_position(cIter, pcmk__xf_skip) != position)) {
             continue;
         }
 
         return cIter;
     }
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Simplified, more efficient alternative to get_xpath_object()
  *
  * \param[in] top              Root of XML to search
  * \param[in] key              Search xpath
  * \param[in] target_position  If deleting, where to delete
  *
  * \return XML child matching xpath if found, NULL otherwise
  *
  * \note This only works on simplified xpaths found in v2 patchset diffs,
  *       i.e. the only allowed search predicate is [@id='XXX'].
  */
 static xmlNode *
 search_v2_xpath(const xmlNode *top, const char *key, int target_position)
 {
     xmlNode *target = (xmlNode *) top->doc;
     const char *current = key;
     char *section;
     char *remainder;
     char *id;
     char *tag;
     char *path = NULL;
     int rc;
     size_t key_len;
 
     CRM_CHECK(key != NULL, return NULL);
     key_len = strlen(key);
 
     /* These are scanned from key after a slash, so they can't be bigger
      * than key_len - 1 characters plus a null terminator.
      */
 
     remainder = calloc(key_len, sizeof(char));
     CRM_ASSERT(remainder != NULL);
 
     section = calloc(key_len, sizeof(char));
     CRM_ASSERT(section != NULL);
 
     id = calloc(key_len, sizeof(char));
     CRM_ASSERT(id != NULL);
 
     tag = calloc(key_len, sizeof(char));
     CRM_ASSERT(tag != NULL);
 
     do {
         // Look for /NEXT_COMPONENT/REMAINING_COMPONENTS
         rc = sscanf(current, "/%[^/]%s", section, remainder);
         if (rc > 0) {
             // Separate FIRST_COMPONENT into TAG[@id='ID']
             int f = sscanf(section, "%[^[][@" XML_ATTR_ID "='%[^']", tag, id);
             int current_position = -1;
 
             /* The target position is for the final component tag, so only use
              * it if there is nothing left to search after this component.
              */
             if ((rc == 1) && (target_position >= 0)) {
                 current_position = target_position;
             }
 
             switch (f) {
                 case 1:
                     target = first_matching_xml_child(target, tag, NULL,
                                                       current_position);
                     break;
                 case 2:
                     target = first_matching_xml_child(target, tag, id,
                                                       current_position);
                     break;
                 default:
                     // This should not be possible
                     target = NULL;
                     break;
             }
             current = remainder;
         }
 
     // Continue if something remains to search, and we've matched so far
     } while ((rc == 2) && target);
 
     if (target) {
         crm_trace("Found %s for %s",
                   (path = (char *) xmlGetNodePath(target)), key);
         free(path);
     } else {
         crm_debug("No match for %s", key);
     }
 
     free(remainder);
     free(section);
     free(tag);
     free(id);
     return target;
 }
 
 typedef struct xml_change_obj_s {
     const xmlNode *change;
     xmlNode *match;
 } xml_change_obj_t;
 
 static gint
 sort_change_obj_by_position(gconstpointer a, gconstpointer b)
 {
     const xml_change_obj_t *change_obj_a = a;
     const xml_change_obj_t *change_obj_b = b;
     int position_a = -1;
     int position_b = -1;
 
     crm_element_value_int(change_obj_a->change, XML_DIFF_POSITION, &position_a);
     crm_element_value_int(change_obj_b->change, XML_DIFF_POSITION, &position_b);
 
     if (position_a < position_b) {
         return -1;
 
     } else if (position_a > position_b) {
         return 1;
     }
 
     return 0;
 }
 
 /*!
  * \internal
  * \brief Apply a version 2 patchset to an XML node
  *
  * \param[in,out] xml       XML to apply patchset to
  * \param[in]     patchset  Patchset to apply
  *
  * \return Standard Pacemaker return code
  */
 static int
 apply_v2_patchset(xmlNode *xml, const xmlNode *patchset)
 {
     int rc = pcmk_rc_ok;
     const xmlNode *change = NULL;
     GList *change_objs = NULL;
     GList *gIter = NULL;
 
     for (change = pcmk__xml_first_child(patchset); change != NULL;
          change = pcmk__xml_next(change)) {
         xmlNode *match = NULL;
         const char *op = crm_element_value(change, XML_DIFF_OP);
         const char *xpath = crm_element_value(change, XML_DIFF_PATH);
         int position = -1;
 
         if (op == NULL) {
             continue;
         }
 
         crm_trace("Processing %s %s", change->name, op);
 
         // "delete" changes for XML comments are generated with "position"
         if (strcmp(op, "delete") == 0) {
             crm_element_value_int(change, XML_DIFF_POSITION, &position);
         }
         match = search_v2_xpath(xml, xpath, position);
         crm_trace("Performing %s on %s with %p", op, xpath, match);
 
         if ((match == NULL) && (strcmp(op, "delete") == 0)) {
             crm_debug("No %s match for %s in %p", op, xpath, xml->doc);
             continue;
 
         } else if (match == NULL) {
             crm_err("No %s match for %s in %p", op, xpath, xml->doc);
             rc = pcmk_rc_diff_failed;
             continue;
 
         } else if ((strcmp(op, "create") == 0) || (strcmp(op, "move") == 0)) {
             // Delay the adding of a "create" object
             xml_change_obj_t *change_obj = calloc(1, sizeof(xml_change_obj_t));
 
             CRM_ASSERT(change_obj != NULL);
 
             change_obj->change = change;
             change_obj->match = match;
 
             change_objs = g_list_append(change_objs, change_obj);
 
             if (strcmp(op, "move") == 0) {
                 // Temporarily put the "move" object after the last sibling
                 if ((match->parent != NULL) && (match->parent->last != NULL)) {
                     xmlAddNextSibling(match->parent->last, match);
                 }
             }
 
         } else if (strcmp(op, "delete") == 0) {
             free_xml(match);
 
         } else if (strcmp(op, "modify") == 0) {
             xmlNode *attrs = NULL;
 
             attrs = pcmk__xml_first_child(first_named_child(change,
                                                             XML_DIFF_RESULT));
             if (attrs == NULL) {
                 rc = ENOMSG;
                 continue;
             }
             pcmk__xe_remove_matching_attrs(match, NULL, NULL); // Remove all
 
             for (xmlAttrPtr pIter = pcmk__xe_first_attr(attrs); pIter != NULL;
                  pIter = pIter->next) {
                 const char *name = (const char *) pIter->name;
                 const char *value = crm_element_value(attrs, name);
 
                 crm_xml_add(match, name, value);
             }
 
         } else {
             crm_err("Unknown operation: %s", op);
             rc = pcmk_rc_diff_failed;
         }
     }
 
     // Changes should be generated in the right order. Double checking.
     change_objs = g_list_sort(change_objs, sort_change_obj_by_position);
 
     for (gIter = change_objs; gIter; gIter = gIter->next) {
         xml_change_obj_t *change_obj = gIter->data;
         xmlNode *match = change_obj->match;
         const char *op = NULL;
         const char *xpath = NULL;
 
         change = change_obj->change;
 
         op = crm_element_value(change, XML_DIFF_OP);
         xpath = crm_element_value(change, XML_DIFF_PATH);
 
         crm_trace("Continue performing %s on %s with %p", op, xpath, match);
 
         if (strcmp(op, "create") == 0) {
             int position = 0;
             xmlNode *child = NULL;
             xmlNode *match_child = NULL;
 
             match_child = match->children;
             crm_element_value_int(change, XML_DIFF_POSITION, &position);
 
             while ((match_child != NULL)
                    && (position != pcmk__xml_position(match_child, pcmk__xf_skip))) {
                 match_child = match_child->next;
             }
 
             child = xmlDocCopyNode(change->children, match->doc, 1);
             if (match_child) {
                 crm_trace("Adding %s at position %d", child->name, position);
                 xmlAddPrevSibling(match_child, child);
 
             } else if (match->last) {
                 crm_trace("Adding %s at position %d (end)",
                           child->name, position);
                 xmlAddNextSibling(match->last, child);
 
             } else {
                 crm_trace("Adding %s at position %d (first)",
                           child->name, position);
                 CRM_LOG_ASSERT(position == 0);
                 xmlAddChild(match, child);
             }
             pcmk__mark_xml_created(child);
 
         } else if (strcmp(op, "move") == 0) {
             int position = 0;
 
             crm_element_value_int(change, XML_DIFF_POSITION, &position);
             if (position != pcmk__xml_position(match, pcmk__xf_skip)) {
                 xmlNode *match_child = NULL;
                 int p = position;
 
                 if (p > pcmk__xml_position(match, pcmk__xf_skip)) {
                     p++; // Skip ourselves
                 }
 
                 CRM_ASSERT(match->parent != NULL);
                 match_child = match->parent->children;
 
                 while ((match_child != NULL)
                        && (p != pcmk__xml_position(match_child, pcmk__xf_skip))) {
                     match_child = match_child->next;
                 }
 
                 crm_trace("Moving %s to position %d (was %d, prev %p, %s %p)",
                           match->name, position,
                           pcmk__xml_position(match, pcmk__xf_skip),
                           match->prev, (match_child? "next":"last"),
                           (match_child? match_child : match->parent->last));
 
                 if (match_child) {
                     xmlAddPrevSibling(match_child, match);
 
                 } else {
                     CRM_ASSERT(match->parent->last != NULL);
                     xmlAddNextSibling(match->parent->last, match);
                 }
 
             } else {
                 crm_trace("%s is already in position %d",
                           match->name, position);
             }
 
             if (position != pcmk__xml_position(match, pcmk__xf_skip)) {
                 crm_err("Moved %s.%s to position %d instead of %d (%p)",
                         match->name, ID(match),
                         pcmk__xml_position(match, pcmk__xf_skip),
                         position, match->prev);
                 rc = pcmk_rc_diff_failed;
             }
         }
     }
 
     g_list_free_full(change_objs, free);
     return rc;
 }
 
 int
 xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version)
 {
     int format = 1;
     int rc = pcmk_ok;
     xmlNode *old = NULL;
     const char *digest = crm_element_value(patchset, XML_ATTR_DIGEST);
 
     if (patchset == NULL) {
         return rc;
     }
 
-    pcmk__xml_log_patchset(LOG_TRACE, patchset);
+    pcmk__if_tracing(
+        {
+            pcmk__output_t *logger_out = NULL;
+
+            rc = pcmk_rc2legacy(pcmk__log_output_new(&logger_out));
+            CRM_CHECK(rc == pcmk_ok, return rc);
+
+            pcmk__output_set_log_level(logger_out, LOG_TRACE);
+            rc = logger_out->message(logger_out, "xml-patchset", patchset);
+            logger_out->finish(logger_out, pcmk_rc2exitc(rc), true,
+                               NULL);
+            pcmk__output_free(logger_out);
+            rc = pcmk_ok;
+        },
+        {}
+    );
 
     crm_element_value_int(patchset, "format", &format);
     if (check_version) {
         rc = pcmk_rc2legacy(xml_patch_version_check(xml, patchset, format));
         if (rc != pcmk_ok) {
             return rc;
         }
     }
 
     if (digest) {
         // Make it available for logging if result doesn't have expected digest
         old = copy_xml(xml);
     }
 
     if (rc == pcmk_ok) {
         switch (format) {
             case 1:
                 rc = pcmk_rc2legacy(apply_v1_patchset(xml, patchset));
                 break;
             case 2:
                 rc = pcmk_rc2legacy(apply_v2_patchset(xml, patchset));
                 break;
             default:
                 crm_err("Unknown patch format: %d", format);
                 rc = -EINVAL;
         }
     }
 
     if ((rc == pcmk_ok) && (digest != NULL)) {
         char *new_digest = NULL;
         char *version = crm_element_value_copy(xml, XML_ATTR_CRM_VERSION);
 
         new_digest = calculate_xml_versioned_digest(xml, FALSE, TRUE, version);
         if (!pcmk__str_eq(new_digest, digest, pcmk__str_casei)) {
             crm_info("v%d digest mis-match: expected %s, calculated %s",
                      format, digest, new_digest);
             rc = -pcmk_err_diff_failed;
             pcmk__if_tracing(
                 {
                     save_xml_to_file(old, "PatchDigest:input", NULL);
                     save_xml_to_file(xml, "PatchDigest:result", NULL);
                     save_xml_to_file(patchset, "PatchDigest:diff", NULL);
                 },
                 {}
             );
 
         } else {
             crm_trace("v%d digest matched: expected %s, calculated %s",
                       format, digest, new_digest);
         }
         free(new_digest);
         free(version);
     }
     free_xml(old);
     return rc;
 }
 
 void
 purge_diff_markers(xmlNode *a_node)
 {
     xmlNode *child = NULL;
 
     CRM_CHECK(a_node != NULL, return);
 
     xml_remove_prop(a_node, XML_DIFF_MARKER);
     for (child = pcmk__xml_first_child(a_node); child != NULL;
          child = pcmk__xml_next(child)) {
         purge_diff_markers(child);
     }
 }
 
 xmlNode *
 diff_xml_object(xmlNode *old, xmlNode *new, gboolean suppress)
 {
     xmlNode *tmp1 = NULL;
     xmlNode *diff = create_xml_node(NULL, "diff");
     xmlNode *removed = create_xml_node(diff, "diff-removed");
     xmlNode *added = create_xml_node(diff, "diff-added");
 
     crm_xml_add(diff, XML_ATTR_CRM_VERSION, CRM_FEATURE_SET);
 
     tmp1 = subtract_xml_object(removed, old, new, FALSE, NULL, "removed:top");
     if (suppress && (tmp1 != NULL) && can_prune_leaf(tmp1)) {
         free_xml(tmp1);
     }
 
     tmp1 = subtract_xml_object(added, new, old, TRUE, NULL, "added:top");
     if (suppress && (tmp1 != NULL) && can_prune_leaf(tmp1)) {
         free_xml(tmp1);
     }
 
     if ((added->children == NULL) && (removed->children == NULL)) {
         free_xml(diff);
         diff = NULL;
     }
 
     return diff;
 }
 
 static xmlNode *
 subtract_xml_comment(xmlNode *parent, xmlNode *left, xmlNode *right,
                      gboolean *changed)
 {
     CRM_CHECK(left != NULL, return NULL);
     CRM_CHECK(left->type == XML_COMMENT_NODE, return NULL);
 
     if ((right == NULL) || !pcmk__str_eq((const char *)left->content,
                                          (const char *)right->content,
                                          pcmk__str_casei)) {
         xmlNode *deleted = NULL;
 
         deleted = add_node_copy(parent, left);
         *changed = TRUE;
 
         return deleted;
     }
 
     return NULL;
 }
 
 xmlNode *
 subtract_xml_object(xmlNode *parent, xmlNode *left, xmlNode *right,
                     gboolean full, gboolean *changed, const char *marker)
 {
     gboolean dummy = FALSE;
     xmlNode *diff = NULL;
     xmlNode *right_child = NULL;
     xmlNode *left_child = NULL;
     xmlAttrPtr xIter = NULL;
 
     const char *id = NULL;
     const char *name = NULL;
     const char *value = NULL;
     const char *right_val = NULL;
 
     if (changed == NULL) {
         changed = &dummy;
     }
 
     if (left == NULL) {
         return NULL;
     }
 
     if (left->type == XML_COMMENT_NODE) {
         return subtract_xml_comment(parent, left, right, changed);
     }
 
     id = ID(left);
     if (right == NULL) {
         xmlNode *deleted = NULL;
 
         crm_trace("Processing <%s " XML_ATTR_ID "=%s> (complete copy)",
                   crm_element_name(left), id);
         deleted = add_node_copy(parent, left);
         crm_xml_add(deleted, XML_DIFF_MARKER, marker);
 
         *changed = TRUE;
         return deleted;
     }
 
     name = crm_element_name(left);
     CRM_CHECK(name != NULL, return NULL);
     CRM_CHECK(pcmk__str_eq(crm_element_name(left), crm_element_name(right),
                            pcmk__str_casei),
               return NULL);
 
     // Check for XML_DIFF_MARKER in a child
     value = crm_element_value(right, XML_DIFF_MARKER);
     if ((value != NULL) && (strcmp(value, "removed:top") == 0)) {
         crm_trace("We are the root of the deletion: %s.id=%s", name, id);
         *changed = TRUE;
         return NULL;
     }
 
     // @TODO Avoiding creating the full hierarchy would save work here
     diff = create_xml_node(parent, name);
 
     // Changes to child objects
     for (left_child = pcmk__xml_first_child(left); left_child != NULL;
          left_child = pcmk__xml_next(left_child)) {
         gboolean child_changed = FALSE;
 
         right_child = pcmk__xml_match(right, left_child, false);
         subtract_xml_object(diff, left_child, right_child, full, &child_changed,
                             marker);
         if (child_changed) {
             *changed = TRUE;
         }
     }
 
     if (!*changed) {
         /* Nothing to do */
 
     } else if (full) {
         xmlAttrPtr pIter = NULL;
 
         for (pIter = pcmk__xe_first_attr(left); pIter != NULL;
              pIter = pIter->next) {
             const char *p_name = (const char *)pIter->name;
             const char *p_value = pcmk__xml_attr_value(pIter);
 
             xmlSetProp(diff, (pcmkXmlStr) p_name, (pcmkXmlStr) p_value);
         }
 
         // We have everything we need
         goto done;
     }
 
     // Changes to name/value pairs
     for (xIter = pcmk__xe_first_attr(left); xIter != NULL;
          xIter = xIter->next) {
         const char *prop_name = (const char *) xIter->name;
         xmlAttrPtr right_attr = NULL;
         xml_node_private_t *nodepriv = NULL;
 
         if (strcmp(prop_name, XML_ATTR_ID) == 0) {
             // id already obtained when present ~ this case, so just reuse
             xmlSetProp(diff, (pcmkXmlStr) XML_ATTR_ID, (pcmkXmlStr) id);
             continue;
         }
 
         if (pcmk__xa_filterable(prop_name)) {
             continue;
         }
 
         right_attr = xmlHasProp(right, (pcmkXmlStr) prop_name);
         if (right_attr) {
             nodepriv = right_attr->_private;
         }
 
         right_val = crm_element_value(right, prop_name);
         if ((right_val == NULL) || (nodepriv && pcmk_is_set(nodepriv->flags, pcmk__xf_deleted))) {
             /* new */
             *changed = TRUE;
             if (full) {
                 xmlAttrPtr pIter = NULL;
 
                 for (pIter = pcmk__xe_first_attr(left); pIter != NULL;
                      pIter = pIter->next) {
                     const char *p_name = (const char *) pIter->name;
                     const char *p_value = pcmk__xml_attr_value(pIter);
 
                     xmlSetProp(diff, (pcmkXmlStr) p_name, (pcmkXmlStr) p_value);
                 }
                 break;
 
             } else {
                 const char *left_value = crm_element_value(left, prop_name);
 
                 xmlSetProp(diff, (pcmkXmlStr) prop_name, (pcmkXmlStr) value);
                 crm_xml_add(diff, prop_name, left_value);
             }
 
         } else {
             /* Only now do we need the left value */
             const char *left_value = crm_element_value(left, prop_name);
 
             if (strcmp(left_value, right_val) == 0) {
                 /* unchanged */
 
             } else {
                 *changed = TRUE;
                 if (full) {
                     xmlAttrPtr pIter = NULL;
 
                     crm_trace("Changes detected to %s in "
                               "<%s " XML_ATTR_ID "=%s>",
                               prop_name, crm_element_name(left), id);
                     for (pIter = pcmk__xe_first_attr(left); pIter != NULL;
                          pIter = pIter->next) {
                         const char *p_name = (const char *) pIter->name;
                         const char *p_value = pcmk__xml_attr_value(pIter);
 
                         xmlSetProp(diff, (pcmkXmlStr) p_name,
                                    (pcmkXmlStr) p_value);
                     }
                     break;
 
                 } else {
                     crm_trace("Changes detected to %s (%s -> %s) in "
                               "<%s " XML_ATTR_ID "=%s>",
                               prop_name, left_value, right_val,
                               crm_element_name(left), id);
                     crm_xml_add(diff, prop_name, left_value);
                 }
             }
         }
     }
 
     if (!*changed) {
         free_xml(diff);
         return NULL;
 
     } else if (!full && (id != NULL)) {
         crm_xml_add(diff, XML_ATTR_ID, id);
     }
   done:
     return diff;
 }
 
 // Deprecated functions kept only for backward API compatibility
 // LCOV_EXCL_START
 
 #include <crm/common/xml_compat.h>
 
 gboolean
 apply_xml_diff(xmlNode *old_xml, xmlNode *diff, xmlNode **new_xml)
 {
     gboolean result = TRUE;
     int root_nodes_seen = 0;
     const char *digest = crm_element_value(diff, XML_ATTR_DIGEST);
     const char *version = crm_element_value(diff, XML_ATTR_CRM_VERSION);
 
     xmlNode *child_diff = NULL;
     xmlNode *added = find_xml_node(diff, "diff-added", FALSE);
     xmlNode *removed = find_xml_node(diff, "diff-removed", FALSE);
 
     CRM_CHECK(new_xml != NULL, return FALSE);
 
     crm_trace("Subtraction Phase");
     for (child_diff = pcmk__xml_first_child(removed); child_diff != NULL;
          child_diff = pcmk__xml_next(child_diff)) {
         CRM_CHECK(root_nodes_seen == 0, result = FALSE);
         if (root_nodes_seen == 0) {
             *new_xml = subtract_xml_object(NULL, old_xml, child_diff, FALSE,
                                            NULL, NULL);
         }
         root_nodes_seen++;
     }
 
     if (root_nodes_seen == 0) {
         *new_xml = copy_xml(old_xml);
 
     } else if (root_nodes_seen > 1) {
         crm_err("(-) Diffs cannot contain more than one change set... saw %d",
                 root_nodes_seen);
         result = FALSE;
     }
 
     root_nodes_seen = 0;
     crm_trace("Addition Phase");
     if (result) {
         xmlNode *child_diff = NULL;
 
         for (child_diff = pcmk__xml_first_child(added); child_diff != NULL;
              child_diff = pcmk__xml_next(child_diff)) {
             CRM_CHECK(root_nodes_seen == 0, result = FALSE);
             if (root_nodes_seen == 0) {
                 pcmk__xml_update(NULL, *new_xml, child_diff, true);
             }
             root_nodes_seen++;
         }
     }
 
     if (root_nodes_seen > 1) {
         crm_err("(+) Diffs cannot contain more than one change set... saw %d",
                 root_nodes_seen);
         result = FALSE;
 
     } else if (result && (digest != NULL)) {
         char *new_digest = NULL;
 
         purge_diff_markers(*new_xml); // Purge now so diff is ok
         new_digest = calculate_xml_versioned_digest(*new_xml, FALSE, TRUE,
                                                     version);
         if (!pcmk__str_eq(new_digest, digest, pcmk__str_casei)) {
             crm_info("Digest mis-match: expected %s, calculated %s",
                      digest, new_digest);
             result = FALSE;
 
             pcmk__if_tracing(
                 {
                     save_xml_to_file(old_xml, "diff:original", NULL);
                     save_xml_to_file(diff, "diff:input", NULL);
                     save_xml_to_file(*new_xml, "diff:new", NULL);
                 },
                 {}
             );
 
         } else {
             crm_trace("Digest matched: expected %s, calculated %s",
                       digest, new_digest);
         }
         free(new_digest);
 
     } else if (result) {
         purge_diff_markers(*new_xml); // Purge now so diff is ok
     }
 
     return result;
 }
 
 // LCOV_EXCL_STOP
 // End deprecated API
diff --git a/lib/common/patchset_display.c b/lib/common/patchset_display.c
index a87ae593d3..47834be7ab 100644
--- a/lib/common/patchset_display.c
+++ b/lib/common/patchset_display.c
@@ -1,400 +1,519 @@
 /*
  * Copyright 2004-2023 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/msg_xml.h>
 
 #include "crmcommon_private.h"
 
 /*!
  * \internal
  * \brief Output an XML patchset header
  *
  * This function parses a header from an XML patchset (an \p XML_ATTR_DIFF
  * element and its children).
  *
  * All header lines contain three integers separated by dots, of the form
  * <tt>{0}.{1}.{2}</tt>:
  * * \p {0}: \p XML_ATTR_GENERATION_ADMIN
  * * \p {1}: \p XML_ATTR_GENERATION
  * * \p {2}: \p XML_ATTR_NUMUPDATES
  *
  * Lines containing \p "---" describe removals and end with the patch format
  * number. Lines containing \p "+++" describe additions and end with the patch
  * digest.
  *
  * \param[in,out] out       Output object
  * \param[in]     patchset  XML patchset to output
  *
  * \return Standard Pacemaker return code
  *
  * \note This function produces output only for text-like formats.
  */
 static int
 xml_show_patchset_header(pcmk__output_t *out, const xmlNode *patchset)
 {
     int rc = pcmk_rc_no_output;
     int add[] = { 0, 0, 0 };
     int del[] = { 0, 0, 0 };
 
     xml_patch_versions(patchset, add, del);
 
     if ((add[0] != del[0]) || (add[1] != del[1]) || (add[2] != del[2])) {
         const char *fmt = crm_element_value(patchset, "format");
         const char *digest = crm_element_value(patchset, XML_ATTR_DIGEST);
 
         out->info(out, "Diff: --- %d.%d.%d %s", del[0], del[1], del[2], fmt);
         rc = out->info(out, "Diff: +++ %d.%d.%d %s",
                        add[0], add[1], add[2], digest);
 
     } else if ((add[0] != 0) || (add[1] != 0) || (add[2] != 0)) {
         rc = out->info(out, "Local-only Change: %d.%d.%d",
                        add[0], add[1], add[2]);
     }
 
     return rc;
 }
 
 /*!
  * \internal
  * \brief Output a user-friendly form of XML additions or removals
  *
  * \param[in,out] out      Output object
  * \param[in]     prefix   String to prepend to every line of output
  * \param[in]     data     XML node to output
  * \param[in]     depth    Current indentation level
  * \param[in]     options  Group of \p pcmk__xml_fmt_options flags
  *
  * \return Standard Pacemaker return code
  *
  * \note This function produces output only for text-like formats.
  */
 static int
 xml_show_patchset_v1_recursive(pcmk__output_t *out, const char *prefix,
                                const xmlNode *data, int depth, uint32_t options)
 {
     if (!xml_has_children(data)
         || (crm_element_value(data, XML_DIFF_MARKER) != NULL)) {
 
         // Found a change; clear the pcmk__xml_fmt_diff_short option if set
         options &= ~pcmk__xml_fmt_diff_short;
 
         if (pcmk_is_set(options, pcmk__xml_fmt_diff_plus)) {
             prefix = PCMK__XML_PREFIX_CREATED;
         } else {    // pcmk_is_set(options, pcmk__xml_fmt_diff_minus)
             prefix = PCMK__XML_PREFIX_DELETED;
         }
     }
 
     if (pcmk_is_set(options, pcmk__xml_fmt_diff_short)) {
         int rc = pcmk_rc_no_output;
 
         // Keep looking for the actual change
         for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL;
              child = pcmk__xml_next(child)) {
             int temp_rc = xml_show_patchset_v1_recursive(out, prefix, child,
                                                          depth + 1, options);
             rc = pcmk__output_select_rc(rc, temp_rc);
         }
         return rc;
-
     }
 
     return pcmk__xml_show(out, prefix, data, depth,
                           options
                           |pcmk__xml_fmt_open
                           |pcmk__xml_fmt_children
                           |pcmk__xml_fmt_close);
 }
 
 /*!
  * \internal
  * \brief Output a user-friendly form of an XML patchset (format 1)
  *
  * This function parses an XML patchset (an \p XML_ATTR_DIFF element and its
  * children) into a user-friendly combined diff output.
  *
  * \param[in,out] out       Output object
  * \param[in]     patchset  XML patchset to output
  * \param[in]     options   Group of \p pcmk__xml_fmt_options flags
  *
  * \return Standard Pacemaker return code
  *
  * \note This function produces output only for text-like formats.
  */
 static int
 xml_show_patchset_v1(pcmk__output_t *out, const xmlNode *patchset,
                      uint32_t options)
 {
     const xmlNode *removed = NULL;
     const xmlNode *added = NULL;
     const xmlNode *child = NULL;
     bool is_first = true;
     int rc = xml_show_patchset_header(out, patchset);
 
     /* It's not clear whether "- " or "+ " ever does *not* get overridden by
      * PCMK__XML_PREFIX_DELETED or PCMK__XML_PREFIX_CREATED in practice.
      * However, v1 patchsets can only exist during rolling upgrades from
      * Pacemaker 1.1.11, so not worth worrying about.
      */
     removed = find_xml_node(patchset, "diff-removed", FALSE);
     for (child = pcmk__xml_first_child(removed); child != NULL;
          child = pcmk__xml_next(child)) {
         int temp_rc = xml_show_patchset_v1_recursive(out, "- ", child, 0,
                                                      options
                                                      |pcmk__xml_fmt_diff_minus);
         rc = pcmk__output_select_rc(rc, temp_rc);
 
         if (is_first) {
             is_first = false;
         } else {
             rc = pcmk__output_select_rc(rc, out->info(out, " --- "));
         }
     }
 
     is_first = true;
     added = find_xml_node(patchset, "diff-added", FALSE);
     for (child = pcmk__xml_first_child(added); child != NULL;
          child = pcmk__xml_next(child)) {
         int temp_rc = xml_show_patchset_v1_recursive(out, "+ ", child, 0,
                                                      options
                                                      |pcmk__xml_fmt_diff_plus);
         rc = pcmk__output_select_rc(rc, temp_rc);
 
         if (is_first) {
             is_first = false;
         } else {
             rc = pcmk__output_select_rc(rc, out->info(out, " +++ "));
         }
     }
 
     return rc;
 }
 
 /*!
  * \internal
  * \brief Output a user-friendly form of an XML patchset (format 2)
  *
  * This function parses an XML patchset (an \p XML_ATTR_DIFF element and its
  * children) into a user-friendly combined diff output.
  *
  * \param[in,out] out       Output object
  * \param[in]     patchset  XML patchset to output
  *
  * \return Standard Pacemaker return code
  *
  * \note This function produces output only for text-like formats.
  */
 static int
 xml_show_patchset_v2(pcmk__output_t *out, const xmlNode *patchset)
 {
     int rc = xml_show_patchset_header(out, patchset);
     int temp_rc = pcmk_rc_no_output;
 
     for (const xmlNode *change = pcmk__xml_first_child(patchset);
          change != NULL; change = pcmk__xml_next(change)) {
         const char *op = crm_element_value(change, XML_DIFF_OP);
         const char *xpath = crm_element_value(change, XML_DIFF_PATH);
 
         if (op == NULL) {
             continue;
         }
 
         if (strcmp(op, "create") == 0) {
             char *prefix = crm_strdup_printf(PCMK__XML_PREFIX_CREATED " %s: ",
                                              xpath);
 
             temp_rc = pcmk__xml_show(out, prefix, change->children, 0,
                                      pcmk__xml_fmt_pretty|pcmk__xml_fmt_open);
             rc = pcmk__output_select_rc(rc, temp_rc);
 
             // Overwrite all except the first two characters with spaces
             for (char *ch = prefix + 2; *ch != '\0'; ch++) {
                 *ch = ' ';
             }
 
             temp_rc = pcmk__xml_show(out, prefix, change->children, 0,
                                      pcmk__xml_fmt_pretty
                                      |pcmk__xml_fmt_children
                                      |pcmk__xml_fmt_close);
             rc = pcmk__output_select_rc(rc, temp_rc);
             free(prefix);
 
         } else if (strcmp(op, "move") == 0) {
             const char *position = crm_element_value(change, XML_DIFF_POSITION);
 
             temp_rc = out->info(out,
                                 PCMK__XML_PREFIX_MOVED " %s moved to offset %s",
                                 xpath, position);
             rc = pcmk__output_select_rc(rc, temp_rc);
 
         } else if (strcmp(op, "modify") == 0) {
             xmlNode *clist = first_named_child(change, XML_DIFF_LIST);
             GString *buffer_set = NULL;
             GString *buffer_unset = NULL;
 
             for (const xmlNode *child = pcmk__xml_first_child(clist);
                  child != NULL; child = pcmk__xml_next(child)) {
                 const char *name = crm_element_value(child, "name");
 
                 op = crm_element_value(child, XML_DIFF_OP);
                 if (op == NULL) {
                     continue;
                 }
 
                 if (strcmp(op, "set") == 0) {
                     const char *value = crm_element_value(child, "value");
 
                     pcmk__add_separated_word(&buffer_set, 256, "@", ", ");
                     pcmk__g_strcat(buffer_set, name, "=", value, NULL);
 
                 } else if (strcmp(op, "unset") == 0) {
                     pcmk__add_separated_word(&buffer_unset, 256, "@", ", ");
                     g_string_append(buffer_unset, name);
                 }
             }
 
             if (buffer_set != NULL) {
                 temp_rc = out->info(out, "+  %s:  %s", xpath, buffer_set->str);
                 rc = pcmk__output_select_rc(rc, temp_rc);
                 g_string_free(buffer_set, TRUE);
             }
 
             if (buffer_unset != NULL) {
                 temp_rc = out->info(out, "-- %s:  %s",
                                     xpath, buffer_unset->str);
                 rc = pcmk__output_select_rc(rc, temp_rc);
                 g_string_free(buffer_unset, TRUE);
             }
 
         } else if (strcmp(op, "delete") == 0) {
             int position = -1;
 
             crm_element_value_int(change, XML_DIFF_POSITION, &position);
             if (position >= 0) {
                 temp_rc = out->info(out, "-- %s (%d)", xpath, position);
             } else {
                 temp_rc = out->info(out, "-- %s", xpath);
             }
             rc = pcmk__output_select_rc(rc, temp_rc);
         }
     }
 
     return rc;
 }
 
 /*!
  * \internal
  * \brief Output a user-friendly form of an XML patchset
  *
  * This function parses an XML patchset (an \p XML_ATTR_DIFF element and its
- * children) into a user-friendly combined diff output. Depending on the value
- * of \p log_level, the output may be written to \p stdout or to a log file.
+ * children) into a user-friendly combined diff output.
  *
- * \param[in] log_level  Priority at which to log the messages
- * \param[in] patchset   XML patchset to log
+ * \param[in,out] out   Output object
+ * \param[in]     args  Message-specific arguments
  *
  * \return Standard Pacemaker return code
+ *
+ * \note \p args should contain only the XML patchset
  */
-int
-pcmk__xml_log_patchset(uint8_t log_level, const xmlNode *patchset)
+PCMK__OUTPUT_ARGS("xml-patchset", "xmlNodePtr")
+static int
+xml_patchset_default(pcmk__output_t *out, va_list args)
 {
-    pcmk__output_t *out = NULL;
+    xmlNodePtr patchset = va_arg(args, xmlNodePtr);
+
+    int format = 1;
+
+    if (patchset == NULL) {
+        crm_trace("Empty patch");
+        return pcmk_rc_no_output;
+    }
+
+    crm_element_value_int(patchset, "format", &format);
+    switch (format) {
+        case 1:
+            return xml_show_patchset_v1(out, patchset, pcmk__xml_fmt_pretty);
+        case 2:
+            return xml_show_patchset_v2(out, patchset);
+        default:
+            crm_err("Unknown patch format: %d", format);
+            return pcmk_rc_unknown_format;
+    }
+}
+
+/*!
+ * \internal
+ * \brief Output a user-friendly form of an XML patchset
+ *
+ * This function parses an XML patchset (an \p XML_ATTR_DIFF element and its
+ * children) into a user-friendly combined diff output.
+ *
+ * \param[in,out] out   Output object
+ * \param[in]     args  Message-specific arguments
+ *
+ * \return Standard Pacemaker return code
+ *
+ * \note \p args should contain only the XML patchset
+ */
+PCMK__OUTPUT_ARGS("xml-patchset", "xmlNodePtr")
+static int
+xml_patchset_log(pcmk__output_t *out, va_list args)
+{
+    static struct qb_log_callsite *patchset_cs = NULL;
+
+    xmlNodePtr patchset = va_arg(args, xmlNodePtr);
+
+    uint8_t log_level = pcmk__output_get_log_level(out);
     int format = 1;
-    int rc = pcmk_rc_ok;
 
+    if (log_level == LOG_NEVER) {
+        return pcmk_rc_no_output;
+    }
+
+    if (patchset == NULL) {
+        crm_trace("Empty patch");
+        return pcmk_rc_no_output;
+    }
+
+    if (patchset_cs == NULL) {
+        patchset_cs = qb_log_callsite_get(__func__, __FILE__, "xml-patchset",
+                                          log_level, __LINE__,
+                                          crm_trace_nonlog);
+    }
+
+    if (!crm_is_callsite_active(patchset_cs, log_level, crm_trace_nonlog)) {
+        // Nothing would be logged, so skip all the work
+        return pcmk_rc_no_output;
+    }
+
+    crm_element_value_int(patchset, "format", &format);
+    switch (format) {
+        case 1:
+            if (log_level < LOG_DEBUG) {
+                return xml_show_patchset_v1(out, patchset,
+                                            pcmk__xml_fmt_pretty
+                                            |pcmk__xml_fmt_diff_short);
+            }
+            return xml_show_patchset_v1(out, patchset, pcmk__xml_fmt_pretty);
+        case 2:
+            return xml_show_patchset_v2(out, patchset);
+        default:
+            crm_err("Unknown patch format: %d", format);
+            return pcmk_rc_unknown_format;
+    }
+}
+
+/*!
+ * \internal
+ * \brief Output an XML patchset
+ *
+ * This function outputs an XML patchset (an \p XML_ATTR_DIFF element and its
+ * children) without modification, as a CDATA block.
+ *
+ * \param[in,out] out   Output object
+ * \param[in]     args  Message-specific arguments
+ *
+ * \return Standard Pacemaker return code
+ *
+ * \note \p args should contain only the XML patchset
+ */
+PCMK__OUTPUT_ARGS("xml-patchset", "xmlNodePtr")
+static int
+xml_patchset_xml(pcmk__output_t *out, va_list args)
+{
+    xmlNodePtr patchset = va_arg(args, xmlNodePtr);
+
+    if (patchset != NULL) {
+        char *buf = dump_xml_formatted_with_text(patchset);
+
+        out->output_xml(out, "xml-patchset", buf);
+        free(buf);
+        return pcmk_rc_ok;
+    }
+    crm_trace("Empty patch");
+    return pcmk_rc_no_output;
+}
+
+static pcmk__message_entry_t fmt_functions[] = {
+    { "xml-patchset", "default", xml_patchset_default },
+    { "xml-patchset", "log", xml_patchset_log },
+    { "xml-patchset", "xml", xml_patchset_xml },
+
+    { NULL, NULL, NULL }
+};
+
+/*!
+ * \internal
+ * \brief Register the formatting functions for XML patchsets
+ *
+ * \param[in,out] out  Output object
+ */
+void
+pcmk__register_patchset_messages(pcmk__output_t *out) {
+    pcmk__register_messages(out, fmt_functions);
+}
+
+// Deprecated functions kept only for backward API compatibility
+// LCOV_EXCL_START
+
+#include <crm/common/xml_compat.h>
+
+void
+xml_log_patchset(uint8_t log_level, const char *function,
+                 const xmlNode *patchset)
+{
+    /* This function has some duplication relative to the message functions.
+     * This way, we can maintain the const xmlNode * in the signature. The
+     * message functions must be non-const. They have to support XML output
+     * objects, which must make a copy of a the patchset, requiring a non-const
+     * function call.
+     *
+     * In contrast, this legacy function doesn't need to support XML output.
+     */
     static struct qb_log_callsite *patchset_cs = NULL;
 
+    pcmk__output_t *out = NULL;
+    int format = 1;
+    int rc = pcmk_rc_no_output;
+
     switch (log_level) {
         case LOG_NEVER:
-            return pcmk_rc_no_output;
+            return;
         case LOG_STDOUT:
-            rc = pcmk__text_output_new(&out, NULL);
-            CRM_CHECK(rc == pcmk_rc_ok, return rc);
+            CRM_CHECK(pcmk__text_output_new(&out, NULL) == pcmk_rc_ok, return);
             break;
         default:
             if (patchset_cs == NULL) {
                 patchset_cs = qb_log_callsite_get(__func__, __FILE__,
                                                   "xml-patchset", log_level,
                                                   __LINE__, crm_trace_nonlog);
             }
             if (!crm_is_callsite_active(patchset_cs, log_level,
                                         crm_trace_nonlog)) {
-                return pcmk_rc_no_output;
+                return;
             }
-
-            rc = pcmk__log_output_new(&out);
-            CRM_CHECK(rc == pcmk_rc_ok, return rc);
-
+            CRM_CHECK(pcmk__log_output_new(&out) == pcmk_rc_ok, return);
             pcmk__output_set_log_level(out, log_level);
             break;
     }
 
     if (patchset == NULL) {
         // Should come after the LOG_NEVER check
         crm_trace("Empty patch");
         goto done;
     }
 
     crm_element_value_int(patchset, "format", &format);
     switch (format) {
         case 1:
             if (log_level < LOG_DEBUG) {
                 rc = xml_show_patchset_v1(out, patchset,
                                           pcmk__xml_fmt_pretty
                                           |pcmk__xml_fmt_diff_short);
             } else {    // Note: LOG_STDOUT > LOG_DEBUG
                 rc = xml_show_patchset_v1(out, patchset, pcmk__xml_fmt_pretty);
             }
             break;
         case 2:
             rc = xml_show_patchset_v2(out, patchset);
             break;
         default:
             crm_err("Unknown patch format: %d", format);
             rc = pcmk_rc_unknown_format;
             break;
     }
 
 done:
     out->finish(out, pcmk_rc2exitc(rc), true, NULL);
     pcmk__output_free(out);
-    return rc;
-}
-
-static pcmk__message_entry_t fmt_functions[] = {
-    { NULL, NULL, NULL }
-};
-
-/*!
- * \internal
- * \brief Register the formatting functions for XML patchsets
- *
- * \param[in,out] out  Output object
- */
-void
-pcmk__register_patchset_messages(pcmk__output_t *out) {
-    pcmk__register_messages(out, fmt_functions);
-}
-
-// Deprecated functions kept only for backward API compatibility
-// LCOV_EXCL_START
-
-#include <crm/common/xml_compat.h>
-
-void
-xml_log_patchset(uint8_t log_level, const char *function,
-                 const xmlNode *patchset)
-{
-    pcmk__xml_log_patchset(log_level, patchset);
 }
 
 // LCOV_EXCL_STOP
 // End deprecated API
diff --git a/tools/crm_diff.c b/tools/crm_diff.c
index a00b0c99f9..68433ed44c 100644
--- a/tools/crm_diff.c
+++ b/tools/crm_diff.c
@@ -1,386 +1,399 @@
 /*
  * Copyright 2005-2023 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 <unistd.h>
 #include <stdlib.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <sys/param.h>
 #include <sys/types.h>
 
 #include <crm/crm.h>
 #include <crm/msg_xml.h>
 #include <crm/common/cmdline_internal.h>
 #include <crm/common/output_internal.h>
 #include <crm/common/xml.h>
 #include <crm/common/ipc.h>
 #include <crm/cib.h>
 
 #define SUMMARY "Compare two Pacemaker configurations (in XML format) to produce a custom diff-like output, " \
                 "or apply such an output as a patch"
 
 struct {
     gboolean apply;
     gboolean as_cib;
     gboolean no_version;
     gboolean raw_1;
     gboolean raw_2;
     gboolean use_stdin;
     char *xml_file_1;
     char *xml_file_2;
 } options;
 
 gboolean new_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean original_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean patch_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 
 static GOptionEntry original_xml_entries[] = {
     { "original", 'o', 0, G_OPTION_ARG_STRING, &options.xml_file_1,
       "XML is contained in the named file",
       "FILE" },
     { "original-string", 'O', 0, G_OPTION_ARG_CALLBACK, original_string_cb,
       "XML is contained in the supplied string",
       "STRING" },
 
     { NULL }
 };
 
 static GOptionEntry operation_entries[] = {
     { "new", 'n', 0, G_OPTION_ARG_STRING, &options.xml_file_2,
       "Compare the original XML to the contents of the named file",
       "FILE" },
     { "new-string", 'N', 0, G_OPTION_ARG_CALLBACK, new_string_cb,
       "Compare the original XML with the contents of the supplied string",
       "STRING" },
     { "patch", 'p', 0, G_OPTION_ARG_CALLBACK, patch_cb,
       "Patch the original XML with the contents of the named file",
       "FILE" },
 
     { NULL }
 };
 
 static GOptionEntry addl_entries[] = {
     { "cib", 'c', 0, G_OPTION_ARG_NONE, &options.as_cib,
       "Compare/patch the inputs as a CIB (includes versions details)",
       NULL },
     { "stdin", 's', 0, G_OPTION_ARG_NONE, &options.use_stdin,
       "",
       NULL },
     { "no-version", 'u', 0, G_OPTION_ARG_NONE, &options.no_version,
       "Generate the difference without versions details",
       NULL },
 
     { NULL }
 };
 
 gboolean
 new_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.raw_2 = TRUE;
     pcmk__str_update(&options.xml_file_2, optarg);
     return TRUE;
 }
 
 gboolean
 original_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.raw_1 = TRUE;
     pcmk__str_update(&options.xml_file_1, optarg);
     return TRUE;
 }
 
 gboolean
 patch_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.apply = TRUE;
     pcmk__str_update(&options.xml_file_2, optarg);
     return TRUE;
 }
 
 static void
 print_patch(xmlNode *patch)
 {
     char *buffer = dump_xml_formatted(patch);
 
     printf("%s", pcmk__s(buffer, "<null>\n"));
     free(buffer);
     fflush(stdout);
 }
 
 // \return Standard Pacemaker return code
 static int
 apply_patch(xmlNode *input, xmlNode *patch, gboolean as_cib)
 {
     xmlNode *output = copy_xml(input);
     int rc = xml_apply_patchset(output, patch, as_cib);
 
     rc = pcmk_legacy2rc(rc);
     if (rc != pcmk_rc_ok) {
         fprintf(stderr, "Could not apply patch: %s\n", pcmk_rc_str(rc));
         free_xml(output);
         return rc;
     }
 
     if (output != NULL) {
         const char *version;
         char *buffer;
 
         print_patch(output);
 
         version = crm_element_value(output, XML_ATTR_CRM_VERSION);
         buffer = calculate_xml_versioned_digest(output, FALSE, TRUE, version);
         crm_trace("Digest: %s", pcmk__s(buffer, "<null>\n"));
         free(buffer);
         free_xml(output);
     }
     return pcmk_rc_ok;
 }
 
 static void
 log_patch_cib_versions(xmlNode *patch)
 {
     int add[] = { 0, 0, 0 };
     int del[] = { 0, 0, 0 };
 
     const char *fmt = NULL;
     const char *digest = NULL;
 
     xml_patch_versions(patch, add, del);
     fmt = crm_element_value(patch, "format");
     digest = crm_element_value(patch, XML_ATTR_DIGEST);
 
     if (add[2] != del[2] || add[1] != del[1] || add[0] != del[0]) {
         crm_info("Patch: --- %d.%d.%d %s", del[0], del[1], del[2], fmt);
         crm_info("Patch: +++ %d.%d.%d %s", add[0], add[1], add[2], digest);
     }
 }
 
 static void
 strip_patch_cib_version(xmlNode *patch, const char **vfields, size_t nvfields)
 {
     int format = 1;
 
     crm_element_value_int(patch, "format", &format);
     if (format == 2) {
         xmlNode *version_xml = find_xml_node(patch, "version", FALSE);
 
         if (version_xml) {
             free_xml(version_xml);
         }
 
     } else {
         int i = 0;
 
         const char *tags[] = {
             XML_TAG_DIFF_REMOVED,
             XML_TAG_DIFF_ADDED,
         };
 
         for (i = 0; i < PCMK__NELEM(tags); i++) {
             xmlNode *tmp = NULL;
             int lpc;
 
             tmp = find_xml_node(patch, tags[i], FALSE);
             if (tmp) {
                 for (lpc = 0; lpc < nvfields; lpc++) {
                     xml_remove_prop(tmp, vfields[lpc]);
                 }
 
                 tmp = find_xml_node(tmp, XML_TAG_CIB, FALSE);
                 if (tmp) {
                     for (lpc = 0; lpc < nvfields; lpc++) {
                         xml_remove_prop(tmp, vfields[lpc]);
                     }
                 }
             }
         }
     }
 }
 
 // \return Standard Pacemaker return code
 static int
 generate_patch(xmlNode *object_1, xmlNode *object_2, const char *xml_file_2,
                gboolean as_cib, gboolean no_version)
 {
     xmlNode *output = NULL;
+    int rc = pcmk_rc_ok;
+
+    pcmk__output_t *logger_out = NULL;
+    int out_rc = pcmk_rc_no_output;
+    int temp_rc = pcmk_rc_no_output;
 
     const char *vfields[] = {
         XML_ATTR_GENERATION_ADMIN,
         XML_ATTR_GENERATION,
         XML_ATTR_NUMUPDATES,
     };
 
+    rc = pcmk__log_output_new(&logger_out);
+    CRM_CHECK(rc == pcmk_rc_ok, return rc);
+
     /* If we're ignoring the version, make the version information
      * identical, so it isn't detected as a change. */
     if (no_version) {
         int lpc;
 
         for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
             crm_copy_xml_element(object_1, object_2, vfields[lpc]);
         }
     }
 
     xml_track_changes(object_2, NULL, object_2, FALSE);
     if(as_cib) {
         xml_calculate_significant_changes(object_1, object_2);
     } else {
         xml_calculate_changes(object_1, object_2);
     }
     crm_log_xml_debug(object_2, (xml_file_2? xml_file_2: "target"));
 
     output = xml_create_patchset(0, object_1, object_2, NULL, FALSE);
 
-    {
-        pcmk__output_t *logger_out = NULL;
-        int rc = pcmk__log_output_new(&logger_out);
-
-        CRM_CHECK(rc == pcmk_rc_ok, {free_xml(output); return rc;});
-
-        pcmk__output_set_log_level(logger_out, LOG_INFO);
-        rc = pcmk__xml_show_changes(logger_out, object_2);
-        logger_out->finish(logger_out, pcmk_rc2exitc(rc), true, NULL);
-        pcmk__output_free(logger_out);
-    }
+    pcmk__output_set_log_level(logger_out, LOG_INFO);
+    out_rc = pcmk__xml_show_changes(logger_out, object_2);
 
     xml_accept_changes(object_2);
 
     if (output == NULL) {
-        return pcmk_rc_ok;
+        goto done;  // rc == pcmk_rc_ok
     }
 
+    /* pcmk_rc_error means there's non-empty diff.
+     * @COMPAT: Choose a more descriptive return code, like one that maps to
+     * CRM_EX_DIGEST?
+     */
+    rc = pcmk_rc_error;
+
     patchset_process_digest(output, object_1, object_2, as_cib);
 
     if (as_cib) {
         log_patch_cib_versions(output);
 
     } else if (no_version) {
         strip_patch_cib_version(output, vfields, PCMK__NELEM(vfields));
     }
 
-    pcmk__xml_log_patchset(LOG_NOTICE, output);
+    pcmk__output_set_log_level(logger_out, LOG_NOTICE);
+    temp_rc = logger_out->message(logger_out, "xml-patchset", output);
+    out_rc = pcmk__output_select_rc(out_rc, temp_rc);
+
     print_patch(output);
     free_xml(output);
-    return pcmk_rc_error;
+
+done:
+    logger_out->finish(logger_out, pcmk_rc2exitc(out_rc), true, NULL);
+    pcmk__output_free(logger_out);
+
+    return rc;
 }
 
 static GOptionContext *
 build_arg_context(pcmk__common_args_t *args) {
     GOptionContext *context = NULL;
 
     const char *description = "Examples:\n\n"
                               "Obtain the two different configuration files by running cibadmin on the two cluster setups to compare:\n\n"
                               "\t# cibadmin --query > cib-old.xml\n\n"
                               "\t# cibadmin --query > cib-new.xml\n\n"
                               "Calculate and save the difference between the two files:\n\n"
                               "\t# crm_diff --original cib-old.xml --new cib-new.xml > patch.xml\n\n"
                               "Apply the patch to the original file:\n\n"
                               "\t# crm_diff --original cib-old.xml --patch patch.xml > updated.xml\n\n"
                               "Apply the patch to the running cluster:\n\n"
                               "\t# cibadmin --patch -x patch.xml\n";
 
     context = pcmk__build_arg_context(args, NULL, NULL, NULL);
     g_option_context_set_description(context, description);
 
     pcmk__add_arg_group(context, "xml", "Original XML:",
                         "Show original XML options", original_xml_entries);
     pcmk__add_arg_group(context, "operation", "Operation:",
                         "Show operation options", operation_entries);
     pcmk__add_arg_group(context, "additional", "Additional Options:",
                         "Show additional options", addl_entries);
     return context;
 }
 
 int
 main(int argc, char **argv)
 {
     xmlNode *object_1 = NULL;
     xmlNode *object_2 = NULL;
 
     crm_exit_t exit_code = CRM_EX_OK;
     GError *error = NULL;
 
     pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
     gchar **processed_args = pcmk__cmdline_preproc(argv, "nopNO");
     GOptionContext *context = build_arg_context(args);
 
     int rc = pcmk_rc_ok;
 
     if (!g_option_context_parse_strv(context, &processed_args, &error)) {
         exit_code = CRM_EX_USAGE;
         goto done;
     }
 
     pcmk__cli_init_logging("crm_diff", args->verbosity);
 
     if (args->version) {
         g_strfreev(processed_args);
         pcmk__free_arg_context(context);
         /* FIXME:  When crm_diff is converted to use formatted output, this can go. */
         pcmk__cli_help('v');
     }
 
     if (options.apply && options.no_version) {
         fprintf(stderr, "warning: -u/--no-version ignored with -p/--patch\n");
     } else if (options.as_cib && options.no_version) {
         fprintf(stderr, "error: -u/--no-version incompatible with -c/--cib\n");
         exit_code = CRM_EX_USAGE;
         goto done;
     }
 
     if (options.raw_1) {
         object_1 = string2xml(options.xml_file_1);
 
     } else if (options.use_stdin) {
         fprintf(stderr, "Input first XML fragment:");
         object_1 = stdin2xml();
 
     } else if (options.xml_file_1 != NULL) {
         object_1 = filename2xml(options.xml_file_1);
     }
 
     if (options.raw_2) {
         object_2 = string2xml(options.xml_file_2);
 
     } else if (options.use_stdin) {
         fprintf(stderr, "Input second XML fragment:");
         object_2 = stdin2xml();
 
     } else if (options.xml_file_2 != NULL) {
         object_2 = filename2xml(options.xml_file_2);
     }
 
     if (object_1 == NULL) {
         fprintf(stderr, "Could not parse the first XML fragment\n");
         exit_code = CRM_EX_DATAERR;
         goto done;
     }
     if (object_2 == NULL) {
         fprintf(stderr, "Could not parse the second XML fragment\n");
         exit_code = CRM_EX_DATAERR;
         goto done;
     }
 
     if (options.apply) {
         rc = apply_patch(object_1, object_2, options.as_cib);
     } else {
         rc = generate_patch(object_1, object_2, options.xml_file_2, options.as_cib, options.no_version);
     }
     exit_code = pcmk_rc2exitc(rc);
 
 done:
     g_strfreev(processed_args);
     pcmk__free_arg_context(context);
     free(options.xml_file_1);
     free(options.xml_file_2);
     free_xml(object_1);
     free_xml(object_2);
 
     pcmk__output_and_clear_error(error, NULL);
     crm_exit(exit_code);
 }
diff --git a/tools/crm_shadow.c b/tools/crm_shadow.c
index d4ec1117f7..0d0845c3c0 100644
--- a/tools/crm_shadow.c
+++ b/tools/crm_shadow.c
@@ -1,683 +1,692 @@
 /*
  * Copyright 2004-2023 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 <unistd.h>
 
 #include <sys/param.h>
 #include <crm/crm.h>
 #include <sys/stat.h>
 #include <sys/types.h>
 
 #include <stdlib.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <crm/msg_xml.h>
 
 #include <crm/common/cmdline_internal.h>
 #include <crm/common/ipc.h>
 #include <crm/common/xml.h>
 
 #include <crm/cib.h>
 #include <crm/cib/internal.h>
 
 #define SUMMARY "perform Pacemaker configuration changes in a sandbox\n\n"  \
                 "This command sets up an environment in which "             \
                 "configuration tools (cibadmin,\n"                          \
                 "crm_resource, etc.) work offline instead of against a "    \
                 "live cluster, allowing\n"                                  \
                 "changes to be previewed and tested for side effects."
 
 #define INDENT "                              "
 
 enum shadow_command {
     shadow_cmd_none = 0,
     shadow_cmd_which,
     shadow_cmd_display,
     shadow_cmd_diff,
     shadow_cmd_file,
     shadow_cmd_create,
     shadow_cmd_create_empty,
     shadow_cmd_commit,
     shadow_cmd_delete,
     shadow_cmd_edit,
     shadow_cmd_reset,
     shadow_cmd_switch,
 };
 
 static crm_exit_t exit_code = CRM_EX_OK;
 
 static cib_t *real_cib = NULL;
 
 static struct {
     enum shadow_command cmd;
     int cmd_options;
     char *shadow;
     gboolean force;
     gboolean batch;
     gboolean full_upload;
     gchar *validate_with;
 } options = {
     .cmd_options = cib_sync_call,
 };
 
 #if 0
 // @COMPAT Possibly enable this at next backward compatibility break
 #define SET_COMMAND(command) do {                                       \
         if (options.cmd != shadow_cmd_none) {                           \
             g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_USAGE,         \
                         "Only one command option may be specified");    \
             return FALSE;                                               \
         }                                                               \
         options.cmd = (command);                                        \
     } while (0)
 #else
 #define SET_COMMAND(command) do {   \
         options.cmd = (command);    \
     } while (0)
 #endif
 
 static char *
 get_shadow_prompt(const char *name)
 {
     return crm_strdup_printf("shadow[%.40s] # ", name);
 }
 
 static void
 shadow_setup(char *name, gboolean do_switch)
 {
     const char *prompt = getenv("PS1");
     const char *shell = getenv("SHELL");
     char *new_prompt = get_shadow_prompt(name);
 
     printf("Setting up shadow instance\n");
 
     if (pcmk__str_eq(new_prompt, prompt, pcmk__str_casei)) {
         /* nothing to do */
         goto done;
 
     } else if (!options.batch && (shell != NULL)) {
         setenv("PS1", new_prompt, 1);
         setenv("CIB_shadow", name, 1);
         printf("Type Ctrl-D to exit the crm_shadow shell\n");
 
         if (strstr(shell, "bash")) {
             execl(shell, shell, "--norc", "--noprofile", NULL);
         } else {
             execl(shell, shell, NULL);
         }
 
     } else if (do_switch) {
         printf("To switch to the named shadow instance, paste the following into your shell:\n");
 
     } else {
         printf
             ("A new shadow instance was created.  To begin using it paste the following into your shell:\n");
     }
     printf("  CIB_shadow=%s ; export CIB_shadow\n", name);
 
   done:
     free(new_prompt);
 }
 
 static void
 shadow_teardown(char *name)
 {
     const char *prompt = getenv("PS1");
     char *our_prompt = get_shadow_prompt(name);
 
     if (prompt != NULL && strstr(prompt, our_prompt)) {
         printf("Now type Ctrl-D to exit the crm_shadow shell\n");
 
     } else {
         printf
             ("Please remember to unset the CIB_shadow variable by pasting the following into your shell:\n");
         printf("  unset CIB_shadow\n");
     }
     free(our_prompt);
 }
 
 static bool
 cmd_is_dangerous(enum shadow_command cmd)
 {
     switch (cmd) {
         case shadow_cmd_commit:
         case shadow_cmd_delete:
             return true;
         default:
             return false;
     }
 }
 
 static gboolean
 command_cb(const gchar *option_name, const gchar *optarg, gpointer data,
            GError **error)
 {
     if (pcmk__str_any_of(option_name, "-w", "--which", NULL)) {
         SET_COMMAND(shadow_cmd_which);
 
     } else if (pcmk__str_any_of(option_name, "-p", "--display", NULL)) {
         SET_COMMAND(shadow_cmd_display);
 
     } else if (pcmk__str_any_of(option_name, "-d", "--diff", NULL)) {
         SET_COMMAND(shadow_cmd_diff);
 
     } else if (pcmk__str_any_of(option_name, "-F", "--file", NULL)) {
         SET_COMMAND(shadow_cmd_file);
 
     } else if (pcmk__str_any_of(option_name, "-c", "--create", NULL)) {
         SET_COMMAND(shadow_cmd_create);
 
     } else if (pcmk__str_any_of(option_name, "-e", "--create-empty", NULL)) {
         SET_COMMAND(shadow_cmd_create_empty);
 
     } else if (pcmk__str_any_of(option_name, "-C", "--commit", NULL)) {
         SET_COMMAND(shadow_cmd_commit);
 
     } else if (pcmk__str_any_of(option_name, "-D", "--delete", NULL)) {
         SET_COMMAND(shadow_cmd_delete);
 
     } else if (pcmk__str_any_of(option_name, "-E", "--edit", NULL)) {
         SET_COMMAND(shadow_cmd_edit);
 
     } else if (pcmk__str_any_of(option_name, "-r", "--reset", NULL)) {
         SET_COMMAND(shadow_cmd_reset);
 
     } else if (pcmk__str_any_of(option_name, "-s", "--switch", NULL)) {
         SET_COMMAND(shadow_cmd_switch);
 
     } else {
         // Should be impossible
         return FALSE;
     }
 
     // optarg may be NULL and that's okay
     pcmk__str_update(&options.shadow, optarg);
     return TRUE;
 }
 
 static GOptionEntry query_entries[] = {
     { "which", 'w', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Indicate the active shadow copy", NULL },
 
     { "display", 'p', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Display the contents of the active shadow copy", NULL },
 
     { "diff", 'd', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Display the changes in the active shadow copy", NULL },
 
     { "file", 'F', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Display the location of the active shadow copy file", NULL },
 
     { NULL }
 };
 
 static GOptionEntry command_entries[] = {
     { "create", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
       "Create the named shadow copy of the active cluster configuration",
       "name" },
 
     { "create-empty", 'e', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK,
       command_cb,
       "Create the named shadow copy with an empty cluster configuration.\n"
       INDENT "Optional: --validate-with", "name" },
 
     { "commit", 'C', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
       "Upload the contents of the named shadow copy to the cluster", "name" },
 
     { "delete", 'D', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
       "Delete the contents of the named shadow copy", "name" },
 
     { "edit", 'E', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Edit the contents of the active shadow copy with your favorite $EDITOR",
       NULL },
 
     { "reset", 'r', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
       "Recreate named shadow copy from the active cluster configuration",
       "name" },
 
     { "switch", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
       "(Advanced) Switch to the named shadow copy", "name" },
 
     { NULL }
 };
 
 static GOptionEntry addl_entries[] = {
     { "force", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.force,
       "(Advanced) Force the action to be performed", NULL },
 
     { "batch", 'b', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.batch,
       "(Advanced) Don't spawn a new shell", NULL },
 
     { "all", 'a', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.full_upload,
       "(Advanced) Upload entire CIB, including status, with --commit", NULL },
 
     { "validate-with", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING,
       &options.validate_with,
       "(Advanced) Create an older configuration version", NULL },
 
     { NULL }
 };
 
 static GOptionContext *
 build_arg_context(pcmk__common_args_t *args)
 {
     const char *desc = NULL;
     GOptionContext *context = NULL;
 
     desc = "Examples:\n\n"
            "Create a blank shadow configuration:\n\n"
            "\t# crm_shadow --create-empty myShadow\n\n"
            "Create a shadow configuration from the running cluster\n\n"
            "\t# crm_shadow --create myShadow\n\n"
            "Display the current shadow configuration:\n\n"
            "\t# crm_shadow --display\n\n"
            "Discard the current shadow configuration (named myShadow):\n\n"
            "\t# crm_shadow --delete myShadow --force\n\n"
            "Upload current shadow configuration (named myShadow) to running "
            "cluster:\n\n"
            "\t# crm_shadow --commit myShadow\n\n";
 
     context = pcmk__build_arg_context(args, NULL, NULL, "<query>|<command>");
     g_option_context_set_description(context, desc);
 
     pcmk__add_arg_group(context, "queries", "Queries:",
                         "Show query help", query_entries);
     pcmk__add_arg_group(context, "commands", "Commands:",
                         "Show command help", command_entries);
     pcmk__add_arg_group(context, "additional", "Additional Options:",
                         "Show additional options", addl_entries);
     return context;
 }
 
 int
 main(int argc, char **argv)
 {
     int rc = pcmk_ok;
     char *shadow_file = NULL;
     bool needs_teardown = false;
     struct stat buf;
 
     GError *error = NULL;
 
     pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
     gchar **processed_args = pcmk__cmdline_preproc(argv, "ceCDrsv");
     GOptionContext *context = build_arg_context(args);
 
     crm_log_preinit(NULL, argc, argv);
 
     if (!g_option_context_parse_strv(context, &processed_args, &error)) {
         exit_code = CRM_EX_USAGE;
         goto done;
     }
 
     if (g_strv_length(processed_args) > 1) {
         gchar *help = g_option_context_get_help(context, TRUE, NULL);
         GString *extra = g_string_sized_new(128);
 
         for (int lpc = 1; processed_args[lpc] != NULL; lpc++) {
             if (extra->len > 0) {
                 g_string_append_c(extra, ' ');
             }
             g_string_append(extra, processed_args[lpc]);
         }
 
         exit_code = CRM_EX_USAGE;
         g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                     "non-option ARGV-elements: %s\n\n%s", extra->str, help);
         g_free(help);
         g_string_free(extra, TRUE);
         goto done;
     }
 
     if (args->version) {
         g_strfreev(processed_args);
         pcmk__free_arg_context(context);
 
         /* FIXME: When crm_shadow is converted to use formatted output,
          * this can go.
          */
         pcmk__cli_help('v');
     }
 
     if (options.cmd == shadow_cmd_none) {
         // @COMPAT: Create a default command if other tools have one
         gchar *help = g_option_context_get_help(context, TRUE, NULL);
 
         exit_code = CRM_EX_USAGE;
         g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                     "Must specify a query or command option\n\n%s", help);
         g_free(help);
         goto done;
     }
 
     pcmk__cli_init_logging("crm_shadow", args->verbosity);
 
     if (args->verbosity > 0) {
         cib__set_call_options(options.cmd_options, crm_system_name,
                               cib_verbose);
     }
 
     if (options.force) {
         cib__set_call_options(options.cmd_options, crm_system_name,
                               cib_quorum_override);
     }
 
     // Some commands get options.shadow from the environment
     switch (options.cmd) {
         case shadow_cmd_which:
         case shadow_cmd_display:
         case shadow_cmd_diff:
         case shadow_cmd_file:
         case shadow_cmd_edit:
             pcmk__str_update(&options.shadow, getenv("CIB_shadow"));
             if (options.shadow == NULL) {
                 exit_code = CRM_EX_NOSUCH;
                 g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                             "No active shadow configuration defined");
                 goto done;
             }
             break;
         default:
             // The rest already set options.shadow from their optarg
             break;
     }
 
     if (options.cmd == shadow_cmd_which) {
         // Show the active shadow instance
         printf("%s\n", options.shadow);
         goto done;
     }
 
     // Check for shadow instance mismatch
     if ((options.cmd != shadow_cmd_switch)
         && (options.cmd != shadow_cmd_create)) {
 
         const char *local = getenv("CIB_shadow");
 
         if ((local != NULL)
             && !pcmk__str_eq(local, options.shadow, pcmk__str_none)
             && !options.force) {
             exit_code = CRM_EX_USAGE;
             g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                         "The supplied shadow instance (%s) is not the same as "
                         "the active one (%s).\n"
                         "To prevent accidental destruction of the cluster, the "
                         "--force flag is required in order to proceed.",
                         options.shadow, local);
             goto done;
         }
     }
 
     // Check for dangerous commands
     if (cmd_is_dangerous(options.cmd) && !options.force) {
         exit_code = CRM_EX_USAGE;
         g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                     "The supplied command is considered dangerous.\n"
                     "To prevent accidental destruction of the cluster, the "
                     "--force flag is required in order to proceed.");
         goto done;
     }
 
     shadow_file = get_shadow_file(options.shadow);
 
     if (options.cmd == shadow_cmd_delete) {
         // Delete the shadow file
         if ((unlink(shadow_file) < 0) && (errno != ENOENT)) {
             exit_code = pcmk_rc2exitc(errno);
             g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                         "Could not remove shadow instance '%s': %s",
                         options.shadow, strerror(errno));
         }
         needs_teardown = true;
         goto done;
     }
 
     if (options.cmd == shadow_cmd_file) {
         // Show the shadow file path
         printf("%s\n", shadow_file);
         goto done;
     }
 
     // Connect to the CIB if necessary
     switch (options.cmd) {
         case shadow_cmd_commit:
         case shadow_cmd_create:
         case shadow_cmd_diff:
         case shadow_cmd_reset:
             real_cib = cib_new_no_shadow();
             rc = real_cib->cmds->signon(real_cib, crm_system_name, cib_command);
             if (rc != pcmk_ok) {
                 rc = pcmk_legacy2rc(rc);
                 exit_code = pcmk_rc2exitc(rc);
                 g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                             "Could not connect to CIB: %s", pcmk_rc_str(rc));
                 goto done;
             }
             break;
         default:
             break;
     }
 
     // Check existence of the shadow file
     rc = stat(shadow_file, &buf);
     switch (options.cmd) {
         case shadow_cmd_create:
         case shadow_cmd_create_empty:
             if ((rc == 0) && !options.force) {
                 exit_code = CRM_EX_CANTCREAT;
                 g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                             "A shadow instance '%s' already exists.\n"
                             "To prevent accidental destruction of the cluster, "
                             "the --force flag is required in order to proceed.",
                             options.shadow);
                 goto done;
             }
             break;
         default:
             if (rc < 0) {
                 exit_code = CRM_EX_NOSUCH;
                 g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                             "Could not access shadow instance '%s': %s",
                             options.shadow, strerror(errno));
                 goto done;
             }
             break;
     }
 
     // Run the command if we haven't already
     switch (options.cmd) {
         case shadow_cmd_create:
         case shadow_cmd_create_empty:
         case shadow_cmd_reset:
             // Create or reset the shadow file
             {
                 xmlNode *output = NULL;
 
                 if (options.cmd == shadow_cmd_create_empty) {
                     output = createEmptyCib(0);
                     crm_xml_add(output, XML_ATTR_VALIDATION,
                                 options.validate_with);
                     printf("Created new %s configuration\n",
                            crm_element_value(output, XML_ATTR_VALIDATION));
 
                 } else {
                     // Create a shadow instance based on the current CIB
                     rc = real_cib->cmds->query(real_cib, NULL, &output,
                                                options.cmd_options);
                     if (rc != pcmk_ok) {
                         rc = pcmk_legacy2rc(rc);
                         exit_code = pcmk_rc2exitc(rc);
                         g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                                     "Could not connect to the CIB manager: %s",
                                     pcmk_rc_str(rc));
                         goto done;
                     }
                 }
 
                 rc = write_xml_file(output, shadow_file, FALSE);
                 free_xml(output);
 
                 if (rc < 0) {
                     const char *action = "create";
                     rc = pcmk_legacy2rc(rc);
                     exit_code = pcmk_rc2exitc(rc);
 
                     if (options.cmd == shadow_cmd_reset) {
                         action = "reset";
                     }
 
                     g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                                 "Could not %s the shadow instance '%s': %s",
                                 action, options.shadow, pcmk_rc_str(rc));
                     goto done;
                 }
                 shadow_setup(options.shadow, FALSE);
             }
             break;
 
         case shadow_cmd_edit:
             // Open the shadow file in a text editor
             {
                 const char *editor = getenv("EDITOR");
 
                 if (editor == NULL) {
                     exit_code = CRM_EX_NOT_CONFIGURED;
                     g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                                 "No value for EDITOR defined");
                     goto done;
                 }
 
                 execlp(editor, "--", shadow_file, NULL);
                 exit_code = CRM_EX_OSFILE;
                 g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                             "Could not invoke EDITOR (%s %s): %s",
                             editor, shadow_file, strerror(errno));
                 goto done;
             }
             break;
 
         case shadow_cmd_switch:
             // Switch to the named shadow instance
             shadow_setup(options.shadow, TRUE);
             break;
 
         case shadow_cmd_display:
             // Display the current shadow file contents
             {
                 char *output_s = NULL;
                 xmlNode *output = filename2xml(shadow_file);
 
                 output_s = dump_xml_formatted(output);
                 printf("%s", output_s);
 
                 free(output_s);
                 free_xml(output);
             }
             break;
 
         case shadow_cmd_diff:
             // Diff the shadow file against the cluster
             {
                 xmlNode *diff = NULL;
                 xmlNode *old_config = NULL;
                 xmlNode *new_config = filename2xml(shadow_file);
 
                 rc = real_cib->cmds->query(real_cib, NULL, &old_config,
                                            options.cmd_options);
                 if (rc != pcmk_ok) {
                     rc = pcmk_legacy2rc(rc);
                     exit_code = pcmk_rc2exitc(rc);
                     g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                                 "Could not query the CIB: %s", pcmk_rc_str(rc));
                     goto done;
                 }
 
                 xml_track_changes(new_config, NULL, new_config, false);
                 xml_calculate_changes(old_config, new_config);
 
                 diff = xml_create_patchset(0, old_config, new_config, NULL,
                                            false);
 
                 {
                     pcmk__output_t *logger_out = NULL;
-                    rc = pcmk__log_output_new(&logger_out);
 
+                    rc = pcmk__log_output_new(&logger_out);
                     CRM_CHECK(rc == pcmk_rc_ok,
-                              exit_code = pcmk_rc2exitc(rc); goto done;);
+                              exit_code = pcmk_rc2exitc(rc); goto done);
 
                     pcmk__output_set_log_level(logger_out, LOG_INFO);
                     rc = pcmk__xml_show_changes(logger_out, new_config);
-                    logger_out->finish(logger_out, pcmk_rc2exitc(rc), true, NULL);
+                    logger_out->finish(logger_out, pcmk_rc2exitc(rc), true,
+                                       NULL);
                     pcmk__output_free(logger_out);
-                    rc = pcmk_rc_ok;
                 }
 
                 xml_accept_changes(new_config);
                 if (diff != NULL) {
+                    pcmk__output_t *out = NULL;
+
+                    rc = pcmk__text_output_new(&out, NULL);
+                    CRM_CHECK(rc == pcmk_rc_ok,
+                              exit_code = pcmk_rc2exitc(rc); goto done);
+
+                    rc = out->message(out, "xml-patchset", diff);
+                    out->finish(out, pcmk_rc2exitc(rc), true, NULL);
+                    pcmk__output_free(out);
+
                     /* @COMPAT: Exit with CRM_EX_DIGEST? This is not really an
                      * error; we just want to indicate that there are
                      * differences (as the diff command does).
                      */
-                    pcmk__xml_log_patchset(LOG_STDOUT, diff);
                     exit_code = CRM_EX_ERROR;
                 }
             }
             break;
 
         case shadow_cmd_commit:
             // Commit the shadow file to the cluster
             {
                 xmlNode *input = filename2xml(shadow_file);
                 xmlNode *section_xml = input;
                 const char *section = NULL;
 
                 if (!options.full_upload) {
                     section = XML_CIB_TAG_CONFIGURATION;
                     section_xml = first_named_child(input, section);
                 }
 
                 rc = real_cib->cmds->replace(real_cib, section, section_xml,
                                              options.cmd_options);
                 if (rc != pcmk_ok) {
                     rc = pcmk_legacy2rc(rc);
                     exit_code = pcmk_rc2exitc(rc);
                     g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                                 "Could not commit shadow instance '%s' to the "
                                 "CIB: %s",
                                 options.shadow, pcmk_rc_str(rc));
                     goto done;
                 }
                 needs_teardown = true;
                 free_xml(input);
             }
             break;
 
         default:
             // Should never reach this point
             break;
     }
 
 done:
     g_strfreev(processed_args);
     pcmk__free_arg_context(context);
 
     pcmk__output_and_clear_error(error, NULL);
 
     if (needs_teardown) {
         // Teardown message should be the last thing we output
         shadow_teardown(options.shadow);
     }
     free(shadow_file);
     free(options.shadow);
     g_free(options.validate_with);
     crm_exit(exit_code);
 }