diff --git a/daemons/controld/controld_messages.c b/daemons/controld/controld_messages.c
index 0be04d0c18..423f006b9f 100644
--- a/daemons/controld/controld_messages.c
+++ b/daemons/controld/controld_messages.c
@@ -1,1251 +1,1291 @@
 /*
  * Copyright 2004-2020 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 <string.h>
 #include <time.h>
 
 #include <crm/crm.h>
 #include <crm/msg_xml.h>
 #include <crm/common/xml.h>
 #include <crm/cluster/internal.h>
 #include <crm/cib.h>
 #include <crm/common/ipc_internal.h>
 
 #include <pacemaker-controld.h>
 
 GListPtr fsa_message_queue = NULL;
 extern void crm_shutdown(int nsig);
 
 static enum crmd_fsa_input handle_message(xmlNode *msg,
                                           enum crmd_fsa_cause cause);
 static void handle_response(xmlNode *stored_msg);
 static enum crmd_fsa_input handle_request(xmlNode *stored_msg,
                                           enum crmd_fsa_cause cause);
 static enum crmd_fsa_input handle_shutdown_request(xmlNode *stored_msg);
 static void send_msg_via_ipc(xmlNode * msg, const char *sys);
 
 /* debug only, can wrap all it likes */
 int last_data_id = 0;
 
 void
 register_fsa_error_adv(enum crmd_fsa_cause cause, enum crmd_fsa_input input,
                        fsa_data_t * cur_data, void *new_data, const char *raised_from)
 {
     /* save the current actions if any */
     if (fsa_actions != A_NOTHING) {
         register_fsa_input_adv(cur_data ? cur_data->fsa_cause : C_FSA_INTERNAL,
                                I_NULL, cur_data ? cur_data->data : NULL,
                                fsa_actions, TRUE, __FUNCTION__);
     }
 
     /* reset the action list */
     crm_info("Resetting the current action list");
     fsa_dump_actions(fsa_actions, "Drop");
     fsa_actions = A_NOTHING;
 
     /* register the error */
     register_fsa_input_adv(cause, input, new_data, A_NOTHING, TRUE, raised_from);
 }
 
 int
 register_fsa_input_adv(enum crmd_fsa_cause cause, enum crmd_fsa_input input,
                        void *data, long long with_actions,
                        gboolean prepend, const char *raised_from)
 {
     unsigned old_len = g_list_length(fsa_message_queue);
     fsa_data_t *fsa_data = NULL;
 
     if (raised_from == NULL) {
         raised_from = "<unknown>";
     }
 
     if (input == I_NULL && with_actions == A_NOTHING /* && data == NULL */ ) {
         /* no point doing anything */
         crm_err("Cannot add entry to queue: no input and no action");
         return 0;
     }
 
     if (input == I_WAIT_FOR_EVENT) {
         do_fsa_stall = TRUE;
         crm_debug("Stalling the FSA pending further input: source=%s cause=%s data=%p queue=%d",
                   raised_from, fsa_cause2string(cause), data, old_len);
 
         if (old_len > 0) {
             fsa_dump_queue(LOG_TRACE);
             prepend = FALSE;
         }
 
         if (data == NULL) {
             fsa_actions |= with_actions;
             fsa_dump_actions(with_actions, "Restored");
             return 0;
         }
 
         /* Store everything in the new event and reset fsa_actions */
         with_actions |= fsa_actions;
         fsa_actions = A_NOTHING;
     }
 
     last_data_id++;
     crm_trace("%s %s FSA input %d (%s) due to %s, %s data",
               raised_from, (prepend? "prepended" : "appended"), last_data_id,
               fsa_input2string(input), fsa_cause2string(cause),
               (data? "with" : "without"));
 
     fsa_data = calloc(1, sizeof(fsa_data_t));
     fsa_data->id = last_data_id;
     fsa_data->fsa_input = input;
     fsa_data->fsa_cause = cause;
     fsa_data->origin = raised_from;
     fsa_data->data = NULL;
     fsa_data->data_type = fsa_dt_none;
     fsa_data->actions = with_actions;
 
     if (with_actions != A_NOTHING) {
         crm_trace("Adding actions %.16llx to input", with_actions);
     }
 
     if (data != NULL) {
         switch (cause) {
             case C_FSA_INTERNAL:
             case C_CRMD_STATUS_CALLBACK:
             case C_IPC_MESSAGE:
             case C_HA_MESSAGE:
                 CRM_CHECK(((ha_msg_input_t *) data)->msg != NULL,
                           crm_err("Bogus data from %s", raised_from));
                 crm_trace("Copying %s data from %s as cluster message data",
                           fsa_cause2string(cause), raised_from);
                 fsa_data->data = copy_ha_msg_input(data);
                 fsa_data->data_type = fsa_dt_ha_msg;
                 break;
 
             case C_LRM_OP_CALLBACK:
                 crm_trace("Copying %s data from %s as lrmd_event_data_t",
                           fsa_cause2string(cause), raised_from);
                 fsa_data->data = lrmd_copy_event((lrmd_event_data_t *) data);
                 fsa_data->data_type = fsa_dt_lrm;
                 break;
 
             case C_TIMER_POPPED:
             case C_SHUTDOWN:
             case C_UNKNOWN:
             case C_STARTUP:
                 crm_crit("Copying %s data (from %s) is not yet implemented",
                          fsa_cause2string(cause), raised_from);
                 crmd_exit(CRM_EX_SOFTWARE);
                 break;
         }
     }
 
     /* make sure to free it properly later */
     if (prepend) {
         fsa_message_queue = g_list_prepend(fsa_message_queue, fsa_data);
     } else {
         fsa_message_queue = g_list_append(fsa_message_queue, fsa_data);
     }
 
     crm_trace("FSA message queue length is %d",
               g_list_length(fsa_message_queue));
 
     /* fsa_dump_queue(LOG_TRACE); */
 
     if (old_len == g_list_length(fsa_message_queue)) {
         crm_err("Couldn't add message to the queue");
     }
 
     if (fsa_source && input != I_WAIT_FOR_EVENT) {
         crm_trace("Triggering FSA");
         mainloop_set_trigger(fsa_source);
     }
     return last_data_id;
 }
 
 void
 fsa_dump_queue(int log_level)
 {
     int offset = 0;
     GListPtr lpc = NULL;
 
     for (lpc = fsa_message_queue; lpc != NULL; lpc = lpc->next) {
         fsa_data_t *data = (fsa_data_t *) lpc->data;
 
         do_crm_log_unlikely(log_level,
                             "queue[%d.%d]: input %s raised by %s(%p.%d)\t(cause=%s)",
                             offset++, data->id, fsa_input2string(data->fsa_input),
                             data->origin, data->data, data->data_type,
                             fsa_cause2string(data->fsa_cause));
     }
 }
 
 ha_msg_input_t *
 copy_ha_msg_input(ha_msg_input_t * orig)
 {
     ha_msg_input_t *copy = calloc(1, sizeof(ha_msg_input_t));
 
     CRM_ASSERT(copy != NULL);
     copy->msg = (orig && orig->msg)? copy_xml(orig->msg) : NULL;
     copy->xml = get_message_xml(copy->msg, F_CRM_DATA);
     return copy;
 }
 
 void
 delete_fsa_input(fsa_data_t * fsa_data)
 {
     lrmd_event_data_t *op = NULL;
     xmlNode *foo = NULL;
 
     if (fsa_data == NULL) {
         return;
     }
     crm_trace("About to free %s data", fsa_cause2string(fsa_data->fsa_cause));
 
     if (fsa_data->data != NULL) {
         switch (fsa_data->data_type) {
             case fsa_dt_ha_msg:
                 delete_ha_msg_input(fsa_data->data);
                 break;
 
             case fsa_dt_xml:
                 foo = fsa_data->data;
                 free_xml(foo);
                 break;
 
             case fsa_dt_lrm:
                 op = (lrmd_event_data_t *) fsa_data->data;
                 lrmd_free_event(op);
                 break;
 
             case fsa_dt_none:
                 if (fsa_data->data != NULL) {
                     crm_err("Don't know how to free %s data from %s",
                             fsa_cause2string(fsa_data->fsa_cause), fsa_data->origin);
                     crmd_exit(CRM_EX_SOFTWARE);
                 }
                 break;
         }
         crm_trace("%s data freed", fsa_cause2string(fsa_data->fsa_cause));
     }
 
     free(fsa_data);
 }
 
 /* returns the next message */
 fsa_data_t *
 get_message(void)
 {
     fsa_data_t *message = g_list_nth_data(fsa_message_queue, 0);
 
     fsa_message_queue = g_list_remove(fsa_message_queue, message);
     crm_trace("Processing input %d", message->id);
     return message;
 }
 
 void *
 fsa_typed_data_adv(fsa_data_t * fsa_data, enum fsa_data_type a_type, const char *caller)
 {
     void *ret_val = NULL;
 
     if (fsa_data == NULL) {
         crm_err("%s: No FSA data available", caller);
 
     } else if (fsa_data->data == NULL) {
         crm_err("%s: No message data available. Origin: %s", caller, fsa_data->origin);
 
     } else if (fsa_data->data_type != a_type) {
         crm_crit("%s: Message data was the wrong type! %d vs. requested=%d.  Origin: %s",
                  caller, fsa_data->data_type, a_type, fsa_data->origin);
         CRM_ASSERT(fsa_data->data_type == a_type);
     } else {
         ret_val = fsa_data->data;
     }
 
     return ret_val;
 }
 
 /*	A_MSG_ROUTE	*/
 void
 do_msg_route(long long action,
              enum crmd_fsa_cause cause,
              enum crmd_fsa_state cur_state,
              enum crmd_fsa_input current_input, fsa_data_t * msg_data)
 {
     ha_msg_input_t *input = fsa_typed_data(fsa_dt_ha_msg);
 
     route_message(msg_data->fsa_cause, input->msg);
 }
 
 void
 route_message(enum crmd_fsa_cause cause, xmlNode * input)
 {
     ha_msg_input_t fsa_input;
     enum crmd_fsa_input result = I_NULL;
 
     fsa_input.msg = input;
     CRM_CHECK(cause == C_IPC_MESSAGE || cause == C_HA_MESSAGE, return);
 
     /* try passing the buck first */
     if (relay_message(input, cause == C_IPC_MESSAGE)) {
         return;
     }
 
     /* handle locally */
     result = handle_message(input, cause);
 
     /* done or process later? */
     switch (result) {
         case I_NULL:
         case I_CIB_OP:
         case I_ROUTER:
         case I_NODE_JOIN:
         case I_JOIN_REQUEST:
         case I_JOIN_RESULT:
             break;
         default:
             /* Defering local processing of message */
             register_fsa_input_later(cause, result, &fsa_input);
             return;
     }
 
     if (result != I_NULL) {
         /* add to the front of the queue */
         register_fsa_input(cause, result, &fsa_input);
     }
 }
 
 gboolean
 relay_message(xmlNode * msg, gboolean originated_locally)
 {
     int dest = 1;
     int is_for_dc = 0;
     int is_for_dcib = 0;
     int is_for_te = 0;
     int is_for_crm = 0;
     int is_for_cib = 0;
     int is_local = 0;
     const char *host_to = crm_element_value(msg, F_CRM_HOST_TO);
     const char *sys_to = crm_element_value(msg, F_CRM_SYS_TO);
     const char *sys_from = crm_element_value(msg, F_CRM_SYS_FROM);
     const char *type = crm_element_value(msg, F_TYPE);
     const char *task = crm_element_value(msg, F_CRM_TASK);
     const char *ref = crm_element_value(msg, XML_ATTR_REFERENCE);
 
     if (ref == NULL) {
         ref = "without reference ID";
     }
 
     if (msg == NULL) {
         crm_warn("Cannot route empty message");
         return TRUE;
 
     } else if (safe_str_eq(task, CRM_OP_HELLO)) {
         /* quietly ignore */
         crm_trace("No routing needed for hello message %s", ref);
         return TRUE;
 
     } else if (safe_str_neq(type, T_CRM)) {
         crm_warn("Cannot route message %s: Type is '%s' not '" T_CRM "'",
                  ref, (type? type : "missing"));
         crm_log_xml_warn(msg, "[bad message type]");
         return TRUE;
 
     } else if (sys_to == NULL) {
         crm_warn("Cannot route message %s: No subsystem specified", ref);
         crm_log_xml_warn(msg, "[no subsystem]");
         return TRUE;
     }
 
     is_for_dc = (strcasecmp(CRM_SYSTEM_DC, sys_to) == 0);
     is_for_dcib = (strcasecmp(CRM_SYSTEM_DCIB, sys_to) == 0);
     is_for_te = (strcasecmp(CRM_SYSTEM_TENGINE, sys_to) == 0);
     is_for_cib = (strcasecmp(CRM_SYSTEM_CIB, sys_to) == 0);
     is_for_crm = (strcasecmp(CRM_SYSTEM_CRMD, sys_to) == 0);
 
     is_local = 0;
     if (host_to == NULL || strlen(host_to) == 0) {
         if (is_for_dc || is_for_te) {
             is_local = 0;
 
         } else if (is_for_crm) {
-            if (safe_str_eq(task, CRM_OP_NODE_INFO)) {
+            if (pcmk__str_any_of(task, CRM_OP_NODE_INFO,
+                                 PCMK__CONTROLD_CMD_NODES, NULL)) {
                 /* Node info requests do not specify a host, which is normally
                  * treated as "all hosts", because the whole point is that the
-                 * client doesn't know the local node name. Always handle these
+                 * client may not know the local node name. Always handle these
                  * requests locally.
                  */
                 is_local = 1;
             } else {
                 is_local = !originated_locally;
             }
 
         } else {
             is_local = 1;
         }
 
     } else if (safe_str_eq(fsa_our_uname, host_to)) {
         is_local = 1;
     } else if (is_for_crm && safe_str_eq(task, CRM_OP_LRM_DELETE)) {
         xmlNode *msg_data = get_message_xml(msg, F_CRM_DATA);
         const char *mode = crm_element_value(msg_data, PCMK__XA_MODE);
 
         if (safe_str_eq(mode, XML_TAG_CIB)) {
             // Local delete of an offline node's resource history
             is_local = 1;
         }
     }
 
     if (is_for_dc || is_for_dcib || is_for_te) {
         if (AM_I_DC && is_for_te) {
             crm_trace("Route message %s locally as transition request", ref);
             send_msg_via_ipc(msg, sys_to);
 
         } else if (AM_I_DC) {
             crm_trace("Route message %s locally as DC request", ref);
             return FALSE; // More to be done by caller
 
         } else if (originated_locally && pcmk__str_none_of(sys_from, CRM_SYSTEM_PENGINE,
                                                           CRM_SYSTEM_TENGINE, NULL)) {
 
 #if SUPPORT_COROSYNC
             if (is_corosync_cluster()) {
                 dest = text2msg_type(sys_to);
             }
 #endif
             crm_trace("Relay message %s to DC", ref);
             send_cluster_message(host_to ? crm_get_peer(0, host_to) : NULL, dest, msg, TRUE);
 
         } else {
             /* Neither the TE nor the scheduler should be sending messages
              * to DCs on other nodes. By definition, if we are no longer the DC,
              * then the scheduler's or TE's data should be discarded.
              */
             crm_trace("Discard message %s because we are not DC", ref);
         }
 
     } else if (is_local && (is_for_crm || is_for_cib)) {
         crm_trace("Route message %s locally as controller request", ref);
         return FALSE; // More to be done by caller
 
     } else if (is_local) {
         crm_trace("Relay message %s locally to %s",
                   ref, (sys_to? sys_to : "unknown client"));
         crm_log_xml_trace(msg, "[IPC relay]");
         send_msg_via_ipc(msg, sys_to);
 
     } else {
         crm_node_t *node_to = NULL;
 
 #if SUPPORT_COROSYNC
         if (is_corosync_cluster()) {
             dest = text2msg_type(sys_to);
 
             if (dest == crm_msg_none || dest > crm_msg_stonith_ng) {
                 dest = crm_msg_crmd;
             }
         }
 #endif
 
         if (host_to) {
             node_to = crm_find_peer(0, host_to);
             if (node_to == NULL) {
                 crm_warn("Cannot route message %s: Unknown node %s",
                          ref, host_to);
                 return TRUE;
             }
             crm_trace("Relay message %s to %s",
                       ref, (node_to->uname? node_to->uname : "peer"));
         } else {
             crm_trace("Broadcast message %s to all peers", ref);
         }
         send_cluster_message(host_to ? node_to : NULL, dest, msg, TRUE);
     }
 
     return TRUE; // No further processing of message is needed
 }
 
 // Return true if field contains a positive integer
 static bool
 authorize_version(xmlNode *message_data, const char *field,
                   const char *client_name, const char *ref, const char *uuid)
 {
     const char *version = crm_element_value(message_data, field);
 
     if (pcmk__str_empty(version)) {
         crm_warn("IPC hello from %s rejected: No protocol %s",
                  CRM_XS " ref=%s uuid=%s",
                  client_name, field, (ref? ref : "none"), uuid);
         return false;
     } else {
         int version_num = crm_parse_int(version, NULL);
 
         if (version_num < 0) {
             crm_warn("IPC hello from %s rejected: Protocol %s '%s' "
                      "not recognized", CRM_XS " ref=%s uuid=%s",
                      client_name, field, version, (ref? ref : "none"), uuid);
             return false;
         }
     }
     return true;
 }
 
 /*!
  * \internal
  * \brief Check whether a client IPC message is acceptable
  *
  * If a given client IPC message is a hello, "authorize" it by ensuring it has
  * valid information such as a protocol version, and return false indicating
  * that nothing further needs to be done with the message. If the message is not
  * a hello, just return true to indicate it needs further processing.
  *
  * \param[in] client_msg     XML of IPC message
  * \param[in] curr_client    If IPC is not proxied, client that sent message
  * \param[in] proxy_session  If IPC is proxied, the session ID
  *
  * \return true if message needs further processing, false if it doesn't
  */
 bool
 controld_authorize_ipc_message(xmlNode *client_msg, pcmk__client_t *curr_client,
                                const char *proxy_session)
 {
     xmlNode *message_data = NULL;
     const char *client_name = NULL;
     const char *op = crm_element_value(client_msg, F_CRM_TASK);
     const char *ref = crm_element_value(client_msg, XML_ATTR_REFERENCE);
     const char *uuid = (curr_client? curr_client->id : proxy_session);
 
     if (uuid == NULL) {
         crm_warn("IPC message from client rejected: No client identifier "
                  CRM_XS " ref=%s", (ref? ref : "none"));
         goto rejected;
     }
 
     if (safe_str_neq(CRM_OP_HELLO, op)) {
         // Only hello messages need to be authorized
         return true;
     }
 
     message_data = get_message_xml(client_msg, F_CRM_DATA);
 
     client_name = crm_element_value(message_data, "client_name");
     if (pcmk__str_empty(client_name)) {
         crm_warn("IPC hello from client rejected: No client name",
                  CRM_XS " ref=%s uuid=%s", (ref? ref : "none"), uuid);
         goto rejected;
     }
     if (!authorize_version(message_data, "major_version", client_name, ref,
                            uuid)) {
         goto rejected;
     }
     if (!authorize_version(message_data, "minor_version", client_name, ref,
                            uuid)) {
         goto rejected;
     }
 
     crm_trace("Validated IPC hello from client %s", client_name);
     if (curr_client) {
         curr_client->userdata = strdup(client_name);
     }
     mainloop_set_trigger(fsa_source);
     return false;
 
 rejected:
     if (curr_client) {
         qb_ipcs_disconnect(curr_client->ipcs);
     }
     return false;
 }
 
 static enum crmd_fsa_input
 handle_message(xmlNode *msg, enum crmd_fsa_cause cause)
 {
     const char *type = NULL;
 
     CRM_CHECK(msg != NULL, return I_NULL);
 
     type = crm_element_value(msg, F_CRM_MSG_TYPE);
     if (crm_str_eq(type, XML_ATTR_REQUEST, TRUE)) {
         return handle_request(msg, cause);
 
     } else if (crm_str_eq(type, XML_ATTR_RESPONSE, TRUE)) {
         handle_response(msg);
         return I_NULL;
     }
 
     crm_err("Unknown message type: %s", type);
     return I_NULL;
 }
 
 static enum crmd_fsa_input
 handle_failcount_op(xmlNode * stored_msg)
 {
     const char *rsc = NULL;
     const char *uname = NULL;
     const char *op = NULL;
     char *interval_spec = NULL;
     guint interval_ms = 0;
     gboolean is_remote_node = FALSE;
     xmlNode *xml_op = get_message_xml(stored_msg, F_CRM_DATA);
 
     if (xml_op) {
         xmlNode *xml_rsc = first_named_child(xml_op, XML_CIB_TAG_RESOURCE);
         xmlNode *xml_attrs = first_named_child(xml_op, XML_TAG_ATTRS);
 
         if (xml_rsc) {
             rsc = ID(xml_rsc);
         }
         if (xml_attrs) {
             op = crm_element_value(xml_attrs,
                                    CRM_META "_" XML_RSC_ATTR_CLEAR_OP);
             crm_element_value_ms(xml_attrs,
                                  CRM_META "_" XML_RSC_ATTR_CLEAR_INTERVAL,
                                  &interval_ms);
         }
     }
     uname = crm_element_value(xml_op, XML_LRM_ATTR_TARGET);
 
     if ((rsc == NULL) || (uname == NULL)) {
         crm_log_xml_warn(stored_msg, "invalid failcount op");
         return I_NULL;
     }
 
     if (crm_element_value(xml_op, XML_LRM_ATTR_ROUTER_NODE)) {
         is_remote_node = TRUE;
     }
 
     if (interval_ms) {
         interval_spec = crm_strdup_printf("%ums", interval_ms);
     }
     update_attrd_clear_failures(uname, rsc, op, interval_spec, is_remote_node);
     free(interval_spec);
 
     lrm_clear_last_failure(rsc, uname, op, interval_ms);
 
     return I_NULL;
 }
 
 static enum crmd_fsa_input
 handle_lrm_delete(xmlNode *stored_msg)
 {
     const char *mode = NULL;
     xmlNode *msg_data = get_message_xml(stored_msg, F_CRM_DATA);
 
     CRM_CHECK(msg_data != NULL, return I_NULL);
 
     /* CRM_OP_LRM_DELETE has two distinct modes. The default behavior is to
      * relay the operation to the affected node, which will unregister the
      * resource from the local executor, clear the resource's history from the
      * CIB, and do some bookkeeping in the controller.
      *
      * However, if the affected node is offline, the client will specify
      * mode="cib" which means the controller receiving the operation should
      * clear the resource's history from the CIB and nothing else. This is used
      * to clear shutdown locks.
      */
     mode = crm_element_value(msg_data, PCMK__XA_MODE);
     if ((mode == NULL) || strcmp(mode, XML_TAG_CIB)) {
         // Relay to affected node
         crm_xml_add(stored_msg, F_CRM_SYS_TO, CRM_SYSTEM_LRMD);
         return I_ROUTER;
 
     } else {
         // Delete CIB history locally (compare with do_lrm_delete())
         const char *from_sys = NULL;
         const char *user_name = NULL;
         const char *rsc_id = NULL;
         const char *node = NULL;
         xmlNode *rsc_xml = NULL;
         int rc = pcmk_rc_ok;
 
         rsc_xml = first_named_child(msg_data, XML_CIB_TAG_RESOURCE);
         CRM_CHECK(rsc_xml != NULL, return I_NULL);
 
         rsc_id = ID(rsc_xml);
         from_sys = crm_element_value(stored_msg, F_CRM_SYS_FROM);
         node = crm_element_value(msg_data, XML_LRM_ATTR_TARGET);
 #if ENABLE_ACL
         user_name = pcmk__update_acl_user(stored_msg, F_CRM_USER, NULL);
 #endif
         crm_debug("Handling " CRM_OP_LRM_DELETE " for %s on %s locally%s%s "
                   "(clearing CIB resource history only)", rsc_id, node,
                   (user_name? " for user " : ""), (user_name? user_name : ""));
 #if ENABLE_ACL
         rc = controld_delete_resource_history(rsc_id, node, user_name,
                                               cib_dryrun|cib_sync_call);
 #endif
         if (rc == pcmk_rc_ok) {
             rc = controld_delete_resource_history(rsc_id, node, user_name,
                                                   crmd_cib_smart_opt());
         }
 
         //Notify client and tengine.(Only notify tengine if mode = "cib" and CRM_OP_LRM_DELETE.)
         if (from_sys) {
             lrmd_event_data_t *op = NULL;
             const char *from_host = crm_element_value(stored_msg,
                                                       F_CRM_HOST_FROM);
             const char *transition;
 
             if (strcmp(from_sys, CRM_SYSTEM_TENGINE)) {
                 transition = crm_element_value(msg_data,
                                                        XML_ATTR_TRANSITION_KEY);
             } else {
                 transition = crm_element_value(stored_msg,
                                                        XML_ATTR_TRANSITION_KEY);
             }
 
             crm_info("Notifying %s on %s that %s was%s deleted",
                      from_sys, (from_host? from_host : "local node"), rsc_id,
                      ((rc == pcmk_rc_ok)? "" : " not"));
             op = lrmd_new_event(rsc_id, CRMD_ACTION_DELETE, 0);
             op->type = lrmd_event_exec_complete;
             op->user_data = strdup(transition? transition : FAKE_TE_ID);
             op->params = crm_str_table_new();
             g_hash_table_insert(op->params, strdup(XML_ATTR_CRM_VERSION),
                                 strdup(CRM_FEATURE_SET));
             controld_rc2event(op, rc);
             controld_ack_event_directly(from_host, from_sys, NULL, op, rsc_id);
             lrmd_free_event(op);
             controld_trigger_delete_refresh(from_sys, rsc_id);
         }
         return I_NULL;
     }
 }
 
 /*!
  * \brief Handle a CRM_OP_REMOTE_STATE message by updating remote peer cache
  *
  * \param[in] msg  Message XML
  *
  * \return Next FSA input
  */
 static enum crmd_fsa_input
 handle_remote_state(xmlNode *msg)
 {
     const char *remote_uname = ID(msg);
     const char *remote_is_up = crm_element_value(msg, XML_NODE_IN_CLUSTER);
     crm_node_t *remote_peer;
 
     CRM_CHECK(remote_uname && remote_is_up, return I_NULL);
 
     remote_peer = crm_remote_peer_get(remote_uname);
     CRM_CHECK(remote_peer, return I_NULL);
 
     crm_update_peer_state(__FUNCTION__, remote_peer,
                           crm_is_true(remote_is_up)?
                           CRM_NODE_MEMBER : CRM_NODE_LOST, 0);
     return I_NULL;
 }
 
 /*!
  * \brief Handle a CRM_OP_PING message
  *
  * \param[in] msg  Message XML
  *
  * \return Next FSA input
  */
 static enum crmd_fsa_input
 handle_ping(xmlNode *msg)
 {
     const char *value = NULL;
     xmlNode *ping = NULL;
 
     // Build reply
 
     ping = create_xml_node(NULL, XML_CRM_TAG_PING);
     value = crm_element_value(msg, F_CRM_SYS_TO);
     crm_xml_add(ping, XML_PING_ATTR_SYSFROM, value);
 
     // Add controller state
     value = fsa_state2string(fsa_state);
     crm_xml_add(ping, XML_PING_ATTR_CRMDSTATE, value);
     crm_notice("Current ping state: %s", value); // CTS needs this
 
     // Add controller health
     // @TODO maybe do some checks to determine meaningful status
     crm_xml_add(ping, XML_PING_ATTR_STATUS, "ok");
 
     // Send reply
     msg = create_reply(msg, ping);
     free_xml(ping);
     if (msg) {
         (void) relay_message(msg, TRUE);
         free_xml(msg);
     }
 
     // Nothing further to do
     return I_NULL;
 }
 
+/*!
+ * \brief Handle a PCMK__CONTROLD_CMD_NODES message
+ *
+ * \return Next FSA input
+ */
+static enum crmd_fsa_input
+handle_node_list(xmlNode *request)
+{
+    GHashTableIter iter;
+    crm_node_t *node = NULL;
+    xmlNode *reply = NULL;
+    xmlNode *reply_data = NULL;
+
+    // Create message data for reply
+    reply_data = create_xml_node(NULL, XML_CIB_TAG_NODES);
+    g_hash_table_iter_init(&iter, crm_peer_cache);
+    while (g_hash_table_iter_next(&iter, NULL, (gpointer *) & node)) {
+        xmlNode *xml = create_xml_node(reply_data, XML_CIB_TAG_NODE);
+
+        crm_xml_add_ll(xml, XML_ATTR_ID, (long long) node->id); // uint32_t
+        crm_xml_add(xml, XML_ATTR_UNAME, node->uname);
+        crm_xml_add(xml, XML_NODE_IN_CLUSTER, node->state);
+    }
+
+    // Create and send reply
+    reply = create_reply(request, reply_data);
+    free_xml(reply_data);
+    if (reply) {
+        (void) relay_message(reply, TRUE);
+        free_xml(reply);
+    }
+
+    // Nothing further to do
+    return I_NULL;
+}
+
 /*!
  * \brief Handle a CRM_OP_NODE_INFO request
  *
  * \param[in] msg  Message XML
  *
  * \return Next FSA input
  */
 static enum crmd_fsa_input
 handle_node_info_request(xmlNode *msg)
 {
     const char *value = NULL;
     crm_node_t *node = NULL;
     int node_id = 0;
     xmlNode *reply = NULL;
 
     // Build reply
 
     reply = create_xml_node(NULL, XML_CIB_TAG_NODE);
     crm_xml_add(reply, XML_PING_ATTR_SYSFROM, CRM_SYSTEM_CRMD);
 
     // Add whether current partition has quorum
     crm_xml_add_boolean(reply, XML_ATTR_HAVE_QUORUM, fsa_has_quorum);
 
     // Check whether client requested node info by ID and/or name
     crm_element_value_int(msg, XML_ATTR_ID, &node_id);
     if (node_id < 0) {
         node_id = 0;
     }
     value = crm_element_value(msg, XML_ATTR_UNAME);
 
     // Default to local node if none given
     if ((node_id == 0) && (value == NULL)) {
         value = fsa_our_uname;
     }
 
     node = crm_find_peer_full(node_id, value, CRM_GET_PEER_ANY);
     if (node) {
         crm_xml_add_int(reply, XML_ATTR_ID, node->id);
         crm_xml_add(reply, XML_ATTR_UUID, node->uuid);
         crm_xml_add(reply, XML_ATTR_UNAME, node->uname);
         crm_xml_add(reply, XML_NODE_IS_PEER, node->state);
         crm_xml_add_boolean(reply, XML_NODE_IS_REMOTE,
                             node->flags & crm_remote_node);
     }
 
     // Send reply
     msg = create_reply(msg, reply);
     free_xml(reply);
     if (msg) {
         (void) relay_message(msg, TRUE);
         free_xml(msg);
     }
 
     // Nothing further to do
     return I_NULL;
 }
 
 static void
 verify_feature_set(xmlNode *msg)
 {
     const char *dc_version = crm_element_value(msg, XML_ATTR_CRM_VERSION);
 
     if (dc_version == NULL) {
         /* All we really know is that the DC feature set is older than 3.1.0,
          * but that's also all that really matters.
          */
         dc_version = "3.0.14";
     }
 
     if (feature_set_compatible(dc_version, CRM_FEATURE_SET)) {
         crm_trace("Local feature set (%s) is compatible with DC's (%s)",
                   CRM_FEATURE_SET, dc_version);
     } else {
         crm_err("Local feature set (%s) is incompatible with DC's (%s)",
                 CRM_FEATURE_SET, dc_version);
 
         // Nothing is likely to improve without administrator involvement
         set_bit(fsa_input_register, R_STAYDOWN);
         crmd_exit(CRM_EX_FATAL);
     }
 }
 
 // DC gets own shutdown all-clear
 static enum crmd_fsa_input
 handle_shutdown_self_ack(xmlNode *stored_msg)
 {
     const char *host_from = crm_element_value(stored_msg, F_CRM_HOST_FROM);
 
     if (is_set(fsa_input_register, R_SHUTDOWN)) {
         // The expected case -- we initiated own shutdown sequence
         crm_info("Shutting down controller");
         return I_STOP;
     }
 
     if (safe_str_eq(host_from, fsa_our_dc)) {
         // Must be logic error -- DC confirming its own unrequested shutdown
         crm_err("Shutting down controller immediately due to "
                 "unexpected shutdown confirmation");
         return I_TERMINATE;
     }
 
     if (fsa_state != S_STOPPING) {
         // Shouldn't happen -- non-DC confirming unrequested shutdown
         crm_err("Starting new DC election because %s is "
                 "confirming shutdown we did not request",
                 (host_from? host_from : "another node"));
         return I_ELECTION;
     }
 
     // Shouldn't happen, but we are already stopping anyway
     crm_debug("Ignoring unexpected shutdown confirmation from %s",
               (host_from? host_from : "another node"));
     return I_NULL;
 }
 
 // Non-DC gets shutdown all-clear from DC
 static enum crmd_fsa_input
 handle_shutdown_ack(xmlNode *stored_msg)
 {
     const char *host_from = crm_element_value(stored_msg, F_CRM_HOST_FROM);
 
     if (host_from == NULL) {
         crm_warn("Ignoring shutdown request without origin specified");
         return I_NULL;
     }
 
     if ((fsa_our_dc == NULL) || (strcmp(host_from, fsa_our_dc) == 0)) {
 
         if (is_set(fsa_input_register, R_SHUTDOWN)) {
             crm_info("Shutting down controller after confirmation from %s",
                      host_from);
         } else {
             crm_err("Shutting down controller after unexpected "
                     "shutdown request from %s", host_from);
             set_bit(fsa_input_register, R_STAYDOWN);
         }
         return I_STOP;
     }
 
     crm_warn("Ignoring shutdown request from %s because DC is %s",
              host_from, fsa_our_dc);
     return I_NULL;
 }
 
 static enum crmd_fsa_input
 handle_request(xmlNode *stored_msg, enum crmd_fsa_cause cause)
 {
     xmlNode *msg = NULL;
     const char *op = crm_element_value(stored_msg, F_CRM_TASK);
 
     /* Optimize this for the DC - it has the most to do */
 
     if (op == NULL) {
         crm_log_xml_warn(stored_msg, "[request without " F_CRM_TASK "]");
         return I_NULL;
     }
 
     if (strcmp(op, CRM_OP_SHUTDOWN_REQ) == 0) {
         const char *from = crm_element_value(stored_msg, F_CRM_HOST_FROM);
         crm_node_t *node = crm_find_peer(0, from);
 
         crm_update_peer_expected(__FUNCTION__, node, CRMD_JOINSTATE_DOWN);
         if(AM_I_DC == FALSE) {
             return I_NULL; /* Done */
         }
     }
 
     /*========== DC-Only Actions ==========*/
     if (AM_I_DC) {
         if (strcmp(op, CRM_OP_JOIN_ANNOUNCE) == 0) {
             return I_NODE_JOIN;
 
         } else if (strcmp(op, CRM_OP_JOIN_REQUEST) == 0) {
             return I_JOIN_REQUEST;
 
         } else if (strcmp(op, CRM_OP_JOIN_CONFIRM) == 0) {
             return I_JOIN_RESULT;
 
         } else if (strcmp(op, CRM_OP_SHUTDOWN) == 0) {
             return handle_shutdown_self_ack(stored_msg);
 
         } else if (strcmp(op, CRM_OP_SHUTDOWN_REQ) == 0) {
             /* a slave wants to shut down */
             /* create cib fragment and add to message */
             return handle_shutdown_request(stored_msg);
 
         } else if (strcmp(op, CRM_OP_REMOTE_STATE) == 0) {
             /* a remote connection host is letting us know the node state */
             return handle_remote_state(stored_msg);
         }
     }
 
     /*========== common actions ==========*/
     if (strcmp(op, CRM_OP_NOVOTE) == 0) {
         ha_msg_input_t fsa_input;
 
         fsa_input.msg = stored_msg;
         register_fsa_input_adv(C_HA_MESSAGE, I_NULL, &fsa_input,
                                A_ELECTION_COUNT | A_ELECTION_CHECK, FALSE, __FUNCTION__);
 
     } else if (strcmp(op, CRM_OP_THROTTLE) == 0) {
         throttle_update(stored_msg);
         if (AM_I_DC && transition_graph != NULL) {
             if (transition_graph->complete == FALSE) {
                 crm_debug("The throttle changed. Trigger a graph.");
                 trigger_graph();
             }
         }
         return I_NULL;
 
     } else if (strcmp(op, CRM_OP_CLEAR_FAILCOUNT) == 0) {
         return handle_failcount_op(stored_msg);
 
     } else if (strcmp(op, CRM_OP_VOTE) == 0) {
         /* count the vote and decide what to do after that */
         ha_msg_input_t fsa_input;
 
         fsa_input.msg = stored_msg;
         register_fsa_input_adv(C_HA_MESSAGE, I_NULL, &fsa_input,
                                A_ELECTION_COUNT | A_ELECTION_CHECK, FALSE, __FUNCTION__);
 
         /* Sometimes we _must_ go into S_ELECTION */
         if (fsa_state == S_HALT) {
             crm_debug("Forcing an election from S_HALT");
             return I_ELECTION;
 #if 0
         } else if (AM_I_DC) {
             /* This is the old way of doing things but what is gained? */
             return I_ELECTION;
 #endif
         }
 
     } else if (strcmp(op, CRM_OP_JOIN_OFFER) == 0) {
         verify_feature_set(stored_msg);
         crm_debug("Raising I_JOIN_OFFER: join-%s", crm_element_value(stored_msg, F_CRM_JOIN_ID));
         return I_JOIN_OFFER;
 
     } else if (strcmp(op, CRM_OP_JOIN_ACKNAK) == 0) {
         crm_debug("Raising I_JOIN_RESULT: join-%s", crm_element_value(stored_msg, F_CRM_JOIN_ID));
         return I_JOIN_RESULT;
 
     } else if (strcmp(op, CRM_OP_LRM_DELETE) == 0) {
         return handle_lrm_delete(stored_msg);
 
     } else if ((strcmp(op, CRM_OP_LRM_FAIL) == 0)
                || (strcmp(op, CRM_OP_LRM_REFRESH) == 0)
                || (strcmp(op, CRM_OP_REPROBE) == 0)) {
 
         crm_xml_add(stored_msg, F_CRM_SYS_TO, CRM_SYSTEM_LRMD);
         return I_ROUTER;
 
     } else if (strcmp(op, CRM_OP_NOOP) == 0) {
         return I_NULL;
 
     } else if (strcmp(op, CRM_OP_LOCAL_SHUTDOWN) == 0) {
 
         crm_shutdown(SIGTERM);
         /*return I_SHUTDOWN; */
         return I_NULL;
 
     } else if (strcmp(op, CRM_OP_PING) == 0) {
         return handle_ping(stored_msg);
 
     } else if (strcmp(op, CRM_OP_NODE_INFO) == 0) {
         return handle_node_info_request(stored_msg);
 
     } else if (strcmp(op, CRM_OP_RM_NODE_CACHE) == 0) {
         int id = 0;
         const char *name = NULL;
 
         crm_element_value_int(stored_msg, XML_ATTR_ID, &id);
         name = crm_element_value(stored_msg, XML_ATTR_UNAME);
 
         if(cause == C_IPC_MESSAGE) {
             msg = create_request(CRM_OP_RM_NODE_CACHE, NULL, NULL, CRM_SYSTEM_CRMD, CRM_SYSTEM_CRMD, NULL);
             if (send_cluster_message(NULL, crm_msg_crmd, msg, TRUE) == FALSE) {
                 crm_err("Could not instruct peers to remove references to node %s/%u", name, id);
             } else {
                 crm_notice("Instructing peers to remove references to node %s/%u", name, id);
             }
             free_xml(msg);
 
         } else {
             reap_crm_member(id, name);
 
             /* If we're forgetting this node, also forget any failures to fence
              * it, so we don't carry that over to any node added later with the
              * same name.
              */
             st_fail_count_reset(name);
         }
 
     } else if (strcmp(op, CRM_OP_MAINTENANCE_NODES) == 0) {
         xmlNode *xml = get_message_xml(stored_msg, F_CRM_DATA);
 
         remote_ra_process_maintenance_nodes(xml);
 
+    } else if (strcmp(op, PCMK__CONTROLD_CMD_NODES) == 0) {
+        return handle_node_list(stored_msg);
+
         /*========== (NOT_DC)-Only Actions ==========*/
     } else if (!AM_I_DC) {
 
         if (strcmp(op, CRM_OP_SHUTDOWN) == 0) {
             return handle_shutdown_ack(stored_msg);
         }
 
     } else {
         crm_err("Unexpected request (%s) sent to %s", op, AM_I_DC ? "the DC" : "non-DC node");
         crm_log_xml_err(stored_msg, "Unexpected");
     }
 
     return I_NULL;
 }
 
 static void
 handle_response(xmlNode *stored_msg)
 {
     const char *op = crm_element_value(stored_msg, F_CRM_TASK);
 
     if (op == NULL) {
         crm_log_xml_err(stored_msg, "Bad message");
 
     } else if (AM_I_DC && strcmp(op, CRM_OP_PECALC) == 0) {
         // Check whether scheduler answer been superseded by subsequent request
         const char *msg_ref = crm_element_value(stored_msg, XML_ATTR_REFERENCE);
 
         if (msg_ref == NULL) {
             crm_err("%s - Ignoring calculation with no reference", op);
 
         } else if (safe_str_eq(msg_ref, fsa_pe_ref)) {
             ha_msg_input_t fsa_input;
 
             controld_stop_sched_timer();
             fsa_input.msg = stored_msg;
             register_fsa_input_later(C_IPC_MESSAGE, I_PE_SUCCESS, &fsa_input);
 
         } else {
             crm_info("%s calculation %s is obsolete", op, msg_ref);
         }
 
     } else if (strcmp(op, CRM_OP_VOTE) == 0
                || strcmp(op, CRM_OP_SHUTDOWN_REQ) == 0 || strcmp(op, CRM_OP_SHUTDOWN) == 0) {
 
     } else {
         const char *host_from = crm_element_value(stored_msg, F_CRM_HOST_FROM);
 
         crm_err("Unexpected response (op=%s, src=%s) sent to the %s",
                 op, host_from, AM_I_DC ? "DC" : "controller");
     }
 }
 
 static enum crmd_fsa_input
 handle_shutdown_request(xmlNode * stored_msg)
 {
     /* handle here to avoid potential version issues
      *   where the shutdown message/procedure may have
      *   been changed in later versions.
      *
      * This way the DC is always in control of the shutdown
      */
 
     char *now_s = NULL;
     time_t now = time(NULL);
     const char *host_from = crm_element_value(stored_msg, F_CRM_HOST_FROM);
 
     if (host_from == NULL) {
         /* we're shutting down and the DC */
         host_from = fsa_our_uname;
     }
 
     crm_info("Creating shutdown request for %s (state=%s)", host_from, fsa_state2string(fsa_state));
     crm_log_xml_trace(stored_msg, "message");
 
     now_s = crm_itoa(now);
     update_attrd(host_from, XML_CIB_ATTR_SHUTDOWN, now_s, NULL, FALSE);
     free(now_s);
 
     /* will be picked up by the TE as long as its running */
     return I_NULL;
 }
 
 /* msg is deleted by the time this returns */
 extern gboolean process_te_message(xmlNode * msg, xmlNode * xml_data);
 
 static void
 send_msg_via_ipc(xmlNode * msg, const char *sys)
 {
     pcmk__client_t *client_channel = pcmk__find_client_by_id(sys);
 
     if (crm_element_value(msg, F_CRM_HOST_FROM) == NULL) {
         crm_xml_add(msg, F_CRM_HOST_FROM, fsa_our_uname);
     }
 
     if (client_channel != NULL) {
         /* Transient clients such as crmadmin */
         pcmk__ipc_send_xml(client_channel, 0, msg, crm_ipc_server_event);
 
     } else if (sys != NULL && strcmp(sys, CRM_SYSTEM_TENGINE) == 0) {
         xmlNode *data = get_message_xml(msg, F_CRM_DATA);
 
         process_te_message(msg, data);
 
     } else if (sys != NULL && strcmp(sys, CRM_SYSTEM_LRMD) == 0) {
         fsa_data_t fsa_data;
         ha_msg_input_t fsa_input;
 
         fsa_input.msg = msg;
         fsa_input.xml = get_message_xml(msg, F_CRM_DATA);
 
         fsa_data.id = 0;
         fsa_data.actions = 0;
         fsa_data.data = &fsa_input;
         fsa_data.fsa_input = I_MESSAGE;
         fsa_data.fsa_cause = C_IPC_MESSAGE;
         fsa_data.origin = __FUNCTION__;
         fsa_data.data_type = fsa_dt_ha_msg;
 
         do_lrm_invoke(A_LRM_INVOKE, C_IPC_MESSAGE, fsa_state, I_MESSAGE, &fsa_data);
 
     } else if (sys != NULL && crmd_is_proxy_session(sys)) {
         crmd_proxy_send(sys, msg);
 
     } else {
         crm_debug("Unknown Sub-system (%s)... discarding message.", crm_str(sys));
     }
 }
 
 void
 delete_ha_msg_input(ha_msg_input_t * orig)
 {
     if (orig == NULL) {
         return;
     }
     free_xml(orig->msg);
     free(orig);
 }
 
 /*!
  * \internal
  * \brief Notify the DC of a remote node state change
  *
  * \param[in] node_name  Node's name
  * \param[in] node_up    TRUE if node is up, FALSE if down
  */
 void
 send_remote_state_message(const char *node_name, gboolean node_up)
 {
     /* If we don't have a DC, or the message fails, we have a failsafe:
      * the DC will eventually pick up the change via the CIB node state.
      * The message allows it to happen sooner if possible.
      */
     if (fsa_our_dc) {
         xmlNode *msg = create_request(CRM_OP_REMOTE_STATE, NULL, fsa_our_dc,
                                       CRM_SYSTEM_DC, CRM_SYSTEM_CRMD, NULL);
 
         crm_info("Notifying DC %s of pacemaker_remote node %s %s",
                  fsa_our_dc, node_name, (node_up? "coming up" : "going down"));
         crm_xml_add(msg, XML_ATTR_ID, node_name);
         crm_xml_add_boolean(msg, XML_NODE_IN_CLUSTER, node_up);
         send_cluster_message(crm_get_peer(0, fsa_our_dc), crm_msg_crmd, msg,
                              TRUE);
         free_xml(msg);
     } else {
         crm_debug("No DC to notify of pacemaker_remote node %s %s",
                   node_name, (node_up? "coming up" : "going down"));
     }
 }
 
diff --git a/daemons/pacemakerd/Makefile.am b/daemons/pacemakerd/Makefile.am
index b01d8ef4d5..4cc8a7c4a8 100644
--- a/daemons/pacemakerd/Makefile.am
+++ b/daemons/pacemakerd/Makefile.am
@@ -1,37 +1,36 @@
 #
-# Copyright 2004-2019 the Pacemaker project contributors
+# Copyright 2004-2020 the Pacemaker project contributors
 #
 # The version control history for this file may have further details.
 #
 # This source code is licensed under the GNU General Public License version 2
 # or later (GPLv2+) WITHOUT ANY WARRANTY.
 #
 
 include $(top_srcdir)/mk/common.mk
 
-if BUILD_CS_SUPPORT
-
 initdir			= $(INITDIR)
 init_SCRIPTS		= pacemaker
 sbin_PROGRAMS		= pacemakerd
 
 if BUILD_SYSTEMD
 systemdsystemunit_DATA	= pacemaker.service
 endif
 
 EXTRA_DIST		= pacemaker.sysconfig
 
 ## SOURCES
 
 noinst_HEADERS		= pacemakerd.h
 
 pacemakerd_CFLAGS	= $(CFLAGS_HARDENED_EXE)
 pacemakerd_LDFLAGS	= $(LDFLAGS_HARDENED_EXE)
 
 pacemakerd_LDADD	= $(top_builddir)/lib/cluster/libcrmcluster.la $(top_builddir)/lib/common/libcrmcommon.la
 pacemakerd_LDADD	+= $(CLUSTERLIBS)
-pacemakerd_SOURCES	= pacemakerd.c pcmkd_corosync.c
-
+pacemakerd_SOURCES	= pacemakerd.c
+if BUILD_CS_SUPPORT
+pacemakerd_SOURCES	+= pcmkd_corosync.c
 endif
 
 CLEANFILES = $(man8_MANS)
diff --git a/daemons/pacemakerd/pacemakerd.c b/daemons/pacemakerd/pacemakerd.c
index 64c30e2403..652d6ca0ec 100644
--- a/daemons/pacemakerd/pacemakerd.c
+++ b/daemons/pacemakerd/pacemakerd.c
@@ -1,1461 +1,1189 @@
 /*
  * Copyright 2010-2020 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 "pacemakerd.h"
 
 #include <pwd.h>
 #include <grp.h>
 #include <poll.h>
+#include <errno.h>
 #include <stdio.h>
 #include <stdbool.h>
 #include <sys/stat.h>
 #include <sys/types.h>
 #include <sys/time.h>
 #include <sys/resource.h>
 #include <sys/reboot.h>
 
 #include <crm/crm.h>  /* indirectly: CRM_EX_* */
 #include <crm/cib/internal.h>  /* cib_channel_ro */
 #include <crm/msg_xml.h>
 #include <crm/common/ipc_internal.h>
 #include <crm/common/mainloop.h>
 #include <crm/cluster/internal.h>
 #include <crm/cluster.h>
 
-#ifdef SUPPORT_COROSYNC
-#include <corosync/cfg.h>
-#endif
-
 #include <dirent.h>
 #include <ctype.h>
 
-static gboolean pcmk_quorate = FALSE;
 static gboolean fatal_error = FALSE;
 static GMainLoop *mainloop = NULL;
 static bool global_keep_tracking = false;
 
 #define PCMK_PROCESS_CHECK_INTERVAL 5
 
-static const char *local_name = NULL;
-static uint32_t local_nodeid = 0;
 static crm_trigger_t *shutdown_trigger = NULL;
 static const char *pid_file = PCMK_RUN_DIR "/pacemaker.pid";
 
 typedef struct pcmk_child_s {
     pid_t pid;
     long flag;
     int start_seq;
     int respawn_count;
     gboolean respawn;
     const char *name;
     const char *uid;
     const char *command;
     const char *endpoint;  /* IPC server name */
 
     gboolean active_before_startup;
 } pcmk_child_t;
 
 /* Index into the array below */
 #define PCMK_CHILD_CONTROLD  3
 
 static pcmk_child_t pcmk_children[] = {
     {
         0, crm_proc_none,       0, 0, FALSE, "none",
         NULL, NULL
     },
     {
         0, crm_proc_execd,      3, 0, TRUE,  "pacemaker-execd",
         NULL, CRM_DAEMON_DIR "/pacemaker-execd",
         CRM_SYSTEM_LRMD
     },
     {
         0, crm_proc_based,      1, 0, TRUE,  "pacemaker-based",
         CRM_DAEMON_USER, CRM_DAEMON_DIR "/pacemaker-based",
         PCMK__SERVER_BASED_RO
     },
     {
         0, crm_proc_controld,   6, 0, TRUE, "pacemaker-controld",
         CRM_DAEMON_USER, CRM_DAEMON_DIR "/pacemaker-controld",
         CRM_SYSTEM_CRMD
     },
     {
         0, crm_proc_attrd,      4, 0, TRUE, "pacemaker-attrd",
         CRM_DAEMON_USER, CRM_DAEMON_DIR "/pacemaker-attrd",
         T_ATTRD
     },
     {
         0, crm_proc_schedulerd, 5, 0, TRUE, "pacemaker-schedulerd",
         CRM_DAEMON_USER, CRM_DAEMON_DIR "/pacemaker-schedulerd",
         CRM_SYSTEM_PENGINE
     },
     {
         0, crm_proc_fenced,     2, 0, TRUE, "pacemaker-fenced",
         NULL, CRM_DAEMON_DIR "/pacemaker-fenced",
         "stonith-ng"
     },
 };
 
 static gboolean check_active_before_startup_processes(gpointer user_data);
 static int child_liveness(pcmk_child_t *child);
 static gboolean start_child(pcmk_child_t * child);
-static gboolean update_node_processes(uint32_t id, const char *uname,
-                                      uint32_t procs);
-void update_process_clients(pcmk__client_t *client);
-
-static uint32_t
-get_process_list(void)
-{
-    int lpc = 0;
-    uint32_t procs = crm_get_cluster_proc();
-
-    for (lpc = 0; lpc < SIZEOF(pcmk_children); lpc++) {
-        if (pcmk_children[lpc].pid != 0) {
-            procs |= pcmk_children[lpc].flag;
-        }
-    }
-    return procs;
-}
 
 static void
 pcmk_process_exit(pcmk_child_t * child)
 {
     child->pid = 0;
     child->active_before_startup = FALSE;
 
-    /* Broadcast the fact that one of our processes died ASAP
-     *
-     * Try to get some logging of the cause out first though
-     * because we're probably about to get fenced
-     *
-     * Potentially do this only if respawn_count > N
-     * to allow for local recovery
-     */
-    update_node_processes(local_nodeid, NULL, get_process_list());
-
     child->respawn_count += 1;
     if (child->respawn_count > MAX_RESPAWN) {
         crm_err("Child respawn count exceeded by %s", child->name);
         child->respawn = FALSE;
     }
 
     if (shutdown_trigger) {
         /* resume step-wise shutdown (returned TRUE yields no parallelizing) */
         mainloop_set_trigger(shutdown_trigger);
-        /* intended to speed up propagating expected lay-off of the daemons? */
-        update_node_processes(local_nodeid, NULL, get_process_list());
 
     } else if (!child->respawn) {
         /* nothing to do */
 
     } else if (crm_is_true(getenv("PCMK_fail_fast"))) {
         crm_err("Rebooting system because of %s", child->name);
         pcmk_panic(__FUNCTION__);
 
     } else if (child_liveness(child) == pcmk_rc_ok) {
         crm_warn("One-off suppressing strict respawning of a child process %s,"
                  " appears alright per %s IPC end-point",
                  child->name, child->endpoint);
         /* need to monitor how it evolves, and start new process if badly */
         child->active_before_startup = TRUE;
         if (!global_keep_tracking) {
             global_keep_tracking = true;
             g_timeout_add_seconds(PCMK_PROCESS_CHECK_INTERVAL,
                                   check_active_before_startup_processes, NULL);
         }
 
     } else {
         crm_notice("Respawning failed child process: %s", child->name);
         start_child(child);
     }
 }
 
-static void pcmk_exit_with_cluster(int exitcode)
-{
-#ifdef SUPPORT_COROSYNC
-    corosync_cfg_handle_t cfg_handle;
-    cs_error_t err;
-
-    if (exitcode == CRM_EX_FATAL) {
-	    crm_info("Asking Corosync to shut down");
-	    err = corosync_cfg_initialize(&cfg_handle, NULL);
-	    if (err != CS_OK) {
-		    crm_warn("Unable to open handle to corosync to close it down. err=%d", err);
-	    }
-	    err = corosync_cfg_try_shutdown(cfg_handle, COROSYNC_CFG_SHUTDOWN_FLAG_IMMEDIATE);
-	    if (err != CS_OK) {
-		    crm_warn("Corosync shutdown failed. err=%d", err);
-	    }
-	    corosync_cfg_finalize(cfg_handle);
-    }
-#endif
-    crm_exit(exitcode);
-}
-
 static void
 pcmk_child_exit(mainloop_child_t * p, pid_t pid, int core, int signo, int exitcode)
 {
     pcmk_child_t *child = mainloop_child_userdata(p);
     const char *name = mainloop_child_name(p);
 
     if (signo) {
         do_crm_log(((signo == SIGKILL)? LOG_WARNING : LOG_ERR),
                    "%s[%d] terminated with signal %d (core=%d)",
                    name, pid, signo, core);
 
     } else {
         switch(exitcode) {
             case CRM_EX_OK:
                 crm_info("%s[%d] exited with status %d (%s)",
                          name, pid, exitcode, crm_exit_str(exitcode));
                 break;
 
             case CRM_EX_FATAL:
                 crm_warn("Shutting cluster down because %s[%d] had fatal failure",
                          name, pid);
                 child->respawn = FALSE;
                 fatal_error = TRUE;
                 pcmk_shutdown(SIGTERM);
                 break;
 
             case CRM_EX_PANIC:
                 crm_emerg("%s[%d] instructed the machine to reset", name, pid);
                 child->respawn = FALSE;
                 fatal_error = TRUE;
                 pcmk_panic(__FUNCTION__);
                 pcmk_shutdown(SIGTERM);
                 break;
 
             default:
                 crm_err("%s[%d] exited with status %d (%s)",
                         name, pid, exitcode, crm_exit_str(exitcode));
                 break;
         }
     }
 
     pcmk_process_exit(child);
 }
 
 static gboolean
 stop_child(pcmk_child_t * child, int signal)
 {
     if (signal == 0) {
         signal = SIGTERM;
     }
 
     /* why to skip PID of 1?
        - FreeBSD ~ how untrackable process behind IPC is masqueraded as
        - elsewhere: how "init" task is designated; in particular, in systemd
          arrangement of socket-based activation, this is pretty real */
     if (child->command == NULL || child->pid == PCMK__SPECIAL_PID) {
         crm_debug("Nothing to do for child \"%s\" (process %lld)",
                   child->name, (long long) PCMK__SPECIAL_PID_AS_0(child->pid));
         return TRUE;
     }
 
     if (child->pid <= 0) {
         crm_trace("Client %s not running", child->name);
         return TRUE;
     }
 
     errno = 0;
     if (kill(child->pid, signal) == 0) {
         crm_notice("Stopping %s "CRM_XS" sent signal %d to process %lld",
                    child->name, signal, (long long) child->pid);
 
     } else {
         crm_err("Could not stop %s (process %lld) with signal %d: %s",
                 child->name, (long long) child->pid, signal, strerror(errno));
     }
 
     return TRUE;
 }
 
 static char *opts_default[] = { NULL, NULL };
 static char *opts_vgrind[] = { NULL, NULL, NULL, NULL, NULL };
 
 /* TODO once libqb is taught to juggle with IPC end-points carried over as
         bare file descriptor (https://github.com/ClusterLabs/libqb/issues/325)
         it shall hand over these descriptors here if/once they are successfully
         pre-opened in (presumably) child_liveness(), to avoid any remaining
         room for races */
 static gboolean
 start_child(pcmk_child_t * child)
 {
     uid_t uid = 0;
     gid_t gid = 0;
     gboolean use_valgrind = FALSE;
     gboolean use_callgrind = FALSE;
     const char *env_valgrind = getenv("PCMK_valgrind_enabled");
     const char *env_callgrind = getenv("PCMK_callgrind_enabled");
 
     child->active_before_startup = FALSE;
 
     if (child->command == NULL) {
         crm_info("Nothing to do for child \"%s\"", child->name);
         return TRUE;
     }
 
     if (env_callgrind != NULL && crm_is_true(env_callgrind)) {
         use_callgrind = TRUE;
         use_valgrind = TRUE;
 
     } else if (env_callgrind != NULL && strstr(env_callgrind, child->name)) {
         use_callgrind = TRUE;
         use_valgrind = TRUE;
 
     } else if (env_valgrind != NULL && crm_is_true(env_valgrind)) {
         use_valgrind = TRUE;
 
     } else if (env_valgrind != NULL && strstr(env_valgrind, child->name)) {
         use_valgrind = TRUE;
     }
 
     if (use_valgrind && strlen(VALGRIND_BIN) == 0) {
         crm_warn("Cannot enable valgrind for %s:"
                  " The location of the valgrind binary is unknown", child->name);
         use_valgrind = FALSE;
     }
 
     if (child->uid) {
         if (crm_user_lookup(child->uid, &uid, &gid) < 0) {
             crm_err("Invalid user (%s) for %s: not found", child->uid, child->name);
             return FALSE;
         }
         crm_info("Using uid=%u and group=%u for process %s", uid, gid, child->name);
     }
 
     child->pid = fork();
     CRM_ASSERT(child->pid != -1);
 
     if (child->pid > 0) {
         /* parent */
         mainloop_child_add(child->pid, 0, child->name, child, pcmk_child_exit);
 
         crm_info("Forked child %lld for process %s%s",
                  (long long) child->pid, child->name,
                  use_valgrind ? " (valgrind enabled: " VALGRIND_BIN ")" : "");
-        update_node_processes(local_nodeid, NULL, get_process_list());
         return TRUE;
 
     } else {
         /* Start a new session */
         (void)setsid();
 
         /* Setup the two alternate arg arrays */
         opts_vgrind[0] = strdup(VALGRIND_BIN);
         if (use_callgrind) {
             opts_vgrind[1] = strdup("--tool=callgrind");
             opts_vgrind[2] = strdup("--callgrind-out-file=" CRM_STATE_DIR "/callgrind.out.%p");
             opts_vgrind[3] = strdup(child->command);
             opts_vgrind[4] = NULL;
         } else {
             opts_vgrind[1] = strdup(child->command);
             opts_vgrind[2] = NULL;
             opts_vgrind[3] = NULL;
             opts_vgrind[4] = NULL;
         }
         opts_default[0] = strdup(child->command);
 
         if(gid) {
             // Whether we need root group access to talk to cluster layer
             bool need_root_group = TRUE;
 
             if (is_corosync_cluster()) {
                 /* Corosync clusters can drop root group access, because we set
                  * uidgid.gid.${gid}=1 via CMAP, which allows these processes to
                  * connect to corosync.
                  */
                 need_root_group = FALSE;
             }
 
             // Drop root group access if not needed
             if (!need_root_group && (setgid(gid) < 0)) {
-                crm_perror(LOG_ERR, "Could not set group to %d", gid);
+                crm_warn("Could not set group to %d: %s", gid, strerror(errno));
             }
 
             /* Initialize supplementary groups to only those always granted to
              * the user, plus haclient (so we can access IPC).
              */
             if (initgroups(child->uid, gid) < 0) {
                 crm_err("Cannot initialize groups for %s: %s (%d)", child->uid, pcmk_strerror(errno), errno);
             }
         }
 
         if (uid && setuid(uid) < 0) {
-            crm_perror(LOG_ERR, "Could not set user to %d (%s)", uid, child->uid);
+            crm_warn("Could not set user to %s (id %d): %s",
+                     child->uid, uid, strerror(errno));
         }
 
         pcmk__close_fds_in_child(true);
 
         pcmk__open_devnull(O_RDONLY);   // stdin (fd 0)
         pcmk__open_devnull(O_WRONLY);   // stdout (fd 1)
         pcmk__open_devnull(O_WRONLY);   // stderr (fd 2)
 
         if (use_valgrind) {
             (void)execvp(VALGRIND_BIN, opts_vgrind);
         } else {
             (void)execvp(child->command, opts_default);
         }
-        crm_perror(LOG_ERR, "FATAL: Cannot exec %s", child->command);
+        crm_crit("Could not execute %s: %s", child->command, strerror(errno));
         crm_exit(CRM_EX_FATAL);
     }
     return TRUE;                /* never reached */
 }
 
 static gboolean
 escalate_shutdown(gpointer data)
 {
 
     pcmk_child_t *child = data;
 
     if (child->pid == PCMK__SPECIAL_PID) {
         pcmk_process_exit(child);
 
     } else if (child->pid != 0) {
         /* Use SIGSEGV instead of SIGKILL to create a core so we can see what it was up to */
         crm_err("Child %s not terminating in a timely manner, forcing", child->name);
         stop_child(child, SIGSEGV);
     }
     return FALSE;
 }
 
 #define SHUTDOWN_ESCALATION_PERIOD 180000  /* 3m */
 
 static gboolean
 pcmk_shutdown_worker(gpointer user_data)
 {
     static int phase = 0;
     static time_t next_log = 0;
     static int max = SIZEOF(pcmk_children);
 
     int lpc = 0;
 
     if (phase == 0) {
         crm_notice("Shutting down Pacemaker");
         phase = max;
     }
 
     for (; phase > 0; phase--) {
         /* Don't stop anything with start_seq < 1 */
 
         for (lpc = max - 1; lpc >= 0; lpc--) {
             pcmk_child_t *child = &(pcmk_children[lpc]);
 
             if (phase != child->start_seq) {
                 continue;
             }
 
             if (child->pid != 0) {
                 time_t now = time(NULL);
 
                 if (child->respawn) {
                     if (child->pid == PCMK__SPECIAL_PID) {
                         crm_warn("The process behind %s IPC cannot be"
                                  " terminated, so either wait the graceful"
                                  " period of %ld s for its native termination"
                                  " if it vitally depends on some other daemons"
                                  " going down in a controlled way already,"
                                  " or locate and kill the correct %s process"
                                  " on your own; set PCMK_fail_fast=1 to avoid"
                                  " this altogether next time around",
                                  child->name, (long) SHUTDOWN_ESCALATION_PERIOD,
                                  child->command);
                     }
                     next_log = now + 30;
                     child->respawn = FALSE;
                     stop_child(child, SIGTERM);
                     if (phase < pcmk_children[PCMK_CHILD_CONTROLD].start_seq) {
                         g_timeout_add(SHUTDOWN_ESCALATION_PERIOD,
                                       escalate_shutdown, child);
                     }
 
                 } else if (now >= next_log) {
                     next_log = now + 30;
                     crm_notice("Still waiting for %s to terminate "
                                CRM_XS " pid=%lld seq=%d",
                                child->name, (long long) child->pid,
                                child->start_seq);
                 }
                 return TRUE;
             }
 
             /* cleanup */
             crm_debug("%s confirmed stopped", child->name);
             child->pid = 0;
         }
     }
 
-    /* send_cluster_id(); */
     crm_notice("Shutdown complete");
 
     {
         const char *delay = pcmk__env_option("shutdown_delay");
         if(delay) {
             sync();
             sleep(crm_get_msec(delay) / 1000);
         }
     }
 
     g_main_loop_quit(mainloop);
 
     if (fatal_error) {
         crm_notice("Shutting down and staying down after fatal error");
-        pcmk_exit_with_cluster(CRM_EX_FATAL);
+#ifdef SUPPORT_COROSYNC
+        pcmkd_shutdown_corosync();
+#endif
+        crm_exit(CRM_EX_FATAL);
     }
 
     return TRUE;
 }
 
 static void
 pcmk_ignore(int nsig)
 {
     crm_info("Ignoring signal %s (%d)", strsignal(nsig), nsig);
 }
 
 static void
 pcmk_sigquit(int nsig)
 {
     pcmk_panic(__FUNCTION__);
 }
 
 void
 pcmk_shutdown(int nsig)
 {
     if (shutdown_trigger == NULL) {
         shutdown_trigger = mainloop_add_trigger(G_PRIORITY_HIGH, pcmk_shutdown_worker, NULL);
     }
     mainloop_set_trigger(shutdown_trigger);
 }
 
 static int32_t
 pcmk_ipc_accept(qb_ipcs_connection_t * c, uid_t uid, gid_t gid)
 {
     crm_trace("Connection %p", c);
     if (pcmk__new_client(c, uid, gid) == NULL) {
         return -EIO;
     }
     return 0;
 }
 
 /* Exit code means? */
 static int32_t
 pcmk_ipc_dispatch(qb_ipcs_connection_t * qbc, void *data, size_t size)
 {
     uint32_t id = 0;
     uint32_t flags = 0;
     const char *task = NULL;
     pcmk__client_t *c = pcmk__find_client(qbc);
     xmlNode *msg = pcmk__client_data2xml(c, data, &id, &flags);
 
     pcmk__ipc_send_ack(c, id, flags, "ack");
     if (msg == NULL) {
         return 0;
     }
 
     task = crm_element_value(msg, F_CRM_TASK);
     if (crm_str_eq(task, CRM_OP_QUIT, TRUE)) {
-        /* Time to quit */
-        crm_notice("Shutting down in response to ticket %s (%s)",
+        crm_notice("Shutting down in response to IPC request %s from %s",
                    crm_element_value(msg, F_CRM_REFERENCE), crm_element_value(msg, F_CRM_ORIGIN));
         pcmk_shutdown(15);
 
     } else if (crm_str_eq(task, CRM_OP_RM_NODE_CACHE, TRUE)) {
-        /* Send to everyone */
-        struct iovec *iov;
-        int id = 0;
-        const char *name = NULL;
-
-        crm_element_value_int(msg, XML_ATTR_ID, &id);
-        name = crm_element_value(msg, XML_ATTR_UNAME);
-        crm_notice("Instructing peers to remove references to node %s/%u", name, id);
-
-        iov = calloc(1, sizeof(struct iovec));
-        iov->iov_base = dump_xml_unformatted(msg);
-        iov->iov_len = 1 + strlen(iov->iov_base);
-        send_cpg_iov(iov);
+        crm_trace("Ignoring IPC request to purge node "
+                  "because peer cache is not used");
 
     } else {
-        update_process_clients(c);
+        crm_debug("Unrecognized IPC command '%s' sent to pacemakerd",
+                  crm_str(task));
     }
 
     free_xml(msg);
     return 0;
 }
 
 /* Error code means? */
 static int32_t
 pcmk_ipc_closed(qb_ipcs_connection_t * c)
 {
     pcmk__client_t *client = pcmk__find_client(c);
 
     if (client == NULL) {
         return 0;
     }
     crm_trace("Connection %p", c);
     pcmk__free_client(client);
     return 0;
 }
 
 static void
 pcmk_ipc_destroy(qb_ipcs_connection_t * c)
 {
     crm_trace("Connection %p", c);
     pcmk_ipc_closed(c);
 }
 
 struct qb_ipcs_service_handlers mcp_ipc_callbacks = {
     .connection_accept = pcmk_ipc_accept,
     .connection_created = NULL,
     .msg_process = pcmk_ipc_dispatch,
     .connection_closed = pcmk_ipc_closed,
     .connection_destroyed = pcmk_ipc_destroy
 };
 
-static void
-send_xml_to_client(gpointer key, gpointer value, gpointer user_data)
-{
-    pcmk__ipc_send_xml((pcmk__client_t *) value, 0, (xmlNode *) user_data,
-                       crm_ipc_server_event);
-}
-
-/*!
- * \internal
- * \brief Send an XML message with process list of all known peers to client(s)
- *
- * \param[in] client  Send message to this client, or all clients if NULL
- */
-void
-update_process_clients(pcmk__client_t *client)
-{
-    GHashTableIter iter;
-    crm_node_t *node = NULL;
-    xmlNode *update = create_xml_node(NULL, "nodes");
-
-    if (is_corosync_cluster()) {
-        crm_xml_add_int(update, "quorate", pcmk_quorate);
-    }
-
-    g_hash_table_iter_init(&iter, crm_peer_cache);
-    while (g_hash_table_iter_next(&iter, NULL, (gpointer *) & node)) {
-        xmlNode *xml = create_xml_node(update, "node");
-
-        crm_xml_add_int(xml, "id", node->id);
-        crm_xml_add(xml, "uname", node->uname);
-        crm_xml_add(xml, "state", node->state);
-        crm_xml_add_int(xml, "processes", node->processes);
-    }
-
-    if(client) {
-        crm_trace("Sending process list to client %s", client->id);
-        send_xml_to_client(NULL, client, update);
-
-    } else {
-        crm_trace("Sending process list to %d clients",
-                  pcmk__ipc_client_count());
-        pcmk__foreach_ipc_client(send_xml_to_client, update);
-    }
-    free_xml(update);
-}
-
-/*!
- * \internal
- * \brief Send a CPG message with local node's process list to all peers
- */
-static void
-update_process_peers(void)
-{
-    /* Do nothing for corosync-2 based clusters */
-
-    struct iovec *iov = calloc(1, sizeof(struct iovec));
-
-    CRM_ASSERT(iov);
-    if (local_name) {
-        iov->iov_base = crm_strdup_printf("<node uname=\"%s\" proclist=\"%u\"/>",
-                                          local_name, get_process_list());
-    } else {
-        iov->iov_base = crm_strdup_printf("<node proclist=\"%u\"/>",
-                                          get_process_list());
-    }
-    iov->iov_len = strlen(iov->iov_base) + 1;
-    crm_trace("Sending %s", (char*) iov->iov_base);
-    send_cpg_iov(iov);
-}
-
-/*!
- * \internal
- * \brief Update a node's process list, notifying clients and peers if needed
- *
- * \param[in] id     Node ID of affected node
- * \param[in] uname  Uname of affected node
- * \param[in] procs  Affected node's process list mask
- *
- * \return TRUE if the process list changed, FALSE otherwise
- */
-static gboolean
-update_node_processes(uint32_t id, const char *uname, uint32_t procs)
-{
-    gboolean changed = FALSE;
-    crm_node_t *node = crm_get_peer(id, uname);
-
-    if (procs != 0) {
-        if (procs != node->processes) {
-            crm_debug("Node %s now has process list: %.32x (was %.32x)",
-                      node->uname, procs, node->processes);
-            node->processes = procs;
-            changed = TRUE;
-
-            /* If local node's processes have changed, notify clients/peers */
-            if (id == local_nodeid) {
-                update_process_clients(NULL);
-                update_process_peers();
-            }
-
-        } else {
-            crm_trace("Node %s still has process list: %.32x", node->uname, procs);
-        }
-    }
-    return changed;
-}
-
-
 static pcmk__cli_option_t long_options[] = {
     // long option, argument type, storage, short option, description, flags
     {
         "help", no_argument, NULL, '?',
         "\tThis text", pcmk__option_default
     },
     {
         "version", no_argument, NULL, '$',
         "\tVersion information", pcmk__option_default
     },
     {
         "verbose", no_argument, NULL, 'V',
         "\tIncrease debug output", pcmk__option_default
     },
     {
         "shutdown", no_argument, NULL, 'S',
         "\tInstruct Pacemaker to shutdown on this machine", pcmk__option_default
     },
     {
         "features", no_argument, NULL, 'F',
         "\tDisplay full version and list of features Pacemaker was built with",
         pcmk__option_default
     },
     {
         "-spacer-", no_argument, NULL, '-',
         "\nAdditional Options:", pcmk__option_default
     },
     {
         "foreground", no_argument, NULL, 'f',
         "\t(Ignored) Pacemaker always runs in the foreground",
         pcmk__option_default
     },
     {
         "pid-file", required_argument, NULL, 'p',
         "\t(Ignored) Daemon pid file location", pcmk__option_default
     },
     {
         "standby", no_argument, NULL, 's',
         "\tStart node in standby state", pcmk__option_default
     },
     { 0, 0, 0, 0 }
 };
 
 static void
 mcp_chown(const char *path, uid_t uid, gid_t gid)
 {
     int rc = chown(path, uid, gid);
 
     if (rc < 0) {
         crm_warn("Cannot change the ownership of %s to user %s and gid %d: %s",
                  path, CRM_DAEMON_USER, gid, pcmk_strerror(errno));
     }
 }
 
 /*!
  * \internal
  * \brief Check the liveness of the child based on IPC name and PID if tracked
  *
  * \param[inout] child  Child tracked data
  *
  * \return Standard Pacemaker return code
  *
  * \note Return codes of particular interest include pcmk_rc_ipc_unresponsive
  *       indicating that no trace of IPC liveness was detected,
  *       pcmk_rc_ipc_unauthorized indicating that the IPC endpoint is blocked by
  *       an unauthorized process, and pcmk_rc_ipc_pid_only indicating that
  *       the child is up by PID but not IPC end-point (possibly starting).
  * \note This function doesn't modify any of \p child members but \c pid,
  *       and is not actively toying with processes as such but invoking
  *       \c stop_child in one particular case (there's for some reason
  *       a different authentic holder of the IPC end-point).
  */
 static int
 child_liveness(pcmk_child_t *child)
 {
     uid_t cl_uid = 0;
     gid_t cl_gid = 0;
     const uid_t root_uid = 0;
     const gid_t root_gid = 0;
     const uid_t *ref_uid;
     const gid_t *ref_gid;
     int rc = pcmk_rc_ipc_unresponsive;
     pid_t ipc_pid = 0;
 
     if (child->endpoint == NULL
             && (child->pid <= 0 || child->pid == PCMK__SPECIAL_PID)) {
         crm_err("Cannot track child %s for missing both API end-point and PID",
                 child->name);
         rc = EINVAL; // Misuse of function when child is not trackable
 
     } else if (child->endpoint != NULL) {
         int legacy_rc = pcmk_ok;
 
         if (child->uid == NULL) {
             ref_uid = &root_uid;
             ref_gid = &root_gid;
         } else {
             ref_uid = &cl_uid;
             ref_gid = &cl_gid;
             legacy_rc = pcmk_daemon_user(&cl_uid, &cl_gid);
         }
 
         if (legacy_rc < 0) {
             rc = pcmk_legacy2rc(legacy_rc);
             crm_err("Could not find user and group IDs for user %s: %s "
                     CRM_XS " rc=%d", CRM_DAEMON_USER, pcmk_rc_str(rc), rc);
         } else {
             rc = pcmk__ipc_is_authentic_process_active(child->endpoint,
                                                        *ref_uid, *ref_gid,
                                                        &ipc_pid);
             if ((rc == pcmk_rc_ok) || (rc == pcmk_rc_ipc_unresponsive)) {
                 if (child->pid <= 0) {
                     /* If rc is pcmk_rc_ok, ipc_pid is nonzero and this
                      * initializes a new child. If rc is
                      * pcmk_rc_ipc_unresponsive, ipc_pid is zero, and we will
                      * investigate further.
                      */
                     child->pid = ipc_pid;
                 } else if ((ipc_pid != 0) && (child->pid != ipc_pid)) {
                     /* An unexpected (but authorized) process is responding to
                      * IPC. Investigate further.
                      */
                     rc = pcmk_rc_ipc_unresponsive;
                 }
             }
         }
     }
 
     if (rc == pcmk_rc_ipc_unresponsive) {
         /* If we get here, a child without IPC is being tracked, no IPC liveness
          * has been detected, or IPC liveness has been detected with an
          * unexpected (but authorized) process. This is safe on FreeBSD since
          * the only change possible from a proper child's PID into "special" PID
          * of 1 behind more loosely related process.
          */
         int ret = pcmk__pid_active(child->pid, child->name);
 
         if (ipc_pid && ((ret != pcmk_rc_ok)
                         || ipc_pid == PCMK__SPECIAL_PID
                         || (pcmk__pid_active(ipc_pid,
                                              child->name) == pcmk_rc_ok))) {
             /* An unexpected (but authorized) process was detected at the IPC
              * endpoint, and either it is active, or the child we're tracking is
              * not.
              */
 
             if (ret == pcmk_rc_ok) {
                 /* The child we're tracking is active. Kill it, and adopt the
                  * detected process. This assumes that our children don't fork
                  * (thus getting a different PID owning the IPC), but rather the
                  * tracking got out of sync because of some means external to
                  * Pacemaker, and adopting the detected process is better than
                  * killing it and possibly having to spawn a new child.
                  */
                 /* not possessing IPC, afterall (what about corosync CPG?) */
                 stop_child(child, SIGKILL);
             }
             rc = pcmk_rc_ok;
             child->pid = ipc_pid;
         } else if (ret == pcmk_rc_ok) {
             // Our tracked child's PID was found active, but not its IPC
             rc = pcmk_rc_ipc_pid_only;
         } else if ((child->pid == 0) && (ret == EINVAL)) {
             // FreeBSD can return EINVAL
             rc = pcmk_rc_ipc_unresponsive;
         } else {
             switch (ret) {
                 case EACCES:
                     rc = pcmk_rc_ipc_unauthorized;
                     break;
                 case ESRCH:
                     rc = pcmk_rc_ipc_unresponsive;
                     break;
                 default:
                     rc = ret;
                     break;
             }
         }
     }
     return rc;
 }
 
 static gboolean
 check_active_before_startup_processes(gpointer user_data)
 {
     int start_seq = 1, lpc = 0;
     static int max = SIZEOF(pcmk_children);
     gboolean keep_tracking = FALSE;
 
     for (start_seq = 1; start_seq < max; start_seq++) {
         for (lpc = 0; lpc < max; lpc++) {
             if (pcmk_children[lpc].active_before_startup == FALSE) {
                 /* we are already tracking it as a child process. */
                 continue;
             } else if (start_seq != pcmk_children[lpc].start_seq) {
                 continue;
             } else {
                 int rc = child_liveness(&pcmk_children[lpc]);
 
                 switch (rc) {
                     case pcmk_rc_ok:
                         break;
                     case pcmk_rc_ipc_unresponsive:
                     case pcmk_rc_ipc_pid_only: // This case: it was previously OK
                         if (pcmk_children[lpc].respawn == TRUE) {
                             crm_err("%s[%lld] terminated%s", pcmk_children[lpc].name,
                                     (long long) PCMK__SPECIAL_PID_AS_0(pcmk_children[lpc].pid),
                                     (rc == pcmk_rc_ipc_pid_only)? " as IPC server" : "");
                         } else {
                             /* orderly shutdown */
                             crm_notice("%s[%lld] terminated%s", pcmk_children[lpc].name,
                                        (long long) PCMK__SPECIAL_PID_AS_0(pcmk_children[lpc].pid),
                                        (rc == pcmk_rc_ipc_pid_only)? " as IPC server" : "");
                         }
                         pcmk_process_exit(&(pcmk_children[lpc]));
                         continue;
                     default:
                         crm_exit(CRM_EX_FATAL);
                         break;  /* static analysis/noreturn */
                 }
             }
             /* at least one of the processes found at startup
              * is still going, so keep this recurring timer around */
             keep_tracking = TRUE;
         }
     }
 
     global_keep_tracking = keep_tracking;
     return keep_tracking;
 }
 
 /*!
  * \internal
  * \brief Initial one-off check of the pre-existing "child" processes
  *
  * With "child" process, we mean the subdaemon that defines an API end-point
  * (all of them do as of the comment) -- the possible complement is skipped
  * as it is deemed it has no such shared resources to cause conflicts about,
  * hence it can presumably be started anew without hesitation.
  * If that won't hold true in the future, the concept of a shared resource
  * will have to be generalized beyond the API end-point.
  *
  * For boundary cases that the "child" is still starting (IPC end-point is yet
  * to be witnessed), or more rarely (practically FreeBSD only), when there's
  * a pre-existing "untrackable" authentic process, we give the situation some
  * time to possibly unfold in the right direction, meaning that said socket
  * will appear or the unattainable process will disappear per the observable
  * IPC, respectively.
  *
  * \return Standard Pacemaker return code
  *
  * \note Since this gets run at the very start, \c respawn_count fields
  *       for particular children get temporarily overloaded with "rounds
  *       of waiting" tracking, restored once we are about to finish with
  *       success (i.e. returning value >=0) and will remain unrestored
  *       otherwise.  One way to suppress liveness detection logic for
  *       particular child is to set the said value to a negative number.
  */
 #define WAIT_TRIES 4  /* together with interleaved sleeps, worst case ~ 1s */
 static int
 find_and_track_existing_processes(void)
 {
     bool tracking = false;
     bool wait_in_progress;
     int rc;
     size_t i, rounds;
 
     for (rounds = 1; rounds <= WAIT_TRIES; rounds++) {
         wait_in_progress = false;
         for (i = 0; i < SIZEOF(pcmk_children); i++) {
 
             if ((pcmk_children[i].endpoint == NULL)
                 || (pcmk_children[i].respawn_count < 0)) {
                 continue;
             }
 
             rc = child_liveness(&pcmk_children[i]);
             if (rc == pcmk_rc_ipc_unresponsive) {
                 /* As a speculation, don't give up if there are more rounds to
                  * come for other reasons, but don't artificially wait just
                  * because of this, since we would preferably start ASAP.
                  */
                 continue;
             }
 
             pcmk_children[i].respawn_count = rounds;
             switch (rc) {
                 case pcmk_rc_ok:
                     if (pcmk_children[i].pid == PCMK__SPECIAL_PID) {
                         if (crm_is_true(getenv("PCMK_fail_fast"))) {
                             crm_crit("Cannot reliably track pre-existing"
                                      " authentic process behind %s IPC on this"
                                      " platform and PCMK_fail_fast requested",
                                      pcmk_children[i].endpoint);
                             return EOPNOTSUPP;
                         } else if (pcmk_children[i].respawn_count == WAIT_TRIES) {
                             crm_notice("Assuming pre-existing authentic, though"
                                        " on this platform untrackable, process"
                                        " behind %s IPC is stable (was in %d"
                                        " previous samples) so rather than"
                                        " bailing out (PCMK_fail_fast not"
                                        " requested), we just switch to a less"
                                        " optimal IPC liveness monitoring"
                                        " (not very suitable for heavy load)",
                                        pcmk_children[i].name, WAIT_TRIES - 1);
                             crm_warn("The process behind %s IPC cannot be"
                                      " terminated, so the overall shutdown"
                                      " will get delayed implicitly (%ld s),"
                                      " which serves as a graceful period for"
                                      " its native termination if it vitally"
                                      " depends on some other daemons going"
                                      " down in a controlled way already",
                                      pcmk_children[i].name,
                                      (long) SHUTDOWN_ESCALATION_PERIOD);
                         } else {
                             wait_in_progress = true;
                             crm_warn("Cannot reliably track pre-existing"
                                      " authentic process behind %s IPC on this"
                                      " platform, can still disappear in %d"
                                      " attempt(s)", pcmk_children[i].endpoint,
                                      WAIT_TRIES - pcmk_children[i].respawn_count);
                             continue;
                         }
                     }
                     crm_notice("Tracking existing %s process (pid=%lld)",
                                pcmk_children[i].name,
                                (long long) PCMK__SPECIAL_PID_AS_0(
                                                pcmk_children[i].pid));
                     pcmk_children[i].respawn_count = -1;  /* 0~keep watching */
                     pcmk_children[i].active_before_startup = TRUE;
                     tracking = true;
                     break;
                 case pcmk_rc_ipc_pid_only:
                     if (pcmk_children[i].respawn_count == WAIT_TRIES) {
                         crm_crit("%s IPC end-point for existing authentic"
                                  " process %lld did not (re)appear",
                                  pcmk_children[i].endpoint,
                                  (long long) PCMK__SPECIAL_PID_AS_0(
                                                  pcmk_children[i].pid));
                         return rc;
                     }
                     wait_in_progress = true;
                     crm_warn("Cannot find %s IPC end-point for existing"
                              " authentic process %lld, can still (re)appear"
                              " in %d attempts (?)",
                              pcmk_children[i].endpoint,
                              (long long) PCMK__SPECIAL_PID_AS_0(
                                              pcmk_children[i].pid),
                              WAIT_TRIES - pcmk_children[i].respawn_count);
                     continue;
                 default:
                     crm_crit("Checked liveness of %s: %s " CRM_XS " rc=%d",
                              pcmk_children[i].name, pcmk_rc_str(rc), rc);
                     return rc;
             }
         }
         if (!wait_in_progress) {
             break;
         }
         (void) poll(NULL, 0, 250);  /* a bit for changes to possibly happen */
     }
     for (i = 0; i < SIZEOF(pcmk_children); i++) {
         pcmk_children[i].respawn_count = 0;  /* restore pristine state */
     }
 
     if (tracking) {
         g_timeout_add_seconds(PCMK_PROCESS_CHECK_INTERVAL,
                               check_active_before_startup_processes, NULL);
     }
     return pcmk_rc_ok;
 }
 
 static void
 init_children_processes(void)
 {
     int start_seq = 1, lpc = 0;
     static int max = SIZEOF(pcmk_children);
 
     /* start any children that have not been detected */
     for (start_seq = 1; start_seq < max; start_seq++) {
         /* don't start anything with start_seq < 1 */
         for (lpc = 0; lpc < max; lpc++) {
             if (pcmk_children[lpc].pid != 0) {
                 /* we are already tracking it */
                 continue;
             }
 
             if (start_seq == pcmk_children[lpc].start_seq) {
                 start_child(&(pcmk_children[lpc]));
             }
         }
     }
 
     /* From this point on, any daemons being started will be due to
      * respawning rather than node start.
      *
      * This may be useful for the daemons to know
      */
     setenv("PCMK_respawned", "true", 1);
 }
 
 static void
-mcp_cpg_destroy(gpointer user_data)
+remove_core_file_limit(void)
 {
-    crm_crit("Lost connection to cluster layer, shutting down");
-    crm_exit(CRM_EX_DISCONNECT);
-}
-
-/*!
- * \internal
- * \brief Process a CPG message (process list or manual peer cache removal)
- *
- * \param[in] handle     CPG connection (ignored)
- * \param[in] groupName  CPG group name (ignored)
- * \param[in] nodeid     ID of affected node
- * \param[in] pid        Process ID (ignored)
- * \param[in] msg        CPG XML message
- * \param[in] msg_len    Length of msg in bytes (ignored)
- */
-static void
-mcp_cpg_deliver(cpg_handle_t handle,
-                 const struct cpg_name *groupName,
-                 uint32_t nodeid, uint32_t pid, void *msg, size_t msg_len)
-{
-    xmlNode *xml = string2xml(msg);
-    const char *task = crm_element_value(xml, F_CRM_TASK);
-
-    crm_trace("Received CPG message (%s): %.200s",
-              (task? task : "process list"), (char*)msg);
-
-    if (task == NULL) {
-        if (nodeid == local_nodeid) {
-            crm_debug("Ignoring message with local node's process list");
-        } else {
-            uint32_t procs = 0;
-            const char *uname = crm_element_value(xml, "uname");
-
-            crm_element_value_int(xml, "proclist", (int *)&procs);
-            if (update_node_processes(nodeid, uname, procs)) {
-                update_process_clients(NULL);
-            }
-        }
-
-    } else if (crm_str_eq(task, CRM_OP_RM_NODE_CACHE, TRUE)) {
-        int id = 0;
-        const char *name = NULL;
+    struct rlimit cores;
+    int rc = getrlimit(RLIMIT_CORE, &cores);
 
-        crm_element_value_int(xml, XML_ATTR_ID, &id);
-        name = crm_element_value(xml, XML_ATTR_UNAME);
-        reap_crm_member(id, name);
+    if (rc < 0) {
+        crm_warn("Cannot determine current maximum core file size: %s",
+                 strerror(errno));
+        return;
     }
 
-    if (xml != NULL) {
-        free_xml(xml);
+    if ((cores.rlim_max == 0) && (geteuid() == 0)) {
+        cores.rlim_max = RLIM_INFINITY;
+    } else {
+        crm_info("Maximum core file size is %llu bytes",
+                 (unsigned long long) cores.rlim_max);
     }
-}
-
-static void
-mcp_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)
-{
-    /* Update peer cache if needed */
-    pcmk_cpg_membership(handle, groupName, member_list, member_list_entries,
-                        left_list, left_list_entries,
-                        joined_list, joined_list_entries);
-
-    /* Always broadcast our own presence after any membership change */
-    update_process_peers();
-}
+    cores.rlim_cur = cores.rlim_max;
 
-static gboolean
-mcp_quorum_callback(unsigned long long seq, gboolean quorate)
-{
-    pcmk_quorate = quorate;
-    return TRUE;
-}
-
-static void
-mcp_quorum_destroy(gpointer user_data)
-{
-    crm_info("connection lost");
+    rc = setrlimit(RLIMIT_CORE, &cores);
+    if (rc < 0) {
+        crm_warn("Cannot raise system limit on core file size "
+                 "(consider doing so manually)");
+    }
 }
 
 int
 main(int argc, char **argv)
 {
-    int rc;
     int flag;
     int argerr = 0;
 
     int option_index = 0;
     gboolean shutdown = FALSE;
 
     uid_t pcmk_uid = 0;
     gid_t pcmk_gid = 0;
-    struct rlimit cores;
     crm_ipc_t *old_instance = NULL;
     qb_ipcs_service_t *ipcs = NULL;
-    static crm_cluster_t cluster;
 
     crm_log_preinit(NULL, argc, argv);
     pcmk__set_cli_options(NULL, "[options]", long_options,
                           "primary Pacemaker daemon that launches and "
                           "monitors all subsidiary Pacemaker daemons");
     mainloop_add_signal(SIGHUP, pcmk_ignore);
     mainloop_add_signal(SIGQUIT, pcmk_sigquit);
 
     while (1) {
         flag = pcmk__next_cli_option(argc, argv, &option_index, NULL);
         if (flag == -1)
             break;
 
         switch (flag) {
             case 'V':
                 crm_bump_log_level(argc, argv);
                 break;
             case 'f':
                 /* Legacy */
                 break;
             case 'p':
                 pid_file = optarg;
                 break;
             case 's':
                 pcmk__set_env_option("node_start_state", "standby");
                 break;
             case '$':
             case '?':
                 pcmk__cli_help(flag, CRM_EX_OK);
                 break;
             case 'S':
                 shutdown = TRUE;
                 break;
             case 'F':
                 printf("Pacemaker %s (Build: %s)\n Supporting v%s: %s\n", PACEMAKER_VERSION, BUILD_VERSION,
                        CRM_FEATURE_SET, CRM_FEATURES);
                 crm_exit(CRM_EX_OK);
             default:
                 printf("Argument code 0%o (%c) is not (?yet?) supported\n", flag, flag);
                 ++argerr;
                 break;
         }
     }
 
     if (optind < argc) {
         printf("non-option ARGV-elements: ");
         while (optind < argc)
             printf("%s ", argv[optind++]);
         printf("\n");
     }
     if (argerr) {
         pcmk__cli_help('?', CRM_EX_USAGE);
     }
 
 
     setenv("LC_ALL", "C", 1);
 
     pcmk__set_env_option("mcp", "true");
 
     crm_log_init(NULL, LOG_INFO, TRUE, FALSE, argc, argv, FALSE);
 
     crm_debug("Checking for existing Pacemaker instance");
     old_instance = crm_ipc_new(CRM_SYSTEM_MCP, 0);
     (void) crm_ipc_connect(old_instance);
 
     if (shutdown) {
         crm_debug("Shutting down existing Pacemaker instance by request");
         while (crm_ipc_connected(old_instance)) {
             xmlNode *cmd =
                 create_request(CRM_OP_QUIT, NULL, NULL, CRM_SYSTEM_MCP, CRM_SYSTEM_MCP, NULL);
 
             crm_debug(".");
             crm_ipc_send(old_instance, cmd, 0, 0, NULL);
             free_xml(cmd);
 
             sleep(2);
         }
         crm_ipc_close(old_instance);
         crm_ipc_destroy(old_instance);
         crm_exit(CRM_EX_OK);
 
     } else if (crm_ipc_connected(old_instance)) {
         crm_ipc_close(old_instance);
         crm_ipc_destroy(old_instance);
         crm_err("Aborting start-up because active Pacemaker instance found");
         crm_exit(CRM_EX_FATAL);
     }
 
     crm_ipc_close(old_instance);
     crm_ipc_destroy(old_instance);
 
+#ifdef SUPPORT_COROSYNC
     if (mcp_read_config() == FALSE) {
-        crm_notice("Could not obtain corosync config data, exiting");
         crm_exit(CRM_EX_UNAVAILABLE);
     }
+#endif
 
     // OCF shell functions and cluster-glue need facility under different name
     {
         const char *facility = pcmk__env_option("logfacility");
 
         if (facility && safe_str_neq(facility, "none")) {
             setenv("HA_LOGFACILITY", facility, 1);
         }
     }
 
     crm_notice("Starting Pacemaker %s "CRM_XS" build=%s features:%s",
                PACEMAKER_VERSION, BUILD_VERSION, CRM_FEATURES);
     mainloop = g_main_loop_new(NULL, FALSE);
 
-    rc = getrlimit(RLIMIT_CORE, &cores);
-    if (rc < 0) {
-        crm_perror(LOG_ERR, "Cannot determine current maximum core size.");
-    } else {
-        if (cores.rlim_max == 0 && geteuid() == 0) {
-            cores.rlim_max = RLIM_INFINITY;
-        } else {
-            crm_info("Maximum core file size is: %lu", (unsigned long)cores.rlim_max);
-        }
-        cores.rlim_cur = cores.rlim_max;
-
-        rc = setrlimit(RLIMIT_CORE, &cores);
-        if (rc < 0) {
-            crm_perror(LOG_ERR,
-                       "Core file generation will remain disabled."
-                       " Core files are an important diagnostic tool, so"
-                       " please consider enabling them by default.");
-        }
-    }
+    remove_core_file_limit();
 
     if (pcmk_daemon_user(&pcmk_uid, &pcmk_gid) < 0) {
         crm_err("Cluster user %s does not exist, aborting Pacemaker startup", CRM_DAEMON_USER);
         crm_exit(CRM_EX_NOUSER);
     }
 
     // Used by some resource agents
     if ((mkdir(CRM_STATE_DIR, 0750) < 0) && (errno != EEXIST)) {
         crm_warn("Could not create " CRM_STATE_DIR ": %s", pcmk_strerror(errno));
     } else {
         mcp_chown(CRM_STATE_DIR, pcmk_uid, pcmk_gid);
     }
 
     /* Used to store core/blackbox/scheduler/cib files in */
     crm_build_path(CRM_PACEMAKER_DIR, 0750);
     mcp_chown(CRM_PACEMAKER_DIR, pcmk_uid, pcmk_gid);
 
     /* Used to store core files in */
     crm_build_path(CRM_CORE_DIR, 0750);
     mcp_chown(CRM_CORE_DIR, pcmk_uid, pcmk_gid);
 
     /* Used to store blackbox dumps in */
     crm_build_path(CRM_BLACKBOX_DIR, 0750);
     mcp_chown(CRM_BLACKBOX_DIR, pcmk_uid, pcmk_gid);
 
     // Used to store scheduler inputs in
     crm_build_path(PE_STATE_DIR, 0750);
     mcp_chown(PE_STATE_DIR, pcmk_uid, pcmk_gid);
 
     /* Used to store the cluster configuration */
     crm_build_path(CRM_CONFIG_DIR, 0750);
     mcp_chown(CRM_CONFIG_DIR, pcmk_uid, pcmk_gid);
 
     // Don't build CRM_RSCTMP_DIR, pacemaker-execd will do it
 
     ipcs = mainloop_add_ipc_server(CRM_SYSTEM_MCP, QB_IPC_NATIVE, &mcp_ipc_callbacks);
     if (ipcs == NULL) {
         crm_err("Couldn't start IPC server");
         crm_exit(CRM_EX_OSERR);
     }
 
+#ifdef SUPPORT_COROSYNC
     /* Allows us to block shutdown */
-    if (cluster_connect_cfg(&local_nodeid) == FALSE) {
-        crm_err("Couldn't connect to Corosync's CFG service");
+    if (!cluster_connect_cfg()) {
         crm_exit(CRM_EX_PROTOCOL);
     }
+#endif
 
     if(pcmk_locate_sbd() > 0) {
         setenv("PCMK_watchdog", "true", 1);
     } else {
         setenv("PCMK_watchdog", "false", 1);
     }
 
     switch (find_and_track_existing_processes()) {
         case pcmk_rc_ok:
             break;
         case pcmk_rc_ipc_unauthorized:
             crm_exit(CRM_EX_CANTCREAT);
         default:
             crm_exit(CRM_EX_FATAL);
     };
 
-    cluster.destroy = mcp_cpg_destroy;
-    cluster.cpg.cpg_deliver_fn = mcp_cpg_deliver;
-    cluster.cpg.cpg_confchg_fn = mcp_cpg_membership;
-
-    crm_set_autoreap(FALSE);
-
-    rc = pcmk_ok;
-
-    if (cluster_connect_cpg(&cluster) == FALSE) {
-        crm_err("Couldn't connect to Corosync's CPG service");
-        rc = -ENOPROTOOPT;
-
-    } else if (cluster_connect_quorum(mcp_quorum_callback, mcp_quorum_destroy)
-               == FALSE) {
-        rc = -ENOTCONN;
+    mainloop_add_signal(SIGTERM, pcmk_shutdown);
+    mainloop_add_signal(SIGINT, pcmk_shutdown);
 
-    } else {
-        local_name = get_local_node_name();
-        update_node_processes(local_nodeid, local_name, get_process_list());
-
-        mainloop_add_signal(SIGTERM, pcmk_shutdown);
-        mainloop_add_signal(SIGINT, pcmk_shutdown);
+    init_children_processes();
 
-        init_children_processes();
-
-        crm_notice("Pacemaker daemon successfully started and accepting connections");
-        g_main_loop_run(mainloop);
-    }
+    crm_notice("Pacemaker daemon successfully started and accepting connections");
+    g_main_loop_run(mainloop);
 
     if (ipcs) {
         crm_trace("Closing IPC server");
         mainloop_del_ipc_server(ipcs);
         ipcs = NULL;
     }
 
     g_main_loop_unref(mainloop);
-
-    cluster_disconnect_cpg(&cluster);
+#ifdef SUPPORT_COROSYNC
     cluster_disconnect_cfg();
-
-    crm_exit(crm_errno2exit(rc));
+#endif
+    crm_exit(CRM_EX_OK);
 }
diff --git a/daemons/pacemakerd/pacemakerd.h b/daemons/pacemakerd/pacemakerd.h
index d66ab1064a..5f475fd492 100644
--- a/daemons/pacemakerd/pacemakerd.h
+++ b/daemons/pacemakerd/pacemakerd.h
@@ -1,28 +1,29 @@
 /*
  * Copyright 2010-2018 Andrew Beekhof <andrew@beekhof.net>
  *
  * 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 <sys/types.h>
 #include <sys/wait.h>
 #include <sys/resource.h>
 
 #include <stdint.h>
 
 #include <crm/crm.h>
 #include <crm/common/xml.h>
 
 #define SIZEOF(a)   (sizeof(a) / sizeof(a[0]))
 #define MAX_RESPAWN		100
 
 gboolean mcp_read_config(void);
 
-gboolean cluster_connect_cfg(uint32_t * nodeid);
+gboolean cluster_connect_cfg(void);
 gboolean cluster_disconnect_cfg(void);
+void pcmkd_shutdown_corosync(void);
 
 void pcmk_shutdown(int nsig);
diff --git a/daemons/pacemakerd/pcmkd_corosync.c b/daemons/pacemakerd/pcmkd_corosync.c
index ec74908856..82bd25702d 100644
--- a/daemons/pacemakerd/pcmkd_corosync.c
+++ b/daemons/pacemakerd/pcmkd_corosync.c
@@ -1,287 +1,305 @@
 /*
  * Copyright 2010-2020 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 "pacemakerd.h"
 
 #include <sys/utsname.h>
 #include <sys/stat.h>           /* for calls to stat() */
 #include <libgen.h>             /* For basename() and dirname() */
 
 #include <sys/types.h>
 #include <pwd.h>                /* For getpwname() */
 
 #include <corosync/hdb.h>
 #include <corosync/cfg.h>
 #include <corosync/cpg.h>
 #include <corosync/cmap.h>
 
 #include <crm/cluster/internal.h>
 #include <crm/common/ipc.h>     /* for crm_ipc_is_authentic_process */
 #include <crm/common/mainloop.h>
 
 #include <crm/common/ipc_internal.h>  /* PCMK__SPECIAL_PID* */
 
-enum cluster_type_e stack = pcmk_cluster_unknown;
-static corosync_cfg_handle_t cfg_handle;
+static corosync_cfg_handle_t cfg_handle = 0;
 
 /* =::=::=::= CFG - Shutdown stuff =::=::=::= */
 
 static void
 cfg_shutdown_callback(corosync_cfg_handle_t h, corosync_cfg_shutdown_flags_t flags)
 {
     crm_info("Corosync wants to shut down: %s",
              (flags == COROSYNC_CFG_SHUTDOWN_FLAG_IMMEDIATE) ? "immediate" :
              (flags == COROSYNC_CFG_SHUTDOWN_FLAG_REGARDLESS) ? "forced" : "optional");
 
     /* Never allow corosync to shut down while we're running */
     corosync_cfg_replyto_shutdown(h, COROSYNC_CFG_SHUTDOWN_FLAG_NO);
 }
 
 static corosync_cfg_callbacks_t cfg_callbacks = {
     .corosync_cfg_shutdown_callback = cfg_shutdown_callback,
 };
 
 static int
 pcmk_cfg_dispatch(gpointer user_data)
 {
     corosync_cfg_handle_t *handle = (corosync_cfg_handle_t *) user_data;
     cs_error_t rc = corosync_cfg_dispatch(*handle, CS_DISPATCH_ALL);
 
     if (rc != CS_OK) {
         return -1;
     }
     return 0;
 }
 
 static void
 cfg_connection_destroy(gpointer user_data)
 {
-    crm_err("Connection destroyed");
+    crm_err("Lost connection to Corosync");
     cfg_handle = 0;
-
     pcmk_shutdown(SIGTERM);
 }
 
 gboolean
 cluster_disconnect_cfg(void)
 {
     if (cfg_handle) {
         corosync_cfg_finalize(cfg_handle);
         cfg_handle = 0;
     }
 
     pcmk_shutdown(SIGTERM);
     return TRUE;
 }
 
 #define cs_repeat(counter, max, code) do {		\
 	code;						\
 	if(rc == CS_ERR_TRY_AGAIN || rc == CS_ERR_QUEUE_FULL) {  \
 	    counter++;					\
-	    crm_debug("Retrying operation after %ds", counter);	\
+	    crm_debug("Retrying Corosync operation after %ds", counter);    \
 	    sleep(counter);				\
 	} else {                                        \
             break;                                      \
 	}						\
     } while(counter < max)
 
 gboolean
-cluster_connect_cfg(uint32_t * nodeid)
+cluster_connect_cfg(void)
 {
     cs_error_t rc;
     int fd = -1, retries = 0, rv;
     uid_t found_uid = 0;
     gid_t found_gid = 0;
     pid_t found_pid = 0;
+    uint32_t nodeid;
 
     static struct mainloop_fd_callbacks cfg_fd_callbacks = {
         .dispatch = pcmk_cfg_dispatch,
         .destroy = cfg_connection_destroy,
     };
 
     cs_repeat(retries, 30, rc = corosync_cfg_initialize(&cfg_handle, &cfg_callbacks));
 
     if (rc != CS_OK) {
-        crm_err("corosync cfg init: %s (%d)", cs_strerror(rc), rc);
+        crm_crit("Could not connect to Corosync CFG: %s " CRM_XS " rc=%d",
+                 cs_strerror(rc), rc);
         return FALSE;
     }
 
     rc = corosync_cfg_fd_get(cfg_handle, &fd);
     if (rc != CS_OK) {
-        crm_err("corosync cfg fd_get: %s (%d)", cs_strerror(rc), rc);
+        crm_crit("Could not get Corosync CFG descriptor: %s " CRM_XS " rc=%d",
+                 cs_strerror(rc), rc);
         goto bail;
     }
 
     /* CFG 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("CFG 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);
+        crm_crit("Rejecting Corosync CFG provider because process %lld "
+                 "is running as uid %lld gid %lld, not root",
+                  (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 CFG provider: %s (%d)",
-                strerror(-rv), -rv);
+        crm_crit("Could not authenticate Corosync CFG provider: %s "
+                 CRM_XS " rc=%d", strerror(-rv), -rv);
         goto bail;
     }
 
     retries = 0;
-    cs_repeat(retries, 30, rc = corosync_cfg_local_get(cfg_handle, nodeid));
-
+    cs_repeat(retries, 30, rc = corosync_cfg_local_get(cfg_handle, &nodeid));
     if (rc != CS_OK) {
-        crm_err("corosync cfg local_get error %d", rc);
+        crm_crit("Could not get local node ID from Corosync: %s "
+                 CRM_XS " rc=%d", cs_strerror(rc), rc);
         goto bail;
     }
+    crm_debug("Corosync reports local node ID is %lu", (unsigned long) nodeid);
 
-    crm_debug("Our nodeid: %d", *nodeid);
     mainloop_add_fd("corosync-cfg", G_PRIORITY_DEFAULT, fd, &cfg_handle, &cfg_fd_callbacks);
-
     return TRUE;
 
   bail:
     corosync_cfg_finalize(cfg_handle);
     return FALSE;
 }
 
+void
+pcmkd_shutdown_corosync(void)
+{
+    cs_error_t rc;
+
+    if (cfg_handle == 0) {
+        crm_warn("Unable to shut down Corosync: No connection");
+        return;
+    }
+    crm_info("Asking Corosync to shut down");
+    rc = corosync_cfg_try_shutdown(cfg_handle,
+                                    COROSYNC_CFG_SHUTDOWN_FLAG_IMMEDIATE);
+    if (rc == CS_OK) {
+        corosync_cfg_finalize(cfg_handle);
+        cfg_handle = 0;
+    } else {
+        crm_warn("Corosync shutdown failed: %s " CRM_XS " rc=%d",
+                 cs_strerror(rc), rc);
+    }
+}
+
+
 /* =::=::=::= Configuration =::=::=::= */
 static int
 get_config_opt(uint64_t unused, cmap_handle_t object_handle, const char *key, char **value,
                const char *fallback)
 {
     int rc = 0, retries = 0;
 
     cs_repeat(retries, 5, rc = cmap_get_string(object_handle, key, value));
     if (rc != CS_OK) {
         crm_trace("Search for %s failed %d, defaulting to %s", key, rc, fallback);
         if (fallback) {
             *value = strdup(fallback);
         } else {
             *value = NULL;
         }
     }
     crm_trace("%s: %s", key, *value);
     return rc;
 }
 
 gboolean
 mcp_read_config(void)
 {
     cs_error_t rc = CS_OK;
     int retries = 0;
     cmap_handle_t local_handle;
     uint64_t config = 0;
     int fd = -1;
     uid_t found_uid = 0;
     gid_t found_gid = 0;
     pid_t found_pid = 0;
     int rv;
+    enum cluster_type_e stack;
 
     // There can be only one possibility
     do {
         rc = cmap_initialize(&local_handle);
         if (rc != CS_OK) {
             retries++;
-            printf("cmap connection setup failed: %s.  Retrying in %ds\n", cs_strerror(rc), retries);
-            crm_info("cmap connection setup failed: %s.  Retrying in %ds", cs_strerror(rc), retries);
+            crm_info("Could not connect to Corosync CMAP: %s (retrying in %ds) "
+                     CRM_XS " rc=%d", cs_strerror(rc), retries, rc);
             sleep(retries);
 
         } else {
             break;
         }
 
     } while (retries < 5);
 
     if (rc != CS_OK) {
-        printf("Could not connect to Cluster Configuration Database API, error %d\n", rc);
-        crm_warn("Could not connect to Cluster Configuration Database API, error %d", rc);
+        crm_crit("Could not connect to Corosync CMAP: %s "
+                 CRM_XS " rc=%d", cs_strerror(rc), rc);
         return FALSE;
     }
 
     rc = cmap_fd_get(local_handle, &fd);
     if (rc != CS_OK) {
-        crm_err("Could not obtain the CMAP API connection: %s (%d)",
-                cs_strerror(rc), rc);
+        crm_crit("Could not get Corosync CMAP descriptor: %s " CRM_XS " rc=%d",
+                 cs_strerror(rc), rc);
         cmap_finalize(local_handle);
         return FALSE;
     }
 
     /* CMAP 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("CMAP 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);
+        crm_crit("Rejecting Corosync CMAP provider because process %lld "
+                 "is running as uid %lld gid %lld, not root",
+                 (long long) PCMK__SPECIAL_PID_AS_0(found_pid),
+                 (long long) found_uid, (long long) found_gid);
         cmap_finalize(local_handle);
         return FALSE;
     } else if (rv < 0) {
-        crm_err("Could not verify authenticity of CMAP provider: %s (%d)",
-                strerror(-rv), -rv);
+        crm_crit("Could not authenticate Corosync CMAP provider: %s "
+                 CRM_XS " rc=%d", strerror(-rv), -rv);
         cmap_finalize(local_handle);
         return FALSE;
     }
 
     stack = get_cluster_type();
-    crm_info("Reading configure for stack: %s", name_for_cluster_type(stack));
-
-    /* =::=::= Should we be here =::=::= */
-    if (stack == pcmk_cluster_corosync) {
-        pcmk__set_env_option("cluster_type", "corosync");
-        pcmk__set_env_option("quorum_type", "corosync");
-
-    } else {
-        crm_err("Unsupported stack type: %s", name_for_cluster_type(stack));
+    if (stack != pcmk_cluster_corosync) {
+        crm_crit("Expected corosync stack but detected %s " CRM_XS " stack=%d",
+                 name_for_cluster_type(stack), stack);
         return FALSE;
     }
 
-    /* =::=::= Logging =::=::= */
-    if (pcmk__env_option("debug")) {
-        /* Syslog logging is already setup by crm_log_init() */
+    crm_info("Reading configuration for %s stack",
+             name_for_cluster_type(stack));
+    pcmk__set_env_option("cluster_type", "corosync");
+    pcmk__set_env_option("quorum_type", "corosync");
 
-    } else {
-        /* Check corosync */
+    // If debug logging is not configured, check whether corosync has it
+    if (pcmk__env_option("debug") == NULL) {
         char *debug_enabled = NULL;
 
         get_config_opt(config, local_handle, "logging.debug", &debug_enabled, "off");
 
         if (crm_is_true(debug_enabled)) {
             pcmk__set_env_option("debug", "1");
             if (get_crm_log_level() < LOG_DEBUG) {
                 set_crm_log_level(LOG_DEBUG);
             }
 
         } else {
             pcmk__set_env_option("debug", "0");
         }
 
         free(debug_enabled);
     }
 
     if(local_handle){
         gid_t gid = 0;
         if (pcmk_daemon_user(NULL, &gid) < 0) {
-            crm_warn("Could not authorize group with corosync " CRM_XS
+            crm_warn("Could not authorize group with Corosync " CRM_XS
                      " No group found for user %s", CRM_DAEMON_USER);
 
         } else {
             char key[PATH_MAX];
             snprintf(key, PATH_MAX, "uidgid.gid.%u", gid);
             rc = cmap_set_uint8(local_handle, key, 1);
             if (rc != CS_OK) {
-                crm_warn("Could not authorize group with corosync "CRM_XS
-                         " group=%u rc=%d (%s)", gid, rc, ais_error2text(rc));
+                crm_warn("Could not authorize group with Corosync: %s " CRM_XS
+                         " group=%u rc=%d", ais_error2text(rc), gid, rc);
             }
         }
     }
     cmap_finalize(local_handle);
 
     return TRUE;
 }
diff --git a/include/crm/common/ipc_controld.h b/include/crm/common/ipc_controld.h
index 0ebabfcfb0..b817357a75 100644
--- a/include/crm/common/ipc_controld.h
+++ b/include/crm/common/ipc_controld.h
@@ -1,99 +1,112 @@
 /*
  * Copyright 2020 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__IPC_CONTROLD__H
 #  define PCMK__IPC_CONTROLD__H
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 /**
  * \file
  * \brief IPC commands for Pacemaker controller
  *
  * \ingroup core
  */
 
 #include <stdbool.h>                    // bool
+#include <glib.h>                       // GList
 #include <libxml/tree.h>                // xmlNode
 #include <crm/common/ipc.h>             // pcmk_ipc_api_t
 
 //! Possible types of controller replies
 enum pcmk_controld_api_reply {
     pcmk_controld_reply_unknown,
     pcmk_controld_reply_reprobe,
     pcmk_controld_reply_info,
     pcmk_controld_reply_resource,
     pcmk_controld_reply_ping,
+    pcmk_controld_reply_nodes,
 };
 
+// Node information passed with pcmk_controld_reply_nodes
+typedef struct {
+    uint32_t id;
+    const char *uname;
+    const char *state;
+} pcmk_controld_api_node_t;
+
 /*!
  * Controller reply passed to event callback
  *
  * \note Shutdown and election calls have no reply. Reprobe calls are
  *       acknowledged but contain no data (reply_type will be the only item
  *       set). Node info and ping calls have their own reply data. Fail and
  *       refresh calls use the resource reply type and reply data.
  * \note The pointers in the reply are only guaranteed to be meaningful for the
  *       execution of the callback; if the values are needed for later, the
  *       callback should copy them.
  */
 typedef struct {
     enum pcmk_controld_api_reply reply_type;
     const char *feature_set; //!< CRM feature set advertised by controller
     const char *host_from;   //!< Name of node that sent reply
 
     union {
         // pcmk_controld_reply_info
         struct {
             bool have_quorum;
             bool is_remote;
             int id;
             const char *uuid;
             const char *uname;
             const char *state;
         } node_info;
 
         // pcmk_controld_reply_resource
         struct {
             xmlNode *node_state;    //<! Resource operation history XML
         } resource;
 
         // pcmk_controld_reply_ping
         struct {
             const char *sys_from;
             const char *fsa_state;
             const char *result;
         } ping;
+
+        // pcmk_controld_reply_nodes
+        GList *nodes; // list of pcmk_controld_api_node_t *
     } data;
 } pcmk_controld_api_reply_t;
 
 int pcmk_controld_api_reprobe(pcmk_ipc_api_t *api, const char *target_node,
                               const char *router_node);
 int pcmk_controld_api_node_info(pcmk_ipc_api_t *api, uint32_t nodeid);
 int pcmk_controld_api_fail(pcmk_ipc_api_t *api, const char *target_node,
                            const char *router_node, const char *rsc_id,
                            const char *rsc_long_id, const char *standard,
                            const char *provider, const char *type);
 int pcmk_controld_api_refresh(pcmk_ipc_api_t *api, const char *target_node,
                               const char *router_node, const char *rsc_id,
                               const char *rsc_long_id, const char *standard,
                               const char *provider, const char *type,
                               bool cib_only);
 int pcmk_controld_api_ping(pcmk_ipc_api_t *api, const char *node_name);
+int pcmk_controld_api_list_nodes(pcmk_ipc_api_t *api);
 int pcmk_controld_api_shutdown(pcmk_ipc_api_t *api, const char *node_name);
 int pcmk_controld_api_start_election(pcmk_ipc_api_t *api);
 unsigned int pcmk_controld_api_replies_expected(pcmk_ipc_api_t *api);
 
 #ifdef __cplusplus
 }
 #endif
 
 #endif // PCMK__IPC_CONTROLD__H
diff --git a/include/crm/common/util.h b/include/crm/common/util.h
index 67d74d22db..bb97b0a35d 100644
--- a/include/crm/common/util.h
+++ b/include/crm/common/util.h
@@ -1,228 +1,229 @@
 /*
  * Copyright 2004-2020 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 CRM_COMMON_UTIL__H
 #  define CRM_COMMON_UTIL__H
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 /**
  * \file
  * \brief Utility functions
  * \ingroup core
  */
 
 #  include <sys/types.h>    // gid_t, mode_t, size_t, time_t, uid_t
 #  include <stdlib.h>
 #  include <stdbool.h>
 #  include <stdint.h>       // uint32_t
 #  include <limits.h>
 #  include <signal.h>
 #  include <glib.h>
 
 #  include <libxml/tree.h>
 
 #  include <crm/lrmd.h>
 #  include <crm/common/acl.h>
 #  include <crm/common/results.h>
 
 #  define ONLINESTATUS  "online"  // Status of an online client
 #  define OFFLINESTATUS "offline" // Status of an offline client
 
 // public name/value pair functions (from nvpair.c)
 int pcmk_scan_nvpair(const char *input, char **name, char **value);
 char *pcmk_format_nvpair(const char *name, const char *value, const char *units);
 char *pcmk_format_named_time(const char *name, time_t epoch_time);
 
 /* public Pacemaker Remote functions (from remote.c) */
 int crm_default_remote_port(void);
 
 /* public string functions (from strings.c) */
 char *crm_itoa_stack(int an_int, char *buf, size_t len);
 gboolean crm_is_true(const char *s);
 int crm_str_to_boolean(const char *s, int *ret);
 long long crm_parse_ll(const char *text, const char *default_text);
 int crm_parse_int(const char *text, const char *default_text);
 long long crm_get_msec(const char *input);
 char * crm_strip_trailing_newline(char *str);
 gboolean crm_str_eq(const char *a, const char *b, gboolean use_case);
 gboolean safe_str_neq(const char *a, const char *b);
 gboolean crm_strcase_equal(gconstpointer a, gconstpointer b);
 guint crm_strcase_hash(gconstpointer v);
 guint g_str_hash_traditional(gconstpointer v);
 char *crm_strdup_printf(char const *format, ...) __attribute__ ((__format__ (__printf__, 1, 2)));
+int pcmk_numeric_strcasecmp(const char *s1, const char *s2);
 
 #  define safe_str_eq(a, b) crm_str_eq(a, b, FALSE)
 #  define crm_str_hash g_str_hash_traditional
 
 static inline char *
 crm_itoa(int an_int)
 {
     return crm_strdup_printf("%d", an_int);
 }
 
 static inline char *
 crm_ftoa(double a_float)
 {
     return crm_strdup_printf("%f", a_float);
 }
 
 static inline char *
 crm_ttoa(time_t epoch_time)
 {
     return crm_strdup_printf("%lld", (long long) epoch_time);
 }
 
 /*!
  * \brief Create hash table with dynamically allocated string keys/values
  *
  * \return Newly allocated hash table
  * \note It is the caller's responsibility to free the result, using
  *       g_hash_table_destroy().
  */
 static inline GHashTable *
 crm_str_table_new(void)
 {
     return g_hash_table_new_full(crm_str_hash, g_str_equal, free, free);
 }
 
 /*!
  * \brief Create hash table with case-insensitive dynamically allocated string keys/values
  *
  * \return Newly allocated hash table
  * \note It is the caller's responsibility to free the result, using
  *       g_hash_table_destroy().
  */
 static inline GHashTable *
 crm_strcase_table_new(void)
 {
     return g_hash_table_new_full(crm_strcase_hash, crm_strcase_equal, free, free);
 }
 
 GHashTable *crm_str_table_dup(GHashTable *old_table);
 
 #  define crm_atoi(text, default_text) crm_parse_int(text, default_text)
 
 /* public I/O functions (from io.c) */
 void crm_build_path(const char *path_c, mode_t mode);
 
 guint crm_parse_interval_spec(const char *input);
 int char2score(const char *score);
 char *score2char(int score);
 char *score2char_stack(int score, char *buf, size_t len);
 
 /* public operation functions (from operations.c) */
 gboolean parse_op_key(const char *key, char **rsc_id, char **op_type,
                       guint *interval_ms);
 gboolean decode_transition_key(const char *key, char **uuid, int *transition_id,
                                int *action_id, int *target_rc);
 gboolean decode_transition_magic(const char *magic, char **uuid,
                                  int *transition_id, int *action_id,
                                  int *op_status, int *op_rc, int *target_rc);
 int rsc_op_expected_rc(lrmd_event_data_t *event);
 gboolean did_rsc_op_fail(lrmd_event_data_t *event, int target_rc);
 bool crm_op_needs_metadata(const char *rsc_class, const char *op);
 xmlNode *crm_create_op_xml(xmlNode *parent, const char *prefix,
                            const char *task, const char *interval_spec,
                            const char *timeout);
 #define CRM_DEFAULT_OP_TIMEOUT_S "20s"
 
 // Public resource agent functions (from agents.c)
 
 // Capabilities supported by a resource agent standard
 enum pcmk_ra_caps {
     pcmk_ra_cap_none         = 0,
     pcmk_ra_cap_provider     = (1 << 0), // Requires provider
     pcmk_ra_cap_status       = (1 << 1), // Supports status instead of monitor
     pcmk_ra_cap_params       = (1 << 2), // Supports parameters
     pcmk_ra_cap_unique       = (1 << 3), // Supports unique clones
     pcmk_ra_cap_promotable   = (1 << 4), // Supports promotable clones
     pcmk_ra_cap_stdin        = (1 << 5), // Reads from standard input
 };
 
 uint32_t pcmk_get_ra_caps(const char *standard);
 char *crm_generate_ra_key(const char *standard, const char *provider,
                           const char *type);
 int crm_parse_agent_spec(const char *spec, char **standard, char **provider,
                          char **type);
 
 
 int compare_version(const char *version1, const char *version2);
 
 /* coverity[+kill] */
 void crm_abort(const char *file, const char *function, int line,
                const char *condition, gboolean do_core, gboolean do_fork);
 
 static inline gboolean
 is_not_set(long long word, long long bit)
 {
     return ((word & bit) == 0);
 }
 
 static inline gboolean
 is_set(long long word, long long bit)
 {
     return ((word & bit) == bit);
 }
 
 static inline gboolean
 is_set_any(long long word, long long bit)
 {
     return ((word & bit) != 0);
 }
 
 static inline guint
 crm_hash_table_size(GHashTable * hashtable)
 {
     if (hashtable == NULL) {
         return 0;
     }
     return g_hash_table_size(hashtable);
 }
 
 char *crm_meta_name(const char *field);
 const char *crm_meta_value(GHashTable * hash, const char *field);
 
 char *crm_md5sum(const char *buffer);
 
 char *crm_generate_uuid(void);
 bool crm_is_daemon_name(const char *name);
 
 int crm_user_lookup(const char *name, uid_t * uid, gid_t * gid);
 int pcmk_daemon_user(uid_t *uid, gid_t *gid);
 
 #ifdef HAVE_GNUTLS_GNUTLS_H
 void crm_gnutls_global_init(void);
 #endif
 
 char *pcmk_hostname(void);
 
 bool pcmk_str_is_infinity(const char *s);
 bool pcmk_str_is_minus_infinity(const char *s);
 
 #ifndef PCMK__NO_COMPAT
 /* Everything here is deprecated and kept only for public API backward
  * compatibility. It will be moved to compatibility.h in a future release.
  */
 
 //! \deprecated Use crm_parse_interval_spec() instead
 #define crm_get_interval crm_parse_interval_spec
 
 //! \deprecated Use pcmk_get_ra_caps() instead
 bool crm_provider_required(const char *standard);
 
 #endif // PCMK__NO_COMPAT
 
 #ifdef __cplusplus
 }
 #endif
 
 #endif
diff --git a/include/crm_internal.h b/include/crm_internal.h
index fd56fc66ef..cf8999fff4 100644
--- a/include/crm_internal.h
+++ b/include/crm_internal.h
@@ -1,178 +1,179 @@
 /*
  * Copyright 2006-2020 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 CRM_INTERNAL__H
 #  define CRM_INTERNAL__H
 
 #  include <config.h>
 #  include <portability.h>
 
 #  include <glib.h>
 #  include <stdbool.h>
 #  include <libxml/tree.h>
 
 /* Public API headers can guard deprecated code with this symbol, thus
  * preventing internal code (which includes this header) from using it, while
  * still allowing external code (which can't include this header) to use it,
  * for backward compatibility.
  */
 #define PCMK__NO_COMPAT
 
 #  include <crm/lrmd.h>
 #  include <crm/common/logging.h>
 #  include <crm/common/ipc_internal.h>
 #  include <crm/common/options_internal.h>
 #  include <crm/common/internal.h>
 
 /* Assorted convenience functions */
 void crm_make_daemon(const char *name, gboolean daemonize, const char *pidfile);
 
 static inline long long
 crm_clear_bit(const char *function, int line, const char *target, long long word, long long bit)
 {
     long long rc = (word & ~bit);
 
     if (rc == word) {
         /* Unchanged */
     } else if (target) {
         crm_trace("Bit 0x%.8llx for %s cleared by %s:%d", bit, target, function, line);
     } else {
         crm_trace("Bit 0x%.8llx cleared by %s:%d", bit, function, line);
     }
 
     return rc;
 }
 
 static inline long long
 crm_set_bit(const char *function, int line, const char *target, long long word, long long bit)
 {
     long long rc = (word | bit);
 
     if (rc == word) {
         /* Unchanged */
     } else if (target) {
         crm_trace("Bit 0x%.8llx for %s set by %s:%d", bit, target, function, line);
     } else {
         crm_trace("Bit 0x%.8llx set by %s:%d", bit, function, line);
     }
 
     return rc;
 }
 
 #  define set_bit(word, bit) word = crm_set_bit(__FUNCTION__, __LINE__, NULL, word, bit)
 #  define clear_bit(word, bit) word = crm_clear_bit(__FUNCTION__, __LINE__, NULL, word, bit)
 
 void strip_text_nodes(xmlNode * xml);
 void pcmk_panic(const char *origin);
 pid_t pcmk_locate_sbd(void);
 
 
 /*
  * XML attribute names used only by internal code
  */
 
 #define PCMK__XA_ATTR_DAMPENING         "attr_dampening"
 #define PCMK__XA_ATTR_FORCE             "attrd_is_force_write"
 #define PCMK__XA_ATTR_INTERVAL          "attr_clear_interval"
 #define PCMK__XA_ATTR_IS_PRIVATE        "attr_is_private"
 #define PCMK__XA_ATTR_IS_REMOTE         "attr_is_remote"
 #define PCMK__XA_ATTR_NAME              "attr_name"
 #define PCMK__XA_ATTR_NODE_ID           "attr_host_id"
 #define PCMK__XA_ATTR_NODE_NAME         "attr_host"
 #define PCMK__XA_ATTR_OPERATION         "attr_clear_operation"
 #define PCMK__XA_ATTR_PATTERN           "attr_regex"
 #define PCMK__XA_ATTR_RESOURCE          "attr_resource"
 #define PCMK__XA_ATTR_SECTION           "attr_section"
 #define PCMK__XA_ATTR_SET               "attr_set"
 #define PCMK__XA_ATTR_USER              "attr_user"
 #define PCMK__XA_ATTR_UUID              "attr_key"
 #define PCMK__XA_ATTR_VALUE             "attr_value"
 #define PCMK__XA_ATTR_VERSION           "attr_version"
 #define PCMK__XA_ATTR_WRITER            "attr_writer"
 #define PCMK__XA_MODE                   "mode"
 #define PCMK__XA_TASK                   "task"
 
 
 /*
  * IPC service names that are only used internally
  */
 
 #  define PCMK__SERVER_BASED_RO		"cib_ro"
 #  define PCMK__SERVER_BASED_RW		"cib_rw"
 #  define PCMK__SERVER_BASED_SHM		"cib_shm"
 
 /*
  * IPC commands that can be sent to Pacemaker daemons
  */
 
 #define PCMK__ATTRD_CMD_PEER_REMOVE     "peer-remove"
 #define PCMK__ATTRD_CMD_UPDATE          "update"
 #define PCMK__ATTRD_CMD_UPDATE_BOTH     "update-both"
 #define PCMK__ATTRD_CMD_UPDATE_DELAY    "update-delay"
 #define PCMK__ATTRD_CMD_QUERY           "query"
 #define PCMK__ATTRD_CMD_REFRESH         "refresh"
 #define PCMK__ATTRD_CMD_FLUSH           "flush"
 #define PCMK__ATTRD_CMD_SYNC            "sync"
 #define PCMK__ATTRD_CMD_SYNC_RESPONSE   "sync-response"
 #define PCMK__ATTRD_CMD_CLEAR_FAILURE   "clear-failure"
 
+#define PCMK__CONTROLD_CMD_NODES        "list-nodes"
 
 /*
  * Environment variables used by Pacemaker
  */
 
 #define PCMK__ENV_PHYSICAL_HOST         "physical_host"
 
 
 #  if SUPPORT_COROSYNC
 #    include <qb/qbipc_common.h>
 #    include <corosync/corotypes.h>
 typedef struct qb_ipc_request_header cs_ipc_header_request_t;
 typedef struct qb_ipc_response_header cs_ipc_header_response_t;
 #  else
 typedef struct {
     int size __attribute__ ((aligned(8)));
     int id __attribute__ ((aligned(8)));
 } __attribute__ ((aligned(8))) cs_ipc_header_request_t;
 
 typedef struct {
     int size __attribute__ ((aligned(8)));
     int id __attribute__ ((aligned(8)));
     int error __attribute__ ((aligned(8)));
 } __attribute__ ((aligned(8))) cs_ipc_header_response_t;
 
 #  endif
 
 static inline void *
 realloc_safe(void *ptr, size_t size)
 {
     void *new_ptr;
 
     // realloc(p, 0) can replace free(p) but this wrapper can't
     CRM_ASSERT(size > 0);
 
     new_ptr = realloc(ptr, size);
     if (new_ptr == NULL) {
         free(ptr);
         abort();
     }
     return new_ptr;
 }
 
 const char *crm_xml_add_last_written(xmlNode *xml_node);
 void crm_xml_dump(xmlNode * data, int options, char **buffer, int *offset, int *max, int depth);
 void crm_buffer_add_char(char **buffer, int *offset, int *max, char c);
 
 #if defined(PCMK__WITH_ATTRIBUTE_OUTPUT_ARGS)
 #  define PCMK__OUTPUT_ARGS(ARGS...) __attribute__((output_args(ARGS)))
 #else
 #  define PCMK__OUTPUT_ARGS(ARGS...)
 #endif
 
 #endif                          /* CRM_INTERNAL__H */
diff --git a/lib/common/ipc_controld.c b/lib/common/ipc_controld.c
index 22bb7338b6..5917cc5dc0 100644
--- a/lib/common/ipc_controld.c
+++ b/lib/common/ipc_controld.c
@@ -1,609 +1,668 @@
 /*
  * Copyright 2020 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 <stdbool.h>
 #include <errno.h>
 #include <libxml/tree.h>
 
 #include <crm/crm.h>
 #include <crm/msg_xml.h>
 #include <crm/common/xml.h>
 #include <crm/common/ipc.h>
 #include <crm/common/ipc_internal.h>
 #include <crm/common/ipc_controld.h>
 #include "crmcommon_private.h"
 
 struct controld_api_private_s {
     char *client_uuid;
     unsigned int replies_expected;
 };
 
 // \return Standard Pacemaker return code
 static int
 new_data(pcmk_ipc_api_t *api)
 {
     struct controld_api_private_s *private = NULL;
 
     api->api_data = calloc(1, sizeof(struct controld_api_private_s));
 
     if (api->api_data == NULL) {
         return errno;
     }
 
     private = api->api_data;
 
     /* This is set to the PID because that's how it was always done, but PIDs
      * are not unique because clients can be remote. The value appears to be
      * unused other than as part of F_CRM_SYS_FROM in IPC requests, which is
      * only compared against the internal system names (CRM_SYSTEM_TENGINE,
      * etc.), so it shouldn't be a problem.
      */
     private->client_uuid = pcmk__getpid_s();
 
     /* @TODO Implement a call ID model similar to the CIB, executor, and fencer
      *       IPC APIs, so that requests and replies can be matched, and
      *       duplicate replies can be discarded.
      */
     return pcmk_rc_ok;
 }
 
 static void
 free_data(void *data)
 {
     free(((struct controld_api_private_s *) data)->client_uuid);
     free(data);
 }
 
 // \return Standard Pacemaker return code
 static int
 post_connect(pcmk_ipc_api_t *api)
 {
     /* The controller currently requires clients to register via a hello
      * request, but does not reply back.
      */
     struct controld_api_private_s *private = api->api_data;
     const char *client_name = crm_system_name? crm_system_name : "client";
     xmlNode *hello;
     int rc;
 
     hello = create_hello_message(private->client_uuid, client_name,
                                  PCMK__CONTROLD_API_MAJOR,
                                  PCMK__CONTROLD_API_MINOR);
     rc = pcmk__send_ipc_request(api, hello);
     free_xml(hello);
     if (rc != pcmk_rc_ok) {
         crm_info("Could not send IPC hello to %s: %s " CRM_XS " rc=%s",
                  pcmk_ipc_name(api, true), pcmk_rc_str(rc), rc);
     } else {
         crm_debug("Sent IPC hello to %s", pcmk_ipc_name(api, true));
     }
     return rc;
 }
 
 #define xml_true(xml, field) crm_is_true(crm_element_value(xml, field))
 
 static void
 set_node_info_data(pcmk_controld_api_reply_t *data, xmlNode *msg_data)
 {
     data->reply_type = pcmk_controld_reply_info;
     if (msg_data == NULL) {
         return;
     }
     data->data.node_info.have_quorum = xml_true(msg_data, XML_ATTR_HAVE_QUORUM);
     data->data.node_info.is_remote = xml_true(msg_data, XML_NODE_IS_REMOTE);
     crm_element_value_int(msg_data, XML_ATTR_ID, &(data->data.node_info.id));
     data->data.node_info.uuid = crm_element_value(msg_data, XML_ATTR_UUID);
     data->data.node_info.uname = crm_element_value(msg_data, XML_ATTR_UNAME);
     data->data.node_info.state = crm_element_value(msg_data, XML_NODE_IS_PEER);
 }
 
 static void
 set_ping_data(pcmk_controld_api_reply_t *data, xmlNode *msg_data)
 {
     data->reply_type = pcmk_controld_reply_ping;
     if (msg_data == NULL) {
         return;
     }
     data->data.ping.sys_from = crm_element_value(msg_data,
                                                  XML_PING_ATTR_SYSFROM);
     data->data.ping.fsa_state = crm_element_value(msg_data,
                                                   XML_PING_ATTR_CRMDSTATE);
     data->data.ping.result = crm_element_value(msg_data, XML_PING_ATTR_STATUS);
 }
 
+static void
+set_nodes_data(pcmk_controld_api_reply_t *data, xmlNode *msg_data)
+{
+    pcmk_controld_api_node_t *node_info;
+
+    data->reply_type = pcmk_controld_reply_nodes;
+    for (xmlNode *node = first_named_child(msg_data, XML_CIB_TAG_NODE);
+         node != NULL; node = crm_next_same_xml(node)) {
+
+        long long id_ll = 0;
+
+        node_info = calloc(1, sizeof(pcmk_controld_api_node_t));
+        crm_element_value_ll(node, XML_ATTR_ID, &id_ll);
+        if (id_ll > 0) {
+            node_info->id = id_ll;
+        }
+        node_info->uname = crm_element_value(node, XML_ATTR_UNAME);
+        node_info->state = crm_element_value(node, XML_NODE_IN_CLUSTER);
+        data->data.nodes = g_list_prepend(data->data.nodes, node_info);
+    }
+}
+
 static bool
 reply_expected(pcmk_ipc_api_t *api, xmlNode *request)
 {
     const char *command = crm_element_value(request, F_CRM_TASK);
 
     if (command == NULL) {
         return false;
     }
 
     // We only need to handle commands that functions in this file can send
     return !strcmp(command, CRM_OP_REPROBE)
            || !strcmp(command, CRM_OP_NODE_INFO)
            || !strcmp(command, CRM_OP_PING)
            || !strcmp(command, CRM_OP_LRM_FAIL)
            || !strcmp(command, CRM_OP_LRM_DELETE);
 }
 
 static void
 dispatch(pcmk_ipc_api_t *api, xmlNode *reply)
 {
     struct controld_api_private_s *private = api->api_data;
     crm_exit_t status = CRM_EX_OK;
     xmlNode *msg_data = NULL;
     const char *value = NULL;
     pcmk_controld_api_reply_t reply_data = {
         pcmk_controld_reply_unknown, NULL, NULL,
     };
 
     if (private->replies_expected > 0) {
         private->replies_expected--;
     }
 
     // Do some basic validation of the reply
 
     /* @TODO We should be able to verify that value is always a response, but
      *       currently the controller doesn't always properly set the type. Even
      *       if we fix the controller, we'll still need to handle replies from
      *       old versions (feature set could be used to differentiate).
      */
     value = crm_element_value(reply, F_CRM_MSG_TYPE);
     if ((value == NULL) || (strcmp(value, XML_ATTR_REQUEST)
                             && strcmp(value, XML_ATTR_RESPONSE))) {
         crm_debug("Unrecognizable controller message: invalid message type '%s'",
                   crm_str(value));
         status = CRM_EX_PROTOCOL;
         reply = NULL;
     }
 
     if (crm_element_value(reply, XML_ATTR_REFERENCE) == NULL) {
         crm_debug("Unrecognizable controller message: no reference");
         status = CRM_EX_PROTOCOL;
         reply = NULL;
     }
 
     value = crm_element_value(reply, F_CRM_TASK);
     if (value == NULL) {
         crm_debug("Unrecognizable controller message: no command name");
         status = CRM_EX_PROTOCOL;
         reply = NULL;
     }
 
     // Parse useful info from reply
 
     if (reply != NULL) {
         reply_data.feature_set = crm_element_value(reply, XML_ATTR_VERSION);
         reply_data.host_from = crm_element_value(reply, F_CRM_HOST_FROM);
         msg_data = get_message_xml(reply, F_CRM_DATA);
 
         if (!strcmp(value, CRM_OP_REPROBE)) {
             reply_data.reply_type = pcmk_controld_reply_reprobe;
 
         } else if (!strcmp(value, CRM_OP_NODE_INFO)) {
             set_node_info_data(&reply_data, msg_data);
 
         } else if (!strcmp(value, CRM_OP_INVOKE_LRM)) {
             reply_data.reply_type = pcmk_controld_reply_resource;
             reply_data.data.resource.node_state = msg_data;
 
         } else if (!strcmp(value, CRM_OP_PING)) {
             set_ping_data(&reply_data, msg_data);
 
+        } else if (!strcmp(value, PCMK__CONTROLD_CMD_NODES)) {
+            set_nodes_data(&reply_data, msg_data);
+
         } else {
             crm_debug("Unrecognizable controller message: unknown command '%s'",
                       value);
             status = CRM_EX_PROTOCOL;
             reply = NULL;
         }
     }
 
     pcmk__call_ipc_callback(api, pcmk_ipc_event_reply, status, &reply_data);
+
+    // Free any reply data that was allocated
+    if (safe_str_eq(value, PCMK__CONTROLD_CMD_NODES)) {
+        g_list_free_full(reply_data.data.nodes, free);
+    }
 }
 
 pcmk__ipc_methods_t *
 pcmk__controld_api_methods()
 {
     pcmk__ipc_methods_t *cmds = calloc(1, sizeof(pcmk__ipc_methods_t));
 
     if (cmds != NULL) {
         cmds->new_data = new_data;
         cmds->free_data = free_data;
         cmds->post_connect = post_connect;
         cmds->reply_expected = reply_expected;
         cmds->dispatch = dispatch;
     }
     return cmds;
 }
 
 /*!
  * \internal
  * \brief Create XML for a controller IPC request
  *
  * \param[in] api       Controller connection
  * \param[in] op        Controller IPC command name
  * \param[in] node      Node name to set as destination host
  * \param[in] msg_data  XML to attach to request as message data
  *
  * \return Newly allocated XML for request
  */
 static xmlNode *
 create_controller_request(pcmk_ipc_api_t *api, const char *op,
                           const char *node, xmlNode *msg_data)
 {
     struct controld_api_private_s *private = api->api_data;
     const char *sys_to = NULL;
 
     if ((node == NULL) && !strcmp(op, CRM_OP_PING)) {
         sys_to = CRM_SYSTEM_DC;
     } else {
         sys_to = CRM_SYSTEM_CRMD;
     }
     return create_request(op, msg_data, node, sys_to,
                           (crm_system_name? crm_system_name : "client"),
                           private->client_uuid);
 }
 
 // \return Standard Pacemaker return code
 static int
 send_controller_request(pcmk_ipc_api_t *api, xmlNode *request,
                         bool reply_is_expected)
 {
     int rc;
 
     if (crm_element_value(request, XML_ATTR_REFERENCE) == NULL) {
         return EINVAL;
     }
     rc = pcmk__send_ipc_request(api, request);
     if ((rc == pcmk_rc_ok) && reply_is_expected) {
         struct controld_api_private_s *private = api->api_data;
 
         private->replies_expected++;
     }
     return rc;
 }
 
 static xmlNode *
 create_reprobe_message_data(const char *target_node, const char *router_node)
 {
     xmlNode *msg_data;
 
     msg_data = create_xml_node(NULL, "data_for_" CRM_OP_REPROBE);
     crm_xml_add(msg_data, XML_LRM_ATTR_TARGET, target_node);
     if ((router_node != NULL) && safe_str_neq(router_node, target_node)) {
         crm_xml_add(msg_data, XML_LRM_ATTR_ROUTER_NODE, router_node);
     }
     return msg_data;
 }
 
 /*!
  * \brief Send a reprobe controller operation
  *
  * \param[in] api          Controller connection
  * \param[in] target_node  Name of node to reprobe
  * \param[in] router_node  Router node for host
  *
  * \return Standard Pacemaker return code
  * \note Event callback will get a reply of type pcmk_controld_reply_reprobe.
  */
 int
 pcmk_controld_api_reprobe(pcmk_ipc_api_t *api, const char *target_node,
                           const char *router_node)
 {
     xmlNode *request;
     xmlNode *msg_data;
     int rc = pcmk_rc_ok;
 
     if (api == NULL) {
         return EINVAL;
     }
     if (router_node == NULL) {
         router_node = target_node;
     }
     crm_debug("Sending %s IPC request to reprobe %s via %s",
               pcmk_ipc_name(api, true), crm_str(target_node),
               crm_str(router_node));
     msg_data = create_reprobe_message_data(target_node, router_node);
     request = create_controller_request(api, CRM_OP_REPROBE, router_node,
                                         msg_data);
     rc = send_controller_request(api, request, true);
     free_xml(msg_data);
     free_xml(request);
     return rc;
 }
 
 /*!
  * \brief Send a "node info" controller operation
  *
  * \param[in] api          Controller connection
  * \param[in] nodeid       ID of node to get info for (or 0 for local node)
  *
  * \return Standard Pacemaker return code
  * \note Event callback will get a reply of type pcmk_controld_reply_info.
  */
 int
 pcmk_controld_api_node_info(pcmk_ipc_api_t *api, uint32_t nodeid)
 {
     xmlNode *request;
     int rc = pcmk_rc_ok;
 
     request = create_controller_request(api, CRM_OP_NODE_INFO, NULL, NULL);
     if (request == NULL) {
         return EINVAL;
     }
     if (nodeid > 0) {
         crm_xml_set_id(request, "%lu", (unsigned long) nodeid);
     }
 
     rc = send_controller_request(api, request, true);
     free_xml(request);
     return rc;
 }
 
 /*!
  * \brief Ask the controller for status
  *
  * \param[in] api          Controller connection
  * \param[in] node_name    Name of node whose status is desired (or NULL for DC)
  *
  * \return Standard Pacemaker return code
  * \note Event callback will get a reply of type pcmk_controld_reply_ping.
  */
 int
 pcmk_controld_api_ping(pcmk_ipc_api_t *api, const char *node_name)
 {
     xmlNode *request;
     int rc = pcmk_rc_ok;
 
     request = create_controller_request(api, CRM_OP_PING, node_name, NULL);
     if (request == NULL) {
         return EINVAL;
     }
     rc = send_controller_request(api, request, true);
     free_xml(request);
     return rc;
 }
 
+/*!
+ * \brief Ask the controller for cluster information
+ *
+ * \param[in] api          Controller connection
+ *
+ * \return Standard Pacemaker return code
+ * \note Event callback will get a reply of type pcmk_controld_reply_nodes.
+ */
+int
+pcmk_controld_api_list_nodes(pcmk_ipc_api_t *api)
+{
+    xmlNode *request;
+    int rc = EINVAL;
+
+    request = create_controller_request(api, PCMK__CONTROLD_CMD_NODES, NULL,
+                                        NULL);
+    if (request != NULL) {
+        rc = send_controller_request(api, request, true);
+        free_xml(request);
+    }
+    return rc;
+}
+
 /*!
  * \internal
  * \brief Ask the controller to shut down
  *
  * \param[in] api          Controller connection
  * \param[in] node_name    Name of node whose controller should shut down
  *
  * \return Standard Pacemaker return code
  *
  * \note This capability currently does not work, so the function is considered
  *       internal. It will likely be removed.
  * \note Event callback will not get a reply.
  */
 int
 pcmk_controld_api_shutdown(pcmk_ipc_api_t *api, const char *node_name)
 {
     xmlNode *request;
     int rc = pcmk_rc_ok;
 
     request = create_controller_request(api, CRM_OP_SHUTDOWN, NULL, NULL);
     if (request == NULL) {
         return EINVAL;
     }
     rc = send_controller_request(api, request, false);
     free_xml(request);
     return rc;
 }
 
 /*!
  * \internal
  * \brief Ask the controller to start a DC election
  *
  * \param[in] api          Controller connection
  *
  * \return Standard Pacemaker return code
  *
  * \note This capability currently does not work, so the function is considered
  *       internal. It will likely be removed.
  * \note Event callback will not get a reply.
  */
 int
 pcmk_controld_api_start_election(pcmk_ipc_api_t *api)
 {
     xmlNode *request;
     int rc = pcmk_rc_ok;
 
     request = create_controller_request(api, CRM_OP_VOTE, NULL, NULL);
     if (request == NULL) {
         return EINVAL;
     }
     rc = send_controller_request(api, request, false);
     free_xml(request);
     return rc;
 }
 
 // \return Standard Pacemaker return code
 static int
 controller_resource_op(pcmk_ipc_api_t *api, const char *op,
                        const char *target_node, const char *router_node,
                        bool cib_only, const char *rsc_id,
                        const char *rsc_long_id, const char *standard,
                        const char *provider, const char *type)
 {
     int rc = pcmk_rc_ok;
     char *key;
     xmlNode *request, *msg_data, *xml_rsc, *params;
 
     if (api == NULL) {
         return EINVAL;
     }
     if (router_node == NULL) {
         router_node = target_node;
     }
 
     msg_data = create_xml_node(NULL, XML_GRAPH_TAG_RSC_OP);
 
     /* The controller logs the transition key from resource op requests, so we
      * need to have *something* for it.
      * @TODO don't use "crm-resource"
      */
     key = pcmk__transition_key(0, getpid(), 0,
                                "xxxxxxxx-xrsc-opxx-xcrm-resourcexxxx");
     crm_xml_add(msg_data, XML_ATTR_TRANSITION_KEY, key);
     free(key);
 
     crm_xml_add(msg_data, XML_LRM_ATTR_TARGET, target_node);
     if (safe_str_neq(router_node, target_node)) {
         crm_xml_add(msg_data, XML_LRM_ATTR_ROUTER_NODE, router_node);
     }
 
     if (cib_only) {
         // Indicate that only the CIB needs to be cleaned
         crm_xml_add(msg_data, PCMK__XA_MODE, XML_TAG_CIB);
     }
 
     xml_rsc = create_xml_node(msg_data, XML_CIB_TAG_RESOURCE);
     crm_xml_add(xml_rsc, XML_ATTR_ID, rsc_id);
     crm_xml_add(xml_rsc, XML_ATTR_ID_LONG, rsc_long_id);
     crm_xml_add(xml_rsc, XML_AGENT_ATTR_CLASS, standard);
     crm_xml_add(xml_rsc, XML_AGENT_ATTR_PROVIDER, provider);
     crm_xml_add(xml_rsc, XML_ATTR_TYPE, type);
 
     params = create_xml_node(msg_data, XML_TAG_ATTRS);
     crm_xml_add(params, XML_ATTR_CRM_VERSION, CRM_FEATURE_SET);
 
     // The controller parses the timeout from the request
     key = crm_meta_name(XML_ATTR_TIMEOUT);
     crm_xml_add(params, key, "60000");  /* 1 minute */ //@TODO pass as arg
     free(key);
 
     request = create_controller_request(api, op, router_node, msg_data);
     rc = send_controller_request(api, request, true);
     free_xml(msg_data);
     free_xml(request);
     return rc;
 }
 
 /*!
  * \brief Ask the controller to fail a resource
  *
  * \param[in] api          Controller connection
  * \param[in] target_node  Name of node resource is on
  * \param[in] router_node  Router node for target
  * \param[in] rsc_id       ID of resource to fail
  * \param[in] rsc_long_id  Long ID of resource (if any)
  * \param[in] standard     Standard of resource
  * \param[in] provider     Provider of resource (if any)
  * \param[in] type         Type of resource to fail
  *
  * \return Standard Pacemaker return code
  * \note Event callback will get a reply of type pcmk_controld_reply_resource.
  */
 int
 pcmk_controld_api_fail(pcmk_ipc_api_t *api,
                        const char *target_node, const char *router_node,
                        const char *rsc_id, const char *rsc_long_id,
                        const char *standard, const char *provider,
                        const char *type)
 {
     crm_debug("Sending %s IPC request to fail %s (a.k.a. %s) on %s via %s",
               pcmk_ipc_name(api, true), crm_str(rsc_id), crm_str(rsc_long_id),
               crm_str(target_node), crm_str(router_node));
     return controller_resource_op(api, CRM_OP_LRM_FAIL, target_node,
                                   router_node, false, rsc_id, rsc_long_id,
                                   standard, provider, type);
 }
 
 /*!
  * \brief Ask the controller to refresh a resource
  *
  * \param[in] api          Controller connection
  * \param[in] target_node  Name of node resource is on
  * \param[in] router_node  Router node for target
  * \param[in] rsc_id       ID of resource to refresh
  * \param[in] rsc_long_id  Long ID of resource (if any)
  * \param[in] standard     Standard of resource
  * \param[in] provider     Provider of resource (if any)
  * \param[in] type         Type of resource
  * \param[in] cib_only     If true, clean resource from CIB only
  *
  * \return Standard Pacemaker return code
  * \note Event callback will get a reply of type pcmk_controld_reply_resource.
  */
 int
 pcmk_controld_api_refresh(pcmk_ipc_api_t *api, const char *target_node,
                           const char *router_node,
                           const char *rsc_id, const char *rsc_long_id,
                           const char *standard, const char *provider,
                           const char *type, bool cib_only)
 {
     crm_debug("Sending %s IPC request to refresh %s (a.k.a. %s) on %s via %s",
               pcmk_ipc_name(api, true), crm_str(rsc_id), crm_str(rsc_long_id),
               crm_str(target_node), crm_str(router_node));
     return controller_resource_op(api, CRM_OP_LRM_DELETE, target_node,
                                   router_node, cib_only, rsc_id, rsc_long_id,
                                   standard, provider, type);
 }
 
 /*!
  * \brief Get the number of IPC replies currently expected from the controller
  *
  * \param[in] api  Controller IPC API connection
  *
  * \return Number of replies expected
  */
 unsigned int
 pcmk_controld_api_replies_expected(pcmk_ipc_api_t *api)
 {
     struct controld_api_private_s *private = api->api_data;
 
     return private->replies_expected;
 }
 
+/*!
+ * \brief Create XML for a controller IPC "hello" message
+ *
+ * \deprecated This function is deprecated as part of the public C API.
+ */
+// \todo make this static to this file when breaking API backward compatibility
 xmlNode *
 create_hello_message(const char *uuid, const char *client_name,
                      const char *major_version, const char *minor_version)
 {
     xmlNode *hello_node = NULL;
     xmlNode *hello = NULL;
 
     if (pcmk__str_empty(uuid) || pcmk__str_empty(client_name)
         || pcmk__str_empty(major_version) || pcmk__str_empty(minor_version)) {
         crm_err("Could not create IPC hello message from %s (UUID %s): "
                 "missing information",
                 client_name? client_name : "unknown client",
                 uuid? uuid : "unknown");
         return NULL;
     }
 
     hello_node = create_xml_node(NULL, XML_TAG_OPTIONS);
     if (hello_node == NULL) {
         crm_err("Could not create IPC hello message from %s (UUID %s): "
                 "Message data creation failed", client_name, uuid);
         return NULL;
     }
 
     crm_xml_add(hello_node, "major_version", major_version);
     crm_xml_add(hello_node, "minor_version", minor_version);
     crm_xml_add(hello_node, "client_name", client_name);
     crm_xml_add(hello_node, "client_uuid", uuid);
 
     hello = create_request(CRM_OP_HELLO, hello_node, NULL, NULL, client_name, uuid);
     if (hello == NULL) {
         crm_err("Could not create IPC hello message from %s (UUID %s): "
                 "Request creation failed", client_name, uuid);
         return NULL;
     }
     free_xml(hello_node);
 
     crm_trace("Created hello message from %s (UUID %s)", client_name, uuid);
     return hello;
 }
diff --git a/lib/common/strings.c b/lib/common/strings.c
index 45627383fa..bd68ccf69f 100644
--- a/lib/common/strings.c
+++ b/lib/common/strings.c
@@ -1,717 +1,782 @@
 /*
  * Copyright 2004-2020 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>
 
 #ifndef _GNU_SOURCE
 #  define _GNU_SOURCE
 #endif
 
 #include <stdio.h>
 #include <string.h>
 #include <stdlib.h>
+#include <ctype.h>
 #include <limits.h>
 #include <bzlib.h>
 #include <sys/types.h>
 
 char *
 crm_itoa_stack(int an_int, char *buffer, size_t len)
 {
     if (buffer != NULL) {
         snprintf(buffer, len, "%d", an_int);
     }
 
     return buffer;
 }
 
 /*!
  * \internal
  * \brief Scan a long long integer from a string
  *
  * \param[in]  text      String to scan
  * \param[out] result    If not NULL, where to store scanned value
  * \param[out] end_text  If not NULL, where to store pointer to just after value
  *
  * \return Standard Pacemaker return code (also set errno on error)
  */
 static int
 scan_ll(const char *text, long long *result, char **end_text)
 {
     long long local_result = -1;
     char *local_end_text = NULL;
     int rc = pcmk_rc_ok;
 
     errno = 0;
     if (text != NULL) {
 #ifdef ANSI_ONLY
         local_result = (long long) strtol(text, &local_end_text, 10);
 #else
         local_result = strtoll(text, &local_end_text, 10);
 #endif
         if (errno == ERANGE) {
             rc = errno;
             crm_warn("Integer parsed from %s was clipped to %lld",
                      text, local_result);
 
         } else if (errno != 0) {
             rc = errno;
             local_result = -1;
             crm_err("Could not parse integer from %s (using -1 instead): %s",
                     text, pcmk_rc_str(rc));
 
         } else if (local_end_text == text) {
             rc = EINVAL;
             local_result = -1;
             crm_err("Could not parse integer from %s (using -1 instead): "
                     "No digits found", text);
         }
 
         if ((end_text == NULL) && (local_end_text != NULL)
             && (local_end_text[0] != '\0')) {
             crm_warn("Characters left over after parsing '%s': '%s'",
                      text, local_end_text);
         }
         errno = rc;
     }
     if (end_text != NULL) {
         *end_text = local_end_text;
     }
     if (result != NULL) {
         *result = local_result;
     }
     return rc;
 }
 
 /*!
  * \brief Parse a long long integer value from a string
  *
  * \param[in] text          The string to parse
  * \param[in] default_text  Default string to parse if text is NULL
  *
  * \return Parsed value on success, -1 (and set errno) on error
  */
 long long
 crm_parse_ll(const char *text, const char *default_text)
 {
     long long result;
 
     if (text == NULL) {
         text = default_text;
         if (text == NULL) {
             crm_err("No default conversion value supplied");
             errno = EINVAL;
             return -1;
         }
     }
     scan_ll(text, &result, NULL);
     return result;
 }
 
 /*!
  * \brief Parse an integer value from a string
  *
  * \param[in] text          The string to parse
  * \param[in] default_text  Default string to parse if text is NULL
  *
  * \return Parsed value on success, INT_MIN or INT_MAX (and set errno to ERANGE)
  *         if parsed value is out of integer range, otherwise -1 (and set errno)
  */
 int
 crm_parse_int(const char *text, const char *default_text)
 {
     long long result = crm_parse_ll(text, default_text);
 
     if (result < INT_MIN) {
         // If errno is ERANGE, crm_parse_ll() has already logged a message
         if (errno != ERANGE) {
             crm_err("Conversion of %s was clipped: %lld", text, result);
             errno = ERANGE;
         }
         return INT_MIN;
 
     } else if (result > INT_MAX) {
         // If errno is ERANGE, crm_parse_ll() has already logged a message
         if (errno != ERANGE) {
             crm_err("Conversion of %s was clipped: %lld", text, result);
             errno = ERANGE;
         }
         return INT_MAX;
     }
 
     return (int) result;
 }
 
 /*!
  * \internal
  * \brief Parse a guint from a string stored in a hash table
  *
  * \param[in]  table        Hash table to search
  * \param[in]  key          Hash table key to use to retrieve string
  * \param[in]  default_val  What to use if key has no entry in table
  * \param[out] result       If not NULL, where to store parsed integer
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__guint_from_hash(GHashTable *table, const char *key, guint default_val,
                       guint *result)
 {
     const char *value;
     long long value_ll;
 
     CRM_CHECK((table != NULL) && (key != NULL), return EINVAL);
 
     value = g_hash_table_lookup(table, key);
     if (value == NULL) {
         if (result != NULL) {
             *result = default_val;
         }
         return pcmk_rc_ok;
     }
 
     errno = 0;
     value_ll = crm_parse_ll(value, NULL);
     if (errno != 0) {
         return errno; // Message already logged
     }
     if ((value_ll < 0) || (value_ll > G_MAXUINT)) {
         crm_warn("Could not parse non-negative integer from %s", value);
         return ERANGE;
     }
 
     if (result != NULL) {
         *result = (guint) value_ll;
     }
     return pcmk_rc_ok;
 }
 
 #ifndef NUMCHARS
 #  define	NUMCHARS	"0123456789."
 #endif
 
 #ifndef WHITESPACE
 #  define	WHITESPACE	" \t\n\r\f"
 #endif
 
 /*!
  * \brief Parse a time+units string and return milliseconds equivalent
  *
  * \param[in] input  String with a number and units (optionally with whitespace
  *                   before and/or after the number)
  *
  * \return Milliseconds corresponding to string expression, or -1 on error
  */
 long long
 crm_get_msec(const char *input)
 {
     const char *num_start = NULL;
     const char *units;
     long long multiplier = 1000;
     long long divisor = 1;
     long long msec = -1;
     size_t num_len = 0;
     char *end_text = NULL;
 
     if (input == NULL) {
         return -1;
     }
 
     num_start = input + strspn(input, WHITESPACE);
     num_len = strspn(num_start, NUMCHARS);
     if (num_len < 1) {
         return -1;
     }
     units = num_start + num_len;
     units += strspn(units, WHITESPACE);
 
     if (!strncasecmp(units, "ms", 2) || !strncasecmp(units, "msec", 4)) {
         multiplier = 1;
         divisor = 1;
     } else if (!strncasecmp(units, "us", 2) || !strncasecmp(units, "usec", 4)) {
         multiplier = 1;
         divisor = 1000;
     } else if (!strncasecmp(units, "s", 1) || !strncasecmp(units, "sec", 3)) {
         multiplier = 1000;
         divisor = 1;
     } else if (!strncasecmp(units, "m", 1) || !strncasecmp(units, "min", 3)) {
         multiplier = 60 * 1000;
         divisor = 1;
     } else if (!strncasecmp(units, "h", 1) || !strncasecmp(units, "hr", 2)) {
         multiplier = 60 * 60 * 1000;
         divisor = 1;
     } else if ((*units != EOS) && (*units != '\n') && (*units != '\r')) {
         return -1;
     }
 
     scan_ll(num_start, &msec, &end_text);
     if (msec > (LLONG_MAX / multiplier)) {
         // Arithmetics overflow while multiplier/divisor mutually exclusive
         return LLONG_MAX;
     }
     msec *= multiplier;
     msec /= divisor;
     return msec;
 }
 
 gboolean
 safe_str_neq(const char *a, const char *b)
 {
     if (a == b) {
         return FALSE;
 
     } else if (a == NULL || b == NULL) {
         return TRUE;
 
     } else if (strcasecmp(a, b) == 0) {
         return FALSE;
     }
     return TRUE;
 }
 
 gboolean
 crm_is_true(const char *s)
 {
     gboolean ret = FALSE;
 
     if (s != NULL) {
         crm_str_to_boolean(s, &ret);
     }
     return ret;
 }
 
 int
 crm_str_to_boolean(const char *s, int *ret)
 {
     if (s == NULL) {
         return -1;
 
     } else if (strcasecmp(s, "true") == 0
                || strcasecmp(s, "on") == 0
                || strcasecmp(s, "yes") == 0 || strcasecmp(s, "y") == 0 || strcasecmp(s, "1") == 0) {
         *ret = TRUE;
         return 1;
 
     } else if (strcasecmp(s, "false") == 0
                || strcasecmp(s, "off") == 0
                || strcasecmp(s, "no") == 0 || strcasecmp(s, "n") == 0 || strcasecmp(s, "0") == 0) {
         *ret = FALSE;
         return 1;
     }
     return -1;
 }
 
 char *
 crm_strip_trailing_newline(char *str)
 {
     int len;
 
     if (str == NULL) {
         return str;
     }
 
     for (len = strlen(str) - 1; len >= 0 && str[len] == '\n'; len--) {
         str[len] = '\0';
     }
 
     return str;
 }
 
 gboolean
 crm_str_eq(const char *a, const char *b, gboolean use_case)
 {
     if (use_case) {
         return g_strcmp0(a, b) == 0;
 
         /* TODO - Figure out which calls, if any, really need to be case independent */
     } else if (a == b) {
         return TRUE;
 
     } else if (a == NULL || b == NULL) {
         /* shouldn't be comparing NULLs */
         return FALSE;
 
     } else if (strcasecmp(a, b) == 0) {
         return TRUE;
     }
     return FALSE;
 }
 
 /*!
  * \brief Check whether a string starts with a certain sequence
  *
  * \param[in] str    String to check
  * \param[in] prefix Sequence to match against beginning of \p str
  *
  * \return \c true if \p str begins with match, \c false otherwise
  * \note This is equivalent to !strncmp(s, prefix, strlen(prefix))
  *       but is likely less efficient when prefix is a string literal
  *       if the compiler optimizes away the strlen() at compile time,
  *       and more efficient otherwise.
  */
 bool
 pcmk__starts_with(const char *str, const char *prefix)
 {
     const char *s = str;
     const char *p = prefix;
 
     if (!s || !p) {
         return false;
     }
     while (*s && *p) {
         if (*s++ != *p++) {
             return false;
         }
     }
     return (*p == 0);
 }
 
 static inline bool
 ends_with(const char *s, const char *match, bool as_extension)
 {
     if (pcmk__str_empty(match)) {
         return true;
     } else if (s == NULL) {
         return false;
     } else {
         size_t slen, mlen;
 
         /* Besides as_extension, we could also check
            !strchr(&match[1], match[0]) but that would be inefficient.
          */
         if (as_extension) {
             s = strrchr(s, match[0]);
             return (s == NULL)? false : !strcmp(s, match);
         }
 
         mlen = strlen(match);
         slen = strlen(s);
         return ((slen >= mlen) && !strcmp(s + slen - mlen, match));
     }
 }
 
 /*!
  * \internal
  * \brief Check whether a string ends with a certain sequence
  *
  * \param[in] s      String to check
  * \param[in] match  Sequence to match against end of \p s
  *
  * \return \c true if \p s ends case-sensitively with match, \c false otherwise
  * \note pcmk__ends_with_ext() can be used if the first character of match
  *       does not recur in match.
  */
 bool
 pcmk__ends_with(const char *s, const char *match)
 {
     return ends_with(s, match, false);
 }
 
 /*!
  * \internal
  * \brief Check whether a string ends with a certain "extension"
  *
  * \param[in] s      String to check
  * \param[in] match  Extension to match against end of \p s, that is,
  *                   its first character must not occur anywhere
  *                   in the rest of that very sequence (example: file
  *                   extension where the last dot is its delimiter,
  *                   e.g., ".html"); incorrect results may be
  *                   returned otherwise.
  *
  * \return \c true if \p s ends (verbatim, i.e., case sensitively)
  *         with "extension" designated as \p match (including empty
  *         string), \c false otherwise
  *
  * \note Main incentive to prefer this function over \c pcmk__ends_with()
  *       where possible is the efficiency (at the cost of added
  *       restriction on \p match as stated; the complexity class
  *       remains the same, though: BigO(M+N) vs. BigO(M+2N)).
  */
 bool
 pcmk__ends_with_ext(const char *s, const char *match)
 {
     return ends_with(s, match, true);
 }
 
 /*
  * This re-implements g_str_hash as it was prior to glib2-2.28:
  *
  * https://gitlab.gnome.org/GNOME/glib/commit/354d655ba8a54b754cb5a3efb42767327775696c
  *
  * Note that the new g_str_hash is presumably a *better* hash (it's actually
  * a correct implementation of DJB's hash), but we need to preserve existing
  * behaviour, because the hash key ultimately determines the "sort" order
  * when iterating through GHashTables, which affects allocation of scores to
  * clone instances when iterating through rsc->allowed_nodes.  It (somehow)
  * also appears to have some minor impact on the ordering of a few
  * pseudo_event IDs in the transition graph.
  */
 guint
 g_str_hash_traditional(gconstpointer v)
 {
     const signed char *p;
     guint32 h = 0;
 
     for (p = v; *p != '\0'; p++)
         h = (h << 5) - h + *p;
 
     return h;
 }
 
 /* used with hash tables where case does not matter */
 gboolean
 crm_strcase_equal(gconstpointer a, gconstpointer b)
 {
     return crm_str_eq((const char *) a, (const char *) b, FALSE);
 }
 
 guint
 crm_strcase_hash(gconstpointer v)
 {
     const signed char *p;
     guint32 h = 0;
 
     for (p = v; *p != '\0'; p++)
         h = (h << 5) - h + g_ascii_tolower(*p);
 
     return h;
 }
 
 static void
 copy_str_table_entry(gpointer key, gpointer value, gpointer user_data)
 {
     if (key && value && user_data) {
         g_hash_table_insert((GHashTable*)user_data, strdup(key), strdup(value));
     }
 }
 
 GHashTable *
 crm_str_table_dup(GHashTable *old_table)
 {
     GHashTable *new_table = NULL;
 
     if (old_table) {
         new_table = crm_str_table_new();
         g_hash_table_foreach(old_table, copy_str_table_entry, new_table);
     }
     return new_table;
 }
 
 /*!
  * \internal
  * \brief Add a word to a space-separated string list
  *
  * \param[in,out] list  Pointer to beginning of list
  * \param[in]     word  Word to add to list
  *
  * \return (Potentially new) beginning of list
  * \note This dynamically reallocates list as needed.
  */
 char *
 pcmk__add_word(char *list, const char *word)
 {
     if (word != NULL) {
         size_t len = list? strlen(list) : 0;
 
         list = realloc_safe(list, len + strlen(word) + 2); // 2 = space + EOS
         sprintf(list + len, " %s", word);
     }
     return list;
 }
 
 /*!
  * \internal
  * \brief Compress data
  *
  * \param[in]  data        Data to compress
  * \param[in]  length      Number of characters of data to compress
  * \param[in]  max         Maximum size of compressed data (or 0 to estimate)
  * \param[out] result      Where to store newly allocated compressed result
  * \param[out] result_len  Where to store actual compressed length of result
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__compress(const char *data, unsigned int length, unsigned int max,
                char **result, unsigned int *result_len)
 {
     int rc;
     char *compressed = NULL;
     char *uncompressed = strdup(data);
 #ifdef CLOCK_MONOTONIC
     struct timespec after_t;
     struct timespec before_t;
 #endif
 
     if (max == 0) {
         max = (length * 1.01) + 601; // Size guaranteed to hold result
     }
 
 #ifdef CLOCK_MONOTONIC
     clock_gettime(CLOCK_MONOTONIC, &before_t);
 #endif
 
     compressed = calloc((size_t) max, sizeof(char));
     CRM_ASSERT(compressed);
 
     *result_len = max;
     rc = BZ2_bzBuffToBuffCompress(compressed, result_len, uncompressed, length,
                                   CRM_BZ2_BLOCKS, 0, CRM_BZ2_WORK);
     free(uncompressed);
     if (rc != BZ_OK) {
         crm_err("Compression of %d bytes failed: %s " CRM_XS " bzerror=%d",
                 length, bz2_strerror(rc), rc);
         free(compressed);
         return pcmk_rc_error;
     }
 
 #ifdef CLOCK_MONOTONIC
     clock_gettime(CLOCK_MONOTONIC, &after_t);
 
     crm_trace("Compressed %d bytes into %d (ratio %d:1) in %.0fms",
              length, *result_len, length / (*result_len),
              (after_t.tv_sec - before_t.tv_sec) * 1000 +
              (after_t.tv_nsec - before_t.tv_nsec) / 1e6);
 #else
     crm_trace("Compressed %d bytes into %d (ratio %d:1)",
              length, *result_len, length / (*result_len));
 #endif
 
     *result = compressed;
     return pcmk_rc_ok;
 }
 
 char *
 crm_strdup_printf(char const *format, ...)
 {
     va_list ap;
     int len = 0;
     char *string = NULL;
 
     va_start(ap, format);
     len = vasprintf (&string, format, ap);
     CRM_ASSERT(len > 0);
     va_end(ap);
     return string;
 }
 
 int
 pcmk__parse_ll_range(const char *srcstring, long long *start, long long *end)
 {
     char *remainder = NULL;
 
     CRM_ASSERT(start != NULL && end != NULL);
 
     *start = -1;
     *end = -1;
 
     crm_trace("Attempting to decode: [%s]", srcstring);
     if (srcstring == NULL || strcmp(srcstring, "") == 0 || strcmp(srcstring, "-") == 0) {
         return pcmk_rc_unknown_format;
     }
 
     /* String starts with a dash, so this is either a range with
      * no beginning or garbage.
      * */
     if (*srcstring == '-') {
         int rc = scan_ll(srcstring+1, end, &remainder);
 
         if (rc != pcmk_rc_ok || *remainder != '\0') {
             return pcmk_rc_unknown_format;
         } else {
             return pcmk_rc_ok;
         }
     }
 
     if (scan_ll(srcstring, start, &remainder) != pcmk_rc_ok) {
         return pcmk_rc_unknown_format;
     }
 
     if (*remainder && *remainder == '-') {
         if (*(remainder+1)) {
             char *more_remainder = NULL;
             int rc = scan_ll(remainder+1, end, &more_remainder);
 
             if (rc != pcmk_rc_ok || *more_remainder != '\0') {
                 return pcmk_rc_unknown_format;
             }
         }
     } else if (*remainder && *remainder != '-') {
         *start = -1;
         return pcmk_rc_unknown_format;
     } else {
         /* The input string contained only one number.  Set start and end
          * to the same value and return pcmk_rc_ok.  This gives the caller
          * a way to tell this condition apart from a range with no end.
          */
         *end = *start;
     }
 
     return pcmk_rc_ok;
 }
 
 gboolean
 pcmk__str_in_list(GList *lst, const gchar *s)
 {
     if (lst == NULL) {
         return FALSE;
     }
 
     if (strcmp(lst->data, "*") == 0 && lst->next == NULL) {
         return TRUE;
     }
 
     return g_list_find_custom(lst, s, (GCompareFunc) strcmp) != NULL;
 }
 
 bool
 pcmk__str_any_of(const char *s, ...)
 {
     bool rc = false;
     va_list ap;
 
     va_start(ap, s);
 
     while (1) {
         const char *ele = va_arg(ap, const char *);
 
         if (ele == NULL) {
             break;
         } else if (crm_str_eq(s, ele, FALSE)) {
             rc = true;
             break;
         }
     }
 
     va_end(ap);
     return rc;
 }
 
 bool
 pcmk__str_none_of(const char *s, ...)
 {
     bool rc = true;
     va_list ap;
 
     va_start(ap, s);
 
     while (1) {
         const char *ele = va_arg(ap, const char *);
 
         if (ele == NULL) {
             break;
         } else if (crm_str_eq(s, ele, FALSE)) {
             rc = false;
             break;
         }
     }
 
     va_end(ap);
     return rc;
 }
+
+/*
+ * \brief Sort strings, with numeric portions sorted numerically
+ *
+ * Sort two strings case-insensitively like strcasecmp(), but with any numeric
+ * portions of the string sorted numerically. This is particularly useful for
+ * node names (for example, "node10" will sort higher than "node9" but lower
+ * than "remotenode9").
+ *
+ * \param[in] s1  First string to compare (must not be NULL)
+ * \param[in] s2  Second string to compare (must not be NULL)
+ *
+ * \retval -1 \p s1 comes before \p s2
+ * \retval  0 \p s1 and \p s2 are equal
+ * \retval  1 \p s1 comes after \p s2
+ */
+int
+pcmk_numeric_strcasecmp(const char *s1, const char *s2)
+{
+    while (*s1 && *s2) {
+        if (isdigit(*s1) && isdigit(*s2)) {
+            // If node names contain a number, sort numerically
+
+            char *end1 = NULL;
+            char *end2 = NULL;
+            long num1 = strtol(s1, &end1, 10);
+            long num2 = strtol(s2, &end2, 10);
+
+            // allow ordering e.g. 007 > 7
+            size_t len1 = end1 - s1;
+            size_t len2 = end2 - s2;
+
+            if (num1 < num2) {
+                return -1;
+            } else if (num1 > num2) {
+                return 1;
+            } else if (len1 < len2) {
+                return -1;
+            } else if (len1 > len2) {
+                return 1;
+            }
+            s1 = end1;
+            s2 = end2;
+        } else {
+            // Compare non-digits case-insensitively
+            int lower1 = tolower(*s1);
+            int lower2 = tolower(*s2);
+
+            if (lower1 < lower2) {
+                return -1;
+            } else if (lower1 > lower2) {
+                return 1;
+            }
+            ++s1;
+            ++s2;
+        }
+    }
+    if (!*s1 && *s2) {
+        return -1;
+    } else if (*s1 && !*s2) {
+        return 1;
+    }
+    return 0;
+}
diff --git a/lib/pengine/utils.c b/lib/pengine/utils.c
index ce3c260bfc..584def405b 100644
--- a/lib/pengine/utils.c
+++ b/lib/pengine/utils.c
@@ -1,2854 +1,2808 @@
 /*
  * Copyright 2004-2020 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 <crm/msg_xml.h>
 #include <crm/common/xml.h>
 #include <crm/common/util.h>
 
-#include <ctype.h>
 #include <glib.h>
 #include <stdbool.h>
 
 #include <crm/pengine/rules.h>
 #include <crm/pengine/internal.h>
 
 extern xmlNode *get_object_root(const char *object_type, xmlNode * the_root);
 void print_str_str(gpointer key, gpointer value, gpointer user_data);
 gboolean ghash_free_str_str(gpointer key, gpointer value, gpointer user_data);
 static void unpack_operation(pe_action_t * action, xmlNode * xml_obj, pe_resource_t * container,
                              pe_working_set_t * data_set, guint interval_ms);
 static xmlNode *find_rsc_op_entry_helper(pe_resource_t * rsc, const char *key,
                                          gboolean include_disabled);
 
 #if ENABLE_VERSIONED_ATTRS
 pe_rsc_action_details_t *
 pe_rsc_action_details(pe_action_t *action)
 {
     pe_rsc_action_details_t *details;
 
     CRM_CHECK(action != NULL, return NULL);
 
     if (action->action_details == NULL) {
         action->action_details = calloc(1, sizeof(pe_rsc_action_details_t));
         CRM_CHECK(action->action_details != NULL, return NULL);
     }
 
     details = (pe_rsc_action_details_t *) action->action_details;
     if (details->versioned_parameters == NULL) {
         details->versioned_parameters = create_xml_node(NULL,
                                                         XML_TAG_OP_VER_ATTRS);
     }
     if (details->versioned_meta == NULL) {
         details->versioned_meta = create_xml_node(NULL, XML_TAG_OP_VER_META);
     }
     return details;
 }
 
 static void
 pe_free_rsc_action_details(pe_action_t *action)
 {
     pe_rsc_action_details_t *details;
 
     if ((action == NULL) || (action->action_details == NULL)) {
         return;
     }
 
     details = (pe_rsc_action_details_t *) action->action_details;
 
     if (details->versioned_parameters) {
         free_xml(details->versioned_parameters);
     }
     if (details->versioned_meta) {
         free_xml(details->versioned_meta);
     }
 
     action->action_details = NULL;
 }
 #endif
 
 /*!
  * \internal
  * \brief Check whether we can fence a particular node
  *
  * \param[in] data_set  Working set for cluster
  * \param[in] node      Name of node to check
  *
  * \return true if node can be fenced, false otherwise
  */
 bool
 pe_can_fence(pe_working_set_t *data_set, pe_node_t *node)
 {
     if (pe__is_guest_node(node)) {
         /* Guest nodes are fenced by stopping their container resource. We can
          * do that if the container's host is either online or fenceable.
          */
         pe_resource_t *rsc = node->details->remote_rsc->container;
 
         for (GList *n = rsc->running_on; n != NULL; n = n->next) {
             pe_node_t *container_node = n->data;
 
             if (!container_node->details->online
                 && !pe_can_fence(data_set, container_node)) {
                 return false;
             }
         }
         return true;
 
     } else if(is_not_set(data_set->flags, pe_flag_stonith_enabled)) {
         return false; /* Turned off */
 
     } else if (is_not_set(data_set->flags, pe_flag_have_stonith_resource)) {
         return false; /* No devices */
 
     } else if (is_set(data_set->flags, pe_flag_have_quorum)) {
         return true;
 
     } else if (data_set->no_quorum_policy == no_quorum_ignore) {
         return true;
 
     } else if(node == NULL) {
         return false;
 
     } else if(node->details->online) {
         crm_notice("We can fence %s without quorum because they're in our membership", node->details->uname);
         return true;
     }
 
     crm_trace("Cannot fence %s", node->details->uname);
     return false;
 }
 
 /*!
  * \internal
  * \brief Copy a node object
  *
  * \param[in] this_node  Node object to copy
  *
  * \return Newly allocated shallow copy of this_node
  * \note This function asserts on errors and is guaranteed to return non-NULL.
  */
 pe_node_t *
 pe__copy_node(const pe_node_t *this_node)
 {
     pe_node_t *new_node = NULL;
 
     CRM_ASSERT(this_node != NULL);
 
     new_node = calloc(1, sizeof(pe_node_t));
     CRM_ASSERT(new_node != NULL);
 
     new_node->rsc_discover_mode = this_node->rsc_discover_mode;
     new_node->weight = this_node->weight;
     new_node->fixed = this_node->fixed;
     new_node->details = this_node->details;
 
     return new_node;
 }
 
 /* any node in list1 or list2 and not in the other gets a score of -INFINITY */
 void
 node_list_exclude(GHashTable * hash, GListPtr list, gboolean merge_scores)
 {
     GHashTable *result = hash;
     pe_node_t *other_node = NULL;
     GListPtr gIter = list;
 
     GHashTableIter iter;
     pe_node_t *node = NULL;
 
     g_hash_table_iter_init(&iter, hash);
     while (g_hash_table_iter_next(&iter, NULL, (void **)&node)) {
 
         other_node = pe_find_node_id(list, node->details->id);
         if (other_node == NULL) {
             node->weight = -INFINITY;
         } else if (merge_scores) {
             node->weight = pe__add_scores(node->weight, other_node->weight);
         }
     }
 
     for (; gIter != NULL; gIter = gIter->next) {
         pe_node_t *node = (pe_node_t *) gIter->data;
 
         other_node = pe_hash_table_lookup(result, node->details->id);
 
         if (other_node == NULL) {
             pe_node_t *new_node = pe__copy_node(node);
 
             new_node->weight = -INFINITY;
             g_hash_table_insert(result, (gpointer) new_node->details->id, new_node);
         }
     }
 }
 
 /*!
  * \internal
  * \brief Create a node hash table from a node list
  *
  * \param[in] list  Node list
  *
  * \return Hash table equivalent of node list
  */
 GHashTable *
 pe__node_list2table(GList *list)
 {
     GHashTable *result = NULL;
 
     result = g_hash_table_new_full(crm_str_hash, g_str_equal, NULL, free);
     for (GList *gIter = list; gIter != NULL; gIter = gIter->next) {
         pe_node_t *new_node = pe__copy_node((pe_node_t *) gIter->data);
 
         g_hash_table_insert(result, (gpointer) new_node->details->id, new_node);
     }
     return result;
 }
 
 gint
 sort_node_uname(gconstpointer a, gconstpointer b)
 {
-    const char *name_a = ((const pe_node_t *) a)->details->uname;
-    const char *name_b = ((const pe_node_t *) b)->details->uname;
-
-    while (*name_a && *name_b) {
-        if (isdigit(*name_a) && isdigit(*name_b)) {
-            // If node names contain a number, sort numerically
-
-            char *end_a = NULL;
-            char *end_b = NULL;
-            long num_a = strtol(name_a, &end_a, 10);
-            long num_b = strtol(name_b, &end_b, 10);
-
-            // allow ordering e.g. 007 > 7
-            size_t len_a = end_a - name_a;
-            size_t len_b = end_b - name_b;
-
-            if (num_a < num_b) {
-                return -1;
-            } else if (num_a > num_b) {
-                return 1;
-            } else if (len_a < len_b) {
-                return -1;
-            } else if (len_a > len_b) {
-                return 1;
-            }
-            name_a = end_a;
-            name_b = end_b;
-        } else {
-            // Compare non-digits case-insensitively
-            int lower_a = tolower(*name_a);
-            int lower_b = tolower(*name_b);
-
-            if (lower_a < lower_b) {
-                return -1;
-            } else if (lower_a > lower_b) {
-                return 1;
-            }
-            ++name_a;
-            ++name_b;
-        }
-    }
-    if (!*name_a && *name_b) {
-        return -1;
-    } else if (*name_a && !*name_b) {
-        return 1;
-    }
-    return 0;
+    return pcmk_numeric_strcasecmp(((const pe_node_t *) a)->details->uname,
+                                   ((const pe_node_t *) b)->details->uname);
 }
 
 /*!
  * \internal
  * \brief Output node weights to stdout
  *
  * \param[in] rsc       Use allowed nodes for this resource
  * \param[in] comment   Text description to prefix lines with
  * \param[in] nodes     If rsc is not specified, use these nodes
  */
 static void
 pe__output_node_weights(pe_resource_t *rsc, const char *comment,
                         GHashTable *nodes)
 {
     char score[128]; // Stack-allocated since this is called frequently
 
     // Sort the nodes so the output is consistent for regression tests
     GList *list = g_list_sort(g_hash_table_get_values(nodes), sort_node_uname);
 
     for (GList *gIter = list; gIter != NULL; gIter = gIter->next) {
         pe_node_t *node = (pe_node_t *) gIter->data;
 
         score2char_stack(node->weight, score, sizeof(score));
         if (rsc) {
             printf("%s: %s allocation score on %s: %s\n",
                    comment, rsc->id, node->details->uname, score);
         } else {
             printf("%s: %s = %s\n", comment, node->details->uname, score);
         }
     }
     g_list_free(list);
 }
 
 /*!
  * \internal
  * \brief Log node weights at trace level
  *
  * \param[in] file      Caller's filename
  * \param[in] function  Caller's function name
  * \param[in] line      Caller's line number
  * \param[in] rsc       Use allowed nodes for this resource
  * \param[in] comment   Text description to prefix lines with
  * \param[in] nodes     If rsc is not specified, use these nodes
  */
 static void
 pe__log_node_weights(const char *file, const char *function, int line,
                      pe_resource_t *rsc, const char *comment, GHashTable *nodes)
 {
     GHashTableIter iter;
     pe_node_t *node = NULL;
     char score[128]; // Stack-allocated since this is called frequently
 
     // Don't waste time if we're not tracing at this point
     pcmk__log_else(LOG_TRACE, return);
 
     g_hash_table_iter_init(&iter, nodes);
     while (g_hash_table_iter_next(&iter, NULL, (void **) &node)) {
         score2char_stack(node->weight, score, sizeof(score));
         if (rsc) {
             qb_log_from_external_source(function, file,
                                         "%s: %s allocation score on %s: %s",
                                         LOG_TRACE, line, 0,
                                         comment, rsc->id,
                                         node->details->uname, score);
         } else {
             qb_log_from_external_source(function, file, "%s: %s = %s",
                                         LOG_TRACE, line, 0,
                                         comment, node->details->uname,
                                         score);
         }
     }
 }
 
 /*!
  * \internal
  * \brief Log or output node weights
  *
  * \param[in] file      Caller's filename
  * \param[in] function  Caller's function name
  * \param[in] line      Caller's line number
  * \param[in] to_log    Log if true, otherwise output
  * \param[in] rsc       Use allowed nodes for this resource
  * \param[in] comment   Text description to prefix lines with
  * \param[in] nodes     If rsc is not specified, use these nodes
  */
 void
 pe__show_node_weights_as(const char *file, const char *function, int line,
                          bool to_log, pe_resource_t *rsc, const char *comment,
                          GHashTable *nodes)
 {
     if (rsc != NULL) {
         if (is_set(rsc->flags, pe_rsc_orphan)) {
             // Don't show allocation scores for orphans
             return;
         }
         nodes = rsc->allowed_nodes;
     }
     if (nodes == NULL) {
         // Nothing to show
         return;
     }
 
     if (to_log) {
         pe__log_node_weights(file, function, line, rsc, comment, nodes);
     } else {
         pe__output_node_weights(rsc, comment, nodes);
     }
 
     // If this resource has children, repeat recursively for each
     if (rsc && rsc->children) {
         for (GList *gIter = rsc->children; gIter != NULL; gIter = gIter->next) {
             pe_resource_t *child = (pe_resource_t *) gIter->data;
 
             pe__show_node_weights_as(file, function, line, to_log, child,
                                      comment, nodes);
         }
     }
 }
 
 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;
 }
 
 void
 dump_node_capacity(int level, const char *comment, pe_node_t * node)
 {
     char *dump_text = crm_strdup_printf("%s: %s capacity:",
                                         comment, node->details->uname);
 
     g_hash_table_foreach(node->details->utilization, append_dump_text, &dump_text);
 
     if (level == LOG_STDOUT) {
         fprintf(stdout, "%s\n", dump_text);
     } else {
         crm_trace("%s", dump_text);
     }
 
     free(dump_text);
 }
 
 void
 dump_rsc_utilization(int level, const char *comment, pe_resource_t * rsc, pe_node_t * node)
 {
     char *dump_text = crm_strdup_printf("%s: %s utilization on %s:",
                                         comment, rsc->id, node->details->uname);
 
     g_hash_table_foreach(rsc->utilization, append_dump_text, &dump_text);
     switch (level) {
         case LOG_STDOUT:
             fprintf(stdout, "%s\n", dump_text);
             break;
         case LOG_NEVER:
             break;
         default:
             crm_trace("%s", dump_text);
     }
     free(dump_text);
 }
 
 gint
 sort_rsc_index(gconstpointer a, gconstpointer b)
 {
     const pe_resource_t *resource1 = (const pe_resource_t *)a;
     const pe_resource_t *resource2 = (const pe_resource_t *)b;
 
     if (a == NULL && b == NULL) {
         return 0;
     }
     if (a == NULL) {
         return 1;
     }
     if (b == NULL) {
         return -1;
     }
 
     if (resource1->sort_index > resource2->sort_index) {
         return -1;
     }
 
     if (resource1->sort_index < resource2->sort_index) {
         return 1;
     }
 
     return 0;
 }
 
 gint
 sort_rsc_priority(gconstpointer a, gconstpointer b)
 {
     const pe_resource_t *resource1 = (const pe_resource_t *)a;
     const pe_resource_t *resource2 = (const pe_resource_t *)b;
 
     if (a == NULL && b == NULL) {
         return 0;
     }
     if (a == NULL) {
         return 1;
     }
     if (b == NULL) {
         return -1;
     }
 
     if (resource1->priority > resource2->priority) {
         return -1;
     }
 
     if (resource1->priority < resource2->priority) {
         return 1;
     }
 
     return 0;
 }
 
 static enum pe_quorum_policy
 effective_quorum_policy(pe_resource_t *rsc, pe_working_set_t *data_set)
 {
     enum pe_quorum_policy policy = data_set->no_quorum_policy;
 
     if (is_set(data_set->flags, pe_flag_have_quorum)) {
         policy = no_quorum_ignore;
 
     } else if (data_set->no_quorum_policy == no_quorum_demote) {
         switch (rsc->role) {
             case RSC_ROLE_MASTER:
             case RSC_ROLE_SLAVE:
                 if (rsc->next_role > RSC_ROLE_SLAVE) {
                     rsc->next_role = RSC_ROLE_SLAVE;
                 }
                 policy = no_quorum_ignore;
                 break;
             default:
                 policy = no_quorum_stop;
                 break;
         }
     }
     return policy;
 }
 
 pe_action_t *
 custom_action(pe_resource_t * rsc, char *key, const char *task,
               pe_node_t * on_node, gboolean optional, gboolean save_action,
               pe_working_set_t * data_set)
 {
     pe_action_t *action = NULL;
     GListPtr possible_matches = NULL;
 
     CRM_CHECK(key != NULL, return NULL);
     CRM_CHECK(task != NULL, free(key); return NULL);
 
     if (save_action && rsc != NULL) {
         possible_matches = find_actions(rsc->actions, key, on_node);
     } else if(save_action) {
 #if 0
         action = g_hash_table_lookup(data_set->singletons, key);
 #else
         /* More expensive but takes 'node' into account */
         possible_matches = find_actions(data_set->actions, key, on_node);
 #endif
     }
 
     if(data_set->singletons == NULL) {
         data_set->singletons = g_hash_table_new_full(crm_str_hash, g_str_equal, NULL, NULL);
     }
 
     if (possible_matches != NULL) {
         if (pcmk__list_of_multiple(possible_matches)) {
             pe_warn("Action %s for %s on %s exists %d times",
                     task, rsc ? rsc->id : "<NULL>",
                     on_node ? on_node->details->uname : "<NULL>", g_list_length(possible_matches));
         }
 
         action = g_list_nth_data(possible_matches, 0);
         pe_rsc_trace(rsc, "Found existing action %d (%s) for %s (%s) on %s",
                      action->id, action->uuid,
                      (rsc? rsc->id : "no resource"), task,
                      (on_node? on_node->details->uname : "no node"));
         g_list_free(possible_matches);
     }
 
     if (action == NULL) {
         if (save_action) {
             pe_rsc_trace(rsc, "Creating %s action %d: %s for %s (%s) on %s",
                          (optional? "optional" : "mandatory"),
                          data_set->action_id, key,
                          (rsc? rsc->id : "no resource"), task,
                          (on_node? on_node->details->uname : "no node"));
         }
 
         action = calloc(1, sizeof(pe_action_t));
         if (save_action) {
             action->id = data_set->action_id++;
         } else {
             action->id = 0;
         }
         action->rsc = rsc;
         CRM_ASSERT(task != NULL);
         action->task = strdup(task);
         if (on_node) {
             action->node = pe__copy_node(on_node);
         }
         action->uuid = strdup(key);
 
         if (safe_str_eq(task, CRM_OP_LRM_DELETE)) {
             // Resource history deletion for a node can be done on the DC
             pe_set_action_bit(action, pe_action_dc);
         }
 
         pe_set_action_bit(action, pe_action_runnable);
         if (optional) {
             pe_set_action_bit(action, pe_action_optional);
         } else {
             pe_clear_action_bit(action, pe_action_optional);
         }
 
         action->extra = crm_str_table_new();
         action->meta = crm_str_table_new();
 
         if (save_action) {
             data_set->actions = g_list_prepend(data_set->actions, action);
             if(rsc == NULL) {
                 g_hash_table_insert(data_set->singletons, action->uuid, action);
             }
         }
 
         if (rsc != NULL) {
             guint interval_ms = 0;
 
             action->op_entry = find_rsc_op_entry_helper(rsc, key, TRUE);
             parse_op_key(key, NULL, NULL, &interval_ms);
 
             unpack_operation(action, action->op_entry, rsc->container, data_set,
                              interval_ms);
 
             if (save_action) {
                 rsc->actions = g_list_prepend(rsc->actions, action);
             }
         }
 
         if (save_action) {
             pe_rsc_trace(rsc, "Action %d created", action->id);
         }
     }
 
     if (!optional && is_set(action->flags, pe_action_optional)) {
         pe_rsc_trace(rsc, "Unset optional on action %d", action->id);
         pe_clear_action_bit(action, pe_action_optional);
     }
 
     if (rsc != NULL) {
         enum action_tasks a_task = text2task(action->task);
         enum pe_quorum_policy quorum_policy = effective_quorum_policy(rsc, data_set);
         int warn_level = LOG_TRACE;
 
         if (save_action) {
             warn_level = LOG_WARNING;
         }
 
         if (is_set(action->flags, pe_action_have_node_attrs) == FALSE
             && action->node != NULL && action->op_entry != NULL) {
             pe_rule_eval_data_t rule_data = {
                 .node_hash = action->node->details->attrs,
                 .role = RSC_ROLE_UNKNOWN,
                 .now = data_set->now,
                 .match_data = NULL,
                 .rsc_data = NULL,
                 .op_data = NULL
             };
 
             pe_set_action_bit(action, pe_action_have_node_attrs);
             pe__unpack_dataset_nvpairs(action->op_entry, XML_TAG_ATTR_SETS,
                                        &rule_data, action->extra, NULL,
                                        FALSE, data_set);
         }
 
         if (is_set(action->flags, pe_action_pseudo)) {
             /* leave untouched */
 
         } else if (action->node == NULL) {
             pe_rsc_trace(rsc, "Unset runnable on %s", action->uuid);
             pe_clear_action_bit(action, pe_action_runnable);
 
         } else if (is_not_set(rsc->flags, pe_rsc_managed)
                    && g_hash_table_lookup(action->meta,
                                           XML_LRM_ATTR_INTERVAL_MS) == NULL) {
             crm_debug("Action %s (unmanaged)", action->uuid);
             pe_rsc_trace(rsc, "Set optional on %s", action->uuid);
             pe_set_action_bit(action, pe_action_optional);
 /*   			action->runnable = FALSE; */
 
         } else if (is_not_set(action->flags, pe_action_dc)
                    && !(action->node->details->online)
                    && (!pe__is_guest_node(action->node)
                        || action->node->details->remote_requires_reset)) {
             pe_clear_action_bit(action, pe_action_runnable);
             do_crm_log(warn_level, "Action %s on %s is unrunnable (offline)",
                        action->uuid, action->node->details->uname);
             if (is_set(action->rsc->flags, pe_rsc_managed)
                 && save_action && a_task == stop_rsc
                 && action->node->details->unclean == FALSE) {
                 pe_fence_node(data_set, action->node, "resource actions are unrunnable", FALSE);
             }
 
         } else if (is_not_set(action->flags, pe_action_dc)
                    && action->node->details->pending) {
             pe_clear_action_bit(action, pe_action_runnable);
             do_crm_log(warn_level, "Action %s on %s is unrunnable (pending)",
                        action->uuid, action->node->details->uname);
 
         } else if (action->needs == rsc_req_nothing) {
             pe_rsc_trace(rsc, "Action %s does not require anything", action->uuid);
             pe_action_set_reason(action, NULL, TRUE);
             if (pe__is_guest_node(action->node)
                 && !pe_can_fence(data_set, action->node)) {
                 /* An action that requires nothing usually does not require any
                  * fencing in order to be runnable. However, there is an
                  * exception: an action cannot be completed if it is on a guest
                  * node whose host is unclean and cannot be fenced.
                  */
                 pe_clear_action_bit(action, pe_action_runnable);
                 crm_debug("%s\t%s (cancelled : host cannot be fenced)",
                           action->node->details->uname, action->uuid);
             } else {
                 pe_set_action_bit(action, pe_action_runnable);
             }
 #if 0
             /*
              * No point checking this
              * - if we don't have quorum we can't stonith anyway
              */
         } else if (action->needs == rsc_req_stonith) {
             crm_trace("Action %s requires only stonith", action->uuid);
             action->runnable = TRUE;
 #endif
         } else if (quorum_policy == no_quorum_stop) {
             pe_action_set_flag_reason(__FUNCTION__, __LINE__, action, NULL, "no quorum", pe_action_runnable, TRUE);
             crm_debug("%s\t%s (cancelled : quorum)", action->node->details->uname, action->uuid);
 
         } else if (quorum_policy == no_quorum_freeze) {
             pe_rsc_trace(rsc, "Check resource is already active: %s %s %s %s", rsc->id, action->uuid, role2text(rsc->next_role), role2text(rsc->role));
             if (rsc->fns->active(rsc, TRUE) == FALSE || rsc->next_role > rsc->role) {
                 pe_action_set_flag_reason(__FUNCTION__, __LINE__, action, NULL, "quorum freeze", pe_action_runnable, TRUE);
                 pe_rsc_debug(rsc, "%s\t%s (cancelled : quorum freeze)",
                              action->node->details->uname, action->uuid);
             }
 
         } else if(is_not_set(action->flags, pe_action_runnable)) {
             pe_rsc_trace(rsc, "Action %s is runnable", action->uuid);
             //pe_action_set_reason(action, NULL, TRUE);
             pe_set_action_bit(action, pe_action_runnable);
         }
 
         if (save_action) {
             switch (a_task) {
                 case stop_rsc:
                     set_bit(rsc->flags, pe_rsc_stopping);
                     break;
                 case start_rsc:
                     clear_bit(rsc->flags, pe_rsc_starting);
                     if (is_set(action->flags, pe_action_runnable)) {
                         set_bit(rsc->flags, pe_rsc_starting);
                     }
                     break;
                 default:
                     break;
             }
         }
     }
 
     free(key);
     return action;
 }
 
 static bool
 valid_stop_on_fail(const char *value)
 {
     return pcmk__str_none_of(value, "standby", "demote", "stop", NULL);
 }
 
 static const char *
 unpack_operation_on_fail(pe_action_t * action)
 {
 
     const char *name = NULL;
     const char *role = NULL;
     const char *on_fail = NULL;
     const char *interval_spec = NULL;
     const char *enabled = NULL;
     const char *value = g_hash_table_lookup(action->meta, XML_OP_ATTR_ON_FAIL);
 
     if (safe_str_eq(action->task, CRMD_ACTION_STOP)
         && !valid_stop_on_fail(value)) {
 
         pcmk__config_err("Resetting '" XML_OP_ATTR_ON_FAIL "' for %s stop "
                          "action to default value because '%s' is not "
                          "allowed for stop", action->rsc->id, value);
         return NULL;
 
     } else if (safe_str_eq(action->task, CRMD_ACTION_DEMOTE) && !value) {
         /* demote on_fail defaults to master monitor value if present */
         xmlNode *operation = NULL;
 
         CRM_CHECK(action->rsc != NULL, return NULL);
 
         for (operation = __xml_first_child_element(action->rsc->ops_xml);
              operation && !value; operation = __xml_next_element(operation)) {
 
             if (!crm_str_eq((const char *)operation->name, "op", TRUE)) {
                 continue;
             }
             name = crm_element_value(operation, "name");
             role = crm_element_value(operation, "role");
             on_fail = crm_element_value(operation, XML_OP_ATTR_ON_FAIL);
             enabled = crm_element_value(operation, "enabled");
             interval_spec = crm_element_value(operation, XML_LRM_ATTR_INTERVAL);
             if (!on_fail) {
                 continue;
             } else if (enabled && !crm_is_true(enabled)) {
                 continue;
             } else if (safe_str_neq(name, "monitor") || safe_str_neq(role, "Master")) {
                 continue;
             } else if (crm_parse_interval_spec(interval_spec) == 0) {
                 continue;
             } else if (safe_str_eq(on_fail, "demote")) {
                 continue;
             }
 
             value = on_fail;
         }
     } else if (safe_str_eq(action->task, CRM_OP_LRM_DELETE)) {
         value = "ignore";
 
     } else if (safe_str_eq(value, "demote")) {
         name = crm_element_value(action->op_entry, "name");
         role = crm_element_value(action->op_entry, "role");
         on_fail = crm_element_value(action->op_entry, XML_OP_ATTR_ON_FAIL);
         interval_spec = crm_element_value(action->op_entry,
                                           XML_LRM_ATTR_INTERVAL);
 
         if (safe_str_neq(name, CRMD_ACTION_PROMOTE)
             && (safe_str_neq(name, CRMD_ACTION_STATUS)
                 || safe_str_neq(role, "Master")
                 || (crm_parse_interval_spec(interval_spec) == 0))) {
             pcmk__config_err("Resetting '" XML_OP_ATTR_ON_FAIL "' for %s %s "
                              "action to default value because 'demote' is not "
                              "allowed for it", action->rsc->id, name);
             return NULL;
         }
     }
 
     return value;
 }
 
 static xmlNode *
 find_min_interval_mon(pe_resource_t * rsc, gboolean include_disabled)
 {
     guint interval_ms = 0;
     guint min_interval_ms = G_MAXUINT;
     const char *name = NULL;
     const char *value = NULL;
     const char *interval_spec = NULL;
     xmlNode *op = NULL;
     xmlNode *operation = NULL;
 
     for (operation = __xml_first_child_element(rsc->ops_xml); operation != NULL;
          operation = __xml_next_element(operation)) {
 
         if (crm_str_eq((const char *)operation->name, "op", TRUE)) {
             name = crm_element_value(operation, "name");
             interval_spec = crm_element_value(operation, XML_LRM_ATTR_INTERVAL);
             value = crm_element_value(operation, "enabled");
             if (!include_disabled && value && crm_is_true(value) == FALSE) {
                 continue;
             }
 
             if (safe_str_neq(name, RSC_STATUS)) {
                 continue;
             }
 
             interval_ms = crm_parse_interval_spec(interval_spec);
 
             if (interval_ms && (interval_ms < min_interval_ms)) {
                 min_interval_ms = interval_ms;
                 op = operation;
             }
         }
     }
 
     return op;
 }
 
 static int
 unpack_start_delay(const char *value, GHashTable *meta)
 {
     int start_delay = 0;
 
     if (value != NULL) {
         start_delay = crm_get_msec(value);
 
         if (start_delay < 0) {
             start_delay = 0;
         }
 
         if (meta) {
             g_hash_table_replace(meta, strdup(XML_OP_ATTR_START_DELAY), crm_itoa(start_delay));
         }
     }
 
     return start_delay;
 }
 
 // true if value contains valid, non-NULL interval origin for recurring op
 static bool
 unpack_interval_origin(const char *value, xmlNode *xml_obj, guint interval_ms,
                        crm_time_t *now, long long *start_delay)
 {
     long long result = 0;
     guint interval_sec = interval_ms / 1000;
     crm_time_t *origin = NULL;
 
     // Ignore unspecified values and non-recurring operations
     if ((value == NULL) || (interval_ms == 0) || (now == NULL)) {
         return false;
     }
 
     // Parse interval origin from text
     origin = crm_time_new(value);
     if (origin == NULL) {
         pcmk__config_err("Ignoring '" XML_OP_ATTR_ORIGIN "' for operation "
                          "'%s' because '%s' is not valid",
                          (ID(xml_obj)? ID(xml_obj) : "(missing ID)"), value);
         return false;
     }
 
     // Get seconds since origin (negative if origin is in the future)
     result = crm_time_get_seconds(now) - crm_time_get_seconds(origin);
     crm_time_free(origin);
 
     // Calculate seconds from closest interval to now
     result = result % interval_sec;
 
     // Calculate seconds remaining until next interval
     result = ((result <= 0)? 0 : interval_sec) - result;
     crm_info("Calculated a start delay of %llds for operation '%s'",
              result,
              (ID(xml_obj)? ID(xml_obj) : "(unspecified)"));
 
     if (start_delay != NULL) {
         *start_delay = result * 1000; // milliseconds
     }
     return true;
 }
 
 static int
 unpack_timeout(const char *value)
 {
     int timeout = crm_get_msec(value);
 
     if (timeout < 0) {
         timeout = crm_get_msec(CRM_DEFAULT_OP_TIMEOUT_S);
     }
     return timeout;
 }
 
 int
 pe_get_configured_timeout(pe_resource_t *rsc, const char *action, pe_working_set_t *data_set)
 {
     xmlNode *child = NULL;
     const char *timeout = NULL;
     int timeout_ms = 0;
 
     pe_rule_eval_data_t rule_data = {
         .node_hash = NULL,
         .role = RSC_ROLE_UNKNOWN,
         .now = data_set->now,
         .match_data = NULL,
         .rsc_data = NULL,
         .op_data = NULL
     };
 
     for (child = first_named_child(rsc->ops_xml, XML_ATTR_OP);
          child != NULL; child = crm_next_same_xml(child)) {
         if (safe_str_eq(action, crm_element_value(child, XML_NVPAIR_ATTR_NAME))) {
             timeout = crm_element_value(child, XML_ATTR_TIMEOUT);
             break;
         }
     }
 
     if (timeout == NULL && data_set->op_defaults) {
         GHashTable *action_meta = crm_str_table_new();
         pe__unpack_dataset_nvpairs(data_set->op_defaults, XML_TAG_META_SETS,
                                    &rule_data, action_meta, NULL, FALSE, data_set);
         timeout = g_hash_table_lookup(action_meta, XML_ATTR_TIMEOUT);
     }
 
     // @TODO check meta-attributes (including versioned meta-attributes)
     // @TODO maybe use min-interval monitor timeout as default for monitors
 
     timeout_ms = crm_get_msec(timeout);
     if (timeout_ms < 0) {
         timeout_ms = crm_get_msec(CRM_DEFAULT_OP_TIMEOUT_S);
     }
     return timeout_ms;
 }
 
 #if ENABLE_VERSIONED_ATTRS
 static void
 unpack_versioned_meta(xmlNode *versioned_meta, xmlNode *xml_obj,
                       guint interval_ms, crm_time_t *now)
 {
     xmlNode *attrs = NULL;
     xmlNode *attr = NULL;
 
     for (attrs = __xml_first_child_element(versioned_meta); attrs != NULL;
          attrs = __xml_next_element(attrs)) {
 
         for (attr = __xml_first_child_element(attrs); attr != NULL;
              attr = __xml_next_element(attr)) {
 
             const char *name = crm_element_value(attr, XML_NVPAIR_ATTR_NAME);
             const char *value = crm_element_value(attr, XML_NVPAIR_ATTR_VALUE);
 
             if (safe_str_eq(name, XML_OP_ATTR_START_DELAY)) {
                 int start_delay = unpack_start_delay(value, NULL);
 
                 crm_xml_add_int(attr, XML_NVPAIR_ATTR_VALUE, start_delay);
             } else if (safe_str_eq(name, XML_OP_ATTR_ORIGIN)) {
                 long long start_delay = 0;
 
                 if (unpack_interval_origin(value, xml_obj, interval_ms, now,
                                            &start_delay)) {
                     crm_xml_add(attr, XML_NVPAIR_ATTR_NAME,
                                 XML_OP_ATTR_START_DELAY);
                     crm_xml_add_ll(attr, XML_NVPAIR_ATTR_VALUE, start_delay);
                 }
             } else if (safe_str_eq(name, XML_ATTR_TIMEOUT)) {
                 int timeout = unpack_timeout(value);
 
                 crm_xml_add_int(attr, XML_NVPAIR_ATTR_VALUE, timeout);
             }
         }
     }
 }
 #endif
 
 /*!
  * \brief Unpack operation XML into an action structure
  *
  * Unpack an operation's meta-attributes (normalizing the interval, timeout,
  * and start delay values as integer milliseconds), requirements, and
  * failure policy.
  *
  * \param[in,out] action      Action to unpack into
  * \param[in]     xml_obj     Operation XML (or NULL if all defaults)
  * \param[in]     container   Resource that contains affected resource, if any
  * \param[in]     data_set    Cluster state
  * \param[in]     interval_ms How frequently to perform the operation
  */
 static void
 unpack_operation(pe_action_t * action, xmlNode * xml_obj, pe_resource_t * container,
                  pe_working_set_t * data_set, guint interval_ms)
 {
     int timeout = 0;
     char *value_ms = NULL;
     const char *value = NULL;
     const char *field = XML_LRM_ATTR_INTERVAL;
     char *default_timeout = NULL;
 #if ENABLE_VERSIONED_ATTRS
     pe_rsc_action_details_t *rsc_details = NULL;
 #endif
 
     pe_rsc_eval_data_t rsc_rule_data = {
         .standard = crm_element_value(action->rsc->xml, XML_AGENT_ATTR_CLASS),
         .provider = crm_element_value(action->rsc->xml, XML_AGENT_ATTR_PROVIDER),
         .agent = crm_element_value(action->rsc->xml, XML_EXPR_ATTR_TYPE)
     };
 
     pe_op_eval_data_t op_rule_data = {
         .op_name = action->task,
         .interval = interval_ms
     };
 
     pe_rule_eval_data_t rule_data = {
         .node_hash = NULL,
         .role = RSC_ROLE_UNKNOWN,
         .now = data_set->now,
         .match_data = NULL,
         .rsc_data = &rsc_rule_data,
         .op_data = &op_rule_data
     };
 
     CRM_CHECK(action && action->rsc, return);
 
     // Cluster-wide <op_defaults> <meta_attributes>
     pe__unpack_dataset_nvpairs(data_set->op_defaults, XML_TAG_META_SETS, &rule_data,
                                action->meta, NULL, FALSE, data_set);
 
     // Probe timeouts default differently, so handle timeout default later
     default_timeout = g_hash_table_lookup(action->meta, XML_ATTR_TIMEOUT);
     if (default_timeout) {
         default_timeout = strdup(default_timeout);
         g_hash_table_remove(action->meta, XML_ATTR_TIMEOUT);
     }
 
     if (xml_obj) {
         xmlAttrPtr xIter = NULL;
 
         // <op> <meta_attributes> take precedence over defaults
         pe__unpack_dataset_nvpairs(xml_obj, XML_TAG_META_SETS, &rule_data,
                                    action->meta, NULL, TRUE, data_set);
 
 #if ENABLE_VERSIONED_ATTRS
         rsc_details = pe_rsc_action_details(action);
 
         pe_eval_versioned_attributes(data_set->input, xml_obj,
                                      XML_TAG_ATTR_SETS, &rule_data,
                                      rsc_details->versioned_parameters,
                                      NULL);
         pe_eval_versioned_attributes(data_set->input, xml_obj,
                                      XML_TAG_META_SETS, &rule_data,
                                      rsc_details->versioned_meta,
                                      NULL);
 #endif
 
         /* Anything set as an <op> XML property has highest precedence.
          * This ensures we use the name and interval from the <op> tag.
          */
         for (xIter = xml_obj->properties; xIter; xIter = xIter->next) {
             const char *prop_name = (const char *)xIter->name;
             const char *prop_value = crm_element_value(xml_obj, prop_name);
 
             g_hash_table_replace(action->meta, strdup(prop_name), strdup(prop_value));
         }
     }
 
     g_hash_table_remove(action->meta, "id");
 
     // Normalize interval to milliseconds
     if (interval_ms > 0) {
         value_ms = crm_strdup_printf("%u", interval_ms);
         g_hash_table_replace(action->meta, strdup(field), value_ms);
 
     } else if (g_hash_table_lookup(action->meta, field) != NULL) {
         g_hash_table_remove(action->meta, field);
     }
 
     // Handle timeout default, now that we know the interval
     if (g_hash_table_lookup(action->meta, XML_ATTR_TIMEOUT)) {
         free(default_timeout);
 
     } else {
         // Probe timeouts default to minimum-interval monitor's
         if (safe_str_eq(action->task, RSC_STATUS) && (interval_ms == 0)) {
 
             xmlNode *min_interval_mon = find_min_interval_mon(action->rsc, FALSE);
 
             if (min_interval_mon) {
                 value = crm_element_value(min_interval_mon, XML_ATTR_TIMEOUT);
                 if (value) {
                     crm_trace("\t%s defaults to minimum-interval monitor's timeout '%s'",
                               action->uuid, value);
                     free(default_timeout);
                     default_timeout = strdup(value);
                 }
             }
         }
 
         if (default_timeout) {
             g_hash_table_insert(action->meta, strdup(XML_ATTR_TIMEOUT),
                                 default_timeout);
         }
     }
 
     if (pcmk__str_none_of(action->task, RSC_START, RSC_PROMOTE, NULL)) {
         action->needs = rsc_req_nothing;
         value = "nothing (not start/promote)";
 
     } else if (is_set(action->rsc->flags, pe_rsc_needs_fencing)) {
         action->needs = rsc_req_stonith;
         value = "fencing (resource)";
 
     } else if (is_set(action->rsc->flags, pe_rsc_needs_quorum)) {
         action->needs = rsc_req_quorum;
         value = "quorum (resource)";
 
     } else {
         action->needs = rsc_req_nothing;
         value = "nothing (resource)";
     }
 
     pe_rsc_trace(action->rsc, "\tAction %s requires: %s", action->uuid, value);
 
     value = unpack_operation_on_fail(action);
 
     if (value == NULL) {
 
     } else if (safe_str_eq(value, "block")) {
         action->on_fail = action_fail_block;
         g_hash_table_insert(action->meta, strdup(XML_OP_ATTR_ON_FAIL), strdup("block"));
         value = "block"; // The above could destroy the original string
 
     } else if (safe_str_eq(value, "fence")) {
         action->on_fail = action_fail_fence;
         value = "node fencing";
 
         if (is_set(data_set->flags, pe_flag_stonith_enabled) == FALSE) {
             pcmk__config_err("Resetting '" XML_OP_ATTR_ON_FAIL "' for "
                              "operation '%s' to 'stop' because 'fence' is not "
                              "valid when fencing is disabled", action->uuid);
             action->on_fail = action_fail_stop;
             action->fail_role = RSC_ROLE_STOPPED;
             value = "stop resource";
         }
 
     } else if (safe_str_eq(value, "standby")) {
         action->on_fail = action_fail_standby;
         value = "node standby";
 
     } else if (pcmk__str_any_of(value, "ignore", "nothing", NULL)) {
         action->on_fail = action_fail_ignore;
         value = "ignore";
 
     } else if (safe_str_eq(value, "migrate")) {
         action->on_fail = action_fail_migrate;
         value = "force migration";
 
     } else if (safe_str_eq(value, "stop")) {
         action->on_fail = action_fail_stop;
         action->fail_role = RSC_ROLE_STOPPED;
         value = "stop resource";
 
     } else if (safe_str_eq(value, "restart")) {
         action->on_fail = action_fail_recover;
         value = "restart (and possibly migrate)";
 
     } else if (safe_str_eq(value, "restart-container")) {
         if (container) {
             action->on_fail = action_fail_restart_container;
             value = "restart container (and possibly migrate)";
 
         } else {
             value = NULL;
         }
 
     } else if (safe_str_eq(value, "demote")) {
         action->on_fail = action_fail_demote;
         value = "demote instance";
 
     } else {
         pe_err("Resource %s: Unknown failure type (%s)", action->rsc->id, value);
         value = NULL;
     }
 
     /* defaults */
     if (value == NULL && container) {
         action->on_fail = action_fail_restart_container;
         value = "restart container (and possibly migrate) (default)";
 
     /* For remote nodes, ensure that any failure that results in dropping an
      * active connection to the node results in fencing of the node.
      *
      * There are only two action failures that don't result in fencing.
      * 1. probes - probe failures are expected.
      * 2. start - a start failure indicates that an active connection does not already
      * exist. The user can set op on-fail=fence if they really want to fence start
      * failures. */
     } else if (((value == NULL) || !is_set(action->rsc->flags, pe_rsc_managed)) &&
                 (pe__resource_is_remote_conn(action->rsc, data_set) &&
                !(safe_str_eq(action->task, CRMD_ACTION_STATUS) && (interval_ms == 0)) &&
                 (safe_str_neq(action->task, CRMD_ACTION_START)))) {
 
         if (!is_set(action->rsc->flags, pe_rsc_managed)) {
             action->on_fail = action_fail_stop;
             action->fail_role = RSC_ROLE_STOPPED;
             value = "stop unmanaged remote node (enforcing default)";
 
         } else {
             if (is_set(data_set->flags, pe_flag_stonith_enabled)) {
                 value = "fence remote node (default)";
             } else {
                 value = "recover remote node connection (default)";
             }
 
             if (action->rsc->remote_reconnect_ms) {
                 action->fail_role = RSC_ROLE_STOPPED;
             }
             action->on_fail = action_fail_reset_remote;
         }
 
     } else if (value == NULL && safe_str_eq(action->task, CRMD_ACTION_STOP)) {
         if (is_set(data_set->flags, pe_flag_stonith_enabled)) {
             action->on_fail = action_fail_fence;
             value = "resource fence (default)";
 
         } else {
             action->on_fail = action_fail_block;
             value = "resource block (default)";
         }
 
     } else if (value == NULL) {
         action->on_fail = action_fail_recover;
         value = "restart (and possibly migrate) (default)";
     }
 
     pe_rsc_trace(action->rsc, "\t%s failure handling: %s", action->task, value);
 
     value = NULL;
     if (xml_obj != NULL) {
         value = g_hash_table_lookup(action->meta, "role_after_failure");
         if (value) {
             pe_warn_once(pe_wo_role_after,
                         "Support for role_after_failure is deprecated and will be removed in a future release");
         }
     }
     if (value != NULL && action->fail_role == RSC_ROLE_UNKNOWN) {
         action->fail_role = text2role(value);
     }
     /* defaults */
     if (action->fail_role == RSC_ROLE_UNKNOWN) {
         if (safe_str_eq(action->task, CRMD_ACTION_PROMOTE)) {
             action->fail_role = RSC_ROLE_SLAVE;
         } else {
             action->fail_role = RSC_ROLE_STARTED;
         }
     }
     pe_rsc_trace(action->rsc, "\t%s failure results in: %s", action->task,
                  role2text(action->fail_role));
 
     value = g_hash_table_lookup(action->meta, XML_OP_ATTR_START_DELAY);
     if (value) {
         unpack_start_delay(value, action->meta);
     } else {
         long long start_delay = 0;
 
         value = g_hash_table_lookup(action->meta, XML_OP_ATTR_ORIGIN);
         if (unpack_interval_origin(value, xml_obj, interval_ms, data_set->now,
                                    &start_delay)) {
             g_hash_table_replace(action->meta, strdup(XML_OP_ATTR_START_DELAY),
                                  crm_strdup_printf("%lld", start_delay));
         }
     }
 
     value = g_hash_table_lookup(action->meta, XML_ATTR_TIMEOUT);
     timeout = unpack_timeout(value);
     g_hash_table_replace(action->meta, strdup(XML_ATTR_TIMEOUT), crm_itoa(timeout));
 
 #if ENABLE_VERSIONED_ATTRS
     unpack_versioned_meta(rsc_details->versioned_meta, xml_obj, interval_ms,
                           data_set->now);
 #endif
 }
 
 static xmlNode *
 find_rsc_op_entry_helper(pe_resource_t * rsc, const char *key, gboolean include_disabled)
 {
     guint interval_ms = 0;
     gboolean do_retry = TRUE;
     char *local_key = NULL;
     const char *name = NULL;
     const char *value = NULL;
     const char *interval_spec = NULL;
     char *match_key = NULL;
     xmlNode *op = NULL;
     xmlNode *operation = NULL;
 
   retry:
     for (operation = __xml_first_child_element(rsc->ops_xml); operation != NULL;
          operation = __xml_next_element(operation)) {
         if (crm_str_eq((const char *)operation->name, "op", TRUE)) {
             name = crm_element_value(operation, "name");
             interval_spec = crm_element_value(operation, XML_LRM_ATTR_INTERVAL);
             value = crm_element_value(operation, "enabled");
             if (!include_disabled && value && crm_is_true(value) == FALSE) {
                 continue;
             }
 
             interval_ms = crm_parse_interval_spec(interval_spec);
             match_key = pcmk__op_key(rsc->id, name, interval_ms);
             if (safe_str_eq(key, match_key)) {
                 op = operation;
             }
             free(match_key);
 
             if (rsc->clone_name) {
                 match_key = pcmk__op_key(rsc->clone_name, name, interval_ms);
                 if (safe_str_eq(key, match_key)) {
                     op = operation;
                 }
                 free(match_key);
             }
 
             if (op != NULL) {
                 free(local_key);
                 return op;
             }
         }
     }
 
     free(local_key);
     if (do_retry == FALSE) {
         return NULL;
     }
 
     do_retry = FALSE;
     if (strstr(key, CRMD_ACTION_MIGRATE) || strstr(key, CRMD_ACTION_MIGRATED)) {
         local_key = pcmk__op_key(rsc->id, "migrate", 0);
         key = local_key;
         goto retry;
 
     } else if (strstr(key, "_notify_")) {
         local_key = pcmk__op_key(rsc->id, "notify", 0);
         key = local_key;
         goto retry;
     }
 
     return NULL;
 }
 
 xmlNode *
 find_rsc_op_entry(pe_resource_t * rsc, const char *key)
 {
     return find_rsc_op_entry_helper(rsc, key, FALSE);
 }
 
 void
 print_node(const char *pre_text, pe_node_t * node, gboolean details)
 {
     if (node == NULL) {
         crm_trace("%s%s: <NULL>", pre_text == NULL ? "" : pre_text, pre_text == NULL ? "" : ": ");
         return;
     }
 
     CRM_ASSERT(node->details);
     crm_trace("%s%s%sNode %s: (weight=%d, fixed=%s)",
               pre_text == NULL ? "" : pre_text,
               pre_text == NULL ? "" : ": ",
               node->details->online ? "" : "Unavailable/Unclean ",
               node->details->uname, node->weight, node->fixed ? "True" : "False");
 
     if (details) {
         int log_level = LOG_TRACE;
 
         char *pe_mutable = strdup("\t\t");
         GListPtr gIter = node->details->running_rsc;
 
         crm_trace("\t\t===Node Attributes");
         g_hash_table_foreach(node->details->attrs, print_str_str, pe_mutable);
         free(pe_mutable);
 
         crm_trace("\t\t=== Resources");
 
         for (; gIter != NULL; gIter = gIter->next) {
             pe_resource_t *rsc = (pe_resource_t *) gIter->data;
 
             rsc->fns->print(rsc, "\t\t", pe_print_log|pe_print_pending,
                             &log_level);
         }
     }
 }
 
 /*
  * Used by the HashTable for-loop
  */
 void
 print_str_str(gpointer key, gpointer value, gpointer user_data)
 {
     crm_trace("%s%s %s ==> %s",
               user_data == NULL ? "" : (char *)user_data,
               user_data == NULL ? "" : ": ", (char *)key, (char *)value);
 }
 
 void
 pe_free_action(pe_action_t * action)
 {
     if (action == NULL) {
         return;
     }
     g_list_free_full(action->actions_before, free);     /* pe_action_wrapper_t* */
     g_list_free_full(action->actions_after, free);      /* pe_action_wrapper_t* */
     if (action->extra) {
         g_hash_table_destroy(action->extra);
     }
     if (action->meta) {
         g_hash_table_destroy(action->meta);
     }
 #if ENABLE_VERSIONED_ATTRS
     if (action->rsc) {
         pe_free_rsc_action_details(action);
     }
 #endif
     free(action->cancel_task);
     free(action->reason);
     free(action->task);
     free(action->uuid);
     free(action->node);
     free(action);
 }
 
 GListPtr
 find_recurring_actions(GListPtr input, pe_node_t * not_on_node)
 {
     const char *value = NULL;
     GListPtr result = NULL;
     GListPtr gIter = input;
 
     CRM_CHECK(input != NULL, return NULL);
 
     for (; gIter != NULL; gIter = gIter->next) {
         pe_action_t *action = (pe_action_t *) gIter->data;
 
         value = g_hash_table_lookup(action->meta, XML_LRM_ATTR_INTERVAL_MS);
         if (value == NULL) {
             /* skip */
         } else if (safe_str_eq(value, "0")) {
             /* skip */
         } else if (safe_str_eq(CRMD_ACTION_CANCEL, action->task)) {
             /* skip */
         } else if (not_on_node == NULL) {
             crm_trace("(null) Found: %s", action->uuid);
             result = g_list_prepend(result, action);
 
         } else if (action->node == NULL) {
             /* skip */
         } else if (action->node->details != not_on_node->details) {
             crm_trace("Found: %s", action->uuid);
             result = g_list_prepend(result, action);
         }
     }
 
     return result;
 }
 
 enum action_tasks
 get_complex_task(pe_resource_t * rsc, const char *name, gboolean allow_non_atomic)
 {
     enum action_tasks task = text2task(name);
 
     if (rsc == NULL) {
         return task;
 
     } else if (allow_non_atomic == FALSE || rsc->variant == pe_native) {
         switch (task) {
             case stopped_rsc:
             case started_rsc:
             case action_demoted:
             case action_promoted:
                 crm_trace("Folding %s back into its atomic counterpart for %s", name, rsc->id);
                 return task - 1;
                 break;
             default:
                 break;
         }
     }
     return task;
 }
 
 pe_action_t *
 find_first_action(GListPtr input, const char *uuid, const char *task, pe_node_t * on_node)
 {
     GListPtr gIter = NULL;
 
     CRM_CHECK(uuid || task, return NULL);
 
     for (gIter = input; gIter != NULL; gIter = gIter->next) {
         pe_action_t *action = (pe_action_t *) gIter->data;
 
         if (uuid != NULL && safe_str_neq(uuid, action->uuid)) {
             continue;
 
         } else if (task != NULL && safe_str_neq(task, action->task)) {
             continue;
 
         } else if (on_node == NULL) {
             return action;
 
         } else if (action->node == NULL) {
             continue;
 
         } else if (on_node->details == action->node->details) {
             return action;
         }
     }
 
     return NULL;
 }
 
 GListPtr
 find_actions(GListPtr input, const char *key, const pe_node_t *on_node)
 {
     GListPtr gIter = input;
     GListPtr result = NULL;
 
     CRM_CHECK(key != NULL, return NULL);
 
     for (; gIter != NULL; gIter = gIter->next) {
         pe_action_t *action = (pe_action_t *) gIter->data;
 
         if (safe_str_neq(key, action->uuid)) {
             crm_trace("%s does not match action %s", key, action->uuid);
             continue;
 
         } else if (on_node == NULL) {
             crm_trace("Action %s matches (ignoring node)", key);
             result = g_list_prepend(result, action);
 
         } else if (action->node == NULL) {
             crm_trace("Action %s matches (unallocated, assigning to %s)",
                       key, on_node->details->uname);
 
             action->node = pe__copy_node(on_node);
             result = g_list_prepend(result, action);
 
         } else if (on_node->details == action->node->details) {
             crm_trace("Action %s on %s matches", key, on_node->details->uname);
             result = g_list_prepend(result, action);
 
         } else {
             crm_trace("Action %s on node %s does not match requested node %s",
                       key, action->node->details->uname,
                       on_node->details->uname);
         }
     }
 
     return result;
 }
 
 GList *
 find_actions_exact(GList *input, const char *key, const pe_node_t *on_node)
 {
     GList *result = NULL;
 
     CRM_CHECK(key != NULL, return NULL);
 
     if (on_node == NULL) {
         crm_trace("Not searching for action %s because node not specified",
                   key);
         return NULL;
     }
 
     for (GList *gIter = input; gIter != NULL; gIter = gIter->next) {
         pe_action_t *action = (pe_action_t *) gIter->data;
 
         if (action->node == NULL) {
             crm_trace("Skipping comparison of %s vs action %s without node",
                       key, action->uuid);
 
         } else if (safe_str_neq(key, action->uuid)) {
             crm_trace("Desired action %s doesn't match %s", key, action->uuid);
 
         } else if (safe_str_neq(on_node->details->id,
                                 action->node->details->id)) {
             crm_trace("Action %s desired node ID %s doesn't match %s",
                       key, on_node->details->id, action->node->details->id);
 
         } else {
             crm_trace("Action %s matches", key);
             result = g_list_prepend(result, action);
         }
     }
 
     return result;
 }
 
 /*!
  * \brief Find all actions of given type for a resource
  *
  * \param[in] rsc           Resource to search
  * \param[in] node          Find only actions scheduled on this node
  * \param[in] task          Action name to search for
  * \param[in] require_node  If TRUE, NULL node or action node will not match
  *
  * \return List of actions found (or NULL if none)
  * \note If node is not NULL and require_node is FALSE, matching actions
  *       without a node will be assigned to node.
  */
 GList *
 pe__resource_actions(const pe_resource_t *rsc, const pe_node_t *node,
                      const char *task, bool require_node)
 {
     GList *result = NULL;
     char *key = pcmk__op_key(rsc->id, task, 0);
 
     if (require_node) {
         result = find_actions_exact(rsc->actions, key, node);
     } else {
         result = find_actions(rsc->actions, key, node);
     }
     free(key);
     return result;
 }
 
 static void
 resource_node_score(pe_resource_t * rsc, pe_node_t * node, int score, const char *tag)
 {
     pe_node_t *match = NULL;
 
     if ((rsc->exclusive_discover || (node->rsc_discover_mode == pe_discover_never))
         && safe_str_eq(tag, "symmetric_default")) {
         /* This string comparision may be fragile, but exclusive resources and
          * exclusive nodes should not have the symmetric_default constraint
          * applied to them.
          */
         return;
 
     } else if (rsc->children) {
         GListPtr gIter = rsc->children;
 
         for (; gIter != NULL; gIter = gIter->next) {
             pe_resource_t *child_rsc = (pe_resource_t *) gIter->data;
 
             resource_node_score(child_rsc, node, score, tag);
         }
     }
 
     pe_rsc_trace(rsc, "Setting %s for %s on %s: %d", tag, rsc->id, node->details->uname, score);
     match = pe_hash_table_lookup(rsc->allowed_nodes, node->details->id);
     if (match == NULL) {
         match = pe__copy_node(node);
         g_hash_table_insert(rsc->allowed_nodes, (gpointer) match->details->id, match);
     }
     match->weight = pe__add_scores(match->weight, score);
 }
 
 void
 resource_location(pe_resource_t * rsc, pe_node_t * node, int score, const char *tag,
                   pe_working_set_t * data_set)
 {
     if (node != NULL) {
         resource_node_score(rsc, node, score, tag);
 
     } else if (data_set != NULL) {
         GListPtr gIter = data_set->nodes;
 
         for (; gIter != NULL; gIter = gIter->next) {
             pe_node_t *node_iter = (pe_node_t *) gIter->data;
 
             resource_node_score(rsc, node_iter, score, tag);
         }
 
     } else {
         GHashTableIter iter;
         pe_node_t *node_iter = NULL;
 
         g_hash_table_iter_init(&iter, rsc->allowed_nodes);
         while (g_hash_table_iter_next(&iter, NULL, (void **)&node_iter)) {
             resource_node_score(rsc, node_iter, score, tag);
         }
     }
 
     if (node == NULL && score == -INFINITY) {
         if (rsc->allocated_to) {
             crm_info("Deallocating %s from %s", rsc->id, rsc->allocated_to->details->uname);
             free(rsc->allocated_to);
             rsc->allocated_to = NULL;
         }
     }
 }
 
 #define sort_return(an_int, why) do {					\
 	free(a_uuid);						\
 	free(b_uuid);						\
 	crm_trace("%s (%d) %c %s (%d) : %s",				\
 		  a_xml_id, a_call_id, an_int>0?'>':an_int<0?'<':'=',	\
 		  b_xml_id, b_call_id, why);				\
 	return an_int;							\
     } while(0)
 
 gint
 sort_op_by_callid(gconstpointer a, gconstpointer b)
 {
     int a_call_id = -1;
     int b_call_id = -1;
 
     char *a_uuid = NULL;
     char *b_uuid = NULL;
 
     const xmlNode *xml_a = a;
     const xmlNode *xml_b = b;
 
     const char *a_xml_id = crm_element_value(xml_a, XML_ATTR_ID);
     const char *b_xml_id = crm_element_value(xml_b, XML_ATTR_ID);
 
     if (safe_str_eq(a_xml_id, b_xml_id)) {
         /* We have duplicate lrm_rsc_op entries in the status
          * section which is unlikely to be a good thing
          *    - we can handle it easily enough, but we need to get
          *    to the bottom of why it's happening.
          */
         pe_err("Duplicate lrm_rsc_op entries named %s", a_xml_id);
         sort_return(0, "duplicate");
     }
 
     crm_element_value_int(xml_a, XML_LRM_ATTR_CALLID, &a_call_id);
     crm_element_value_int(xml_b, XML_LRM_ATTR_CALLID, &b_call_id);
 
     if (a_call_id == -1 && b_call_id == -1) {
         /* both are pending ops so it doesn't matter since
          *   stops are never pending
          */
         sort_return(0, "pending");
 
     } else if (a_call_id >= 0 && a_call_id < b_call_id) {
         sort_return(-1, "call id");
 
     } else if (b_call_id >= 0 && a_call_id > b_call_id) {
         sort_return(1, "call id");
 
     } else if (b_call_id >= 0 && a_call_id == b_call_id) {
         /*
          * The op and last_failed_op are the same
          * Order on last-rc-change
          */
         time_t last_a = -1;
         time_t last_b = -1;
 
         crm_element_value_epoch(xml_a, XML_RSC_OP_LAST_CHANGE, &last_a);
         crm_element_value_epoch(xml_b, XML_RSC_OP_LAST_CHANGE, &last_b);
 
         crm_trace("rc-change: %lld vs %lld",
                   (long long) last_a, (long long) last_b);
         if (last_a >= 0 && last_a < last_b) {
             sort_return(-1, "rc-change");
 
         } else if (last_b >= 0 && last_a > last_b) {
             sort_return(1, "rc-change");
         }
         sort_return(0, "rc-change");
 
     } else {
         /* One of the inputs is a pending operation
          * Attempt to use XML_ATTR_TRANSITION_MAGIC to determine its age relative to the other
          */
 
         int a_id = -1;
         int b_id = -1;
 
         const char *a_magic = crm_element_value(xml_a, XML_ATTR_TRANSITION_MAGIC);
         const char *b_magic = crm_element_value(xml_b, XML_ATTR_TRANSITION_MAGIC);
 
         CRM_CHECK(a_magic != NULL && b_magic != NULL, sort_return(0, "No magic"));
         if (!decode_transition_magic(a_magic, &a_uuid, &a_id, NULL, NULL, NULL,
                                      NULL)) {
             sort_return(0, "bad magic a");
         }
         if (!decode_transition_magic(b_magic, &b_uuid, &b_id, NULL, NULL, NULL,
                                      NULL)) {
             sort_return(0, "bad magic b");
         }
         /* try to determine the relative age of the operation...
          * some pending operations (e.g. a start) may have been superseded
          *   by a subsequent stop
          *
          * [a|b]_id == -1 means it's a shutdown operation and _always_ comes last
          */
         if (safe_str_neq(a_uuid, b_uuid) || a_id == b_id) {
             /*
              * some of the logic in here may be redundant...
              *
              * if the UUID from the TE doesn't match then one better
              *   be a pending operation.
              * pending operations don't survive between elections and joins
              *   because we query the LRM directly
              */
 
             if (b_call_id == -1) {
                 sort_return(-1, "transition + call");
 
             } else if (a_call_id == -1) {
                 sort_return(1, "transition + call");
             }
 
         } else if ((a_id >= 0 && a_id < b_id) || b_id == -1) {
             sort_return(-1, "transition");
 
         } else if ((b_id >= 0 && a_id > b_id) || a_id == -1) {
             sort_return(1, "transition");
         }
     }
 
     /* we should never end up here */
     CRM_CHECK(FALSE, sort_return(0, "default"));
 
 }
 
 time_t
 get_effective_time(pe_working_set_t * data_set)
 {
     if(data_set) {
         if (data_set->now == NULL) {
             crm_trace("Recording a new 'now'");
             data_set->now = crm_time_new(NULL);
         }
         return crm_time_get_seconds_since_epoch(data_set->now);
     }
 
     crm_trace("Defaulting to 'now'");
     return time(NULL);
 }
 
 gboolean
 get_target_role(pe_resource_t * rsc, enum rsc_role_e * role)
 {
     enum rsc_role_e local_role = RSC_ROLE_UNKNOWN;
     const char *value = g_hash_table_lookup(rsc->meta, XML_RSC_ATTR_TARGET_ROLE);
 
     CRM_CHECK(role != NULL, return FALSE);
 
     if (value == NULL || safe_str_eq("started", value)
         || safe_str_eq("default", value)) {
         return FALSE;
     }
 
     local_role = text2role(value);
     if (local_role == RSC_ROLE_UNKNOWN) {
         pcmk__config_err("Ignoring '" XML_RSC_ATTR_TARGET_ROLE "' for %s "
                          "because '%s' is not valid", rsc->id, value);
         return FALSE;
 
     } else if (local_role > RSC_ROLE_STARTED) {
         if (is_set(uber_parent(rsc)->flags, pe_rsc_promotable)) {
             if (local_role > RSC_ROLE_SLAVE) {
                 /* This is what we'd do anyway, just leave the default to avoid messing up the placement algorithm */
                 return FALSE;
             }
 
         } else {
             pcmk__config_err("Ignoring '" XML_RSC_ATTR_TARGET_ROLE "' for %s "
                              "because '%s' only makes sense for promotable "
                              "clones", rsc->id, value);
             return FALSE;
         }
     }
 
     *role = local_role;
     return TRUE;
 }
 
 gboolean
 order_actions(pe_action_t * lh_action, pe_action_t * rh_action, enum pe_ordering order)
 {
     GListPtr gIter = NULL;
     pe_action_wrapper_t *wrapper = NULL;
     GListPtr list = NULL;
 
     if (order == pe_order_none) {
         return FALSE;
     }
 
     if (lh_action == NULL || rh_action == NULL) {
         return FALSE;
     }
 
     crm_trace("Ordering Action %s before %s", lh_action->uuid, rh_action->uuid);
 
     /* Ensure we never create a dependency on ourselves... it's happened */
     CRM_ASSERT(lh_action != rh_action);
 
     /* Filter dups, otherwise update_action_states() has too much work to do */
     gIter = lh_action->actions_after;
     for (; gIter != NULL; gIter = gIter->next) {
         pe_action_wrapper_t *after = (pe_action_wrapper_t *) gIter->data;
 
         if (after->action == rh_action && (after->type & order)) {
             return FALSE;
         }
     }
 
     wrapper = calloc(1, sizeof(pe_action_wrapper_t));
     wrapper->action = rh_action;
     wrapper->type = order;
 
     list = lh_action->actions_after;
     list = g_list_prepend(list, wrapper);
     lh_action->actions_after = list;
 
     wrapper = NULL;
 
 /* 	order |= pe_order_implies_then; */
 /* 	order ^= pe_order_implies_then; */
 
     wrapper = calloc(1, sizeof(pe_action_wrapper_t));
     wrapper->action = lh_action;
     wrapper->type = order;
     list = rh_action->actions_before;
     list = g_list_prepend(list, wrapper);
     rh_action->actions_before = list;
     return TRUE;
 }
 
 pe_action_t *
 get_pseudo_op(const char *name, pe_working_set_t * data_set)
 {
     pe_action_t *op = NULL;
 
     if(data_set->singletons) {
         op = g_hash_table_lookup(data_set->singletons, name);
     }
     if (op == NULL) {
         op = custom_action(NULL, strdup(name), name, NULL, TRUE, TRUE, data_set);
         set_bit(op->flags, pe_action_pseudo);
         set_bit(op->flags, pe_action_runnable);
     }
 
     return op;
 }
 
 void
 destroy_ticket(gpointer data)
 {
     pe_ticket_t *ticket = data;
 
     if (ticket->state) {
         g_hash_table_destroy(ticket->state);
     }
     free(ticket->id);
     free(ticket);
 }
 
 pe_ticket_t *
 ticket_new(const char *ticket_id, pe_working_set_t * data_set)
 {
     pe_ticket_t *ticket = NULL;
 
     if (ticket_id == NULL || strlen(ticket_id) == 0) {
         return NULL;
     }
 
     if (data_set->tickets == NULL) {
         data_set->tickets =
             g_hash_table_new_full(crm_str_hash, g_str_equal, free,
                                   destroy_ticket);
     }
 
     ticket = g_hash_table_lookup(data_set->tickets, ticket_id);
     if (ticket == NULL) {
 
         ticket = calloc(1, sizeof(pe_ticket_t));
         if (ticket == NULL) {
             crm_err("Cannot allocate ticket '%s'", ticket_id);
             return NULL;
         }
 
         crm_trace("Creaing ticket entry for %s", ticket_id);
 
         ticket->id = strdup(ticket_id);
         ticket->granted = FALSE;
         ticket->last_granted = -1;
         ticket->standby = FALSE;
         ticket->state = crm_str_table_new();
 
         g_hash_table_insert(data_set->tickets, strdup(ticket->id), ticket);
     }
 
     return ticket;
 }
 
 static void
 filter_parameters(xmlNode * param_set, const char *param_string, bool need_present)
 {
     if (param_set && param_string) {
         xmlAttrPtr xIter = param_set->properties;
 
         while (xIter) {
             const char *prop_name = (const char *)xIter->name;
             char *name = crm_strdup_printf(" %s ", prop_name);
             char *match = strstr(param_string, name);
 
             free(name);
 
             //  Do now, because current entry might get removed below
             xIter = xIter->next;
 
             if (need_present && match == NULL) {
                 crm_trace("%s not found in %s", prop_name, param_string);
                 xml_remove_prop(param_set, prop_name);
 
             } else if (need_present == FALSE && match) {
                 crm_trace("%s found in %s", prop_name, param_string);
                 xml_remove_prop(param_set, prop_name);
             }
         }
     }
 }
 
 #if ENABLE_VERSIONED_ATTRS
 static void
 append_versioned_params(xmlNode *versioned_params, const char *ra_version, xmlNode *params)
 {
     GHashTable *hash = pe_unpack_versioned_parameters(versioned_params, ra_version);
     char *key = NULL;
     char *value = NULL;
     GHashTableIter iter;
 
     g_hash_table_iter_init(&iter, hash);
     while (g_hash_table_iter_next(&iter, (gpointer *) &key, (gpointer *) &value)) {
         crm_xml_add(params, key, value);
     }
     g_hash_table_destroy(hash);
 }
 #endif
 
 /*!
  * \internal
  * \brief Calculate action digests and store in node's digest cache
  *
  * \param[in] rsc          Resource that action was for
  * \param[in] task         Name of action performed
  * \param[in] key          Action's task key
  * \param[in] node         Node action was performed on
  * \param[in] xml_op       XML of operation in CIB status (if available)
  * \param[in] calc_secure  Whether to calculate secure digest
  * \param[in] data_set     Cluster working set
  *
  * \return Pointer to node's digest cache entry
  */
 static op_digest_cache_t *
 rsc_action_digest(pe_resource_t *rsc, const char *task, const char *key,
                   pe_node_t *node, xmlNode *xml_op, bool calc_secure,
                   pe_working_set_t *data_set)
 {
     op_digest_cache_t *data = NULL;
 
     data = g_hash_table_lookup(node->details->digest_cache, key);
     if (data == NULL) {
         GHashTable *local_rsc_params = crm_str_table_new();
         pe_action_t *action = custom_action(rsc, strdup(key), task, node, TRUE, FALSE, data_set);
 #if ENABLE_VERSIONED_ATTRS
         xmlNode *local_versioned_params = create_xml_node(NULL, XML_TAG_RSC_VER_ATTRS);
         const char *ra_version = NULL;
 #endif
 
         const char *op_version;
         const char *restart_list = NULL;
         const char *secure_list = " passwd password ";
 
         data = calloc(1, sizeof(op_digest_cache_t));
         CRM_ASSERT(data != NULL);
 
         get_rsc_attributes(local_rsc_params, rsc, node, data_set);
 #if ENABLE_VERSIONED_ATTRS
         pe_get_versioned_attributes(local_versioned_params, rsc, node, data_set);
 #endif
 
         data->params_all = create_xml_node(NULL, XML_TAG_PARAMS);
 
         // REMOTE_CONTAINER_HACK: Allow remote nodes that start containers with pacemaker remote inside
         if (pe__add_bundle_remote_name(rsc, data->params_all,
                                        XML_RSC_ATTR_REMOTE_RA_ADDR)) {
             crm_trace("Set address for bundle connection %s (on %s)",
                       rsc->id, node->details->uname);
         }
 
         g_hash_table_foreach(local_rsc_params, hash2field, data->params_all);
         g_hash_table_foreach(action->extra, hash2field, data->params_all);
         g_hash_table_foreach(rsc->parameters, hash2field, data->params_all);
         g_hash_table_foreach(action->meta, hash2metafield, data->params_all);
 
         if(xml_op) {
             secure_list = crm_element_value(xml_op, XML_LRM_ATTR_OP_SECURE);
             restart_list = crm_element_value(xml_op, XML_LRM_ATTR_OP_RESTART);
 
             op_version = crm_element_value(xml_op, XML_ATTR_CRM_VERSION);
 #if ENABLE_VERSIONED_ATTRS
             ra_version = crm_element_value(xml_op, XML_ATTR_RA_VERSION);
 #endif
 
         } else {
             op_version = CRM_FEATURE_SET;
         }
 
 #if ENABLE_VERSIONED_ATTRS
         append_versioned_params(local_versioned_params, ra_version, data->params_all);
         append_versioned_params(rsc->versioned_parameters, ra_version, data->params_all);
 
         {
             pe_rsc_action_details_t *details = pe_rsc_action_details(action);
             append_versioned_params(details->versioned_parameters, ra_version, data->params_all);
         }
 #endif
 
         pcmk__filter_op_for_digest(data->params_all);
 
         g_hash_table_destroy(local_rsc_params);
         pe_free_action(action);
 
         data->digest_all_calc = calculate_operation_digest(data->params_all, op_version);
 
         if (calc_secure) {
             data->params_secure = copy_xml(data->params_all);
             if(secure_list) {
                 filter_parameters(data->params_secure, secure_list, FALSE);
             }
             data->digest_secure_calc = calculate_operation_digest(data->params_secure, op_version);
         }
 
         if(xml_op && crm_element_value(xml_op, XML_LRM_ATTR_RESTART_DIGEST) != NULL) {
             data->params_restart = copy_xml(data->params_all);
             if (restart_list) {
                 filter_parameters(data->params_restart, restart_list, TRUE);
             }
             data->digest_restart_calc = calculate_operation_digest(data->params_restart, op_version);
         }
 
         g_hash_table_insert(node->details->digest_cache, strdup(key), data);
     }
 
     return data;
 }
 
 op_digest_cache_t *
 rsc_action_digest_cmp(pe_resource_t * rsc, xmlNode * xml_op, pe_node_t * node,
                       pe_working_set_t * data_set)
 {
     op_digest_cache_t *data = NULL;
 
     char *key = NULL;
     guint interval_ms = 0;
 
     const char *op_version;
     const char *task = crm_element_value(xml_op, XML_LRM_ATTR_TASK);
     const char *digest_all;
     const char *digest_restart;
 
     CRM_ASSERT(node != NULL);
 
     op_version = crm_element_value(xml_op, XML_ATTR_CRM_VERSION);
     digest_all = crm_element_value(xml_op, XML_LRM_ATTR_OP_DIGEST);
     digest_restart = crm_element_value(xml_op, XML_LRM_ATTR_RESTART_DIGEST);
 
     crm_element_value_ms(xml_op, XML_LRM_ATTR_INTERVAL_MS, &interval_ms);
     key = pcmk__op_key(rsc->id, task, interval_ms);
     data = rsc_action_digest(rsc, task, key, node, xml_op,
                              is_set(data_set->flags, pe_flag_sanitized),
                              data_set);
 
     data->rc = RSC_DIGEST_MATCH;
     if (digest_restart && data->digest_restart_calc && strcmp(data->digest_restart_calc, digest_restart) != 0) {
         pe_rsc_info(rsc, "Parameters to %s on %s changed: was %s vs. now %s (restart:%s) %s",
                  key, node->details->uname,
                  crm_str(digest_restart), data->digest_restart_calc,
                  op_version, crm_element_value(xml_op, XML_ATTR_TRANSITION_MAGIC));
         data->rc = RSC_DIGEST_RESTART;
 
     } else if (digest_all == NULL) {
         /* it is unknown what the previous op digest was */
         data->rc = RSC_DIGEST_UNKNOWN;
 
     } else if (strcmp(digest_all, data->digest_all_calc) != 0) {
         pe_rsc_info(rsc, "Parameters to %s on %s changed: was %s vs. now %s (%s:%s) %s",
                  key, node->details->uname,
                  crm_str(digest_all), data->digest_all_calc,
                  (interval_ms > 0)? "reschedule" : "reload",
                  op_version, crm_element_value(xml_op, XML_ATTR_TRANSITION_MAGIC));
         data->rc = RSC_DIGEST_ALL;
     }
 
     free(key);
     return data;
 }
 
 /*!
  * \internal
  * \brief Create an unfencing summary for use in special node attribute
  *
  * Create a string combining a fence device's resource ID, agent type, and
  * parameter digest (whether for all parameters or just non-private parameters).
  * This can be stored in a special node attribute, allowing us to detect changes
  * in either the agent type or parameters, to know whether unfencing must be
  * redone or can be safely skipped when the device's history is cleaned.
  *
  * \param[in] rsc_id        Fence device resource ID
  * \param[in] agent_type    Fence device agent
  * \param[in] param_digest  Fence device parameter digest
  *
  * \return Newly allocated string with unfencing digest
  * \note The caller is responsible for freeing the result.
  */
 static inline char *
 create_unfencing_summary(const char *rsc_id, const char *agent_type,
                          const char *param_digest)
 {
     return crm_strdup_printf("%s:%s:%s", rsc_id, agent_type, param_digest);
 }
 
 /*!
  * \internal
  * \brief Check whether a node can skip unfencing
  *
  * Check whether a fence device's current definition matches a node's
  * stored summary of when it was last unfenced by the device.
  *
  * \param[in] rsc_id        Fence device's resource ID
  * \param[in] agent         Fence device's agent type
  * \param[in] digest_calc   Fence device's current parameter digest
  * \param[in] node_summary  Value of node's special unfencing node attribute
  *                          (a comma-separated list of unfencing summaries for
  *                          all devices that have unfenced this node)
  *
  * \return TRUE if digest matches, FALSE otherwise
  */
 static bool
 unfencing_digest_matches(const char *rsc_id, const char *agent,
                          const char *digest_calc, const char *node_summary)
 {
     bool matches = FALSE;
 
     if (rsc_id && agent && digest_calc && node_summary) {
         char *search_secure = create_unfencing_summary(rsc_id, agent,
                                                        digest_calc);
 
         /* The digest was calculated including the device ID and agent,
          * so there is no risk of collision using strstr().
          */
         matches = (strstr(node_summary, search_secure) != NULL);
         crm_trace("Calculated unfencing digest '%s' %sfound in '%s'",
                   search_secure, matches? "" : "not ", node_summary);
         free(search_secure);
     }
     return matches;
 }
 
 /* Magic string to use as action name for digest cache entries used for
  * unfencing checks. This is not a real action name (i.e. "on"), so
  * check_action_definition() won't confuse these entries with real actions.
  */
 #define STONITH_DIGEST_TASK "stonith-on"
 
 /*!
  * \internal
  * \brief Calculate fence device digests and digest comparison result
  *
  * \param[in] rsc       Fence device resource
  * \param[in] agent     Fence device's agent type
  * \param[in] node      Node with digest cache to use
  * \param[in] data_set  Cluster working set
  *
  * \return Node's digest cache entry
  */
 static op_digest_cache_t *
 fencing_action_digest_cmp(pe_resource_t *rsc, const char *agent,
                           pe_node_t *node, pe_working_set_t *data_set)
 {
     const char *node_summary = NULL;
 
     // Calculate device's current parameter digests
     char *key = pcmk__op_key(rsc->id, STONITH_DIGEST_TASK, 0);
     op_digest_cache_t *data = rsc_action_digest(rsc, STONITH_DIGEST_TASK, key,
                                                 node, NULL, TRUE, data_set);
 
     free(key);
 
     // Check whether node has special unfencing summary node attribute
     node_summary = pe_node_attribute_raw(node, CRM_ATTR_DIGESTS_ALL);
     if (node_summary == NULL) {
         data->rc = RSC_DIGEST_UNKNOWN;
         return data;
     }
 
     // Check whether full parameter digest matches
     if (unfencing_digest_matches(rsc->id, agent, data->digest_all_calc,
                                  node_summary)) {
         data->rc = RSC_DIGEST_MATCH;
         return data;
     }
 
     // Check whether secure parameter digest matches
     node_summary = pe_node_attribute_raw(node, CRM_ATTR_DIGESTS_SECURE);
     if (unfencing_digest_matches(rsc->id, agent, data->digest_secure_calc,
                                  node_summary)) {
         data->rc = RSC_DIGEST_MATCH;
         if (is_set(data_set->flags, pe_flag_stdout)) {
             printf("Only 'private' parameters to %s for unfencing %s changed\n",
                    rsc->id, node->details->uname);
         }
         return data;
     }
 
     // Parameters don't match
     data->rc = RSC_DIGEST_ALL;
     if (is_set(data_set->flags, (pe_flag_sanitized|pe_flag_stdout))
         && data->digest_secure_calc) {
         char *digest = create_unfencing_summary(rsc->id, agent,
                                                 data->digest_secure_calc);
 
         printf("Parameters to %s for unfencing %s changed, try '%s'\n",
                rsc->id, node->details->uname, digest);
         free(digest);
     }
     return data;
 }
 
 const char *rsc_printable_id(pe_resource_t *rsc)
 {
     if (is_not_set(rsc->flags, pe_rsc_unique)) {
         return ID(rsc->xml);
     }
     return rsc->id;
 }
 
 void
 clear_bit_recursive(pe_resource_t * rsc, unsigned long long flag)
 {
     GListPtr gIter = rsc->children;
 
     clear_bit(rsc->flags, flag);
     for (; gIter != NULL; gIter = gIter->next) {
         pe_resource_t *child_rsc = (pe_resource_t *) gIter->data;
 
         clear_bit_recursive(child_rsc, flag);
     }
 }
 
 void
 set_bit_recursive(pe_resource_t * rsc, unsigned long long flag)
 {
     GListPtr gIter = rsc->children;
 
     set_bit(rsc->flags, flag);
     for (; gIter != NULL; gIter = gIter->next) {
         pe_resource_t *child_rsc = (pe_resource_t *) gIter->data;
 
         set_bit_recursive(child_rsc, flag);
     }
 }
 
 static GListPtr
 find_unfencing_devices(GListPtr candidates, GListPtr matches) 
 {
     for (GListPtr gIter = candidates; gIter != NULL; gIter = gIter->next) {
         pe_resource_t *candidate = gIter->data;
         const char *provides = g_hash_table_lookup(candidate->meta, XML_RSC_ATTR_PROVIDES);
         const char *requires = g_hash_table_lookup(candidate->meta, XML_RSC_ATTR_REQUIRES);
 
         if(candidate->children) {
             matches = find_unfencing_devices(candidate->children, matches);
         } else if (is_not_set(candidate->flags, pe_rsc_fence_device)) {
             continue;
 
         } else if (crm_str_eq(provides, "unfencing", FALSE) || crm_str_eq(requires, "unfencing", FALSE)) {
             matches = g_list_prepend(matches, candidate);
         }
     }
     return matches;
 }
 
 static int
 node_priority_fencing_delay(pe_node_t * node, pe_working_set_t * data_set)
 {
     int member_count = 0;
     int online_count = 0;
     int top_priority = 0;
     int lowest_priority = 0;
     GListPtr gIter = NULL;
 
     // `priority-fencing-delay` is disabled
     if (data_set->priority_fencing_delay <= 0) {
         return 0;
     }
 
     /* No need to request a delay if the fencing target is not a normal cluster
      * member, for example if it's a remote node or a guest node. */
     if (node->details->type != node_member) {
         return 0;
     }
 
     // No need to request a delay if the fencing target is in our partition
     if (node->details->online) {
         return 0;
     }
 
     for (gIter = data_set->nodes; gIter != NULL; gIter = gIter->next) {
         pe_node_t *n =  gIter->data;
 
         if (n->details->type != node_member) {
             continue;
         }
 
         member_count ++;
 
         if (n->details->online) {
             online_count++;
         }
 
         if (member_count == 1
             || n->details->priority > top_priority) {
             top_priority = n->details->priority;
         }
 
         if (member_count == 1
             || n->details->priority < lowest_priority) {
             lowest_priority = n->details->priority;
         }
     }
 
     // No need to delay if we have more than half of the cluster members
     if (online_count > member_count / 2) {
         return 0;
     }
 
     /* All the nodes have equal priority.
      * Any configured corresponding `pcmk_delay_base/max` will be applied. */
     if (lowest_priority == top_priority) {
         return 0;
     }
 
     if (node->details->priority < top_priority) {
         return 0;
     }
 
     return data_set->priority_fencing_delay;
 }
 
 pe_action_t *
 pe_fence_op(pe_node_t * node, const char *op, bool optional, const char *reason,
             bool priority_delay, pe_working_set_t * data_set)
 {
     char *op_key = NULL;
     pe_action_t *stonith_op = NULL;
 
     if(op == NULL) {
         op = data_set->stonith_action;
     }
 
     op_key = crm_strdup_printf("%s-%s-%s", CRM_OP_FENCE, node->details->uname, op);
 
     if(data_set->singletons) {
         stonith_op = g_hash_table_lookup(data_set->singletons, op_key);
     }
 
     if(stonith_op == NULL) {
         stonith_op = custom_action(NULL, op_key, CRM_OP_FENCE, node, TRUE, TRUE, data_set);
 
         add_hash_param(stonith_op->meta, XML_LRM_ATTR_TARGET, node->details->uname);
         add_hash_param(stonith_op->meta, XML_LRM_ATTR_TARGET_UUID, node->details->id);
         add_hash_param(stonith_op->meta, "stonith_action", op);
 
         if (pe__is_guest_or_remote_node(node)
             && is_set(data_set->flags, pe_flag_enable_unfencing)) {
             /* Extra work to detect device changes on remotes
              *
              * We may do this for all nodes in the future, but for now
              * the check_action_definition() based stuff works fine.
              */
             long max = 1024;
             long digests_all_offset = 0;
             long digests_secure_offset = 0;
 
             char *digests_all = calloc(max, sizeof(char));
             char *digests_secure = calloc(max, sizeof(char));
             GListPtr matches = find_unfencing_devices(data_set->resources, NULL);
 
             for (GListPtr gIter = matches; gIter != NULL; gIter = gIter->next) {
                 pe_resource_t *match = gIter->data;
                 const char *agent = g_hash_table_lookup(match->meta,
                                                         XML_ATTR_TYPE);
                 op_digest_cache_t *data = NULL;
 
                 data = fencing_action_digest_cmp(match, agent, node, data_set);
                 if(data->rc == RSC_DIGEST_ALL) {
                     optional = FALSE;
                     crm_notice("Unfencing %s (remote): because the definition of %s changed", node->details->uname, match->id);
                     if (is_set(data_set->flags, pe_flag_stdout)) {
                         fprintf(stdout, "  notice: Unfencing %s (remote): because the definition of %s changed\n", node->details->uname, match->id);
                     }
                 }
 
                 digests_all_offset += snprintf(
                     digests_all+digests_all_offset, max-digests_all_offset,
                     "%s:%s:%s,", match->id, agent, data->digest_all_calc);
 
                 digests_secure_offset += snprintf(
                     digests_secure+digests_secure_offset, max-digests_secure_offset,
                     "%s:%s:%s,", match->id, agent, data->digest_secure_calc);
             }
             g_hash_table_insert(stonith_op->meta,
                                 strdup(XML_OP_ATTR_DIGESTS_ALL),
                                 digests_all);
             g_hash_table_insert(stonith_op->meta,
                                 strdup(XML_OP_ATTR_DIGESTS_SECURE),
                                 digests_secure);
         }
 
     } else {
         free(op_key);
     }
 
     if (data_set->priority_fencing_delay > 0
 
             /* It's a suitable case where `priority-fencing-delay` applies.
              * At least add `priority-fencing-delay` field as an indicator. */
         && (priority_delay
 
             /* Re-calculate priority delay for the suitable case when
              * pe_fence_op() is called again by stage6() after node priority has
              * been actually calculated with native_add_running() */
             || g_hash_table_lookup(stonith_op->meta,
                                    XML_CONFIG_ATTR_PRIORITY_FENCING_DELAY) != NULL)) {
 
             /* Add `priority-fencing-delay` to the fencing op even if it's 0 for
              * the targeting node. So that it takes precedence over any possible
              * `pcmk_delay_base/max`.
              */
             char *delay_s = crm_itoa(node_priority_fencing_delay(node, data_set));
 
             g_hash_table_insert(stonith_op->meta,
                                 strdup(XML_CONFIG_ATTR_PRIORITY_FENCING_DELAY),
                                 delay_s);
     }
 
     if(optional == FALSE && pe_can_fence(data_set, node)) {
         pe_action_required(stonith_op, NULL, reason);
     } else if(reason && stonith_op->reason == NULL) {
         stonith_op->reason = strdup(reason);
     }
 
     return stonith_op;
 }
 
 void
 trigger_unfencing(
     pe_resource_t * rsc, pe_node_t *node, const char *reason, pe_action_t *dependency, pe_working_set_t * data_set) 
 {
     if(is_not_set(data_set->flags, pe_flag_enable_unfencing)) {
         /* No resources require it */
         return;
 
     } else if (rsc != NULL && is_not_set(rsc->flags, pe_rsc_fence_device)) {
         /* Wasn't a stonith device */
         return;
 
     } else if(node
               && node->details->online
               && node->details->unclean == FALSE
               && node->details->shutdown == FALSE) {
         pe_action_t *unfence = pe_fence_op(node, "on", FALSE, reason, FALSE, data_set);
 
         if(dependency) {
             order_actions(unfence, dependency, pe_order_optional);
         }
 
     } else if(rsc) {
         GHashTableIter iter;
 
         g_hash_table_iter_init(&iter, rsc->allowed_nodes);
         while (g_hash_table_iter_next(&iter, NULL, (void **)&node)) {
             if(node->details->online && node->details->unclean == FALSE && node->details->shutdown == FALSE) {
                 trigger_unfencing(rsc, node, reason, dependency, data_set);
             }
         }
     }
 }
 
 gboolean
 add_tag_ref(GHashTable * tags, const char * tag_name,  const char * obj_ref)
 {
     pe_tag_t *tag = NULL;
     GListPtr gIter = NULL;
     gboolean is_existing = FALSE;
 
     CRM_CHECK(tags && tag_name && obj_ref, return FALSE);
 
     tag = g_hash_table_lookup(tags, tag_name);
     if (tag == NULL) {
         tag = calloc(1, sizeof(pe_tag_t));
         if (tag == NULL) {
             return FALSE;
         }
         tag->id = strdup(tag_name);
         tag->refs = NULL;
         g_hash_table_insert(tags, strdup(tag_name), tag);
     }
 
     for (gIter = tag->refs; gIter != NULL; gIter = gIter->next) {
         const char *existing_ref = (const char *) gIter->data;
 
         if (crm_str_eq(existing_ref, obj_ref, TRUE)){
             is_existing = TRUE;
             break;
         }
     }
 
     if (is_existing == FALSE) {
         tag->refs = g_list_append(tag->refs, strdup(obj_ref));
         crm_trace("Added: tag=%s ref=%s", tag->id, obj_ref);
     }
 
     return TRUE;
 }
 
 void pe_action_set_flag_reason(const char *function, long line,
                                pe_action_t *action, pe_action_t *reason, const char *text,
                                enum pe_action_flags flags, bool overwrite)
 {
     bool unset = FALSE;
     bool update = FALSE;
     const char *change = NULL;
 
     if(is_set(flags, pe_action_runnable)) {
         unset = TRUE;
         change = "unrunnable";
     } else if(is_set(flags, pe_action_optional)) {
         unset = TRUE;
         change = "required";
     } else if(is_set(flags, pe_action_migrate_runnable)) {
         unset = TRUE;
         overwrite = TRUE;
         change = "unrunnable";
     } else if(is_set(flags, pe_action_dangle)) {
         change = "dangling";
     } else if(is_set(flags, pe_action_requires_any)) {
         change = "required";
     } else {
         crm_err("Unknown flag change to %x by %s: 0x%s",
                 flags, action->uuid, (reason? reason->uuid : "0"));
     }
 
     if(unset) {
         if(is_set(action->flags, flags)) {
             action->flags = crm_clear_bit(function, line, action->uuid, action->flags, flags);
             update = TRUE;
         }
 
     } else {
         if(is_not_set(action->flags, flags)) {
             action->flags = crm_set_bit(function, line, action->uuid, action->flags, flags);
             update = TRUE;
         }
     }
 
     if((change && update) || text) {
         char *reason_text = NULL;
         if(reason == NULL) {
             pe_action_set_reason(action, text, overwrite);
 
         } else if(reason->rsc == NULL) {
             reason_text = crm_strdup_printf("%s %s%c %s", change, reason->task, text?':':0, text?text:"");
         } else {
             reason_text = crm_strdup_printf("%s %s %s%c %s", change, reason->rsc->id, reason->task, text?':':0, text?text:"NA");
         }
 
         if(reason_text && action->rsc != reason->rsc) {
             pe_action_set_reason(action, reason_text, overwrite);
         }
         free(reason_text);
     }
  }
 
 void pe_action_set_reason(pe_action_t *action, const char *reason, bool overwrite) 
 {
     if (action->reason != NULL && overwrite) {
         pe_rsc_trace(action->rsc, "Changing %s reason from '%s' to '%s'",
                      action->uuid, action->reason, crm_str(reason));
         free(action->reason);
     } else if (action->reason == NULL) {
         pe_rsc_trace(action->rsc, "Set %s reason to '%s'",
                      action->uuid, crm_str(reason));
     } else {
         // crm_assert(action->reason != NULL && !overwrite);
         return;
     }
 
     if (reason != NULL) {
         action->reason = strdup(reason);
     } else {
         action->reason = NULL;
     }
 }
 
 /*!
  * \internal
  * \brief Check whether shutdown has been requested for a node
  *
  * \param[in] node  Node to check
  *
  * \return TRUE if node has shutdown attribute set and nonzero, FALSE otherwise
  * \note This differs from simply using node->details->shutdown in that it can
  *       be used before that has been determined (and in fact to determine it),
  *       and it can also be used to distinguish requested shutdown from implicit
  *       shutdown of remote nodes by virtue of their connection stopping.
  */
 bool
 pe__shutdown_requested(pe_node_t *node)
 {
     const char *shutdown = pe_node_attribute_raw(node, XML_CIB_ATTR_SHUTDOWN);
 
     return shutdown && strcmp(shutdown, "0");
 }
 
 /*!
  * \internal
  * \brief Update a data set's "recheck by" time
  *
  * \param[in]     recheck   Epoch time when recheck should happen
  * \param[in,out] data_set  Current working set
  */
 void
 pe__update_recheck_time(time_t recheck, pe_working_set_t *data_set)
 {
     if ((recheck > get_effective_time(data_set))
         && ((data_set->recheck_by == 0)
             || (data_set->recheck_by > recheck))) {
         data_set->recheck_by = recheck;
     }
 }
 
 /*!
  * \internal
  * \brief Wrapper for pe_unpack_nvpairs() using a cluster working set
  */
 void
 pe__unpack_dataset_nvpairs(xmlNode *xml_obj, const char *set_name,
                            pe_rule_eval_data_t *rule_data, GHashTable *hash,
                            const char *always_first, gboolean overwrite,
                            pe_working_set_t *data_set)
 {
     crm_time_t *next_change = crm_time_new_undefined();
 
     pe_eval_nvpairs(data_set->input, xml_obj, set_name, rule_data, hash,
                     always_first, overwrite, next_change);
     if (crm_time_is_defined(next_change)) {
         time_t recheck = (time_t) crm_time_get_seconds_since_epoch(next_change);
 
         pe__update_recheck_time(recheck, data_set);
     }
     crm_time_free(next_change);
 }
 
 bool
 pe__resource_is_disabled(pe_resource_t *rsc)
 {
     const char *target_role = NULL;
 
     CRM_CHECK(rsc != NULL, return false);
     target_role = g_hash_table_lookup(rsc->meta, XML_RSC_ATTR_TARGET_ROLE);
     if (target_role) {
         enum rsc_role_e target_role_e = text2role(target_role);
 
         if ((target_role_e == RSC_ROLE_STOPPED)
             || ((target_role_e == RSC_ROLE_SLAVE)
                 && is_set(uber_parent(rsc)->flags, pe_rsc_promotable))) {
             return true;
         }
     }
     return false;
 }
 
 /*!
  * \internal
  * \brief Create an action to clear a resource's history from CIB
  *
  * \param[in] rsc   Resource to clear
  * \param[in] node  Node to clear history on
  *
  * \return New action to clear resource history
  */
 pe_action_t *
 pe__clear_resource_history(pe_resource_t *rsc, pe_node_t *node,
                            pe_working_set_t *data_set)
 {
     char *key = NULL;
 
     CRM_ASSERT(rsc && node);
     key = pcmk__op_key(rsc->id, CRM_OP_LRM_DELETE, 0);
     return custom_action(rsc, key, CRM_OP_LRM_DELETE, node, FALSE, TRUE,
                          data_set);
 }
 
 bool
 pe__rsc_running_on_any_node_in_list(pe_resource_t *rsc, GListPtr node_list)
 {
     for (GListPtr ele = rsc->running_on; ele; ele = ele->next) {
         pe_node_t *node = (pe_node_t *) ele->data;
         if (pcmk__str_in_list(node_list, node->details->uname)) {
             return true;
         }
     }
 
     return false;
 }
 
 bool
 pcmk__rsc_is_filtered(pe_resource_t *rsc, GListPtr only_show)
 {
     return (rsc->fns->active(rsc, FALSE) && !pe__rsc_running_on_any_node_in_list(rsc, only_show));
 }
diff --git a/tools/crm_node.c b/tools/crm_node.c
index 57c2ee598f..146454da67 100644
--- a/tools/crm_node.c
+++ b/tools/crm_node.c
@@ -1,645 +1,603 @@
 /*
  * Copyright 2004-2020 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 <stdlib.h>
 #include <errno.h>
 #include <sys/types.h>
 
 #include <crm/crm.h>
 #include <crm/common/cmdline_internal.h>
 #include <crm/common/mainloop.h>
 #include <crm/msg_xml.h>
 #include <crm/cib.h>
 #include <crm/common/ipc_controld.h>
 #include <crm/common/attrd_internal.h>
 
 #define SUMMARY "crm_node - Tool for displaying low-level node information"
 
 struct {
     gboolean corosync;
     gboolean dangerous_cmd;
     gboolean force_flag;
     char command;
     int nodeid;
     char *target_uname;
 } options = {
     .command = '\0',
     .force_flag = FALSE
 };
 
 gboolean command_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean name_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean remove_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 
 static GMainLoop *mainloop = NULL;
 static crm_exit_t exit_code = CRM_EX_OK;
 
 #define INDENT "                           "
 
 static GOptionEntry command_entries[] = {
     { "cluster-id", 'i', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Display this node's cluster id",
       NULL },
     { "list", 'l', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Display all known members (past and present) of this cluster",
       NULL },
     { "name", 'n', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Display the name used by the cluster for this node",
       NULL },
     { "partition", 'p', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Display the members of this partition",
       NULL },
     { "quorum", 'q', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Display a 1 if our partition has quorum, 0 if not",
       NULL },
     { "name-for-id", 'N', 0, G_OPTION_ARG_CALLBACK, name_cb,
       "Display the name used by the cluster for the node with the specified ID",
       "ID" },
     { "remove", 'R', 0, G_OPTION_ARG_CALLBACK, remove_cb,
       "(Advanced) Remove the (stopped) node with the specified name from Pacemaker's\n"
       INDENT "configuration and caches (the node must already have been removed from\n"
       INDENT "the underlying cluster stack configuration",
       "NAME" },
 
     { NULL }
 };
 
 static GOptionEntry addl_entries[] = {
     { "force", 'f', 0, G_OPTION_ARG_NONE, &options.force_flag,
       NULL,
       NULL },
 #if SUPPORT_COROSYNC
     /* Unused and deprecated */
     { "corosync", 'C', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.corosync,
       NULL,
       NULL },
 #endif
 
     // @TODO add timeout option for when IPC replies are needed
 
     { NULL }
 };
 
 gboolean
 command_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     if (safe_str_eq("-i", option_name) || safe_str_eq("--cluster-id", option_name)) {
         options.command = 'i';
     } else if (safe_str_eq("-l", option_name) || safe_str_eq("--list", option_name)) {
         options.command = 'l';
     } else if (safe_str_eq("-n", option_name) || safe_str_eq("--name", option_name)) {
         options.command = 'n';
     } else if (safe_str_eq("-p", option_name) || safe_str_eq("--partition", option_name)) {
         options.command = 'p';
     } else if (safe_str_eq("-q", option_name) || safe_str_eq("--quorum", option_name)) {
         options.command = 'q';
     } else {
         g_set_error(error, G_OPTION_ERROR, CRM_EX_INVALID_PARAM, "Unknown param passed to command_cb: %s\n", option_name);
         return FALSE;
     }
 
     return TRUE;
 }
 
 gboolean
 name_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.command = 'N';
     options.nodeid = crm_parse_int(optarg, NULL);
     return TRUE;
 }
 
 gboolean
 remove_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     if (optarg == NULL) {
         crm_err("-R option requires an argument");
         g_set_error(error, G_OPTION_ERROR, CRM_EX_INVALID_PARAM, "-R option requires an argument");
         return FALSE;
     }
 
     options.command = 'R';
     options.dangerous_cmd = TRUE;
     options.target_uname = strdup(optarg);
     return TRUE;
 }
 
