diff --git a/daemons/fenced/pacemaker-fenced.c b/daemons/fenced/pacemaker-fenced.c
index d6ff3c32ee..bab27ef154 100644
--- a/daemons/fenced/pacemaker-fenced.c
+++ b/daemons/fenced/pacemaker-fenced.c
@@ -1,979 +1,981 @@
 /*
  * Copyright 2009-2024 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/common/cmdline_internal.h>
 #include <crm/common/ipc.h>
 #include <crm/common/ipc_internal.h>
 #include <crm/common/output_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 <pacemaker-fenced.h>
 
 #define SUMMARY "daemon for executing fencing devices in a Pacemaker cluster"
 
 char *stonith_our_uname = NULL;
 long long stonith_watchdog_timeout_ms = 0;
 GList *stonith_watchdog_targets = NULL;
 
 static GMainLoop *mainloop = NULL;
 
 gboolean stand_alone = FALSE;
 gboolean stonith_shutdown_flag = FALSE;
 
 static qb_ipcs_service_t *ipcs = 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;
 
 crm_exit_t exit_code = CRM_EX_OK;
 
 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 -ECONNREFUSED;
     }
 
     if (pcmk__new_client(c, uid, gid) == NULL) {
         return -ENOMEM;
     }
     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, PCMK__XE_NACK, NULL, CRM_EX_PROTOCOL);
         return 0;
     }
 
 
     op = crm_element_value(request, PCMK__XA_CRM_TASK);
     if(pcmk__str_eq(op, CRM_OP_RM_NODE_CACHE, pcmk__str_casei)) {
         crm_xml_add(request, PCMK__XA_T, PCMK__VALUE_STONITH_NG);
         crm_xml_add(request, PCMK__XA_ST_OP, op);
         crm_xml_add(request, PCMK__XA_ST_CLIENTID, c->id);
         crm_xml_add(request, PCMK__XA_ST_CLIENTNAME, pcmk__client_name(c));
         crm_xml_add(request, PCMK__XA_ST_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, PCMK__XA_ST_CLIENTNAME);
 
         c->name = crm_strdup_printf("%s.%u", pcmk__s(value, "unknown"), c->pid);
     }
 
     crm_element_value_int(request, PCMK__XA_ST_CALLOPT, &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, PCMK__XA_ST_CLIENTID, c->id);
     crm_xml_add(request, PCMK__XA_ST_CLIENTNAME, pcmk__client_name(c));
     crm_xml_add(request, PCMK__XA_ST_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, PCMK__XA_SRC);
     const char *op = crm_element_value(msg, PCMK__XA_ST_OP);
 
     if (pcmk__str_eq(op, STONITH_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 = pcmk__xml_parse(data);
         if (xml == NULL) {
             crm_err("Invalid XML: '%.120s'", data);
             free(data);
             return;
         }
         crm_xml_add(xml, PCMK__XA_SRC, from);
         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(const 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, PCMK__VALUE_ST_NOTIFY_FENCE, pcmk__str_none)) {
         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, PCMK__VALUE_ST_NOTIFY_HISTORY,
                             pcmk__str_none)) {
         return st_callback_notify_history;
 
     } else if (pcmk__str_eq(name, PCMK__VALUE_ST_NOTIFY_HISTORY_SYNCED,
                             pcmk__str_none)) {
         return st_callback_notify_history_synced;
 
     }
     return st_callback_unknown;
 }
 
 static void
 stonith_notify_client(gpointer key, gpointer value, gpointer user_data)
 {
 
     const 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, PCMK__XA_SUBT);
     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, PCMK__XE_ST_ASYNC_TIMEOUT_VALUE);
     crm_xml_add(notify_data, PCMK__XA_T, PCMK__VALUE_ST_ASYNC_TIMEOUT_VALUE);
     crm_xml_add(notify_data, PCMK__XA_ST_CALLID, call_id);
     crm_xml_add_int(notify_data, PCMK__XA_ST_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, PCMK__XE_NOTIFY);
 
     CRM_LOG_ASSERT(type != NULL);
 
     crm_xml_add(update_msg, PCMK__XA_T, PCMK__VALUE_ST_NOTIFY);
     crm_xml_add(update_msg, PCMK__XA_SUBT, type);
     crm_xml_add(update_msg, PCMK__XA_ST_OP, type);
     stonith__xe_set_result(update_msg, result);
 
     if (data != NULL) {
         add_message_xml(update_msg, PCMK__XA_ST_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 (\c STONITH_OP_DEVICE_ADD,
  *                    \c STONITH_OP_DEVICE_DEL, \c STONITH_OP_LEVEL_ADD, or
  *                    \c STONITH_OP_LEVEL_DEL)
  * \param[in] result  Operation result
  * \param[in] desc    Description of what changed (either device ID or string
  *                    representation of level
  *                    (<tt><target>[<level_index>]</tt>))
  */
 void
 fenced_send_config_notification(const char *op,
                                 const pcmk__action_result_t *result,
                                 const char *desc)
 {
     xmlNode *notify_data = create_xml_node(NULL, op);
 
     CRM_CHECK(notify_data != NULL, return);
 
     crm_xml_add(notify_data, PCMK__XA_ST_DEVICE_ID, desc);
 
     fenced_send_notification(op, result, notify_data);
     free_xml(notify_data);
 }
 
 /*!
  * \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));
 }
 
 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
 stonith_cleanup(void)
 {
     fenced_cib_cleanup();
     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;
 }
 
 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;
 }
 
 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, PCMK__XE_STONITH_COMMAND);
 
         crm_xml_add(query, PCMK__XA_T, PCMK__VALUE_STONITH_NG);
         crm_xml_add(query, PCMK__XA_ST_OP, STONITH_OP_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,
      * flags,
      * short description,
      * long description
      */
     {
         PCMK_STONITH_HOST_ARGUMENT, NULL, "string", NULL,
         "port", NULL,
         pcmk__opt_advanced,
         N_("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, NULL,
         pcmk__opt_none,
         N_("A mapping of node names to port numbers for devices that do not "
             "support node names."),
         N_("For example, \"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, NULL,
         pcmk__opt_none,
         N_("A list of nodes that can be targeted by this device (optional "
             "unless pcmk_host_list=\"static-list\")"),
         N_("For example, \"node1,node2,node3\"."),
     },
     {
         PCMK_STONITH_HOST_CHECK, NULL, "select",
             "dynamic-list, static-list, status, none",
         NULL, NULL,
         pcmk__opt_none,
         N_("How to determine which nodes can be targeted by the device"),
         N_("Use \"dynamic-list\" to query the device via the 'list' command; "
             "\"static-list\" to check the pcmk_host_list attribute; "
             "\"status\" to query the device via the 'status' command; or "
             "\"none\" to assume every device can fence every node. "
             "The default value is \"static-list\" if pcmk_host_map or "
             "pcmk_host_list is set; otherwise \"dynamic-list\" if the device "
             "supports the list operation; otherwise \"status\" if the device "
             "supports the status operation; otherwise \"none\""),
     },
     {
         PCMK_STONITH_DELAY_MAX, NULL, "time", NULL,
         "0s", NULL,
         pcmk__opt_none,
         N_("Enable a delay of no more than the time specified before executing "
             "fencing actions."),
         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,
         pcmk__opt_none,
         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 for each target."),
     },
     {
         PCMK_STONITH_ACTION_LIMIT, NULL, "integer", NULL,
         "1", NULL,
         pcmk__opt_none,
         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. A value of "
             "-1 means an unlimited number of actions can be performed in "
             "parallel."),
     },
     {
         "pcmk_reboot_action", NULL, "string", NULL,
         PCMK_ACTION_REBOOT, NULL,
         pcmk__opt_advanced,
         N_("An alternate command to run instead of 'reboot'"),
         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 'reboot' action."),
     },
     {
         "pcmk_reboot_timeout", NULL, "time", NULL,
         "60s", NULL,
         pcmk__opt_advanced,
         N_("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,
         pcmk__opt_advanced,
         N_("The maximum number of times to try 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. In that case, "
             "Pacemaker will automatically retry the operation if there is time "
             "remaining. Use this option to alter the number of times Pacemaker "
             "tries a 'reboot' action before giving up."),
     },
     {
         "pcmk_off_action", NULL, "string", NULL,
         PCMK_ACTION_OFF, NULL,
         pcmk__opt_advanced,
         N_("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,
         pcmk__opt_advanced,
         N_("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,
         pcmk__opt_advanced,
         N_("The maximum number of times to try 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. In that case, "
             "Pacemaker will automatically retry the operation if there is time "
             "remaining. Use this option to alter the number of times Pacemaker "
             "tries a 'off' action before giving up."),
     },
     {
         "pcmk_on_action", NULL, "string", NULL,
         PCMK_ACTION_ON, NULL,
         pcmk__opt_advanced,
         N_("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,
         pcmk__opt_advanced,
         N_("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,
         pcmk__opt_advanced,
         N_("The maximum number of times to try 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. In that case, "
             "Pacemaker will automatically retry the operation if there is time "
             "remaining. Use this option to alter the number of times Pacemaker "
             "tries a 'on' action before giving up."),
     },
     {
         "pcmk_list_action", NULL, "string", NULL,
         PCMK_ACTION_LIST, NULL,
         pcmk__opt_advanced,
         N_("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,
         pcmk__opt_advanced,
         N_("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,
         pcmk__opt_advanced,
         N_("The maximum number of times to try 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. In that case, "
             "Pacemaker will automatically retry the operation if there is time "
             "remaining. Use this option to alter the number of times Pacemaker "
             "tries a 'list' action before giving up."),
     },
     {
         "pcmk_monitor_action", NULL, "string", NULL,
         PCMK_ACTION_MONITOR, NULL,
         pcmk__opt_advanced,
         N_("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,
         pcmk__opt_advanced,
         N_("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. "
             "Use this to specify an alternate, device-specific, timeout for "
             "'monitor' actions."),
     },
     {
         "pcmk_monitor_retries", NULL, "integer", NULL,
         "2", NULL,
         pcmk__opt_advanced,
         N_("The maximum number of times to try 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. In that case, "
             "Pacemaker will automatically retry the operation if there is time "
             "remaining. Use this option to alter the number of times Pacemaker "
             "tries a 'monitor' action before giving up."),
     },
     {
         "pcmk_status_action", NULL, "string", NULL,
         PCMK_ACTION_STATUS, NULL,
         pcmk__opt_advanced,
         N_("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,
         pcmk__opt_advanced,
         N_("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,
         pcmk__opt_advanced,
         N_("The maximum number of times to try 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. In that case, "
             "Pacemaker will automatically retry the operation if there is time "
             "remaining. Use this option to alter the number of times Pacemaker "
             "tries a 'status' action before giving up."),
     },
 
     { NULL, },
 };
 
 static int
 fencer_metadata(void)
 {
     // @TODO Use pcmk__daemon_metadata when fencer_options moves to options.c
     const char *name = "pacemaker-fenced";
     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");
 
     pcmk__output_t *tmp_out = NULL;
     xmlNode *top = NULL;
     const xmlNode *metadata = NULL;
-    gchar *metadata_s = NULL;
+    GString *metadata_s = NULL;
 
     int rc = pcmk__output_new(&tmp_out, "xml", "/dev/null", NULL);
     if (rc != pcmk_rc_ok) {
         return rc;
     }
 
     out->message(tmp_out, "option-list", name, desc_short, desc_long,
                  pcmk__opt_none, fencer_options, true);
 
     tmp_out->finish(tmp_out, CRM_EX_OK, false, (void **) &top);
     metadata = first_named_child(top, PCMK_XE_RESOURCE_AGENT);
-    metadata_s = pcmk__xml_dump(metadata,
-                                pcmk__xml_fmt_pretty|pcmk__xml_fmt_text);
 
-    out->output_xml(out, PCMK_XE_METADATA, metadata_s);
+    metadata_s = g_string_sized_new(16384);
+    pcmk__xml_string(metadata, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text,
+                     metadata_s, 0);
+
+    out->output_xml(out, PCMK_XE_METADATA, metadata_s->str);
 
     pcmk__output_free(tmp_out);
     free_xml(top);
-    g_free(metadata_s);
+    g_string_free(metadata_s, TRUE);
     return pcmk_rc_ok;
 }
 
 static GOptionEntry entries[] = {
     { "stand-alone", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &stand_alone,
       N_("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, N_("Intended for use in regression testing only"), NULL },
 
     { "logfile", 'l', G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME_ARRAY,
       &options.log_files, N_("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)) {
 
         rc = fencer_metadata();
         if (rc != pcmk_rc_ok) {
             exit_code = CRM_EX_FATAL;
             g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                         "Unable to display metadata: %s", pcmk_rc_str(rc));
         }
         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 (pcmk__connect_generic_ipc(old_instance) == pcmk_rc_ok) {
         // 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();
 
     rc = fenced_scheduler_init();
     if (rc != pcmk_rc_ok) {
         exit_code = CRM_EX_FATAL;
         g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                     "Error initializing scheduler data: %s", pcmk_rc_str(rc));
         goto done;
     }
 
     cluster = pcmk_cluster_new();
 
     if (!stand_alone) {
 #if SUPPORT_COROSYNC
         if (is_corosync_cluster()) {
             cluster->destroy = stonith_peer_cs_destroy;
             cluster->cpg.cpg_deliver_fn = stonith_peer_ais_callback;
             cluster->cpg.cpg_confchg_fn = pcmk_cpg_membership;
         }
 #endif // SUPPORT_COROSYNC
 
         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);
 
     // 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);
     fenced_scheduler_cleanup();
 
     pcmk__output_and_clear_error(&error, 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_io_internal.h b/include/crm/common/xml_io_internal.h
index 18a84840c2..6c2b625504 100644
--- a/include/crm/common/xml_io_internal.h
+++ b/include/crm/common/xml_io_internal.h
@@ -1,35 +1,34 @@
 /*
  * Copyright 2017-2024 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_IO_INTERNAL__H
 #define PCMK__XML_IO_INTERNAL__H
 
 /*
  * Internal-only wrappers for and extensions to libxml2 I/O
  */
 
 #include <stdbool.h>        // bool
 
+#include <glib.h>           // GString
 #include <libxml/tree.h>    // xmlNode
 
 xmlNode *pcmk__xml_read(const char *filename);
 xmlNode *pcmk__xml_parse(const char *input);
 
 void pcmk__xml_string(const xmlNode *data, uint32_t options, GString *buffer,
                       int depth);
 
 int pcmk__xml2fd(int fd, xmlNode *cur);
 int pcmk__xml_write_fd(const xmlNode *xml, const char *filename, int fd,
                        bool compress, unsigned int *nbytes);
 int pcmk__xml_write_file(const xmlNode *xml, const char *filename,
                          bool compress, unsigned int *nbytes);
 
-gchar *pcmk__xml_dump(const xmlNode *xml, uint32_t flags);
-
 #endif  // PCMK__XML_IO_INTERNAL__H
diff --git a/lib/cluster/cpg.c b/lib/cluster/cpg.c
index 082cdc1f9e..d592561685 100644
--- a/lib/cluster/cpg.c
+++ b/lib/cluster/cpg.c
@@ -1,1094 +1,1096 @@
 /*
  * Copyright 2004-2024 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 <bzlib.h>
 #include <sys/socket.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
 #include <netdb.h>
 
 #include <crm/common/ipc.h>
 #include <crm/cluster/internal.h>
 #include <crm/common/mainloop.h>
 #include <sys/utsname.h>
 
 #include <qb/qbipc_common.h>
 #include <qb/qbipcc.h>
 #include <qb/qbutil.h>
 
 #include <corosync/corodefs.h>
 #include <corosync/corotypes.h>
 #include <corosync/hdb.h>
 #include <corosync/cpg.h>
 
 #include <crm/common/xml.h>
 
 #include <crm/common/ipc_internal.h>  /* PCMK__SPECIAL_PID* */
 #include "crmcluster_private.h"
 
 /* @TODO Once we can update the public API to require crm_cluster_t* in more
  *       functions, we can ditch this in favor of cluster->cpg_handle.
  */
 static cpg_handle_t pcmk_cpg_handle = 0;
 
 // @TODO These could be moved to crm_cluster_t* at that time as well
 static bool cpg_evicted = false;
 static GList *cs_message_queue = NULL;
 static int cs_message_timer = 0;
 
 struct pcmk__cpg_host_s {
     uint32_t id;
     uint32_t pid;
     gboolean local;
     enum crm_ais_msg_types type;
     uint32_t size;
     char uname[MAX_NAME];
 } __attribute__ ((packed));
 
 typedef struct pcmk__cpg_host_s pcmk__cpg_host_t;
 
 struct pcmk__cpg_msg_s {
     struct qb_ipc_response_header header __attribute__ ((aligned(8)));
     uint32_t id;
     gboolean is_compressed;
 
     pcmk__cpg_host_t host;
     pcmk__cpg_host_t sender;
 
     uint32_t size;
     uint32_t compressed_size;
     /* 584 bytes */
     char data[0];
 
 } __attribute__ ((packed));
 
 typedef struct pcmk__cpg_msg_s pcmk__cpg_msg_t;
 
 static void crm_cs_flush(gpointer data);
 
 #define msg_data_len(msg) (msg->is_compressed?msg->compressed_size:msg->size)
 
 #define cs_repeat(rc, counter, max, code) do {                          \
         rc = code;                                                      \
         if ((rc == CS_ERR_TRY_AGAIN) || (rc == CS_ERR_QUEUE_FULL)) {    \
             counter++;                                                  \
             crm_debug("Retrying operation after %ds", counter);         \
             sleep(counter);                                             \
         } else {                                                        \
             break;                                                      \
         }                                                               \
     } while (counter < max)
 
 /*!
  * \brief Disconnect from Corosync CPG
  *
  * \param[in,out] cluster  Cluster to disconnect
  */
 void
 cluster_disconnect_cpg(crm_cluster_t *cluster)
 {
     pcmk_cpg_handle = 0;
     if (cluster->cpg_handle) {
         crm_trace("Disconnecting CPG");
         cpg_leave(cluster->cpg_handle, &cluster->group);
         cpg_finalize(cluster->cpg_handle);
         cluster->cpg_handle = 0;
 
     } else {
         crm_info("No CPG connection");
     }
 }
 
 /*!
  * \brief Get the local Corosync node ID (via CPG)
  *
  * \param[in] handle  CPG connection to use (or 0 to use new connection)
  *
  * \return Corosync ID of local node (or 0 if not known)
  */
 uint32_t
 get_local_nodeid(cpg_handle_t handle)
 {
     cs_error_t rc = CS_OK;
     int retries = 0;
     static uint32_t local_nodeid = 0;
     cpg_handle_t local_handle = handle;
     cpg_model_v1_data_t cpg_model_info = {CPG_MODEL_V1, NULL, NULL, NULL, 0};
     int fd = -1;
     uid_t found_uid = 0;
     gid_t found_gid = 0;
     pid_t found_pid = 0;
     int rv;
 
     if(local_nodeid != 0) {
         return local_nodeid;
     }
 
     if(handle == 0) {
         crm_trace("Creating connection");
         cs_repeat(rc, retries, 5, cpg_model_initialize(&local_handle, CPG_MODEL_V1, (cpg_model_data_t *)&cpg_model_info, NULL));
         if (rc != CS_OK) {
             crm_err("Could not connect to the CPG API: %s (%d)",
                     cs_strerror(rc), rc);
             return 0;
         }
 
         rc = cpg_fd_get(local_handle, &fd);
         if (rc != CS_OK) {
             crm_err("Could not obtain the CPG API connection: %s (%d)",
                     cs_strerror(rc), rc);
             goto bail;
         }
 
         /* CPG provider run as root (in given user namespace, anyway)? */
         if (!(rv = crm_ipc_is_authentic_process(fd, (uid_t) 0,(gid_t) 0, &found_pid,
                                                 &found_uid, &found_gid))) {
             crm_err("CPG provider is not authentic:"
                     " process %lld (uid: %lld, gid: %lld)",
                     (long long) PCMK__SPECIAL_PID_AS_0(found_pid),
                     (long long) found_uid, (long long) found_gid);
             goto bail;
         } else if (rv < 0) {
             crm_err("Could not verify authenticity of CPG provider: %s (%d)",
                     strerror(-rv), -rv);
             goto bail;
         }
     }
 
     if (rc == CS_OK) {
         retries = 0;
         crm_trace("Performing lookup");
         cs_repeat(rc, retries, 5, cpg_local_get(local_handle, &local_nodeid));
     }
 
     if (rc != CS_OK) {
         crm_err("Could not get local node id from the CPG API: %s (%d)",
                 pcmk__cs_err_str(rc), rc);
     }
 
 bail:
     if(handle == 0) {
         crm_trace("Closing connection");
         cpg_finalize(local_handle);
     }
     crm_debug("Local nodeid is %u", local_nodeid);
     return local_nodeid;
 }
 
 /*!
  * \internal
  * \brief Callback function for Corosync message queue timer
  *
  * \param[in] data  CPG handle
  *
  * \return FALSE (to indicate to glib that timer should not be removed)
  */
 static gboolean
 crm_cs_flush_cb(gpointer data)
 {
     cs_message_timer = 0;
     crm_cs_flush(data);
     return FALSE;
 }
 
 // Send no more than this many CPG messages in one flush
 #define CS_SEND_MAX 200
 
 /*!
  * \internal
  * \brief Send messages in Corosync CPG message queue
  *
  * \param[in] data   CPG handle
  */
 static void
 crm_cs_flush(gpointer data)
 {
     unsigned int sent = 0;
     guint queue_len = 0;
     cs_error_t rc = 0;
     cpg_handle_t *handle = (cpg_handle_t *) data;
 
     if (*handle == 0) {
         crm_trace("Connection is dead");
         return;
     }
 
     queue_len = g_list_length(cs_message_queue);
     if (((queue_len % 1000) == 0) && (queue_len > 1)) {
         crm_err("CPG queue has grown to %d", queue_len);
 
     } else if (queue_len == CS_SEND_MAX) {
         crm_warn("CPG queue has grown to %d", queue_len);
     }
 
     if (cs_message_timer != 0) {
         /* There is already a timer, wait until it goes off */
         crm_trace("Timer active %d", cs_message_timer);
         return;
     }
 
     while ((cs_message_queue != NULL) && (sent < CS_SEND_MAX)) {
         struct iovec *iov = cs_message_queue->data;
 
         rc = cpg_mcast_joined(*handle, CPG_TYPE_AGREED, iov, 1);
         if (rc != CS_OK) {
             break;
         }
 
         sent++;
         crm_trace("CPG message sent, size=%llu",
                   (unsigned long long) iov->iov_len);
 
         cs_message_queue = g_list_remove(cs_message_queue, iov);
         free(iov->iov_base);
         free(iov);
     }
 
     queue_len -= sent;
     do_crm_log((queue_len > 5)? LOG_INFO : LOG_TRACE,
                "Sent %u CPG message%s (%d still queued): %s (rc=%d)",
                sent, pcmk__plural_s(sent), queue_len, pcmk__cs_err_str(rc),
                (int) rc);
 
     if (cs_message_queue) {
         uint32_t delay_ms = 100;
         if (rc != CS_OK) {
             /* Proportionally more if sending failed but cap at 1s */
             delay_ms = QB_MIN(1000, CS_SEND_MAX + (10 * queue_len));
         }
         cs_message_timer = g_timeout_add(delay_ms, crm_cs_flush_cb, data);
     }
 }
 
 /*!
  * \internal
  * \brief Dispatch function for CPG handle
  *
  * \param[in,out] user_data  Cluster object
  *
  * \return 0 on success, -1 on error (per mainloop_io_t interface)
  */
 static int
 pcmk_cpg_dispatch(gpointer user_data)
 {
     cs_error_t rc = CS_OK;
     crm_cluster_t *cluster = (crm_cluster_t *) user_data;
 
     rc = cpg_dispatch(cluster->cpg_handle, CS_DISPATCH_ONE);
     if (rc != CS_OK) {
         crm_err("Connection to the CPG API failed: %s (%d)",
                 pcmk__cs_err_str(rc), rc);
         cpg_finalize(cluster->cpg_handle);
         cluster->cpg_handle = 0;
         return -1;
 
     } else if (cpg_evicted) {
         crm_err("Evicted from CPG membership");
         return -1;
     }
     return 0;
 }
 
 static inline const char *
 ais_dest(const pcmk__cpg_host_t *host)
 {
     if (host->local) {
         return "local";
     } else if (host->size > 0) {
         return host->uname;
     } else {
         return "<all>";
     }
 }
 
 static inline const char *
 msg_type2text(enum crm_ais_msg_types type)
 {
     const char *text = "unknown";
 
     switch (type) {
         case crm_msg_none:
             text = "unknown";
             break;
         case crm_msg_ais:
             text = "ais";
             break;
         case crm_msg_cib:
             text = "cib";
             break;
         case crm_msg_crmd:
             text = "crmd";
             break;
         case crm_msg_pe:
             text = "pengine";
             break;
         case crm_msg_te:
             text = "tengine";
             break;
         case crm_msg_lrmd:
             text = "lrmd";
             break;
         case crm_msg_attrd:
             text = "attrd";
             break;
         case crm_msg_stonithd:
             text = "stonithd";
             break;
         case crm_msg_stonith_ng:
             text = "stonith-ng";
             break;
     }
     return text;
 }
 
 /*!
  * \internal
  * \brief Check whether a Corosync CPG message is valid
  *
  * \param[in] msg   Corosync CPG message to check
  *
  * \return true if \p msg is valid, otherwise false
  */
 static bool
 check_message_sanity(const pcmk__cpg_msg_t *msg)
 {
     int32_t payload_size = msg->header.size - sizeof(pcmk__cpg_msg_t);
 
     if (payload_size < 1) {
         crm_err("%sCPG message %d from %s invalid: "
                 "Claimed size of %d bytes is too small "
                 CRM_XS " from %s[%u] to %s@%s",
                 (msg->is_compressed? "Compressed " : ""),
                 msg->id, ais_dest(&(msg->sender)),
                 (int) msg->header.size,
                 msg_type2text(msg->sender.type), msg->sender.pid,
                 msg_type2text(msg->host.type), ais_dest(&(msg->host)));
         return false;
     }
 
     if (msg->header.error != CS_OK) {
         crm_err("%sCPG message %d from %s invalid: "
                 "Sender indicated error %d "
                 CRM_XS " from %s[%u] to %s@%s",
                 (msg->is_compressed? "Compressed " : ""),
                 msg->id, ais_dest(&(msg->sender)),
                 msg->header.error,
                 msg_type2text(msg->sender.type), msg->sender.pid,
                 msg_type2text(msg->host.type), ais_dest(&(msg->host)));
         return false;
     }
 
     if (msg_data_len(msg) != payload_size) {
         crm_err("%sCPG message %d from %s invalid: "
                 "Total size %d inconsistent with payload size %d "
                 CRM_XS " from %s[%u] to %s@%s",
                 (msg->is_compressed? "Compressed " : ""),
                 msg->id, ais_dest(&(msg->sender)),
                 (int) msg->header.size, (int) msg_data_len(msg),
                 msg_type2text(msg->sender.type), msg->sender.pid,
                 msg_type2text(msg->host.type), ais_dest(&(msg->host)));
         return false;
     }
 
     if (!msg->is_compressed &&
         /* msg->size != (strlen(msg->data) + 1) would be a stronger check,
          * but checking the last byte or two should be quick
          */
         (((msg->size > 1) && (msg->data[msg->size - 2] == '\0'))
          || (msg->data[msg->size - 1] != '\0'))) {
         crm_err("CPG message %d from %s invalid: "
                 "Payload does not end at byte %llu "
                 CRM_XS " from %s[%u] to %s@%s",
                 msg->id, ais_dest(&(msg->sender)),
                 (unsigned long long) msg->size,
                 msg_type2text(msg->sender.type), msg->sender.pid,
                 msg_type2text(msg->host.type), ais_dest(&(msg->host)));
         return false;
     }
 
     crm_trace("Verified %d-byte %sCPG message %d from %s[%u]@%s to %s@%s",
               (int) msg->header.size, (msg->is_compressed? "compressed " : ""),
               msg->id, msg_type2text(msg->sender.type), msg->sender.pid,
               ais_dest(&(msg->sender)),
               msg_type2text(msg->host.type), ais_dest(&(msg->host)));
     return true;
 }
 
 /*!
  * \brief Extract text data from a Corosync CPG message
  *
  * \param[in]     handle   CPG connection (to get local node ID if not known)
  * \param[in]     nodeid   Corosync ID of node that sent message
  * \param[in]     pid      Process ID of message sender (for logging only)
  * \param[in,out] content  CPG message
  * \param[out]    kind     If not NULL, will be set to CPG header ID
  *                         (which should be an enum crm_ais_msg_class value,
  *                         currently always crm_class_cluster)
  * \param[out]    from     If not NULL, will be set to sender uname
  *                         (valid for the lifetime of \p content)
  *
  * \return Newly allocated string with message data
  * \note It is the caller's responsibility to free the return value with free().
  */
 char *
 pcmk_message_common_cs(cpg_handle_t handle, uint32_t nodeid, uint32_t pid, void *content,
                         uint32_t *kind, const char **from)
 {
     char *data = NULL;
     pcmk__cpg_msg_t *msg = (pcmk__cpg_msg_t *) content;
 
     if(handle) {
         // Do filtering and field massaging
         uint32_t local_nodeid = get_local_nodeid(handle);
         const char *local_name = get_local_node_name();
 
         if (msg->sender.id > 0 && msg->sender.id != nodeid) {
             crm_err("Nodeid mismatch from %d.%d: claimed nodeid=%u", nodeid, pid, msg->sender.id);
             return NULL;
 
         } else if (msg->host.id != 0 && (local_nodeid != msg->host.id)) {
             /* Not for us */
             crm_trace("Not for us: %u != %u", msg->host.id, local_nodeid);
             return NULL;
         } else if (msg->host.size != 0 && !pcmk__str_eq(msg->host.uname, local_name, pcmk__str_casei)) {
             /* Not for us */
             crm_trace("Not for us: %s != %s", msg->host.uname, local_name);
             return NULL;
         }
 
         msg->sender.id = nodeid;
         if (msg->sender.size == 0) {
             crm_node_t *peer = pcmk__get_node(nodeid, NULL, NULL,
                                               pcmk__node_search_cluster);
 
             if (peer == NULL) {
                 crm_err("Peer with nodeid=%u is unknown", nodeid);
 
             } else if (peer->uname == NULL) {
                 crm_err("No uname for peer with nodeid=%u", nodeid);
 
             } else {
                 crm_notice("Fixing uname for peer with nodeid=%u", nodeid);
                 msg->sender.size = strlen(peer->uname);
                 memset(msg->sender.uname, 0, MAX_NAME);
                 memcpy(msg->sender.uname, peer->uname, msg->sender.size);
             }
         }
     }
 
     crm_trace("Got new%s message (size=%d, %d, %d)",
               msg->is_compressed ? " compressed" : "",
               msg_data_len(msg), msg->size, msg->compressed_size);
 
     if (kind != NULL) {
         *kind = msg->header.id;
     }
     if (from != NULL) {
         *from = msg->sender.uname;
     }
 
     if (msg->is_compressed && msg->size > 0) {
         int rc = BZ_OK;
         char *uncompressed = NULL;
         unsigned int new_size = msg->size + 1;
 
         if (!check_message_sanity(msg)) {
             goto badmsg;
         }
 
         crm_trace("Decompressing message data");
         uncompressed = calloc(1, new_size);
         rc = BZ2_bzBuffToBuffDecompress(uncompressed, &new_size, msg->data, msg->compressed_size, 1, 0);
 
         rc = pcmk__bzlib2rc(rc);
 
         if (rc != pcmk_rc_ok) {
             crm_err("Decompression failed: %s " CRM_XS " rc=%d", pcmk_rc_str(rc), rc);
             free(uncompressed);
             goto badmsg;
         }
 
         CRM_ASSERT(new_size == msg->size);
 
         data = uncompressed;
 
     } else if (!check_message_sanity(msg)) {
         goto badmsg;
 
     } else {
         data = strdup(msg->data);
     }
 
     // Is this necessary?
     pcmk__get_node(msg->sender.id, msg->sender.uname, NULL,
                    pcmk__node_search_cluster);
 
     crm_trace("Payload: %.200s", data);
     return data;
 
   badmsg:
     crm_err("Invalid message (id=%d, dest=%s:%s, from=%s:%s.%d):"
             " min=%d, total=%d, size=%d, bz2_size=%d",
             msg->id, ais_dest(&(msg->host)), msg_type2text(msg->host.type),
             ais_dest(&(msg->sender)), msg_type2text(msg->sender.type),
             msg->sender.pid, (int)sizeof(pcmk__cpg_msg_t),
             msg->header.size, msg->size, msg->compressed_size);
 
     free(data);
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Compare cpg_address objects by node ID
  *
  * \param[in] first   First cpg_address structure to compare
  * \param[in] second  Second cpg_address structure to compare
  *
  * \return Negative number if first's node ID is lower,
  *         positive number if first's node ID is greater,
  *         or 0 if both node IDs are equal
  */
 static int
 cmp_member_list_nodeid(const void *first, const void *second)
 {
     const struct cpg_address *const a = *((const struct cpg_address **) first),
                              *const b = *((const struct cpg_address **) second);
     if (a->nodeid < b->nodeid) {
         return -1;
     } else if (a->nodeid > b->nodeid) {
         return 1;
     }
     /* don't bother with "reason" nor "pid" */
     return 0;
 }
 
 /*!
  * \internal
  * \brief Get a readable string equivalent of a cpg_reason_t value
  *
  * \param[in] reason  CPG reason value
  *
  * \return Readable string suitable for logging
  */
 static const char *
 cpgreason2str(cpg_reason_t reason)
 {
     switch (reason) {
         case CPG_REASON_JOIN:       return " via cpg_join";
         case CPG_REASON_LEAVE:      return " via cpg_leave";
         case CPG_REASON_NODEDOWN:   return " via cluster exit";
         case CPG_REASON_NODEUP:     return " via cluster join";
         case CPG_REASON_PROCDOWN:   return " for unknown reason";
         default:                    break;
     }
     return "";
 }
 
 /*!
  * \internal
  * \brief Get a log-friendly node name
  *
  * \param[in] peer  Node to check
  *
  * \return Node's uname, or readable string if not known
  */
 static inline const char *
 peer_name(const crm_node_t *peer)
 {
     if (peer == NULL) {
         return "unknown node";
     } else if (peer->uname == NULL) {
         return "peer node";
     } else {
         return peer->uname;
     }
 }
 
 /*!
  * \internal
  * \brief Process a CPG peer's leaving the cluster
  *
  * \param[in] cpg_group_name      CPG group name (for logging)
  * \param[in] event_counter       Event number (for logging)
  * \param[in] local_nodeid        Node ID of local node
  * \param[in] cpg_peer            CPG peer that left
  * \param[in] sorted_member_list  List of remaining members, qsort()-ed by ID
  * \param[in] member_list_entries Number of entries in \p sorted_member_list
  */
 static void
 node_left(const char *cpg_group_name, int event_counter,
           uint32_t local_nodeid, const struct cpg_address *cpg_peer,
           const struct cpg_address **sorted_member_list,
           size_t member_list_entries)
 {
     crm_node_t *peer = pcmk__search_node_caches(cpg_peer->nodeid, NULL,
                                                 pcmk__node_search_cluster);
     const struct cpg_address **rival = NULL;
 
     /* Most CPG-related Pacemaker code assumes that only one process on a node
      * can be in the process group, but Corosync does not impose this
      * limitation, and more than one can be a member in practice due to a
      * daemon attempting to start while another instance is already running.
      *
      * Check for any such duplicate instances, because we don't want to process
      * their leaving as if our actual peer left. If the peer that left still has
      * an entry in sorted_member_list (with a different PID), we will ignore the
      * leaving.
      *
      * @TODO Track CPG members' PIDs so we can tell exactly who left.
      */
     if (peer != NULL) {
         rival = bsearch(&cpg_peer, sorted_member_list, member_list_entries,
                         sizeof(const struct cpg_address *),
                         cmp_member_list_nodeid);
     }
 
     if (rival == NULL) {
         crm_info("Group %s event %d: %s (node %u pid %u) left%s",
                  cpg_group_name, event_counter, peer_name(peer),
                  cpg_peer->nodeid, cpg_peer->pid,
                  cpgreason2str(cpg_peer->reason));
         if (peer != NULL) {
             crm_update_peer_proc(__func__, peer, crm_proc_cpg,
                                  PCMK_VALUE_OFFLINE);
         }
     } else if (cpg_peer->nodeid == local_nodeid) {
         crm_warn("Group %s event %d: duplicate local pid %u left%s",
                  cpg_group_name, event_counter,
                  cpg_peer->pid, cpgreason2str(cpg_peer->reason));
     } else {
         crm_warn("Group %s event %d: "
                  "%s (node %u) duplicate pid %u left%s (%u remains)",
                  cpg_group_name, event_counter, peer_name(peer),
                  cpg_peer->nodeid, cpg_peer->pid,
                  cpgreason2str(cpg_peer->reason), (*rival)->pid);
     }
 }
 
 /*!
  * \brief Handle a CPG configuration change event
  *
  * \param[in] handle               CPG connection
  * \param[in] cpg_name             CPG group name
  * \param[in] member_list          List of current CPG members
  * \param[in] member_list_entries  Number of entries in \p member_list
  * \param[in] left_list            List of CPG members that left
  * \param[in] left_list_entries    Number of entries in \p left_list
  * \param[in] joined_list          List of CPG members that joined
  * \param[in] joined_list_entries  Number of entries in \p joined_list
  */
 void
 pcmk_cpg_membership(cpg_handle_t handle,
                     const struct cpg_name *groupName,
                     const struct cpg_address *member_list, size_t member_list_entries,
                     const struct cpg_address *left_list, size_t left_list_entries,
                     const struct cpg_address *joined_list, size_t joined_list_entries)
 {
     int i;
     gboolean found = FALSE;
     static int counter = 0;
     uint32_t local_nodeid = get_local_nodeid(handle);
     const struct cpg_address **sorted;
 
     sorted = malloc(member_list_entries * sizeof(const struct cpg_address *));
     CRM_ASSERT(sorted != NULL);
 
     for (size_t iter = 0; iter < member_list_entries; iter++) {
         sorted[iter] = member_list + iter;
     }
     /* so that the cross-matching multiply-subscribed nodes is then cheap */
     qsort(sorted, member_list_entries, sizeof(const struct cpg_address *),
           cmp_member_list_nodeid);
 
     for (i = 0; i < left_list_entries; i++) {
         node_left(groupName->value, counter, local_nodeid, &left_list[i],
                   sorted, member_list_entries);
     }
     free(sorted);
     sorted = NULL;
 
     for (i = 0; i < joined_list_entries; i++) {
         crm_info("Group %s event %d: node %u pid %u joined%s",
                  groupName->value, counter, joined_list[i].nodeid,
                  joined_list[i].pid, cpgreason2str(joined_list[i].reason));
     }
 
     for (i = 0; i < member_list_entries; i++) {
         crm_node_t *peer = pcmk__get_node(member_list[i].nodeid, NULL, NULL,
                                           pcmk__node_search_cluster);
 
         if (member_list[i].nodeid == local_nodeid
                 && member_list[i].pid != getpid()) {
             // See the note in node_left()
             crm_warn("Group %s event %d: detected duplicate local pid %u",
                      groupName->value, counter, member_list[i].pid);
             continue;
         }
         crm_info("Group %s event %d: %s (node %u pid %u) is member",
                  groupName->value, counter, peer_name(peer),
                  member_list[i].nodeid, member_list[i].pid);
 
         /* If the caller left auto-reaping enabled, this will also update the
          * state to member.
          */
         peer = crm_update_peer_proc(__func__, peer, crm_proc_cpg,
                                     PCMK_VALUE_ONLINE);
 
         if (peer && peer->state && strcmp(peer->state, CRM_NODE_MEMBER)) {
             /* The node is a CPG member, but we currently think it's not a
              * cluster member. This is possible only if auto-reaping was
              * disabled. The node may be joining, and we happened to get the CPG
              * notification before the quorum notification; or the node may have
              * just died, and we are processing its final messages; or a bug
              * has affected the peer cache.
              */
             time_t now = time(NULL);
 
             if (peer->when_lost == 0) {
                 // Track when we first got into this contradictory state
                 peer->when_lost = now;
 
             } else if (now > (peer->when_lost + 60)) {
                 // If it persists for more than a minute, update the state
                 crm_warn("Node %u is member of group %s but was believed offline",
                          member_list[i].nodeid, groupName->value);
                 pcmk__update_peer_state(__func__, peer, CRM_NODE_MEMBER, 0);
             }
         }
 
         if (local_nodeid == member_list[i].nodeid) {
             found = TRUE;
         }
     }
 
     if (!found) {
         crm_err("Local node was evicted from group %s", groupName->value);
         cpg_evicted = true;
     }
 
     counter++;
 }
 
 /*!
  * \brief Connect to Corosync CPG
  *
  * \param[in,out] cluster  Cluster object
  *
  * \return TRUE on success, otherwise FALSE
  */
 gboolean
 cluster_connect_cpg(crm_cluster_t *cluster)
 {
     cs_error_t rc;
     int fd = -1;
     int retries = 0;
     uint32_t id = 0;
     crm_node_t *peer = NULL;
     cpg_handle_t handle = 0;
     const char *message_name = pcmk__message_name(crm_system_name);
     uid_t found_uid = 0;
     gid_t found_gid = 0;
     pid_t found_pid = 0;
     int rv;
 
     struct mainloop_fd_callbacks cpg_fd_callbacks = {
         .dispatch = pcmk_cpg_dispatch,
         .destroy = cluster->destroy,
     };
 
     cpg_model_v1_data_t cpg_model_info = {
 	    .model = CPG_MODEL_V1,
 	    .cpg_deliver_fn = cluster->cpg.cpg_deliver_fn,
 	    .cpg_confchg_fn = cluster->cpg.cpg_confchg_fn,
 	    .cpg_totem_confchg_fn = NULL,
 	    .flags = 0,
     };
 
     cpg_evicted = false;
     cluster->group.length = 0;
     cluster->group.value[0] = 0;
 
     /* group.value is char[128] */
     strncpy(cluster->group.value, message_name, 127);
     cluster->group.value[127] = 0;
     cluster->group.length = 1 + QB_MIN(127, strlen(cluster->group.value));
 
     cs_repeat(rc, retries, 30, cpg_model_initialize(&handle, CPG_MODEL_V1, (cpg_model_data_t *)&cpg_model_info, NULL));
     if (rc != CS_OK) {
         crm_err("Could not connect to the CPG API: %s (%d)",
                 cs_strerror(rc), rc);
         goto bail;
     }
 
     rc = cpg_fd_get(handle, &fd);
     if (rc != CS_OK) {
         crm_err("Could not obtain the CPG API connection: %s (%d)",
                 cs_strerror(rc), rc);
         goto bail;
     }
 
     /* CPG provider run as root (in given user namespace, anyway)? */
     if (!(rv = crm_ipc_is_authentic_process(fd, (uid_t) 0,(gid_t) 0, &found_pid,
                                             &found_uid, &found_gid))) {
         crm_err("CPG provider is not authentic:"
                 " process %lld (uid: %lld, gid: %lld)",
                 (long long) PCMK__SPECIAL_PID_AS_0(found_pid),
                 (long long) found_uid, (long long) found_gid);
         rc = CS_ERR_ACCESS;
         goto bail;
     } else if (rv < 0) {
         crm_err("Could not verify authenticity of CPG provider: %s (%d)",
                 strerror(-rv), -rv);
         rc = CS_ERR_ACCESS;
         goto bail;
     }
 
     id = get_local_nodeid(handle);
     if (id == 0) {
         crm_err("Could not get local node id from the CPG API");
         goto bail;
 
     }
     cluster->nodeid = id;
 
     retries = 0;
     cs_repeat(rc, retries, 30, cpg_join(handle, &cluster->group));
     if (rc != CS_OK) {
         crm_err("Could not join the CPG group '%s': %d", message_name, rc);
         goto bail;
     }
 
     pcmk_cpg_handle = handle;
     cluster->cpg_handle = handle;
     mainloop_add_fd("corosync-cpg", G_PRIORITY_MEDIUM, fd, cluster, &cpg_fd_callbacks);
 
   bail:
     if (rc != CS_OK) {
         cpg_finalize(handle);
         return FALSE;
     }
 
     peer = pcmk__get_node(id, NULL, NULL, pcmk__node_search_cluster);
     crm_update_peer_proc(__func__, peer, crm_proc_cpg, PCMK_VALUE_ONLINE);
     return TRUE;
 }
 
 /*!
  * \internal
  * \brief Send an XML message via Corosync CPG
  *
  * \param[in] msg   XML message to send
  * \param[in] node  Cluster node to send message to
  * \param[in] dest  Type of message to send
  *
  * \return TRUE on success, otherwise FALSE
  */
 bool
 pcmk__cpg_send_xml(const xmlNode *msg, const crm_node_t *node,
                    enum crm_ais_msg_types dest)
 {
     bool rc = true;
-    gchar *data = pcmk__xml_dump(msg, 0);
+    GString *data = g_string_sized_new(1024);
 
-    rc = send_cluster_text(crm_class_cluster, data, FALSE, node, dest);
-    g_free(data);
+    pcmk__xml_string(msg, 0, data, 0);
+
+    rc = send_cluster_text(crm_class_cluster, data->str, FALSE, node, dest);
+    g_string_free(data, TRUE);
     return rc;
 }
 
 /*!
  * \internal
  * \brief Send string data via Corosync CPG
  *
  * \param[in] msg_class  Message class (to set as CPG header ID)
  * \param[in] data       Data to send
  * \param[in] local      What to set as host "local" value (which is never used)
  * \param[in] node       Cluster node to send message to
  * \param[in] dest       Type of message to send
  *
  * \return TRUE on success, otherwise FALSE
  */
 gboolean
 send_cluster_text(enum crm_ais_msg_class msg_class, const char *data,
                   gboolean local, const crm_node_t *node,
                   enum crm_ais_msg_types dest)
 {
     static int msg_id = 0;
     static int local_pid = 0;
     static int local_name_len = 0;
     static const char *local_name = NULL;
 
     char *target = NULL;
     struct iovec *iov;
     pcmk__cpg_msg_t *msg = NULL;
     enum crm_ais_msg_types sender = text2msg_type(crm_system_name);
 
     switch (msg_class) {
         case crm_class_cluster:
             break;
         default:
             crm_err("Invalid message class: %d", msg_class);
             return FALSE;
     }
 
     CRM_CHECK(dest != crm_msg_ais, return FALSE);
 
     if (local_name == NULL) {
         local_name = get_local_node_name();
     }
     if ((local_name_len == 0) && (local_name != NULL)) {
         local_name_len = strlen(local_name);
     }
 
     if (data == NULL) {
         data = "";
     }
 
     if (local_pid == 0) {
         local_pid = getpid();
     }
 
     if (sender == crm_msg_none) {
         sender = local_pid;
     }
 
     msg = calloc(1, sizeof(pcmk__cpg_msg_t));
 
     msg_id++;
     msg->id = msg_id;
     msg->header.id = msg_class;
     msg->header.error = CS_OK;
 
     msg->host.type = dest;
     msg->host.local = local;
 
     if (node) {
         if (node->uname) {
             target = strdup(node->uname);
             msg->host.size = strlen(node->uname);
             memset(msg->host.uname, 0, MAX_NAME);
             memcpy(msg->host.uname, node->uname, msg->host.size);
         } else {
             target = crm_strdup_printf("%u", node->id);
         }
         msg->host.id = node->id;
     } else {
         target = strdup("all");
     }
 
     msg->sender.id = 0;
     msg->sender.type = sender;
     msg->sender.pid = local_pid;
     msg->sender.size = local_name_len;
     memset(msg->sender.uname, 0, MAX_NAME);
     if ((local_name != NULL) && (msg->sender.size != 0)) {
         memcpy(msg->sender.uname, local_name, msg->sender.size);
     }
 
     msg->size = 1 + strlen(data);
     msg->header.size = sizeof(pcmk__cpg_msg_t) + msg->size;
 
     if (msg->size < CRM_BZ2_THRESHOLD) {
         msg = pcmk__realloc(msg, msg->header.size);
         memcpy(msg->data, data, msg->size);
 
     } else {
         char *compressed = NULL;
         unsigned int new_size = 0;
         char *uncompressed = strdup(data);
 
         if (pcmk__compress(uncompressed, (unsigned int) msg->size, 0,
                            &compressed, &new_size) == pcmk_rc_ok) {
 
             msg->header.size = sizeof(pcmk__cpg_msg_t) + new_size;
             msg = pcmk__realloc(msg, msg->header.size);
             memcpy(msg->data, compressed, new_size);
 
             msg->is_compressed = TRUE;
             msg->compressed_size = new_size;
 
         } else {
             // cppcheck seems not to understand the abort logic in pcmk__realloc
             // cppcheck-suppress memleak
             msg = pcmk__realloc(msg, msg->header.size);
             memcpy(msg->data, data, msg->size);
         }
 
         free(uncompressed);
         free(compressed);
     }
 
     iov = calloc(1, sizeof(struct iovec));
     iov->iov_base = msg;
     iov->iov_len = msg->header.size;
 
     if (msg->compressed_size) {
         crm_trace("Queueing CPG message %u to %s (%llu bytes, %d bytes compressed payload): %.200s",
                   msg->id, target, (unsigned long long) iov->iov_len,
                   msg->compressed_size, data);
     } else {
         crm_trace("Queueing CPG message %u to %s (%llu bytes, %d bytes payload): %.200s",
                   msg->id, target, (unsigned long long) iov->iov_len,
                   msg->size, data);
     }
     free(target);
 
     cs_message_queue = g_list_append(cs_message_queue, iov);
     crm_cs_flush(&pcmk_cpg_handle);
 
     return TRUE;
 }
 
 /*!
  * \brief Get the message type equivalent of a string
  *
  * \param[in] text  String of message type
  *
  * \return Message type equivalent of \p text
  */
 enum crm_ais_msg_types
 text2msg_type(const char *text)
 {
     int type = crm_msg_none;
 
     CRM_CHECK(text != NULL, return type);
     text = pcmk__message_name(text);
     if (pcmk__str_eq(text, "ais", pcmk__str_casei)) {
         type = crm_msg_ais;
     } else if (pcmk__str_eq(text, CRM_SYSTEM_CIB, pcmk__str_casei)) {
         type = crm_msg_cib;
     } else if (pcmk__strcase_any_of(text, CRM_SYSTEM_CRMD, CRM_SYSTEM_DC, NULL)) {
         type = crm_msg_crmd;
     } else if (pcmk__str_eq(text, CRM_SYSTEM_TENGINE, pcmk__str_casei)) {
         type = crm_msg_te;
     } else if (pcmk__str_eq(text, CRM_SYSTEM_PENGINE, pcmk__str_casei)) {
         type = crm_msg_pe;
     } else if (pcmk__str_eq(text, CRM_SYSTEM_LRMD, pcmk__str_casei)) {
         type = crm_msg_lrmd;
     } else if (pcmk__str_eq(text, CRM_SYSTEM_STONITHD, pcmk__str_casei)) {
         type = crm_msg_stonithd;
     } else if (pcmk__str_eq(text, "stonith-ng", pcmk__str_casei)) {
         type = crm_msg_stonith_ng;
     } else if (pcmk__str_eq(text, "attrd", pcmk__str_casei)) {
         type = crm_msg_attrd;
 
     } else {
         /* This will normally be a transient client rather than
          * a cluster daemon.  Set the type to the pid of the client
          */
         int scan_rc = sscanf(text, "%d", &type);
 
         if (scan_rc != 1 || type <= crm_msg_stonith_ng) {
             /* Ensure it's sane */
             type = crm_msg_none;
         }
     }
     return type;
 }
diff --git a/lib/common/digest.c b/lib/common/digest.c
index 933769fde3..9a06ff44a7 100644
--- a/lib/common/digest.c
+++ b/lib/common/digest.c
@@ -1,336 +1,336 @@
 /*
  * Copyright 2015-2024 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 <unistd.h>
 #include <string.h>
 #include <stdlib.h>
 #include <md5.h>
 
 #include <crm/crm.h>
 #include <crm/common/xml.h>
 #include "crmcommon_private.h"
 
 #define BEST_EFFORT_STATUS 0
 
 /*!
  * \internal
  * \brief Dump XML in a format used with v1 digests
  *
  * \param[in] xml  Root of XML to dump
  *
  * \return Newly allocated buffer containing dumped XML
  */
 static GString *
 dump_xml_for_digest(xmlNodePtr xml)
 {
     GString *buffer = g_string_sized_new(1024);
 
     /* for compatibility with the old result which is used for v1 digests */
     g_string_append_c(buffer, ' ');
     pcmk__xml_string(xml, 0, buffer, 0);
     g_string_append_c(buffer, '\n');
 
     return buffer;
 }
 
 /*!
  * \brief Calculate and return v1 digest of XML tree
  *
  * \param[in] input Root of XML to digest
  * \param[in] sort Whether to sort the XML before calculating digest
  * \param[in] ignored Not used
  *
  * \return Newly allocated string containing digest
  * \note Example return value: "c048eae664dba840e1d2060f00299e9d"
  */
 static char *
 calculate_xml_digest_v1(xmlNode *input, gboolean sort, gboolean ignored)
 {
     char *digest = NULL;
     GString *buffer = NULL;
     xmlNode *copy = NULL;
 
     if (sort) {
         crm_trace("Sorting xml...");
         copy = sorted_xml(input, NULL, TRUE);
         crm_trace("Done");
         input = copy;
     }
 
     buffer = dump_xml_for_digest(input);
     CRM_CHECK(buffer->len > 0, free_xml(copy);
               g_string_free(buffer, TRUE);
               return NULL);
 
     digest = crm_md5sum((const char *) buffer->str);
     crm_log_xml_trace(input, "digest:source");
 
     g_string_free(buffer, TRUE);
     free_xml(copy);
     return digest;
 }
 
 /*!
  * \brief Calculate and return v2 digest of XML tree
  *
  * \param[in] source Root of XML to digest
  * \param[in] do_filter Whether to filter certain XML attributes
  *
  * \return Newly allocated string containing digest
  */
 static char *
 calculate_xml_digest_v2(const xmlNode *source, gboolean do_filter)
 {
     char *digest = NULL;
-    gchar *buf = NULL;
+    GString *buf = g_string_sized_new(1024);
 
     crm_trace("Begin digest %s", do_filter?"filtered":"");
 
-    buf = pcmk__xml_dump(source, (do_filter? pcmk__xml_fmt_filtered : 0));
-    digest = crm_md5sum(buf);
+    pcmk__xml_string(source, (do_filter? pcmk__xml_fmt_filtered : 0), buf, 0);
+    digest = crm_md5sum(buf->str);
 
     pcmk__if_tracing(
         {
             char *trace_file = crm_strdup_printf("%s/digest-%s",
                                                  pcmk__get_tmpdir(), digest);
 
             crm_trace("Saving %s.%s.%s to %s",
                       crm_element_value(source, PCMK_XA_ADMIN_EPOCH),
                       crm_element_value(source, PCMK_XA_EPOCH),
                       crm_element_value(source, PCMK_XA_NUM_UPDATES),
                       trace_file);
             save_xml_to_file(source, "digest input", trace_file);
             free(trace_file);
         },
         {}
     );
     crm_trace("End digest");
-    g_free(buf);
+    g_string_free(buf, TRUE);
     return digest;
 }
 
 /*!
  * \brief Calculate and return digest of XML tree, suitable for storing on disk
  *
  * \param[in] input Root of XML to digest
  *
  * \return Newly allocated string containing digest
  */
 char *
 calculate_on_disk_digest(xmlNode *input)
 {
     /* Always use the v1 format for on-disk digests
      * a) it's a compatibility nightmare
      * b) we only use this once at startup, all other
      *    invocations are in a separate child process
      */
     return calculate_xml_digest_v1(input, FALSE, FALSE);
 }
 
 /*!
  * \brief Calculate and return digest of XML operation
  *
  * \param[in] input    Root of XML to digest
  * \param[in] version  Unused
  *
  * \return Newly allocated string containing digest
  */
 char *
 calculate_operation_digest(xmlNode *input, const char *version)
 {
     /* We still need the sorting for operation digests */
     return calculate_xml_digest_v1(input, TRUE, FALSE);
 }
 
 /*!
  * \brief Calculate and return digest of XML tree
  *
  * \param[in] input      Root of XML to digest
  * \param[in] sort       Whether to sort XML before calculating digest
  * \param[in] do_filter  Whether to filter certain XML attributes
  * \param[in] version    CRM feature set version (used to select v1/v2 digest)
  *
  * \return Newly allocated string containing digest
  */
 char *
 calculate_xml_versioned_digest(xmlNode *input, gboolean sort,
                                gboolean do_filter, const char *version)
 {
     /*
      * @COMPAT digests (on-disk or in diffs/patchsets) created <1.1.4;
      * removing this affects even full-restart upgrades from old versions
      *
      * The sorting associated with v1 digest creation accounted for 23% of
      * the CIB manager's CPU usage on the server. v2 drops this.
      *
      * The filtering accounts for an additional 2.5% and we may want to
      * remove it in future.
      *
      * v2 also uses the xmlBuffer contents directly to avoid additional copying
      */
     if (version == NULL || compare_version("3.0.5", version) > 0) {
         crm_trace("Using v1 digest algorithm for %s",
                   pcmk__s(version, "unknown feature set"));
         return calculate_xml_digest_v1(input, sort, do_filter);
     }
     crm_trace("Using v2 digest algorithm for %s",
               pcmk__s(version, "unknown feature set"));
     return calculate_xml_digest_v2(input, do_filter);
 }
 
 /*!
  * \internal
  * \brief Check whether calculated digest of given XML matches expected digest
  *
  * \param[in] input     Root of XML tree to digest
  * \param[in] expected  Expected digest in on-disk format
  *
  * \return true if digests match, false on mismatch or error
  */
 bool
 pcmk__verify_digest(xmlNode *input, const char *expected)
 {
     char *calculated = NULL;
     bool passed;
 
     if (input != NULL) {
         calculated = calculate_on_disk_digest(input);
         if (calculated == NULL) {
             crm_perror(LOG_ERR, "Could not calculate digest for comparison");
             return false;
         }
     }
     passed = pcmk__str_eq(expected, calculated, pcmk__str_casei);
     if (passed) {
         crm_trace("Digest comparison passed: %s", calculated);
     } else {
         crm_err("Digest comparison failed: expected %s, calculated %s",
                 expected, calculated);
     }
     free(calculated);
     return passed;
 }
 
 /*!
  * \internal
  * \brief Check whether an XML attribute should be excluded from CIB digests
  *
  * \param[in] name  XML attribute name
  *
  * \return true if XML attribute should be excluded from CIB digest calculation
  */
 bool
 pcmk__xa_filterable(const char *name)
 {
     static const char *filter[] = {
         PCMK_XA_CRM_DEBUG_ORIGIN,
         PCMK_XA_CIB_LAST_WRITTEN,
         PCMK_XA_UPDATE_ORIGIN,
         PCMK_XA_UPDATE_CLIENT,
         PCMK_XA_UPDATE_USER,
     };
 
     for (int i = 0; i < PCMK__NELEM(filter); i++) {
         if (strcmp(name, filter[i]) == 0) {
             return true;
         }
     }
     return false;
 }
 
 char *
 crm_md5sum(const char *buffer)
 {
     int lpc = 0, len = 0;
     char *digest = NULL;
     unsigned char raw_digest[MD5_DIGEST_SIZE];
 
     if (buffer == NULL) {
         buffer = "";
     }
     len = strlen(buffer);
 
     crm_trace("Beginning digest of %d bytes", len);
     digest = malloc(2 * MD5_DIGEST_SIZE + 1);
     if (digest) {
         md5_buffer(buffer, len, raw_digest);
         for (lpc = 0; lpc < MD5_DIGEST_SIZE; lpc++) {
             sprintf(digest + (2 * lpc), "%02x", raw_digest[lpc]);
         }
         digest[(2 * MD5_DIGEST_SIZE)] = 0;
         crm_trace("Digest %s.", digest);
 
     } else {
         crm_err("Could not create digest");
     }
     return digest;
 }
 
 // Return true if a is an attribute that should be filtered
 static bool
 should_filter_for_digest(xmlAttrPtr a, void *user_data)
 {
     if (strncmp((const char *) a->name, CRM_META "_",
                 sizeof(CRM_META " ") - 1) == 0) {
         return true;
     }
     return pcmk__str_any_of((const char *) a->name,
                             PCMK_XA_ID,
                             PCMK_XA_CRM_FEATURE_SET,
                             PCMK__XA_OP_DIGEST,
                             PCMK__META_ON_NODE,
                             PCMK__META_ON_NODE_UUID,
                             "pcmk_external_ip",
                             NULL);
 }
 
 /*!
  * \internal
  * \brief Remove XML attributes not needed for operation digest
  *
  * \param[in,out] param_set  XML with operation parameters
  */
 void
 pcmk__filter_op_for_digest(xmlNode *param_set)
 {
     char *key = NULL;
     char *timeout = NULL;
     guint interval_ms = 0;
 
     if (param_set == NULL) {
         return;
     }
 
     /* Timeout is useful for recurring operation digests, so grab it before
      * removing meta-attributes
      */
     key = crm_meta_name(PCMK_META_INTERVAL);
     if (crm_element_value_ms(param_set, key, &interval_ms) != pcmk_ok) {
         interval_ms = 0;
     }
     free(key);
     key = NULL;
     if (interval_ms != 0) {
         key = crm_meta_name(PCMK_META_TIMEOUT);
         timeout = crm_element_value_copy(param_set, key);
     }
 
     // Remove all CRM_meta_* attributes and certain other attributes
     pcmk__xe_remove_matching_attrs(param_set, should_filter_for_digest, NULL);
 
     // Add timeout back for recurring operation digests
     if (timeout != NULL) {
         crm_xml_add(param_set, key, timeout);
     }
     free(timeout);
     free(key);
 }
diff --git a/lib/common/ipc_server.c b/lib/common/ipc_server.c
index c62e663c33..237e15fac1 100644
--- a/lib/common/ipc_server.c
+++ b/lib/common/ipc_server.c
@@ -1,1017 +1,1023 @@
 /*
  * Copyright 2004-2024 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 <errno.h>
 #include <bzlib.h>
 #include <sys/stat.h>
 #include <sys/types.h>
 
 #include <crm/crm.h>
 #include <crm/common/xml.h>
 #include <crm/common/ipc.h>
 #include <crm/common/ipc_internal.h>
 #include "crmcommon_private.h"
 
 /* Evict clients whose event queue grows this large (by default) */
 #define PCMK_IPC_DEFAULT_QUEUE_MAX 500
 
 static GHashTable *client_connections = NULL;
 
 /*!
  * \internal
  * \brief Count IPC clients
  *
  * \return Number of active IPC client connections
  */
 guint
 pcmk__ipc_client_count(void)
 {
     return client_connections? g_hash_table_size(client_connections) : 0;
 }
 
 /*!
  * \internal
  * \brief Execute a function for each active IPC client connection
  *
  * \param[in]     func       Function to call
  * \param[in,out] user_data  Pointer to pass to function
  *
  * \note The parameters are the same as for g_hash_table_foreach().
  */
 void
 pcmk__foreach_ipc_client(GHFunc func, gpointer user_data)
 {
     if ((func != NULL) && (client_connections != NULL)) {
         g_hash_table_foreach(client_connections, func, user_data);
     }
 }
 
 pcmk__client_t *
 pcmk__find_client(const qb_ipcs_connection_t *c)
 {
     if (client_connections) {
         return g_hash_table_lookup(client_connections, c);
     }
 
     crm_trace("No client found for %p", c);
     return NULL;
 }
 
 pcmk__client_t *
 pcmk__find_client_by_id(const char *id)
 {
     if ((client_connections != NULL) && (id != NULL)) {
         gpointer key;
         pcmk__client_t *client = NULL;
         GHashTableIter iter;
 
         g_hash_table_iter_init(&iter, client_connections);
         while (g_hash_table_iter_next(&iter, &key, (gpointer *) & client)) {
             if (strcmp(client->id, id) == 0) {
                 return client;
             }
         }
     }
     crm_trace("No client found with id='%s'", pcmk__s(id, ""));
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Get a client identifier for use in log messages
  *
  * \param[in] c  Client
  *
  * \return Client's name, client's ID, or a string literal, as available
  * \note This is intended to be used in format strings like "client %s".
  */
 const char *
 pcmk__client_name(const pcmk__client_t *c)
 {
     if (c == NULL) {
         return "(unspecified)";
 
     } else if (c->name != NULL) {
         return c->name;
 
     } else if (c->id != NULL) {
         return c->id;
 
     } else {
         return "(unidentified)";
     }
 }
 
 void
 pcmk__client_cleanup(void)
 {
     if (client_connections != NULL) {
         int active = g_hash_table_size(client_connections);
 
         if (active > 0) {
             crm_warn("Exiting with %d active IPC client%s",
                      active, pcmk__plural_s(active));
         }
         g_hash_table_destroy(client_connections);
         client_connections = NULL;
     }
 }
 
 void
 pcmk__drop_all_clients(qb_ipcs_service_t *service)
 {
     qb_ipcs_connection_t *c = NULL;
 
     if (service == NULL) {
         return;
     }
 
     c = qb_ipcs_connection_first_get(service);
 
     while (c != NULL) {
         qb_ipcs_connection_t *last = c;
 
         c = qb_ipcs_connection_next_get(service, last);
 
         /* There really shouldn't be anyone connected at this point */
         crm_notice("Disconnecting client %p, pid=%d...",
                    last, pcmk__client_pid(last));
         qb_ipcs_disconnect(last);
         qb_ipcs_connection_unref(last);
     }
 }
 
 /*!
  * \internal
  * \brief Allocate a new pcmk__client_t object based on an IPC connection
  *
  * \param[in] c           IPC connection (NULL to allocate generic client)
  * \param[in] key         Connection table key (NULL to use sane default)
  * \param[in] uid_client  UID corresponding to c (ignored if c is NULL)
  *
  * \return Pointer to new pcmk__client_t (or NULL on error)
  */
 static pcmk__client_t *
 client_from_connection(qb_ipcs_connection_t *c, void *key, uid_t uid_client)
 {
     pcmk__client_t *client = calloc(1, sizeof(pcmk__client_t));
 
     if (client == NULL) {
         crm_perror(LOG_ERR, "Allocating client");
         return NULL;
     }
 
     if (c) {
         client->user = pcmk__uid2username(uid_client);
         if (client->user == NULL) {
             client->user = strdup("#unprivileged");
             CRM_CHECK(client->user != NULL, free(client); return NULL);
             crm_err("Unable to enforce ACLs for user ID %d, assuming unprivileged",
                     uid_client);
         }
         client->ipcs = c;
         pcmk__set_client_flags(client, pcmk__client_ipc);
         client->pid = pcmk__client_pid(c);
         if (key == NULL) {
             key = c;
         }
     }
 
     client->id = crm_generate_uuid();
     if (key == NULL) {
         key = client->id;
     }
     if (client_connections == NULL) {
         crm_trace("Creating IPC client table");
         client_connections = g_hash_table_new(g_direct_hash, g_direct_equal);
     }
     g_hash_table_insert(client_connections, key, client);
     return client;
 }
 
 /*!
  * \brief Allocate a new pcmk__client_t object and generate its ID
  *
  * \param[in] key  What to use as connections hash table key (NULL to use ID)
  *
  * \return Pointer to new pcmk__client_t (asserts on failure)
  */
 pcmk__client_t *
 pcmk__new_unauth_client(void *key)
 {
     pcmk__client_t *client = client_from_connection(NULL, key, 0);
 
     CRM_ASSERT(client != NULL);
     return client;
 }
 
 pcmk__client_t *
 pcmk__new_client(qb_ipcs_connection_t *c, uid_t uid_client, gid_t gid_client)
 {
     gid_t uid_cluster = 0;
     gid_t gid_cluster = 0;
 
     pcmk__client_t *client = NULL;
 
     CRM_CHECK(c != NULL, return NULL);
 
     if (pcmk_daemon_user(&uid_cluster, &gid_cluster) < 0) {
         static bool need_log = TRUE;
 
         if (need_log) {
             crm_warn("Could not find user and group IDs for user %s",
                      CRM_DAEMON_USER);
             need_log = FALSE;
         }
     }
 
     if (uid_client != 0) {
         crm_trace("Giving group %u access to new IPC connection", gid_cluster);
         /* Passing -1 to chown(2) means don't change */
         qb_ipcs_connection_auth_set(c, -1, gid_cluster, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
     }
 
     /* TODO: Do our own auth checking, return NULL if unauthorized */
     client = client_from_connection(c, NULL, uid_client);
     if (client == NULL) {
         return NULL;
     }
 
     if ((uid_client == 0) || (uid_client == uid_cluster)) {
         /* Remember when a connection came from root or hacluster */
         pcmk__set_client_flags(client, pcmk__client_privileged);
     }
 
     crm_debug("New IPC client %s for PID %u with uid %d and gid %d",
               client->id, client->pid, uid_client, gid_client);
     return client;
 }
 
 static struct iovec *
 pcmk__new_ipc_event(void)
 {
     struct iovec *iov = calloc(2, sizeof(struct iovec));
 
     CRM_ASSERT(iov != NULL);
     return iov;
 }
 
 /*!
  * \brief Free an I/O vector created by pcmk__ipc_prepare_iov()
  *
  * \param[in,out] event  I/O vector to free
  */
 void
 pcmk_free_ipc_event(struct iovec *event)
 {
     if (event != NULL) {
         free(event[0].iov_base);
         free(event[1].iov_base);
         free(event);
     }
 }
 
 static void
 free_event(gpointer data)
 {
     pcmk_free_ipc_event((struct iovec *) data);
 }
 
 static void
 add_event(pcmk__client_t *c, struct iovec *iov)
 {
     if (c->event_queue == NULL) {
         c->event_queue = g_queue_new();
     }
     g_queue_push_tail(c->event_queue, iov);
 }
 
 void
 pcmk__free_client(pcmk__client_t *c)
 {
     if (c == NULL) {
         return;
     }
 
     if (client_connections) {
         if (c->ipcs) {
             crm_trace("Destroying %p/%p (%d remaining)",
                       c, c->ipcs, g_hash_table_size(client_connections) - 1);
             g_hash_table_remove(client_connections, c->ipcs);
 
         } else {
             crm_trace("Destroying remote connection %p (%d remaining)",
                       c, g_hash_table_size(client_connections) - 1);
             g_hash_table_remove(client_connections, c->id);
         }
     }
 
     if (c->event_timer) {
         g_source_remove(c->event_timer);
     }
 
     if (c->event_queue) {
         crm_debug("Destroying %d events", g_queue_get_length(c->event_queue));
         g_queue_free_full(c->event_queue, free_event);
     }
 
     free(c->id);
     free(c->name);
     free(c->user);
     if (c->remote) {
         if (c->remote->auth_timeout) {
             g_source_remove(c->remote->auth_timeout);
         }
         free(c->remote->buffer);
         free(c->remote);
     }
     free(c);
 }
 
 /*!
  * \internal
  * \brief Raise IPC eviction threshold for a client, if allowed
  *
  * \param[in,out] client     Client to modify
  * \param[in]     qmax       New threshold (as non-NULL string)
  *
  * \return true if change was allowed, false otherwise
  */
 bool
 pcmk__set_client_queue_max(pcmk__client_t *client, const char *qmax)
 {
     if (pcmk_is_set(client->flags, pcmk__client_privileged)) {
         long long qmax_ll;
 
         if ((pcmk__scan_ll(qmax, &qmax_ll, 0LL) == pcmk_rc_ok)
             && (qmax_ll > 0LL) && (qmax_ll <= UINT_MAX)) {
             client->queue_max = (unsigned int) qmax_ll;
             return true;
         }
     }
     return false;
 }
 
 int
 pcmk__client_pid(qb_ipcs_connection_t *c)
 {
     struct qb_ipcs_connection_stats stats;
 
     stats.client_pid = 0;
     qb_ipcs_connection_stats_get(c, &stats, 0);
     return stats.client_pid;
 }
 
 /*!
  * \internal
  * \brief Retrieve message XML from data read from client IPC
  *
  * \param[in,out]  c       IPC client connection
  * \param[in]      data    Data read from client connection
  * \param[out]     id      Where to store message ID from libqb header
  * \param[out]     flags   Where to store flags from libqb header
  *
  * \return Message XML on success, NULL otherwise
  */
 xmlNode *
 pcmk__client_data2xml(pcmk__client_t *c, void *data, uint32_t *id,
                       uint32_t *flags)
 {
     xmlNode *xml = NULL;
     char *uncompressed = NULL;
     char *text = ((char *)data) + sizeof(pcmk__ipc_header_t);
     pcmk__ipc_header_t *header = data;
 
     if (!pcmk__valid_ipc_header(header)) {
         return NULL;
     }
 
     if (id) {
         *id = ((struct qb_ipc_response_header *)data)->id;
     }
     if (flags) {
         *flags = header->flags;
     }
 
     if (pcmk_is_set(header->flags, crm_ipc_proxied)) {
         /* Mark this client as being the endpoint of a proxy connection.
          * Proxy connections responses are sent on the event channel, to avoid
          * blocking the controller serving as proxy.
          */
         pcmk__set_client_flags(c, pcmk__client_proxied);
     }
 
     if (header->size_compressed) {
         int rc = 0;
         unsigned int size_u = 1 + header->size_uncompressed;
         uncompressed = calloc(1, size_u);
 
         crm_trace("Decompressing message data %u bytes into %u bytes",
                   header->size_compressed, size_u);
 
         rc = BZ2_bzBuffToBuffDecompress(uncompressed, &size_u, text, header->size_compressed, 1, 0);
         text = uncompressed;
 
         rc = pcmk__bzlib2rc(rc);
 
         if (rc != pcmk_rc_ok) {
             crm_err("Decompression failed: %s " CRM_XS " rc=%d",
                     pcmk_rc_str(rc), rc);
             free(uncompressed);
             return NULL;
         }
     }
 
     CRM_ASSERT(text[header->size_uncompressed - 1] == 0);
 
     xml = pcmk__xml_parse(text);
     crm_log_xml_trace(xml, "[IPC received]");
 
     free(uncompressed);
     return xml;
 }
 
 static int crm_ipcs_flush_events(pcmk__client_t *c);
 
 static gboolean
 crm_ipcs_flush_events_cb(gpointer data)
 {
     pcmk__client_t *c = data;
 
     c->event_timer = 0;
     crm_ipcs_flush_events(c);
     return FALSE;
 }
 
 /*!
  * \internal
  * \brief Add progressive delay before next event queue flush
  *
  * \param[in,out] c          Client connection to add delay to
  * \param[in]     queue_len  Current event queue length
  */
 static inline void
 delay_next_flush(pcmk__client_t *c, unsigned int queue_len)
 {
     /* Delay a maximum of 1.5 seconds */
     guint delay = (queue_len < 5)? (1000 + 100 * queue_len) : 1500;
 
     c->event_timer = g_timeout_add(delay, crm_ipcs_flush_events_cb, c);
 }
 
 /*!
  * \internal
  * \brief Send client any messages in its queue
  *
  * \param[in,out] c  Client to flush
  *
  * \return Standard Pacemaker return value
  */
 static int
 crm_ipcs_flush_events(pcmk__client_t *c)
 {
     int rc = pcmk_rc_ok;
     ssize_t qb_rc = 0;
     unsigned int sent = 0;
     unsigned int queue_len = 0;
 
     if (c == NULL) {
         return rc;
 
     } else if (c->event_timer) {
         /* There is already a timer, wait until it goes off */
         crm_trace("Timer active for %p - %d", c->ipcs, c->event_timer);
         return rc;
     }
 
     if (c->event_queue) {
         queue_len = g_queue_get_length(c->event_queue);
     }
     while (sent < 100) {
         pcmk__ipc_header_t *header = NULL;
         struct iovec *event = NULL;
 
         if (c->event_queue) {
             // We don't pop unless send is successful
             event = g_queue_peek_head(c->event_queue);
         }
         if (event == NULL) { // Queue is empty
             break;
         }
 
         qb_rc = qb_ipcs_event_sendv(c->ipcs, event, 2);
         if (qb_rc < 0) {
             rc = (int) -qb_rc;
             break;
         }
         event = g_queue_pop_head(c->event_queue);
 
         sent++;
         header = event[0].iov_base;
         if (header->size_compressed) {
             crm_trace("Event %d to %p[%d] (%lld compressed bytes) sent",
                       header->qb.id, c->ipcs, c->pid, (long long) qb_rc);
         } else {
             crm_trace("Event %d to %p[%d] (%lld bytes) sent: %.120s",
                       header->qb.id, c->ipcs, c->pid, (long long) qb_rc,
                       (char *) (event[1].iov_base));
         }
         pcmk_free_ipc_event(event);
     }
 
     queue_len -= sent;
     if (sent > 0 || queue_len) {
         crm_trace("Sent %d events (%d remaining) for %p[%d]: %s (%lld)",
                   sent, queue_len, c->ipcs, c->pid,
                   pcmk_rc_str(rc), (long long) qb_rc);
     }
 
     if (queue_len) {
 
         /* Allow clients to briefly fall behind on processing incoming messages,
          * but drop completely unresponsive clients so the connection doesn't
          * consume resources indefinitely.
          */
         if (queue_len > QB_MAX(c->queue_max, PCMK_IPC_DEFAULT_QUEUE_MAX)) {
             if ((c->queue_backlog <= 1) || (queue_len < c->queue_backlog)) {
                 /* Don't evict for a new or shrinking backlog */
                 crm_warn("Client with process ID %u has a backlog of %u messages "
                          CRM_XS " %p", c->pid, queue_len, c->ipcs);
             } else {
                 crm_err("Evicting client with process ID %u due to backlog of %u messages "
                          CRM_XS " %p", c->pid, queue_len, c->ipcs);
                 c->queue_backlog = 0;
                 qb_ipcs_disconnect(c->ipcs);
                 return rc;
             }
         }
 
         c->queue_backlog = queue_len;
         delay_next_flush(c, queue_len);
 
     } else {
         /* Event queue is empty, there is no backlog */
         c->queue_backlog = 0;
     }
 
     return rc;
 }
 
 /*!
  * \internal
  * \brief Create an I/O vector for sending an IPC XML message
  *
  * \param[in]  request        Identifier for libqb response header
  * \param[in]  message        XML message to send
  * \param[in]  max_send_size  If 0, default IPC buffer size is used
  * \param[out] result         Where to store prepared I/O vector
  * \param[out] bytes          Size of prepared data in bytes
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__ipc_prepare_iov(uint32_t request, const xmlNode *message,
                       uint32_t max_send_size, struct iovec **result,
                       ssize_t *bytes)
 {
-    static unsigned int biggest = 0;
     struct iovec *iov;
     unsigned int total = 0;
-    char *compressed = NULL;
-    char *buffer = NULL;
-    gchar *g_buffer = NULL;
+    GString *buffer = NULL;
     pcmk__ipc_header_t *header = NULL;
+    int rc = pcmk_rc_ok;
 
     if ((message == NULL) || (result == NULL)) {
-        return EINVAL;
+        rc = EINVAL;
+        goto done;
     }
 
     header = calloc(1, sizeof(pcmk__ipc_header_t));
     if (header == NULL) {
-       return ENOMEM; /* errno mightn't be set by allocator */
+       rc = ENOMEM;
+       goto done;
     }
 
-    g_buffer = pcmk__xml_dump(message, 0);
-    pcmk__str_update(&buffer, g_buffer);
-    g_free(g_buffer);
+    buffer = g_string_sized_new(1024);
+    pcmk__xml_string(message, 0, buffer, 0);
 
     if (max_send_size == 0) {
         max_send_size = crm_ipc_default_buffer_size();
     }
     CRM_LOG_ASSERT(max_send_size != 0);
 
     *result = NULL;
     iov = pcmk__new_ipc_event();
     iov[0].iov_len = sizeof(pcmk__ipc_header_t);
     iov[0].iov_base = header;
 
     header->version = PCMK__IPC_VERSION;
-    header->size_uncompressed = 1 + strlen(buffer);
+    header->size_uncompressed = 1 + buffer->len;
     total = iov[0].iov_len + header->size_uncompressed;
 
     if (total < max_send_size) {
-        iov[1].iov_base = buffer;
+        pcmk__str_update((char **) &(iov[1].iov_base), buffer->str);
         iov[1].iov_len = header->size_uncompressed;
 
     } else {
+        static unsigned int biggest = 0;
+
+        char *compressed = NULL;
         unsigned int new_size = 0;
 
-        if (pcmk__compress(buffer, (unsigned int) header->size_uncompressed,
+        if (pcmk__compress(buffer->str,
+                           (unsigned int) header->size_uncompressed,
                            (unsigned int) max_send_size, &compressed,
                            &new_size) == pcmk_rc_ok) {
 
             pcmk__set_ipc_flags(header->flags, "send data", crm_ipc_compressed);
             header->size_compressed = new_size;
 
             iov[1].iov_len = header->size_compressed;
             iov[1].iov_base = compressed;
 
-            free(buffer);
-
             biggest = QB_MAX(header->size_compressed, biggest);
 
         } else {
             crm_log_xml_trace(message, "EMSGSIZE");
             biggest = QB_MAX(header->size_uncompressed, biggest);
 
             crm_err("Could not compress %u-byte message into less than IPC "
                     "limit of %u bytes; set PCMK_ipc_buffer to higher value "
                     "(%u bytes suggested)",
                     header->size_uncompressed, max_send_size, 4 * biggest);
 
             free(compressed);
-            free(buffer);
             pcmk_free_ipc_event(iov);
-            return EMSGSIZE;
+            rc = EMSGSIZE;
+            goto done;
         }
     }
 
     header->qb.size = iov[0].iov_len + iov[1].iov_len;
     header->qb.id = (int32_t)request;    /* Replying to a specific request */
 
     *result = iov;
     CRM_ASSERT(header->qb.size > 0);
     if (bytes != NULL) {
         *bytes = header->qb.size;
     }
-    return pcmk_rc_ok;
+
+done:
+    if (buffer != NULL) {
+        g_string_free(buffer, TRUE);
+    }
+    return rc;
 }
 
 int
 pcmk__ipc_send_iov(pcmk__client_t *c, struct iovec *iov, uint32_t flags)
 {
     int rc = pcmk_rc_ok;
     static uint32_t id = 1;
     pcmk__ipc_header_t *header = iov[0].iov_base;
 
     if (c->flags & pcmk__client_proxied) {
         /* _ALL_ replies to proxied connections need to be sent as events */
         if (!pcmk_is_set(flags, crm_ipc_server_event)) {
             /* The proxied flag lets us know this was originally meant to be a
              * response, even though we're sending it over the event channel.
              */
             pcmk__set_ipc_flags(flags, "server event",
                                 crm_ipc_server_event
                                 |crm_ipc_proxied_relay_response);
         }
     }
 
     pcmk__set_ipc_flags(header->flags, "server event", flags);
     if (flags & crm_ipc_server_event) {
         header->qb.id = id++;   /* We don't really use it, but doesn't hurt to set one */
 
         if (flags & crm_ipc_server_free) {
             crm_trace("Sending the original to %p[%d]", c->ipcs, c->pid);
             add_event(c, iov);
 
         } else {
             struct iovec *iov_copy = pcmk__new_ipc_event();
 
             crm_trace("Sending a copy to %p[%d]", c->ipcs, c->pid);
             iov_copy[0].iov_len = iov[0].iov_len;
             iov_copy[0].iov_base = malloc(iov[0].iov_len);
             memcpy(iov_copy[0].iov_base, iov[0].iov_base, iov[0].iov_len);
 
             iov_copy[1].iov_len = iov[1].iov_len;
             iov_copy[1].iov_base = malloc(iov[1].iov_len);
             memcpy(iov_copy[1].iov_base, iov[1].iov_base, iov[1].iov_len);
 
             add_event(c, iov_copy);
         }
 
     } else {
         ssize_t qb_rc;
 
         CRM_LOG_ASSERT(header->qb.id != 0);     /* Replying to a specific request */
 
         qb_rc = qb_ipcs_response_sendv(c->ipcs, iov, 2);
         if (qb_rc < header->qb.size) {
             if (qb_rc < 0) {
                 rc = (int) -qb_rc;
             }
             crm_notice("Response %d to pid %d failed: %s "
                        CRM_XS " bytes=%u rc=%lld ipcs=%p",
                        header->qb.id, c->pid, pcmk_rc_str(rc),
                        header->qb.size, (long long) qb_rc, c->ipcs);
 
         } else {
             crm_trace("Response %d sent, %lld bytes to %p[%d]",
                       header->qb.id, (long long) qb_rc, c->ipcs, c->pid);
         }
 
         if (flags & crm_ipc_server_free) {
             pcmk_free_ipc_event(iov);
         }
     }
 
     if (flags & crm_ipc_server_event) {
         rc = crm_ipcs_flush_events(c);
     } else {
         crm_ipcs_flush_events(c);
     }
 
     if ((rc == EPIPE) || (rc == ENOTCONN)) {
         crm_trace("Client %p disconnected", c->ipcs);
     }
     return rc;
 }
 
 int
 pcmk__ipc_send_xml(pcmk__client_t *c, uint32_t request, const xmlNode *message,
                    uint32_t flags)
 {
     struct iovec *iov = NULL;
     int rc = pcmk_rc_ok;
 
     if (c == NULL) {
         return EINVAL;
     }
     rc = pcmk__ipc_prepare_iov(request, message, crm_ipc_default_buffer_size(),
                                &iov, NULL);
     if (rc == pcmk_rc_ok) {
         pcmk__set_ipc_flags(flags, "send data", crm_ipc_server_free);
         rc = pcmk__ipc_send_iov(c, iov, flags);
     } else {
         pcmk_free_ipc_event(iov);
         crm_notice("IPC message to pid %d failed: %s " CRM_XS " rc=%d",
                    c->pid, pcmk_rc_str(rc), rc);
     }
     return rc;
 }
 
 /*!
  * \internal
  * \brief Create an acknowledgement with a status code to send to a client
  *
  * \param[in] function  Calling function
  * \param[in] line      Source file line within calling function
  * \param[in] flags     IPC flags to use when sending
  * \param[in] tag       Element name to use for acknowledgement
  * \param[in] ver       IPC protocol version (can be NULL)
  * \param[in] status    Exit status code to add to ack
  *
  * \return Newly created XML for ack
  * \note The caller is responsible for freeing the return value with free_xml().
  */
 xmlNode *
 pcmk__ipc_create_ack_as(const char *function, int line, uint32_t flags,
                         const char *tag, const char *ver, crm_exit_t status)
 {
     xmlNode *ack = NULL;
 
     if (pcmk_is_set(flags, crm_ipc_client_response)) {
         ack = create_xml_node(NULL, tag);
         crm_xml_add(ack, PCMK_XA_FUNCTION, function);
         crm_xml_add_int(ack, PCMK__XA_LINE, line);
         crm_xml_add_int(ack, PCMK_XA_STATUS, (int) status);
         crm_xml_add(ack, PCMK__XA_IPC_PROTO_VERSION, ver);
     }
     return ack;
 }
 
 /*!
  * \internal
  * \brief Send an acknowledgement with a status code to a client
  *
  * \param[in] function  Calling function
  * \param[in] line      Source file line within calling function
  * \param[in] c         Client to send ack to
  * \param[in] request   Request ID being replied to
  * \param[in] flags     IPC flags to use when sending
  * \param[in] tag       Element name to use for acknowledgement
  * \param[in] ver       IPC protocol version (can be NULL)
  * \param[in] status    Status code to send with acknowledgement
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__ipc_send_ack_as(const char *function, int line, pcmk__client_t *c,
                       uint32_t request, uint32_t flags, const char *tag,
                       const char *ver, crm_exit_t status)
 {
     int rc = pcmk_rc_ok;
     xmlNode *ack = pcmk__ipc_create_ack_as(function, line, flags, tag, ver, status);
 
     if (ack != NULL) {
         crm_trace("Ack'ing IPC message from client %s as <%s status=%d>",
                   pcmk__client_name(c), tag, status);
         crm_log_xml_trace(ack, "sent-ack");
         c->request_id = 0;
         rc = pcmk__ipc_send_xml(c, request, ack, flags);
         free_xml(ack);
     }
     return rc;
 }
 
 /*!
  * \internal
  * \brief Add an IPC server to the main loop for the pacemaker-based API
  *
  * \param[out] ipcs_ro   New IPC server for read-only pacemaker-based API
  * \param[out] ipcs_rw   New IPC server for read/write pacemaker-based API
  * \param[out] ipcs_shm  New IPC server for shared-memory pacemaker-based API
  * \param[in]  ro_cb     IPC callbacks for read-only API
  * \param[in]  rw_cb     IPC callbacks for read/write and shared-memory APIs
  *
  * \note This function exits fatally if unable to create the servers.
  */
 void pcmk__serve_based_ipc(qb_ipcs_service_t **ipcs_ro,
                            qb_ipcs_service_t **ipcs_rw,
                            qb_ipcs_service_t **ipcs_shm,
                            struct qb_ipcs_service_handlers *ro_cb,
                            struct qb_ipcs_service_handlers *rw_cb)
 {
     *ipcs_ro = mainloop_add_ipc_server(PCMK__SERVER_BASED_RO,
                                        QB_IPC_NATIVE, ro_cb);
 
     *ipcs_rw = mainloop_add_ipc_server(PCMK__SERVER_BASED_RW,
                                        QB_IPC_NATIVE, rw_cb);
 
     *ipcs_shm = mainloop_add_ipc_server(PCMK__SERVER_BASED_SHM,
                                         QB_IPC_SHM, rw_cb);
 
     if (*ipcs_ro == NULL || *ipcs_rw == NULL || *ipcs_shm == NULL) {
         crm_err("Failed to create the CIB manager: exiting and inhibiting respawn");
         crm_warn("Verify pacemaker and pacemaker_remote are not both enabled");
         crm_exit(CRM_EX_FATAL);
     }
 }
 
 /*!
  * \internal
  * \brief Destroy IPC servers for pacemaker-based API
  *
  * \param[out] ipcs_ro   IPC server for read-only pacemaker-based API
  * \param[out] ipcs_rw   IPC server for read/write pacemaker-based API
  * \param[out] ipcs_shm  IPC server for shared-memory pacemaker-based API
  *
  * \note This is a convenience function for calling qb_ipcs_destroy() for each
  *       argument.
  */
 void
 pcmk__stop_based_ipc(qb_ipcs_service_t *ipcs_ro,
                      qb_ipcs_service_t *ipcs_rw,
                      qb_ipcs_service_t *ipcs_shm)
 {
     qb_ipcs_destroy(ipcs_ro);
     qb_ipcs_destroy(ipcs_rw);
     qb_ipcs_destroy(ipcs_shm);
 }
 
 /*!
  * \internal
  * \brief Add an IPC server to the main loop for the pacemaker-controld API
  *
  * \param[in] cb  IPC callbacks
  *
  * \return Newly created IPC server
  */
 qb_ipcs_service_t *
 pcmk__serve_controld_ipc(struct qb_ipcs_service_handlers *cb)
 {
     return mainloop_add_ipc_server(CRM_SYSTEM_CRMD, QB_IPC_NATIVE, cb);
 }
 
 /*!
  * \internal
  * \brief Add an IPC server to the main loop for the pacemaker-attrd API
  *
  * \param[out] ipcs  Where to store newly created IPC server
  * \param[in] cb  IPC callbacks
  *
  * \note This function exits fatally if unable to create the servers.
  */
 void
 pcmk__serve_attrd_ipc(qb_ipcs_service_t **ipcs,
                       struct qb_ipcs_service_handlers *cb)
 {
     *ipcs = mainloop_add_ipc_server(PCMK__VALUE_ATTRD, QB_IPC_NATIVE, cb);
 
     if (*ipcs == NULL) {
         crm_err("Failed to create pacemaker-attrd server: exiting and inhibiting respawn");
         crm_warn("Verify pacemaker and pacemaker_remote are not both enabled.");
         crm_exit(CRM_EX_FATAL);
     }
 }
 
 /*!
  * \internal
  * \brief Add an IPC server to the main loop for the pacemaker-fenced API
  *
  * \param[out] ipcs  Where to store newly created IPC server
  * \param[in]  cb    IPC callbacks
  *
  * \note This function exits fatally if unable to create the servers.
  */
 void
 pcmk__serve_fenced_ipc(qb_ipcs_service_t **ipcs,
                        struct qb_ipcs_service_handlers *cb)
 {
     *ipcs = mainloop_add_ipc_server_with_prio("stonith-ng", QB_IPC_NATIVE, cb,
                                               QB_LOOP_HIGH);
 
     if (*ipcs == NULL) {
         crm_err("Failed to create fencer: exiting and inhibiting respawn.");
         crm_warn("Verify pacemaker and pacemaker_remote are not both enabled.");
         crm_exit(CRM_EX_FATAL);
     }
 }
 
 /*!
  * \internal
  * \brief Add an IPC server to the main loop for the pacemakerd API
  *
  * \param[out] ipcs  Where to store newly created IPC server
  * \param[in]  cb    IPC callbacks
  *
  * \note This function exits with CRM_EX_OSERR if unable to create the servers.
  */
 void
 pcmk__serve_pacemakerd_ipc(qb_ipcs_service_t **ipcs,
                        struct qb_ipcs_service_handlers *cb)
 {
     *ipcs = mainloop_add_ipc_server(CRM_SYSTEM_MCP, QB_IPC_NATIVE, cb);
 
     if (*ipcs == NULL) {
         crm_err("Couldn't start pacemakerd IPC server");
         crm_warn("Verify pacemaker and pacemaker_remote are not both enabled.");
         /* sub-daemons are observed by pacemakerd. Thus we exit CRM_EX_FATAL
          * if we want to prevent pacemakerd from restarting them.
          * With pacemakerd we leave the exit-code shown to e.g. systemd
          * to what it was prior to moving the code here from pacemakerd.c
          */
         crm_exit(CRM_EX_OSERR);
     }
 }
 
 /*!
  * \internal
  * \brief Add an IPC server to the main loop for the pacemaker-schedulerd API
  *
  * \param[in] cb  IPC callbacks
  *
  * \return Newly created IPC server
  * \note This function exits fatally if unable to create the servers.
  */
 qb_ipcs_service_t *
 pcmk__serve_schedulerd_ipc(struct qb_ipcs_service_handlers *cb)
 {
     return mainloop_add_ipc_server(CRM_SYSTEM_PENGINE, QB_IPC_NATIVE, cb);
 }
 
 /*!
  * \brief Check whether string represents a client name used by cluster daemons
  *
  * \param[in] name  String to check
  *
  * \return true if name is standard client name used by daemons, false otherwise
  *
  * \note This is provided by the client, and so cannot be used by itself as a
  *       secure means of authentication.
  */
 bool
 crm_is_daemon_name(const char *name)
 {
     return pcmk__str_any_of(pcmk__message_name(name),
                             "attrd",
                             CRM_SYSTEM_CIB,
                             CRM_SYSTEM_CRMD,
                             CRM_SYSTEM_DC,
                             CRM_SYSTEM_LRMD,
                             CRM_SYSTEM_MCP,
                             CRM_SYSTEM_PENGINE,
                             CRM_SYSTEM_STONITHD,
                             CRM_SYSTEM_TENGINE,
                             "pacemaker-remoted",
                             "stonith-ng",
                             NULL);
 }
diff --git a/lib/common/options.c b/lib/common/options.c
index 2b3adbe2bc..24285362bb 100644
--- a/lib/common/options.c
+++ b/lib/common/options.c
@@ -1,1012 +1,1014 @@
 /*
  * Copyright 2004-2024 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 _GNU_SOURCE
 #  define _GNU_SOURCE
 #endif
 
 #include <crm_internal.h>
 
 #include <stdio.h>
 #include <string.h>
 #include <stdlib.h>
 #include <sys/types.h>
 #include <sys/stat.h>
 
 #include <crm/crm.h>
 #include <crm/common/xml.h>
 
 void
 pcmk__cli_help(char cmd)
 {
     if (cmd == 'v' || cmd == '$') {
         printf("Pacemaker %s\n", PACEMAKER_VERSION);
         printf("Written by Andrew Beekhof and "
                "the Pacemaker project contributors\n");
 
     } else if (cmd == '!') {
         printf("Pacemaker %s (Build: %s): %s\n", PACEMAKER_VERSION, BUILD_VERSION, CRM_FEATURES);
     }
 
     crm_exit(CRM_EX_OK);
     while(1); // above does not return
 }
 
 
 /*
  * Option metadata
  */
 
 static pcmk__cluster_option_t cluster_options[] = {
     /* name, old name, type, allowed values,
      * default value, validator,
      * flags,
      * short description,
      * long description
      */
     {
         PCMK_OPT_DC_VERSION, NULL, "string", NULL,
         NULL, NULL,
         pcmk__opt_controld|pcmk__opt_generated,
         N_("Pacemaker version on cluster node elected Designated Controller "
             "(DC)"),
         N_("Includes a hash which identifies the exact revision the code was "
             "built from. Used for diagnostic purposes."),
     },
     {
         PCMK_OPT_CLUSTER_INFRASTRUCTURE, NULL, "string", NULL,
         NULL, NULL,
         pcmk__opt_controld|pcmk__opt_generated,
         N_("The messaging layer on which Pacemaker is currently running"),
         N_("Used for informational and diagnostic purposes."),
     },
     {
         PCMK_OPT_CLUSTER_NAME, NULL, "string", NULL,
         NULL, NULL,
         pcmk__opt_controld,
         N_("An arbitrary name for the cluster"),
         N_("This optional value is mostly for users' convenience as desired "
             "in administration, but may also be used in Pacemaker "
             "configuration rules via the #cluster-name node attribute, and "
             "by higher-level tools and resource agents."),
     },
     {
         PCMK_OPT_DC_DEADTIME, NULL, "time", NULL,
         "20s", pcmk__valid_interval_spec,
         pcmk__opt_controld,
         N_("How long to wait for a response from other nodes during start-up"),
         N_("The optimal value will depend on the speed and load of your "
             "network and the type of switches used."),
     },
     {
         PCMK_OPT_CLUSTER_RECHECK_INTERVAL, NULL, "time", NULL,
         "15min", pcmk__valid_interval_spec,
         pcmk__opt_controld,
         N_("Polling interval to recheck cluster state and evaluate rules "
             "with date specifications"),
         N_("Pacemaker is primarily event-driven, and looks ahead to know when "
             "to recheck cluster state for failure-timeout settings and most "
             "time-based rules. However, it will also recheck the cluster after "
             "this amount of inactivity, to evaluate rules with date "
             "specifications and serve as a fail-safe for certain types of "
             "scheduler bugs. A value of 0 disables polling. A positive value "
             "sets an interval in seconds, unless other units are specified "
             "(for example, \"5min\")."),
     },
     {
         PCMK_OPT_FENCE_REACTION, NULL, "select",
             PCMK_VALUE_STOP ", " PCMK_VALUE_PANIC,
         PCMK_VALUE_STOP, NULL,
         pcmk__opt_controld,
         N_("How a cluster node should react if notified of its own fencing"),
         N_("A cluster node may receive notification of a \"succeeded\" "
             "fencing that targeted it if fencing is misconfigured, or if "
             "fabric fencing is in use that doesn't cut cluster communication. "
             "Use \"stop\" to attempt to immediately stop Pacemaker and stay "
             "stopped, or \"panic\" to attempt to immediately reboot the local "
             "node, falling back to stop on failure."),
     },
     {
         PCMK_OPT_ELECTION_TIMEOUT, NULL, "time", NULL,
         "2min", pcmk__valid_interval_spec,
         pcmk__opt_controld|pcmk__opt_advanced,
         N_("Declare an election failed if it is not decided within this much "
             "time. If you need to adjust this value, it probably indicates "
             "the presence of a bug."),
         NULL,
     },
     {
         PCMK_OPT_SHUTDOWN_ESCALATION, NULL, "time", NULL,
         "20min", pcmk__valid_interval_spec,
         pcmk__opt_controld|pcmk__opt_advanced,
         N_("Exit immediately if shutdown does not complete within this much "
             "time. If you need to adjust this value, it probably indicates "
             "the presence of a bug."),
         NULL,
     },
     {
         PCMK_OPT_JOIN_INTEGRATION_TIMEOUT, "crmd-integration-timeout", "time",
             NULL,
         "3min", pcmk__valid_interval_spec,
         pcmk__opt_controld|pcmk__opt_advanced,
         N_("If you need to adjust this value, it probably indicates "
             "the presence of a bug."),
         NULL,
     },
     {
         PCMK_OPT_JOIN_FINALIZATION_TIMEOUT, "crmd-finalization-timeout",
             "time", NULL,
         "30min", pcmk__valid_interval_spec,
         pcmk__opt_controld|pcmk__opt_advanced,
         N_("If you need to adjust this value, it probably indicates "
             "the presence of a bug."),
         NULL,
     },
     {
         PCMK_OPT_TRANSITION_DELAY, "crmd-transition-delay", "time", NULL,
         "0s", pcmk__valid_interval_spec,
         pcmk__opt_controld|pcmk__opt_advanced,
         N_("Enabling this option will slow down cluster recovery under all "
             "conditions"),
         N_("Delay cluster recovery for this much time to allow for additional "
             "events to occur. Useful if your configuration is sensitive to "
             "the order in which ping updates arrive."),
     },
     {
         PCMK_OPT_NO_QUORUM_POLICY, NULL, "select",
             PCMK_VALUE_STOP ", " PCMK_VALUE_FREEZE ", " PCMK_VALUE_IGNORE
                 ", " PCMK_VALUE_DEMOTE ", " PCMK_VALUE_FENCE_LEGACY,
         PCMK_VALUE_STOP, pcmk__valid_no_quorum_policy,
         pcmk__opt_schedulerd,
         N_("What to do when the cluster does not have quorum"),
         NULL,
     },
     {
         PCMK_OPT_SHUTDOWN_LOCK, NULL, "boolean", NULL,
         PCMK_VALUE_FALSE, pcmk__valid_boolean,
         pcmk__opt_schedulerd,
         N_("Whether to lock resources to a cleanly shut down node"),
         N_("When true, resources active on a node when it is cleanly shut down "
             "are kept \"locked\" to that node (not allowed to run elsewhere) "
             "until they start again on that node after it rejoins (or for at "
             "most shutdown-lock-limit, if set). Stonith resources and "
             "Pacemaker Remote connections are never locked. Clone and bundle "
             "instances and the promoted role of promotable clones are "
             "currently never locked, though support could be added in a future "
             "release."),
     },
     {
         PCMK_OPT_SHUTDOWN_LOCK_LIMIT, NULL, "time", NULL,
         "0", pcmk__valid_interval_spec,
         pcmk__opt_schedulerd,
         N_("Do not lock resources to a cleanly shut down node longer than "
            "this"),
         N_("If shutdown-lock is true and this is set to a nonzero time "
             "duration, shutdown locks will expire after this much time has "
             "passed since the shutdown was initiated, even if the node has not "
             "rejoined."),
     },
     {
         PCMK_OPT_ENABLE_ACL, NULL, "boolean", NULL,
         PCMK_VALUE_FALSE, pcmk__valid_boolean,
         pcmk__opt_based,
         N_("Enable Access Control Lists (ACLs) for the CIB"),
         NULL,
     },
     {
         PCMK_OPT_SYMMETRIC_CLUSTER, NULL, "boolean", NULL,
         PCMK_VALUE_TRUE, pcmk__valid_boolean,
         pcmk__opt_schedulerd,
         N_("Whether resources can run on any node by default"),
         NULL,
     },
     {
         PCMK_OPT_MAINTENANCE_MODE, NULL, "boolean", NULL,
         PCMK_VALUE_FALSE, pcmk__valid_boolean,
         pcmk__opt_schedulerd,
         N_("Whether the cluster should refrain from monitoring, starting, and "
             "stopping resources"),
         NULL,
     },
     {
         PCMK_OPT_START_FAILURE_IS_FATAL, NULL, "boolean", NULL,
         PCMK_VALUE_TRUE, pcmk__valid_boolean,
         pcmk__opt_schedulerd,
         N_("Whether a start failure should prevent a resource from being "
             "recovered on the same node"),
         N_("When true, the cluster will immediately ban a resource from a node "
             "if it fails to start there. When false, the cluster will instead "
             "check the resource's fail count against its migration-threshold.")
     },
     {
         PCMK_OPT_ENABLE_STARTUP_PROBES, NULL, "boolean", NULL,
         PCMK_VALUE_TRUE, pcmk__valid_boolean,
         pcmk__opt_schedulerd,
         N_("Whether the cluster should check for active resources during "
             "start-up"),
         NULL,
     },
 
     // Fencing-related options
     {
         PCMK_OPT_STONITH_ENABLED, NULL, "boolean", NULL,
         PCMK_VALUE_TRUE, pcmk__valid_boolean,
         pcmk__opt_schedulerd|pcmk__opt_advanced,
         N_("Whether nodes may be fenced as part of recovery"),
         N_("If false, unresponsive nodes are immediately assumed to be "
             "harmless, and resources that were active on them may be recovered "
             "elsewhere. This can result in a \"split-brain\" situation, "
             "potentially leading to data loss and/or service unavailability."),
     },
     {
         PCMK_OPT_STONITH_ACTION, NULL, "select", "reboot, off, poweroff",
         PCMK_ACTION_REBOOT, pcmk__is_fencing_action,
         pcmk__opt_schedulerd,
         N_("Action to send to fence device when a node needs to be fenced "
             "(\"poweroff\" is a deprecated alias for \"off\")"),
         NULL,
     },
     {
         PCMK_OPT_STONITH_TIMEOUT, NULL, "time", NULL,
         "60s", pcmk__valid_interval_spec,
         pcmk__opt_schedulerd,
         N_("How long to wait for on, off, and reboot fence actions to complete "
             "by default"),
         NULL,
     },
     {
         PCMK_OPT_HAVE_WATCHDOG, NULL, "boolean", NULL,
         PCMK_VALUE_FALSE, pcmk__valid_boolean,
         pcmk__opt_schedulerd|pcmk__opt_generated,
         N_("Whether watchdog integration is enabled"),
         N_("This is set automatically by the cluster according to whether SBD "
             "is detected to be in use. User-configured values are ignored. "
             "The value `true` is meaningful if diskless SBD is used and "
             "`stonith-watchdog-timeout` is nonzero. In that case, if fencing "
             "is required, watchdog-based self-fencing will be performed via "
             "SBD without requiring a fencing resource explicitly configured."),
     },
     {
         /* @COMPAT Currently, unparsable values default to -1 (auto-calculate),
          * while missing values default to 0 (disable). All values are accepted
          * (unless the controller finds that the value conflicts with the
          * SBD_WATCHDOG_TIMEOUT).
          *
          * At a compatibility break: properly validate as a timeout, let
          * either negative values or a particular string like "auto" mean auto-
          * calculate, and use 0 as the single default for when the option either
          * is unset or fails to validate.
          */
         PCMK_OPT_STONITH_WATCHDOG_TIMEOUT, NULL, "time", NULL,
         "0", NULL,
         pcmk__opt_controld,
         N_("How long before nodes can be assumed to be safely down when "
            "watchdog-based self-fencing via SBD is in use"),
         N_("If this is set to a positive value, lost nodes are assumed to "
            "achieve self-fencing using watchdog-based SBD within this much "
            "time. This does not require a fencing resource to be explicitly "
            "configured, though a fence_watchdog resource can be configured, to "
            "limit use to specific nodes. If this is set to 0 (the default), "
            "the cluster will never assume watchdog-based self-fencing. If this "
            "is set to a negative value, the cluster will use twice the local "
            "value of the `SBD_WATCHDOG_TIMEOUT` environment variable if that "
            "is positive, or otherwise treat this as 0. WARNING: When used, "
            "this timeout must be larger than `SBD_WATCHDOG_TIMEOUT` on all "
            "nodes that use watchdog-based SBD, and Pacemaker will refuse to "
            "start on any of those nodes where this is not true for the local "
            "value or SBD is not active. When this is set to a negative value, "
            "`SBD_WATCHDOG_TIMEOUT` must be set to the same value on all nodes "
            "that use SBD, otherwise data corruption or loss could occur."),
     },
     {
         PCMK_OPT_STONITH_MAX_ATTEMPTS, NULL, "integer", NULL,
         "10", pcmk__valid_positive_int,
         pcmk__opt_controld,
         N_("How many times fencing can fail before it will no longer be "
             "immediately re-attempted on a target"),
         NULL,
     },
     {
         PCMK_OPT_CONCURRENT_FENCING, NULL, "boolean", NULL,
         PCMK__CONCURRENT_FENCING_DEFAULT, pcmk__valid_boolean,
         pcmk__opt_schedulerd,
         N_("Allow performing fencing operations in parallel"),
         NULL,
     },
     {
         PCMK_OPT_STARTUP_FENCING, NULL, "boolean", NULL,
         PCMK_VALUE_TRUE, pcmk__valid_boolean,
         pcmk__opt_schedulerd|pcmk__opt_advanced,
         N_("Whether to fence unseen nodes at start-up"),
         N_("Setting this to false may lead to a \"split-brain\" situation, "
             "potentially leading to data loss and/or service unavailability."),
     },
     {
         PCMK_OPT_PRIORITY_FENCING_DELAY, NULL, "time", NULL,
         "0", pcmk__valid_interval_spec,
         pcmk__opt_schedulerd,
         N_("Apply fencing delay targeting the lost nodes with the highest "
             "total resource priority"),
         N_("Apply specified delay for the fencings that are targeting the lost "
             "nodes with the highest total resource priority in case we don't "
             "have the majority of the nodes in our cluster partition, so that "
             "the more significant nodes potentially win any fencing match, "
             "which is especially meaningful under split-brain of 2-node "
             "cluster. A promoted resource instance takes the base priority + 1 "
             "on calculation if the base priority is not 0. Any static/random "
             "delays that are introduced by `pcmk_delay_base/max` configured "
             "for the corresponding fencing resources will be added to this "
             "delay. This delay should be significantly greater than, safely "
             "twice, the maximum `pcmk_delay_base/max`. By default, priority "
             "fencing delay is disabled."),
     },
     {
         PCMK_OPT_NODE_PENDING_TIMEOUT, NULL, "time", NULL,
         "0", pcmk__valid_interval_spec,
         pcmk__opt_schedulerd,
         N_("How long to wait for a node that has joined the cluster to join "
            "the controller process group"),
         N_("Fence nodes that do not join the controller process group within "
            "this much time after joining the cluster, to allow the cluster "
            "to continue managing resources. A value of 0 means never fence "
            "pending nodes. Setting the value to 2h means fence nodes after "
            "2 hours."),
     },
     {
         PCMK_OPT_CLUSTER_DELAY, NULL, "time", NULL,
         "60s", pcmk__valid_interval_spec,
         pcmk__opt_schedulerd,
         N_("Maximum time for node-to-node communication"),
         N_("The node elected Designated Controller (DC) will consider an action "
             "failed if it does not get a response from the node executing the "
             "action within this time (after considering the action's own "
             "timeout). The \"correct\" value will depend on the speed and "
             "load of your network and cluster nodes.")
     },
 
     // Limits
     {
         PCMK_OPT_LOAD_THRESHOLD, NULL, "percentage", NULL,
         "80%", pcmk__valid_percentage,
         pcmk__opt_controld,
         N_("Maximum amount of system load that should be used by cluster "
             "nodes"),
         N_("The cluster will slow down its recovery process when the amount of "
             "system resources used (currently CPU) approaches this limit"),
     },
     {
         PCMK_OPT_NODE_ACTION_LIMIT, NULL, "integer", NULL,
         "0", pcmk__valid_int,
         pcmk__opt_controld,
         N_("Maximum number of jobs that can be scheduled per node (defaults to "
             "2x cores)"),
         NULL,
     },
     {
         PCMK_OPT_BATCH_LIMIT, NULL, "integer", NULL,
         "0", pcmk__valid_int,
         pcmk__opt_schedulerd,
         N_("Maximum number of jobs that the cluster may execute in parallel "
             "across all nodes"),
         N_("The \"correct\" value will depend on the speed and load of your "
             "network and cluster nodes. If set to 0, the cluster will "
             "impose a dynamically calculated limit when any node has a "
             "high load."),
     },
     {
         PCMK_OPT_MIGRATION_LIMIT, NULL, "integer", NULL,
         "-1", pcmk__valid_int,
         pcmk__opt_schedulerd,
         N_("The number of live migration actions that the cluster is allowed "
             "to execute in parallel on a node (-1 means no limit)"),
         NULL,
     },
     {
         PCMK_OPT_CLUSTER_IPC_LIMIT, NULL, "integer", NULL,
         "500", pcmk__valid_positive_int,
         pcmk__opt_based,
         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)."),
     },
 
     // Orphans and stopping
     {
         PCMK_OPT_STOP_ALL_RESOURCES, NULL, "boolean", NULL,
         PCMK_VALUE_FALSE, pcmk__valid_boolean,
         pcmk__opt_schedulerd,
         N_("Whether the cluster should stop all active resources"),
         NULL,
     },
     {
         PCMK_OPT_STOP_ORPHAN_RESOURCES, NULL, "boolean", NULL,
         PCMK_VALUE_TRUE, pcmk__valid_boolean,
         pcmk__opt_schedulerd,
         N_("Whether to stop resources that were removed from the "
             "configuration"),
         NULL,
     },
     {
         PCMK_OPT_STOP_ORPHAN_ACTIONS, NULL, "boolean", NULL,
         PCMK_VALUE_TRUE, pcmk__valid_boolean,
         pcmk__opt_schedulerd,
         N_("Whether to cancel recurring actions removed from the "
             "configuration"),
         NULL,
     },
     {
         PCMK__OPT_REMOVE_AFTER_STOP, NULL, "boolean", NULL,
         PCMK_VALUE_FALSE, pcmk__valid_boolean,
         pcmk__opt_schedulerd|pcmk__opt_deprecated,
         N_("Whether to remove stopped resources from the executor"),
         N_("Values other than default are poorly tested and potentially "
             "dangerous."),
     },
 
     // Storing inputs
     {
         PCMK_OPT_PE_ERROR_SERIES_MAX, NULL, "integer", NULL,
         "-1", pcmk__valid_int,
         pcmk__opt_schedulerd,
         N_("The number of scheduler inputs resulting in errors to save"),
         N_("Zero to disable, -1 to store unlimited."),
     },
     {
         PCMK_OPT_PE_WARN_SERIES_MAX, NULL, "integer", NULL,
         "5000", pcmk__valid_int,
         pcmk__opt_schedulerd,
         N_("The number of scheduler inputs resulting in warnings to save"),
         N_("Zero to disable, -1 to store unlimited."),
     },
     {
         PCMK_OPT_PE_INPUT_SERIES_MAX, NULL, "integer", NULL,
         "4000", pcmk__valid_int,
         pcmk__opt_schedulerd,
         N_("The number of scheduler inputs without errors or warnings to save"),
         N_("Zero to disable, -1 to store unlimited."),
     },
 
     // Node health
     {
         PCMK_OPT_NODE_HEALTH_STRATEGY, NULL, "select",
             PCMK_VALUE_NONE ", " PCMK_VALUE_MIGRATE_ON_RED ", "
                 PCMK_VALUE_ONLY_GREEN ", " PCMK_VALUE_PROGRESSIVE ", "
                 PCMK_VALUE_CUSTOM,
         PCMK_VALUE_NONE, pcmk__validate_health_strategy,
         pcmk__opt_schedulerd,
         N_("How cluster should react to node health attributes"),
         N_("Requires external entities to create node attributes (named with "
             "the prefix \"#health\") with values \"red\", \"yellow\", or "
             "\"green\".")
     },
     {
         PCMK_OPT_NODE_HEALTH_BASE, NULL, "integer", NULL,
         "0", pcmk__valid_int,
         pcmk__opt_schedulerd,
         N_("Base health score assigned to a node"),
         N_("Only used when \"node-health-strategy\" is set to "
             "\"progressive\"."),
     },
     {
         PCMK_OPT_NODE_HEALTH_GREEN, NULL, "integer", NULL,
         "0", pcmk__valid_int,
         pcmk__opt_schedulerd,
         N_("The score to use for a node health attribute whose value is "
             "\"green\""),
         N_("Only used when \"node-health-strategy\" is set to \"custom\" or "
             "\"progressive\"."),
     },
     {
         PCMK_OPT_NODE_HEALTH_YELLOW, NULL, "integer", NULL,
         "0", pcmk__valid_int,
         pcmk__opt_schedulerd,
         N_("The score to use for a node health attribute whose value is "
             "\"yellow\""),
         N_("Only used when \"node-health-strategy\" is set to \"custom\" or "
             "\"progressive\"."),
     },
     {
         PCMK_OPT_NODE_HEALTH_RED, NULL, "integer", NULL,
         "-INFINITY", pcmk__valid_int,
         pcmk__opt_schedulerd,
         N_("The score to use for a node health attribute whose value is "
             "\"red\""),
         N_("Only used when \"node-health-strategy\" is set to \"custom\" or "
             "\"progressive\".")
     },
 
     // Placement strategy
     {
         PCMK_OPT_PLACEMENT_STRATEGY, NULL, "select",
             PCMK_VALUE_DEFAULT ", " PCMK_VALUE_UTILIZATION ", "
                 PCMK_VALUE_MINIMAL ", " PCMK_VALUE_BALANCED,
         PCMK_VALUE_DEFAULT, pcmk__valid_placement_strategy,
         pcmk__opt_schedulerd,
         N_("How the cluster should allocate resources to nodes"),
         NULL,
     },
 
     { NULL, },
 };
 
 
 /*
  * Environment variable option handling
  */
 
 /*!
  * \internal
  * \brief Get the value of a Pacemaker environment variable option
  *
  * If an environment variable option is set, with either a PCMK_ or (for
  * backward compatibility) HA_ prefix, log and return the value.
  *
  * \param[in] option  Environment variable name (without prefix)
  *
  * \return Value of environment variable option, or NULL in case of
  *         option name too long or value not found
  */
 const char *
 pcmk__env_option(const char *option)
 {
     const char *const prefixes[] = {"PCMK_", "HA_"};
     char env_name[NAME_MAX];
     const char *value = NULL;
 
     CRM_CHECK(!pcmk__str_empty(option), return NULL);
 
     for (int i = 0; i < PCMK__NELEM(prefixes); i++) {
         int rv = snprintf(env_name, NAME_MAX, "%s%s", prefixes[i], option);
 
         if (rv < 0) {
             crm_err("Failed to write %s%s to buffer: %s", prefixes[i], option,
                     strerror(errno));
             return NULL;
         }
 
         if (rv >= sizeof(env_name)) {
             crm_trace("\"%s%s\" is too long", prefixes[i], option);
             continue;
         }
 
         value = getenv(env_name);
         if (value != NULL) {
             crm_trace("Found %s = %s", env_name, value);
             return value;
         }
     }
 
     crm_trace("Nothing found for %s", option);
     return NULL;
 }
 
 /*!
  * \brief Set or unset a Pacemaker environment variable option
  *
  * Set an environment variable option with a \c "PCMK_" prefix and optionally
  * an \c "HA_" prefix for backward compatibility.
  *
  * \param[in] option  Environment variable name (without prefix)
  * \param[in] value   New value (or NULL to unset)
  * \param[in] compat  If false and \p value is not \c NULL, set only
  *                    \c "PCMK_<option>"; otherwise, set (or unset) both
  *                    \c "PCMK_<option>" and \c "HA_<option>"
  *
  * \note \p compat is ignored when \p value is \c NULL. A \c NULL \p value
  *       means we're unsetting \p option. \c pcmk__get_env_option() checks for
  *       both prefixes, so we want to clear them both.
  */
 void
 pcmk__set_env_option(const char *option, const char *value, bool compat)
 {
     // @COMPAT Drop support for "HA_" options eventually
     const char *const prefixes[] = {"PCMK_", "HA_"};
     char env_name[NAME_MAX];
 
     CRM_CHECK(!pcmk__str_empty(option) && (strchr(option, '=') == NULL),
               return);
 
     for (int i = 0; i < PCMK__NELEM(prefixes); i++) {
         int rv = snprintf(env_name, NAME_MAX, "%s%s", prefixes[i], option);
 
         if (rv < 0) {
             crm_err("Failed to write %s%s to buffer: %s", prefixes[i], option,
                     strerror(errno));
             return;
         }
 
         if (rv >= sizeof(env_name)) {
             crm_trace("\"%s%s\" is too long", prefixes[i], option);
             continue;
         }
 
         if (value != NULL) {
             crm_trace("Setting %s to %s", env_name, value);
             rv = setenv(env_name, value, 1);
         } else {
             crm_trace("Unsetting %s", env_name);
             rv = unsetenv(env_name);
         }
 
         if (rv < 0) {
             crm_err("Failed to %sset %s: %s", (value != NULL)? "" : "un",
                     env_name, strerror(errno));
         }
 
         if (!compat && (value != NULL)) {
             // For set, don't proceed to HA_<option> unless compat is enabled
             break;
         }
     }
 }
 
 /*!
  * \internal
  * \brief Check whether Pacemaker environment variable option is enabled
  *
  * Given a Pacemaker environment variable option that can either be boolean
  * or a list of daemon names, return true if the option is enabled for a given
  * daemon.
  *
  * \param[in] daemon   Daemon name (can be NULL)
  * \param[in] option   Pacemaker environment variable name
  *
  * \return true if variable is enabled for daemon, otherwise false
  */
 bool
 pcmk__env_option_enabled(const char *daemon, const char *option)
 {
     const char *value = pcmk__env_option(option);
 
     return (value != NULL)
         && (crm_is_true(value)
             || ((daemon != NULL) && (strstr(value, daemon) != NULL)));
 }
 
 
 /*
  * Cluster option handling
  */
 
 /*!
  * \internal
  * \brief Check whether a string represents a valid interval specification
  *
  * \param[in] value  String to validate
  *
  * \return \c true if \p value is a valid interval specification, or \c false
  *         otherwise
  */
 bool
 pcmk__valid_interval_spec(const char *value)
 {
     return pcmk_parse_interval_spec(value, NULL) == pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Check whether a string represents a valid boolean value
  *
  * \param[in] value  String to validate
  *
  * \return \c true if \p value is a valid boolean value, or \c false otherwise
  */
 bool
 pcmk__valid_boolean(const char *value)
 {
     return crm_str_to_boolean(value, NULL) == 1;
 }
 
 /*!
  * \internal
  * \brief Check whether a string represents a valid integer
  *
  * Valid values include \c INFINITY, \c -INFINITY, and all 64-bit integers.
  *
  * \param[in] value  String to validate
  *
  * \return \c true if \p value is a valid integer, or \c false otherwise
  */
 bool
 pcmk__valid_int(const char *value)
 {
     return (value != NULL)
            && (pcmk_str_is_infinity(value)
                || pcmk_str_is_minus_infinity(value)
                || (pcmk__scan_ll(value, NULL, 0LL) == pcmk_rc_ok));
 }
 
 /*!
  * \internal
  * \brief Check whether a string represents a valid positive integer
  *
  * Valid values include \c INFINITY and all 64-bit positive integers.
  *
  * \param[in] value  String to validate
  *
  * \return \c true if \p value is a valid positive integer, or \c false
  *         otherwise
  */
 bool
 pcmk__valid_positive_int(const char *value)
 {
     long long num = 0LL;
 
     return pcmk_str_is_infinity(value)
            || ((pcmk__scan_ll(value, &num, 0LL) == pcmk_rc_ok)
                && (num > 0));
 }
 
 /*!
  * \internal
  * \brief Check whether a string represents a valid
  *        \c PCMK__OPT_NO_QUORUM_POLICY value
  *
  * \param[in] value  String to validate
  *
  * \return \c true if \p value is a valid \c PCMK__OPT_NO_QUORUM_POLICY value,
  *         or \c false otherwise
  */
 bool
 pcmk__valid_no_quorum_policy(const char *value)
 {
     return pcmk__strcase_any_of(value,
                                 PCMK_VALUE_STOP, PCMK_VALUE_FREEZE,
                                 PCMK_VALUE_IGNORE, PCMK_VALUE_DEMOTE,
                                 PCMK_VALUE_FENCE_LEGACY, NULL);
 }
 
 /*!
  * \internal
  * \brief Check whether a string represents a valid percentage
  *
  * Valid values include long integers, with an optional trailing string
  * beginning with '%'.
  *
  * \param[in] value  String to validate
  *
  * \return \c true if \p value is a valid percentage value, or \c false
  *         otherwise
  */
 bool
 pcmk__valid_percentage(const char *value)
 {
     char *end = NULL;
     float number = strtof(value, &end);
 
     return ((end == NULL) || (end[0] == '%')) && (number >= 0);
 }
 
 /*!
  * \internal
  * \brief Check whether a string represents a valid script
  *
  * Valid values include \c /dev/null and paths of executable regular files
  *
  * \param[in] value  String to validate
  *
  * \return \c true if \p value is a valid script, or \c false otherwise
  */
 bool
 pcmk__valid_script(const char *value)
 {
     struct stat st;
 
     if (pcmk__str_eq(value, "/dev/null", pcmk__str_none)) {
         return true;
     }
 
     if (stat(value, &st) != 0) {
         crm_err("Script %s does not exist", value);
         return false;
     }
 
     if (S_ISREG(st.st_mode) == 0) {
         crm_err("Script %s is not a regular file", value);
         return false;
     }
 
     if ((st.st_mode & (S_IXUSR | S_IXGRP)) == 0) {
         crm_err("Script %s is not executable", value);
         return false;
     }
 
     return true;
 }
 
 /*!
  * \internal
  * \brief Check whether a string represents a valid placement strategy
  *
  * \param[in] value  String to validate
  *
  * \return \c true if \p value is a valid placement strategy, or \c false
  *         otherwise
  */
 bool
 pcmk__valid_placement_strategy(const char *value)
 {
     return pcmk__strcase_any_of(value,
                                 PCMK_VALUE_DEFAULT, PCMK_VALUE_UTILIZATION,
                                 PCMK_VALUE_MINIMAL, PCMK_VALUE_BALANCED, NULL);
 }
 
 /*!
  * \internal
  * \brief Check a table of configured options for a particular option
  *
  * \param[in,out] table   Name/value pairs for configured options
  * \param[in]     option  Option to look up
  *
  * \return Option value (from supplied options table or default value)
  */
 static const char *
 cluster_option_value(GHashTable *table, const pcmk__cluster_option_t *option)
 {
     const char *value = NULL;
 
     CRM_ASSERT((option != NULL) && (option->name != NULL));
 
     if (table != NULL) {
         value = g_hash_table_lookup(table, option->name);
 
         if ((value == NULL) && (option->alt_name != NULL)) {
             value = g_hash_table_lookup(table, option->alt_name);
             if (value != NULL) {
                 pcmk__config_warn("Support for legacy name '%s' for cluster "
                                   "option '%s' is deprecated and will be "
                                   "removed in a future release",
                                   option->alt_name, option->name);
 
                 // Inserting copy with current name ensures we only warn once
                 pcmk__insert_dup(table, option->name, value);
             }
         }
 
         if ((value != NULL) && (option->is_valid != NULL)
             && !option->is_valid(value)) {
 
             pcmk__config_err("Using default value for cluster option '%s' "
                              "because '%s' is invalid", option->name, value);
             value = NULL;
         }
 
         if (value != NULL) {
             return value;
         }
     }
 
     // No value found, use default
     value = option->default_value;
 
     if (value == NULL) {
         crm_trace("No value or default provided for cluster option '%s'",
                   option->name);
         return NULL;
     }
 
     CRM_CHECK((option->is_valid == NULL) || option->is_valid(value),
               crm_err("Bug: default value for cluster option '%s' is invalid",
                       option->name);
               return NULL);
 
     crm_trace("Using default value '%s' for cluster option '%s'",
               value, option->name);
     if (table != NULL) {
         pcmk__insert_dup(table, option->name, value);
     }
     return value;
 }
 
 /*!
  * \internal
  * \brief Get the value of a cluster option
  *
  * \param[in,out] options  Name/value pairs for configured options
  * \param[in]     name     (Primary) option name to look for
  *
  * \return Option value
  */
 const char *
 pcmk__cluster_option(GHashTable *options, const char *name)
 {
     for (const pcmk__cluster_option_t *option = cluster_options;
          option->name != NULL; option++) {
 
         if (pcmk__str_eq(name, option->name, pcmk__str_casei)) {
             return cluster_option_value(options, option);
         }
     }
     CRM_CHECK(FALSE, crm_err("Bug: looking for unknown option '%s'", name));
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Output cluster option metadata as OCF-like XML
  *
  * \param[in,out] out         Output object
  * \param[in]     name        Fake resource agent name for the option list
  * \param[in]     desc_short  Short description of the option list
  * \param[in]     desc_long   Long description of the option list
  * \param[in]     filter      Group of <tt>enum pcmk__opt_flags</tt>; output an
  *                            option only if its \c flags member has all these
  *                            flags set
  * \param[in]     all         If \c true, output all options; otherwise, exclude
  *                            advanced and deprecated options unless
  *                            \c pcmk__opt_advanced and \c pcmk__opt_deprecated
  *                            flags (respectively) are set in \p filter. This is
  *                            always treated as true for XML output objects.
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__output_cluster_options(pcmk__output_t *out, const char *name,
                              const char *desc_short, const char *desc_long,
                              uint32_t filter, bool all)
 {
     return out->message(out, "option-list", name, desc_short, desc_long, filter,
                         cluster_options, all);
 }
 
 /*!
  * \internal
  * \brief Output a list of cluster options for a daemon
  *
  * \brief[in,out] out         Output object
  * \brief[in]     name        Daemon name
  * \brief[in]     desc_short  Short description of the option list
  * \brief[in]     desc_long   Long description of the option list
  * \brief[in]     filter      <tt>enum pcmk__opt_flags</tt> flag corresponding
  *                            to daemon
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__daemon_metadata(pcmk__output_t *out, const char *name,
                       const char *desc_short, const char *desc_long,
                       enum pcmk__opt_flags filter)
 {
     // @COMPAT Drop this function when we drop daemon metadata
     pcmk__output_t *tmp_out = NULL;
     xmlNode *top = NULL;
     const xmlNode *metadata = NULL;
-    gchar *metadata_s = NULL;
+    GString *metadata_s = NULL;
 
     int rc = pcmk__output_new(&tmp_out, "xml", "/dev/null", NULL);
     if (rc != pcmk_rc_ok) {
         return rc;
     }
 
     pcmk__output_cluster_options(tmp_out, name, desc_short, desc_long,
                                  (uint32_t) filter, true);
 
     tmp_out->finish(tmp_out, CRM_EX_OK, false, (void **) &top);
     metadata = first_named_child(top, PCMK_XE_RESOURCE_AGENT);
-    metadata_s = pcmk__xml_dump(metadata,
-                                pcmk__xml_fmt_pretty|pcmk__xml_fmt_text);
 
-    out->output_xml(out, PCMK_XE_METADATA, metadata_s);
+    metadata_s = g_string_sized_new(16384);
+    pcmk__xml_string(metadata, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text,
+                     metadata_s, 0);
+
+    out->output_xml(out, PCMK_XE_METADATA, metadata_s->str);
 
     pcmk__output_free(tmp_out);
     free_xml(top);
-    g_free(metadata_s);
+    g_string_free(metadata_s, TRUE);
     return pcmk_rc_ok;
 }
 
 void
 pcmk__validate_cluster_options(GHashTable *options)
 {
     for (const pcmk__cluster_option_t *option = cluster_options;
          option->name != NULL; option++) {
 
         cluster_option_value(options, option);
     }
 }
diff --git a/lib/common/patchset_display.c b/lib/common/patchset_display.c
index 02c75a0786..6120b6f9ed 100644
--- a/lib/common/patchset_display.c
+++ b/lib/common/patchset_display.c
@@ -1,523 +1,525 @@
 /*
  * Copyright 2004-2024 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/common/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}: \c PCMK_XA_ADMIN_EPOCH
  * * \p {1}: \c PCMK_XA_EPOCH
  * * \p {2}: \c PCMK_XA_NUM_UPDATES
  *
  * 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, PCMK_XA_FORMAT);
         const char *digest = crm_element_value(patchset, PCMK__XA_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 ((data->children == NULL)
         || (crm_element_value(data, PCMK__XA_CRM_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, PCMK__XE_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, PCMK__XE_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, PCMK_XA_OPERATION);
         const char *xpath = crm_element_value(change, PCMK_XA_PATH);
 
         if (op == NULL) {
             continue;
         }
 
         if (strcmp(op, PCMK_VALUE_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, PCMK_VALUE_MOVE) == 0) {
             const char *position = crm_element_value(change, PCMK_XE_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, PCMK_VALUE_MODIFY) == 0) {
             xmlNode *clist = first_named_child(change, PCMK_XE_CHANGE_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, PCMK_XA_NAME);
 
                 op = crm_element_value(child, PCMK_XA_OPERATION);
                 if (op == NULL) {
                     continue;
                 }
 
                 if (strcmp(op, "set") == 0) {
                     const char *value = crm_element_value(child, PCMK_XA_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, PCMK_VALUE_DELETE) == 0) {
             int position = -1;
 
             crm_element_value_int(change, PCMK_XE_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.
  *
  * \param[in,out] out   Output object
  * \param[in]     args  Message-specific arguments
  *
  * \return Standard Pacemaker return code
  *
  * \note \p args should contain the following:
  *       -# XML patchset
  */
 PCMK__OUTPUT_ARGS("xml-patchset", "const xmlNode *")
 static int
 xml_patchset_default(pcmk__output_t *out, va_list args)
 {
     const xmlNode *patchset = va_arg(args, const xmlNode *);
 
     int format = 1;
 
     if (patchset == NULL) {
         crm_trace("Empty patch");
         return pcmk_rc_no_output;
     }
 
     crm_element_value_int(patchset, PCMK_XA_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_bad_xml_patch;
     }
 }
 
 /*!
  * \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 the following:
  *       -# XML patchset
  */
 PCMK__OUTPUT_ARGS("xml-patchset", "const xmlNode *")
 static int
 xml_patchset_log(pcmk__output_t *out, va_list args)
 {
     static struct qb_log_callsite *patchset_cs = NULL;
 
     const xmlNode *patchset = va_arg(args, const xmlNode *);
 
     uint8_t log_level = pcmk__output_get_log_level(out);
     int format = 1;
 
     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, PCMK_XA_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_bad_xml_patch;
     }
 }
 
 /*!
  * \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 the following:
  *       -# XML patchset
  */
 PCMK__OUTPUT_ARGS("xml-patchset", "const xmlNode *")
 static int
 xml_patchset_xml(pcmk__output_t *out, va_list args)
 {
     const xmlNode *patchset = va_arg(args, const xmlNode *);
 
     if (patchset != NULL) {
-        gchar *buf = pcmk__xml_dump(patchset,
-                                    pcmk__xml_fmt_pretty|pcmk__xml_fmt_text);
+        GString *buf = g_string_sized_new(1024);
 
-        out->output_xml(out, PCMK_XE_XML_PATCHSET, buf);
-        g_free(buf);
+        pcmk__xml_string(patchset, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, buf,
+                         0);
+
+        out->output_xml(out, PCMK_XE_XML_PATCHSET, buf->str);
+        g_string_free(buf, TRUE);
         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;
         case LOG_STDOUT:
             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;
             }
             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, PCMK_XA_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_bad_xml_patch;
             break;
     }
 
 done:
     out->finish(out, pcmk_rc2exitc(rc), true, NULL);
     pcmk__output_free(out);
 }
 
 // LCOV_EXCL_STOP
 // End deprecated API
diff --git a/lib/common/remote.c b/lib/common/remote.c
index 0a42a00bd7..c70dcd8147 100644
--- a/lib/common/remote.c
+++ b/lib/common/remote.c
@@ -1,1284 +1,1286 @@
 /*
  * Copyright 2008-2024 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/crm.h>
 
 #include <sys/param.h>
 #include <stdio.h>
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <unistd.h>
 #include <sys/socket.h>
 #include <arpa/inet.h>
 #include <netinet/in.h>
 #include <netinet/ip.h>
 #include <netinet/tcp.h>
 #include <netdb.h>
 #include <stdlib.h>
 #include <errno.h>
 #include <inttypes.h>   // PRIx32
 
 #include <glib.h>
 #include <bzlib.h>
 
 #include <crm/common/ipc_internal.h>
 #include <crm/common/xml.h>
 #include <crm/common/mainloop.h>
 #include <crm/common/remote_internal.h>
 
 #ifdef HAVE_GNUTLS_GNUTLS_H
 #  include <gnutls/gnutls.h>
 #endif
 
 /* Swab macros from linux/swab.h */
 #ifdef HAVE_LINUX_SWAB_H
 #  include <linux/swab.h>
 #else
 /*
  * casts are necessary for constants, because we never know how for sure
  * how U/UL/ULL map to __u16, __u32, __u64. At least not in a portable way.
  */
 #define __swab16(x) ((uint16_t)(                                      \
         (((uint16_t)(x) & (uint16_t)0x00ffU) << 8) |                  \
         (((uint16_t)(x) & (uint16_t)0xff00U) >> 8)))
 
 #define __swab32(x) ((uint32_t)(                                      \
         (((uint32_t)(x) & (uint32_t)0x000000ffUL) << 24) |            \
         (((uint32_t)(x) & (uint32_t)0x0000ff00UL) <<  8) |            \
         (((uint32_t)(x) & (uint32_t)0x00ff0000UL) >>  8) |            \
         (((uint32_t)(x) & (uint32_t)0xff000000UL) >> 24)))
 
 #define __swab64(x) ((uint64_t)(                                      \
         (((uint64_t)(x) & (uint64_t)0x00000000000000ffULL) << 56) |   \
         (((uint64_t)(x) & (uint64_t)0x000000000000ff00ULL) << 40) |   \
         (((uint64_t)(x) & (uint64_t)0x0000000000ff0000ULL) << 24) |   \
         (((uint64_t)(x) & (uint64_t)0x00000000ff000000ULL) <<  8) |   \
         (((uint64_t)(x) & (uint64_t)0x000000ff00000000ULL) >>  8) |   \
         (((uint64_t)(x) & (uint64_t)0x0000ff0000000000ULL) >> 24) |   \
         (((uint64_t)(x) & (uint64_t)0x00ff000000000000ULL) >> 40) |   \
         (((uint64_t)(x) & (uint64_t)0xff00000000000000ULL) >> 56)))
 #endif
 
 #define REMOTE_MSG_VERSION 1
 #define ENDIAN_LOCAL 0xBADADBBD
 
 struct remote_header_v0 {
     uint32_t endian;    /* Detect messages from hosts with different endian-ness */
     uint32_t version;
     uint64_t id;
     uint64_t flags;
     uint32_t size_total;
     uint32_t payload_offset;
     uint32_t payload_compressed;
     uint32_t payload_uncompressed;
 
         /* New fields get added here */
 
 } __attribute__ ((packed));
 
 /*!
  * \internal
  * \brief Retrieve remote message header, in local endianness
  *
  * Return a pointer to the header portion of a remote connection's message
  * buffer, converting the header to local endianness if needed.
  *
  * \param[in,out] remote  Remote connection with new message
  *
  * \return Pointer to message header, localized if necessary
  */
 static struct remote_header_v0 *
 localized_remote_header(pcmk__remote_t *remote)
 {
     struct remote_header_v0 *header = (struct remote_header_v0 *)remote->buffer;
     if(remote->buffer_offset < sizeof(struct remote_header_v0)) {
         return NULL;
 
     } else if(header->endian != ENDIAN_LOCAL) {
         uint32_t endian = __swab32(header->endian);
 
         CRM_LOG_ASSERT(endian == ENDIAN_LOCAL);
         if(endian != ENDIAN_LOCAL) {
             crm_err("Invalid message detected, endian mismatch: %" PRIx32
                     " is neither %" PRIx32 " nor the swab'd %" PRIx32,
                     ENDIAN_LOCAL, header->endian, endian);
             return NULL;
         }
 
         header->id = __swab64(header->id);
         header->flags = __swab64(header->flags);
         header->endian = __swab32(header->endian);
 
         header->version = __swab32(header->version);
         header->size_total = __swab32(header->size_total);
         header->payload_offset = __swab32(header->payload_offset);
         header->payload_compressed = __swab32(header->payload_compressed);
         header->payload_uncompressed = __swab32(header->payload_uncompressed);
     }
 
     return header;
 }
 
 #ifdef HAVE_GNUTLS_GNUTLS_H
 
 int
 pcmk__tls_client_handshake(pcmk__remote_t *remote, int timeout_ms)
 {
     int rc = 0;
     int pollrc = 0;
     time_t time_limit = time(NULL) + timeout_ms / 1000;
 
     do {
         rc = gnutls_handshake(*remote->tls_session);
         if ((rc == GNUTLS_E_INTERRUPTED) || (rc == GNUTLS_E_AGAIN)) {
             pollrc = pcmk__remote_ready(remote, 1000);
             if ((pollrc != pcmk_rc_ok) && (pollrc != ETIME)) {
                 /* poll returned error, there is no hope */
                 crm_trace("TLS handshake poll failed: %s (%d)",
                           pcmk_strerror(pollrc), pollrc);
                 return pcmk_legacy2rc(pollrc);
             }
         } else if (rc < 0) {
             crm_trace("TLS handshake failed: %s (%d)",
                       gnutls_strerror(rc), rc);
             return EPROTO;
         } else {
             return pcmk_rc_ok;
         }
     } while (time(NULL) < time_limit);
     return ETIME;
 }
 
 /*!
  * \internal
  * \brief Set minimum prime size required by TLS client
  *
  * \param[in] session  TLS session to affect
  */
 static void
 set_minimum_dh_bits(const gnutls_session_t *session)
 {
     int dh_min_bits;
 
     pcmk__scan_min_int(pcmk__env_option(PCMK__ENV_DH_MIN_BITS), &dh_min_bits,
                        0);
 
     /* This function is deprecated since GnuTLS 3.1.7, in favor of letting
      * the priority string imply the DH requirements, but this is the only
      * way to give the user control over compatibility with older servers.
      */
     if (dh_min_bits > 0) {
         crm_info("Requiring server use a Diffie-Hellman prime of at least %d bits",
                  dh_min_bits);
         gnutls_dh_set_prime_bits(*session, dh_min_bits);
     }
 }
 
 static unsigned int
 get_bound_dh_bits(unsigned int dh_bits)
 {
     int dh_min_bits;
     int dh_max_bits;
 
     pcmk__scan_min_int(pcmk__env_option(PCMK__ENV_DH_MIN_BITS), &dh_min_bits,
                        0);
     pcmk__scan_min_int(pcmk__env_option(PCMK__ENV_DH_MAX_BITS), &dh_max_bits,
                        0);
 
     if ((dh_max_bits > 0) && (dh_max_bits < dh_min_bits)) {
         crm_warn("Ignoring PCMK_dh_max_bits less than PCMK_dh_min_bits");
         dh_max_bits = 0;
     }
     if ((dh_min_bits > 0) && (dh_bits < dh_min_bits)) {
         return dh_min_bits;
     }
     if ((dh_max_bits > 0) && (dh_bits > dh_max_bits)) {
         return dh_max_bits;
     }
     return dh_bits;
 }
 
 /*!
  * \internal
  * \brief Initialize a new TLS session
  *
  * \param[in] csock       Connected socket for TLS session
  * \param[in] conn_type   GNUTLS_SERVER or GNUTLS_CLIENT
  * \param[in] cred_type   GNUTLS_CRD_ANON or GNUTLS_CRD_PSK
  * \param[in] credentials TLS session credentials
  *
  * \return Pointer to newly created session object, or NULL on error
  */
 gnutls_session_t *
 pcmk__new_tls_session(int csock, unsigned int conn_type,
                       gnutls_credentials_type_t cred_type, void *credentials)
 {
     int rc = GNUTLS_E_SUCCESS;
     const char *prio_base = NULL;
     char *prio = NULL;
     gnutls_session_t *session = NULL;
 
     /* Determine list of acceptable ciphers, etc. Pacemaker always adds the
      * values required for its functionality.
      *
      * For an example of anonymous authentication, see:
      * http://www.manpagez.com/info/gnutls/gnutls-2.10.4/gnutls_81.php#Echo-Server-with-anonymous-authentication
      */
 
     prio_base = pcmk__env_option(PCMK__ENV_TLS_PRIORITIES);
     if (prio_base == NULL) {
         prio_base = PCMK_GNUTLS_PRIORITIES;
     }
     prio = crm_strdup_printf("%s:%s", prio_base,
                              (cred_type == GNUTLS_CRD_ANON)? "+ANON-DH" : "+DHE-PSK:+PSK");
 
     session = gnutls_malloc(sizeof(gnutls_session_t));
     if (session == NULL) {
         rc = GNUTLS_E_MEMORY_ERROR;
         goto error;
     }
 
     rc = gnutls_init(session, conn_type);
     if (rc != GNUTLS_E_SUCCESS) {
         goto error;
     }
 
     /* @TODO On the server side, it would be more efficient to cache the
      * priority with gnutls_priority_init2() and set it with
      * gnutls_priority_set() for all sessions.
      */
     rc = gnutls_priority_set_direct(*session, prio, NULL);
     if (rc != GNUTLS_E_SUCCESS) {
         goto error;
     }
     if (conn_type == GNUTLS_CLIENT) {
         set_minimum_dh_bits(session);
     }
 
     gnutls_transport_set_ptr(*session,
                              (gnutls_transport_ptr_t) GINT_TO_POINTER(csock));
 
     rc = gnutls_credentials_set(*session, cred_type, credentials);
     if (rc != GNUTLS_E_SUCCESS) {
         goto error;
     }
     free(prio);
     return session;
 
 error:
     crm_err("Could not initialize %s TLS %s session: %s "
             CRM_XS " rc=%d priority='%s'",
             (cred_type == GNUTLS_CRD_ANON)? "anonymous" : "PSK",
             (conn_type == GNUTLS_SERVER)? "server" : "client",
             gnutls_strerror(rc), rc, prio);
     free(prio);
     if (session != NULL) {
         gnutls_free(session);
     }
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Initialize Diffie-Hellman parameters for a TLS server
  *
  * \param[out] dh_params  Parameter object to initialize
  *
  * \return Standard Pacemaker return code
  * \todo The current best practice is to allow the client and server to
  *       negotiate the Diffie-Hellman parameters via a TLS extension (RFC 7919).
  *       However, we have to support both older versions of GnuTLS (<3.6) that
  *       don't support the extension on our side, and older Pacemaker versions
  *       that don't support the extension on the other side. The next best
  *       practice would be to use a known good prime (see RFC 5114 section 2.2),
  *       possibly stored in a file distributed with Pacemaker.
  */
 int
 pcmk__init_tls_dh(gnutls_dh_params_t *dh_params)
 {
     int rc = GNUTLS_E_SUCCESS;
     unsigned int dh_bits = 0;
 
     rc = gnutls_dh_params_init(dh_params);
     if (rc != GNUTLS_E_SUCCESS) {
         goto error;
     }
 
     dh_bits = gnutls_sec_param_to_pk_bits(GNUTLS_PK_DH,
                                           GNUTLS_SEC_PARAM_NORMAL);
     if (dh_bits == 0) {
         rc = GNUTLS_E_DH_PRIME_UNACCEPTABLE;
         goto error;
     }
     dh_bits = get_bound_dh_bits(dh_bits);
 
     crm_info("Generating Diffie-Hellman parameters with %u-bit prime for TLS",
              dh_bits);
     rc = gnutls_dh_params_generate2(*dh_params, dh_bits);
     if (rc != GNUTLS_E_SUCCESS) {
         goto error;
     }
 
     return pcmk_rc_ok;
 
 error:
     crm_err("Could not initialize Diffie-Hellman parameters for TLS: %s "
             CRM_XS " rc=%d", gnutls_strerror(rc), rc);
     return EPROTO;
 }
 
 /*!
  * \internal
  * \brief Process handshake data from TLS client
  *
  * Read as much TLS handshake data as is available.
  *
  * \param[in] client  Client connection
  *
  * \return Standard Pacemaker return code (of particular interest, EAGAIN
  *         if some data was successfully read but more data is needed)
  */
 int
 pcmk__read_handshake_data(const pcmk__client_t *client)
 {
     int rc = 0;
 
     CRM_ASSERT(client && client->remote && client->remote->tls_session);
 
     do {
         rc = gnutls_handshake(*client->remote->tls_session);
     } while (rc == GNUTLS_E_INTERRUPTED);
 
     if (rc == GNUTLS_E_AGAIN) {
         /* No more data is available at the moment. This function should be
          * invoked again once the client sends more.
          */
         return EAGAIN;
     } else if (rc != GNUTLS_E_SUCCESS) {
         crm_err("TLS handshake with remote client failed: %s "
                 CRM_XS " rc=%d", gnutls_strerror(rc), rc);
         return EPROTO;
     }
     return pcmk_rc_ok;
 }
 
 // \return Standard Pacemaker return code
 static int
 send_tls(gnutls_session_t *session, struct iovec *iov)
 {
     const char *unsent = iov->iov_base;
     size_t unsent_len = iov->iov_len;
     ssize_t gnutls_rc;
 
     if (unsent == NULL) {
         return EINVAL;
     }
 
     crm_trace("Sending TLS message of %llu bytes",
               (unsigned long long) unsent_len);
     while (true) {
         gnutls_rc = gnutls_record_send(*session, unsent, unsent_len);
 
         if (gnutls_rc == GNUTLS_E_INTERRUPTED || gnutls_rc == GNUTLS_E_AGAIN) {
             crm_trace("Retrying to send %llu bytes remaining",
                       (unsigned long long) unsent_len);
 
         } else if (gnutls_rc < 0) {
             // Caller can log as error if necessary
             crm_info("TLS connection terminated: %s " CRM_XS " rc=%lld",
                      gnutls_strerror((int) gnutls_rc),
                      (long long) gnutls_rc);
             return ECONNABORTED;
 
         } else if (gnutls_rc < unsent_len) {
             crm_trace("Sent %lld of %llu bytes remaining",
                       (long long) gnutls_rc, (unsigned long long) unsent_len);
             unsent_len -= gnutls_rc;
             unsent += gnutls_rc;
         } else {
             crm_trace("Sent all %lld bytes remaining", (long long) gnutls_rc);
             break;
         }
     }
     return pcmk_rc_ok;
 }
 #endif
 
 // \return Standard Pacemaker return code
 static int
 send_plaintext(int sock, struct iovec *iov)
 {
     const char *unsent = iov->iov_base;
     size_t unsent_len = iov->iov_len;
     ssize_t write_rc;
 
     if (unsent == NULL) {
         return EINVAL;
     }
 
     crm_debug("Sending plaintext message of %llu bytes to socket %d",
               (unsigned long long) unsent_len, sock);
     while (true) {
         write_rc = write(sock, unsent, unsent_len);
         if (write_rc < 0) {
             int rc = errno;
 
             if ((errno == EINTR) || (errno == EAGAIN)) {
                 crm_trace("Retrying to send %llu bytes remaining to socket %d",
                           (unsigned long long) unsent_len, sock);
                 continue;
             }
 
             // Caller can log as error if necessary
             crm_info("Could not send message: %s " CRM_XS " rc=%d socket=%d",
                      pcmk_rc_str(rc), rc, sock);
             return rc;
 
         } else if (write_rc < unsent_len) {
             crm_trace("Sent %lld of %llu bytes remaining",
                       (long long) write_rc, (unsigned long long) unsent_len);
             unsent += write_rc;
             unsent_len -= write_rc;
             continue;
 
         } else {
             crm_trace("Sent all %lld bytes remaining: %.100s",
                       (long long) write_rc, (char *) (iov->iov_base));
             break;
         }
     }
     return pcmk_rc_ok;
 }
 
 // \return Standard Pacemaker return code
 static int
 remote_send_iovs(pcmk__remote_t *remote, struct iovec *iov, int iovs)
 {
     int rc = pcmk_rc_ok;
 
     for (int lpc = 0; (lpc < iovs) && (rc == pcmk_rc_ok); lpc++) {
 #ifdef HAVE_GNUTLS_GNUTLS_H
         if (remote->tls_session) {
             rc = send_tls(remote->tls_session, &(iov[lpc]));
             continue;
         }
 #endif
         if (remote->tcp_socket) {
             rc = send_plaintext(remote->tcp_socket, &(iov[lpc]));
         } else {
             rc = ESOCKTNOSUPPORT;
         }
     }
     return rc;
 }
 
 /*!
  * \internal
  * \brief Send an XML message over a Pacemaker Remote connection
  *
  * \param[in,out] remote  Pacemaker Remote connection to use
  * \param[in]     msg     XML to send
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__remote_send_xml(pcmk__remote_t *remote, const xmlNode *msg)
 {
     int rc = pcmk_rc_ok;
     static uint64_t id = 0;
-    gchar *xml_text = NULL;
+    GString *xml_text = NULL;
 
     struct iovec iov[2];
     struct remote_header_v0 *header;
 
     CRM_CHECK((remote != NULL) && (msg != NULL), return EINVAL);
 
-    xml_text = pcmk__xml_dump(msg, 0);
-    CRM_CHECK(xml_text != NULL, return EINVAL);
+    xml_text = g_string_sized_new(1024);
+    pcmk__xml_string(msg, 0, xml_text, 0);
+    CRM_CHECK(xml_text->len > 0,
+              g_string_free(xml_text, TRUE); return EINVAL);
 
     header = calloc(1, sizeof(struct remote_header_v0));
     CRM_ASSERT(header != NULL);
 
     iov[0].iov_base = header;
     iov[0].iov_len = sizeof(struct remote_header_v0);
 
-    iov[1].iov_base = xml_text;
-    iov[1].iov_len = 1 + strlen(xml_text);
+    iov[1].iov_len = 1 + xml_text->len;
+    iov[1].iov_base = g_string_free(xml_text, FALSE);
 
     id++;
     header->id = id;
     header->endian = ENDIAN_LOCAL;
     header->version = REMOTE_MSG_VERSION;
     header->payload_offset = iov[0].iov_len;
     header->payload_uncompressed = iov[1].iov_len;
     header->size_total = iov[0].iov_len + iov[1].iov_len;
 
     rc = remote_send_iovs(remote, iov, 2);
     if (rc != pcmk_rc_ok) {
         crm_err("Could not send remote message: %s " CRM_XS " rc=%d",
                 pcmk_rc_str(rc), rc);
     }
 
     free(iov[0].iov_base);
-    g_free(iov[1].iov_base);
+    g_free((gchar *) iov[1].iov_base);
     return rc;
 }
 
 /*!
  * \internal
  * \brief Obtain the XML from the currently buffered remote connection message
  *
  * \param[in,out] remote  Remote connection possibly with message available
  *
  * \return Newly allocated XML object corresponding to message data, or NULL
  * \note This effectively removes the message from the connection buffer.
  */
 xmlNode *
 pcmk__remote_message_xml(pcmk__remote_t *remote)
 {
     xmlNode *xml = NULL;
     struct remote_header_v0 *header = localized_remote_header(remote);
 
     if (header == NULL) {
         return NULL;
     }
 
     /* Support compression on the receiving end now, in case we ever want to add it later */
     if (header->payload_compressed) {
         int rc = 0;
         unsigned int size_u = 1 + header->payload_uncompressed;
         char *uncompressed = calloc(1, header->payload_offset + size_u);
 
         crm_trace("Decompressing message data %d bytes into %d bytes",
                  header->payload_compressed, size_u);
 
         rc = BZ2_bzBuffToBuffDecompress(uncompressed + header->payload_offset, &size_u,
                                         remote->buffer + header->payload_offset,
                                         header->payload_compressed, 1, 0);
         rc = pcmk__bzlib2rc(rc);
 
         if (rc != pcmk_rc_ok && header->version > REMOTE_MSG_VERSION) {
             crm_warn("Couldn't decompress v%d message, we only understand v%d",
                      header->version, REMOTE_MSG_VERSION);
             free(uncompressed);
             return NULL;
 
         } else if (rc != pcmk_rc_ok) {
             crm_err("Decompression failed: %s " CRM_XS " rc=%d",
                     pcmk_rc_str(rc), rc);
             free(uncompressed);
             return NULL;
         }
 
         CRM_ASSERT(size_u == header->payload_uncompressed);
 
         memcpy(uncompressed, remote->buffer, header->payload_offset);       /* Preserve the header */
         remote->buffer_size = header->payload_offset + size_u;
 
         free(remote->buffer);
         remote->buffer = uncompressed;
         header = localized_remote_header(remote);
     }
 
     /* take ownership of the buffer */
     remote->buffer_offset = 0;
 
     CRM_LOG_ASSERT(remote->buffer[sizeof(struct remote_header_v0) + header->payload_uncompressed - 1] == 0);
 
     xml = pcmk__xml_parse(remote->buffer + header->payload_offset);
     if (xml == NULL && header->version > REMOTE_MSG_VERSION) {
         crm_warn("Couldn't parse v%d message, we only understand v%d",
                  header->version, REMOTE_MSG_VERSION);
 
     } else if (xml == NULL) {
         crm_err("Couldn't parse: '%.120s'", remote->buffer + header->payload_offset);
     }
 
     return xml;
 }
 
 static int
 get_remote_socket(const pcmk__remote_t *remote)
 {
 #ifdef HAVE_GNUTLS_GNUTLS_H
     if (remote->tls_session) {
         void *sock_ptr = gnutls_transport_get_ptr(*remote->tls_session);
 
         return GPOINTER_TO_INT(sock_ptr);
     }
 #endif
 
     if (remote->tcp_socket) {
         return remote->tcp_socket;
     }
 
     crm_err("Remote connection type undetermined (bug?)");
     return -1;
 }
 
 /*!
  * \internal
  * \brief Wait for a remote session to have data to read
  *
  * \param[in] remote      Connection to check
  * \param[in] timeout_ms  Maximum time (in ms) to wait
  *
  * \return Standard Pacemaker return code (of particular interest, pcmk_rc_ok if
  *         there is data ready to be read, and ETIME if there is no data within
  *         the specified timeout)
  */
 int
 pcmk__remote_ready(const pcmk__remote_t *remote, int timeout_ms)
 {
     struct pollfd fds = { 0, };
     int sock = 0;
     int rc = 0;
     time_t start;
     int timeout = timeout_ms;
 
     sock = get_remote_socket(remote);
     if (sock <= 0) {
         crm_trace("No longer connected");
         return ENOTCONN;
     }
 
     start = time(NULL);
     errno = 0;
     do {
         fds.fd = sock;
         fds.events = POLLIN;
 
         /* If we got an EINTR while polling, and we have a
          * specific timeout we are trying to honor, attempt
          * to adjust the timeout to the closest second. */
         if (errno == EINTR && (timeout > 0)) {
             timeout = timeout_ms - ((time(NULL) - start) * 1000);
             if (timeout < 1000) {
                 timeout = 1000;
             }
         }
 
         rc = poll(&fds, 1, timeout);
     } while (rc < 0 && errno == EINTR);
 
     if (rc < 0) {
         return errno;
     }
     return (rc == 0)? ETIME : pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Read bytes from non-blocking remote connection
  *
  * \param[in,out] remote  Remote connection to read
  *
  * \return Standard Pacemaker return code (of particular interest, pcmk_rc_ok if
  *         a full message has been received, or EAGAIN for a partial message)
  * \note Use only with non-blocking sockets after polling the socket.
  * \note This function will return when the socket read buffer is empty or an
  *       error is encountered.
  */
 static int
 read_available_remote_data(pcmk__remote_t *remote)
 {
     int rc = pcmk_rc_ok;
     size_t read_len = sizeof(struct remote_header_v0);
     struct remote_header_v0 *header = localized_remote_header(remote);
     bool received = false;
     ssize_t read_rc;
 
     if(header) {
         /* Stop at the end of the current message */
         read_len = header->size_total;
     }
 
     /* automatically grow the buffer when needed */
     if(remote->buffer_size < read_len) {
         remote->buffer_size = 2 * read_len;
         crm_trace("Expanding buffer to %llu bytes",
                   (unsigned long long) remote->buffer_size);
         remote->buffer = pcmk__realloc(remote->buffer, remote->buffer_size + 1);
     }
 
 #ifdef HAVE_GNUTLS_GNUTLS_H
     if (!received && remote->tls_session) {
         read_rc = gnutls_record_recv(*(remote->tls_session),
                                      remote->buffer + remote->buffer_offset,
                                      remote->buffer_size - remote->buffer_offset);
         if (read_rc == GNUTLS_E_INTERRUPTED) {
             rc = EINTR;
         } else if (read_rc == GNUTLS_E_AGAIN) {
             rc = EAGAIN;
         } else if (read_rc < 0) {
             crm_debug("TLS receive failed: %s (%lld)",
                       gnutls_strerror(read_rc), (long long) read_rc);
             rc = EIO;
         }
         received = true;
     }
 #endif
 
     if (!received && remote->tcp_socket) {
         read_rc = read(remote->tcp_socket,
                        remote->buffer + remote->buffer_offset,
                        remote->buffer_size - remote->buffer_offset);
         if (read_rc < 0) {
             rc = errno;
         }
         received = true;
     }
 
     if (!received) {
         crm_err("Remote connection type undetermined (bug?)");
         return ESOCKTNOSUPPORT;
     }
 
     /* process any errors. */
     if (read_rc > 0) {
         remote->buffer_offset += read_rc;
         /* always null terminate buffer, the +1 to alloc always allows for this. */
         remote->buffer[remote->buffer_offset] = '\0';
         crm_trace("Received %lld more bytes (%llu total)",
                   (long long) read_rc,
                   (unsigned long long) remote->buffer_offset);
 
     } else if ((rc == EINTR) || (rc == EAGAIN)) {
         crm_trace("No data available for non-blocking remote read: %s (%d)",
                   pcmk_rc_str(rc), rc);
 
     } else if (read_rc == 0) {
         crm_debug("End of remote data encountered after %llu bytes",
                   (unsigned long long) remote->buffer_offset);
         return ENOTCONN;
 
     } else {
         crm_debug("Error receiving remote data after %llu bytes: %s (%d)",
                   (unsigned long long) remote->buffer_offset,
                   pcmk_rc_str(rc), rc);
         return ENOTCONN;
     }
 
     header = localized_remote_header(remote);
     if(header) {
         if(remote->buffer_offset < header->size_total) {
             crm_trace("Read partial remote message (%llu of %u bytes)",
                       (unsigned long long) remote->buffer_offset,
                       header->size_total);
         } else {
             crm_trace("Read full remote message of %llu bytes",
                       (unsigned long long) remote->buffer_offset);
             return pcmk_rc_ok;
         }
     }
 
     return EAGAIN;
 }
 
 /*!
  * \internal
  * \brief Read one message from a remote connection
  *
  * \param[in,out] remote      Remote connection to read
  * \param[in]     timeout_ms  Fail if message not read in this many milliseconds
  *                            (10s will be used if 0, and 60s if negative)
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__read_remote_message(pcmk__remote_t *remote, int timeout_ms)
 {
     int rc = pcmk_rc_ok;
     time_t start = time(NULL);
     int remaining_timeout = 0;
 
     if (timeout_ms == 0) {
         timeout_ms = 10000;
     } else if (timeout_ms < 0) {
         timeout_ms = 60000;
     }
 
     remaining_timeout = timeout_ms;
     while (remaining_timeout > 0) {
 
         crm_trace("Waiting for remote data (%d ms of %d ms timeout remaining)",
                   remaining_timeout, timeout_ms);
         rc = pcmk__remote_ready(remote, remaining_timeout);
 
         if (rc == ETIME) {
             crm_err("Timed out (%d ms) while waiting for remote data",
                     remaining_timeout);
             return rc;
 
         } else if (rc != pcmk_rc_ok) {
             crm_debug("Wait for remote data aborted (will retry): %s "
                       CRM_XS " rc=%d", pcmk_rc_str(rc), rc);
 
         } else {
             rc = read_available_remote_data(remote);
             if (rc == pcmk_rc_ok) {
                 return rc;
             } else if (rc == EAGAIN) {
                 crm_trace("Waiting for more remote data");
             } else {
                 crm_debug("Could not receive remote data: %s " CRM_XS " rc=%d",
                           pcmk_rc_str(rc), rc);
             }
         }
 
         // Don't waste time retrying after fatal errors
         if ((rc == ENOTCONN) || (rc == ESOCKTNOSUPPORT)) {
             return rc;
         }
 
         remaining_timeout = timeout_ms - ((time(NULL) - start) * 1000);
     }
     return ETIME;
 }
 
 struct tcp_async_cb_data {
     int sock;
     int timeout_ms;
     time_t start;
     void *userdata;
     void (*callback) (void *userdata, int rc, int sock);
 };
 
 // \return TRUE if timer should be rescheduled, FALSE otherwise
 static gboolean
 check_connect_finished(gpointer userdata)
 {
     struct tcp_async_cb_data *cb_data = userdata;
     int rc;
 
     fd_set rset, wset;
     struct timeval ts = { 0, };
 
     if (cb_data->start == 0) {
         // Last connect() returned success immediately
         rc = pcmk_rc_ok;
         goto dispatch_done;
     }
 
     // If the socket is ready for reading or writing, the connect succeeded
     FD_ZERO(&rset);
     FD_SET(cb_data->sock, &rset);
     wset = rset;
     rc = select(cb_data->sock + 1, &rset, &wset, NULL, &ts);
 
     if (rc < 0) { // select() error
         rc = errno;
         if ((rc == EINPROGRESS) || (rc == EAGAIN)) {
             if ((time(NULL) - cb_data->start) < (cb_data->timeout_ms / 1000)) {
                 return TRUE; // There is time left, so reschedule timer
             } else {
                 rc = ETIMEDOUT;
             }
         }
         crm_trace("Could not check socket %d for connection success: %s (%d)",
                   cb_data->sock, pcmk_rc_str(rc), rc);
 
     } else if (rc == 0) { // select() timeout
         if ((time(NULL) - cb_data->start) < (cb_data->timeout_ms / 1000)) {
             return TRUE; // There is time left, so reschedule timer
         }
         crm_debug("Timed out while waiting for socket %d connection success",
                   cb_data->sock);
         rc = ETIMEDOUT;
 
     // select() returned number of file descriptors that are ready
 
     } else if (FD_ISSET(cb_data->sock, &rset)
                || FD_ISSET(cb_data->sock, &wset)) {
 
         // The socket is ready; check it for connection errors
         int error = 0;
         socklen_t len = sizeof(error);
 
         if (getsockopt(cb_data->sock, SOL_SOCKET, SO_ERROR, &error, &len) < 0) {
             rc = errno;
             crm_trace("Couldn't check socket %d for connection errors: %s (%d)",
                       cb_data->sock, pcmk_rc_str(rc), rc);
         } else if (error != 0) {
             rc = error;
             crm_trace("Socket %d connected with error: %s (%d)",
                       cb_data->sock, pcmk_rc_str(rc), rc);
         } else {
             rc = pcmk_rc_ok;
         }
 
     } else { // Should not be possible
         crm_trace("select() succeeded, but socket %d not in resulting "
                   "read/write sets", cb_data->sock);
         rc = EAGAIN;
     }
 
   dispatch_done:
     if (rc == pcmk_rc_ok) {
         crm_trace("Socket %d is connected", cb_data->sock);
     } else {
         close(cb_data->sock);
         cb_data->sock = -1;
     }
 
     if (cb_data->callback) {
         cb_data->callback(cb_data->userdata, rc, cb_data->sock);
     }
     free(cb_data);
     return FALSE; // Do not reschedule timer
 }
 
 /*!
  * \internal
  * \brief Attempt to connect socket, calling callback when done
  *
  * Set a given socket non-blocking, then attempt to connect to it,
  * retrying periodically until success or a timeout is reached.
  * Call a caller-supplied callback function when completed.
  *
  * \param[in]  sock        Newly created socket
  * \param[in]  addr        Socket address information for connect
  * \param[in]  addrlen     Size of socket address information in bytes
  * \param[in]  timeout_ms  Fail if not connected within this much time
  * \param[out] timer_id    If not NULL, store retry timer ID here
  * \param[in]  userdata    User data to pass to callback
  * \param[in]  callback    Function to call when connection attempt completes
  *
  * \return Standard Pacemaker return code
  */
 static int
 connect_socket_retry(int sock, const struct sockaddr *addr, socklen_t addrlen,
                      int timeout_ms, int *timer_id, void *userdata,
                      void (*callback) (void *userdata, int rc, int sock))
 {
     int rc = 0;
     int interval = 500;
     int timer;
     struct tcp_async_cb_data *cb_data = NULL;
 
     rc = pcmk__set_nonblocking(sock);
     if (rc != pcmk_rc_ok) {
         crm_warn("Could not set socket non-blocking: %s " CRM_XS " rc=%d",
                  pcmk_rc_str(rc), rc);
         return rc;
     }
 
     rc = connect(sock, addr, addrlen);
     if (rc < 0 && (errno != EINPROGRESS) && (errno != EAGAIN)) {
         rc = errno;
         crm_warn("Could not connect socket: %s " CRM_XS " rc=%d",
                  pcmk_rc_str(rc), rc);
         return rc;
     }
 
     cb_data = calloc(1, sizeof(struct tcp_async_cb_data));
     cb_data->userdata = userdata;
     cb_data->callback = callback;
     cb_data->sock = sock;
     cb_data->timeout_ms = timeout_ms;
 
     if (rc == 0) {
         /* The connect was successful immediately, we still return to mainloop
          * and let this callback get called later. This avoids the user of this api
          * to have to account for the fact the callback could be invoked within this
          * function before returning. */
         cb_data->start = 0;
         interval = 1;
     } else {
         cb_data->start = time(NULL);
     }
 
     /* This timer function does a non-blocking poll on the socket to see if we
      * can use it. Once we can, the connect has completed. This method allows us
      * to connect without blocking the mainloop.
      *
      * @TODO Use a mainloop fd callback for this instead of polling. Something
      *       about the way mainloop is currently polling prevents this from
      *       working at the moment though. (See connect(2) regarding EINPROGRESS
      *       for possible new handling needed.)
      */
     crm_trace("Scheduling check in %dms for whether connect to fd %d finished",
               interval, sock);
     timer = g_timeout_add(interval, check_connect_finished, cb_data);
     if (timer_id) {
         *timer_id = timer;
     }
 
     // timer callback should be taking care of cb_data
     // cppcheck-suppress memleak
     return pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Attempt once to connect socket and set it non-blocking
  *
  * \param[in]  sock        Newly created socket
  * \param[in]  addr        Socket address information for connect
  * \param[in]  addrlen     Size of socket address information in bytes
  *
  * \return Standard Pacemaker return code
  */
 static int
 connect_socket_once(int sock, const struct sockaddr *addr, socklen_t addrlen)
 {
     int rc = connect(sock, addr, addrlen);
 
     if (rc < 0) {
         rc = errno;
         crm_warn("Could not connect socket: %s " CRM_XS " rc=%d",
                  pcmk_rc_str(rc), rc);
         return rc;
     }
 
     rc = pcmk__set_nonblocking(sock);
     if (rc != pcmk_rc_ok) {
         crm_warn("Could not set socket non-blocking: %s " CRM_XS " rc=%d",
                  pcmk_rc_str(rc), rc);
         return rc;
     }
 
     return pcmk_ok;
 }
 
 /*!
  * \internal
  * \brief Connect to server at specified TCP port
  *
  * \param[in]  host        Name of server to connect to
  * \param[in]  port        Server port to connect to
  * \param[in]  timeout_ms  If asynchronous, fail if not connected in this time
  * \param[out] timer_id    If asynchronous and this is non-NULL, retry timer ID
  *                         will be put here (for ease of cancelling by caller)
  * \param[out] sock_fd     Where to store socket file descriptor
  * \param[in]  userdata    If asynchronous, data to pass to callback
  * \param[in]  callback    If NULL, attempt a single synchronous connection,
  *                         otherwise retry asynchronously then call this
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__connect_remote(const char *host, int port, int timeout, int *timer_id,
                      int *sock_fd, void *userdata,
                      void (*callback) (void *userdata, int rc, int sock))
 {
     char buffer[INET6_ADDRSTRLEN];
     struct addrinfo *res = NULL;
     struct addrinfo *rp = NULL;
     struct addrinfo hints;
     const char *server = host;
     int rc;
     int sock = -1;
 
     CRM_CHECK((host != NULL) && (sock_fd != NULL), return EINVAL);
 
     // Get host's IP address(es)
     memset(&hints, 0, sizeof(struct addrinfo));
     hints.ai_family = AF_UNSPEC;        /* Allow IPv4 or IPv6 */
     hints.ai_socktype = SOCK_STREAM;
     hints.ai_flags = AI_CANONNAME;
 
     rc = getaddrinfo(server, NULL, &hints, &res);
     rc = pcmk__gaierror2rc(rc);
 
     if (rc != pcmk_rc_ok) {
         crm_err("Unable to get IP address info for %s: %s",
                 server, pcmk_rc_str(rc));
         goto async_cleanup;
     }
 
     if (!res || !res->ai_addr) {
         crm_err("Unable to get IP address info for %s: no result", server);
         rc = ENOTCONN;
         goto async_cleanup;
     }
 
     // getaddrinfo() returns a list of host's addresses, try them in order
     for (rp = res; rp != NULL; rp = rp->ai_next) {
         struct sockaddr *addr = rp->ai_addr;
 
         if (!addr) {
             continue;
         }
 
         if (rp->ai_canonname) {
             server = res->ai_canonname;
         }
         crm_debug("Got canonical name %s for %s", server, host);
 
         sock = socket(rp->ai_family, SOCK_STREAM, IPPROTO_TCP);
         if (sock == -1) {
             rc = errno;
             crm_warn("Could not create socket for remote connection to %s:%d: "
                      "%s " CRM_XS " rc=%d", server, port, pcmk_rc_str(rc), rc);
             continue;
         }
 
         /* Set port appropriately for address family */
         /* (void*) casts avoid false-positive compiler alignment warnings */
         if (addr->sa_family == AF_INET6) {
             ((struct sockaddr_in6 *)(void*)addr)->sin6_port = htons(port);
         } else {
             ((struct sockaddr_in *)(void*)addr)->sin_port = htons(port);
         }
 
         memset(buffer, 0, PCMK__NELEM(buffer));
         pcmk__sockaddr2str(addr, buffer);
         crm_info("Attempting remote connection to %s:%d", buffer, port);
 
         if (callback) {
             if (connect_socket_retry(sock, rp->ai_addr, rp->ai_addrlen, timeout,
                                      timer_id, userdata, callback) == pcmk_rc_ok) {
                 goto async_cleanup; /* Success for now, we'll hear back later in the callback */
             }
 
         } else if (connect_socket_once(sock, rp->ai_addr,
                                        rp->ai_addrlen) == pcmk_rc_ok) {
             break;          /* Success */
         }
 
         // Connect failed
         close(sock);
         sock = -1;
         rc = ENOTCONN;
     }
 
 async_cleanup:
 
     if (res) {
         freeaddrinfo(res);
     }
     *sock_fd = sock;
     return rc;
 }
 
 /*!
  * \internal
  * \brief Convert an IP address (IPv4 or IPv6) to a string for logging
  *
  * \param[in]  sa  Socket address for IP
  * \param[out] s   Storage for at least INET6_ADDRSTRLEN bytes
  *
  * \note sa The socket address can be a pointer to struct sockaddr_in (IPv4),
  *          struct sockaddr_in6 (IPv6) or struct sockaddr_storage (either),
  *          as long as its sa_family member is set correctly.
  */
 void
 pcmk__sockaddr2str(const void *sa, char *s)
 {
     switch (((const struct sockaddr *) sa)->sa_family) {
         case AF_INET:
             inet_ntop(AF_INET, &(((const struct sockaddr_in *) sa)->sin_addr),
                       s, INET6_ADDRSTRLEN);
             break;
 
         case AF_INET6:
             inet_ntop(AF_INET6,
                       &(((const struct sockaddr_in6 *) sa)->sin6_addr),
                       s, INET6_ADDRSTRLEN);
             break;
 
         default:
             strcpy(s, "<invalid>");
     }
 }
 
 /*!
  * \internal
  * \brief Accept a client connection on a remote server socket
  *
  * \param[in]  ssock  Server socket file descriptor being listened on
  * \param[out] csock  Where to put new client socket's file descriptor
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__accept_remote_connection(int ssock, int *csock)
 {
     int rc;
     struct sockaddr_storage addr;
     socklen_t laddr = sizeof(addr);
     char addr_str[INET6_ADDRSTRLEN];
 #ifdef TCP_USER_TIMEOUT
     long sbd_timeout = 0;
 #endif
 
     /* accept the connection */
     memset(&addr, 0, sizeof(addr));
     *csock = accept(ssock, (struct sockaddr *)&addr, &laddr);
     if (*csock == -1) {
         rc = errno;
         crm_err("Could not accept remote client connection: %s "
                 CRM_XS " rc=%d", pcmk_rc_str(rc), rc);
         return rc;
     }
     pcmk__sockaddr2str(&addr, addr_str);
     crm_info("Accepted new remote client connection from %s", addr_str);
 
     rc = pcmk__set_nonblocking(*csock);
     if (rc != pcmk_rc_ok) {
         crm_err("Could not set socket non-blocking: %s " CRM_XS " rc=%d",
                 pcmk_rc_str(rc), rc);
         close(*csock);
         *csock = -1;
         return rc;
     }
 
 #ifdef TCP_USER_TIMEOUT
     sbd_timeout = pcmk__get_sbd_watchdog_timeout();
     if (sbd_timeout > 0) {
         // Time to fail and retry before watchdog
         long half = sbd_timeout / 2;
         unsigned int optval = (half <= UINT_MAX)? half : UINT_MAX;
 
         rc = setsockopt(*csock, SOL_TCP, TCP_USER_TIMEOUT,
                         &optval, sizeof(optval));
         if (rc < 0) {
             rc = errno;
             crm_err("Could not set TCP timeout to %d ms on remote connection: "
                     "%s " CRM_XS " rc=%d", optval, pcmk_rc_str(rc), rc);
             close(*csock);
             *csock = -1;
             return rc;
         }
     }
 #endif
 
     return rc;
 }
 
 /*!
  * \brief Get the default remote connection TCP port on this host
  *
  * \return Remote connection TCP port number
  */
 int
 crm_default_remote_port(void)
 {
     static int port = 0;
 
     if (port == 0) {
         const char *env = pcmk__env_option(PCMK__ENV_REMOTE_PORT);
 
         if (env) {
             errno = 0;
             port = strtol(env, NULL, 10);
             if (errno || (port < 1) || (port > 65535)) {
                 crm_warn("Environment variable PCMK_" PCMK__ENV_REMOTE_PORT
                          " has invalid value '%s', using %d instead",
                          env, DEFAULT_REMOTE_PORT);
                 port = DEFAULT_REMOTE_PORT;
             }
         } else {
             port = DEFAULT_REMOTE_PORT;
         }
     }
     return port;
 }
diff --git a/lib/common/xml_io.c b/lib/common/xml_io.c
index 019d1f5a51..12a5e3ef5a 100644
--- a/lib/common/xml_io.c
+++ b/lib/common/xml_io.c
@@ -1,854 +1,840 @@
 /*
  * Copyright 2004-2024 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 <stdlib.h>
 #include <string.h>
 #include <sys/types.h>
 
 #include <bzlib.h>
 #include <libxml/parser.h>
 #include <libxml/tree.h>
 #include <libxml/xmlIO.h>               // xmlOutputBuffer*
 
 #include <crm/crm.h>
 #include <crm/common/xml.h>
 #include <crm/common/xml_io.h>
 #include "crmcommon_private.h"
 
 /* @COMPAT XML_PARSE_RECOVER allows some XML errors to be silently worked around
  * by libxml2, which is potentially ambiguous and dangerous. We should drop it
  * when we can break backward compatibility with configurations that might be
  * relying on it (i.e. pacemaker 3.0.0).
  */
 #define PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER    (XML_PARSE_NOBLANKS)
 #define PCMK__XML_PARSE_OPTS_WITH_RECOVER       (XML_PARSE_NOBLANKS \
                                                  |XML_PARSE_RECOVER)
 
 /*!
  * \internal
  * \brief Read from \c stdin until EOF or error
  *
  * \return Newly allocated string containing the bytes read from \c stdin, or
  *         \c NULL on error
  *
  * \note The caller is responsible for freeing the return value using \c free().
  */
 static char *
 read_stdin(void)
 {
     char *buf = NULL;
     size_t length = 0;
 
     do {
         buf = pcmk__realloc(buf, length + PCMK__BUFFER_SIZE + 1);
         length += fread(buf + length, 1, PCMK__BUFFER_SIZE, stdin);
     } while ((feof(stdin) == 0) && (ferror(stdin) == 0));
 
     if (ferror(stdin) != 0) {
         crm_err("Error reading input from stdin");
         free(buf);
         buf = NULL;
     } else {
         buf[length] = '\0';
     }
     clearerr(stdin);
     return buf;
 }
 
 /*!
  * \internal
  * \brief Decompress a <tt>bzip2</tt>-compressed file into a string buffer
  *
  * \param[in] filename  Name of file to decompress
  *
  * \return Newly allocated string with the decompressed contents of \p filename,
  *         or \c NULL on error.
  *
  * \note The caller is responsible for freeing the return value using \c free().
  */
 static char *
 decompress_file(const char *filename)
 {
     char *buffer = NULL;
     int rc = pcmk_rc_ok;
     size_t length = 0;
     BZFILE *bz_file = NULL;
     FILE *input = fopen(filename, "r");
 
     if (input == NULL) {
         crm_perror(LOG_ERR, "Could not open %s for reading", filename);
         return NULL;
     }
 
     bz_file = BZ2_bzReadOpen(&rc, input, 0, 0, NULL, 0);
     rc = pcmk__bzlib2rc(rc);
     if (rc != pcmk_rc_ok) {
         crm_err("Could not prepare to read compressed %s: %s "
                 CRM_XS " rc=%d", filename, pcmk_rc_str(rc), rc);
         goto done;
     }
 
     // cppcheck seems not to understand the abort-logic in pcmk__realloc
     // cppcheck-suppress memleak
     do {
         int read_len = 0;
 
         buffer = pcmk__realloc(buffer, length + PCMK__BUFFER_SIZE + 1);
         read_len = BZ2_bzRead(&rc, bz_file, buffer + length, PCMK__BUFFER_SIZE);
 
         if ((rc == BZ_OK) || (rc == BZ_STREAM_END)) {
             crm_trace("Read %ld bytes from file: %d", (long) read_len, rc);
             length += read_len;
         }
     } while (rc == BZ_OK);
 
     rc = pcmk__bzlib2rc(rc);
     if (rc != pcmk_rc_ok) {
         rc = pcmk__bzlib2rc(rc);
         crm_err("Could not read compressed %s: %s " CRM_XS " rc=%d",
                 filename, pcmk_rc_str(rc), rc);
         free(buffer);
         buffer = NULL;
     } else {
         buffer[length] = '\0';
     }
 
 done:
     BZ2_bzReadClose(&rc, bz_file);
     fclose(input);
     return buffer;
 }
 
 // @COMPAT Remove macro at 3.0.0 when we drop XML_PARSE_RECOVER
 /*!
  * \internal
  * \brief Try to parse XML first without and then with recovery enabled
  *
  * \param[out] result  Where to store the resulting XML doc (<tt>xmlDoc **</tt>)
  * \param[in]  fn      XML parser function
  * \param[in]  ...     All arguments for \p fn except the final one (an
  *                     \c xmlParserOption group)
  */
 #define parse_xml_recover(result, fn, ...) do {                             \
         *result = fn(__VA_ARGS__, PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER);    \
         if (*result == NULL) {                                              \
             *result = fn(__VA_ARGS__, PCMK__XML_PARSE_OPTS_WITH_RECOVER);   \
                                                                             \
             if (*result != NULL) {                                          \
                 crm_warn("Successfully recovered from XML errors "          \
                          "(note: a future release will treat this as a "    \
                          "fatal failure)");                                 \
             }                                                               \
         }                                                                   \
     } while (0);
 
 /*!
  * \internal
  * \brief Parse XML from a file
  *
  * \param[in] filename  Name of file containing XML (\c NULL or \c "-" for
  *                      \c stdin); if \p filename ends in \c ".bz2", the file
  *                      will be decompressed using \c bzip2
  *
  * \return XML tree parsed from the given file; may be \c NULL or only partial
  *         on error
  */
 xmlNode *
 pcmk__xml_read(const char *filename)
 {
     bool use_stdin = pcmk__str_eq(filename, "-", pcmk__str_null_matches);
     xmlNode *xml = NULL;
     xmlDoc *output = NULL;
     xmlParserCtxt *ctxt = NULL;
     const xmlError *last_error = NULL;
 
     // Create a parser context
     ctxt = xmlNewParserCtxt();
     CRM_CHECK(ctxt != NULL, return NULL);
 
     xmlCtxtResetLastError(ctxt);
     xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err);
 
     if (use_stdin) {
         /* @COMPAT After dropping XML_PARSE_RECOVER, we can avoid capturing
          * stdin into a buffer and instead call
          * xmlCtxtReadFd(ctxt, STDIN_FILENO, NULL, NULL, XML_PARSE_NOBLANKS);
          *
          * For now we have to save the input so that we can use it twice.
          */
         char *input = read_stdin();
 
         if (input != NULL) {
             parse_xml_recover(&output, xmlCtxtReadDoc, ctxt, (pcmkXmlStr) input,
                               NULL, NULL);
             free(input);
         }
 
     } else if (pcmk__ends_with_ext(filename, ".bz2")) {
         char *input = decompress_file(filename);
 
         if (input != NULL) {
             parse_xml_recover(&output, xmlCtxtReadDoc, ctxt, (pcmkXmlStr) input,
                               NULL, NULL);
             free(input);
         }
 
     } else {
         parse_xml_recover(&output, xmlCtxtReadFile, ctxt, filename, NULL);
     }
 
     if (output != NULL) {
         xml = xmlDocGetRootElement(output);
         if (xml != NULL) {
             /* @TODO Should we really be stripping out text? This seems like an
              * overly broad way to get rid of whitespace, if that's the goal.
              * Text nodes may be invalid in most or all Pacemaker inputs, but
              * stripping them in a generic "parse XML from file" function may
              * not be the best way to ignore them.
              */
             pcmk__strip_xml_text(xml);
         }
     }
 
     // @COMPAT At 3.0.0, free xml and return NULL if xml != NULL on error
     last_error = xmlCtxtGetLastError(ctxt);
     if (last_error != NULL) {
         if (xml != NULL) {
             crm_log_xml_info(xml, "Partial");
         }
     }
 
     xmlFreeParserCtxt(ctxt);
     return xml;
 }
 
 /*!
  * \internal
  * \brief Parse XML from a string
  *
  * \param[in] input  String to parse
  *
  * \return XML tree parsed from the given string; may be \c NULL or only partial
  *         on error
  */
 xmlNode *
 pcmk__xml_parse(const char *input)
 {
     xmlNode *xml = NULL;
     xmlDoc *output = NULL;
     xmlParserCtxt *ctxt = NULL;
     const xmlError *last_error = NULL;
 
     if (input == NULL) {
         return NULL;
     }
 
     ctxt = xmlNewParserCtxt();
     if (ctxt == NULL) {
         return NULL;
     }
 
     xmlCtxtResetLastError(ctxt);
     xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err);
 
     parse_xml_recover(&output, xmlCtxtReadDoc, ctxt, (pcmkXmlStr) input, NULL,
                       NULL);
 
     if (output != NULL) {
         xml = xmlDocGetRootElement(output);
     }
 
     // @COMPAT At 3.0.0, free xml and return NULL if xml != NULL; update doxygen
     last_error = xmlCtxtGetLastError(ctxt);
     if (last_error != NULL) {
         if (xml != NULL) {
             crm_log_xml_info(xml, "Partial");
         }
     }
 
     xmlFreeParserCtxt(ctxt);
     return xml;
 }
 
 /*!
  * \internal
  * \brief Append a string representation of an XML element to a buffer
  *
  * \param[in]     data     XML whose representation to append
  * \param[in]     options  Group of \p pcmk__xml_fmt_options flags
  * \param[in,out] buffer   Where to append the content (must not be \p NULL)
  * \param[in]     depth    Current indentation level
  */
 static void
 dump_xml_element(const xmlNode *data, uint32_t options, GString *buffer,
                  int depth)
 {
     bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty);
     bool filtered = pcmk_is_set(options, pcmk__xml_fmt_filtered);
     int spaces = pretty? (2 * depth) : 0;
 
     for (int lpc = 0; lpc < spaces; lpc++) {
         g_string_append_c(buffer, ' ');
     }
 
     pcmk__g_strcat(buffer, "<", data->name, NULL);
 
     for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL;
          attr = attr->next) {
 
         if (!filtered || !pcmk__xa_filterable((const char *) (attr->name))) {
             pcmk__dump_xml_attr(attr, buffer);
         }
     }
 
     if (data->children == NULL) {
         g_string_append(buffer, "/>");
 
     } else {
         g_string_append_c(buffer, '>');
     }
 
     if (pretty) {
         g_string_append_c(buffer, '\n');
     }
 
     if (data->children) {
         for (const xmlNode *child = data->children; child != NULL;
              child = child->next) {
             pcmk__xml_string(child, options, buffer, depth + 1);
         }
 
         for (int lpc = 0; lpc < spaces; lpc++) {
             g_string_append_c(buffer, ' ');
         }
 
         pcmk__g_strcat(buffer, "</", data->name, ">", NULL);
 
         if (pretty) {
             g_string_append_c(buffer, '\n');
         }
     }
 }
 
 /*!
  * \internal
  * \brief Append XML text content to a buffer
  *
  * \param[in]     data     XML whose content to append
  * \param[in]     options  Group of \p xml_log_options flags
  * \param[in,out] buffer   Where to append the content (must not be \p NULL)
  * \param[in]     depth    Current indentation level
  */
 static void
 dump_xml_text(const xmlNode *data, uint32_t options, GString *buffer,
               int depth)
 {
     bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty);
     int spaces = pretty? (2 * depth) : 0;
     const char *content = (const char *) data->content;
     char *content_esc = NULL;
 
     if (pcmk__xml_needs_escape(content, false)) {
         content_esc = pcmk__xml_escape(content, false);
         content = content_esc;
     }
 
     for (int lpc = 0; lpc < spaces; lpc++) {
         g_string_append_c(buffer, ' ');
     }
 
     g_string_append(buffer, content);
 
     if (pretty) {
         g_string_append_c(buffer, '\n');
     }
     free(content_esc);
 }
 
 /*!
  * \internal
  * \brief Append XML CDATA content to a buffer
  *
  * \param[in]     data     XML whose content to append
  * \param[in]     options  Group of \p pcmk__xml_fmt_options flags
  * \param[in,out] buffer   Where to append the content (must not be \p NULL)
  * \param[in]     depth    Current indentation level
  */
 static void
 dump_xml_cdata(const xmlNode *data, uint32_t options, GString *buffer,
                int depth)
 {
     bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty);
     int spaces = pretty? (2 * depth) : 0;
 
     for (int lpc = 0; lpc < spaces; lpc++) {
         g_string_append_c(buffer, ' ');
     }
 
     pcmk__g_strcat(buffer, "<![CDATA[", (const char *) data->content, "]]>",
                    NULL);
 
     if (pretty) {
         g_string_append_c(buffer, '\n');
     }
 }
 
 /*!
  * \internal
  * \brief Append an XML comment to a buffer
  *
  * \param[in]     data     XML whose content to append
  * \param[in]     options  Group of \p pcmk__xml_fmt_options flags
  * \param[in,out] buffer   Where to append the content (must not be \p NULL)
  * \param[in]     depth    Current indentation level
  */
 static void
 dump_xml_comment(const xmlNode *data, uint32_t options, GString *buffer,
                  int depth)
 {
     bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty);
     int spaces = pretty? (2 * depth) : 0;
 
     for (int lpc = 0; lpc < spaces; lpc++) {
         g_string_append_c(buffer, ' ');
     }
 
     pcmk__g_strcat(buffer, "<!--", (const char *) data->content, "-->", NULL);
 
     if (pretty) {
         g_string_append_c(buffer, '\n');
     }
 }
 
 /*!
  * \internal
  * \brief Get a string representation of an XML element type
  *
  * \param[in] type  XML element type
  *
  * \return String representation of \p type
  */
 static const char *
 xml_element_type2str(xmlElementType type)
 {
     static const char *const element_type_names[] = {
         [XML_ELEMENT_NODE]       = "element",
         [XML_ATTRIBUTE_NODE]     = "attribute",
         [XML_TEXT_NODE]          = "text",
         [XML_CDATA_SECTION_NODE] = "CDATA section",
         [XML_ENTITY_REF_NODE]    = "entity reference",
         [XML_ENTITY_NODE]        = "entity",
         [XML_PI_NODE]            = "PI",
         [XML_COMMENT_NODE]       = "comment",
         [XML_DOCUMENT_NODE]      = "document",
         [XML_DOCUMENT_TYPE_NODE] = "document type",
         [XML_DOCUMENT_FRAG_NODE] = "document fragment",
         [XML_NOTATION_NODE]      = "notation",
         [XML_HTML_DOCUMENT_NODE] = "HTML document",
         [XML_DTD_NODE]           = "DTD",
         [XML_ELEMENT_DECL]       = "element declaration",
         [XML_ATTRIBUTE_DECL]     = "attribute declaration",
         [XML_ENTITY_DECL]        = "entity declaration",
         [XML_NAMESPACE_DECL]     = "namespace declaration",
         [XML_XINCLUDE_START]     = "XInclude start",
         [XML_XINCLUDE_END]       = "XInclude end",
     };
 
     if ((type < 0) || (type >= PCMK__NELEM(element_type_names))) {
         return "unrecognized type";
     }
     return element_type_names[type];
 }
 
 /*!
  * \internal
  * \brief Create a string representation of an XML object
  *
+ * libxml2's \c xmlNodeDumpOutput() doesn't allow filtering, doesn't escape
+ * special characters thoroughly, and doesn't allow a const argument.
+ *
  * \param[in]     data     XML to convert
  * \param[in]     options  Group of \p pcmk__xml_fmt_options flags
  * \param[in,out] buffer   Where to store the text (must not be \p NULL)
  * \param[in]     depth    Current indentation level
+ *
+ * \todo Create a wrapper that doesn't require \p depth. Only used with
+ *       recursive calls currently.
  */
 void
 pcmk__xml_string(const xmlNode *data, uint32_t options, GString *buffer,
                  int depth)
 {
     if (data == NULL) {
         crm_trace("Nothing to dump");
         return;
     }
 
     CRM_ASSERT(buffer != NULL);
     CRM_CHECK(depth >= 0, depth = 0);
 
     switch(data->type) {
         case XML_ELEMENT_NODE:
             /* Handle below */
             dump_xml_element(data, options, buffer, depth);
             break;
         case XML_TEXT_NODE:
             if (pcmk_is_set(options, pcmk__xml_fmt_text)) {
                 dump_xml_text(data, options, buffer, depth);
             }
             break;
         case XML_COMMENT_NODE:
             dump_xml_comment(data, options, buffer, depth);
             break;
         case XML_CDATA_SECTION_NODE:
             dump_xml_cdata(data, options, buffer, depth);
             break;
         default:
             crm_warn("Cannot convert XML %s node to text " CRM_XS " type=%d",
                      xml_element_type2str(data->type), data->type);
             break;
     }
 }
 
-/*!
- * \internal
- * \brief Dump an XML tree to a string
- *
- * \param[in] xml    XML tree to dump
- * \param[in] flags  Group of <tt>enum pcmk__xml_fmt_options</tt> flags
- *
- * \return Newly allocated string representation of \p xml
- *
- * \note The caller is responsible for freeing the return value using
- *       \c g_free().
- */
-gchar *
-pcmk__xml_dump(const xmlNode *xml, uint32_t flags)
-{
-    /* libxml2's xmlNodeDumpOutput() doesn't allow filtering, doesn't escape
-     * special characters thoroughly, and doesn't allow a const argument.
-     *
-     * @COMPAT Can we start including text nodes unconditionally?
-     */
-    GString *g_buffer = g_string_sized_new(1024);
-
-    pcmk__xml_string(xml, flags, g_buffer, 0);
-    return g_string_free(g_buffer, FALSE);
-}
-
 /*!
  * \internal
  * \brief Write a string to a file stream, compressed using \c bzip2
  *
  * \param[in]     text       String to write
  * \param[in]     filename   Name of file being written (for logging only)
  * \param[in,out] stream     Open file stream to write to
  * \param[out]    bytes_out  Number of bytes written (valid only on success)
  *
  * \return Standard Pacemaker return code
  */
 static int
 write_compressed_stream(char *text, const char *filename, FILE *stream,
                         unsigned int *bytes_out)
 {
     unsigned int bytes_in = 0;
     int rc = pcmk_rc_ok;
 
     // (5, 0, 0): (intermediate block size, silent, default workFactor)
     BZFILE *bz_file = BZ2_bzWriteOpen(&rc, stream, 5, 0, 0);
 
     rc = pcmk__bzlib2rc(rc);
     if (rc != pcmk_rc_ok) {
         crm_warn("Not compressing %s: could not prepare file stream: %s "
                  CRM_XS " rc=%d",
                  filename, pcmk_rc_str(rc), rc);
         goto done;
     }
 
     BZ2_bzWrite(&rc, bz_file, text, strlen(text));
     rc = pcmk__bzlib2rc(rc);
     if (rc != pcmk_rc_ok) {
         crm_warn("Not compressing %s: could not compress data: %s "
                  CRM_XS " rc=%d errno=%d",
                  filename, pcmk_rc_str(rc), rc, errno);
         goto done;
     }
 
     BZ2_bzWriteClose(&rc, bz_file, 0, &bytes_in, bytes_out);
     bz_file = NULL;
     rc = pcmk__bzlib2rc(rc);
     if (rc != pcmk_rc_ok) {
         crm_warn("Not compressing %s: could not write compressed data: %s "
                  CRM_XS " rc=%d errno=%d",
                  filename, pcmk_rc_str(rc), rc, errno);
         goto done;
     }
 
     crm_trace("Compressed XML for %s from %u bytes to %u",
               filename, bytes_in, *bytes_out);
 
 done:
     if (bz_file != NULL) {
         BZ2_bzWriteClose(&rc, bz_file, 0, NULL, NULL);
     }
     return rc;
 }
 
 /*!
  * \internal
  * \brief Write XML to a file stream
  *
  * \param[in]     xml       XML to write
  * \param[in]     filename  Name of file being written (for logging only)
  * \param[in,out] stream    Open file stream corresponding to filename (closed
  *                          when this function returns)
  * \param[in]     compress  Whether to compress XML before writing
  * \param[out]    nbytes    Number of bytes written
  *
  * \return Standard Pacemaker return code
  */
 static int
 write_xml_stream(const xmlNode *xml, const char *filename, FILE *stream,
                  bool compress, unsigned int *nbytes)
 {
     // @COMPAT Drop nbytes as arg when we drop write_xml_fd()/write_xml_file()
-    gchar *buffer = NULL;
+    GString *buffer = g_string_sized_new(1024);
     unsigned int bytes_out = 0;
     int rc = pcmk_rc_ok;
 
-    buffer = pcmk__xml_dump(xml, pcmk__xml_fmt_pretty);
-    CRM_CHECK(!pcmk__str_empty(buffer),
+    pcmk__xml_string(xml, pcmk__xml_fmt_pretty, buffer, 0);
+    CRM_CHECK(!pcmk__str_empty(buffer->str),
               crm_log_xml_info(xml, "dump-failed");
               rc = pcmk_rc_error;
               goto done);
 
     crm_log_xml_trace(xml, "writing");
 
     if (compress
-        && (write_compressed_stream(buffer, filename, stream,
+        && (write_compressed_stream(buffer->str, filename, stream,
                                     &bytes_out) == pcmk_rc_ok)) {
         goto done;
     }
 
-    rc = fprintf(stream, "%s", buffer);
+    rc = fprintf(stream, "%s", buffer->str);
     if (rc < 0) {
         rc = EIO;
         crm_perror(LOG_ERR, "writing %s", filename);
         goto done;
     }
     bytes_out = (unsigned int) rc;
     rc = pcmk_rc_ok;
 
 done:
     if (fflush(stream) != 0) {
         rc = errno;
         crm_perror(LOG_ERR, "flushing %s", filename);
     }
 
     // Don't report error if the file does not support synchronization
     if ((fsync(fileno(stream)) < 0) && (errno != EROFS) && (errno != EINVAL)) {
         rc = errno;
         crm_perror(LOG_ERR, "synchronizing %s", filename);
     }
 
     fclose(stream);
     crm_trace("Saved %u bytes to %s as XML", bytes_out, filename);
 
     if (nbytes != NULL) {
         *nbytes = bytes_out;
     }
-    g_free(buffer);
+    g_string_free(buffer, TRUE);
     return rc;
 }
 
 /*!
  * \internal
  * \brief Write XML to a file descriptor
  *
  * \param[in]  xml       XML to write
  * \param[in]  filename  Name of file being written (for logging only)
  * \param[in]  fd        Open file descriptor corresponding to \p filename
  * \param[in]  compress  If \c true, compress XML before writing
  * \param[out] nbytes    Number of bytes written (can be \c NULL)
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__xml_write_fd(const xmlNode *xml, const char *filename, int fd,
                    bool compress, unsigned int *nbytes)
 {
     // @COMPAT Drop compress and nbytes arguments when we drop write_xml_fd()
     FILE *stream = NULL;
 
     CRM_CHECK((xml != NULL) && (fd > 0), return EINVAL);
     stream = fdopen(fd, "w");
     if (stream == NULL) {
         return errno;
     }
 
     return write_xml_stream(xml, pcmk__s(filename, "unnamed file"), stream,
                             compress, nbytes);
 }
 
 /*!
  * \internal
  * \brief Write XML to a file
  *
  * \param[in]  xml       XML to write
  * \param[in]  filename  Name of file to write
  * \param[in]  compress  If \c true, compress XML before writing
  * \param[out] nbytes    Number of bytes written (can be \c NULL)
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__xml_write_file(const xmlNode *xml, const char *filename, bool compress,
                      unsigned int *nbytes)
 {
     // @COMPAT Drop nbytes argument when we drop write_xml_fd()
     FILE *stream = NULL;
 
     CRM_CHECK((xml != NULL) && (filename != NULL), return EINVAL);
     stream = fopen(filename, "w");
     if (stream == NULL) {
         return errno;
     }
 
     return write_xml_stream(xml, filename, stream, compress, nbytes);
 }
 
 /*!
  * \internal
  * \brief Serialize XML (using libxml) into provided descriptor
  *
  * \param[in] fd  File descriptor to (piece-wise) write to
  * \param[in] cur XML subtree to proceed
  *
  * \return a standard Pacemaker return code
  */
 int
 pcmk__xml2fd(int fd, xmlNode *cur)
 {
     bool success;
 
     xmlOutputBuffer *fd_out = xmlOutputBufferCreateFd(fd, NULL);
     CRM_ASSERT(fd_out != NULL);
     xmlNodeDumpOutput(fd_out, cur->doc, cur, 0, pcmk__xml_fmt_pretty, NULL);
 
     success = xmlOutputBufferWrite(fd_out, sizeof("\n") - 1, "\n") != -1;
 
     success = xmlOutputBufferClose(fd_out) != -1 && success;
 
     if (!success) {
         return EIO;
     }
 
     fsync(fd);
     return pcmk_rc_ok;
 }
 
 void
 save_xml_to_file(const xmlNode *xml, const char *desc, const char *filename)
 {
     char *f = NULL;
 
     if (filename == NULL) {
         char *uuid = crm_generate_uuid();
 
         f = crm_strdup_printf("%s/%s", pcmk__get_tmpdir(), uuid);
         filename = f;
         free(uuid);
     }
 
     crm_info("Saving %s to %s", desc, filename);
     pcmk__xml_write_file(xml, filename, false, NULL);
     free(f);
 }
 
 
 // Deprecated functions kept only for backward API compatibility
 // LCOV_EXCL_START
 
 #include <crm/common/xml_io_compat.h>
 
 xmlNode *
 filename2xml(const char *filename)
 {
     return pcmk__xml_read(filename);
 }
 
 xmlNode *
 stdin2xml(void)
 {
     return pcmk__xml_read(NULL);
 }
 
 xmlNode *
 string2xml(const char *input)
 {
     return pcmk__xml_parse(input);
 }
 
 char *
 dump_xml_formatted(const xmlNode *xml)
 {
     char *str = NULL;
-    gchar *g_str = pcmk__xml_dump(xml, pcmk__xml_fmt_pretty);
+    GString *buffer = g_string_sized_new(1024);
 
-    pcmk__str_update(&str, g_str);
-    g_free(g_str);
+    pcmk__xml_string(xml, pcmk__xml_fmt_pretty, buffer, 0);
+
+    pcmk__str_update(&str, buffer->str);
+    g_string_free(buffer, TRUE);
     return str;
 }
 
 char *
 dump_xml_formatted_with_text(const xmlNode *xml)
 {
     char *str = NULL;
-    gchar *g_str = pcmk__xml_dump(xml, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text);
+    GString *buffer = g_string_sized_new(1024);
+
+    pcmk__xml_string(xml, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, buffer, 0);
 
-    pcmk__str_update(&str, g_str);
-    g_free(g_str);
+    pcmk__str_update(&str, buffer->str);
+    g_string_free(buffer, TRUE);
     return str;
 }
 
 char *
 dump_xml_unformatted(const xmlNode *xml)
 {
     char *str = NULL;
-    gchar *g_str = pcmk__xml_dump(xml, 0);
+    GString *buffer = g_string_sized_new(1024);
+
+    pcmk__xml_string(xml, 0, buffer, 0);
 
-    pcmk__str_update(&str, g_str);
-    g_free(g_str);
+    pcmk__str_update(&str, buffer->str);
+    g_string_free(buffer, TRUE);
     return str;
 }
 
 int
 write_xml_fd(const xmlNode *xml, const char *filename, int fd,
              gboolean compress)
 {
     unsigned int nbytes = 0;
     int rc = pcmk__xml_write_fd(xml, filename, fd, compress, &nbytes);
 
     if (rc != pcmk_rc_ok) {
         return pcmk_rc2legacy(rc);
     }
     return (int) nbytes;
 }
 
 int
 write_xml_file(const xmlNode *xml, const char *filename, gboolean compress)
 {
     unsigned int nbytes = 0;
     int rc = pcmk__xml_write_file(xml, filename, compress, &nbytes);
 
     if (rc != pcmk_rc_ok) {
         return pcmk_rc2legacy(rc);
     }
     return (int) nbytes;
 }
 
 // LCOV_EXCL_STOP
 // End deprecated API
diff --git a/lib/fencing/st_rhcs.c b/lib/fencing/st_rhcs.c
index 9a76c03d78..0547a32866 100644
--- a/lib/fencing/st_rhcs.c
+++ b/lib/fencing/st_rhcs.c
@@ -1,324 +1,330 @@
 /*
  * Copyright 2004-2024 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 <string.h>
 #include <sys/stat.h>
 #include <glib.h>
 #include <dirent.h>
 
 #include <crm/crm.h>
 #include <crm/common/xml.h>
 #include <crm/stonith-ng.h>
 #include <crm/fencing/internal.h>
 
 #include "fencing_private.h"
 
 #define RH_STONITH_PREFIX "fence_"
 
 /*!
  * \internal
  * \brief Add available RHCS-compatible agents to a list
  *
  * \param[in,out]  List to add to
  *
  * \return Number of agents added
  */
 int
 stonith__list_rhcs_agents(stonith_key_value_t **devices)
 {
     // Essentially: ls -1 @sbin_dir@/fence_*
 
     int count = 0, i;
     struct dirent **namelist;
     const int file_num = scandir(PCMK__FENCE_BINDIR, &namelist, 0, alphasort);
 
 #if _POSIX_C_SOURCE < 200809L && !(defined(O_SEARCH) || defined(O_PATH))
     char buffer[FILENAME_MAX + 1];
 #elif defined(O_SEARCH)
     const int dirfd = open(PCMK__FENCE_BINDIR, O_SEARCH);
 #else
     const int dirfd = open(PCMK__FENCE_BINDIR, O_PATH);
 #endif
 
     for (i = 0; i < file_num; i++) {
         struct stat prop;
 
         if (pcmk__starts_with(namelist[i]->d_name, RH_STONITH_PREFIX)) {
 #if _POSIX_C_SOURCE < 200809L && !(defined(O_SEARCH) || defined(O_PATH))
             snprintf(buffer, sizeof(buffer), "%s/%s", PCMK__FENCE_BINDIR,
                      namelist[i]->d_name);
             if (stat(buffer, &prop) == 0 && S_ISREG(prop.st_mode)) {
 #else
             if (dirfd == -1) {
                 if (i == 0) {
                     crm_notice("Problem with listing %s directory"
                                CRM_XS "errno=%d", RH_STONITH_PREFIX, errno);
                 }
                 free(namelist[i]);
                 continue;
             }
             /* note: we can possibly prevent following symlinks here,
                      which may be a good idea, but fall on the nose when
                      these agents are moved elsewhere & linked back */
             if (fstatat(dirfd, namelist[i]->d_name, &prop, 0) == 0
                     && S_ISREG(prop.st_mode)) {
 #endif
                 *devices = stonith_key_value_add(*devices, NULL,
                                                  namelist[i]->d_name);
                 count++;
             }
         }
         free(namelist[i]);
     }
     if (file_num > 0) {
         free(namelist);
     }
 #if _POSIX_C_SOURCE >= 200809L || defined(O_SEARCH) || defined(O_PATH)
     if (dirfd >= 0) {
         close(dirfd);
     }
 #endif
     return count;
 }
 
 static void
 stonith_rhcs_parameter_not_required(xmlNode *metadata, const char *parameter)
 {
     char *xpath = NULL;
     xmlXPathObject *xpathObj = NULL;
 
     CRM_CHECK(metadata != NULL, return);
     CRM_CHECK(parameter != NULL, return);
 
     xpath = crm_strdup_printf("//" PCMK_XE_PARAMETER "[@" PCMK_XA_NAME "='%s']",
                               parameter);
     /* Fudge metadata so that the parameter isn't required in config
      * Pacemaker handles and adds it */
     xpathObj = xpath_search(metadata, xpath);
     if (numXpathResults(xpathObj) > 0) {
         xmlNode *tmp = getXpathResult(xpathObj, 0);
 
         crm_xml_add(tmp, "required", "0");
     }
     freeXpathObject(xpathObj);
     free(xpath);
 }
 
 /*!
  * \brief Execute RHCS-compatible agent's metadata action
  *
  * \param[in]  agent        Agent to execute
  * \param[in]  timeout_sec  Action timeout
  * \param[out] metadata     Where to store output xmlNode (or NULL to ignore)
  */
 static int
 stonith__rhcs_get_metadata(const char *agent, int timeout_sec,
                            xmlNode **metadata)
 {
     xmlNode *xml = NULL;
     xmlNode *actions = NULL;
     xmlXPathObject *xpathObj = NULL;
     stonith_action_t *action = stonith__action_create(agent,
                                                       PCMK_ACTION_METADATA,
                                                       NULL, 0, timeout_sec,
                                                       NULL, NULL, NULL);
     int rc = stonith__execute(action);
     pcmk__action_result_t *result = stonith__action_result(action);
 
     if (result == NULL) {
         if (rc < 0) {
             crm_warn("Could not execute metadata action for %s: %s "
                      CRM_XS " rc=%d", agent, pcmk_strerror(rc), rc);
         }
         stonith__destroy_action(action);
         return rc;
     }
 
     if (result->execution_status != PCMK_EXEC_DONE) {
         crm_warn("Could not execute metadata action for %s: %s",
                  agent, pcmk_exec_status_str(result->execution_status));
         rc = pcmk_rc2legacy(stonith__result2rc(result));
         stonith__destroy_action(action);
         return rc;
     }
 
     if (!pcmk__result_ok(result)) {
         crm_warn("Metadata action for %s returned error code %d",
                  agent, result->exit_status);
         rc = pcmk_rc2legacy(stonith__result2rc(result));
         stonith__destroy_action(action);
         return rc;
     }
 
     if (result->action_stdout == NULL) {
         crm_warn("Metadata action for %s returned no data", agent);
         stonith__destroy_action(action);
         return -ENODATA;
     }
 
     xml = pcmk__xml_parse(result->action_stdout);
     stonith__destroy_action(action);
 
     if (xml == NULL) {
         crm_warn("Metadata for %s is invalid", agent);
         return -pcmk_err_schema_validation;
     }
 
     xpathObj = xpath_search(xml, "//" PCMK_XE_ACTIONS);
     if (numXpathResults(xpathObj) > 0) {
         actions = getXpathResult(xpathObj, 0);
     }
     freeXpathObject(xpathObj);
 
     // Add start and stop (implemented by pacemaker, not agent) to meta-data
     xpathObj = xpath_search(xml,
                             "//" PCMK_XE_ACTION
                             "[@" PCMK_XA_NAME "='" PCMK_ACTION_STOP "']");
     if (numXpathResults(xpathObj) <= 0) {
         xmlNode *tmp = NULL;
         const char *timeout_str = NULL;
 
         timeout_str = pcmk__readable_interval(PCMK_DEFAULT_ACTION_TIMEOUT_MS);
 
         tmp = create_xml_node(actions, PCMK_XE_ACTION);
         crm_xml_add(tmp, PCMK_XA_NAME, PCMK_ACTION_STOP);
         crm_xml_add(tmp, PCMK_META_TIMEOUT, timeout_str);
 
         tmp = create_xml_node(actions, PCMK_XE_ACTION);
         crm_xml_add(tmp, PCMK_XA_NAME, PCMK_ACTION_START);
         crm_xml_add(tmp, PCMK_META_TIMEOUT, timeout_str);
     }
     freeXpathObject(xpathObj);
 
     // Fudge metadata so parameters are not required in config (pacemaker adds them)
     stonith_rhcs_parameter_not_required(xml, "action");
     stonith_rhcs_parameter_not_required(xml, "plug");
     stonith_rhcs_parameter_not_required(xml, "port");
 
     if (metadata) {
         *metadata = xml;
 
     } else {
         free_xml(xml);
     }
 
     return pcmk_ok;
 }
 
 /*!
  * \brief Retrieve metadata for RHCS-compatible fence agent
  *
  * \param[in]  agent        Agent to execute
  * \param[in]  timeout_sec  Action timeout
  * \param[out] output       Where to store action output (or NULL to ignore)
  */
 int
 stonith__rhcs_metadata(const char *agent, int timeout_sec, char **output)
 {
-    gchar *buffer = NULL;
+    GString *buffer = NULL;
     xmlNode *xml = NULL;
 
     int rc = stonith__rhcs_get_metadata(agent, timeout_sec, &xml);
 
     if (rc != pcmk_ok) {
-        free_xml(xml);
-        return rc;
+        goto done;
     }
 
-    buffer = pcmk__xml_dump(xml, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text);
-    free_xml(xml);
-    if (buffer == NULL) {
-        return -pcmk_err_schema_validation;
+    buffer = g_string_sized_new(1024);
+    pcmk__xml_string(xml, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, buffer, 0);
+
+    if (pcmk__str_empty(buffer->str)) {
+        rc = -pcmk_err_schema_validation;
+        goto done;
     }
 
     if (output != NULL) {
-        pcmk__str_update(output, buffer);
+        pcmk__str_update(output, buffer->str);
     }
-    g_free(buffer);
-    return pcmk_ok;
+
+done:
+    if (buffer != NULL) {
+        g_string_free(buffer, TRUE);
+    }
+    free_xml(xml);
+    return rc;
 }
 
 bool
 stonith__agent_is_rhcs(const char *agent)
 {
     struct stat prop;
     char *buffer = crm_strdup_printf(PCMK__FENCE_BINDIR "/%s", agent);
     int rc = stat(buffer, &prop);
 
     free(buffer);
     return (rc >= 0) && S_ISREG(prop.st_mode);
 }
 
 int
 stonith__rhcs_validate(stonith_t *st, int call_options, const char *target,
                        const char *agent, GHashTable *params,
                        const char * host_arg, int timeout,
                        char **output, char **error_output)
 {
     int rc = pcmk_ok;
     int remaining_timeout = timeout;
     xmlNode *metadata = NULL;
     stonith_action_t *action = NULL;
     pcmk__action_result_t *result = NULL;
 
     if (host_arg == NULL) {
         time_t start_time = time(NULL);
 
         rc = stonith__rhcs_get_metadata(agent, remaining_timeout, &metadata);
 
         if (rc == pcmk_ok) {
             uint32_t device_flags = 0;
 
             stonith__device_parameter_flags(&device_flags, agent, metadata);
             if (pcmk_is_set(device_flags, st_device_supports_parameter_port)) {
                 host_arg = "port";
 
             } else if (pcmk_is_set(device_flags,
                                    st_device_supports_parameter_plug)) {
                 host_arg = "plug";
             }
         }
 
         free_xml(metadata);
 
         remaining_timeout -= time(NULL) - start_time;
 
         if (rc == -ETIME || remaining_timeout <= 0 ) {
             return -ETIME;
         }
 
     } else if (pcmk__str_eq(host_arg, PCMK_VALUE_NONE, pcmk__str_casei)) {
         host_arg = NULL;
     }
 
     action = stonith__action_create(agent, PCMK_ACTION_VALIDATE_ALL, target, 0,
                                     remaining_timeout, params, NULL, host_arg);
 
     rc = stonith__execute(action);
     result = stonith__action_result(action);
 
     if (result != NULL) {
         rc = pcmk_rc2legacy(stonith__result2rc(result));
 
         // Take ownership of output so stonith__destroy_action() doesn't free it
         if (output != NULL) {
             *output = result->action_stdout;
             result->action_stdout = NULL;
         }
         if (error_output != NULL) {
             *error_output = result->action_stderr;
             result->action_stderr = NULL;
         }
     }
     stonith__destroy_action(action);
     return rc;
 }
diff --git a/lib/pengine/pe_output.c b/lib/pengine/pe_output.c
index 51600a1b63..0f8bb03e8c 100644
--- a/lib/pengine/pe_output.c
+++ b/lib/pengine/pe_output.c
@@ -1,3317 +1,3310 @@
 /*
  * Copyright 2019-2024 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 <stdint.h>
 
 #include <crm/common/xml_internal.h>
 #include <crm/common/output.h>
 #include <crm/common/scheduler_internal.h>
 #include <crm/cib/util.h>
 #include <crm/common/xml.h>
 #include <crm/pengine/internal.h>
 
 const char *
 pe__resource_description(const pcmk_resource_t *rsc, uint32_t show_opts)
 {
     const char * desc = NULL;
     // User-supplied description
     if (pcmk_any_flags_set(show_opts, pcmk_show_rsc_only|pcmk_show_description)) {
         desc = crm_element_value(rsc->xml, PCMK_XA_DESCRIPTION);
     }
     return desc;
 }
 
 /* Never display node attributes whose name starts with one of these prefixes */
 #define FILTER_STR { PCMK__FAIL_COUNT_PREFIX, PCMK__LAST_FAILURE_PREFIX,    \
                      PCMK__NODE_ATTR_SHUTDOWN, PCMK_NODE_ATTR_TERMINATE,    \
                      PCMK_NODE_ATTR_STANDBY, "#", NULL }
 
 static int
 compare_attribute(gconstpointer a, gconstpointer b)
 {
     int rc;
 
     rc = strcmp((const char *)a, (const char *)b);
 
     return rc;
 }
 
 /*!
  * \internal
  * \brief Determine whether extended information about an attribute should be added.
  *
  * \param[in]     node            Node that ran this resource
  * \param[in,out] rsc_list        List of resources for this node
  * \param[in,out] scheduler       Scheduler data
  * \param[in]     attrname        Attribute to find
  * \param[out]    expected_score  Expected value for this attribute
  *
  * \return true if extended information should be printed, false otherwise
  * \note Currently, extended information is only supported for ping/pingd
  *       resources, for which a message will be printed if connectivity is lost
  *       or degraded.
  */
 static bool
 add_extra_info(const pcmk_node_t *node, GList *rsc_list,
                pcmk_scheduler_t *scheduler, const char *attrname,
                int *expected_score)
 {
     GList *gIter = NULL;
 
     for (gIter = rsc_list; gIter != NULL; gIter = gIter->next) {
         pcmk_resource_t *rsc = (pcmk_resource_t *) gIter->data;
         const char *type = g_hash_table_lookup(rsc->meta, PCMK_XA_TYPE);
         const char *name = NULL;
         GHashTable *params = NULL;
 
         if (rsc->children != NULL) {
             if (add_extra_info(node, rsc->children, scheduler, attrname,
                                expected_score)) {
                 return true;
             }
         }
 
         if (!pcmk__strcase_any_of(type, "ping", "pingd", NULL)) {
             continue;
         }
 
         params = pe_rsc_params(rsc, node, scheduler);
         name = g_hash_table_lookup(params, PCMK_XA_NAME);
 
         if (name == NULL) {
             name = "pingd";
         }
 
         /* To identify the resource with the attribute name. */
         if (pcmk__str_eq(name, attrname, pcmk__str_casei)) {
             int host_list_num = 0;
             const char *hosts = g_hash_table_lookup(params, "host_list");
             const char *multiplier = g_hash_table_lookup(params, "multiplier");
             int multiplier_i;
 
             if (hosts) {
                 char **host_list = g_strsplit(hosts, " ", 0);
                 host_list_num = g_strv_length(host_list);
                 g_strfreev(host_list);
             }
 
             if ((multiplier == NULL)
                 || (pcmk__scan_min_int(multiplier, &multiplier_i,
                                        INT_MIN) != pcmk_rc_ok)) {
                 /* The ocf:pacemaker:ping resource agent defaults multiplier to
                  * 1. The agent currently does not handle invalid text, but it
                  * should, and this would be a reasonable choice ...
                  */
                 multiplier_i = 1;
             }
             *expected_score = host_list_num * multiplier_i;
 
             return true;
         }
     }
     return false;
 }
 
 static GList *
 filter_attr_list(GList *attr_list, char *name)
 {
     int i;
     const char *filt_str[] = FILTER_STR;
 
     CRM_CHECK(name != NULL, return attr_list);
 
     /* filtering automatic attributes */
     for (i = 0; filt_str[i] != NULL; i++) {
         if (g_str_has_prefix(name, filt_str[i])) {
             return attr_list;
         }
     }
 
     return g_list_insert_sorted(attr_list, name, compare_attribute);
 }
 
 static GList *
 get_operation_list(xmlNode *rsc_entry) {
     GList *op_list = NULL;
     xmlNode *rsc_op = NULL;
 
     for (rsc_op = pcmk__xe_first_child(rsc_entry); rsc_op != NULL;
          rsc_op = pcmk__xe_next(rsc_op)) {
         const char *task = crm_element_value(rsc_op, PCMK_XA_OPERATION);
         const char *interval_ms_s = crm_element_value(rsc_op,
                                                       PCMK_META_INTERVAL);
         const char *op_rc = crm_element_value(rsc_op, PCMK__XA_RC_CODE);
         int op_rc_i;
 
         pcmk__scan_min_int(op_rc, &op_rc_i, 0);
 
         /* Display 0-interval monitors as "probe" */
         if (pcmk__str_eq(task, PCMK_ACTION_MONITOR, pcmk__str_casei)
             && pcmk__str_eq(interval_ms_s, "0", pcmk__str_null_matches | pcmk__str_casei)) {
             task = "probe";
         }
 
         /* Ignore notifies and some probes */
         if (pcmk__str_eq(task, PCMK_ACTION_NOTIFY, pcmk__str_none)
             || (pcmk__str_eq(task, "probe", pcmk__str_none)
                 && (op_rc_i == CRM_EX_NOT_RUNNING))) {
             continue;
         }
 
         if (pcmk__xe_is(rsc_op, PCMK__XE_LRM_RSC_OP)) {
             op_list = g_list_append(op_list, rsc_op);
         }
     }
 
     op_list = g_list_sort(op_list, sort_op_by_callid);
     return op_list;
 }
 
 static void
 add_dump_node(gpointer key, gpointer value, gpointer user_data)
 {
     xmlNodePtr node = user_data;
     pcmk_create_xml_text_node(node, (const char *) key, (const char *) value);
 }
 
 static void
 append_dump_text(gpointer key, gpointer value, gpointer user_data)
 {
     char **dump_text = user_data;
     char *new_text = crm_strdup_printf("%s %s=%s",
                                        *dump_text, (char *)key, (char *)value);
 
     free(*dump_text);
     *dump_text = new_text;
 }
 
 #define XPATH_STACK "//" PCMK_XE_NVPAIR     \
                     "[@" PCMK_XA_NAME "='"  \
                         PCMK_OPT_CLUSTER_INFRASTRUCTURE "']"
 
 static const char *
 get_cluster_stack(pcmk_scheduler_t *scheduler)
 {
     xmlNode *stack = get_xpath_object(XPATH_STACK, scheduler->input, LOG_DEBUG);
 
     if (stack != NULL) {
         return crm_element_value(stack, PCMK_XA_VALUE);
     }
     return PCMK_VALUE_UNKNOWN;
 }
 
 static char *
 last_changed_string(const char *last_written, const char *user,
                     const char *client, const char *origin) {
     if (last_written != NULL || user != NULL || client != NULL || origin != NULL) {
         return crm_strdup_printf("%s%s%s%s%s%s%s",
                                  last_written ? last_written : "",
                                  user ? " by " : "",
                                  user ? user : "",
                                  client ? " via " : "",
                                  client ? client : "",
                                  origin ? " on " : "",
                                  origin ? origin : "");
     } else {
         return strdup("");
     }
 }
 
 static char *
 op_history_string(xmlNode *xml_op, const char *task, const char *interval_ms_s,
                   int rc, bool print_timing) {
     const char *call = crm_element_value(xml_op, PCMK__XA_CALL_ID);
     char *interval_str = NULL;
     char *buf = NULL;
 
     if (interval_ms_s && !pcmk__str_eq(interval_ms_s, "0", pcmk__str_casei)) {
         char *pair = pcmk__format_nvpair(PCMK_XA_INTERVAL, interval_ms_s, "ms");
         interval_str = crm_strdup_printf(" %s", pair);
         free(pair);
     }
 
     if (print_timing) {
         char *last_change_str = NULL;
         char *exec_str = NULL;
         char *queue_str = NULL;
 
         const char *value = NULL;
 
         time_t epoch = 0;
 
         if ((crm_element_value_epoch(xml_op, PCMK_XA_LAST_RC_CHANGE,
                                      &epoch) == pcmk_ok)
             && (epoch > 0)) {
             char *epoch_str = pcmk__epoch2str(&epoch, 0);
 
             last_change_str = crm_strdup_printf(" %s=\"%s\"",
                                                 PCMK_XA_LAST_RC_CHANGE,
                                                 pcmk__s(epoch_str, ""));
             free(epoch_str);
         }
 
         value = crm_element_value(xml_op, PCMK_XA_EXEC_TIME);
         if (value) {
             char *pair = pcmk__format_nvpair(PCMK_XA_EXEC_TIME, value, "ms");
             exec_str = crm_strdup_printf(" %s", pair);
             free(pair);
         }
 
         value = crm_element_value(xml_op, PCMK_XA_QUEUE_TIME);
         if (value) {
             char *pair = pcmk__format_nvpair(PCMK_XA_QUEUE_TIME, value, "ms");
             queue_str = crm_strdup_printf(" %s", pair);
             free(pair);
         }
 
         buf = crm_strdup_printf("(%s) %s:%s%s%s%s rc=%d (%s)", call, task,
                                 interval_str ? interval_str : "",
                                 last_change_str ? last_change_str : "",
                                 exec_str ? exec_str : "",
                                 queue_str ? queue_str : "",
                                 rc, services_ocf_exitcode_str(rc));
 
         if (last_change_str) {
             free(last_change_str);
         }
 
         if (exec_str) {
             free(exec_str);
         }
 
         if (queue_str) {
             free(queue_str);
         }
     } else {
         buf = crm_strdup_printf("(%s) %s%s%s", call, task,
                                 interval_str ? ":" : "",
                                 interval_str ? interval_str : "");
     }
 
     if (interval_str) {
         free(interval_str);
     }
 
     return buf;
 }
 
 static char *
 resource_history_string(pcmk_resource_t *rsc, const char *rsc_id, bool all,
                         int failcount, time_t last_failure) {
     char *buf = NULL;
 
     if (rsc == NULL) {
         buf = crm_strdup_printf("%s: orphan", rsc_id);
     } else if (all || failcount || last_failure > 0) {
         char *failcount_s = NULL;
         char *lastfail_s = NULL;
 
         if (failcount > 0) {
             failcount_s = crm_strdup_printf(" %s=%d",
                                             PCMK_XA_FAIL_COUNT, failcount);
         } else {
             failcount_s = strdup("");
         }
         if (last_failure > 0) {
             buf = pcmk__epoch2str(&last_failure, 0);
             lastfail_s = crm_strdup_printf(" %s='%s'",
                                            PCMK_XA_LAST_FAILURE, buf);
             free(buf);
         }
 
         buf = crm_strdup_printf("%s: " PCMK_META_MIGRATION_THRESHOLD "=%d%s%s",
                                 rsc_id, rsc->migration_threshold, failcount_s,
                                 lastfail_s? lastfail_s : "");
         free(failcount_s);
         free(lastfail_s);
     } else {
         buf = crm_strdup_printf("%s:", rsc_id);
     }
 
     return buf;
 }
 
 /*!
  * \internal
  * \brief Get a node's feature set for status display purposes
  *
  * \param[in] node  Node to check
  *
  * \return String representation of feature set if the node is fully up (using
  *         "<3.15.1" for older nodes that don't set the #feature-set attribute),
  *         otherwise NULL
  */
 static const char *
 get_node_feature_set(const pcmk_node_t *node)
 {
     if (node->details->online && node->details->expected_up
         && !pcmk__is_pacemaker_remote_node(node)) {
 
         const char *feature_set = g_hash_table_lookup(node->details->attrs,
                                                       CRM_ATTR_FEATURE_SET);
 
         /* The feature set attribute is present since 3.15.1. If it is missing,
          * then the node must be running an earlier version.
          */
         return pcmk__s(feature_set, "<3.15.1");
     }
     return NULL;
 }
 
 static bool
 is_mixed_version(pcmk_scheduler_t *scheduler)
 {
     const char *feature_set = NULL;
     for (GList *gIter = scheduler->nodes; gIter != NULL; gIter = gIter->next) {
         pcmk_node_t *node = gIter->data;
         const char *node_feature_set = get_node_feature_set(node);
         if (node_feature_set != NULL) {
             if (feature_set == NULL) {
                 feature_set = node_feature_set;
             } else if (strcmp(feature_set, node_feature_set) != 0) {
                 return true;
             }
         }
     }
     return false;
 }
 
-static gchar *
-formatted_xml_buf(const pcmk_resource_t *rsc, bool raw)
+static void
+formatted_xml_buf(const pcmk_resource_t *rsc, GString *xml_buf, bool raw)
 {
     if (raw && (rsc->orig_xml != NULL)) {
-        return pcmk__xml_dump(rsc->orig_xml, pcmk__xml_fmt_pretty);
+        pcmk__xml_string(rsc->orig_xml, pcmk__xml_fmt_pretty, xml_buf, 0);
     } else {
-        return pcmk__xml_dump(rsc->xml, pcmk__xml_fmt_pretty);
+        pcmk__xml_string(rsc->xml, pcmk__xml_fmt_pretty, xml_buf, 0);
     }
 }
 
 #define XPATH_DC_VERSION "//" PCMK_XE_NVPAIR    \
                          "[@" PCMK_XA_NAME "='" PCMK_OPT_DC_VERSION "']"
 
 PCMK__OUTPUT_ARGS("cluster-summary", "pcmk_scheduler_t *",
                   "enum pcmk_pacemakerd_state", "uint32_t", "uint32_t")
 static int
 cluster_summary(pcmk__output_t *out, va_list args) {
     pcmk_scheduler_t *scheduler = va_arg(args, pcmk_scheduler_t *);
     enum pcmk_pacemakerd_state pcmkd_state =
         (enum pcmk_pacemakerd_state) va_arg(args, int);
     uint32_t section_opts = va_arg(args, uint32_t);
     uint32_t show_opts = va_arg(args, uint32_t);
 
     int rc = pcmk_rc_no_output;
     const char *stack_s = get_cluster_stack(scheduler);
 
     if (pcmk_is_set(section_opts, pcmk_section_stack)) {
         PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Cluster Summary");
         out->message(out, "cluster-stack", stack_s, pcmkd_state);
     }
 
     if (pcmk_is_set(section_opts, pcmk_section_dc)) {
         xmlNode *dc_version = get_xpath_object(XPATH_DC_VERSION,
                                                scheduler->input, LOG_DEBUG);
         const char *dc_version_s = dc_version?
                                    crm_element_value(dc_version, PCMK_XA_VALUE)
                                    : NULL;
         const char *quorum = crm_element_value(scheduler->input,
                                                PCMK_XA_HAVE_QUORUM);
         char *dc_name = scheduler->dc_node? pe__node_display_name(scheduler->dc_node, pcmk_is_set(show_opts, pcmk_show_node_id)) : NULL;
         bool mixed_version = is_mixed_version(scheduler);
 
         PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Cluster Summary");
         out->message(out, "cluster-dc", scheduler->dc_node, quorum,
                      dc_version_s, dc_name, mixed_version);
         free(dc_name);
     }
 
     if (pcmk_is_set(section_opts, pcmk_section_times)) {
         const char *last_written = crm_element_value(scheduler->input,
                                                      PCMK_XA_CIB_LAST_WRITTEN);
         const char *user = crm_element_value(scheduler->input,
                                              PCMK_XA_UPDATE_USER);
         const char *client = crm_element_value(scheduler->input,
                                                PCMK_XA_UPDATE_CLIENT);
         const char *origin = crm_element_value(scheduler->input,
                                                PCMK_XA_UPDATE_ORIGIN);
 
         PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Cluster Summary");
         out->message(out, "cluster-times",
                      scheduler->localhost, last_written, user, client, origin);
     }
 
     if (pcmk_is_set(section_opts, pcmk_section_counts)) {
         PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Cluster Summary");
         out->message(out, "cluster-counts", g_list_length(scheduler->nodes),
                      scheduler->ninstances, scheduler->disabled_resources,
                      scheduler->blocked_resources);
     }
 
     if (pcmk_is_set(section_opts, pcmk_section_options)) {
         PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Cluster Summary");
         out->message(out, "cluster-options", scheduler);
     }
 
     PCMK__OUTPUT_LIST_FOOTER(out, rc);
 
     if (pcmk_is_set(section_opts, pcmk_section_maint_mode)) {
         if (out->message(out, "maint-mode", scheduler->flags) == pcmk_rc_ok) {
             rc = pcmk_rc_ok;
         }
     }
 
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("cluster-summary", "pcmk_scheduler_t *",
                   "enum pcmk_pacemakerd_state", "uint32_t", "uint32_t")
 static int
 cluster_summary_html(pcmk__output_t *out, va_list args) {
     pcmk_scheduler_t *scheduler = va_arg(args, pcmk_scheduler_t *);
     enum pcmk_pacemakerd_state pcmkd_state =
         (enum pcmk_pacemakerd_state) va_arg(args, int);
     uint32_t section_opts = va_arg(args, uint32_t);
     uint32_t show_opts = va_arg(args, uint32_t);
 
     int rc = pcmk_rc_no_output;
     const char *stack_s = get_cluster_stack(scheduler);
 
     if (pcmk_is_set(section_opts, pcmk_section_stack)) {
         PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Cluster Summary");
         out->message(out, "cluster-stack", stack_s, pcmkd_state);
     }
 
     /* Always print DC if none, even if not requested */
     if ((scheduler->dc_node == NULL)
         || pcmk_is_set(section_opts, pcmk_section_dc)) {
         xmlNode *dc_version = get_xpath_object(XPATH_DC_VERSION,
                                                scheduler->input, LOG_DEBUG);
         const char *dc_version_s = dc_version?
                                    crm_element_value(dc_version, PCMK_XA_VALUE)
                                    : NULL;
         const char *quorum = crm_element_value(scheduler->input,
                                                PCMK_XA_HAVE_QUORUM);
         char *dc_name = scheduler->dc_node? pe__node_display_name(scheduler->dc_node, pcmk_is_set(show_opts, pcmk_show_node_id)) : NULL;
         bool mixed_version = is_mixed_version(scheduler);
 
         PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Cluster Summary");
         out->message(out, "cluster-dc", scheduler->dc_node, quorum,
                      dc_version_s, dc_name, mixed_version);
         free(dc_name);
     }
 
     if (pcmk_is_set(section_opts, pcmk_section_times)) {
         const char *last_written = crm_element_value(scheduler->input,
                                                      PCMK_XA_CIB_LAST_WRITTEN);
         const char *user = crm_element_value(scheduler->input,
                                              PCMK_XA_UPDATE_USER);
         const char *client = crm_element_value(scheduler->input,
                                                PCMK_XA_UPDATE_CLIENT);
         const char *origin = crm_element_value(scheduler->input,
                                                PCMK_XA_UPDATE_ORIGIN);
 
         PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Cluster Summary");
         out->message(out, "cluster-times",
                      scheduler->localhost, last_written, user, client, origin);
     }
 
     if (pcmk_is_set(section_opts, pcmk_section_counts)) {
         PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Cluster Summary");
         out->message(out, "cluster-counts", g_list_length(scheduler->nodes),
                      scheduler->ninstances, scheduler->disabled_resources,
                      scheduler->blocked_resources);
     }
 
     if (pcmk_is_set(section_opts, pcmk_section_options)) {
         /* Kind of a hack - close the list we may have opened earlier in this
          * function so we can put all the options into their own list.  We
          * only want to do this on HTML output, though.
          */
         PCMK__OUTPUT_LIST_FOOTER(out, rc);
 
         out->begin_list(out, NULL, NULL, "Config Options");
         out->message(out, "cluster-options", scheduler);
     }
 
     PCMK__OUTPUT_LIST_FOOTER(out, rc);
 
     if (pcmk_is_set(section_opts, pcmk_section_maint_mode)) {
         if (out->message(out, "maint-mode", scheduler->flags) == pcmk_rc_ok) {
             rc = pcmk_rc_ok;
         }
     }
 
     return rc;
 }
 
 char *
 pe__node_display_name(pcmk_node_t *node, bool print_detail)
 {
     char *node_name;
     const char *node_host = NULL;
     const char *node_id = NULL;
     int name_len;
 
     CRM_ASSERT((node != NULL) && (node->details != NULL) && (node->details->uname != NULL));
 
     /* Host is displayed only if this is a guest node and detail is requested */
     if (print_detail && pcmk__is_guest_or_bundle_node(node)) {
         const pcmk_resource_t *container = node->details->remote_rsc->container;
         const pcmk_node_t *host_node = pcmk__current_node(container);
 
         if (host_node && host_node->details) {
             node_host = host_node->details->uname;
         }
         if (node_host == NULL) {
             node_host = ""; /* so we at least get "uname@" to indicate guest */
         }
     }
 
     /* Node ID is displayed if different from uname and detail is requested */
     if (print_detail && !pcmk__str_eq(node->details->uname, node->details->id, pcmk__str_casei)) {
         node_id = node->details->id;
     }
 
     /* Determine name length */
     name_len = strlen(node->details->uname) + 1;
     if (node_host) {
         name_len += strlen(node_host) + 1; /* "@node_host" */
     }
     if (node_id) {
         name_len += strlen(node_id) + 3; /* + " (node_id)" */
     }
 
     /* Allocate and populate display name */
     node_name = malloc(name_len);
     CRM_ASSERT(node_name != NULL);
     strcpy(node_name, node->details->uname);
     if (node_host) {
         strcat(node_name, "@");
         strcat(node_name, node_host);
     }
     if (node_id) {
         strcat(node_name, " (");
         strcat(node_name, node_id);
         strcat(node_name, ")");
     }
     return node_name;
 }
 
 int
 pe__name_and_nvpairs_xml(pcmk__output_t *out, bool is_list, const char *tag_name
                          , size_t pairs_count, ...)
 {
     xmlNodePtr xml_node = NULL;
     va_list args;
 
     CRM_ASSERT(tag_name != NULL);
 
     xml_node = pcmk__output_xml_peek_parent(out);
     CRM_ASSERT(xml_node != NULL);
     xml_node = create_xml_node(xml_node, tag_name);
 
     va_start(args, pairs_count);
     while(pairs_count--) {
         const char *param_name = va_arg(args, const char *);
         const char *param_value = va_arg(args, const char *);
         if (param_name && param_value) {
             crm_xml_add(xml_node, param_name, param_value);
         }
     };
     va_end(args);
 
     if (is_list) {
         pcmk__output_xml_push_parent(out, xml_node);
     }
     return pcmk_rc_ok;
 }
 
 static const char *
 role_desc(enum rsc_role_e role)
 {
     if (role == pcmk_role_promoted) {
 #ifdef PCMK__COMPAT_2_0
         return "as " PCMK__ROLE_PROMOTED_LEGACY " ";
 #else
         return "in " PCMK__ROLE_PROMOTED " role ";
 #endif
     }
     return "";
 }
 
 PCMK__OUTPUT_ARGS("ban", "pcmk_node_t *", "pcmk__location_t *", "uint32_t")
 static int
 ban_html(pcmk__output_t *out, va_list args) {
     pcmk_node_t *pe_node = va_arg(args, pcmk_node_t *);
     pcmk__location_t *location = va_arg(args, pcmk__location_t *);
     uint32_t show_opts = va_arg(args, uint32_t);
 
     char *node_name = pe__node_display_name(pe_node,
                                             pcmk_is_set(show_opts, pcmk_show_node_id));
     char *buf = crm_strdup_printf("%s\tprevents %s from running %son %s",
                                   location->id, location->rsc->id,
                                   role_desc(location->role_filter), node_name);
 
     pcmk__output_create_html_node(out, "li", NULL, NULL, buf);
 
     free(node_name);
     free(buf);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("ban", "pcmk_node_t *", "pcmk__location_t *", "uint32_t")
 static int
 ban_text(pcmk__output_t *out, va_list args) {
     pcmk_node_t *pe_node = va_arg(args, pcmk_node_t *);
     pcmk__location_t *location = va_arg(args, pcmk__location_t *);
     uint32_t show_opts = va_arg(args, uint32_t);
 
     char *node_name = pe__node_display_name(pe_node,
                                             pcmk_is_set(show_opts, pcmk_show_node_id));
     out->list_item(out, NULL, "%s\tprevents %s from running %son %s",
                    location->id, location->rsc->id,
                    role_desc(location->role_filter), node_name);
 
     free(node_name);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("ban", "pcmk_node_t *", "pcmk__location_t *", "uint32_t")
 static int
 ban_xml(pcmk__output_t *out, va_list args) {
     pcmk_node_t *pe_node = va_arg(args, pcmk_node_t *);
     pcmk__location_t *location = va_arg(args, pcmk__location_t *);
     uint32_t show_opts G_GNUC_UNUSED = va_arg(args, uint32_t);
 
     const char *promoted_only = pcmk__btoa(location->role_filter == pcmk_role_promoted);
     char *weight_s = pcmk__itoa(pe_node->weight);
 
     pcmk__output_create_xml_node(out, PCMK_XE_BAN,
                                  PCMK_XA_ID, location->id,
                                  PCMK_XA_RESOURCE, location->rsc->id,
                                  PCMK_XA_NODE, pe_node->details->uname,
                                  PCMK_XA_WEIGHT, weight_s,
                                  PCMK_XA_PROMOTED_ONLY, promoted_only,
                                  /* This is a deprecated alias for
                                   * promoted_only. Removing it will break
                                   * backward compatibility of the API schema,
                                   * which will require an API schema major
                                   * version bump.
                                   */
                                  PCMK__XA_PROMOTED_ONLY_LEGACY, promoted_only,
                                  NULL);
 
     free(weight_s);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("ban-list", "pcmk_scheduler_t *", "const char *", "GList *",
                   "uint32_t", "bool")
 static int
 ban_list(pcmk__output_t *out, va_list args) {
     pcmk_scheduler_t *scheduler = va_arg(args, pcmk_scheduler_t *);
     const char *prefix = va_arg(args, const char *);
     GList *only_rsc = va_arg(args, GList *);
     uint32_t show_opts = va_arg(args, uint32_t);
     bool print_spacer = va_arg(args, int);
 
     GList *gIter, *gIter2;
     int rc = pcmk_rc_no_output;
 
     /* Print each ban */
     for (gIter = scheduler->placement_constraints;
          gIter != NULL; gIter = gIter->next) {
         pcmk__location_t *location = gIter->data;
         const pcmk_resource_t *rsc = location->rsc;
 
         if (prefix != NULL && !g_str_has_prefix(location->id, prefix)) {
             continue;
         }
 
         if (!pcmk__str_in_list(rsc_printable_id(rsc), only_rsc,
                                pcmk__str_star_matches)
             && !pcmk__str_in_list(rsc_printable_id(pe__const_top_resource(rsc, false)),
                                   only_rsc, pcmk__str_star_matches)) {
             continue;
         }
 
         for (gIter2 = location->nodes; gIter2 != NULL; gIter2 = gIter2->next) {
             pcmk_node_t *node = (pcmk_node_t *) gIter2->data;
 
             if (node->weight < 0) {
                 PCMK__OUTPUT_LIST_HEADER(out, print_spacer, rc, "Negative Location Constraints");
                 out->message(out, "ban", node, location, show_opts);
             }
         }
     }
 
     PCMK__OUTPUT_LIST_FOOTER(out, rc);
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("cluster-counts", "unsigned int", "int", "int", "int")
 static int
 cluster_counts_html(pcmk__output_t *out, va_list args) {
     unsigned int nnodes = va_arg(args, unsigned int);
     int nresources = va_arg(args, int);
     int ndisabled = va_arg(args, int);
     int nblocked = va_arg(args, int);
 
     xmlNodePtr nodes_node = pcmk__output_create_xml_node(out, "li", NULL);
     xmlNodePtr resources_node = pcmk__output_create_xml_node(out, "li", NULL);
 
     char *nnodes_str = crm_strdup_printf("%d node%s configured",
                                          nnodes, pcmk__plural_s(nnodes));
 
     pcmk_create_html_node(nodes_node, PCMK__XE_SPAN, NULL, NULL, nnodes_str);
     free(nnodes_str);
 
     if (ndisabled && nblocked) {
         char *s = crm_strdup_printf("%d resource instance%s configured (%d ",
                                     nresources, pcmk__plural_s(nresources),
                                     ndisabled);
         pcmk_create_html_node(resources_node, PCMK__XE_SPAN, NULL, NULL, s);
         free(s);
 
         pcmk_create_html_node(resources_node, PCMK__XE_SPAN, NULL,
                               PCMK__VALUE_BOLD, "DISABLED");
 
         s = crm_strdup_printf(", %d ", nblocked);
         pcmk_create_html_node(resources_node, PCMK__XE_SPAN, NULL, NULL, s);
         free(s);
 
         pcmk_create_html_node(resources_node, PCMK__XE_SPAN, NULL,
                               PCMK__VALUE_BOLD, "BLOCKED");
         pcmk_create_html_node(resources_node, PCMK__XE_SPAN, NULL, NULL,
                               " from further action due to failure)");
     } else if (ndisabled && !nblocked) {
         char *s = crm_strdup_printf("%d resource instance%s configured (%d ",
                                     nresources, pcmk__plural_s(nresources),
                                     ndisabled);
         pcmk_create_html_node(resources_node, PCMK__XE_SPAN, NULL, NULL, s);
         free(s);
 
         pcmk_create_html_node(resources_node, PCMK__XE_SPAN, NULL,
                               PCMK__VALUE_BOLD, "DISABLED");
         pcmk_create_html_node(resources_node, PCMK__XE_SPAN, NULL, NULL, ")");
     } else if (!ndisabled && nblocked) {
         char *s = crm_strdup_printf("%d resource instance%s configured (%d ",
                                     nresources, pcmk__plural_s(nresources),
                                     nblocked);
         pcmk_create_html_node(resources_node, PCMK__XE_SPAN, NULL, NULL, s);
         free(s);
 
         pcmk_create_html_node(resources_node, PCMK__XE_SPAN, NULL,
                               PCMK__VALUE_BOLD, "BLOCKED");
         pcmk_create_html_node(resources_node, PCMK__XE_SPAN, NULL, NULL,
                               " from further action due to failure)");
     } else {
         char *s = crm_strdup_printf("%d resource instance%s configured",
                                     nresources, pcmk__plural_s(nresources));
         pcmk_create_html_node(resources_node, PCMK__XE_SPAN, NULL, NULL, s);
         free(s);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("cluster-counts", "unsigned int", "int", "int", "int")
 static int
 cluster_counts_text(pcmk__output_t *out, va_list args) {
     unsigned int nnodes = va_arg(args, unsigned int);
     int nresources = va_arg(args, int);
     int ndisabled = va_arg(args, int);
     int nblocked = va_arg(args, int);
 
     out->list_item(out, NULL, "%d node%s configured",
                    nnodes, pcmk__plural_s(nnodes));
 
     if (ndisabled && nblocked) {
         out->list_item(out, NULL, "%d resource instance%s configured "
                                   "(%d DISABLED, %d BLOCKED from "
                                   "further action due to failure)",
                        nresources, pcmk__plural_s(nresources), ndisabled,
                        nblocked);
     } else if (ndisabled && !nblocked) {
         out->list_item(out, NULL, "%d resource instance%s configured "
                                   "(%d DISABLED)",
                        nresources, pcmk__plural_s(nresources), ndisabled);
     } else if (!ndisabled && nblocked) {
         out->list_item(out, NULL, "%d resource instance%s configured "
                                   "(%d BLOCKED from further action "
                                   "due to failure)",
                        nresources, pcmk__plural_s(nresources), nblocked);
     } else {
         out->list_item(out, NULL, "%d resource instance%s configured",
                        nresources, pcmk__plural_s(nresources));
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("cluster-counts", "unsigned int", "int", "int", "int")
 static int
 cluster_counts_xml(pcmk__output_t *out, va_list args) {
     unsigned int nnodes = va_arg(args, unsigned int);
     int nresources = va_arg(args, int);
     int ndisabled = va_arg(args, int);
     int nblocked = va_arg(args, int);
 
     xmlNodePtr nodes_node = NULL;
     xmlNodePtr resources_node = NULL;
     char *s = NULL;
 
     nodes_node = pcmk__output_create_xml_node(out, PCMK_XE_NODES_CONFIGURED,
                                               NULL);
     resources_node = pcmk__output_create_xml_node(out,
                                                   PCMK_XE_RESOURCES_CONFIGURED,
                                                   NULL);
 
     s = pcmk__itoa(nnodes);
     crm_xml_add(nodes_node, PCMK_XA_NUMBER, s);
     free(s);
 
     s = pcmk__itoa(nresources);
     crm_xml_add(resources_node, PCMK_XA_NUMBER, s);
     free(s);
 
     s = pcmk__itoa(ndisabled);
     crm_xml_add(resources_node, PCMK_XA_DISABLED, s);
     free(s);
 
     s = pcmk__itoa(nblocked);
     crm_xml_add(resources_node, PCMK_XA_BLOCKED, s);
     free(s);
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("cluster-dc", "pcmk_node_t *", "const char *", "const char *",
                   "char *", "int")
 static int
 cluster_dc_html(pcmk__output_t *out, va_list args) {
     pcmk_node_t *dc = va_arg(args, pcmk_node_t *);
     const char *quorum = va_arg(args, const char *);
     const char *dc_version_s = va_arg(args, const char *);
     char *dc_name = va_arg(args, char *);
     bool mixed_version = va_arg(args, int);
 
     xmlNodePtr node = pcmk__output_create_xml_node(out, "li", NULL);
 
     pcmk_create_html_node(node, PCMK__XE_SPAN, NULL, PCMK__VALUE_BOLD,
                           "Current DC: ");
 
     if (dc) {
         char *buf = crm_strdup_printf("%s (version %s) -", dc_name,
                                       dc_version_s ? dc_version_s : "unknown");
         pcmk_create_html_node(node, PCMK__XE_SPAN, NULL, NULL, buf);
         free(buf);
 
         if (mixed_version) {
             pcmk_create_html_node(node, PCMK__XE_SPAN, NULL,
                                   PCMK__VALUE_WARNING, " MIXED-VERSION");
         }
         pcmk_create_html_node(node, PCMK__XE_SPAN, NULL, NULL, " partition");
         if (crm_is_true(quorum)) {
             pcmk_create_html_node(node, PCMK__XE_SPAN, NULL, NULL, " with");
         } else {
             pcmk_create_html_node(node, PCMK__XE_SPAN, NULL,
                                   PCMK__VALUE_WARNING, " WITHOUT");
         }
         pcmk_create_html_node(node, PCMK__XE_SPAN, NULL, NULL, " quorum");
     } else {
         pcmk_create_html_node(node, PCMK__XE_SPAN, NULL, PCMK__VALUE_WARNING,
                               "NONE");
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("cluster-dc", "pcmk_node_t *", "const char *", "const char *",
                   "char *", "int")
 static int
 cluster_dc_text(pcmk__output_t *out, va_list args) {
     pcmk_node_t *dc = va_arg(args, pcmk_node_t *);
     const char *quorum = va_arg(args, const char *);
     const char *dc_version_s = va_arg(args, const char *);
     char *dc_name = va_arg(args, char *);
     bool mixed_version = va_arg(args, int);
 
     if (dc) {
         out->list_item(out, "Current DC",
                        "%s (version %s) - %spartition %s quorum",
                        dc_name, dc_version_s ? dc_version_s : "unknown",
                        mixed_version ? "MIXED-VERSION " : "",
                        crm_is_true(quorum) ? "with" : "WITHOUT");
     } else {
         out->list_item(out, "Current DC", "NONE");
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("cluster-dc", "pcmk_node_t *", "const char *", "const char *",
                   "char *", "int")
 static int
 cluster_dc_xml(pcmk__output_t *out, va_list args) {
     pcmk_node_t *dc = va_arg(args, pcmk_node_t *);
     const char *quorum = va_arg(args, const char *);
     const char *dc_version_s = va_arg(args, const char *);
     char *dc_name G_GNUC_UNUSED = va_arg(args, char *);
     bool mixed_version = va_arg(args, int);
 
     if (dc) {
         const char *with_quorum = pcmk__btoa(crm_is_true(quorum));
         const char *mixed_version_s = pcmk__btoa(mixed_version);
 
         pcmk__output_create_xml_node(out, PCMK_XE_CURRENT_DC,
                                      PCMK_XA_PRESENT, PCMK_VALUE_TRUE,
                                      PCMK_XA_VERSION, pcmk__s(dc_version_s, ""),
                                      PCMK_XA_NAME, dc->details->uname,
                                      PCMK_XA_ID, dc->details->id,
                                      PCMK_XA_WITH_QUORUM, with_quorum,
                                      PCMK_XA_MIXED_VERSION, mixed_version_s,
                                      NULL);
     } else {
         pcmk__output_create_xml_node(out, PCMK_XE_CURRENT_DC,
                                      PCMK_XA_PRESENT, PCMK_VALUE_FALSE,
                                      NULL);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("maint-mode", "unsigned long long int")
 static int
 cluster_maint_mode_text(pcmk__output_t *out, va_list args) {
     unsigned long long flags = va_arg(args, unsigned long long);
 
     if (pcmk_is_set(flags, pcmk_sched_in_maintenance)) {
         pcmk__formatted_printf(out, "\n              *** Resource management is DISABLED ***\n");
         pcmk__formatted_printf(out, "  The cluster will not attempt to start, stop or recover services\n");
         return pcmk_rc_ok;
     } else if (pcmk_is_set(flags, pcmk_sched_stop_all)) {
         pcmk__formatted_printf(out, "\n    *** Resource management is DISABLED ***\n");
         pcmk__formatted_printf(out, "  The cluster will keep all resources stopped\n");
         return pcmk_rc_ok;
     } else {
         return pcmk_rc_no_output;
     }
 }
 
 PCMK__OUTPUT_ARGS("cluster-options", "pcmk_scheduler_t *")
 static int
 cluster_options_html(pcmk__output_t *out, va_list args) {
     pcmk_scheduler_t *scheduler = va_arg(args, pcmk_scheduler_t *);
 
     if (pcmk_is_set(scheduler->flags, pcmk_sched_fencing_enabled)) {
         out->list_item(out, NULL, "STONITH of failed nodes enabled");
     } else {
         out->list_item(out, NULL, "STONITH of failed nodes disabled");
     }
 
     if (pcmk_is_set(scheduler->flags, pcmk_sched_symmetric_cluster)) {
         out->list_item(out, NULL, "Cluster is symmetric");
     } else {
         out->list_item(out, NULL, "Cluster is asymmetric");
     }
 
     switch (scheduler->no_quorum_policy) {
         case pcmk_no_quorum_freeze:
             out->list_item(out, NULL, "No quorum policy: Freeze resources");
             break;
 
         case pcmk_no_quorum_stop:
             out->list_item(out, NULL, "No quorum policy: Stop ALL resources");
             break;
 
         case pcmk_no_quorum_demote:
             out->list_item(out, NULL, "No quorum policy: Demote promotable "
                            "resources and stop all other resources");
             break;
 
         case pcmk_no_quorum_ignore:
             out->list_item(out, NULL, "No quorum policy: Ignore");
             break;
 
         case pcmk_no_quorum_fence:
             out->list_item(out, NULL, "No quorum policy: Suicide");
             break;
     }
 
     if (pcmk_is_set(scheduler->flags, pcmk_sched_in_maintenance)) {
         xmlNodePtr node = pcmk__output_create_xml_node(out, "li", NULL);
 
         pcmk_create_html_node(node, PCMK__XE_SPAN, NULL, NULL,
                               "Resource management: ");
         pcmk_create_html_node(node, PCMK__XE_SPAN, NULL, PCMK__VALUE_BOLD,
                               "DISABLED");
         pcmk_create_html_node(node, PCMK__XE_SPAN, NULL, NULL,
                               " (the cluster will not attempt to start, stop,"
                               " or recover services)");
     } else if (pcmk_is_set(scheduler->flags, pcmk_sched_stop_all)) {
         xmlNodePtr node = pcmk__output_create_xml_node(out, "li", NULL);
 
         pcmk_create_html_node(node, PCMK__XE_SPAN, NULL, NULL,
                               "Resource management: ");
         pcmk_create_html_node(node, PCMK__XE_SPAN, NULL, PCMK__VALUE_BOLD,
                               "STOPPED");
         pcmk_create_html_node(node, PCMK__XE_SPAN, NULL, NULL,
                               " (the cluster will keep all resources stopped)");
     } else {
         out->list_item(out, NULL, "Resource management: enabled");
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("cluster-options", "pcmk_scheduler_t *")
 static int
 cluster_options_log(pcmk__output_t *out, va_list args) {
     pcmk_scheduler_t *scheduler = va_arg(args, pcmk_scheduler_t *);
 
     if (pcmk_is_set(scheduler->flags, pcmk_sched_in_maintenance)) {
         return out->info(out, "Resource management is DISABLED.  The cluster will not attempt to start, stop or recover services.");
     } else if (pcmk_is_set(scheduler->flags, pcmk_sched_stop_all)) {
         return out->info(out, "Resource management is DISABLED.  The cluster has stopped all resources.");
     } else {
         return pcmk_rc_no_output;
     }
 }
 
 PCMK__OUTPUT_ARGS("cluster-options", "pcmk_scheduler_t *")
 static int
 cluster_options_text(pcmk__output_t *out, va_list args) {
     pcmk_scheduler_t *scheduler = va_arg(args, pcmk_scheduler_t *);
 
     if (pcmk_is_set(scheduler->flags, pcmk_sched_fencing_enabled)) {
         out->list_item(out, NULL, "STONITH of failed nodes enabled");
     } else {
         out->list_item(out, NULL, "STONITH of failed nodes disabled");
     }
 
     if (pcmk_is_set(scheduler->flags, pcmk_sched_symmetric_cluster)) {
         out->list_item(out, NULL, "Cluster is symmetric");
     } else {
         out->list_item(out, NULL, "Cluster is asymmetric");
     }
 
     switch (scheduler->no_quorum_policy) {
         case pcmk_no_quorum_freeze:
             out->list_item(out, NULL, "No quorum policy: Freeze resources");
             break;
 
         case pcmk_no_quorum_stop:
             out->list_item(out, NULL, "No quorum policy: Stop ALL resources");
             break;
 
         case pcmk_no_quorum_demote:
             out->list_item(out, NULL, "No quorum policy: Demote promotable "
                            "resources and stop all other resources");
             break;
 
         case pcmk_no_quorum_ignore:
             out->list_item(out, NULL, "No quorum policy: Ignore");
             break;
 
         case pcmk_no_quorum_fence:
             out->list_item(out, NULL, "No quorum policy: Suicide");
             break;
     }
 
     return pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Get readable string representation of a no-quorum policy
  *
  * \param[in] policy  No-quorum policy
  *
  * \return String representation of \p policy
  */
 static const char *
 no_quorum_policy_text(enum pe_quorum_policy policy)
 {
     switch (policy) {
         case pcmk_no_quorum_freeze:
             return PCMK_VALUE_FREEZE;
 
         case pcmk_no_quorum_stop:
             return PCMK_VALUE_STOP;
 
         case pcmk_no_quorum_demote:
             return PCMK_VALUE_DEMOTE;
 
         case pcmk_no_quorum_ignore:
             return PCMK_VALUE_IGNORE;
 
         case pcmk_no_quorum_fence:
             return PCMK_VALUE_FENCE_LEGACY;
 
         default:
             return PCMK_VALUE_UNKNOWN;
     }
 }
 
 PCMK__OUTPUT_ARGS("cluster-options", "pcmk_scheduler_t *")
 static int
 cluster_options_xml(pcmk__output_t *out, va_list args) {
     pcmk_scheduler_t *scheduler = va_arg(args, pcmk_scheduler_t *);
 
     const char *stonith_enabled = pcmk__flag_text(scheduler->flags,
                                                   pcmk_sched_fencing_enabled);
     const char *symmetric_cluster =
         pcmk__flag_text(scheduler->flags, pcmk_sched_symmetric_cluster);
     const char *no_quorum_policy =
         no_quorum_policy_text(scheduler->no_quorum_policy);
     const char *maintenance_mode = pcmk__flag_text(scheduler->flags,
                                                    pcmk_sched_in_maintenance);
     const char *stop_all_resources = pcmk__flag_text(scheduler->flags,
                                                      pcmk_sched_stop_all);
     char *stonith_timeout_ms_s = pcmk__itoa(scheduler->stonith_timeout);
     char *priority_fencing_delay_ms_s =
         pcmk__itoa(scheduler->priority_fencing_delay * 1000);
 
     pcmk__output_create_xml_node(out, PCMK_XE_CLUSTER_OPTIONS,
                                  PCMK_XA_STONITH_ENABLED, stonith_enabled,
                                  PCMK_XA_SYMMETRIC_CLUSTER, symmetric_cluster,
                                  PCMK_XA_NO_QUORUM_POLICY, no_quorum_policy,
                                  PCMK_XA_MAINTENANCE_MODE, maintenance_mode,
                                  PCMK_XA_STOP_ALL_RESOURCES, stop_all_resources,
                                  PCMK_XA_STONITH_TIMEOUT_MS,
                                      stonith_timeout_ms_s,
                                  PCMK_XA_PRIORITY_FENCING_DELAY_MS,
                                      priority_fencing_delay_ms_s,
                                  NULL);
     free(stonith_timeout_ms_s);
     free(priority_fencing_delay_ms_s);
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("cluster-stack", "const char *", "enum pcmk_pacemakerd_state")
 static int
 cluster_stack_html(pcmk__output_t *out, va_list args) {
     const char *stack_s = va_arg(args, const char *);
     enum pcmk_pacemakerd_state pcmkd_state =
         (enum pcmk_pacemakerd_state) va_arg(args, int);
 
     xmlNodePtr node = pcmk__output_create_xml_node(out, "li", NULL);
 
     pcmk_create_html_node(node, PCMK__XE_SPAN, NULL, PCMK__VALUE_BOLD,
                           "Stack: ");
     pcmk_create_html_node(node, PCMK__XE_SPAN, NULL, NULL, stack_s);
 
     if (pcmkd_state != pcmk_pacemakerd_state_invalid) {
         pcmk_create_html_node(node, PCMK__XE_SPAN, NULL, NULL, " (");
         pcmk_create_html_node(node, PCMK__XE_SPAN, NULL, NULL,
                               pcmk__pcmkd_state_enum2friendly(pcmkd_state));
         pcmk_create_html_node(node, PCMK__XE_SPAN, NULL, NULL, ")");
     }
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("cluster-stack", "const char *", "enum pcmk_pacemakerd_state")
 static int
 cluster_stack_text(pcmk__output_t *out, va_list args) {
     const char *stack_s = va_arg(args, const char *);
     enum pcmk_pacemakerd_state pcmkd_state =
         (enum pcmk_pacemakerd_state) va_arg(args, int);
 
     if (pcmkd_state != pcmk_pacemakerd_state_invalid) {
         out->list_item(out, "Stack", "%s (%s)",
                        stack_s, pcmk__pcmkd_state_enum2friendly(pcmkd_state));
     } else {
         out->list_item(out, "Stack", "%s", stack_s);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("cluster-stack", "const char *", "enum pcmk_pacemakerd_state")
 static int
 cluster_stack_xml(pcmk__output_t *out, va_list args) {
     const char *stack_s = va_arg(args, const char *);
     enum pcmk_pacemakerd_state pcmkd_state =
         (enum pcmk_pacemakerd_state) va_arg(args, int);
 
     const char *state_s = NULL;
 
     if (pcmkd_state != pcmk_pacemakerd_state_invalid) {
         state_s = pcmk_pacemakerd_api_daemon_state_enum2text(pcmkd_state);
     }
 
     pcmk__output_create_xml_node(out, PCMK_XE_STACK,
                                  PCMK_XA_TYPE, stack_s,
                                  PCMK_XA_PACEMAKERD_STATE, state_s,
                                  NULL);
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("cluster-times", "const char *", "const char *",
                   "const char *", "const char *", "const char *")
 static int
 cluster_times_html(pcmk__output_t *out, va_list args) {
     const char *our_nodename = va_arg(args, const char *);
     const char *last_written = va_arg(args, const char *);
     const char *user = va_arg(args, const char *);
     const char *client = va_arg(args, const char *);
     const char *origin = va_arg(args, const char *);
 
     xmlNodePtr updated_node = pcmk__output_create_xml_node(out, "li", NULL);
     xmlNodePtr changed_node = pcmk__output_create_xml_node(out, "li", NULL);
 
     char *time_s = pcmk__epoch2str(NULL, 0);
 
     pcmk_create_html_node(updated_node, PCMK__XE_SPAN, NULL, PCMK__VALUE_BOLD,
                           "Last updated: ");
     pcmk_create_html_node(updated_node, PCMK__XE_SPAN, NULL, NULL, time_s);
 
     if (our_nodename != NULL) {
         pcmk_create_html_node(updated_node, PCMK__XE_SPAN, NULL, NULL, " on ");
         pcmk_create_html_node(updated_node, PCMK__XE_SPAN, NULL, NULL,
                               our_nodename);
     }
 
     free(time_s);
     time_s = last_changed_string(last_written, user, client, origin);
 
     pcmk_create_html_node(changed_node, PCMK__XE_SPAN, NULL, PCMK__VALUE_BOLD,
                           "Last change: ");
     pcmk_create_html_node(changed_node, PCMK__XE_SPAN, NULL, NULL, time_s);
 
     free(time_s);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("cluster-times", "const char *", "const char *",
                   "const char *", "const char *", "const char *")
 static int
 cluster_times_xml(pcmk__output_t *out, va_list args) {
     const char *our_nodename = va_arg(args, const char *);
     const char *last_written = va_arg(args, const char *);
     const char *user = va_arg(args, const char *);
     const char *client = va_arg(args, const char *);
     const char *origin = va_arg(args, const char *);
 
     char *time_s = pcmk__epoch2str(NULL, 0);
 
     pcmk__output_create_xml_node(out, PCMK_XE_LAST_UPDATE,
                                  PCMK_XA_TIME, time_s,
                                  PCMK_XA_ORIGIN, our_nodename,
                                  NULL);
 
     pcmk__output_create_xml_node(out, PCMK_XE_LAST_CHANGE,
                                  PCMK_XA_TIME, pcmk__s(last_written, ""),
                                  PCMK_XA_USER, pcmk__s(user, ""),
                                  PCMK_XA_CLIENT, pcmk__s(client, ""),
                                  PCMK_XA_ORIGIN, pcmk__s(origin, ""),
                                  NULL);
 
     free(time_s);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("cluster-times", "const char *", "const char *",
                   "const char *", "const char *", "const char *")
 static int
 cluster_times_text(pcmk__output_t *out, va_list args) {
     const char *our_nodename = va_arg(args, const char *);
     const char *last_written = va_arg(args, const char *);
     const char *user = va_arg(args, const char *);
     const char *client = va_arg(args, const char *);
     const char *origin = va_arg(args, const char *);
 
     char *time_s = pcmk__epoch2str(NULL, 0);
 
     out->list_item(out, "Last updated", "%s%s%s",
                    time_s, (our_nodename != NULL)? " on " : "",
                    pcmk__s(our_nodename, ""));
 
     free(time_s);
     time_s = last_changed_string(last_written, user, client, origin);
 
     out->list_item(out, "Last change", " %s", time_s);
 
     free(time_s);
     return pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Display a failed action in less-technical natural language
  *
  * \param[in,out] out          Output object to use for display
  * \param[in]     xml_op       XML containing failed action
  * \param[in]     op_key       Operation key of failed action
  * \param[in]     node_name    Where failed action occurred
  * \param[in]     rc           OCF exit code of failed action
  * \param[in]     status       Execution status of failed action
  * \param[in]     exit_reason  Exit reason given for failed action
  * \param[in]     exec_time    String containing execution time in milliseconds
  */
 static void
 failed_action_friendly(pcmk__output_t *out, const xmlNode *xml_op,
                        const char *op_key, const char *node_name, int rc,
                        int status, const char *exit_reason,
                        const char *exec_time)
 {
     char *rsc_id = NULL;
     char *task = NULL;
     guint interval_ms = 0;
     time_t last_change_epoch = 0;
     GString *str = NULL;
 
     if (pcmk__str_empty(op_key)
         || !parse_op_key(op_key, &rsc_id, &task, &interval_ms)) {
         rsc_id = strdup("unknown resource");
         task = strdup("unknown action");
         interval_ms = 0;
     }
     CRM_ASSERT((rsc_id != NULL) && (task != NULL));
 
     str = g_string_sized_new(256); // Should be sufficient for most messages
 
     pcmk__g_strcat(str, rsc_id, " ", NULL);
 
     if (interval_ms != 0) {
         pcmk__g_strcat(str, pcmk__readable_interval(interval_ms), "-interval ",
                        NULL);
     }
     pcmk__g_strcat(str, pcmk__readable_action(task, interval_ms), " on ",
                    node_name, NULL);
 
     if (status == PCMK_EXEC_DONE) {
         pcmk__g_strcat(str, " returned '", services_ocf_exitcode_str(rc), "'",
                        NULL);
         if (!pcmk__str_empty(exit_reason)) {
             pcmk__g_strcat(str, " (", exit_reason, ")", NULL);
         }
 
     } else {
         pcmk__g_strcat(str, " could not be executed (",
                        pcmk_exec_status_str(status), NULL);
         if (!pcmk__str_empty(exit_reason)) {
             pcmk__g_strcat(str, ": ", exit_reason, NULL);
         }
         g_string_append_c(str, ')');
     }
 
 
     if (crm_element_value_epoch(xml_op, PCMK_XA_LAST_RC_CHANGE,
                                 &last_change_epoch) == pcmk_ok) {
         char *s = pcmk__epoch2str(&last_change_epoch, 0);
 
         pcmk__g_strcat(str, " at ", s, NULL);
         free(s);
     }
     if (!pcmk__str_empty(exec_time)) {
         int exec_time_ms = 0;
 
         if ((pcmk__scan_min_int(exec_time, &exec_time_ms, 0) == pcmk_rc_ok)
             && (exec_time_ms > 0)) {
 
             pcmk__g_strcat(str, " after ",
                            pcmk__readable_interval(exec_time_ms), NULL);
         }
     }
 
     out->list_item(out, NULL, "%s", str->str);
     g_string_free(str, TRUE);
     free(rsc_id);
     free(task);
 }
 
 /*!
  * \internal
  * \brief Display a failed action with technical details
  *
  * \param[in,out] out          Output object to use for display
  * \param[in]     xml_op       XML containing failed action
  * \param[in]     op_key       Operation key of failed action
  * \param[in]     node_name    Where failed action occurred
  * \param[in]     rc           OCF exit code of failed action
  * \param[in]     status       Execution status of failed action
  * \param[in]     exit_reason  Exit reason given for failed action
  * \param[in]     exec_time    String containing execution time in milliseconds
  */
 static void
 failed_action_technical(pcmk__output_t *out, const xmlNode *xml_op,
                         const char *op_key, const char *node_name, int rc,
                         int status, const char *exit_reason,
                         const char *exec_time)
 {
     const char *call_id = crm_element_value(xml_op, PCMK__XA_CALL_ID);
     const char *queue_time = crm_element_value(xml_op, PCMK_XA_QUEUE_TIME);
     const char *exit_status = services_ocf_exitcode_str(rc);
     const char *lrm_status = pcmk_exec_status_str(status);
     time_t last_change_epoch = 0;
     GString *str = NULL;
 
     if (pcmk__str_empty(op_key)) {
         op_key = "unknown operation";
     }
     if (pcmk__str_empty(exit_status)) {
         exit_status = "unknown exit status";
     }
     if (pcmk__str_empty(call_id)) {
         call_id = "unknown";
     }
 
     str = g_string_sized_new(256);
 
     g_string_append_printf(str, "%s on %s '%s' (%d): call=%s, status='%s'",
                            op_key, node_name, exit_status, rc, call_id,
                            lrm_status);
 
     if (!pcmk__str_empty(exit_reason)) {
         pcmk__g_strcat(str, ", exitreason='", exit_reason, "'", NULL);
     }
 
     if (crm_element_value_epoch(xml_op, PCMK_XA_LAST_RC_CHANGE,
                                 &last_change_epoch) == pcmk_ok) {
         char *last_change_str = pcmk__epoch2str(&last_change_epoch, 0);
 
         pcmk__g_strcat(str,
                        ", " PCMK_XA_LAST_RC_CHANGE "="
                        "'", last_change_str, "'", NULL);
         free(last_change_str);
     }
     if (!pcmk__str_empty(queue_time)) {
         pcmk__g_strcat(str, ", queued=", queue_time, "ms", NULL);
     }
     if (!pcmk__str_empty(exec_time)) {
         pcmk__g_strcat(str, ", exec=", exec_time, "ms", NULL);
     }
 
     out->list_item(out, NULL, "%s", str->str);
     g_string_free(str, TRUE);
 }
 
 PCMK__OUTPUT_ARGS("failed-action", "xmlNode *", "uint32_t")
 static int
 failed_action_default(pcmk__output_t *out, va_list args)
 {
     xmlNodePtr xml_op = va_arg(args, xmlNodePtr);
     uint32_t show_opts = va_arg(args, uint32_t);
 
     const char *op_key = pcmk__xe_history_key(xml_op);
     const char *node_name = crm_element_value(xml_op, PCMK_XA_UNAME);
     const char *exit_reason = crm_element_value(xml_op, PCMK_XA_EXIT_REASON);
     const char *exec_time = crm_element_value(xml_op, PCMK_XA_EXEC_TIME);
 
     int rc;
     int status;
 
     pcmk__scan_min_int(crm_element_value(xml_op, PCMK__XA_RC_CODE), &rc, 0);
 
     pcmk__scan_min_int(crm_element_value(xml_op, PCMK__XA_OP_STATUS), &status,
                        0);
 
     if (pcmk__str_empty(node_name)) {
         node_name = "unknown node";
     }
 
     if (pcmk_is_set(show_opts, pcmk_show_failed_detail)) {
         failed_action_technical(out, xml_op, op_key, node_name, rc, status,
                                 exit_reason, exec_time);
     } else {
         failed_action_friendly(out, xml_op, op_key, node_name, rc, status,
                                exit_reason, exec_time);
     }
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("failed-action", "xmlNode *", "uint32_t")
 static int
 failed_action_xml(pcmk__output_t *out, va_list args) {
     xmlNodePtr xml_op = va_arg(args, xmlNodePtr);
     uint32_t show_opts G_GNUC_UNUSED = va_arg(args, uint32_t);
 
     const char *op_key = pcmk__xe_history_key(xml_op);
     const char *op_key_name = PCMK_XA_OP_KEY;
     int rc;
     int status;
     const char *uname = crm_element_value(xml_op, PCMK_XA_UNAME);
     const char *call_id = crm_element_value(xml_op, PCMK__XA_CALL_ID);
     const char *exitstatus = NULL;
     const char *exit_reason = pcmk__s(crm_element_value(xml_op,
                                                         PCMK_XA_EXIT_REASON),
                                       "none");
     const char *status_s = NULL;
 
     time_t epoch = 0;
     char *exit_reason_esc = NULL;
     char *rc_s = NULL;
     xmlNodePtr node = NULL;
 
     if (pcmk__xml_needs_escape(exit_reason, true)) {
         exit_reason_esc = pcmk__xml_escape(exit_reason, true);
         exit_reason = exit_reason_esc;
     }
     pcmk__scan_min_int(crm_element_value(xml_op, PCMK__XA_RC_CODE), &rc, 0);
     pcmk__scan_min_int(crm_element_value(xml_op, PCMK__XA_OP_STATUS), &status,
                        0);
 
     if (crm_element_value(xml_op, PCMK__XA_OPERATION_KEY) == NULL) {
         op_key_name = PCMK_XA_ID;
     }
     exitstatus = services_ocf_exitcode_str(rc);
     rc_s = pcmk__itoa(rc);
     status_s = pcmk_exec_status_str(status);
     node = pcmk__output_create_xml_node(out, PCMK_XE_FAILURE,
                                         op_key_name, op_key,
                                         PCMK_XA_NODE, uname,
                                         PCMK_XA_EXITSTATUS, exitstatus,
                                         PCMK_XA_EXITREASON, exit_reason,
                                         PCMK_XA_EXITCODE, rc_s,
                                         PCMK_XA_CALL, call_id,
                                         PCMK_XA_STATUS, status_s,
                                         NULL);
     free(rc_s);
 
     if ((crm_element_value_epoch(xml_op, PCMK_XA_LAST_RC_CHANGE,
                                  &epoch) == pcmk_ok) && (epoch > 0)) {
 
         const char *queue_time = crm_element_value(xml_op, PCMK_XA_QUEUE_TIME);
         const char *exec = crm_element_value(xml_op, PCMK_XA_EXEC_TIME);
         const char *task = crm_element_value(xml_op, PCMK_XA_OPERATION);
         guint interval_ms = 0;
         char *interval_ms_s = NULL;
         char *rc_change = pcmk__epoch2str(&epoch,
                                           crm_time_log_date
                                           |crm_time_log_timeofday
                                           |crm_time_log_with_timezone);
 
         crm_element_value_ms(xml_op, PCMK_META_INTERVAL, &interval_ms);
         interval_ms_s = crm_strdup_printf("%u", interval_ms);
 
         pcmk__xe_set_props(node,
                            PCMK_XA_LAST_RC_CHANGE, rc_change,
                            PCMK_XA_QUEUED, queue_time,
                            PCMK_XA_EXEC, exec,
                            PCMK_XA_INTERVAL, interval_ms_s,
                            PCMK_XA_TASK, task,
                            NULL);
 
         free(interval_ms_s);
         free(rc_change);
     }
 
     free(exit_reason_esc);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("failed-action-list", "pcmk_scheduler_t *", "GList *",
                   "GList *", "uint32_t", "bool")
 static int
 failed_action_list(pcmk__output_t *out, va_list args) {
     pcmk_scheduler_t *scheduler = va_arg(args, pcmk_scheduler_t *);
     GList *only_node = va_arg(args, GList *);
     GList *only_rsc = va_arg(args, GList *);
     uint32_t show_opts = va_arg(args, uint32_t);
     bool print_spacer = va_arg(args, int);
 
     xmlNode *xml_op = NULL;
     int rc = pcmk_rc_no_output;
 
     if (xmlChildElementCount(scheduler->failed) == 0) {
         return rc;
     }
 
     for (xml_op = pcmk__xml_first_child(scheduler->failed); xml_op != NULL;
          xml_op = pcmk__xml_next(xml_op)) {
         char *rsc = NULL;
 
         if (!pcmk__str_in_list(crm_element_value(xml_op, PCMK_XA_UNAME),
                                only_node,
                                pcmk__str_star_matches|pcmk__str_casei)) {
             continue;
         }
 
         if (pcmk_xe_mask_probe_failure(xml_op)) {
             continue;
         }
 
         if (!parse_op_key(pcmk__xe_history_key(xml_op), &rsc, NULL, NULL)) {
             continue;
         }
 
         if (!pcmk__str_in_list(rsc, only_rsc, pcmk__str_star_matches)) {
             free(rsc);
             continue;
         }
 
         free(rsc);
 
         PCMK__OUTPUT_LIST_HEADER(out, print_spacer, rc, "Failed Resource Actions");
         out->message(out, "failed-action", xml_op, show_opts);
     }
 
     PCMK__OUTPUT_LIST_FOOTER(out, rc);
     return rc;
 }
 
 static void
 status_node(pcmk_node_t *node, xmlNodePtr parent, uint32_t show_opts)
 {
     int health = pe__node_health(node);
 
     // Cluster membership
     if (node->details->online) {
         pcmk_create_html_node(parent, PCMK__XE_SPAN, NULL, PCMK_VALUE_ONLINE,
                               " online");
     } else {
         pcmk_create_html_node(parent, PCMK__XE_SPAN, NULL, PCMK_VALUE_OFFLINE,
                               " OFFLINE");
     }
 
     // Standby mode
     if (node->details->standby_onfail && (node->details->running_rsc != NULL)) {
         pcmk_create_html_node(parent, PCMK__XE_SPAN, NULL, PCMK_VALUE_STANDBY,
                               " (in standby due to on-fail,"
                               " with active resources)");
     } else if (node->details->standby_onfail) {
         pcmk_create_html_node(parent, PCMK__XE_SPAN, NULL, PCMK_VALUE_STANDBY,
                               " (in standby due to on-fail)");
     } else if (node->details->standby && (node->details->running_rsc != NULL)) {
         pcmk_create_html_node(parent, PCMK__XE_SPAN, NULL, PCMK_VALUE_STANDBY,
                               " (in standby, with active resources)");
     } else if (node->details->standby) {
         pcmk_create_html_node(parent, PCMK__XE_SPAN, NULL, PCMK_VALUE_STANDBY,
                               " (in standby)");
     }
 
     // Maintenance mode
     if (node->details->maintenance) {
         pcmk_create_html_node(parent, PCMK__XE_SPAN, NULL, PCMK__VALUE_MAINT,
                               " (in maintenance mode)");
     }
 
     // Node health
     if (health < 0) {
         pcmk_create_html_node(parent, PCMK__XE_SPAN, NULL,
                               PCMK__VALUE_HEALTH_RED, " (health is RED)");
     } else if (health == 0) {
         pcmk_create_html_node(parent, PCMK__XE_SPAN, NULL,
                               PCMK__VALUE_HEALTH_YELLOW, " (health is YELLOW)");
     }
 
     // Feature set
     if (pcmk_is_set(show_opts, pcmk_show_feature_set)) {
         const char *feature_set = get_node_feature_set(node);
         if (feature_set != NULL) {
             char *buf = crm_strdup_printf(", feature set %s", feature_set);
             pcmk_create_html_node(parent, PCMK__XE_SPAN, NULL, NULL, buf);
             free(buf);
         }
     }
 }
 
 PCMK__OUTPUT_ARGS("node", "pcmk_node_t *", "uint32_t", "bool",
                   "GList *", "GList *")
 static int
 node_html(pcmk__output_t *out, va_list args) {
     pcmk_node_t *node = va_arg(args, pcmk_node_t *);
     uint32_t show_opts = va_arg(args, uint32_t);
     bool full = va_arg(args, int);
     GList *only_node = va_arg(args, GList *);
     GList *only_rsc = va_arg(args, GList *);
 
     char *node_name = pe__node_display_name(node, pcmk_is_set(show_opts, pcmk_show_node_id));
 
     if (full) {
         xmlNodePtr item_node;
 
         if (pcmk_all_flags_set(show_opts, pcmk_show_brief | pcmk_show_rscs_by_node)) {
             GList *rscs = pe__filter_rsc_list(node->details->running_rsc, only_rsc);
 
             out->begin_list(out, NULL, NULL, "%s:", node_name);
             item_node = pcmk__output_xml_create_parent(out, "li", NULL);
             pcmk_create_html_node(item_node, PCMK__XE_SPAN, NULL, NULL,
                                   "Status:");
             status_node(node, item_node, show_opts);
 
             if (rscs != NULL) {
                 uint32_t new_show_opts = (show_opts | pcmk_show_rsc_only) & ~pcmk_show_inactive_rscs;
                 out->begin_list(out, NULL, NULL, "Resources");
                 pe__rscs_brief_output(out, rscs, new_show_opts);
                 out->end_list(out);
             }
 
             pcmk__output_xml_pop_parent(out);
             out->end_list(out);
 
         } else if (pcmk_is_set(show_opts, pcmk_show_rscs_by_node)) {
             GList *lpc2 = NULL;
             int rc = pcmk_rc_no_output;
 
             out->begin_list(out, NULL, NULL, "%s:", node_name);
             item_node = pcmk__output_xml_create_parent(out, "li", NULL);
             pcmk_create_html_node(item_node, PCMK__XE_SPAN, NULL, NULL,
                                   "Status:");
             status_node(node, item_node, show_opts);
 
             for (lpc2 = node->details->running_rsc; lpc2 != NULL; lpc2 = lpc2->next) {
                 pcmk_resource_t *rsc = (pcmk_resource_t *) lpc2->data;
                 PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Resources");
 
                 show_opts |= pcmk_show_rsc_only;
                 out->message(out, crm_map_element_name(rsc->xml), show_opts,
                              rsc, only_node, only_rsc);
             }
 
             PCMK__OUTPUT_LIST_FOOTER(out, rc);
             pcmk__output_xml_pop_parent(out);
             out->end_list(out);
 
         } else {
             char *buf = crm_strdup_printf("%s:", node_name);
 
             item_node = pcmk__output_create_xml_node(out, "li", NULL);
             pcmk_create_html_node(item_node, PCMK__XE_SPAN, NULL,
                                   PCMK__VALUE_BOLD, buf);
             status_node(node, item_node, show_opts);
 
             free(buf);
         }
     } else {
         out->begin_list(out, NULL, NULL, "%s:", node_name);
     }
 
     free(node_name);
     return pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Get a human-friendly textual description of a node's status
  *
  * \param[in] node  Node to check
  *
  * \return String representation of node's status
  */
 static const char *
 node_text_status(const pcmk_node_t *node)
 {
     if (node->details->unclean) {
         if (node->details->online) {
             return "UNCLEAN (online)";
 
         } else if (node->details->pending) {
             return "UNCLEAN (pending)";
 
         } else {
             return "UNCLEAN (offline)";
         }
 
     } else if (node->details->pending) {
         return "pending";
 
     } else if (node->details->standby_onfail && node->details->online) {
         return "standby (on-fail)";
 
     } else if (node->details->standby) {
         if (node->details->online) {
             if (node->details->running_rsc) {
                 return "standby (with active resources)";
             } else {
                 return "standby";
             }
         } else {
             return "OFFLINE (standby)";
         }
 
     } else if (node->details->maintenance) {
         if (node->details->online) {
             return "maintenance";
         } else {
             return "OFFLINE (maintenance)";
         }
 
     } else if (node->details->online) {
         return "online";
     }
 
     return "OFFLINE";
 }
 
 PCMK__OUTPUT_ARGS("node", "pcmk_node_t *", "uint32_t", "bool", "GList *",
                   "GList *")
 static int
 node_text(pcmk__output_t *out, va_list args) {
     pcmk_node_t *node = va_arg(args, pcmk_node_t *);
     uint32_t show_opts = va_arg(args, uint32_t);
     bool full = va_arg(args, int);
     GList *only_node = va_arg(args, GList *);
     GList *only_rsc = va_arg(args, GList *);
 
     if (full) {
         char *node_name = pe__node_display_name(node, pcmk_is_set(show_opts, pcmk_show_node_id));
         GString *str = g_string_sized_new(64);
         int health = pe__node_health(node);
 
         // Create a summary line with node type, name, and status
         if (pcmk__is_guest_or_bundle_node(node)) {
             g_string_append(str, "GuestNode");
         } else if (pcmk__is_remote_node(node)) {
             g_string_append(str, "RemoteNode");
         } else {
             g_string_append(str, "Node");
         }
         pcmk__g_strcat(str, " ", node_name, ": ", node_text_status(node), NULL);
 
         if (health < 0) {
             g_string_append(str, " (health is RED)");
         } else if (health == 0) {
             g_string_append(str, " (health is YELLOW)");
         }
         if (pcmk_is_set(show_opts, pcmk_show_feature_set)) {
             const char *feature_set = get_node_feature_set(node);
             if (feature_set != NULL) {
                 pcmk__g_strcat(str, ", feature set ", feature_set, NULL);
             }
         }
 
         /* If we're grouping by node, print its resources */
         if (pcmk_is_set(show_opts, pcmk_show_rscs_by_node)) {
             if (pcmk_is_set(show_opts, pcmk_show_brief)) {
                 GList *rscs = pe__filter_rsc_list(node->details->running_rsc, only_rsc);
 
                 if (rscs != NULL) {
                     uint32_t new_show_opts = (show_opts | pcmk_show_rsc_only) & ~pcmk_show_inactive_rscs;
                     out->begin_list(out, NULL, NULL, "%s", str->str);
                     out->begin_list(out, NULL, NULL, "Resources");
 
                     pe__rscs_brief_output(out, rscs, new_show_opts);
 
                     out->end_list(out);
                     out->end_list(out);
 
                     g_list_free(rscs);
                 }
 
             } else {
                 GList *gIter2 = NULL;
 
                 out->begin_list(out, NULL, NULL, "%s", str->str);
                 out->begin_list(out, NULL, NULL, "Resources");
 
                 for (gIter2 = node->details->running_rsc; gIter2 != NULL; gIter2 = gIter2->next) {
                     pcmk_resource_t *rsc = (pcmk_resource_t *) gIter2->data;
 
                     show_opts |= pcmk_show_rsc_only;
                     out->message(out, crm_map_element_name(rsc->xml), show_opts,
                                  rsc, only_node, only_rsc);
                 }
 
                 out->end_list(out);
                 out->end_list(out);
             }
         } else {
             out->list_item(out, NULL, "%s", str->str);
         }
 
         g_string_free(str, TRUE);
         free(node_name);
     } else {
         char *node_name = pe__node_display_name(node, pcmk_is_set(show_opts, pcmk_show_node_id));
         out->begin_list(out, NULL, NULL, "Node: %s", node_name);
         free(node_name);
     }
 
     return pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Convert an integer health value to a string representation
  *
  * \param[in] health  Integer health value
  *
  * \retval \c PCMK_VALUE_RED if \p health is less than 0
  * \retval \c PCMK_VALUE_YELLOW if \p health is equal to 0
  * \retval \c PCMK_VALUE_GREEN if \p health is greater than 0
  */
 static const char *
 health_text(int health)
 {
     if (health < 0) {
         return PCMK_VALUE_RED;
     } else if (health == 0) {
         return PCMK_VALUE_YELLOW;
     } else {
         return PCMK_VALUE_GREEN;
     }
 }
 
 /*!
  * \internal
  * \brief Convert a node type to a string representation
  *
  * \param[in] type  Node type
  *
  * \retval \c PCMK_VALUE_MEMBER if \p node_type is \c pcmk_node_variant_cluster
  * \retval \c PCMK_VALUE_REMOTE if \p node_type is \c pcmk_node_variant_remote
  * \retval \c PCMK__VALUE_PING if \p node_type is \c node_ping
  * \retval \c PCMK_VALUE_UNKNOWN otherwise
  */
 static const char *
 node_type_str(enum node_type type)
 {
     switch (type) {
         case pcmk_node_variant_cluster:
             return PCMK_VALUE_MEMBER;
         case pcmk_node_variant_remote:
             return PCMK_VALUE_REMOTE;
         case node_ping:
             return PCMK__VALUE_PING;
         default:
             return PCMK_VALUE_UNKNOWN;
     }
 }
 
 PCMK__OUTPUT_ARGS("node", "pcmk_node_t *", "uint32_t", "bool", "GList *",
                   "GList *")
 static int
 node_xml(pcmk__output_t *out, va_list args) {
     pcmk_node_t *node = va_arg(args, pcmk_node_t *);
     uint32_t show_opts G_GNUC_UNUSED = va_arg(args, uint32_t);
     bool full = va_arg(args, int);
     GList *only_node = va_arg(args, GList *);
     GList *only_rsc = va_arg(args, GList *);
 
     if (full) {
         const char *online = pcmk__btoa(node->details->online);
         const char *standby = pcmk__btoa(node->details->standby);
         const char *standby_onfail = pcmk__btoa(node->details->standby_onfail);
         const char *maintenance = pcmk__btoa(node->details->maintenance);
         const char *pending = pcmk__btoa(node->details->pending);
         const char *unclean = pcmk__btoa(node->details->unclean);
         const char *health = health_text(pe__node_health(node));
         const char *feature_set = get_node_feature_set(node);
         const char *shutdown = pcmk__btoa(node->details->shutdown);
         const char *expected_up = pcmk__btoa(node->details->expected_up);
         const char *is_dc = pcmk__btoa(node->details->is_dc);
         int length = g_list_length(node->details->running_rsc);
         char *resources_running = pcmk__itoa(length);
         const char *node_type = node_type_str(node->details->type);
 
         pe__name_and_nvpairs_xml(out, true, PCMK_XE_NODE, 15,
                                  PCMK_XA_NAME, node->details->uname,
                                  PCMK_XA_ID, node->details->id,
                                  PCMK_XA_ONLINE, online,
                                  PCMK_XA_STANDBY, standby,
                                  PCMK_XA_STANDBY_ONFAIL, standby_onfail,
                                  PCMK_XA_MAINTENANCE, maintenance,
                                  PCMK_XA_PENDING, pending,
                                  PCMK_XA_UNCLEAN, unclean,
                                  PCMK_XA_HEALTH, health,
                                  PCMK_XA_FEATURE_SET, feature_set,
                                  PCMK_XA_SHUTDOWN, shutdown,
                                  PCMK_XA_EXPECTED_UP, expected_up,
                                  PCMK_XA_IS_DC, is_dc,
                                  PCMK_XA_RESOURCES_RUNNING, resources_running,
                                  PCMK_XA_TYPE, node_type);
 
         if (pcmk__is_guest_or_bundle_node(node)) {
             xmlNodePtr xml_node = pcmk__output_xml_peek_parent(out);
             crm_xml_add(xml_node, PCMK_XA_ID_AS_RESOURCE,
                         node->details->remote_rsc->container->id);
         }
 
         if (pcmk_is_set(show_opts, pcmk_show_rscs_by_node)) {
             GList *lpc = NULL;
 
             for (lpc = node->details->running_rsc; lpc != NULL; lpc = lpc->next) {
                 pcmk_resource_t *rsc = (pcmk_resource_t *) lpc->data;
 
                 show_opts |= pcmk_show_rsc_only;
                 out->message(out, crm_map_element_name(rsc->xml), show_opts,
                              rsc, only_node, only_rsc);
             }
         }
 
         free(resources_running);
 
         out->end_list(out);
     } else {
         pcmk__output_xml_create_parent(out, PCMK_XE_NODE,
                                        PCMK_XA_NAME, node->details->uname,
                                        NULL);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("node-attribute", "const char *", "const char *", "bool", "int")
 static int
 node_attribute_text(pcmk__output_t *out, va_list args) {
     const char *name = va_arg(args, const char *);
     const char *value = va_arg(args, const char *);
     bool add_extra = va_arg(args, int);
     int expected_score = va_arg(args, int);
 
     if (add_extra) {
         int v;
 
         if (value == NULL) {
             v = 0;
         } else {
             pcmk__scan_min_int(value, &v, INT_MIN);
         }
         if (v <= 0) {
             out->list_item(out, NULL, "%-32s\t: %-10s\t: Connectivity is lost", name, value);
         } else if (v < expected_score) {
             out->list_item(out, NULL, "%-32s\t: %-10s\t: Connectivity is degraded (Expected=%d)", name, value, expected_score);
         } else {
             out->list_item(out, NULL, "%-32s\t: %-10s", name, value);
         }
     } else {
         out->list_item(out, NULL, "%-32s\t: %-10s", name, value);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("node-attribute", "const char *", "const char *", "bool", "int")
 static int
 node_attribute_html(pcmk__output_t *out, va_list args) {
     const char *name = va_arg(args, const char *);
     const char *value = va_arg(args, const char *);
     bool add_extra = va_arg(args, int);
     int expected_score = va_arg(args, int);
 
     if (add_extra) {
         int v;
         char *s = crm_strdup_printf("%s: %s", name, value);
         xmlNodePtr item_node = pcmk__output_create_xml_node(out, "li", NULL);
 
         if (value == NULL) {
             v = 0;
         } else {
             pcmk__scan_min_int(value, &v, INT_MIN);
         }
 
         pcmk_create_html_node(item_node, PCMK__XE_SPAN, NULL, NULL, s);
         free(s);
 
         if (v <= 0) {
             pcmk_create_html_node(item_node, PCMK__XE_SPAN, NULL,
                                   PCMK__VALUE_BOLD, "(connectivity is lost)");
         } else if (v < expected_score) {
             char *buf = crm_strdup_printf("(connectivity is degraded -- expected %d", expected_score);
             pcmk_create_html_node(item_node, PCMK__XE_SPAN, NULL,
                                   PCMK__VALUE_BOLD, buf);
             free(buf);
         }
     } else {
         out->list_item(out, NULL, "%s: %s", name, value);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("node-and-op", "pcmk_scheduler_t *", "xmlNode *")
 static int
 node_and_op(pcmk__output_t *out, va_list args) {
     pcmk_scheduler_t *scheduler = va_arg(args, pcmk_scheduler_t *);
     xmlNodePtr xml_op = va_arg(args, xmlNodePtr);
 
     pcmk_resource_t *rsc = NULL;
     gchar *node_str = NULL;
     char *last_change_str = NULL;
 
     const char *op_rsc = crm_element_value(xml_op, PCMK_XA_RESOURCE);
     int status;
     time_t last_change = 0;
 
     pcmk__scan_min_int(crm_element_value(xml_op, PCMK__XA_OP_STATUS), &status,
                        PCMK_EXEC_UNKNOWN);
 
     rsc = pe_find_resource(scheduler->resources, op_rsc);
 
     if (rsc) {
         const pcmk_node_t *node = pcmk__current_node(rsc);
         const char *target_role = g_hash_table_lookup(rsc->meta,
                                                       PCMK_META_TARGET_ROLE);
         uint32_t show_opts = pcmk_show_rsc_only | pcmk_show_pending;
 
         if (node == NULL) {
             node = rsc->pending_node;
         }
 
         node_str = pcmk__native_output_string(rsc, rsc_printable_id(rsc), node,
                                               show_opts, target_role, false);
     } else {
         node_str = crm_strdup_printf("Unknown resource %s", op_rsc);
     }
 
     if (crm_element_value_epoch(xml_op, PCMK_XA_LAST_RC_CHANGE,
                                 &last_change) == pcmk_ok) {
         const char *exec_time = crm_element_value(xml_op, PCMK_XA_EXEC_TIME);
 
         last_change_str = crm_strdup_printf(", %s='%s', exec=%sms",
                                             PCMK_XA_LAST_RC_CHANGE,
                                             pcmk__trim(ctime(&last_change)),
                                             exec_time);
     }
 
     out->list_item(out, NULL, "%s: %s (node=%s, call=%s, rc=%s%s): %s",
                    node_str, pcmk__xe_history_key(xml_op),
                    crm_element_value(xml_op, PCMK_XA_UNAME),
                    crm_element_value(xml_op, PCMK__XA_CALL_ID),
                    crm_element_value(xml_op, PCMK__XA_RC_CODE),
                    last_change_str ? last_change_str : "",
                    pcmk_exec_status_str(status));
 
     g_free(node_str);
     free(last_change_str);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("node-and-op", "pcmk_scheduler_t *", "xmlNode *")
 static int
 node_and_op_xml(pcmk__output_t *out, va_list args) {
     pcmk_scheduler_t *scheduler = va_arg(args, pcmk_scheduler_t *);
     xmlNodePtr xml_op = va_arg(args, xmlNodePtr);
 
     pcmk_resource_t *rsc = NULL;
     const char *uname = crm_element_value(xml_op, PCMK_XA_UNAME);
     const char *call_id = crm_element_value(xml_op, PCMK__XA_CALL_ID);
     const char *rc_s = crm_element_value(xml_op, PCMK__XA_RC_CODE);
     const char *status_s = NULL;
     const char *op_rsc = crm_element_value(xml_op, PCMK_XA_RESOURCE);
     int status;
     time_t last_change = 0;
     xmlNode *node = NULL;
 
     pcmk__scan_min_int(crm_element_value(xml_op, PCMK__XA_OP_STATUS),
                        &status, PCMK_EXEC_UNKNOWN);
     status_s = pcmk_exec_status_str(status);
 
     node = pcmk__output_create_xml_node(out, PCMK_XE_OPERATION,
                                         PCMK_XA_OP, pcmk__xe_history_key(xml_op),
                                         PCMK_XA_NODE, uname,
                                         PCMK_XA_CALL, call_id,
                                         PCMK_XA_RC, rc_s,
                                         PCMK_XA_STATUS, status_s,
                                         NULL);
 
     rsc = pe_find_resource(scheduler->resources, op_rsc);
 
     if (rsc) {
         const char *class = crm_element_value(rsc->xml, PCMK_XA_CLASS);
         const char *provider = crm_element_value(rsc->xml, PCMK_XA_PROVIDER);
         const char *kind = crm_element_value(rsc->xml, PCMK_XA_TYPE);
         bool has_provider = pcmk_is_set(pcmk_get_ra_caps(class),
                                         pcmk_ra_cap_provider);
 
         char *agent_tuple = crm_strdup_printf("%s:%s:%s",
                                               class,
                                               (has_provider? provider : ""),
                                               kind);
 
         pcmk__xe_set_props(node,
                            PCMK_XA_RSC, rsc_printable_id(rsc),
                            PCMK_XA_AGENT, agent_tuple,
                            NULL);
         free(agent_tuple);
     }
 
     if (crm_element_value_epoch(xml_op, PCMK_XA_LAST_RC_CHANGE,
                                 &last_change) == pcmk_ok) {
         const char *last_rc_change = pcmk__trim(ctime(&last_change));
         const char *exec_time = crm_element_value(xml_op, PCMK_XA_EXEC_TIME);
 
         pcmk__xe_set_props(node,
                            PCMK_XA_LAST_RC_CHANGE, last_rc_change,
                            PCMK_XA_EXEC_TIME, exec_time,
                            NULL);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("node-attribute", "const char *", "const char *", "bool", "int")
 static int
 node_attribute_xml(pcmk__output_t *out, va_list args) {
     const char *name = va_arg(args, const char *);
     const char *value = va_arg(args, const char *);
     bool add_extra = va_arg(args, int);
     int expected_score = va_arg(args, int);
 
     xmlNodePtr node = pcmk__output_create_xml_node(out, PCMK_XE_ATTRIBUTE,
                                                    PCMK_XA_NAME, name,
                                                    PCMK_XA_VALUE, value,
                                                    NULL);
 
     if (add_extra) {
         char *buf = pcmk__itoa(expected_score);
         crm_xml_add(node, PCMK_XA_EXPECTED, buf);
         free(buf);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("node-attribute-list", "pcmk_scheduler_t *", "uint32_t",
                   "bool", "GList *", "GList *")
 static int
 node_attribute_list(pcmk__output_t *out, va_list args) {
     pcmk_scheduler_t *scheduler = va_arg(args, pcmk_scheduler_t *);
     uint32_t show_opts = va_arg(args, uint32_t);
     bool print_spacer = va_arg(args, int);
     GList *only_node = va_arg(args, GList *);
     GList *only_rsc = va_arg(args, GList *);
 
     int rc = pcmk_rc_no_output;
 
     /* Display each node's attributes */
     for (GList *gIter = scheduler->nodes; gIter != NULL; gIter = gIter->next) {
         pcmk_node_t *node = gIter->data;
 
         GList *attr_list = NULL;
         GHashTableIter iter;
         gpointer key;
 
         if (!node || !node->details || !node->details->online) {
             continue;
         }
 
         g_hash_table_iter_init(&iter, node->details->attrs);
         while (g_hash_table_iter_next (&iter, &key, NULL)) {
             attr_list = filter_attr_list(attr_list, key);
         }
 
         if (attr_list == NULL) {
             continue;
         }
 
         if (!pcmk__str_in_list(node->details->uname, only_node, pcmk__str_star_matches|pcmk__str_casei)) {
             g_list_free(attr_list);
             continue;
         }
 
         PCMK__OUTPUT_LIST_HEADER(out, print_spacer, rc, "Node Attributes");
 
         out->message(out, "node", node, show_opts, false, only_node, only_rsc);
 
         for (GList *aIter = attr_list; aIter != NULL; aIter = aIter->next) {
             const char *name = aIter->data;
             const char *value = NULL;
             int expected_score = 0;
             bool add_extra = false;
 
             value = pcmk__node_attr(node, name, NULL, pcmk__rsc_node_current);
 
             add_extra = add_extra_info(node, node->details->running_rsc,
                                        scheduler, name, &expected_score);
 
             /* Print attribute name and value */
             out->message(out, "node-attribute", name, value, add_extra,
                          expected_score);
         }
 
         g_list_free(attr_list);
         out->end_list(out);
     }
 
     PCMK__OUTPUT_LIST_FOOTER(out, rc);
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("node-capacity", "const pcmk_node_t *", "const char *")
 static int
 node_capacity(pcmk__output_t *out, va_list args)
 {
     const pcmk_node_t *node = va_arg(args, pcmk_node_t *);
     const char *comment = va_arg(args, const char *);
 
     char *dump_text = crm_strdup_printf("%s: %s capacity:",
                                         comment, pcmk__node_name(node));
 
     g_hash_table_foreach(node->details->utilization, append_dump_text, &dump_text);
     out->list_item(out, NULL, "%s", dump_text);
     free(dump_text);
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("node-capacity", "const pcmk_node_t *", "const char *")
 static int
 node_capacity_xml(pcmk__output_t *out, va_list args)
 {
     const pcmk_node_t *node = va_arg(args, pcmk_node_t *);
     const char *uname = node->details->uname;
     const char *comment = va_arg(args, const char *);
 
     xmlNodePtr xml_node = pcmk__output_create_xml_node(out, PCMK_XE_CAPACITY,
                                                        PCMK_XA_NODE, uname,
                                                        PCMK_XA_COMMENT, comment,
                                                        NULL);
     g_hash_table_foreach(node->details->utilization, add_dump_node, xml_node);
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("node-history-list", "pcmk_scheduler_t *", "pcmk_node_t *",
                   "xmlNode *", "GList *", "GList *", "uint32_t", "uint32_t")
 static int
 node_history_list(pcmk__output_t *out, va_list args) {
     pcmk_scheduler_t *scheduler = va_arg(args, pcmk_scheduler_t *);
     pcmk_node_t *node = va_arg(args, pcmk_node_t *);
     xmlNode *node_state = va_arg(args, xmlNode *);
     GList *only_node = va_arg(args, GList *);
     GList *only_rsc = va_arg(args, GList *);
     uint32_t section_opts = va_arg(args, uint32_t);
     uint32_t show_opts = va_arg(args, uint32_t);
 
     xmlNode *lrm_rsc = NULL;
     xmlNode *rsc_entry = NULL;
     int rc = pcmk_rc_no_output;
 
     lrm_rsc = find_xml_node(node_state, PCMK__XE_LRM, FALSE);
     lrm_rsc = find_xml_node(lrm_rsc, PCMK__XE_LRM_RESOURCES, FALSE);
 
     /* Print history of each of the node's resources */
     for (rsc_entry = first_named_child(lrm_rsc, PCMK__XE_LRM_RESOURCE);
          rsc_entry != NULL; rsc_entry = crm_next_same_xml(rsc_entry)) {
         const char *rsc_id = crm_element_value(rsc_entry, PCMK_XA_ID);
         pcmk_resource_t *rsc = pe_find_resource(scheduler->resources, rsc_id);
         const pcmk_resource_t *parent = pe__const_top_resource(rsc, false);
 
         /* We can't use is_filtered here to filter group resources.  For is_filtered,
          * we have to decide whether to check the parent or not.  If we check the
          * parent, all elements of a group will always be printed because that's how
          * is_filtered works for groups.  If we do not check the parent, sometimes
          * this will filter everything out.
          *
          * For other resource types, is_filtered is okay.
          */
         if (parent->variant == pcmk_rsc_variant_group) {
             if (!pcmk__str_in_list(rsc_printable_id(rsc), only_rsc,
                                    pcmk__str_star_matches)
                 && !pcmk__str_in_list(rsc_printable_id(parent), only_rsc,
                                       pcmk__str_star_matches)) {
                 continue;
             }
         } else {
             if (rsc->fns->is_filtered(rsc, only_rsc, TRUE)) {
                 continue;
             }
         }
 
         if (!pcmk_is_set(section_opts, pcmk_section_operations)) {
             time_t last_failure = 0;
             int failcount = pe_get_failcount(node, rsc, &last_failure,
                                              pcmk__fc_default, NULL);
 
             if (failcount <= 0) {
                 continue;
             }
 
             if (rc == pcmk_rc_no_output) {
                 rc = pcmk_rc_ok;
                 out->message(out, "node", node, show_opts, false, only_node,
                              only_rsc);
             }
 
             out->message(out, "resource-history", rsc, rsc_id, false,
                          failcount, last_failure, false);
         } else {
             GList *op_list = get_operation_list(rsc_entry);
             pcmk_resource_t *rsc = NULL;
 
             if (op_list == NULL) {
                 continue;
             }
 
             rsc = pe_find_resource(scheduler->resources,
                                    crm_element_value(rsc_entry, PCMK_XA_ID));
 
             if (rc == pcmk_rc_no_output) {
                 rc = pcmk_rc_ok;
                 out->message(out, "node", node, show_opts, false, only_node,
                              only_rsc);
             }
 
             out->message(out, "resource-operation-list", scheduler, rsc, node,
                          op_list, show_opts);
         }
     }
 
     PCMK__OUTPUT_LIST_FOOTER(out, rc);
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("node-list", "GList *", "GList *", "GList *", "uint32_t", "bool")
 static int
 node_list_html(pcmk__output_t *out, va_list args) {
     GList *nodes = va_arg(args, GList *);
     GList *only_node = va_arg(args, GList *);
     GList *only_rsc = va_arg(args, GList *);
     uint32_t show_opts = va_arg(args, uint32_t);
     bool print_spacer G_GNUC_UNUSED = va_arg(args, int);
 
     int rc = pcmk_rc_no_output;
 
     for (GList *gIter = nodes; gIter != NULL; gIter = gIter->next) {
         pcmk_node_t *node = (pcmk_node_t *) gIter->data;
 
         if (!pcmk__str_in_list(node->details->uname, only_node,
                                pcmk__str_star_matches|pcmk__str_casei)) {
             continue;
         }
 
         PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Node List");
 
         out->message(out, "node", node, show_opts, true, only_node, only_rsc);
     }
 
     PCMK__OUTPUT_LIST_FOOTER(out, rc);
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("node-list", "GList *", "GList *", "GList *", "uint32_t", "bool")
 static int
 node_list_text(pcmk__output_t *out, va_list args) {
     GList *nodes = va_arg(args, GList *);
     GList *only_node = va_arg(args, GList *);
     GList *only_rsc = va_arg(args, GList *);
     uint32_t show_opts = va_arg(args, uint32_t);
     bool print_spacer = va_arg(args, int);
 
     /* space-separated lists of node names */
     GString *online_nodes = NULL;
     GString *online_remote_nodes = NULL;
     GString *online_guest_nodes = NULL;
     GString *offline_nodes = NULL;
     GString *offline_remote_nodes = NULL;
 
     int rc = pcmk_rc_no_output;
 
     for (GList *gIter = nodes; gIter != NULL; gIter = gIter->next) {
         pcmk_node_t *node = (pcmk_node_t *) gIter->data;
         char *node_name = pe__node_display_name(node, pcmk_is_set(show_opts, pcmk_show_node_id));
 
         if (!pcmk__str_in_list(node->details->uname, only_node,
                                pcmk__str_star_matches|pcmk__str_casei)) {
             free(node_name);
             continue;
         }
 
         PCMK__OUTPUT_LIST_HEADER(out, print_spacer, rc, "Node List");
 
         // Determine whether to display node individually or in a list
         if (node->details->unclean || node->details->pending
             || (node->details->standby_onfail && node->details->online)
             || node->details->standby || node->details->maintenance
             || pcmk_is_set(show_opts, pcmk_show_rscs_by_node)
             || pcmk_is_set(show_opts, pcmk_show_feature_set)
             || (pe__node_health(node) <= 0)) {
             // Display node individually
 
         } else if (node->details->online) {
             // Display online node in a list
             if (pcmk__is_guest_or_bundle_node(node)) {
                 pcmk__add_word(&online_guest_nodes, 1024, node_name);
 
             } else if (pcmk__is_remote_node(node)) {
                 pcmk__add_word(&online_remote_nodes, 1024, node_name);
 
             } else {
                 pcmk__add_word(&online_nodes, 1024, node_name);
             }
             free(node_name);
             continue;
 
         } else {
             // Display offline node in a list
             if (pcmk__is_remote_node(node)) {
                 pcmk__add_word(&offline_remote_nodes, 1024, node_name);
 
             } else if (pcmk__is_guest_or_bundle_node(node)) {
                 /* ignore offline guest nodes */
 
             } else {
                 pcmk__add_word(&offline_nodes, 1024, node_name);
             }
             free(node_name);
             continue;
         }
 
         /* If we get here, node is in bad state, or we're grouping by node */
         out->message(out, "node", node, show_opts, true, only_node, only_rsc);
         free(node_name);
     }
 
     /* If we're not grouping by node, summarize nodes by status */
     if (online_nodes != NULL) {
         out->list_item(out, "Online", "[ %s ]",
                        (const char *) online_nodes->str);
         g_string_free(online_nodes, TRUE);
     }
     if (offline_nodes != NULL) {
         out->list_item(out, "OFFLINE", "[ %s ]",
                        (const char *) offline_nodes->str);
         g_string_free(offline_nodes, TRUE);
     }
     if (online_remote_nodes) {
         out->list_item(out, "RemoteOnline", "[ %s ]",
                        (const char *) online_remote_nodes->str);
         g_string_free(online_remote_nodes, TRUE);
     }
     if (offline_remote_nodes) {
         out->list_item(out, "RemoteOFFLINE", "[ %s ]",
                        (const char *) offline_remote_nodes->str);
         g_string_free(offline_remote_nodes, TRUE);
     }
     if (online_guest_nodes != NULL) {
         out->list_item(out, "GuestOnline", "[ %s ]",
                        (const char *) online_guest_nodes->str);
         g_string_free(online_guest_nodes, TRUE);
     }
 
     PCMK__OUTPUT_LIST_FOOTER(out, rc);
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("node-list", "GList *", "GList *", "GList *", "uint32_t", "bool")
 static int
 node_list_xml(pcmk__output_t *out, va_list args) {
     GList *nodes = va_arg(args, GList *);
     GList *only_node = va_arg(args, GList *);
     GList *only_rsc = va_arg(args, GList *);
     uint32_t show_opts = va_arg(args, uint32_t);
     bool print_spacer G_GNUC_UNUSED = va_arg(args, int);
 
     /* PCMK_XE_NODES acts as the list's element name for CLI tools that use
      * pcmk__output_enable_list_element.  Otherwise PCMK_XE_NODES is the
      * value of the list's PCMK_XA_NAME attribute.
      */
     out->begin_list(out, NULL, NULL, PCMK_XE_NODES);
     for (GList *gIter = nodes; gIter != NULL; gIter = gIter->next) {
         pcmk_node_t *node = (pcmk_node_t *) gIter->data;
 
         if (!pcmk__str_in_list(node->details->uname, only_node,
                                pcmk__str_star_matches|pcmk__str_casei)) {
             continue;
         }
 
         out->message(out, "node", node, show_opts, true, only_node, only_rsc);
     }
     out->end_list(out);
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("node-summary", "pcmk_scheduler_t *", "GList *", "GList *",
                   "uint32_t", "uint32_t", "bool")
 static int
 node_summary(pcmk__output_t *out, va_list args) {
     pcmk_scheduler_t *scheduler = va_arg(args, pcmk_scheduler_t *);
     GList *only_node = va_arg(args, GList *);
     GList *only_rsc = va_arg(args, GList *);
     uint32_t section_opts = va_arg(args, uint32_t);
     uint32_t show_opts = va_arg(args, uint32_t);
     bool print_spacer = va_arg(args, int);
 
     xmlNode *node_state = NULL;
     xmlNode *cib_status = pcmk_find_cib_element(scheduler->input,
                                                 PCMK_XE_STATUS);
     int rc = pcmk_rc_no_output;
 
     if (xmlChildElementCount(cib_status) == 0) {
         return rc;
     }
 
     for (node_state = first_named_child(cib_status, PCMK__XE_NODE_STATE);
          node_state != NULL; node_state = crm_next_same_xml(node_state)) {
 
         pcmk_node_t *node = pe_find_node_id(scheduler->nodes,
                                             pcmk__xe_id(node_state));
 
         if (!node || !node->details || !node->details->online) {
             continue;
         }
 
         if (!pcmk__str_in_list(node->details->uname, only_node,
                                pcmk__str_star_matches|pcmk__str_casei)) {
             continue;
         }
 
         PCMK__OUTPUT_LIST_HEADER(out, print_spacer, rc,
                                  pcmk_is_set(section_opts, pcmk_section_operations) ? "Operations" : "Migration Summary");
 
         out->message(out, "node-history-list", scheduler, node, node_state,
                      only_node, only_rsc, section_opts, show_opts);
     }
 
     PCMK__OUTPUT_LIST_FOOTER(out, rc);
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("node-weight", "const pcmk_resource_t *", "const char *",
                   "const char *", "const char *")
 static int
 node_weight(pcmk__output_t *out, va_list args)
 {
     const pcmk_resource_t *rsc = va_arg(args, const pcmk_resource_t *);
     const char *prefix = va_arg(args, const char *);
     const char *uname = va_arg(args, const char *);
     const char *score = va_arg(args, const char *);
 
     if (rsc) {
         out->list_item(out, NULL, "%s: %s allocation score on %s: %s",
                        prefix, rsc->id, uname, score);
     } else {
         out->list_item(out, NULL, "%s: %s = %s", prefix, uname, score);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("node-weight", "const pcmk_resource_t *", "const char *",
                   "const char *", "const char *")
 static int
 node_weight_xml(pcmk__output_t *out, va_list args)
 {
     const pcmk_resource_t *rsc = va_arg(args, const pcmk_resource_t *);
     const char *prefix = va_arg(args, const char *);
     const char *uname = va_arg(args, const char *);
     const char *score = va_arg(args, const char *);
 
     xmlNodePtr node = pcmk__output_create_xml_node(out, PCMK_XE_NODE_WEIGHT,
                                                    PCMK_XA_FUNCTION, prefix,
                                                    PCMK_XA_NODE, uname,
                                                    PCMK_XA_SCORE, score,
                                                    NULL);
 
     if (rsc) {
         crm_xml_add(node, PCMK_XA_ID, rsc->id);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("op-history", "xmlNode *", "const char *", "const char *", "int", "uint32_t")
 static int
 op_history_text(pcmk__output_t *out, va_list args) {
     xmlNodePtr xml_op = va_arg(args, xmlNodePtr);
     const char *task = va_arg(args, const char *);
     const char *interval_ms_s = va_arg(args, const char *);
     int rc = va_arg(args, int);
     uint32_t show_opts = va_arg(args, uint32_t);
 
     char *buf = op_history_string(xml_op, task, interval_ms_s, rc,
                                   pcmk_is_set(show_opts, pcmk_show_timing));
 
     out->list_item(out, NULL, "%s", buf);
 
     free(buf);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("op-history", "xmlNode *", "const char *", "const char *", "int", "uint32_t")
 static int
 op_history_xml(pcmk__output_t *out, va_list args) {
     xmlNodePtr xml_op = va_arg(args, xmlNodePtr);
     const char *task = va_arg(args, const char *);
     const char *interval_ms_s = va_arg(args, const char *);
     int rc = va_arg(args, int);
     uint32_t show_opts = va_arg(args, uint32_t);
 
     const char *call_id = crm_element_value(xml_op, PCMK__XA_CALL_ID);
     char *rc_s = pcmk__itoa(rc);
     const char *rc_text = services_ocf_exitcode_str(rc);
     xmlNodePtr node = NULL;
 
     node = pcmk__output_create_xml_node(out, PCMK_XE_OPERATION_HISTORY,
                                         PCMK_XA_CALL, call_id,
                                         PCMK_XA_TASK, task,
                                         PCMK_XA_RC, rc_s,
                                         PCMK_XA_RC_TEXT, rc_text,
                                         NULL);
     free(rc_s);
 
     if (interval_ms_s && !pcmk__str_eq(interval_ms_s, "0", pcmk__str_casei)) {
         char *s = crm_strdup_printf("%sms", interval_ms_s);
         crm_xml_add(node, PCMK_XA_INTERVAL, s);
         free(s);
     }
 
     if (pcmk_is_set(show_opts, pcmk_show_timing)) {
         const char *value = NULL;
         time_t epoch = 0;
 
         if ((crm_element_value_epoch(xml_op, PCMK_XA_LAST_RC_CHANGE,
                                      &epoch) == pcmk_ok) && (epoch > 0)) {
             char *s = pcmk__epoch2str(&epoch, 0);
             crm_xml_add(node, PCMK_XA_LAST_RC_CHANGE, s);
             free(s);
         }
 
         value = crm_element_value(xml_op, PCMK_XA_EXEC_TIME);
         if (value) {
             char *s = crm_strdup_printf("%sms", value);
             crm_xml_add(node, PCMK_XA_EXEC_TIME, s);
             free(s);
         }
         value = crm_element_value(xml_op, PCMK_XA_QUEUE_TIME);
         if (value) {
             char *s = crm_strdup_printf("%sms", value);
             crm_xml_add(node, PCMK_XA_QUEUE_TIME, s);
             free(s);
         }
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("promotion-score", "pcmk_resource_t *", "pcmk_node_t *",
                   "const char *")
 static int
 promotion_score(pcmk__output_t *out, va_list args)
 {
     pcmk_resource_t *child_rsc = va_arg(args, pcmk_resource_t *);
     pcmk_node_t *chosen = va_arg(args, pcmk_node_t *);
     const char *score = va_arg(args, const char *);
 
     out->list_item(out, NULL, "%s promotion score on %s: %s",
                    child_rsc->id,
                    chosen? chosen->details->uname : "none",
                    score);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("promotion-score", "pcmk_resource_t *", "pcmk_node_t *",
                   "const char *")
 static int
 promotion_score_xml(pcmk__output_t *out, va_list args)
 {
     pcmk_resource_t *child_rsc = va_arg(args, pcmk_resource_t *);
     pcmk_node_t *chosen = va_arg(args, pcmk_node_t *);
     const char *score = va_arg(args, const char *);
 
     xmlNodePtr node = pcmk__output_create_xml_node(out, PCMK_XE_PROMOTION_SCORE,
                                                    PCMK_XA_ID, child_rsc->id,
                                                    PCMK_XA_SCORE, score,
                                                    NULL);
 
     if (chosen) {
         crm_xml_add(node, PCMK_XA_NODE, chosen->details->uname);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("resource-config", "const pcmk_resource_t *", "bool")
 static int
 resource_config(pcmk__output_t *out, va_list args) {
     const pcmk_resource_t *rsc = va_arg(args, const pcmk_resource_t *);
+    GString *xml_buf = g_string_sized_new(1024);
     bool raw = va_arg(args, int);
 
-    gchar *rsc_xml = formatted_xml_buf(rsc, raw);
+    formatted_xml_buf(rsc, xml_buf, raw);
 
-    out->output_xml(out, PCMK_XE_XML, rsc_xml);
+    out->output_xml(out, PCMK_XE_XML, xml_buf->str);
 
-    g_free(rsc_xml);
+    g_string_free(xml_buf, TRUE);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("resource-config", "const pcmk_resource_t *", "bool")
 static int
 resource_config_text(pcmk__output_t *out, va_list args) {
-    const pcmk_resource_t *rsc = va_arg(args, const pcmk_resource_t *);
-    bool raw = va_arg(args, int);
-
-    gchar *rsc_xml = formatted_xml_buf(rsc, raw);
-
     pcmk__formatted_printf(out, "Resource XML:\n");
-    out->output_xml(out, PCMK_XE_XML, rsc_xml);
-
-    g_free(rsc_xml);
-    return pcmk_rc_ok;
+    return resource_config(out, args);
 }
 
 PCMK__OUTPUT_ARGS("resource-history", "pcmk_resource_t *", "const char *",
                   "bool", "int", "time_t", "bool")
 static int
 resource_history_text(pcmk__output_t *out, va_list args) {
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     const char *rsc_id = va_arg(args, const char *);
     bool all = va_arg(args, int);
     int failcount = va_arg(args, int);
     time_t last_failure = va_arg(args, time_t);
     bool as_header = va_arg(args, int);
 
     char *buf = resource_history_string(rsc, rsc_id, all, failcount, last_failure);
 
     if (as_header) {
         out->begin_list(out, NULL, NULL, "%s", buf);
     } else {
         out->list_item(out, NULL, "%s", buf);
     }
 
     free(buf);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("resource-history", "pcmk_resource_t *", "const char *",
                   "bool", "int", "time_t", "bool")
 static int
 resource_history_xml(pcmk__output_t *out, va_list args) {
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     const char *rsc_id = va_arg(args, const char *);
     bool all = va_arg(args, int);
     int failcount = va_arg(args, int);
     time_t last_failure = va_arg(args, time_t);
     bool as_header = va_arg(args, int);
 
     xmlNodePtr node = pcmk__output_xml_create_parent(out,
                                                      PCMK_XE_RESOURCE_HISTORY,
                                                      PCMK_XA_ID, rsc_id,
                                                      NULL);
 
     if (rsc == NULL) {
         pcmk__xe_set_bool_attr(node, PCMK_XA_ORPHAN, true);
     } else if (all || failcount || last_failure > 0) {
         char *migration_s = pcmk__itoa(rsc->migration_threshold);
 
         pcmk__xe_set_props(node,
                            PCMK_XA_ORPHAN, PCMK_VALUE_FALSE,
                            PCMK_META_MIGRATION_THRESHOLD, migration_s,
                            NULL);
         free(migration_s);
 
         if (failcount > 0) {
             char *s = pcmk__itoa(failcount);
 
             crm_xml_add(node, PCMK_XA_FAIL_COUNT, s);
             free(s);
         }
 
         if (last_failure > 0) {
             char *s = pcmk__epoch2str(&last_failure, 0);
 
             crm_xml_add(node, PCMK_XA_LAST_FAILURE, s);
             free(s);
         }
     }
 
     if (!as_header) {
         pcmk__output_xml_pop_parent(out);
     }
 
     return pcmk_rc_ok;
 }
 
 static void
 print_resource_header(pcmk__output_t *out, uint32_t show_opts)
 {
     if (pcmk_is_set(show_opts, pcmk_show_rscs_by_node)) {
         /* Active resources have already been printed by node */
         out->begin_list(out, NULL, NULL, "Inactive Resources");
     } else if (pcmk_is_set(show_opts, pcmk_show_inactive_rscs)) {
         out->begin_list(out, NULL, NULL, "Full List of Resources");
     } else {
         out->begin_list(out, NULL, NULL, "Active Resources");
     }
 }
 
 
 PCMK__OUTPUT_ARGS("resource-list", "pcmk_scheduler_t *", "uint32_t", "bool",
                   "GList *", "GList *", "bool")
 static int
 resource_list(pcmk__output_t *out, va_list args)
 {
     pcmk_scheduler_t *scheduler = va_arg(args, pcmk_scheduler_t *);
     uint32_t show_opts = va_arg(args, uint32_t);
     bool print_summary = va_arg(args, int);
     GList *only_node = va_arg(args, GList *);
     GList *only_rsc = va_arg(args, GList *);
     bool print_spacer = va_arg(args, int);
 
     GList *rsc_iter;
     int rc = pcmk_rc_no_output;
     bool printed_header = false;
 
     /* If we already showed active resources by node, and
      * we're not showing inactive resources, we have nothing to do
      */
     if (pcmk_is_set(show_opts, pcmk_show_rscs_by_node) &&
         !pcmk_is_set(show_opts, pcmk_show_inactive_rscs)) {
         return rc;
     }
 
     /* If we haven't already printed resources grouped by node,
      * and brief output was requested, print resource summary */
     if (pcmk_is_set(show_opts, pcmk_show_brief)
         && !pcmk_is_set(show_opts, pcmk_show_rscs_by_node)) {
         GList *rscs = pe__filter_rsc_list(scheduler->resources, only_rsc);
 
         PCMK__OUTPUT_SPACER_IF(out, print_spacer);
         print_resource_header(out, show_opts);
         printed_header = true;
 
         rc = pe__rscs_brief_output(out, rscs, show_opts);
         g_list_free(rscs);
     }
 
     /* For each resource, display it if appropriate */
     for (rsc_iter = scheduler->resources; rsc_iter != NULL; rsc_iter = rsc_iter->next) {
         pcmk_resource_t *rsc = (pcmk_resource_t *) rsc_iter->data;
         int x;
 
         /* Complex resources may have some sub-resources active and some inactive */
         gboolean is_active = rsc->fns->active(rsc, TRUE);
         gboolean partially_active = rsc->fns->active(rsc, FALSE);
 
         /* Skip inactive orphans (deleted but still in CIB) */
         if (pcmk_is_set(rsc->flags, pcmk_rsc_removed) && !is_active) {
             continue;
 
         /* Skip active resources if we already displayed them by node */
         } else if (pcmk_is_set(show_opts, pcmk_show_rscs_by_node)) {
             if (is_active) {
                 continue;
             }
 
         /* Skip primitives already counted in a brief summary */
         } else if (pcmk_is_set(show_opts, pcmk_show_brief)
                    && (rsc->variant == pcmk_rsc_variant_primitive)) {
             continue;
 
         /* Skip resources that aren't at least partially active,
          * unless we're displaying inactive resources
          */
         } else if (!partially_active && !pcmk_is_set(show_opts, pcmk_show_inactive_rscs)) {
             continue;
 
         } else if (partially_active && !pe__rsc_running_on_any(rsc, only_node)) {
             continue;
         }
 
         if (!printed_header) {
             PCMK__OUTPUT_SPACER_IF(out, print_spacer);
             print_resource_header(out, show_opts);
             printed_header = true;
         }
 
         /* Print this resource */
         x = out->message(out, crm_map_element_name(rsc->xml), show_opts, rsc,
                          only_node, only_rsc);
         if (x == pcmk_rc_ok) {
             rc = pcmk_rc_ok;
         }
     }
 
     if (print_summary && rc != pcmk_rc_ok) {
         if (!printed_header) {
             PCMK__OUTPUT_SPACER_IF(out, print_spacer);
             print_resource_header(out, show_opts);
             printed_header = true;
         }
 
         if (pcmk_is_set(show_opts, pcmk_show_rscs_by_node)) {
             out->list_item(out, NULL, "No inactive resources");
         } else if (pcmk_is_set(show_opts, pcmk_show_inactive_rscs)) {
             out->list_item(out, NULL, "No resources");
         } else {
             out->list_item(out, NULL, "No active resources");
         }
     }
 
     if (printed_header) {
         out->end_list(out);
     }
 
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("resource-operation-list", "pcmk_scheduler_t *",
                   "pcmk_resource_t *", "pcmk_node_t *", "GList *", "uint32_t")
 static int
 resource_operation_list(pcmk__output_t *out, va_list args)
 {
     pcmk_scheduler_t *scheduler G_GNUC_UNUSED = va_arg(args,
                                                        pcmk_scheduler_t *);
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     pcmk_node_t *node = va_arg(args, pcmk_node_t *);
     GList *op_list = va_arg(args, GList *);
     uint32_t show_opts = va_arg(args, uint32_t);
 
     GList *gIter = NULL;
     int rc = pcmk_rc_no_output;
 
     /* Print each operation */
     for (gIter = op_list; gIter != NULL; gIter = gIter->next) {
         xmlNode *xml_op = (xmlNode *) gIter->data;
         const char *task = crm_element_value(xml_op, PCMK_XA_OPERATION);
         const char *interval_ms_s = crm_element_value(xml_op,
                                                       PCMK_META_INTERVAL);
         const char *op_rc = crm_element_value(xml_op, PCMK__XA_RC_CODE);
         int op_rc_i;
 
         pcmk__scan_min_int(op_rc, &op_rc_i, 0);
 
         /* Display 0-interval monitors as "probe" */
         if (pcmk__str_eq(task, PCMK_ACTION_MONITOR, pcmk__str_casei)
             && pcmk__str_eq(interval_ms_s, "0", pcmk__str_null_matches | pcmk__str_casei)) {
             task = "probe";
         }
 
         /* If this is the first printed operation, print heading for resource */
         if (rc == pcmk_rc_no_output) {
             time_t last_failure = 0;
             int failcount = pe_get_failcount(node, rsc, &last_failure,
                                              pcmk__fc_default, NULL);
 
             out->message(out, "resource-history", rsc, rsc_printable_id(rsc), true,
                          failcount, last_failure, true);
             rc = pcmk_rc_ok;
         }
 
         /* Print the operation */
         out->message(out, "op-history", xml_op, task, interval_ms_s,
                      op_rc_i, show_opts);
     }
 
     /* Free the list we created (no need to free the individual items) */
     g_list_free(op_list);
 
     PCMK__OUTPUT_LIST_FOOTER(out, rc);
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("resource-util", "pcmk_resource_t *", "pcmk_node_t *",
                   "const char *")
 static int
 resource_util(pcmk__output_t *out, va_list args)
 {
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     pcmk_node_t *node = va_arg(args, pcmk_node_t *);
     const char *fn = va_arg(args, const char *);
 
     char *dump_text = crm_strdup_printf("%s: %s utilization on %s:",
                                         fn, rsc->id, pcmk__node_name(node));
 
     g_hash_table_foreach(rsc->utilization, append_dump_text, &dump_text);
     out->list_item(out, NULL, "%s", dump_text);
     free(dump_text);
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("resource-util", "pcmk_resource_t *", "pcmk_node_t *",
                   "const char *")
 static int
 resource_util_xml(pcmk__output_t *out, va_list args)
 {
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     pcmk_node_t *node = va_arg(args, pcmk_node_t *);
     const char *uname = node->details->uname;
     const char *fn = va_arg(args, const char *);
 
     xmlNodePtr xml_node = NULL;
 
     xml_node = pcmk__output_create_xml_node(out, PCMK_XE_UTILIZATION,
                                             PCMK_XA_RESOURCE, rsc->id,
                                             PCMK_XA_NODE, uname,
                                             PCMK_XA_FUNCTION, fn,
                                             NULL);
     g_hash_table_foreach(rsc->utilization, add_dump_node, xml_node);
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("ticket", "pcmk_ticket_t *")
 static int
 ticket_html(pcmk__output_t *out, va_list args) {
     pcmk_ticket_t *ticket = va_arg(args, pcmk_ticket_t *);
 
     if (ticket->last_granted > -1) {
         char *epoch_str = pcmk__epoch2str(&(ticket->last_granted), 0);
 
         out->list_item(out, NULL, "%s:\t%s%s last-granted=\"%s\"",
                        ticket->id, (ticket->granted? "granted" : "revoked"),
                        (ticket->standby? " [standby]" : ""),
                        pcmk__s(epoch_str, ""));
         free(epoch_str);
     } else {
         out->list_item(out, NULL, "%s:\t%s%s", ticket->id,
                        ticket->granted ? "granted" : "revoked",
                        ticket->standby ? " [standby]" : "");
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("ticket", "pcmk_ticket_t *")
 static int
 ticket_text(pcmk__output_t *out, va_list args) {
     pcmk_ticket_t *ticket = va_arg(args, pcmk_ticket_t *);
 
     if (ticket->last_granted > -1) {
         char *epoch_str = pcmk__epoch2str(&(ticket->last_granted), 0);
 
         out->list_item(out, ticket->id, "%s%s last-granted=\"%s\"",
                        (ticket->granted? "granted" : "revoked"),
                        (ticket->standby? " [standby]" : ""),
                        pcmk__s(epoch_str, ""));
         free(epoch_str);
     } else {
         out->list_item(out, ticket->id, "%s%s",
                        ticket->granted ? "granted" : "revoked",
                        ticket->standby ? " [standby]" : "");
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("ticket", "pcmk_ticket_t *")
 static int
 ticket_xml(pcmk__output_t *out, va_list args) {
     pcmk_ticket_t *ticket = va_arg(args, pcmk_ticket_t *);
     const char *status = NULL;
     const char *standby = pcmk__btoa(ticket->standby);
 
     xmlNodePtr node = NULL;
 
     status = ticket->granted? PCMK_VALUE_GRANTED : PCMK_VALUE_REVOKED;
 
     node = pcmk__output_create_xml_node(out, PCMK_XE_TICKET,
                                         PCMK_XA_ID, ticket->id,
                                         PCMK_XA_STATUS, status,
                                         PCMK_XA_STANDBY, standby,
                                         NULL);
 
     if (ticket->last_granted > -1) {
         char *buf = pcmk__epoch2str(&ticket->last_granted, 0);
 
         crm_xml_add(node, PCMK_XA_LAST_GRANTED, buf);
         free(buf);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("ticket-list", "pcmk_scheduler_t *", "bool")
 static int
 ticket_list(pcmk__output_t *out, va_list args) {
     pcmk_scheduler_t *scheduler = va_arg(args, pcmk_scheduler_t *);
     bool print_spacer = va_arg(args, int);
 
     GHashTableIter iter;
     gpointer key, value;
 
     if (g_hash_table_size(scheduler->tickets) == 0) {
         return pcmk_rc_no_output;
     }
 
     PCMK__OUTPUT_SPACER_IF(out, print_spacer);
 
     /* Print section heading */
     out->begin_list(out, NULL, NULL, "Tickets");
 
     /* Print each ticket */
     g_hash_table_iter_init(&iter, scheduler->tickets);
     while (g_hash_table_iter_next(&iter, &key, &value)) {
         pcmk_ticket_t *ticket = (pcmk_ticket_t *) value;
         out->message(out, "ticket", ticket);
     }
 
     /* Close section */
     out->end_list(out);
     return pcmk_rc_ok;
 }
 
 static pcmk__message_entry_t fmt_functions[] = {
     { "ban", "default", ban_text },
     { "ban", "html", ban_html },
     { "ban", "xml", ban_xml },
     { "ban-list", "default", ban_list },
     { "bundle", "default", pe__bundle_text },
     { "bundle", "xml",  pe__bundle_xml },
     { "bundle", "html",  pe__bundle_html },
     { "clone", "default", pe__clone_default },
     { "clone", "xml",  pe__clone_xml },
     { "cluster-counts", "default", cluster_counts_text },
     { "cluster-counts", "html", cluster_counts_html },
     { "cluster-counts", "xml", cluster_counts_xml },
     { "cluster-dc", "default", cluster_dc_text },
     { "cluster-dc", "html", cluster_dc_html },
     { "cluster-dc", "xml", cluster_dc_xml },
     { "cluster-options", "default", cluster_options_text },
     { "cluster-options", "html", cluster_options_html },
     { "cluster-options", "log", cluster_options_log },
     { "cluster-options", "xml", cluster_options_xml },
     { "cluster-summary", "default", cluster_summary },
     { "cluster-summary", "html", cluster_summary_html },
     { "cluster-stack", "default", cluster_stack_text },
     { "cluster-stack", "html", cluster_stack_html },
     { "cluster-stack", "xml", cluster_stack_xml },
     { "cluster-times", "default", cluster_times_text },
     { "cluster-times", "html", cluster_times_html },
     { "cluster-times", "xml", cluster_times_xml },
     { "failed-action", "default", failed_action_default },
     { "failed-action", "xml", failed_action_xml },
     { "failed-action-list", "default", failed_action_list },
     { "group", "default",  pe__group_default},
     { "group", "xml",  pe__group_xml },
     { "maint-mode", "text", cluster_maint_mode_text },
     { "node", "default", node_text },
     { "node", "html", node_html },
     { "node", "xml", node_xml },
     { "node-and-op", "default", node_and_op },
     { "node-and-op", "xml", node_and_op_xml },
     { "node-capacity", "default", node_capacity },
     { "node-capacity", "xml", node_capacity_xml },
     { "node-history-list", "default", node_history_list },
     { "node-list", "default", node_list_text },
     { "node-list", "html", node_list_html },
     { "node-list", "xml", node_list_xml },
     { "node-weight", "default", node_weight },
     { "node-weight", "xml", node_weight_xml },
     { "node-attribute", "default", node_attribute_text },
     { "node-attribute", "html", node_attribute_html },
     { "node-attribute", "xml", node_attribute_xml },
     { "node-attribute-list", "default", node_attribute_list },
     { "node-summary", "default", node_summary },
     { "op-history", "default", op_history_text },
     { "op-history", "xml", op_history_xml },
     { "primitive", "default",  pe__resource_text },
     { "primitive", "xml",  pe__resource_xml },
     { "primitive", "html",  pe__resource_html },
     { "promotion-score", "default", promotion_score },
     { "promotion-score", "xml", promotion_score_xml },
     { "resource-config", "default", resource_config },
     { "resource-config", "text", resource_config_text },
     { "resource-history", "default", resource_history_text },
     { "resource-history", "xml", resource_history_xml },
     { "resource-list", "default", resource_list },
     { "resource-operation-list", "default", resource_operation_list },
     { "resource-util", "default", resource_util },
     { "resource-util", "xml", resource_util_xml },
     { "ticket", "default", ticket_text },
     { "ticket", "html", ticket_html },
     { "ticket", "xml", ticket_xml },
     { "ticket-list", "default", ticket_list },
 
     { NULL, NULL, NULL }
 };
 
 void
 pe__register_messages(pcmk__output_t *out) {
     pcmk__register_messages(out, fmt_functions);
 }
diff --git a/tools/cibadmin.c b/tools/cibadmin.c
index 12353612ef..91d9f488ba 100644
--- a/tools/cibadmin.c
+++ b/tools/cibadmin.c
@@ -1,952 +1,954 @@
 /*
  * Copyright 2004-2024 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 <crm/crm.h>
 #include <crm/common/cmdline_internal.h>
 #include <crm/common/ipc.h>
 #include <crm/common/xml.h>
 #include <crm/cib/internal.h>
 
 #include <pacemaker-internal.h>
 
 #define SUMMARY "query and edit the Pacemaker configuration"
 
 #define INDENT "                                "
 
 enum cibadmin_section_type {
     cibadmin_section_all = 0,
     cibadmin_section_scope,
     cibadmin_section_xpath,
 };
 
 static int request_id = 0;
 
 static cib_t *the_cib = NULL;
 static GMainLoop *mainloop = NULL;
 static crm_exit_t exit_code = CRM_EX_OK;
 
 static struct {
     const char *cib_action;
     int cmd_options;
     enum cibadmin_section_type section_type;
     char *cib_section;
     char *validate_with;
     gint message_timeout_sec;
     enum pcmk__acl_render_how acl_render_mode;
     gchar *cib_user;
     gchar *dest_node;
     gchar *input_file;
     gchar *input_xml;
     gboolean input_stdin;
     bool delete_all;
     gboolean allow_create;
     gboolean force;
     gboolean get_node_path;
     gboolean local;
     gboolean no_children;
     gboolean sync_call;
 
     /* @COMPAT: For "-!" version option. Not advertised nor marked as
      * deprecated, but accepted.
      */
     gboolean extended_version;
 
     //! \deprecated
     gboolean no_bcast;
 } options;
 
 int do_init(void);
 static int do_work(xmlNode *input, xmlNode **output);
 void cibadmin_op_callback(xmlNode *msg, int call_id, int rc, xmlNode *output,
                           void *user_data);
 
 static void
 print_xml_output(xmlNode * xml)
 {
     if (!xml) {
         return;
     } else if (xml->type != XML_ELEMENT_NODE) {
         return;
     }
 
     if (pcmk_is_set(options.cmd_options, cib_xpath_address)) {
         const char *id = crm_element_value(xml, PCMK_XA_ID);
 
         if (pcmk__xe_is(xml, PCMK__XE_XPATH_QUERY)) {
             xmlNode *child = NULL;
 
             for (child = xml->children; child; child = child->next) {
                 print_xml_output(child);
             }
 
         } else if (id) {
             printf("%s\n", id);
         }
 
     } else {
-        gchar *buffer = pcmk__xml_dump(xml, pcmk__xml_fmt_pretty);
+        GString *buf = g_string_sized_new(1024);
 
-        fprintf(stdout, "%s", buffer);
-        g_free(buffer);
+        pcmk__xml_string(xml, pcmk__xml_fmt_pretty, buf, 0);
+
+        fprintf(stdout, "%s", buf->str);
+        g_string_free(buf, TRUE);
     }
 }
 
 // Upgrade requested but already at latest schema
 static void
 report_schema_unchanged(void)
 {
     const char *err = pcmk_rc_str(pcmk_rc_schema_unchanged);
 
     crm_info("Upgrade unnecessary: %s\n", err);
     printf("Upgrade unnecessary: %s\n", err);
     exit_code = CRM_EX_OK;
 }
 
 /*!
  * \internal
  * \brief Check whether the current CIB action is dangerous
  * \return true if \p options.cib_action is dangerous, or false otherwise
  */
 static inline bool
 cib_action_is_dangerous(void)
 {
     return options.no_bcast || options.delete_all
            || pcmk__str_any_of(options.cib_action,
                                PCMK__CIB_REQUEST_UPGRADE,
                                PCMK__CIB_REQUEST_ERASE,
                                NULL);
 }
 
 /*!
  * \internal
  * \brief Determine whether the given CIB scope is valid for \p cibadmin
  *
  * \param[in] scope  Scope to validate
  *
  * \return true if \p scope is valid, or false otherwise
  * \note An invalid scope applies the operation to the entire CIB.
  */
 static inline bool
 scope_is_valid(const char *scope)
 {
     return pcmk__str_any_of(scope,
                             PCMK_XE_CONFIGURATION,
                             PCMK_XE_NODES,
                             PCMK_XE_RESOURCES,
                             PCMK_XE_CONSTRAINTS,
                             PCMK_XE_CRM_CONFIG,
                             PCMK_XE_RSC_DEFAULTS,
                             PCMK_XE_OP_DEFAULTS,
                             PCMK_XE_ACLS,
                             PCMK_XE_FENCING_TOPOLOGY,
                             PCMK_XE_TAGS,
                             PCMK_XE_ALERTS,
                             PCMK_XE_STATUS,
                             NULL);
 }
 
 static gboolean
 command_cb(const gchar *option_name, const gchar *optarg, gpointer data,
            GError **error)
 {
     options.delete_all = false;
 
     if (pcmk__str_any_of(option_name, "-u", "--upgrade", NULL)) {
         options.cib_action = PCMK__CIB_REQUEST_UPGRADE;
 
     } else if (pcmk__str_any_of(option_name, "-Q", "--query", NULL)) {
         options.cib_action = PCMK__CIB_REQUEST_QUERY;
 
     } else if (pcmk__str_any_of(option_name, "-E", "--erase", NULL)) {
         options.cib_action = PCMK__CIB_REQUEST_ERASE;
 
     } else if (pcmk__str_any_of(option_name, "-B", "--bump", NULL)) {
         options.cib_action = PCMK__CIB_REQUEST_BUMP;
 
     } else if (pcmk__str_any_of(option_name, "-C", "--create", NULL)) {
         options.cib_action = PCMK__CIB_REQUEST_CREATE;
 
     } else if (pcmk__str_any_of(option_name, "-M", "--modify", NULL)) {
         options.cib_action = PCMK__CIB_REQUEST_MODIFY;
 
     } else if (pcmk__str_any_of(option_name, "-P", "--patch", NULL)) {
         options.cib_action = PCMK__CIB_REQUEST_APPLY_PATCH;
 
     } else if (pcmk__str_any_of(option_name, "-R", "--replace", NULL)) {
         options.cib_action = PCMK__CIB_REQUEST_REPLACE;
 
     } else if (pcmk__str_any_of(option_name, "-D", "--delete", NULL)) {
         options.cib_action = PCMK__CIB_REQUEST_DELETE;
 
     } else if (pcmk__str_any_of(option_name, "-d", "--delete-all", NULL)) {
         options.cib_action = PCMK__CIB_REQUEST_DELETE;
         options.delete_all = true;
 
     } else if (pcmk__str_any_of(option_name, "-a", "--empty", NULL)) {
         options.cib_action = "empty";
         pcmk__str_update(&options.validate_with, optarg);
 
     } else if (pcmk__str_any_of(option_name, "-5", "--md5-sum", NULL)) {
         options.cib_action = "md5-sum";
 
     } else if (pcmk__str_any_of(option_name, "-6", "--md5-sum-versioned",
                                 NULL)) {
         options.cib_action = "md5-sum-versioned";
 
     } else {
         // Should be impossible
         return FALSE;
     }
 
     return TRUE;
 }
 
 static gboolean
 show_access_cb(const gchar *option_name, const gchar *optarg, gpointer data,
                GError **error)
 {
     if (pcmk__str_eq(optarg, "auto", pcmk__str_null_matches)) {
         options.acl_render_mode = pcmk__acl_render_default;
 
     } else if (g_strcmp0(optarg, "namespace") == 0) {
         options.acl_render_mode = pcmk__acl_render_namespace;
 
     } else if (g_strcmp0(optarg, "text") == 0) {
         options.acl_render_mode = pcmk__acl_render_text;
 
     } else if (g_strcmp0(optarg, "color") == 0) {
         options.acl_render_mode = pcmk__acl_render_color;
 
     } else {
         g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
                     "Invalid value '%s' for option '%s'",
                     optarg, option_name);
         return FALSE;
     }
     return TRUE;
 }
 
 static gboolean
 section_cb(const gchar *option_name, const gchar *optarg, gpointer data,
            GError **error)
 {
     if (pcmk__str_any_of(option_name, "-o", "--scope", NULL)) {
         options.section_type = cibadmin_section_scope;
 
     } else if (pcmk__str_any_of(option_name, "-A", "--xpath", NULL)) {
         options.section_type = cibadmin_section_xpath;
 
     } else {
         // Should be impossible
         return FALSE;
     }
 
     pcmk__str_update(&options.cib_section, optarg);
     return TRUE;
 }
 
 static GOptionEntry command_entries[] = {
     { "upgrade", 'u', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Upgrade the configuration to the latest syntax", NULL },
 
     { "query", 'Q', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Query the contents of the CIB", NULL },
 
     { "erase", 'E', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Erase the contents of the whole CIB", NULL },
 
     { "bump", 'B', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Increase the CIB's epoch value by 1", NULL },
 
     { "create", 'C', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Create an object in the CIB (will fail if object already exists)",
       NULL },
 
     { "modify", 'M', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Find object somewhere in CIB's XML tree and update it (fails if object "
       "does not exist unless -c is also specified)",
       NULL },
 
     { "patch", 'P', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Supply an update in the form of an XML diff (see crm_diff(8))", NULL },
 
     { "replace", 'R', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Recursively replace an object in the CIB", NULL },
 
     { "delete", 'D', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Delete first object matching supplied criteria (for example, "
       "<" PCMK_XE_OP " " PCMK_XA_ID "=\"rsc1_op1\" "
           PCMK_XA_NAME "=\"monitor\"/>).\n"
       INDENT "The XML element name and all attributes must match in order for "
       "the element to be deleted.",
       NULL },
 
     { "delete-all", 'd', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
       command_cb,
       "When used with --xpath, remove all matching objects in the "
       "configuration instead of just the first one",
       NULL },
 
     { "empty", 'a', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK,
       command_cb,
       "Output an empty CIB. Accepts an optional schema name argument to use as "
       "the " PCMK_XA_VALIDATE_WITH " value.\n"
       INDENT "If no schema is given, the latest will be used.",
       "[schema]" },
 
     { "md5-sum", '5', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Calculate the on-disk CIB digest", NULL },
 
     { "md5-sum-versioned", '6', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
       command_cb, "Calculate an on-the-wire versioned CIB digest", NULL },
 
     { NULL }
 };
 
 static GOptionEntry data_entries[] = {
     /* @COMPAT: These arguments should be last-wins. We can have an enum option
      * that stores the input type, along with a single string option that stores
      * the XML string for --xml-text, filename for --xml-file, or NULL for
      * --xml-pipe.
      */
     { "xml-text", 'X', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING,
       &options.input_xml, "Retrieve XML from the supplied string", "value" },
 
     { "xml-file", 'x', G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME,
       &options.input_file, "Retrieve XML from the named file", "value" },
 
     { "xml-pipe", 'p', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
       &options.input_stdin, "Retrieve XML from stdin", NULL },
 
     { NULL }
 };
 
 static GOptionEntry addl_entries[] = {
     { "force", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.force,
       "Force the action to be performed", NULL },
 
     { "timeout", 't', G_OPTION_FLAG_NONE, G_OPTION_ARG_INT,
       &options.message_timeout_sec,
       "Time (in seconds) to wait before declaring the operation failed",
       "value" },
 
     { "user", 'U', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.cib_user,
       "Run the command with permissions of the named user (valid only for the "
       "root and " CRM_DAEMON_USER " accounts)", "value" },
 
     { "sync-call", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
       &options.sync_call, "Wait for call to complete before returning", NULL },
 
     { "local", 'l', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.local,
       "Command takes effect locally (should be used only for queries)", NULL },
 
     { "scope", 'o', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, section_cb,
       "Limit scope of operation to specific section of CIB\n"
       INDENT "Valid values: " PCMK_XE_CONFIGURATION ", " PCMK_XE_NODES
       ", " PCMK_XE_RESOURCES ", " PCMK_XE_CONSTRAINTS
       ", " PCMK_XE_CRM_CONFIG ", " PCMK_XE_RSC_DEFAULTS ",\n"
       INDENT "              " PCMK_XE_OP_DEFAULTS ", " PCMK_XE_ACLS
       ", " PCMK_XE_FENCING_TOPOLOGY ", " PCMK_XE_TAGS ", " PCMK_XE_ALERTS
       ", " PCMK_XE_STATUS "\n"
       INDENT "If both --scope/-o and --xpath/-a are specified, the last one to "
       "appear takes effect",
       "value" },
 
     { "xpath", 'A', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, section_cb,
       "A valid XPath to use instead of --scope/-o\n"
       INDENT "If both --scope/-o and --xpath/-a are specified, the last one to "
       "appear takes effect",
       "value" },
 
     { "node-path", 'e', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
       &options.get_node_path,
       "When performing XPath queries, return paths of any matches found\n"
       INDENT "(for example, "
       "\"/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION
       "/" PCMK_XE_RESOURCES "/" PCMK_XE_CLONE
       "[@" PCMK_XA_ID "='dummy-clone']"
       "/" PCMK_XE_PRIMITIVE "[@" PCMK_XA_ID "='dummy']\")",
       NULL },
 
     { "show-access", 'S', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK,
       show_access_cb,
       "Whether to use syntax highlighting for ACLs (with -Q/--query and "
       "-U/--user)\n"
       INDENT "Allowed values: 'color' (default for terminal), 'text' (plain text, "
       "default for non-terminal),\n"
       INDENT "                'namespace', or 'auto' (use default value)\n"
       INDENT "Default value: 'auto'",
       "[value]" },
 
     { "allow-create", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
       &options.allow_create,
       "(Advanced) Allow target of --modify/-M to be created if it does not "
       "exist",
       NULL },
 
     { "no-children", 'n', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
       &options.no_children,
       "(Advanced) When querying an object, do not include its children in the "
       "result",
       NULL },
 
     { "node", 'N', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.dest_node,
       "(Advanced) Send command to the specified host", "value" },
 
     // @COMPAT: Deprecated
     { "no-bcast", 'b', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE,
       &options.no_bcast, "deprecated", NULL },
 
     // @COMPAT: Deprecated
     { "host", 'h', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING,
       &options.dest_node, "deprecated", NULL },
 
     { NULL }
 };
 
 static GOptionContext *
 build_arg_context(pcmk__common_args_t *args)
 {
     const char *desc = NULL;
     GOptionContext *context = NULL;
 
     GOptionEntry extra_prog_entries[] = {
         // @COMPAT: Deprecated
         { "extended-version", '!', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE,
           &options.extended_version, "deprecated", NULL },
 
         { NULL }
     };
 
     desc = "Examples:\n\n"
            "Query the configuration from the local node:\n\n"
            "\t# cibadmin --query --local\n\n"
            "Query just the cluster options configuration:\n\n"
            "\t# cibadmin --query --scope " PCMK_XE_CRM_CONFIG "\n\n"
            "Query all '" PCMK_META_TARGET_ROLE "' settings:\n\n"
            "\t# cibadmin --query --xpath "
                "\"//" PCMK_XE_NVPAIR
                "[@" PCMK_XA_NAME "='" PCMK_META_TARGET_ROLE"']\"\n\n"
            "Remove all '" PCMK_META_IS_MANAGED "' settings:\n\n"
            "\t# cibadmin --delete-all --xpath "
                "\"//" PCMK_XE_NVPAIR
                "[@" PCMK_XA_NAME "='" PCMK_META_IS_MANAGED "']\"\n\n"
            "Remove the resource named 'old':\n\n"
            "\t# cibadmin --delete --xml-text "
                "'<" PCMK_XE_PRIMITIVE " " PCMK_XA_ID "=\"old\"/>'\n\n"
            "Remove all resources from the configuration:\n\n"
            "\t# cibadmin --replace --scope " PCMK_XE_RESOURCES
                " --xml-text '<" PCMK_XE_RESOURCES "/>'\n\n"
            "Replace complete configuration with contents of "
                "$HOME/pacemaker.xml:\n\n"
            "\t# cibadmin --replace --xml-file $HOME/pacemaker.xml\n\n"
            "Replace " PCMK_XE_CONSTRAINTS " section of configuration with "
                "contents of $HOME/constraints.xml:\n\n"
            "\t# cibadmin --replace --scope " PCMK_XE_CONSTRAINTS
                " --xml-file $HOME/constraints.xml\n\n"
            "Increase configuration version to prevent old configurations from "
                "being loaded accidentally:\n\n"
            "\t# cibadmin --modify --xml-text "
                "'<" PCMK_XE_CIB " " PCMK_XA_ADMIN_EPOCH
                    "=\"" PCMK_XA_ADMIN_EPOCH "++\"/>'\n\n"
            "Edit the configuration with your favorite $EDITOR:\n\n"
            "\t# cibadmin --query > $HOME/local.xml\n\n"
            "\t# $EDITOR $HOME/local.xml\n\n"
            "\t# cibadmin --replace --xml-file $HOME/local.xml\n\n"
            "Assuming terminal, render configuration in color (green for "
                "writable, blue for readable, red for\n"
                "denied) to visualize permissions for user tony:\n\n"
            "\t# cibadmin --show-access=color --query --user tony | less -r\n\n"
            "SEE ALSO:\n"
            " crm(8), pcs(8), crm_shadow(8), crm_diff(8)\n";
 
     context = pcmk__build_arg_context(args, NULL, NULL, "<command>");
     g_option_context_set_description(context, desc);
 
     pcmk__add_main_args(context, extra_prog_entries);
 
     pcmk__add_arg_group(context, "commands", "Commands:", "Show command help",
                         command_entries);
     pcmk__add_arg_group(context, "data", "Data:", "Show data help",
                         data_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_rc_ok;
     const char *source = NULL;
     xmlNode *output = NULL;
     xmlNode *input = NULL;
     gchar *acl_cred = NULL;
 
     GError *error = NULL;
 
     pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
     gchar **processed_args = pcmk__cmdline_preproc(argv, "ANSUXhotx");
     GOptionContext *context = build_arg_context(args);
 
     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 || options.extended_version) {
         g_strfreev(processed_args);
         pcmk__free_arg_context(context);
 
         /* FIXME: When cibadmin is converted to use formatted output, this can
          * be replaced by out->version with the appropriate boolean flag.
          *
          * options.extended_version is deprecated and will be removed in a
          * future release.
          */
         pcmk__cli_help(options.extended_version? '!' : 'v');
     }
 
     /* At LOG_ERR, stderr for CIB calls is rather verbose. Several lines like
      *
      * (func@file:line)      error: CIB <op> failures   <XML>
      *
      * In cibadmin we explicitly output the XML portion without the prefixes. So
      * we default to LOG_CRIT.
      */
     pcmk__cli_init_logging("cibadmin", 0);
     set_crm_log_level(LOG_CRIT);
 
     if (args->verbosity > 0) {
         cib__set_call_options(options.cmd_options, crm_system_name,
                               cib_verbose);
 
         for (int i = 0; i < args->verbosity; i++) {
             crm_bump_log_level(argc, argv);
         }
     }
 
     if (options.cib_action == NULL) {
         // @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 command option\n\n%s", help);
         g_free(help);
         goto done;
     }
 
     if (strcmp(options.cib_action, "empty") == 0) {
         // Output an empty CIB
-        gchar *buf = NULL;
+        GString *buf = g_string_sized_new(1024);
 
         output = createEmptyCib(1);
         crm_xml_add(output, PCMK_XA_VALIDATE_WITH, options.validate_with);
 
-        buf = pcmk__xml_dump(output, pcmk__xml_fmt_pretty);
-        fprintf(stdout, "%s", buf);
-        g_free(buf);
+        pcmk__xml_string(output, pcmk__xml_fmt_pretty, buf, 0);
+        fprintf(stdout, "%s", buf->str);
+        g_string_free(buf, TRUE);
         goto done;
     }
 
     if (cib_action_is_dangerous() && !options.force) {
         exit_code = CRM_EX_UNSAFE;
         g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                     "The supplied command is considered dangerous. To prevent "
                     "accidental destruction of the cluster, the --force flag "
                     "is required in order to proceed.");
         goto done;
     }
 
     if (options.message_timeout_sec < 1) {
         // Set default timeout
         options.message_timeout_sec = 30;
     }
 
     if (options.section_type == cibadmin_section_xpath) {
         // Enable getting section by XPath
         cib__set_call_options(options.cmd_options, crm_system_name,
                               cib_xpath);
 
     } else if (options.section_type == cibadmin_section_scope) {
         if (!scope_is_valid(options.cib_section)) {
             // @COMPAT: Consider requiring --force to proceed
             fprintf(stderr,
                     "Invalid value '%s' for '--scope'. Operation will apply "
                     "to the entire CIB.\n", options.cib_section);
         }
     }
 
     if (options.allow_create) {
         // Allow target of --modify/-M to be created if it does not exist
         cib__set_call_options(options.cmd_options, crm_system_name,
                               cib_can_create);
     }
 
     if (options.delete_all) {
         // With cibadmin_section_xpath, remove all matching objects
         cib__set_call_options(options.cmd_options, crm_system_name,
                               cib_multiple);
     }
 
     if (options.get_node_path) {
         /* Enable getting node path of XPath query matches.
          * Meaningful only if options.section_type == cibadmin_section_xpath.
          */
         cib__set_call_options(options.cmd_options, crm_system_name,
                               cib_xpath_address);
     }
 
     if (options.local) {
         // Configure command to take effect only locally
         cib__set_call_options(options.cmd_options, crm_system_name,
                               cib_scope_local);
     }
 
     // @COMPAT: Deprecated option
     if (options.no_bcast) {
         // Configure command to take effect only locally and not to broadcast
         cib__set_call_options(options.cmd_options, crm_system_name,
                               cib_inhibit_bcast|cib_scope_local);
     }
 
     if (options.no_children) {
         // When querying an object, don't include its children in the result
         cib__set_call_options(options.cmd_options, crm_system_name,
                               cib_no_children);
     }
 
     if (options.sync_call
         || (options.acl_render_mode != pcmk__acl_render_none)) {
         /* Wait for call to complete before returning.
          *
          * The ACL render modes work only with sync calls due to differences in
          * output handling between sync/async. It shouldn't matter to the user
          * whether the call is synchronous; for a CIB query, we have to wait for
          * the result in order to display it in any case.
          */
         cib__set_call_options(options.cmd_options, crm_system_name,
                               cib_sync_call);
     }
 
     if (options.input_file != NULL) {
         input = pcmk__xml_read(options.input_file);
         source = options.input_file;
 
     } else if (options.input_xml != NULL) {
         input = pcmk__xml_parse(options.input_xml);
         source = "input string";
 
     } else if (options.input_stdin) {
         input = pcmk__xml_read(NULL);
         source = "STDIN";
 
     } else if (options.acl_render_mode != pcmk__acl_render_none) {
         char *username = pcmk__uid2username(geteuid());
         bool required = pcmk_acl_required(username);
 
         free(username);
 
         if (required) {
             if (options.force) {
                 fprintf(stderr, "The supplied command can provide skewed"
                                  " result since it is run under user that also"
                                  " gets guarded per ACLs on their own right."
                                  " Continuing since --force flag was"
                                  " provided.\n");
 
             } else {
                 exit_code = CRM_EX_UNSAFE;
                 g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                             "The supplied command can provide skewed result "
                             "since it is run under user that also gets guarded "
                             "per ACLs in their own right. To accept the risk "
                             "of such a possible distortion (without even "
                             "knowing it at this time), use the --force flag.");
                 goto done;
             }
         }
 
         if (options.cib_user == NULL) {
             exit_code = CRM_EX_USAGE;
             g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                         "The supplied command requires -U user specified.");
             goto done;
         }
 
         /* We already stopped/warned ACL-controlled users about consequences.
          *
          * Note: acl_cred takes ownership of options.cib_user here.
          * options.cib_user is set to NULL so that the CIB is obtained as the
          * user running the cibadmin command. The CIB must be obtained as a user
          * with full permissions in order to show the CIB correctly annotated
          * for the options.cib_user's permissions.
          */
         acl_cred = options.cib_user;
         options.cib_user = NULL;
     }
 
     if (input != NULL) {
         crm_log_xml_debug(input, "[admin input]");
 
     } else if (source != NULL) {
         exit_code = CRM_EX_CONFIG;
         g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                     "Couldn't parse input from %s.", source);
         goto done;
     }
 
     if (pcmk__str_eq(options.cib_action, "md5-sum", pcmk__str_casei)) {
         char *digest = NULL;
 
         if (input == NULL) {
             exit_code = CRM_EX_USAGE;
             g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                         "Please supply XML to process with -X, -x, or -p");
             goto done;
         }
 
         digest = calculate_on_disk_digest(input);
         fprintf(stderr, "Digest: ");
         fprintf(stdout, "%s\n", pcmk__s(digest, "<null>"));
         free(digest);
         goto done;
 
     } else if (strcmp(options.cib_action, "md5-sum-versioned") == 0) {
         char *digest = NULL;
         const char *version = NULL;
 
         if (input == NULL) {
             exit_code = CRM_EX_USAGE;
             g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                         "Please supply XML to process with -X, -x, or -p");
             goto done;
         }
 
         version = crm_element_value(input, PCMK_XA_CRM_FEATURE_SET);
         digest = calculate_xml_versioned_digest(input, FALSE, TRUE, version);
         fprintf(stderr, "Versioned (%s) digest: ", version);
         fprintf(stdout, "%s\n", pcmk__s(digest, "<null>"));
         free(digest);
         goto done;
     }
 
     rc = do_init();
     if (rc != pcmk_ok) {
         rc = pcmk_legacy2rc(rc);
         exit_code = pcmk_rc2exitc(rc);
 
         crm_err("Init failed, could not perform requested operations: %s",
                 pcmk_rc_str(rc));
         g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                     "Init failed, could not perform requested operations: %s",
                     pcmk_rc_str(rc));
         goto done;
     }
 
     rc = do_work(input, &output);
     if (rc > 0) {
         /* wait for the reply by creating a mainloop and running it until
          * the callbacks are invoked...
          */
         request_id = rc;
 
         the_cib->cmds->register_callback(the_cib, request_id,
                                          options.message_timeout_sec, FALSE,
                                          NULL, "cibadmin_op_callback",
                                          cibadmin_op_callback);
 
         mainloop = g_main_loop_new(NULL, FALSE);
 
         crm_trace("%s waiting for reply from the local CIB", crm_system_name);
 
         crm_info("Starting mainloop");
         g_main_loop_run(mainloop);
 
     } else if ((rc == -pcmk_err_schema_unchanged)
                && (strcmp(options.cib_action,
                           PCMK__CIB_REQUEST_UPGRADE) == 0)) {
         report_schema_unchanged();
 
     } else if (rc < 0) {
         rc = pcmk_legacy2rc(rc);
         crm_err("Call failed: %s", pcmk_rc_str(rc));
         fprintf(stderr, "Call failed: %s\n", pcmk_rc_str(rc));
 
         if (rc == pcmk_rc_schema_validation) {
             if (strcmp(options.cib_action, PCMK__CIB_REQUEST_UPGRADE) == 0) {
                 xmlNode *obj = NULL;
                 int version = 0;
 
                 if (the_cib->cmds->query(the_cib, NULL, &obj,
                                          options.cmd_options) == pcmk_ok) {
                     update_validation(&obj, &version, 0, TRUE, FALSE);
                 }
                 free_xml(obj);
 
             } else if (output) {
                 validate_xml_verbose(output);
             }
         }
         exit_code = pcmk_rc2exitc(rc);
     }
 
     if ((output != NULL)
         && (options.acl_render_mode != pcmk__acl_render_none)) {
 
         xmlDoc *acl_evaled_doc;
         rc = pcmk__acl_annotate_permissions(acl_cred, output->doc, &acl_evaled_doc);
         if (rc == pcmk_rc_ok) {
             xmlChar *rendered = NULL;
 
             rc = pcmk__acl_evaled_render(acl_evaled_doc,
                                          options.acl_render_mode, &rendered);
             if (rc != pcmk_rc_ok) {
                 exit_code = CRM_EX_CONFIG;
                 g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                             "Could not render evaluated access: %s",
                             pcmk_rc_str(rc));
                 goto done;
             }
             printf("%s\n", (char *) rendered);
             free(rendered);
 
         } else {
             exit_code = CRM_EX_CONFIG;
             g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                         "Could not evaluate access per request (%s, error: %s)",
                         acl_cred, pcmk_rc_str(rc));
             goto done;
         }
 
     } else if (output != NULL) {
         print_xml_output(output);
     }
 
     crm_trace("%s exiting normally", crm_system_name);
 
 done:
     g_strfreev(processed_args);
     pcmk__free_arg_context(context);
 
     g_free(options.cib_user);
     g_free(options.dest_node);
     g_free(options.input_file);
     g_free(options.input_xml);
     free(options.cib_section);
     free(options.validate_with);
 
     g_free(acl_cred);
     free_xml(input);
     free_xml(output);
 
     rc = cib__clean_up_connection(&the_cib);
     if (exit_code == CRM_EX_OK) {
         exit_code = pcmk_rc2exitc(rc);
     }
 
     pcmk__output_and_clear_error(&error, NULL);
     crm_exit(exit_code);
 }
 
 static int
 do_work(xmlNode *input, xmlNode **output)
 {
     /* construct the request */
     the_cib->call_timeout = options.message_timeout_sec;
     if ((strcmp(options.cib_action, PCMK__CIB_REQUEST_REPLACE) == 0)
         && pcmk__xe_is(input, PCMK_XE_CIB)) {
         xmlNode *status = pcmk_find_cib_element(input, PCMK_XE_STATUS);
 
         if (status == NULL) {
             create_xml_node(input, PCMK_XE_STATUS);
         }
     }
 
     crm_trace("Passing \"%s\" to variant_op...", options.cib_action);
     return cib_internal_op(the_cib, options.cib_action, options.dest_node,
                            options.cib_section, input, output,
                            options.cmd_options, options.cib_user);
 }
 
 int
 do_init(void)
 {
     int rc = pcmk_ok;
 
     the_cib = cib_new();
     rc = the_cib->cmds->signon(the_cib, crm_system_name, cib_command);
     if (rc != pcmk_ok) {
         crm_err("Could not connect to the CIB: %s", pcmk_strerror(rc));
         fprintf(stderr, "Could not connect to the CIB: %s\n",
                 pcmk_strerror(rc));
     }
 
     return rc;
 }
 
 void
 cibadmin_op_callback(xmlNode * msg, int call_id, int rc, xmlNode * output, void *user_data)
 {
     rc = pcmk_legacy2rc(rc);
     exit_code = pcmk_rc2exitc(rc);
 
     if (rc == pcmk_rc_schema_unchanged) {
         report_schema_unchanged();
 
     } else if (rc != pcmk_rc_ok) {
         crm_warn("Call %s failed: %s " CRM_XS " rc=%d",
                  options.cib_action, pcmk_rc_str(rc), rc);
         fprintf(stderr, "Call %s failed: %s\n",
                 options.cib_action, pcmk_rc_str(rc));
         print_xml_output(output);
 
     } else if ((strcmp(options.cib_action, PCMK__CIB_REQUEST_QUERY) == 0)
                && (output == NULL)) {
         crm_err("Query returned no output");
         crm_log_xml_err(msg, "no output");
 
     } else if (output == NULL) {
         crm_info("Call passed");
 
     } else {
         crm_info("Call passed");
         print_xml_output(output);
     }
 
     if (call_id == request_id) {
         g_main_loop_quit(mainloop);
 
     } else {
         crm_info("Message was not the response we were looking for (%d vs. %d)",
                  call_id, request_id);
     }
 }
diff --git a/tools/crm_diff.c b/tools/crm_diff.c
index 0ae30370bc..45e311323d 100644
--- a/tools/crm_diff.c
+++ b/tools/crm_diff.c
@@ -1,379 +1,381 @@
 /*
  * Copyright 2005-2024 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/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)
 {
-    gchar *buffer = pcmk__xml_dump(patch, pcmk__xml_fmt_pretty);
+    GString *buffer = g_string_sized_new(1024);
 
-    printf("%s", buffer);
-    g_free(buffer);
+    pcmk__xml_string(patch, pcmk__xml_fmt_pretty, buffer, 0);
+
+    printf("%s", buffer->str);
+    g_string_free(buffer, TRUE);
     fflush(stdout);
 }
 
 // \return Standard Pacemaker return code
 static int
 apply_patch(xmlNode *input, xmlNode *patch, gboolean as_cib)
 {
     xmlNode *output = pcmk__xml_copy(NULL, 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, PCMK_XA_CRM_FEATURE_SET);
         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, PCMK_XA_FORMAT);
     digest = crm_element_value(patch, PCMK__XA_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, PCMK_XA_FORMAT, &format);
     if (format == 2) {
         xmlNode *version_xml = find_xml_node(patch, PCMK_XE_VERSION, FALSE);
 
         if (version_xml) {
             free_xml(version_xml);
         }
 
     } else {
         int i = 0;
 
         const char *tags[] = {
             PCMK__XE_DIFF_REMOVED,
             PCMK__XE_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, PCMK_XE_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)
 {
     const char *vfields[] = {
         PCMK_XA_ADMIN_EPOCH,
         PCMK_XA_EPOCH,
         PCMK_XA_NUM_UPDATES,
     };
 
     xmlNode *output = NULL;
 
     /* 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__log_xml_changes(LOG_INFO, object_2);
     xml_accept_changes(object_2);
 
     if (output == NULL) {
         return pcmk_rc_ok;  // No changes
     }
 
     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__log_xml_patchset(LOG_NOTICE, output);
     print_patch(output);
     free_xml(output);
 
     /* pcmk_rc_error means there's a non-empty diff.
      * @COMPAT Choose a more descriptive return code, like one that maps to
      * CRM_EX_DIGEST?
      */
     return pcmk_rc_error;
 }
 
 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 = pcmk__xml_parse(options.xml_file_1);
 
     } else if (options.use_stdin) {
         fprintf(stderr, "Input first XML fragment:");
         object_1 = pcmk__xml_read(NULL);
 
     } else if (options.xml_file_1 != NULL) {
         object_1 = pcmk__xml_read(options.xml_file_1);
     }
 
     if (options.raw_2) {
         object_2 = pcmk__xml_parse(options.xml_file_2);
 
     } else if (options.use_stdin) {
         fprintf(stderr, "Input second XML fragment:");
         object_2 = pcmk__xml_read(NULL);
 
     } else if (options.xml_file_2 != NULL) {
         object_2 = pcmk__xml_read(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 7303dfbc97..58a8d53899 100644
--- a/tools/crm_shadow.c
+++ b/tools/crm_shadow.c
@@ -1,1313 +1,1321 @@
 /*
  * Copyright 2004-2024 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/common/cmdline_internal.h>
 #include <crm/common/ipc.h>
 #include <crm/common/output_internal.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,
 };
 
 /*!
  * \internal
  * \enum shadow_disp_flags
  * \brief Bit flags to control which fields of shadow CIB info are displayed
  *
  * \note Ignored for XML output.
  */
 enum shadow_disp_flags {
     shadow_disp_instance = (1 << 0),
     shadow_disp_file     = (1 << 1),
     shadow_disp_content  = (1 << 2),
     shadow_disp_diff     = (1 << 3),
 };
 
 static crm_exit_t exit_code = CRM_EX_OK;
 
 static struct {
     enum shadow_command cmd;
     int cmd_options;
     char *instance;
     gboolean force;
     gboolean batch;
     gboolean full_upload;
     gchar *validate_with;
 } options = {
     .cmd_options = cib_sync_call,
 };
 
 /*!
  * \internal
  * \brief Display an instruction to the user
  *
  * \param[in,out] out   Output object
  * \param[in]     args  Message-specific arguments
  *
  * \return Standard Pacemaker return code
  *
  * \note \p args should contain the following:
  *       -# Instructional message
  */
 PCMK__OUTPUT_ARGS("instruction", "const char *")
 static int
 instruction_default(pcmk__output_t *out, va_list args)
 {
     const char *msg = va_arg(args, const char *);
 
     if (msg == NULL) {
         return pcmk_rc_no_output;
     }
     return out->info(out, "%s", msg);
 }
 
 /*!
  * \internal
  * \brief Display an instruction to the user
  *
  * \param[in,out] out   Output object
  * \param[in]     args  Message-specific arguments
  *
  * \return Standard Pacemaker return code
  *
  * \note \p args should contain the following:
  *       -# Instructional message
  */
 PCMK__OUTPUT_ARGS("instruction", "const char *")
 static int
 instruction_xml(pcmk__output_t *out, va_list args)
 {
     const char *msg = va_arg(args, const char *);
 
     if (msg == NULL) {
         return pcmk_rc_no_output;
     }
     pcmk__output_create_xml_text_node(out, "instruction", msg);
     return pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Display information about a shadow CIB instance
  *
  * \param[in,out] out   Output object
  * \param[in]     args  Message-specific arguments
  *
  * \return Standard Pacemaker return code
  *
  * \note \p args should contain the following:
  *       -# Instance name (can be \p NULL)
  *       -# Shadow file name (can be \p NULL)
  *       -# Shadow file content (can be \p NULL)
  *       -# Patchset containing the changes in the shadow CIB (can be \p NULL)
  *       -# Group of \p shadow_disp_flags indicating which fields to display
  */
 PCMK__OUTPUT_ARGS("shadow", "const char *", "const char *", "const xmlNode *",
                   "const xmlNode *", "enum shadow_disp_flags")
 static int
 shadow_default(pcmk__output_t *out, va_list args)
 {
     const char *instance = va_arg(args, const char *);
     const char *filename = va_arg(args, const char *);
     const xmlNode *content = va_arg(args, const xmlNode *);
     const xmlNode *diff = va_arg(args, const xmlNode *);
     enum shadow_disp_flags flags = (enum shadow_disp_flags) va_arg(args, int);
 
     int rc = pcmk_rc_no_output;
 
     if (pcmk_is_set(flags, shadow_disp_instance)) {
         rc = out->info(out, "Instance: %s", pcmk__s(instance, "<unknown>"));
     }
     if (pcmk_is_set(flags, shadow_disp_file)) {
         rc = out->info(out, "File name: %s", pcmk__s(filename, "<unknown>"));
     }
     if (pcmk_is_set(flags, shadow_disp_content)) {
         rc = out->info(out, "Content:");
 
         if (content != NULL) {
-            gchar *buf = pcmk__xml_dump(content,
-                                        pcmk__xml_fmt_pretty
-                                        |pcmk__xml_fmt_text);
+            GString *buf = g_string_sized_new(1024);
+            gchar *str = NULL;
 
-            buf = pcmk__trim(buf);
-            if (!pcmk__str_empty(buf)) {
-                out->info(out, "%s", buf);
+            pcmk__xml_string(content, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text,
+                             buf, 0);
+
+            str = g_string_free(buf, FALSE);
+            str = pcmk__trim(str);
+            if (!pcmk__str_empty(str)) {
+                out->info(out, "%s", str);
             }
-            g_free(buf);
+            g_free(str);
 
         } else {
             out->info(out, "<unknown>");
         }
     }
     if (pcmk_is_set(flags, shadow_disp_diff)) {
         rc = out->info(out, "Diff:");
 
         if (diff != NULL) {
             out->message(out, "xml-patchset", diff);
         } else {
             out->info(out, "<empty>");
         }
     }
 
     return rc;
 }
 
 /*!
  * \internal
  * \brief Display information about a shadow CIB instance
  *
  * \param[in,out] out   Output object
  * \param[in]     args  Message-specific arguments
  *
  * \return Standard Pacemaker return code
  *
  * \note \p args should contain the following:
  *       -# Instance name (can be \p NULL)
  *       -# Shadow file name (can be \p NULL)
  *       -# Shadow file content (can be \p NULL)
  *       -# Patchset containing the changes in the shadow CIB (can be \p NULL)
  *       -# Group of \p shadow_disp_flags indicating which fields to display
  */
 PCMK__OUTPUT_ARGS("shadow", "const char *", "const char *", "const xmlNode *",
                   "const xmlNode *", "enum shadow_disp_flags")
 static int
 shadow_text(pcmk__output_t *out, va_list args)
 {
     if (!out->is_quiet(out)) {
         return shadow_default(out, args);
 
     } else {
         const char *instance = va_arg(args, const char *);
         const char *filename = va_arg(args, const char *);
         const xmlNode *content = va_arg(args, const xmlNode *);
         const xmlNode *diff = va_arg(args, const xmlNode *);
         enum shadow_disp_flags flags = (enum shadow_disp_flags) va_arg(args, int);
 
         int rc = pcmk_rc_no_output;
         bool quiet_orig = out->quiet;
 
         /* We have to disable quiet mode for the "xml-patchset" message if we
          * call it, so we might as well do so for this whole section.
          */
         out->quiet = false;
 
         if (pcmk_is_set(flags, shadow_disp_instance) && (instance != NULL)) {
             rc = out->info(out, "%s", instance);
         }
         if (pcmk_is_set(flags, shadow_disp_file) && (filename != NULL)) {
             rc = out->info(out, "%s", filename);
         }
         if (pcmk_is_set(flags, shadow_disp_content) && (content != NULL)) {
-            gchar *buf = pcmk__xml_dump(content,
-                                        pcmk__xml_fmt_pretty
-                                        |pcmk__xml_fmt_text);
+            GString *buf = g_string_sized_new(1024);
+            gchar *str = NULL;
+
+            pcmk__xml_string(content, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text,
+                             buf, 0);
 
-            buf = pcmk__trim(buf);
-            rc = out->info(out, "%s", buf);
-            g_free(buf);
+            str = g_string_free(buf, FALSE);
+            str = pcmk__trim(str);
+            rc = out->info(out, "%s", str);
+            g_free(str);
         }
         if (pcmk_is_set(flags, shadow_disp_diff) && (diff != NULL)) {
             rc = out->message(out, "xml-patchset", diff);
         }
 
         out->quiet = quiet_orig;
         return rc;
     }
 }
 
 /*!
  * \internal
  * \brief Display information about a shadow CIB instance
  *
  * \param[in,out] out   Output object
  * \param[in]     args  Message-specific arguments
  *
  * \return Standard Pacemaker return code
  *
  * \note \p args should contain the following:
  *       -# Instance name (can be \p NULL)
  *       -# Shadow file name (can be \p NULL)
  *       -# Shadow file content (can be \p NULL)
  *       -# Patchset containing the changes in the shadow CIB (can be \p NULL)
  *       -# Group of \p shadow_disp_flags indicating which fields to display
  *          (ignored)
  */
 PCMK__OUTPUT_ARGS("shadow", "const char *", "const char *", "const xmlNode *",
                   "const xmlNode *", "enum shadow_disp_flags")
 static int
 shadow_xml(pcmk__output_t *out, va_list args)
 {
     const char *instance = va_arg(args, const char *);
     const char *filename = va_arg(args, const char *);
     const xmlNode *content = va_arg(args, const xmlNode *);
     const xmlNode *diff = va_arg(args, const xmlNode *);
     enum shadow_disp_flags flags G_GNUC_UNUSED =
         (enum shadow_disp_flags) va_arg(args, int);
 
     pcmk__output_xml_create_parent(out, PCMK_XE_SHADOW,
                                    PCMK_XA_INSTANCE, instance,
                                    PCMK_XA_FILE, filename,
                                    NULL);
 
     if (content != NULL) {
-        gchar *buf = pcmk__xml_dump(content,
-                                    pcmk__xml_fmt_pretty|pcmk__xml_fmt_text);
+        GString *buf = g_string_sized_new(1024);
+
+        pcmk__xml_string(content, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, buf,
+                         0);
 
-        out->output_xml(out, PCMK_XE_CONTENT, buf);
-        g_free(buf);
+        out->output_xml(out, PCMK_XE_CONTENT, buf->str);
+        g_string_free(buf, TRUE);
     }
 
     if (diff != NULL) {
         out->message(out, "xml-patchset", diff);
     }
 
     pcmk__output_xml_pop_parent(out);
     return pcmk_rc_ok;
 }
 
 static const pcmk__supported_format_t formats[] = {
     PCMK__SUPPORTED_FORMAT_NONE,
     PCMK__SUPPORTED_FORMAT_TEXT,
     PCMK__SUPPORTED_FORMAT_XML,
     { NULL, NULL, NULL }
 };
 
 static const pcmk__message_entry_t fmt_functions[] = {
     { "instruction", "default", instruction_default },
     { "instruction", "xml", instruction_xml },
     { "shadow", "default", shadow_default },
     { "shadow", "text", shadow_text },
     { "shadow", "xml", shadow_xml },
 
     { NULL, NULL, NULL }
 };
 
 /*!
  * \internal
  * \brief Set the error when \p --force is not passed with a dangerous command
  *
  * \param[in]  reason         Why command is dangerous
  * \param[in]  for_shadow     If true, command is dangerous to the shadow file.
  *                            Otherwise, command is dangerous to the active
  *                            cluster.
  * \param[in]  show_mismatch  If true and the supplied shadow instance is not
  *                            the same as the active shadow instance, report
  *                            this
  * \param[out] error          Where to store error
  */
 static void
 set_danger_error(const char *reason, bool for_shadow, bool show_mismatch,
                  GError **error)
 {
     const char *active = getenv("CIB_shadow");
     char *full = NULL;
 
     if (show_mismatch
         && !pcmk__str_eq(active, options.instance, pcmk__str_null_matches)) {
 
         full = crm_strdup_printf("%s.\nAdditionally, the supplied shadow "
                                  "instance (%s) is not the same as the active "
                                  "one (%s)",
                                 reason, options.instance, active);
         reason = full;
     }
 
     g_set_error(error, PCMK__EXITC_ERROR, exit_code,
                 "%s%sTo prevent accidental destruction of the %s, the --force "
                 "flag is required in order to proceed.",
                 pcmk__s(reason, ""), ((reason != NULL)? ".\n" : ""),
                 (for_shadow? "shadow file" : "cluster"));
     free(full);
 }
 
 /*!
  * \internal
  * \brief Get the active shadow instance from the environment
  *
  * This sets \p options.instance to the value of the \p CIB_shadow env variable.
  *
  * \param[out] error  Where to store error
  */
 static int
 get_instance_from_env(GError **error)
 {
     int rc = pcmk_rc_ok;
 
     pcmk__str_update(&options.instance, getenv("CIB_shadow"));
     if (options.instance == NULL) {
         rc = ENXIO;
         exit_code = pcmk_rc2exitc(rc);
         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
                     "No active shadow configuration defined");
     }
     return rc;
 }
 
 /*!
  * \internal
  * \brief Validate that the shadow file does or does not exist, as appropriate
  *
  * \param[in]  filename      Absolute path of shadow file
  * \param[in]  should_exist  Whether the shadow file is expected to exist
  * \param[out] error         Where to store error
  *
  * \return Standard Pacemaker return code
  */
 static int
 check_file_exists(const char *filename, bool should_exist, GError **error)
 {
     struct stat buf;
 
     if (!should_exist && (stat(filename, &buf) == 0)) {
         char *reason = crm_strdup_printf("A shadow instance '%s' already "
                                          "exists", options.instance);
 
         exit_code = CRM_EX_CANTCREAT;
         set_danger_error(reason, true, false, error);
         free(reason);
         return EEXIST;
     }
 
     if (should_exist && (stat(filename, &buf) < 0)) {
         // @COMPAT: Use pcmk_rc2exitc(errno)?
         exit_code = CRM_EX_NOSUCH;
         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
                     "Could not access shadow instance '%s': %s",
                     options.instance, strerror(errno));
         return errno;
     }
 
     return pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Connect to the "real" (non-shadow) CIB
  *
  * \param[out] real_cib  Where to store CIB connection
  * \param[out] error     Where to store error
  *
  * \return Standard Pacemaker return code
  */
 static int
 connect_real_cib(cib_t **real_cib, GError **error)
 {
     int rc = pcmk_rc_ok;
 
     *real_cib = cib_new_no_shadow();
     if (*real_cib == NULL) {
         rc = ENOMEM;
         exit_code = pcmk_rc2exitc(rc);
         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
                     "Could not create a CIB connection object");
         return rc;
     }
 
     rc = (*real_cib)->cmds->signon(*real_cib, crm_system_name, cib_command);
     rc = pcmk_legacy2rc(rc);
     if (rc != pcmk_rc_ok) {
         exit_code = pcmk_rc2exitc(rc);
         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
                     "Could not connect to CIB: %s", pcmk_rc_str(rc));
     }
     return rc;
 }
 
 /*!
  * \internal
  * \brief Query the "real" (non-shadow) CIB and store the result
  *
  * \param[out]    output    Where to store query output
  * \param[out]    error     Where to store error
  *
  * \return Standard Pacemaker return code
  */
 static int
 query_real_cib(xmlNode **output, GError **error)
 {
     cib_t *real_cib = NULL;
     int rc = connect_real_cib(&real_cib, error);
 
     if (rc != pcmk_rc_ok) {
         goto done;
     }
 
     rc = real_cib->cmds->query(real_cib, NULL, output, options.cmd_options);
     rc = pcmk_legacy2rc(rc);
     if (rc != pcmk_rc_ok) {
         exit_code = pcmk_rc2exitc(rc);
         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
                     "Could not query the non-shadow CIB: %s", pcmk_rc_str(rc));
     }
 
 done:
     cib_delete(real_cib);
     return rc;
 }
 
 /*!
  * \internal
  * \brief Read XML from the given file
  *
  * \param[in]  filename  Path of input file
  * \param[out] output    Where to store XML read from \p filename
  * \param[out] error     Where to store error
  *
  * \return Standard Pacemaker return code
  */
 static int
 read_xml(const char *filename, xmlNode **output, GError **error)
 {
     int rc = pcmk_rc_ok;
 
     *output = pcmk__xml_read(filename);
     if (*output == NULL) {
         rc = pcmk_rc_no_input;
         exit_code = pcmk_rc2exitc(rc);
         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
                     "Could not parse XML from input file '%s'", filename);
     }
     return rc;
 }
 
 /*!
  * \internal
  * \brief Write the shadow XML to a file
  *
  * \param[in]  xml       Shadow XML
  * \param[in]  filename  Name of destination file
  * \param[in]  reset     Whether the write is a reset (for logging only)
  * \param[out] error     Where to store error
  */
 static int
 write_shadow_file(const xmlNode *xml, const char *filename, bool reset,
                   GError **error)
 {
     int rc = pcmk__xml_write_file(xml, filename, false, NULL);
 
     if (rc != pcmk_rc_ok) {
         exit_code = pcmk_rc2exitc(rc);
         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
                     "Could not %s the shadow instance '%s': %s",
                     reset? "reset" : "create", options.instance,
                     pcmk_rc_str(rc));
     }
     return rc;
 }
 
 /*!
  * \internal
  * \brief Create a shell prompt based on the given shadow instance name
  *
  * \return Newly created prompt
  *
  * \note The caller is responsible for freeing the return value using \p free().
  */
 static inline char *
 get_shadow_prompt(void)
 {
     return crm_strdup_printf("shadow[%.40s] # ", options.instance);
 }
 
 /*!
  * \internal
  * \brief Set up environment variables for a shadow instance
  *
  * \param[in,out] out      Output object
  * \param[in]     do_switch  If true, switch to an existing instance (logging
  *                           only)
  * \param[out]    error      Where to store error
  */
 static void
 shadow_setup(pcmk__output_t *out, bool do_switch, GError **error)
 {
     const char *active = getenv("CIB_shadow");
     const char *prompt = getenv("PS1");
     const char *shell = getenv("SHELL");
     char *new_prompt = get_shadow_prompt();
 
     if (pcmk__str_eq(active, options.instance, pcmk__str_none)
         && pcmk__str_eq(new_prompt, prompt, pcmk__str_none)) {
         // CIB_shadow and prompt environment variables are already set up
         goto done;
     }
 
     if (!options.batch && (shell != NULL)) {
         out->info(out, "Setting up shadow instance");
         setenv("PS1", new_prompt, 1);
         setenv("CIB_shadow", options.instance, 1);
 
         out->message(out, PCMK_XE_INSTRUCTION,
                      "Press Ctrl+D to exit the crm_shadow shell");
 
         if (pcmk__str_eq(shell, "(^|/)bash$", pcmk__str_regex)) {
             execl(shell, shell, "--norc", "--noprofile", NULL);
         } else {
             execl(shell, shell, NULL);
         }
 
         exit_code = pcmk_rc2exitc(errno);
         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
                     "Failed to launch shell '%s': %s",
                     shell, pcmk_rc_str(errno));
 
     } else {
         char *msg = NULL;
         const char *prefix = "A new shadow instance was created. To begin "
                              "using it";
 
         if (do_switch) {
             prefix = "To switch to the named shadow instance";
         }
 
         msg = crm_strdup_printf("%s, enter the following into your shell:\n"
                                 "\texport CIB_shadow=%s",
                                 prefix, options.instance);
         out->message(out, "instruction", msg);
         free(msg);
     }
 
 done:
     free(new_prompt);
 }
 
 /*!
  * \internal
  * \brief Remind the user to clean up the shadow environment
  *
  * \param[in,out] out  Output object
  */
 static void
 shadow_teardown(pcmk__output_t *out)
 {
     const char *active = getenv("CIB_shadow");
     const char *prompt = getenv("PS1");
 
     if (pcmk__str_eq(active, options.instance, pcmk__str_none)) {
         char *our_prompt = get_shadow_prompt();
 
         if (pcmk__str_eq(prompt, our_prompt, pcmk__str_none)) {
             out->message(out, "instruction",
                          "Press Ctrl+D to exit the crm_shadow shell");
 
         } else {
             out->message(out, "instruction",
                          "Remember to unset the CIB_shadow variable by "
                          "entering the following into your shell:\n"
                          "\tunset CIB_shadow");
         }
         free(our_prompt);
     }
 }
 
 /*!
  * \internal
  * \brief Commit the shadow file contents to the active cluster
  *
  * \param[out] error  Where to store error
  */
 static void
 commit_shadow_file(GError **error)
 {
     char *filename = NULL;
     cib_t *real_cib = NULL;
 
     xmlNodePtr input = NULL;
     xmlNodePtr section_xml = NULL;
     const char *section = NULL;
 
     int rc = pcmk_rc_ok;
 
     if (!options.force) {
         const char *reason = "The commit command overwrites the active cluster "
                              "configuration";
 
         exit_code = CRM_EX_USAGE;
         set_danger_error(reason, false, true, error);
         return;
     }
 
     filename = get_shadow_file(options.instance);
     if (check_file_exists(filename, true, error) != pcmk_rc_ok) {
         goto done;
     }
 
     if (connect_real_cib(&real_cib, error) != pcmk_rc_ok) {
         goto done;
     }
 
     if (read_xml(filename, &input, error) != pcmk_rc_ok) {
         goto done;
     }
 
     section_xml = input;
 
     if (!options.full_upload) {
         section = PCMK_XE_CONFIGURATION;
         section_xml = first_named_child(input, section);
     }
 
     rc = real_cib->cmds->replace(real_cib, section, section_xml,
                                  options.cmd_options);
     rc = pcmk_legacy2rc(rc);
 
     if (rc != pcmk_rc_ok) {
         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.instance, pcmk_rc_str(rc));
     }
 
 done:
     free(filename);
     cib_delete(real_cib);
     free_xml(input);
 }
 
 /*!
  * \internal
  * \brief Create a new empty shadow instance
  *
  * \param[in,out] out    Output object
  * \param[out]    error  Where to store error
  *
  * \note If \p --force is given, we try to write the file regardless of whether
  *       it already exists.
  */
 static void
 create_shadow_empty(pcmk__output_t *out, GError **error)
 {
     char *filename = get_shadow_file(options.instance);
     xmlNode *output = NULL;
 
     if (!options.force
         && (check_file_exists(filename, false, error) != pcmk_rc_ok)) {
         goto done;
     }
 
     output = createEmptyCib(0);
     crm_xml_add(output, PCMK_XA_VALIDATE_WITH, options.validate_with);
     out->info(out, "Created new %s configuration",
               crm_element_value(output, PCMK_XA_VALIDATE_WITH));
 
     if (write_shadow_file(output, filename, false, error) != pcmk_rc_ok) {
         goto done;
     }
     shadow_setup(out, false, error);
 
 done:
     free(filename);
     free_xml(output);
 }
 
 /*!
  * \internal
  * \brief Create a shadow instance based on the active CIB
  *
  * \param[in,out] out    Output object
  * \param[in]     reset  If true, overwrite the given existing shadow instance.
  *                       Otherwise, create a new shadow instance with the given
  *                       name.
  * \param[out]    error  Where to store error
  *
  * \note If \p --force is given, we try to write the file regardless of whether
  *       it already exists.
  */
 static void
 create_shadow_from_cib(pcmk__output_t *out, bool reset, GError **error)
 {
     char *filename = get_shadow_file(options.instance);
     xmlNode *output = NULL;
 
     if (!options.force) {
         if (reset) {
             /* @COMPAT: Reset is dangerous to the shadow file, but to preserve
              * compatibility we can't require --force unless there's a mismatch.
              * At a compatibility break, call set_danger_error() with for_shadow
              * and show_mismatch set to true.
              */
             const char *local = getenv("CIB_shadow");
 
             if (!pcmk__str_eq(local, options.instance, pcmk__str_null_matches)) {
                 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 shadow "
                             "file, the --force flag is required in order to "
                             "proceed.",
                             options.instance, local);
                 goto done;
             }
         }
 
         if (check_file_exists(filename, reset, error) != pcmk_rc_ok) {
             goto done;
         }
     }
 
     if (query_real_cib(&output, error) != pcmk_rc_ok) {
         goto done;
     }
 
     if (write_shadow_file(output, filename, reset, error) != pcmk_rc_ok) {
         goto done;
     }
     shadow_setup(out, false, error);
 
 done:
     free(filename);
     free_xml(output);
 }
 
 /*!
  * \internal
  * \brief Delete the shadow file
  *
  * \param[in,out] out  Output object
  * \param[out]    error  Where to store error
  */
 static void
 delete_shadow_file(pcmk__output_t *out, GError **error)
 {
     char *filename = NULL;
 
     if (!options.force) {
         const char *reason = "The delete command removes the specified shadow "
                              "file";
 
         exit_code = CRM_EX_USAGE;
         set_danger_error(reason, true, true, error);
         return;
     }
 
     filename = get_shadow_file(options.instance);
 
     if ((unlink(filename) < 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.instance, strerror(errno));
     } else {
         shadow_teardown(out);
     }
     free(filename);
 }
 
 /*!
  * \internal
  * \brief Open the shadow file in a text editor
  *
  * \param[out] error  Where to store error
  *
  * \note The \p EDITOR environment variable must be set.
  */
 static void
 edit_shadow_file(GError **error)
 {
     char *filename = NULL;
     const char *editor = NULL;
 
     if (get_instance_from_env(error) != pcmk_rc_ok) {
         return;
     }
 
     filename = get_shadow_file(options.instance);
     if (check_file_exists(filename, true, error) != pcmk_rc_ok) {
         goto done;
     }
 
     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, "--", filename, NULL);
     exit_code = CRM_EX_OSFILE;
     g_set_error(error, PCMK__EXITC_ERROR, exit_code,
                 "Could not invoke EDITOR (%s %s): %s",
                 editor, filename, strerror(errno));
 
 done:
     free(filename);
 }
 
 /*!
  * \internal
  * \brief Show the contents of the active shadow instance
  *
  * \param[in,out] out    Output object
  * \param[out]    error  Where to store error
  */
 static void
 show_shadow_contents(pcmk__output_t *out, GError **error)
 {
     char *filename = NULL;
 
     if (get_instance_from_env(error) != pcmk_rc_ok) {
         return;
     }
 
     filename = get_shadow_file(options.instance);
 
     if (check_file_exists(filename, true, error) == pcmk_rc_ok) {
         xmlNode *output = NULL;
         bool quiet_orig = out->quiet;
 
         if (read_xml(filename, &output, error) != pcmk_rc_ok) {
             goto done;
         }
 
         out->quiet = true;
         out->message(out, "shadow",
                      options.instance, NULL, output, NULL, shadow_disp_content);
         out->quiet = quiet_orig;
 
         free_xml(output);
     }
 
 done:
     free(filename);
 }
 
 /*!
  * \internal
  * \brief Show the changes in the active shadow instance
  *
  * \param[in,out] out    Output object
  * \param[out]    error  Where to store error
  */
 static void
 show_shadow_diff(pcmk__output_t *out, GError **error)
 {
     char *filename = NULL;
     xmlNodePtr old_config = NULL;
     xmlNodePtr new_config = NULL;
     xmlNodePtr diff = NULL;
     bool quiet_orig = out->quiet;
 
     if (get_instance_from_env(error) != pcmk_rc_ok) {
         return;
     }
 
     filename = get_shadow_file(options.instance);
     if (check_file_exists(filename, true, error) != pcmk_rc_ok) {
         goto done;
     }
 
     if (query_real_cib(&old_config, error) != pcmk_rc_ok) {
         goto done;
     }
 
     if (read_xml(filename, &new_config, error) != pcmk_rc_ok) {
         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__log_xml_changes(LOG_INFO, new_config);
     xml_accept_changes(new_config);
 
     out->quiet = true;
     out->message(out, "shadow",
                  options.instance, NULL, NULL, diff, shadow_disp_diff);
     out->quiet = quiet_orig;
 
     if (diff != NULL) {
         /* @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).
          */
         exit_code = CRM_EX_ERROR;
     }
 
 done:
     free(filename);
     free_xml(old_config);
     free_xml(new_config);
     free_xml(diff);
 }
 
 /*!
  * \internal
  * \brief Show the absolute path of the active shadow instance
  *
  * \param[in,out] out    Output object
  * \param[out]    error  Where to store error
  */
 static void
 show_shadow_filename(pcmk__output_t *out, GError **error)
 {
     if (get_instance_from_env(error) == pcmk_rc_ok) {
         char *filename = get_shadow_file(options.instance);
         bool quiet_orig = out->quiet;
 
         out->quiet = true;
         out->message(out, "shadow",
                      options.instance, filename, NULL, NULL, shadow_disp_file);
         out->quiet = quiet_orig;
 
         free(filename);
     }
 }
 
 /*!
  * \internal
  * \brief Show the active shadow instance
  *
  * \param[in,out] out    Output object
  * \param[out]    error  Where to store error
  */
 static void
 show_shadow_instance(pcmk__output_t *out, GError **error)
 {
     if (get_instance_from_env(error) == pcmk_rc_ok) {
         bool quiet_orig = out->quiet;
 
         out->quiet = true;
         out->message(out, "shadow",
                      options.instance, NULL, NULL, NULL, shadow_disp_instance);
         out->quiet = quiet_orig;
     }
 }
 
 /*!
  * \internal
  * \brief Switch to the given shadow instance
  *
  * \param[in,out] out    Output object
  * \param[out]    error  Where to store error
  */
 static void
 switch_shadow_instance(pcmk__output_t *out, GError **error)
 {
     char *filename = NULL;
 
     filename = get_shadow_file(options.instance);
     if (check_file_exists(filename, true, error) == pcmk_rc_ok) {
         shadow_setup(out, true, error);
     }
     free(filename);
 }
 
 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)) {
         options.cmd = shadow_cmd_which;
 
     } else if (pcmk__str_any_of(option_name, "-p", "--display", NULL)) {
         options.cmd = shadow_cmd_display;
 
     } else if (pcmk__str_any_of(option_name, "-d", "--diff", NULL)) {
         options.cmd = shadow_cmd_diff;
 
     } else if (pcmk__str_any_of(option_name, "-F", "--file", NULL)) {
         options.cmd = shadow_cmd_file;
 
     } else if (pcmk__str_any_of(option_name, "-c", "--create", NULL)) {
         options.cmd = shadow_cmd_create;
 
     } else if (pcmk__str_any_of(option_name, "-e", "--create-empty", NULL)) {
         options.cmd = shadow_cmd_create_empty;
 
     } else if (pcmk__str_any_of(option_name, "-C", "--commit", NULL)) {
         options.cmd = shadow_cmd_commit;
 
     } else if (pcmk__str_any_of(option_name, "-D", "--delete", NULL)) {
         options.cmd = shadow_cmd_delete;
 
     } else if (pcmk__str_any_of(option_name, "-E", "--edit", NULL)) {
         options.cmd = shadow_cmd_edit;
 
     } else if (pcmk__str_any_of(option_name, "-r", "--reset", NULL)) {
         options.cmd = shadow_cmd_reset;
 
     } else if (pcmk__str_any_of(option_name, "-s", "--switch", NULL)) {
         options.cmd = shadow_cmd_switch;
 
     } else {
         // Should be impossible
         return FALSE;
     }
 
     // optarg may be NULL and that's okay
     pcmk__str_update(&options.instance, 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, GOptionGroup **group)
 {
     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, "text (default), xml", group,
                                       "<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_rc_ok;
     pcmk__output_t *out = 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, "CDcersv");
     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 (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) {
         out->version(out, false);
         goto done;
     }
 
     pcmk__register_messages(out, fmt_functions);
 
     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);
     }
 
     // Run the command
     switch (options.cmd) {
         case shadow_cmd_commit:
             commit_shadow_file(&error);
             break;
         case shadow_cmd_create:
             create_shadow_from_cib(out, false, &error);
             break;
         case shadow_cmd_create_empty:
             create_shadow_empty(out, &error);
             break;
         case shadow_cmd_reset:
             create_shadow_from_cib(out, true, &error);
             break;
         case shadow_cmd_delete:
             delete_shadow_file(out, &error);
             break;
         case shadow_cmd_diff:
             show_shadow_diff(out, &error);
             break;
         case shadow_cmd_display:
             show_shadow_contents(out, &error);
             break;
         case shadow_cmd_edit:
             edit_shadow_file(&error);
             break;
         case shadow_cmd_file:
             show_shadow_filename(out, &error);
             break;
         case shadow_cmd_switch:
             switch_shadow_instance(out, &error);
             break;
         case shadow_cmd_which:
             show_shadow_instance(out, &error);
             break;
         default:
             // Should never reach this point
             break;
     }
 
 done:
     g_strfreev(processed_args);
     pcmk__free_arg_context(context);
 
     pcmk__output_and_clear_error(&error, out);
 
     free(options.instance);
     g_free(options.validate_with);
 
     if (out != NULL) {
         out->finish(out, exit_code, true, NULL);
         pcmk__output_free(out);
     }
 
     crm_exit(exit_code);
 }
diff --git a/tools/crm_ticket.c b/tools/crm_ticket.c
index 5aa62cc68e..44d274828f 100644
--- a/tools/crm_ticket.c
+++ b/tools/crm_ticket.c
@@ -1,1013 +1,1017 @@
 /*
  * Copyright 2012-2024 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 <crm/crm.h>
 
 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 
 #include <stdlib.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <libgen.h>
 
 #include <crm/common/xml.h>
 #include <crm/common/ipc.h>
 #include <crm/common/cmdline_internal.h>
 
 #include <crm/cib.h>
 #include <crm/cib/internal.h>
 #include <crm/pengine/rules.h>
 #include <crm/pengine/status.h>
 #include <crm/pengine/internal.h>
 
 #include <pacemaker-internal.h>
 
 GError *error = NULL;
 
 #define SUMMARY "Perform tasks related to cluster tickets\n\n" \
                 "Allows ticket attributes to be queried, modified and deleted."
 
 struct {
     gchar *attr_default;
     gchar *attr_id;
     char *attr_name;
     char *attr_value;
     gboolean force;
     char *get_attr_name;
     gboolean quiet;
     gchar *set_name;
     char ticket_cmd;
     gchar *ticket_id;
     gchar *xml_file;
 } options = {
     .ticket_cmd = 'S'
 };
 
 GList *attr_delete;
 GHashTable *attr_set;
 bool modified = false;
 int cib_options = cib_sync_call;
 
 #define INDENT "                               "
 
 static gboolean
 attr_value_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
     pcmk__str_update(&options.attr_value, optarg);
 
     if (!options.attr_name || !options.attr_value) {
         return TRUE;
     }
 
     pcmk__insert_dup(attr_set, options.attr_name, options.attr_value);
     pcmk__str_update(&options.attr_name, NULL);
     pcmk__str_update(&options.attr_value, NULL);
 
     modified = true;
 
     return TRUE;
 }
 
 static gboolean
 command_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
     if (pcmk__str_any_of(option_name, "--info", "-l", NULL)) {
         options.ticket_cmd = 'l';
     } else if (pcmk__str_any_of(option_name, "--details", "-L", NULL)) {
         options.ticket_cmd = 'L';
     } else if (pcmk__str_any_of(option_name, "--raw", "-w", NULL)) {
         options.ticket_cmd = 'w';
     } else if (pcmk__str_any_of(option_name, "--query-xml", "-q", NULL)) {
         options.ticket_cmd = 'q';
     } else if (pcmk__str_any_of(option_name, "--constraints", "-c", NULL)) {
         options.ticket_cmd = 'c';
     } else if (pcmk__str_any_of(option_name, "--cleanup", "-C", NULL)) {
         options.ticket_cmd = 'C';
     }
 
     return TRUE;
 }
 
 static gboolean
 delete_attr_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
     attr_delete = g_list_append(attr_delete, strdup(optarg));
     modified = true;
     return TRUE;
 }
 
 static gboolean
 get_attr_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
     pcmk__str_update(&options.get_attr_name, optarg);
     options.ticket_cmd = 'G';
     return TRUE;
 }
 
 static gboolean
 grant_standby_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
     if (pcmk__str_any_of(option_name, "--grant", "-g", NULL)) {
         pcmk__insert_dup(attr_set, PCMK__XA_GRANTED, PCMK_VALUE_TRUE);
         modified = true;
     } else if (pcmk__str_any_of(option_name, "--revoke", "-r", NULL)) {
         pcmk__insert_dup(attr_set, PCMK__XA_GRANTED, PCMK_VALUE_FALSE);
         modified = true;
     } else if (pcmk__str_any_of(option_name, "--standby", "-s", NULL)) {
         pcmk__insert_dup(attr_set, PCMK_XA_STANDBY, PCMK_VALUE_TRUE);
         modified = true;
     } else if (pcmk__str_any_of(option_name, "--activate", "-a", NULL)) {
         pcmk__insert_dup(attr_set, PCMK_XA_STANDBY, PCMK_VALUE_FALSE);
         modified = true;
     }
 
     return TRUE;
 }
 
 static gboolean
 set_attr_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
     pcmk__str_update(&options.attr_name, optarg);
 
     if (!options.attr_name || !options.attr_value) {
         return TRUE;
     }
 
     pcmk__insert_dup(attr_set, options.attr_name, options.attr_value);
     pcmk__str_update(&options.attr_name, NULL);
     pcmk__str_update(&options.attr_value, NULL);
 
     modified = true;
 
     return TRUE;
 }
 
 static GOptionEntry query_entries[] = {
     { "info", 'l', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Display the information of ticket(s)",
       NULL },
 
     { "details", 'L', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Display the details of ticket(s)",
       NULL },
 
     { "raw", 'w', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Display the IDs of ticket(s)",
       NULL },
 
     { "query-xml", 'q', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Query the XML of ticket(s)",
       NULL },
 
     { "constraints", 'c', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Display the " PCMK_XE_RSC_TICKET " constraints that apply to ticket(s)",
       NULL },
 
     { NULL }
 };
 
 static GOptionEntry command_entries[] = {
     { "grant", 'g', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, grant_standby_cb,
       "Grant a ticket to this cluster site",
       NULL },
 
     { "revoke", 'r', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, grant_standby_cb,
       "Revoke a ticket from this cluster site",
       NULL },
 
     { "standby", 's', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, grant_standby_cb,
       "Tell this cluster site this ticket is standby",
       NULL },
 
     { "activate", 'a', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, grant_standby_cb,
       "Tell this cluster site this ticket is active",
       NULL },
 
     { NULL }
 };
 
 static GOptionEntry advanced_entries[] = {
     { "get-attr", 'G', 0, G_OPTION_ARG_CALLBACK, get_attr_cb,
       "Display the named attribute for a ticket",
       "ATTRIBUTE" },
 
     { "set-attr", 'S', 0, G_OPTION_ARG_CALLBACK, set_attr_cb,
       "Set the named attribute for a ticket",
       "ATTRIBUTE" },
 
     { "delete-attr", 'D', 0, G_OPTION_ARG_CALLBACK, delete_attr_cb,
       "Delete the named attribute for a ticket",
       "ATTRIBUTE" },
 
     { "cleanup", 'C', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Delete all state of a ticket at this cluster site",
       NULL },
 
     { NULL}
 };
 
 static GOptionEntry addl_entries[] = {
     { "attr-value", 'v', 0, G_OPTION_ARG_CALLBACK, attr_value_cb,
       "Attribute value to use with -S",
       "VALUE" },
 
     { "default", 'd', 0, G_OPTION_ARG_STRING, &options.attr_default,
       "(Advanced) Default attribute value to display if none is found\n"
       INDENT "(for use with -G)",
       "VALUE" },
 
     { "force", 'f', 0, G_OPTION_ARG_NONE, &options.force,
       "(Advanced) Force the action to be performed",
       NULL },
 
     { "ticket", 't', 0, G_OPTION_ARG_STRING, &options.ticket_id,
       "Ticket ID",
       "ID" },
 
     { "xml-file", 'x', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &options.xml_file,
       NULL,
       NULL },
 
     { NULL }
 };
 
 static GOptionEntry deprecated_entries[] = {
     { "set-name", 'n', 0, G_OPTION_ARG_STRING, &options.set_name,
       "(Advanced) ID of the " PCMK_XE_INSTANCE_ATTRIBUTES " object to change",
       "ID" },
 
     { "nvpair", 'i', 0, G_OPTION_ARG_STRING, &options.attr_id,
       "(Advanced) ID of the nvpair object to change/delete",
       "ID" },
 
     { "quiet", 'Q', 0, G_OPTION_ARG_NONE, &options.quiet,
       "Print only the value on stdout",
       NULL },
 
     { NULL }
 };
 
 static pcmk_ticket_t *
 find_ticket(gchar *ticket_id, pcmk_scheduler_t *scheduler)
 {
     return g_hash_table_lookup(scheduler->tickets, ticket_id);
 }
 
 static void
 print_date(time_t time)
 {
     int lpc = 0;
     char date_str[26];
 
     asctime_r(localtime(&time), date_str);
     for (; lpc < 26; lpc++) {
         if (date_str[lpc] == '\n') {
             date_str[lpc] = 0;
         }
     }
     fprintf(stdout, "'%s'", date_str);
 }
 
 static void
 print_ticket(pcmk_ticket_t *ticket, bool raw, bool details)
 {
     if (raw) {
         fprintf(stdout, "%s\n", ticket->id);
         return;
     }
 
     fprintf(stdout, "%s\t%s %s",
             ticket->id, ticket->granted ? "granted" : "revoked",
             ticket->standby ? "[standby]" : "         ");
 
     if (details && g_hash_table_size(ticket->state) > 0) {
         GHashTableIter iter;
         const char *name = NULL;
         const char *value = NULL;
         int lpc = 0;
 
         fprintf(stdout, " (");
 
         g_hash_table_iter_init(&iter, ticket->state);
         while (g_hash_table_iter_next(&iter, (void **)&name, (void **)&value)) {
             if (lpc > 0) {
                 fprintf(stdout, ", ");
             }
             fprintf(stdout, "%s=", name);
             if (pcmk__str_any_of(name, PCMK_XA_LAST_GRANTED, "expires", NULL)) {
                 long long time_ll;
 
                 pcmk__scan_ll(value, &time_ll, 0);
                 print_date((time_t) time_ll);
             } else {
                 fprintf(stdout, "%s", value);
             }
             lpc++;
         }
 
         fprintf(stdout, ")\n");
 
     } else {
         if (ticket->last_granted > -1) {
             fprintf(stdout, " " PCMK_XA_LAST_GRANTED "=");
             print_date(ticket->last_granted);
         }
         fprintf(stdout, "\n");
     }
 
     return;
 }
 
 static void
 print_ticket_list(pcmk_scheduler_t *scheduler, bool raw, bool details)
 {
     GHashTableIter iter;
     pcmk_ticket_t *ticket = NULL;
 
     g_hash_table_iter_init(&iter, scheduler->tickets);
 
     while (g_hash_table_iter_next(&iter, NULL, (void **)&ticket)) {
         print_ticket(ticket, raw, details);
     }
 }
 
 static int
 find_ticket_state(cib_t * the_cib, gchar *ticket_id, xmlNode ** ticket_state_xml)
 {
     int rc = pcmk_rc_ok;
     xmlNode *xml_search = NULL;
 
     GString *xpath = NULL;
 
     CRM_ASSERT(ticket_state_xml != NULL);
     *ticket_state_xml = NULL;
 
     xpath = g_string_sized_new(1024);
     g_string_append(xpath,
                     "/" PCMK_XE_CIB "/" PCMK_XE_STATUS "/" PCMK_XE_TICKETS);
 
     if (ticket_id != NULL) {
         pcmk__g_strcat(xpath,
                        "/" PCMK__XE_TICKET_STATE
                        "[@" PCMK_XA_ID "=\"", ticket_id, "\"]", NULL);
     }
 
     rc = the_cib->cmds->query(the_cib, (const char *) xpath->str, &xml_search,
                               cib_sync_call | cib_scope_local | cib_xpath);
     rc = pcmk_legacy2rc(rc);
     g_string_free(xpath, TRUE);
 
     if (rc != pcmk_rc_ok) {
         return rc;
     }
 
     crm_log_xml_debug(xml_search, "Match");
     if (xml_search->children != NULL) {
         if (ticket_id) {
             fprintf(stdout,
                     "Multiple " PCMK__XE_TICKET_STATE "s match ticket_id=%s\n",
                     ticket_id);
         }
         *ticket_state_xml = xml_search;
     } else {
         *ticket_state_xml = xml_search;
     }
     return rc;
 }
 
 static int
 find_ticket_constraints(cib_t * the_cib, gchar *ticket_id, xmlNode ** ticket_cons_xml)
 {
     int rc = pcmk_rc_ok;
     xmlNode *xml_search = NULL;
 
     GString *xpath = NULL;
     const char *xpath_base = NULL;
 
     CRM_ASSERT(ticket_cons_xml != NULL);
     *ticket_cons_xml = NULL;
 
     xpath_base = pcmk_cib_xpath_for(PCMK_XE_CONSTRAINTS);
     if (xpath_base == NULL) {
         crm_err(PCMK_XE_CONSTRAINTS " CIB element not known (bug?)");
         return -ENOMSG;
     }
 
     xpath = g_string_sized_new(1024);
     pcmk__g_strcat(xpath, xpath_base, "/" PCMK_XE_RSC_TICKET, NULL);
 
     if (ticket_id != NULL) {
         pcmk__g_strcat(xpath, "[@" PCMK_XA_TICKET "=\"", ticket_id, "\"]",
                        NULL);
     }
 
     rc = the_cib->cmds->query(the_cib, (const char *) xpath->str, &xml_search,
                               cib_sync_call | cib_scope_local | cib_xpath);
     rc = pcmk_legacy2rc(rc);
     g_string_free(xpath, TRUE);
 
     if (rc != pcmk_rc_ok) {
         return rc;
     }
 
     crm_log_xml_debug(xml_search, "Match");
     *ticket_cons_xml = xml_search;
 
     return rc;
 }
 
 static int
 dump_ticket_xml(cib_t * the_cib, gchar *ticket_id)
 {
     int rc = pcmk_rc_ok;
     xmlNode *state_xml = NULL;
 
     rc = find_ticket_state(the_cib, ticket_id, &state_xml);
 
     if (state_xml == NULL) {
         return rc;
     }
 
     fprintf(stdout, "State XML:\n");
     if (state_xml) {
-        gchar *state_xml_str = pcmk__xml_dump(state_xml, pcmk__xml_fmt_pretty);
+        GString *buf = g_string_sized_new(1024);
 
-        fprintf(stdout, "\n%s", state_xml_str);
+        pcmk__xml_string(state_xml, pcmk__xml_fmt_pretty, buf, 0);
+
+        fprintf(stdout, "\n%s", buf->str);
         free_xml(state_xml);
-        g_free(state_xml_str);
+        g_string_free(buf, TRUE);
     }
 
     return rc;
 }
 
 static int
 dump_constraints(cib_t * the_cib, gchar *ticket_id)
 {
     int rc = pcmk_rc_ok;
     xmlNode *cons_xml = NULL;
-    gchar *cons_xml_str = NULL;
+    GString *buf = NULL;
 
     rc = find_ticket_constraints(the_cib, ticket_id, &cons_xml);
 
     if (cons_xml == NULL) {
         return rc;
     }
 
-    cons_xml_str = pcmk__xml_dump(cons_xml, pcmk__xml_fmt_pretty);
-    fprintf(stdout, "Constraints XML:\n\n%s", cons_xml_str);
+    buf = g_string_sized_new(1024);
+    pcmk__xml_string(cons_xml, pcmk__xml_fmt_pretty, buf, 0);
+
+    fprintf(stdout, "Constraints XML:\n\n%s", buf->str);
     free_xml(cons_xml);
-    g_free(cons_xml_str);
+    g_string_free(buf, TRUE);
 
     return rc;
 }
 
 static int
 get_ticket_state_attr(gchar *ticket_id, const char *attr_name, const char **attr_value,
                       pcmk_scheduler_t *scheduler)
 {
     pcmk_ticket_t *ticket = NULL;
 
     CRM_ASSERT(attr_value != NULL);
     *attr_value = NULL;
 
     ticket = g_hash_table_lookup(scheduler->tickets, ticket_id);
     if (ticket == NULL) {
         return ENXIO;
     }
 
     *attr_value = g_hash_table_lookup(ticket->state, attr_name);
     if (*attr_value == NULL) {
         return ENXIO;
     }
 
     return pcmk_rc_ok;
 }
 
 static void
 ticket_warning(gchar *ticket_id, const char *action)
 {
     GString *warning = g_string_sized_new(1024);
     const char *word = NULL;
 
     CRM_ASSERT(action != NULL);
 
     if (strcmp(action, "grant") == 0) {
         pcmk__g_strcat(warning,
                        "This command cannot help you verify whether '",
                        ticket_id,
                        "' has been already granted elsewhere.\n", NULL);
         word = "to";
 
     } else {
         pcmk__g_strcat(warning,
                        "Revoking '", ticket_id, "' can trigger the specified "
                        "'" PCMK_XA_LOSS_POLICY "'(s) "
                        "relating to '", ticket_id, "'.\n\n"
                        "You can check that with:\n"
                        "crm_ticket --ticket ", ticket_id, " --constraints\n\n"
                        "Otherwise before revoking '", ticket_id, "', "
                        "you may want to make '", ticket_id, "' "
                        "standby with:\n"
                        "crm_ticket --ticket ", ticket_id, " --standby\n\n",
                        NULL);
         word = "from";
     }
 
     pcmk__g_strcat(warning,
                    "If you really want to ", action, " '", ticket_id, "' ",
                    word, " this site now, and you know what you are doing,\n"
                    "please specify --force.", NULL);
 
     fprintf(stdout, "%s\n", (const char *) warning->str);
 
     g_string_free(warning, TRUE);
 }
 
 static bool
 allow_modification(gchar *ticket_id)
 {
     const char *value = NULL;
     GList *list_iter = NULL;
 
     if (options.force) {
         return true;
     }
 
     if (g_hash_table_lookup_extended(attr_set, PCMK__XA_GRANTED, NULL,
                                      (gpointer *) &value)) {
         if (crm_is_true(value)) {
             ticket_warning(ticket_id, "grant");
             return false;
 
         } else {
             ticket_warning(ticket_id, "revoke");
             return false;
         }
     }
 
     for(list_iter = attr_delete; list_iter; list_iter = list_iter->next) {
         const char *key = (const char *)list_iter->data;
 
         if (pcmk__str_eq(key, PCMK__XA_GRANTED, pcmk__str_none)) {
             ticket_warning(ticket_id, "revoke");
             return false;
         }
     }
 
     return true;
 }
 
 static int
 modify_ticket_state(gchar *ticket_id, cib_t *cib, pcmk_scheduler_t *scheduler)
 {
     int rc = pcmk_rc_ok;
     xmlNode *xml_top = NULL;
     xmlNode *ticket_state_xml = NULL;
     bool found = false;
 
     GList *list_iter = NULL;
     GHashTableIter hash_iter;
 
     char *key = NULL;
     char *value = NULL;
 
     pcmk_ticket_t *ticket = NULL;
 
     rc = find_ticket_state(cib, ticket_id, &ticket_state_xml);
     if (rc == pcmk_rc_ok) {
         crm_debug("Found a match state for ticket: id=%s", ticket_id);
         xml_top = ticket_state_xml;
         found = true;
 
     } else if (rc != ENXIO) {
         return rc;
 
     } else if (g_hash_table_size(attr_set) == 0){
         return pcmk_rc_ok;
 
     } else {
         xmlNode *xml_obj = NULL;
 
         xml_top = create_xml_node(NULL, PCMK_XE_STATUS);
         xml_obj = create_xml_node(xml_top, PCMK_XE_TICKETS);
         ticket_state_xml = create_xml_node(xml_obj, PCMK__XE_TICKET_STATE);
         crm_xml_add(ticket_state_xml, PCMK_XA_ID, ticket_id);
     }
 
     for(list_iter = attr_delete; list_iter; list_iter = list_iter->next) {
         const char *key = (const char *)list_iter->data;
         xml_remove_prop(ticket_state_xml, key);
     }
 
     ticket = find_ticket(ticket_id, scheduler);
 
     g_hash_table_iter_init(&hash_iter, attr_set);
     while (g_hash_table_iter_next(&hash_iter, (gpointer *) & key, (gpointer *) & value)) {
         crm_xml_add(ticket_state_xml, key, value);
 
         if (pcmk__str_eq(key, PCMK__XA_GRANTED, pcmk__str_none)
             && (ticket == NULL || ticket->granted == FALSE)
             && crm_is_true(value)) {
 
             char *now = pcmk__ttoa(time(NULL));
 
             crm_xml_add(ticket_state_xml, PCMK_XA_LAST_GRANTED, now);
             free(now);
         }
     }
 
     if (found && (attr_delete != NULL)) {
         crm_log_xml_debug(xml_top, "Replace");
         rc = cib->cmds->replace(cib, PCMK_XE_STATUS, ticket_state_xml,
                                 cib_options);
         rc = pcmk_legacy2rc(rc);
 
     } else {
         crm_log_xml_debug(xml_top, "Update");
         rc = cib->cmds->modify(cib, PCMK_XE_STATUS, xml_top, cib_options);
         rc = pcmk_legacy2rc(rc);
     }
 
     free_xml(xml_top);
     return rc;
 }
 
 static int
 delete_ticket_state(gchar *ticket_id, cib_t * cib)
 {
     xmlNode *ticket_state_xml = NULL;
 
     int rc = pcmk_rc_ok;
 
     rc = find_ticket_state(cib, ticket_id, &ticket_state_xml);
 
     if (rc == ENXIO) {
         return pcmk_rc_ok;
 
     } else if (rc != pcmk_rc_ok) {
         return rc;
     }
 
     crm_log_xml_debug(ticket_state_xml, "Delete");
 
     rc = cib->cmds->remove(cib, PCMK_XE_STATUS, ticket_state_xml, cib_options);
     rc = pcmk_legacy2rc(rc);
 
     if (rc == pcmk_rc_ok) {
         fprintf(stdout, "Cleaned up %s\n", ticket_id);
     }
 
     free_xml(ticket_state_xml);
     return rc;
 }
 
 static GOptionContext *
 build_arg_context(pcmk__common_args_t *args) {
     GOptionContext *context = NULL;
 
     const char *description = "Examples:\n\n"
                               "Display the info of tickets:\n\n"
                               "\tcrm_ticket --info\n\n"
                               "Display the detailed info of tickets:\n\n"
                               "\tcrm_ticket --details\n\n"
                               "Display the XML of 'ticketA':\n\n"
                               "\tcrm_ticket --ticket ticketA --query-xml\n\n"
                               "Display the " PCMK_XE_RSC_TICKET " constraints that apply to 'ticketA':\n\n"
                               "\tcrm_ticket --ticket ticketA --constraints\n\n"
                               "Grant 'ticketA' to this cluster site:\n\n"
                               "\tcrm_ticket --ticket ticketA --grant\n\n"
                               "Revoke 'ticketA' from this cluster site:\n\n"
                               "\tcrm_ticket --ticket ticketA --revoke\n\n"
                               "Make 'ticketA' standby (the cluster site will treat a granted\n"
                               "'ticketA' as 'standby', and the dependent resources will be\n"
                               "stopped or demoted gracefully without triggering loss-policies):\n\n"
                               "\tcrm_ticket --ticket ticketA --standby\n\n"
                               "Activate 'ticketA' from being standby:\n\n"
                               "\tcrm_ticket --ticket ticketA --activate\n\n"
                               "Get the value of the 'granted' attribute for 'ticketA':\n\n"
                               "\tcrm_ticket --ticket ticketA --get-attr granted\n\n"
                               "Set the value of the 'standby' attribute for 'ticketA':\n\n"
                               "\tcrm_ticket --ticket ticketA --set-attr standby --attr-value true\n\n"
                               "Delete the 'granted' attribute for 'ticketA':\n\n"
                               "\tcrm_ticket --ticket ticketA --delete-attr granted\n\n"
                               "Erase the operation history of 'ticketA' at this cluster site,\n"
                               "causing the cluster site to 'forget' the existing ticket state:\n\n"
                               "\tcrm_ticket --ticket ticketA --cleanup\n\n";
 
     context = pcmk__build_arg_context(args, NULL, NULL, NULL);
     g_option_context_set_description(context, description);
 
     pcmk__add_arg_group(context, "queries", "Queries:",
                         "Show queries", query_entries);
     pcmk__add_arg_group(context, "commands", "Commands:",
                         "Show command options", command_entries);
     pcmk__add_arg_group(context, "advanced", "Advanced Options:",
                         "Show advanced options", advanced_entries);
     pcmk__add_arg_group(context, "additional", "Additional Options:",
                         "Show additional options", addl_entries);
     pcmk__add_arg_group(context, "deprecated", "Deprecated Options:",
                         "Show deprecated options", deprecated_entries);
 
     return context;
 }
 
 int
 main(int argc, char **argv)
 {
     pcmk_scheduler_t *scheduler = NULL;
     xmlNode *cib_xml_copy = NULL;
 
     cib_t *cib_conn = NULL;
     crm_exit_t exit_code = CRM_EX_OK;
     int rc = pcmk_rc_ok;
 
     pcmk__common_args_t *args = NULL;
     GOptionContext *context = NULL;
     gchar **processed_args = NULL;
 
     attr_set = pcmk__strkey_table(free, free);
     attr_delete = NULL;
 
     args = pcmk__new_common_args(SUMMARY);
     context = build_arg_context(args);
     processed_args = pcmk__cmdline_preproc(argv, "dintvxCDGS");
 
     if (!g_option_context_parse_strv(context, &processed_args, &error)) {
         exit_code = CRM_EX_USAGE;
         goto done;
     }
 
     pcmk__cli_init_logging("crm_ticket", args->verbosity);
 
     if (args->version) {
         g_strfreev(processed_args);
         pcmk__free_arg_context(context);
         /* FIXME:  When crm_ticket is converted to use formatted output, this can go. */
         pcmk__cli_help('v');
     }
 
     scheduler = pe_new_working_set();
     if (scheduler == NULL) {
         rc = errno;
         exit_code = pcmk_rc2exitc(rc);
         g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                     "Could not allocate scheduler data: %s", pcmk_rc_str(rc));
         goto done;
     }
     pcmk__set_scheduler_flags(scheduler,
                               pcmk_sched_no_counts|pcmk_sched_no_compat);
 
     cib_conn = cib_new();
     if (cib_conn == NULL) {
         exit_code = CRM_EX_DISCONNECT;
         g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not connect to the CIB manager");
         goto done;
     }
 
     rc = cib_conn->cmds->signon(cib_conn, crm_system_name, cib_command);
     rc = pcmk_legacy2rc(rc);
 
     if (rc != pcmk_rc_ok) {
         exit_code = pcmk_rc2exitc(rc);
         g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not connect to the CIB: %s",
                     pcmk_rc_str(rc));
         goto done;
     }
 
     if (options.xml_file != NULL) {
         cib_xml_copy = pcmk__xml_read(options.xml_file);
 
     } else {
         rc = cib_conn->cmds->query(cib_conn, NULL, &cib_xml_copy, cib_scope_local | cib_sync_call);
         rc = pcmk_legacy2rc(rc);
 
         if (rc != pcmk_rc_ok) {
             exit_code = pcmk_rc2exitc(rc);
             g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not get local CIB: %s",
                         pcmk_rc_str(rc));
             goto done;
         }
     }
 
     if (!cli_config_update(&cib_xml_copy, NULL, FALSE)) {
         exit_code = CRM_EX_CONFIG;
         g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                     "Could not update local CIB to latest schema version");
         goto done;
     }
 
     scheduler->input = cib_xml_copy;
     scheduler->now = crm_time_new(NULL);
 
     cluster_status(scheduler);
 
     /* For recording the tickets that are referenced in PCMK_XE_RSC_TICKET
      * constraints but have never been granted yet.
      */
     pcmk__unpack_constraints(scheduler);
 
     if (options.ticket_cmd == 'l' || options.ticket_cmd == 'L' || options.ticket_cmd == 'w') {
         bool raw = false;
         bool details = false;
 
         if (options.ticket_cmd == 'L') {
             details = true;
         } else if (options.ticket_cmd == 'w') {
             raw = true;
         }
 
         if (options.ticket_id) {
             pcmk_ticket_t *ticket = find_ticket(options.ticket_id, scheduler);
 
             if (ticket == NULL) {
                 exit_code = CRM_EX_NOSUCH;
                 g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                             "No such ticket '%s'", options.ticket_id);
                 goto done;
             }
             print_ticket(ticket, raw, details);
 
         } else {
             print_ticket_list(scheduler, raw, details);
         }
 
     } else if (options.ticket_cmd == 'q') {
         rc = dump_ticket_xml(cib_conn, options.ticket_id);
         exit_code = pcmk_rc2exitc(rc);
 
         if (rc != pcmk_rc_ok) {
             g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                         "Could not query ticket XML: %s", pcmk_rc_str(rc));
         }
 
     } else if (options.ticket_cmd == 'c') {
         rc = dump_constraints(cib_conn, options.ticket_id);
         exit_code = pcmk_rc2exitc(rc);
 
         if (rc != pcmk_rc_ok) {
             g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                         "Could not show ticket constraints: %s", pcmk_rc_str(rc));
         }
 
     } else if (options.ticket_cmd == 'G') {
         const char *value = NULL;
 
         if (options.ticket_id == NULL) {
             exit_code = CRM_EX_NOSUCH;
             g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                         "Must supply ticket ID with -t");
             goto done;
         }
 
         rc = get_ticket_state_attr(options.ticket_id, options.get_attr_name,
                                    &value, scheduler);
         if (rc == pcmk_rc_ok) {
             fprintf(stdout, "%s\n", value);
         } else if (rc == ENXIO && options.attr_default) {
             fprintf(stdout, "%s\n", options.attr_default);
             rc = pcmk_rc_ok;
         }
         exit_code = pcmk_rc2exitc(rc);
 
     } else if (options.ticket_cmd == 'C') {
         if (options.ticket_id == NULL) {
             exit_code = CRM_EX_USAGE;
             g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                         "Must supply ticket ID with -t");
             goto done;
         }
 
         if (options.force == FALSE) {
             pcmk_ticket_t *ticket = NULL;
 
             ticket = find_ticket(options.ticket_id, scheduler);
             if (ticket == NULL) {
                 exit_code = CRM_EX_NOSUCH;
                 g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                             "No such ticket '%s'", options.ticket_id);
                 goto done;
             }
 
             if (ticket->granted) {
                 ticket_warning(options.ticket_id, "revoke");
                 exit_code = CRM_EX_INSUFFICIENT_PRIV;
                 goto done;
             }
         }
 
         rc = delete_ticket_state(options.ticket_id, cib_conn);
         exit_code = pcmk_rc2exitc(rc);
 
         if (rc != pcmk_rc_ok) {
             g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                         "Could not clean up ticket: %s", pcmk_rc_str(rc));
         }
 
     } else if (modified) {
         if (options.ticket_id == NULL) {
             exit_code = CRM_EX_USAGE;
             g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                         "Must supply ticket ID with -t");
             goto done;
         }
 
         if (options.attr_value
             && (pcmk__str_empty(options.attr_name))) {
             exit_code = CRM_EX_USAGE;
             g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                         "Must supply attribute name with -S for -v %s", options.attr_value);
             goto done;
         }
 
         if (options.attr_name
             && (pcmk__str_empty(options.attr_value))) {
             exit_code = CRM_EX_USAGE;
             g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                         "Must supply attribute value with -v for -S %s", options.attr_value);
             goto done;
         }
 
         if (!allow_modification(options.ticket_id)) {
             exit_code = CRM_EX_INSUFFICIENT_PRIV;
             g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                         "Ticket modification not allowed");
             goto done;
         }
 
         rc = modify_ticket_state(options.ticket_id, cib_conn, scheduler);
         exit_code = pcmk_rc2exitc(rc);
 
         if (rc != pcmk_rc_ok) {
             g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                         "Could not modify ticket: %s", pcmk_rc_str(rc));
         }
 
     } else if (options.ticket_cmd == 'S') {
         /* Correct usage was handled in the "if (modified)" block above, so
          * this is just for reporting usage errors
          */
 
         if (pcmk__str_empty(options.attr_name)) {
             // We only get here if ticket_cmd was left as default
             exit_code = CRM_EX_USAGE;
             g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply a command");
             goto done;
         }
 
         if (options.ticket_id == NULL) {
             exit_code = CRM_EX_USAGE;
             g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                         "Must supply ticket ID with -t");
             goto done;
         }
 
         if (pcmk__str_empty(options.attr_value)) {
             exit_code = CRM_EX_USAGE;
             g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                         "Must supply value with -v for -S %s", options.attr_name);
             goto done;
         }
 
     } else {
         exit_code = CRM_EX_USAGE;
         g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                     "Unknown command: %c", options.ticket_cmd);
     }
 
  done:
     if (attr_set) {
         g_hash_table_destroy(attr_set);
     }
     attr_set = NULL;
 
     if (attr_delete) {
         g_list_free_full(attr_delete, free);
     }
     attr_delete = NULL;
 
     pe_free_working_set(scheduler);
     scheduler = NULL;
 
     cib__clean_up_connection(&cib_conn);
 
     g_strfreev(processed_args);
     pcmk__free_arg_context(context);
     g_free(options.attr_default);
     g_free(options.attr_id);
     free(options.attr_name);
     free(options.attr_value);
     free(options.get_attr_name);
     g_free(options.set_name);
     g_free(options.ticket_id);
     g_free(options.xml_file);
 
     pcmk__output_and_clear_error(&error, NULL);
 
     crm_exit(exit_code);
 }