+static gint
+sort_node(gconstpointer a, gconstpointer b)
+{
+    const pcmk_controld_api_node_t *node_a = a;
+    const pcmk_controld_api_node_t *node_b = b;
+
+    return pcmk_numeric_strcasecmp((node_a->uname? node_a->uname : ""),
+                                   (node_b->uname? node_b->uname : ""));
+}
+
 static void
 controller_event_cb(pcmk_ipc_api_t *controld_api,
                     enum pcmk_ipc_event event_type, crm_exit_t status,
                     void *event_data, void *user_data)
 {
     pcmk_controld_api_reply_t *reply = event_data;
 
     switch (event_type) {
         case pcmk_ipc_event_disconnect:
             if (exit_code == CRM_EX_DISCONNECT) { // Unexpected
                 fprintf(stderr, "error: Lost connection to controller\n");
             }
             goto done;
             break;
 
         case pcmk_ipc_event_reply:
             break;
 
         default:
             return;
     }
 
     if (status != CRM_EX_OK) {
         fprintf(stderr, "error: Bad reply from controller: %s\n",
                 crm_exit_str(status));
         goto done;
     }
-    if (reply->reply_type != pcmk_controld_reply_info) {
-        fprintf(stderr, "error: Unknown reply type %d from controller\n",
-                reply->reply_type);
-        goto done;
-    }
 
     // Parse desired info from reply and display to user
     switch (options.command) {
         case 'i':
+            if (reply->reply_type != pcmk_controld_reply_info) {
+                fprintf(stderr,
+                        "error: Unknown reply type %d from controller\n",
+                        reply->reply_type);
+                goto done;
+            }
             if (reply->data.node_info.id == 0) {
                 fprintf(stderr,
                         "error: Controller reply did not contain node ID\n");
                 exit_code = CRM_EX_PROTOCOL;
                 goto done;
             }
             printf("%d\n", reply->data.node_info.id);
             break;
 
         case 'n':
         case 'N':
+            if (reply->reply_type != pcmk_controld_reply_info) {
+                fprintf(stderr,
+                        "error: Unknown reply type %d from controller\n",
+                        reply->reply_type);
+                goto done;
+            }
             if (reply->data.node_info.uname == NULL) {
                 fprintf(stderr, "Node is not known to cluster\n");
                 exit_code = CRM_EX_NOHOST;
                 goto done;
             }
             printf("%s\n", reply->data.node_info.uname);
             break;
 
         case 'q':
+            if (reply->reply_type != pcmk_controld_reply_info) {
+                fprintf(stderr,
+                        "error: Unknown reply type %d from controller\n",
+                        reply->reply_type);
+                goto done;
+            }
             printf("%d\n", reply->data.node_info.have_quorum);
             if (!(reply->data.node_info.have_quorum)) {
                 exit_code = CRM_EX_QUORUM;
                 goto done;
             }
             break;
 
+        case 'l':
+        case 'p':
+            if (reply->reply_type != pcmk_controld_reply_nodes) {
+                fprintf(stderr,
+                        "error: Unknown reply type %d from controller\n",
+                        reply->reply_type);
+                goto done;
+            }
+            reply->data.nodes = g_list_sort(reply->data.nodes, sort_node);
+            for (GList *node_iter = reply->data.nodes;
+                 node_iter != NULL; node_iter = node_iter->next) {
+
+                pcmk_controld_api_node_t *node = node_iter->data;
+                const char *uname = (node->uname? node->uname : "");
+                const char *state = (node->state? node->state : "");
+
+                if (options.command == 'l') {
+                    printf("%lu %s %s\n",
+                           (unsigned long) node->id, uname, state);
+
+                // i.e. CRM_NODE_MEMBER, but we don't want to include cluster.h
+                } else if (!strcmp(state, "member")) {
+                    printf("%s ", uname);
+                }
+            }
+            if (options.command == 'p') {
+                printf("\n");
+            }
+            break;
+
         default:
             fprintf(stderr, "internal error: Controller reply not expected\n");
             exit_code = CRM_EX_SOFTWARE;
             goto done;
     }
 
     // Success
     exit_code = CRM_EX_OK;
 done:
     pcmk_disconnect_ipc(controld_api);
     pcmk_quit_main_loop(mainloop, 10);
 }
 
 static void
-run_controller_mainloop(uint32_t nodeid)
+run_controller_mainloop(uint32_t nodeid, bool list_nodes)
 {
     pcmk_ipc_api_t *controld_api = NULL;
     int rc;
 
     // Set disconnect exit code to handle unexpected disconnects
     exit_code = CRM_EX_DISCONNECT;
 
     // Create controller IPC object
     rc = pcmk_new_ipc_api(&controld_api, pcmk_ipc_controld);
     if (rc != pcmk_rc_ok) {
         fprintf(stderr, "error: Could not connect to controller: %s\n",
                 pcmk_rc_str(rc));
         return;
     }
     pcmk_register_ipc_callback(controld_api, controller_event_cb, NULL);
 
     // Connect to controller
     rc = pcmk_connect_ipc(controld_api, pcmk_ipc_dispatch_main);
     if (rc != pcmk_rc_ok) {
         fprintf(stderr, "error: Could not connect to controller: %s\n",
                 pcmk_rc_str(rc));
         exit_code = pcmk_rc2exitc(rc);
         return;
     }
 
-    rc = pcmk_controld_api_node_info(controld_api, nodeid);
+    if (list_nodes) {
+        rc = pcmk_controld_api_list_nodes(controld_api);
+    } else {
+        rc = pcmk_controld_api_node_info(controld_api, nodeid);
+    }
     if (rc != pcmk_rc_ok) {
         fprintf(stderr, "error: Could not ping controller: %s\n",
                 pcmk_rc_str(rc));
         pcmk_disconnect_ipc(controld_api);
         exit_code = pcmk_rc2exitc(rc);
         return;
     }
 
     // Run main loop to get controller reply via controller_event_cb()
     mainloop = g_main_loop_new(NULL, FALSE);
     g_main_loop_run(mainloop);
     g_main_loop_unref(mainloop);
     mainloop = NULL;
     pcmk_free_ipc_api(controld_api);
 }
 
 static void
 print_node_name(void)
 {
     // Check environment first (i.e. when called by resource agent)
     const char *name = getenv("OCF_RESKEY_" CRM_META "_" XML_LRM_ATTR_TARGET);
 
     if (name != NULL) {
         printf("%s\n", name);
         exit_code = CRM_EX_OK;
         return;
 
     } else {
         // Otherwise ask the controller
-        run_controller_mainloop(0);
+        run_controller_mainloop(0, false);
     }
 }
 
 static int
 cib_remove_node(long id, const char *name)
 {
     int rc;
     cib_t *cib = NULL;
     xmlNode *node = NULL;
     xmlNode *node_state = NULL;
 
     crm_trace("Removing %s from the CIB", name);
 
     if(name == NULL && id == 0) {
         return -ENOTUNIQ;
     }
 
     node = create_xml_node(NULL, XML_CIB_TAG_NODE);
     node_state = create_xml_node(NULL, XML_CIB_TAG_STATE);
 
     crm_xml_add(node, XML_ATTR_UNAME, name);
     crm_xml_add(node_state, XML_ATTR_UNAME, name);
     if (id > 0) {
         crm_xml_set_id(node, "%ld", id);
         crm_xml_add(node_state, XML_ATTR_ID, ID(node));
     }
 
     cib = cib_new();
     cib->cmds->signon(cib, crm_system_name, cib_command);
 
     rc = cib->cmds->remove(cib, XML_CIB_TAG_NODES, node, cib_sync_call);
     if (rc != pcmk_ok) {
         printf("Could not remove %s[%ld] from " XML_CIB_TAG_NODES ": %s",
                 name, id, pcmk_strerror(rc));
     }
     rc = cib->cmds->remove(cib, XML_CIB_TAG_STATUS, node_state, cib_sync_call);
     if (rc != pcmk_ok) {
         printf("Could not remove %s[%ld] from " XML_CIB_TAG_STATUS ": %s",
                 name, id, pcmk_strerror(rc));
     }
 
     cib->cmds->signoff(cib);
     cib_delete(cib);
     return rc;
 }
 
 static int
 controller_remove_node(const char *node_name, long nodeid)
 {
     pcmk_ipc_api_t *controld_api = NULL;
     int rc;
 
     // Create controller IPC object
     rc = pcmk_new_ipc_api(&controld_api, pcmk_ipc_controld);
     if (rc != pcmk_rc_ok) {
         fprintf(stderr, "error: Could not connect to controller: %s\n",
                 pcmk_rc_str(rc));
         return ENOTCONN;
     }
 
     // Connect to controller (without main loop)
     rc = pcmk_connect_ipc(controld_api, pcmk_ipc_dispatch_sync);
     if (rc != pcmk_rc_ok) {
         fprintf(stderr, "error: Could not connect to controller: %s\n",
                 pcmk_rc_str(rc));
         pcmk_free_ipc_api(controld_api);
         return rc;
     }
 
     rc = pcmk_ipc_purge_node(controld_api, node_name, nodeid);
     if (rc != pcmk_rc_ok) {
         fprintf(stderr,
                 "error: Could not clear node from controller's cache: %s\n",
                 pcmk_rc_str(rc));
     }
 
     pcmk_free_ipc_api(controld_api);
     return pcmk_rc_ok;
 }
 
 static int
 tools_remove_node_cache(const char *node_name, long nodeid, const char *target)
 {
     int rc = -1;
     crm_ipc_t *conn = NULL;
     xmlNode *cmd = NULL;
 
     conn = crm_ipc_new(target, 0);
     if (!conn) {
         return -ENOTCONN;
     }
     if (!crm_ipc_connect(conn)) {
         crm_perror(LOG_ERR, "Connection to %s failed", target);
         crm_ipc_destroy(conn);
         return -ENOTCONN;
     }
 
     crm_trace("Removing %s[%ld] from the %s membership cache",
               node_name, nodeid, target);
 
     if(safe_str_eq(target, T_ATTRD)) {
         cmd = create_xml_node(NULL, __FUNCTION__);
 
         crm_xml_add(cmd, F_TYPE, T_ATTRD);
         crm_xml_add(cmd, F_ORIG, crm_system_name);
 
         crm_xml_add(cmd, PCMK__XA_TASK, PCMK__ATTRD_CMD_PEER_REMOVE);
         crm_xml_add(cmd, PCMK__XA_ATTR_NODE_NAME, node_name);
 
         if (nodeid > 0) {
             crm_xml_add_int(cmd, PCMK__XA_ATTR_NODE_ID, (int) nodeid);
         }
 
     } else { // Fencer or pacemakerd
         cmd = create_request(CRM_OP_RM_NODE_CACHE, NULL, NULL, target,
                              crm_system_name, NULL);
         if (nodeid > 0) {
             crm_xml_set_id(cmd, "%ld", nodeid);
         }
         crm_xml_add(cmd, XML_ATTR_UNAME, node_name);
     }
 
     rc = crm_ipc_send(conn, cmd, 0, 0, NULL);
     crm_debug("%s peer cache cleanup for %s (%ld): %d",
               target, node_name, nodeid, rc);
 
     if (rc > 0) {
         // @TODO Should this be done just once after all the rest?
         rc = cib_remove_node(nodeid, node_name);
     }
 
     if (conn) {
         crm_ipc_close(conn);
         crm_ipc_destroy(conn);
     }
     free_xml(cmd);
     return rc > 0 ? 0 : rc;
 }
 
 static void
 remove_node(const char *target_uname)
 {
     int rc;
     int d = 0;
     long nodeid = 0;
     const char *node_name = NULL;
     char *endptr = NULL;
     const char *daemons[] = {
         "stonith-ng",
         T_ATTRD,
         CRM_SYSTEM_MCP,
     };
 
     // Check whether node was specified by name or numeric ID
     errno = 0;
     nodeid = strtol(target_uname, &endptr, 10);
     if ((errno != 0) || (endptr == target_uname) || (*endptr != '\0')
         || (nodeid <= 0)) {
         // It's not a positive integer, so assume it's a node name
         nodeid = 0;
         node_name = target_uname;
     }
 
     rc = controller_remove_node(node_name, nodeid);
     if (rc != pcmk_rc_ok) {
         exit_code = pcmk_rc2exitc(rc);
         return;
     }
 
     for (d = 0; d < DIMOF(daemons); d++) {
         if (tools_remove_node_cache(node_name, nodeid, daemons[d])) {
             crm_err("Failed to connect to %s to remove node '%s'",
                     daemons[d], target_uname);
             exit_code = CRM_EX_ERROR;
             return;
         }
     }
     exit_code = CRM_EX_OK;
 }
 
-static gint
-compare_node_xml(gconstpointer a, gconstpointer b)
-{
-    const char *a_name = crm_element_value((xmlNode*) a, "uname");
-    const char *b_name = crm_element_value((xmlNode*) b, "uname");
-
-    return strcmp((a_name? a_name : ""), (b_name? b_name : ""));
-}
-
-static int
-node_mcp_dispatch(const char *buffer, ssize_t length, gpointer userdata)
-{
-    GList *nodes = NULL;
-    xmlNode *node = NULL;
-    xmlNode *msg = string2xml(buffer);
-    const char *uname;
-    const char *state;
-
-    if (msg == NULL) {
-        fprintf(stderr, "error: Could not understand pacemakerd response\n");
-        exit_code = CRM_EX_PROTOCOL;
-        g_main_loop_quit(mainloop);
-        return 0;
-    }
-
-    crm_log_xml_trace(msg, "message");
-
-    for (node = __xml_first_child(msg); node != NULL; node = __xml_next(node)) {
-        nodes = g_list_insert_sorted(nodes, node, compare_node_xml);
-    }
-
-    for (GList *iter = nodes; iter; iter = iter->next) {
-        node = (xmlNode*) iter->data;
-        uname = crm_element_value(node, "uname");
-        state = crm_element_value(node, "state");
-
-        if (options.command == 'l') {
-            int id = 0;
-
-            crm_element_value_int(node, "id", &id);
-            printf("%d %s %s\n", id, (uname? uname : ""), (state? state : ""));
-
-        // This is CRM_NODE_MEMBER but we don't want to include cluster header
-        } else if ((options.command == 'p') && safe_str_eq(state, "member")) {
-            printf("%s ", (uname? uname : ""));
-        }
-    }
-    if (options.command == 'p') {
-        fprintf(stdout, "\n");
-    }
-
-    free_xml(msg);
-    exit_code = CRM_EX_OK;
-    g_main_loop_quit(mainloop);
-    return 0;
-}
-
-static void
-lost_pacemakerd(gpointer user_data)
-{
-    fprintf(stderr, "error: Lost connection to cluster\n");
-    exit_code = CRM_EX_DISCONNECT;
-    g_main_loop_quit(mainloop);
-}
-
-static void
-run_pacemakerd_mainloop(void)
-{
-    crm_ipc_t *ipc = NULL;
-    xmlNode *poke = NULL;
-    mainloop_io_t *source = NULL;
-
-    struct ipc_client_callbacks ipc_callbacks = {
-        .dispatch = node_mcp_dispatch,
-        .destroy = lost_pacemakerd
-    };
-
-    source = mainloop_add_ipc_client(CRM_SYSTEM_MCP, G_PRIORITY_DEFAULT, 0,
-                                     NULL, &ipc_callbacks);
-    ipc = mainloop_get_ipc_client(source);
-    if (ipc == NULL) {
-        fprintf(stderr,
-                "error: Could not connect to cluster (is it running?)\n");
-        exit_code = CRM_EX_DISCONNECT;
-        return;
-    }
-
-    // Sending anything will get us a list of nodes
-    poke = create_xml_node(NULL, "poke");
-    crm_ipc_send(ipc, poke, 0, 0, NULL);
-    free_xml(poke);
-
-    // Handle reply via node_mcp_dispatch()
-    mainloop = g_main_loop_new(NULL, FALSE);
-    g_main_loop_run(mainloop);
-    g_main_loop_unref(mainloop);
-    mainloop = NULL;
-}
-
 static GOptionContext *
 build_arg_context(pcmk__common_args_t *args, GOptionGroup *group) {
     GOptionContext *context = NULL;
 
     GOptionEntry extra_prog_entries[] = {
         { "quiet", 'Q', 0, G_OPTION_ARG_NONE, &(args->quiet),
           "Be less descriptive in output.",
           NULL },
 
         { NULL }
     };
 
     context = pcmk__build_arg_context(args, NULL, &group, NULL);
 
     /* Add the -q option, which cannot be part of the globally supported options
      * because some tools use that flag for something else.
      */
     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, "additional", "Additional Options:",
                         "Show additional options", addl_entries);
     return context;
 }
 
 int
 main(int argc, char **argv)
 {
     pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
 
     GError *error = NULL;
     GOptionContext *context = NULL;
     GOptionGroup *output_group = NULL;
     gchar **processed_args = NULL;
 
     context = build_arg_context(args, output_group);
 
     crm_log_cli_init("crm_node");
 
     processed_args = pcmk__cmdline_preproc(argv, "NR");
 
     if (!g_option_context_parse_strv(context, &processed_args, &error)) {
         fprintf(stderr, "%s: %s\n", g_get_prgname(), error->message);
         exit_code = CRM_EX_USAGE;
         goto done;
     }
 
     for (int i = 0; i < args->verbosity; i++) {
         crm_bump_log_level(argc, argv);
     }
 
     if (args->version) {
         /* FIXME:  When crm_node is converted to use formatted output, this can go. */
         pcmk__cli_help('v', CRM_EX_USAGE);
     }
 
     if (optind > argc || options.command == 0) {
         char *help = g_option_context_get_help(context, TRUE, NULL);
 
         fprintf(stderr, "%s", help);
         g_free(help);
         exit_code = CRM_EX_USAGE;
         goto done;
     }
 
     if (options.dangerous_cmd && options.force_flag == FALSE) {
         fprintf(stderr, "The supplied command is considered dangerous."
                 "  To prevent accidental destruction of the cluster,"
                 " the --force flag is required in order to proceed.\n");
         exit_code = CRM_EX_USAGE;
         goto done;
     }
 
     switch (options.command) {
         case 'n':
             print_node_name();
             break;
         case 'R':
             remove_node(options.target_uname);
             break;
         case 'i':
         case 'q':
         case 'N':
-            run_controller_mainloop(options.nodeid);
+            run_controller_mainloop(options.nodeid, false);
             break;
         case 'l':
         case 'p':
-            run_pacemakerd_mainloop();
+            run_controller_mainloop(0, true);
             break;
         default:
             break;
     }
 
 done:
     g_strfreev(processed_args);
     g_clear_error(&error);
     pcmk__free_arg_context(context);
     return crm_exit(exit_code);
 }