diff --git a/include/crm/common/resources.h b/include/crm/common/resources.h
index 264b776f2f..8cd27c1984 100644
--- a/include/crm/common/resources.h
+++ b/include/crm/common/resources.h
@@ -1,352 +1,351 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #ifndef PCMK__CRM_COMMON_RESOURCES__H
 #define PCMK__CRM_COMMON_RESOURCES__H
 
 #include <stdbool.h>                    // bool
 #include <sys/types.h>                  // time_t
 #include <libxml/tree.h>                // xmlNode
 #include <glib.h>                       // gboolean, guint, GList, GHashTable
 
 #include <crm/common/roles.h>           // enum rsc_role_e
 #include <crm/common/scheduler_types.h> // pcmk_resource_t, etc.
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 /*!
  * \file
  * \brief Scheduler API for resources
  * \ingroup core
  */
 
 // Resource variants supported by Pacemaker
 //!@{
 //! \deprecated Do not use
 enum pe_obj_types {
     // Order matters: some code compares greater or lesser than
     pcmk_rsc_variant_unknown    = -1,   // Unknown resource variant
     pcmk_rsc_variant_primitive  = 0,    // Primitive resource
     pcmk_rsc_variant_group      = 1,    // Group resource
     pcmk_rsc_variant_clone      = 2,    // Clone resource
     pcmk_rsc_variant_bundle     = 3,    // Bundle resource
 
 #if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1)
     pe_unknown      = pcmk_rsc_variant_unknown,
     pe_native       = pcmk_rsc_variant_primitive,
     pe_group        = pcmk_rsc_variant_group,
     pe_clone        = pcmk_rsc_variant_clone,
     pe_container    = pcmk_rsc_variant_bundle,
 #endif
 };
 
 // What resource needs before it can be recovered from a failed node
 enum rsc_start_requirement {
     pcmk_requires_nothing   = 0,    // Resource can be recovered immediately
     pcmk_requires_quorum    = 1,    // Resource can be recovered if quorate
     pcmk_requires_fencing   = 2,    // Resource can be recovered after fencing
 
 #if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1)
     rsc_req_nothing         = pcmk_requires_nothing,
     rsc_req_quorum          = pcmk_requires_quorum,
     rsc_req_stonith         = pcmk_requires_fencing,
 #endif
 };
 
 // How to recover a resource that is incorrectly active on multiple nodes
 enum rsc_recovery_type {
     pcmk_multiply_active_restart    = 0,    // Stop on all, start on desired
     pcmk_multiply_active_stop       = 1,    // Stop on all and leave stopped
     pcmk_multiply_active_block      = 2,    // Do nothing to resource
     pcmk_multiply_active_unexpected = 3,    // Stop unexpected instances
 
 #if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1)
     recovery_stop_start             = pcmk_multiply_active_restart,
     recovery_stop_only              = pcmk_multiply_active_stop,
     recovery_block                  = pcmk_multiply_active_block,
     recovery_stop_unexpected        = pcmk_multiply_active_unexpected,
 #endif
 };
 
 // Resource scheduling flags
 enum pcmk_rsc_flags {
     // No resource flags set (compare with equality rather than bit set)
     pcmk_no_rsc_flags               = 0ULL,
 
     // Whether resource has been removed from the configuration
     pcmk_rsc_removed                = (1ULL << 0),
 
     // Whether resource is managed
     pcmk_rsc_managed                = (1ULL << 1),
 
     // Whether resource is blocked from further action
     pcmk_rsc_blocked                = (1ULL << 2),
 
     // Whether resource has been removed but has a container
     pcmk_rsc_removed_filler         = (1ULL << 3),
 
     // Whether resource has clone notifications enabled
     pcmk_rsc_notify                 = (1ULL << 4),
 
     // Whether resource is not an anonymous clone instance
     pcmk_rsc_unique                 = (1ULL << 5),
 
     // Whether resource's class is "stonith"
     pcmk_rsc_fence_device           = (1ULL << 6),
 
     // Whether resource can be promoted and demoted
     pcmk_rsc_promotable             = (1ULL << 7),
 
     // Whether resource has not yet been assigned to a node
     pcmk_rsc_unassigned             = (1ULL << 8),
 
     // Whether resource is in the process of being assigned to a node
     pcmk_rsc_assigning              = (1ULL << 9),
 
     // Whether resource is in the process of modifying allowed node scores
     pcmk_rsc_updating_nodes         = (1ULL << 10),
 
     // Whether resource is in the process of scheduling actions to restart
     pcmk_rsc_restarting             = (1ULL << 11),
 
     // Whether resource must be stopped (instead of demoted) if it is failed
     pcmk_rsc_stop_if_failed         = (1ULL << 12),
 
     // Whether a reload action has been scheduled for resource
     pcmk_rsc_reload                 = (1ULL << 13),
 
     // Whether resource is a remote connection allowed to run on a remote node
     pcmk_rsc_remote_nesting_allowed = (1ULL << 14),
 
     // Whether resource has \c PCMK_META_CRITICAL meta-attribute enabled
     pcmk_rsc_critical               = (1ULL << 15),
 
     // Whether resource is considered failed
     pcmk_rsc_failed                 = (1ULL << 16),
 
     // Flag for non-scheduler code to use to detect recursion loops
     pcmk_rsc_detect_loop            = (1ULL << 17),
 
     // \deprecated Do not use
     pcmk_rsc_runnable               = (1ULL << 18),
 
     // Whether resource has pending start action in history
     pcmk_rsc_start_pending          = (1ULL << 19),
 
     // \deprecated Do not use
     pcmk_rsc_starting               = (1ULL << 20),
 
     // \deprecated Do not use
     pcmk_rsc_stopping               = (1ULL << 21),
 
     /*
      * Whether resource is multiply active with recovery set to
      * \c PCMK_VALUE_STOP_UNEXPECTED
      */
     pcmk_rsc_stop_unexpected        = (1ULL << 22),
 
     // Whether resource is allowed to live-migrate
     pcmk_rsc_migratable             = (1ULL << 23),
 
     // Whether resource has an ignorable failure
     pcmk_rsc_ignore_failure         = (1ULL << 24),
 
     // Whether resource is an implicit container resource for a bundle replica
     pcmk_rsc_replica_container      = (1ULL << 25),
 
     // Whether resource, its node, or entire cluster is in maintenance mode
     pcmk_rsc_maintenance            = (1ULL << 26),
 
     // \deprecated Do not use
     pcmk_rsc_has_filler             = (1ULL << 27),
 
     // Whether resource can be started or promoted only on quorate nodes
     pcmk_rsc_needs_quorum           = (1ULL << 28),
 
     // Whether resource requires fencing before recovery if on unclean node
     pcmk_rsc_needs_fencing          = (1ULL << 29),
 
     // Whether resource can be started or promoted only on unfenced nodes
     pcmk_rsc_needs_unfencing        = (1ULL << 30),
 };
 //!@}
 
 //! Search options for resources (exact resource ID always matches)
 enum pe_find {
     //! Also match clone instance ID from resource history
     pcmk_rsc_match_history          = (1 << 0),
 
     //! Also match anonymous clone instances by base name
     pcmk_rsc_match_anon_basename    = (1 << 1),
 
     //! Match only clones and their instances, by either clone or instance ID
     pcmk_rsc_match_clone_only       = (1 << 2),
 
     //! If matching by node, compare current node instead of assigned node
     pcmk_rsc_match_current_node     = (1 << 3),
 
     //! \deprecated Do not use
     pe_find_inactive                = (1 << 4),
 
     //! Match clone instances (even unique) by base name as well as exact ID
     pcmk_rsc_match_basename         = (1 << 5),
 
 #if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1)
     //! \deprecated Use pcmk_rsc_match_history instead
     pe_find_renamed     = pcmk_rsc_match_history,
 
     //! \deprecated Use pcmk_rsc_match_anon_basename instead
     pe_find_anon        = pcmk_rsc_match_anon_basename,
 
     //! \deprecated Use pcmk_rsc_match_clone_only instead
     pe_find_clone       = pcmk_rsc_match_clone_only,
 
     //! \deprecated Use pcmk_rsc_match_current_node instead
     pe_find_current     = pcmk_rsc_match_current_node,
 
     //! \deprecated Use pcmk_rsc_match_basename instead
     pe_find_any         = pcmk_rsc_match_basename,
 #endif
 };
 
 //! \deprecated Do not use
 enum pe_restart {
     pe_restart_restart,
     pe_restart_ignore,
 };
 
 //! \internal Do not use
 typedef struct pcmk__resource_private pcmk__resource_private_t;
 
 // Resource assignment methods (implementation defined by libpacemaker)
 //! \deprecated Do not use (public access will be removed in a future release)
 typedef struct resource_alloc_functions_s pcmk_assignment_methods_t;
 
 // Implementation of pcmk_resource_t
 // @COMPAT Make this internal when we can break API backward compatibility
 //!@{
 //! \deprecated Do not use (public access will be removed in a future release)
 struct pe_resource_s {
     /* @COMPAT Once all members are moved to pcmk__resource_private_t,
      * We can make that the pcmk_resource_t implementation and drop this
      * struct altogether, leaving pcmk_resource_t as an opaque public type.
      */
     pcmk__resource_private_t *private;
 
     // NOTE: sbd (as of at least 1.5.2) uses this
     //! \deprecated Call pcmk_resource_id() instead
     char *id;                           // Resource ID in configuration
 
     char *clone_name;                   // Resource instance ID in history
 
     // Resource configuration (possibly expanded from template)
     xmlNode *xml;
 
     // Original resource configuration, if using template
     xmlNode *orig_xml;
 
     // Configuration of resource operations (possibly expanded from template)
     xmlNode *ops_xml;
 
     pcmk_scheduler_t *cluster;          // Cluster that resource is part of
     pcmk_resource_t *parent;            // Resource's parent resource, if any
     enum pe_obj_types variant;          // Resource variant
     void *variant_opaque;               // Variant-specific (and private) data
-    pcmk_assignment_methods_t *cmds;    // Resource assignment methods
 
     enum rsc_recovery_type recovery_type;   // How to recover if failed
 
     enum pe_restart restart_type;   // \deprecated Do not use
     int priority;                   // Configured priority
     int stickiness;                 // Extra preference for current node
     int sort_index;                 // Promotion score on assigned node
     int failure_timeout;            // Failure timeout
     int migration_threshold;        // Migration threshold
     guint remote_reconnect_ms;      // Retry interval for remote connections
     char *pending_task;             // Pending action in history, if any
 
     // NOTE: sbd (as of at least 1.5.2) uses this
     //! \deprecated Call pcmk_resource_is_managed() instead
     unsigned long long flags;       // Group of enum pcmk_rsc_flags
 
     // @TODO Merge these into flags
     gboolean is_remote_node;        // Whether this is a remote connection
     gboolean exclusive_discover;    // Whether exclusive probing is enabled
 
     /* Pay special attention to whether you want to use rsc_cons_lhs and
      * rsc_cons directly, which include only colocations explicitly involving
      * this resource, or call libpacemaker's pcmk__with_this_colocations() and
      * pcmk__this_with_colocations() functions, which may return relevant
      * colocations involving the resource's ancestors as well.
      */
 
     GList *rsc_cons_lhs;      // Colocations of other resources with this one
     GList *rsc_cons;          // Colocations of this resource with others
     GList *rsc_location;      // Location constraints for resource
     GList *actions;           // Actions scheduled for resource
     GList *rsc_tickets;       // Ticket constraints for resource
 
     pcmk_node_t *allocated_to;  // Node resource is assigned to
 
     // The destination node, if migrate_to completed but migrate_from has not
     pcmk_node_t *partial_migration_target;
 
     // The source node, if migrate_to completed but migrate_from has not
     pcmk_node_t *partial_migration_source;
 
     // Nodes where resource may be active
     GList *running_on;
 
     // Nodes where resource has been probed (key is node ID, not name)
     GHashTable *known_on;
 
     // Nodes where resource may run (key is node ID, not name)
     GHashTable *allowed_nodes;
 
     enum rsc_role_e role;           // Resource's current role
     enum rsc_role_e next_role;      // Resource's scheduled next role
 
     GHashTable *meta;               // Resource's meta-attributes
     GHashTable *parameters;         // \deprecated Use pe_rsc_params() instead
     GHashTable *utilization;        // Resource's utilization attributes
 
     GList *children;                // Resource's child resources, if any
 
     // Source nodes where stop is needed after migrate_from and migrate_to
     GList *dangling_migrations;
 
     pcmk_resource_t *container;     // Resource containing this one, if any
     GList *fillers;                 // Resources contained by this one, if any
 
     // @COMPAT These should be made const at next API compatibility break
     pcmk_node_t *pending_node;      // Node on which pending_task is happening
     pcmk_node_t *lock_node;         // Resource shutdown-locked to this node
 
     time_t lock_time;               // When shutdown lock started
 
     /*
      * Resource parameters may have node-attribute-based rules, which means the
      * values can vary by node. This table has node names as keys and parameter
      * name/value tables as values. Use pe_rsc_params() to get the table for a
      * given node rather than use this directly.
      */
     GHashTable *parameter_cache;
 };
 //!@}
 
 const char *pcmk_resource_id(const pcmk_resource_t *rsc);
 bool pcmk_resource_is_managed(const pcmk_resource_t *rsc);
 
 #ifdef __cplusplus
 }
 #endif
 
 #endif // PCMK__CRM_COMMON_RESOURCES__H
diff --git a/include/crm/common/resources_internal.h b/include/crm/common/resources_internal.h
index 1999ff74f5..e11da15f99 100644
--- a/include/crm/common/resources_internal.h
+++ b/include/crm/common/resources_internal.h
@@ -1,216 +1,217 @@
 /*
  * Copyright 2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #ifndef PCMK__CRM_COMMON_RESOURCES_INTERNAL__H
 #define PCMK__CRM_COMMON_RESOURCES_INTERNAL__H
 
 #include <glib.h>                       // gboolean, GList
 #include <crm/common/resources.h>       // enum rsc_recovery_type
 #include <crm/common/roles.h>           // enum rsc_role_e
 #include <crm/common/scheduler_types.h> // pcmk_node_t, pcmk_resource_t, etc.
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 /*!
  * \internal
  * \brief Set resource flags
  *
  * \param[in,out] resource      Resource to set flags for
  * \param[in]     flags_to_set  Group of enum pcmk_rsc_flags to set
  */
 #define pcmk__set_rsc_flags(resource, flags_to_set) do {                    \
         (resource)->flags = pcmk__set_flags_as(__func__, __LINE__,          \
             LOG_TRACE, "Resource", (resource)->id, (resource)->flags,       \
             (flags_to_set), #flags_to_set);                                 \
     } while (0)
 
 /*!
  * \internal
  * \brief Clear resource flags
  *
  * \param[in,out] resource        Resource to clear flags for
  * \param[in]     flags_to_clear  Group of enum pcmk_rsc_flags to clear
  */
 #define pcmk__clear_rsc_flags(resource, flags_to_clear) do {                \
         (resource)->flags = pcmk__clear_flags_as(__func__, __LINE__,        \
             LOG_TRACE, "Resource", (resource)->id, (resource)->flags,       \
             (flags_to_clear), #flags_to_clear);                             \
     } while (0)
 
 //! Resource object methods
 typedef struct {
     /*!
      * \internal
      * \brief Parse variant-specific resource XML from CIB into struct members
      *
      * \param[in,out] rsc        Partially unpacked resource
      * \param[in,out] scheduler  Scheduler data
      *
      * \return TRUE if resource was unpacked successfully, otherwise FALSE
      */
     gboolean (*unpack)(pcmk_resource_t *rsc, pcmk_scheduler_t *scheduler);
 
     /*!
      * \internal
      * \brief Search for a resource ID in a resource and its children
      *
      * \param[in] rsc      Search this resource and its children
      * \param[in] id       Search for this resource ID
      * \param[in] on_node  If not NULL, limit search to resources on this node
      * \param[in] flags    Group of enum pe_find flags
      *
      * \return Resource that matches search criteria if any, otherwise NULL
      */
     pcmk_resource_t *(*find_rsc)(pcmk_resource_t *rsc, const char *search,
                                  const pcmk_node_t *node, int flags);
 
     /*!
      * \internal
      * \brief Get value of a resource instance attribute
      *
      * \param[in,out] rsc        Resource to check
      * \param[in]     node       Node to use to evaluate rules
      * \param[in]     create     Ignored
      * \param[in]     name       Name of instance attribute to check
      * \param[in,out] scheduler  Scheduler data
      *
      * \return Value of requested attribute if available, otherwise NULL
      * \note The caller is responsible for freeing the result using free().
      */
     char *(*parameter)(pcmk_resource_t *rsc, pcmk_node_t *node, gboolean create,
                        const char *name, pcmk_scheduler_t *scheduler);
 
     /*!
      * \internal
      * \brief Check whether a resource is active
      *
      * \param[in] rsc  Resource to check
      * \param[in] all  If \p rsc is collective, all instances must be active
      *
      * \return TRUE if \p rsc is active, otherwise FALSE
      */
     gboolean (*active)(pcmk_resource_t *rsc, gboolean all);
 
     /*!
      * \internal
      * \brief Get resource's current or assigned role
      *
      * \param[in] rsc      Resource to check
      * \param[in] current  If TRUE, check current role, otherwise assigned role
      *
      * \return Current or assigned role of \p rsc
      */
     enum rsc_role_e (*state)(const pcmk_resource_t *rsc, gboolean current);
 
     /*!
      * \internal
      * \brief List nodes where a resource (or any of its children) is
      *
      * \param[in]  rsc      Resource to check
      * \param[out] list     List to add result to
      * \param[in]  current  If 0, list nodes where \p rsc is assigned;
      *                      if 1, where active; if 2, where active or pending
      *
      * \return If list contains only one node, that node, otherwise NULL
      */
     pcmk_node_t *(*location)(const pcmk_resource_t *rsc, GList **list,
                              int current);
 
     /*!
      * \internal
      * \brief Free all memory used by a resource
      *
      * \param[in,out] rsc  Resource to free
      */
     void (*free)(pcmk_resource_t *rsc);
 
     /*!
      * \internal
      * \brief Increment cluster's instance counts for a resource
      *
      * Given a resource, increment its cluster's ninstances, disabled_resources,
      * and blocked_resources counts for the resource and its descendants.
      *
      * \param[in,out] rsc  Resource to count
      */
     void (*count)(pcmk_resource_t *rsc);
 
     /*!
      * \internal
      * \brief Check whether a given resource is in a list of resources
      *
      * \param[in] rsc           Resource ID to check for
      * \param[in] only_rsc      List of resource IDs to check
      * \param[in] check_parent  If TRUE, check top ancestor as well
      *
      * \return TRUE if \p rsc, its top parent if requested, or '*' is in
      *         \p only_rsc, otherwise FALSE
      */
     gboolean (*is_filtered)(const pcmk_resource_t *rsc, GList *only_rsc,
                             gboolean check_parent);
 
     /*!
      * \internal
      * \brief Find a node (and optionally count all) where resource is active
      *
      * \param[in]  rsc          Resource to check
      * \param[out] count_all    If not NULL, set this to count of active nodes
      * \param[out] count_clean  If not NULL, set this to count of clean nodes
      *
      * \return A node where the resource is active, preferring the source node
      *         if the resource is involved in a partial migration, or a clean,
      *         online node if the resource's \c PCMK_META_REQUIRES is
      *         \c PCMK_VALUE_QUORUM or \c PCMK_VALUE_NOTHING, otherwise \c NULL.
      */
     pcmk_node_t *(*active_node)(const pcmk_resource_t *rsc,
                                 unsigned int *count_all,
                                 unsigned int *count_clean);
 
     /*!
      * \internal
      * \brief Get maximum resource instances per node
      *
      * \param[in] rsc  Resource to check
      *
      * \return Maximum number of \p rsc instances that can be active on one node
      */
     unsigned int (*max_per_node)(const pcmk_resource_t *rsc);
 } pcmk__rsc_methods_t;
 
 // Implementation of pcmk__resource_private_t
 struct pcmk__resource_private {
     const pcmk__rsc_methods_t *fns;         // Resource object methods
+    const pcmk_assignment_methods_t *cmds;  // Resource assignment methods
 };
 
 const char *pcmk__multiply_active_text(enum rsc_recovery_type recovery);
 
 /*!
  * \internal
  * \brief Get node where resource is currently active (if any)
  *
  * \param[in] rsc  Resource to check
  *
  * \return Node that \p rsc is active on, if any, otherwise NULL
  */
 static inline pcmk_node_t *
 pcmk__current_node(const pcmk_resource_t *rsc)
 {
     if (rsc == NULL) {
         return NULL;
     }
     return rsc->private->fns->active_node(rsc, NULL, NULL);
 }
 
 #ifdef __cplusplus
 }
 #endif
 
 #endif // PCMK__CRM_COMMON_RESOURCES_INTERNAL__H
diff --git a/lib/pacemaker/pcmk_graph_producer.c b/lib/pacemaker/pcmk_graph_producer.c
index 6cb287a595..364a51f390 100644
--- a/lib/pacemaker/pcmk_graph_producer.c
+++ b/lib/pacemaker/pcmk_graph_producer.c
@@ -1,1097 +1,1097 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <sys/param.h>
 #include <crm/crm.h>
 #include <crm/cib.h>
 #include <crm/common/xml.h>
 
 #include <glib.h>
 
 #include <pacemaker-internal.h>
 
 #include "libpacemaker_private.h"
 
 // Convenience macros for logging action properties
 
 #define action_type_str(flags) \
     (pcmk_is_set((flags), pcmk_action_pseudo)? "pseudo-action" : "action")
 
 #define action_optional_str(flags) \
     (pcmk_is_set((flags), pcmk_action_optional)? "optional" : "required")
 
 #define action_runnable_str(flags) \
     (pcmk_is_set((flags), pcmk_action_runnable)? "runnable" : "unrunnable")
 
 #define action_node_str(a) \
     (((a)->node == NULL)? "no node" : (a)->node->details->uname)
 
 /*!
  * \internal
  * \brief Add an XML node tag for a specified ID
  *
  * \param[in]     id      Node UUID to add
  * \param[in,out] xml     Parent XML tag to add to
  */
 static xmlNode*
 add_node_to_xml_by_id(const char *id, xmlNode *xml)
 {
     xmlNode *node_xml;
 
     node_xml = pcmk__xe_create(xml, PCMK_XE_NODE);
     crm_xml_add(node_xml, PCMK_XA_ID, id);
 
     return node_xml;
 }
 
 /*!
  * \internal
  * \brief Add an XML node tag for a specified node
  *
  * \param[in]     node  Node to add
  * \param[in,out] xml   XML to add node to
  */
 static void
 add_node_to_xml(const pcmk_node_t *node, void *xml)
 {
     add_node_to_xml_by_id(node->details->id, (xmlNode *) xml);
 }
 
 /*!
  * \internal
  * \brief Count (optionally add to XML) nodes needing maintenance state update
  *
  * \param[in,out] xml        Parent XML tag to add to, if any
  * \param[in]     scheduler  Scheduler data
  *
  * \return Count of nodes added
  * \note Only Pacemaker Remote nodes are considered currently
  */
 static int
 add_maintenance_nodes(xmlNode *xml, const pcmk_scheduler_t *scheduler)
 {
     xmlNode *maintenance = NULL;
     int count = 0;
 
     if (xml != NULL) {
         maintenance = pcmk__xe_create(xml, PCMK__XE_MAINTENANCE);
     }
     for (const GList *iter = scheduler->nodes;
          iter != NULL; iter = iter->next) {
         const pcmk_node_t *node = iter->data;
 
         if (pcmk__is_pacemaker_remote_node(node) &&
             (node->details->maintenance != node->details->remote_maintenance)) {
 
             if (maintenance != NULL) {
                 crm_xml_add(add_node_to_xml_by_id(node->details->id,
                                                   maintenance),
                             PCMK__XA_NODE_IN_MAINTENANCE,
                             (node->details->maintenance? "1" : "0"));
             }
             count++;
         }
     }
     crm_trace("%s %d nodes in need of maintenance mode update in state",
               ((maintenance == NULL)? "Counted" : "Added"), count);
     return count;
 }
 
 /*!
  * \internal
  * \brief Add pseudo action with nodes needing maintenance state update
  *
  * \param[in,out] scheduler  Scheduler data
  */
 static void
 add_maintenance_update(pcmk_scheduler_t *scheduler)
 {
     pcmk_action_t *action = NULL;
 
     if (add_maintenance_nodes(NULL, scheduler) != 0) {
         action = get_pseudo_op(PCMK_ACTION_MAINTENANCE_NODES, scheduler);
         pcmk__set_action_flags(action, pcmk_action_always_in_graph);
     }
 }
 
 /*!
  * \internal
  * \brief Add XML with nodes that an action is expected to bring down
  *
  * If a specified action is expected to bring any nodes down, add an XML block
  * with their UUIDs. When a node is lost, this allows the controller to
  * determine whether it was expected.
  *
  * \param[in,out] xml       Parent XML tag to add to
  * \param[in]     action    Action to check for downed nodes
  */
 static void
 add_downed_nodes(xmlNode *xml, const pcmk_action_t *action)
 {
     CRM_CHECK((xml != NULL) && (action != NULL) && (action->node != NULL),
               return);
 
     if (pcmk__str_eq(action->task, PCMK_ACTION_DO_SHUTDOWN, pcmk__str_none)) {
 
         /* Shutdown makes the action's node down */
         xmlNode *downed = pcmk__xe_create(xml, PCMK__XE_DOWNED);
         add_node_to_xml_by_id(action->node->details->id, downed);
 
     } else if (pcmk__str_eq(action->task, PCMK_ACTION_STONITH,
                             pcmk__str_none)) {
 
         /* Fencing makes the action's node and any hosted guest nodes down */
         const char *fence = g_hash_table_lookup(action->meta,
                                                 PCMK__META_STONITH_ACTION);
 
         if (pcmk__is_fencing_action(fence)) {
             xmlNode *downed = pcmk__xe_create(xml, PCMK__XE_DOWNED);
             add_node_to_xml_by_id(action->node->details->id, downed);
             pe_foreach_guest_node(action->node->details->data_set,
                                   action->node, add_node_to_xml, downed);
         }
 
     } else if (action->rsc && action->rsc->is_remote_node
                && pcmk__str_eq(action->task, PCMK_ACTION_STOP,
                                pcmk__str_none)) {
 
         /* Stopping a remote connection resource makes connected node down,
          * unless it's part of a migration
          */
         GList *iter;
         pcmk_action_t *input;
         bool migrating = false;
 
         for (iter = action->actions_before; iter != NULL; iter = iter->next) {
             input = ((pcmk__related_action_t *) iter->data)->action;
             if ((input->rsc != NULL)
                 && pcmk__str_eq(action->rsc->id, input->rsc->id, pcmk__str_none)
                 && pcmk__str_eq(input->task, PCMK_ACTION_MIGRATE_FROM,
                                 pcmk__str_none)) {
                 migrating = true;
                 break;
             }
         }
         if (!migrating) {
             xmlNode *downed = pcmk__xe_create(xml, PCMK__XE_DOWNED);
             add_node_to_xml_by_id(action->rsc->id, downed);
         }
     }
 }
 
 /*!
  * \internal
  * \brief Create a transition graph operation key for a clone action
  *
  * \param[in] action       Clone action
  * \param[in] interval_ms  Action interval in milliseconds
  *
  * \return Newly allocated string with transition graph operation key
  */
 static char *
 clone_op_key(const pcmk_action_t *action, guint interval_ms)
 {
     if (pcmk__str_eq(action->task, PCMK_ACTION_NOTIFY, pcmk__str_none)) {
         const char *n_type = g_hash_table_lookup(action->meta, "notify_type");
         const char *n_task = g_hash_table_lookup(action->meta,
                                                  "notify_operation");
 
         CRM_LOG_ASSERT((n_type != NULL) && (n_task != NULL));
         return pcmk__notify_key(action->rsc->clone_name, n_type, n_task);
 
     } else if (action->cancel_task != NULL) {
         return pcmk__op_key(action->rsc->clone_name, action->cancel_task,
                             interval_ms);
     } else {
         return pcmk__op_key(action->rsc->clone_name, action->task, interval_ms);
     }
 }
 
 /*!
  * \internal
  * \brief Add node details to transition graph action XML
  *
  * \param[in]     action  Scheduled action
  * \param[in,out] xml     Transition graph action XML for \p action
  */
 static void
 add_node_details(const pcmk_action_t *action, xmlNode *xml)
 {
     pcmk_node_t *router_node = pcmk__connection_host_for_action(action);
 
     crm_xml_add(xml, PCMK__META_ON_NODE, action->node->details->uname);
     crm_xml_add(xml, PCMK__META_ON_NODE_UUID, action->node->details->id);
     if (router_node != NULL) {
         crm_xml_add(xml, PCMK__XA_ROUTER_NODE, router_node->details->uname);
     }
 }
 
 /*!
  * \internal
  * \brief Add resource details to transition graph action XML
  *
  * \param[in]     action      Scheduled action
  * \param[in,out] action_xml  Transition graph action XML for \p action
  */
 static void
 add_resource_details(const pcmk_action_t *action, xmlNode *action_xml)
 {
     xmlNode *rsc_xml = NULL;
     const char *attr_list[] = {
         PCMK_XA_CLASS,
         PCMK_XA_PROVIDER,
         PCMK_XA_TYPE,
     };
 
     /* If a resource is locked to a node via PCMK_OPT_SHUTDOWN_LOCK, mark its
      * actions so the controller can preserve the lock when the action
      * completes.
      */
     if (pcmk__action_locks_rsc_to_node(action)) {
         crm_xml_add_ll(action_xml, PCMK_OPT_SHUTDOWN_LOCK,
                        (long long) action->rsc->lock_time);
     }
 
     // List affected resource
 
     rsc_xml = pcmk__xe_create(action_xml,
                               (const char *) action->rsc->xml->name);
     if (pcmk_is_set(action->rsc->flags, pcmk_rsc_removed)
         && (action->rsc->clone_name != NULL)) {
         /* Use the numbered instance name here, because if there is more
          * than one instance on a node, we need to make sure the command
          * goes to the right one.
          *
          * This is important even for anonymous clones, because the clone's
          * unique meta-attribute might have just been toggled from on to
          * off.
          */
         crm_debug("Using orphan clone name %s instead of %s",
                   action->rsc->id, action->rsc->clone_name);
         crm_xml_add(rsc_xml, PCMK_XA_ID, action->rsc->clone_name);
         crm_xml_add(rsc_xml, PCMK__XA_LONG_ID, action->rsc->id);
 
     } else if (!pcmk_is_set(action->rsc->flags, pcmk_rsc_unique)) {
         const char *xml_id = pcmk__xe_id(action->rsc->xml);
 
         crm_debug("Using anonymous clone name %s for %s (aka %s)",
                   xml_id, action->rsc->id, action->rsc->clone_name);
 
         /* ID is what we'd like client to use
          * LONG_ID is what they might know it as instead
          *
          * LONG_ID is only strictly needed /here/ during the
          * transition period until all nodes in the cluster
          * are running the new software /and/ have rebooted
          * once (meaning that they've only ever spoken to a DC
          * supporting this feature).
          *
          * If anyone toggles the unique flag to 'on', the
          * 'instance free' name will correspond to an orphan
          * and fall into the clause above instead
          */
         crm_xml_add(rsc_xml, PCMK_XA_ID, xml_id);
         if ((action->rsc->clone_name != NULL)
             && !pcmk__str_eq(xml_id, action->rsc->clone_name,
                              pcmk__str_none)) {
             crm_xml_add(rsc_xml, PCMK__XA_LONG_ID, action->rsc->clone_name);
         } else {
             crm_xml_add(rsc_xml, PCMK__XA_LONG_ID, action->rsc->id);
         }
 
     } else {
         CRM_ASSERT(action->rsc->clone_name == NULL);
         crm_xml_add(rsc_xml, PCMK_XA_ID, action->rsc->id);
     }
 
     for (int lpc = 0; lpc < PCMK__NELEM(attr_list); lpc++) {
         crm_xml_add(rsc_xml, attr_list[lpc],
                     g_hash_table_lookup(action->rsc->meta, attr_list[lpc]));
     }
 }
 
 /*!
  * \internal
  * \brief Add action attributes to transition graph action XML
  *
  * \param[in,out] action      Scheduled action
  * \param[in,out] action_xml  Transition graph action XML for \p action
  */
 static void
 add_action_attributes(pcmk_action_t *action, xmlNode *action_xml)
 {
     xmlNode *args_xml = NULL;
 
     /* We create free-standing XML to start, so we can sort the attributes
      * before adding it to action_xml, which keeps the scheduler regression
      * test graphs comparable.
      */
     args_xml = pcmk__xe_create(NULL, PCMK__XE_ATTRIBUTES);
 
     crm_xml_add(args_xml, PCMK_XA_CRM_FEATURE_SET, CRM_FEATURE_SET);
     g_hash_table_foreach(action->extra, hash2field, args_xml);
 
     if ((action->rsc != NULL) && (action->node != NULL)) {
         // Get the resource instance attributes, evaluated properly for node
         GHashTable *params = pe_rsc_params(action->rsc, action->node,
                                            action->rsc->cluster);
 
         pcmk__substitute_remote_addr(action->rsc, params);
 
         g_hash_table_foreach(params, hash2smartfield, args_xml);
 
     } else if ((action->rsc != NULL)
                && (action->rsc->variant <= pcmk_rsc_variant_primitive)) {
         GHashTable *params = pe_rsc_params(action->rsc, NULL,
                                            action->rsc->cluster);
 
         g_hash_table_foreach(params, hash2smartfield, args_xml);
     }
 
     g_hash_table_foreach(action->meta, hash2metafield, args_xml);
     if (action->rsc != NULL) {
         pcmk_resource_t *parent = action->rsc;
 
         while (parent != NULL) {
-            parent->cmds->add_graph_meta(parent, args_xml);
+            parent->private->cmds->add_graph_meta(parent, args_xml);
             parent = parent->parent;
         }
 
         pcmk__add_guest_meta_to_xml(args_xml, action);
 
     } else if (pcmk__str_eq(action->task, PCMK_ACTION_STONITH, pcmk__str_none)
                && (action->node != NULL)) {
         /* Pass the node's attributes as meta-attributes.
          *
          * @TODO: Determine whether it is still necessary to do this. It was
          * added in 33d99707, probably for the libfence-based implementation in
          * c9a90bd, which is no longer used.
          */
         g_hash_table_foreach(action->node->details->attrs, hash2metafield,
                              args_xml);
     }
 
     sorted_xml(args_xml, action_xml, FALSE);
     pcmk__xml_free(args_xml);
 }
 
 /*!
  * \internal
  * \brief Create the transition graph XML for a scheduled action
  *
  * \param[in,out] parent        Parent XML element to add action to
  * \param[in,out] action        Scheduled action
  * \param[in]     skip_details  If false, add action details as sub-elements
  * \param[in]     scheduler     Scheduler data
  */
 static void
 create_graph_action(xmlNode *parent, pcmk_action_t *action, bool skip_details,
                     const pcmk_scheduler_t *scheduler)
 {
     bool needs_node_info = true;
     bool needs_maintenance_info = false;
     xmlNode *action_xml = NULL;
 
     if ((action == NULL) || (scheduler == NULL)) {
         return;
     }
 
     // Create the top-level element based on task
 
     if (pcmk__str_eq(action->task, PCMK_ACTION_STONITH, pcmk__str_none)) {
         /* All fences need node info; guest node fences are pseudo-events */
         if (pcmk_is_set(action->flags, pcmk_action_pseudo)) {
             action_xml = pcmk__xe_create(parent, PCMK__XE_PSEUDO_EVENT);
         } else {
             action_xml = pcmk__xe_create(parent, PCMK__XE_CRM_EVENT);
         }
 
     } else if (pcmk__str_any_of(action->task,
                                 PCMK_ACTION_DO_SHUTDOWN,
                                 PCMK_ACTION_CLEAR_FAILCOUNT, NULL)) {
         action_xml = pcmk__xe_create(parent, PCMK__XE_CRM_EVENT);
 
     } else if (pcmk__str_eq(action->task, PCMK_ACTION_LRM_DELETE,
                             pcmk__str_none)) {
         // CIB-only clean-up for shutdown locks
         action_xml = pcmk__xe_create(parent, PCMK__XE_CRM_EVENT);
         crm_xml_add(action_xml, PCMK__XA_MODE, PCMK__VALUE_CIB);
 
     } else if (pcmk_is_set(action->flags, pcmk_action_pseudo)) {
         if (pcmk__str_eq(action->task, PCMK_ACTION_MAINTENANCE_NODES,
                          pcmk__str_none)) {
             needs_maintenance_info = true;
         }
         action_xml = pcmk__xe_create(parent, PCMK__XE_PSEUDO_EVENT);
         needs_node_info = false;
 
     } else {
         action_xml = pcmk__xe_create(parent, PCMK__XE_RSC_OP);
     }
 
     crm_xml_add_int(action_xml, PCMK_XA_ID, action->id);
     crm_xml_add(action_xml, PCMK_XA_OPERATION, action->task);
 
     if ((action->rsc != NULL) && (action->rsc->clone_name != NULL)) {
         char *clone_key = NULL;
         guint interval_ms;
 
         if (pcmk__guint_from_hash(action->meta, PCMK_META_INTERVAL, 0,
                                   &interval_ms) != pcmk_rc_ok) {
             interval_ms = 0;
         }
         clone_key = clone_op_key(action, interval_ms);
         crm_xml_add(action_xml, PCMK__XA_OPERATION_KEY, clone_key);
         crm_xml_add(action_xml, "internal_" PCMK__XA_OPERATION_KEY,
                     action->uuid);
         free(clone_key);
     } else {
         crm_xml_add(action_xml, PCMK__XA_OPERATION_KEY, action->uuid);
     }
 
     if (needs_node_info && (action->node != NULL)) {
         add_node_details(action, action_xml);
         pcmk__insert_dup(action->meta, PCMK__META_ON_NODE,
                          action->node->details->uname);
         pcmk__insert_dup(action->meta, PCMK__META_ON_NODE_UUID,
                          action->node->details->id);
     }
 
     if (skip_details) {
         return;
     }
 
     if ((action->rsc != NULL)
         && !pcmk_is_set(action->flags, pcmk_action_pseudo)) {
 
         // This is a real resource action, so add resource details
         add_resource_details(action, action_xml);
     }
 
     /* List any attributes in effect */
     add_action_attributes(action, action_xml);
 
     /* List any nodes this action is expected to make down */
     if (needs_node_info && (action->node != NULL)) {
         add_downed_nodes(action_xml, action);
     }
 
     if (needs_maintenance_info) {
         add_maintenance_nodes(action_xml, scheduler);
     }
 }
 
 /*!
  * \internal
  * \brief Check whether an action should be added to the transition graph
  *
  * \param[in] action  Action to check
  *
  * \return true if action should be added to graph, otherwise false
  */
 static bool
 should_add_action_to_graph(const pcmk_action_t *action)
 {
     if (!pcmk_is_set(action->flags, pcmk_action_runnable)) {
         crm_trace("Ignoring action %s (%d): unrunnable",
                   action->uuid, action->id);
         return false;
     }
 
     if (pcmk_is_set(action->flags, pcmk_action_optional)
         && !pcmk_is_set(action->flags, pcmk_action_always_in_graph)) {
         crm_trace("Ignoring action %s (%d): optional",
                   action->uuid, action->id);
         return false;
     }
 
     /* Actions for unmanaged resources should be excluded from the graph,
      * with the exception of monitors and cancellation of recurring monitors.
      */
     if ((action->rsc != NULL)
         && !pcmk_is_set(action->rsc->flags, pcmk_rsc_managed)
         && !pcmk__str_eq(action->task, PCMK_ACTION_MONITOR, pcmk__str_none)) {
 
         const char *interval_ms_s;
 
         /* A cancellation of a recurring monitor will get here because the task
          * is cancel rather than monitor, but the interval can still be used to
          * recognize it. The interval has been normalized to milliseconds by
          * this point, so a string comparison is sufficient.
          */
         interval_ms_s = g_hash_table_lookup(action->meta, PCMK_META_INTERVAL);
         if (pcmk__str_eq(interval_ms_s, "0", pcmk__str_null_matches)) {
             crm_trace("Ignoring action %s (%d): for unmanaged resource (%s)",
                       action->uuid, action->id, action->rsc->id);
             return false;
         }
     }
 
     /* Always add pseudo-actions, fence actions, and shutdown actions (already
      * determined to be required and runnable by this point)
      */
     if (pcmk_is_set(action->flags, pcmk_action_pseudo)
         || pcmk__strcase_any_of(action->task, PCMK_ACTION_STONITH,
                                 PCMK_ACTION_DO_SHUTDOWN, NULL)) {
         return true;
     }
 
     if (action->node == NULL) {
         pcmk__sched_err("Skipping action %s (%d) "
                         "because it was not assigned to a node (bug?)",
                         action->uuid, action->id);
         pcmk__log_action("Unassigned", action, false);
         return false;
     }
 
     if (pcmk_is_set(action->flags, pcmk_action_on_dc)) {
         crm_trace("Action %s (%d) should be dumped: "
                   "can run on DC instead of %s",
                   action->uuid, action->id, pcmk__node_name(action->node));
 
     } else if (pcmk__is_guest_or_bundle_node(action->node)
                && !action->node->details->remote_requires_reset) {
         crm_trace("Action %s (%d) should be dumped: "
                   "assuming will be runnable on guest %s",
                   action->uuid, action->id, pcmk__node_name(action->node));
 
     } else if (!action->node->details->online) {
         pcmk__sched_err("Skipping action %s (%d) "
                         "because it was scheduled for offline node (bug?)",
                         action->uuid, action->id);
         pcmk__log_action("Offline node", action, false);
         return false;
 
     } else if (action->node->details->unclean) {
         pcmk__sched_err("Skipping action %s (%d) "
                         "because it was scheduled for unclean node (bug?)",
                         action->uuid, action->id);
         pcmk__log_action("Unclean node", action, false);
         return false;
     }
     return true;
 }
 
 /*!
  * \internal
  * \brief Check whether an ordering's flags can change an action
  *
  * \param[in] ordering  Ordering to check
  *
  * \return true if ordering has flags that can change an action, false otherwise
  */
 static bool
 ordering_can_change_actions(const pcmk__related_action_t *ordering)
 {
     return pcmk_any_flags_set(ordering->type,
                               ~(pcmk__ar_then_implies_first_graphed
                                 |pcmk__ar_first_implies_then_graphed
                                 |pcmk__ar_ordered));
 }
 
 /*!
  * \internal
  * \brief Check whether an action input should be in the transition graph
  *
  * \param[in]     action  Action to check
  * \param[in,out] input   Action input to check
  *
  * \return true if input should be in graph, false otherwise
  * \note This function may not only check an input, but disable it under certian
  *       circumstances (load or anti-colocation orderings that are not needed).
  */
 static bool
 should_add_input_to_graph(const pcmk_action_t *action,
                           pcmk__related_action_t *input)
 {
     if (input->state == pe_link_dumped) {
         return true;
     }
 
     if ((uint32_t) input->type == pcmk__ar_none) {
         crm_trace("Ignoring %s (%d) input %s (%d): "
                   "ordering disabled",
                   action->uuid, action->id,
                   input->action->uuid, input->action->id);
         return false;
 
     } else if (!pcmk_is_set(input->action->flags, pcmk_action_runnable)
                && !ordering_can_change_actions(input)) {
         crm_trace("Ignoring %s (%d) input %s (%d): "
                   "optional and input unrunnable",
                   action->uuid, action->id,
                   input->action->uuid, input->action->id);
         return false;
 
     } else if (!pcmk_is_set(input->action->flags, pcmk_action_runnable)
                && pcmk_is_set(input->type, pcmk__ar_min_runnable)) {
         crm_trace("Ignoring %s (%d) input %s (%d): "
                   "minimum number of instances required but input unrunnable",
                   action->uuid, action->id,
                   input->action->uuid, input->action->id);
         return false;
 
     } else if (pcmk_is_set(input->type, pcmk__ar_unmigratable_then_blocks)
                && !pcmk_is_set(input->action->flags, pcmk_action_runnable)) {
         crm_trace("Ignoring %s (%d) input %s (%d): "
                   "input blocked if 'then' unmigratable",
                   action->uuid, action->id,
                   input->action->uuid, input->action->id);
         return false;
 
     } else if (pcmk_is_set(input->type, pcmk__ar_if_first_unmigratable)
                && pcmk_is_set(input->action->flags, pcmk_action_migratable)) {
         crm_trace("Ignoring %s (%d) input %s (%d): ordering applies "
                   "only if input is unmigratable, but it is migratable",
                   action->uuid, action->id,
                   input->action->uuid, input->action->id);
         return false;
 
     } else if (((uint32_t) input->type == pcmk__ar_ordered)
                && pcmk_is_set(input->action->flags, pcmk_action_migratable)
                && pcmk__ends_with(input->action->uuid, "_stop_0")) {
         crm_trace("Ignoring %s (%d) input %s (%d): "
                   "optional but stop in migration",
                   action->uuid, action->id,
                   input->action->uuid, input->action->id);
         return false;
 
     } else if ((uint32_t) input->type == pcmk__ar_if_on_same_node_or_target) {
         pcmk_node_t *input_node = input->action->node;
 
         if ((action->rsc != NULL)
             && pcmk__str_eq(action->task, PCMK_ACTION_MIGRATE_TO,
                             pcmk__str_none)) {
 
             pcmk_node_t *assigned = action->rsc->allocated_to;
 
             /* For load_stopped -> migrate_to orderings, we care about where
              * the resource has been assigned, not where migrate_to will be
              * executed.
              */
             if (!pcmk__same_node(input_node, assigned)) {
                 crm_trace("Ignoring %s (%d) input %s (%d): "
                           "migration target %s is not same as input node %s",
                           action->uuid, action->id,
                           input->action->uuid, input->action->id,
                           (assigned? assigned->details->uname : "<none>"),
                           (input_node? input_node->details->uname : "<none>"));
                 input->type = (enum pe_ordering) pcmk__ar_none;
                 return false;
             }
 
         } else if (!pcmk__same_node(input_node, action->node)) {
             crm_trace("Ignoring %s (%d) input %s (%d): "
                       "not on same node (%s vs %s)",
                       action->uuid, action->id,
                       input->action->uuid, input->action->id,
                       (action->node? action->node->details->uname : "<none>"),
                       (input_node? input_node->details->uname : "<none>"));
             input->type = (enum pe_ordering) pcmk__ar_none;
             return false;
 
         } else if (pcmk_is_set(input->action->flags, pcmk_action_optional)) {
             crm_trace("Ignoring %s (%d) input %s (%d): "
                       "ordering optional",
                       action->uuid, action->id,
                       input->action->uuid, input->action->id);
             input->type = (enum pe_ordering) pcmk__ar_none;
             return false;
         }
 
     } else if ((uint32_t) input->type == pcmk__ar_if_required_on_same_node) {
         if (input->action->node && action->node
             && !pcmk__same_node(input->action->node, action->node)) {
             crm_trace("Ignoring %s (%d) input %s (%d): "
                       "not on same node (%s vs %s)",
                       action->uuid, action->id,
                       input->action->uuid, input->action->id,
                       pcmk__node_name(action->node),
                       pcmk__node_name(input->action->node));
             input->type = (enum pe_ordering) pcmk__ar_none;
             return false;
 
         } else if (pcmk_is_set(input->action->flags, pcmk_action_optional)) {
             crm_trace("Ignoring %s (%d) input %s (%d): optional",
                       action->uuid, action->id,
                       input->action->uuid, input->action->id);
             input->type = (enum pe_ordering) pcmk__ar_none;
             return false;
         }
 
     } else if (input->action->rsc
                && input->action->rsc != action->rsc
                && pcmk_is_set(input->action->rsc->flags, pcmk_rsc_failed)
                && !pcmk_is_set(input->action->rsc->flags, pcmk_rsc_managed)
                && pcmk__ends_with(input->action->uuid, "_stop_0")
                && pcmk__is_clone(action->rsc)) {
         crm_warn("Ignoring requirement that %s complete before %s:"
                  " unmanaged failed resources cannot prevent clone shutdown",
                  input->action->uuid, action->uuid);
         return false;
 
     } else if (pcmk_is_set(input->action->flags, pcmk_action_optional)
                && !pcmk_any_flags_set(input->action->flags,
                                       pcmk_action_always_in_graph
                                       |pcmk_action_added_to_graph)
                && !should_add_action_to_graph(input->action)) {
         crm_trace("Ignoring %s (%d) input %s (%d): "
                   "input optional",
                   action->uuid, action->id,
                   input->action->uuid, input->action->id);
         return false;
     }
 
     crm_trace("%s (%d) input %s %s (%d) on %s should be dumped: %s %s %#.6x",
               action->uuid, action->id, action_type_str(input->action->flags),
               input->action->uuid, input->action->id,
               action_node_str(input->action),
               action_runnable_str(input->action->flags),
               action_optional_str(input->action->flags), input->type);
     return true;
 }
 
 /*!
  * \internal
  * \brief Check whether an ordering creates an ordering loop
  *
  * \param[in]     init_action  "First" action in ordering
  * \param[in]     action       Callers should always set this the same as
  *                             \p init_action (this function may use a different
  *                             value for recursive calls)
  * \param[in,out] input        Action wrapper for "then" action in ordering
  *
  * \return true if the ordering creates a loop, otherwise false
  */
 bool
 pcmk__graph_has_loop(const pcmk_action_t *init_action,
                      const pcmk_action_t *action, pcmk__related_action_t *input)
 {
     bool has_loop = false;
 
     if (pcmk_is_set(input->action->flags, pcmk_action_detect_loop)) {
         crm_trace("Breaking tracking loop: %s@%s -> %s@%s (%#.6x)",
                   input->action->uuid,
                   input->action->node? input->action->node->details->uname : "",
                   action->uuid,
                   action->node? action->node->details->uname : "",
                   input->type);
         return false;
     }
 
     // Don't need to check inputs that won't be used
     if (!should_add_input_to_graph(action, input)) {
         return false;
     }
 
     if (input->action == init_action) {
         crm_debug("Input loop found in %s@%s ->...-> %s@%s",
                   action->uuid,
                   action->node? action->node->details->uname : "",
                   init_action->uuid,
                   init_action->node? init_action->node->details->uname : "");
         return true;
     }
 
     pcmk__set_action_flags(input->action, pcmk_action_detect_loop);
 
     crm_trace("Checking inputs of action %s@%s input %s@%s (%#.6x)"
               "for graph loop with %s@%s ",
               action->uuid,
               action->node? action->node->details->uname : "",
               input->action->uuid,
               input->action->node? input->action->node->details->uname : "",
               input->type,
               init_action->uuid,
               init_action->node? init_action->node->details->uname : "");
 
     // Recursively check input itself for loops
     for (GList *iter = input->action->actions_before;
          iter != NULL; iter = iter->next) {
 
         if (pcmk__graph_has_loop(init_action, input->action,
                                  (pcmk__related_action_t *) iter->data)) {
             // Recursive call already logged a debug message
             has_loop = true;
             break;
         }
     }
 
     pcmk__clear_action_flags(input->action, pcmk_action_detect_loop);
 
     if (!has_loop) {
         crm_trace("No input loop found in %s@%s -> %s@%s (%#.6x)",
                   input->action->uuid,
                   input->action->node? input->action->node->details->uname : "",
                   action->uuid,
                   action->node? action->node->details->uname : "",
                   input->type);
     }
     return has_loop;
 }
 
 /*!
  * \internal
  * \brief Create a synapse XML element for a transition graph
  *
  * \param[in]     action     Action that synapse is for
  * \param[in,out] scheduler  Scheduler data containing graph
  *
  * \return Newly added XML element for new graph synapse
  */
 static xmlNode *
 create_graph_synapse(const pcmk_action_t *action, pcmk_scheduler_t *scheduler)
 {
     int synapse_priority = 0;
     xmlNode *syn = pcmk__xe_create(scheduler->graph, "synapse");
 
     crm_xml_add_int(syn, PCMK_XA_ID, scheduler->num_synapse);
     scheduler->num_synapse++;
 
     if (action->rsc != NULL) {
         synapse_priority = action->rsc->priority;
     }
     if (action->priority > synapse_priority) {
         synapse_priority = action->priority;
     }
     if (synapse_priority > 0) {
         crm_xml_add_int(syn, PCMK__XA_PRIORITY, synapse_priority);
     }
     return syn;
 }
 
 /*!
  * \internal
  * \brief Add an action to the transition graph XML if appropriate
  *
  * \param[in,out] data       Action to possibly add
  * \param[in,out] user_data  Scheduler data
  *
  * \note This will de-duplicate the action inputs, meaning that the
  *       pcmk__related_action_t:type flags can no longer be relied on to retain
  *       their original settings. That means this MUST be called after
  *       pcmk__apply_orderings() is complete, and nothing after this should rely
  *       on those type flags. (For example, some code looks for type equal to
  *       some flag rather than whether the flag is set, and some code looks for
  *       particular combinations of flags -- such code must be done before
  *       pcmk__create_graph().)
  */
 static void
 add_action_to_graph(gpointer data, gpointer user_data)
 {
     pcmk_action_t *action = (pcmk_action_t *) data;
     pcmk_scheduler_t *scheduler = (pcmk_scheduler_t *) user_data;
 
     xmlNode *syn = NULL;
     xmlNode *set = NULL;
     xmlNode *in = NULL;
 
     /* If we haven't already, de-duplicate inputs (even if we won't be adding
      * the action to the graph, so that crm_simulate's dot graphs don't have
      * duplicates).
      */
     if (!pcmk_is_set(action->flags, pcmk_action_inputs_deduplicated)) {
         pcmk__deduplicate_action_inputs(action);
         pcmk__set_action_flags(action, pcmk_action_inputs_deduplicated);
     }
 
     if (pcmk_is_set(action->flags, pcmk_action_added_to_graph)
         || !should_add_action_to_graph(action)) {
         return; // Already added, or shouldn't be
     }
     pcmk__set_action_flags(action, pcmk_action_added_to_graph);
 
     crm_trace("Adding action %d (%s%s%s) to graph",
               action->id, action->uuid,
               ((action->node == NULL)? "" : " on "),
               ((action->node == NULL)? "" : action->node->details->uname));
 
     syn = create_graph_synapse(action, scheduler);
     set = pcmk__xe_create(syn, "action_set");
     in = pcmk__xe_create(syn, "inputs");
 
     create_graph_action(set, action, false, scheduler);
 
     for (GList *lpc = action->actions_before; lpc != NULL; lpc = lpc->next) {
         pcmk__related_action_t *input = lpc->data;
 
         if (should_add_input_to_graph(action, input)) {
             xmlNode *input_xml = pcmk__xe_create(in, "trigger");
 
             input->state = pe_link_dumped;
             create_graph_action(input_xml, input->action, true, scheduler);
         }
     }
 }
 
 static int transition_id = -1;
 
 /*!
  * \internal
  * \brief Log a message after calculating a transition
  *
  * \param[in] filename  Where transition input is stored
  */
 void
 pcmk__log_transition_summary(const char *filename)
 {
     if (was_processing_error || crm_config_error) {
         crm_err("Calculated transition %d (with errors)%s%s",
                 transition_id,
                 (filename == NULL)? "" : ", saving inputs in ",
                 (filename == NULL)? "" : filename);
 
     } else if (was_processing_warning || crm_config_warning) {
         crm_warn("Calculated transition %d (with warnings)%s%s",
                  transition_id,
                  (filename == NULL)? "" : ", saving inputs in ",
                  (filename == NULL)? "" : filename);
 
     } else {
         crm_notice("Calculated transition %d%s%s",
                    transition_id,
                    (filename == NULL)? "" : ", saving inputs in ",
                    (filename == NULL)? "" : filename);
     }
     if (crm_config_error) {
         crm_notice("Configuration errors found during scheduler processing,"
                    "  please run \"crm_verify -L\" to identify issues");
     }
 }
 
 /*!
  * \internal
  * \brief Add a resource's actions to the transition graph
  *
  * \param[in,out] rsc  Resource whose actions should be added
  */
 void
 pcmk__add_rsc_actions_to_graph(pcmk_resource_t *rsc)
 {
     GList *iter = NULL;
 
     CRM_ASSERT(rsc != NULL);
     pcmk__rsc_trace(rsc, "Adding actions for %s to graph", rsc->id);
 
     // First add the resource's own actions
     g_list_foreach(rsc->actions, add_action_to_graph, rsc->cluster);
 
     // Then recursively add its children's actions (appropriate to variant)
     for (iter = rsc->children; iter != NULL; iter = iter->next) {
         pcmk_resource_t *child_rsc = (pcmk_resource_t *) iter->data;
 
-        child_rsc->cmds->add_actions_to_graph(child_rsc);
+        child_rsc->private->cmds->add_actions_to_graph(child_rsc);
     }
 }
 
 /*!
  * \internal
  * \brief Create a transition graph with all cluster actions needed
  *
  * \param[in,out] scheduler  Scheduler data
  */
 void
 pcmk__create_graph(pcmk_scheduler_t *scheduler)
 {
     GList *iter = NULL;
     const char *value = NULL;
     long long limit = 0LL;
     GHashTable *config_hash = scheduler->config_hash;
 
     transition_id++;
     crm_trace("Creating transition graph %d", transition_id);
 
     scheduler->graph = pcmk__xe_create(NULL, PCMK__XE_TRANSITION_GRAPH);
 
     value = pcmk__cluster_option(config_hash, PCMK_OPT_CLUSTER_DELAY);
     crm_xml_add(scheduler->graph, PCMK_OPT_CLUSTER_DELAY, value);
 
     value = pcmk__cluster_option(config_hash, PCMK_OPT_STONITH_TIMEOUT);
     crm_xml_add(scheduler->graph, PCMK_OPT_STONITH_TIMEOUT, value);
 
     crm_xml_add(scheduler->graph, "failed-stop-offset", "INFINITY");
 
     if (pcmk_is_set(scheduler->flags, pcmk_sched_start_failure_fatal)) {
         crm_xml_add(scheduler->graph, "failed-start-offset", "INFINITY");
     } else {
         crm_xml_add(scheduler->graph, "failed-start-offset", "1");
     }
 
     value = pcmk__cluster_option(config_hash, PCMK_OPT_BATCH_LIMIT);
     crm_xml_add(scheduler->graph, PCMK_OPT_BATCH_LIMIT, value);
 
     crm_xml_add_int(scheduler->graph, "transition_id", transition_id);
 
     value = pcmk__cluster_option(config_hash, PCMK_OPT_MIGRATION_LIMIT);
     if ((pcmk__scan_ll(value, &limit, 0LL) == pcmk_rc_ok) && (limit > 0)) {
         crm_xml_add(scheduler->graph, PCMK_OPT_MIGRATION_LIMIT, value);
     }
 
     if (scheduler->recheck_by > 0) {
         char *recheck_epoch = NULL;
 
         recheck_epoch = crm_strdup_printf("%llu",
                                           (long long) scheduler->recheck_by);
         crm_xml_add(scheduler->graph, "recheck-by", recheck_epoch);
         free(recheck_epoch);
     }
 
     /* The following code will de-duplicate action inputs, so nothing past this
      * should rely on the action input type flags retaining their original
      * values.
      */
 
     // Add resource actions to graph
     for (iter = scheduler->resources; iter != NULL; iter = iter->next) {
         pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data;
 
         pcmk__rsc_trace(rsc, "Processing actions for %s", rsc->id);
-        rsc->cmds->add_actions_to_graph(rsc);
+        rsc->private->cmds->add_actions_to_graph(rsc);
     }
 
     // Add pseudo-action for list of nodes with maintenance state update
     add_maintenance_update(scheduler);
 
     // Add non-resource (node) actions
     for (iter = scheduler->actions; iter != NULL; iter = iter->next) {
         pcmk_action_t *action = (pcmk_action_t *) iter->data;
 
         if ((action->rsc != NULL)
             && (action->node != NULL)
             && action->node->details->shutdown
             && !pcmk_is_set(action->rsc->flags, pcmk_rsc_maintenance)
             && !pcmk_any_flags_set(action->flags,
                                    pcmk_action_optional|pcmk_action_runnable)
             && pcmk__str_eq(action->task, PCMK_ACTION_STOP, pcmk__str_none)) {
             /* Eventually we should just ignore the 'fence' case, but for now
              * it's the best way to detect (in CTS) when CIB resource updates
              * are being lost.
              */
             if (pcmk_is_set(scheduler->flags, pcmk_sched_quorate)
                 || (scheduler->no_quorum_policy == pcmk_no_quorum_ignore)) {
                 const bool managed = pcmk_is_set(action->rsc->flags,
                                                  pcmk_rsc_managed);
                 const bool failed = pcmk_is_set(action->rsc->flags,
                                                 pcmk_rsc_failed);
 
                 crm_crit("Cannot %s %s because of %s:%s%s (%s)",
                          action->node->details->unclean? "fence" : "shut down",
                          pcmk__node_name(action->node), action->rsc->id,
                          (managed? " blocked" : " unmanaged"),
                          (failed? " failed" : ""), action->uuid);
             }
         }
 
         add_action_to_graph((gpointer) action, (gpointer) scheduler);
     }
 
     crm_log_xml_trace(scheduler->graph, "graph");
 }
diff --git a/lib/pacemaker/pcmk_output.c b/lib/pacemaker/pcmk_output.c
index 12a6710571..c332facadf 100644
--- a/lib/pacemaker/pcmk_output.c
+++ b/lib/pacemaker/pcmk_output.c
@@ -1,2686 +1,2686 @@
 /*
  * Copyright 2019-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 #include <crm/common/output.h>
 #include <crm/common/results.h>
 #include <crm/common/xml.h>
 #include <crm/stonith-ng.h>
 #include <crm/fencing/internal.h>   // stonith__*
 #include <crm/pengine/internal.h>
 #include <libxml/tree.h>
 #include <pacemaker-internal.h>
 
 #include <inttypes.h>
 #include <stdint.h>
 
 static char *
 colocations_header(pcmk_resource_t *rsc, pcmk__colocation_t *cons,
                    bool dependents) {
     char *retval = NULL;
 
     if (cons->primary_role > pcmk_role_started) {
         retval = crm_strdup_printf("%s (score=%s, %s role=%s, id=%s)",
                                    rsc->id, pcmk_readable_score(cons->score),
                                    (dependents? "needs" : "with"),
                                    pcmk_role_text(cons->primary_role),
                                    cons->id);
     } else {
         retval = crm_strdup_printf("%s (score=%s, id=%s)",
                                    rsc->id, pcmk_readable_score(cons->score),
                                    cons->id);
     }
     return retval;
 }
 
 static void
 colocations_xml_node(pcmk__output_t *out, pcmk_resource_t *rsc,
                      pcmk__colocation_t *cons) {
     xmlNodePtr node = NULL;
 
     node = pcmk__output_create_xml_node(out, PCMK_XE_RSC_COLOCATION,
                                         PCMK_XA_ID, cons->id,
                                         PCMK_XA_RSC, cons->dependent->id,
                                         PCMK_XA_WITH_RSC, cons->primary->id,
                                         PCMK_XA_SCORE,
                                         pcmk_readable_score(cons->score),
                                         NULL);
 
     if (cons->node_attribute) {
         xmlSetProp(node, (pcmkXmlStr) PCMK_XA_NODE_ATTRIBUTE,
                    (pcmkXmlStr) cons->node_attribute);
     }
 
     if (cons->dependent_role != pcmk_role_unknown) {
         xmlSetProp(node, (pcmkXmlStr) PCMK_XA_RSC_ROLE,
                    (pcmkXmlStr) pcmk_role_text(cons->dependent_role));
     }
 
     if (cons->primary_role != pcmk_role_unknown) {
         xmlSetProp(node, (pcmkXmlStr) PCMK_XA_WITH_RSC_ROLE,
                    (pcmkXmlStr) pcmk_role_text(cons->primary_role));
     }
 }
 
 static int
 do_locations_list_xml(pcmk__output_t *out, pcmk_resource_t *rsc,
                       bool add_header)
 {
     GList *lpc = NULL;
     GList *list = rsc->rsc_location;
     int rc = pcmk_rc_no_output;
 
     for (lpc = list; lpc != NULL; lpc = lpc->next) {
         pcmk__location_t *cons = lpc->data;
 
         GList *lpc2 = NULL;
 
         for (lpc2 = cons->nodes; lpc2 != NULL; lpc2 = lpc2->next) {
             pcmk_node_t *node = (pcmk_node_t *) lpc2->data;
 
             if (add_header) {
                 PCMK__OUTPUT_LIST_HEADER(out, false, rc, "locations");
             }
 
             pcmk__output_create_xml_node(out, PCMK_XE_RSC_LOCATION,
                                          PCMK_XA_NODE, node->details->uname,
                                          PCMK_XA_RSC, rsc->id,
                                          PCMK_XA_ID, cons->id,
                                          PCMK_XA_SCORE,
                                          pcmk_readable_score(node->weight),
                                          NULL);
         }
     }
 
     if (add_header) {
         PCMK__OUTPUT_LIST_FOOTER(out, rc);
     }
 
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("rsc-action-item", "const char *", "pcmk_resource_t *",
                   "pcmk_node_t *", "pcmk_node_t *", "pcmk_action_t *",
                   "pcmk_action_t *")
 static int
 rsc_action_item(pcmk__output_t *out, va_list args)
 {
     const char *change = va_arg(args, const char *);
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     pcmk_node_t *origin = va_arg(args, pcmk_node_t *);
     pcmk_node_t *destination = va_arg(args, pcmk_node_t *);
     pcmk_action_t *action = va_arg(args, pcmk_action_t *);
     pcmk_action_t *source = va_arg(args, pcmk_action_t *);
 
     int len = 0;
     char *reason = NULL;
     char *details = NULL;
     bool same_host = false;
     bool same_role = false;
     bool need_role = false;
 
     static int rsc_width = 5;
     static int detail_width = 5;
 
     CRM_ASSERT(action);
     CRM_ASSERT(destination != NULL || origin != NULL);
 
     if (source == NULL) {
         source = action;
     }
 
     len = strlen(rsc->id);
     if (len > rsc_width) {
         rsc_width = len + 2;
     }
 
     if ((rsc->role > pcmk_role_started)
         || (rsc->next_role > pcmk_role_unpromoted)) {
         need_role = true;
     }
 
     if (pcmk__same_node(origin, destination)) {
         same_host = true;
     }
 
     if (rsc->role == rsc->next_role) {
         same_role = true;
     }
 
     if (need_role && (origin == NULL)) {
         /* Starting and promoting a promotable clone instance */
         details = crm_strdup_printf("%s -> %s %s", pcmk_role_text(rsc->role),
                                     pcmk_role_text(rsc->next_role),
                                     pcmk__node_name(destination));
 
     } else if (origin == NULL) {
         /* Starting a resource */
         details = crm_strdup_printf("%s", pcmk__node_name(destination));
 
     } else if (need_role && (destination == NULL)) {
         /* Stopping a promotable clone instance */
         details = crm_strdup_printf("%s %s", pcmk_role_text(rsc->role),
                                     pcmk__node_name(origin));
 
     } else if (destination == NULL) {
         /* Stopping a resource */
         details = crm_strdup_printf("%s", pcmk__node_name(origin));
 
     } else if (need_role && same_role && same_host) {
         /* Recovering, restarting or re-promoting a promotable clone instance */
         details = crm_strdup_printf("%s %s", pcmk_role_text(rsc->role),
                                     pcmk__node_name(origin));
 
     } else if (same_role && same_host) {
         /* Recovering or Restarting a normal resource */
         details = crm_strdup_printf("%s", pcmk__node_name(origin));
 
     } else if (need_role && same_role) {
         /* Moving a promotable clone instance */
         details = crm_strdup_printf("%s -> %s %s", pcmk__node_name(origin),
                                     pcmk__node_name(destination),
                                     pcmk_role_text(rsc->role));
 
     } else if (same_role) {
         /* Moving a normal resource */
         details = crm_strdup_printf("%s -> %s", pcmk__node_name(origin),
                                     pcmk__node_name(destination));
 
     } else if (same_host) {
         /* Promoting or demoting a promotable clone instance */
         details = crm_strdup_printf("%s -> %s %s", pcmk_role_text(rsc->role),
                                     pcmk_role_text(rsc->next_role),
                                     pcmk__node_name(origin));
 
     } else {
         /* Moving and promoting/demoting */
         details = crm_strdup_printf("%s %s -> %s %s",
                                     pcmk_role_text(rsc->role),
                                     pcmk__node_name(origin),
                                     pcmk_role_text(rsc->next_role),
                                     pcmk__node_name(destination));
     }
 
     len = strlen(details);
     if (len > detail_width) {
         detail_width = len;
     }
 
     if ((source->reason != NULL)
         && !pcmk_is_set(action->flags, pcmk_action_runnable)) {
         reason = crm_strdup_printf("due to %s (blocked)", source->reason);
 
     } else if (source->reason) {
         reason = crm_strdup_printf("due to %s", source->reason);
 
     } else if (!pcmk_is_set(action->flags, pcmk_action_runnable)) {
         reason = strdup("blocked");
 
     }
 
     out->list_item(out, NULL, "%-8s   %-*s   ( %*s )%s%s",
                    change, rsc_width, rsc->id, detail_width, details,
                    ((reason == NULL)? "" : "  "), pcmk__s(reason, ""));
 
     free(details);
     free(reason);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("rsc-action-item", "const char *", "pcmk_resource_t *",
                   "pcmk_node_t *", "pcmk_node_t *", "pcmk_action_t *",
                   "pcmk_action_t *")
 static int
 rsc_action_item_xml(pcmk__output_t *out, va_list args)
 {
     const char *change = va_arg(args, const char *);
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     pcmk_node_t *origin = va_arg(args, pcmk_node_t *);
     pcmk_node_t *destination = va_arg(args, pcmk_node_t *);
     pcmk_action_t *action = va_arg(args, pcmk_action_t *);
     pcmk_action_t *source = va_arg(args, pcmk_action_t *);
 
     char *change_str = NULL;
 
     bool same_host = false;
     bool same_role = false;
     bool need_role = false;
     xmlNode *xml = NULL;
 
     CRM_ASSERT(action);
     CRM_ASSERT(destination != NULL || origin != NULL);
 
     if (source == NULL) {
         source = action;
     }
 
     if ((rsc->role > pcmk_role_started)
         || (rsc->next_role > pcmk_role_unpromoted)) {
         need_role = true;
     }
 
     if (pcmk__same_node(origin, destination)) {
         same_host = true;
     }
 
     if (rsc->role == rsc->next_role) {
         same_role = true;
     }
 
     change_str = g_ascii_strdown(change, -1);
     xml = pcmk__output_create_xml_node(out, PCMK_XE_RSC_ACTION,
                                        PCMK_XA_ACTION, change_str,
                                        PCMK_XA_RESOURCE, rsc->id,
                                        NULL);
     g_free(change_str);
 
     if (need_role && (origin == NULL)) {
         /* Starting and promoting a promotable clone instance */
         pcmk__xe_set_props(xml,
                            PCMK_XA_ROLE, pcmk_role_text(rsc->role),
                            PCMK_XA_NEXT_ROLE, pcmk_role_text(rsc->next_role),
                            PCMK_XA_DEST, destination->details->uname,
                            NULL);
 
     } else if (origin == NULL) {
         /* Starting a resource */
         crm_xml_add(xml, PCMK_XA_NODE, destination->details->uname);
 
     } else if (need_role && (destination == NULL)) {
         /* Stopping a promotable clone instance */
         pcmk__xe_set_props(xml,
                            PCMK_XA_ROLE, pcmk_role_text(rsc->role),
                            PCMK_XA_NODE, origin->details->uname,
                            NULL);
 
     } else if (destination == NULL) {
         /* Stopping a resource */
         crm_xml_add(xml, PCMK_XA_NODE, origin->details->uname);
 
     } else if (need_role && same_role && same_host) {
         /* Recovering, restarting or re-promoting a promotable clone instance */
         pcmk__xe_set_props(xml,
                            PCMK_XA_ROLE, pcmk_role_text(rsc->role),
                            PCMK_XA_SOURCE, origin->details->uname,
                            NULL);
 
     } else if (same_role && same_host) {
         /* Recovering or Restarting a normal resource */
         crm_xml_add(xml, PCMK_XA_SOURCE, origin->details->uname);
 
     } else if (need_role && same_role) {
         /* Moving a promotable clone instance */
         pcmk__xe_set_props(xml,
                            PCMK_XA_SOURCE, origin->details->uname,
                            PCMK_XA_DEST, destination->details->uname,
                            PCMK_XA_ROLE, pcmk_role_text(rsc->role),
                            NULL);
 
     } else if (same_role) {
         /* Moving a normal resource */
         pcmk__xe_set_props(xml,
                            PCMK_XA_SOURCE, origin->details->uname,
                            PCMK_XA_DEST, destination->details->uname,
                            NULL);
 
     } else if (same_host) {
         /* Promoting or demoting a promotable clone instance */
         pcmk__xe_set_props(xml,
                            PCMK_XA_ROLE, pcmk_role_text(rsc->role),
                            PCMK_XA_NEXT_ROLE, pcmk_role_text(rsc->next_role),
                            PCMK_XA_SOURCE, origin->details->uname,
                            NULL);
 
     } else {
         /* Moving and promoting/demoting */
         pcmk__xe_set_props(xml,
                            PCMK_XA_ROLE, pcmk_role_text(rsc->role),
                            PCMK_XA_SOURCE, origin->details->uname,
                            PCMK_XA_NEXT_ROLE, pcmk_role_text(rsc->next_role),
                            PCMK_XA_DEST, destination->details->uname,
                            NULL);
     }
 
     if ((source->reason != NULL)
         && !pcmk_is_set(action->flags, pcmk_action_runnable)) {
         pcmk__xe_set_props(xml,
                            PCMK_XA_REASON, source->reason,
                            PCMK_XA_BLOCKED, PCMK_VALUE_TRUE,
                            NULL);
 
     } else if (source->reason != NULL) {
         crm_xml_add(xml, PCMK_XA_REASON, source->reason);
 
     } else if (!pcmk_is_set(action->flags, pcmk_action_runnable)) {
         pcmk__xe_set_bool_attr(xml, PCMK_XA_BLOCKED, true);
 
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("rsc-is-colocated-with-list", "pcmk_resource_t *", "bool")
 static int
 rsc_is_colocated_with_list(pcmk__output_t *out, va_list args) {
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     bool recursive = va_arg(args, int);
 
     int rc = pcmk_rc_no_output;
 
     if (pcmk_is_set(rsc->flags, pcmk_rsc_detect_loop)) {
         return rc;
     }
 
     /* We're listing constraints explicitly involving rsc, so use rsc->rsc_cons
-     * directly rather than rsc->cmds->this_with_colocations().
+     * directly rather than rsc->private->cmds->this_with_colocations().
      */
     pcmk__set_rsc_flags(rsc, pcmk_rsc_detect_loop);
     for (GList *lpc = rsc->rsc_cons; lpc != NULL; lpc = lpc->next) {
         pcmk__colocation_t *cons = (pcmk__colocation_t *) lpc->data;
         char *hdr = NULL;
 
         PCMK__OUTPUT_LIST_HEADER(out, false, rc,
                                  "Resources %s is colocated with", rsc->id);
 
         if (pcmk_is_set(cons->primary->flags, pcmk_rsc_detect_loop)) {
             out->list_item(out, NULL, "%s (id=%s - loop)",
                            cons->primary->id, cons->id);
             continue;
         }
 
         hdr = colocations_header(cons->primary, cons, false);
         out->list_item(out, NULL, "%s", hdr);
         free(hdr);
 
         // Empty list header for indentation of information about this resource
         out->begin_list(out, NULL, NULL, NULL);
 
         out->message(out, "locations-list", cons->primary);
         if (recursive) {
             out->message(out, "rsc-is-colocated-with-list",
                          cons->primary, recursive);
         }
 
         out->end_list(out);
     }
 
     PCMK__OUTPUT_LIST_FOOTER(out, rc);
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("rsc-is-colocated-with-list", "pcmk_resource_t *", "bool")
 static int
 rsc_is_colocated_with_list_xml(pcmk__output_t *out, va_list args) {
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     bool recursive = va_arg(args, int);
 
     int rc = pcmk_rc_no_output;
 
     if (pcmk_is_set(rsc->flags, pcmk_rsc_detect_loop)) {
         return rc;
     }
 
     /* We're listing constraints explicitly involving rsc, so use rsc->rsc_cons
-     * directly rather than rsc->cmds->this_with_colocations().
+     * directly rather than rsc->private->cmds->this_with_colocations().
      */
     pcmk__set_rsc_flags(rsc, pcmk_rsc_detect_loop);
     for (GList *lpc = rsc->rsc_cons; lpc != NULL; lpc = lpc->next) {
         pcmk__colocation_t *cons = (pcmk__colocation_t *) lpc->data;
 
         if (pcmk_is_set(cons->primary->flags, pcmk_rsc_detect_loop)) {
             colocations_xml_node(out, cons->primary, cons);
             continue;
         }
 
         colocations_xml_node(out, cons->primary, cons);
         do_locations_list_xml(out, cons->primary, false);
 
         if (recursive) {
             out->message(out, "rsc-is-colocated-with-list",
                          cons->primary, recursive);
         }
     }
 
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("rscs-colocated-with-list", "pcmk_resource_t *", "bool")
 static int
 rscs_colocated_with_list(pcmk__output_t *out, va_list args) {
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     bool recursive = va_arg(args, int);
 
     int rc = pcmk_rc_no_output;
 
     if (pcmk_is_set(rsc->flags, pcmk_rsc_detect_loop)) {
         return rc;
     }
 
     /* We're listing constraints explicitly involving rsc, so use
      * rsc->rsc_cons_lhs directly rather than
-     * rsc->cmds->with_this_colocations().
+     * rsc->private->cmds->with_this_colocations().
      */
     pcmk__set_rsc_flags(rsc, pcmk_rsc_detect_loop);
     for (GList *lpc = rsc->rsc_cons_lhs; lpc != NULL; lpc = lpc->next) {
         pcmk__colocation_t *cons = (pcmk__colocation_t *) lpc->data;
         char *hdr = NULL;
 
         PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Resources colocated with %s",
                                  rsc->id);
 
         if (pcmk_is_set(cons->dependent->flags, pcmk_rsc_detect_loop)) {
             out->list_item(out, NULL, "%s (id=%s - loop)",
                            cons->dependent->id, cons->id);
             continue;
         }
 
         hdr = colocations_header(cons->dependent, cons, true);
         out->list_item(out, NULL, "%s", hdr);
         free(hdr);
 
         // Empty list header for indentation of information about this resource
         out->begin_list(out, NULL, NULL, NULL);
 
         out->message(out, "locations-list", cons->dependent);
         if (recursive) {
             out->message(out, "rscs-colocated-with-list",
                          cons->dependent, recursive);
         }
 
         out->end_list(out);
     }
 
     PCMK__OUTPUT_LIST_FOOTER(out, rc);
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("rscs-colocated-with-list", "pcmk_resource_t *", "bool")
 static int
 rscs_colocated_with_list_xml(pcmk__output_t *out, va_list args) {
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     bool recursive = va_arg(args, int);
 
     int rc = pcmk_rc_no_output;
 
     if (pcmk_is_set(rsc->flags, pcmk_rsc_detect_loop)) {
         return rc;
     }
 
     /* We're listing constraints explicitly involving rsc, so use
      * rsc->rsc_cons_lhs directly rather than
-     * rsc->cmds->with_this_colocations().
+     * rsc->private->cmds->with_this_colocations().
      */
     pcmk__set_rsc_flags(rsc, pcmk_rsc_detect_loop);
     for (GList *lpc = rsc->rsc_cons_lhs; lpc != NULL; lpc = lpc->next) {
         pcmk__colocation_t *cons = (pcmk__colocation_t *) lpc->data;
 
         if (pcmk_is_set(cons->dependent->flags, pcmk_rsc_detect_loop)) {
             colocations_xml_node(out, cons->dependent, cons);
             continue;
         }
 
         colocations_xml_node(out, cons->dependent, cons);
         do_locations_list_xml(out, cons->dependent, false);
 
         if (recursive) {
             out->message(out, "rscs-colocated-with-list",
                          cons->dependent, recursive);
         }
     }
 
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("locations-list", "pcmk_resource_t *")
 static int
 locations_list(pcmk__output_t *out, va_list args) {
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
 
     GList *lpc = NULL;
     GList *list = rsc->rsc_location;
     int rc = pcmk_rc_no_output;
 
     for (lpc = list; lpc != NULL; lpc = lpc->next) {
         pcmk__location_t *cons = lpc->data;
 
         GList *lpc2 = NULL;
 
         for (lpc2 = cons->nodes; lpc2 != NULL; lpc2 = lpc2->next) {
             pcmk_node_t *node = (pcmk_node_t *) lpc2->data;
 
             PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Locations");
             out->list_item(out, NULL, "Node %s (score=%s, id=%s, rsc=%s)",
                            pcmk__node_name(node),
                            pcmk_readable_score(node->weight), cons->id,
                            rsc->id);
         }
     }
 
     PCMK__OUTPUT_LIST_FOOTER(out, rc);
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("locations-list", "pcmk_resource_t *")
 static int
 locations_list_xml(pcmk__output_t *out, va_list args) {
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     return do_locations_list_xml(out, rsc, true);
 }
 
 PCMK__OUTPUT_ARGS("locations-and-colocations", "pcmk_resource_t *",
                   "bool", "bool")
 static int
 locations_and_colocations(pcmk__output_t *out, va_list args)
 {
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     bool recursive = va_arg(args, int);
     bool force = va_arg(args, int);
 
     pcmk__unpack_constraints(rsc->cluster);
 
     // Constraints apply to group/clone, not member/instance
     if (!force) {
         rsc = uber_parent(rsc);
     }
 
     out->message(out, "locations-list", rsc);
 
     pe__clear_resource_flags_on_all(rsc->cluster, pcmk_rsc_detect_loop);
     out->message(out, "rscs-colocated-with-list", rsc, recursive);
 
     pe__clear_resource_flags_on_all(rsc->cluster, pcmk_rsc_detect_loop);
     out->message(out, "rsc-is-colocated-with-list", rsc, recursive);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("locations-and-colocations", "pcmk_resource_t *",
                   "bool", "bool")
 static int
 locations_and_colocations_xml(pcmk__output_t *out, va_list args)
 {
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     bool recursive = va_arg(args, int);
     bool force = va_arg(args, int);
 
     pcmk__unpack_constraints(rsc->cluster);
 
     // Constraints apply to group/clone, not member/instance
     if (!force) {
         rsc = uber_parent(rsc);
     }
 
     pcmk__output_xml_create_parent(out, PCMK_XE_CONSTRAINTS, NULL);
     do_locations_list_xml(out, rsc, false);
 
     pe__clear_resource_flags_on_all(rsc->cluster, pcmk_rsc_detect_loop);
     out->message(out, "rscs-colocated-with-list", rsc, recursive);
 
     pe__clear_resource_flags_on_all(rsc->cluster, pcmk_rsc_detect_loop);
     out->message(out, "rsc-is-colocated-with-list", rsc, recursive);
 
     pcmk__output_xml_pop_parent(out);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("health", "const char *", "const char *", "const char *",
                   "const char *")
 static int
 health(pcmk__output_t *out, va_list args)
 {
     const char *sys_from G_GNUC_UNUSED = va_arg(args, const char *);
     const char *host_from = va_arg(args, const char *);
     const char *fsa_state = va_arg(args, const char *);
     const char *result = va_arg(args, const char *);
 
     return out->info(out, "Controller on %s in state %s: %s",
                      pcmk__s(host_from, "unknown node"),
                      pcmk__s(fsa_state, "unknown"),
                      pcmk__s(result, "unknown result"));
 }
 
 PCMK__OUTPUT_ARGS("health", "const char *", "const char *", "const char *",
                   "const char *")
 static int
 health_text(pcmk__output_t *out, va_list args)
 {
     if (!out->is_quiet(out)) {
         return health(out, args);
     } else {
         const char *sys_from G_GNUC_UNUSED = va_arg(args, const char *);
         const char *host_from G_GNUC_UNUSED = va_arg(args, const char *);
         const char *fsa_state = va_arg(args, const char *);
         const char *result G_GNUC_UNUSED = va_arg(args, const char *);
 
         if (fsa_state != NULL) {
             pcmk__formatted_printf(out, "%s\n", fsa_state);
             return pcmk_rc_ok;
         }
     }
 
     return pcmk_rc_no_output;
 }
 
 PCMK__OUTPUT_ARGS("health", "const char *", "const char *", "const char *",
                   "const char *")
 static int
 health_xml(pcmk__output_t *out, va_list args)
 {
     const char *sys_from = va_arg(args, const char *);
     const char *host_from = va_arg(args, const char *);
     const char *fsa_state = va_arg(args, const char *);
     const char *result = va_arg(args, const char *);
 
     pcmk__output_create_xml_node(out, pcmk__s(sys_from, ""),
                                  PCMK_XA_NODE_NAME, pcmk__s(host_from, ""),
                                  PCMK_XA_STATE, pcmk__s(fsa_state, ""),
                                  PCMK_XA_RESULT, pcmk__s(result, ""),
                                  NULL);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("pacemakerd-health", "const char *",
                   "enum pcmk_pacemakerd_state", "const char *", "time_t")
 static int
 pacemakerd_health(pcmk__output_t *out, va_list args)
 {
     const char *sys_from = va_arg(args, const char *);
     enum pcmk_pacemakerd_state state =
         (enum pcmk_pacemakerd_state) va_arg(args, int);
     const char *state_s = va_arg(args, const char *);
     time_t last_updated = va_arg(args, time_t);
 
     char *last_updated_s = NULL;
     int rc = pcmk_rc_ok;
 
     if (sys_from == NULL) {
         if (state == pcmk_pacemakerd_state_remote) {
             sys_from = "pacemaker-remoted";
         } else {
             sys_from = CRM_SYSTEM_MCP;
         }
     }
 
     if (state_s == NULL) {
         state_s = pcmk__pcmkd_state_enum2friendly(state);
     }
 
     if (last_updated != 0) {
         last_updated_s = pcmk__epoch2str(&last_updated,
                                          crm_time_log_date
                                          |crm_time_log_timeofday
                                          |crm_time_log_with_timezone);
     }
 
     rc = out->info(out, "Status of %s: '%s' (last updated %s)",
                    sys_from, state_s,
                    pcmk__s(last_updated_s, "at unknown time"));
 
     free(last_updated_s);
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("pacemakerd-health", "const char *",
                   "enum pcmk_pacemakerd_state", "const char *", "time_t")
 static int
 pacemakerd_health_html(pcmk__output_t *out, va_list args)
 {
     const char *sys_from = va_arg(args, const char *);
     enum pcmk_pacemakerd_state state =
         (enum pcmk_pacemakerd_state) va_arg(args, int);
     const char *state_s = va_arg(args, const char *);
     time_t last_updated = va_arg(args, time_t);
 
     char *last_updated_s = NULL;
     char *msg = NULL;
 
     if (sys_from == NULL) {
         if (state == pcmk_pacemakerd_state_remote) {
             sys_from = "pacemaker-remoted";
         } else {
             sys_from = CRM_SYSTEM_MCP;
         }
     }
 
     if (state_s == NULL) {
         state_s = pcmk__pcmkd_state_enum2friendly(state);
     }
 
     if (last_updated != 0) {
         last_updated_s = pcmk__epoch2str(&last_updated,
                                          crm_time_log_date
                                          |crm_time_log_timeofday
                                          |crm_time_log_with_timezone);
     }
 
     msg = crm_strdup_printf("Status of %s: '%s' (last updated %s)",
                             sys_from, state_s,
                             pcmk__s(last_updated_s, "at unknown time"));
     pcmk__output_create_html_node(out, "li", NULL, NULL, msg);
 
     free(msg);
     free(last_updated_s);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("pacemakerd-health", "const char *",
                   "enum pcmk_pacemakerd_state", "const char *", "time_t")
 static int
 pacemakerd_health_text(pcmk__output_t *out, va_list args)
 {
     if (!out->is_quiet(out)) {
         return pacemakerd_health(out, args);
     } else {
         const char *sys_from G_GNUC_UNUSED = va_arg(args, const char *);
         enum pcmk_pacemakerd_state state =
             (enum pcmk_pacemakerd_state) va_arg(args, int);
         const char *state_s = va_arg(args, const char *);
         time_t last_updated G_GNUC_UNUSED = va_arg(args, time_t);
 
         if (state_s == NULL) {
             state_s = pcmk_pacemakerd_api_daemon_state_enum2text(state);
         }
         pcmk__formatted_printf(out, "%s\n", state_s);
         return pcmk_rc_ok;
     }
 }
 
 PCMK__OUTPUT_ARGS("pacemakerd-health", "const char *",
                   "enum pcmk_pacemakerd_state", "const char *", "time_t")
 static int
 pacemakerd_health_xml(pcmk__output_t *out, va_list args)
 {
     const char *sys_from = va_arg(args, const char *);
     enum pcmk_pacemakerd_state state =
         (enum pcmk_pacemakerd_state) va_arg(args, int);
     const char *state_s = va_arg(args, const char *);
     time_t last_updated = va_arg(args, time_t);
 
     char *last_updated_s = NULL;
 
     if (sys_from == NULL) {
         if (state == pcmk_pacemakerd_state_remote) {
             sys_from = "pacemaker-remoted";
         } else {
             sys_from = CRM_SYSTEM_MCP;
         }
     }
 
     if (state_s == NULL) {
         state_s = pcmk_pacemakerd_api_daemon_state_enum2text(state);
     }
 
     if (last_updated != 0) {
         last_updated_s = pcmk__epoch2str(&last_updated,
                                          crm_time_log_date
                                          |crm_time_log_timeofday
                                          |crm_time_log_with_timezone);
     }
 
     pcmk__output_create_xml_node(out, PCMK_XE_PACEMAKERD,
                                  PCMK_XA_SYS_FROM, sys_from,
                                  PCMK_XA_STATE, state_s,
                                  PCMK_XA_LAST_UPDATED, last_updated_s,
                                  NULL);
     free(last_updated_s);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("profile", "const char *", "clock_t", "clock_t")
 static int
 profile_default(pcmk__output_t *out, va_list args) {
     const char *xml_file = va_arg(args, const char *);
     clock_t start = va_arg(args, clock_t);
     clock_t end = va_arg(args, clock_t);
 
     out->list_item(out, NULL, "Testing %s ... %.2f secs", xml_file,
                    (end - start) / (float) CLOCKS_PER_SEC);
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("profile", "const char *", "clock_t", "clock_t")
 static int
 profile_xml(pcmk__output_t *out, va_list args) {
     const char *xml_file = va_arg(args, const char *);
     clock_t start = va_arg(args, clock_t);
     clock_t end = va_arg(args, clock_t);
 
     char *duration = pcmk__ftoa((end - start) / (float) CLOCKS_PER_SEC);
 
     pcmk__output_create_xml_node(out, PCMK_XE_TIMING,
                                  PCMK_XA_FILE, xml_file,
                                  PCMK_XA_DURATION, duration,
                                  NULL);
 
     free(duration);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("dc", "const char *")
 static int
 dc(pcmk__output_t *out, va_list args)
 {
     const char *dc = va_arg(args, const char *);
 
     return out->info(out, "Designated Controller is: %s",
                      pcmk__s(dc, "not yet elected"));
 }
 
 PCMK__OUTPUT_ARGS("dc", "const char *")
 static int
 dc_text(pcmk__output_t *out, va_list args)
 {
     if (!out->is_quiet(out)) {
         return dc(out, args);
     } else {
         const char *dc = va_arg(args, const char *);
 
         if (dc != NULL) {
             pcmk__formatted_printf(out, "%s\n", pcmk__s(dc, ""));
             return pcmk_rc_ok;
         }
     }
 
     return pcmk_rc_no_output;
 }
 
 PCMK__OUTPUT_ARGS("dc", "const char *")
 static int
 dc_xml(pcmk__output_t *out, va_list args)
 {
     const char *dc = va_arg(args, const char *);
 
     pcmk__output_create_xml_node(out, PCMK_XE_DC,
                                  PCMK_XA_NODE_NAME, pcmk__s(dc, ""),
                                  NULL);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("crmadmin-node", "const char *", "const char *",
                   "const char *", "bool")
 static int
 crmadmin_node(pcmk__output_t *out, va_list args)
 {
     const char *type = va_arg(args, const char *);
     const char *name = va_arg(args, const char *);
     const char *id = va_arg(args, const char *);
     bool bash_export = va_arg(args, int);
 
     if (bash_export) {
         return out->info(out, "export %s=%s",
                          pcmk__s(name, "<null>"), pcmk__s(id, ""));
     } else {
         return out->info(out, "%s node: %s (%s)", type ? type : "cluster",
                          pcmk__s(name, "<null>"), pcmk__s(id, "<null>"));
     }
 }
 
 PCMK__OUTPUT_ARGS("crmadmin-node", "const char *", "const char *",
                   "const char *", "bool")
 static int
 crmadmin_node_text(pcmk__output_t *out, va_list args)
 {
     if (!out->is_quiet(out)) {
         return crmadmin_node(out, args);
     } else {
         const char *type G_GNUC_UNUSED = va_arg(args, const char *);
         const char *name = va_arg(args, const char *);
         const char *id G_GNUC_UNUSED = va_arg(args, const char *);
         bool bash_export G_GNUC_UNUSED = va_arg(args, int);
 
         pcmk__formatted_printf(out, "%s\n", pcmk__s(name, "<null>"));
         return pcmk_rc_ok;
     }
 }
 
 PCMK__OUTPUT_ARGS("crmadmin-node", "const char *", "const char *",
                   "const char *", "bool")
 static int
 crmadmin_node_xml(pcmk__output_t *out, va_list args)
 {
     const char *type = va_arg(args, const char *);
     const char *name = va_arg(args, const char *);
     const char *id = va_arg(args, const char *);
     bool bash_export G_GNUC_UNUSED = va_arg(args, int);
 
     pcmk__output_create_xml_node(out, PCMK_XE_NODE,
                                  PCMK_XA_TYPE, pcmk__s(type, "cluster"),
                                  PCMK_XA_NAME, pcmk__s(name, ""),
                                  PCMK_XA_ID, pcmk__s(id, ""),
                                  NULL);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("digests", "const pcmk_resource_t *", "const pcmk_node_t *",
                   "const char *", "guint", "const pcmk__op_digest_t *")
 static int
 digests_text(pcmk__output_t *out, va_list args)
 {
     const pcmk_resource_t *rsc = va_arg(args, const pcmk_resource_t *);
     const pcmk_node_t *node = va_arg(args, const pcmk_node_t *);
     const char *task = va_arg(args, const char *);
     guint interval_ms = va_arg(args, guint);
     const pcmk__op_digest_t *digests = va_arg(args, const pcmk__op_digest_t *);
 
     char *action_desc = NULL;
     const char *rsc_desc = "unknown resource";
     const char *node_desc = "unknown node";
 
     if (interval_ms != 0) {
         action_desc = crm_strdup_printf("%ums-interval %s action", interval_ms,
                                         ((task == NULL)? "unknown" : task));
     } else if (pcmk__str_eq(task, PCMK_ACTION_MONITOR, pcmk__str_none)) {
         action_desc = strdup("probe action");
     } else {
         action_desc = crm_strdup_printf("%s action",
                                         ((task == NULL)? "unknown" : task));
     }
     if ((rsc != NULL) && (rsc->id != NULL)) {
         rsc_desc = rsc->id;
     }
     if ((node != NULL) && (node->details->uname != NULL)) {
         node_desc = node->details->uname;
     }
     out->begin_list(out, NULL, NULL, "Digests for %s %s on %s",
                     rsc_desc, action_desc, node_desc);
     free(action_desc);
 
     if (digests == NULL) {
         out->list_item(out, NULL, "none");
         out->end_list(out);
         return pcmk_rc_ok;
     }
     if (digests->digest_all_calc != NULL) {
         out->list_item(out, NULL, "%s (all parameters)",
                        digests->digest_all_calc);
     }
     if (digests->digest_secure_calc != NULL) {
         out->list_item(out, NULL, "%s (non-private parameters)",
                        digests->digest_secure_calc);
     }
     if (digests->digest_restart_calc != NULL) {
         out->list_item(out, NULL, "%s (non-reloadable parameters)",
                        digests->digest_restart_calc);
     }
     out->end_list(out);
     return pcmk_rc_ok;
 }
 
 static void
 add_digest_xml(xmlNode *parent, const char *type, const char *digest,
                xmlNode *digest_source)
 {
     if (digest != NULL) {
         xmlNodePtr digest_xml = pcmk__xe_create(parent, PCMK_XE_DIGEST);
 
         crm_xml_add(digest_xml, PCMK_XA_TYPE, pcmk__s(type, "unspecified"));
         crm_xml_add(digest_xml, PCMK_XA_HASH, digest);
         pcmk__xml_copy(digest_xml, digest_source);
     }
 }
 
 PCMK__OUTPUT_ARGS("digests", "const pcmk_resource_t *", "const pcmk_node_t *",
                   "const char *", "guint", "const pcmk__op_digest_t *")
 static int
 digests_xml(pcmk__output_t *out, va_list args)
 {
     const pcmk_resource_t *rsc = va_arg(args, const pcmk_resource_t *);
     const pcmk_node_t *node = va_arg(args, const pcmk_node_t *);
     const char *task = va_arg(args, const char *);
     guint interval_ms = va_arg(args, guint);
     const pcmk__op_digest_t *digests = va_arg(args, const pcmk__op_digest_t *);
 
     char *interval_s = crm_strdup_printf("%ums", interval_ms);
     xmlNode *xml = NULL;
 
     xml = pcmk__output_create_xml_node(out, PCMK_XE_DIGESTS,
                                        PCMK_XA_RESOURCE, pcmk__s(rsc->id, ""),
                                        PCMK_XA_NODE,
                                        pcmk__s(node->details->uname, ""),
                                        PCMK_XA_TASK, pcmk__s(task, ""),
                                        PCMK_XA_INTERVAL, interval_s,
                                        NULL);
     free(interval_s);
     if (digests != NULL) {
         add_digest_xml(xml, "all", digests->digest_all_calc,
                        digests->params_all);
         add_digest_xml(xml, "nonprivate", digests->digest_secure_calc,
                        digests->params_secure);
         add_digest_xml(xml, "nonreloadable", digests->digest_restart_calc,
                        digests->params_restart);
     }
     return pcmk_rc_ok;
 }
 
 #define STOP_SANITY_ASSERT(lineno) do {                                 \
         if ((current != NULL) && current->details->unclean) {           \
             /* It will be a pseudo op */                                \
         } else if (stop == NULL) {                                      \
             crm_err("%s:%d: No stop action exists for %s",              \
                     __func__, lineno, rsc->id);                         \
             CRM_ASSERT(stop != NULL);                                   \
         } else if (pcmk_is_set(stop->flags, pcmk_action_optional)) {    \
             crm_err("%s:%d: Action %s is still optional",               \
                     __func__, lineno, stop->uuid);                      \
             CRM_ASSERT(!pcmk_is_set(stop->flags, pcmk_action_optional));\
         }                                                               \
     } while (0)
 
 PCMK__OUTPUT_ARGS("rsc-action", "pcmk_resource_t *", "pcmk_node_t *",
                   "pcmk_node_t *")
 static int
 rsc_action_default(pcmk__output_t *out, va_list args)
 {
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     pcmk_node_t *current = va_arg(args, pcmk_node_t *);
     pcmk_node_t *next = va_arg(args, pcmk_node_t *);
 
     GList *possible_matches = NULL;
     char *key = NULL;
     int rc = pcmk_rc_no_output;
     bool moving = false;
 
     pcmk_node_t *start_node = NULL;
     pcmk_action_t *start = NULL;
     pcmk_action_t *stop = NULL;
     pcmk_action_t *promote = NULL;
     pcmk_action_t *demote = NULL;
     pcmk_action_t *reason_op = NULL;
 
     if (!pcmk_is_set(rsc->flags, pcmk_rsc_managed)
         || (current == NULL && next == NULL)) {
         const bool managed = pcmk_is_set(rsc->flags, pcmk_rsc_managed);
 
         pcmk__rsc_info(rsc, "Leave   %s\t(%s%s)",
                        rsc->id, pcmk_role_text(rsc->role),
                        (managed? "" : " unmanaged"));
         return rc;
     }
 
     moving = (current != NULL) && (next != NULL)
              && !pcmk__same_node(current, next);
 
     possible_matches = pe__resource_actions(rsc, next, PCMK_ACTION_START,
                                             false);
     if (possible_matches) {
         start = possible_matches->data;
         g_list_free(possible_matches);
     }
 
     if ((start == NULL)
         || !pcmk_is_set(start->flags, pcmk_action_runnable)) {
         start_node = NULL;
     } else {
         start_node = current;
     }
     possible_matches = pe__resource_actions(rsc, start_node, PCMK_ACTION_STOP,
                                             false);
     if (possible_matches) {
         stop = possible_matches->data;
         g_list_free(possible_matches);
     } else if (pcmk_is_set(rsc->flags, pcmk_rsc_stop_unexpected)) {
         /* The resource is multiply active with PCMK_META_MULTIPLE_ACTIVE set to
          * PCMK_VALUE_STOP_UNEXPECTED, and not stopping on its current node, but
          * it should be stopping elsewhere.
          */
         possible_matches = pe__resource_actions(rsc, NULL, PCMK_ACTION_STOP,
                                                 false);
         if (possible_matches != NULL) {
             stop = possible_matches->data;
             g_list_free(possible_matches);
         }
     }
 
     possible_matches = pe__resource_actions(rsc, next, PCMK_ACTION_PROMOTE,
                                             false);
     if (possible_matches) {
         promote = possible_matches->data;
         g_list_free(possible_matches);
     }
 
     possible_matches = pe__resource_actions(rsc, next, PCMK_ACTION_DEMOTE,
                                             false);
     if (possible_matches) {
         demote = possible_matches->data;
         g_list_free(possible_matches);
     }
 
     if (rsc->role == rsc->next_role) {
         pcmk_action_t *migrate_op = NULL;
 
         CRM_CHECK(next != NULL, return rc);
 
         possible_matches = pe__resource_actions(rsc, next,
                                                 PCMK_ACTION_MIGRATE_FROM,
                                                 false);
         if (possible_matches) {
             migrate_op = possible_matches->data;
         }
 
         if ((migrate_op != NULL) && (current != NULL)
             && pcmk_is_set(migrate_op->flags, pcmk_action_runnable)) {
             rc = out->message(out, "rsc-action-item", "Migrate", rsc, current,
                               next, start, NULL);
 
         } else if (pcmk_is_set(rsc->flags, pcmk_rsc_reload)) {
             rc = out->message(out, "rsc-action-item", "Reload", rsc, current,
                               next, start, NULL);
 
         } else if ((start == NULL)
                    || pcmk_is_set(start->flags, pcmk_action_optional)) {
             if ((demote != NULL) && (promote != NULL)
                 && !pcmk_is_set(demote->flags, pcmk_action_optional)
                 && !pcmk_is_set(promote->flags, pcmk_action_optional)) {
                 rc = out->message(out, "rsc-action-item", "Re-promote", rsc,
                                   current, next, promote, demote);
             } else {
                 pcmk__rsc_info(rsc, "Leave   %s\t(%s %s)", rsc->id,
                                pcmk_role_text(rsc->role),
                                pcmk__node_name(next));
             }
 
         } else if (!pcmk_is_set(start->flags, pcmk_action_runnable)) {
             if ((stop == NULL) || (stop->reason == NULL)) {
                 reason_op = start;
             } else {
                 reason_op = stop;
             }
             rc = out->message(out, "rsc-action-item", "Stop", rsc, current,
                               NULL, stop, reason_op);
             STOP_SANITY_ASSERT(__LINE__);
 
         } else if (moving && current) {
             const bool failed = pcmk_is_set(rsc->flags, pcmk_rsc_failed);
 
             rc = out->message(out, "rsc-action-item",
                               (failed? "Recover" : "Move"), rsc, current, next,
                               stop, NULL);
 
         } else if (pcmk_is_set(rsc->flags, pcmk_rsc_failed)) {
             rc = out->message(out, "rsc-action-item", "Recover", rsc, current,
                               NULL, stop, NULL);
             STOP_SANITY_ASSERT(__LINE__);
 
         } else {
             rc = out->message(out, "rsc-action-item", "Restart", rsc, current,
                               next, start, NULL);
 #if 0
             /* @TODO This can be reached in situations that should really be
              * "Start" (see for example the migrate-fail-7 regression test)
              */
             STOP_SANITY_ASSERT(__LINE__);
 #endif
         }
 
         g_list_free(possible_matches);
         return rc;
     }
 
     if ((stop != NULL)
         && ((rsc->next_role == pcmk_role_stopped)
             || ((start != NULL)
                 && !pcmk_is_set(start->flags, pcmk_action_runnable)))) {
 
         key = stop_key(rsc);
         for (GList *iter = rsc->running_on; iter != NULL; iter = iter->next) {
             pcmk_node_t *node = iter->data;
             pcmk_action_t *stop_op = NULL;
 
             reason_op = start;
             possible_matches = find_actions(rsc->actions, key, node);
             if (possible_matches) {
                 stop_op = possible_matches->data;
                 g_list_free(possible_matches);
             }
 
             if (stop_op != NULL) {
                 if (pcmk_is_set(stop_op->flags, pcmk_action_runnable)) {
                     STOP_SANITY_ASSERT(__LINE__);
                 }
                 if (stop_op->reason != NULL) {
                     reason_op = stop_op;
                 }
             }
 
             if (out->message(out, "rsc-action-item", "Stop", rsc, node, NULL,
                              stop_op, reason_op) == pcmk_rc_ok) {
                 rc = pcmk_rc_ok;
             }
         }
 
         free(key);
 
     } else if ((stop != NULL)
                && pcmk_all_flags_set(rsc->flags,
                                      pcmk_rsc_failed|pcmk_rsc_stop_if_failed)) {
         /* 'stop' may be NULL if the failure was ignored */
         rc = out->message(out, "rsc-action-item", "Recover", rsc, current,
                           next, stop, start);
         STOP_SANITY_ASSERT(__LINE__);
 
     } else if (moving) {
         rc = out->message(out, "rsc-action-item", "Move", rsc, current, next,
                           stop, NULL);
         STOP_SANITY_ASSERT(__LINE__);
 
     } else if (pcmk_is_set(rsc->flags, pcmk_rsc_reload)) {
         rc = out->message(out, "rsc-action-item", "Reload", rsc, current, next,
                           start, NULL);
 
     } else if ((stop != NULL)
                && !pcmk_is_set(stop->flags, pcmk_action_optional)) {
         rc = out->message(out, "rsc-action-item", "Restart", rsc, current,
                           next, start, NULL);
         STOP_SANITY_ASSERT(__LINE__);
 
     } else if (rsc->role == pcmk_role_promoted) {
         CRM_LOG_ASSERT(current != NULL);
         rc = out->message(out, "rsc-action-item", "Demote", rsc, current,
                           next, demote, NULL);
 
     } else if (rsc->next_role == pcmk_role_promoted) {
         CRM_LOG_ASSERT(next);
         rc = out->message(out, "rsc-action-item", "Promote", rsc, current,
                           next, promote, NULL);
 
     } else if ((rsc->role == pcmk_role_stopped)
                && (rsc->next_role > pcmk_role_stopped)) {
         rc = out->message(out, "rsc-action-item", "Start", rsc, current, next,
                           start, NULL);
     }
 
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("node-action", "const char *", "const char *", "const char *")
 static int
 node_action(pcmk__output_t *out, va_list args)
 {
     const char *task = va_arg(args, const char *);
     const char *node_name = va_arg(args, const char *);
     const char *reason = va_arg(args, const char *);
 
     if (task == NULL) {
         return pcmk_rc_no_output;
     } else if (reason) {
         out->list_item(out, NULL, "%s %s '%s'", task, node_name, reason);
     } else {
         crm_notice(" * %s %s", task, node_name);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("node-action", "const char *", "const char *", "const char *")
 static int
 node_action_xml(pcmk__output_t *out, va_list args)
 {
     const char *task = va_arg(args, const char *);
     const char *node_name = va_arg(args, const char *);
     const char *reason = va_arg(args, const char *);
 
     if (task == NULL) {
         return pcmk_rc_no_output;
     } else if (reason) {
         pcmk__output_create_xml_node(out, PCMK_XE_NODE_ACTION,
                                      PCMK_XA_TASK, task,
                                      PCMK_XA_NODE, node_name,
                                      PCMK_XA_REASON, reason,
                                      NULL);
     } else {
         crm_notice(" * %s %s", task, node_name);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("node-info", "uint32_t", "const char *", "const char *",
                   "const char *", "bool", "bool")
 static int
 node_info_default(pcmk__output_t *out, va_list args)
 {
     uint32_t node_id = va_arg(args, uint32_t);
     const char *node_name = va_arg(args, const char *);
     const char *uuid = va_arg(args, const char *);
     const char *state = va_arg(args, const char *);
     bool have_quorum = (bool) va_arg(args, int);
     bool is_remote = (bool) va_arg(args, int);
 
     return out->info(out,
                      "Node %" PRIu32 ": %s "
                      "(uuid=%s, state=%s, have_quorum=%s, is_remote=%s)",
                      node_id, pcmk__s(node_name, "unknown"),
                      pcmk__s(uuid, "unknown"), pcmk__s(state, "unknown"),
                      pcmk__btoa(have_quorum), pcmk__btoa(is_remote));
 }
 
 PCMK__OUTPUT_ARGS("node-info", "uint32_t", "const char *", "const char *",
                   "const char *", "bool", "bool")
 static int
 node_info_xml(pcmk__output_t *out, va_list args)
 {
     uint32_t node_id = va_arg(args, uint32_t);
     const char *node_name = va_arg(args, const char *);
     const char *uuid = va_arg(args, const char *);
     const char *state = va_arg(args, const char *);
     bool have_quorum = (bool) va_arg(args, int);
     bool is_remote = (bool) va_arg(args, int);
 
     char *id_s = crm_strdup_printf("%" PRIu32, node_id);
 
     pcmk__output_create_xml_node(out, PCMK_XE_NODE_INFO,
                                  PCMK_XA_NODEID, id_s,
                                  PCMK_XA_UNAME, node_name,
                                  PCMK_XA_ID, uuid,
                                  PCMK_XA_CRMD, state,
                                  PCMK_XA_HAVE_QUORUM, pcmk__btoa(have_quorum),
                                  PCMK_XA_REMOTE_NODE, pcmk__btoa(is_remote),
                                  NULL);
     free(id_s);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("inject-cluster-action", "const char *", "const char *",
                   "xmlNode *")
 static int
 inject_cluster_action(pcmk__output_t *out, va_list args)
 {
     const char *node = va_arg(args, const char *);
     const char *task = va_arg(args, const char *);
     xmlNodePtr rsc = va_arg(args, xmlNodePtr);
 
     if (out->is_quiet(out)) {
         return pcmk_rc_no_output;
     }
 
     if (rsc != NULL) {
         out->list_item(out, NULL, "Cluster action:  %s for %s on %s",
                        task, pcmk__xe_id(rsc), node);
     } else {
         out->list_item(out, NULL, "Cluster action:  %s on %s", task, node);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("inject-cluster-action", "const char *", "const char *",
                   "xmlNode *")
 static int
 inject_cluster_action_xml(pcmk__output_t *out, va_list args)
 {
     const char *node = va_arg(args, const char *);
     const char *task = va_arg(args, const char *);
     xmlNodePtr rsc = va_arg(args, xmlNodePtr);
 
     xmlNodePtr xml_node = NULL;
 
     if (out->is_quiet(out)) {
         return pcmk_rc_no_output;
     }
 
     xml_node = pcmk__output_create_xml_node(out, PCMK_XE_CLUSTER_ACTION,
                                             PCMK_XA_TASK, task,
                                             PCMK_XA_NODE, node,
                                             NULL);
 
     if (rsc) {
         crm_xml_add(xml_node, PCMK_XA_ID, pcmk__xe_id(rsc));
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("inject-fencing-action", "const char *", "const char *")
 static int
 inject_fencing_action(pcmk__output_t *out, va_list args)
 {
     const char *target = va_arg(args, const char *);
     const char *op = va_arg(args, const char *);
 
     if (out->is_quiet(out)) {
         return pcmk_rc_no_output;
     }
 
     out->list_item(out, NULL, "Fencing %s (%s)", target, op);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("inject-fencing-action", "const char *", "const char *")
 static int
 inject_fencing_action_xml(pcmk__output_t *out, va_list args)
 {
     const char *target = va_arg(args, const char *);
     const char *op = va_arg(args, const char *);
 
     if (out->is_quiet(out)) {
         return pcmk_rc_no_output;
     }
 
     pcmk__output_create_xml_node(out, PCMK_XE_FENCING_ACTION,
                                  PCMK_XA_TARGET, target,
                                  PCMK_XA_OP, op,
                                  NULL);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("inject-attr", "const char *", "const char *", "xmlNode *")
 static int
 inject_attr(pcmk__output_t *out, va_list args)
 {
     const char *name = va_arg(args, const char *);
     const char *value = va_arg(args, const char *);
     xmlNodePtr cib_node = va_arg(args, xmlNodePtr);
 
     xmlChar *node_path = NULL;
 
     if (out->is_quiet(out)) {
         return pcmk_rc_no_output;
     }
 
     node_path = xmlGetNodePath(cib_node);
 
     out->list_item(out, NULL, "Injecting attribute %s=%s into %s '%s'",
                    name, value, node_path, pcmk__xe_id(cib_node));
 
     free(node_path);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("inject-attr", "const char *", "const char *", "xmlNode *")
 static int
 inject_attr_xml(pcmk__output_t *out, va_list args)
 {
     const char *name = va_arg(args, const char *);
     const char *value = va_arg(args, const char *);
     xmlNodePtr cib_node = va_arg(args, xmlNodePtr);
 
     xmlChar *node_path = NULL;
 
     if (out->is_quiet(out)) {
         return pcmk_rc_no_output;
     }
 
     node_path = xmlGetNodePath(cib_node);
 
     pcmk__output_create_xml_node(out, PCMK_XE_INJECT_ATTR,
                                  PCMK_XA_NAME, name,
                                  PCMK_XA_VALUE, value,
                                  PCMK_XA_NODE_PATH, node_path,
                                  PCMK_XA_CIB_NODE, pcmk__xe_id(cib_node),
                                  NULL);
     free(node_path);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("inject-spec", "const char *")
 static int
 inject_spec(pcmk__output_t *out, va_list args)
 {
     const char *spec = va_arg(args, const char *);
 
     if (out->is_quiet(out)) {
         return pcmk_rc_no_output;
     }
 
     out->list_item(out, NULL, "Injecting %s into the configuration", spec);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("inject-spec", "const char *")
 static int
 inject_spec_xml(pcmk__output_t *out, va_list args)
 {
     const char *spec = va_arg(args, const char *);
 
     if (out->is_quiet(out)) {
         return pcmk_rc_no_output;
     }
 
     pcmk__output_create_xml_node(out, PCMK_XE_INJECT_SPEC,
                                  PCMK_XA_SPEC, spec,
                                  NULL);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("inject-modify-config", "const char *", "const char *")
 static int
 inject_modify_config(pcmk__output_t *out, va_list args)
 {
     const char *quorum = va_arg(args, const char *);
     const char *watchdog = va_arg(args, const char *);
 
     if (out->is_quiet(out)) {
         return pcmk_rc_no_output;
     }
 
     out->begin_list(out, NULL, NULL, "Performing Requested Modifications");
 
     if (quorum) {
         out->list_item(out, NULL, "Setting quorum: %s", quorum);
     }
 
     if (watchdog) {
         out->list_item(out, NULL, "Setting watchdog: %s", watchdog);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("inject-modify-config", "const char *", "const char *")
 static int
 inject_modify_config_xml(pcmk__output_t *out, va_list args)
 {
     const char *quorum = va_arg(args, const char *);
     const char *watchdog = va_arg(args, const char *);
 
     xmlNodePtr node = NULL;
 
     if (out->is_quiet(out)) {
         return pcmk_rc_no_output;
     }
 
     node = pcmk__output_xml_create_parent(out, PCMK_XE_MODIFICATIONS, NULL);
 
     if (quorum) {
         crm_xml_add(node, PCMK_XA_QUORUM, quorum);
     }
 
     if (watchdog) {
         crm_xml_add(node, PCMK_XA_WATCHDOG, watchdog);
     }
 
     pcmk__output_xml_pop_parent(out);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("inject-modify-node", "const char *", "const char *")
 static int
 inject_modify_node(pcmk__output_t *out, va_list args)
 {
     const char *action = va_arg(args, const char *);
     const char *node = va_arg(args, const char *);
 
     if (out->is_quiet(out)) {
         return pcmk_rc_no_output;
     }
 
     if (pcmk__str_eq(action, "Online", pcmk__str_none)) {
         out->list_item(out, NULL, "Bringing node %s online", node);
         return pcmk_rc_ok;
     } else if (pcmk__str_eq(action, "Offline", pcmk__str_none)) {
         out->list_item(out, NULL, "Taking node %s offline", node);
         return pcmk_rc_ok;
     } else if (pcmk__str_eq(action, "Failing", pcmk__str_none)) {
         out->list_item(out, NULL, "Failing node %s", node);
         return pcmk_rc_ok;
     }
 
     return pcmk_rc_no_output;
 }
 
 PCMK__OUTPUT_ARGS("inject-modify-node", "const char *", "const char *")
 static int
 inject_modify_node_xml(pcmk__output_t *out, va_list args)
 {
     const char *action = va_arg(args, const char *);
     const char *node = va_arg(args, const char *);
 
     if (out->is_quiet(out)) {
         return pcmk_rc_no_output;
     }
 
     pcmk__output_create_xml_node(out, PCMK_XE_MODIFY_NODE,
                                  PCMK_XA_ACTION, action,
                                  PCMK_XA_NODE, node,
                                  NULL);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("inject-modify-ticket", "const char *", "const char *")
 static int
 inject_modify_ticket(pcmk__output_t *out, va_list args)
 {
     const char *action = va_arg(args, const char *);
     const char *ticket = va_arg(args, const char *);
 
     if (out->is_quiet(out)) {
         return pcmk_rc_no_output;
     }
 
     if (pcmk__str_eq(action, "Standby", pcmk__str_none)) {
         out->list_item(out, NULL, "Making ticket %s standby", ticket);
     } else {
         out->list_item(out, NULL, "%s ticket %s", action, ticket);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("inject-modify-ticket", "const char *", "const char *")
 static int
 inject_modify_ticket_xml(pcmk__output_t *out, va_list args)
 {
     const char *action = va_arg(args, const char *);
     const char *ticket = va_arg(args, const char *);
 
     if (out->is_quiet(out)) {
         return pcmk_rc_no_output;
     }
 
     pcmk__output_create_xml_node(out, PCMK_XE_MODIFY_TICKET,
                                  PCMK_XA_ACTION, action,
                                  PCMK_XA_TICKET, ticket,
                                  NULL);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("inject-pseudo-action", "const char *", "const char *")
 static int
 inject_pseudo_action(pcmk__output_t *out, va_list args)
 {
     const char *node = va_arg(args, const char *);
     const char *task = va_arg(args, const char *);
 
     if (out->is_quiet(out)) {
         return pcmk_rc_no_output;
     }
 
     out->list_item(out, NULL, "Pseudo action:   %s%s%s",
                    task, ((node == NULL)? "" : " on "), pcmk__s(node, ""));
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("inject-pseudo-action", "const char *", "const char *")
 static int
 inject_pseudo_action_xml(pcmk__output_t *out, va_list args)
 {
     const char *node = va_arg(args, const char *);
     const char *task = va_arg(args, const char *);
 
     xmlNodePtr xml_node = NULL;
 
     if (out->is_quiet(out)) {
         return pcmk_rc_no_output;
     }
 
     xml_node = pcmk__output_create_xml_node(out, PCMK_XE_PSEUDO_ACTION,
                                             PCMK_XA_TASK, task,
                                             NULL);
     if (node) {
         crm_xml_add(xml_node, PCMK_XA_NODE, node);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("inject-rsc-action", "const char *", "const char *",
                   "const char *", "guint")
 static int
 inject_rsc_action(pcmk__output_t *out, va_list args)
 {
     const char *rsc = va_arg(args, const char *);
     const char *operation = va_arg(args, const char *);
     const char *node = va_arg(args, const char *);
     guint interval_ms = va_arg(args, guint);
 
     if (out->is_quiet(out)) {
         return pcmk_rc_no_output;
     }
 
     if (interval_ms) {
         out->list_item(out, NULL, "Resource action: %-15s %s=%u on %s",
                        rsc, operation, interval_ms, node);
     } else {
         out->list_item(out, NULL, "Resource action: %-15s %s on %s",
                        rsc, operation, node);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("inject-rsc-action", "const char *", "const char *",
                   "const char *", "guint")
 static int
 inject_rsc_action_xml(pcmk__output_t *out, va_list args)
 {
     const char *rsc = va_arg(args, const char *);
     const char *operation = va_arg(args, const char *);
     const char *node = va_arg(args, const char *);
     guint interval_ms = va_arg(args, guint);
 
     xmlNodePtr xml_node = NULL;
 
     if (out->is_quiet(out)) {
         return pcmk_rc_no_output;
     }
 
     xml_node = pcmk__output_create_xml_node(out, PCMK_XE_RSC_ACTION,
                                             PCMK_XA_RESOURCE, rsc,
                                             PCMK_XA_OP, operation,
                                             PCMK_XA_NODE, node,
                                             NULL);
 
     if (interval_ms) {
         char *interval_s = pcmk__itoa(interval_ms);
 
         crm_xml_add(xml_node, PCMK_XA_INTERVAL, interval_s);
         free(interval_s);
     }
 
     return pcmk_rc_ok;
 }
 
 #define CHECK_RC(retcode, retval)   \
     if (retval == pcmk_rc_ok) {     \
         retcode = pcmk_rc_ok;       \
     }
 
 PCMK__OUTPUT_ARGS("cluster-status", "pcmk_scheduler_t *",
                   "enum pcmk_pacemakerd_state", "crm_exit_t",
                   "stonith_history_t *", "enum pcmk__fence_history", "uint32_t",
                   "uint32_t", "const char *", "GList *", "GList *")
 int
 pcmk__cluster_status_text(pcmk__output_t *out, va_list args)
 {
     pcmk_scheduler_t *scheduler = va_arg(args, pcmk_scheduler_t *);
     enum pcmk_pacemakerd_state pcmkd_state =
         (enum pcmk_pacemakerd_state) va_arg(args, int);
     crm_exit_t history_rc = va_arg(args, crm_exit_t);
     stonith_history_t *stonith_history = va_arg(args, stonith_history_t *);
     enum pcmk__fence_history fence_history = va_arg(args, int);
     uint32_t section_opts = va_arg(args, uint32_t);
     uint32_t show_opts = va_arg(args, uint32_t);
     const char *prefix = va_arg(args, const char *);
     GList *unames = va_arg(args, GList *);
     GList *resources = va_arg(args, GList *);
 
     int rc = pcmk_rc_no_output;
     bool already_printed_failure = false;
 
     CHECK_RC(rc, out->message(out, "cluster-summary", scheduler, pcmkd_state,
                               section_opts, show_opts));
 
     if (pcmk_is_set(section_opts, pcmk_section_nodes) && unames) {
         CHECK_RC(rc, out->message(out, "node-list", scheduler->nodes, unames,
                                   resources, show_opts, rc == pcmk_rc_ok));
     }
 
     /* Print resources section, if needed */
     if (pcmk_is_set(section_opts, pcmk_section_resources)) {
         CHECK_RC(rc, out->message(out, "resource-list", scheduler, show_opts,
                                   true, unames, resources, rc == pcmk_rc_ok));
     }
 
     /* print Node Attributes section if requested */
     if (pcmk_is_set(section_opts, pcmk_section_attributes)) {
         CHECK_RC(rc, out->message(out, "node-attribute-list", scheduler,
                                   show_opts, (rc == pcmk_rc_ok), unames,
                                   resources));
     }
 
     /* If requested, print resource operations (which includes failcounts)
      * or just failcounts
      */
     if (pcmk_any_flags_set(section_opts,
                            pcmk_section_operations|pcmk_section_failcounts)) {
         CHECK_RC(rc, out->message(out, "node-summary", scheduler, unames,
                                   resources, section_opts, show_opts,
                                   (rc == pcmk_rc_ok)));
     }
 
     /* If there were any failed actions, print them */
     if (pcmk_is_set(section_opts, pcmk_section_failures)
         && (scheduler->failed != NULL)
         && (scheduler->failed->children != NULL)) {
 
         CHECK_RC(rc, out->message(out, "failed-action-list", scheduler, unames,
                                   resources, show_opts, rc == pcmk_rc_ok));
     }
 
     /* Print failed stonith actions */
     if (pcmk_is_set(section_opts, pcmk_section_fence_failed) &&
         fence_history != pcmk__fence_history_none) {
         if (history_rc == 0) {
             stonith_history_t *hp = NULL;
 
             hp = stonith__first_matching_event(stonith_history,
                                                stonith__event_state_eq,
                                                GINT_TO_POINTER(st_failed));
             if (hp) {
                 CHECK_RC(rc, out->message(out, "failed-fencing-list",
                                           stonith_history, unames, section_opts,
                                           show_opts, rc == pcmk_rc_ok));
             }
         } else {
             PCMK__OUTPUT_SPACER_IF(out, rc == pcmk_rc_ok);
             out->begin_list(out, NULL, NULL, "Failed Fencing Actions");
             out->list_item(out, NULL, "Failed to get fencing history: %s",
                            crm_exit_str(history_rc));
             out->end_list(out);
 
             already_printed_failure = true;
         }
     }
 
     /* Print tickets if requested */
     if (pcmk_is_set(section_opts, pcmk_section_tickets)) {
         CHECK_RC(rc, out->message(out, "ticket-list", scheduler->tickets,
                                   (rc == pcmk_rc_ok), false, false));
     }
 
     /* Print negative location constraints if requested */
     if (pcmk_is_set(section_opts, pcmk_section_bans)) {
         CHECK_RC(rc, out->message(out, "ban-list", scheduler, prefix, resources,
                                   show_opts, rc == pcmk_rc_ok));
     }
 
     /* Print stonith history */
     if (pcmk_any_flags_set(section_opts, pcmk_section_fencing_all) &&
         fence_history != pcmk__fence_history_none) {
         if (history_rc != 0) {
             if (!already_printed_failure) {
                 PCMK__OUTPUT_SPACER_IF(out, rc == pcmk_rc_ok);
                 out->begin_list(out, NULL, NULL, "Failed Fencing Actions");
                 out->list_item(out, NULL, "Failed to get fencing history: %s",
                                crm_exit_str(history_rc));
                 out->end_list(out);
             }
         } else if (pcmk_is_set(section_opts, pcmk_section_fence_worked)) {
             stonith_history_t *hp = NULL;
 
             hp = stonith__first_matching_event(stonith_history,
                                                stonith__event_state_neq,
                                                GINT_TO_POINTER(st_failed));
             if (hp) {
                 CHECK_RC(rc, out->message(out, "fencing-list", hp, unames,
                                           section_opts, show_opts,
                                           rc == pcmk_rc_ok));
             }
         } else if (pcmk_is_set(section_opts, pcmk_section_fence_pending)) {
             stonith_history_t *hp = NULL;
 
             hp = stonith__first_matching_event(stonith_history,
                                                stonith__event_state_pending,
                                                NULL);
             if (hp) {
                 CHECK_RC(rc, out->message(out, "pending-fencing-list", hp,
                                           unames, section_opts, show_opts,
                                           rc == pcmk_rc_ok));
             }
         }
     }
 
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("cluster-status", "pcmk_scheduler_t *",
                   "enum pcmk_pacemakerd_state", "crm_exit_t",
                   "stonith_history_t *", "enum pcmk__fence_history", "uint32_t",
                   "uint32_t", "const char *", "GList *", "GList *")
 static int
 cluster_status_xml(pcmk__output_t *out, va_list args)
 {
     pcmk_scheduler_t *scheduler = va_arg(args, pcmk_scheduler_t *);
     enum pcmk_pacemakerd_state pcmkd_state =
         (enum pcmk_pacemakerd_state) va_arg(args, int);
     crm_exit_t history_rc = va_arg(args, crm_exit_t);
     stonith_history_t *stonith_history = va_arg(args, stonith_history_t *);
     enum pcmk__fence_history fence_history = va_arg(args, int);
     uint32_t section_opts = va_arg(args, uint32_t);
     uint32_t show_opts = va_arg(args, uint32_t);
     const char *prefix = va_arg(args, const char *);
     GList *unames = va_arg(args, GList *);
     GList *resources = va_arg(args, GList *);
 
     out->message(out, "cluster-summary", scheduler, pcmkd_state, section_opts,
                  show_opts);
 
     /*** NODES ***/
     if (pcmk_is_set(section_opts, pcmk_section_nodes)) {
         out->message(out, "node-list", scheduler->nodes, unames, resources,
                      show_opts, false);
     }
 
     /* Print resources section, if needed */
     if (pcmk_is_set(section_opts, pcmk_section_resources)) {
         /* XML output always displays full details. */
         uint32_t full_show_opts = show_opts & ~pcmk_show_brief;
 
         out->message(out, "resource-list", scheduler, full_show_opts,
                      false, unames, resources, false);
     }
 
     /* print Node Attributes section if requested */
     if (pcmk_is_set(section_opts, pcmk_section_attributes)) {
         out->message(out, "node-attribute-list", scheduler, show_opts, false,
                      unames, resources);
     }
 
     /* If requested, print resource operations (which includes failcounts)
      * or just failcounts
      */
     if (pcmk_any_flags_set(section_opts,
                            pcmk_section_operations|pcmk_section_failcounts)) {
         out->message(out, "node-summary", scheduler, unames,
                      resources, section_opts, show_opts, false);
     }
 
     /* If there were any failed actions, print them */
     if (pcmk_is_set(section_opts, pcmk_section_failures)
         && (scheduler->failed != NULL)
         && (scheduler->failed->children != NULL)) {
 
         out->message(out, "failed-action-list", scheduler, unames, resources,
                      show_opts, false);
     }
 
     /* Print stonith history */
     if (pcmk_is_set(section_opts, pcmk_section_fencing_all) &&
         fence_history != pcmk__fence_history_none) {
         out->message(out, "full-fencing-list", history_rc, stonith_history,
                      unames, section_opts, show_opts, false);
     }
 
     /* Print tickets if requested */
     if (pcmk_is_set(section_opts, pcmk_section_tickets)) {
         out->message(out, "ticket-list", scheduler->tickets, false, false, false);
     }
 
     /* Print negative location constraints if requested */
     if (pcmk_is_set(section_opts, pcmk_section_bans)) {
         out->message(out, "ban-list", scheduler, prefix, resources, show_opts,
                      false);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("cluster-status", "pcmk_scheduler_t *",
                   "enum pcmk_pacemakerd_state", "crm_exit_t",
                   "stonith_history_t *", "enum pcmk__fence_history", "uint32_t",
                   "uint32_t", "const char *", "GList *", "GList *")
 static int
 cluster_status_html(pcmk__output_t *out, va_list args)
 {
     pcmk_scheduler_t *scheduler = va_arg(args, pcmk_scheduler_t *);
     enum pcmk_pacemakerd_state pcmkd_state =
         (enum pcmk_pacemakerd_state) va_arg(args, int);
     crm_exit_t history_rc = va_arg(args, crm_exit_t);
     stonith_history_t *stonith_history = va_arg(args, stonith_history_t *);
     enum pcmk__fence_history fence_history = va_arg(args, int);
     uint32_t section_opts = va_arg(args, uint32_t);
     uint32_t show_opts = va_arg(args, uint32_t);
     const char *prefix = va_arg(args, const char *);
     GList *unames = va_arg(args, GList *);
     GList *resources = va_arg(args, GList *);
     bool already_printed_failure = false;
 
     out->message(out, "cluster-summary", scheduler, pcmkd_state, section_opts,
                  show_opts);
 
     /*** NODE LIST ***/
     if (pcmk_is_set(section_opts, pcmk_section_nodes) && unames) {
         out->message(out, "node-list", scheduler->nodes, unames, resources,
                      show_opts, false);
     }
 
     /* Print resources section, if needed */
     if (pcmk_is_set(section_opts, pcmk_section_resources)) {
         out->message(out, "resource-list", scheduler, show_opts, true, unames,
                      resources, false);
     }
 
     /* print Node Attributes section if requested */
     if (pcmk_is_set(section_opts, pcmk_section_attributes)) {
         out->message(out, "node-attribute-list", scheduler, show_opts, false,
                      unames, resources);
     }
 
     /* If requested, print resource operations (which includes failcounts)
      * or just failcounts
      */
     if (pcmk_any_flags_set(section_opts,
                            pcmk_section_operations|pcmk_section_failcounts)) {
         out->message(out, "node-summary", scheduler, unames,
                      resources, section_opts, show_opts, false);
     }
 
     /* If there were any failed actions, print them */
     if (pcmk_is_set(section_opts, pcmk_section_failures)
         && (scheduler->failed != NULL)
         && (scheduler->failed->children != NULL)) {
 
         out->message(out, "failed-action-list", scheduler, unames, resources,
                      show_opts, false);
     }
 
     /* Print failed stonith actions */
     if (pcmk_is_set(section_opts, pcmk_section_fence_failed) &&
         fence_history != pcmk__fence_history_none) {
         if (history_rc == 0) {
             stonith_history_t *hp = NULL;
 
             hp = stonith__first_matching_event(stonith_history,
                                                stonith__event_state_eq,
                                                GINT_TO_POINTER(st_failed));
             if (hp) {
                 out->message(out, "failed-fencing-list", stonith_history,
                              unames, section_opts, show_opts, false);
             }
         } else {
             out->begin_list(out, NULL, NULL, "Failed Fencing Actions");
             out->list_item(out, NULL, "Failed to get fencing history: %s",
                            crm_exit_str(history_rc));
             out->end_list(out);
         }
     }
 
     /* Print stonith history */
     if (pcmk_any_flags_set(section_opts, pcmk_section_fencing_all) &&
         fence_history != pcmk__fence_history_none) {
         if (history_rc != 0) {
             if (!already_printed_failure) {
                 out->begin_list(out, NULL, NULL, "Failed Fencing Actions");
                 out->list_item(out, NULL, "Failed to get fencing history: %s",
                                crm_exit_str(history_rc));
                 out->end_list(out);
             }
         } else if (pcmk_is_set(section_opts, pcmk_section_fence_worked)) {
             stonith_history_t *hp = NULL;
 
             hp = stonith__first_matching_event(stonith_history,
                                                stonith__event_state_neq,
                                                GINT_TO_POINTER(st_failed));
             if (hp) {
                 out->message(out, "fencing-list", hp, unames, section_opts,
                              show_opts, false);
             }
         } else if (pcmk_is_set(section_opts, pcmk_section_fence_pending)) {
             stonith_history_t *hp = NULL;
 
             hp = stonith__first_matching_event(stonith_history,
                                                stonith__event_state_pending,
                                                NULL);
             if (hp) {
                 out->message(out, "pending-fencing-list", hp, unames,
                              section_opts, show_opts, false);
             }
         }
     }
 
     /* Print tickets if requested */
     if (pcmk_is_set(section_opts, pcmk_section_tickets)) {
         out->message(out, "ticket-list", scheduler->tickets, false, false, false);
     }
 
     /* Print negative location constraints if requested */
     if (pcmk_is_set(section_opts, pcmk_section_bans)) {
         out->message(out, "ban-list", scheduler, prefix, resources, show_opts,
                      false);
     }
 
     return pcmk_rc_ok;
 }
 
 #define KV_PAIR(k, v) do { \
     if (legacy) { \
         pcmk__g_strcat(s, k "=", pcmk__s(v, ""), " ", NULL); \
     } else { \
         pcmk__g_strcat(s, k "=\"", pcmk__s(v, ""), "\"", NULL); \
     } \
 } while (0)
 
 PCMK__OUTPUT_ARGS("attribute", "const char *", "const char *", "const char *",
                   "const char *", "const char *", "bool", "bool")
 static int
 attribute_default(pcmk__output_t *out, va_list args)
 {
     const char *scope = va_arg(args, const char *);
     const char *instance = va_arg(args, const char *);
     const char *name = va_arg(args, const char *);
     const char *value = va_arg(args, const char *);
     const char *host = va_arg(args, const char *);
     bool quiet = va_arg(args, int);
     bool legacy = va_arg(args, int);
 
     gchar *value_esc = NULL;
     GString *s = NULL;
 
     if (quiet) {
         if (value != NULL) {
             /* Quiet needs to be turned off for ->info() to do anything */
             bool was_quiet = out->is_quiet(out);
 
             if (was_quiet) {
                 out->quiet = false;
             }
 
             out->info(out, "%s", value);
 
             out->quiet = was_quiet;
         }
 
         return pcmk_rc_ok;
     }
 
     s = g_string_sized_new(50);
 
     if (pcmk__xml_needs_escape(value, pcmk__xml_escape_attr_pretty)) {
         value_esc = pcmk__xml_escape(value, pcmk__xml_escape_attr_pretty);
         value = value_esc;
     }
 
     if (!pcmk__str_empty(scope)) {
         KV_PAIR(PCMK_XA_SCOPE, scope);
     }
 
     if (!pcmk__str_empty(instance)) {
         KV_PAIR(PCMK_XA_ID, instance);
     }
 
     KV_PAIR(PCMK_XA_NAME, name);
 
     if (!pcmk__str_empty(host)) {
         KV_PAIR(PCMK_XA_HOST, host);
     }
 
     if (legacy) {
         pcmk__g_strcat(s, PCMK_XA_VALUE "=", pcmk__s(value, "(null)"), NULL);
     } else {
         pcmk__g_strcat(s, PCMK_XA_VALUE "=\"", pcmk__s(value, ""), "\"", NULL);
     }
 
     out->info(out, "%s", s->str);
 
     g_free(value_esc);
     g_string_free(s, TRUE);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("attribute", "const char *", "const char *", "const char *",
                   "const char *", "const char *", "bool", "bool")
 static int
 attribute_xml(pcmk__output_t *out, va_list args)
 {
     const char *scope = va_arg(args, const char *);
     const char *instance = va_arg(args, const char *);
     const char *name = va_arg(args, const char *);
     const char *value = va_arg(args, const char *);
     const char *host = va_arg(args, const char *);
     bool quiet G_GNUC_UNUSED = va_arg(args, int);
     bool legacy G_GNUC_UNUSED = va_arg(args, int);
 
     xmlNodePtr node = NULL;
 
     node = pcmk__output_create_xml_node(out, PCMK_XE_ATTRIBUTE,
                                         PCMK_XA_NAME, name,
                                         PCMK_XA_VALUE, pcmk__s(value, ""),
                                         NULL);
 
     if (!pcmk__str_empty(scope)) {
         crm_xml_add(node, PCMK_XA_SCOPE, scope);
     }
 
     if (!pcmk__str_empty(instance)) {
         crm_xml_add(node, PCMK_XA_ID, instance);
     }
 
     if (!pcmk__str_empty(host)) {
         crm_xml_add(node, PCMK_XA_HOST, host);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("rule-check", "const char *", "int", "const char *")
 static int
 rule_check_default(pcmk__output_t *out, va_list args)
 {
     const char *rule_id = va_arg(args, const char *);
     int result = va_arg(args, int);
     const char *error = va_arg(args, const char *);
 
     switch (result) {
         case pcmk_rc_within_range:
             return out->info(out, "Rule %s is still in effect", rule_id);
         case pcmk_rc_ok:
             return out->info(out, "Rule %s satisfies conditions", rule_id);
         case pcmk_rc_after_range:
             return out->info(out, "Rule %s is expired", rule_id);
         case pcmk_rc_before_range:
             return out->info(out, "Rule %s has not yet taken effect", rule_id);
         case pcmk_rc_op_unsatisfied:
             return out->info(out, "Rule %s does not satisfy conditions",
                              rule_id);
         default:
             out->err(out,
                      "Could not determine whether rule %s is in effect: %s",
                      rule_id, ((error != NULL)? error : "unexpected error"));
             return pcmk_rc_ok;
     }
 }
 
 PCMK__OUTPUT_ARGS("rule-check", "const char *", "int", "const char *")
 static int
 rule_check_xml(pcmk__output_t *out, va_list args)
 {
     const char *rule_id = va_arg(args, const char *);
     int result = va_arg(args, int);
     const char *error = va_arg(args, const char *);
 
     char *rc_str = pcmk__itoa(pcmk_rc2exitc(result));
 
     pcmk__output_create_xml_node(out, PCMK_XE_RULE_CHECK,
                                  PCMK_XA_RULE_ID, rule_id,
                                  PCMK_XA_RC, rc_str,
                                  NULL);
     free(rc_str);
 
     switch (result) {
         case pcmk_rc_within_range:
         case pcmk_rc_ok:
         case pcmk_rc_after_range:
         case pcmk_rc_before_range:
         case pcmk_rc_op_unsatisfied:
             return pcmk_rc_ok;
         default:
             out->err(out,
                     "Could not determine whether rule %s is in effect: %s",
                     rule_id, ((error != NULL)? error : "unexpected error"));
             return pcmk_rc_ok;
     }
 }
 
 PCMK__OUTPUT_ARGS("result-code", "int", "const char *", "const char *")
 static int
 result_code_none(pcmk__output_t *out, va_list args)
 {
     return pcmk_rc_no_output;
 }
 
 PCMK__OUTPUT_ARGS("result-code", "int", "const char *", "const char *")
 static int
 result_code_text(pcmk__output_t *out, va_list args)
 {
     int code = va_arg(args, int);
     const char *name = va_arg(args, const char *);
     const char *desc = va_arg(args, const char *);
 
     static int code_width = 0;
 
     if (out->is_quiet(out)) {
         /* If out->is_quiet(), don't print the code. Print name and/or desc in a
          * compact format for text output, or print nothing at all for none-type
          * output.
          */
         if ((name != NULL) && (desc != NULL)) {
             pcmk__formatted_printf(out, "%s - %s\n", name, desc);
 
         } else if ((name != NULL) || (desc != NULL)) {
             pcmk__formatted_printf(out, "%s\n", ((name != NULL)? name : desc));
         }
         return pcmk_rc_ok;
     }
 
     /* Get length of longest (most negative) standard Pacemaker return code
      * This should be longer than all the values of any other type of return
      * code.
      */
     if (code_width == 0) {
         long long most_negative = pcmk_rc_error - (long long) pcmk__n_rc + 1;
         code_width = (int) snprintf(NULL, 0, "%lld", most_negative);
     }
 
     if ((name != NULL) && (desc != NULL)) {
         static int name_width = 0;
 
         if (name_width == 0) {
             // Get length of longest standard Pacemaker return code name
             for (int lpc = 0; lpc < pcmk__n_rc; lpc++) {
                 int len = (int) strlen(pcmk_rc_name(pcmk_rc_error - lpc));
                 name_width = QB_MAX(name_width, len);
             }
         }
         return out->info(out, "% *d: %-*s  %s", code_width, code, name_width,
                          name, desc);
     }
 
     if ((name != NULL) || (desc != NULL)) {
         return out->info(out, "% *d: %s", code_width, code,
                          ((name != NULL)? name : desc));
     }
 
     return out->info(out, "% *d", code_width, code);
 }
 
 PCMK__OUTPUT_ARGS("result-code", "int", "const char *", "const char *")
 static int
 result_code_xml(pcmk__output_t *out, va_list args)
 {
     int code = va_arg(args, int);
     const char *name = va_arg(args, const char *);
     const char *desc = va_arg(args, const char *);
 
     char *code_str = pcmk__itoa(code);
 
     pcmk__output_create_xml_node(out, PCMK_XE_RESULT_CODE,
                                  PCMK_XA_CODE, code_str,
                                  PCMK_XA_NAME, name,
                                  PCMK_XA_DESCRIPTION, desc,
                                  NULL);
     free(code_str);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("ticket-attribute", "const char *", "const char *", "const char *")
 static int
 ticket_attribute_default(pcmk__output_t *out, va_list args)
 {
     const char *ticket_id G_GNUC_UNUSED = va_arg(args, const char *);
     const char *name G_GNUC_UNUSED = va_arg(args, const char *);
     const char *value = va_arg(args, const char *);
 
     out->info(out, "%s", value);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("ticket-attribute", "const char *", "const char *", "const char *")
 static int
 ticket_attribute_xml(pcmk__output_t *out, va_list args)
 {
     const char *ticket_id = va_arg(args, const char *);
     const char *name = va_arg(args, const char *);
     const char *value = va_arg(args, const char *);
 
     /* Create:
      * <tickets>
      *   <ticket id="">
      *     <attribute name="" value="" />
      *   </ticket>
      * </tickets>
      */
     pcmk__output_xml_create_parent(out, PCMK_XE_TICKETS, NULL);
     pcmk__output_xml_create_parent(out, PCMK_XE_TICKET,
                                    PCMK_XA_ID, ticket_id, NULL);
     pcmk__output_create_xml_node(out, PCMK_XA_ATTRIBUTE,
                                  PCMK_XA_NAME, name,
                                  PCMK_XA_VALUE, value,
                                  NULL);
     pcmk__output_xml_pop_parent(out);
     pcmk__output_xml_pop_parent(out);
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("ticket-constraints", "xmlNode *")
 static int
 ticket_constraints_default(pcmk__output_t *out, va_list args)
 {
     xmlNode *constraint_xml = va_arg(args, xmlNode *);
 
     /* constraint_xml can take two forms:
      *
      * <rsc_ticket id="rsc1-req-ticketA" rsc="rsc1" ticket="ticketA" ... />
      *
      * for when there's only one ticket in the CIB, or when the user asked
      * for a specific ticket (crm_ticket -c -t for instance)
      *
      * <xpath-query>
      *   <rsc_ticket id="rsc1-req-ticketA" rsc="rsc1" ticket="ticketA" ... />
      *   <rsc_ticket id="rsc1-req-ticketB" rsc="rsc2" ticket="ticketB" ... />
      * </xpath-query>
      *
      * for when there's multiple tickets in the and the user did not ask for
      * a specific one.
      *
      * In both cases, we simply output a <rsc_ticket> element for each ticket
      * in the results.
      */
     out->info(out, "Constraints XML:\n");
 
     if (pcmk__xe_is(constraint_xml, PCMK__XE_XPATH_QUERY)) {
         xmlNode *child = pcmk__xe_first_child(constraint_xml, NULL, NULL, NULL);
 
         do {
             GString *buf = g_string_sized_new(1024);
 
             pcmk__xml_string(child, pcmk__xml_fmt_pretty, buf, 0);
             out->output_xml(out, PCMK_XE_CONSTRAINT, buf->str);
             g_string_free(buf, TRUE);
 
             child = pcmk__xe_next(child);
         } while (child != NULL);
     } else {
         GString *buf = g_string_sized_new(1024);
 
         pcmk__xml_string(constraint_xml, pcmk__xml_fmt_pretty, buf, 0);
         out->output_xml(out, PCMK_XE_CONSTRAINT, buf->str);
         g_string_free(buf, TRUE);
     }
 
     return pcmk_rc_ok;
 }
 
 static int
 add_ticket_element_with_constraints(xmlNode *node, void *userdata)
 {
     pcmk__output_t *out = (pcmk__output_t *) userdata;
     const char *ticket_id = crm_element_value(node, PCMK_XA_TICKET);
 
     pcmk__output_xml_create_parent(out, PCMK_XE_TICKET,
                                    PCMK_XA_ID, ticket_id, NULL);
     pcmk__output_xml_create_parent(out, PCMK_XE_CONSTRAINTS, NULL);
     pcmk__output_xml_add_node_copy(out, node);
 
     /* Pop two parents so now we are back under the <tickets> element */
     pcmk__output_xml_pop_parent(out);
     pcmk__output_xml_pop_parent(out);
 
     return pcmk_rc_ok;
 }
 
 static int
 add_resource_element(xmlNode *node, void *userdata)
 {
     pcmk__output_t *out = (pcmk__output_t *) userdata;
     const char *rsc = crm_element_value(node, PCMK_XA_RSC);
 
     pcmk__output_create_xml_node(out, PCMK_XE_RESOURCE,
                                  PCMK_XA_ID, rsc, NULL);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("ticket-constraints", "xmlNode *")
 static int
 ticket_constraints_xml(pcmk__output_t *out, va_list args)
 {
     xmlNode *constraint_xml = va_arg(args, xmlNode *);
 
     /* Create:
      * <tickets>
      *   <ticket id="">
      *     <constraints>
      *       <rsc_ticket />
      *     </constraints>
      *   </ticket>
      *   ...
      * </tickets>
      */
     pcmk__output_xml_create_parent(out, PCMK_XE_TICKETS, NULL);
 
     if (pcmk__xe_is(constraint_xml, PCMK__XE_XPATH_QUERY)) {
         /* Iterate through the list of children once to create all the
          * ticket/constraint elements.
          */
         pcmk__xe_foreach_child(constraint_xml, NULL, add_ticket_element_with_constraints, out);
 
         /* Put us back at the same level as where <tickets> was created. */
         pcmk__output_xml_pop_parent(out);
 
         /* Constraints can reference a resource ID that is defined in the XML
          * schema as an IDREF.  This requires some other element to be present
          * with an id= attribute that matches.
          *
          * Iterate through the list of children a second time to create the
          * following:
          *
          * <resources>
          *   <resource id="" />
          *   ...
          * </resources>
          */
         pcmk__output_xml_create_parent(out, PCMK_XE_RESOURCES, NULL);
         pcmk__xe_foreach_child(constraint_xml, NULL, add_resource_element, out);
         pcmk__output_xml_pop_parent(out);
 
     } else {
         /* Creating the output for a single constraint is much easier.  All the
          * comments in the above block apply here.
          */
         add_ticket_element_with_constraints(constraint_xml, out);
         pcmk__output_xml_pop_parent(out);
 
         pcmk__output_xml_create_parent(out, PCMK_XE_RESOURCES, NULL);
         add_resource_element(constraint_xml, out);
         pcmk__output_xml_pop_parent(out);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("ticket-state", "xmlNode *")
 static int
 ticket_state_default(pcmk__output_t *out, va_list args)
 {
     xmlNode *state_xml = va_arg(args, xmlNode *);
 
     GString *buf = g_string_sized_new(1024);
 
     out->info(out, "State XML:\n");
     pcmk__xml_string(state_xml, pcmk__xml_fmt_pretty, buf, 0);
     out->output_xml(out, PCMK__XE_TICKET_STATE, buf->str);
 
     g_string_free(buf, TRUE);
     return pcmk_rc_ok;
 }
 
 static int
 add_ticket_element(xmlNode *node, void *userdata)
 {
     pcmk__output_t *out = (pcmk__output_t *) userdata;
     xmlNode *ticket_node = NULL;
 
     ticket_node = pcmk__output_create_xml_node(out, PCMK_XE_TICKET, NULL);
     pcmk__xe_copy_attrs(ticket_node, node, pcmk__xaf_none);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("ticket-state", "xmlNode *")
 static int
 ticket_state_xml(pcmk__output_t *out, va_list args)
 {
     xmlNode *state_xml = va_arg(args, xmlNode *);
 
     /* Create:
      * <tickets>
      *   <ticket />
      *   ...
      * </tickets>
      */
     pcmk__output_xml_create_parent(out, PCMK_XE_TICKETS, NULL);
 
     if (state_xml->children != NULL) {
         /* Iterate through the list of children once to create all the
          * ticket elements.
          */
         pcmk__xe_foreach_child(state_xml, PCMK__XE_TICKET_STATE, add_ticket_element, out);
 
     } else {
         add_ticket_element(state_xml, out);
     }
 
     pcmk__output_xml_pop_parent(out);
     return pcmk_rc_ok;
 }
 
 static pcmk__message_entry_t fmt_functions[] = {
     { "attribute", "default", attribute_default },
     { "attribute", "xml", attribute_xml },
     { "cluster-status", "default", pcmk__cluster_status_text },
     { "cluster-status", "html", cluster_status_html },
     { "cluster-status", "xml", cluster_status_xml },
     { "crmadmin-node", "default", crmadmin_node },
     { "crmadmin-node", "text", crmadmin_node_text },
     { "crmadmin-node", "xml", crmadmin_node_xml },
     { "dc", "default", dc },
     { "dc", "text", dc_text },
     { "dc", "xml", dc_xml },
     { "digests", "default", digests_text },
     { "digests", "xml", digests_xml },
     { "health", "default", health },
     { "health", "text", health_text },
     { "health", "xml", health_xml },
     { "inject-attr", "default", inject_attr },
     { "inject-attr", "xml", inject_attr_xml },
     { "inject-cluster-action", "default", inject_cluster_action },
     { "inject-cluster-action", "xml", inject_cluster_action_xml },
     { "inject-fencing-action", "default", inject_fencing_action },
     { "inject-fencing-action", "xml", inject_fencing_action_xml },
     { "inject-modify-config", "default", inject_modify_config },
     { "inject-modify-config", "xml", inject_modify_config_xml },
     { "inject-modify-node", "default", inject_modify_node },
     { "inject-modify-node", "xml", inject_modify_node_xml },
     { "inject-modify-ticket", "default", inject_modify_ticket },
     { "inject-modify-ticket", "xml", inject_modify_ticket_xml },
     { "inject-pseudo-action", "default", inject_pseudo_action },
     { "inject-pseudo-action", "xml", inject_pseudo_action_xml },
     { "inject-rsc-action", "default", inject_rsc_action },
     { "inject-rsc-action", "xml", inject_rsc_action_xml },
     { "inject-spec", "default", inject_spec },
     { "inject-spec", "xml", inject_spec_xml },
     { "locations-and-colocations", "default", locations_and_colocations },
     { "locations-and-colocations", "xml", locations_and_colocations_xml },
     { "locations-list", "default", locations_list },
     { "locations-list", "xml", locations_list_xml },
     { "node-action", "default", node_action },
     { "node-action", "xml", node_action_xml },
     { "node-info", "default", node_info_default },
     { "node-info", "xml", node_info_xml },
     { "pacemakerd-health", "default", pacemakerd_health },
     { "pacemakerd-health", "html", pacemakerd_health_html },
     { "pacemakerd-health", "text", pacemakerd_health_text },
     { "pacemakerd-health", "xml", pacemakerd_health_xml },
     { "profile", "default", profile_default, },
     { "profile", "xml", profile_xml },
     { "result-code", PCMK_VALUE_NONE, result_code_none },
     { "result-code", "text", result_code_text },
     { "result-code", "xml", result_code_xml },
     { "rsc-action", "default", rsc_action_default },
     { "rsc-action-item", "default", rsc_action_item },
     { "rsc-action-item", "xml", rsc_action_item_xml },
     { "rsc-is-colocated-with-list", "default", rsc_is_colocated_with_list },
     { "rsc-is-colocated-with-list", "xml", rsc_is_colocated_with_list_xml },
     { "rscs-colocated-with-list", "default", rscs_colocated_with_list },
     { "rscs-colocated-with-list", "xml", rscs_colocated_with_list_xml },
     { "rule-check", "default", rule_check_default },
     { "rule-check", "xml", rule_check_xml },
     { "ticket-attribute", "default", ticket_attribute_default },
     { "ticket-attribute", "xml", ticket_attribute_xml },
     { "ticket-constraints", "default", ticket_constraints_default },
     { "ticket-constraints", "xml", ticket_constraints_xml },
     { "ticket-state", "default", ticket_state_default },
     { "ticket-state", "xml", ticket_state_xml },
 
     { NULL, NULL, NULL }
 };
 
 void
 pcmk__register_lib_messages(pcmk__output_t *out) {
     pcmk__register_messages(out, fmt_functions);
 }
diff --git a/lib/pacemaker/pcmk_sched_actions.c b/lib/pacemaker/pcmk_sched_actions.c
index d9fb0b2ace..43727c5733 100644
--- a/lib/pacemaker/pcmk_sched_actions.c
+++ b/lib/pacemaker/pcmk_sched_actions.c
@@ -1,1935 +1,1935 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <stdio.h>
 #include <sys/param.h>
 #include <glib.h>
 
 #include <crm/lrmd_internal.h>
 #include <crm/common/scheduler_internal.h>
 #include <pacemaker-internal.h>
 #include "libpacemaker_private.h"
 
 /*!
  * \internal
  * \brief Get the action flags relevant to ordering constraints
  *
  * \param[in,out] action  Action to check
  * \param[in]     node    Node that *other* action in the ordering is on
  *                        (used only for clone resource actions)
  *
  * \return Action flags that should be used for orderings
  */
 static uint32_t
 action_flags_for_ordering(pcmk_action_t *action, const pcmk_node_t *node)
 {
     bool runnable = false;
     uint32_t flags;
 
     // For non-resource actions, return the action flags
     if (action->rsc == NULL) {
         return action->flags;
     }
 
     /* For non-clone resources, or a clone action not assigned to a node,
      * return the flags as determined by the resource method without a node
      * specified.
      */
-    flags = action->rsc->cmds->action_flags(action, NULL);
+    flags = action->rsc->private->cmds->action_flags(action, NULL);
     if ((node == NULL) || !pcmk__is_clone(action->rsc)) {
         return flags;
     }
 
     /* Otherwise (i.e., for clone resource actions on a specific node), first
      * remember whether the non-node-specific action is runnable.
      */
     runnable = pcmk_is_set(flags, pcmk_action_runnable);
 
     // Then recheck the resource method with the node
-    flags = action->rsc->cmds->action_flags(action, node);
+    flags = action->rsc->private->cmds->action_flags(action, node);
 
     /* For clones in ordering constraints, the node-specific "runnable" doesn't
      * matter, just the non-node-specific setting (i.e., is the action runnable
      * anywhere).
      *
      * This applies only to runnable, and only for ordering constraints. This
      * function shouldn't be used for other types of constraints without
      * changes. Not very satisfying, but it's logical and appears to work well.
      */
     if (runnable && !pcmk_is_set(flags, pcmk_action_runnable)) {
         pcmk__set_raw_action_flags(flags, action->rsc->id,
                                    pcmk_action_runnable);
     }
     return flags;
 }
 
 /*!
  * \internal
  * \brief Get action UUID that should be used with a resource ordering
  *
  * When an action is ordered relative to an action for a collective resource
  * (clone, group, or bundle), it actually needs to be ordered after all
  * instances of the collective have completed the relevant action (for example,
  * given "start CLONE then start RSC", RSC must wait until all instances of
  * CLONE have started). Given the UUID and resource of the first action in an
  * ordering, this returns the UUID of the action that should actually be used
  * for ordering (for example, "CLONE_started_0" instead of "CLONE_start_0").
  *
  * \param[in] first_uuid    UUID of first action in ordering
  * \param[in] first_rsc     Resource of first action in ordering
  *
  * \return Newly allocated copy of UUID to use with ordering
  * \note It is the caller's responsibility to free the return value.
  */
 static char *
 action_uuid_for_ordering(const char *first_uuid,
                          const pcmk_resource_t *first_rsc)
 {
     guint interval_ms = 0;
     char *uuid = NULL;
     char *rid = NULL;
     char *first_task_str = NULL;
     enum action_tasks first_task = pcmk_action_unspecified;
     enum action_tasks remapped_task = pcmk_action_unspecified;
 
     // Only non-notify actions for collective resources need remapping
     if ((strstr(first_uuid, PCMK_ACTION_NOTIFY) != NULL)
         || (first_rsc->variant < pcmk_rsc_variant_group)) {
         goto done;
     }
 
     // Only non-recurring actions need remapping
     CRM_ASSERT(parse_op_key(first_uuid, &rid, &first_task_str, &interval_ms));
     if (interval_ms > 0) {
         goto done;
     }
 
     first_task = pcmk_parse_action(first_task_str);
     switch (first_task) {
         case pcmk_action_stop:
         case pcmk_action_start:
         case pcmk_action_notify:
         case pcmk_action_promote:
         case pcmk_action_demote:
             remapped_task = first_task + 1;
             break;
         case pcmk_action_stopped:
         case pcmk_action_started:
         case pcmk_action_notified:
         case pcmk_action_promoted:
         case pcmk_action_demoted:
             remapped_task = first_task;
             break;
         case pcmk_action_monitor:
         case pcmk_action_shutdown:
         case pcmk_action_fence:
             break;
         default:
             crm_err("Unknown action '%s' in ordering", first_task_str);
             break;
     }
 
     if (remapped_task != pcmk_action_unspecified) {
         /* If a clone or bundle has notifications enabled, the ordering will be
          * relative to when notifications have been sent for the remapped task.
          */
         if (pcmk_is_set(first_rsc->flags, pcmk_rsc_notify)
             && (pcmk__is_clone(first_rsc) || pcmk__is_bundled(first_rsc))) {
             uuid = pcmk__notify_key(rid, "confirmed-post",
                                     pcmk_action_text(remapped_task));
         } else {
             uuid = pcmk__op_key(rid, pcmk_action_text(remapped_task), 0);
         }
         pcmk__rsc_trace(first_rsc,
                         "Remapped action UUID %s to %s for ordering purposes",
                         first_uuid, uuid);
     }
 
 done:
     free(first_task_str);
     free(rid);
     return (uuid != NULL)? uuid : pcmk__str_copy(first_uuid);
 }
 
 /*!
  * \internal
  * \brief Get actual action that should be used with an ordering
  *
  * When an action is ordered relative to an action for a collective resource
  * (clone, group, or bundle), it actually needs to be ordered after all
  * instances of the collective have completed the relevant action (for example,
  * given "start CLONE then start RSC", RSC must wait until all instances of
  * CLONE have started). Given the first action in an ordering, this returns the
  * the action that should actually be used for ordering (for example, the
  * started action instead of the start action).
  *
  * \param[in] action  First action in an ordering
  *
  * \return Actual action that should be used for the ordering
  */
 static pcmk_action_t *
 action_for_ordering(pcmk_action_t *action)
 {
     pcmk_action_t *result = action;
     pcmk_resource_t *rsc = action->rsc;
 
     if ((rsc != NULL) && (rsc->variant >= pcmk_rsc_variant_group)
         && (action->uuid != NULL)) {
         char *uuid = action_uuid_for_ordering(action->uuid, rsc);
 
         result = find_first_action(rsc->actions, uuid, NULL, NULL);
         if (result == NULL) {
             crm_warn("Not remapping %s to %s because %s does not have "
                      "remapped action", action->uuid, uuid, rsc->id);
             result = action;
         }
         free(uuid);
     }
     return result;
 }
 
 /*!
  * \internal
  * \brief Wrapper for update_ordered_actions() method for readability
  *
  * \param[in,out] rsc        Resource to call method for
  * \param[in,out] first      'First' action in an ordering
  * \param[in,out] then       'Then' action in an ordering
  * \param[in]     node       If not NULL, limit scope of ordering to this
  *                           node (only used when interleaving instances)
  * \param[in]     flags      Action flags for \p first for ordering purposes
  * \param[in]     filter     Action flags to limit scope of certain updates
  *                           (may include pcmk_action_optional to affect only
  *                           mandatory actions, and pe_action_runnable to
  *                           affect only runnable actions)
  * \param[in]     type       Group of enum pcmk__action_relation_flags to apply
  * \param[in,out] scheduler  Scheduler data
  *
  * \return Group of enum pcmk__updated flags indicating what was updated
  */
 static inline uint32_t
 update(pcmk_resource_t *rsc, pcmk_action_t *first, pcmk_action_t *then,
        const pcmk_node_t *node, uint32_t flags, uint32_t filter, uint32_t type,
        pcmk_scheduler_t *scheduler)
 {
-    return rsc->cmds->update_ordered_actions(first, then, node, flags, filter,
-                                             type, scheduler);
+    return rsc->private->cmds->update_ordered_actions(first, then, node, flags,
+                                                      filter, type, scheduler);
 }
 
 /*!
  * \internal
  * \brief Update flags for ordering's actions appropriately for ordering's flags
  *
  * \param[in,out] first        First action in an ordering
  * \param[in,out] then         Then action in an ordering
  * \param[in]     first_flags  Action flags for \p first for ordering purposes
  * \param[in]     then_flags   Action flags for \p then for ordering purposes
  * \param[in,out] order        Action wrapper for \p first in ordering
  * \param[in,out] scheduler    Scheduler data
  *
  * \return Group of enum pcmk__updated flags
  */
 static uint32_t
 update_action_for_ordering_flags(pcmk_action_t *first, pcmk_action_t *then,
                                  uint32_t first_flags, uint32_t then_flags,
                                  pcmk__related_action_t *order,
                                  pcmk_scheduler_t *scheduler)
 {
     uint32_t changed = pcmk__updated_none;
 
     /* The node will only be used for clones. If interleaved, node will be NULL,
      * otherwise the ordering scope will be limited to the node. Normally, the
      * whole 'then' clone should restart if 'first' is restarted, so then->node
      * is needed.
      */
     pcmk_node_t *node = then->node;
 
     if (pcmk_is_set(order->type, pcmk__ar_first_implies_same_node_then)) {
         /* For unfencing, only instances of 'then' on the same node as 'first'
          * (the unfencing operation) should restart, so reset node to
          * first->node, at which point this case is handled like a normal
          * pcmk__ar_first_implies_then.
          */
         pcmk__clear_relation_flags(order->type,
                                    pcmk__ar_first_implies_same_node_then);
         pcmk__set_relation_flags(order->type, pcmk__ar_first_implies_then);
         node = first->node;
         pcmk__rsc_trace(then->rsc,
                         "%s then %s: mapped "
                         "pcmk__ar_first_implies_same_node_then to "
                         "pcmk__ar_first_implies_then on %s",
                         first->uuid, then->uuid, pcmk__node_name(node));
     }
 
     if (pcmk_is_set(order->type, pcmk__ar_first_implies_then)) {
         if (then->rsc != NULL) {
             changed |= update(then->rsc, first, then, node,
                               first_flags & pcmk_action_optional,
                               pcmk_action_optional, pcmk__ar_first_implies_then,
                               scheduler);
         } else if (!pcmk_is_set(first_flags, pcmk_action_optional)
                    && pcmk_is_set(then->flags, pcmk_action_optional)) {
             pcmk__clear_action_flags(then, pcmk_action_optional);
             pcmk__set_updated_flags(changed, first, pcmk__updated_then);
         }
         pcmk__rsc_trace(then->rsc,
                         "%s then %s: %s after pcmk__ar_first_implies_then",
                         first->uuid, then->uuid,
                         (changed? "changed" : "unchanged"));
     }
 
     if (pcmk_is_set(order->type, pcmk__ar_intermediate_stop)
         && (then->rsc != NULL)) {
         enum pe_action_flags restart = pcmk_action_optional
                                        |pcmk_action_runnable;
 
         changed |= update(then->rsc, first, then, node, first_flags, restart,
                           pcmk__ar_intermediate_stop, scheduler);
         pcmk__rsc_trace(then->rsc,
                         "%s then %s: %s after pcmk__ar_intermediate_stop",
                         first->uuid, then->uuid,
                         (changed? "changed" : "unchanged"));
     }
 
     if (pcmk_is_set(order->type, pcmk__ar_then_implies_first)) {
         if (first->rsc != NULL) {
             changed |= update(first->rsc, first, then, node, first_flags,
                               pcmk_action_optional, pcmk__ar_then_implies_first,
                               scheduler);
         } else if (!pcmk_is_set(first_flags, pcmk_action_optional)
                    && pcmk_is_set(first->flags, pcmk_action_runnable)) {
             pcmk__clear_action_flags(first, pcmk_action_runnable);
             pcmk__set_updated_flags(changed, first, pcmk__updated_first);
         }
         pcmk__rsc_trace(then->rsc,
                         "%s then %s: %s after pcmk__ar_then_implies_first",
                         first->uuid, then->uuid,
                         (changed? "changed" : "unchanged"));
     }
 
     if (pcmk_is_set(order->type, pcmk__ar_promoted_then_implies_first)) {
         if (then->rsc != NULL) {
             changed |= update(then->rsc, first, then, node,
                               first_flags & pcmk_action_optional,
                               pcmk_action_optional,
                               pcmk__ar_promoted_then_implies_first, scheduler);
         }
         pcmk__rsc_trace(then->rsc,
                         "%s then %s: %s after "
                         "pcmk__ar_promoted_then_implies_first",
                         first->uuid, then->uuid,
                         (changed? "changed" : "unchanged"));
     }
 
     if (pcmk_is_set(order->type, pcmk__ar_min_runnable)) {
         if (then->rsc != NULL) {
             changed |= update(then->rsc, first, then, node, first_flags,
                               pcmk_action_runnable, pcmk__ar_min_runnable,
                               scheduler);
 
         } else if (pcmk_is_set(first_flags, pcmk_action_runnable)) {
             // We have another runnable instance of "first"
             then->runnable_before++;
 
             /* Mark "then" as runnable if it requires a certain number of
              * "before" instances to be runnable, and they now are.
              */
             if ((then->runnable_before >= then->required_runnable_before)
                 && !pcmk_is_set(then->flags, pcmk_action_runnable)) {
 
                 pcmk__set_action_flags(then, pcmk_action_runnable);
                 pcmk__set_updated_flags(changed, first, pcmk__updated_then);
             }
         }
         pcmk__rsc_trace(then->rsc, "%s then %s: %s after pcmk__ar_min_runnable",
                         first->uuid, then->uuid,
                         (changed? "changed" : "unchanged"));
     }
 
     if (pcmk_is_set(order->type, pcmk__ar_nested_remote_probe)
         && (then->rsc != NULL)) {
 
         if (!pcmk_is_set(first_flags, pcmk_action_runnable)
             && (first->rsc != NULL) && (first->rsc->running_on != NULL)) {
 
             pcmk__rsc_trace(then->rsc,
                             "%s then %s: ignoring because first is stopping",
                             first->uuid, then->uuid);
             order->type = (enum pe_ordering) pcmk__ar_none;
         } else {
             changed |= update(then->rsc, first, then, node, first_flags,
                               pcmk_action_runnable,
                               pcmk__ar_unrunnable_first_blocks, scheduler);
         }
         pcmk__rsc_trace(then->rsc,
                         "%s then %s: %s after pcmk__ar_nested_remote_probe",
                         first->uuid, then->uuid,
                         (changed? "changed" : "unchanged"));
     }
 
     if (pcmk_is_set(order->type, pcmk__ar_unrunnable_first_blocks)) {
         if (then->rsc != NULL) {
             changed |= update(then->rsc, first, then, node, first_flags,
                               pcmk_action_runnable,
                               pcmk__ar_unrunnable_first_blocks, scheduler);
 
         } else if (!pcmk_is_set(first_flags, pcmk_action_runnable)
                    && pcmk_is_set(then->flags, pcmk_action_runnable)) {
 
             pcmk__clear_action_flags(then, pcmk_action_runnable);
             pcmk__set_updated_flags(changed, first, pcmk__updated_then);
         }
         pcmk__rsc_trace(then->rsc,
                         "%s then %s: %s after pcmk__ar_unrunnable_first_blocks",
                         first->uuid, then->uuid,
                         (changed? "changed" : "unchanged"));
     }
 
     if (pcmk_is_set(order->type, pcmk__ar_unmigratable_then_blocks)) {
         if (then->rsc != NULL) {
             changed |= update(then->rsc, first, then, node, first_flags,
                               pcmk_action_optional,
                               pcmk__ar_unmigratable_then_blocks, scheduler);
         }
         pcmk__rsc_trace(then->rsc,
                         "%s then %s: %s after "
                         "pcmk__ar_unmigratable_then_blocks",
                         first->uuid, then->uuid,
                         (changed? "changed" : "unchanged"));
     }
 
     if (pcmk_is_set(order->type, pcmk__ar_first_else_then)) {
         if (then->rsc != NULL) {
             changed |= update(then->rsc, first, then, node, first_flags,
                               pcmk_action_optional, pcmk__ar_first_else_then,
                               scheduler);
         }
         pcmk__rsc_trace(then->rsc,
                         "%s then %s: %s after pcmk__ar_first_else_then",
                         first->uuid, then->uuid,
                         (changed? "changed" : "unchanged"));
     }
 
     if (pcmk_is_set(order->type, pcmk__ar_ordered)) {
         if (then->rsc != NULL) {
             changed |= update(then->rsc, first, then, node, first_flags,
                               pcmk_action_runnable, pcmk__ar_ordered,
                               scheduler);
         }
         pcmk__rsc_trace(then->rsc, "%s then %s: %s after pcmk__ar_ordered",
                         first->uuid, then->uuid,
                         (changed? "changed" : "unchanged"));
     }
 
     if (pcmk_is_set(order->type, pcmk__ar_asymmetric)) {
         if (then->rsc != NULL) {
             changed |= update(then->rsc, first, then, node, first_flags,
                               pcmk_action_runnable, pcmk__ar_asymmetric,
                               scheduler);
         }
         pcmk__rsc_trace(then->rsc, "%s then %s: %s after pcmk__ar_asymmetric",
                         first->uuid, then->uuid,
                         (changed? "changed" : "unchanged"));
     }
 
     if (pcmk_is_set(first->flags, pcmk_action_runnable)
         && pcmk_is_set(order->type, pcmk__ar_first_implies_then_graphed)
         && !pcmk_is_set(first_flags, pcmk_action_optional)) {
 
         pcmk__rsc_trace(then->rsc, "%s will be in graph because %s is required",
                         then->uuid, first->uuid);
         pcmk__set_action_flags(then, pcmk_action_always_in_graph);
         // Don't bother marking 'then' as changed just for this
     }
 
     if (pcmk_is_set(order->type, pcmk__ar_then_implies_first_graphed)
         && !pcmk_is_set(then_flags, pcmk_action_optional)) {
 
         pcmk__rsc_trace(then->rsc, "%s will be in graph because %s is required",
                         first->uuid, then->uuid);
         pcmk__set_action_flags(first, pcmk_action_always_in_graph);
         // Don't bother marking 'first' as changed just for this
     }
 
     if (pcmk_any_flags_set(order->type, pcmk__ar_first_implies_then
                                         |pcmk__ar_then_implies_first
                                         |pcmk__ar_intermediate_stop)
         && (first->rsc != NULL)
         && !pcmk_is_set(first->rsc->flags, pcmk_rsc_managed)
         && pcmk_is_set(first->rsc->flags, pcmk_rsc_blocked)
         && !pcmk_is_set(first->flags, pcmk_action_runnable)
         && pcmk__str_eq(first->task, PCMK_ACTION_STOP, pcmk__str_none)) {
 
         if (pcmk_is_set(then->flags, pcmk_action_runnable)) {
             pcmk__clear_action_flags(then, pcmk_action_runnable);
             pcmk__set_updated_flags(changed, first, pcmk__updated_then);
         }
         pcmk__rsc_trace(then->rsc,
                         "%s then %s: %s after checking whether first "
                         "is blocked, unmanaged, unrunnable stop",
                         first->uuid, then->uuid,
                         (changed? "changed" : "unchanged"));
     }
 
     return changed;
 }
 
 // Convenience macros for logging action properties
 
 #define action_type_str(flags) \
     (pcmk_is_set((flags), pcmk_action_pseudo)? "pseudo-action" : "action")
 
 #define action_optional_str(flags) \
     (pcmk_is_set((flags), pcmk_action_optional)? "optional" : "required")
 
 #define action_runnable_str(flags) \
     (pcmk_is_set((flags), pcmk_action_runnable)? "runnable" : "unrunnable")
 
 #define action_node_str(a) \
     (((a)->node == NULL)? "no node" : (a)->node->details->uname)
 
 /*!
  * \internal
  * \brief Update an action's flags for all orderings where it is "then"
  *
  * \param[in,out] then       Action to update
  * \param[in,out] scheduler  Scheduler data
  */
 void
 pcmk__update_action_for_orderings(pcmk_action_t *then,
                                   pcmk_scheduler_t *scheduler)
 {
     GList *lpc = NULL;
     uint32_t changed = pcmk__updated_none;
     int last_flags = then->flags;
 
     pcmk__rsc_trace(then->rsc, "Updating %s %s (%s %s) on %s",
                     action_type_str(then->flags), then->uuid,
                     action_optional_str(then->flags),
                     action_runnable_str(then->flags), action_node_str(then));
 
     if (pcmk_is_set(then->flags, pcmk_action_min_runnable)) {
         /* Initialize current known "runnable before" actions. As
          * update_action_for_ordering_flags() is called for each of then's
          * before actions, this number will increment as runnable 'first'
          * actions are encountered.
          */
         then->runnable_before = 0;
 
         if (then->required_runnable_before == 0) {
             /* @COMPAT This ordering constraint uses the deprecated
              * PCMK_XA_REQUIRE_ALL=PCMK_VALUE_FALSE attribute. Treat it like
              * PCMK_META_CLONE_MIN=1.
              */
             then->required_runnable_before = 1;
         }
 
         /* The pcmk__ar_min_runnable clause of
          * update_action_for_ordering_flags() (called below)
          * will reset runnable if appropriate.
          */
         pcmk__clear_action_flags(then, pcmk_action_runnable);
     }
 
     for (lpc = then->actions_before; lpc != NULL; lpc = lpc->next) {
         pcmk__related_action_t *other = lpc->data;
         pcmk_action_t *first = other->action;
 
         pcmk_node_t *then_node = then->node;
         pcmk_node_t *first_node = first->node;
 
         if ((first->rsc != NULL)
             && pcmk__is_group(first->rsc)
             && pcmk__str_eq(first->task, PCMK_ACTION_START, pcmk__str_none)) {
 
             first_node = first->rsc->private->fns->location(first->rsc, NULL,
                                                             FALSE);
             if (first_node != NULL) {
                 pcmk__rsc_trace(first->rsc, "Found %s for 'first' %s",
                                 pcmk__node_name(first_node), first->uuid);
             }
         }
 
         if (pcmk__is_group(then->rsc)
             && pcmk__str_eq(then->task, PCMK_ACTION_START, pcmk__str_none)) {
 
             then_node = then->rsc->private->fns->location(then->rsc, NULL,
                                                           FALSE);
             if (then_node != NULL) {
                 pcmk__rsc_trace(then->rsc, "Found %s for 'then' %s",
                                 pcmk__node_name(then_node), then->uuid);
             }
         }
 
         // Disable constraint if it only applies when on same node, but isn't
         if (pcmk_is_set(other->type, pcmk__ar_if_on_same_node)
             && (first_node != NULL) && (then_node != NULL)
             && !pcmk__same_node(first_node, then_node)) {
 
             pcmk__rsc_trace(then->rsc,
                             "Disabled ordering %s on %s then %s on %s: "
                             "not same node",
                             other->action->uuid, pcmk__node_name(first_node),
                             then->uuid, pcmk__node_name(then_node));
             other->type = (enum pe_ordering) pcmk__ar_none;
             continue;
         }
 
         pcmk__clear_updated_flags(changed, then, pcmk__updated_first);
 
         if ((first->rsc != NULL)
             && pcmk_is_set(other->type, pcmk__ar_then_cancels_first)
             && !pcmk_is_set(then->flags, pcmk_action_optional)) {
 
             /* 'then' is required, so we must abandon 'first'
              * (e.g. a required stop cancels any agent reload).
              */
             pcmk__set_action_flags(other->action, pcmk_action_optional);
             if (!strcmp(first->task, PCMK_ACTION_RELOAD_AGENT)) {
                 pcmk__clear_rsc_flags(first->rsc, pcmk_rsc_reload);
             }
         }
 
         if ((first->rsc != NULL) && (then->rsc != NULL)
             && (first->rsc != then->rsc) && !is_parent(then->rsc, first->rsc)) {
             first = action_for_ordering(first);
         }
         if (first != other->action) {
             pcmk__rsc_trace(then->rsc, "Ordering %s after %s instead of %s",
                             then->uuid, first->uuid, other->action->uuid);
         }
 
         pcmk__rsc_trace(then->rsc,
                         "%s (%#.6x) then %s (%#.6x): type=%#.6x node=%s",
                         first->uuid, first->flags, then->uuid, then->flags,
                         other->type, action_node_str(first));
 
         if (first == other->action) {
             /* 'first' was not remapped (e.g. from 'start' to 'running'), which
              * could mean it is a non-resource action, a primitive resource
              * action, or already expanded.
              */
             uint32_t first_flags, then_flags;
 
             first_flags = action_flags_for_ordering(first, then_node);
             then_flags = action_flags_for_ordering(then, first_node);
 
             changed |= update_action_for_ordering_flags(first, then,
                                                         first_flags, then_flags,
                                                         other, scheduler);
 
             /* 'first' was for a complex resource (clone, group, etc),
              * create a new dependency if necessary
              */
         } else if (order_actions(first, then, other->type)) {
             /* This was the first time 'first' and 'then' were associated,
              * start again to get the new actions_before list
              */
             pcmk__set_updated_flags(changed, then, pcmk__updated_then);
             pcmk__rsc_trace(then->rsc,
                             "Disabled ordering %s then %s in favor of %s "
                             "then %s",
                             other->action->uuid, then->uuid, first->uuid,
                             then->uuid);
             other->type = (enum pe_ordering) pcmk__ar_none;
         }
 
 
         if (pcmk_is_set(changed, pcmk__updated_first)) {
             crm_trace("Re-processing %s and its 'after' actions "
                       "because it changed", first->uuid);
             for (GList *lpc2 = first->actions_after; lpc2 != NULL;
                  lpc2 = lpc2->next) {
                 pcmk__related_action_t *other = lpc2->data;
 
                 pcmk__update_action_for_orderings(other->action, scheduler);
             }
             pcmk__update_action_for_orderings(first, scheduler);
         }
     }
 
     if (pcmk_is_set(then->flags, pcmk_action_min_runnable)) {
         if (last_flags == then->flags) {
             pcmk__clear_updated_flags(changed, then, pcmk__updated_then);
         } else {
             pcmk__set_updated_flags(changed, then, pcmk__updated_then);
         }
     }
 
     if (pcmk_is_set(changed, pcmk__updated_then)) {
         crm_trace("Re-processing %s and its 'after' actions because it changed",
                   then->uuid);
         if (pcmk_is_set(last_flags, pcmk_action_runnable)
             && !pcmk_is_set(then->flags, pcmk_action_runnable)) {
             pcmk__block_colocation_dependents(then);
         }
         pcmk__update_action_for_orderings(then, scheduler);
         for (lpc = then->actions_after; lpc != NULL; lpc = lpc->next) {
             pcmk__related_action_t *other = lpc->data;
 
             pcmk__update_action_for_orderings(other->action, scheduler);
         }
     }
 }
 
 static inline bool
 is_primitive_action(const pcmk_action_t *action)
 {
     return (action != NULL) && pcmk__is_primitive(action->rsc);
 }
 
 /*!
  * \internal
  * \brief Clear a single action flag and set reason text
  *
  * \param[in,out] action  Action whose flag should be cleared
  * \param[in]     flag    Action flag that should be cleared
  * \param[in]     reason  Action that is the reason why flag is being cleared
  */
 #define clear_action_flag_because(action, flag, reason) do {                \
         if (pcmk_is_set((action)->flags, (flag))) {                         \
             pcmk__clear_action_flags(action, flag);                         \
             if ((action)->rsc != (reason)->rsc) {                           \
                 char *reason_text = pe__action2reason((reason), (flag));    \
                 pe_action_set_reason((action), reason_text, false);         \
                 free(reason_text);                                          \
             }                                                               \
         }                                                                   \
     } while (0)
 
 /*!
  * \internal
  * \brief Update actions in an asymmetric ordering
  *
  * If the "first" action in an asymmetric ordering is unrunnable, make the
  * "second" action unrunnable as well, if appropriate.
  *
  * \param[in]     first  'First' action in an asymmetric ordering
  * \param[in,out] then   'Then' action in an asymmetric ordering
  */
 static void
 handle_asymmetric_ordering(const pcmk_action_t *first, pcmk_action_t *then)
 {
     /* Only resource actions after an unrunnable 'first' action need updates for
      * asymmetric ordering.
      */
     if ((then->rsc == NULL)
         || pcmk_is_set(first->flags, pcmk_action_runnable)) {
         return;
     }
 
     // Certain optional 'then' actions are unaffected by unrunnable 'first'
     if (pcmk_is_set(then->flags, pcmk_action_optional)) {
         enum rsc_role_e then_rsc_role;
 
         then_rsc_role = then->rsc->private->fns->state(then->rsc, TRUE);
 
         if ((then_rsc_role == pcmk_role_stopped)
             && pcmk__str_eq(then->task, PCMK_ACTION_STOP, pcmk__str_none)) {
             /* If 'then' should stop after 'first' but is already stopped, the
              * ordering is irrelevant.
              */
             return;
         } else if ((then_rsc_role >= pcmk_role_started)
             && pcmk__str_eq(then->task, PCMK_ACTION_START, pcmk__str_none)
             && pe__rsc_running_on_only(then->rsc, then->node)) {
             /* Similarly if 'then' should start after 'first' but is already
              * started on a single node.
              */
             return;
         }
     }
 
     // 'First' can't run, so 'then' can't either
     clear_action_flag_because(then, pcmk_action_optional, first);
     clear_action_flag_because(then, pcmk_action_runnable, first);
 }
 
 /*!
  * \internal
  * \brief Set action bits appropriately when pe_restart_order is used
  *
  * \param[in,out] first   'First' action in an ordering with pe_restart_order
  * \param[in,out] then    'Then' action in an ordering with pe_restart_order
  * \param[in]     filter  What action flags to care about
  *
  * \note pe_restart_order is set for "stop resource before starting it" and
  *       "stop later group member before stopping earlier group member"
  */
 static void
 handle_restart_ordering(pcmk_action_t *first, pcmk_action_t *then,
                         uint32_t filter)
 {
     const char *reason = NULL;
 
     CRM_ASSERT(is_primitive_action(first));
     CRM_ASSERT(is_primitive_action(then));
 
     // We need to update the action in two cases:
 
     // ... if 'then' is required
     if (pcmk_is_set(filter, pcmk_action_optional)
         && !pcmk_is_set(then->flags, pcmk_action_optional)) {
         reason = "restart";
     }
 
     /* ... if 'then' is unrunnable action on same resource (if a resource
      * should restart but can't start, we still want to stop)
      */
     if (pcmk_is_set(filter, pcmk_action_runnable)
         && !pcmk_is_set(then->flags, pcmk_action_runnable)
         && pcmk_is_set(then->rsc->flags, pcmk_rsc_managed)
         && (first->rsc == then->rsc)) {
         reason = "stop";
     }
 
     if (reason == NULL) {
         return;
     }
 
     pcmk__rsc_trace(first->rsc, "Handling %s -> %s for %s",
                     first->uuid, then->uuid, reason);
 
     // Make 'first' required if it is runnable
     if (pcmk_is_set(first->flags, pcmk_action_runnable)) {
         clear_action_flag_because(first, pcmk_action_optional, then);
     }
 
     // Make 'first' required if 'then' is required
     if (!pcmk_is_set(then->flags, pcmk_action_optional)) {
         clear_action_flag_because(first, pcmk_action_optional, then);
     }
 
     // Make 'first' unmigratable if 'then' is unmigratable
     if (!pcmk_is_set(then->flags, pcmk_action_migratable)) {
         clear_action_flag_because(first, pcmk_action_migratable, then);
     }
 
     // Make 'then' unrunnable if 'first' is required but unrunnable
     if (!pcmk_is_set(first->flags, pcmk_action_optional)
         && !pcmk_is_set(first->flags, pcmk_action_runnable)) {
         clear_action_flag_because(then, pcmk_action_runnable, first);
     }
 }
 
 /*!
  * \internal
  * \brief Update two actions according to an ordering between them
  *
  * Given information about an ordering of two actions, update the actions' flags
  * (and runnable_before members if appropriate) as appropriate for the ordering.
  * Effects may cascade to other orderings involving the actions as well.
  *
  * \param[in,out] first      'First' action in an ordering
  * \param[in,out] then       'Then' action in an ordering
  * \param[in]     node       If not NULL, limit scope of ordering to this node
  *                           (ignored)
  * \param[in]     flags      Action flags for \p first for ordering purposes
  * \param[in]     filter     Action flags to limit scope of certain updates (may
  *                           include pcmk_action_optional to affect only
  *                           mandatory actions, and pcmk_action_runnable to
  *                           affect only runnable actions)
  * \param[in]     type       Group of enum pcmk__action_relation_flags to apply
  * \param[in,out] scheduler  Scheduler data
  *
  * \return Group of enum pcmk__updated flags indicating what was updated
  */
 uint32_t
 pcmk__update_ordered_actions(pcmk_action_t *first, pcmk_action_t *then,
                              const pcmk_node_t *node, uint32_t flags,
                              uint32_t filter, uint32_t type,
                              pcmk_scheduler_t *scheduler)
 {
     uint32_t changed = pcmk__updated_none;
     uint32_t then_flags = 0U;
     uint32_t first_flags = 0U;
 
     CRM_ASSERT((first != NULL) && (then != NULL) && (scheduler != NULL));
 
     then_flags = then->flags;
     first_flags = first->flags;
     if (pcmk_is_set(type, pcmk__ar_asymmetric)) {
         handle_asymmetric_ordering(first, then);
     }
 
     if (pcmk_is_set(type, pcmk__ar_then_implies_first)
         && !pcmk_is_set(then_flags, pcmk_action_optional)) {
         // Then is required, and implies first should be, too
 
         if (pcmk_is_set(filter, pcmk_action_optional)
             && !pcmk_is_set(flags, pcmk_action_optional)
             && pcmk_is_set(first_flags, pcmk_action_optional)) {
             clear_action_flag_because(first, pcmk_action_optional, then);
         }
 
         if (pcmk_is_set(flags, pcmk_action_migratable)
             && !pcmk_is_set(then->flags, pcmk_action_migratable)) {
             clear_action_flag_because(first, pcmk_action_migratable, then);
         }
     }
 
     if (pcmk_is_set(type, pcmk__ar_promoted_then_implies_first)
         && (then->rsc != NULL) && (then->rsc->role == pcmk_role_promoted)
         && pcmk_is_set(filter, pcmk_action_optional)
         && !pcmk_is_set(then->flags, pcmk_action_optional)) {
 
         clear_action_flag_because(first, pcmk_action_optional, then);
 
         if (pcmk_is_set(first->flags, pcmk_action_migratable)
             && !pcmk_is_set(then->flags, pcmk_action_migratable)) {
             clear_action_flag_because(first, pcmk_action_migratable, then);
         }
     }
 
     if (pcmk_is_set(type, pcmk__ar_unmigratable_then_blocks)
         && pcmk_is_set(filter, pcmk_action_optional)) {
 
         if (!pcmk_all_flags_set(then->flags, pcmk_action_migratable
                                              |pcmk_action_runnable)) {
             clear_action_flag_because(first, pcmk_action_runnable, then);
         }
 
         if (!pcmk_is_set(then->flags, pcmk_action_optional)) {
             clear_action_flag_because(first, pcmk_action_optional, then);
         }
     }
 
     if (pcmk_is_set(type, pcmk__ar_first_else_then)
         && pcmk_is_set(filter, pcmk_action_optional)
         && !pcmk_is_set(first->flags, pcmk_action_runnable)) {
 
         clear_action_flag_because(then, pcmk_action_migratable, first);
         pcmk__clear_action_flags(then, pcmk_action_pseudo);
     }
 
     if (pcmk_is_set(type, pcmk__ar_unrunnable_first_blocks)
         && pcmk_is_set(filter, pcmk_action_runnable)
         && pcmk_is_set(then->flags, pcmk_action_runnable)
         && !pcmk_is_set(flags, pcmk_action_runnable)) {
 
         clear_action_flag_because(then, pcmk_action_runnable, first);
         clear_action_flag_because(then, pcmk_action_migratable, first);
     }
 
     if (pcmk_is_set(type, pcmk__ar_first_implies_then)
         && pcmk_is_set(filter, pcmk_action_optional)
         && pcmk_is_set(then->flags, pcmk_action_optional)
         && !pcmk_is_set(flags, pcmk_action_optional)
         && !pcmk_is_set(first->flags, pcmk_action_migratable)) {
 
         clear_action_flag_because(then, pcmk_action_optional, first);
     }
 
     if (pcmk_is_set(type, pcmk__ar_intermediate_stop)) {
         handle_restart_ordering(first, then, filter);
     }
 
     if (then_flags != then->flags) {
         pcmk__set_updated_flags(changed, first, pcmk__updated_then);
         pcmk__rsc_trace(then->rsc,
                         "%s on %s: flags are now %#.6x (was %#.6x) "
                         "because of 'first' %s (%#.6x)",
                         then->uuid, pcmk__node_name(then->node),
                         then->flags, then_flags, first->uuid, first->flags);
 
         if ((then->rsc != NULL) && (then->rsc->parent != NULL)) {
             // Required to handle "X_stop then X_start" for cloned groups
             pcmk__update_action_for_orderings(then, scheduler);
         }
     }
 
     if (first_flags != first->flags) {
         pcmk__set_updated_flags(changed, first, pcmk__updated_first);
         pcmk__rsc_trace(first->rsc,
                         "%s on %s: flags are now %#.6x (was %#.6x) "
                         "because of 'then' %s (%#.6x)",
                         first->uuid, pcmk__node_name(first->node),
                         first->flags, first_flags, then->uuid, then->flags);
     }
 
     return changed;
 }
 
 /*!
  * \internal
  * \brief Trace-log an action (optionally with its dependent actions)
  *
  * \param[in] pre_text  If not NULL, prefix the log with this plus ": "
  * \param[in] action    Action to log
  * \param[in] details   If true, recursively log dependent actions
  */
 void
 pcmk__log_action(const char *pre_text, const pcmk_action_t *action,
                  bool details)
 {
     const char *node_uname = NULL;
     const char *node_uuid = NULL;
     const char *desc = NULL;
 
     CRM_CHECK(action != NULL, return);
 
     if (!pcmk_is_set(action->flags, pcmk_action_pseudo)) {
         if (action->node != NULL) {
             node_uname = action->node->details->uname;
             node_uuid = action->node->details->id;
         } else {
             node_uname = "<none>";
         }
     }
 
     switch (pcmk_parse_action(action->task)) {
         case pcmk_action_fence:
         case pcmk_action_shutdown:
             if (pcmk_is_set(action->flags, pcmk_action_pseudo)) {
                 desc = "Pseudo ";
             } else if (pcmk_is_set(action->flags, pcmk_action_optional)) {
                 desc = "Optional ";
             } else if (!pcmk_is_set(action->flags, pcmk_action_runnable)) {
                 desc = "!!Non-Startable!! ";
             } else {
                desc = "(Provisional) ";
             }
             crm_trace("%s%s%sAction %d: %s%s%s%s%s%s",
                       ((pre_text == NULL)? "" : pre_text),
                       ((pre_text == NULL)? "" : ": "),
                       desc, action->id, action->uuid,
                       (node_uname? "\ton " : ""), (node_uname? node_uname : ""),
                       (node_uuid? "\t\t(" : ""), (node_uuid? node_uuid : ""),
                       (node_uuid? ")" : ""));
             break;
         default:
             if (pcmk_is_set(action->flags, pcmk_action_optional)) {
                 desc = "Optional ";
             } else if (pcmk_is_set(action->flags, pcmk_action_pseudo)) {
                 desc = "Pseudo ";
             } else if (!pcmk_is_set(action->flags, pcmk_action_runnable)) {
                 desc = "!!Non-Startable!! ";
             } else {
                desc = "(Provisional) ";
             }
             crm_trace("%s%s%sAction %d: %s %s%s%s%s%s%s",
                       ((pre_text == NULL)? "" : pre_text),
                       ((pre_text == NULL)? "" : ": "),
                       desc, action->id, action->uuid,
                       (action->rsc? action->rsc->id : "<none>"),
                       (node_uname? "\ton " : ""), (node_uname? node_uname : ""),
                       (node_uuid? "\t\t(" : ""), (node_uuid? node_uuid : ""),
                       (node_uuid? ")" : ""));
             break;
     }
 
     if (details) {
         const GList *iter = NULL;
         const pcmk__related_action_t *other = NULL;
 
         crm_trace("\t\t====== Preceding Actions");
         for (iter = action->actions_before; iter != NULL; iter = iter->next) {
             other = (const pcmk__related_action_t *) iter->data;
             pcmk__log_action("\t\t", other->action, false);
         }
         crm_trace("\t\t====== Subsequent Actions");
         for (iter = action->actions_after; iter != NULL; iter = iter->next) {
             other = (const pcmk__related_action_t *) iter->data;
             pcmk__log_action("\t\t", other->action, false);
         }
         crm_trace("\t\t====== End");
 
     } else {
         crm_trace("\t\t(before=%d, after=%d)",
                   g_list_length(action->actions_before),
                   g_list_length(action->actions_after));
     }
 }
 
 /*!
  * \internal
  * \brief Create a new shutdown action for a node
  *
  * \param[in,out] node  Node being shut down
  *
  * \return Newly created shutdown action for \p node
  */
 pcmk_action_t *
 pcmk__new_shutdown_action(pcmk_node_t *node)
 {
     char *shutdown_id = NULL;
     pcmk_action_t *shutdown_op = NULL;
 
     CRM_ASSERT(node != NULL);
 
     shutdown_id = crm_strdup_printf("%s-%s", PCMK_ACTION_DO_SHUTDOWN,
                                     node->details->uname);
 
     shutdown_op = custom_action(NULL, shutdown_id, PCMK_ACTION_DO_SHUTDOWN,
                                 node, FALSE, node->details->data_set);
 
     pcmk__order_stops_before_shutdown(node, shutdown_op);
     pcmk__insert_meta(shutdown_op, PCMK__META_OP_NO_WAIT, PCMK_VALUE_TRUE);
     return shutdown_op;
 }
 
 /*!
  * \internal
  * \brief Calculate and add an operation digest to XML
  *
  * Calculate an operation digest, which enables us to later determine when a
  * restart is needed due to the resource's parameters being changed, and add it
  * to given XML.
  *
  * \param[in]     op      Operation result from executor
  * \param[in,out] update  XML to add digest to
  */
 static void
 add_op_digest_to_xml(const lrmd_event_data_t *op, xmlNode *update)
 {
     char *digest = NULL;
     xmlNode *args_xml = NULL;
 
     if (op->params == NULL) {
         return;
     }
     args_xml = pcmk__xe_create(NULL, PCMK_XE_PARAMETERS);
     g_hash_table_foreach(op->params, hash2field, args_xml);
     pcmk__filter_op_for_digest(args_xml);
     digest = calculate_operation_digest(args_xml, NULL);
     crm_xml_add(update, PCMK__XA_OP_DIGEST, digest);
     pcmk__xml_free(args_xml);
     free(digest);
 }
 
 #define FAKE_TE_ID     "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
 
 /*!
  * \internal
  * \brief Create XML for resource operation history update
  *
  * \param[in,out] parent          Parent XML node to add to
  * \param[in,out] op              Operation event data
  * \param[in]     caller_version  DC feature set
  * \param[in]     target_rc       Expected result of operation
  * \param[in]     node            Name of node on which operation was performed
  * \param[in]     origin          Arbitrary description of update source
  *
  * \return Newly created XML node for history update
  */
 xmlNode *
 pcmk__create_history_xml(xmlNode *parent, lrmd_event_data_t *op,
                          const char *caller_version, int target_rc,
                          const char *node, const char *origin)
 {
     char *key = NULL;
     char *magic = NULL;
     char *op_id = NULL;
     char *op_id_additional = NULL;
     char *local_user_data = NULL;
     const char *exit_reason = NULL;
 
     xmlNode *xml_op = NULL;
     const char *task = NULL;
 
     CRM_CHECK(op != NULL, return NULL);
     crm_trace("Creating history XML for %s-interval %s action for %s on %s "
               "(DC version: %s, origin: %s)",
               pcmk__readable_interval(op->interval_ms), op->op_type, op->rsc_id,
               ((node == NULL)? "no node" : node), caller_version, origin);
 
     task = op->op_type;
 
     /* Record a successful agent reload as a start, and a failed one as a
      * monitor, to make life easier for the scheduler when determining the
      * current state.
      *
      * @COMPAT We should check "reload" here only if the operation was for a
      * pre-OCF-1.1 resource agent, but we don't know that here, and we should
      * only ever get results for actions scheduled by us, so we can reasonably
      * assume any "reload" is actually a pre-1.1 agent reload.
      */
     if (pcmk__str_any_of(task, PCMK_ACTION_RELOAD, PCMK_ACTION_RELOAD_AGENT,
                          NULL)) {
         if (op->op_status == PCMK_EXEC_DONE) {
             task = PCMK_ACTION_START;
         } else {
             task = PCMK_ACTION_MONITOR;
         }
     }
 
     key = pcmk__op_key(op->rsc_id, task, op->interval_ms);
     if (pcmk__str_eq(task, PCMK_ACTION_NOTIFY, pcmk__str_none)) {
         const char *n_type = crm_meta_value(op->params, "notify_type");
         const char *n_task = crm_meta_value(op->params, "notify_operation");
 
         CRM_LOG_ASSERT(n_type != NULL);
         CRM_LOG_ASSERT(n_task != NULL);
         op_id = pcmk__notify_key(op->rsc_id, n_type, n_task);
 
         if (op->op_status != PCMK_EXEC_PENDING) {
             /* Ignore notify errors.
              *
              * @TODO It might be better to keep the correct result here, and
              * ignore it in process_graph_event().
              */
             lrmd__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL);
         }
 
     /* Migration history is preserved separately, which usually matters for
      * multiple nodes and is important for future cluster transitions.
      */
     } else if (pcmk__str_any_of(op->op_type, PCMK_ACTION_MIGRATE_TO,
                                 PCMK_ACTION_MIGRATE_FROM, NULL)) {
         op_id = strdup(key);
 
     } else if (did_rsc_op_fail(op, target_rc)) {
         op_id = pcmk__op_key(op->rsc_id, "last_failure", 0);
         if (op->interval_ms == 0) {
             /* Ensure 'last' gets updated, in case PCMK_META_RECORD_PENDING is
              * true
              */
             op_id_additional = pcmk__op_key(op->rsc_id, "last", 0);
         }
         exit_reason = op->exit_reason;
 
     } else if (op->interval_ms > 0) {
         op_id = strdup(key);
 
     } else {
         op_id = pcmk__op_key(op->rsc_id, "last", 0);
     }
 
   again:
     xml_op = pcmk__xe_first_child(parent, PCMK__XE_LRM_RSC_OP, PCMK_XA_ID,
                                   op_id);
     if (xml_op == NULL) {
         xml_op = pcmk__xe_create(parent, PCMK__XE_LRM_RSC_OP);
     }
 
     if (op->user_data == NULL) {
         crm_debug("Generating fake transition key for: " PCMK__OP_FMT
                   " %d from %s", op->rsc_id, op->op_type, op->interval_ms,
                   op->call_id, origin);
         local_user_data = pcmk__transition_key(-1, op->call_id, target_rc,
                                                FAKE_TE_ID);
         op->user_data = local_user_data;
     }
 
     if (magic == NULL) {
         magic = crm_strdup_printf("%d:%d;%s", op->op_status, op->rc,
                                   (const char *) op->user_data);
     }
 
     crm_xml_add(xml_op, PCMK_XA_ID, op_id);
     crm_xml_add(xml_op, PCMK__XA_OPERATION_KEY, key);
     crm_xml_add(xml_op, PCMK_XA_OPERATION, task);
     crm_xml_add(xml_op, PCMK_XA_CRM_DEBUG_ORIGIN, origin);
     crm_xml_add(xml_op, PCMK_XA_CRM_FEATURE_SET, caller_version);
     crm_xml_add(xml_op, PCMK__XA_TRANSITION_KEY, op->user_data);
     crm_xml_add(xml_op, PCMK__XA_TRANSITION_MAGIC, magic);
     crm_xml_add(xml_op, PCMK_XA_EXIT_REASON, pcmk__s(exit_reason, ""));
     crm_xml_add(xml_op, PCMK__META_ON_NODE, node); // For context during triage
 
     crm_xml_add_int(xml_op, PCMK__XA_CALL_ID, op->call_id);
     crm_xml_add_int(xml_op, PCMK__XA_RC_CODE, op->rc);
     crm_xml_add_int(xml_op, PCMK__XA_OP_STATUS, op->op_status);
     crm_xml_add_ms(xml_op, PCMK_META_INTERVAL, op->interval_ms);
 
     if (compare_version("2.1", caller_version) <= 0) {
         if (op->t_run || op->t_rcchange || op->exec_time || op->queue_time) {
             crm_trace("Timing data (" PCMK__OP_FMT
                       "): last=%u change=%u exec=%u queue=%u",
                       op->rsc_id, op->op_type, op->interval_ms,
                       op->t_run, op->t_rcchange, op->exec_time, op->queue_time);
 
             if ((op->interval_ms != 0) && (op->t_rcchange != 0)) {
                 // Recurring ops may have changed rc after initial run
                 crm_xml_add_ll(xml_op, PCMK_XA_LAST_RC_CHANGE,
                                (long long) op->t_rcchange);
             } else {
                 crm_xml_add_ll(xml_op, PCMK_XA_LAST_RC_CHANGE,
                                (long long) op->t_run);
             }
 
             crm_xml_add_int(xml_op, PCMK_XA_EXEC_TIME, op->exec_time);
             crm_xml_add_int(xml_op, PCMK_XA_QUEUE_TIME, op->queue_time);
         }
     }
 
     if (pcmk__str_any_of(op->op_type, PCMK_ACTION_MIGRATE_TO,
                          PCMK_ACTION_MIGRATE_FROM, NULL)) {
         /* Record PCMK__META_MIGRATE_SOURCE and PCMK__META_MIGRATE_TARGET always
          * for migrate ops.
          */
         const char *name = PCMK__META_MIGRATE_SOURCE;
 
         crm_xml_add(xml_op, name, crm_meta_value(op->params, name));
 
         name = PCMK__META_MIGRATE_TARGET;
         crm_xml_add(xml_op, name, crm_meta_value(op->params, name));
     }
 
     add_op_digest_to_xml(op, xml_op);
 
     if (op_id_additional) {
         free(op_id);
         op_id = op_id_additional;
         op_id_additional = NULL;
         goto again;
     }
 
     if (local_user_data) {
         free(local_user_data);
         op->user_data = NULL;
     }
     free(magic);
     free(op_id);
     free(key);
     return xml_op;
 }
 
 /*!
  * \internal
  * \brief Check whether an action shutdown-locks a resource to a node
  *
  * If the PCMK_OPT_SHUTDOWN_LOCK cluster property is set, resources will not be
  * recovered on a different node if cleanly stopped, and may start only on that
  * same node. This function checks whether that applies to a given action, so
  * that the transition graph can be marked appropriately.
  *
  * \param[in] action  Action to check
  *
  * \return true if \p action locks its resource to the action's node,
  *         otherwise false
  */
 bool
 pcmk__action_locks_rsc_to_node(const pcmk_action_t *action)
 {
     // Only resource actions taking place on resource's lock node are locked
     if ((action == NULL) || (action->rsc == NULL)
         || !pcmk__same_node(action->node, action->rsc->lock_node)) {
         return false;
     }
 
     /* During shutdown, only stops are locked (otherwise, another action such as
      * a demote would cause the controller to clear the lock)
      */
     if (action->node->details->shutdown && (action->task != NULL)
         && (strcmp(action->task, PCMK_ACTION_STOP) != 0)) {
         return false;
     }
 
     return true;
 }
 
 /* lowest to highest */
 static gint
 sort_action_id(gconstpointer a, gconstpointer b)
 {
     const pcmk__related_action_t *action_wrapper2 = a;
     const pcmk__related_action_t *action_wrapper1 = b;
 
     if (a == NULL) {
         return 1;
     }
     if (b == NULL) {
         return -1;
     }
     if (action_wrapper1->action->id < action_wrapper2->action->id) {
         return 1;
     }
     if (action_wrapper1->action->id > action_wrapper2->action->id) {
         return -1;
     }
     return 0;
 }
 
 /*!
  * \internal
  * \brief Remove any duplicate action inputs, merging action flags
  *
  * \param[in,out] action  Action whose inputs should be checked
  */
 void
 pcmk__deduplicate_action_inputs(pcmk_action_t *action)
 {
     GList *item = NULL;
     GList *next = NULL;
     pcmk__related_action_t *last_input = NULL;
 
     action->actions_before = g_list_sort(action->actions_before,
                                          sort_action_id);
     for (item = action->actions_before; item != NULL; item = next) {
         pcmk__related_action_t *input = item->data;
 
         next = item->next;
         if ((last_input != NULL)
             && (input->action->id == last_input->action->id)) {
             crm_trace("Input %s (%d) duplicate skipped for action %s (%d)",
                       input->action->uuid, input->action->id,
                       action->uuid, action->id);
 
             /* For the purposes of scheduling, the ordering flags no longer
              * matter, but crm_simulate looks at certain ones when creating a
              * dot graph. Combining the flags is sufficient for that purpose.
              */
             last_input->type |= input->type;
             if (input->state == pe_link_dumped) {
                 last_input->state = pe_link_dumped;
             }
 
             free(item->data);
             action->actions_before = g_list_delete_link(action->actions_before,
                                                         item);
         } else {
             last_input = input;
             input->state = pe_link_not_dumped;
         }
     }
 }
 
 /*!
  * \internal
  * \brief Output all scheduled actions
  *
  * \param[in,out] scheduler  Scheduler data
  */
 void
 pcmk__output_actions(pcmk_scheduler_t *scheduler)
 {
     pcmk__output_t *out = scheduler->priv;
 
     // Output node (non-resource) actions
     for (GList *iter = scheduler->actions; iter != NULL; iter = iter->next) {
         char *node_name = NULL;
         char *task = NULL;
         pcmk_action_t *action = (pcmk_action_t *) iter->data;
 
         if (action->rsc != NULL) {
             continue; // Resource actions will be output later
 
         } else if (pcmk_is_set(action->flags, pcmk_action_optional)) {
             continue; // This action was not scheduled
         }
 
         if (pcmk__str_eq(action->task, PCMK_ACTION_DO_SHUTDOWN,
                          pcmk__str_none)) {
             task = strdup("Shutdown");
 
         } else if (pcmk__str_eq(action->task, PCMK_ACTION_STONITH,
                                 pcmk__str_none)) {
             const char *op = g_hash_table_lookup(action->meta,
                                                  PCMK__META_STONITH_ACTION);
 
             task = crm_strdup_printf("Fence (%s)", op);
 
         } else {
             continue; // Don't display other node action types
         }
 
         if (pcmk__is_guest_or_bundle_node(action->node)) {
             const pcmk_resource_t *remote = action->node->details->remote_rsc;
 
             node_name = crm_strdup_printf("%s (resource: %s)",
                                           pcmk__node_name(action->node),
                                           remote->container->id);
         } else if (action->node != NULL) {
             node_name = crm_strdup_printf("%s", pcmk__node_name(action->node));
         }
 
         out->message(out, "node-action", task, node_name, action->reason);
 
         free(node_name);
         free(task);
     }
 
     // Output resource actions
     for (GList *iter = scheduler->resources; iter != NULL; iter = iter->next) {
         pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data;
 
-        rsc->cmds->output_actions(rsc);
+        rsc->private->cmds->output_actions(rsc);
     }
 }
 
 /*!
  * \internal
  * \brief Get action name needed to compare digest for configuration changes
  *
  * \param[in] task         Action name from history
  * \param[in] interval_ms  Action interval (in milliseconds)
  *
  * \return Action name whose digest should be compared
  */
 static const char *
 task_for_digest(const char *task, guint interval_ms)
 {
     /* Certain actions need to be compared against the parameters used to start
      * the resource.
      */
     if ((interval_ms == 0)
         && pcmk__str_any_of(task, PCMK_ACTION_MONITOR, PCMK_ACTION_MIGRATE_FROM,
                             PCMK_ACTION_PROMOTE, NULL)) {
         task = PCMK_ACTION_START;
     }
     return task;
 }
 
 /*!
  * \internal
  * \brief Check whether only sanitized parameters to an action changed
  *
  * When collecting CIB files for troubleshooting, crm_report will mask
  * sensitive resource parameters. If simulations were run using that, affected
  * resources would appear to need a restart, which would complicate
  * troubleshooting. To avoid that, we save a "secure digest" of non-sensitive
  * parameters. This function used that digest to check whether only masked
  * parameters are different.
  *
  * \param[in] xml_op       Resource history entry with secure digest
  * \param[in] digest_data  Operation digest information being compared
  * \param[in] scheduler    Scheduler data
  *
  * \return true if only sanitized parameters changed, otherwise false
  */
 static bool
 only_sanitized_changed(const xmlNode *xml_op,
                        const pcmk__op_digest_t *digest_data,
                        const pcmk_scheduler_t *scheduler)
 {
     const char *digest_secure = NULL;
 
     if (!pcmk_is_set(scheduler->flags, pcmk_sched_sanitized)) {
         // The scheduler is not being run as a simulation
         return false;
     }
 
     digest_secure = crm_element_value(xml_op, PCMK__XA_OP_SECURE_DIGEST);
 
     return (digest_data->rc != pcmk__digest_match) && (digest_secure != NULL)
            && (digest_data->digest_secure_calc != NULL)
            && (strcmp(digest_data->digest_secure_calc, digest_secure) == 0);
 }
 
 /*!
  * \internal
  * \brief Force a restart due to a configuration change
  *
  * \param[in,out] rsc          Resource that action is for
  * \param[in]     task         Name of action whose configuration changed
  * \param[in]     interval_ms  Action interval (in milliseconds)
  * \param[in,out] node         Node where resource should be restarted
  */
 static void
 force_restart(pcmk_resource_t *rsc, const char *task, guint interval_ms,
               pcmk_node_t *node)
 {
     char *key = pcmk__op_key(rsc->id, task, interval_ms);
     pcmk_action_t *required = custom_action(rsc, key, task, NULL, FALSE,
                                             rsc->cluster);
 
     pe_action_set_reason(required, "resource definition change", true);
     trigger_unfencing(rsc, node, "Device parameters changed", NULL,
                       rsc->cluster);
 }
 
 /*!
  * \internal
  * \brief Schedule a reload of a resource on a node
  *
  * \param[in,out] data       Resource to reload
  * \param[in]     user_data  Where resource should be reloaded
  */
 static void
 schedule_reload(gpointer data, gpointer user_data)
 {
     pcmk_resource_t *rsc = data;
     const pcmk_node_t *node = user_data;
     pcmk_action_t *reload = NULL;
 
     // For collective resources, just call recursively for children
     if (rsc->variant > pcmk_rsc_variant_primitive) {
         g_list_foreach(rsc->children, schedule_reload, user_data);
         return;
     }
 
     // Skip the reload in certain situations
     if ((node == NULL)
         || !pcmk_is_set(rsc->flags, pcmk_rsc_managed)
         || pcmk_is_set(rsc->flags, pcmk_rsc_failed)) {
         pcmk__rsc_trace(rsc, "Skip reload of %s:%s%s %s",
                         rsc->id,
                         pcmk_is_set(rsc->flags, pcmk_rsc_managed)? "" : " unmanaged",
                         pcmk_is_set(rsc->flags, pcmk_rsc_failed)? " failed" : "",
                         (node == NULL)? "inactive" : node->details->uname);
         return;
     }
 
     /* If a resource's configuration changed while a start was pending,
      * force a full restart instead of a reload.
      */
     if (pcmk_is_set(rsc->flags, pcmk_rsc_start_pending)) {
         pcmk__rsc_trace(rsc,
                         "%s: preventing agent reload because start pending",
                         rsc->id);
         custom_action(rsc, stop_key(rsc), PCMK_ACTION_STOP, node, FALSE,
                       rsc->cluster);
         return;
     }
 
     // Schedule the reload
     pcmk__set_rsc_flags(rsc, pcmk_rsc_reload);
     reload = custom_action(rsc, reload_key(rsc), PCMK_ACTION_RELOAD_AGENT, node,
                            FALSE, rsc->cluster);
     pe_action_set_reason(reload, "resource definition change", FALSE);
 
     // Set orderings so that a required stop or demote cancels the reload
     pcmk__new_ordering(NULL, NULL, reload, rsc, stop_key(rsc), NULL,
                        pcmk__ar_ordered|pcmk__ar_then_cancels_first,
                        rsc->cluster);
     pcmk__new_ordering(NULL, NULL, reload, rsc, demote_key(rsc), NULL,
                        pcmk__ar_ordered|pcmk__ar_then_cancels_first,
                        rsc->cluster);
 }
 
 /*!
  * \internal
  * \brief Handle any configuration change for an action
  *
  * Given an action from resource history, if the resource's configuration
  * changed since the action was done, schedule any actions needed (restart,
  * reload, unfencing, rescheduling recurring actions, etc.).
  *
  * \param[in,out] rsc     Resource that action is for
  * \param[in,out] node    Node that action was on
  * \param[in]     xml_op  Action XML from resource history
  *
  * \return true if action configuration changed, otherwise false
  */
 bool
 pcmk__check_action_config(pcmk_resource_t *rsc, pcmk_node_t *node,
                           const xmlNode *xml_op)
 {
     guint interval_ms = 0;
     const char *task = NULL;
     const pcmk__op_digest_t *digest_data = NULL;
 
     CRM_CHECK((rsc != NULL) && (node != NULL) && (xml_op != NULL),
               return false);
 
     task = crm_element_value(xml_op, PCMK_XA_OPERATION);
     CRM_CHECK(task != NULL, return false);
 
     crm_element_value_ms(xml_op, PCMK_META_INTERVAL, &interval_ms);
 
     // If this is a recurring action, check whether it has been orphaned
     if (interval_ms > 0) {
         if (pcmk__find_action_config(rsc, task, interval_ms, false) != NULL) {
             pcmk__rsc_trace(rsc,
                             "%s-interval %s for %s on %s is in configuration",
                             pcmk__readable_interval(interval_ms), task, rsc->id,
                             pcmk__node_name(node));
         } else if (pcmk_is_set(rsc->cluster->flags,
                                pcmk_sched_cancel_removed_actions)) {
             pcmk__schedule_cancel(rsc,
                                   crm_element_value(xml_op, PCMK__XA_CALL_ID),
                                   task, interval_ms, node, "orphan");
             return true;
         } else {
             pcmk__rsc_debug(rsc, "%s-interval %s for %s on %s is orphaned",
                             pcmk__readable_interval(interval_ms), task, rsc->id,
                             pcmk__node_name(node));
             return true;
         }
     }
 
     crm_trace("Checking %s-interval %s for %s on %s for configuration changes",
               pcmk__readable_interval(interval_ms), task, rsc->id,
               pcmk__node_name(node));
     task = task_for_digest(task, interval_ms);
     digest_data = rsc_action_digest_cmp(rsc, xml_op, node, rsc->cluster);
 
     if (only_sanitized_changed(xml_op, digest_data, rsc->cluster)) {
         if (!pcmk__is_daemon && (rsc->cluster->priv != NULL)) {
             pcmk__output_t *out = rsc->cluster->priv;
 
             out->info(out,
                       "Only 'private' parameters to %s-interval %s for %s "
                       "on %s changed: %s",
                       pcmk__readable_interval(interval_ms), task, rsc->id,
                       pcmk__node_name(node),
                       crm_element_value(xml_op, PCMK__XA_TRANSITION_MAGIC));
         }
         return false;
     }
 
     switch (digest_data->rc) {
         case pcmk__digest_restart:
             crm_log_xml_debug(digest_data->params_restart, "params:restart");
             force_restart(rsc, task, interval_ms, node);
             return true;
 
         case pcmk__digest_unknown:
         case pcmk__digest_mismatch:
             // Changes that can potentially be handled by an agent reload
 
             if (interval_ms > 0) {
                 /* Recurring actions aren't reloaded per se, they are just
                  * re-scheduled so the next run uses the new parameters.
                  * The old instance will be cancelled automatically.
                  */
                 crm_log_xml_debug(digest_data->params_all, "params:reschedule");
                 pcmk__reschedule_recurring(rsc, task, interval_ms, node);
 
             } else if (crm_element_value(xml_op,
                                          PCMK__XA_OP_RESTART_DIGEST) != NULL) {
                 // Agent supports reload, so use it
                 trigger_unfencing(rsc, node,
                                   "Device parameters changed (reload)", NULL,
                                   rsc->cluster);
                 crm_log_xml_debug(digest_data->params_all, "params:reload");
                 schedule_reload((gpointer) rsc, (gpointer) node);
 
             } else {
                 pcmk__rsc_trace(rsc,
                                 "Restarting %s "
                                 "because agent doesn't support reload",
                                 rsc->id);
                 crm_log_xml_debug(digest_data->params_restart,
                                   "params:restart");
                 force_restart(rsc, task, interval_ms, node);
             }
             return true;
 
         default:
             break;
     }
     return false;
 }
 
 /*!
  * \internal
  * \brief Create a list of resource's action history entries, sorted by call ID
  *
  * \param[in]  rsc_entry    Resource's \c PCMK__XE_LRM_RSC_OP status XML
  * \param[out] start_index  Where to store index of start-like action, if any
  * \param[out] stop_index   Where to store index of stop action, if any
  */
 static GList *
 rsc_history_as_list(const xmlNode *rsc_entry, int *start_index, int *stop_index)
 {
     GList *ops = NULL;
 
     for (xmlNode *rsc_op = pcmk__xe_first_child(rsc_entry, PCMK__XE_LRM_RSC_OP,
                                                 NULL, NULL);
          rsc_op != NULL; rsc_op = pcmk__xe_next_same(rsc_op)) {
 
         ops = g_list_prepend(ops, rsc_op);
     }
     ops = g_list_sort(ops, sort_op_by_callid);
     calculate_active_ops(ops, start_index, stop_index);
     return ops;
 }
 
 /*!
  * \internal
  * \brief Process a resource's action history from the CIB status
  *
  * Given a resource's action history, if the resource's configuration
  * changed since the actions were done, schedule any actions needed (restart,
  * reload, unfencing, rescheduling recurring actions, clean-up, etc.).
  * (This also cancels recurring actions for maintenance mode, which is not
  * entirely related but convenient to do here.)
  *
  * \param[in]     rsc_entry  Resource's \c PCMK__XE_LRM_RSC_OP status XML
  * \param[in,out] rsc        Resource whose history is being processed
  * \param[in,out] node       Node whose history is being processed
  */
 static void
 process_rsc_history(const xmlNode *rsc_entry, pcmk_resource_t *rsc,
                     pcmk_node_t *node)
 {
     int offset = -1;
     int stop_index = 0;
     int start_index = 0;
     GList *sorted_op_list = NULL;
 
     if (pcmk_is_set(rsc->flags, pcmk_rsc_removed)) {
         if (pcmk__is_anonymous_clone(pe__const_top_resource(rsc, false))) {
             pcmk__rsc_trace(rsc,
                             "Skipping configuration check "
                             "for orphaned clone instance %s",
                             rsc->id);
         } else {
             pcmk__rsc_trace(rsc,
                             "Skipping configuration check and scheduling "
                             "clean-up for orphaned resource %s", rsc->id);
             pcmk__schedule_cleanup(rsc, node, false);
         }
         return;
     }
 
     if (pe_find_node_id(rsc->running_on, node->details->id) == NULL) {
         if (pcmk__rsc_agent_changed(rsc, node, rsc_entry, false)) {
             pcmk__schedule_cleanup(rsc, node, false);
         }
         pcmk__rsc_trace(rsc,
                         "Skipping configuration check for %s "
                         "because no longer active on %s",
                         rsc->id, pcmk__node_name(node));
         return;
     }
 
     pcmk__rsc_trace(rsc, "Checking for configuration changes for %s on %s",
                     rsc->id, pcmk__node_name(node));
 
     if (pcmk__rsc_agent_changed(rsc, node, rsc_entry, true)) {
         pcmk__schedule_cleanup(rsc, node, false);
     }
 
     sorted_op_list = rsc_history_as_list(rsc_entry, &start_index, &stop_index);
     if (start_index < stop_index) {
         return; // Resource is stopped
     }
 
     for (GList *iter = sorted_op_list; iter != NULL; iter = iter->next) {
         xmlNode *rsc_op = (xmlNode *) iter->data;
         const char *task = NULL;
         guint interval_ms = 0;
 
         if (++offset < start_index) {
             // Skip actions that happened before a start
             continue;
         }
 
         task = crm_element_value(rsc_op, PCMK_XA_OPERATION);
         crm_element_value_ms(rsc_op, PCMK_META_INTERVAL, &interval_ms);
 
         if ((interval_ms > 0)
             && (pcmk_is_set(rsc->flags, pcmk_rsc_maintenance)
                 || node->details->maintenance)) {
             // Maintenance mode cancels recurring operations
             pcmk__schedule_cancel(rsc,
                                   crm_element_value(rsc_op, PCMK__XA_CALL_ID),
                                   task, interval_ms, node, "maintenance mode");
 
         } else if ((interval_ms > 0)
                    || pcmk__strcase_any_of(task, PCMK_ACTION_MONITOR,
                                            PCMK_ACTION_START,
                                            PCMK_ACTION_PROMOTE,
                                            PCMK_ACTION_MIGRATE_FROM, NULL)) {
             /* If a resource operation failed, and the operation's definition
              * has changed, clear any fail count so they can be retried fresh.
              */
 
             if (pe__bundle_needs_remote_name(rsc)) {
                 /* We haven't assigned resources to nodes yet, so if the
                  * REMOTE_CONTAINER_HACK is used, we may calculate the digest
                  * based on the literal "#uname" value rather than the properly
                  * substituted value. That would mistakenly make the action
                  * definition appear to have been changed. Defer the check until
                  * later in this case.
                  */
                 pe__add_param_check(rsc_op, rsc, node, pcmk__check_active,
                                     rsc->cluster);
 
             } else if (pcmk__check_action_config(rsc, node, rsc_op)
                        && (pe_get_failcount(node, rsc, NULL, pcmk__fc_effective,
                                             NULL) != 0)) {
                 pe__clear_failcount(rsc, node, "action definition changed",
                                     rsc->cluster);
             }
         }
     }
     g_list_free(sorted_op_list);
 }
 
 /*!
  * \internal
  * \brief Process a node's action history from the CIB status
  *
  * Given a node's resource history, if the resource's configuration changed
  * since the actions were done, schedule any actions needed (restart,
  * reload, unfencing, rescheduling recurring actions, clean-up, etc.).
  * (This also cancels recurring actions for maintenance mode, which is not
  * entirely related but convenient to do here.)
  *
  * \param[in,out] node      Node whose history is being processed
  * \param[in]     lrm_rscs  Node's \c PCMK__XE_LRM_RESOURCES from CIB status XML
  */
 static void
 process_node_history(pcmk_node_t *node, const xmlNode *lrm_rscs)
 {
     crm_trace("Processing node history for %s", pcmk__node_name(node));
     for (const xmlNode *rsc_entry = pcmk__xe_first_child(lrm_rscs,
                                                          PCMK__XE_LRM_RESOURCE,
                                                          NULL, NULL);
          rsc_entry != NULL; rsc_entry = pcmk__xe_next_same(rsc_entry)) {
 
         if (rsc_entry->children != NULL) {
             GList *result = pcmk__rscs_matching_id(pcmk__xe_id(rsc_entry),
                                                    node->details->data_set);
 
             for (GList *iter = result; iter != NULL; iter = iter->next) {
                 pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data;
 
                 if (pcmk__is_primitive(rsc)) {
                     process_rsc_history(rsc_entry, rsc, node);
                 }
             }
             g_list_free(result);
         }
     }
 }
 
 // XPath to find a node's resource history
 #define XPATH_NODE_HISTORY "/" PCMK_XE_CIB "/" PCMK_XE_STATUS   \
                            "/" PCMK__XE_NODE_STATE              \
                            "[@" PCMK_XA_UNAME "='%s']"          \
                            "/" PCMK__XE_LRM "/" PCMK__XE_LRM_RESOURCES
 
 /*!
  * \internal
  * \brief Process any resource configuration changes in the CIB status
  *
  * Go through all nodes' resource history, and if a resource's configuration
  * changed since its actions were done, schedule any actions needed (restart,
  * reload, unfencing, rescheduling recurring actions, clean-up, etc.).
  * (This also cancels recurring actions for maintenance mode, which is not
  * entirely related but convenient to do here.)
  *
  * \param[in,out] scheduler  Scheduler data
  */
 void
 pcmk__handle_rsc_config_changes(pcmk_scheduler_t *scheduler)
 {
     crm_trace("Check resource and action configuration for changes");
 
     /* Rather than iterate through the status section, iterate through the nodes
      * and search for the appropriate status subsection for each. This skips
      * orphaned nodes and lets us eliminate some cases before searching the XML.
      */
     for (GList *iter = scheduler->nodes; iter != NULL; iter = iter->next) {
         pcmk_node_t *node = (pcmk_node_t *) iter->data;
 
         /* Don't bother checking actions for a node that can't run actions ...
          * unless it's in maintenance mode, in which case we still need to
          * cancel any existing recurring monitors.
          */
         if (node->details->maintenance
             || pcmk__node_available(node, false, false)) {
 
             char *xpath = NULL;
             xmlNode *history = NULL;
 
             xpath = crm_strdup_printf(XPATH_NODE_HISTORY, node->details->uname);
             history = get_xpath_object(xpath, scheduler->input, LOG_NEVER);
             free(xpath);
 
             process_node_history(node, history);
         }
     }
 }
diff --git a/lib/pacemaker/pcmk_sched_bundle.c b/lib/pacemaker/pcmk_sched_bundle.c
index d97dc87b12..3b3406eb02 100644
--- a/lib/pacemaker/pcmk_sched_bundle.c
+++ b/lib/pacemaker/pcmk_sched_bundle.c
@@ -1,1052 +1,1052 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <stdbool.h>
 
 #include <crm/common/xml.h>
 #include <pacemaker-internal.h>
 
 #include "libpacemaker_private.h"
 
 struct assign_data {
     const pcmk_node_t *prefer;
     bool stop_if_fail;
 };
 
 /*!
  * \internal
  * \brief Assign a single bundle replica's resources (other than container)
  *
  * \param[in,out] replica    Replica to assign
  * \param[in]     user_data  Preferred node, if any
  *
  * \return true (to indicate that any further replicas should be processed)
  */
 static bool
 assign_replica(pcmk__bundle_replica_t *replica, void *user_data)
 {
     pcmk_node_t *container_host = NULL;
 
     struct assign_data *assign_data = user_data;
     const pcmk_node_t *prefer = assign_data->prefer;
     bool stop_if_fail = assign_data->stop_if_fail;
 
     const pcmk_resource_t *bundle = pe__const_top_resource(replica->container,
                                                            true);
 
     if (replica->ip != NULL) {
         pcmk__rsc_trace(bundle, "Assigning bundle %s IP %s",
                         bundle->id, replica->ip->id);
-        replica->ip->cmds->assign(replica->ip, prefer, stop_if_fail);
+        replica->ip->private->cmds->assign(replica->ip, prefer, stop_if_fail);
     }
 
     container_host = replica->container->allocated_to;
     if (replica->remote != NULL) {
         if (pcmk__is_pacemaker_remote_node(container_host)) {
             /* REMOTE_CONTAINER_HACK: "Nested" connection resources must be on
              * the same host because Pacemaker Remote only supports a single
              * active connection.
              */
             pcmk__new_colocation("#replica-remote-with-host-remote", NULL,
                                  PCMK_SCORE_INFINITY, replica->remote,
                                  container_host->details->remote_rsc, NULL,
                                  NULL, pcmk__coloc_influence);
         }
         pcmk__rsc_trace(bundle, "Assigning bundle %s connection %s",
                         bundle->id, replica->remote->id);
-        replica->remote->cmds->assign(replica->remote, prefer, stop_if_fail);
+        replica->remote->private->cmds->assign(replica->remote, prefer,
+                                               stop_if_fail);
     }
 
     if (replica->child != NULL) {
         pcmk_node_t *node = NULL;
         GHashTableIter iter;
 
         g_hash_table_iter_init(&iter, replica->child->allowed_nodes);
         while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) {
             if (!pcmk__same_node(node, replica->node)) {
                 node->weight = -PCMK_SCORE_INFINITY;
             } else if (!pcmk__threshold_reached(replica->child, node, NULL)) {
                 node->weight = PCMK_SCORE_INFINITY;
             }
         }
 
         pcmk__set_rsc_flags(replica->child->parent, pcmk_rsc_assigning);
         pcmk__rsc_trace(bundle, "Assigning bundle %s replica child %s",
                         bundle->id, replica->child->id);
-        replica->child->cmds->assign(replica->child, replica->node,
-                                     stop_if_fail);
+        replica->child->private->cmds->assign(replica->child, replica->node,
+                                              stop_if_fail);
         pcmk__clear_rsc_flags(replica->child->parent, pcmk_rsc_assigning);
     }
     return true;
 }
 
 /*!
  * \internal
  * \brief Assign a bundle resource to a node
  *
  * \param[in,out] rsc           Resource to assign to a node
  * \param[in]     prefer        Node to prefer, if all else is equal
  * \param[in]     stop_if_fail  If \c true and a primitive descendant of \p rsc
  *                              can't be assigned to a node, set the
  *                              descendant's next role to stopped and update
  *                              existing actions
  *
  * \return Node that \p rsc is assigned to, if assigned entirely to one node
  *
  * \note If \p stop_if_fail is \c false, then \c pcmk__unassign_resource() can
  *       completely undo the assignment. A successful assignment can be either
  *       undone or left alone as final. A failed assignment has the same effect
  *       as calling pcmk__unassign_resource(); there are no side effects on
  *       roles or actions.
  */
 pcmk_node_t *
 pcmk__bundle_assign(pcmk_resource_t *rsc, const pcmk_node_t *prefer,
                     bool stop_if_fail)
 {
     GList *containers = NULL;
     pcmk_resource_t *bundled_resource = NULL;
     struct assign_data assign_data = { prefer, stop_if_fail };
 
     CRM_ASSERT(pcmk__is_bundle(rsc));
 
     pcmk__rsc_trace(rsc, "Assigning bundle %s", rsc->id);
     pcmk__set_rsc_flags(rsc, pcmk_rsc_assigning);
 
     pe__show_node_scores(!pcmk_is_set(rsc->cluster->flags,
                                       pcmk_sched_output_scores),
                          rsc, __func__, rsc->allowed_nodes, rsc->cluster);
 
     // Assign all containers first, so we know what nodes the bundle will be on
     containers = g_list_sort(pe__bundle_containers(rsc), pcmk__cmp_instance);
     pcmk__assign_instances(rsc, containers, pe__bundle_max(rsc),
                            rsc->private->fns->max_per_node(rsc));
     g_list_free(containers);
 
     // Then assign remaining replica resources
     pe__foreach_bundle_replica(rsc, assign_replica, (void *) &assign_data);
 
     // Finally, assign the bundled resources to each bundle node
     bundled_resource = pe__bundled_resource(rsc);
     if (bundled_resource != NULL) {
         pcmk_node_t *node = NULL;
         GHashTableIter iter;
 
         g_hash_table_iter_init(&iter, bundled_resource->allowed_nodes);
         while (g_hash_table_iter_next(&iter, NULL, (gpointer *) & node)) {
             if (pe__node_is_bundle_instance(rsc, node)) {
                 node->weight = 0;
             } else {
                 node->weight = -PCMK_SCORE_INFINITY;
             }
         }
-        bundled_resource->cmds->assign(bundled_resource, prefer, stop_if_fail);
+        bundled_resource->private->cmds->assign(bundled_resource, prefer,
+                                                stop_if_fail);
     }
 
     pcmk__clear_rsc_flags(rsc, pcmk_rsc_assigning|pcmk_rsc_unassigned);
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Create actions for a bundle replica's resources (other than child)
  *
  * \param[in,out] replica    Replica to create actions for
  * \param[in]     user_data  Unused
  *
  * \return true (to indicate that any further replicas should be processed)
  */
 static bool
 create_replica_actions(pcmk__bundle_replica_t *replica, void *user_data)
 {
     if (replica->ip != NULL) {
-        replica->ip->cmds->create_actions(replica->ip);
+        replica->ip->private->cmds->create_actions(replica->ip);
     }
     if (replica->container != NULL) {
-        replica->container->cmds->create_actions(replica->container);
+        replica->container->private->cmds->create_actions(replica->container);
     }
     if (replica->remote != NULL) {
-        replica->remote->cmds->create_actions(replica->remote);
+        replica->remote->private->cmds->create_actions(replica->remote);
     }
     return true;
 }
 
 /*!
  * \internal
  * \brief Create all actions needed for a given bundle resource
  *
  * \param[in,out] rsc  Bundle resource to create actions for
  */
 void
 pcmk__bundle_create_actions(pcmk_resource_t *rsc)
 {
     pcmk_action_t *action = NULL;
     GList *containers = NULL;
     pcmk_resource_t *bundled_resource = NULL;
 
     CRM_ASSERT(pcmk__is_bundle(rsc));
 
     pe__foreach_bundle_replica(rsc, create_replica_actions, NULL);
 
     containers = pe__bundle_containers(rsc);
     pcmk__create_instance_actions(rsc, containers);
     g_list_free(containers);
 
     bundled_resource = pe__bundled_resource(rsc);
     if (bundled_resource != NULL) {
-        bundled_resource->cmds->create_actions(bundled_resource);
+        bundled_resource->private->cmds->create_actions(bundled_resource);
 
         if (pcmk_is_set(bundled_resource->flags, pcmk_rsc_promotable)) {
             pe__new_rsc_pseudo_action(rsc, PCMK_ACTION_PROMOTE, true, true);
             action = pe__new_rsc_pseudo_action(rsc, PCMK_ACTION_PROMOTED,
                                                true, true);
             action->priority = PCMK_SCORE_INFINITY;
 
             pe__new_rsc_pseudo_action(rsc, PCMK_ACTION_DEMOTE, true, true);
             action = pe__new_rsc_pseudo_action(rsc, PCMK_ACTION_DEMOTED,
                                                true, true);
             action->priority = PCMK_SCORE_INFINITY;
         }
     }
 }
 
 /*!
  * \internal
  * \brief Create internal constraints for a bundle replica's resources
  *
  * \param[in,out] replica    Replica to create internal constraints for
  * \param[in,out] user_data  Replica's parent bundle
  *
  * \return true (to indicate that any further replicas should be processed)
  */
 static bool
 replica_internal_constraints(pcmk__bundle_replica_t *replica, void *user_data)
 {
     pcmk_resource_t *bundle = user_data;
 
-    replica->container->cmds->internal_constraints(replica->container);
+    replica->container->private->cmds->internal_constraints(replica->container);
 
     // Start bundle -> start replica container
     pcmk__order_starts(bundle, replica->container,
                        pcmk__ar_unrunnable_first_blocks
                        |pcmk__ar_then_implies_first_graphed);
 
     // Stop bundle -> stop replica child and container
     if (replica->child != NULL) {
         pcmk__order_stops(bundle, replica->child,
                           pcmk__ar_then_implies_first_graphed);
     }
     pcmk__order_stops(bundle, replica->container,
                       pcmk__ar_then_implies_first_graphed);
 
     // Start replica container -> bundle is started
     pcmk__order_resource_actions(replica->container, PCMK_ACTION_START, bundle,
                                  PCMK_ACTION_RUNNING,
                                  pcmk__ar_first_implies_then_graphed);
 
     // Stop replica container -> bundle is stopped
     pcmk__order_resource_actions(replica->container, PCMK_ACTION_STOP, bundle,
                                  PCMK_ACTION_STOPPED,
                                  pcmk__ar_first_implies_then_graphed);
 
     if (replica->ip != NULL) {
-        replica->ip->cmds->internal_constraints(replica->ip);
+        replica->ip->private->cmds->internal_constraints(replica->ip);
 
         // Replica IP address -> replica container (symmetric)
         pcmk__order_starts(replica->ip, replica->container,
                            pcmk__ar_unrunnable_first_blocks
                            |pcmk__ar_guest_allowed);
         pcmk__order_stops(replica->container, replica->ip,
                           pcmk__ar_then_implies_first|pcmk__ar_guest_allowed);
 
         pcmk__new_colocation("#ip-with-container", NULL, PCMK_SCORE_INFINITY,
                              replica->ip, replica->container, NULL, NULL,
                              pcmk__coloc_influence);
     }
 
     if (replica->remote != NULL) {
         /* This handles ordering and colocating remote relative to container
          * (via "#resource-with-container"). Since IP is also ordered and
          * colocated relative to the container, we don't need to do anything
          * explicit here with IP.
          */
-        replica->remote->cmds->internal_constraints(replica->remote);
+        replica->remote->private->cmds->internal_constraints(replica->remote);
     }
 
     if (replica->child != NULL) {
         CRM_ASSERT(replica->remote != NULL);
         // "Start remote then child" is implicit in scheduler's remote logic
     }
     return true;
 }
 
 /*!
  * \internal
  * \brief Create implicit constraints needed for a bundle resource
  *
  * \param[in,out] rsc  Bundle resource to create implicit constraints for
  */
 void
 pcmk__bundle_internal_constraints(pcmk_resource_t *rsc)
 {
     pcmk_resource_t *bundled_resource = NULL;
 
     CRM_ASSERT(pcmk__is_bundle(rsc));
 
     pe__foreach_bundle_replica(rsc, replica_internal_constraints, rsc);
 
     bundled_resource = pe__bundled_resource(rsc);
     if (bundled_resource == NULL) {
         return;
     }
 
     // Start bundle -> start bundled clone
     pcmk__order_resource_actions(rsc, PCMK_ACTION_START, bundled_resource,
                                  PCMK_ACTION_START,
                                  pcmk__ar_then_implies_first_graphed);
 
     // Bundled clone is started -> bundle is started
     pcmk__order_resource_actions(bundled_resource, PCMK_ACTION_RUNNING,
                                  rsc, PCMK_ACTION_RUNNING,
                                  pcmk__ar_first_implies_then_graphed);
 
     // Stop bundle -> stop bundled clone
     pcmk__order_resource_actions(rsc, PCMK_ACTION_STOP, bundled_resource,
                                  PCMK_ACTION_STOP,
                                  pcmk__ar_then_implies_first_graphed);
 
     // Bundled clone is stopped -> bundle is stopped
     pcmk__order_resource_actions(bundled_resource, PCMK_ACTION_STOPPED,
                                  rsc, PCMK_ACTION_STOPPED,
                                  pcmk__ar_first_implies_then_graphed);
 
-    bundled_resource->cmds->internal_constraints(bundled_resource);
+    bundled_resource->private->cmds->internal_constraints(bundled_resource);
 
     if (!pcmk_is_set(bundled_resource->flags, pcmk_rsc_promotable)) {
         return;
     }
     pcmk__promotable_restart_ordering(rsc);
 
     // Demote bundle -> demote bundled clone
     pcmk__order_resource_actions(rsc, PCMK_ACTION_DEMOTE, bundled_resource,
                                  PCMK_ACTION_DEMOTE,
                                  pcmk__ar_then_implies_first_graphed);
 
     // Bundled clone is demoted -> bundle is demoted
     pcmk__order_resource_actions(bundled_resource, PCMK_ACTION_DEMOTED,
                                  rsc, PCMK_ACTION_DEMOTED,
                                  pcmk__ar_first_implies_then_graphed);
 
     // Promote bundle -> promote bundled clone
     pcmk__order_resource_actions(rsc, PCMK_ACTION_PROMOTE,
                                  bundled_resource, PCMK_ACTION_PROMOTE,
                                  pcmk__ar_then_implies_first_graphed);
 
     // Bundled clone is promoted -> bundle is promoted
     pcmk__order_resource_actions(bundled_resource, PCMK_ACTION_PROMOTED,
                                  rsc, PCMK_ACTION_PROMOTED,
                                  pcmk__ar_first_implies_then_graphed);
 }
 
 struct match_data {
     const pcmk_node_t *node;    // Node to compare against replica
     pcmk_resource_t *container; // Replica container corresponding to node
 };
 
 /*!
  * \internal
  * \brief Check whether a replica container is assigned to a given node
  *
  * \param[in]     replica    Replica to check
  * \param[in,out] user_data  struct match_data with node to compare against
  *
  * \return true if the replica does not match (to indicate further replicas
  *         should be processed), otherwise false
  */
 static bool
 match_replica_container(const pcmk__bundle_replica_t *replica, void *user_data)
 {
     struct match_data *match_data = user_data;
 
     if (pcmk__instance_matches(replica->container, match_data->node,
                                pcmk_role_unknown, false)) {
         match_data->container = replica->container;
         return false; // Match found, don't bother searching further replicas
     }
     return true; // No match, keep searching
 }
 
 /*!
  * \internal
  * \brief Get the host to which a bundle node is assigned
  *
  * \param[in] node  Possible bundle node to check
  *
  * \return Node to which the container for \p node is assigned if \p node is a
  *         bundle node, otherwise \p node itself
  */
 static const pcmk_node_t *
 get_bundle_node_host(const pcmk_node_t *node)
 {
     if (pcmk__is_bundle_node(node)) {
         const pcmk_resource_t *container = node->details->remote_rsc->container;
 
         return container->private->fns->location(container, NULL, 0);
     }
     return node;
 }
 
 /*!
  * \internal
  * \brief Find a bundle container compatible with a dependent resource
  *
  * \param[in] dependent  Dependent resource in colocation with bundle
  * \param[in] bundle     Bundle that \p dependent is colocated with
  *
  * \return A container from \p bundle assigned to the same node as \p dependent
  *         if assigned, otherwise assigned to any of dependent's allowed nodes,
  *         otherwise NULL.
  */
 static pcmk_resource_t *
 compatible_container(const pcmk_resource_t *dependent,
                      const pcmk_resource_t *bundle)
 {
     GList *scratch = NULL;
     struct match_data match_data = { NULL, NULL };
 
     // If dependent is assigned, only check there
     match_data.node = dependent->private->fns->location(dependent, NULL, 0);
     match_data.node = get_bundle_node_host(match_data.node);
     if (match_data.node != NULL) {
         pe__foreach_const_bundle_replica(bundle, match_replica_container,
                                          &match_data);
         return match_data.container;
     }
 
     // Otherwise, check for any of the dependent's allowed nodes
     scratch = g_hash_table_get_values(dependent->allowed_nodes);
     scratch = pcmk__sort_nodes(scratch, NULL);
     for (const GList *iter = scratch; iter != NULL; iter = iter->next) {
         match_data.node = iter->data;
         match_data.node = get_bundle_node_host(match_data.node);
         if (match_data.node == NULL) {
             continue;
         }
 
         pe__foreach_const_bundle_replica(bundle, match_replica_container,
                                          &match_data);
         if (match_data.container != NULL) {
             break;
         }
     }
     g_list_free(scratch);
     return match_data.container;
 }
 
 struct coloc_data {
     const pcmk__colocation_t *colocation;
     pcmk_resource_t *dependent;
     GList *container_hosts;
 };
 
 /*!
  * \internal
  * \brief Apply a colocation score to replica node scores or resource priority
  *
  * \param[in]     replica    Replica of primary bundle resource in colocation
  * \param[in,out] user_data  struct coloc_data for colocation being applied
  *
  * \return true (to indicate that any further replicas should be processed)
  */
 static bool
 replica_apply_coloc_score(const pcmk__bundle_replica_t *replica,
                           void *user_data)
 {
     struct coloc_data *coloc_data = user_data;
     pcmk_node_t *chosen = NULL;
+    pcmk_resource_t *container = replica->container;
 
     if (coloc_data->colocation->score < PCMK_SCORE_INFINITY) {
-        replica->container->cmds->apply_coloc_score(coloc_data->dependent,
-                                                    replica->container,
+        container->private->cmds->apply_coloc_score(coloc_data->dependent,
+                                                    container,
                                                     coloc_data->colocation,
                                                     false);
         return true;
     }
 
-    chosen = replica->container->private->fns->location(replica->container,
-                                                        NULL, 0);
+    chosen = container->private->fns->location(container, NULL, 0);
     if ((chosen == NULL)
-        || is_set_recursive(replica->container, pcmk_rsc_blocked, true)) {
+        || is_set_recursive(container, pcmk_rsc_blocked, true)) {
         return true;
     }
 
     if ((coloc_data->colocation->primary_role >= pcmk_role_promoted)
         && ((replica->child == NULL)
             || (replica->child->next_role < pcmk_role_promoted))) {
         return true;
     }
 
-    pcmk__rsc_trace(pe__const_top_resource(replica->container, true),
+    pcmk__rsc_trace(pe__const_top_resource(container, true),
                     "Allowing mandatory colocation %s using %s @%d",
                     coloc_data->colocation->id, pcmk__node_name(chosen),
                     chosen->weight);
     coloc_data->container_hosts = g_list_prepend(coloc_data->container_hosts,
                                                  chosen);
     return true;
 }
 
 /*!
  * \internal
  * \brief Apply a colocation's score to node scores or resource priority
  *
  * Given a colocation constraint, apply its score to the dependent's
  * allowed node scores (if we are still placing resources) or priority (if
  * we are choosing promotable clone instance roles).
  *
  * \param[in,out] dependent      Dependent resource in colocation
  * \param[in]     primary        Primary resource in colocation
  * \param[in]     colocation     Colocation constraint to apply
  * \param[in]     for_dependent  true if called on behalf of dependent
  */
 void
 pcmk__bundle_apply_coloc_score(pcmk_resource_t *dependent,
                                const pcmk_resource_t *primary,
                                const pcmk__colocation_t *colocation,
                                bool for_dependent)
 {
     struct coloc_data coloc_data = { colocation, dependent, NULL };
 
     /* This should never be called for the bundle itself as a dependent.
      * Instead, we add its colocation constraints to its containers and bundled
      * primitive and call the apply_coloc_score() method for them as dependents.
      */
     CRM_ASSERT(pcmk__is_bundle(primary) && pcmk__is_primitive(dependent)
                && (colocation != NULL) && !for_dependent);
 
     if (pcmk_is_set(primary->flags, pcmk_rsc_unassigned)) {
         pcmk__rsc_trace(primary,
                         "Skipping applying colocation %s "
                         "because %s is still provisional",
                         colocation->id, primary->id);
         return;
     }
     pcmk__rsc_trace(primary, "Applying colocation %s (%s with %s at %s)",
                     colocation->id, dependent->id, primary->id,
                     pcmk_readable_score(colocation->score));
 
     /* If the constraint dependent is a clone or bundle, "dependent" here is one
      * of its instances. Look for a compatible instance of this bundle.
      */
     if (colocation->dependent->variant > pcmk_rsc_variant_group) {
         const pcmk_resource_t *primary_container = NULL;
 
         primary_container = compatible_container(dependent, primary);
         if (primary_container != NULL) { // Success, we found one
             pcmk__rsc_debug(primary, "Pairing %s with %s",
                             dependent->id, primary_container->id);
-            dependent->cmds->apply_coloc_score(dependent, primary_container,
-                                               colocation, true);
+            dependent->private->cmds->apply_coloc_score(dependent,
+                                                        primary_container,
+                                                        colocation, true);
 
         } else if (colocation->score >= PCMK_SCORE_INFINITY) {
             // Failure, and it's fatal
             crm_notice("%s cannot run because there is no compatible "
                        "instance of %s to colocate with",
                        dependent->id, primary->id);
             pcmk__assign_resource(dependent, NULL, true, true);
 
         } else { // Failure, but we can ignore it
             pcmk__rsc_debug(primary,
                             "%s cannot be colocated with any instance of %s",
                             dependent->id, primary->id);
         }
         return;
     }
 
     pe__foreach_const_bundle_replica(primary, replica_apply_coloc_score,
                                      &coloc_data);
 
     if (colocation->score >= PCMK_SCORE_INFINITY) {
         pcmk__colocation_intersect_nodes(dependent, primary, colocation,
                                          coloc_data.container_hosts, false);
     }
     g_list_free(coloc_data.container_hosts);
 }
 
 // Bundle implementation of pcmk_assignment_methods_t:with_this_colocations()
 void
 pcmk__with_bundle_colocations(const pcmk_resource_t *rsc,
                               const pcmk_resource_t *orig_rsc, GList **list)
 {
     const pcmk_resource_t *bundled_rsc = NULL;
 
     CRM_ASSERT(pcmk__is_bundle(rsc) && (orig_rsc != NULL) && (list != NULL));
 
     // The bundle itself and its containers always get its colocations
     if ((orig_rsc == rsc)
         || pcmk_is_set(orig_rsc->flags, pcmk_rsc_replica_container)) {
 
         pcmk__add_with_this_list(list, rsc->rsc_cons_lhs, orig_rsc);
         return;
     }
 
     /* The bundled resource gets the colocations if it's promotable and we've
      * begun choosing roles
      */
     bundled_rsc = pe__bundled_resource(rsc);
     if ((bundled_rsc == NULL)
         || !pcmk_is_set(bundled_rsc->flags, pcmk_rsc_promotable)
         || (pe__const_top_resource(orig_rsc, false) != bundled_rsc)) {
         return;
     }
 
     if (orig_rsc == bundled_rsc) {
         if (pe__clone_flag_is_set(orig_rsc,
                                   pcmk__clone_promotion_constrained)) {
             /* orig_rsc is the clone and we're setting roles (or have already
              * done so)
              */
             pcmk__add_with_this_list(list, rsc->rsc_cons_lhs, orig_rsc);
         }
 
     } else if (!pcmk_is_set(orig_rsc->flags, pcmk_rsc_unassigned)) {
         /* orig_rsc is an instance and is already assigned. If something
          * requests colocations for orig_rsc now, it's for setting roles.
          */
         pcmk__add_with_this_list(list, rsc->rsc_cons_lhs, orig_rsc);
     }
 }
 
 // Bundle implementation of pcmk_assignment_methods_t:this_with_colocations()
 void
 pcmk__bundle_with_colocations(const pcmk_resource_t *rsc,
                               const pcmk_resource_t *orig_rsc, GList **list)
 {
     const pcmk_resource_t *bundled_rsc = NULL;
 
     CRM_ASSERT(pcmk__is_bundle(rsc) && (orig_rsc != NULL) && (list != NULL));
 
     // The bundle itself and its containers always get its colocations
     if ((orig_rsc == rsc)
         || pcmk_is_set(orig_rsc->flags, pcmk_rsc_replica_container)) {
 
         pcmk__add_this_with_list(list, rsc->rsc_cons, orig_rsc);
         return;
     }
 
     /* The bundled resource gets the colocations if it's promotable and we've
      * begun choosing roles
      */
     bundled_rsc = pe__bundled_resource(rsc);
     if ((bundled_rsc == NULL)
         || !pcmk_is_set(bundled_rsc->flags, pcmk_rsc_promotable)
         || (pe__const_top_resource(orig_rsc, false) != bundled_rsc)) {
         return;
     }
 
     if (orig_rsc == bundled_rsc) {
         if (pe__clone_flag_is_set(orig_rsc,
                                   pcmk__clone_promotion_constrained)) {
             /* orig_rsc is the clone and we're setting roles (or have already
              * done so)
              */
             pcmk__add_this_with_list(list, rsc->rsc_cons, orig_rsc);
         }
 
     } else if (!pcmk_is_set(orig_rsc->flags, pcmk_rsc_unassigned)) {
         /* orig_rsc is an instance and is already assigned. If something
          * requests colocations for orig_rsc now, it's for setting roles.
          */
         pcmk__add_this_with_list(list, rsc->rsc_cons, orig_rsc);
     }
 }
 
 /*!
  * \internal
  * \brief Return action flags for a given bundle resource action
  *
  * \param[in,out] action  Bundle resource action to get flags for
  * \param[in]     node    If not NULL, limit effects to this node
  *
  * \return Flags appropriate to \p action on \p node
  */
 uint32_t
 pcmk__bundle_action_flags(pcmk_action_t *action, const pcmk_node_t *node)
 {
     GList *containers = NULL;
     uint32_t flags = 0;
     pcmk_resource_t *bundled_resource = NULL;
 
     CRM_ASSERT((action != NULL) && pcmk__is_bundle(action->rsc));
 
     bundled_resource = pe__bundled_resource(action->rsc);
     if (bundled_resource != NULL) {
         // Clone actions are done on the bundled clone resource, not container
         switch (get_complex_task(bundled_resource, action->task)) {
             case pcmk_action_unspecified:
             case pcmk_action_notify:
             case pcmk_action_notified:
             case pcmk_action_promote:
             case pcmk_action_promoted:
             case pcmk_action_demote:
             case pcmk_action_demoted:
                 return pcmk__collective_action_flags(action,
                                                      bundled_resource->children,
                                                      node);
             default:
                 break;
         }
     }
 
     containers = pe__bundle_containers(action->rsc);
     flags = pcmk__collective_action_flags(action, containers, node);
     g_list_free(containers);
     return flags;
 }
 
 /*!
  * \internal
  * \brief Apply a location constraint to a bundle replica
  *
  * \param[in,out] replica    Replica to apply constraint to
  * \param[in,out] user_data  Location constraint to apply
  *
  * \return true (to indicate that any further replicas should be processed)
  */
 static bool
 apply_location_to_replica(pcmk__bundle_replica_t *replica, void *user_data)
 {
     pcmk__location_t *location = user_data;
 
-    if (replica->container != NULL) {
-        replica->container->cmds->apply_location(replica->container, location);
-    }
+    replica->container->private->cmds->apply_location(replica->container,
+                                                      location);
     if (replica->ip != NULL) {
-        replica->ip->cmds->apply_location(replica->ip, location);
+        replica->ip->private->cmds->apply_location(replica->ip, location);
     }
     return true;
 }
 
 /*!
  * \internal
  * \brief Apply a location constraint to a bundle resource's allowed node scores
  *
  * \param[in,out] rsc       Bundle resource to apply constraint to
  * \param[in,out] location  Location constraint to apply
  */
 void
 pcmk__bundle_apply_location(pcmk_resource_t *rsc, pcmk__location_t *location)
 {
     pcmk_resource_t *bundled_resource = NULL;
 
     CRM_ASSERT((location != NULL) && pcmk__is_bundle(rsc));
 
     pcmk__apply_location(rsc, location);
     pe__foreach_bundle_replica(rsc, apply_location_to_replica, location);
 
     bundled_resource = pe__bundled_resource(rsc);
     if ((bundled_resource != NULL)
         && ((location->role_filter == pcmk_role_unpromoted)
             || (location->role_filter == pcmk_role_promoted))) {
-        bundled_resource->cmds->apply_location(bundled_resource, location);
+
+        bundled_resource->private->cmds->apply_location(bundled_resource,
+                                                        location);
         bundled_resource->rsc_location = g_list_prepend(
             bundled_resource->rsc_location, location);
     }
 }
 
 #define XPATH_REMOTE "//nvpair[@name='" PCMK_REMOTE_RA_ADDR "']"
 
 /*!
  * \internal
  * \brief Add a bundle replica's actions to transition graph
  *
  * \param[in,out] replica    Replica to add to graph
  * \param[in]     user_data  Bundle that replica belongs to (for logging only)
  *
  * \return true (to indicate that any further replicas should be processed)
  */
 static bool
 add_replica_actions_to_graph(pcmk__bundle_replica_t *replica, void *user_data)
 {
-    if ((replica->remote != NULL) && (replica->container != NULL)
+    if ((replica->remote != NULL)
         && pe__bundle_needs_remote_name(replica->remote)) {
 
         /* REMOTE_CONTAINER_HACK: Allow remote nodes to run containers that
          * run pacemaker-remoted inside, without needing a separate IP for
          * the container. This is done by configuring the inner remote's
          * connection host as the magic string "#uname", then
          * replacing it with the underlying host when needed.
          */
         xmlNode *nvpair = get_xpath_object(XPATH_REMOTE, replica->remote->xml,
                                            LOG_ERR);
         const char *calculated_addr = NULL;
 
         // Replace the value in replica->remote->xml (if appropriate)
         calculated_addr = pe__add_bundle_remote_name(replica->remote, nvpair,
                                                      PCMK_XA_VALUE);
         if (calculated_addr != NULL) {
             /* Since this is for the bundle as a resource, and not any
              * particular action, replace the value in the default
              * parameters (not evaluated for node). create_graph_action()
              * will grab it from there to replace it in node-evaluated
              * parameters.
              */
             GHashTable *params = pe_rsc_params(replica->remote,
                                                NULL, replica->remote->cluster);
 
             pcmk__insert_dup(params, PCMK_REMOTE_RA_ADDR, calculated_addr);
         } else {
             pcmk_resource_t *bundle = user_data;
 
             /* The only way to get here is if the remote connection is
              * neither currently running nor scheduled to run. That means we
              * won't be doing any operations that require addr (only start
              * requires it; we additionally use it to compare digests when
              * unpacking status, promote, and migrate_from history, but
              * that's already happened by this point).
              */
             pcmk__rsc_info(bundle,
                            "Unable to determine address for bundle %s "
                            "remote connection", bundle->id);
         }
     }
     if (replica->ip != NULL) {
-        replica->ip->cmds->add_actions_to_graph(replica->ip);
-    }
-    if (replica->container != NULL) {
-        replica->container->cmds->add_actions_to_graph(replica->container);
+        replica->ip->private->cmds->add_actions_to_graph(replica->ip);
     }
+    replica->container->private->cmds->add_actions_to_graph(replica->container);
     if (replica->remote != NULL) {
-        replica->remote->cmds->add_actions_to_graph(replica->remote);
+        replica->remote->private->cmds->add_actions_to_graph(replica->remote);
     }
     return true;
 }
 
 /*!
  * \internal
  * \brief Add a bundle resource's actions to the transition graph
  *
  * \param[in,out] rsc  Bundle resource whose actions should be added
  */
 void
 pcmk__bundle_add_actions_to_graph(pcmk_resource_t *rsc)
 {
     pcmk_resource_t *bundled_resource = NULL;
 
     CRM_ASSERT(pcmk__is_bundle(rsc));
 
     bundled_resource = pe__bundled_resource(rsc);
     if (bundled_resource != NULL) {
-        bundled_resource->cmds->add_actions_to_graph(bundled_resource);
+        bundled_resource->private->cmds->add_actions_to_graph(bundled_resource);
     }
     pe__foreach_bundle_replica(rsc, add_replica_actions_to_graph, rsc);
 }
 
 struct probe_data {
     pcmk_resource_t *bundle;    // Bundle being probed
     pcmk_node_t *node;          // Node to create probes on
     bool any_created;           // Whether any probes have been created
 };
 
 /*!
  * \internal
  * \brief Order a bundle replica's start after another replica's probe
  *
  * \param[in,out] replica    Replica to order start for
  * \param[in,out] user_data  Replica with probe to order after
  *
  * \return true (to indicate that any further replicas should be processed)
  */
 static bool
 order_replica_start_after(pcmk__bundle_replica_t *replica, void *user_data)
 {
     pcmk__bundle_replica_t *probed_replica = user_data;
 
     if ((replica == probed_replica) || (replica->container == NULL)) {
         return true;
     }
     pcmk__new_ordering(probed_replica->container,
                        pcmk__op_key(probed_replica->container->id,
                                     PCMK_ACTION_MONITOR, 0),
                        NULL, replica->container,
                        pcmk__op_key(replica->container->id, PCMK_ACTION_START,
                                     0),
                        NULL, pcmk__ar_ordered|pcmk__ar_if_on_same_node,
                        replica->container->cluster);
     return true;
 }
 
 /*!
  * \internal
  * \brief Create probes for a bundle replica's resources
  *
  * \param[in,out] replica    Replica to create probes for
  * \param[in,out] user_data  struct probe_data
  *
  * \return true (to indicate that any further replicas should be processed)
  */
 static bool
 create_replica_probes(pcmk__bundle_replica_t *replica, void *user_data)
 {
     struct probe_data *probe_data = user_data;
     pcmk_resource_t *bundle = probe_data->bundle;
 
     if ((replica->ip != NULL)
-        && replica->ip->cmds->create_probe(replica->ip, probe_data->node)) {
+        && replica->ip->private->cmds->create_probe(replica->ip,
+                                                    probe_data->node)) {
         probe_data->any_created = true;
     }
     if ((replica->child != NULL)
         && pcmk__same_node(probe_data->node, replica->node)
-        && replica->child->cmds->create_probe(replica->child,
-                                              probe_data->node)) {
+        && replica->child->private->cmds->create_probe(replica->child,
+                                                       probe_data->node)) {
         probe_data->any_created = true;
     }
-    if ((replica->container != NULL)
-        && replica->container->cmds->create_probe(replica->container,
-                                                  probe_data->node)) {
+    if (replica->container->private->cmds->create_probe(replica->container,
+                                                        probe_data->node)) {
         probe_data->any_created = true;
 
         /* If we're limited to one replica per host (due to
          * the lack of an IP range probably), then we don't
          * want any of our peer containers starting until
          * we've established that no other copies are already
          * running.
          *
          * Partly this is to ensure that the maximum replicas per host is
          * observed, but also to ensure that the containers
          * don't fail to start because the necessary port
          * mappings (which won't include an IP for uniqueness)
          * are already taken
          */
         if (bundle->private->fns->max_per_node(bundle) == 1) {
             pe__foreach_bundle_replica(bundle, order_replica_start_after,
                                        replica);
         }
     }
-    if ((replica->container != NULL) && (replica->remote != NULL)
-        && replica->remote->cmds->create_probe(replica->remote,
-                                               probe_data->node)) {
+    if ((replica->remote != NULL)
+        && replica->remote->private->cmds->create_probe(replica->remote,
+                                                        probe_data->node)) {
         /* Do not probe the remote resource until we know where the container is
          * running. This is required for REMOTE_CONTAINER_HACK to correctly
          * probe remote resources.
          */
         char *probe_uuid = pcmk__op_key(replica->remote->id,
                                         PCMK_ACTION_MONITOR, 0);
         pcmk_action_t *probe = find_first_action(replica->remote->actions,
                                                  probe_uuid, NULL,
                                                  probe_data->node);
 
         free(probe_uuid);
         if (probe != NULL) {
             probe_data->any_created = true;
             pcmk__rsc_trace(bundle, "Ordering %s probe on %s",
                             replica->remote->id,
                             pcmk__node_name(probe_data->node));
             pcmk__new_ordering(replica->container,
                                pcmk__op_key(replica->container->id,
                                             PCMK_ACTION_START, 0),
                                NULL, replica->remote, NULL, probe,
                                pcmk__ar_nested_remote_probe, bundle->cluster);
         }
     }
     return true;
 }
 
 /*!
  * \internal
  *
  * \brief Schedule any probes needed for a bundle resource on a node
  *
  * \param[in,out] rsc   Bundle resource to create probes for
  * \param[in,out] node  Node to create probe on
  *
  * \return true if any probe was created, otherwise false
  */
 bool
 pcmk__bundle_create_probe(pcmk_resource_t *rsc, pcmk_node_t *node)
 {
     struct probe_data probe_data = { rsc, node, false };
 
     CRM_ASSERT(pcmk__is_bundle(rsc));
     pe__foreach_bundle_replica(rsc, create_replica_probes, &probe_data);
     return probe_data.any_created;
 }
 
 /*!
  * \internal
  * \brief Output actions for one bundle replica
  *
  * \param[in,out] replica    Replica to output actions for
  * \param[in]     user_data  Unused
  *
  * \return true (to indicate that any further replicas should be processed)
  */
 static bool
 output_replica_actions(pcmk__bundle_replica_t *replica, void *user_data)
 {
     if (replica->ip != NULL) {
-        replica->ip->cmds->output_actions(replica->ip);
-    }
-    if (replica->container != NULL) {
-        replica->container->cmds->output_actions(replica->container);
+        replica->ip->private->cmds->output_actions(replica->ip);
     }
+    replica->container->private->cmds->output_actions(replica->container);
     if (replica->remote != NULL) {
-        replica->remote->cmds->output_actions(replica->remote);
+        replica->remote->private->cmds->output_actions(replica->remote);
     }
     if (replica->child != NULL) {
-        replica->child->cmds->output_actions(replica->child);
+        replica->child->private->cmds->output_actions(replica->child);
     }
     return true;
 }
 
 /*!
  * \internal
  * \brief Output a summary of scheduled actions for a bundle resource
  *
  * \param[in,out] rsc  Bundle resource to output actions for
  */
 void
 pcmk__output_bundle_actions(pcmk_resource_t *rsc)
 {
     CRM_ASSERT(pcmk__is_bundle(rsc));
     pe__foreach_bundle_replica(rsc, output_replica_actions, NULL);
 }
 
 // Bundle implementation of pcmk_assignment_methods_t:add_utilization()
 void
 pcmk__bundle_add_utilization(const pcmk_resource_t *rsc,
                              const pcmk_resource_t *orig_rsc, GList *all_rscs,
                              GHashTable *utilization)
 {
     pcmk_resource_t *container = NULL;
 
     CRM_ASSERT(pcmk__is_bundle(rsc));
 
     if (!pcmk_is_set(rsc->flags, pcmk_rsc_unassigned)) {
         return;
     }
 
     /* All bundle replicas are identical, so using the utilization of the first
      * is sufficient for any. Only the implicit container resource can have
      * utilization values.
      */
     container = pe__first_container(rsc);
     if (container != NULL) {
-        container->cmds->add_utilization(container, orig_rsc, all_rscs,
-                                         utilization);
+        container->private->cmds->add_utilization(container, orig_rsc, all_rscs,
+                                                  utilization);
     }
 }
 
 // Bundle implementation of pcmk_assignment_methods_t:shutdown_lock()
 void
 pcmk__bundle_shutdown_lock(pcmk_resource_t *rsc)
 {
     CRM_ASSERT(pcmk__is_bundle(rsc));
     // Bundles currently don't support shutdown locks
 }
diff --git a/lib/pacemaker/pcmk_sched_clone.c b/lib/pacemaker/pcmk_sched_clone.c
index 2e55de7d03..4ed5fee950 100644
--- a/lib/pacemaker/pcmk_sched_clone.c
+++ b/lib/pacemaker/pcmk_sched_clone.c
@@ -1,710 +1,715 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <crm/common/xml.h>
 #include <pacemaker-internal.h>
 
 #include "libpacemaker_private.h"
 
 /*!
  * \internal
  * \brief Assign a clone resource's instances to nodes
  *
  * \param[in,out] rsc           Clone resource to assign
  * \param[in]     prefer        Node to prefer, if all else is equal
  * \param[in]     stop_if_fail  If \c true and a primitive descendant of \p rsc
  *                              can't be assigned to a node, set the
  *                              descendant's next role to stopped and update
  *                              existing actions
  *
  * \return NULL (clones are not assigned to a single node)
  *
  * \note If \p stop_if_fail is \c false, then \c pcmk__unassign_resource() can
  *       completely undo the assignment. A successful assignment can be either
  *       undone or left alone as final. A failed assignment has the same effect
  *       as calling pcmk__unassign_resource(); there are no side effects on
  *       roles or actions.
  */
 pcmk_node_t *
 pcmk__clone_assign(pcmk_resource_t *rsc, const pcmk_node_t *prefer,
                    bool stop_if_fail)
 {
     GList *colocations = NULL;
 
     CRM_ASSERT(pcmk__is_clone(rsc));
 
     if (!pcmk_is_set(rsc->flags, pcmk_rsc_unassigned)) {
         return NULL; // Assignment has already been done
     }
 
     // Detect assignment loops
     if (pcmk_is_set(rsc->flags, pcmk_rsc_assigning)) {
         pcmk__rsc_debug(rsc, "Breaking assignment loop involving %s", rsc->id);
         return NULL;
     }
     pcmk__set_rsc_flags(rsc, pcmk_rsc_assigning);
 
     // If this clone is promotable, consider nodes' promotion scores
     if (pcmk_is_set(rsc->flags, pcmk_rsc_promotable)) {
         pcmk__add_promotion_scores(rsc);
     }
 
     // If this clone is colocated with any other resources, assign those first
     colocations = pcmk__this_with_colocations(rsc);
     for (GList *iter = colocations; iter != NULL; iter = iter->next) {
         pcmk__colocation_t *constraint = (pcmk__colocation_t *) iter->data;
+        pcmk_resource_t *primary = constraint->primary;
 
         pcmk__rsc_trace(rsc, "%s: Assigning colocation %s primary %s first",
-                        rsc->id, constraint->id, constraint->primary->id);
-        constraint->primary->cmds->assign(constraint->primary, prefer,
-                                          stop_if_fail);
+                        rsc->id, constraint->id, primary->id);
+        primary->private->cmds->assign(primary, prefer, stop_if_fail);
     }
     g_list_free(colocations);
 
     // If any resources are colocated with this one, consider their preferences
     colocations = pcmk__with_this_colocations(rsc);
     g_list_foreach(colocations, pcmk__add_dependent_scores, rsc);
     g_list_free(colocations);
 
     pe__show_node_scores(!pcmk_is_set(rsc->cluster->flags,
                                       pcmk_sched_output_scores),
                          rsc, __func__, rsc->allowed_nodes, rsc->cluster);
 
     rsc->children = g_list_sort(rsc->children, pcmk__cmp_instance);
     pcmk__assign_instances(rsc, rsc->children, pe__clone_max(rsc),
                            pe__clone_node_max(rsc));
 
     if (pcmk_is_set(rsc->flags, pcmk_rsc_promotable)) {
         pcmk__set_instance_roles(rsc);
     }
 
     pcmk__clear_rsc_flags(rsc, pcmk_rsc_unassigned|pcmk_rsc_assigning);
     pcmk__rsc_trace(rsc, "Assigned clone %s", rsc->id);
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Create all actions needed for a given clone resource
  *
  * \param[in,out] rsc  Clone resource to create actions for
  */
 void
 pcmk__clone_create_actions(pcmk_resource_t *rsc)
 {
     CRM_ASSERT(pcmk__is_clone(rsc));
 
     pcmk__rsc_trace(rsc, "Creating actions for clone %s", rsc->id);
     pcmk__create_instance_actions(rsc, rsc->children);
     if (pcmk_is_set(rsc->flags, pcmk_rsc_promotable)) {
         pcmk__create_promotable_actions(rsc);
     }
 }
 
 /*!
  * \internal
  * \brief Create implicit constraints needed for a clone resource
  *
  * \param[in,out] rsc  Clone resource to create implicit constraints for
  */
 void
 pcmk__clone_internal_constraints(pcmk_resource_t *rsc)
 {
     bool ordered = false;
 
     CRM_ASSERT(pcmk__is_clone(rsc));
 
     pcmk__rsc_trace(rsc, "Creating internal constraints for clone %s", rsc->id);
 
     // Restart ordering: Stop -> stopped -> start -> started
     pcmk__order_resource_actions(rsc, PCMK_ACTION_STOPPED,
                                  rsc, PCMK_ACTION_START,
                                  pcmk__ar_ordered);
     pcmk__order_resource_actions(rsc, PCMK_ACTION_START,
                                  rsc, PCMK_ACTION_RUNNING,
                                  pcmk__ar_unrunnable_first_blocks);
     pcmk__order_resource_actions(rsc, PCMK_ACTION_STOP,
                                  rsc, PCMK_ACTION_STOPPED,
                                  pcmk__ar_unrunnable_first_blocks);
 
     // Demoted -> stop and started -> promote
     if (pcmk_is_set(rsc->flags, pcmk_rsc_promotable)) {
         pcmk__order_resource_actions(rsc, PCMK_ACTION_DEMOTED,
                                      rsc, PCMK_ACTION_STOP,
                                      pcmk__ar_ordered);
         pcmk__order_resource_actions(rsc, PCMK_ACTION_RUNNING,
                                      rsc, PCMK_ACTION_PROMOTE,
                                      pcmk__ar_unrunnable_first_blocks);
     }
 
     ordered = pe__clone_is_ordered(rsc);
     if (ordered) {
         /* Ordered clone instances must start and stop by instance number. The
          * instances might have been previously shuffled for assignment or
          * promotion purposes, so re-sort them.
          */
         rsc->children = g_list_sort(rsc->children, pcmk__cmp_instance_number);
     }
     for (GList *iter = rsc->children; iter != NULL; iter = iter->next) {
         pcmk_resource_t *instance = (pcmk_resource_t *) iter->data;
 
-        instance->cmds->internal_constraints(instance);
+        instance->private->cmds->internal_constraints(instance);
 
         // Start clone -> start instance -> clone started
         pcmk__order_starts(rsc, instance, pcmk__ar_unrunnable_first_blocks
                                           |pcmk__ar_then_implies_first_graphed);
         pcmk__order_resource_actions(instance, PCMK_ACTION_START,
                                      rsc, PCMK_ACTION_RUNNING,
                                      pcmk__ar_first_implies_then_graphed);
 
         // Stop clone -> stop instance -> clone stopped
         pcmk__order_stops(rsc, instance, pcmk__ar_then_implies_first_graphed);
         pcmk__order_resource_actions(instance, PCMK_ACTION_STOP,
                                      rsc, PCMK_ACTION_STOPPED,
                                      pcmk__ar_first_implies_then_graphed);
 
         /* Instances of ordered clones must be started and stopped by instance
          * number. Since only some instances may be starting or stopping, order
          * each instance relative to every later instance.
          */
         if (ordered) {
             for (GList *later = iter->next;
                  later != NULL; later = later->next) {
                 pcmk__order_starts(instance, (pcmk_resource_t *) later->data,
                                    pcmk__ar_ordered);
                 pcmk__order_stops((pcmk_resource_t *) later->data, instance,
                                   pcmk__ar_ordered);
             }
         }
     }
     if (pcmk_is_set(rsc->flags, pcmk_rsc_promotable)) {
         pcmk__order_promotable_instances(rsc);
     }
 }
 
 /*!
  * \internal
  * \brief Check whether colocated resources can be interleaved
  *
  * \param[in] colocation  Colocation constraint with clone as primary
  *
  * \return true if colocated resources can be interleaved, otherwise false
  */
 static bool
 can_interleave(const pcmk__colocation_t *colocation)
 {
     const pcmk_resource_t *primary = colocation->primary;
     const pcmk_resource_t *dependent = colocation->dependent;
 
     // Only colocations between clone or bundle resources use interleaving
     if (dependent->variant <= pcmk_rsc_variant_group) {
         return false;
     }
 
     // Only the dependent needs to be marked for interleaving
     if (!crm_is_true(g_hash_table_lookup(dependent->meta,
                                          PCMK_META_INTERLEAVE))) {
         return false;
     }
 
     /* @TODO Do we actually care about multiple primary instances sharing a
      * dependent instance?
      */
     if (dependent->private->fns->max_per_node(dependent)
         != primary->private->fns->max_per_node(primary)) {
         pcmk__config_err("Cannot interleave %s and %s because they do not "
                          "support the same number of instances per node",
                          dependent->id, primary->id);
         return false;
     }
 
     return true;
 }
 
 /*!
  * \internal
  * \brief Apply a colocation's score to node scores or resource priority
  *
  * Given a colocation constraint, apply its score to the dependent's
  * allowed node scores (if we are still placing resources) or priority (if
  * we are choosing promotable clone instance roles).
  *
  * \param[in,out] dependent      Dependent resource in colocation
  * \param[in]     primary        Primary resource in colocation
  * \param[in]     colocation     Colocation constraint to apply
  * \param[in]     for_dependent  true if called on behalf of dependent
  */
 void
 pcmk__clone_apply_coloc_score(pcmk_resource_t *dependent,
                               const pcmk_resource_t *primary,
                               const pcmk__colocation_t *colocation,
                               bool for_dependent)
 {
     const GList *iter = NULL;
 
     /* This should never be called for the clone itself as a dependent. Instead,
      * we add its colocation constraints to its instances and call the
      * apply_coloc_score() method for the instances as dependents.
      */
     CRM_ASSERT(!for_dependent);
 
     CRM_ASSERT((colocation != NULL) && pcmk__is_clone(primary)
                && pcmk__is_primitive(dependent));
 
     if (pcmk_is_set(primary->flags, pcmk_rsc_unassigned)) {
         pcmk__rsc_trace(primary,
                         "Delaying processing colocation %s "
                         "because cloned primary %s is still provisional",
                         colocation->id, primary->id);
         return;
     }
 
     pcmk__rsc_trace(primary, "Processing colocation %s (%s with clone %s @%s)",
                     colocation->id, dependent->id, primary->id,
                     pcmk_readable_score(colocation->score));
 
     // Apply role-specific colocations
     if (pcmk_is_set(primary->flags, pcmk_rsc_promotable)
         && (colocation->primary_role != pcmk_role_unknown)) {
 
         if (pcmk_is_set(dependent->flags, pcmk_rsc_unassigned)) {
             // We're assigning the dependent to a node
             pcmk__update_dependent_with_promotable(primary, dependent,
                                                    colocation);
             return;
         }
 
         if (colocation->dependent_role == pcmk_role_promoted) {
             // We're choosing a role for the dependent
             pcmk__update_promotable_dependent_priority(primary, dependent,
                                                        colocation);
             return;
         }
     }
 
     // Apply interleaved colocations
     if (can_interleave(colocation)) {
         const pcmk_resource_t *primary_instance = NULL;
 
         primary_instance = pcmk__find_compatible_instance(dependent, primary,
                                                           pcmk_role_unknown,
                                                           false);
         if (primary_instance != NULL) {
             pcmk__rsc_debug(primary, "Interleaving %s with %s",
                             dependent->id, primary_instance->id);
-            dependent->cmds->apply_coloc_score(dependent, primary_instance,
-                                               colocation, true);
+            dependent->private->cmds->apply_coloc_score(dependent,
+                                                        primary_instance,
+                                                        colocation, true);
 
         } else if (colocation->score >= PCMK_SCORE_INFINITY) {
             crm_notice("%s cannot run because it cannot interleave with "
                        "any instance of %s", dependent->id, primary->id);
             pcmk__assign_resource(dependent, NULL, true, true);
 
         } else {
             pcmk__rsc_debug(primary,
                             "%s will not colocate with %s "
                             "because no instance can interleave with it",
                             dependent->id, primary->id);
         }
 
         return;
     }
 
     // Apply mandatory colocations
     if (colocation->score >= PCMK_SCORE_INFINITY) {
         GList *primary_nodes = NULL;
 
         // Dependent can run only where primary will have unblocked instances
         for (iter = primary->children; iter != NULL; iter = iter->next) {
             const pcmk_resource_t *instance = iter->data;
             pcmk_node_t *chosen = NULL;
 
             chosen = instance->private->fns->location(instance, NULL, 0);
             if ((chosen != NULL)
                 && !is_set_recursive(instance, pcmk_rsc_blocked, TRUE)) {
                 pcmk__rsc_trace(primary, "Allowing %s: %s %d",
                                 colocation->id, pcmk__node_name(chosen),
                                 chosen->weight);
                 primary_nodes = g_list_prepend(primary_nodes, chosen);
             }
         }
         pcmk__colocation_intersect_nodes(dependent, primary, colocation,
                                          primary_nodes, false);
         g_list_free(primary_nodes);
         return;
     }
 
     // Apply optional colocations
     for (iter = primary->children; iter != NULL; iter = iter->next) {
         const pcmk_resource_t *instance = iter->data;
 
-        instance->cmds->apply_coloc_score(dependent, instance, colocation,
-                                          false);
+        instance->private->cmds->apply_coloc_score(dependent, instance,
+                                                   colocation, false);
     }
 }
 
 // Clone implementation of pcmk_assignment_methods_t:with_this_colocations()
 void
 pcmk__with_clone_colocations(const pcmk_resource_t *rsc,
                              const pcmk_resource_t *orig_rsc, GList **list)
 {
     CRM_CHECK((rsc != NULL) && (orig_rsc != NULL) && (list != NULL), return);
 
     pcmk__add_with_this_list(list, rsc->rsc_cons_lhs, orig_rsc);
 
     if (rsc->parent != NULL) {
-        rsc->parent->cmds->with_this_colocations(rsc->parent, orig_rsc, list);
+        rsc->parent->private->cmds->with_this_colocations(rsc->parent, orig_rsc,
+                                                          list);
     }
 }
 
 // Clone implementation of pcmk_assignment_methods_t:this_with_colocations()
 void
 pcmk__clone_with_colocations(const pcmk_resource_t *rsc,
                              const pcmk_resource_t *orig_rsc, GList **list)
 {
     CRM_CHECK((rsc != NULL) && (orig_rsc != NULL) && (list != NULL), return);
 
     pcmk__add_this_with_list(list, rsc->rsc_cons, orig_rsc);
 
     if (rsc->parent != NULL) {
-        rsc->parent->cmds->this_with_colocations(rsc->parent, orig_rsc, list);
+        rsc->parent->private->cmds->this_with_colocations(rsc->parent, orig_rsc,
+                                                          list);
     }
 }
 
 /*!
  * \internal
  * \brief Return action flags for a given clone resource action
  *
  * \param[in,out] action  Action to get flags for
  * \param[in]     node    If not NULL, limit effects to this node
  *
  * \return Flags appropriate to \p action on \p node
  */
 uint32_t
 pcmk__clone_action_flags(pcmk_action_t *action, const pcmk_node_t *node)
 {
     CRM_ASSERT((action != NULL) && pcmk__is_clone(action->rsc));
 
     return pcmk__collective_action_flags(action, action->rsc->children, node);
 }
 
 /*!
  * \internal
  * \brief Apply a location constraint to a clone resource's allowed node scores
  *
  * \param[in,out] rsc       Clone resource to apply constraint to
  * \param[in,out] location  Location constraint to apply
  */
 void
 pcmk__clone_apply_location(pcmk_resource_t *rsc, pcmk__location_t *location)
 {
     CRM_CHECK((location != NULL) && pcmk__is_clone(rsc), return);
 
     pcmk__apply_location(rsc, location);
 
     for (GList *iter = rsc->children; iter != NULL; iter = iter->next) {
         pcmk_resource_t *instance = (pcmk_resource_t *) iter->data;
 
-        instance->cmds->apply_location(instance, location);
+        instance->private->cmds->apply_location(instance, location);
     }
 }
 
 // GFunc wrapper for calling the action_flags() resource method
 static void
 call_action_flags(gpointer data, gpointer user_data)
 {
     pcmk_resource_t *rsc = user_data;
 
-    rsc->cmds->action_flags((pcmk_action_t *) data, NULL);
+    rsc->private->cmds->action_flags((pcmk_action_t *) data, NULL);
 }
 
 /*!
  * \internal
  * \brief Add a clone resource's actions to the transition graph
  *
  * \param[in,out] rsc  Resource whose actions should be added
  */
 void
 pcmk__clone_add_actions_to_graph(pcmk_resource_t *rsc)
 {
     CRM_ASSERT(pcmk__is_clone(rsc));
 
     g_list_foreach(rsc->actions, call_action_flags, rsc);
     pe__create_clone_notifications(rsc);
 
     for (GList *iter = rsc->children; iter != NULL; iter = iter->next) {
         pcmk_resource_t *child_rsc = (pcmk_resource_t *) iter->data;
 
-        child_rsc->cmds->add_actions_to_graph(child_rsc);
+        child_rsc->private->cmds->add_actions_to_graph(child_rsc);
     }
 
     pcmk__add_rsc_actions_to_graph(rsc);
     pe__free_clone_notification_data(rsc);
 }
 
 /*!
  * \internal
  * \brief Check whether a resource or any children have been probed on a node
  *
  * \param[in] rsc   Resource to check
  * \param[in] node  Node to check
  *
  * \return true if \p node is in the known_on table of \p rsc or any of its
  *         children, otherwise false
  */
 static bool
 rsc_probed_on(const pcmk_resource_t *rsc, const pcmk_node_t *node)
 {
     if (rsc->children != NULL) {
         for (GList *child_iter = rsc->children; child_iter != NULL;
              child_iter = child_iter->next) {
 
             pcmk_resource_t *child = (pcmk_resource_t *) child_iter->data;
 
             if (rsc_probed_on(child, node)) {
                 return true;
             }
         }
         return false;
     }
 
     if (rsc->known_on != NULL) {
         GHashTableIter iter;
         pcmk_node_t *known_node = NULL;
 
         g_hash_table_iter_init(&iter, rsc->known_on);
         while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &known_node)) {
             if (pcmk__same_node(node, known_node)) {
                 return true;
             }
         }
     }
     return false;
 }
 
 /*!
  * \internal
  * \brief Find clone instance that has been probed on given node
  *
  * \param[in] clone  Clone resource to check
  * \param[in] node   Node to check
  *
  * \return Instance of \p clone that has been probed on \p node if any,
  *         otherwise NULL
  */
 static pcmk_resource_t *
 find_probed_instance_on(const pcmk_resource_t *clone, const pcmk_node_t *node)
 {
     for (GList *iter = clone->children; iter != NULL; iter = iter->next) {
         pcmk_resource_t *instance = (pcmk_resource_t *) iter->data;
 
         if (rsc_probed_on(instance, node)) {
             return instance;
         }
     }
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Probe an anonymous clone on a node
  *
  * \param[in,out] clone  Anonymous clone to probe
  * \param[in,out] node   Node to probe \p clone on
  */
 static bool
 probe_anonymous_clone(pcmk_resource_t *clone, pcmk_node_t *node)
 {
     // Check whether we already probed an instance on this node
     pcmk_resource_t *child = find_probed_instance_on(clone, node);
 
     // Otherwise, check if we plan to start an instance on this node
     for (GList *iter = clone->children; (iter != NULL) && (child == NULL);
          iter = iter->next) {
         pcmk_resource_t *instance = (pcmk_resource_t *) iter->data;
         const pcmk_node_t *instance_node = NULL;
 
         instance_node = instance->private->fns->location(instance, NULL, 0);
         if (pcmk__same_node(instance_node, node)) {
             child = instance;
         }
     }
 
     // Otherwise, use the first clone instance
     if (child == NULL) {
         child = clone->children->data;
     }
 
     // Anonymous clones only need to probe a single instance
-    return child->cmds->create_probe(child, node);
+    return child->private->cmds->create_probe(child, node);
 }
 
 /*!
  * \internal
  * \brief Schedule any probes needed for a resource on a node
  *
  * \param[in,out] rsc   Resource to create probe for
  * \param[in,out] node  Node to create probe on
  *
  * \return true if any probe was created, otherwise false
  */
 bool
 pcmk__clone_create_probe(pcmk_resource_t *rsc, pcmk_node_t *node)
 {
     CRM_ASSERT((node != NULL) && pcmk__is_clone(rsc));
 
     if (rsc->exclusive_discover) {
         /* The clone is configured to be probed only where a location constraint
          * exists with PCMK_XA_RESOURCE_DISCOVERY set to exclusive.
          *
          * This check is not strictly necessary here since the instance's
          * create_probe() method would also check, but doing it here is more
          * efficient (especially for unique clones with a large number of
          * instances), and affects the CRM_meta_notify_available_uname variable
          * passed with notify actions.
          */
         pcmk_node_t *allowed = g_hash_table_lookup(rsc->allowed_nodes,
                                                    node->details->id);
 
         if ((allowed == NULL)
             || (allowed->rsc_discover_mode != pcmk_probe_exclusive)) {
             /* This node is not marked for resource discovery. Remove it from
              * allowed_nodes so that notifications contain only nodes that the
              * clone can possibly run on.
              */
             pcmk__rsc_trace(rsc,
                             "Skipping probe for %s on %s because resource has "
                             "exclusive discovery but is not allowed on node",
                             rsc->id, pcmk__node_name(node));
             g_hash_table_remove(rsc->allowed_nodes, node->details->id);
             return false;
         }
     }
 
     rsc->children = g_list_sort(rsc->children, pcmk__cmp_instance_number);
     if (pcmk_is_set(rsc->flags, pcmk_rsc_unique)) {
         return pcmk__probe_resource_list(rsc->children, node);
     } else {
         return probe_anonymous_clone(rsc, node);
     }
 }
 
 /*!
  * \internal
  * \brief Add meta-attributes relevant to transition graph actions to XML
  *
  * Add clone-specific meta-attributes needed for transition graph actions.
  *
  * \param[in]     rsc  Clone resource whose meta-attributes should be added
  * \param[in,out] xml  Transition graph action attributes XML to add to
  */
 void
 pcmk__clone_add_graph_meta(const pcmk_resource_t *rsc, xmlNode *xml)
 {
     char *name = NULL;
 
     CRM_ASSERT(pcmk__is_clone(rsc) && (xml != NULL));
 
     name = crm_meta_name(PCMK_META_GLOBALLY_UNIQUE);
     crm_xml_add(xml, name, pcmk__flag_text(rsc->flags, pcmk_rsc_unique));
     free(name);
 
     name = crm_meta_name(PCMK_META_NOTIFY);
     crm_xml_add(xml, name, pcmk__flag_text(rsc->flags, pcmk_rsc_notify));
     free(name);
 
     name = crm_meta_name(PCMK_META_CLONE_MAX);
     crm_xml_add_int(xml, name, pe__clone_max(rsc));
     free(name);
 
     name = crm_meta_name(PCMK_META_CLONE_NODE_MAX);
     crm_xml_add_int(xml, name, pe__clone_node_max(rsc));
     free(name);
 
     if (pcmk_is_set(rsc->flags, pcmk_rsc_promotable)) {
         int promoted_max = pe__clone_promoted_max(rsc);
         int promoted_node_max = pe__clone_promoted_node_max(rsc);
 
         name = crm_meta_name(PCMK_META_PROMOTED_MAX);
         crm_xml_add_int(xml, name, promoted_max);
         free(name);
 
         name = crm_meta_name(PCMK_META_PROMOTED_NODE_MAX);
         crm_xml_add_int(xml, name, promoted_node_max);
         free(name);
 
         /* @COMPAT Maintain backward compatibility with resource agents that
          * expect the old names (deprecated since 2.0.0).
          */
         name = crm_meta_name(PCMK__META_PROMOTED_MAX_LEGACY);
         crm_xml_add_int(xml, name, promoted_max);
         free(name);
 
         name = crm_meta_name(PCMK__META_PROMOTED_NODE_MAX_LEGACY);
         crm_xml_add_int(xml, name, promoted_node_max);
         free(name);
     }
 }
 
 // Clone implementation of pcmk_assignment_methods_t:add_utilization()
 void
 pcmk__clone_add_utilization(const pcmk_resource_t *rsc,
                             const pcmk_resource_t *orig_rsc, GList *all_rscs,
                             GHashTable *utilization)
 {
     bool existing = false;
     pcmk_resource_t *child = NULL;
 
     CRM_ASSERT(pcmk__is_clone(rsc) && (orig_rsc != NULL)
                && (utilization != NULL));
 
     if (!pcmk_is_set(rsc->flags, pcmk_rsc_unassigned)) {
         return;
     }
 
     // Look for any child already existing in the list
     for (GList *iter = rsc->children; iter != NULL; iter = iter->next) {
         child = (pcmk_resource_t *) iter->data;
         if (g_list_find(all_rscs, child)) {
             existing = true; // Keep checking remaining children
         } else {
             // If this is a clone of a group, look for group's members
             for (GList *member_iter = child->children; member_iter != NULL;
                  member_iter = member_iter->next) {
 
                 pcmk_resource_t *member = (pcmk_resource_t *) member_iter->data;
 
                 if (g_list_find(all_rscs, member) != NULL) {
                     // Add *child's* utilization, not group member's
-                    child->cmds->add_utilization(child, orig_rsc, all_rscs,
-                                                 utilization);
+                    child->private->cmds->add_utilization(child, orig_rsc,
+                                                          all_rscs,
+                                                          utilization);
                     existing = true;
                     break;
                 }
             }
         }
     }
 
     if (!existing && (rsc->children != NULL)) {
         // If nothing was found, still add first child's utilization
         child = (pcmk_resource_t *) rsc->children->data;
 
-        child->cmds->add_utilization(child, orig_rsc, all_rscs, utilization);
+        child->private->cmds->add_utilization(child, orig_rsc, all_rscs,
+                                              utilization);
     }
 }
 
 // Clone implementation of pcmk_assignment_methods_t:shutdown_lock()
 void
 pcmk__clone_shutdown_lock(pcmk_resource_t *rsc)
 {
     CRM_ASSERT(pcmk__is_clone(rsc));
     return; // Clones currently don't support shutdown locks
 }
diff --git a/lib/pacemaker/pcmk_sched_colocation.c b/lib/pacemaker/pcmk_sched_colocation.c
index 6279ad50f3..c423a95bbe 100644
--- a/lib/pacemaker/pcmk_sched_colocation.c
+++ b/lib/pacemaker/pcmk_sched_colocation.c
@@ -1,1935 +1,1938 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <stdbool.h>
 #include <glib.h>
 
 #include <crm/crm.h>
 #include <crm/common/scheduler_internal.h>
 #include <crm/pengine/status.h>
 #include <pacemaker-internal.h>
 
 #include "crm/common/util.h"
 #include "crm/common/xml_internal.h"
 #include "crm/common/xml.h"
 #include "libpacemaker_private.h"
 
 // Used to temporarily mark a node as unusable
 #define INFINITY_HACK   (PCMK_SCORE_INFINITY * -100)
 
 /*!
  * \internal
  * \brief Compare two colocations according to priority
  *
  * Compare two colocations according to the order in which they should be
  * considered, based on either their dependent resources or their primary
  * resources -- preferring (in order):
  *  * Colocation that is not \c NULL
  *  * Colocation whose resource has higher priority
  *  * Colocation whose resource is of a higher-level variant
  *    (bundle > clone > group > primitive)
  *  * Colocation whose resource is promotable, if both are clones
  *  * Colocation whose resource has lower ID in lexicographic order
  *
  * \param[in] colocation1  First colocation to compare
  * \param[in] colocation2  Second colocation to compare
  * \param[in] dependent    If \c true, compare colocations by dependent
  *                         priority; otherwise compare them by primary priority
  *
  * \return A negative number if \p colocation1 should be considered first,
  *         a positive number if \p colocation2 should be considered first,
  *         or 0 if order doesn't matter
  */
 static gint
 cmp_colocation_priority(const pcmk__colocation_t *colocation1,
                         const pcmk__colocation_t *colocation2, bool dependent)
 {
     const pcmk_resource_t *rsc1 = NULL;
     const pcmk_resource_t *rsc2 = NULL;
 
     if (colocation1 == NULL) {
         return 1;
     }
     if (colocation2 == NULL) {
         return -1;
     }
 
     if (dependent) {
         rsc1 = colocation1->dependent;
         rsc2 = colocation2->dependent;
         CRM_ASSERT(colocation1->primary != NULL);
     } else {
         rsc1 = colocation1->primary;
         rsc2 = colocation2->primary;
         CRM_ASSERT(colocation1->dependent != NULL);
     }
     CRM_ASSERT((rsc1 != NULL) && (rsc2 != NULL));
 
     if (rsc1->priority > rsc2->priority) {
         return -1;
     }
     if (rsc1->priority < rsc2->priority) {
         return 1;
     }
 
     // Process clones before primitives and groups
     if (rsc1->variant > rsc2->variant) {
         return -1;
     }
     if (rsc1->variant < rsc2->variant) {
         return 1;
     }
 
     /* @COMPAT scheduler <2.0.0: Process promotable clones before nonpromotable
      * clones (probably unnecessary, but avoids having to update regression
      * tests)
      */
     if (pcmk__is_clone(rsc1)) {
         if (pcmk_is_set(rsc1->flags, pcmk_rsc_promotable)
             && !pcmk_is_set(rsc2->flags, pcmk_rsc_promotable)) {
             return -1;
         }
         if (!pcmk_is_set(rsc1->flags, pcmk_rsc_promotable)
             && pcmk_is_set(rsc2->flags, pcmk_rsc_promotable)) {
             return 1;
         }
     }
 
     return strcmp(rsc1->id, rsc2->id);
 }
 
 /*!
  * \internal
  * \brief Compare two colocations according to priority based on dependents
  *
  * Compare two colocations according to the order in which they should be
  * considered, based on their dependent resources -- preferring (in order):
  *  * Colocation that is not \c NULL
  *  * Colocation whose resource has higher priority
  *  * Colocation whose resource is of a higher-level variant
  *    (bundle > clone > group > primitive)
  *  * Colocation whose resource is promotable, if both are clones
  *  * Colocation whose resource has lower ID in lexicographic order
  *
  * \param[in] a  First colocation to compare
  * \param[in] b  Second colocation to compare
  *
  * \return A negative number if \p a should be considered first,
  *         a positive number if \p b should be considered first,
  *         or 0 if order doesn't matter
  */
 static gint
 cmp_dependent_priority(gconstpointer a, gconstpointer b)
 {
     return cmp_colocation_priority(a, b, true);
 }
 
 /*!
  * \internal
  * \brief Compare two colocations according to priority based on primaries
  *
  * Compare two colocations according to the order in which they should be
  * considered, based on their primary resources -- preferring (in order):
  *  * Colocation that is not \c NULL
  *  * Colocation whose primary has higher priority
  *  * Colocation whose primary is of a higher-level variant
  *    (bundle > clone > group > primitive)
  *  * Colocation whose primary is promotable, if both are clones
  *  * Colocation whose primary has lower ID in lexicographic order
  *
  * \param[in] a  First colocation to compare
  * \param[in] b  Second colocation to compare
  *
  * \return A negative number if \p a should be considered first,
  *         a positive number if \p b should be considered first,
  *         or 0 if order doesn't matter
  */
 static gint
 cmp_primary_priority(gconstpointer a, gconstpointer b)
 {
     return cmp_colocation_priority(a, b, false);
 }
 
 /*!
  * \internal
  * \brief Add a "this with" colocation constraint to a sorted list
  *
  * \param[in,out] list        List of constraints to add \p colocation to
  * \param[in]     colocation  Colocation constraint to add to \p list
  * \param[in]     rsc         Resource whose colocations we're getting (for
  *                            logging only)
  *
  * \note The list will be sorted using cmp_primary_priority().
  */
 void
 pcmk__add_this_with(GList **list, const pcmk__colocation_t *colocation,
                     const pcmk_resource_t *rsc)
 {
     CRM_ASSERT((list != NULL) && (colocation != NULL) && (rsc != NULL));
 
     pcmk__rsc_trace(rsc,
                     "Adding colocation %s (%s with %s using %s @%s) to "
                     "'this with' list for %s",
                     colocation->id, colocation->dependent->id,
                     colocation->primary->id, colocation->node_attribute,
                     pcmk_readable_score(colocation->score), rsc->id);
     *list = g_list_insert_sorted(*list, (gpointer) colocation,
                                  cmp_primary_priority);
 }
 
 /*!
  * \internal
  * \brief Add a list of "this with" colocation constraints to a list
  *
  * \param[in,out] list      List of constraints to add \p addition to
  * \param[in]     addition  List of colocation constraints to add to \p list
  * \param[in]     rsc       Resource whose colocations we're getting (for
  *                          logging only)
  *
  * \note The lists must be pre-sorted by cmp_primary_priority().
  */
 void
 pcmk__add_this_with_list(GList **list, GList *addition,
                          const pcmk_resource_t *rsc)
 {
     CRM_ASSERT((list != NULL) && (rsc != NULL));
 
     pcmk__if_tracing(
         {}, // Always add each colocation individually if tracing
         {
             if (*list == NULL) {
                 // Trivial case for efficiency if not tracing
                 *list = g_list_copy(addition);
                 return;
             }
         }
     );
 
     for (const GList *iter = addition; iter != NULL; iter = iter->next) {
         pcmk__add_this_with(list, addition->data, rsc);
     }
 }
 
 /*!
  * \internal
  * \brief Add a "with this" colocation constraint to a sorted list
  *
  * \param[in,out] list        List of constraints to add \p colocation to
  * \param[in]     colocation  Colocation constraint to add to \p list
  * \param[in]     rsc         Resource whose colocations we're getting (for
  *                            logging only)
  *
  * \note The list will be sorted using cmp_dependent_priority().
  */
 void
 pcmk__add_with_this(GList **list, const pcmk__colocation_t *colocation,
                     const pcmk_resource_t *rsc)
 {
     CRM_ASSERT((list != NULL) && (colocation != NULL) && (rsc != NULL));
 
     pcmk__rsc_trace(rsc,
                     "Adding colocation %s (%s with %s using %s @%s) to "
                     "'with this' list for %s",
                     colocation->id, colocation->dependent->id,
                     colocation->primary->id, colocation->node_attribute,
                     pcmk_readable_score(colocation->score), rsc->id);
     *list = g_list_insert_sorted(*list, (gpointer) colocation,
                                  cmp_dependent_priority);
 }
 
 /*!
  * \internal
  * \brief Add a list of "with this" colocation constraints to a list
  *
  * \param[in,out] list      List of constraints to add \p addition to
  * \param[in]     addition  List of colocation constraints to add to \p list
  * \param[in]     rsc       Resource whose colocations we're getting (for
  *                          logging only)
  *
  * \note The lists must be pre-sorted by cmp_dependent_priority().
  */
 void
 pcmk__add_with_this_list(GList **list, GList *addition,
                          const pcmk_resource_t *rsc)
 {
     CRM_ASSERT((list != NULL) && (rsc != NULL));
 
     pcmk__if_tracing(
         {}, // Always add each colocation individually if tracing
         {
             if (*list == NULL) {
                 // Trivial case for efficiency if not tracing
                 *list = g_list_copy(addition);
                 return;
             }
         }
     );
 
     for (const GList *iter = addition; iter != NULL; iter = iter->next) {
         pcmk__add_with_this(list, addition->data, rsc);
     }
 }
 
 /*!
  * \internal
  * \brief Add orderings necessary for an anti-colocation constraint
  *
  * \param[in,out] first_rsc   One resource in an anti-colocation
  * \param[in]     first_role  Anti-colocation role of \p first_rsc
  * \param[in]     then_rsc    Other resource in the anti-colocation
  * \param[in]     then_role   Anti-colocation role of \p then_rsc
  */
 static void
 anti_colocation_order(pcmk_resource_t *first_rsc, int first_role,
                       pcmk_resource_t *then_rsc, int then_role)
 {
     const char *first_tasks[] = { NULL, NULL };
     const char *then_tasks[] = { NULL, NULL };
 
     /* Actions to make first_rsc lose first_role */
     if (first_role == pcmk_role_promoted) {
         first_tasks[0] = PCMK_ACTION_DEMOTE;
 
     } else {
         first_tasks[0] = PCMK_ACTION_STOP;
 
         if (first_role == pcmk_role_unpromoted) {
             first_tasks[1] = PCMK_ACTION_PROMOTE;
         }
     }
 
     /* Actions to make then_rsc gain then_role */
     if (then_role == pcmk_role_promoted) {
         then_tasks[0] = PCMK_ACTION_PROMOTE;
 
     } else {
         then_tasks[0] = PCMK_ACTION_START;
 
         if (then_role == pcmk_role_unpromoted) {
             then_tasks[1] = PCMK_ACTION_DEMOTE;
         }
     }
 
     for (int first_lpc = 0;
          (first_lpc <= 1) && (first_tasks[first_lpc] != NULL); first_lpc++) {
 
         for (int then_lpc = 0;
              (then_lpc <= 1) && (then_tasks[then_lpc] != NULL); then_lpc++) {
 
             pcmk__order_resource_actions(first_rsc, first_tasks[first_lpc],
                                          then_rsc, then_tasks[then_lpc],
                                          pcmk__ar_if_required_on_same_node);
         }
     }
 }
 
 /*!
  * \internal
  * \brief Add a new colocation constraint to scheduler data
  *
  * \param[in]     id              XML ID for this constraint
  * \param[in]     node_attr       Colocate by this attribute (NULL for #uname)
  * \param[in]     score           Constraint score
  * \param[in,out] dependent       Resource to be colocated
  * \param[in,out] primary         Resource to colocate \p dependent with
  * \param[in]     dependent_role  Current role of \p dependent
  * \param[in]     primary_role    Current role of \p primary
  * \param[in]     flags           Group of enum pcmk__coloc_flags
  */
 void
 pcmk__new_colocation(const char *id, const char *node_attr, int score,
                      pcmk_resource_t *dependent, pcmk_resource_t *primary,
                      const char *dependent_role, const char *primary_role,
                      uint32_t flags)
 {
     pcmk__colocation_t *new_con = NULL;
 
     CRM_CHECK(id != NULL, return);
 
     if ((dependent == NULL) || (primary == NULL)) {
         pcmk__config_err("Ignoring colocation '%s' because resource "
                          "does not exist", id);
         return;
     }
 
     if (score == 0) {
         pcmk__rsc_trace(dependent,
                         "Ignoring colocation '%s' (%s with %s) because score is 0",
                         id, dependent->id, primary->id);
         return;
     }
 
     new_con = pcmk__assert_alloc(1, sizeof(pcmk__colocation_t));
 
     if (pcmk__str_eq(dependent_role, PCMK_ROLE_STARTED,
                      pcmk__str_null_matches|pcmk__str_casei)) {
         dependent_role = PCMK__ROLE_UNKNOWN;
     }
 
     if (pcmk__str_eq(primary_role, PCMK_ROLE_STARTED,
                      pcmk__str_null_matches|pcmk__str_casei)) {
         primary_role = PCMK__ROLE_UNKNOWN;
     }
 
     new_con->id = id;
     new_con->dependent = dependent;
     new_con->primary = primary;
     new_con->score = score;
     new_con->dependent_role = pcmk_parse_role(dependent_role);
     new_con->primary_role = pcmk_parse_role(primary_role);
     new_con->node_attribute = pcmk__s(node_attr, CRM_ATTR_UNAME);
     new_con->flags = flags;
 
     pcmk__add_this_with(&(dependent->rsc_cons), new_con, dependent);
     pcmk__add_with_this(&(primary->rsc_cons_lhs), new_con, primary);
 
     dependent->cluster->colocation_constraints = g_list_prepend(
         dependent->cluster->colocation_constraints, new_con);
 
     if (score <= -PCMK_SCORE_INFINITY) {
         anti_colocation_order(dependent, new_con->dependent_role, primary,
                               new_con->primary_role);
         anti_colocation_order(primary, new_con->primary_role, dependent,
                               new_con->dependent_role);
     }
 }
 
 /*!
  * \internal
  * \brief Return the boolean influence corresponding to configuration
  *
  * \param[in] coloc_id     Colocation XML ID (for error logging)
  * \param[in] rsc          Resource involved in constraint (for default)
  * \param[in] influence_s  String value of \c PCMK_XA_INFLUENCE option
  *
  * \return \c pcmk__coloc_influence if string evaluates true, or string is
  *         \c NULL or invalid and resource's \c PCMK_META_CRITICAL option
  *         evaluates true, otherwise \c pcmk__coloc_none
  */
 static uint32_t
 unpack_influence(const char *coloc_id, const pcmk_resource_t *rsc,
                  const char *influence_s)
 {
     if (influence_s != NULL) {
         int influence_i = 0;
 
         if (crm_str_to_boolean(influence_s, &influence_i) < 0) {
             pcmk__config_err("Constraint '%s' has invalid value for "
                              PCMK_XA_INFLUENCE " (using default)",
                              coloc_id);
         } else {
             return (influence_i == 0)? pcmk__coloc_none : pcmk__coloc_influence;
         }
     }
     if (pcmk_is_set(rsc->flags, pcmk_rsc_critical)) {
         return pcmk__coloc_influence;
     }
     return pcmk__coloc_none;
 }
 
 static void
 unpack_colocation_set(xmlNode *set, int score, const char *coloc_id,
                       const char *influence_s, pcmk_scheduler_t *scheduler)
 {
     xmlNode *xml_rsc = NULL;
     pcmk_resource_t *other = NULL;
     pcmk_resource_t *resource = NULL;
     const char *set_id = pcmk__xe_id(set);
     const char *role = crm_element_value(set, PCMK_XA_ROLE);
     bool with_previous = false;
     int local_score = score;
     bool sequential = false;
     uint32_t flags = pcmk__coloc_none;
     const char *xml_rsc_id = NULL;
     const char *score_s = crm_element_value(set, PCMK_XA_SCORE);
 
     if (score_s) {
         local_score = char2score(score_s);
     }
     if (local_score == 0) {
         crm_trace("Ignoring colocation '%s' for set '%s' because score is 0",
                   coloc_id, set_id);
         return;
     }
 
     /* @COMPAT The deprecated PCMK__XA_ORDERING attribute specifies whether
      * resources in a positive-score set are colocated with the previous or next
      * resource.
      */
     if (pcmk__str_eq(crm_element_value(set, PCMK__XA_ORDERING),
                      PCMK__VALUE_GROUP,
                      pcmk__str_null_matches|pcmk__str_casei)) {
         with_previous = true;
     } else {
         pcmk__warn_once(pcmk__wo_set_ordering,
                         "Support for '" PCMK__XA_ORDERING "' other than"
                         " '" PCMK__VALUE_GROUP "' in " PCMK_XE_RESOURCE_SET
                         " (such as %s) is deprecated and will be removed in a"
                         " future release",
                         set_id);
     }
 
     if ((pcmk__xe_get_bool_attr(set, PCMK_XA_SEQUENTIAL,
                                 &sequential) == pcmk_rc_ok)
         && !sequential) {
         return;
     }
 
     if (local_score > 0) {
         for (xml_rsc = pcmk__xe_first_child(set, PCMK_XE_RESOURCE_REF, NULL,
                                             NULL);
              xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) {
 
             xml_rsc_id = pcmk__xe_id(xml_rsc);
             resource = pcmk__find_constraint_resource(scheduler->resources,
                                                       xml_rsc_id);
             if (resource == NULL) {
                 // Should be possible only with validation disabled
                 pcmk__config_err("Ignoring %s and later resources in set %s: "
                                  "No such resource", xml_rsc_id, set_id);
                 return;
             }
             if (other != NULL) {
                 flags = pcmk__coloc_explicit
                         | unpack_influence(coloc_id, resource, influence_s);
                 if (with_previous) {
                     pcmk__rsc_trace(resource, "Colocating %s with %s in set %s",
                                     resource->id, other->id, set_id);
                     pcmk__new_colocation(set_id, NULL, local_score, resource,
                                          other, role, role, flags);
                 } else {
                     pcmk__rsc_trace(resource, "Colocating %s with %s in set %s",
                                     other->id, resource->id, set_id);
                     pcmk__new_colocation(set_id, NULL, local_score, other,
                                          resource, role, role, flags);
                 }
             }
             other = resource;
         }
 
     } else {
         /* Anti-colocating with every prior resource is
          * the only way to ensure the intuitive result
          * (i.e. that no one in the set can run with anyone else in the set)
          */
 
         for (xml_rsc = pcmk__xe_first_child(set, PCMK_XE_RESOURCE_REF, NULL,
                                             NULL);
              xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) {
 
             xmlNode *xml_rsc_with = NULL;
 
             xml_rsc_id = pcmk__xe_id(xml_rsc);
             resource = pcmk__find_constraint_resource(scheduler->resources,
                                                       xml_rsc_id);
             if (resource == NULL) {
                 // Should be possible only with validation disabled
                 pcmk__config_err("Ignoring %s and later resources in set %s: "
                                  "No such resource", xml_rsc_id, set_id);
                 return;
             }
             flags = pcmk__coloc_explicit
                     | unpack_influence(coloc_id, resource, influence_s);
             for (xml_rsc_with = pcmk__xe_first_child(set, PCMK_XE_RESOURCE_REF,
                                                      NULL, NULL);
                  xml_rsc_with != NULL;
                  xml_rsc_with = pcmk__xe_next_same(xml_rsc_with)) {
 
                 xml_rsc_id = pcmk__xe_id(xml_rsc_with);
                 if (pcmk__str_eq(resource->id, xml_rsc_id, pcmk__str_none)) {
                     break;
                 }
                 other = pcmk__find_constraint_resource(scheduler->resources,
                                                        xml_rsc_id);
                 CRM_ASSERT(other != NULL); // We already processed it
                 pcmk__new_colocation(set_id, NULL, local_score,
                                      resource, other, role, role, flags);
             }
         }
     }
 }
 
 /*!
  * \internal
  * \brief Colocate two resource sets relative to each other
  *
  * \param[in]     id           Colocation XML ID
  * \param[in]     set1         Dependent set
  * \param[in]     set2         Primary set
  * \param[in]     score        Colocation score
  * \param[in]     influence_s  Value of colocation's \c PCMK_XA_INFLUENCE
  *                             attribute
  * \param[in,out] scheduler    Scheduler data
  */
 static void
 colocate_rsc_sets(const char *id, const xmlNode *set1, const xmlNode *set2,
                   int score, const char *influence_s,
                   pcmk_scheduler_t *scheduler)
 {
     xmlNode *xml_rsc = NULL;
     pcmk_resource_t *rsc_1 = NULL;
     pcmk_resource_t *rsc_2 = NULL;
 
     const char *xml_rsc_id = NULL;
     const char *role_1 = crm_element_value(set1, PCMK_XA_ROLE);
     const char *role_2 = crm_element_value(set2, PCMK_XA_ROLE);
 
     int rc = pcmk_rc_ok;
     bool sequential = false;
     uint32_t flags = pcmk__coloc_none;
 
     if (score == 0) {
         crm_trace("Ignoring colocation '%s' between sets %s and %s "
                   "because score is 0",
                   id, pcmk__xe_id(set1), pcmk__xe_id(set2));
         return;
     }
 
     rc = pcmk__xe_get_bool_attr(set1, PCMK_XA_SEQUENTIAL, &sequential);
     if ((rc != pcmk_rc_ok) || sequential) {
         // Get the first one
         xml_rsc = pcmk__xe_first_child(set1, PCMK_XE_RESOURCE_REF, NULL, NULL);
         if (xml_rsc != NULL) {
             xml_rsc_id = pcmk__xe_id(xml_rsc);
             rsc_1 = pcmk__find_constraint_resource(scheduler->resources,
                                                    xml_rsc_id);
             if (rsc_1 == NULL) {
                 // Should be possible only with validation disabled
                 pcmk__config_err("Ignoring colocation of set %s with set %s "
                                  "because first resource %s not found",
                                  pcmk__xe_id(set1), pcmk__xe_id(set2),
                                  xml_rsc_id);
                 return;
             }
         }
     }
 
     rc = pcmk__xe_get_bool_attr(set2, PCMK_XA_SEQUENTIAL, &sequential);
     if ((rc != pcmk_rc_ok) || sequential) {
         // Get the last one
         for (xml_rsc = pcmk__xe_first_child(set2, PCMK_XE_RESOURCE_REF, NULL,
                                             NULL);
              xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) {
 
             xml_rsc_id = pcmk__xe_id(xml_rsc);
         }
         rsc_2 = pcmk__find_constraint_resource(scheduler->resources,
                                                xml_rsc_id);
         if (rsc_2 == NULL) {
             // Should be possible only with validation disabled
             pcmk__config_err("Ignoring colocation of set %s with set %s "
                              "because last resource %s not found",
                              pcmk__xe_id(set1), pcmk__xe_id(set2), xml_rsc_id);
             return;
         }
     }
 
     if ((rsc_1 != NULL) && (rsc_2 != NULL)) { // Both sets are sequential
         flags = pcmk__coloc_explicit | unpack_influence(id, rsc_1, influence_s);
         pcmk__new_colocation(id, NULL, score, rsc_1, rsc_2, role_1, role_2,
                              flags);
 
     } else if (rsc_1 != NULL) { // Only set1 is sequential
         flags = pcmk__coloc_explicit | unpack_influence(id, rsc_1, influence_s);
         for (xml_rsc = pcmk__xe_first_child(set2, PCMK_XE_RESOURCE_REF, NULL,
                                             NULL);
              xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) {
 
             xml_rsc_id = pcmk__xe_id(xml_rsc);
             rsc_2 = pcmk__find_constraint_resource(scheduler->resources,
                                                    xml_rsc_id);
             if (rsc_2 == NULL) {
                 // Should be possible only with validation disabled
                 pcmk__config_err("Ignoring set %s colocation with resource %s "
                                  "in set %s: No such resource",
                                  pcmk__xe_id(set1), xml_rsc_id,
                                  pcmk__xe_id(set2));
                 continue;
             }
             pcmk__new_colocation(id, NULL, score, rsc_1, rsc_2, role_1,
                                  role_2, flags);
         }
 
     } else if (rsc_2 != NULL) { // Only set2 is sequential
         for (xml_rsc = pcmk__xe_first_child(set1, PCMK_XE_RESOURCE_REF, NULL,
                                             NULL);
              xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) {
 
             xml_rsc_id = pcmk__xe_id(xml_rsc);
             rsc_1 = pcmk__find_constraint_resource(scheduler->resources,
                                                    xml_rsc_id);
             if (rsc_1 == NULL) {
                 // Should be possible only with validation disabled
                 pcmk__config_err("Ignoring colocation of set %s resource %s "
                                  "with set %s: No such resource",
                                  pcmk__xe_id(set1), xml_rsc_id,
                                  pcmk__xe_id(set2));
                 continue;
             }
             flags = pcmk__coloc_explicit
                     | unpack_influence(id, rsc_1, influence_s);
             pcmk__new_colocation(id, NULL, score, rsc_1, rsc_2, role_1,
                                  role_2, flags);
         }
 
     } else { // Neither set is sequential
         for (xml_rsc = pcmk__xe_first_child(set1, PCMK_XE_RESOURCE_REF, NULL,
                                             NULL);
              xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) {
 
             xmlNode *xml_rsc_2 = NULL;
 
             xml_rsc_id = pcmk__xe_id(xml_rsc);
             rsc_1 = pcmk__find_constraint_resource(scheduler->resources,
                                                    xml_rsc_id);
             if (rsc_1 == NULL) {
                 // Should be possible only with validation disabled
                 pcmk__config_err("Ignoring colocation of set %s resource %s "
                                  "with set %s: No such resource",
                                  pcmk__xe_id(set1), xml_rsc_id,
                                  pcmk__xe_id(set2));
                 continue;
             }
 
             flags = pcmk__coloc_explicit
                     | unpack_influence(id, rsc_1, influence_s);
             for (xml_rsc_2 = pcmk__xe_first_child(set2, PCMK_XE_RESOURCE_REF,
                                                   NULL, NULL);
                  xml_rsc_2 != NULL; xml_rsc_2 = pcmk__xe_next_same(xml_rsc_2)) {
 
                 xml_rsc_id = pcmk__xe_id(xml_rsc_2);
                 rsc_2 = pcmk__find_constraint_resource(scheduler->resources,
                                                        xml_rsc_id);
                 if (rsc_2 == NULL) {
                     // Should be possible only with validation disabled
                     pcmk__config_err("Ignoring colocation of set %s resource "
                                      "%s with set %s resource %s: No such "
                                      "resource",
                                      pcmk__xe_id(set1), pcmk__xe_id(xml_rsc),
                                      pcmk__xe_id(set2), xml_rsc_id);
                     continue;
                 }
                 pcmk__new_colocation(id, NULL, score, rsc_1, rsc_2,
                                      role_1, role_2, flags);
             }
         }
     }
 }
 
 static void
 unpack_simple_colocation(xmlNode *xml_obj, const char *id,
                          const char *influence_s, pcmk_scheduler_t *scheduler)
 {
     int score_i = 0;
     uint32_t flags = pcmk__coloc_none;
 
     const char *score = crm_element_value(xml_obj, PCMK_XA_SCORE);
     const char *dependent_id = crm_element_value(xml_obj, PCMK_XA_RSC);
     const char *primary_id = crm_element_value(xml_obj, PCMK_XA_WITH_RSC);
     const char *dependent_role = crm_element_value(xml_obj, PCMK_XA_RSC_ROLE);
     const char *primary_role = crm_element_value(xml_obj,
                                                  PCMK_XA_WITH_RSC_ROLE);
     const char *attr = crm_element_value(xml_obj, PCMK_XA_NODE_ATTRIBUTE);
 
     const char *primary_instance = NULL;
     const char *dependent_instance = NULL;
     pcmk_resource_t *primary = NULL;
     pcmk_resource_t *dependent = NULL;
 
     primary = pcmk__find_constraint_resource(scheduler->resources, primary_id);
     dependent = pcmk__find_constraint_resource(scheduler->resources,
                                                dependent_id);
 
     // @COMPAT: Deprecated since 2.1.5
     primary_instance = crm_element_value(xml_obj, PCMK__XA_WITH_RSC_INSTANCE);
     dependent_instance = crm_element_value(xml_obj, PCMK__XA_RSC_INSTANCE);
     if (dependent_instance != NULL) {
         pcmk__warn_once(pcmk__wo_coloc_inst,
                         "Support for " PCMK__XA_RSC_INSTANCE " is deprecated "
                         "and will be removed in a future release");
     }
     if (primary_instance != NULL) {
         pcmk__warn_once(pcmk__wo_coloc_inst,
                         "Support for " PCMK__XA_WITH_RSC_INSTANCE " is "
                         "deprecated and will be removed in a future release");
     }
 
     if (dependent == NULL) {
         pcmk__config_err("Ignoring constraint '%s' because resource '%s' "
                          "does not exist", id, dependent_id);
         return;
 
     } else if (primary == NULL) {
         pcmk__config_err("Ignoring constraint '%s' because resource '%s' "
                          "does not exist", id, primary_id);
         return;
 
     } else if ((dependent_instance != NULL) && !pcmk__is_clone(dependent)) {
         pcmk__config_err("Ignoring constraint '%s' because resource '%s' "
                          "is not a clone but instance '%s' was requested",
                          id, dependent_id, dependent_instance);
         return;
 
     } else if ((primary_instance != NULL) && !pcmk__is_clone(primary)) {
         pcmk__config_err("Ignoring constraint '%s' because resource '%s' "
                          "is not a clone but instance '%s' was requested",
                          id, primary_id, primary_instance);
         return;
     }
 
     if (dependent_instance != NULL) {
         dependent = find_clone_instance(dependent, dependent_instance);
         if (dependent == NULL) {
             pcmk__config_warn("Ignoring constraint '%s' because resource '%s' "
                               "does not have an instance '%s'",
                               id, dependent_id, dependent_instance);
             return;
         }
     }
 
     if (primary_instance != NULL) {
         primary = find_clone_instance(primary, primary_instance);
         if (primary == NULL) {
             pcmk__config_warn("Ignoring constraint '%s' because resource '%s' "
                               "does not have an instance '%s'",
                               "'%s'", id, primary_id, primary_instance);
             return;
         }
     }
 
     if (pcmk__xe_attr_is_true(xml_obj, PCMK_XA_SYMMETRICAL)) {
         pcmk__config_warn("The colocation constraint "
                           "'" PCMK_XA_SYMMETRICAL "' attribute has been "
                           "removed");
     }
 
     if (score) {
         score_i = char2score(score);
     }
 
     flags = pcmk__coloc_explicit | unpack_influence(id, dependent, influence_s);
     pcmk__new_colocation(id, attr, score_i, dependent, primary,
                          dependent_role, primary_role, flags);
 }
 
 // \return Standard Pacemaker return code
 static int
 unpack_colocation_tags(xmlNode *xml_obj, xmlNode **expanded_xml,
                        pcmk_scheduler_t *scheduler)
 {
     const char *id = NULL;
     const char *dependent_id = NULL;
     const char *primary_id = NULL;
     const char *dependent_role = NULL;
     const char *primary_role = NULL;
 
     pcmk_resource_t *dependent = NULL;
     pcmk_resource_t *primary = NULL;
 
     pcmk_tag_t *dependent_tag = NULL;
     pcmk_tag_t *primary_tag = NULL;
 
     xmlNode *dependent_set = NULL;
     xmlNode *primary_set = NULL;
     bool any_sets = false;
 
     *expanded_xml = NULL;
 
     CRM_CHECK(xml_obj != NULL, return EINVAL);
 
     id = pcmk__xe_id(xml_obj);
     if (id == NULL) {
         pcmk__config_err("Ignoring <%s> constraint without " PCMK_XA_ID,
                          xml_obj->name);
         return pcmk_rc_unpack_error;
     }
 
     // Check whether there are any resource sets with template or tag references
     *expanded_xml = pcmk__expand_tags_in_sets(xml_obj, scheduler);
     if (*expanded_xml != NULL) {
         crm_log_xml_trace(*expanded_xml, "Expanded " PCMK_XE_RSC_COLOCATION);
         return pcmk_rc_ok;
     }
 
     dependent_id = crm_element_value(xml_obj, PCMK_XA_RSC);
     primary_id = crm_element_value(xml_obj, PCMK_XA_WITH_RSC);
     if ((dependent_id == NULL) || (primary_id == NULL)) {
         return pcmk_rc_ok;
     }
 
     if (!pcmk__valid_resource_or_tag(scheduler, dependent_id, &dependent,
                                      &dependent_tag)) {
         pcmk__config_err("Ignoring constraint '%s' because '%s' is not a "
                          "valid resource or tag", id, dependent_id);
         return pcmk_rc_unpack_error;
     }
 
     if (!pcmk__valid_resource_or_tag(scheduler, primary_id, &primary,
                                      &primary_tag)) {
         pcmk__config_err("Ignoring constraint '%s' because '%s' is not a "
                          "valid resource or tag", id, primary_id);
         return pcmk_rc_unpack_error;
     }
 
     if ((dependent != NULL) && (primary != NULL)) {
         /* Neither side references any template/tag. */
         return pcmk_rc_ok;
     }
 
     if ((dependent_tag != NULL) && (primary_tag != NULL)) {
         // A colocation constraint between two templates/tags makes no sense
         pcmk__config_err("Ignoring constraint '%s' because two templates or "
                          "tags cannot be colocated", id);
         return pcmk_rc_unpack_error;
     }
 
     dependent_role = crm_element_value(xml_obj, PCMK_XA_RSC_ROLE);
     primary_role = crm_element_value(xml_obj, PCMK_XA_WITH_RSC_ROLE);
 
     *expanded_xml = pcmk__xml_copy(NULL, xml_obj);
 
     /* Convert dependent's template/tag reference into constraint
      * PCMK_XE_RESOURCE_SET
      */
     if (!pcmk__tag_to_set(*expanded_xml, &dependent_set, PCMK_XA_RSC, true,
                           scheduler)) {
         pcmk__xml_free(*expanded_xml);
         *expanded_xml = NULL;
         return pcmk_rc_unpack_error;
     }
 
     if (dependent_set != NULL) {
         if (dependent_role != NULL) {
             /* Move PCMK_XA_RSC_ROLE into converted PCMK_XE_RESOURCE_SET as
              * PCMK_XA_ROLE
              */
             crm_xml_add(dependent_set, PCMK_XA_ROLE, dependent_role);
             pcmk__xe_remove_attr(*expanded_xml, PCMK_XA_RSC_ROLE);
         }
         any_sets = true;
     }
 
     /* Convert primary's template/tag reference into constraint
      * PCMK_XE_RESOURCE_SET
      */
     if (!pcmk__tag_to_set(*expanded_xml, &primary_set, PCMK_XA_WITH_RSC, true,
                           scheduler)) {
         pcmk__xml_free(*expanded_xml);
         *expanded_xml = NULL;
         return pcmk_rc_unpack_error;
     }
 
     if (primary_set != NULL) {
         if (primary_role != NULL) {
             /* Move PCMK_XA_WITH_RSC_ROLE into converted PCMK_XE_RESOURCE_SET as
              * PCMK_XA_ROLE
              */
             crm_xml_add(primary_set, PCMK_XA_ROLE, primary_role);
             pcmk__xe_remove_attr(*expanded_xml, PCMK_XA_WITH_RSC_ROLE);
         }
         any_sets = true;
     }
 
     if (any_sets) {
         crm_log_xml_trace(*expanded_xml, "Expanded " PCMK_XE_RSC_COLOCATION);
     } else {
         pcmk__xml_free(*expanded_xml);
         *expanded_xml = NULL;
     }
 
     return pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Parse a colocation constraint from XML into scheduler data
  *
  * \param[in,out] xml_obj    Colocation constraint XML to unpack
  * \param[in,out] scheduler  Scheduler data to add constraint to
  */
 void
 pcmk__unpack_colocation(xmlNode *xml_obj, pcmk_scheduler_t *scheduler)
 {
     int score_i = 0;
     xmlNode *set = NULL;
     xmlNode *last = NULL;
 
     xmlNode *orig_xml = NULL;
     xmlNode *expanded_xml = NULL;
 
     const char *id = crm_element_value(xml_obj, PCMK_XA_ID);
     const char *score = NULL;
     const char *influence_s = NULL;
 
     if (pcmk__str_empty(id)) {
         pcmk__config_err("Ignoring " PCMK_XE_RSC_COLOCATION
                          " without " CRM_ATTR_ID);
         return;
     }
 
     if (unpack_colocation_tags(xml_obj, &expanded_xml,
                                scheduler) != pcmk_rc_ok) {
         return;
     }
     if (expanded_xml != NULL) {
         orig_xml = xml_obj;
         xml_obj = expanded_xml;
     }
 
     score = crm_element_value(xml_obj, PCMK_XA_SCORE);
     if (score != NULL) {
         score_i = char2score(score);
     }
     influence_s = crm_element_value(xml_obj, PCMK_XA_INFLUENCE);
 
     for (set = pcmk__xe_first_child(xml_obj, PCMK_XE_RESOURCE_SET, NULL, NULL);
          set != NULL; set = pcmk__xe_next_same(set)) {
 
         set = pcmk__xe_resolve_idref(set, scheduler->input);
         if (set == NULL) { // Configuration error, message already logged
             if (expanded_xml != NULL) {
                 pcmk__xml_free(expanded_xml);
             }
             return;
         }
 
         if (pcmk__str_empty(pcmk__xe_id(set))) {
             pcmk__config_err("Ignoring " PCMK_XE_RESOURCE_SET
                              " without " CRM_ATTR_ID);
             continue;
         }
         unpack_colocation_set(set, score_i, id, influence_s, scheduler);
 
         if (last != NULL) {
             colocate_rsc_sets(id, last, set, score_i, influence_s, scheduler);
         }
         last = set;
     }
 
     if (expanded_xml) {
         pcmk__xml_free(expanded_xml);
         xml_obj = orig_xml;
     }
 
     if (last == NULL) {
         unpack_simple_colocation(xml_obj, id, influence_s, scheduler);
     }
 }
 
 /*!
  * \internal
  * \brief Make actions of a given type unrunnable for a given resource
  *
  * \param[in,out] rsc     Resource whose actions should be blocked
  * \param[in]     task    Name of action to block
  * \param[in]     reason  Unrunnable start action causing the block
  */
 static void
 mark_action_blocked(pcmk_resource_t *rsc, const char *task,
                     const pcmk_resource_t *reason)
 {
     GList *iter = NULL;
     char *reason_text = crm_strdup_printf("colocation with %s", reason->id);
 
     for (iter = rsc->actions; iter != NULL; iter = iter->next) {
         pcmk_action_t *action = iter->data;
 
         if (pcmk_is_set(action->flags, pcmk_action_runnable)
             && pcmk__str_eq(action->task, task, pcmk__str_none)) {
 
             pcmk__clear_action_flags(action, pcmk_action_runnable);
             pe_action_set_reason(action, reason_text, false);
             pcmk__block_colocation_dependents(action);
             pcmk__update_action_for_orderings(action, rsc->cluster);
         }
     }
 
     // If parent resource can't perform an action, neither can any children
     for (iter = rsc->children; iter != NULL; iter = iter->next) {
         mark_action_blocked((pcmk_resource_t *) (iter->data), task, reason);
     }
     free(reason_text);
 }
 
 /*!
  * \internal
  * \brief If an action is unrunnable, block any relevant dependent actions
  *
  * If a given action is an unrunnable start or promote, block the start or
  * promote actions of resources colocated with it, as appropriate to the
  * colocations' configured roles.
  *
  * \param[in,out] action  Action to check
  */
 void
 pcmk__block_colocation_dependents(pcmk_action_t *action)
 {
     GList *iter = NULL;
     GList *colocations = NULL;
     pcmk_resource_t *rsc = NULL;
     bool is_start = false;
 
     if (pcmk_is_set(action->flags, pcmk_action_runnable)) {
         return; // Only unrunnable actions block dependents
     }
 
     is_start = pcmk__str_eq(action->task, PCMK_ACTION_START, pcmk__str_none);
     if (!is_start
         && !pcmk__str_eq(action->task, PCMK_ACTION_PROMOTE, pcmk__str_none)) {
         return; // Only unrunnable starts and promotes block dependents
     }
 
     CRM_ASSERT(action->rsc != NULL); // Start and promote are resource actions
 
     /* If this resource is part of a collective resource, dependents are blocked
      * only if all instances of the collective are unrunnable, so check the
      * collective resource.
      */
     rsc = uber_parent(action->rsc);
     if (rsc->parent != NULL) {
         rsc = rsc->parent; // Bundle
     }
 
     // Colocation fails only if entire primary can't reach desired role
     for (iter = rsc->children; iter != NULL; iter = iter->next) {
         pcmk_resource_t *child = iter->data;
         pcmk_action_t *child_action = find_first_action(child->actions, NULL,
                                                         action->task, NULL);
 
         if ((child_action == NULL)
             || pcmk_is_set(child_action->flags, pcmk_action_runnable)) {
             crm_trace("Not blocking %s colocation dependents because "
                       "at least %s has runnable %s",
                       rsc->id, child->id, action->task);
             return; // At least one child can reach desired role
         }
     }
 
     crm_trace("Blocking %s colocation dependents due to unrunnable %s %s",
               rsc->id, action->rsc->id, action->task);
 
     // Check each colocation where this resource is primary
     colocations = pcmk__with_this_colocations(rsc);
     for (iter = colocations; iter != NULL; iter = iter->next) {
         pcmk__colocation_t *colocation = iter->data;
 
         if (colocation->score < PCMK_SCORE_INFINITY) {
             continue; // Only mandatory colocations block dependent
         }
 
         /* If the primary can't start, the dependent can't reach its colocated
          * role, regardless of what the primary or dependent colocation role is.
          *
          * If the primary can't be promoted, the dependent can't reach its
          * colocated role if the primary's colocation role is promoted.
          */
         if (!is_start && (colocation->primary_role != pcmk_role_promoted)) {
             continue;
         }
 
         // Block the dependent from reaching its colocated role
         if (colocation->dependent_role == pcmk_role_promoted) {
             mark_action_blocked(colocation->dependent, PCMK_ACTION_PROMOTE,
                                 action->rsc);
         } else {
             mark_action_blocked(colocation->dependent, PCMK_ACTION_START,
                                 action->rsc);
         }
     }
     g_list_free(colocations);
 }
 
 /*!
  * \internal
  * \brief Get the resource to use for role comparisons
  *
  * A bundle replica includes a container and possibly an instance of the bundled
  * resource. The dependent in a "with bundle" colocation is colocated with a
  * particular bundle container. However, if the colocation includes a role, then
  * the role must be checked on the bundled resource instance inside the
  * container. The container itself will never be promoted; the bundled resource
  * may be.
  *
  * If the given resource is a bundle replica container, return the resource
  * inside it, if any. Otherwise, return the resource itself.
  *
  * \param[in] rsc  Resource to check
  *
  * \return Resource to use for role comparisons
  */
 static const pcmk_resource_t *
 get_resource_for_role(const pcmk_resource_t *rsc)
 {
     if (pcmk_is_set(rsc->flags, pcmk_rsc_replica_container)) {
         const pcmk_resource_t *child = pe__get_rsc_in_container(rsc);
 
         if (child != NULL) {
             return child;
         }
     }
     return rsc;
 }
 
 /*!
  * \internal
  * \brief Determine how a colocation constraint should affect a resource
  *
  * Colocation constraints have different effects at different points in the
  * scheduler sequence. Initially, they affect a resource's location; once that
  * is determined, then for promotable clones they can affect a resource
  * instance's role; after both are determined, the constraints no longer matter.
  * Given a specific colocation constraint, check what has been done so far to
  * determine what should be affected at the current point in the scheduler.
  *
  * \param[in] dependent   Dependent resource in colocation
  * \param[in] primary     Primary resource in colocation
  * \param[in] colocation  Colocation constraint
  * \param[in] preview     If true, pretend resources have already been assigned
  *
  * \return How colocation constraint should be applied at this point
  */
 enum pcmk__coloc_affects
 pcmk__colocation_affects(const pcmk_resource_t *dependent,
                          const pcmk_resource_t *primary,
                          const pcmk__colocation_t *colocation, bool preview)
 {
     const pcmk_resource_t *dependent_role_rsc = NULL;
     const pcmk_resource_t *primary_role_rsc = NULL;
 
     CRM_ASSERT((dependent != NULL) && (primary != NULL)
                && (colocation != NULL));
 
     if (!preview && pcmk_is_set(primary->flags, pcmk_rsc_unassigned)) {
         // Primary resource has not been assigned yet, so we can't do anything
         return pcmk__coloc_affects_nothing;
     }
 
     dependent_role_rsc = get_resource_for_role(dependent);
     primary_role_rsc = get_resource_for_role(primary);
 
     if ((colocation->dependent_role >= pcmk_role_unpromoted)
         && (dependent_role_rsc->parent != NULL)
         && pcmk_is_set(dependent_role_rsc->parent->flags, pcmk_rsc_promotable)
         && !pcmk_is_set(dependent_role_rsc->flags, pcmk_rsc_unassigned)) {
 
         /* This is a colocation by role, and the dependent is a promotable clone
          * that has already been assigned, so the colocation should now affect
          * the role.
          */
         return pcmk__coloc_affects_role;
     }
 
     if (!preview && !pcmk_is_set(dependent->flags, pcmk_rsc_unassigned)) {
         /* The dependent resource has already been through assignment, so the
          * constraint no longer has any effect. Log an error if a mandatory
          * colocation constraint has been violated.
          */
 
         const pcmk_node_t *primary_node = primary->allocated_to;
 
         if (dependent->allocated_to == NULL) {
             crm_trace("Skipping colocation '%s': %s will not run anywhere",
                       colocation->id, dependent->id);
 
         } else if (colocation->score >= PCMK_SCORE_INFINITY) {
             // Dependent resource must colocate with primary resource
 
             if (!pcmk__same_node(primary_node, dependent->allocated_to)) {
                 pcmk__sched_err("%s must be colocated with %s but is not "
                                 "(%s vs. %s)",
                                 dependent->id, primary->id,
                                 pcmk__node_name(dependent->allocated_to),
                                 pcmk__node_name(primary_node));
             }
 
         } else if (colocation->score <= -PCMK_SCORE_INFINITY) {
             // Dependent resource must anti-colocate with primary resource
 
             if (pcmk__same_node(dependent->allocated_to, primary_node)) {
                 pcmk__sched_err("%s and %s must be anti-colocated but are "
                                 "assigned to the same node (%s)",
                                 dependent->id, primary->id,
                                 pcmk__node_name(primary_node));
             }
         }
         return pcmk__coloc_affects_nothing;
     }
 
     if ((colocation->dependent_role != pcmk_role_unknown)
         && (colocation->dependent_role != dependent_role_rsc->next_role)) {
         crm_trace("Skipping %scolocation '%s': dependent limited to %s role "
 
                   "but %s next role is %s",
                   ((colocation->score < 0)? "anti-" : ""),
                   colocation->id, pcmk_role_text(colocation->dependent_role),
                   dependent_role_rsc->id,
                   pcmk_role_text(dependent_role_rsc->next_role));
         return pcmk__coloc_affects_nothing;
     }
 
     if ((colocation->primary_role != pcmk_role_unknown)
         && (colocation->primary_role != primary_role_rsc->next_role)) {
         crm_trace("Skipping %scolocation '%s': primary limited to %s role "
                   "but %s next role is %s",
                   ((colocation->score < 0)? "anti-" : ""),
                   colocation->id, pcmk_role_text(colocation->primary_role),
                   primary_role_rsc->id,
                   pcmk_role_text(primary_role_rsc->next_role));
         return pcmk__coloc_affects_nothing;
     }
 
     return pcmk__coloc_affects_location;
 }
 
 /*!
  * \internal
  * \brief Apply colocation to dependent for assignment purposes
  *
  * Update the allowed node scores of the dependent resource in a colocation,
  * for the purposes of assigning it to a node.
  *
  * \param[in,out] dependent   Dependent resource in colocation
  * \param[in]     primary     Primary resource in colocation
  * \param[in]     colocation  Colocation constraint
  */
 void
 pcmk__apply_coloc_to_scores(pcmk_resource_t *dependent,
                             const pcmk_resource_t *primary,
                             const pcmk__colocation_t *colocation)
 {
     const char *attr = colocation->node_attribute;
     const char *value = NULL;
     GHashTable *work = NULL;
     GHashTableIter iter;
     pcmk_node_t *node = NULL;
 
     if (primary->allocated_to != NULL) {
         value = pcmk__colocation_node_attr(primary->allocated_to, attr,
                                            primary);
 
     } else if (colocation->score < 0) {
         // Nothing to do (anti-colocation with something that is not running)
         return;
     }
 
     work = pcmk__copy_node_table(dependent->allowed_nodes);
 
     g_hash_table_iter_init(&iter, work);
     while (g_hash_table_iter_next(&iter, NULL, (void **)&node)) {
         if (primary->allocated_to == NULL) {
             node->weight = pcmk__add_scores(-colocation->score, node->weight);
             pcmk__rsc_trace(dependent,
                             "Applied %s to %s score on %s (now %s after "
                             "subtracting %s because primary %s inactive)",
                             colocation->id, dependent->id,
                             pcmk__node_name(node),
                             pcmk_readable_score(node->weight),
                             pcmk_readable_score(colocation->score), primary->id);
             continue;
         }
 
         if (pcmk__str_eq(pcmk__colocation_node_attr(node, attr, dependent),
                          value, pcmk__str_casei)) {
 
             /* Add colocation score only if optional (or minus infinity). A
              * mandatory colocation is a requirement rather than a preference,
              * so we don't need to consider it for relative assignment purposes.
              * The resource will simply be forbidden from running on the node if
              * the primary isn't active there (via the condition above).
              */
             if (colocation->score < PCMK_SCORE_INFINITY) {
                 node->weight = pcmk__add_scores(colocation->score,
                                                 node->weight);
                 pcmk__rsc_trace(dependent,
                                 "Applied %s to %s score on %s (now %s after "
                                 "adding %s)",
                                 colocation->id, dependent->id,
                                 pcmk__node_name(node),
                                 pcmk_readable_score(node->weight),
                                 pcmk_readable_score(colocation->score));
             }
             continue;
         }
 
         if (colocation->score >= PCMK_SCORE_INFINITY) {
             /* Only mandatory colocations are relevant when the colocation
              * attribute doesn't match, because an attribute not matching is not
              * a negative preference -- the colocation is simply relevant only
              * where it matches.
              */
             node->weight = -PCMK_SCORE_INFINITY;
             pcmk__rsc_trace(dependent,
                             "Banned %s from %s because colocation %s attribute %s "
                             "does not match",
                             dependent->id, pcmk__node_name(node),
                             colocation->id, attr);
         }
     }
 
     if ((colocation->score <= -PCMK_SCORE_INFINITY)
         || (colocation->score >= PCMK_SCORE_INFINITY)
         || pcmk__any_node_available(work)) {
 
         g_hash_table_destroy(dependent->allowed_nodes);
         dependent->allowed_nodes = work;
         work = NULL;
 
     } else {
         pcmk__rsc_info(dependent,
                        "%s: Rolling back scores from %s (no available nodes)",
                        dependent->id, primary->id);
     }
 
     if (work != NULL) {
         g_hash_table_destroy(work);
     }
 }
 
 /*!
  * \internal
  * \brief Apply colocation to dependent for role purposes
  *
  * Update the priority of the dependent resource in a colocation, for the
  * purposes of selecting its role
  *
  * \param[in,out] dependent   Dependent resource in colocation
  * \param[in]     primary     Primary resource in colocation
  * \param[in]     colocation  Colocation constraint
  */
 void
 pcmk__apply_coloc_to_priority(pcmk_resource_t *dependent,
                               const pcmk_resource_t *primary,
                               const pcmk__colocation_t *colocation)
 {
     const char *dependent_value = NULL;
     const char *primary_value = NULL;
     const char *attr = colocation->node_attribute;
     int score_multiplier = 1;
 
     const pcmk_resource_t *primary_role_rsc = NULL;
 
     CRM_ASSERT((dependent != NULL) && (primary != NULL) &&
                (colocation != NULL));
 
     if ((primary->allocated_to == NULL) || (dependent->allocated_to == NULL)) {
         return;
     }
 
     dependent_value = pcmk__colocation_node_attr(dependent->allocated_to, attr,
                                                  dependent);
     primary_value = pcmk__colocation_node_attr(primary->allocated_to, attr,
                                                primary);
 
     primary_role_rsc = get_resource_for_role(primary);
 
     if (!pcmk__str_eq(dependent_value, primary_value, pcmk__str_casei)) {
         if ((colocation->score == PCMK_SCORE_INFINITY)
             && (colocation->dependent_role == pcmk_role_promoted)) {
             dependent->priority = -PCMK_SCORE_INFINITY;
         }
         return;
     }
 
     if ((colocation->primary_role != pcmk_role_unknown)
         && (colocation->primary_role != primary_role_rsc->next_role)) {
         return;
     }
 
     if (colocation->dependent_role == pcmk_role_unpromoted) {
         score_multiplier = -1;
     }
 
     dependent->priority = pcmk__add_scores(score_multiplier * colocation->score,
                                            dependent->priority);
     pcmk__rsc_trace(dependent,
                     "Applied %s to %s promotion priority (now %s after %s %s)",
                     colocation->id, dependent->id,
                     pcmk_readable_score(dependent->priority),
                     ((score_multiplier == 1)? "adding" : "subtracting"),
                     pcmk_readable_score(colocation->score));
 }
 
 /*!
  * \internal
  * \brief Find score of highest-scored node that matches colocation attribute
  *
  * \param[in] rsc    Resource whose allowed nodes should be searched
  * \param[in] attr   Colocation attribute name (must not be NULL)
  * \param[in] value  Colocation attribute value to require
  */
 static int
 best_node_score_matching_attr(const pcmk_resource_t *rsc, const char *attr,
                               const char *value)
 {
     GHashTableIter iter;
     pcmk_node_t *node = NULL;
     int best_score = -PCMK_SCORE_INFINITY;
     const char *best_node = NULL;
 
     // Find best allowed node with matching attribute
     g_hash_table_iter_init(&iter, rsc->allowed_nodes);
     while (g_hash_table_iter_next(&iter, NULL, (void **) &node)) {
 
         if ((node->weight > best_score)
             && pcmk__node_available(node, false, false)
             && pcmk__str_eq(value, pcmk__colocation_node_attr(node, attr, rsc),
                             pcmk__str_casei)) {
 
             best_score = node->weight;
             best_node = node->details->uname;
         }
     }
 
     if (!pcmk__str_eq(attr, CRM_ATTR_UNAME, pcmk__str_none)) {
         if (best_node == NULL) {
             crm_info("No allowed node for %s matches node attribute %s=%s",
                      rsc->id, attr, value);
         } else {
             crm_info("Allowed node %s for %s had best score (%d) "
                      "of those matching node attribute %s=%s",
                      best_node, rsc->id, best_score, attr, value);
         }
     }
     return best_score;
 }
 
 /*!
  * \internal
  * \brief Check whether a resource is allowed only on a single node
  *
  * \param[in] rsc   Resource to check
  *
  * \return \c true if \p rsc is allowed only on one node, otherwise \c false
  */
 static bool
 allowed_on_one(const pcmk_resource_t *rsc)
 {
     GHashTableIter iter;
     pcmk_node_t *allowed_node = NULL;
     int allowed_nodes = 0;
 
     g_hash_table_iter_init(&iter, rsc->allowed_nodes);
     while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &allowed_node)) {
         if ((allowed_node->weight >= 0) && (++allowed_nodes > 1)) {
             pcmk__rsc_trace(rsc, "%s is allowed on multiple nodes", rsc->id);
             return false;
         }
     }
     pcmk__rsc_trace(rsc, "%s is allowed %s", rsc->id,
                     ((allowed_nodes == 1)? "on a single node" : "nowhere"));
     return (allowed_nodes == 1);
 }
 
 /*!
  * \internal
  * \brief Add resource's colocation matches to current node assignment scores
  *
  * For each node in a given table, if any of a given resource's allowed nodes
  * have a matching value for the colocation attribute, add the highest of those
  * nodes' scores to the node's score.
  *
  * \param[in,out] nodes          Table of nodes with assignment scores so far
  * \param[in]     source_rsc     Resource whose node scores to add
  * \param[in]     target_rsc     Resource on whose behalf to update \p nodes
  * \param[in]     colocation     Original colocation constraint (used to get
  *                               configured primary resource's stickiness, and
  *                               to get colocation node attribute; pass NULL to
  *                               ignore stickiness and use default attribute)
  * \param[in]     factor         Factor by which to multiply scores being added
  * \param[in]     only_positive  Whether to add only positive scores
  */
 static void
 add_node_scores_matching_attr(GHashTable *nodes,
                               const pcmk_resource_t *source_rsc,
                               const pcmk_resource_t *target_rsc,
                               const pcmk__colocation_t *colocation,
                               float factor, bool only_positive)
 {
     GHashTableIter iter;
     pcmk_node_t *node = NULL;
     const char *attr = colocation->node_attribute;
 
     // Iterate through each node
     g_hash_table_iter_init(&iter, nodes);
     while (g_hash_table_iter_next(&iter, NULL, (void **)&node)) {
         float delta_f = 0;
         int delta = 0;
         int score = 0;
         int new_score = 0;
         const char *value = pcmk__colocation_node_attr(node, attr, target_rsc);
 
         score = best_node_score_matching_attr(source_rsc, attr, value);
 
         if ((factor < 0) && (score < 0)) {
             /* If the dependent is anti-colocated, we generally don't want the
              * primary to prefer nodes that the dependent avoids. That could
              * lead to unnecessary shuffling of the primary when the dependent
              * hits its migration threshold somewhere, for example.
              *
              * However, there are cases when it is desirable. If the dependent
              * can't run anywhere but where the primary is, it would be
              * worthwhile to move the primary for the sake of keeping the
              * dependent active.
              *
              * We can't know that exactly at this point since we don't know
              * where the primary will be assigned, but we can limit considering
              * the preference to when the dependent is allowed only on one node.
              * This is less than ideal for multiple reasons:
              *
              * - the dependent could be allowed on more than one node but have
              *   anti-colocation primaries on each;
              * - the dependent could be a clone or bundle with multiple
              *   instances, and the dependent as a whole is allowed on multiple
              *   nodes but some instance still can't run
              * - the dependent has considered node-specific criteria such as
              *   location constraints and stickiness by this point, but might
              *   have other factors that end up disallowing a node
              *
              * but the alternative is making the primary move when it doesn't
              * need to.
              *
              * We also consider the primary's stickiness and influence, so the
              * user has some say in the matter. (This is the configured primary,
              * not a particular instance of the primary, but that doesn't matter
              * unless stickiness uses a rule to vary by node, and that seems
              * acceptable to ignore.)
              */
             if ((colocation->primary->stickiness >= -score)
                 || !pcmk__colocation_has_influence(colocation, NULL)
                 || !allowed_on_one(colocation->dependent)) {
                 crm_trace("%s: Filtering %d + %f * %d "
                           "(double negative disallowed)",
                           pcmk__node_name(node), node->weight, factor, score);
                 continue;
             }
         }
 
         if (node->weight == INFINITY_HACK) {
             crm_trace("%s: Filtering %d + %f * %d (node was marked unusable)",
                       pcmk__node_name(node), node->weight, factor, score);
             continue;
         }
 
         delta_f = factor * score;
 
         // Round the number; see http://c-faq.com/fp/round.html
         delta = (int) ((delta_f < 0)? (delta_f - 0.5) : (delta_f + 0.5));
 
         /* Small factors can obliterate the small scores that are often actually
          * used in configurations. If the score and factor are nonzero, ensure
          * that the result is nonzero as well.
          */
         if ((delta == 0) && (score != 0)) {
             if (factor > 0.0) {
                 delta = 1;
             } else if (factor < 0.0) {
                 delta = -1;
             }
         }
 
         new_score = pcmk__add_scores(delta, node->weight);
 
         if (only_positive && (new_score < 0) && (node->weight > 0)) {
             crm_trace("%s: Filtering %d + %f * %d = %d "
                       "(negative disallowed, marking node unusable)",
                       pcmk__node_name(node), node->weight, factor, score,
                       new_score);
             node->weight = INFINITY_HACK;
             continue;
         }
 
         if (only_positive && (new_score < 0) && (node->weight == 0)) {
             crm_trace("%s: Filtering %d + %f * %d = %d (negative disallowed)",
                       pcmk__node_name(node), node->weight, factor, score,
                       new_score);
             continue;
         }
 
         crm_trace("%s: %d + %f * %d = %d", pcmk__node_name(node),
                   node->weight, factor, score, new_score);
         node->weight = new_score;
     }
 }
 
 /*!
  * \internal
  * \brief Update nodes with scores of colocated resources' nodes
  *
  * Given a table of nodes and a resource, update the nodes' scores with the
  * scores of the best nodes matching the attribute used for each of the
  * resource's relevant colocations.
  *
  * \param[in,out] source_rsc  Resource whose node scores to add
  * \param[in]     target_rsc  Resource on whose behalf to update \p *nodes
  * \param[in]     log_id      Resource ID for logs (if \c NULL, use
  *                            \p source_rsc ID)
  * \param[in,out] nodes       Nodes to update (set initial contents to \c NULL
  *                            to copy allowed nodes from \p source_rsc)
  * \param[in]     colocation  Original colocation constraint (used to get
  *                            configured primary resource's stickiness, and
  *                            to get colocation node attribute; if \c NULL,
  *                            <tt>source_rsc</tt>'s own matching node scores
  *                            will not be added, and \p *nodes must be \c NULL
  *                            as well)
  * \param[in]     factor      Incorporate scores multiplied by this factor
  * \param[in]     flags       Bitmask of enum pcmk__coloc_select values
  *
  * \note \c NULL \p target_rsc, \c NULL \p *nodes, \c NULL \p colocation, and
  *       the \c pcmk__coloc_select_this_with flag are used together (and only by
  *       \c cmp_resources()).
  * \note The caller remains responsible for freeing \p *nodes.
  * \note This is the shared implementation of
  *       \c pcmk_assignment_methods_t:add_colocated_node_scores().
  */
 void
 pcmk__add_colocated_node_scores(pcmk_resource_t *source_rsc,
                                 const pcmk_resource_t *target_rsc,
                                 const char *log_id,
                                 GHashTable **nodes,
                                 const pcmk__colocation_t *colocation,
                                 float factor, uint32_t flags)
 {
     GHashTable *work = NULL;
 
     CRM_ASSERT((source_rsc != NULL) && (nodes != NULL)
                && ((colocation != NULL)
                    || ((target_rsc == NULL) && (*nodes == NULL))));
 
     if (log_id == NULL) {
         log_id = source_rsc->id;
     }
 
     // Avoid infinite recursion
     if (pcmk_is_set(source_rsc->flags, pcmk_rsc_updating_nodes)) {
         pcmk__rsc_info(source_rsc, "%s: Breaking dependency loop at %s",
                        log_id, source_rsc->id);
         return;
     }
     pcmk__set_rsc_flags(source_rsc, pcmk_rsc_updating_nodes);
 
     if (*nodes == NULL) {
         work = pcmk__copy_node_table(source_rsc->allowed_nodes);
         target_rsc = source_rsc;
     } else {
         const bool pos = pcmk_is_set(flags, pcmk__coloc_select_nonnegative);
 
         pcmk__rsc_trace(source_rsc, "%s: Merging %s scores from %s (at %.6f)",
                         log_id, (pos? "positive" : "all"), source_rsc->id, factor);
         work = pcmk__copy_node_table(*nodes);
         add_node_scores_matching_attr(work, source_rsc, target_rsc, colocation,
                                       factor, pos);
     }
 
     if (work == NULL) {
         pcmk__clear_rsc_flags(source_rsc, pcmk_rsc_updating_nodes);
         return;
     }
 
     if (pcmk__any_node_available(work)) {
         GList *colocations = NULL;
 
         if (pcmk_is_set(flags, pcmk__coloc_select_this_with)) {
             colocations = pcmk__this_with_colocations(source_rsc);
             pcmk__rsc_trace(source_rsc,
                             "Checking additional %d optional '%s with' "
                             "constraints",
                             g_list_length(colocations), source_rsc->id);
         } else {
             colocations = pcmk__with_this_colocations(source_rsc);
             pcmk__rsc_trace(source_rsc,
                             "Checking additional %d optional 'with %s' "
                             "constraints",
                             g_list_length(colocations), source_rsc->id);
         }
         flags |= pcmk__coloc_select_active;
 
         for (GList *iter = colocations; iter != NULL; iter = iter->next) {
             pcmk__colocation_t *constraint = iter->data;
 
             pcmk_resource_t *other = NULL;
             float other_factor = factor * constraint->score
                                  / (float) PCMK_SCORE_INFINITY;
 
             if (pcmk_is_set(flags, pcmk__coloc_select_this_with)) {
                 other = constraint->primary;
             } else if (!pcmk__colocation_has_influence(constraint, NULL)) {
                 continue;
             } else {
                 other = constraint->dependent;
             }
 
             pcmk__rsc_trace(source_rsc,
                             "Optionally merging score of '%s' constraint "
                             "(%s with %s)",
                             constraint->id, constraint->dependent->id,
                             constraint->primary->id);
-            other->cmds->add_colocated_node_scores(other, target_rsc, log_id,
-                                                   &work, constraint,
-                                                   other_factor, flags);
+            other->private->cmds->add_colocated_node_scores(other, target_rsc,
+                                                            log_id, &work,
+                                                            constraint,
+                                                            other_factor,
+                                                            flags);
             pe__show_node_scores(true, NULL, log_id, work, source_rsc->cluster);
         }
         g_list_free(colocations);
 
     } else if (pcmk_is_set(flags, pcmk__coloc_select_active)) {
         pcmk__rsc_info(source_rsc, "%s: Rolling back optional scores from %s",
                        log_id, source_rsc->id);
         g_hash_table_destroy(work);
         pcmk__clear_rsc_flags(source_rsc, pcmk_rsc_updating_nodes);
         return;
     }
 
 
     if (pcmk_is_set(flags, pcmk__coloc_select_nonnegative)) {
         pcmk_node_t *node = NULL;
         GHashTableIter iter;
 
         g_hash_table_iter_init(&iter, work);
         while (g_hash_table_iter_next(&iter, NULL, (void **)&node)) {
             if (node->weight == INFINITY_HACK) {
                 node->weight = 1;
             }
         }
     }
 
     if (*nodes != NULL) {
        g_hash_table_destroy(*nodes);
     }
     *nodes = work;
 
     pcmk__clear_rsc_flags(source_rsc, pcmk_rsc_updating_nodes);
 }
 
 /*!
  * \internal
  * \brief Apply a "with this" colocation to a resource's allowed node scores
  *
  * \param[in,out] data       Colocation to apply
  * \param[in,out] user_data  Resource being assigned
  */
 void
 pcmk__add_dependent_scores(gpointer data, gpointer user_data)
 {
     pcmk__colocation_t *colocation = data;
-    pcmk_resource_t *target_rsc = user_data;
+    pcmk_resource_t *primary = user_data;
 
-    pcmk_resource_t *source_rsc = colocation->dependent;
+    pcmk_resource_t *dependent = colocation->dependent;
     const float factor = colocation->score / (float) PCMK_SCORE_INFINITY;
     uint32_t flags = pcmk__coloc_select_active;
 
     if (!pcmk__colocation_has_influence(colocation, NULL)) {
         return;
     }
-    if (pcmk__is_clone(target_rsc)) {
+    if (pcmk__is_clone(primary)) {
         flags |= pcmk__coloc_select_nonnegative;
     }
-    pcmk__rsc_trace(target_rsc,
+    pcmk__rsc_trace(primary,
                     "%s: Incorporating attenuated %s assignment scores due "
                     "to colocation %s",
-                    target_rsc->id, source_rsc->id, colocation->id);
-    source_rsc->cmds->add_colocated_node_scores(source_rsc, target_rsc,
-                                                source_rsc->id,
-                                                &target_rsc->allowed_nodes,
-                                                colocation, factor, flags);
+                    primary->id, dependent->id, colocation->id);
+    dependent->private->cmds->add_colocated_node_scores(dependent, primary,
+                                                        dependent->id,
+                                                        &primary->allowed_nodes,
+                                                        colocation, factor,
+                                                        flags);
 }
 
 /*!
  * \internal
  * \brief Exclude nodes from a dependent's node table if not in a given list
  *
  * Given a dependent resource in a colocation and a list of nodes where the
  * primary resource will run, set a node's score to \c -INFINITY in the
  * dependent's node table if not found in the primary nodes list.
  *
  * \param[in,out] dependent      Dependent resource
  * \param[in]     primary        Primary resource (for logging only)
  * \param[in]     colocation     Colocation constraint (for logging only)
  * \param[in]     primary_nodes  List of nodes where the primary will have
  *                               unblocked instances in a suitable role
  * \param[in]     merge_scores   If \c true and a node is found in both \p table
  *                               and \p list, add the node's score in \p list to
  *                               the node's score in \p table
  */
 void
 pcmk__colocation_intersect_nodes(pcmk_resource_t *dependent,
                                  const pcmk_resource_t *primary,
                                  const pcmk__colocation_t *colocation,
                                  const GList *primary_nodes, bool merge_scores)
 {
     GHashTableIter iter;
     pcmk_node_t *dependent_node = NULL;
 
     CRM_ASSERT((dependent != NULL) && (primary != NULL)
                && (colocation != NULL));
 
     g_hash_table_iter_init(&iter, dependent->allowed_nodes);
     while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &dependent_node)) {
         const pcmk_node_t *primary_node = NULL;
 
         primary_node = pe_find_node_id(primary_nodes,
                                        dependent_node->details->id);
         if (primary_node == NULL) {
             dependent_node->weight = -PCMK_SCORE_INFINITY;
             pcmk__rsc_trace(dependent,
                             "Banning %s from %s (no primary instance) for %s",
                             dependent->id, pcmk__node_name(dependent_node),
                             colocation->id);
 
         } else if (merge_scores) {
             dependent_node->weight = pcmk__add_scores(dependent_node->weight,
                                                       primary_node->weight);
             pcmk__rsc_trace(dependent,
                             "Added %s's score %s to %s's score for %s (now %s) "
                             "for colocation %s",
                             primary->id, pcmk_readable_score(primary_node->weight),
                             dependent->id, pcmk__node_name(dependent_node),
                             pcmk_readable_score(dependent_node->weight),
                             colocation->id);
         }
     }
 }
 
 /*!
  * \internal
  * \brief Get all colocations affecting a resource as the primary
  *
  * \param[in] rsc  Resource to get colocations for
  *
  * \return Newly allocated list of colocations affecting \p rsc as primary
  *
  * \note This is a convenience wrapper for the with_this_colocations() method.
  */
 GList *
 pcmk__with_this_colocations(const pcmk_resource_t *rsc)
 {
     GList *list = NULL;
 
-    rsc->cmds->with_this_colocations(rsc, rsc, &list);
+    rsc->private->cmds->with_this_colocations(rsc, rsc, &list);
     return list;
 }
 
 /*!
  * \internal
  * \brief Get all colocations affecting a resource as the dependent
  *
  * \param[in] rsc  Resource to get colocations for
  *
  * \return Newly allocated list of colocations affecting \p rsc as dependent
  *
  * \note This is a convenience wrapper for the this_with_colocations() method.
  */
 GList *
 pcmk__this_with_colocations(const pcmk_resource_t *rsc)
 {
     GList *list = NULL;
 
-    rsc->cmds->this_with_colocations(rsc, rsc, &list);
+    rsc->private->cmds->this_with_colocations(rsc, rsc, &list);
     return list;
 }
diff --git a/lib/pacemaker/pcmk_sched_constraints.c b/lib/pacemaker/pcmk_sched_constraints.c
index e9ddbcd281..82885e5729 100644
--- a/lib/pacemaker/pcmk_sched_constraints.c
+++ b/lib/pacemaker/pcmk_sched_constraints.c
@@ -1,436 +1,436 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <sys/param.h>
 #include <sys/types.h>
 #include <stdbool.h>
 #include <regex.h>
 #include <glib.h>
 
 #include <crm/crm.h>
 #include <crm/cib.h>
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>
 #include <crm/common/iso8601.h>
 #include <crm/pengine/status.h>
 #include <crm/pengine/internal.h>
 #include <crm/pengine/rules.h>
 #include <pacemaker-internal.h>
 #include "libpacemaker_private.h"
 
 static bool
 evaluate_lifetime(xmlNode *lifetime, pcmk_scheduler_t *scheduler)
 {
     bool result = false;
     crm_time_t *next_change = crm_time_new_undefined();
     pcmk_rule_input_t rule_input = {
         .now = scheduler->now,
     };
 
     result = (pcmk__evaluate_rules(lifetime, &rule_input,
                                    next_change) == pcmk_rc_ok);
 
     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, scheduler, "constraint lifetime");
     }
     crm_time_free(next_change);
     return result;
 }
 
 /*!
  * \internal
  * \brief Unpack constraints from XML
  *
  * Given scheduler data, unpack all constraints from its input XML into
  * data structures.
  *
  * \param[in,out] scheduler  Scheduler data
  */
 void
 pcmk__unpack_constraints(pcmk_scheduler_t *scheduler)
 {
     xmlNode *xml_constraints = pcmk_find_cib_element(scheduler->input,
                                                      PCMK_XE_CONSTRAINTS);
 
     for (xmlNode *xml_obj = pcmk__xe_first_child(xml_constraints, NULL, NULL,
                                                  NULL);
          xml_obj != NULL; xml_obj = pcmk__xe_next(xml_obj)) {
 
         xmlNode *lifetime = NULL;
         const char *id = crm_element_value(xml_obj, PCMK_XA_ID);
         const char *tag = (const char *) xml_obj->name;
 
         if (id == NULL) {
             pcmk__config_err("Ignoring <%s> constraint without "
                              PCMK_XA_ID, tag);
             continue;
         }
 
         crm_trace("Unpacking %s constraint '%s'", tag, id);
 
         lifetime = pcmk__xe_first_child(xml_obj, PCMK__XE_LIFETIME, NULL, NULL);
         if (lifetime != NULL) {
             pcmk__config_warn("Support for '" PCMK__XE_LIFETIME "' element "
                               "(in %s) is deprecated and will be dropped "
                               "in a later release", id);
         }
 
         if ((lifetime != NULL) && !evaluate_lifetime(lifetime, scheduler)) {
             crm_info("Constraint %s %s is not active", tag, id);
 
         } else if (pcmk__str_eq(PCMK_XE_RSC_ORDER, tag, pcmk__str_none)) {
             pcmk__unpack_ordering(xml_obj, scheduler);
 
         } else if (pcmk__str_eq(PCMK_XE_RSC_COLOCATION, tag, pcmk__str_none)) {
             pcmk__unpack_colocation(xml_obj, scheduler);
 
         } else if (pcmk__str_eq(PCMK_XE_RSC_LOCATION, tag, pcmk__str_none)) {
             pcmk__unpack_location(xml_obj, scheduler);
 
         } else if (pcmk__str_eq(PCMK_XE_RSC_TICKET, tag, pcmk__str_none)) {
             pcmk__unpack_rsc_ticket(xml_obj, scheduler);
 
         } else {
             pcmk__config_err("Unsupported constraint type: %s", tag);
         }
     }
 }
 
 pcmk_resource_t *
 pcmk__find_constraint_resource(GList *rsc_list, const char *id)
 {
     if (id == NULL) {
         return NULL;
     }
     for (GList *iter = rsc_list; iter != NULL; iter = iter->next) {
         pcmk_resource_t *parent = iter->data;
         pcmk_resource_t *match = NULL;
 
         match = parent->private->fns->find_rsc(parent, id, NULL,
                                                pcmk_rsc_match_history);
         if (match != NULL) {
             if (!pcmk__str_eq(match->id, id, pcmk__str_none)) {
                 /* We found an instance of a clone instead */
                 match = uber_parent(match);
                 crm_debug("Found %s for %s", match->id, id);
             }
             return match;
         }
     }
     crm_trace("No match for %s", id);
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Check whether an ID references a resource tag
  *
  * \param[in]  scheduler  Scheduler data
  * \param[in]  id         Tag ID to search for
  * \param[out] tag        Where to store tag, if found
  *
  * \return true if ID refers to a tagged resource or resource set template,
  *         otherwise false
  */
 static bool
 find_constraint_tag(const pcmk_scheduler_t *scheduler, const char *id,
                     pcmk_tag_t **tag)
 {
     *tag = NULL;
 
     // Check whether id refers to a resource set template
     if (g_hash_table_lookup_extended(scheduler->template_rsc_sets, id,
                                      NULL, (gpointer *) tag)) {
         if (*tag == NULL) {
             crm_notice("No resource is derived from template '%s'", id);
             return false;
         }
         return true;
     }
 
     // If not, check whether id refers to a tag
     if (g_hash_table_lookup_extended(scheduler->tags, id,
                                      NULL, (gpointer *) tag)) {
         if (*tag == NULL) {
             crm_notice("No resource is tagged with '%s'", id);
             return false;
         }
         return true;
     }
 
     pcmk__config_warn("No resource, template, or tag named '%s'", id);
     return false;
 }
 
 /*!
  * \brief
  * \internal Check whether an ID refers to a valid resource or tag
  *
  * \param[in]  scheduler  Scheduler data
  * \param[in]  id         ID to search for
  * \param[out] rsc        Where to store resource, if found
  *                        (or NULL to skip searching resources)
  * \param[out] tag        Where to store tag, if found
  *                        (or NULL to skip searching tags)
  *
  * \return true if id refers to a resource (possibly indirectly via a tag)
  */
 bool
 pcmk__valid_resource_or_tag(const pcmk_scheduler_t *scheduler, const char *id,
                             pcmk_resource_t **rsc, pcmk_tag_t **tag)
 {
     if (rsc != NULL) {
         *rsc = pcmk__find_constraint_resource(scheduler->resources, id);
         if (*rsc != NULL) {
             return true;
         }
     }
 
     if ((tag != NULL) && find_constraint_tag(scheduler, id, tag)) {
         return true;
     }
 
     return false;
 }
 
 /*!
  * \internal
  * \brief Replace any resource tags with equivalent \C PCMK_XE_RESOURCE_REF
  *        entries
  *
  * If a given constraint has resource sets, check each set for
  * \c PCMK_XE_RESOURCE_REF entries that list tags rather than resource IDs, and
  * replace any found with \c PCMK_XE_RESOURCE_REF entries for the corresponding
  * resource IDs.
  *
  * \param[in,out] xml_obj    Constraint XML
  * \param[in]     scheduler  Scheduler data
  *
  * \return Equivalent XML with resource tags replaced (or NULL if none)
  * \note It is the caller's responsibility to free the return value with
  *       \c pcmk__xml_free().
  */
 xmlNode *
 pcmk__expand_tags_in_sets(xmlNode *xml_obj, const pcmk_scheduler_t *scheduler)
 {
     xmlNode *new_xml = NULL;
     bool any_refs = false;
 
     // Short-circuit if there are no sets
     if (pcmk__xe_first_child(xml_obj, PCMK_XE_RESOURCE_SET, NULL,
                              NULL) == NULL) {
         return NULL;
     }
 
     new_xml = pcmk__xml_copy(NULL, xml_obj);
 
     for (xmlNode *set = pcmk__xe_first_child(new_xml, PCMK_XE_RESOURCE_SET,
                                              NULL, NULL);
          set != NULL; set = pcmk__xe_next_same(set)) {
 
         GList *tag_refs = NULL;
         GList *iter = NULL;
 
         for (xmlNode *xml_rsc = pcmk__xe_first_child(set, PCMK_XE_RESOURCE_REF,
                                                      NULL, NULL);
              xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) {
 
             pcmk_resource_t *rsc = NULL;
             pcmk_tag_t *tag = NULL;
 
             if (!pcmk__valid_resource_or_tag(scheduler, pcmk__xe_id(xml_rsc),
                                              &rsc, &tag)) {
                 pcmk__config_err("Ignoring resource sets for constraint '%s' "
                                  "because '%s' is not a valid resource or tag",
                                  pcmk__xe_id(xml_obj), pcmk__xe_id(xml_rsc));
                 pcmk__xml_free(new_xml);
                 return NULL;
 
             } else if (rsc) {
                 continue;
 
             } else if (tag) {
                 /* PCMK_XE_RESOURCE_REF under PCMK_XE_RESOURCE_SET references
                  * template or tag
                  */
                 xmlNode *last_ref = xml_rsc;
 
                 /* For example, given the original XML:
                  *
                  *   <resource_set id="tag1-colocation-0" sequential="true">
                  *     <resource_ref id="rsc1"/>
                  *     <resource_ref id="tag1"/>
                  *     <resource_ref id="rsc4"/>
                  *   </resource_set>
                  *
                  * If rsc2 and rsc3 are tagged with tag1, we add them after it:
                  *
                  *   <resource_set id="tag1-colocation-0" sequential="true">
                  *     <resource_ref id="rsc1"/>
                  *     <resource_ref id="tag1"/>
                  *     <resource_ref id="rsc2"/>
                  *     <resource_ref id="rsc3"/>
                  *     <resource_ref id="rsc4"/>
                  *   </resource_set>
                  */
 
                 for (iter = tag->refs; iter != NULL; iter = iter->next) {
                     const char *obj_ref = iter->data;
                     xmlNode *new_rsc_ref = NULL;
 
                     new_rsc_ref = xmlNewDocRawNode(set->doc, NULL,
                                                    (pcmkXmlStr)
                                                    PCMK_XE_RESOURCE_REF,
                                                    NULL);
                     crm_xml_add(new_rsc_ref, PCMK_XA_ID, obj_ref);
                     xmlAddNextSibling(last_ref, new_rsc_ref);
 
                     last_ref = new_rsc_ref;
                 }
 
                 any_refs = true;
 
                 /* Freeing the resource_ref now would break the XML child
                  * iteration, so just remember it for freeing later.
                  */
                 tag_refs = g_list_append(tag_refs, xml_rsc);
             }
         }
 
         /* Now free '<resource_ref id="tag1"/>', and finally get:
 
            <resource_set id="tag1-colocation-0" sequential="true">
              <resource_ref id="rsc1"/>
              <resource_ref id="rsc2"/>
              <resource_ref id="rsc3"/>
              <resource_ref id="rsc4"/>
            </resource_set>
 
          */
         for (iter = tag_refs; iter != NULL; iter = iter->next) {
             xmlNode *tag_ref = iter->data;
 
             pcmk__xml_free(tag_ref);
         }
         g_list_free(tag_refs);
     }
 
     if (!any_refs) {
         pcmk__xml_free(new_xml);
         new_xml = NULL;
     }
     return new_xml;
 }
 
 /*!
  * \internal
  * \brief Convert a tag into a resource set of tagged resources
  *
  * \param[in,out] xml_obj      Constraint XML
  * \param[out]    rsc_set      Where to store resource set XML
  * \param[in]     attr         Name of XML attribute with resource or tag ID
  * \param[in]     convert_rsc  If true, convert to set even if \p attr
  *                             references a resource
  * \param[in]     scheduler    Scheduler data
  */
 bool
 pcmk__tag_to_set(xmlNode *xml_obj, xmlNode **rsc_set, const char *attr,
                  bool convert_rsc, const pcmk_scheduler_t *scheduler)
 {
     const char *cons_id = NULL;
     const char *id = NULL;
 
     pcmk_resource_t *rsc = NULL;
     pcmk_tag_t *tag = NULL;
 
     *rsc_set = NULL;
 
     CRM_CHECK((xml_obj != NULL) && (attr != NULL), return false);
 
     cons_id = pcmk__xe_id(xml_obj);
     if (cons_id == NULL) {
         pcmk__config_err("Ignoring <%s> constraint without " PCMK_XA_ID,
                          xml_obj->name);
         return false;
     }
 
     id = crm_element_value(xml_obj, attr);
     if (id == NULL) {
         return true;
     }
 
     if (!pcmk__valid_resource_or_tag(scheduler, id, &rsc, &tag)) {
         pcmk__config_err("Ignoring constraint '%s' because '%s' is not a "
                          "valid resource or tag", cons_id, id);
         return false;
 
     } else if (tag) {
         /* The "attr" attribute (for a resource in a constraint) specifies a
          * template or tag. Add the corresponding PCMK_XE_RESOURCE_SET
          * containing the resources derived from or tagged with it.
          */
         *rsc_set = pcmk__xe_create(xml_obj, PCMK_XE_RESOURCE_SET);
         crm_xml_add(*rsc_set, PCMK_XA_ID, id);
 
         for (GList *iter = tag->refs; iter != NULL; iter = iter->next) {
             const char *obj_ref = iter->data;
             xmlNode *rsc_ref = NULL;
 
             rsc_ref = pcmk__xe_create(*rsc_set, PCMK_XE_RESOURCE_REF);
             crm_xml_add(rsc_ref, PCMK_XA_ID, obj_ref);
         }
 
         // Set PCMK_XA_SEQUENTIAL=PCMK_VALUE_FALSE for the PCMK_XE_RESOURCE_SET
         pcmk__xe_set_bool_attr(*rsc_set, PCMK_XA_SEQUENTIAL, false);
 
     } else if ((rsc != NULL) && convert_rsc) {
         /* Even if a regular resource is referenced by "attr", convert it into a
          * PCMK_XE_RESOURCE_SET, because the other resource reference in the
          * constraint could be a template or tag.
          */
         xmlNode *rsc_ref = NULL;
 
         *rsc_set = pcmk__xe_create(xml_obj, PCMK_XE_RESOURCE_SET);
         crm_xml_add(*rsc_set, PCMK_XA_ID, id);
 
         rsc_ref = pcmk__xe_create(*rsc_set, PCMK_XE_RESOURCE_REF);
         crm_xml_add(rsc_ref, PCMK_XA_ID, id);
 
     } else {
         return true;
     }
 
     /* Remove the "attr" attribute referencing the template/tag */
     if (*rsc_set != NULL) {
         pcmk__xe_remove_attr(xml_obj, attr);
     }
 
     return true;
 }
 
 /*!
  * \internal
  * \brief Create constraints inherent to resource types
  *
  * \param[in,out] scheduler  Scheduler data
  */
 void
 pcmk__create_internal_constraints(pcmk_scheduler_t *scheduler)
 {
     crm_trace("Create internal constraints");
     for (GList *iter = scheduler->resources; iter != NULL; iter = iter->next) {
         pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data;
 
-        rsc->cmds->internal_constraints(rsc);
+        rsc->private->cmds->internal_constraints(rsc);
     }
 }
diff --git a/lib/pacemaker/pcmk_sched_group.c b/lib/pacemaker/pcmk_sched_group.c
index 74b5efaaa9..b85ffb1ab1 100644
--- a/lib/pacemaker/pcmk_sched_group.c
+++ b/lib/pacemaker/pcmk_sched_group.c
@@ -1,947 +1,959 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <stdbool.h>
 
 #include <crm/common/xml.h>
 
 #include <pacemaker-internal.h>
 #include "libpacemaker_private.h"
 
 /*!
  * \internal
  * \brief Assign a group resource to a node
  *
  * \param[in,out] rsc           Group resource to assign to a node
  * \param[in]     prefer        Node to prefer, if all else is equal
  * \param[in]     stop_if_fail  If \c true and a child of \p rsc can't be
  *                              assigned to a node, set the child's next role to
  *                              stopped and update existing actions
  *
  * \return Node that \p rsc is assigned to, if assigned entirely to one node
  *
  * \note If \p stop_if_fail is \c false, then \c pcmk__unassign_resource() can
  *       completely undo the assignment. A successful assignment can be either
  *       undone or left alone as final. A failed assignment has the same effect
  *       as calling pcmk__unassign_resource(); there are no side effects on
  *       roles or actions.
  */
 pcmk_node_t *
 pcmk__group_assign(pcmk_resource_t *rsc, const pcmk_node_t *prefer,
                    bool stop_if_fail)
 {
     pcmk_node_t *first_assigned_node = NULL;
     pcmk_resource_t *first_member = NULL;
 
     CRM_ASSERT(pcmk__is_group(rsc));
 
     if (!pcmk_is_set(rsc->flags, pcmk_rsc_unassigned)) {
         return rsc->allocated_to; // Assignment already done
     }
     if (pcmk_is_set(rsc->flags, pcmk_rsc_assigning)) {
         pcmk__rsc_debug(rsc, "Assignment dependency loop detected involving %s",
                         rsc->id);
         return NULL;
     }
 
     if (rsc->children == NULL) {
         // No members to assign
         pcmk__clear_rsc_flags(rsc, pcmk_rsc_unassigned);
         return NULL;
     }
 
     pcmk__set_rsc_flags(rsc, pcmk_rsc_assigning);
     first_member = (pcmk_resource_t *) rsc->children->data;
     rsc->role = first_member->role;
 
     pe__show_node_scores(!pcmk_is_set(rsc->cluster->flags,
                                       pcmk_sched_output_scores),
                          rsc, __func__, rsc->allowed_nodes, rsc->cluster);
 
     for (GList *iter = rsc->children; iter != NULL; iter = iter->next) {
         pcmk_resource_t *member = (pcmk_resource_t *) iter->data;
         pcmk_node_t *node = NULL;
 
         pcmk__rsc_trace(rsc, "Assigning group %s member %s",
                         rsc->id, member->id);
-        node = member->cmds->assign(member, prefer, stop_if_fail);
+        node = member->private->cmds->assign(member, prefer, stop_if_fail);
         if (first_assigned_node == NULL) {
             first_assigned_node = node;
         }
     }
 
     pe__set_next_role(rsc, first_member->next_role, "first group member");
     pcmk__clear_rsc_flags(rsc, pcmk_rsc_assigning|pcmk_rsc_unassigned);
 
     if (!pe__group_flag_is_set(rsc, pcmk__group_colocated)) {
         return NULL;
     }
     return first_assigned_node;
 }
 
 /*!
  * \internal
  * \brief Create a pseudo-operation for a group as an ordering point
  *
  * \param[in,out] group   Group resource to create action for
  * \param[in]     action  Action name
  *
  * \return Newly created pseudo-operation
  */
 static pcmk_action_t *
 create_group_pseudo_op(pcmk_resource_t *group, const char *action)
 {
     pcmk_action_t *op = custom_action(group, pcmk__op_key(group->id, action, 0),
                                       action, NULL, TRUE, group->cluster);
 
     pcmk__set_action_flags(op, pcmk_action_pseudo|pcmk_action_runnable);
     return op;
 }
 
 /*!
  * \internal
  * \brief Create all actions needed for a given group resource
  *
  * \param[in,out] rsc  Group resource to create actions for
  */
 void
 pcmk__group_create_actions(pcmk_resource_t *rsc)
 {
     CRM_ASSERT(pcmk__is_group(rsc));
 
     pcmk__rsc_trace(rsc, "Creating actions for group %s", rsc->id);
 
     // Create actions for individual group members
     for (GList *iter = rsc->children; iter != NULL; iter = iter->next) {
         pcmk_resource_t *member = (pcmk_resource_t *) iter->data;
 
-        member->cmds->create_actions(member);
+        member->private->cmds->create_actions(member);
     }
 
     // Create pseudo-actions for group itself to serve as ordering points
     create_group_pseudo_op(rsc, PCMK_ACTION_START);
     create_group_pseudo_op(rsc, PCMK_ACTION_RUNNING);
     create_group_pseudo_op(rsc, PCMK_ACTION_STOP);
     create_group_pseudo_op(rsc, PCMK_ACTION_STOPPED);
     if (crm_is_true(g_hash_table_lookup(rsc->meta, PCMK_META_PROMOTABLE))) {
         create_group_pseudo_op(rsc, PCMK_ACTION_DEMOTE);
         create_group_pseudo_op(rsc, PCMK_ACTION_DEMOTED);
         create_group_pseudo_op(rsc, PCMK_ACTION_PROMOTE);
         create_group_pseudo_op(rsc, PCMK_ACTION_PROMOTED);
     }
 }
 
 // User data for member_internal_constraints()
 struct member_data {
     // These could be derived from member but this avoids some function calls
     bool ordered;
     bool colocated;
     bool promotable;
 
     pcmk_resource_t *last_active;
     pcmk_resource_t *previous_member;
 };
 
 /*!
  * \internal
  * \brief Create implicit constraints needed for a group member
  *
  * \param[in,out] data       Group member to create implicit constraints for
  * \param[in,out] user_data  Member data (struct member_data *)
  */
 static void
 member_internal_constraints(gpointer data, gpointer user_data)
 {
     pcmk_resource_t *member = (pcmk_resource_t *) data;
     struct member_data *member_data = (struct member_data *) user_data;
 
     // For ordering demote vs demote or stop vs stop
     uint32_t down_flags = pcmk__ar_then_implies_first_graphed;
 
     // For ordering demote vs demoted or stop vs stopped
     uint32_t post_down_flags = pcmk__ar_first_implies_then_graphed;
 
     // Create the individual member's implicit constraints
-    member->cmds->internal_constraints(member);
+    member->private->cmds->internal_constraints(member);
 
     if (member_data->previous_member == NULL) {
         // This is first member
         if (member_data->ordered) {
             pcmk__set_relation_flags(down_flags, pcmk__ar_ordered);
             post_down_flags = pcmk__ar_first_implies_then;
         }
 
     } else if (member_data->colocated) {
         uint32_t flags = pcmk__coloc_none;
 
         if (pcmk_is_set(member->flags, pcmk_rsc_critical)) {
             flags |= pcmk__coloc_influence;
         }
 
         // Colocate this member with the previous one
         pcmk__new_colocation("#group-members", NULL, PCMK_SCORE_INFINITY,
                              member, member_data->previous_member, NULL, NULL,
                              flags);
     }
 
     if (member_data->promotable) {
         // Demote group -> demote member -> group is demoted
         pcmk__order_resource_actions(member->parent, PCMK_ACTION_DEMOTE,
                                      member, PCMK_ACTION_DEMOTE, down_flags);
         pcmk__order_resource_actions(member, PCMK_ACTION_DEMOTE,
                                      member->parent, PCMK_ACTION_DEMOTED,
                                      post_down_flags);
 
         // Promote group -> promote member -> group is promoted
         pcmk__order_resource_actions(member, PCMK_ACTION_PROMOTE,
                                      member->parent, PCMK_ACTION_PROMOTED,
                                      pcmk__ar_unrunnable_first_blocks
                                      |pcmk__ar_first_implies_then
                                      |pcmk__ar_first_implies_then_graphed);
         pcmk__order_resource_actions(member->parent, PCMK_ACTION_PROMOTE,
                                      member, PCMK_ACTION_PROMOTE,
                                      pcmk__ar_then_implies_first_graphed);
     }
 
     // Stop group -> stop member -> group is stopped
     pcmk__order_stops(member->parent, member, down_flags);
     pcmk__order_resource_actions(member, PCMK_ACTION_STOP,
                                  member->parent, PCMK_ACTION_STOPPED,
                                  post_down_flags);
 
     // Start group -> start member -> group is started
     pcmk__order_starts(member->parent, member,
                        pcmk__ar_then_implies_first_graphed);
     pcmk__order_resource_actions(member, PCMK_ACTION_START,
                                  member->parent, PCMK_ACTION_RUNNING,
                                  pcmk__ar_unrunnable_first_blocks
                                  |pcmk__ar_first_implies_then
                                  |pcmk__ar_first_implies_then_graphed);
 
     if (!member_data->ordered) {
         pcmk__order_starts(member->parent, member,
                            pcmk__ar_first_implies_then
                            |pcmk__ar_unrunnable_first_blocks
                            |pcmk__ar_then_implies_first_graphed);
         if (member_data->promotable) {
             pcmk__order_resource_actions(member->parent, PCMK_ACTION_PROMOTE,
                                          member, PCMK_ACTION_PROMOTE,
                                          pcmk__ar_first_implies_then
                                          |pcmk__ar_unrunnable_first_blocks
                                          |pcmk__ar_then_implies_first_graphed);
         }
 
     } else if (member_data->previous_member == NULL) {
         pcmk__order_starts(member->parent, member, pcmk__ar_none);
         if (member_data->promotable) {
             pcmk__order_resource_actions(member->parent, PCMK_ACTION_PROMOTE,
                                          member, PCMK_ACTION_PROMOTE,
                                          pcmk__ar_none);
         }
 
     } else {
         // Order this member relative to the previous one
 
         pcmk__order_starts(member_data->previous_member, member,
                            pcmk__ar_first_implies_then
                            |pcmk__ar_unrunnable_first_blocks);
         pcmk__order_stops(member, member_data->previous_member,
                           pcmk__ar_ordered|pcmk__ar_intermediate_stop);
 
         /* In unusual circumstances (such as adding a new member to the middle
          * of a group with unmanaged later members), this member may be active
          * while the previous (new) member is inactive. In this situation, the
          * usual restart orderings will be irrelevant, so we need to order this
          * member's stop before the previous member's start.
          */
         if ((member->running_on != NULL)
             && (member_data->previous_member->running_on == NULL)) {
             pcmk__order_resource_actions(member, PCMK_ACTION_STOP,
                                          member_data->previous_member,
                                          PCMK_ACTION_START,
                                          pcmk__ar_then_implies_first
                                          |pcmk__ar_unrunnable_first_blocks);
         }
 
         if (member_data->promotable) {
             pcmk__order_resource_actions(member_data->previous_member,
                                          PCMK_ACTION_PROMOTE, member,
                                          PCMK_ACTION_PROMOTE,
                                          pcmk__ar_first_implies_then
                                          |pcmk__ar_unrunnable_first_blocks);
             pcmk__order_resource_actions(member, PCMK_ACTION_DEMOTE,
                                          member_data->previous_member,
                                          PCMK_ACTION_DEMOTE, pcmk__ar_ordered);
         }
     }
 
     // Make sure partially active groups shut down in sequence
     if (member->running_on != NULL) {
         if (member_data->ordered && (member_data->previous_member != NULL)
             && (member_data->previous_member->running_on == NULL)
             && (member_data->last_active != NULL)
             && (member_data->last_active->running_on != NULL)) {
             pcmk__order_stops(member, member_data->last_active,
                               pcmk__ar_ordered);
         }
         member_data->last_active = member;
     }
 
     member_data->previous_member = member;
 }
 
 /*!
  * \internal
  * \brief Create implicit constraints needed for a group resource
  *
  * \param[in,out] rsc  Group resource to create implicit constraints for
  */
 void
 pcmk__group_internal_constraints(pcmk_resource_t *rsc)
 {
     struct member_data member_data = { false, };
     const pcmk_resource_t *top = NULL;
 
     CRM_ASSERT(pcmk__is_group(rsc));
 
     /* Order group pseudo-actions relative to each other for restarting:
      * stop group -> group is stopped -> start group -> group is started
      */
     pcmk__order_resource_actions(rsc, PCMK_ACTION_STOP,
                                  rsc, PCMK_ACTION_STOPPED,
                                  pcmk__ar_unrunnable_first_blocks);
     pcmk__order_resource_actions(rsc, PCMK_ACTION_STOPPED,
                                  rsc, PCMK_ACTION_START,
                                  pcmk__ar_ordered);
     pcmk__order_resource_actions(rsc, PCMK_ACTION_START,
                                  rsc, PCMK_ACTION_RUNNING,
                                  pcmk__ar_unrunnable_first_blocks);
 
     top = pe__const_top_resource(rsc, false);
 
     member_data.ordered = pe__group_flag_is_set(rsc, pcmk__group_ordered);
     member_data.colocated = pe__group_flag_is_set(rsc, pcmk__group_colocated);
     member_data.promotable = pcmk_is_set(top->flags, pcmk_rsc_promotable);
     g_list_foreach(rsc->children, member_internal_constraints, &member_data);
 }
 
 /*!
  * \internal
  * \brief Apply a colocation's score to node scores or resource priority
  *
  * Given a colocation constraint for a group with some other resource, apply the
  * score to the dependent's allowed node scores (if we are still placing
  * resources) or priority (if we are choosing promotable clone instance roles).
  *
  * \param[in,out] dependent      Dependent group resource in colocation
  * \param[in]     primary        Primary resource in colocation
  * \param[in]     colocation     Colocation constraint to apply
  */
 static void
 colocate_group_with(pcmk_resource_t *dependent, const pcmk_resource_t *primary,
                     const pcmk__colocation_t *colocation)
 {
     pcmk_resource_t *member = NULL;
 
     if (dependent->children == NULL) {
         return;
     }
 
     pcmk__rsc_trace(primary, "Processing %s (group %s with %s) for dependent",
                     colocation->id, dependent->id, primary->id);
 
     if (pe__group_flag_is_set(dependent, pcmk__group_colocated)) {
         // Colocate first member (internal colocations will handle the rest)
         member = (pcmk_resource_t *) dependent->children->data;
-        member->cmds->apply_coloc_score(member, primary, colocation, true);
+        member->private->cmds->apply_coloc_score(member, primary, colocation,
+                                                 true);
         return;
     }
 
     if (colocation->score >= PCMK_SCORE_INFINITY) {
         pcmk__config_err("%s: Cannot perform mandatory colocation between "
                          "non-colocated group and %s",
                          dependent->id, primary->id);
         return;
     }
 
     // Colocate each member individually
     for (GList *iter = dependent->children; iter != NULL; iter = iter->next) {
         member = (pcmk_resource_t *) iter->data;
-        member->cmds->apply_coloc_score(member, primary, colocation, true);
+        member->private->cmds->apply_coloc_score(member, primary, colocation,
+                                                 true);
     }
 }
 
 /*!
  * \internal
  * \brief Apply a colocation's score to node scores or resource priority
  *
  * Given a colocation constraint for some other resource with a group, apply the
  * score to the dependent's allowed node scores (if we are still placing
  * resources) or priority (if we are choosing promotable clone instance roles).
  *
  * \param[in,out] dependent      Dependent resource in colocation
  * \param[in]     primary        Primary group resource in colocation
  * \param[in]     colocation     Colocation constraint to apply
  */
 static void
 colocate_with_group(pcmk_resource_t *dependent, const pcmk_resource_t *primary,
                     const pcmk__colocation_t *colocation)
 {
     const pcmk_resource_t *member = NULL;
 
     pcmk__rsc_trace(primary,
                     "Processing colocation %s (%s with group %s) for primary",
                     colocation->id, dependent->id, primary->id);
 
     if (pcmk_is_set(primary->flags, pcmk_rsc_unassigned)) {
         return;
     }
 
     if (pe__group_flag_is_set(primary, pcmk__group_colocated)) {
 
         if (colocation->score >= PCMK_SCORE_INFINITY) {
             /* For mandatory colocations, the entire group must be assignable
              * (and in the specified role if any), so apply the colocation based
              * on the last member.
              */
             member = pe__last_group_member(primary);
         } else if (primary->children != NULL) {
             /* For optional colocations, whether the group is partially or fully
              * up doesn't matter, so apply the colocation based on the first
              * member.
              */
             member = (pcmk_resource_t *) primary->children->data;
         }
         if (member == NULL) {
             return; // Nothing to colocate with
         }
 
-        member->cmds->apply_coloc_score(dependent, member, colocation, false);
+        member->private->cmds->apply_coloc_score(dependent, member, colocation,
+                                                 false);
         return;
     }
 
     if (colocation->score >= PCMK_SCORE_INFINITY) {
         pcmk__config_err("%s: Cannot perform mandatory colocation with"
                          " non-colocated group %s",
                          dependent->id, primary->id);
         return;
     }
 
     // Colocate dependent with each member individually
     for (const GList *iter = primary->children; iter != NULL;
          iter = iter->next) {
         member = iter->data;
-        member->cmds->apply_coloc_score(dependent, member, colocation, false);
+        member->private->cmds->apply_coloc_score(dependent, member, colocation,
+                                                 false);
     }
 }
 
 /*!
  * \internal
  * \brief Apply a colocation's score to node scores or resource priority
  *
  * Given a colocation constraint, apply its score to the dependent's
  * allowed node scores (if we are still placing resources) or priority (if
  * we are choosing promotable clone instance roles).
  *
  * \param[in,out] dependent      Dependent resource in colocation
  * \param[in]     primary        Primary resource in colocation
  * \param[in]     colocation     Colocation constraint to apply
  * \param[in]     for_dependent  true if called on behalf of dependent
  */
 void
 pcmk__group_apply_coloc_score(pcmk_resource_t *dependent,
                               const pcmk_resource_t *primary,
                               const pcmk__colocation_t *colocation,
                               bool for_dependent)
 {
     CRM_ASSERT((dependent != NULL) && (primary != NULL)
                && (colocation != NULL));
 
     if (for_dependent) {
         colocate_group_with(dependent, primary, colocation);
 
     } else {
         // Method should only be called for primitive dependents
         CRM_ASSERT(pcmk__is_primitive(dependent));
 
         colocate_with_group(dependent, primary, colocation);
     }
 }
 
 /*!
  * \internal
  * \brief Return action flags for a given group resource action
  *
  * \param[in,out] action  Group action to get flags for
  * \param[in]     node    If not NULL, limit effects to this node
  *
  * \return Flags appropriate to \p action on \p node
  */
 uint32_t
 pcmk__group_action_flags(pcmk_action_t *action, const pcmk_node_t *node)
 {
     // Default flags for a group action
     uint32_t flags = pcmk_action_optional
                      |pcmk_action_runnable
                      |pcmk_action_pseudo;
 
     CRM_ASSERT(action != NULL);
 
     // Update flags considering each member's own flags for same action
     for (GList *iter = action->rsc->children; iter != NULL; iter = iter->next) {
         pcmk_resource_t *member = (pcmk_resource_t *) iter->data;
 
         // Check whether member has the same action
         enum action_tasks task = get_complex_task(member, action->task);
         const char *task_s = pcmk_action_text(task);
         pcmk_action_t *member_action = find_first_action(member->actions, NULL,
                                                          task_s, node);
 
         if (member_action != NULL) {
-            uint32_t member_flags = member->cmds->action_flags(member_action,
+            uint32_t member_flags = 0U;
+
+            member_flags = member->private->cmds->action_flags(member_action,
                                                                node);
 
             // Group action is mandatory if any member action is
             if (pcmk_is_set(flags, pcmk_action_optional)
                 && !pcmk_is_set(member_flags, pcmk_action_optional)) {
                 pcmk__rsc_trace(action->rsc, "%s is mandatory because %s is",
                                 action->uuid, member_action->uuid);
                 pcmk__clear_raw_action_flags(flags, "group action",
                                              pcmk_action_optional);
                 pcmk__clear_action_flags(action, pcmk_action_optional);
             }
 
             // Group action is unrunnable if any member action is
             if (!pcmk__str_eq(task_s, action->task, pcmk__str_none)
                 && pcmk_is_set(flags, pcmk_action_runnable)
                 && !pcmk_is_set(member_flags, pcmk_action_runnable)) {
 
                 pcmk__rsc_trace(action->rsc, "%s is unrunnable because %s is",
                                 action->uuid, member_action->uuid);
                 pcmk__clear_raw_action_flags(flags, "group action",
                                              pcmk_action_runnable);
                 pcmk__clear_action_flags(action, pcmk_action_runnable);
             }
 
         /* Group (pseudo-)actions other than stop or demote are unrunnable
          * unless every member will do it.
          */
         } else if ((task != pcmk_action_stop) && (task != pcmk_action_demote)) {
             pcmk__rsc_trace(action->rsc,
                             "%s is not runnable because %s will not %s",
                             action->uuid, member->id, task_s);
             pcmk__clear_raw_action_flags(flags, "group action",
                                          pcmk_action_runnable);
         }
     }
 
     return flags;
 }
 
 /*!
  * \internal
  * \brief Update two actions according to an ordering between them
  *
  * Given information about an ordering of two actions, update the actions' flags
  * (and runnable_before members if appropriate) as appropriate for the ordering.
  * Effects may cascade to other orderings involving the actions as well.
  *
  * \param[in,out] first      'First' action in an ordering
  * \param[in,out] then       'Then' action in an ordering
  * \param[in]     node       If not NULL, limit scope of ordering to this node
  *                           (only used when interleaving instances)
  * \param[in]     flags      Action flags for \p first for ordering purposes
  * \param[in]     filter     Action flags to limit scope of certain updates (may
  *                           include pcmk_action_optional to affect only
  *                           mandatory actions, and pcmk_action_runnable to
  *                           affect only runnable actions)
  * \param[in]     type       Group of enum pcmk__action_relation_flags to apply
  * \param[in,out] scheduler  Scheduler data
  *
  * \return Group of enum pcmk__updated flags indicating what was updated
  */
 uint32_t
 pcmk__group_update_ordered_actions(pcmk_action_t *first, pcmk_action_t *then,
                                    const pcmk_node_t *node, uint32_t flags,
                                    uint32_t filter, uint32_t type,
                                    pcmk_scheduler_t *scheduler)
 {
     uint32_t changed = pcmk__updated_none;
 
     // Group method can be called only on behalf of "then" action
     CRM_ASSERT((first != NULL) && (then != NULL) && (then->rsc != NULL)
                && (scheduler != NULL));
 
     // Update the actions for the group itself
     changed |= pcmk__update_ordered_actions(first, then, node, flags, filter,
                                             type, scheduler);
 
     // Update the actions for each group member
     for (GList *iter = then->rsc->children; iter != NULL; iter = iter->next) {
         pcmk_resource_t *member = (pcmk_resource_t *) iter->data;
 
         pcmk_action_t *member_action = find_first_action(member->actions, NULL,
                                                          then->task, node);
 
-        if (member_action != NULL) {
-            changed |= member->cmds->update_ordered_actions(first,
-                                                            member_action, node,
-                                                            flags, filter, type,
-                                                            scheduler);
+        if (member_action == NULL) {
+            continue;
         }
+        changed |= member->private->cmds->update_ordered_actions(first,
+                                                                 member_action,
+                                                                 node, flags,
+                                                                 filter, type,
+                                                                 scheduler);
     }
     return changed;
 }
 
 /*!
  * \internal
  * \brief Apply a location constraint to a group's allowed node scores
  *
  * \param[in,out] rsc       Group resource to apply constraint to
  * \param[in,out] location  Location constraint to apply
  */
 void
 pcmk__group_apply_location(pcmk_resource_t *rsc, pcmk__location_t *location)
 {
     GList *node_list_orig = NULL;
     GList *node_list_copy = NULL;
     bool reset_scores = true;
 
     CRM_ASSERT(pcmk__is_group(rsc) && (location != NULL));
 
     node_list_orig = location->nodes;
     node_list_copy = pcmk__copy_node_list(node_list_orig, true);
     reset_scores = pe__group_flag_is_set(rsc, pcmk__group_colocated);
 
     // Apply the constraint for the group itself (updates node scores)
     pcmk__apply_location(rsc, location);
 
     // Apply the constraint for each member
     for (GList *iter = rsc->children; iter != NULL; iter = iter->next) {
         pcmk_resource_t *member = (pcmk_resource_t *) iter->data;
 
-        member->cmds->apply_location(member, location);
+        member->private->cmds->apply_location(member, location);
 
         if (reset_scores) {
             /* The first member of colocated groups needs to use the original
              * node scores, but subsequent members should work on a copy, since
              * the first member's scores already incorporate theirs.
              */
             reset_scores = false;
             location->nodes = node_list_copy;
         }
     }
 
     location->nodes = node_list_orig;
     g_list_free_full(node_list_copy, free);
 }
 
 // Group implementation of pcmk_assignment_methods_t:colocated_resources()
 GList *
 pcmk__group_colocated_resources(const pcmk_resource_t *rsc,
                                 const pcmk_resource_t *orig_rsc,
                                 GList *colocated_rscs)
 {
-    const pcmk_resource_t *member = NULL;
-
     CRM_ASSERT(pcmk__is_group(rsc));
 
     if (orig_rsc == NULL) {
         orig_rsc = rsc;
     }
 
     if (pe__group_flag_is_set(rsc, pcmk__group_colocated)
         || pcmk__is_clone(rsc->parent)) {
         /* This group has colocated members and/or is cloned -- either way,
          * add every child's colocated resources to the list. The first and last
          * members will include the group's own colocations.
          */
         colocated_rscs = g_list_prepend(colocated_rscs, (gpointer) rsc);
         for (const GList *iter = rsc->children;
              iter != NULL; iter = iter->next) {
+            const pcmk_resource_t *member = iter->data;
 
-            member = (const pcmk_resource_t *) iter->data;
-            colocated_rscs = member->cmds->colocated_resources(member, orig_rsc,
-                                                               colocated_rscs);
+            colocated_rscs = member->private->cmds->colocated_resources(member,
+                                                                        orig_rsc,
+                                                                        colocated_rscs);
         }
 
     } else if (rsc->children != NULL) {
         /* This group's members are not colocated, and the group is not cloned,
          * so just add the group's own colocations to the list.
          */
         colocated_rscs = pcmk__colocated_resources(rsc, orig_rsc,
                                                    colocated_rscs);
     }
 
     return colocated_rscs;
 }
 
 // Group implementation of pcmk_assignment_methods_t:with_this_colocations()
 void
 pcmk__with_group_colocations(const pcmk_resource_t *rsc,
                              const pcmk_resource_t *orig_rsc, GList **list)
 
 {
     CRM_ASSERT((orig_rsc != NULL) && (list != NULL) && pcmk__is_group(rsc));
 
     // Ignore empty groups
     if (rsc->children == NULL) {
         return;
     }
 
     /* "With this" colocations are needed only for the group itself and for its
      * last member. (Previous members will chain via the group internal
      * colocations.)
      */
     if ((orig_rsc != rsc) && (orig_rsc != pe__last_group_member(rsc))) {
         return;
     }
 
     pcmk__rsc_trace(rsc, "Adding 'with %s' colocations to list for %s",
                     rsc->id, orig_rsc->id);
 
     // Add the group's own colocations
     pcmk__add_with_this_list(list, rsc->rsc_cons_lhs, orig_rsc);
 
     // If cloned, add any relevant colocations with the clone
     if (rsc->parent != NULL) {
-        rsc->parent->cmds->with_this_colocations(rsc->parent, orig_rsc,
-                                                 list);
+        rsc->parent->private->cmds->with_this_colocations(rsc->parent, orig_rsc,
+                                                          list);
     }
 
     if (!pe__group_flag_is_set(rsc, pcmk__group_colocated)) {
         // @COMPAT Non-colocated groups are deprecated
         return;
     }
 
     // Add explicit colocations with the group's (other) children
     for (const GList *iter = rsc->children; iter != NULL; iter = iter->next) {
         const pcmk_resource_t *member = iter->data;
 
-        if (member != orig_rsc) {
-            member->cmds->with_this_colocations(member, orig_rsc, list);
+        if (member == orig_rsc) {
+            continue;
         }
+        member->private->cmds->with_this_colocations(member, orig_rsc, list);
     }
 }
 
 // Group implementation of pcmk_assignment_methods_t:this_with_colocations()
 void
 pcmk__group_with_colocations(const pcmk_resource_t *rsc,
                              const pcmk_resource_t *orig_rsc, GList **list)
 {
     const pcmk_resource_t *member = NULL;
 
     CRM_ASSERT((orig_rsc != NULL) && (list != NULL) && pcmk__is_group(rsc));
 
     // Ignore empty groups
     if (rsc->children == NULL) {
         return;
     }
 
     /* "This with" colocations are normally needed only for the group itself and
      * for its first member.
      */
     if ((rsc == orig_rsc)
         || (orig_rsc == (const pcmk_resource_t *) rsc->children->data)) {
         pcmk__rsc_trace(rsc, "Adding '%s with' colocations to list for %s",
                         rsc->id, orig_rsc->id);
 
         // Add the group's own colocations
         pcmk__add_this_with_list(list, rsc->rsc_cons, orig_rsc);
 
         // If cloned, add any relevant colocations involving the clone
         if (rsc->parent != NULL) {
-            rsc->parent->cmds->this_with_colocations(rsc->parent, orig_rsc,
-                                                     list);
+            rsc->parent->private->cmds->this_with_colocations(rsc->parent,
+                                                              orig_rsc, list);
         }
 
         if (!pe__group_flag_is_set(rsc, pcmk__group_colocated)) {
             // @COMPAT Non-colocated groups are deprecated
             return;
         }
 
         // Add explicit colocations involving the group's (other) children
         for (const GList *iter = rsc->children;
              iter != NULL; iter = iter->next) {
             member = iter->data;
-            if (member != orig_rsc) {
-                member->cmds->this_with_colocations(member, orig_rsc, list);
+            if (member == orig_rsc) {
+                continue;
             }
+            member->private->cmds->this_with_colocations(member, orig_rsc,
+                                                         list);
         }
         return;
     }
 
     /* Later group members honor the group's colocations indirectly, due to the
      * internal group colocations that chain everything from the first member.
      * However, if an earlier group member is unmanaged, this chaining will not
      * happen, so the group's mandatory colocations must be explicitly added.
      */
     for (const GList *iter = rsc->children; iter != NULL; iter = iter->next) {
         member = iter->data;
         if (orig_rsc == member) {
             break; // We've seen all earlier members, and none are unmanaged
         }
 
         if (!pcmk_is_set(member->flags, pcmk_rsc_managed)) {
             crm_trace("Adding mandatory '%s with' colocations to list for "
                       "member %s because earlier member %s is unmanaged",
                       rsc->id, orig_rsc->id, member->id);
             for (const GList *cons_iter = rsc->rsc_cons; cons_iter != NULL;
                  cons_iter = cons_iter->next) {
                 const pcmk__colocation_t *colocation = NULL;
 
                 colocation = (const pcmk__colocation_t *) cons_iter->data;
                 if (colocation->score == PCMK_SCORE_INFINITY) {
                     pcmk__add_this_with(list, colocation, orig_rsc);
                 }
             }
             // @TODO Add mandatory (or all?) clone constraints if cloned
             break;
         }
     }
 }
 
 /*!
  * \internal
  * \brief Update nodes with scores of colocated resources' nodes
  *
  * Given a table of nodes and a resource, update the nodes' scores with the
  * scores of the best nodes matching the attribute used for each of the
  * resource's relevant colocations.
  *
  * \param[in,out] source_rsc  Group resource whose node scores to add
  * \param[in]     target_rsc  Resource on whose behalf to update \p *nodes
  * \param[in]     log_id      Resource ID for logs (if \c NULL, use
  *                            \p source_rsc ID)
  * \param[in,out] nodes       Nodes to update (set initial contents to \c NULL
  *                            to copy allowed nodes from \p source_rsc)
  * \param[in]     colocation  Original colocation constraint (used to get
  *                            configured primary resource's stickiness, and
  *                            to get colocation node attribute; if \c NULL,
  *                            <tt>source_rsc</tt>'s own matching node scores will
  *                            not be added, and \p *nodes must be \c NULL as
  *                            well)
  * \param[in]     factor      Incorporate scores multiplied by this factor
  * \param[in]     flags       Bitmask of enum pcmk__coloc_select values
  *
  * \note \c NULL \p target_rsc, \c NULL \p *nodes, \c NULL \p colocation, and
  *       the \c pcmk__coloc_select_this_with flag are used together (and only by
  *       \c cmp_resources()).
  * \note The caller remains responsible for freeing \p *nodes.
  * \note This is the group implementation of
  *       \c pcmk_assignment_methods_t:add_colocated_node_scores().
  */
 void
 pcmk__group_add_colocated_node_scores(pcmk_resource_t *source_rsc,
                                       const pcmk_resource_t *target_rsc,
                                       const char *log_id, GHashTable **nodes,
                                       const pcmk__colocation_t *colocation,
                                       float factor, uint32_t flags)
 {
     pcmk_resource_t *member = NULL;
 
     CRM_ASSERT(pcmk__is_group(source_rsc) && (nodes != NULL)
                && ((colocation != NULL)
                    || ((target_rsc == NULL) && (*nodes == NULL))));
 
     if (log_id == NULL) {
         log_id = source_rsc->id;
     }
 
     // Avoid infinite recursion
     if (pcmk_is_set(source_rsc->flags, pcmk_rsc_updating_nodes)) {
         pcmk__rsc_info(source_rsc, "%s: Breaking dependency loop at %s",
                        log_id, source_rsc->id);
         return;
     }
     pcmk__set_rsc_flags(source_rsc, pcmk_rsc_updating_nodes);
 
     // Ignore empty groups (only possible with schema validation disabled)
     if (source_rsc->children == NULL) {
         return;
     }
 
     /* Refer the operation to the first or last member as appropriate.
      *
      * cmp_resources() is the only caller that passes a NULL nodes table,
      * and is also the only caller using pcmk__coloc_select_this_with.
      * For "this with" colocations, the last member will recursively incorporate
      * all the other members' "this with" colocations via the internal group
      * colocations (and via the first member, the group's own colocations).
      *
      * For "with this" colocations, the first member works similarly.
      */
     if (*nodes == NULL) {
         member = pe__last_group_member(source_rsc);
     } else {
         member = source_rsc->children->data;
     }
+
     pcmk__rsc_trace(source_rsc, "%s: Merging scores from group %s using member %s "
                     "(at %.6f)", log_id, source_rsc->id, member->id, factor);
-    member->cmds->add_colocated_node_scores(member, target_rsc, log_id, nodes,
-                                            colocation, factor, flags);
+    member->private->cmds->add_colocated_node_scores(member, target_rsc, log_id,
+                                                     nodes, colocation, factor,
+                                                     flags);
     pcmk__clear_rsc_flags(source_rsc, pcmk_rsc_updating_nodes);
 }
 
 // Group implementation of pcmk_assignment_methods_t:add_utilization()
 void
 pcmk__group_add_utilization(const pcmk_resource_t *rsc,
                             const pcmk_resource_t *orig_rsc, GList *all_rscs,
                             GHashTable *utilization)
 {
     pcmk_resource_t *member = NULL;
 
     CRM_ASSERT((orig_rsc != NULL) && (utilization != NULL)
                && pcmk__is_group(rsc));
 
     if (!pcmk_is_set(rsc->flags, pcmk_rsc_unassigned)) {
         return;
     }
 
     pcmk__rsc_trace(orig_rsc, "%s: Adding group %s as colocated utilization",
                     orig_rsc->id, rsc->id);
     if (pe__group_flag_is_set(rsc, pcmk__group_colocated)
         || pcmk__is_clone(rsc->parent)) {
         // Every group member will be on same node, so sum all members
         for (GList *iter = rsc->children; iter != NULL; iter = iter->next) {
             member = (pcmk_resource_t *) iter->data;
 
             if (pcmk_is_set(member->flags, pcmk_rsc_unassigned)
                 && (g_list_find(all_rscs, member) == NULL)) {
-                member->cmds->add_utilization(member, orig_rsc, all_rscs,
-                                              utilization);
+                member->private->cmds->add_utilization(member, orig_rsc,
+                                                       all_rscs, utilization);
             }
         }
 
     } else if (rsc->children != NULL) {
         // Just add first member's utilization
         member = (pcmk_resource_t *) rsc->children->data;
         if ((member != NULL)
             && pcmk_is_set(member->flags, pcmk_rsc_unassigned)
             && (g_list_find(all_rscs, member) == NULL)) {
 
-            member->cmds->add_utilization(member, orig_rsc, all_rscs,
-                                          utilization);
+            member->private->cmds->add_utilization(member, orig_rsc, all_rscs,
+                                                   utilization);
         }
     }
 }
 
 void
 pcmk__group_shutdown_lock(pcmk_resource_t *rsc)
 {
     CRM_ASSERT(pcmk__is_group(rsc));
 
     for (GList *iter = rsc->children; iter != NULL; iter = iter->next) {
         pcmk_resource_t *member = (pcmk_resource_t *) iter->data;
 
-        member->cmds->shutdown_lock(member);
+        member->private->cmds->shutdown_lock(member);
     }
 }
diff --git a/lib/pacemaker/pcmk_sched_instances.c b/lib/pacemaker/pcmk_sched_instances.c
index a2dccc0c82..6d57dcc04e 100644
--- a/lib/pacemaker/pcmk_sched_instances.c
+++ b/lib/pacemaker/pcmk_sched_instances.c
@@ -1,1690 +1,1697 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 /* This file is intended for code usable with both clone instances and bundle
  * replica containers.
  */
 
 #include <crm_internal.h>
 #include <crm/common/xml.h>
 #include <pacemaker-internal.h>
 #include "libpacemaker_private.h"
 
 /*!
  * \internal
  * \brief Check whether a node is allowed to run an instance
  *
  * \param[in] instance      Clone instance or bundle container to check
  * \param[in] node          Node to check
  * \param[in] max_per_node  Maximum number of instances allowed to run on a node
  *
  * \return true if \p node is allowed to run \p instance, otherwise false
  */
 static bool
 can_run_instance(const pcmk_resource_t *instance, const pcmk_node_t *node,
                  int max_per_node)
 {
     pcmk_node_t *allowed_node = NULL;
 
     if (pcmk_is_set(instance->flags, pcmk_rsc_removed)) {
         pcmk__rsc_trace(instance, "%s cannot run on %s: orphaned",
                         instance->id, pcmk__node_name(node));
         return false;
     }
 
     if (!pcmk__node_available(node, false, false)) {
         pcmk__rsc_trace(instance,
                         "%s cannot run on %s: node cannot run resources",
                         instance->id, pcmk__node_name(node));
         return false;
     }
 
     allowed_node = pcmk__top_allowed_node(instance, node);
     if (allowed_node == NULL) {
         crm_warn("%s cannot run on %s: node not allowed",
                  instance->id, pcmk__node_name(node));
         return false;
     }
 
     if (allowed_node->weight < 0) {
         pcmk__rsc_trace(instance,
                         "%s cannot run on %s: parent score is %s there",
                         instance->id, pcmk__node_name(node),
                         pcmk_readable_score(allowed_node->weight));
         return false;
     }
 
     if (allowed_node->count >= max_per_node) {
         pcmk__rsc_trace(instance,
                         "%s cannot run on %s: node already has %d instance%s",
                         instance->id, pcmk__node_name(node), max_per_node,
                         pcmk__plural_s(max_per_node));
         return false;
     }
 
     pcmk__rsc_trace(instance, "%s can run on %s (%d already running)",
                     instance->id, pcmk__node_name(node), allowed_node->count);
     return true;
 }
 
 /*!
  * \internal
  * \brief Ban a clone instance or bundle replica from unavailable allowed nodes
  *
  * \param[in,out] instance      Clone instance or bundle replica to ban
  * \param[in]     max_per_node  Maximum instances allowed to run on a node
  */
 static void
 ban_unavailable_allowed_nodes(pcmk_resource_t *instance, int max_per_node)
 {
     if (instance->allowed_nodes != NULL) {
         GHashTableIter iter;
         pcmk_node_t *node = NULL;
 
         g_hash_table_iter_init(&iter, instance->allowed_nodes);
         while (g_hash_table_iter_next(&iter, NULL, (void **) &node)) {
             if (!can_run_instance(instance, node, max_per_node)) {
                 pcmk__rsc_trace(instance, "Banning %s from unavailable node %s",
                                 instance->id, pcmk__node_name(node));
                 node->weight = -PCMK_SCORE_INFINITY;
                 for (GList *child_iter = instance->children;
                      child_iter != NULL; child_iter = child_iter->next) {
                     pcmk_resource_t *child = child_iter->data;
                     pcmk_node_t *child_node = NULL;
 
                     child_node = g_hash_table_lookup(child->allowed_nodes,
                                                      node->details->id);
                     if (child_node != NULL) {
                         pcmk__rsc_trace(instance,
                                         "Banning %s child %s "
                                         "from unavailable node %s",
                                         instance->id, child->id,
                                         pcmk__node_name(node));
                         child_node->weight = -PCMK_SCORE_INFINITY;
                     }
                 }
             }
         }
     }
 }
 
 /*!
  * \internal
  * \brief Create a hash table with a single node in it
  *
  * \param[in] node  Node to copy into new table
  *
  * \return Newly created hash table containing a copy of \p node
  * \note The caller is responsible for freeing the result with
  *       g_hash_table_destroy().
  */
 static GHashTable *
 new_node_table(pcmk_node_t *node)
 {
     GHashTable *table = pcmk__strkey_table(NULL, free);
 
     node = pe__copy_node(node);
     g_hash_table_insert(table, (gpointer) node->details->id, node);
     return table;
 }
 
 /*!
  * \internal
  * \brief Apply a resource's parent's colocation scores to a node table
  *
  * \param[in]     rsc    Resource whose colocations should be applied
  * \param[in,out] nodes  Node table to apply colocations to
  */
 static void
 apply_parent_colocations(const pcmk_resource_t *rsc, GHashTable **nodes)
 {
     GList *colocations = pcmk__this_with_colocations(rsc);
 
     for (const GList *iter = colocations; iter != NULL; iter = iter->next) {
         const pcmk__colocation_t *colocation = iter->data;
         pcmk_resource_t *other = colocation->primary;
         float factor = colocation->score / (float) PCMK_SCORE_INFINITY;
 
-        other->cmds->add_colocated_node_scores(other, rsc, rsc->id, nodes,
-                                               colocation, factor,
-                                               pcmk__coloc_select_default);
+        other->private->cmds->add_colocated_node_scores(other, rsc, rsc->id,
+                                                        nodes, colocation,
+                                                        factor,
+                                                        pcmk__coloc_select_default);
     }
     g_list_free(colocations);
     colocations = pcmk__with_this_colocations(rsc);
 
     for (const GList *iter = colocations; iter != NULL; iter = iter->next) {
         const pcmk__colocation_t *colocation = iter->data;
         pcmk_resource_t *other = colocation->dependent;
         float factor = colocation->score / (float) PCMK_SCORE_INFINITY;
 
         if (!pcmk__colocation_has_influence(colocation, rsc)) {
             continue;
         }
-        other->cmds->add_colocated_node_scores(other, rsc, rsc->id, nodes,
-                                               colocation, factor,
-                                               pcmk__coloc_select_nonnegative);
+        other->private->cmds->add_colocated_node_scores(other, rsc, rsc->id,
+                                                        nodes, colocation,
+                                                        factor,
+                                                        pcmk__coloc_select_nonnegative);
     }
     g_list_free(colocations);
 }
 
 /*!
  * \internal
  * \brief Compare clone or bundle instances based on colocation scores
  *
  * Determine the relative order in which two clone or bundle instances should be
  * assigned to nodes, considering the scores of colocation constraints directly
  * or indirectly involving them.
  *
  * \param[in] instance1  First instance to compare
  * \param[in] instance2  Second instance to compare
  *
  * \return A negative number if \p instance1 should be assigned first,
  *         a positive number if \p instance2 should be assigned first,
  *         or 0 if assignment order doesn't matter
  */
 static int
 cmp_instance_by_colocation(const pcmk_resource_t *instance1,
                            const pcmk_resource_t *instance2)
 {
     int rc = 0;
     pcmk_node_t *node1 = NULL;
     pcmk_node_t *node2 = NULL;
     pcmk_node_t *current_node1 = pcmk__current_node(instance1);
     pcmk_node_t *current_node2 = pcmk__current_node(instance2);
     GHashTable *colocated_scores1 = NULL;
     GHashTable *colocated_scores2 = NULL;
 
     CRM_ASSERT((instance1 != NULL) && (instance1->parent != NULL)
                && (instance2 != NULL) && (instance2->parent != NULL)
                && (current_node1 != NULL) && (current_node2 != NULL));
 
     // Create node tables initialized with each node
     colocated_scores1 = new_node_table(current_node1);
     colocated_scores2 = new_node_table(current_node2);
 
     // Apply parental colocations
     apply_parent_colocations(instance1, &colocated_scores1);
     apply_parent_colocations(instance2, &colocated_scores2);
 
     // Find original nodes again, with scores updated for colocations
     node1 = g_hash_table_lookup(colocated_scores1, current_node1->details->id);
     node2 = g_hash_table_lookup(colocated_scores2, current_node2->details->id);
 
     // Compare nodes by updated scores
     if (node1->weight < node2->weight) {
         crm_trace("Assign %s (%d on %s) after %s (%d on %s)",
                   instance1->id, node1->weight, pcmk__node_name(node1),
                   instance2->id, node2->weight, pcmk__node_name(node2));
         rc = 1;
 
     } else if (node1->weight > node2->weight) {
         crm_trace("Assign %s (%d on %s) before %s (%d on %s)",
                   instance1->id, node1->weight, pcmk__node_name(node1),
                   instance2->id, node2->weight, pcmk__node_name(node2));
         rc = -1;
     }
 
     g_hash_table_destroy(colocated_scores1);
     g_hash_table_destroy(colocated_scores2);
     return rc;
 }
 
 /*!
  * \internal
  * \brief Check whether a resource or any of its children are failed
  *
  * \param[in] rsc  Resource to check
  *
  * \return true if \p rsc or any of its children are failed, otherwise false
  */
 static bool
 did_fail(const pcmk_resource_t *rsc)
 {
     if (pcmk_is_set(rsc->flags, pcmk_rsc_failed)) {
         return true;
     }
     for (GList *iter = rsc->children; iter != NULL; iter = iter->next) {
         if (did_fail((const pcmk_resource_t *) iter->data)) {
             return true;
         }
     }
     return false;
 }
 
 /*!
  * \internal
  * \brief Check whether a node is allowed to run a resource
  *
  * \param[in]     rsc   Resource to check
  * \param[in,out] node  Node to check (will be set NULL if not allowed)
  *
  * \return true if *node is either NULL or allowed for \p rsc, otherwise false
  */
 static bool
 node_is_allowed(const pcmk_resource_t *rsc, pcmk_node_t **node)
 {
     if (*node != NULL) {
         pcmk_node_t *allowed = g_hash_table_lookup(rsc->allowed_nodes,
                                                    (*node)->details->id);
 
         if ((allowed == NULL) || (allowed->weight < 0)) {
             pcmk__rsc_trace(rsc, "%s: current location (%s) is unavailable",
                             rsc->id, pcmk__node_name(*node));
             *node = NULL;
             return false;
         }
     }
     return true;
 }
 
 /*!
  * \internal
  * \brief Compare two clone or bundle instances' instance numbers
  *
  * \param[in] a  First instance to compare
  * \param[in] b  Second instance to compare
  *
  * \return A negative number if \p a's instance number is lower,
  *         a positive number if \p b's instance number is lower,
  *         or 0 if their instance numbers are the same
  */
 gint
 pcmk__cmp_instance_number(gconstpointer a, gconstpointer b)
 {
     const pcmk_resource_t *instance1 = (const pcmk_resource_t *) a;
     const pcmk_resource_t *instance2 = (const pcmk_resource_t *) b;
     char *div1 = NULL;
     char *div2 = NULL;
 
     CRM_ASSERT((instance1 != NULL) && (instance2 != NULL));
 
     // Clone numbers are after a colon, bundle numbers after a dash
     div1 = strrchr(instance1->id, ':');
     if (div1 == NULL) {
         div1 = strrchr(instance1->id, '-');
     }
     div2 = strrchr(instance2->id, ':');
     if (div2 == NULL) {
         div2 = strrchr(instance2->id, '-');
     }
     CRM_ASSERT((div1 != NULL) && (div2 != NULL));
 
     return (gint) (strtol(div1 + 1, NULL, 10) - strtol(div2 + 1, NULL, 10));
 }
 
 /*!
  * \internal
  * \brief Compare clone or bundle instances according to assignment order
  *
  * Compare two clone or bundle instances according to the order they should be
  * assigned to nodes, preferring (in order):
  *
  *  - Active instance that is less multiply active
  *  - Instance that is not active on a disallowed node
  *  - Instance with higher configured priority
  *  - Active instance whose current node can run resources
  *  - Active instance whose parent is allowed on current node
  *  - Active instance whose current node has fewer other instances
  *  - Active instance
  *  - Instance that isn't failed
  *  - Instance whose colocations result in higher score on current node
  *  - Instance with lower ID in lexicographic order
  *
  * \param[in] a          First instance to compare
  * \param[in] b          Second instance to compare
  *
  * \return A negative number if \p a should be assigned first,
  *         a positive number if \p b should be assigned first,
  *         or 0 if assignment order doesn't matter
  */
 gint
 pcmk__cmp_instance(gconstpointer a, gconstpointer b)
 {
     int rc = 0;
     pcmk_node_t *node1 = NULL;
     pcmk_node_t *node2 = NULL;
     unsigned int nnodes1 = 0;
     unsigned int nnodes2 = 0;
 
     bool can1 = true;
     bool can2 = true;
 
     const pcmk_resource_t *instance1 = (const pcmk_resource_t *) a;
     const pcmk_resource_t *instance2 = (const pcmk_resource_t *) b;
 
     CRM_ASSERT((instance1 != NULL) && (instance2 != NULL));
 
     node1 = instance1->private->fns->active_node(instance1, &nnodes1, NULL);
     node2 = instance2->private->fns->active_node(instance2, &nnodes2, NULL);
 
     /* If both instances are running and at least one is multiply
      * active, prefer instance that's running on fewer nodes.
      */
     if ((nnodes1 > 0) && (nnodes2 > 0)) {
         if (nnodes1 < nnodes2) {
             crm_trace("Assign %s (active on %d) before %s (active on %d): "
                       "less multiply active",
                       instance1->id, nnodes1, instance2->id, nnodes2);
             return -1;
 
         } else if (nnodes1 > nnodes2) {
             crm_trace("Assign %s (active on %d) after %s (active on %d): "
                       "more multiply active",
                       instance1->id, nnodes1, instance2->id, nnodes2);
             return 1;
         }
     }
 
     /* An instance that is either inactive or active on an allowed node is
      * preferred over an instance that is active on a no-longer-allowed node.
      */
     can1 = node_is_allowed(instance1, &node1);
     can2 = node_is_allowed(instance2, &node2);
     if (can1 && !can2) {
         crm_trace("Assign %s before %s: not active on a disallowed node",
                   instance1->id, instance2->id);
         return -1;
 
     } else if (!can1 && can2) {
         crm_trace("Assign %s after %s: active on a disallowed node",
                   instance1->id, instance2->id);
         return 1;
     }
 
     // Prefer instance with higher configured priority
     if (instance1->priority > instance2->priority) {
         crm_trace("Assign %s before %s: priority (%d > %d)",
                   instance1->id, instance2->id,
                   instance1->priority, instance2->priority);
         return -1;
 
     } else if (instance1->priority < instance2->priority) {
         crm_trace("Assign %s after %s: priority (%d < %d)",
                   instance1->id, instance2->id,
                   instance1->priority, instance2->priority);
         return 1;
     }
 
     // Prefer active instance
     if ((node1 == NULL) && (node2 == NULL)) {
         crm_trace("No assignment preference for %s vs. %s: inactive",
                   instance1->id, instance2->id);
         return 0;
 
     } else if (node1 == NULL) {
         crm_trace("Assign %s after %s: active", instance1->id, instance2->id);
         return 1;
 
     } else if (node2 == NULL) {
         crm_trace("Assign %s before %s: active", instance1->id, instance2->id);
         return -1;
     }
 
     // Prefer instance whose current node can run resources
     can1 = pcmk__node_available(node1, false, false);
     can2 = pcmk__node_available(node2, false, false);
     if (can1 && !can2) {
         crm_trace("Assign %s before %s: current node can run resources",
                   instance1->id, instance2->id);
         return -1;
 
     } else if (!can1 && can2) {
         crm_trace("Assign %s after %s: current node can't run resources",
                   instance1->id, instance2->id);
         return 1;
     }
 
     // Prefer instance whose parent is allowed to run on instance's current node
     node1 = pcmk__top_allowed_node(instance1, node1);
     node2 = pcmk__top_allowed_node(instance2, node2);
     if ((node1 == NULL) && (node2 == NULL)) {
         crm_trace("No assignment preference for %s vs. %s: "
                   "parent not allowed on either instance's current node",
                   instance1->id, instance2->id);
         return 0;
 
     } else if (node1 == NULL) {
         crm_trace("Assign %s after %s: parent not allowed on current node",
                   instance1->id, instance2->id);
         return 1;
 
     } else if (node2 == NULL) {
         crm_trace("Assign %s before %s: parent allowed on current node",
                   instance1->id, instance2->id);
         return -1;
     }
 
     // Prefer instance whose current node is running fewer other instances
     if (node1->count < node2->count) {
         crm_trace("Assign %s before %s: fewer active instances on current node",
                   instance1->id, instance2->id);
         return -1;
 
     } else if (node1->count > node2->count) {
         crm_trace("Assign %s after %s: more active instances on current node",
                   instance1->id, instance2->id);
         return 1;
     }
 
     // Prefer instance that isn't failed
     can1 = did_fail(instance1);
     can2 = did_fail(instance2);
     if (!can1 && can2) {
         crm_trace("Assign %s before %s: not failed",
                   instance1->id, instance2->id);
         return -1;
     } else if (can1 && !can2) {
         crm_trace("Assign %s after %s: failed",
                   instance1->id, instance2->id);
         return 1;
     }
 
     // Prefer instance with higher cumulative colocation score on current node
     rc = cmp_instance_by_colocation(instance1, instance2);
     if (rc != 0) {
         return rc;
     }
 
     // Prefer instance with lower instance number
     rc = pcmk__cmp_instance_number(instance1, instance2);
     if (rc < 0) {
         crm_trace("Assign %s before %s: instance number",
                   instance1->id, instance2->id);
     } else if (rc > 0) {
         crm_trace("Assign %s after %s: instance number",
                   instance1->id, instance2->id);
     } else {
         crm_trace("No assignment preference for %s vs. %s",
                   instance1->id, instance2->id);
     }
     return rc;
 }
 
 /*!
  * \internal
  * \brief Increment the parent's instance count after assigning an instance
  *
  * An instance's parent tracks how many instances have been assigned to each
  * node via its pcmk_node_t:count member. After assigning an instance to a node,
  * find the corresponding node in the parent's allowed table and increment it.
  *
  * \param[in,out] instance     Instance whose parent to update
  * \param[in]     assigned_to  Node to which the instance was assigned
  */
 static void
 increment_parent_count(pcmk_resource_t *instance,
                        const pcmk_node_t *assigned_to)
 {
     pcmk_node_t *allowed = NULL;
 
     if (assigned_to == NULL) {
         return;
     }
     allowed = pcmk__top_allowed_node(instance, assigned_to);
 
     if (allowed == NULL) {
         /* The instance is allowed on the node, but its parent isn't. This
          * shouldn't be possible if the resource is managed, and we won't be
          * able to limit the number of instances assigned to the node.
          */
         CRM_LOG_ASSERT(!pcmk_is_set(instance->flags, pcmk_rsc_managed));
 
     } else {
         allowed->count++;
     }
 }
 
 /*!
  * \internal
  * \brief Assign an instance to a node
  *
  * \param[in,out] instance      Clone instance or bundle replica container
  * \param[in]     prefer        If not NULL, attempt early assignment to this
  *                              node, if still the best choice; otherwise,
  *                              perform final assignment
  * \param[in]     max_per_node  Assign at most this many instances to one node
  *
  * \return Node to which \p instance is assigned
  */
 static const pcmk_node_t *
 assign_instance(pcmk_resource_t *instance, const pcmk_node_t *prefer,
                 int max_per_node)
 {
     pcmk_node_t *chosen = NULL;
 
     pcmk__rsc_trace(instance, "Assigning %s (preferring %s)", instance->id,
                     ((prefer == NULL)? "no node" : prefer->details->uname));
 
     if (pcmk_is_set(instance->flags, pcmk_rsc_assigning)) {
         pcmk__rsc_debug(instance,
                         "Assignment loop detected involving %s colocations",
                         instance->id);
         return NULL;
     }
     ban_unavailable_allowed_nodes(instance, max_per_node);
 
     // Failed early assignments are reversible (stop_if_fail=false)
-    chosen = instance->cmds->assign(instance, prefer, (prefer == NULL));
+    chosen = instance->private->cmds->assign(instance, prefer,
+                                             (prefer == NULL));
     increment_parent_count(instance, chosen);
     return chosen;
 }
 
 /*!
  * \internal
  * \brief Try to assign an instance to its current node early
  *
  * \param[in] rsc           Clone or bundle being assigned (for logs only)
  * \param[in] instance      Clone instance or bundle replica container
  * \param[in] current       Instance's current node
  * \param[in] max_per_node  Maximum number of instances per node
  * \param[in] available     Number of instances still available for assignment
  *
  * \return \c true if \p instance was successfully assigned to its current node,
  *         or \c false otherwise
  */
 static bool
 assign_instance_early(const pcmk_resource_t *rsc, pcmk_resource_t *instance,
                       const pcmk_node_t *current, int max_per_node,
                       int available)
 {
     const pcmk_node_t *chosen = NULL;
     int reserved = 0;
 
     pcmk_resource_t *parent = instance->parent;
     GHashTable *allowed_orig = NULL;
     GHashTable *allowed_orig_parent = parent->allowed_nodes;
     const pcmk_node_t *allowed_node = NULL;
 
     pcmk__rsc_trace(instance, "Trying to assign %s to its current node %s",
                     instance->id, pcmk__node_name(current));
 
     allowed_node = g_hash_table_lookup(instance->allowed_nodes,
                                        current->details->id);
     if (!pcmk__node_available(allowed_node, true, false)) {
         pcmk__rsc_info(instance,
                        "Not assigning %s to current node %s: unavailable",
                        instance->id, pcmk__node_name(current));
         return false;
     }
 
     /* On each iteration, if instance gets assigned to a node other than its
      * current one, we reserve one instance for the chosen node, unassign
      * instance, restore instance's original node tables, and try again. This
      * way, instances are proportionally assigned to nodes based on preferences,
      * but shuffling of specific instances is minimized. If a node will be
      * assigned instances at all, it preferentially receives instances that are
      * currently active there.
      *
      * parent->allowed_nodes tracks the number of instances assigned to each
      * node. If a node already has max_per_node instances assigned,
      * ban_unavailable_allowed_nodes() marks it as unavailable.
      *
      * In the end, we restore the original parent->allowed_nodes to undo the
      * changes to counts during tentative assignments. If we successfully
      * assigned instance to its current node, we increment that node's counter.
      */
 
     // Back up the allowed node tables of instance and its children recursively
     pcmk__copy_node_tables(instance, &allowed_orig);
 
     // Update instances-per-node counts in a scratch table
     parent->allowed_nodes = pcmk__copy_node_table(parent->allowed_nodes);
 
     while (reserved < available) {
         chosen = assign_instance(instance, current, max_per_node);
 
         if (pcmk__same_node(chosen, current)) {
             // Successfully assigned to current node
             break;
         }
 
         // Assignment updates scores, so restore to original state
         pcmk__rsc_debug(instance, "Rolling back node scores for %s",
                         instance->id);
         pcmk__restore_node_tables(instance, allowed_orig);
 
         if (chosen == NULL) {
             // Assignment failed, so give up
             pcmk__rsc_info(instance,
                            "Not assigning %s to current node %s: unavailable",
                            instance->id, pcmk__node_name(current));
             pcmk__set_rsc_flags(instance, pcmk_rsc_unassigned);
             break;
         }
 
         // We prefer more strongly to assign an instance to the chosen node
         pcmk__rsc_debug(instance,
                         "Not assigning %s to current node %s: %s is better",
                         instance->id, pcmk__node_name(current),
                         pcmk__node_name(chosen));
 
         // Reserve one instance for the chosen node and try again
         if (++reserved >= available) {
             pcmk__rsc_info(instance,
                            "Not assigning %s to current node %s: "
                            "other assignments are more important",
                            instance->id, pcmk__node_name(current));
 
         } else {
             pcmk__rsc_debug(instance,
                             "Reserved an instance of %s for %s. Retrying "
                             "assignment of %s to %s",
                             rsc->id, pcmk__node_name(chosen), instance->id,
                             pcmk__node_name(current));
         }
 
         // Clear this assignment (frees chosen); leave instance counts in parent
         pcmk__unassign_resource(instance);
         chosen = NULL;
     }
 
     g_hash_table_destroy(allowed_orig);
 
     // Restore original instances-per-node counts
     g_hash_table_destroy(parent->allowed_nodes);
     parent->allowed_nodes = allowed_orig_parent;
 
     if (chosen == NULL) {
         // Couldn't assign instance to current node
         return false;
     }
     pcmk__rsc_trace(instance, "Assigned %s to current node %s",
                     instance->id, pcmk__node_name(current));
     increment_parent_count(instance, chosen);
     return true;
 }
 
 /*!
  * \internal
  * \brief Reset the node counts of a resource's allowed nodes to zero
  *
  * \param[in,out] rsc  Resource to reset
  *
  * \return Number of nodes that are available to run resources
  */
 static unsigned int
 reset_allowed_node_counts(pcmk_resource_t *rsc)
 {
     unsigned int available_nodes = 0;
     pcmk_node_t *node = NULL;
     GHashTableIter iter;
 
     g_hash_table_iter_init(&iter, rsc->allowed_nodes);
     while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) {
         node->count = 0;
         if (pcmk__node_available(node, false, false)) {
             available_nodes++;
         }
     }
     return available_nodes;
 }
 
 /*!
  * \internal
  * \brief Check whether an instance has a preferred node
  *
  * \param[in] instance          Clone instance or bundle replica container
  * \param[in] optimal_per_node  Optimal number of instances per node
  *
  * \return Instance's current node if still available, otherwise NULL
  */
 static const pcmk_node_t *
 preferred_node(const pcmk_resource_t *instance, int optimal_per_node)
 {
     const pcmk_node_t *node = NULL;
     const pcmk_node_t *parent_node = NULL;
 
     // Check whether instance is active, healthy, and not yet assigned
     if ((instance->running_on == NULL)
         || !pcmk_is_set(instance->flags, pcmk_rsc_unassigned)
         || pcmk_is_set(instance->flags, pcmk_rsc_failed)) {
         return NULL;
     }
 
     // Check whether instance's current node can run resources
     node = pcmk__current_node(instance);
     if (!pcmk__node_available(node, true, false)) {
         pcmk__rsc_trace(instance, "Not assigning %s to %s early (unavailable)",
                         instance->id, pcmk__node_name(node));
         return NULL;
     }
 
     // Check whether node already has optimal number of instances assigned
     parent_node = pcmk__top_allowed_node(instance, node);
     if ((parent_node != NULL) && (parent_node->count >= optimal_per_node)) {
         pcmk__rsc_trace(instance,
                         "Not assigning %s to %s early "
                         "(optimal instances already assigned)",
                         instance->id, pcmk__node_name(node));
         return NULL;
     }
 
     return node;
 }
 
 /*!
  * \internal
  * \brief Assign collective instances to nodes
  *
  * \param[in,out] collective    Clone or bundle resource being assigned
  * \param[in,out] instances     List of clone instances or bundle containers
  * \param[in]     max_total     Maximum instances to assign in total
  * \param[in]     max_per_node  Maximum instances to assign to any one node
  */
 void
 pcmk__assign_instances(pcmk_resource_t *collective, GList *instances,
                        int max_total, int max_per_node)
 {
     // Reuse node count to track number of assigned instances
     unsigned int available_nodes = reset_allowed_node_counts(collective);
 
     int optimal_per_node = 0;
     int assigned = 0;
     GList *iter = NULL;
     pcmk_resource_t *instance = NULL;
     const pcmk_node_t *current = NULL;
 
     if (available_nodes > 0) {
         optimal_per_node = max_total / available_nodes;
     }
     if (optimal_per_node < 1) {
         optimal_per_node = 1;
     }
 
     pcmk__rsc_debug(collective,
                     "Assigning up to %d %s instance%s to up to %u node%s "
                     "(at most %d per host, %d optimal)",
                     max_total, collective->id, pcmk__plural_s(max_total),
                     available_nodes, pcmk__plural_s(available_nodes),
                     max_per_node, optimal_per_node);
 
     // Assign as many instances as possible to their current location
     for (iter = instances; (iter != NULL) && (assigned < max_total);
          iter = iter->next) {
         int available = max_total - assigned;
 
         instance = iter->data;
         if (!pcmk_is_set(instance->flags, pcmk_rsc_unassigned)) {
             continue;   // Already assigned
         }
 
         current = preferred_node(instance, optimal_per_node);
         if ((current != NULL)
             && assign_instance_early(collective, instance, current,
                                      max_per_node, available)) {
             assigned++;
         }
     }
 
     pcmk__rsc_trace(collective, "Assigned %d of %d instance%s to current node",
                     assigned, max_total, pcmk__plural_s(max_total));
 
     for (iter = instances; iter != NULL; iter = iter->next) {
         instance = (pcmk_resource_t *) iter->data;
 
         if (!pcmk_is_set(instance->flags, pcmk_rsc_unassigned)) {
             continue; // Already assigned
         }
 
         if (instance->running_on != NULL) {
             current = pcmk__current_node(instance);
             if (pcmk__top_allowed_node(instance, current) == NULL) {
                 const char *unmanaged = "";
 
                 if (!pcmk_is_set(instance->flags, pcmk_rsc_managed)) {
                     unmanaged = "Unmanaged resource ";
                 }
                 crm_notice("%s%s is running on %s which is no longer allowed",
                            unmanaged, instance->id, pcmk__node_name(current));
             }
         }
 
         if (assigned >= max_total) {
             pcmk__rsc_debug(collective,
                             "Not assigning %s because maximum %d instances "
                             "already assigned",
                             instance->id, max_total);
             resource_location(instance, NULL, -PCMK_SCORE_INFINITY,
                               "collective_limit_reached", collective->cluster);
 
         } else if (assign_instance(instance, NULL, max_per_node) != NULL) {
             assigned++;
         }
     }
 
     pcmk__rsc_debug(collective, "Assigned %d of %d possible instance%s of %s",
                     assigned, max_total, pcmk__plural_s(max_total),
                     collective->id);
 }
 
 enum instance_state {
     instance_starting   = (1 << 0),
     instance_stopping   = (1 << 1),
 
     /* This indicates that some instance is restarting. It's not the same as
      * instance_starting|instance_stopping, which would indicate that some
      * instance is starting, and some instance (not necessarily the same one) is
      * stopping.
      */
     instance_restarting = (1 << 2),
 
     instance_active     = (1 << 3),
 
     instance_all        = instance_starting|instance_stopping
                           |instance_restarting|instance_active,
 };
 
 /*!
  * \internal
  * \brief Check whether an instance is active, starting, and/or stopping
  *
  * \param[in]     instance  Clone instance or bundle replica container
  * \param[in,out] state     Whether any instance is starting, stopping, etc.
  */
 static void
 check_instance_state(const pcmk_resource_t *instance, uint32_t *state)
 {
     const GList *iter = NULL;
     uint32_t instance_state = 0; // State of just this instance
 
     // No need to check further if all conditions have already been detected
     if (pcmk_all_flags_set(*state, instance_all)) {
         return;
     }
 
     // If instance is a collective (a cloned group), check its children instead
     if (instance->variant > pcmk_rsc_variant_primitive) {
         for (iter = instance->children;
              (iter != NULL) && !pcmk_all_flags_set(*state, instance_all);
              iter = iter->next) {
             check_instance_state((const pcmk_resource_t *) iter->data, state);
         }
         return;
     }
 
     // If we get here, instance is a primitive
 
     if (instance->running_on != NULL) {
         instance_state |= instance_active;
     }
 
     // Check each of the instance's actions for runnable start or stop
     for (iter = instance->actions;
          (iter != NULL) && !pcmk_all_flags_set(instance_state,
                                                instance_starting
                                                |instance_stopping);
          iter = iter->next) {
 
         const pcmk_action_t *action = (const pcmk_action_t *) iter->data;
         const bool optional = pcmk_is_set(action->flags, pcmk_action_optional);
 
         if (pcmk__str_eq(PCMK_ACTION_START, action->task, pcmk__str_none)) {
             if (!optional
                 && pcmk_is_set(action->flags, pcmk_action_runnable)) {
 
                 pcmk__rsc_trace(instance, "Instance is starting due to %s",
                                 action->uuid);
                 instance_state |= instance_starting;
             } else {
                 pcmk__rsc_trace(instance, "%s doesn't affect %s state (%s)",
                                 action->uuid, instance->id,
                                 (optional? "optional" : "unrunnable"));
             }
 
         } else if (pcmk__str_eq(PCMK_ACTION_STOP, action->task,
                                 pcmk__str_none)) {
             /* Only stop actions can be pseudo-actions for primitives. That
              * indicates that the node they are on is being fenced, so the stop
              * is implied rather than actually executed.
              */
             if (!optional
                 && pcmk_any_flags_set(action->flags, pcmk_action_pseudo
                                                      |pcmk_action_runnable)) {
                 pcmk__rsc_trace(instance, "Instance is stopping due to %s",
                                 action->uuid);
                 instance_state |= instance_stopping;
             } else {
                 pcmk__rsc_trace(instance, "%s doesn't affect %s state (%s)",
                                 action->uuid, instance->id,
                                 (optional? "optional" : "unrunnable"));
             }
         }
     }
 
     if (pcmk_all_flags_set(instance_state,
                            instance_starting|instance_stopping)) {
         instance_state |= instance_restarting;
     }
     *state |= instance_state;
 }
 
 /*!
  * \internal
  * \brief Create actions for collective resource instances
  *
  * \param[in,out] collective    Clone or bundle resource to create actions for
  * \param[in,out] instances     List of clone instances or bundle containers
  */
 void
 pcmk__create_instance_actions(pcmk_resource_t *collective, GList *instances)
 {
     uint32_t state = 0;
 
     pcmk_action_t *stop = NULL;
     pcmk_action_t *stopped = NULL;
 
     pcmk_action_t *start = NULL;
     pcmk_action_t *started = NULL;
 
     pcmk__rsc_trace(collective, "Creating collective instance actions for %s",
                     collective->id);
 
     // Create actions for each instance appropriate to its variant
     for (GList *iter = instances; iter != NULL; iter = iter->next) {
         pcmk_resource_t *instance = (pcmk_resource_t *) iter->data;
 
-        instance->cmds->create_actions(instance);
+        instance->private->cmds->create_actions(instance);
         check_instance_state(instance, &state);
     }
 
     // Create pseudo-actions for rsc start and started
     start = pe__new_rsc_pseudo_action(collective, PCMK_ACTION_START,
                                       !pcmk_is_set(state, instance_starting),
                                       true);
     started = pe__new_rsc_pseudo_action(collective, PCMK_ACTION_RUNNING,
                                         !pcmk_is_set(state, instance_starting),
                                         false);
     started->priority = PCMK_SCORE_INFINITY;
     if (pcmk_any_flags_set(state, instance_active|instance_starting)) {
         pcmk__set_action_flags(started, pcmk_action_runnable);
     }
 
     // Create pseudo-actions for rsc stop and stopped
     stop = pe__new_rsc_pseudo_action(collective, PCMK_ACTION_STOP,
                                      !pcmk_is_set(state, instance_stopping),
                                      true);
     stopped = pe__new_rsc_pseudo_action(collective, PCMK_ACTION_STOPPED,
                                         !pcmk_is_set(state, instance_stopping),
                                         true);
     stopped->priority = PCMK_SCORE_INFINITY;
     if (!pcmk_is_set(state, instance_restarting)) {
         pcmk__set_action_flags(stop, pcmk_action_migratable);
     }
 
     if (pcmk__is_clone(collective)) {
         pe__create_clone_notif_pseudo_ops(collective, start, started, stop,
                                           stopped);
     }
 }
 
 /*!
  * \internal
  * \brief Get a list of clone instances or bundle replica containers
  *
  * \param[in] rsc  Clone or bundle resource
  *
  * \return Clone instances if \p rsc is a clone, or a newly created list of
  *         \p rsc's replica containers if \p rsc is a bundle
  * \note The caller must call free_instance_list() on the result when the list
  *       is no longer needed.
  */
 static inline GList *
 get_instance_list(const pcmk_resource_t *rsc)
 {
     if (pcmk__is_bundle(rsc)) {
         return pe__bundle_containers(rsc);
     } else {
         return rsc->children;
     }
 }
 
 /*!
  * \internal
  * \brief Free any memory created by get_instance_list()
  *
  * \param[in]     rsc   Clone or bundle resource passed to get_instance_list()
  * \param[in,out] list  Return value of get_instance_list() for \p rsc
  */
 static inline void
 free_instance_list(const pcmk_resource_t *rsc, GList *list)
 {
     if (list != rsc->children) {
         g_list_free(list);
     }
 }
 
 /*!
  * \internal
  * \brief Check whether an instance is compatible with a role and node
  *
  * \param[in] instance  Clone instance or bundle replica container
  * \param[in] node      Instance must match this node
  * \param[in] role      If not pcmk_role_unknown, instance must match this role
  * \param[in] current   If true, compare instance's original node and role,
  *                      otherwise compare assigned next node and role
  *
  * \return true if \p instance is compatible with \p node and \p role,
  *         otherwise false
  */
 bool
 pcmk__instance_matches(const pcmk_resource_t *instance, const pcmk_node_t *node,
                        enum rsc_role_e role, bool current)
 {
     pcmk_node_t *instance_node = NULL;
 
     CRM_CHECK((instance != NULL) && (node != NULL), return false);
 
     if ((role != pcmk_role_unknown)
         && (role != instance->private->fns->state(instance, current))) {
         pcmk__rsc_trace(instance,
                         "%s is not a compatible instance (role is not %s)",
                         instance->id, pcmk_role_text(role));
         return false;
     }
 
     if (!is_set_recursive(instance, pcmk_rsc_blocked, true)) {
         // We only want instances that haven't failed
         instance_node = instance->private->fns->location(instance, NULL,
                                                          current);
     }
 
     if (instance_node == NULL) {
         pcmk__rsc_trace(instance,
                         "%s is not a compatible instance "
                         "(not assigned to a node)",
                         instance->id);
         return false;
     }
 
     if (!pcmk__same_node(instance_node, node)) {
         pcmk__rsc_trace(instance,
                         "%s is not a compatible instance "
                         "(assigned to %s not %s)",
                         instance->id, pcmk__node_name(instance_node),
                         pcmk__node_name(node));
         return false;
     }
 
     return true;
 }
 
 #define display_role(r) \
     (((r) == pcmk_role_unknown)? "matching" : pcmk_role_text(r))
 
 /*!
  * \internal
  * \brief Find an instance that matches a given resource by node and role
  *
  * \param[in] match_rsc  Resource that instance must match (for logging only)
  * \param[in] rsc        Clone or bundle resource to check for matching instance
  * \param[in] node       Instance must match this node
  * \param[in] role       If not pcmk_role_unknown, instance must match this role
  * \param[in] current    If true, compare instance's original node and role,
  *                       otherwise compare assigned next node and role
  *
  * \return \p rsc instance matching \p node and \p role if any, otherwise NULL
  */
 static pcmk_resource_t *
 find_compatible_instance_on_node(const pcmk_resource_t *match_rsc,
                                  const pcmk_resource_t *rsc,
                                  const pcmk_node_t *node, enum rsc_role_e role,
                                  bool current)
 {
     GList *instances = NULL;
 
     instances = get_instance_list(rsc);
     for (GList *iter = instances; iter != NULL; iter = iter->next) {
         pcmk_resource_t *instance = (pcmk_resource_t *) iter->data;
 
         if (pcmk__instance_matches(instance, node, role, current)) {
             pcmk__rsc_trace(match_rsc,
                             "Found %s %s instance %s compatible with %s on %s",
                             display_role(role), rsc->id, instance->id,
                             match_rsc->id, pcmk__node_name(node));
             free_instance_list(rsc, instances); // Only frees list, not contents
             return instance;
         }
     }
     free_instance_list(rsc, instances);
 
     pcmk__rsc_trace(match_rsc,
                     "No %s %s instance found compatible with %s on %s",
                     display_role(role), rsc->id, match_rsc->id,
                     pcmk__node_name(node));
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Find a clone instance or bundle container compatible with a resource
  *
  * \param[in] match_rsc  Resource that instance must match
  * \param[in] rsc        Clone or bundle resource to check for matching instance
  * \param[in] role       If not pcmk_role_unknown, instance must match this role
  * \param[in] current    If true, compare instance's original node and role,
  *                       otherwise compare assigned next node and role
  *
  * \return Compatible (by \p role and \p match_rsc location) instance of \p rsc
  *         if any, otherwise NULL
  */
 pcmk_resource_t *
 pcmk__find_compatible_instance(const pcmk_resource_t *match_rsc,
                                const pcmk_resource_t *rsc, enum rsc_role_e role,
                                bool current)
 {
     pcmk_resource_t *instance = NULL;
     GList *nodes = NULL;
     const pcmk_node_t *node = NULL;
 
     // If match_rsc has a node, check only that node
     node = match_rsc->private->fns->location(match_rsc, NULL, current);
     if (node != NULL) {
         return find_compatible_instance_on_node(match_rsc, rsc, node, role,
                                                 current);
     }
 
     // Otherwise check for an instance matching any of match_rsc's allowed nodes
     nodes = pcmk__sort_nodes(g_hash_table_get_values(match_rsc->allowed_nodes),
                              NULL);
     for (GList *iter = nodes; (iter != NULL) && (instance == NULL);
          iter = iter->next) {
         instance = find_compatible_instance_on_node(match_rsc, rsc,
                                                     (pcmk_node_t *) iter->data,
                                                     role, current);
     }
 
     if (instance == NULL) {
         pcmk__rsc_debug(rsc, "No %s instance found compatible with %s",
                         rsc->id, match_rsc->id);
     }
     g_list_free(nodes);
     return instance;
 }
 
 /*!
  * \internal
  * \brief Unassign an instance if mandatory ordering has no interleave match
  *
  * \param[in]     first          'First' action in an ordering
  * \param[in]     then           'Then' action in an ordering
  * \param[in,out] then_instance  'Then' instance that has no interleave match
  * \param[in]     type           Group of enum pcmk__action_relation_flags
  * \param[in]     current        If true, "then" action is stopped or demoted
  *
  * \return true if \p then_instance was unassigned, otherwise false
  */
 static bool
 unassign_if_mandatory(const pcmk_action_t *first, const pcmk_action_t *then,
                       pcmk_resource_t *then_instance, uint32_t type,
                       bool current)
 {
     // Allow "then" instance to go down even without an interleave match
     if (current) {
         pcmk__rsc_trace(then->rsc,
                         "%s has no instance to order before stopping "
                         "or demoting %s",
                         first->rsc->id, then_instance->id);
 
     /* If the "first" action must be runnable, but there is no "first"
      * instance, the "then" instance must not be allowed to come up.
      */
     } else if (pcmk_any_flags_set(type, pcmk__ar_unrunnable_first_blocks
                                         |pcmk__ar_first_implies_then)) {
         pcmk__rsc_info(then->rsc,
                        "Inhibiting %s from being active "
                        "because there is no %s instance to interleave",
                        then_instance->id, first->rsc->id);
         return pcmk__assign_resource(then_instance, NULL, true, true);
     }
     return false;
 }
 
 /*!
  * \internal
  * \brief Find first matching action for a clone instance or bundle container
  *
  * \param[in] action       Action in an interleaved ordering
  * \param[in] instance     Clone instance or bundle container being interleaved
  * \param[in] action_name  Action to look for
  * \param[in] node         If not NULL, require action to be on this node
  * \param[in] for_first    If true, \p instance is the 'first' resource in the
  *                         ordering, otherwise it is the 'then' resource
  *
  * \return First action for \p instance (or in some cases if \p instance is a
  *         bundle container, its containerized resource) that matches
  *         \p action_name and \p node if any, otherwise NULL
  */
 static pcmk_action_t *
 find_instance_action(const pcmk_action_t *action, const pcmk_resource_t *instance,
                      const char *action_name, const pcmk_node_t *node,
                      bool for_first)
 {
     const pcmk_resource_t *rsc = NULL;
     pcmk_action_t *matching_action = NULL;
 
     /* If instance is a bundle container, sometimes we should interleave the
      * action for the container itself, and sometimes for the containerized
      * resource.
      *
      * For example, given "start bundle A then bundle B", B likely requires the
      * service inside A's container to be active, rather than just the
      * container, so we should interleave the action for A's containerized
      * resource. On the other hand, it's possible B's container itself requires
      * something from A, so we should interleave the action for B's container.
      *
      * Essentially, for 'first', we should use the containerized resource for
      * everything except stop, and for 'then', we should use the container for
      * everything except promote and demote (which can only be performed on the
      * containerized resource).
      */
     if ((for_first && !pcmk__str_any_of(action->task, PCMK_ACTION_STOP,
                                         PCMK_ACTION_STOPPED, NULL))
 
         || (!for_first && pcmk__str_any_of(action->task, PCMK_ACTION_PROMOTE,
                                            PCMK_ACTION_PROMOTED,
                                            PCMK_ACTION_DEMOTE,
                                            PCMK_ACTION_DEMOTED, NULL))) {
 
         rsc = pe__get_rsc_in_container(instance);
     }
     if (rsc == NULL) {
         rsc = instance; // No containerized resource, use instance itself
     } else {
         node = NULL; // Containerized actions are on bundle-created guest
     }
 
     matching_action = find_first_action(rsc->actions, NULL, action_name, node);
     if (matching_action != NULL) {
         return matching_action;
     }
 
     if (pcmk_is_set(instance->flags, pcmk_rsc_removed)
         || pcmk__str_any_of(action_name, PCMK_ACTION_STOP, PCMK_ACTION_DEMOTE,
                             NULL)) {
         crm_trace("No %s action found for %s%s",
                   action_name,
                   pcmk_is_set(instance->flags, pcmk_rsc_removed)? "orphan " : "",
                   instance->id);
     } else {
         crm_err("No %s action found for %s to interleave (bug?)",
                 action_name, instance->id);
     }
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Get the original action name of a bundle or clone action
  *
  * Given an action for a bundle or clone, get the original action name,
  * mapping notify to the action being notified, and if the instances are
  * primitives, mapping completion actions to the action that was completed
  * (for example, stopped to stop).
  *
  * \param[in] action  Clone or bundle action to check
  *
  * \return Original action name for \p action
  */
 static const char *
 orig_action_name(const pcmk_action_t *action)
 {
     // Any instance will do
     const pcmk_resource_t *instance = action->rsc->children->data;
 
     char *action_type = NULL;
     const char *action_name = action->task;
     enum action_tasks orig_task = pcmk_action_unspecified;
 
     if (pcmk__strcase_any_of(action->task, PCMK_ACTION_NOTIFY,
                              PCMK_ACTION_NOTIFIED, NULL)) {
         // action->uuid is RSC_(confirmed-){pre,post}_notify_ACTION_INTERVAL
         CRM_CHECK(parse_op_key(action->uuid, NULL, &action_type, NULL),
                   return pcmk_action_text(pcmk_action_unspecified));
         action_name = strstr(action_type, "_notify_");
         CRM_CHECK(action_name != NULL,
                   return pcmk_action_text(pcmk_action_unspecified));
         action_name += strlen("_notify_");
     }
     orig_task = get_complex_task(instance, action_name);
     free(action_type);
     return pcmk_action_text(orig_task);
 }
 
 /*!
  * \internal
  * \brief Update two interleaved actions according to an ordering between them
  *
  * Given information about an ordering of two interleaved actions, update the
  * actions' flags (and runnable_before members if appropriate) as appropriate
  * for the ordering. Effects may cascade to other orderings involving the
  * actions as well.
  *
  * \param[in,out] first     'First' action in an ordering
  * \param[in,out] then      'Then' action in an ordering
  * \param[in]     node      If not NULL, limit scope of ordering to this node
  * \param[in]     filter    Action flags to limit scope of certain updates (may
  *                          include pcmk_action_optional to affect only
  *                          mandatory actions, and pcmk_action_runnable to
  *                          affect only runnable actions)
  * \param[in]     type      Group of enum pcmk__action_relation_flags to apply
  *
  * \return Group of enum pcmk__updated flags indicating what was updated
  */
 static uint32_t
 update_interleaved_actions(pcmk_action_t *first, pcmk_action_t *then,
                            const pcmk_node_t *node, uint32_t filter,
                            uint32_t type)
 {
     GList *instances = NULL;
     uint32_t changed = pcmk__updated_none;
     const char *orig_first_task = orig_action_name(first);
 
     // Stops and demotes must be interleaved with instance on current node
     bool current = pcmk__ends_with(first->uuid, "_" PCMK_ACTION_STOPPED "_0")
                    || pcmk__ends_with(first->uuid,
                                       "_" PCMK_ACTION_DEMOTED "_0");
 
     // Update the specified actions for each "then" instance individually
     instances = get_instance_list(then->rsc);
     for (GList *iter = instances; iter != NULL; iter = iter->next) {
         pcmk_resource_t *first_instance = NULL;
         pcmk_resource_t *then_instance = iter->data;
 
         pcmk_action_t *first_action = NULL;
         pcmk_action_t *then_action = NULL;
 
         // Find a "first" instance to interleave with this "then" instance
         first_instance = pcmk__find_compatible_instance(then_instance,
                                                         first->rsc,
                                                         pcmk_role_unknown,
                                                         current);
 
         if (first_instance == NULL) { // No instance can be interleaved
             if (unassign_if_mandatory(first, then, then_instance, type,
                                       current)) {
                 pcmk__set_updated_flags(changed, first, pcmk__updated_then);
             }
             continue;
         }
 
         first_action = find_instance_action(first, first_instance,
                                             orig_first_task, node, true);
         if (first_action == NULL) {
             continue;
         }
 
         then_action = find_instance_action(then, then_instance, then->task,
                                            node, false);
         if (then_action == NULL) {
             continue;
         }
 
         if (order_actions(first_action, then_action, type)) {
             pcmk__set_updated_flags(changed, first,
                                     pcmk__updated_first|pcmk__updated_then);
         }
 
-        changed |= then_instance->cmds->update_ordered_actions(
+        changed |= then_instance->private->cmds->update_ordered_actions(
             first_action, then_action, node,
-            first_instance->cmds->action_flags(first_action, node), filter,
-            type, then->rsc->cluster);
+            first_instance->private->cmds->action_flags(first_action, node),
+            filter, type, then->rsc->cluster);
     }
     free_instance_list(then->rsc, instances);
     return changed;
 }
 
 /*!
  * \internal
  * \brief Check whether two actions in an ordering can be interleaved
  *
  * \param[in] first  'First' action in the ordering
  * \param[in] then   'Then' action in the ordering
  *
  * \return true if \p first and \p then can be interleaved, otherwise false
  */
 static bool
 can_interleave_actions(const pcmk_action_t *first, const pcmk_action_t *then)
 {
     bool interleave = false;
     pcmk_resource_t *rsc = NULL;
 
     if ((first->rsc == NULL) || (then->rsc == NULL)) {
         crm_trace("Not interleaving %s with %s: not resource actions",
                   first->uuid, then->uuid);
         return false;
     }
 
     if (first->rsc == then->rsc) {
         crm_trace("Not interleaving %s with %s: same resource",
                   first->uuid, then->uuid);
         return false;
     }
 
     if ((first->rsc->variant < pcmk_rsc_variant_clone)
         || (then->rsc->variant < pcmk_rsc_variant_clone)) {
         crm_trace("Not interleaving %s with %s: not clones or bundles",
                   first->uuid, then->uuid);
         return false;
     }
 
     if (pcmk__ends_with(then->uuid, "_stop_0")
         || pcmk__ends_with(then->uuid, "_demote_0")) {
         rsc = first->rsc;
     } else {
         rsc = then->rsc;
     }
 
     interleave = crm_is_true(g_hash_table_lookup(rsc->meta,
                                                  PCMK_META_INTERLEAVE));
     pcmk__rsc_trace(rsc, "'%s then %s' will %sbe interleaved (based on %s)",
                     first->uuid, then->uuid, (interleave? "" : "not "),
                     rsc->id);
     return interleave;
 }
 
 /*!
  * \internal
  * \brief Update non-interleaved instance actions according to an ordering
  *
  * Given information about an ordering of two non-interleaved actions, update
  * the actions' flags (and runnable_before members if appropriate) as
  * appropriate for the ordering. Effects may cascade to other orderings
  * involving the actions as well.
  *
  * \param[in,out] instance  Clone instance or bundle container
  * \param[in,out] first     "First" action in ordering
  * \param[in]     then      "Then" action in ordering (for \p instance's parent)
  * \param[in]     node      If not NULL, limit scope of ordering to this node
  * \param[in]     flags     Action flags for \p first for ordering purposes
  * \param[in]     filter    Action flags to limit scope of certain updates (may
  *                          include pcmk_action_optional to affect only
  *                          mandatory actions, and pcmk_action_runnable to
  *                          affect only runnable actions)
  * \param[in]     type      Group of enum pcmk__action_relation_flags to apply
  *
  * \return Group of enum pcmk__updated flags indicating what was updated
  */
 static uint32_t
 update_noninterleaved_actions(pcmk_resource_t *instance, pcmk_action_t *first,
                               const pcmk_action_t *then, const pcmk_node_t *node,
                               uint32_t flags, uint32_t filter, uint32_t type)
 {
     pcmk_action_t *instance_action = NULL;
     uint32_t instance_flags = 0;
     uint32_t changed = pcmk__updated_none;
 
     // Check whether instance has an equivalent of "then" action
     instance_action = find_first_action(instance->actions, NULL, then->task,
                                         node);
     if (instance_action == NULL) {
         return changed;
     }
 
     // Check whether action is runnable
-    instance_flags = instance->cmds->action_flags(instance_action, node);
+    instance_flags = instance->private->cmds->action_flags(instance_action,
+                                                           node);
     if (!pcmk_is_set(instance_flags, pcmk_action_runnable)) {
         return changed;
     }
 
     // If so, update actions for the instance
-    changed = instance->cmds->update_ordered_actions(first, instance_action,
-                                                     node, flags, filter, type,
-                                                     instance->cluster);
+    changed = instance->private->cmds->update_ordered_actions(first,
+                                                              instance_action,
+                                                              node, flags,
+                                                              filter, type,
+                                                              instance->cluster);
 
     // Propagate any changes to later actions
     if (pcmk_is_set(changed, pcmk__updated_then)) {
         for (GList *after_iter = instance_action->actions_after;
              after_iter != NULL; after_iter = after_iter->next) {
             pcmk__related_action_t *after = after_iter->data;
 
             pcmk__update_action_for_orderings(after->action, instance->cluster);
         }
     }
 
     return changed;
 }
 
 /*!
  * \internal
  * \brief Update two actions according to an ordering between them
  *
  * Given information about an ordering of two clone or bundle actions, update
  * the actions' flags (and runnable_before members if appropriate) as
  * appropriate for the ordering. Effects may cascade to other orderings
  * involving the actions as well.
  *
  * \param[in,out] first      'First' action in an ordering
  * \param[in,out] then       'Then' action in an ordering
  * \param[in]     node       If not NULL, limit scope of ordering to this node
  *                           (only used when interleaving instances)
  * \param[in]     flags      Action flags for \p first for ordering purposes
  * \param[in]     filter     Action flags to limit scope of certain updates (may
  *                           include pcmk_action_optional to affect only
  *                           mandatory actions, and pcmk_action_runnable to
  *                           affect only runnable actions)
  * \param[in]     type       Group of enum pcmk__action_relation_flags to apply
  * \param[in,out] scheduler  Scheduler data
  *
  * \return Group of enum pcmk__updated flags indicating what was updated
  */
 uint32_t
 pcmk__instance_update_ordered_actions(pcmk_action_t *first, pcmk_action_t *then,
                                       const pcmk_node_t *node, uint32_t flags,
                                       uint32_t filter, uint32_t type,
                                       pcmk_scheduler_t *scheduler)
 {
     CRM_ASSERT((first != NULL) && (then != NULL) && (scheduler != NULL));
 
     if (then->rsc == NULL) {
         return pcmk__updated_none;
 
     } else if (can_interleave_actions(first, then)) {
         return update_interleaved_actions(first, then, node, filter, type);
 
     } else {
         uint32_t changed = pcmk__updated_none;
         GList *instances = get_instance_list(then->rsc);
 
         // Update actions for the clone or bundle resource itself
         changed |= pcmk__update_ordered_actions(first, then, node, flags,
                                                 filter, type, scheduler);
 
         // Update the 'then' clone instances or bundle containers individually
         for (GList *iter = instances; iter != NULL; iter = iter->next) {
             pcmk_resource_t *instance = iter->data;
 
             changed |= update_noninterleaved_actions(instance, first, then,
                                                      node, flags, filter, type);
         }
         free_instance_list(then->rsc, instances);
         return changed;
     }
 }
 
 #define pe__clear_action_summary_flags(flags, action, flag) do {        \
         flags = pcmk__clear_flags_as(__func__, __LINE__, LOG_TRACE,     \
                                      "Action summary", action->rsc->id, \
                                      flags, flag, #flag);               \
     } while (0)
 
 /*!
  * \internal
  * \brief Return action flags for a given clone or bundle action
  *
  * \param[in,out] action     Action for a clone or bundle
  * \param[in]     instances  Clone instances or bundle containers
  * \param[in]     node       If not NULL, limit effects to this node
  *
  * \return Flags appropriate to \p action on \p node
  */
 uint32_t
 pcmk__collective_action_flags(pcmk_action_t *action, const GList *instances,
                               const pcmk_node_t *node)
 {
     bool any_runnable = false;
     const char *action_name = orig_action_name(action);
 
     // Set original assumptions (optional and runnable may be cleared below)
     uint32_t flags = pcmk_action_optional
                      |pcmk_action_runnable
                      |pcmk_action_pseudo;
 
     for (const GList *iter = instances; iter != NULL; iter = iter->next) {
         const pcmk_resource_t *instance = iter->data;
         const pcmk_node_t *instance_node = NULL;
         pcmk_action_t *instance_action = NULL;
         uint32_t instance_flags;
 
         // Node is relevant only to primitive instances
         if (pcmk__is_primitive(instance)) {
             instance_node = node;
         }
 
         instance_action = find_first_action(instance->actions, NULL,
                                             action_name, instance_node);
         if (instance_action == NULL) {
             pcmk__rsc_trace(action->rsc, "%s has no %s action on %s",
                             instance->id, action_name, pcmk__node_name(node));
             continue;
         }
 
         pcmk__rsc_trace(action->rsc, "%s has %s for %s on %s",
                         instance->id, instance_action->uuid, action_name,
                         pcmk__node_name(node));
 
-        instance_flags = instance->cmds->action_flags(instance_action, node);
+        instance_flags = instance->private->cmds->action_flags(instance_action,
+                                                               node);
 
         // If any instance action is mandatory, so is the collective action
         if (pcmk_is_set(flags, pcmk_action_optional)
             && !pcmk_is_set(instance_flags, pcmk_action_optional)) {
             pcmk__rsc_trace(instance, "%s is mandatory because %s is",
                             action->uuid, instance_action->uuid);
             pe__clear_action_summary_flags(flags, action,
                                            pcmk_action_optional);
             pcmk__clear_action_flags(action, pcmk_action_optional);
         }
 
         // If any instance action is runnable, so is the collective action
         if (pcmk_is_set(instance_flags, pcmk_action_runnable)) {
             any_runnable = true;
         }
     }
 
     if (!any_runnable) {
         pcmk__rsc_trace(action->rsc,
                         "%s is not runnable because no instance can run %s",
                         action->uuid, action_name);
         pe__clear_action_summary_flags(flags, action, pcmk_action_runnable);
         if (node == NULL) {
             pcmk__clear_action_flags(action, pcmk_action_runnable);
         }
     }
 
     return flags;
 }
diff --git a/lib/pacemaker/pcmk_sched_location.c b/lib/pacemaker/pcmk_sched_location.c
index b51dcd119b..0160df37d0 100644
--- a/lib/pacemaker/pcmk_sched_location.c
+++ b/lib/pacemaker/pcmk_sched_location.c
@@ -1,726 +1,726 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <stdbool.h>
 #include <glib.h>
 
 #include <crm/crm.h>
 #include <crm/common/rules_internal.h>
 #include <crm/pengine/status.h>
 #include <crm/pengine/rules.h>
 #include <pacemaker-internal.h>
 
 #include "libpacemaker_private.h"
 
 static int
 get_node_score(const char *rule, const char *score, bool raw,
                pcmk_node_t *node, pcmk_resource_t *rsc)
 {
     int score_f = 0;
 
     if (score == NULL) {
         pcmk__config_warn("Rule %s: no score specified (assuming 0)", rule);
 
     } else if (raw) {
         score_f = char2score(score);
 
     } else {
         const char *target = NULL;
         const char *attr_score = NULL;
 
         target = g_hash_table_lookup(rsc->meta,
                                      PCMK_META_CONTAINER_ATTRIBUTE_TARGET);
 
         attr_score = pcmk__node_attr(node, score, target,
                                      pcmk__rsc_node_current);
         if (attr_score == NULL) {
             crm_debug("Rule %s: %s did not have a value for %s",
                       rule, pcmk__node_name(node), score);
             score_f = -PCMK_SCORE_INFINITY;
 
         } else {
             crm_debug("Rule %s: %s had value %s for %s",
                       rule, pcmk__node_name(node), attr_score, score);
             score_f = char2score(attr_score);
         }
     }
     return score_f;
 }
 
 /*!
  * \internal
  * \brief Parse a role configuration for a location constraint
  *
  * \param[in]  role_spec  Role specification
  * \param[out] role       Where to store parsed role
  *
  * \return true if role specification is valid, otherwise false
  */
 static bool
 parse_location_role(const char *role_spec, enum rsc_role_e *role)
 {
     if (role_spec == NULL) {
         *role = pcmk_role_unknown;
         return true;
     }
 
     *role = pcmk_parse_role(role_spec);
     switch (*role) {
         case pcmk_role_unknown:
             return false;
 
         case pcmk_role_started:
         case pcmk_role_unpromoted:
             /* Any promotable clone instance cannot be promoted without being in
              * the unpromoted role first. Therefore, any constraint for the
              * started or unpromoted role applies to every role.
              */
             *role = pcmk_role_unknown;
             break;
 
         default:
             break;
     }
     return true;
 }
 
 /*!
  * \internal
  * \brief Generate a location constraint from a rule
  *
  * \param[in,out] rsc            Resource that constraint is for
  * \param[in]     rule_xml       Rule XML (sub-element of location constraint)
  * \param[in]     discovery      Value of \c PCMK_XA_RESOURCE_DISCOVERY for
  *                               constraint
  * \param[out]    next_change    Where to set when rule evaluation will change
  * \param[in,out] rule_input     Values used to evaluate rule criteria
  *                               (node-specific values will be overwritten by
  *                               this function)
  *
  * \return true if rule is valid, otherwise false
  */
 static bool
 generate_location_rule(pcmk_resource_t *rsc, xmlNode *rule_xml,
                        const char *discovery, crm_time_t *next_change,
                        pcmk_rule_input_t *rule_input)
 {
     const char *rule_id = NULL;
     const char *score = NULL;
     const char *boolean = NULL;
     const char *role_spec = NULL;
 
     GList *iter = NULL;
 
     bool raw_score = true;
     bool score_allocated = false;
 
     pcmk__location_t *location_rule = NULL;
     enum rsc_role_e role = pcmk_role_unknown;
     enum pcmk__combine combine = pcmk__combine_unknown;
 
     rule_xml = pcmk__xe_resolve_idref(rule_xml, rsc->cluster->input);
     if (rule_xml == NULL) {
         return false; // Error already logged
     }
 
     rule_id = crm_element_value(rule_xml, PCMK_XA_ID);
     if (rule_id == NULL) {
         pcmk__config_err("Ignoring " PCMK_XE_RULE " without " PCMK_XA_ID
                          " in location constraint");
         return false;
     }
 
     boolean = crm_element_value(rule_xml, PCMK_XA_BOOLEAN_OP);
     role_spec = crm_element_value(rule_xml, PCMK_XA_ROLE);
 
     if (parse_location_role(role_spec, &role)) {
         crm_trace("Setting rule %s role filter to %s", rule_id, role_spec);
     } else {
         pcmk__config_err("Ignoring rule %s: Invalid " PCMK_XA_ROLE " '%s'",
                          rule_id, role_spec);
         return false;
     }
 
     crm_trace("Processing location constraint rule %s", rule_id);
 
     score = crm_element_value(rule_xml, PCMK_XA_SCORE);
     if (score == NULL) {
         score = crm_element_value(rule_xml, PCMK_XA_SCORE_ATTRIBUTE);
         if (score != NULL) {
             raw_score = false;
         }
     }
 
     combine = pcmk__parse_combine(boolean);
     switch (combine) {
         case pcmk__combine_and:
         case pcmk__combine_or:
             break;
 
         default:
             /* @COMPAT When we can break behavioral backward compatibility,
              * return false
              */
             pcmk__config_warn("Location constraint rule %s has invalid "
                               PCMK_XA_BOOLEAN_OP " value '%s', using default "
                               "'" PCMK_VALUE_AND "'",
                               rule_id, boolean);
             combine = pcmk__combine_and;
             break;
     }
 
     location_rule = pcmk__new_location(rule_id, rsc, 0, discovery, NULL);
     CRM_CHECK(location_rule != NULL, return NULL);
 
     location_rule->role_filter = role;
 
     if ((rule_input->rsc_id != NULL) && (rule_input->rsc_id_nmatches > 0)
         && !raw_score) {
 
         char *result = pcmk__replace_submatches(score, rule_input->rsc_id,
                                                 rule_input->rsc_id_submatches,
                                                 rule_input->rsc_id_nmatches);
 
         if (result != NULL) {
             score = result;
             score_allocated = true;
         }
     }
 
     for (iter = rsc->cluster->nodes; iter != NULL; iter = iter->next) {
         pcmk_node_t *node = iter->data;
 
         rule_input->node_attrs = node->details->attrs;
         rule_input->rsc_params = pe_rsc_params(rsc, node, rsc->cluster);
 
         if (pcmk_evaluate_rule(rule_xml, rule_input,
                                next_change) == pcmk_rc_ok) {
             pcmk_node_t *local = pe__copy_node(node);
 
             location_rule->nodes = g_list_prepend(location_rule->nodes, local);
             local->weight = get_node_score(rule_id, score, raw_score, node,
                                            rsc);
             crm_trace("%s has score %s after %s", pcmk__node_name(node),
                       pcmk_readable_score(local->weight), rule_id);
         }
     }
 
     if (score_allocated) {
         free((char *)score);
     }
 
     if (location_rule->nodes == NULL) {
         crm_trace("No matching nodes for location constraint rule %s", rule_id);
     } else {
         crm_trace("Location constraint rule %s matched %d nodes",
                   rule_id, g_list_length(location_rule->nodes));
     }
     return true;
 }
 
 static void
 unpack_rsc_location(xmlNode *xml_obj, pcmk_resource_t *rsc,
                     const char *role_spec, const char *score,
                     char *rsc_id_match, int rsc_id_nmatches,
                     regmatch_t *rsc_id_submatches)
 {
     const char *rsc_id = crm_element_value(xml_obj, PCMK_XA_RSC);
     const char *id = crm_element_value(xml_obj, PCMK_XA_ID);
     const char *node = crm_element_value(xml_obj, PCMK_XE_NODE);
     const char *discovery = crm_element_value(xml_obj,
                                               PCMK_XA_RESOURCE_DISCOVERY);
 
     if (rsc == NULL) {
         pcmk__config_warn("Ignoring constraint '%s' because resource '%s' "
                           "does not exist", id, rsc_id);
         return;
     }
 
     if (score == NULL) {
         score = crm_element_value(xml_obj, PCMK_XA_SCORE);
     }
 
     if ((node != NULL) && (score != NULL)) {
         int score_i = char2score(score);
         pcmk_node_t *match = pcmk_find_node(rsc->cluster, node);
         enum rsc_role_e role = pcmk_role_unknown;
         pcmk__location_t *location = NULL;
 
         if (match == NULL) {
             crm_info("Ignoring location constraint %s "
                      "because '%s' is not a known node",
                      pcmk__s(id, "without ID"), node);
             return;
         }
 
         if (role_spec == NULL) {
             role_spec = crm_element_value(xml_obj, PCMK_XA_ROLE);
         }
         if (parse_location_role(role_spec, &role)) {
             crm_trace("Setting location constraint %s role filter: %s",
                       id, role_spec);
         } else {
             /* @COMPAT The previous behavior of creating the constraint ignoring
              * the role is retained for now, but we should ignore the entire
              * constraint when we can break backward compatibility.
              */
             pcmk__config_err("Ignoring role in constraint %s: "
                              "Invalid value '%s'", id, role_spec);
         }
 
         location = pcmk__new_location(id, rsc, score_i, discovery, match);
         if (location == NULL) {
             return; // Error already logged
         }
         location->role_filter = role;
 
     } else {
         bool empty = true;
         crm_time_t *next_change = crm_time_new_undefined();
         pcmk_rule_input_t rule_input = {
             .now = rsc->cluster->now,
             .rsc_meta = rsc->meta,
             .rsc_id = rsc_id_match,
             .rsc_id_submatches = rsc_id_submatches,
             .rsc_id_nmatches = rsc_id_nmatches,
         };
 
         /* This loop is logically parallel to pcmk__evaluate_rules(), except
          * instead of checking whether any rule is active, we set up location
          * constraints for each active rule.
          *
          * @COMPAT When we can break backward compatibility, limit location
          * constraints to a single rule, for consistency with other contexts.
          * Since a rule may contain other rules, this does not prohibit any
          * existing use cases.
          */
         for (xmlNode *rule_xml = pcmk__xe_first_child(xml_obj, PCMK_XE_RULE,
                                                       NULL, NULL);
              rule_xml != NULL; rule_xml = pcmk__xe_next_same(rule_xml)) {
 
             if (generate_location_rule(rsc, rule_xml, discovery, next_change,
                                        &rule_input)) {
                 if (empty) {
                     empty = false;
                     continue;
                 }
                 pcmk__warn_once(pcmk__wo_location_rules,
                                 "Support for multiple " PCMK_XE_RULE
                                 " elements in a location constraint is "
                                 "deprecated and will be removed in a future "
                                 "release (use a single new rule combining the "
                                 "previous rules with " PCMK_XA_BOOLEAN_OP
                                 " set to '" PCMK_VALUE_OR "' instead)");
             }
         }
 
         if (empty) {
             pcmk__config_err("Ignoring constraint '%s' because it contains "
                              "no valid rules", id);
         }
 
         /* If there is a point in the future when the evaluation of a rule will
          * change, make sure the scheduler is re-run by that time.
          */
         if (crm_time_is_defined(next_change)) {
             time_t t = (time_t) crm_time_get_seconds_since_epoch(next_change);
 
             pe__update_recheck_time(t, rsc->cluster,
                                     "location rule evaluation");
         }
         crm_time_free(next_change);
     }
 }
 
 static void
 unpack_simple_location(xmlNode *xml_obj, pcmk_scheduler_t *scheduler)
 {
     const char *id = crm_element_value(xml_obj, PCMK_XA_ID);
     const char *value = crm_element_value(xml_obj, PCMK_XA_RSC);
 
     if (value) {
         pcmk_resource_t *rsc;
 
         rsc = pcmk__find_constraint_resource(scheduler->resources, value);
         unpack_rsc_location(xml_obj, rsc, NULL, NULL, NULL, 0, NULL);
     }
 
     value = crm_element_value(xml_obj, PCMK_XA_RSC_PATTERN);
     if (value) {
         regex_t regex;
         bool invert = false;
 
         if (value[0] == '!') {
             value++;
             invert = true;
         }
 
         if (regcomp(&regex, value, REG_EXTENDED) != 0) {
             pcmk__config_err("Ignoring constraint '%s' because "
                              PCMK_XA_RSC_PATTERN
                              " has invalid value '%s'", id, value);
             return;
         }
 
         for (GList *iter = scheduler->resources; iter != NULL;
              iter = iter->next) {
 
             pcmk_resource_t *r = iter->data;
             int nregs = 0;
             regmatch_t *pmatch = NULL;
             int status;
 
             if (regex.re_nsub > 0) {
                 nregs = regex.re_nsub + 1;
             } else {
                 nregs = 1;
             }
             pmatch = pcmk__assert_alloc(nregs, sizeof(regmatch_t));
 
             status = regexec(&regex, r->id, nregs, pmatch, 0);
 
             if (!invert && (status == 0)) {
                 crm_debug("'%s' matched '%s' for %s", r->id, value, id);
                 unpack_rsc_location(xml_obj, r, NULL, NULL, r->id, nregs,
                                     pmatch);
 
             } else if (invert && (status != 0)) {
                 crm_debug("'%s' is an inverted match of '%s' for %s",
                           r->id, value, id);
                 unpack_rsc_location(xml_obj, r, NULL, NULL, NULL, 0, NULL);
 
             } else {
                 crm_trace("'%s' does not match '%s' for %s", r->id, value, id);
             }
 
             free(pmatch);
         }
 
         regfree(&regex);
     }
 }
 
 // \return Standard Pacemaker return code
 static int
 unpack_location_tags(xmlNode *xml_obj, xmlNode **expanded_xml,
                      pcmk_scheduler_t *scheduler)
 {
     const char *id = NULL;
     const char *rsc_id = NULL;
     const char *state = NULL;
     pcmk_resource_t *rsc = NULL;
     pcmk_tag_t *tag = NULL;
     xmlNode *rsc_set = NULL;
 
     *expanded_xml = NULL;
 
     CRM_CHECK(xml_obj != NULL, return EINVAL);
 
     id = pcmk__xe_id(xml_obj);
     if (id == NULL) {
         pcmk__config_err("Ignoring <%s> constraint without " PCMK_XA_ID,
                          xml_obj->name);
         return pcmk_rc_unpack_error;
     }
 
     // Check whether there are any resource sets with template or tag references
     *expanded_xml = pcmk__expand_tags_in_sets(xml_obj, scheduler);
     if (*expanded_xml != NULL) {
         crm_log_xml_trace(*expanded_xml, "Expanded " PCMK_XE_RSC_LOCATION);
         return pcmk_rc_ok;
     }
 
     rsc_id = crm_element_value(xml_obj, PCMK_XA_RSC);
     if (rsc_id == NULL) {
         return pcmk_rc_ok;
     }
 
     if (!pcmk__valid_resource_or_tag(scheduler, rsc_id, &rsc, &tag)) {
         pcmk__config_err("Ignoring constraint '%s' because '%s' is not a "
                          "valid resource or tag", id, rsc_id);
         return pcmk_rc_unpack_error;
 
     } else if (rsc != NULL) {
         // No template is referenced
         return pcmk_rc_ok;
     }
 
     state = crm_element_value(xml_obj, PCMK_XA_ROLE);
 
     *expanded_xml = pcmk__xml_copy(NULL, xml_obj);
 
     /* Convert any template or tag reference into constraint
      * PCMK_XE_RESOURCE_SET
      */
     if (!pcmk__tag_to_set(*expanded_xml, &rsc_set, PCMK_XA_RSC,
                           false, scheduler)) {
         pcmk__xml_free(*expanded_xml);
         *expanded_xml = NULL;
         return pcmk_rc_unpack_error;
     }
 
     if (rsc_set != NULL) {
         if (state != NULL) {
             /* Move PCMK_XA_RSC_ROLE into converted PCMK_XE_RESOURCE_SET as
              * PCMK_XA_ROLE attribute
              */
             crm_xml_add(rsc_set, PCMK_XA_ROLE, state);
             pcmk__xe_remove_attr(*expanded_xml, PCMK_XA_ROLE);
         }
         crm_log_xml_trace(*expanded_xml, "Expanded " PCMK_XE_RSC_LOCATION);
 
     } else {
         // No sets
         pcmk__xml_free(*expanded_xml);
         *expanded_xml = NULL;
     }
 
     return pcmk_rc_ok;
 }
 
 // \return Standard Pacemaker return code
 static int
 unpack_location_set(xmlNode *location, xmlNode *set,
                     pcmk_scheduler_t *scheduler)
 {
     xmlNode *xml_rsc = NULL;
     pcmk_resource_t *resource = NULL;
     const char *set_id;
     const char *role;
     const char *local_score;
 
     CRM_CHECK(set != NULL, return EINVAL);
 
     set_id = pcmk__xe_id(set);
     if (set_id == NULL) {
         pcmk__config_err("Ignoring " PCMK_XE_RESOURCE_SET " without "
                          PCMK_XA_ID " in constraint '%s'",
                          pcmk__s(pcmk__xe_id(location), "(missing ID)"));
         return pcmk_rc_unpack_error;
     }
 
     role = crm_element_value(set, PCMK_XA_ROLE);
     local_score = crm_element_value(set, PCMK_XA_SCORE);
 
     for (xml_rsc = pcmk__xe_first_child(set, PCMK_XE_RESOURCE_REF, NULL, NULL);
          xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) {
 
         resource = pcmk__find_constraint_resource(scheduler->resources,
                                                   pcmk__xe_id(xml_rsc));
         if (resource == NULL) {
             pcmk__config_err("%s: No resource found for %s",
                              set_id, pcmk__xe_id(xml_rsc));
             return pcmk_rc_unpack_error;
         }
 
         unpack_rsc_location(location, resource, role, local_score, NULL, 0,
                             NULL);
     }
 
     return pcmk_rc_ok;
 }
 
 void
 pcmk__unpack_location(xmlNode *xml_obj, pcmk_scheduler_t *scheduler)
 {
     xmlNode *set = NULL;
     bool any_sets = false;
 
     xmlNode *orig_xml = NULL;
     xmlNode *expanded_xml = NULL;
 
     if (unpack_location_tags(xml_obj, &expanded_xml, scheduler) != pcmk_rc_ok) {
         return;
     }
 
     if (expanded_xml) {
         orig_xml = xml_obj;
         xml_obj = expanded_xml;
     }
 
     for (set = pcmk__xe_first_child(xml_obj, PCMK_XE_RESOURCE_SET, NULL, NULL);
          set != NULL; set = pcmk__xe_next_same(set)) {
 
         any_sets = true;
         set = pcmk__xe_resolve_idref(set, scheduler->input);
         if ((set == NULL) // Configuration error, message already logged
             || (unpack_location_set(xml_obj, set, scheduler) != pcmk_rc_ok)) {
 
             if (expanded_xml) {
                 pcmk__xml_free(expanded_xml);
             }
             return;
         }
     }
 
     if (expanded_xml) {
         pcmk__xml_free(expanded_xml);
         xml_obj = orig_xml;
     }
 
     if (!any_sets) {
         unpack_simple_location(xml_obj, scheduler);
     }
 }
 
 /*!
  * \internal
  * \brief Add a new location constraint to scheduler data
  *
  * \param[in]     id             XML ID of location constraint
  * \param[in,out] rsc            Resource in location constraint
  * \param[in]     node_score     Constraint score
  * \param[in]     discover_mode  Resource discovery option for constraint
  * \param[in]     node           Node in constraint (or NULL if rule-based)
  *
  * \return Newly allocated location constraint on success, otherwise NULL
  * \note The result will be added to the cluster (via \p rsc) and should not be
  *       freed separately.
  */
 pcmk__location_t *
 pcmk__new_location(const char *id, pcmk_resource_t *rsc,
                    int node_score, const char *discover_mode, pcmk_node_t *node)
 {
     pcmk__location_t *new_con = NULL;
 
     CRM_CHECK((node != NULL) || (node_score == 0), return NULL);
 
     if (id == NULL) {
         pcmk__config_err("Invalid constraint: no ID specified");
         return NULL;
     }
 
     if (rsc == NULL) {
         pcmk__config_err("Invalid constraint %s: no resource specified", id);
         return NULL;
     }
 
     new_con = pcmk__assert_alloc(1, sizeof(pcmk__location_t));
     new_con->id = pcmk__str_copy(id);
     new_con->rsc = rsc;
     new_con->nodes = NULL;
     new_con->role_filter = pcmk_role_unknown;
 
     if (pcmk__str_eq(discover_mode, PCMK_VALUE_ALWAYS,
                      pcmk__str_null_matches|pcmk__str_casei)) {
         new_con->discover_mode = pcmk_probe_always;
 
     } else if (pcmk__str_eq(discover_mode, PCMK_VALUE_NEVER,
                             pcmk__str_casei)) {
         new_con->discover_mode = pcmk_probe_never;
 
     } else if (pcmk__str_eq(discover_mode, PCMK_VALUE_EXCLUSIVE,
                             pcmk__str_casei)) {
         new_con->discover_mode = pcmk_probe_exclusive;
         rsc->exclusive_discover = TRUE;
 
     } else {
         pcmk__config_err("Invalid " PCMK_XA_RESOURCE_DISCOVERY " value %s "
                          "in location constraint", discover_mode);
     }
 
     if (node != NULL) {
         pcmk_node_t *copy = pe__copy_node(node);
 
         copy->weight = node_score;
         new_con->nodes = g_list_prepend(NULL, copy);
     }
 
     rsc->cluster->placement_constraints = g_list_prepend(
         rsc->cluster->placement_constraints, new_con);
     rsc->rsc_location = g_list_prepend(rsc->rsc_location, new_con);
 
     return new_con;
 }
 
 /*!
  * \internal
  * \brief Apply all location constraints
  *
  * \param[in,out] scheduler  Scheduler data
  */
 void
 pcmk__apply_locations(pcmk_scheduler_t *scheduler)
 {
     for (GList *iter = scheduler->placement_constraints;
          iter != NULL; iter = iter->next) {
         pcmk__location_t *location = iter->data;
 
-        location->rsc->cmds->apply_location(location->rsc, location);
+        location->rsc->private->cmds->apply_location(location->rsc, location);
     }
 }
 
 /*!
  * \internal
  * \brief Apply a location constraint to a resource's allowed node scores
  *
  * \param[in,out] rsc         Resource to apply constraint to
  * \param[in,out] location    Location constraint to apply
  *
  * \note This does not consider the resource's children, so the resource's
  *       apply_location() method should be used instead in most cases.
  */
 void
 pcmk__apply_location(pcmk_resource_t *rsc, pcmk__location_t *location)
 {
     bool need_role = false;
 
     CRM_ASSERT((rsc != NULL) && (location != NULL));
 
     // If a role was specified, ensure constraint is applicable
     need_role = (location->role_filter > pcmk_role_unknown);
     if (need_role && (location->role_filter != rsc->next_role)) {
         pcmk__rsc_trace(rsc,
                         "Not applying %s to %s because role will be %s not %s",
                         location->id, rsc->id, pcmk_role_text(rsc->next_role),
                         pcmk_role_text(location->role_filter));
         return;
     }
 
     if (location->nodes == NULL) {
         pcmk__rsc_trace(rsc, "Not applying %s to %s because no nodes match",
                         location->id, rsc->id);
         return;
     }
 
     pcmk__rsc_trace(rsc, "Applying %s%s%s to %s", location->id,
                     (need_role? " for role " : ""),
                     (need_role? pcmk_role_text(location->role_filter) : ""),
                     rsc->id);
 
     for (GList *iter = location->nodes; iter != NULL; iter = iter->next) {
         pcmk_node_t *node = iter->data;
         pcmk_node_t *allowed_node = g_hash_table_lookup(rsc->allowed_nodes,
                                                         node->details->id);
 
         if (allowed_node == NULL) {
             pcmk__rsc_trace(rsc, "* = %d on %s",
                             node->weight, pcmk__node_name(node));
             allowed_node = pe__copy_node(node);
             g_hash_table_insert(rsc->allowed_nodes,
                                 (gpointer) allowed_node->details->id,
                                 allowed_node);
         } else {
             pcmk__rsc_trace(rsc, "* + %d on %s",
                             node->weight, pcmk__node_name(node));
             allowed_node->weight = pcmk__add_scores(allowed_node->weight,
                                                     node->weight);
         }
 
         if (allowed_node->rsc_discover_mode < location->discover_mode) {
             if (location->discover_mode == pcmk_probe_exclusive) {
                 rsc->exclusive_discover = TRUE;
             }
             /* exclusive > never > always... always is default */
             allowed_node->rsc_discover_mode = location->discover_mode;
         }
     }
 }
diff --git a/lib/pacemaker/pcmk_sched_primitive.c b/lib/pacemaker/pcmk_sched_primitive.c
index 00b8597876..944580bb55 100644
--- a/lib/pacemaker/pcmk_sched_primitive.c
+++ b/lib/pacemaker/pcmk_sched_primitive.c
@@ -1,1678 +1,1682 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <stdbool.h>
 #include <stdint.h>                 // uint8_t, uint32_t
 
 #include <crm/common/xml.h>
 #include <pacemaker-internal.h>
 
 #include "libpacemaker_private.h"
 
 static void stop_resource(pcmk_resource_t *rsc, pcmk_node_t *node,
                           bool optional);
 static void start_resource(pcmk_resource_t *rsc, pcmk_node_t *node,
                            bool optional);
 static void demote_resource(pcmk_resource_t *rsc, pcmk_node_t *node,
                             bool optional);
 static void promote_resource(pcmk_resource_t *rsc, pcmk_node_t *node,
                              bool optional);
 static void assert_role_error(pcmk_resource_t *rsc, pcmk_node_t *node,
                               bool optional);
 
 #define RSC_ROLE_MAX    (pcmk_role_promoted + 1)
 
 static enum rsc_role_e rsc_state_matrix[RSC_ROLE_MAX][RSC_ROLE_MAX] = {
     /* This array lists the immediate next role when transitioning from one role
      * to a target role. For example, when going from Stopped to Promoted, the
      * next role is Unpromoted, because the resource must be started before it
      * can be promoted. The current state then becomes Started, which is fed
      * into this array again, giving a next role of Promoted.
      *
      * Current role       Immediate next role   Final target role
      * ------------       -------------------   -----------------
      */
     /* Unknown */       { pcmk_role_unknown,    /* Unknown */
                           pcmk_role_stopped,    /* Stopped */
                           pcmk_role_stopped,    /* Started */
                           pcmk_role_stopped,    /* Unpromoted */
                           pcmk_role_stopped,    /* Promoted */
                         },
     /* Stopped */       { pcmk_role_stopped,    /* Unknown */
                           pcmk_role_stopped,    /* Stopped */
                           pcmk_role_started,    /* Started */
                           pcmk_role_unpromoted, /* Unpromoted */
                           pcmk_role_unpromoted, /* Promoted */
                         },
     /* Started */       { pcmk_role_stopped,    /* Unknown */
                           pcmk_role_stopped,    /* Stopped */
                           pcmk_role_started,    /* Started */
                           pcmk_role_unpromoted, /* Unpromoted */
                           pcmk_role_promoted,   /* Promoted */
                         },
     /* Unpromoted */    { pcmk_role_stopped,    /* Unknown */
                           pcmk_role_stopped,    /* Stopped */
                           pcmk_role_stopped,    /* Started */
                           pcmk_role_unpromoted, /* Unpromoted */
                           pcmk_role_promoted,   /* Promoted */
                         },
     /* Promoted  */     { pcmk_role_stopped,    /* Unknown */
                           pcmk_role_unpromoted, /* Stopped */
                           pcmk_role_unpromoted, /* Started */
                           pcmk_role_unpromoted, /* Unpromoted */
                           pcmk_role_promoted,   /* Promoted */
                         },
 };
 
 /*!
  * \internal
  * \brief Function to schedule actions needed for a role change
  *
  * \param[in,out] rsc       Resource whose role is changing
  * \param[in,out] node      Node where resource will be in its next role
  * \param[in]     optional  Whether scheduled actions should be optional
  */
 typedef void (*rsc_transition_fn)(pcmk_resource_t *rsc, pcmk_node_t *node,
                                   bool optional);
 
 static rsc_transition_fn rsc_action_matrix[RSC_ROLE_MAX][RSC_ROLE_MAX] = {
     /* This array lists the function needed to transition directly from one role
      * to another. NULL indicates that nothing is needed.
      *
      * Current role         Transition function             Next role
      * ------------         -------------------             ----------
      */
     /* Unknown */       {   assert_role_error,              /* Unknown */
                             stop_resource,                  /* Stopped */
                             assert_role_error,              /* Started */
                             assert_role_error,              /* Unpromoted */
                             assert_role_error,              /* Promoted */
                         },
     /* Stopped */       {   assert_role_error,              /* Unknown */
                             NULL,                           /* Stopped */
                             start_resource,                 /* Started */
                             start_resource,                 /* Unpromoted */
                             assert_role_error,              /* Promoted */
                         },
     /* Started */       {   assert_role_error,              /* Unknown */
                             stop_resource,                  /* Stopped */
                             NULL,                           /* Started */
                             NULL,                           /* Unpromoted */
                             promote_resource,               /* Promoted */
                         },
     /* Unpromoted */    {   assert_role_error,              /* Unknown */
                             stop_resource,                  /* Stopped */
                             stop_resource,                  /* Started */
                             NULL,                           /* Unpromoted */
                             promote_resource,               /* Promoted */
                         },
     /* Promoted  */     {   assert_role_error,              /* Unknown */
                             demote_resource,                /* Stopped */
                             demote_resource,                /* Started */
                             demote_resource,                /* Unpromoted */
                             NULL,                           /* Promoted */
                         },
 };
 
 /*!
  * \internal
  * \brief Get a list of a resource's allowed nodes sorted by node score
  *
  * \param[in] rsc  Resource to check
  *
  * \return List of allowed nodes sorted by node score
  */
 static GList *
 sorted_allowed_nodes(const pcmk_resource_t *rsc)
 {
     if (rsc->allowed_nodes != NULL) {
         GList *nodes = g_hash_table_get_values(rsc->allowed_nodes);
 
         if (nodes != NULL) {
             return pcmk__sort_nodes(nodes, pcmk__current_node(rsc));
         }
     }
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Assign a resource to its best allowed node, if possible
  *
  * \param[in,out] rsc           Resource to choose a node for
  * \param[in]     prefer        If not \c NULL, prefer this node when all else
  *                              equal
  * \param[in]     stop_if_fail  If \c true and \p rsc can't be assigned to a
  *                              node, set next role to stopped and update
  *                              existing actions
  *
  * \return true if \p rsc could be assigned to a node, otherwise false
  *
  * \note If \p stop_if_fail is \c false, then \c pcmk__unassign_resource() can
  *       completely undo the assignment. A successful assignment can be either
  *       undone or left alone as final. A failed assignment has the same effect
  *       as calling pcmk__unassign_resource(); there are no side effects on
  *       roles or actions.
  */
 static bool
 assign_best_node(pcmk_resource_t *rsc, const pcmk_node_t *prefer,
                  bool stop_if_fail)
 {
     GList *nodes = NULL;
     pcmk_node_t *chosen = NULL;
     pcmk_node_t *best = NULL;
     const pcmk_node_t *most_free_node = pcmk__ban_insufficient_capacity(rsc);
 
     if (prefer == NULL) {
         prefer = most_free_node;
     }
 
     if (!pcmk_is_set(rsc->flags, pcmk_rsc_unassigned)) {
         // We've already finished assignment of resources to nodes
         return rsc->allocated_to != NULL;
     }
 
     // Sort allowed nodes by score
     nodes = sorted_allowed_nodes(rsc);
     if (nodes != NULL) {
         best = (pcmk_node_t *) nodes->data; // First node has best score
     }
 
     if ((prefer != NULL) && (nodes != NULL)) {
         // Get the allowed node version of prefer
         chosen = g_hash_table_lookup(rsc->allowed_nodes, prefer->details->id);
 
         if (chosen == NULL) {
             pcmk__rsc_trace(rsc, "Preferred node %s for %s was unknown",
                             pcmk__node_name(prefer), rsc->id);
 
         /* Favor the preferred node as long as its score is at least as good as
          * the best allowed node's.
          *
          * An alternative would be to favor the preferred node even if the best
          * node is better, when the best node's score is less than INFINITY.
          */
         } else if (chosen->weight < best->weight) {
             pcmk__rsc_trace(rsc, "Preferred node %s for %s was unsuitable",
                             pcmk__node_name(chosen), rsc->id);
             chosen = NULL;
 
         } else if (!pcmk__node_available(chosen, true, false)) {
             pcmk__rsc_trace(rsc, "Preferred node %s for %s was unavailable",
                             pcmk__node_name(chosen), rsc->id);
             chosen = NULL;
 
         } else {
             pcmk__rsc_trace(rsc,
                             "Chose preferred node %s for %s "
                             "(ignoring %d candidates)",
                             pcmk__node_name(chosen), rsc->id,
                             g_list_length(nodes));
         }
     }
 
     if ((chosen == NULL) && (best != NULL)) {
         /* Either there is no preferred node, or the preferred node is not
          * suitable, but another node is allowed to run the resource.
          */
 
         chosen = best;
 
         if (!pcmk__is_unique_clone(rsc->parent)
             && (chosen->weight > 0) // Zero not acceptable
             && pcmk__node_available(chosen, false, false)) {
             /* If the resource is already running on a node, prefer that node if
              * it is just as good as the chosen node.
              *
              * We don't do this for unique clone instances, because
              * pcmk__assign_instances() has already assigned instances to their
              * running nodes when appropriate, and if we get here, we don't want
              * remaining unassigned instances to prefer a node that's already
              * running another instance.
              */
             pcmk_node_t *running = pcmk__current_node(rsc);
 
             if (running == NULL) {
                 // Nothing to do
 
             } else if (!pcmk__node_available(running, true, false)) {
                 pcmk__rsc_trace(rsc,
                                 "Current node for %s (%s) can't run resources",
                                 rsc->id, pcmk__node_name(running));
 
             } else {
                 int nodes_with_best_score = 1;
 
                 for (GList *iter = nodes->next; iter; iter = iter->next) {
                     pcmk_node_t *allowed = (pcmk_node_t *) iter->data;
 
                     if (allowed->weight != chosen->weight) {
                         // The nodes are sorted by score, so no more are equal
                         break;
                     }
                     if (pcmk__same_node(allowed, running)) {
                         // Scores are equal, so prefer the current node
                         chosen = allowed;
                     }
                     nodes_with_best_score++;
                 }
 
                 if (nodes_with_best_score > 1) {
                     uint8_t log_level = LOG_INFO;
 
                     if (chosen->weight >= PCMK_SCORE_INFINITY) {
                         log_level = LOG_WARNING;
                     }
                     do_crm_log(log_level,
                                "Chose %s for %s from %d nodes with score %s",
                                pcmk__node_name(chosen), rsc->id,
                                nodes_with_best_score,
                                pcmk_readable_score(chosen->weight));
                 }
             }
         }
 
         pcmk__rsc_trace(rsc, "Chose %s for %s from %d candidates",
                         pcmk__node_name(chosen), rsc->id, g_list_length(nodes));
     }
 
     pcmk__assign_resource(rsc, chosen, false, stop_if_fail);
     g_list_free(nodes);
     return rsc->allocated_to != NULL;
 }
 
 /*!
  * \internal
  * \brief Apply a "this with" colocation to a node's allowed node scores
  *
  * \param[in,out] colocation  Colocation to apply
  * \param[in,out] rsc         Resource being assigned
  */
 static void
 apply_this_with(pcmk__colocation_t *colocation, pcmk_resource_t *rsc)
 {
     GHashTable *archive = NULL;
     pcmk_resource_t *other = colocation->primary;
 
     // In certain cases, we will need to revert the node scores
     if ((colocation->dependent_role >= pcmk_role_promoted)
         || ((colocation->score < 0)
             && (colocation->score > -PCMK_SCORE_INFINITY))) {
         archive = pcmk__copy_node_table(rsc->allowed_nodes);
     }
 
     if (pcmk_is_set(other->flags, pcmk_rsc_unassigned)) {
         pcmk__rsc_trace(rsc,
                         "%s: Assigning colocation %s primary %s first"
                         "(score=%d role=%s)",
                         rsc->id, colocation->id, other->id,
                         colocation->score,
                         pcmk_role_text(colocation->dependent_role));
-        other->cmds->assign(other, NULL, true);
+        other->private->cmds->assign(other, NULL, true);
     }
 
     // Apply the colocation score to this resource's allowed node scores
-    rsc->cmds->apply_coloc_score(rsc, other, colocation, true);
+    rsc->private->cmds->apply_coloc_score(rsc, other, colocation, true);
     if ((archive != NULL)
         && !pcmk__any_node_available(rsc->allowed_nodes)) {
         pcmk__rsc_info(rsc,
                        "%s: Reverting scores from colocation with %s "
                        "because no nodes allowed",
                        rsc->id, other->id);
         g_hash_table_destroy(rsc->allowed_nodes);
         rsc->allowed_nodes = archive;
         archive = NULL;
     }
     if (archive != NULL) {
         g_hash_table_destroy(archive);
     }
 }
 
 /*!
  * \internal
  * \brief Update a Pacemaker Remote node once its connection has been assigned
  *
  * \param[in] connection  Connection resource that has been assigned
  */
 static void
 remote_connection_assigned(const pcmk_resource_t *connection)
 {
     pcmk_node_t *remote_node = pcmk_find_node(connection->cluster,
                                               connection->id);
 
     CRM_CHECK(remote_node != NULL, return);
 
     if ((connection->allocated_to != NULL)
         && (connection->next_role != pcmk_role_stopped)) {
 
         crm_trace("Pacemaker Remote node %s will be online",
                   remote_node->details->id);
         remote_node->details->online = TRUE;
         if (remote_node->details->unseen) {
             // Avoid unnecessary fence, since we will attempt connection
             remote_node->details->unclean = FALSE;
         }
 
     } else {
         crm_trace("Pacemaker Remote node %s will be shut down "
                   "(%sassigned connection's next role is %s)",
                   remote_node->details->id,
                   ((connection->allocated_to == NULL)? "un" : ""),
                   pcmk_role_text(connection->next_role));
         remote_node->details->shutdown = TRUE;
     }
 }
 
 /*!
  * \internal
  * \brief Assign a primitive resource to a node
  *
  * \param[in,out] rsc           Resource to assign to a node
  * \param[in]     prefer        Node to prefer, if all else is equal
  * \param[in]     stop_if_fail  If \c true and \p rsc can't be assigned to a
  *                              node, set next role to stopped and update
  *                              existing actions
  *
  * \return Node that \p rsc is assigned to, if assigned entirely to one node
  *
  * \note If \p stop_if_fail is \c false, then \c pcmk__unassign_resource() can
  *       completely undo the assignment. A successful assignment can be either
  *       undone or left alone as final. A failed assignment has the same effect
  *       as calling pcmk__unassign_resource(); there are no side effects on
  *       roles or actions.
  */
 pcmk_node_t *
 pcmk__primitive_assign(pcmk_resource_t *rsc, const pcmk_node_t *prefer,
                        bool stop_if_fail)
 {
     GList *this_with_colocations = NULL;
     GList *with_this_colocations = NULL;
     GList *iter = NULL;
     pcmk__colocation_t *colocation = NULL;
 
     CRM_ASSERT(pcmk__is_primitive(rsc));
 
     // Never assign a child without parent being assigned first
     if ((rsc->parent != NULL)
         && !pcmk_is_set(rsc->parent->flags, pcmk_rsc_assigning)) {
+
         pcmk__rsc_debug(rsc, "%s: Assigning parent %s first",
                         rsc->id, rsc->parent->id);
-        rsc->parent->cmds->assign(rsc->parent, prefer, stop_if_fail);
+        rsc->parent->private->cmds->assign(rsc->parent, prefer, stop_if_fail);
     }
 
     if (!pcmk_is_set(rsc->flags, pcmk_rsc_unassigned)) {
         // Assignment has already been done
         const char *node_name = "no node";
 
         if (rsc->allocated_to != NULL) {
             node_name = pcmk__node_name(rsc->allocated_to);
         }
         pcmk__rsc_debug(rsc, "%s: pre-assigned to %s", rsc->id, node_name);
         return rsc->allocated_to;
     }
 
     // Ensure we detect assignment loops
     if (pcmk_is_set(rsc->flags, pcmk_rsc_assigning)) {
         pcmk__rsc_debug(rsc, "Breaking assignment loop involving %s", rsc->id);
         return NULL;
     }
     pcmk__set_rsc_flags(rsc, pcmk_rsc_assigning);
 
     pe__show_node_scores(true, rsc, "Pre-assignment", rsc->allowed_nodes,
                          rsc->cluster);
 
     this_with_colocations = pcmk__this_with_colocations(rsc);
     with_this_colocations = pcmk__with_this_colocations(rsc);
 
     // Apply mandatory colocations first, to satisfy as many as possible
     for (iter = this_with_colocations; iter != NULL; iter = iter->next) {
         colocation = iter->data;
 
         if ((colocation->score <= -PCMK_SCORE_INFINITY)
             || (colocation->score >= PCMK_SCORE_INFINITY)) {
             apply_this_with(colocation, rsc);
         }
     }
     for (iter = with_this_colocations; iter != NULL; iter = iter->next) {
         colocation = iter->data;
 
         if ((colocation->score <= -PCMK_SCORE_INFINITY)
             || (colocation->score >= PCMK_SCORE_INFINITY)) {
             pcmk__add_dependent_scores(colocation, rsc);
         }
     }
 
     pe__show_node_scores(true, rsc, "Mandatory-colocations",
                          rsc->allowed_nodes, rsc->cluster);
 
     // Then apply optional colocations
     for (iter = this_with_colocations; iter != NULL; iter = iter->next) {
         colocation = iter->data;
 
         if ((colocation->score > -PCMK_SCORE_INFINITY)
             && (colocation->score < PCMK_SCORE_INFINITY)) {
             apply_this_with(colocation, rsc);
         }
     }
     for (iter = with_this_colocations; iter != NULL; iter = iter->next) {
         colocation = iter->data;
 
         if ((colocation->score > -PCMK_SCORE_INFINITY)
             && (colocation->score < PCMK_SCORE_INFINITY)) {
             pcmk__add_dependent_scores(colocation, rsc);
         }
     }
 
     g_list_free(this_with_colocations);
     g_list_free(with_this_colocations);
 
     if (rsc->next_role == pcmk_role_stopped) {
         pcmk__rsc_trace(rsc,
                         "Banning %s from all nodes because it will be stopped",
                         rsc->id);
         resource_location(rsc, NULL, -PCMK_SCORE_INFINITY,
                           PCMK_META_TARGET_ROLE, rsc->cluster);
 
     } else if ((rsc->next_role > rsc->role)
                && !pcmk_is_set(rsc->cluster->flags, pcmk_sched_quorate)
                && (rsc->cluster->no_quorum_policy == pcmk_no_quorum_freeze)) {
         crm_notice("Resource %s cannot be elevated from %s to %s due to "
                    PCMK_OPT_NO_QUORUM_POLICY "=" PCMK_VALUE_FREEZE,
                    rsc->id, pcmk_role_text(rsc->role),
                    pcmk_role_text(rsc->next_role));
         pe__set_next_role(rsc, rsc->role,
                           PCMK_OPT_NO_QUORUM_POLICY "=" PCMK_VALUE_FREEZE);
     }
 
     pe__show_node_scores(!pcmk_is_set(rsc->cluster->flags,
                                       pcmk_sched_output_scores),
                          rsc, __func__, rsc->allowed_nodes, rsc->cluster);
 
     // Unmanage resource if fencing is enabled but no device is configured
     if (pcmk_is_set(rsc->cluster->flags, pcmk_sched_fencing_enabled)
         && !pcmk_is_set(rsc->cluster->flags, pcmk_sched_have_fencing)) {
         pcmk__clear_rsc_flags(rsc, pcmk_rsc_managed);
     }
 
     if (!pcmk_is_set(rsc->flags, pcmk_rsc_managed)) {
         // Unmanaged resources stay on their current node
         const char *reason = NULL;
         pcmk_node_t *assign_to = NULL;
 
         pe__set_next_role(rsc, rsc->role, "unmanaged");
         assign_to = pcmk__current_node(rsc);
         if (assign_to == NULL) {
             reason = "inactive";
         } else if (rsc->role == pcmk_role_promoted) {
             reason = "promoted";
         } else if (pcmk_is_set(rsc->flags, pcmk_rsc_failed)) {
             reason = "failed";
         } else {
             reason = "active";
         }
         pcmk__rsc_info(rsc, "Unmanaged resource %s assigned to %s: %s", rsc->id,
                        (assign_to? assign_to->details->uname : "no node"),
                        reason);
         pcmk__assign_resource(rsc, assign_to, true, stop_if_fail);
 
     } else if (pcmk_is_set(rsc->cluster->flags, pcmk_sched_stop_all)) {
         // Must stop at some point, but be consistent with stop_if_fail
         if (stop_if_fail) {
             pcmk__rsc_debug(rsc,
                             "Forcing %s to stop: " PCMK_OPT_STOP_ALL_RESOURCES,
                             rsc->id);
         }
         pcmk__assign_resource(rsc, NULL, true, stop_if_fail);
 
     } else if (!assign_best_node(rsc, prefer, stop_if_fail)) {
         // Assignment failed
         if (!pcmk_is_set(rsc->flags, pcmk_rsc_removed)) {
             pcmk__rsc_info(rsc, "Resource %s cannot run anywhere", rsc->id);
         } else if ((rsc->running_on != NULL) && stop_if_fail) {
             pcmk__rsc_info(rsc, "Stopping removed resource %s", rsc->id);
         }
     }
 
     pcmk__clear_rsc_flags(rsc, pcmk_rsc_assigning);
 
     if (rsc->is_remote_node) {
         remote_connection_assigned(rsc);
     }
 
     return rsc->allocated_to;
 }
 
 /*!
  * \internal
  * \brief Schedule actions to bring resource down and back to current role
  *
  * \param[in,out] rsc           Resource to restart
  * \param[in,out] current       Node that resource should be brought down on
  * \param[in]     need_stop     Whether the resource must be stopped
  * \param[in]     need_promote  Whether the resource must be promoted
  *
  * \return Role that resource would have after scheduled actions are taken
  */
 static void
 schedule_restart_actions(pcmk_resource_t *rsc, pcmk_node_t *current,
                          bool need_stop, bool need_promote)
 {
     enum rsc_role_e role = rsc->role;
     enum rsc_role_e next_role;
     rsc_transition_fn fn = NULL;
 
     pcmk__set_rsc_flags(rsc, pcmk_rsc_restarting);
 
     // Bring resource down to a stop on its current node
     while (role != pcmk_role_stopped) {
         next_role = rsc_state_matrix[role][pcmk_role_stopped];
         pcmk__rsc_trace(rsc, "Creating %s action to take %s down from %s to %s",
                         (need_stop? "required" : "optional"), rsc->id,
                         pcmk_role_text(role), pcmk_role_text(next_role));
         fn = rsc_action_matrix[role][next_role];
         if (fn == NULL) {
             break;
         }
         fn(rsc, current, !need_stop);
         role = next_role;
     }
 
     // Bring resource up to its next role on its next node
     while ((rsc->role <= rsc->next_role) && (role != rsc->role)
            && !pcmk_is_set(rsc->flags, pcmk_rsc_blocked)) {
         bool required = need_stop;
 
         next_role = rsc_state_matrix[role][rsc->role];
         if ((next_role == pcmk_role_promoted) && need_promote) {
             required = true;
         }
         pcmk__rsc_trace(rsc, "Creating %s action to take %s up from %s to %s",
                         (required? "required" : "optional"), rsc->id,
                         pcmk_role_text(role), pcmk_role_text(next_role));
         fn = rsc_action_matrix[role][next_role];
         if (fn == NULL) {
             break;
         }
         fn(rsc, rsc->allocated_to, !required);
         role = next_role;
     }
 
     pcmk__clear_rsc_flags(rsc, pcmk_rsc_restarting);
 }
 
 /*!
  * \internal
  * \brief If a resource's next role is not explicitly specified, set a default
  *
  * \param[in,out] rsc  Resource to set next role for
  *
  * \return "explicit" if next role was explicitly set, otherwise "implicit"
  */
 static const char *
 set_default_next_role(pcmk_resource_t *rsc)
 {
     if (rsc->next_role != pcmk_role_unknown) {
         return "explicit";
     }
 
     if (rsc->allocated_to == NULL) {
         pe__set_next_role(rsc, pcmk_role_stopped, "assignment");
     } else {
         pe__set_next_role(rsc, pcmk_role_started, "assignment");
     }
     return "implicit";
 }
 
 /*!
  * \internal
  * \brief Create an action to represent an already pending start
  *
  * \param[in,out] rsc  Resource to create start action for
  */
 static void
 create_pending_start(pcmk_resource_t *rsc)
 {
     pcmk_action_t *start = NULL;
 
     pcmk__rsc_trace(rsc,
                     "Creating action for %s to represent already pending start",
                     rsc->id);
     start = start_action(rsc, rsc->allocated_to, TRUE);
     pcmk__set_action_flags(start, pcmk_action_always_in_graph);
 }
 
 /*!
  * \internal
  * \brief Schedule actions needed to take a resource to its next role
  *
  * \param[in,out] rsc  Resource to schedule actions for
  */
 static void
 schedule_role_transition_actions(pcmk_resource_t *rsc)
 {
     enum rsc_role_e role = rsc->role;
 
     while (role != rsc->next_role) {
         enum rsc_role_e next_role = rsc_state_matrix[role][rsc->next_role];
         rsc_transition_fn fn = NULL;
 
         pcmk__rsc_trace(rsc,
                         "Creating action to take %s from %s to %s "
                         "(ending at %s)",
                         rsc->id, pcmk_role_text(role),
                         pcmk_role_text(next_role),
                         pcmk_role_text(rsc->next_role));
         fn = rsc_action_matrix[role][next_role];
         if (fn == NULL) {
             break;
         }
         fn(rsc, rsc->allocated_to, false);
         role = next_role;
     }
 }
 
 /*!
  * \internal
  * \brief Create all actions needed for a given primitive resource
  *
  * \param[in,out] rsc  Primitive resource to create actions for
  */
 void
 pcmk__primitive_create_actions(pcmk_resource_t *rsc)
 {
     bool need_stop = false;
     bool need_promote = false;
     bool is_moving = false;
     bool allow_migrate = false;
     bool multiply_active = false;
 
     pcmk_node_t *current = NULL;
     unsigned int num_all_active = 0;
     unsigned int num_clean_active = 0;
     const char *next_role_source = NULL;
 
     CRM_ASSERT(pcmk__is_primitive(rsc));
 
     next_role_source = set_default_next_role(rsc);
     pcmk__rsc_trace(rsc,
                     "Creating all actions for %s transition from %s to %s "
                     "(%s) on %s",
                     rsc->id, pcmk_role_text(rsc->role),
                     pcmk_role_text(rsc->next_role), next_role_source,
                     pcmk__node_name(rsc->allocated_to));
 
     current = rsc->private->fns->active_node(rsc, &num_all_active,
                                              &num_clean_active);
 
     g_list_foreach(rsc->dangling_migrations, pcmk__abort_dangling_migration,
                    rsc);
 
     if ((current != NULL) && (rsc->allocated_to != NULL)
         && !pcmk__same_node(current, rsc->allocated_to)
         && (rsc->next_role >= pcmk_role_started)) {
 
         pcmk__rsc_trace(rsc, "Moving %s from %s to %s",
                         rsc->id, pcmk__node_name(current),
                         pcmk__node_name(rsc->allocated_to));
         is_moving = true;
         allow_migrate = pcmk__rsc_can_migrate(rsc, current);
 
         // This is needed even if migrating (though I'm not sure why ...)
         need_stop = true;
     }
 
     // Check whether resource is partially migrated and/or multiply active
     if ((rsc->partial_migration_source != NULL)
         && (rsc->partial_migration_target != NULL)
         && allow_migrate && (num_all_active == 2)
         && pcmk__same_node(current, rsc->partial_migration_source)
         && pcmk__same_node(rsc->allocated_to, rsc->partial_migration_target)) {
         /* A partial migration is in progress, and the migration target remains
          * the same as when the migration began.
          */
         pcmk__rsc_trace(rsc,
                         "Partial migration of %s from %s to %s will continue",
                         rsc->id, pcmk__node_name(rsc->partial_migration_source),
                         pcmk__node_name(rsc->partial_migration_target));
 
     } else if ((rsc->partial_migration_source != NULL)
                || (rsc->partial_migration_target != NULL)) {
         // A partial migration is in progress but can't be continued
 
         if (num_all_active > 2) {
             // The resource is migrating *and* multiply active!
             crm_notice("Forcing recovery of %s because it is migrating "
                        "from %s to %s and possibly active elsewhere",
                        rsc->id, pcmk__node_name(rsc->partial_migration_source),
                        pcmk__node_name(rsc->partial_migration_target));
         } else {
             // The migration source or target isn't available
             crm_notice("Forcing recovery of %s because it can no longer "
                        "migrate from %s to %s",
                        rsc->id, pcmk__node_name(rsc->partial_migration_source),
                        pcmk__node_name(rsc->partial_migration_target));
         }
         need_stop = true;
         rsc->partial_migration_source = rsc->partial_migration_target = NULL;
         allow_migrate = false;
 
     } else if (pcmk_is_set(rsc->flags, pcmk_rsc_needs_fencing)) {
         multiply_active = (num_all_active > 1);
     } else {
         /* If a resource has PCMK_META_REQUIRES set to PCMK_VALUE_NOTHING or
          * PCMK_VALUE_QUORUM, don't consider it active on unclean nodes (similar
          * to how all resources behave when PCMK_OPT_STONITH_ENABLED is false).
          * We can start such resources elsewhere before fencing completes, and
          * if we considered the resource active on the failed node, we would
          * attempt recovery for being active on multiple nodes.
          */
         multiply_active = (num_clean_active > 1);
     }
 
     if (multiply_active) {
         const char *class = crm_element_value(rsc->xml, PCMK_XA_CLASS);
 
         // Resource was (possibly) incorrectly multiply active
         pcmk__sched_err("%s resource %s might be active on %u nodes (%s)",
                         pcmk__s(class, "Untyped"), rsc->id, num_all_active,
                         pcmk__multiply_active_text(rsc->recovery_type));
         crm_notice("For more information, see \"What are multiply active "
                    "resources?\" at "
                    "https://projects.clusterlabs.org/w/clusterlabs/faq/");
 
         switch (rsc->recovery_type) {
             case pcmk_multiply_active_restart:
                 need_stop = true;
                 break;
             case pcmk_multiply_active_unexpected:
                 need_stop = true; // stop_resource() will skip expected node
                 pcmk__set_rsc_flags(rsc, pcmk_rsc_stop_unexpected);
                 break;
             default:
                 break;
         }
 
     } else {
         pcmk__clear_rsc_flags(rsc, pcmk_rsc_stop_unexpected);
     }
 
     if (pcmk_is_set(rsc->flags, pcmk_rsc_start_pending)) {
         create_pending_start(rsc);
     }
 
     if (is_moving) {
         // Remaining tests are only for resources staying where they are
 
     } else if (pcmk_is_set(rsc->flags, pcmk_rsc_failed)) {
         if (pcmk_is_set(rsc->flags, pcmk_rsc_stop_if_failed)) {
             need_stop = true;
             pcmk__rsc_trace(rsc, "Recovering %s", rsc->id);
         } else {
             pcmk__rsc_trace(rsc, "Recovering %s by demotion", rsc->id);
             if (rsc->next_role == pcmk_role_promoted) {
                 need_promote = true;
             }
         }
 
     } else if (pcmk_is_set(rsc->flags, pcmk_rsc_blocked)) {
         pcmk__rsc_trace(rsc, "Blocking further actions on %s", rsc->id);
         need_stop = true;
 
     } else if ((rsc->role > pcmk_role_started) && (current != NULL)
                && (rsc->allocated_to != NULL)) {
         pcmk_action_t *start = NULL;
 
         pcmk__rsc_trace(rsc, "Creating start action for promoted resource %s",
                         rsc->id);
         start = start_action(rsc, rsc->allocated_to, TRUE);
         if (!pcmk_is_set(start->flags, pcmk_action_optional)) {
             // Recovery of a promoted resource
             pcmk__rsc_trace(rsc, "%s restart is required for recovery", rsc->id);
             need_stop = true;
         }
     }
 
     // Create any actions needed to bring resource down and back up to same role
     schedule_restart_actions(rsc, current, need_stop, need_promote);
 
     // Create any actions needed to take resource from this role to the next
     schedule_role_transition_actions(rsc);
 
     pcmk__create_recurring_actions(rsc);
 
     if (allow_migrate) {
         pcmk__create_migration_actions(rsc, current);
     }
 }
 
 /*!
  * \internal
  * \brief Ban a resource from any allowed nodes that are Pacemaker Remote nodes
  *
  * \param[in] rsc  Resource to check
  */
 static void
 rsc_avoids_remote_nodes(const pcmk_resource_t *rsc)
 {
     GHashTableIter iter;
     pcmk_node_t *node = NULL;
 
     g_hash_table_iter_init(&iter, rsc->allowed_nodes);
     while (g_hash_table_iter_next(&iter, NULL, (void **) &node)) {
         if (node->details->remote_rsc != NULL) {
             node->weight = -PCMK_SCORE_INFINITY;
         }
     }
 }
 
 /*!
  * \internal
  * \brief Return allowed nodes as (possibly sorted) list
  *
  * Convert a resource's hash table of allowed nodes to a list. If printing to
  * stdout, sort the list, to keep action ID numbers consistent for regression
  * test output (while avoiding the performance hit on a live cluster).
  *
  * \param[in] rsc       Resource to check for allowed nodes
  *
  * \return List of resource's allowed nodes
  * \note Callers should take care not to rely on the list being sorted.
  */
 static GList *
 allowed_nodes_as_list(const pcmk_resource_t *rsc)
 {
     GList *allowed_nodes = NULL;
 
     if (rsc->allowed_nodes) {
         allowed_nodes = g_hash_table_get_values(rsc->allowed_nodes);
     }
 
     if (!pcmk__is_daemon) {
         allowed_nodes = g_list_sort(allowed_nodes, pe__cmp_node_name);
     }
 
     return allowed_nodes;
 }
 
 /*!
  * \internal
  * \brief Create implicit constraints needed for a primitive resource
  *
  * \param[in,out] rsc  Primitive resource to create implicit constraints for
  */
 void
 pcmk__primitive_internal_constraints(pcmk_resource_t *rsc)
 {
     GList *allowed_nodes = NULL;
     bool check_unfencing = false;
     bool check_utilization = false;
 
     CRM_ASSERT(pcmk__is_primitive(rsc));
 
     if (!pcmk_is_set(rsc->flags, pcmk_rsc_managed)) {
         pcmk__rsc_trace(rsc,
                         "Skipping implicit constraints for unmanaged resource "
                         "%s", rsc->id);
         return;
     }
 
     // Whether resource requires unfencing
     check_unfencing = !pcmk_is_set(rsc->flags, pcmk_rsc_fence_device)
                       && pcmk_is_set(rsc->cluster->flags,
                                      pcmk_sched_enable_unfencing)
                       && pcmk_is_set(rsc->flags, pcmk_rsc_needs_unfencing);
 
     // Whether a non-default placement strategy is used
     check_utilization = (g_hash_table_size(rsc->utilization) > 0)
                          && !pcmk__str_eq(rsc->cluster->placement_strategy,
                                           PCMK_VALUE_DEFAULT, pcmk__str_casei);
 
     // Order stops before starts (i.e. restart)
     pcmk__new_ordering(rsc, pcmk__op_key(rsc->id, PCMK_ACTION_STOP, 0), NULL,
                        rsc, pcmk__op_key(rsc->id, PCMK_ACTION_START, 0), NULL,
                        pcmk__ar_ordered
                        |pcmk__ar_first_implies_then
                        |pcmk__ar_intermediate_stop,
                        rsc->cluster);
 
     // Promotable ordering: demote before stop, start before promote
     if (pcmk_is_set(pe__const_top_resource(rsc, false)->flags,
                     pcmk_rsc_promotable)
         || (rsc->role > pcmk_role_unpromoted)) {
 
         pcmk__new_ordering(rsc, pcmk__op_key(rsc->id, PCMK_ACTION_DEMOTE, 0),
                            NULL,
                            rsc, pcmk__op_key(rsc->id, PCMK_ACTION_STOP, 0),
                            NULL,
                            pcmk__ar_promoted_then_implies_first, rsc->cluster);
 
         pcmk__new_ordering(rsc, pcmk__op_key(rsc->id, PCMK_ACTION_START, 0),
                            NULL,
                            rsc, pcmk__op_key(rsc->id, PCMK_ACTION_PROMOTE, 0),
                            NULL,
                            pcmk__ar_unrunnable_first_blocks, rsc->cluster);
     }
 
     // Don't clear resource history if probing on same node
     pcmk__new_ordering(rsc, pcmk__op_key(rsc->id, PCMK_ACTION_LRM_DELETE, 0),
                        NULL, rsc,
                        pcmk__op_key(rsc->id, PCMK_ACTION_MONITOR, 0),
                        NULL,
                        pcmk__ar_if_on_same_node|pcmk__ar_then_cancels_first,
                        rsc->cluster);
 
     // Certain checks need allowed nodes
     if (check_unfencing || check_utilization || (rsc->container != NULL)) {
         allowed_nodes = allowed_nodes_as_list(rsc);
     }
 
     if (check_unfencing) {
         g_list_foreach(allowed_nodes, pcmk__order_restart_vs_unfence, rsc);
     }
 
     if (check_utilization) {
         pcmk__create_utilization_constraints(rsc, allowed_nodes);
     }
 
     if (rsc->container != NULL) {
         pcmk_resource_t *remote_rsc = NULL;
 
         if (rsc->is_remote_node) {
             // rsc is the implicit remote connection for a guest or bundle node
 
             /* Guest resources are not allowed to run on Pacemaker Remote nodes,
              * to avoid nesting remotes. However, bundles are allowed.
              */
             if (!pcmk_is_set(rsc->flags, pcmk_rsc_remote_nesting_allowed)) {
                 rsc_avoids_remote_nodes(rsc->container);
             }
 
             /* If someone cleans up a guest or bundle node's container, we will
              * likely schedule a (re-)probe of the container and recovery of the
              * connection. Order the connection stop after the container probe,
              * so that if we detect the container running, we will trigger a new
              * transition and avoid the unnecessary recovery.
              */
             pcmk__order_resource_actions(rsc->container, PCMK_ACTION_MONITOR,
                                          rsc, PCMK_ACTION_STOP,
                                          pcmk__ar_ordered);
 
         /* A user can specify that a resource must start on a Pacemaker Remote
          * node by explicitly configuring it with the container=NODENAME
          * meta-attribute. This is of questionable merit, since location
          * constraints can accomplish the same thing. But we support it, so here
          * we check whether a resource (that is not itself a remote connection)
          * has container set to a remote node or guest node resource.
          */
         } else if (rsc->container->is_remote_node) {
             remote_rsc = rsc->container;
         } else  {
             remote_rsc = pe__resource_contains_guest_node(rsc->cluster,
                                                           rsc->container);
         }
 
         if (remote_rsc != NULL) {
             /* Force the resource on the Pacemaker Remote node instead of
              * colocating the resource with the container resource.
              */
             for (GList *item = allowed_nodes; item; item = item->next) {
                 pcmk_node_t *node = item->data;
 
                 if (node->details->remote_rsc != remote_rsc) {
                     node->weight = -PCMK_SCORE_INFINITY;
                 }
             }
 
         } else {
             /* This resource is either a filler for a container that does NOT
              * represent a Pacemaker Remote node, or a Pacemaker Remote
              * connection resource for a guest node or bundle.
              */
             int score;
 
             crm_trace("Order and colocate %s relative to its container %s",
                       rsc->id, rsc->container->id);
 
             pcmk__new_ordering(rsc->container,
                                pcmk__op_key(rsc->container->id,
                                             PCMK_ACTION_START, 0),
                                NULL, rsc,
                                pcmk__op_key(rsc->id, PCMK_ACTION_START, 0),
                                NULL,
                                pcmk__ar_first_implies_then
                                |pcmk__ar_unrunnable_first_blocks,
                                rsc->cluster);
 
             pcmk__new_ordering(rsc,
                                pcmk__op_key(rsc->id, PCMK_ACTION_STOP, 0),
                                NULL,
                                rsc->container,
                                pcmk__op_key(rsc->container->id,
                                             PCMK_ACTION_STOP, 0),
                                NULL, pcmk__ar_then_implies_first, rsc->cluster);
 
             if (pcmk_is_set(rsc->flags, pcmk_rsc_remote_nesting_allowed)) {
                 score = 10000;    /* Highly preferred but not essential */
             } else {
                 score = PCMK_SCORE_INFINITY; // Force to run on same host
             }
             pcmk__new_colocation("#resource-with-container", NULL, score, rsc,
                                  rsc->container, NULL, NULL,
                                  pcmk__coloc_influence);
         }
     }
 
     if (rsc->is_remote_node
         || pcmk_is_set(rsc->flags, pcmk_rsc_fence_device)) {
         /* Remote connections and fencing devices are not allowed to run on
          * Pacemaker Remote nodes
          */
         rsc_avoids_remote_nodes(rsc);
     }
     g_list_free(allowed_nodes);
 }
 
 /*!
  * \internal
  * \brief Apply a colocation's score to node scores or resource priority
  *
  * Given a colocation constraint, apply its score to the dependent's
  * allowed node scores (if we are still placing resources) or priority (if
  * we are choosing promotable clone instance roles).
  *
  * \param[in,out] dependent      Dependent resource in colocation
  * \param[in]     primary        Primary resource in colocation
  * \param[in]     colocation     Colocation constraint to apply
  * \param[in]     for_dependent  true if called on behalf of dependent
  */
 void
 pcmk__primitive_apply_coloc_score(pcmk_resource_t *dependent,
                                   const pcmk_resource_t *primary,
                                   const pcmk__colocation_t *colocation,
                                   bool for_dependent)
 {
     enum pcmk__coloc_affects filter_results;
 
     CRM_ASSERT((dependent != NULL) && (primary != NULL)
                && (colocation != NULL));
 
     if (for_dependent) {
         // Always process on behalf of primary resource
-        primary->cmds->apply_coloc_score(dependent, primary, colocation, false);
+        primary->private->cmds->apply_coloc_score(dependent, primary,
+                                                  colocation, false);
         return;
     }
 
     filter_results = pcmk__colocation_affects(dependent, primary, colocation,
                                               false);
     pcmk__rsc_trace(dependent, "%s %s with %s (%s, score=%d, filter=%d)",
                     ((colocation->score > 0)? "Colocating" : "Anti-colocating"),
                     dependent->id, primary->id, colocation->id,
                     colocation->score,
                     filter_results);
 
     switch (filter_results) {
         case pcmk__coloc_affects_role:
             pcmk__apply_coloc_to_priority(dependent, primary, colocation);
             break;
         case pcmk__coloc_affects_location:
             pcmk__apply_coloc_to_scores(dependent, primary, colocation);
             break;
         default: // pcmk__coloc_affects_nothing
             return;
     }
 }
 
 /* Primitive implementation of
  * pcmk_assignment_methods_t:with_this_colocations()
  */
 void
 pcmk__with_primitive_colocations(const pcmk_resource_t *rsc,
                                  const pcmk_resource_t *orig_rsc, GList **list)
 {
     CRM_ASSERT(pcmk__is_primitive(rsc) && (list != NULL));
 
     if (rsc == orig_rsc) {
         /* For the resource itself, add all of its own colocations and relevant
          * colocations from its parent (if any).
          */
         pcmk__add_with_this_list(list, rsc->rsc_cons_lhs, orig_rsc);
         if (rsc->parent != NULL) {
-            rsc->parent->cmds->with_this_colocations(rsc->parent, orig_rsc, list);
+            rsc->parent->private->cmds->with_this_colocations(rsc->parent,
+                                                              orig_rsc, list);
         }
     } else {
         // For an ancestor, add only explicitly configured constraints
         for (GList *iter = rsc->rsc_cons_lhs; iter != NULL; iter = iter->next) {
             pcmk__colocation_t *colocation = iter->data;
 
             if (pcmk_is_set(colocation->flags, pcmk__coloc_explicit)) {
                 pcmk__add_with_this(list, colocation, orig_rsc);
             }
         }
     }
 }
 
 /* Primitive implementation of
  * pcmk_assignment_methods_t:this_with_colocations()
  */
 void
 pcmk__primitive_with_colocations(const pcmk_resource_t *rsc,
                                  const pcmk_resource_t *orig_rsc, GList **list)
 {
     CRM_ASSERT(pcmk__is_primitive(rsc) && (list != NULL));
 
     if (rsc == orig_rsc) {
         /* For the resource itself, add all of its own colocations and relevant
          * colocations from its parent (if any).
          */
         pcmk__add_this_with_list(list, rsc->rsc_cons, orig_rsc);
         if (rsc->parent != NULL) {
-            rsc->parent->cmds->this_with_colocations(rsc->parent, orig_rsc, list);
+            rsc->parent->private->cmds->this_with_colocations(rsc->parent,
+                                                              orig_rsc, list);
         }
     } else {
         // For an ancestor, add only explicitly configured constraints
         for (GList *iter = rsc->rsc_cons; iter != NULL; iter = iter->next) {
             pcmk__colocation_t *colocation = iter->data;
 
             if (pcmk_is_set(colocation->flags, pcmk__coloc_explicit)) {
                 pcmk__add_this_with(list, colocation, orig_rsc);
             }
         }
     }
 }
 
 /*!
  * \internal
  * \brief Return action flags for a given primitive resource action
  *
  * \param[in,out] action  Action to get flags for
  * \param[in]     node    If not NULL, limit effects to this node (ignored)
  *
  * \return Flags appropriate to \p action on \p node
  */
 uint32_t
 pcmk__primitive_action_flags(pcmk_action_t *action, const pcmk_node_t *node)
 {
     CRM_ASSERT(action != NULL);
     return (uint32_t) action->flags;
 }
 
 /*!
  * \internal
  * \brief Check whether a node is a multiply active resource's expected node
  *
  * \param[in] rsc  Resource to check
  * \param[in] node  Node to check
  *
  * \return \c true if \p rsc is multiply active with
  *         \c PCMK_META_MULTIPLE_ACTIVE set to \c PCMK_VALUE_STOP_UNEXPECTED,
  *         and \p node is the node where it will remain active
  * \note This assumes that the resource's next role cannot be changed to stopped
  *       after this is called, which should be reasonable if status has already
  *       been unpacked and resources have been assigned to nodes.
  */
 static bool
 is_expected_node(const pcmk_resource_t *rsc, const pcmk_node_t *node)
 {
     return pcmk_all_flags_set(rsc->flags,
                               pcmk_rsc_stop_unexpected|pcmk_rsc_restarting)
            && (rsc->next_role > pcmk_role_stopped)
            && pcmk__same_node(rsc->allocated_to, node);
 }
 
 /*!
  * \internal
  * \brief Schedule actions needed to stop a resource wherever it is active
  *
  * \param[in,out] rsc       Resource being stopped
  * \param[in]     node      Node where resource is being stopped (ignored)
  * \param[in]     optional  Whether actions should be optional
  */
 static void
 stop_resource(pcmk_resource_t *rsc, pcmk_node_t *node, bool optional)
 {
     for (GList *iter = rsc->running_on; iter != NULL; iter = iter->next) {
         pcmk_node_t *current = (pcmk_node_t *) iter->data;
         pcmk_action_t *stop = NULL;
 
         if (is_expected_node(rsc, current)) {
             /* We are scheduling restart actions for a multiply active resource
              * with PCMK_META_MULTIPLE_ACTIVE=PCMK_VALUE_STOP_UNEXPECTED, and
              * this is where it should not be stopped.
              */
             pcmk__rsc_trace(rsc,
                             "Skipping stop of multiply active resource %s "
                             "on expected node %s",
                             rsc->id, pcmk__node_name(current));
             continue;
         }
 
         if (rsc->partial_migration_target != NULL) {
             // Continue migration if node originally was and remains target
             if (pcmk__same_node(current, rsc->partial_migration_target)
                 && pcmk__same_node(current, rsc->allocated_to)) {
                 pcmk__rsc_trace(rsc,
                                 "Skipping stop of %s on %s "
                                 "because partial migration there will continue",
                                 rsc->id, pcmk__node_name(current));
                 continue;
             } else {
                 pcmk__rsc_trace(rsc,
                                 "Forcing stop of %s on %s "
                                 "because migration target changed",
                                 rsc->id, pcmk__node_name(current));
                 optional = false;
             }
         }
 
         pcmk__rsc_trace(rsc, "Scheduling stop of %s on %s",
                         rsc->id, pcmk__node_name(current));
         stop = stop_action(rsc, current, optional);
 
         if (rsc->allocated_to == NULL) {
             pe_action_set_reason(stop, "node availability", true);
         } else if (pcmk_all_flags_set(rsc->flags, pcmk_rsc_restarting
                                                   |pcmk_rsc_stop_unexpected)) {
             /* We are stopping a multiply active resource on a node that is
              * not its expected node, and we are still scheduling restart
              * actions, so the stop is for being multiply active.
              */
             pe_action_set_reason(stop, "being multiply active", true);
         }
 
         if (!pcmk_is_set(rsc->flags, pcmk_rsc_managed)) {
             pcmk__clear_action_flags(stop, pcmk_action_runnable);
         }
 
         if (pcmk_is_set(rsc->cluster->flags, pcmk_sched_remove_after_stop)) {
             pcmk__schedule_cleanup(rsc, current, optional);
         }
 
         if (pcmk_is_set(rsc->flags, pcmk_rsc_needs_unfencing)) {
             pcmk_action_t *unfence = pe_fence_op(current, PCMK_ACTION_ON, true,
                                                  NULL, false, rsc->cluster);
 
             order_actions(stop, unfence, pcmk__ar_then_implies_first);
             if (!pcmk__node_unfenced(current)) {
                 pcmk__sched_err("Stopping %s until %s can be unfenced",
                                 rsc->id, pcmk__node_name(current));
             }
         }
     }
 }
 
 /*!
  * \internal
  * \brief Schedule actions needed to start a resource on a node
  *
  * \param[in,out] rsc       Resource being started
  * \param[in,out] node      Node where resource should be started
  * \param[in]     optional  Whether actions should be optional
  */
 static void
 start_resource(pcmk_resource_t *rsc, pcmk_node_t *node, bool optional)
 {
     pcmk_action_t *start = NULL;
 
     CRM_ASSERT(node != NULL);
 
     pcmk__rsc_trace(rsc, "Scheduling %s start of %s on %s (score %d)",
                     (optional? "optional" : "required"), rsc->id,
                     pcmk__node_name(node), node->weight);
     start = start_action(rsc, node, TRUE);
 
     pcmk__order_vs_unfence(rsc, node, start, pcmk__ar_first_implies_then);
 
     if (pcmk_is_set(start->flags, pcmk_action_runnable) && !optional) {
         pcmk__clear_action_flags(start, pcmk_action_optional);
     }
 
     if (is_expected_node(rsc, node)) {
         /* This could be a problem if the start becomes necessary for other
          * reasons later.
          */
         pcmk__rsc_trace(rsc,
                         "Start of multiply active resouce %s "
                         "on expected node %s will be a pseudo-action",
                         rsc->id, pcmk__node_name(node));
         pcmk__set_action_flags(start, pcmk_action_pseudo);
     }
 }
 
 /*!
  * \internal
  * \brief Schedule actions needed to promote a resource on a node
  *
  * \param[in,out] rsc       Resource being promoted
  * \param[in]     node      Node where resource should be promoted
  * \param[in]     optional  Whether actions should be optional
  */
 static void
 promote_resource(pcmk_resource_t *rsc, pcmk_node_t *node, bool optional)
 {
     GList *iter = NULL;
     GList *action_list = NULL;
     bool runnable = true;
 
     CRM_ASSERT(node != NULL);
 
     // Any start must be runnable for promotion to be runnable
     action_list = pe__resource_actions(rsc, node, PCMK_ACTION_START, true);
     for (iter = action_list; iter != NULL; iter = iter->next) {
         pcmk_action_t *start = (pcmk_action_t *) iter->data;
 
         if (!pcmk_is_set(start->flags, pcmk_action_runnable)) {
             runnable = false;
         }
     }
     g_list_free(action_list);
 
     if (runnable) {
         pcmk_action_t *promote = promote_action(rsc, node, optional);
 
         pcmk__rsc_trace(rsc, "Scheduling %s promotion of %s on %s",
                         (optional? "optional" : "required"), rsc->id,
                         pcmk__node_name(node));
 
         if (is_expected_node(rsc, node)) {
             /* This could be a problem if the promote becomes necessary for
              * other reasons later.
              */
             pcmk__rsc_trace(rsc,
                             "Promotion of multiply active resouce %s "
                             "on expected node %s will be a pseudo-action",
                             rsc->id, pcmk__node_name(node));
             pcmk__set_action_flags(promote, pcmk_action_pseudo);
         }
     } else {
         pcmk__rsc_trace(rsc, "Not promoting %s on %s: start unrunnable",
                         rsc->id, pcmk__node_name(node));
         action_list = pe__resource_actions(rsc, node, PCMK_ACTION_PROMOTE,
                                            true);
         for (iter = action_list; iter != NULL; iter = iter->next) {
             pcmk_action_t *promote = (pcmk_action_t *) iter->data;
 
             pcmk__clear_action_flags(promote, pcmk_action_runnable);
         }
         g_list_free(action_list);
     }
 }
 
 /*!
  * \internal
  * \brief Schedule actions needed to demote a resource wherever it is active
  *
  * \param[in,out] rsc       Resource being demoted
  * \param[in]     node      Node where resource should be demoted (ignored)
  * \param[in]     optional  Whether actions should be optional
  */
 static void
 demote_resource(pcmk_resource_t *rsc, pcmk_node_t *node, bool optional)
 {
     /* Since this will only be called for a primitive (possibly as an instance
      * of a collective resource), the resource is multiply active if it is
      * running on more than one node, so we want to demote on all of them as
      * part of recovery, regardless of which one is the desired node.
      */
     for (GList *iter = rsc->running_on; iter != NULL; iter = iter->next) {
         pcmk_node_t *current = (pcmk_node_t *) iter->data;
 
         if (is_expected_node(rsc, current)) {
             pcmk__rsc_trace(rsc,
                             "Skipping demote of multiply active resource %s "
                             "on expected node %s",
                             rsc->id, pcmk__node_name(current));
         } else {
             pcmk__rsc_trace(rsc, "Scheduling %s demotion of %s on %s",
                             (optional? "optional" : "required"), rsc->id,
                             pcmk__node_name(current));
             demote_action(rsc, current, optional);
         }
     }
 }
 
 static void
 assert_role_error(pcmk_resource_t *rsc, pcmk_node_t *node, bool optional)
 {
     CRM_ASSERT(false);
 }
 
 /*!
  * \internal
  * \brief Schedule cleanup of a resource
  *
  * \param[in,out] rsc       Resource to clean up
  * \param[in]     node      Node to clean up on
  * \param[in]     optional  Whether clean-up should be optional
  */
 void
 pcmk__schedule_cleanup(pcmk_resource_t *rsc, const pcmk_node_t *node,
                        bool optional)
 {
     /* If the cleanup is required, its orderings are optional, because they're
      * relevant only if both actions are required. Conversely, if the cleanup is
      * optional, the orderings make the then action required if the first action
      * becomes required.
      */
     uint32_t flag = optional? pcmk__ar_first_implies_then : pcmk__ar_ordered;
 
     CRM_CHECK((rsc != NULL) && (node != NULL), return);
 
     if (pcmk_is_set(rsc->flags, pcmk_rsc_failed)) {
         pcmk__rsc_trace(rsc, "Skipping clean-up of %s on %s: resource failed",
                         rsc->id, pcmk__node_name(node));
         return;
     }
 
     if (node->details->unclean || !node->details->online) {
         pcmk__rsc_trace(rsc, "Skipping clean-up of %s on %s: node unavailable",
                         rsc->id, pcmk__node_name(node));
         return;
     }
 
     crm_notice("Scheduling clean-up of %s on %s",
                rsc->id, pcmk__node_name(node));
     delete_action(rsc, node, optional);
 
     // stop -> clean-up -> start
     pcmk__order_resource_actions(rsc, PCMK_ACTION_STOP,
                                  rsc, PCMK_ACTION_DELETE, flag);
     pcmk__order_resource_actions(rsc, PCMK_ACTION_DELETE,
                                  rsc, PCMK_ACTION_START, flag);
 }
 
 /*!
  * \internal
  * \brief Add primitive meta-attributes relevant to graph actions to XML
  *
  * \param[in]     rsc  Primitive resource whose meta-attributes should be added
  * \param[in,out] xml  Transition graph action attributes XML to add to
  */
 void
 pcmk__primitive_add_graph_meta(const pcmk_resource_t *rsc, xmlNode *xml)
 {
     char *name = NULL;
     char *value = NULL;
     const pcmk_resource_t *parent = NULL;
 
     CRM_ASSERT(pcmk__is_primitive(rsc) && (xml != NULL));
 
     /* Clone instance numbers get set internally as meta-attributes, and are
      * needed in the transition graph (for example, to tell unique clone
      * instances apart).
      */
     value = g_hash_table_lookup(rsc->meta, PCMK__META_CLONE);
     if (value != NULL) {
         name = crm_meta_name(PCMK__META_CLONE);
         crm_xml_add(xml, name, value);
         free(name);
     }
 
     // Not sure if this one is really needed ...
     value = g_hash_table_lookup(rsc->meta, PCMK_META_REMOTE_NODE);
     if (value != NULL) {
         name = crm_meta_name(PCMK_META_REMOTE_NODE);
         crm_xml_add(xml, name, value);
         free(name);
     }
 
     /* The container meta-attribute can be set on the primitive itself or one of
      * its parents (for example, a group inside a container resource), so check
      * them all, and keep the highest one found.
      */
     for (parent = rsc; parent != NULL; parent = parent->parent) {
         if (parent->container != NULL) {
             crm_xml_add(xml, CRM_META "_" PCMK__META_CONTAINER,
                         parent->container->id);
         }
     }
 
     /* Bundle replica children will get their external-ip set internally as a
      * meta-attribute. The graph action needs it, but under a different naming
      * convention than other meta-attributes.
      */
     value = g_hash_table_lookup(rsc->meta, "external-ip");
     if (value != NULL) {
         crm_xml_add(xml, "pcmk_external_ip", value);
     }
 }
 
 // Primitive implementation of pcmk_assignment_methods_t:add_utilization()
 void
 pcmk__primitive_add_utilization(const pcmk_resource_t *rsc,
                                 const pcmk_resource_t *orig_rsc,
                                 GList *all_rscs, GHashTable *utilization)
 {
     CRM_ASSERT(pcmk__is_primitive(rsc)
                && (orig_rsc != NULL) && (utilization != NULL));
 
     if (!pcmk_is_set(rsc->flags, pcmk_rsc_unassigned)) {
         return;
     }
 
     pcmk__rsc_trace(orig_rsc,
                     "%s: Adding primitive %s as colocated utilization",
                     orig_rsc->id, rsc->id);
     pcmk__release_node_capacity(utilization, rsc);
 }
 
 /*!
  * \internal
  * \brief Get epoch time of node's shutdown attribute (or now if none)
  *
  * \param[in,out] node  Node to check
  *
  * \return Epoch time corresponding to shutdown attribute if set or now if not
  */
 static time_t
 shutdown_time(pcmk_node_t *node)
 {
     const char *shutdown = pcmk__node_attr(node, PCMK__NODE_ATTR_SHUTDOWN, NULL,
                                            pcmk__rsc_node_current);
     time_t result = 0;
 
     if (shutdown != NULL) {
         long long result_ll;
 
         if (pcmk__scan_ll(shutdown, &result_ll, 0LL) == pcmk_rc_ok) {
             result = (time_t) result_ll;
         }
     }
     return (result == 0)? get_effective_time(node->details->data_set) : result;
 }
 
 /*!
  * \internal
  * \brief Ban a resource from a node if it's not locked to the node
  *
  * \param[in]     data       Node to check
  * \param[in,out] user_data  Resource to check
  */
 static void
 ban_if_not_locked(gpointer data, gpointer user_data)
 {
     const pcmk_node_t *node = (const pcmk_node_t *) data;
     pcmk_resource_t *rsc = (pcmk_resource_t *) user_data;
 
     if (strcmp(node->details->uname, rsc->lock_node->details->uname) != 0) {
         resource_location(rsc, node, -PCMK_SCORE_INFINITY,
                           PCMK_OPT_SHUTDOWN_LOCK, rsc->cluster);
     }
 }
 
 // Primitive implementation of pcmk_assignment_methods_t:shutdown_lock()
 void
 pcmk__primitive_shutdown_lock(pcmk_resource_t *rsc)
 {
     const char *class = NULL;
 
     CRM_ASSERT(pcmk__is_primitive(rsc));
 
     class = crm_element_value(rsc->xml, PCMK_XA_CLASS);
 
     // Fence devices and remote connections can't be locked
     if (pcmk__str_eq(class, PCMK_RESOURCE_CLASS_STONITH, pcmk__str_null_matches)
         || rsc->is_remote_node) {
         return;
     }
 
     if (rsc->lock_node != NULL) {
         // The lock was obtained from resource history
 
         if (rsc->running_on != NULL) {
             /* The resource was started elsewhere even though it is now
              * considered locked. This shouldn't be possible, but as a
              * failsafe, we don't want to disturb the resource now.
              */
             pcmk__rsc_info(rsc,
                            "Cancelling shutdown lock "
                            "because %s is already active", rsc->id);
             pe__clear_resource_history(rsc, rsc->lock_node);
             rsc->lock_node = NULL;
             rsc->lock_time = 0;
         }
 
     // Only a resource active on exactly one node can be locked
     } else if (pcmk__list_of_1(rsc->running_on)) {
         pcmk_node_t *node = rsc->running_on->data;
 
         if (node->details->shutdown) {
             if (node->details->unclean) {
                 pcmk__rsc_debug(rsc,
                                 "Not locking %s to unclean %s for shutdown",
                                 rsc->id, pcmk__node_name(node));
             } else {
                 rsc->lock_node = node;
                 rsc->lock_time = shutdown_time(node);
             }
         }
     }
 
     if (rsc->lock_node == NULL) {
         // No lock needed
         return;
     }
 
     if (rsc->cluster->shutdown_lock > 0) {
         time_t lock_expiration = rsc->lock_time + rsc->cluster->shutdown_lock;
 
         pcmk__rsc_info(rsc, "Locking %s to %s due to shutdown (expires @%lld)",
                        rsc->id, pcmk__node_name(rsc->lock_node),
                        (long long) lock_expiration);
         pe__update_recheck_time(++lock_expiration, rsc->cluster,
                                 "shutdown lock expiration");
     } else {
         pcmk__rsc_info(rsc, "Locking %s to %s due to shutdown",
                        rsc->id, pcmk__node_name(rsc->lock_node));
     }
 
     // If resource is locked to one node, ban it from all other nodes
     g_list_foreach(rsc->cluster->nodes, ban_if_not_locked, rsc);
 }
diff --git a/lib/pacemaker/pcmk_sched_probes.c b/lib/pacemaker/pcmk_sched_probes.c
index 6335ab083b..78038144d0 100644
--- a/lib/pacemaker/pcmk_sched_probes.c
+++ b/lib/pacemaker/pcmk_sched_probes.c
@@ -1,903 +1,903 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <glib.h>
 
 #include <crm/crm.h>
 #include <crm/pengine/status.h>
 #include <pacemaker-internal.h>
 #include "libpacemaker_private.h"
 
 /*!
  * \internal
  * \brief Add the expected result to a newly created probe
  *
  * \param[in,out] probe  Probe action to add expected result to
  * \param[in]     rsc    Resource that probe is for
  * \param[in]     node   Node that probe will run on
  */
 static void
 add_expected_result(pcmk_action_t *probe, const pcmk_resource_t *rsc,
                     const pcmk_node_t *node)
 {
     // Check whether resource is currently active on node
     pcmk_node_t *running = pe_find_node_id(rsc->running_on, node->details->id);
 
     // The expected result is what we think the resource's current state is
     if (running == NULL) {
         pe__add_action_expected_result(probe, CRM_EX_NOT_RUNNING);
 
     } else if (rsc->role == pcmk_role_promoted) {
         pe__add_action_expected_result(probe, CRM_EX_PROMOTED);
     }
 }
 
 /*!
  * \internal
  * \brief Create any needed robes on a node for a list of resources
  *
  * \param[in,out] rscs  List of resources to create probes for
  * \param[in,out] node  Node to create probes on
  *
  * \return true if any probe was created, otherwise false
  */
 bool
 pcmk__probe_resource_list(GList *rscs, pcmk_node_t *node)
 {
     bool any_created = false;
 
     for (GList *iter = rscs; iter != NULL; iter = iter->next) {
         pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data;
 
-        if (rsc->cmds->create_probe(rsc, node)) {
+        if (rsc->private->cmds->create_probe(rsc, node)) {
             any_created = true;
         }
     }
     return any_created;
 }
 
 /*!
  * \internal
  * \brief Order one resource's start after another's start-up probe
  *
  * \param[in,out] rsc1  Resource that might get start-up probe
  * \param[in]     rsc2  Resource that might be started
  */
 static void
 probe_then_start(pcmk_resource_t *rsc1, pcmk_resource_t *rsc2)
 {
     if ((rsc1->allocated_to != NULL)
         && (g_hash_table_lookup(rsc1->known_on,
                                 rsc1->allocated_to->details->id) == NULL)) {
 
         pcmk__new_ordering(rsc1,
                            pcmk__op_key(rsc1->id, PCMK_ACTION_MONITOR, 0),
                            NULL,
                            rsc2, pcmk__op_key(rsc2->id, PCMK_ACTION_START, 0),
                            NULL,
                            pcmk__ar_ordered, rsc1->cluster);
     }
 }
 
 /*!
  * \internal
  * \brief Check whether a guest resource will stop
  *
  * \param[in] node  Guest node to check
  *
  * \return true if guest resource will likely stop, otherwise false
  */
 static bool
 guest_resource_will_stop(const pcmk_node_t *node)
 {
     const pcmk_resource_t *guest_rsc = node->details->remote_rsc->container;
 
     /* Ideally, we'd check whether the guest has a required stop, but that
      * information doesn't exist yet, so approximate it ...
      */
     return node->details->remote_requires_reset
            || node->details->unclean
            || pcmk_is_set(guest_rsc->flags, pcmk_rsc_failed)
            || (guest_rsc->next_role == pcmk_role_stopped)
 
            // Guest is moving
            || ((guest_rsc->role > pcmk_role_stopped)
                && (guest_rsc->allocated_to != NULL)
                && (pcmk__find_node_in_list(guest_rsc->running_on,
                    guest_rsc->allocated_to->details->uname) == NULL));
 }
 
 /*!
  * \internal
  * \brief Create a probe action for a resource on a node
  *
  * \param[in,out] rsc   Resource to create probe for
  * \param[in,out] node  Node to create probe on
  *
  * \return Newly created probe action
  */
 static pcmk_action_t *
 probe_action(pcmk_resource_t *rsc, pcmk_node_t *node)
 {
     pcmk_action_t *probe = NULL;
     char *key = pcmk__op_key(rsc->id, PCMK_ACTION_MONITOR, 0);
 
     crm_debug("Scheduling probe of %s %s on %s",
               pcmk_role_text(rsc->role), rsc->id, pcmk__node_name(node));
 
     probe = custom_action(rsc, key, PCMK_ACTION_MONITOR, node, FALSE,
                           rsc->cluster);
     pcmk__clear_action_flags(probe, pcmk_action_optional);
 
     pcmk__order_vs_unfence(rsc, node, probe, pcmk__ar_ordered);
     add_expected_result(probe, rsc, node);
     return probe;
 }
 
 /*!
  * \internal
  * \brief Create probes for a resource on a node, if needed
  *
  * \brief Schedule any probes needed for a resource on a node
  *
  * \param[in,out] rsc   Resource to create probe for
  * \param[in,out] node  Node to create probe on
  *
  * \return true if any probe was created, otherwise false
  */
 bool
 pcmk__probe_rsc_on_node(pcmk_resource_t *rsc, pcmk_node_t *node)
 {
     uint32_t flags = pcmk__ar_ordered;
     pcmk_action_t *probe = NULL;
     pcmk_node_t *allowed = NULL;
     pcmk_resource_t *top = uber_parent(rsc);
     const char *reason = NULL;
 
     CRM_ASSERT((rsc != NULL) && (node != NULL));
 
     if (!pcmk_is_set(rsc->cluster->flags, pcmk_sched_probe_resources)) {
         reason = "start-up probes are disabled";
         goto no_probe;
     }
 
     if (pcmk__is_pacemaker_remote_node(node)) {
         const char *class = crm_element_value(rsc->xml, PCMK_XA_CLASS);
 
         if (pcmk__str_eq(class, PCMK_RESOURCE_CLASS_STONITH, pcmk__str_none)) {
             reason = "Pacemaker Remote nodes cannot run stonith agents";
             goto no_probe;
 
         } else if (pcmk__is_guest_or_bundle_node(node)
                    && pe__resource_contains_guest_node(rsc->cluster, rsc)) {
             reason = "guest nodes cannot run resources containing guest nodes";
             goto no_probe;
 
         } else if (rsc->is_remote_node) {
             reason = "Pacemaker Remote nodes cannot host remote connections";
             goto no_probe;
         }
     }
 
     // If this is a collective resource, probes are created for its children
     if (rsc->children != NULL) {
         return pcmk__probe_resource_list(rsc->children, node);
     }
 
     if ((rsc->container != NULL) && !rsc->is_remote_node) {
         reason = "resource is inside a container";
         goto no_probe;
 
     } else if (pcmk_is_set(rsc->flags, pcmk_rsc_removed)) {
         reason = "resource is orphaned";
         goto no_probe;
 
     } else if (g_hash_table_lookup(rsc->known_on, node->details->id) != NULL) {
         reason = "resource state is already known";
         goto no_probe;
     }
 
     allowed = g_hash_table_lookup(rsc->allowed_nodes, node->details->id);
 
     if (rsc->exclusive_discover || top->exclusive_discover) {
         // Exclusive discovery is enabled ...
 
         if (allowed == NULL) {
             // ... but this node is not allowed to run the resource
             reason = "resource has exclusive discovery but is not allowed "
                      "on node";
             goto no_probe;
 
         } else if (allowed->rsc_discover_mode != pcmk_probe_exclusive) {
             // ... but no constraint marks this node for discovery of resource
             reason = "resource has exclusive discovery but is not enabled "
                      "on node";
             goto no_probe;
         }
     }
 
     if (allowed == NULL) {
         allowed = node;
     }
     if (allowed->rsc_discover_mode == pcmk_probe_never) {
         reason = "node has discovery disabled";
         goto no_probe;
     }
 
     if (pcmk__is_guest_or_bundle_node(node)) {
         pcmk_resource_t *guest = node->details->remote_rsc->container;
 
         if (guest->role == pcmk_role_stopped) {
             // The guest is stopped, so we know no resource is active there
             reason = "node's guest is stopped";
             probe_then_start(guest, top);
             goto no_probe;
 
         } else if (guest_resource_will_stop(node)) {
             reason = "node's guest will stop";
 
             // Order resource start after guest stop (in case it's restarting)
             pcmk__new_ordering(guest,
                                pcmk__op_key(guest->id, PCMK_ACTION_STOP, 0),
                                NULL, top,
                                pcmk__op_key(top->id, PCMK_ACTION_START, 0),
                                NULL, pcmk__ar_ordered, rsc->cluster);
             goto no_probe;
         }
     }
 
     // We've eliminated all cases where a probe is not needed, so now it is
     probe = probe_action(rsc, node);
 
     /* Below, we will order the probe relative to start or reload. If this is a
      * clone instance, the start or reload is for the entire clone rather than
      * just the instance. Otherwise, the start or reload is for the resource
      * itself.
      */
     if (!pcmk__is_clone(top)) {
         top = rsc;
     }
 
     /* Prevent a start if the resource can't be probed, but don't cause the
      * resource or entire clone to stop if already active.
      */
     if (!pcmk_is_set(probe->flags, pcmk_action_runnable)
         && (top->running_on == NULL)) {
         pcmk__set_relation_flags(flags, pcmk__ar_unrunnable_first_blocks);
     }
 
     // Start or reload after probing the resource
     pcmk__new_ordering(rsc, NULL, probe,
                        top, pcmk__op_key(top->id, PCMK_ACTION_START, 0), NULL,
                        flags, rsc->cluster);
     pcmk__new_ordering(rsc, NULL, probe, top, reload_key(rsc), NULL,
                        pcmk__ar_ordered, rsc->cluster);
 
     return true;
 
 no_probe:
     pcmk__rsc_trace(rsc,
                     "Skipping probe for %s on %s because %s",
                     rsc->id, node->details->id, reason);
     return false;
 }
 
 /*!
  * \internal
  * \brief Check whether a probe should be ordered before another action
  *
  * \param[in] probe  Probe action to check
  * \param[in] then   Other action to check
  *
  * \return true if \p probe should be ordered before \p then, otherwise false
  */
 static bool
 probe_needed_before_action(const pcmk_action_t *probe,
                            const pcmk_action_t *then)
 {
     // Probes on a node are performed after unfencing it, not before
     if (pcmk__str_eq(then->task, PCMK_ACTION_STONITH, pcmk__str_none)
         && pcmk__same_node(probe->node, then->node)) {
         const char *op = g_hash_table_lookup(then->meta,
                                              PCMK__META_STONITH_ACTION);
 
         if (pcmk__str_eq(op, PCMK_ACTION_ON, pcmk__str_casei)) {
             return false;
         }
     }
 
     // Probes should be done on a node before shutting it down
     if (pcmk__str_eq(then->task, PCMK_ACTION_DO_SHUTDOWN, pcmk__str_none)
         && (probe->node != NULL) && (then->node != NULL)
         && !pcmk__same_node(probe->node, then->node)) {
         return false;
     }
 
     // Otherwise probes should always be done before any other action
     return true;
 }
 
 /*!
  * \internal
  * \brief Add implicit "probe then X" orderings for "stop then X" orderings
  *
  * If the state of a resource is not known yet, a probe will be scheduled,
  * expecting a "not running" result. If the probe fails, a stop will not be
  * scheduled until the next transition. Thus, if there are ordering constraints
  * like "stop this resource then do something else that's not for the same
  * resource", add implicit "probe this resource then do something" equivalents
  * so the relation is upheld until we know whether a stop is needed.
  *
  * \param[in,out] scheduler  Scheduler data
  */
 static void
 add_probe_orderings_for_stops(pcmk_scheduler_t *scheduler)
 {
     for (GList *iter = scheduler->ordering_constraints; iter != NULL;
          iter = iter->next) {
 
         pcmk__action_relation_t *order = iter->data;
         uint32_t order_flags = pcmk__ar_ordered;
         GList *probes = NULL;
         GList *then_actions = NULL;
         pcmk_action_t *first = NULL;
         pcmk_action_t *then = NULL;
 
         // Skip disabled orderings
         if (order->flags == pcmk__ar_none) {
             continue;
         }
 
         // Skip non-resource orderings, and orderings for the same resource
         if ((order->rsc1 == NULL) || (order->rsc1 == order->rsc2)) {
             continue;
         }
 
         // Skip invalid orderings (shouldn't be possible)
         first = order->action1;
         then = order->action2;
         if (((first == NULL) && (order->task1 == NULL))
             || ((then == NULL) && (order->task2 == NULL))) {
             continue;
         }
 
         // Skip orderings for first actions other than stop
         if ((first != NULL) && !pcmk__str_eq(first->task, PCMK_ACTION_STOP,
                                              pcmk__str_none)) {
             continue;
         } else if ((first == NULL)
                    && !pcmk__ends_with(order->task1,
                                        "_" PCMK_ACTION_STOP "_0")) {
             continue;
         }
 
         /* Do not imply a probe ordering for a resource inside of a stopping
          * container. Otherwise, it might introduce a transition loop, since a
          * probe could be scheduled after the container starts again.
          */
         if ((order->rsc2 != NULL) && (order->rsc1->container == order->rsc2)) {
 
             if ((then != NULL) && pcmk__str_eq(then->task, PCMK_ACTION_STOP,
                                                pcmk__str_none)) {
                 continue;
             } else if ((then == NULL)
                        && pcmk__ends_with(order->task2,
                                           "_" PCMK_ACTION_STOP "_0")) {
                 continue;
             }
         }
 
         // Preserve certain order options for future filtering
         if (pcmk_is_set(order->flags, pcmk__ar_if_first_unmigratable)) {
             pcmk__set_relation_flags(order_flags,
                                      pcmk__ar_if_first_unmigratable);
         }
         if (pcmk_is_set(order->flags, pcmk__ar_if_on_same_node)) {
             pcmk__set_relation_flags(order_flags, pcmk__ar_if_on_same_node);
         }
 
         // Preserve certain order types for future filtering
         if ((order->flags == pcmk__ar_if_required_on_same_node)
             || (order->flags == pcmk__ar_if_on_same_node_or_target)) {
             order_flags = order->flags;
         }
 
         // List all scheduled probes for the first resource
         probes = pe__resource_actions(order->rsc1, NULL, PCMK_ACTION_MONITOR,
                                       FALSE);
         if (probes == NULL) { // There aren't any
             continue;
         }
 
         // List all relevant "then" actions
         if (then != NULL) {
             then_actions = g_list_prepend(NULL, then);
 
         } else if (order->rsc2 != NULL) {
             then_actions = find_actions(order->rsc2->actions, order->task2,
                                         NULL);
             if (then_actions == NULL) { // There aren't any
                 g_list_free(probes);
                 continue;
             }
         }
 
         crm_trace("Implying 'probe then' orderings for '%s then %s' "
                   "(id=%d, type=%.6x)",
                   ((first == NULL)? order->task1 : first->uuid),
                   ((then == NULL)? order->task2 : then->uuid),
                   order->id, order->flags);
 
         for (GList *probe_iter = probes; probe_iter != NULL;
              probe_iter = probe_iter->next) {
 
             pcmk_action_t *probe = (pcmk_action_t *) probe_iter->data;
 
             for (GList *then_iter = then_actions; then_iter != NULL;
                  then_iter = then_iter->next) {
 
                 pcmk_action_t *then = (pcmk_action_t *) then_iter->data;
 
                 if (probe_needed_before_action(probe, then)) {
                     order_actions(probe, then, order_flags);
                 }
             }
         }
 
         g_list_free(then_actions);
         g_list_free(probes);
     }
 }
 
 /*!
  * \internal
  * \brief Add necessary orderings between probe and starts of clone instances
  *
  * , in additon to the ordering with the parent resource added upon creating
  * the probe.
  *
  * \param[in,out] probe     Probe as 'first' action in an ordering
  * \param[in,out] after     'then' action wrapper in the ordering
  */
 static void
 add_start_orderings_for_probe(pcmk_action_t *probe,
                               pcmk__related_action_t *after)
 {
     uint32_t flags = pcmk__ar_ordered|pcmk__ar_unrunnable_first_blocks;
 
     /* Although the ordering between the probe of the clone instance and the
      * start of its parent has been added in pcmk__probe_rsc_on_node(), we
      * avoided enforcing `pcmk__ar_unrunnable_first_blocks` order type for that
      * as long as any of the clone instances are running to prevent them from
      * being unexpectedly stopped.
      *
      * On the other hand, we still need to prevent any inactive instances from
      * starting unless the probe is runnable so that we don't risk starting too
      * many instances before we know the state on all nodes.
      */
     if ((after->action->rsc->variant <= pcmk_rsc_variant_group)
         || pcmk_is_set(probe->flags, pcmk_action_runnable)
         // The order type is already enforced for its parent.
         || pcmk_is_set(after->type, pcmk__ar_unrunnable_first_blocks)
         || (pe__const_top_resource(probe->rsc, false) != after->action->rsc)
         || !pcmk__str_eq(after->action->task, PCMK_ACTION_START,
                          pcmk__str_none)) {
         return;
     }
 
     crm_trace("Adding probe start orderings for 'unrunnable %s@%s "
               "then instances of %s@%s'",
               probe->uuid, pcmk__node_name(probe->node),
               after->action->uuid, pcmk__node_name(after->action->node));
 
     for (GList *then_iter = after->action->actions_after; then_iter != NULL;
          then_iter = then_iter->next) {
 
         pcmk__related_action_t *then = then_iter->data;
 
         if (then->action->rsc->running_on
             || (pe__const_top_resource(then->action->rsc, false)
                 != after->action->rsc)
             || !pcmk__str_eq(then->action->task, PCMK_ACTION_START,
                              pcmk__str_none)) {
             continue;
         }
 
         crm_trace("Adding probe start ordering for 'unrunnable %s@%s "
                   "then %s@%s' (type=%#.6x)",
                   probe->uuid, pcmk__node_name(probe->node),
                   then->action->uuid, pcmk__node_name(then->action->node),
                   flags);
 
         /* Prevent the instance from starting if the instance can't, but don't
          * cause any other intances to stop if already active.
          */
         order_actions(probe, then->action, flags);
     }
 
     return;
 }
 
 /*!
  * \internal
  * \brief Order probes before restarts and re-promotes
  *
  * If a given ordering is a "probe then start" or "probe then promote" ordering,
  * add an implicit "probe then stop/demote" ordering in case the action is part
  * of a restart/re-promote, and do the same recursively for all actions ordered
  * after the "then" action.
  *
  * \param[in,out] probe     Probe as 'first' action in an ordering
  * \param[in,out] after     'then' action in the ordering
  */
 static void
 add_restart_orderings_for_probe(pcmk_action_t *probe, pcmk_action_t *after)
 {
     GList *iter = NULL;
     bool interleave = false;
     pcmk_resource_t *compatible_rsc = NULL;
 
     // Validate that this is a resource probe followed by some action
     if ((after == NULL) || (probe == NULL) || !pcmk__is_primitive(probe->rsc)
         || !pcmk__str_eq(probe->task, PCMK_ACTION_MONITOR, pcmk__str_none)) {
         return;
     }
 
     // Avoid running into any possible loop
     if (pcmk_is_set(after->flags, pcmk_action_detect_loop)) {
         return;
     }
     pcmk__set_action_flags(after, pcmk_action_detect_loop);
 
     crm_trace("Adding probe restart orderings for '%s@%s then %s@%s'",
               probe->uuid, pcmk__node_name(probe->node),
               after->uuid, pcmk__node_name(after->node));
 
     /* Add restart orderings if "then" is for a different primitive.
      * Orderings for collective resources will be added later.
      */
     if (pcmk__is_primitive(after->rsc) && (probe->rsc != after->rsc)) {
 
             GList *then_actions = NULL;
 
             if (pcmk__str_eq(after->task, PCMK_ACTION_START, pcmk__str_none)) {
                 then_actions = pe__resource_actions(after->rsc, NULL,
                                                     PCMK_ACTION_STOP, FALSE);
 
             } else if (pcmk__str_eq(after->task, PCMK_ACTION_PROMOTE,
                                     pcmk__str_none)) {
                 then_actions = pe__resource_actions(after->rsc, NULL,
                                                     PCMK_ACTION_DEMOTE, FALSE);
             }
 
             for (iter = then_actions; iter != NULL; iter = iter->next) {
                 pcmk_action_t *then = (pcmk_action_t *) iter->data;
 
                 // Skip pseudo-actions (for example, those implied by fencing)
                 if (!pcmk_is_set(then->flags, pcmk_action_pseudo)) {
                     order_actions(probe, then, pcmk__ar_ordered);
                 }
             }
             g_list_free(then_actions);
     }
 
     /* Detect whether "then" is an interleaved clone action. For these, we want
      * to add orderings only for the relevant instance.
      */
     if ((after->rsc != NULL)
         && (after->rsc->variant > pcmk_rsc_variant_group)) {
         const char *interleave_s = g_hash_table_lookup(after->rsc->meta,
                                                        PCMK_META_INTERLEAVE);
 
         interleave = crm_is_true(interleave_s);
         if (interleave) {
             compatible_rsc = pcmk__find_compatible_instance(probe->rsc,
                                                             after->rsc,
                                                             pcmk_role_unknown,
                                                             false);
         }
     }
 
     /* Now recursively do the same for all actions ordered after "then". This
      * also handles collective resources since the collective action will be
      * ordered before its individual instances' actions.
      */
     for (iter = after->actions_after; iter != NULL; iter = iter->next) {
         pcmk__related_action_t *after_wrapper = iter->data;
 
         /* pcmk__ar_first_implies_then is the reason why a required A.start
          * implies/enforces B.start to be required too, which is the cause of
          * B.restart/re-promote.
          *
          * Not sure about pcmk__ar_first_implies_same_node_then though. It's now
          * only used for unfencing case, which tends to introduce transition
          * loops...
          */
         if (!pcmk_is_set(after_wrapper->type, pcmk__ar_first_implies_then)) {
             /* The order type between a group/clone and its child such as
              * B.start-> B_child.start is:
              * pcmk__ar_then_implies_first_graphed
              * |pcmk__ar_unrunnable_first_blocks
              *
              * Proceed through the ordering chain and build dependencies with
              * its children.
              */
             if ((after->rsc == NULL)
                 || (after->rsc->variant < pcmk_rsc_variant_group)
                 || (probe->rsc->parent == after->rsc)
                 || (after_wrapper->action->rsc == NULL)
                 || (after_wrapper->action->rsc->variant > pcmk_rsc_variant_group)
                 || (after->rsc != after_wrapper->action->rsc->parent)) {
                 continue;
             }
 
             /* Proceed to the children of a group or a non-interleaved clone.
              * For an interleaved clone, proceed only to the relevant child.
              */
             if ((after->rsc->variant > pcmk_rsc_variant_group) && interleave
                 && ((compatible_rsc == NULL)
                     || (compatible_rsc != after_wrapper->action->rsc))) {
                 continue;
             }
         }
 
         crm_trace("Recursively adding probe restart orderings for "
                   "'%s@%s then %s@%s' (type=%#.6x)",
                   after->uuid, pcmk__node_name(after->node),
                   after_wrapper->action->uuid,
                   pcmk__node_name(after_wrapper->action->node),
                   after_wrapper->type);
 
         add_restart_orderings_for_probe(probe, after_wrapper->action);
     }
 }
 
 /*!
  * \internal
  * \brief Clear the tracking flag on all scheduled actions
  *
  * \param[in,out] scheduler  Scheduler data
  */
 static void
 clear_actions_tracking_flag(pcmk_scheduler_t *scheduler)
 {
     for (GList *iter = scheduler->actions; iter != NULL; iter = iter->next) {
         pcmk_action_t *action = iter->data;
 
         pcmk__clear_action_flags(action, pcmk_action_detect_loop);
     }
 }
 
 /*!
  * \internal
  * \brief Add start and restart orderings for probes scheduled for a resource
  *
  * \param[in,out] data       Resource whose probes should be ordered
  * \param[in]     user_data  Unused
  */
 static void
 add_start_restart_orderings_for_rsc(gpointer data, gpointer user_data)
 {
     pcmk_resource_t *rsc = data;
     GList *probes = NULL;
 
     // For collective resources, order each instance recursively
     if (!pcmk__is_primitive(rsc)) {
         g_list_foreach(rsc->children, add_start_restart_orderings_for_rsc,
                        NULL);
         return;
     }
 
     // Find all probes for given resource
     probes = pe__resource_actions(rsc, NULL, PCMK_ACTION_MONITOR, FALSE);
 
     // Add probe restart orderings for each probe found
     for (GList *iter = probes; iter != NULL; iter = iter->next) {
         pcmk_action_t *probe = (pcmk_action_t *) iter->data;
 
         for (GList *then_iter = probe->actions_after; then_iter != NULL;
              then_iter = then_iter->next) {
 
             pcmk__related_action_t *then = then_iter->data;
 
             add_start_orderings_for_probe(probe, then);
             add_restart_orderings_for_probe(probe, then->action);
             clear_actions_tracking_flag(rsc->cluster);
         }
     }
 
     g_list_free(probes);
 }
 
 /*!
  * \internal
  * \brief Add "A then probe B" orderings for "A then B" orderings
  *
  * \param[in,out] scheduler  Scheduler data
  *
  * \note This function is currently disabled (see next comment).
  */
 static void
 order_then_probes(pcmk_scheduler_t *scheduler)
 {
 #if 0
     /* Given an ordering "A then B", we would prefer to wait for A to be started
      * before probing B.
      *
      * For example, if A is a filesystem which B can't even run without, it
      * would be helpful if the author of B's agent could assume that A is
      * running before B.monitor will be called.
      *
      * However, we can't _only_ probe after A is running, otherwise we wouldn't
      * detect the state of B if A could not be started. We can't even do an
      * opportunistic version of this, because B may be moving:
      *
      *   A.stop -> A.start -> B.probe -> B.stop -> B.start
      *
      * and if we add B.stop -> A.stop here, we get a loop:
      *
      *   A.stop -> A.start -> B.probe -> B.stop -> A.stop
      *
      * We could kill the "B.probe -> B.stop" dependency, but that could mean
      * stopping B "too" soon, because B.start must wait for the probe, and
      * we don't want to stop B if we can't start it.
      *
      * We could add the ordering only if A is an anonymous clone with
      * clone-max == node-max (since we'll never be moving it). However, we could
      * still be stopping one instance at the same time as starting another.
      *
      * The complexity of checking for allowed conditions combined with the ever
      * narrowing use case suggests that this code should remain disabled until
      * someone gets smarter.
      */
     for (GList *iter = scheduler->resources; iter != NULL; iter = iter->next) {
         pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data;
 
         pcmk_action_t *start = NULL;
         GList *actions = NULL;
         GList *probes = NULL;
 
         actions = pe__resource_actions(rsc, NULL, PCMK_ACTION_START, FALSE);
 
         if (actions) {
             start = actions->data;
             g_list_free(actions);
         }
 
         if (start == NULL) {
             crm_debug("No start action for %s", rsc->id);
             continue;
         }
 
         probes = pe__resource_actions(rsc, NULL, PCMK_ACTION_MONITOR, FALSE);
 
         for (actions = start->actions_before; actions != NULL;
              actions = actions->next) {
 
             pcmk__related_action_t *before = actions->data;
 
             pcmk_action_t *first = before->action;
             pcmk_resource_t *first_rsc = first->rsc;
 
             if (first->required_runnable_before) {
                 for (GList *clone_actions = first->actions_before;
                      clone_actions != NULL;
                      clone_actions = clone_actions->next) {
 
                     before = clone_actions->data;
 
                     crm_trace("Testing '%s then %s' for %s",
                               first->uuid, before->action->uuid, start->uuid);
 
                     CRM_ASSERT(before->action->rsc != NULL);
                     first_rsc = before->action->rsc;
                     break;
                 }
 
             } else if (!pcmk__str_eq(first->task, PCMK_ACTION_START,
                                      pcmk__str_none)) {
                 crm_trace("Not a start op %s for %s", first->uuid, start->uuid);
             }
 
             if (first_rsc == NULL) {
                 continue;
 
             } else if (pe__const_top_resource(first_rsc, false)
                        == pe__const_top_resource(start->rsc, false)) {
                 crm_trace("Same parent %s for %s", first_rsc->id, start->uuid);
                 continue;
 
             } else if (!pcmk__is_clone(pe__const_top_resource(first_rsc,
                                                               false))) {
                 crm_trace("Not a clone %s for %s", first_rsc->id, start->uuid);
                 continue;
             }
 
             crm_debug("Applying %s before %s %d", first->uuid, start->uuid,
                       pe__const_top_resource(first_rsc, false)->variant);
 
             for (GList *probe_iter = probes; probe_iter != NULL;
                  probe_iter = probe_iter->next) {
 
                 pcmk_action_t *probe = (pcmk_action_t *) probe_iter->data;
 
                 crm_debug("Ordering %s before %s", first->uuid, probe->uuid);
                 order_actions(first, probe, pcmk__ar_ordered);
             }
         }
     }
 #endif
 }
 
 void
 pcmk__order_probes(pcmk_scheduler_t *scheduler)
 {
     // Add orderings for "probe then X"
     g_list_foreach(scheduler->resources, add_start_restart_orderings_for_rsc,
                    NULL);
     add_probe_orderings_for_stops(scheduler);
 
     order_then_probes(scheduler);
 }
 
 /*!
  * \internal
  * \brief Schedule any probes needed
  *
  * \param[in,out] scheduler  Scheduler data
  *
  * \note This may also schedule fencing of failed remote nodes.
  */
 void
 pcmk__schedule_probes(pcmk_scheduler_t *scheduler)
 {
     // Schedule probes on each node in the cluster as needed
     for (GList *iter = scheduler->nodes; iter != NULL; iter = iter->next) {
         pcmk_node_t *node = (pcmk_node_t *) iter->data;
         const char *probed = NULL;
 
         if (!node->details->online) { // Don't probe offline nodes
             if (pcmk__is_failed_remote_node(node)) {
                 pe_fence_node(scheduler, node,
                               "the connection is unrecoverable", FALSE);
             }
             continue;
 
         } else if (node->details->unclean) { // ... or nodes that need fencing
             continue;
 
         } else if (!node->details->rsc_discovery_enabled) {
             // The user requested that probes not be done on this node
             continue;
         }
 
         /* This is no longer needed for live clusters, since the probe_complete
          * node attribute will never be in the CIB. However this is still useful
          * for processing old saved CIBs (< 1.1.14), including the
          * reprobe-target_rc regression test.
          */
         probed = pcmk__node_attr(node, CRM_OP_PROBED, NULL,
                                  pcmk__rsc_node_current);
         if (probed != NULL && crm_is_true(probed) == FALSE) {
             pcmk_action_t *probe_op = NULL;
 
             probe_op = custom_action(NULL,
                                      crm_strdup_printf("%s-%s", CRM_OP_REPROBE,
                                                        node->details->uname),
                                      CRM_OP_REPROBE, node, FALSE, scheduler);
             pcmk__insert_meta(probe_op, PCMK__META_OP_NO_WAIT, PCMK_VALUE_TRUE);
             continue;
         }
 
         // Probe each resource in the cluster on this node, as needed
         pcmk__probe_resource_list(scheduler->resources, node);
     }
 }
diff --git a/lib/pacemaker/pcmk_sched_promotable.c b/lib/pacemaker/pcmk_sched_promotable.c
index 9f252b64a2..c979ce5866 100644
--- a/lib/pacemaker/pcmk_sched_promotable.c
+++ b/lib/pacemaker/pcmk_sched_promotable.c
@@ -1,1317 +1,1321 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <crm/common/xml.h>
 #include <pacemaker-internal.h>
 
 #include "libpacemaker_private.h"
 
 /*!
  * \internal
  * \brief Add implicit promotion ordering for a promotable instance
  *
  * \param[in,out] clone  Clone resource
  * \param[in,out] child  Instance of \p clone being ordered
  * \param[in,out] last   Previous instance ordered (NULL if \p child is first)
  */
 static void
 order_instance_promotion(pcmk_resource_t *clone, pcmk_resource_t *child,
                          pcmk_resource_t *last)
 {
     // "Promote clone" -> promote instance -> "clone promoted"
     pcmk__order_resource_actions(clone, PCMK_ACTION_PROMOTE,
                                  child, PCMK_ACTION_PROMOTE,
                                  pcmk__ar_ordered);
     pcmk__order_resource_actions(child, PCMK_ACTION_PROMOTE,
                                  clone, PCMK_ACTION_PROMOTED,
                                  pcmk__ar_ordered);
 
     // If clone is ordered, order this instance relative to last
     if ((last != NULL) && pe__clone_is_ordered(clone)) {
         pcmk__order_resource_actions(last, PCMK_ACTION_PROMOTE,
                                      child, PCMK_ACTION_PROMOTE,
                                      pcmk__ar_ordered);
     }
 }
 
 /*!
  * \internal
  * \brief Add implicit demotion ordering for a promotable instance
  *
  * \param[in,out] clone  Clone resource
  * \param[in,out] child  Instance of \p clone being ordered
  * \param[in]     last   Previous instance ordered (NULL if \p child is first)
  */
 static void
 order_instance_demotion(pcmk_resource_t *clone, pcmk_resource_t *child,
                         pcmk_resource_t *last)
 {
     // "Demote clone" -> demote instance -> "clone demoted"
     pcmk__order_resource_actions(clone, PCMK_ACTION_DEMOTE, child,
                                  PCMK_ACTION_DEMOTE,
                                  pcmk__ar_then_implies_first_graphed);
     pcmk__order_resource_actions(child, PCMK_ACTION_DEMOTE,
                                  clone, PCMK_ACTION_DEMOTED,
                                  pcmk__ar_first_implies_then_graphed);
 
     // If clone is ordered, order this instance relative to last
     if ((last != NULL) && pe__clone_is_ordered(clone)) {
         pcmk__order_resource_actions(child, PCMK_ACTION_DEMOTE, last,
                                      PCMK_ACTION_DEMOTE, pcmk__ar_ordered);
     }
 }
 
 /*!
  * \internal
  * \brief Check whether an instance will be promoted or demoted
  *
  * \param[in]  rsc        Instance to check
  * \param[out] demoting   If \p rsc will be demoted, this will be set to true
  * \param[out] promoting  If \p rsc will be promoted, this will be set to true
  */
 static void
 check_for_role_change(const pcmk_resource_t *rsc, bool *demoting,
                       bool *promoting)
 {
     const GList *iter = NULL;
 
     // If this is a cloned group, check group members recursively
     if (rsc->children != NULL) {
         for (iter = rsc->children; iter != NULL; iter = iter->next) {
             check_for_role_change((const pcmk_resource_t *) iter->data,
                                   demoting, promoting);
         }
         return;
     }
 
     for (iter = rsc->actions; iter != NULL; iter = iter->next) {
         const pcmk_action_t *action = (const pcmk_action_t *) iter->data;
 
         if (*promoting && *demoting) {
             return;
 
         } else if (pcmk_is_set(action->flags, pcmk_action_optional)) {
             continue;
 
         } else if (pcmk__str_eq(PCMK_ACTION_DEMOTE, action->task,
                                 pcmk__str_none)) {
             *demoting = true;
 
         } else if (pcmk__str_eq(PCMK_ACTION_PROMOTE, action->task,
                                 pcmk__str_none)) {
             *promoting = true;
         }
     }
 }
 
 /*!
  * \internal
  * \brief Add promoted-role location constraint scores to an instance's priority
  *
  * Adjust a promotable clone instance's promotion priority by the scores of any
  * location constraints in a list that are both limited to the promoted role and
  * for the node where the instance will be placed.
  *
  * \param[in,out] child                 Promotable clone instance
  * \param[in]     location_constraints  List of location constraints to apply
  * \param[in]     chosen                Node where \p child will be placed
  */
 static void
 apply_promoted_locations(pcmk_resource_t *child,
                          const GList *location_constraints,
                          const pcmk_node_t *chosen)
 {
     for (const GList *iter = location_constraints; iter; iter = iter->next) {
         const pcmk__location_t *location = iter->data;
         const pcmk_node_t *constraint_node = NULL;
 
         if (location->role_filter == pcmk_role_promoted) {
             constraint_node = pe_find_node_id(location->nodes,
                                               chosen->details->id);
         }
         if (constraint_node != NULL) {
             int new_priority = pcmk__add_scores(child->priority,
                                                 constraint_node->weight);
 
             pcmk__rsc_trace(child,
                             "Applying location %s to %s promotion priority on "
                             "%s: %s + %s = %s",
                             location->id, child->id,
                             pcmk__node_name(constraint_node),
                             pcmk_readable_score(child->priority),
                             pcmk_readable_score(constraint_node->weight),
                             pcmk_readable_score(new_priority));
             child->priority = new_priority;
         }
     }
 }
 
 /*!
  * \internal
  * \brief Get the node that an instance will be promoted on
  *
  * \param[in] rsc  Promotable clone instance to check
  *
  * \return Node that \p rsc will be promoted on, or NULL if none
  */
 static pcmk_node_t *
 node_to_be_promoted_on(const pcmk_resource_t *rsc)
 {
     pcmk_node_t *node = NULL;
     pcmk_node_t *local_node = NULL;
     const pcmk_resource_t *parent = NULL;
 
     // If this is a cloned group, bail if any group member can't be promoted
     for (GList *iter = rsc->children; iter != NULL; iter = iter->next) {
         pcmk_resource_t *child = (pcmk_resource_t *) iter->data;
 
         if (node_to_be_promoted_on(child) == NULL) {
             pcmk__rsc_trace(rsc,
                             "%s can't be promoted because member %s can't",
                             rsc->id, child->id);
             return NULL;
         }
     }
 
     node = rsc->private->fns->location(rsc, NULL, FALSE);
     if (node == NULL) {
         pcmk__rsc_trace(rsc, "%s can't be promoted because it won't be active",
                         rsc->id);
         return NULL;
 
     } else if (!pcmk_is_set(rsc->flags, pcmk_rsc_managed)) {
         if (rsc->private->fns->state(rsc, TRUE) == pcmk_role_promoted) {
             crm_notice("Unmanaged instance %s will be left promoted on %s",
                        rsc->id, pcmk__node_name(node));
         } else {
             pcmk__rsc_trace(rsc, "%s can't be promoted because it is unmanaged",
                             rsc->id);
             return NULL;
         }
 
     } else if (rsc->priority < 0) {
         pcmk__rsc_trace(rsc,
                         "%s can't be promoted because its promotion priority "
                         "%d is negative",
                         rsc->id, rsc->priority);
         return NULL;
 
     } else if (!pcmk__node_available(node, false, true)) {
         pcmk__rsc_trace(rsc,
                         "%s can't be promoted because %s can't run resources",
                         rsc->id, pcmk__node_name(node));
         return NULL;
     }
 
     parent = pe__const_top_resource(rsc, false);
     local_node = g_hash_table_lookup(parent->allowed_nodes, node->details->id);
 
     if (local_node == NULL) {
         /* It should not be possible for the scheduler to have assigned the
          * instance to a node where its parent is not allowed, but it's good to
          * have a fail-safe.
          */
         if (pcmk_is_set(rsc->flags, pcmk_rsc_managed)) {
             pcmk__sched_err("%s can't be promoted because %s is not allowed "
                             "on %s (scheduler bug?)",
                             rsc->id, parent->id, pcmk__node_name(node));
         } // else the instance is unmanaged and already promoted
         return NULL;
 
     } else if ((local_node->count >= pe__clone_promoted_node_max(parent))
                && pcmk_is_set(rsc->flags, pcmk_rsc_managed)) {
         pcmk__rsc_trace(rsc,
                         "%s can't be promoted because %s has "
                         "maximum promoted instances already",
                         rsc->id, pcmk__node_name(node));
         return NULL;
     }
 
     return local_node;
 }
 
 /*!
  * \internal
  * \brief Compare two promotable clone instances by promotion priority
  *
  * \param[in] a  First instance to compare
  * \param[in] b  Second instance to compare
  *
  * \return A negative number if \p a has higher promotion priority,
  *         a positive number if \p b has higher promotion priority,
  *         or 0 if promotion priorities are equal
  */
 static gint
 cmp_promotable_instance(gconstpointer a, gconstpointer b)
 {
     const pcmk_resource_t *rsc1 = (const pcmk_resource_t *) a;
     const pcmk_resource_t *rsc2 = (const pcmk_resource_t *) b;
 
     enum rsc_role_e role1 = pcmk_role_unknown;
     enum rsc_role_e role2 = pcmk_role_unknown;
 
     CRM_ASSERT((rsc1 != NULL) && (rsc2 != NULL));
 
     // Check sort index set by pcmk__set_instance_roles()
     if (rsc1->sort_index > rsc2->sort_index) {
         pcmk__rsc_trace(rsc1,
                         "%s has higher promotion priority than %s "
                         "(sort index %d > %d)",
                         rsc1->id, rsc2->id, rsc1->sort_index, rsc2->sort_index);
         return -1;
     } else if (rsc1->sort_index < rsc2->sort_index) {
         pcmk__rsc_trace(rsc1,
                         "%s has lower promotion priority than %s "
                         "(sort index %d < %d)",
                         rsc1->id, rsc2->id, rsc1->sort_index, rsc2->sort_index);
         return 1;
     }
 
     // If those are the same, prefer instance whose current role is higher
     role1 = rsc1->private->fns->state(rsc1, TRUE);
     role2 = rsc2->private->fns->state(rsc2, TRUE);
     if (role1 > role2) {
         pcmk__rsc_trace(rsc1,
                         "%s has higher promotion priority than %s "
                         "(higher current role)",
                         rsc1->id, rsc2->id);
         return -1;
     } else if (role1 < role2) {
         pcmk__rsc_trace(rsc1,
                         "%s has lower promotion priority than %s "
                         "(lower current role)",
                         rsc1->id, rsc2->id);
         return 1;
     }
 
     // Finally, do normal clone instance sorting
     return pcmk__cmp_instance(a, b);
 }
 
 /*!
  * \internal
  * \brief Add a promotable clone instance's sort index to its node's score
  *
  * Add a promotable clone instance's sort index (which sums its promotion
  * preferences and scores of relevant location constraints for the promoted
  * role) to the node score of the instance's assigned node.
  *
  * \param[in]     data       Promotable clone instance
  * \param[in,out] user_data  Clone parent of \p data
  */
 static void
 add_sort_index_to_node_score(gpointer data, gpointer user_data)
 {
     const pcmk_resource_t *child = (const pcmk_resource_t *) data;
     pcmk_resource_t *clone = (pcmk_resource_t *) user_data;
 
     pcmk_node_t *node = NULL;
     const pcmk_node_t *chosen = NULL;
 
     if (child->sort_index < 0) {
         pcmk__rsc_trace(clone, "Not adding sort index of %s: negative",
                         child->id);
         return;
     }
 
     chosen = child->private->fns->location(child, NULL, FALSE);
     if (chosen == NULL) {
         pcmk__rsc_trace(clone, "Not adding sort index of %s: inactive",
                         child->id);
         return;
     }
 
     node = g_hash_table_lookup(clone->allowed_nodes, chosen->details->id);
     CRM_ASSERT(node != NULL);
 
     node->weight = pcmk__add_scores(child->sort_index, node->weight);
     pcmk__rsc_trace(clone,
                     "Added cumulative priority of %s (%s) to score on %s "
                     "(now %s)",
                     child->id, pcmk_readable_score(child->sort_index),
                     pcmk__node_name(node), pcmk_readable_score(node->weight));
 }
 
 /*!
  * \internal
  * \brief Apply colocation to dependent's node scores if for promoted role
  *
  * \param[in,out] data       Colocation constraint to apply
  * \param[in,out] user_data  Promotable clone that is constraint's dependent
  */
 static void
 apply_coloc_to_dependent(gpointer data, gpointer user_data)
 {
     pcmk__colocation_t *colocation = data;
     pcmk_resource_t *clone = user_data;
     pcmk_resource_t *primary = colocation->primary;
     uint32_t flags = pcmk__coloc_select_default;
     float factor = colocation->score / (float) PCMK_SCORE_INFINITY;
 
     if (colocation->dependent_role != pcmk_role_promoted) {
         return;
     }
     if (colocation->score < PCMK_SCORE_INFINITY) {
         flags = pcmk__coloc_select_active;
     }
     pcmk__rsc_trace(clone, "Applying colocation %s (promoted %s with %s) @%s",
                     colocation->id, colocation->dependent->id,
                     colocation->primary->id,
                     pcmk_readable_score(colocation->score));
-    primary->cmds->add_colocated_node_scores(primary, clone, clone->id,
-                                             &clone->allowed_nodes, colocation,
-                                             factor, flags);
+    primary->private->cmds->add_colocated_node_scores(primary, clone, clone->id,
+                                                      &clone->allowed_nodes,
+                                                      colocation, factor,
+                                                      flags);
 }
 
 /*!
  * \internal
  * \brief Apply colocation to primary's node scores if for promoted role
  *
  * \param[in,out] data       Colocation constraint to apply
  * \param[in,out] user_data  Promotable clone that is constraint's primary
  */
 static void
 apply_coloc_to_primary(gpointer data, gpointer user_data)
 {
     pcmk__colocation_t *colocation = data;
     pcmk_resource_t *clone = user_data;
     pcmk_resource_t *dependent = colocation->dependent;
     const float factor = colocation->score / (float) PCMK_SCORE_INFINITY;
     const uint32_t flags = pcmk__coloc_select_active
                            |pcmk__coloc_select_nonnegative;
 
     if ((colocation->primary_role != pcmk_role_promoted)
          || !pcmk__colocation_has_influence(colocation, NULL)) {
         return;
     }
 
     pcmk__rsc_trace(clone, "Applying colocation %s (%s with promoted %s) @%s",
                     colocation->id, colocation->dependent->id,
                     colocation->primary->id,
                     pcmk_readable_score(colocation->score));
-    dependent->cmds->add_colocated_node_scores(dependent, clone, clone->id,
-                                               &clone->allowed_nodes,
-                                               colocation, factor, flags);
+    dependent->private->cmds->add_colocated_node_scores(dependent, clone,
+                                                        clone->id,
+                                                        &clone->allowed_nodes,
+                                                        colocation, factor,
+                                                        flags);
 }
 
 /*!
  * \internal
  * \brief Set clone instance's sort index to its node's score
  *
  * \param[in,out] data       Promotable clone instance
  * \param[in]     user_data  Parent clone of \p data
  */
 static void
 set_sort_index_to_node_score(gpointer data, gpointer user_data)
 {
     pcmk_resource_t *child = (pcmk_resource_t *) data;
     const pcmk_resource_t *clone = (const pcmk_resource_t *) user_data;
 
     pcmk_node_t *chosen = child->private->fns->location(child, NULL, FALSE);
 
     if (!pcmk_is_set(child->flags, pcmk_rsc_managed)
         && (child->next_role == pcmk_role_promoted)) {
         child->sort_index = PCMK_SCORE_INFINITY;
         pcmk__rsc_trace(clone,
                         "Final sort index for %s is INFINITY "
                         "(unmanaged promoted)",
                         child->id);
 
     } else if ((chosen == NULL) || (child->sort_index < 0)) {
         pcmk__rsc_trace(clone,
                         "Final sort index for %s is %d (ignoring node score)",
                         child->id, child->sort_index);
 
     } else {
         const pcmk_node_t *node = g_hash_table_lookup(clone->allowed_nodes,
                                                       chosen->details->id);
 
         CRM_ASSERT(node != NULL);
         child->sort_index = node->weight;
         pcmk__rsc_trace(clone,
                         "Adding scores for %s: final sort index for %s is %d",
                         clone->id, child->id, child->sort_index);
     }
 }
 
 /*!
  * \internal
  * \brief Sort a promotable clone's instances by descending promotion priority
  *
  * \param[in,out] clone  Promotable clone to sort
  */
 static void
 sort_promotable_instances(pcmk_resource_t *clone)
 {
     GList *colocations = NULL;
 
     if (pe__set_clone_flag(clone, pcmk__clone_promotion_constrained)
             == pcmk_rc_already) {
         return;
     }
     pcmk__set_rsc_flags(clone, pcmk_rsc_updating_nodes);
 
     for (GList *iter = clone->children; iter != NULL; iter = iter->next) {
         pcmk_resource_t *child = (pcmk_resource_t *) iter->data;
 
         pcmk__rsc_trace(clone,
                         "Adding scores for %s: initial sort index for %s is %d",
                         clone->id, child->id, child->sort_index);
     }
     pe__show_node_scores(true, clone, "Before", clone->allowed_nodes,
                          clone->cluster);
 
     g_list_foreach(clone->children, add_sort_index_to_node_score, clone);
 
     colocations = pcmk__this_with_colocations(clone);
     g_list_foreach(colocations, apply_coloc_to_dependent, clone);
     g_list_free(colocations);
 
     colocations = pcmk__with_this_colocations(clone);
     g_list_foreach(colocations, apply_coloc_to_primary, clone);
     g_list_free(colocations);
 
     // Ban resource from all nodes if it needs a ticket but doesn't have it
     pcmk__require_promotion_tickets(clone);
 
     pe__show_node_scores(true, clone, "After", clone->allowed_nodes,
                          clone->cluster);
 
     // Reset sort indexes to final node scores
     g_list_foreach(clone->children, set_sort_index_to_node_score, clone);
 
     // Finally, sort instances in descending order of promotion priority
     clone->children = g_list_sort(clone->children, cmp_promotable_instance);
     pcmk__clear_rsc_flags(clone, pcmk_rsc_updating_nodes);
 }
 
 /*!
  * \internal
  * \brief Find the active instance (if any) of an anonymous clone on a node
  *
  * \param[in] clone  Anonymous clone to check
  * \param[in] id     Instance ID (without instance number) to check
  * \param[in] node   Node to check
  *
  * \return
  */
 static pcmk_resource_t *
 find_active_anon_instance(const pcmk_resource_t *clone, const char *id,
                           const pcmk_node_t *node)
 {
     for (GList *iter = clone->children; iter; iter = iter->next) {
         pcmk_resource_t *child = iter->data;
         pcmk_resource_t *active = NULL;
 
         // Use ->find_rsc() in case this is a cloned group
         active = clone->private->fns->find_rsc(child, id, node,
                                                pcmk_rsc_match_clone_only
                                                |pcmk_rsc_match_current_node);
         if (active != NULL) {
             return active;
         }
     }
     return NULL;
 }
 
 /*
  * \brief Check whether an anonymous clone instance is known on a node
  *
  * \param[in] clone  Anonymous clone to check
  * \param[in] id     Instance ID (without instance number) to check
  * \param[in] node   Node to check
  *
  * \return true if \p id instance of \p clone is known on \p node,
  *         otherwise false
  */
 static bool
 anonymous_known_on(const pcmk_resource_t *clone, const char *id,
                    const pcmk_node_t *node)
 {
     for (GList *iter = clone->children; iter; iter = iter->next) {
         pcmk_resource_t *child = iter->data;
 
         /* Use ->find_rsc() because this might be a cloned group, and knowing
          * that other members of the group are known here implies nothing.
          */
         child = clone->private->fns->find_rsc(child, id, NULL,
                                               pcmk_rsc_match_clone_only);
         CRM_LOG_ASSERT(child != NULL);
         if (child != NULL) {
             if (g_hash_table_lookup(child->known_on, node->details->id)) {
                 return true;
             }
         }
     }
     return false;
 }
 
 /*!
  * \internal
  * \brief Check whether a node is allowed to run a resource
  *
  * \param[in] rsc   Resource to check
  * \param[in] node  Node to check
  *
  * \return true if \p node is allowed to run \p rsc, otherwise false
  */
 static bool
 is_allowed(const pcmk_resource_t *rsc, const pcmk_node_t *node)
 {
     pcmk_node_t *allowed = g_hash_table_lookup(rsc->allowed_nodes,
                                                node->details->id);
 
     return (allowed != NULL) && (allowed->weight >= 0);
 }
 
 /*!
  * \brief Check whether a clone instance's promotion score should be considered
  *
  * \param[in] rsc   Promotable clone instance to check
  * \param[in] node  Node where score would be applied
  *
  * \return true if \p rsc's promotion score should be considered on \p node,
  *         otherwise false
  */
 static bool
 promotion_score_applies(const pcmk_resource_t *rsc, const pcmk_node_t *node)
 {
     char *id = clone_strip(rsc->id);
     const pcmk_resource_t *parent = pe__const_top_resource(rsc, false);
     pcmk_resource_t *active = NULL;
     const char *reason = "allowed";
 
     // Some checks apply only to anonymous clone instances
     if (!pcmk_is_set(rsc->flags, pcmk_rsc_unique)) {
 
         // If instance is active on the node, its score definitely applies
         active = find_active_anon_instance(parent, id, node);
         if (active == rsc) {
             reason = "active";
             goto check_allowed;
         }
 
         /* If *no* instance is active on this node, this instance's score will
          * count if it has been probed on this node.
          */
         if ((active == NULL) && anonymous_known_on(parent, id, node)) {
             reason = "probed";
             goto check_allowed;
         }
     }
 
     /* If this clone's status is unknown on *all* nodes (e.g. cluster startup),
      * take all instances' scores into account, to make sure we use any
      * permanent promotion scores.
      */
     if ((rsc->running_on == NULL) && (g_hash_table_size(rsc->known_on) == 0)) {
         reason = "none probed";
         goto check_allowed;
     }
 
     /* Otherwise, we've probed and/or started the resource *somewhere*, so
      * consider promotion scores on nodes where we know the status.
      */
     if ((g_hash_table_lookup(rsc->known_on, node->details->id) != NULL)
         || (pe_find_node_id(rsc->running_on, node->details->id) != NULL)) {
         reason = "known";
     } else {
         pcmk__rsc_trace(rsc,
                         "Ignoring %s promotion score (for %s) on %s: "
                         "not probed",
                         rsc->id, id, pcmk__node_name(node));
         free(id);
         return false;
     }
 
 check_allowed:
     if (is_allowed(rsc, node)) {
         pcmk__rsc_trace(rsc, "Counting %s promotion score (for %s) on %s: %s",
                         rsc->id, id, pcmk__node_name(node), reason);
         free(id);
         return true;
     }
 
     pcmk__rsc_trace(rsc,
                     "Ignoring %s promotion score (for %s) on %s: not allowed",
                     rsc->id, id, pcmk__node_name(node));
     free(id);
     return false;
 }
 
 /*!
  * \internal
  * \brief Get the value of a promotion score node attribute
  *
  * \param[in] rsc   Promotable clone instance to get promotion score for
  * \param[in] node  Node to get promotion score for
  * \param[in] name  Resource name to use in promotion score attribute name
  *
  * \return Value of promotion score node attribute for \p rsc on \p node
  */
 static const char *
 promotion_attr_value(const pcmk_resource_t *rsc, const pcmk_node_t *node,
                      const char *name)
 {
     char *attr_name = NULL;
     const char *attr_value = NULL;
     const char *target = NULL;
     enum pcmk__rsc_node node_type = pcmk__rsc_node_assigned;
 
     if (pcmk_is_set(rsc->flags, pcmk_rsc_unassigned)) {
         // Not assigned yet
         node_type = pcmk__rsc_node_current;
     }
     target = g_hash_table_lookup(rsc->meta,
                                  PCMK_META_CONTAINER_ATTRIBUTE_TARGET);
     attr_name = pcmk_promotion_score_name(name);
     attr_value = pcmk__node_attr(node, attr_name, target, node_type);
     free(attr_name);
     return attr_value;
 }
 
 /*!
  * \internal
  * \brief Get the promotion score for a clone instance on a node
  *
  * \param[in]  rsc         Promotable clone instance to get score for
  * \param[in]  node        Node to get score for
  * \param[out] is_default  If non-NULL, will be set true if no score available
  *
  * \return Promotion score for \p rsc on \p node (or 0 if none)
  */
 static int
 promotion_score(const pcmk_resource_t *rsc, const pcmk_node_t *node,
                 bool *is_default)
 {
     char *name = NULL;
     const char *attr_value = NULL;
 
     if (is_default != NULL) {
         *is_default = true;
     }
 
     CRM_CHECK((rsc != NULL) && (node != NULL), return 0);
 
     /* If this is an instance of a cloned group, the promotion score is the sum
      * of all members' promotion scores.
      */
     if (rsc->children != NULL) {
         int score = 0;
 
         for (const GList *iter = rsc->children;
              iter != NULL; iter = iter->next) {
 
             const pcmk_resource_t *child = (const pcmk_resource_t *) iter->data;
             bool child_default = false;
             int child_score = promotion_score(child, node, &child_default);
 
             if (!child_default && (is_default != NULL)) {
                 *is_default = false;
             }
             score += child_score;
         }
         return score;
     }
 
     if (!promotion_score_applies(rsc, node)) {
         return 0;
     }
 
     /* For the promotion score attribute name, use the name the resource is
      * known as in resource history, since that's what crm_attribute --promotion
      * would have used.
      */
     name = (rsc->clone_name == NULL)? rsc->id : rsc->clone_name;
 
     attr_value = promotion_attr_value(rsc, node, name);
     if (attr_value != NULL) {
         pcmk__rsc_trace(rsc, "Promotion score for %s on %s = %s",
                         name, pcmk__node_name(node),
                         pcmk__s(attr_value, "(unset)"));
     } else if (!pcmk_is_set(rsc->flags, pcmk_rsc_unique)) {
         /* If we don't have any resource history yet, we won't have clone_name.
          * In that case, for anonymous clones, try the resource name without
          * any instance number.
          */
         name = clone_strip(rsc->id);
         if (strcmp(rsc->id, name) != 0) {
             attr_value = promotion_attr_value(rsc, node, name);
             pcmk__rsc_trace(rsc, "Promotion score for %s on %s (for %s) = %s",
                             name, pcmk__node_name(node), rsc->id,
                             pcmk__s(attr_value, "(unset)"));
         }
         free(name);
     }
 
     if (attr_value == NULL) {
         return 0;
     }
 
     if (is_default != NULL) {
         *is_default = false;
     }
     return char2score(attr_value);
 }
 
 /*!
  * \internal
  * \brief Include promotion scores in instances' node scores and priorities
  *
  * \param[in,out] rsc  Promotable clone resource to update
  */
 void
 pcmk__add_promotion_scores(pcmk_resource_t *rsc)
 {
     if (pe__set_clone_flag(rsc,
                            pcmk__clone_promotion_added) == pcmk_rc_already) {
         return;
     }
 
     for (GList *iter = rsc->children; iter != NULL; iter = iter->next) {
         pcmk_resource_t *child_rsc = (pcmk_resource_t *) iter->data;
 
         GHashTableIter iter;
         pcmk_node_t *node = NULL;
         int score, new_score;
 
         g_hash_table_iter_init(&iter, child_rsc->allowed_nodes);
         while (g_hash_table_iter_next(&iter, NULL, (void **) &node)) {
             if (!pcmk__node_available(node, false, false)) {
                 /* This node will never be promoted, so don't apply the
                  * promotion score, as that may lead to clone shuffling.
                  */
                 continue;
             }
 
             score = promotion_score(child_rsc, node, NULL);
             if (score > 0) {
                 new_score = pcmk__add_scores(node->weight, score);
                 if (new_score != node->weight) { // Could remain INFINITY
                     node->weight = new_score;
                     pcmk__rsc_trace(rsc,
                                     "Added %s promotion priority (%s) to score "
                                     "on %s (now %s)",
                                     child_rsc->id, pcmk_readable_score(score),
                                     pcmk__node_name(node),
                                     pcmk_readable_score(new_score));
                 }
             }
 
             if (score > child_rsc->priority) {
                 pcmk__rsc_trace(rsc,
                                 "Updating %s priority to promotion score "
                                 "(%d->%d)",
                                 child_rsc->id, child_rsc->priority, score);
                 child_rsc->priority = score;
             }
         }
     }
 }
 
 /*!
  * \internal
  * \brief If a resource's current role is started, change it to unpromoted
  *
  * \param[in,out] data       Resource to update
  * \param[in]     user_data  Ignored
  */
 static void
 set_current_role_unpromoted(void *data, void *user_data)
 {
     pcmk_resource_t *rsc = (pcmk_resource_t *) data;
 
     if (rsc->role == pcmk_role_started) {
         // Promotable clones should use unpromoted role instead of started
         rsc->role = pcmk_role_unpromoted;
     }
     g_list_foreach(rsc->children, set_current_role_unpromoted, NULL);
 }
 
 /*!
  * \internal
  * \brief Set a resource's next role to unpromoted (or stopped if unassigned)
  *
  * \param[in,out] data       Resource to update
  * \param[in]     user_data  Ignored
  */
 static void
 set_next_role_unpromoted(void *data, void *user_data)
 {
     pcmk_resource_t *rsc = (pcmk_resource_t *) data;
     GList *assigned = NULL;
 
     rsc->private->fns->location(rsc, &assigned, FALSE);
     if (assigned == NULL) {
         pe__set_next_role(rsc, pcmk_role_stopped, "stopped instance");
     } else {
         pe__set_next_role(rsc, pcmk_role_unpromoted, "unpromoted instance");
         g_list_free(assigned);
     }
     g_list_foreach(rsc->children, set_next_role_unpromoted, NULL);
 }
 
 /*!
  * \internal
  * \brief Set a resource's next role to promoted if not already set
  *
  * \param[in,out] data       Resource to update
  * \param[in]     user_data  Ignored
  */
 static void
 set_next_role_promoted(void *data, gpointer user_data)
 {
     pcmk_resource_t *rsc = (pcmk_resource_t *) data;
 
     if (rsc->next_role == pcmk_role_unknown) {
         pe__set_next_role(rsc, pcmk_role_promoted, "promoted instance");
     }
     g_list_foreach(rsc->children, set_next_role_promoted, NULL);
 }
 
 /*!
  * \internal
  * \brief Show instance's promotion score on node where it will be active
  *
  * \param[in,out] instance  Promotable clone instance to show
  */
 static void
 show_promotion_score(pcmk_resource_t *instance)
 {
     pcmk_node_t *chosen = instance->private->fns->location(instance, NULL,
                                                            FALSE);
 
     if (pcmk_is_set(instance->cluster->flags, pcmk_sched_output_scores)
         && !pcmk__is_daemon && (instance->cluster->priv != NULL)) {
 
         pcmk__output_t *out = instance->cluster->priv;
 
         out->message(out, "promotion-score", instance, chosen,
                      pcmk_readable_score(instance->sort_index));
     } else {
         pcmk__rsc_debug(pe__const_top_resource(instance, false),
                         "%s promotion score on %s: sort=%s priority=%s",
                         instance->id,
                         ((chosen == NULL)? "none" : pcmk__node_name(chosen)),
                         pcmk_readable_score(instance->sort_index),
                         pcmk_readable_score(instance->priority));
     }
 }
 
 /*!
  * \internal
  * \brief Set a clone instance's promotion priority
  *
  * \param[in,out] data       Promotable clone instance to update
  * \param[in]     user_data  Instance's parent clone
  */
 static void
 set_instance_priority(gpointer data, gpointer user_data)
 {
     pcmk_resource_t *instance = (pcmk_resource_t *) data;
     const pcmk_resource_t *clone = (const pcmk_resource_t *) user_data;
 
     const pcmk_node_t *chosen = NULL;
     enum rsc_role_e next_role = pcmk_role_unknown;
     GList *list = NULL;
 
     pcmk__rsc_trace(clone, "Assigning priority for %s: %s", instance->id,
                     pcmk_role_text(instance->next_role));
 
     if (instance->private->fns->state(instance, TRUE) == pcmk_role_started) {
         set_current_role_unpromoted(instance, NULL);
     }
 
     // Only an instance that will be active can be promoted
     chosen = instance->private->fns->location(instance, &list, FALSE);
     if (pcmk__list_of_multiple(list)) {
         pcmk__config_err("Cannot promote non-colocated child %s",
                          instance->id);
     }
     g_list_free(list);
     if (chosen == NULL) {
         return;
     }
 
     next_role = instance->private->fns->state(instance, FALSE);
     switch (next_role) {
         case pcmk_role_started:
         case pcmk_role_unknown:
             // Set instance priority to its promotion score (or -1 if none)
             {
                 bool is_default = false;
 
                 instance->priority = promotion_score(instance, chosen,
                                                       &is_default);
                 if (is_default) {
                     /* Default to -1 if no value is set. This allows instances
                      * eligible for promotion to be specified based solely on
                      * PCMK_XE_RSC_LOCATION constraints, but prevents any
                      * instance from being promoted if neither a constraint nor
                      * a promotion score is present.
                      */
                     instance->priority = -1;
                 }
             }
             break;
 
         case pcmk_role_unpromoted:
         case pcmk_role_stopped:
             // Instance can't be promoted
             instance->priority = -PCMK_SCORE_INFINITY;
             break;
 
         case pcmk_role_promoted:
             // Nothing needed (re-creating actions after scheduling fencing)
             break;
 
         default:
             CRM_CHECK(FALSE, crm_err("Unknown resource role %d for %s",
                                      next_role, instance->id));
     }
 
     // Add relevant location constraint scores for promoted role
     apply_promoted_locations(instance, instance->rsc_location, chosen);
     apply_promoted_locations(instance, clone->rsc_location, chosen);
 
     // Consider instance's role-based colocations with other resources
     list = pcmk__this_with_colocations(instance);
     for (GList *iter = list; iter != NULL; iter = iter->next) {
         pcmk__colocation_t *cons = (pcmk__colocation_t *) iter->data;
 
-        instance->cmds->apply_coloc_score(instance, cons->primary, cons, true);
+        instance->private->cmds->apply_coloc_score(instance, cons->primary,
+                                                   cons, true);
     }
     g_list_free(list);
 
     instance->sort_index = instance->priority;
     if (next_role == pcmk_role_promoted) {
         instance->sort_index = PCMK_SCORE_INFINITY;
     }
     pcmk__rsc_trace(clone, "Assigning %s priority = %d",
                     instance->id, instance->priority);
 }
 
 /*!
  * \internal
  * \brief Set a promotable clone instance's role
  *
  * \param[in,out] data       Promotable clone instance to update
  * \param[in,out] user_data  Pointer to count of instances chosen for promotion
  */
 static void
 set_instance_role(gpointer data, gpointer user_data)
 {
     pcmk_resource_t *instance = (pcmk_resource_t *) data;
     int *count = (int *) user_data;
 
     const pcmk_resource_t *clone = pe__const_top_resource(instance, false);
     pcmk_node_t *chosen = NULL;
 
     show_promotion_score(instance);
 
     if (instance->sort_index < 0) {
         pcmk__rsc_trace(clone, "Not supposed to promote instance %s",
                         instance->id);
 
     } else if ((*count < pe__clone_promoted_max(instance))
                || !pcmk_is_set(clone->flags, pcmk_rsc_managed)) {
         chosen = node_to_be_promoted_on(instance);
     }
 
     if (chosen == NULL) {
         set_next_role_unpromoted(instance, NULL);
         return;
     }
 
     if ((instance->role < pcmk_role_promoted)
         && !pcmk_is_set(instance->cluster->flags, pcmk_sched_quorate)
         && (instance->cluster->no_quorum_policy == pcmk_no_quorum_freeze)) {
         crm_notice("Clone instance %s cannot be promoted without quorum",
                    instance->id);
         set_next_role_unpromoted(instance, NULL);
         return;
     }
 
     chosen->count++;
     pcmk__rsc_info(clone, "Choosing %s (%s) on %s for promotion",
                    instance->id, pcmk_role_text(instance->role),
                    pcmk__node_name(chosen));
     set_next_role_promoted(instance, NULL);
     (*count)++;
 }
 
 /*!
  * \internal
  * \brief Set roles for all instances of a promotable clone
  *
  * \param[in,out] rsc  Promotable clone resource to update
  */
 void
 pcmk__set_instance_roles(pcmk_resource_t *rsc)
 {
     int promoted = 0;
     GHashTableIter iter;
     pcmk_node_t *node = NULL;
 
     // Repurpose count to track the number of promoted instances assigned
     g_hash_table_iter_init(&iter, rsc->allowed_nodes);
     while (g_hash_table_iter_next(&iter, NULL, (void **)&node)) {
         node->count = 0;
     }
 
     // Set instances' promotion priorities and sort by highest priority first
     g_list_foreach(rsc->children, set_instance_priority, rsc);
     sort_promotable_instances(rsc);
 
     // Choose the first N eligible instances to be promoted
     g_list_foreach(rsc->children, set_instance_role, &promoted);
     pcmk__rsc_info(rsc, "%s: Promoted %d instances of a possible %d",
                    rsc->id, promoted, pe__clone_promoted_max(rsc));
 }
 
 /*!
  *
  * \internal
  * \brief Create actions for promotable clone instances
  *
  * \param[in,out] clone          Promotable clone to create actions for
  * \param[out]    any_promoting  Will be set true if any instance is promoting
  * \param[out]    any_demoting   Will be set true if any instance is demoting
  */
 static void
 create_promotable_instance_actions(pcmk_resource_t *clone,
                                    bool *any_promoting, bool *any_demoting)
 {
     for (GList *iter = clone->children; iter != NULL; iter = iter->next) {
         pcmk_resource_t *instance = (pcmk_resource_t *) iter->data;
 
-        instance->cmds->create_actions(instance);
+        instance->private->cmds->create_actions(instance);
         check_for_role_change(instance, any_demoting, any_promoting);
     }
 }
 
 /*!
  * \internal
  * \brief Reset each promotable instance's resource priority
  *
  * Reset the priority of each instance of a promotable clone to the clone's
  * priority (after promotion actions are scheduled, when instance priorities
  * were repurposed as promotion scores).
  *
  * \param[in,out] clone  Promotable clone to reset
  */
 static void
 reset_instance_priorities(pcmk_resource_t *clone)
 {
     for (GList *iter = clone->children; iter != NULL; iter = iter->next) {
         pcmk_resource_t *instance = (pcmk_resource_t *) iter->data;
 
         instance->priority = clone->priority;
     }
 }
 
 /*!
  * \internal
  * \brief Create actions specific to promotable clones
  *
  * \param[in,out] clone  Promotable clone to create actions for
  */
 void
 pcmk__create_promotable_actions(pcmk_resource_t *clone)
 {
     bool any_promoting = false;
     bool any_demoting = false;
 
     // Create actions for each clone instance individually
     create_promotable_instance_actions(clone, &any_promoting, &any_demoting);
 
     // Create pseudo-actions for clone as a whole
     pe__create_promotable_pseudo_ops(clone, any_promoting, any_demoting);
 
     // Undo our temporary repurposing of resource priority for instances
     reset_instance_priorities(clone);
 }
 
 /*!
  * \internal
  * \brief Create internal orderings for a promotable clone's instances
  *
  * \param[in,out] clone  Promotable clone instance to order
  */
 void
 pcmk__order_promotable_instances(pcmk_resource_t *clone)
 {
     pcmk_resource_t *previous = NULL; // Needed for ordered clones
 
     pcmk__promotable_restart_ordering(clone);
 
     for (GList *iter = clone->children; iter != NULL; iter = iter->next) {
         pcmk_resource_t *instance = (pcmk_resource_t *) iter->data;
 
         // Demote before promote
         pcmk__order_resource_actions(instance, PCMK_ACTION_DEMOTE,
                                      instance, PCMK_ACTION_PROMOTE,
                                      pcmk__ar_ordered);
 
         order_instance_promotion(clone, instance, previous);
         order_instance_demotion(clone, instance, previous);
         previous = instance;
     }
 }
 
 /*!
  * \internal
  * \brief Update dependent's allowed nodes for colocation with promotable
  *
  * \param[in,out] dependent     Dependent resource to update
  * \param[in]     primary       Primary resource
  * \param[in]     primary_node  Node where an instance of the primary will be
  * \param[in]     colocation    Colocation constraint to apply
  */
 static void
 update_dependent_allowed_nodes(pcmk_resource_t *dependent,
                                const pcmk_resource_t *primary,
                                const pcmk_node_t *primary_node,
                                const pcmk__colocation_t *colocation)
 {
     GHashTableIter iter;
     pcmk_node_t *node = NULL;
     const char *primary_value = NULL;
     const char *attr = colocation->node_attribute;
 
     if (colocation->score >= PCMK_SCORE_INFINITY) {
         return; // Colocation is mandatory, so allowed node scores don't matter
     }
 
     primary_value = pcmk__colocation_node_attr(primary_node, attr, primary);
 
     pcmk__rsc_trace(colocation->primary,
                     "Applying %s (%s with %s on %s by %s @%d) to %s",
                     colocation->id, colocation->dependent->id,
                     colocation->primary->id, pcmk__node_name(primary_node),
                     attr, colocation->score, dependent->id);
 
     g_hash_table_iter_init(&iter, dependent->allowed_nodes);
     while (g_hash_table_iter_next(&iter, NULL, (void **) &node)) {
         const char *dependent_value = pcmk__colocation_node_attr(node, attr,
                                                                  dependent);
 
         if (pcmk__str_eq(primary_value, dependent_value, pcmk__str_casei)) {
             node->weight = pcmk__add_scores(node->weight, colocation->score);
             pcmk__rsc_trace(colocation->primary,
                             "Added %s score (%s) to %s (now %s)",
                             colocation->id,
                             pcmk_readable_score(colocation->score),
                             pcmk__node_name(node),
                             pcmk_readable_score(node->weight));
         }
     }
 }
 
 /*!
  * \brief Update dependent for a colocation with a promotable clone
  *
  * \param[in]     primary     Primary resource in the colocation
  * \param[in,out] dependent   Dependent resource in the colocation
  * \param[in]     colocation  Colocation constraint to apply
  */
 void
 pcmk__update_dependent_with_promotable(const pcmk_resource_t *primary,
                                        pcmk_resource_t *dependent,
                                        const pcmk__colocation_t *colocation)
 {
     GList *affected_nodes = NULL;
 
     /* Build a list of all nodes where an instance of the primary will be, and
      * (for optional colocations) update the dependent's allowed node scores for
      * each one.
      */
     for (GList *iter = primary->children; iter != NULL; iter = iter->next) {
         pcmk_resource_t *instance = (pcmk_resource_t *) iter->data;
         pcmk_node_t *node = instance->private->fns->location(instance, NULL,
                                                              FALSE);
 
         if (node == NULL) {
             continue;
         }
         if (instance->private->fns->state(instance,
                                           FALSE) == colocation->primary_role) {
             update_dependent_allowed_nodes(dependent, primary, node,
                                            colocation);
             affected_nodes = g_list_prepend(affected_nodes, node);
         }
     }
 
     /* For mandatory colocations, add the primary's node score to the
      * dependent's node score for each affected node, and ban the dependent
      * from all other nodes.
      *
      * However, skip this for promoted-with-promoted colocations, otherwise
      * inactive dependent instances can't start (in the unpromoted role).
      */
     if ((colocation->score >= PCMK_SCORE_INFINITY)
         && ((colocation->dependent_role != pcmk_role_promoted)
             || (colocation->primary_role != pcmk_role_promoted))) {
 
         pcmk__rsc_trace(colocation->primary,
                         "Applying %s (mandatory %s with %s) to %s",
                         colocation->id, colocation->dependent->id,
                         colocation->primary->id, dependent->id);
         pcmk__colocation_intersect_nodes(dependent, primary, colocation,
                                          affected_nodes, true);
     }
     g_list_free(affected_nodes);
 }
 
 /*!
  * \internal
  * \brief Update dependent priority for colocation with promotable
  *
  * \param[in]     primary     Primary resource in the colocation
  * \param[in,out] dependent   Dependent resource in the colocation
  * \param[in]     colocation  Colocation constraint to apply
  */
 void
 pcmk__update_promotable_dependent_priority(const pcmk_resource_t *primary,
                                            pcmk_resource_t *dependent,
                                            const pcmk__colocation_t *colocation)
 {
     pcmk_resource_t *primary_instance = NULL;
 
     // Look for a primary instance where dependent will be
     primary_instance = pcmk__find_compatible_instance(dependent, primary,
                                                       colocation->primary_role,
                                                       false);
 
     if (primary_instance != NULL) {
         // Add primary instance's priority to dependent's
         int new_priority = pcmk__add_scores(dependent->priority,
                                             colocation->score);
 
         pcmk__rsc_trace(colocation->primary,
                         "Applying %s (%s with %s) to %s priority "
                         "(%s + %s = %s)",
                         colocation->id, colocation->dependent->id,
                         colocation->primary->id, dependent->id,
                         pcmk_readable_score(dependent->priority),
                         pcmk_readable_score(colocation->score),
                         pcmk_readable_score(new_priority));
         dependent->priority = new_priority;
 
     } else if (colocation->score >= PCMK_SCORE_INFINITY) {
         // Mandatory colocation, but primary won't be here
         pcmk__rsc_trace(colocation->primary,
                         "Applying %s (%s with %s) to %s: can't be promoted",
                         colocation->id, colocation->dependent->id,
                         colocation->primary->id, dependent->id);
         dependent->priority = -PCMK_SCORE_INFINITY;
     }
 }
diff --git a/lib/pacemaker/pcmk_sched_recurring.c b/lib/pacemaker/pcmk_sched_recurring.c
index 6a861b7958..693abaa4af 100644
--- a/lib/pacemaker/pcmk_sched_recurring.c
+++ b/lib/pacemaker/pcmk_sched_recurring.c
@@ -1,747 +1,747 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <stdbool.h>
 
 #include <crm/common/xml.h>
 #include <crm/common/scheduler_internal.h>
 #include <pacemaker-internal.h>
 
 #include "libpacemaker_private.h"
 
 // Information parsed from an operation history entry in the CIB
 struct op_history {
     // XML attributes
     const char *id;         // ID of history entry
     const char *name;       // Action name
 
     // Parsed information
     char *key;              // Operation key for action
     enum rsc_role_e role;   // Action role (or pcmk_role_unknown for default)
     guint interval_ms;      // Action interval
 };
 
 /*!
  * \internal
  * \brief Parse an interval from XML
  *
  * \param[in] xml  XML containing an interval attribute
  *
  * \return Interval parsed from XML (or 0 as default)
  */
 static guint
 xe_interval(const xmlNode *xml)
 {
     guint interval_ms = 0U;
 
     pcmk_parse_interval_spec(crm_element_value(xml, PCMK_META_INTERVAL),
                              &interval_ms);
     return interval_ms;
 }
 
 /*!
  * \internal
  * \brief Check whether an operation exists multiple times in resource history
  *
  * \param[in] rsc          Resource with history to search
  * \param[in] name         Name of action to search for
  * \param[in] interval_ms  Interval (in milliseconds) of action to search for
  *
  * \return true if an operation with \p name and \p interval_ms exists more than
  *         once in the operation history of \p rsc, otherwise false
  */
 static bool
 is_op_dup(const pcmk_resource_t *rsc, const char *name, guint interval_ms)
 {
     const char *id = NULL;
 
     for (xmlNode *op = pcmk__xe_first_child(rsc->ops_xml, PCMK_XE_OP, NULL,
                                             NULL);
          op != NULL; op = pcmk__xe_next_same(op)) {
 
         // Check whether action name and interval match
         if (!pcmk__str_eq(crm_element_value(op, PCMK_XA_NAME), name,
                           pcmk__str_none)
             || (xe_interval(op) != interval_ms)) {
             continue;
         }
 
         if (pcmk__xe_id(op) == NULL) {
             continue; // Shouldn't be possible
         }
 
         if (id == NULL) {
             id = pcmk__xe_id(op); // First matching op
         } else {
             pcmk__config_err("Operation %s is duplicate of %s (do not use "
                              "same name and interval combination more "
                              "than once per resource)", pcmk__xe_id(op), id);
             return true;
         }
     }
     return false;
 }
 
 /*!
  * \internal
  * \brief Check whether an action name is one that can be recurring
  *
  * \param[in] name  Action name to check
  *
  * \return true if \p name is an action known to be unsuitable as a recurring
  *         operation, otherwise false
  *
  * \note Pacemaker's current philosophy is to allow users to configure recurring
  *       operations except for a short list of actions known not to be suitable
  *       for that (as opposed to allowing only actions known to be suitable,
  *       which includes only monitor). Among other things, this approach allows
  *       users to define their own custom operations and make them recurring,
  *       though that use case is not well tested.
  */
 static bool
 op_cannot_recur(const char *name)
 {
     return pcmk__str_any_of(name, PCMK_ACTION_STOP, PCMK_ACTION_START,
                             PCMK_ACTION_DEMOTE, PCMK_ACTION_PROMOTE,
                             PCMK_ACTION_RELOAD_AGENT,
                             PCMK_ACTION_MIGRATE_TO, PCMK_ACTION_MIGRATE_FROM,
                             NULL);
 }
 
 /*!
  * \internal
  * \brief Check whether a resource history entry is for a recurring action
  *
  * \param[in]  rsc          Resource that history entry is for
  * \param[in]  xml          XML of resource history entry to check
  * \param[out] op           Where to store parsed info if recurring
  *
  * \return true if \p xml is for a recurring action, otherwise false
  */
 static bool
 is_recurring_history(const pcmk_resource_t *rsc, const xmlNode *xml,
                      struct op_history *op)
 {
     const char *role = NULL;
 
     op->interval_ms = xe_interval(xml);
     if (op->interval_ms == 0) {
         return false; // Not recurring
     }
 
     op->id = pcmk__xe_id(xml);
     if (pcmk__str_empty(op->id)) {
         pcmk__config_err("Ignoring resource history entry without ID");
         return false; // Shouldn't be possible (unless CIB was manually edited)
     }
 
     op->name = crm_element_value(xml, PCMK_XA_NAME);
     if (op_cannot_recur(op->name)) {
         pcmk__config_err("Ignoring %s because %s action cannot be recurring",
                          op->id, pcmk__s(op->name, "unnamed"));
         return false;
     }
 
     // There should only be one recurring operation per action/interval
     if (is_op_dup(rsc, op->name, op->interval_ms)) {
         return false;
     }
 
     // Ensure role is valid if specified
     role = crm_element_value(xml, PCMK_XA_ROLE);
     if (role == NULL) {
         op->role = pcmk_role_unknown;
     } else {
         op->role = pcmk_parse_role(role);
         if (op->role == pcmk_role_unknown) {
             pcmk__config_err("Ignoring %s role because %s is not a valid role",
                              op->id, role);
             return false;
         }
     }
 
     // Only actions that are still configured and enabled matter
     if (pcmk__find_action_config(rsc, op->name, op->interval_ms,
                                  false) == NULL) {
         pcmk__rsc_trace(rsc,
                         "Ignoring %s (%s-interval %s for %s) because it is "
                         "disabled or no longer in configuration",
                         op->id, pcmk__readable_interval(op->interval_ms),
                         op->name, rsc->id);
         return false;
     }
 
     op->key = pcmk__op_key(rsc->id, op->name, op->interval_ms);
     return true;
 }
 
 /*!
  * \internal
  * \brief Check whether a recurring action for an active role should be optional
  *
  * \param[in]     rsc    Resource that recurring action is for
  * \param[in]     node   Node that \p rsc will be active on (if any)
  * \param[in]     key    Operation key for recurring action to check
  * \param[in,out] start  Start action for \p rsc
  *
  * \return true if recurring action should be optional, otherwise false
  */
 static bool
 active_recurring_should_be_optional(const pcmk_resource_t *rsc,
                                     const pcmk_node_t *node, const char *key,
                                     pcmk_action_t *start)
 {
     GList *possible_matches = NULL;
 
     if (node == NULL) { // Should only be possible if unmanaged and stopped
         pcmk__rsc_trace(rsc,
                         "%s will be mandatory because resource is unmanaged",
                         key);
         return false;
     }
 
-    if (!pcmk_is_set(rsc->cmds->action_flags(start, NULL),
+    if (!pcmk_is_set(rsc->private->cmds->action_flags(start, NULL),
                      pcmk_action_optional)) {
         pcmk__rsc_trace(rsc, "%s will be mandatory because %s is",
                         key, start->uuid);
         return false;
     }
 
     possible_matches = find_actions_exact(rsc->actions, key, node);
     if (possible_matches == NULL) {
         pcmk__rsc_trace(rsc,
                         "%s will be mandatory because it is not active on %s",
                         key, pcmk__node_name(node));
         return false;
     }
 
     for (const GList *iter = possible_matches;
          iter != NULL; iter = iter->next) {
 
         const pcmk_action_t *op = (const pcmk_action_t *) iter->data;
 
         if (pcmk_is_set(op->flags, pcmk_action_reschedule)) {
             pcmk__rsc_trace(rsc,
                             "%s will be mandatory because "
                             "it needs to be rescheduled", key);
             g_list_free(possible_matches);
             return false;
         }
     }
 
     g_list_free(possible_matches);
     return true;
 }
 
 /*!
  * \internal
  * \brief Create recurring action from resource history entry for an active role
  *
  * \param[in,out] rsc    Resource that resource history is for
  * \param[in,out] start  Start action for \p rsc on \p node
  * \param[in]     node   Node that resource will be active on (if any)
  * \param[in]     op     Resource history entry
  */
 static void
 recurring_op_for_active(pcmk_resource_t *rsc, pcmk_action_t *start,
                         const pcmk_node_t *node, const struct op_history *op)
 {
     pcmk_action_t *mon = NULL;
     bool is_optional = true;
     bool role_match = false;
     enum rsc_role_e monitor_role = op->role;
 
     // We're only interested in recurring actions for active roles
     if (monitor_role == pcmk_role_stopped) {
         return;
     }
 
     is_optional = active_recurring_should_be_optional(rsc, node, op->key,
                                                       start);
 
     // Check whether monitor's role matches role resource will have
     if (monitor_role == pcmk_role_unknown) {
         monitor_role = pcmk_role_unpromoted;
         role_match = (rsc->next_role != pcmk_role_promoted);
     } else {
         role_match = (rsc->next_role == monitor_role);
     }
 
     if (!role_match) {
         if (is_optional) { // It's running, so cancel it
             char *after_key = NULL;
             pcmk_action_t *cancel_op = pcmk__new_cancel_action(rsc, op->name,
                                                                op->interval_ms,
                                                                node);
 
             switch (rsc->role) {
                 case pcmk_role_unpromoted:
                 case pcmk_role_started:
                     if (rsc->next_role == pcmk_role_promoted) {
                         after_key = promote_key(rsc);
 
                     } else if (rsc->next_role == pcmk_role_stopped) {
                         after_key = stop_key(rsc);
                     }
 
                     break;
                 case pcmk_role_promoted:
                     after_key = demote_key(rsc);
                     break;
                 default:
                     break;
             }
 
             if (after_key) {
                 pcmk__new_ordering(rsc, NULL, cancel_op, rsc, after_key, NULL,
                                    pcmk__ar_unrunnable_first_blocks,
                                    rsc->cluster);
             }
         }
 
         do_crm_log((is_optional? LOG_INFO : LOG_TRACE),
                    "%s recurring action %s because %s configured for %s role "
                    "(not %s)",
                    (is_optional? "Cancelling" : "Ignoring"), op->key, op->id,
                    pcmk_role_text(monitor_role),
                    pcmk_role_text(rsc->next_role));
         return;
     }
 
     pcmk__rsc_trace(rsc,
                     "Creating %s recurring action %s for %s (%s %s on %s)",
                     (is_optional? "optional" : "mandatory"), op->key,
                     op->id, rsc->id, pcmk_role_text(rsc->next_role),
                     pcmk__node_name(node));
 
     mon = custom_action(rsc, strdup(op->key), op->name, node, is_optional,
                         rsc->cluster);
 
     if (!pcmk_is_set(start->flags, pcmk_action_runnable)) {
         pcmk__rsc_trace(rsc, "%s is unrunnable because start is", mon->uuid);
         pcmk__clear_action_flags(mon, pcmk_action_runnable);
 
     } else if ((node == NULL) || !node->details->online
                || node->details->unclean) {
         pcmk__rsc_trace(rsc, "%s is unrunnable because no node is available",
                         mon->uuid);
         pcmk__clear_action_flags(mon, pcmk_action_runnable);
 
     } else if (!pcmk_is_set(mon->flags, pcmk_action_optional)) {
         pcmk__rsc_info(rsc, "Start %s-interval %s for %s on %s",
                        pcmk__readable_interval(op->interval_ms), mon->task,
                        rsc->id, pcmk__node_name(node));
     }
 
     if (rsc->next_role == pcmk_role_promoted) {
         pe__add_action_expected_result(mon, CRM_EX_PROMOTED);
     }
 
     // Order monitor relative to other actions
     if ((node == NULL) || pcmk_is_set(rsc->flags, pcmk_rsc_managed)) {
         pcmk__new_ordering(rsc, start_key(rsc), NULL,
                            NULL, strdup(mon->uuid), mon,
                            pcmk__ar_first_implies_then
                            |pcmk__ar_unrunnable_first_blocks,
                            rsc->cluster);
 
         pcmk__new_ordering(rsc, reload_key(rsc), NULL,
                            NULL, strdup(mon->uuid), mon,
                            pcmk__ar_first_implies_then
                            |pcmk__ar_unrunnable_first_blocks,
                            rsc->cluster);
 
         if (rsc->next_role == pcmk_role_promoted) {
             pcmk__new_ordering(rsc, promote_key(rsc), NULL,
                                rsc, NULL, mon,
                                pcmk__ar_ordered
                                |pcmk__ar_unrunnable_first_blocks,
                                rsc->cluster);
 
         } else if (rsc->role == pcmk_role_promoted) {
             pcmk__new_ordering(rsc, demote_key(rsc), NULL,
                                rsc, NULL, mon,
                                pcmk__ar_ordered
                                |pcmk__ar_unrunnable_first_blocks,
                                rsc->cluster);
         }
     }
 }
 
 /*!
  * \internal
  * \brief Cancel a recurring action if running on a node
  *
  * \param[in,out] rsc          Resource that action is for
  * \param[in]     node         Node to cancel action on
  * \param[in]     key          Operation key for action
  * \param[in]     name         Action name
  * \param[in]     interval_ms  Action interval (in milliseconds)
  */
 static void
 cancel_if_running(pcmk_resource_t *rsc, const pcmk_node_t *node,
                   const char *key, const char *name, guint interval_ms)
 {
     GList *possible_matches = find_actions_exact(rsc->actions, key, node);
     pcmk_action_t *cancel_op = NULL;
 
     if (possible_matches == NULL) {
         return; // Recurring action isn't running on this node
     }
     g_list_free(possible_matches);
 
     cancel_op = pcmk__new_cancel_action(rsc, name, interval_ms, node);
 
     switch (rsc->next_role) {
         case pcmk_role_started:
         case pcmk_role_unpromoted:
             /* Order starts after cancel. If the current role is
              * stopped, this cancels the monitor before the resource
              * starts; if the current role is started, then this cancels
              * the monitor on a migration target before starting there.
              */
             pcmk__new_ordering(rsc, NULL, cancel_op,
                                rsc, start_key(rsc), NULL,
                                pcmk__ar_unrunnable_first_blocks, rsc->cluster);
             break;
         default:
             break;
     }
     pcmk__rsc_info(rsc,
                    "Cancelling %s-interval %s action for %s on %s because "
                    "configured for " PCMK_ROLE_STOPPED " role (not %s)",
                    pcmk__readable_interval(interval_ms), name, rsc->id,
                    pcmk__node_name(node), pcmk_role_text(rsc->next_role));
 }
 
 /*!
  * \internal
  * \brief Order an action after all probes of a resource on a node
  *
  * \param[in,out] rsc     Resource to check for probes
  * \param[in]     node    Node to check for probes of \p rsc
  * \param[in,out] action  Action to order after probes of \p rsc on \p node
  */
 static void
 order_after_probes(pcmk_resource_t *rsc, const pcmk_node_t *node,
                    pcmk_action_t *action)
 {
     GList *probes = pe__resource_actions(rsc, node, PCMK_ACTION_MONITOR, FALSE);
 
     for (GList *iter = probes; iter != NULL; iter = iter->next) {
         order_actions((pcmk_action_t *) iter->data, action,
                       pcmk__ar_unrunnable_first_blocks);
     }
     g_list_free(probes);
 }
 
 /*!
  * \internal
  * \brief Order an action after all stops of a resource on a node
  *
  * \param[in,out] rsc     Resource to check for stops
  * \param[in]     node    Node to check for stops of \p rsc
  * \param[in,out] action  Action to order after stops of \p rsc on \p node
  */
 static void
 order_after_stops(pcmk_resource_t *rsc, const pcmk_node_t *node,
                   pcmk_action_t *action)
 {
     GList *stop_ops = pe__resource_actions(rsc, node, PCMK_ACTION_STOP, TRUE);
 
     for (GList *iter = stop_ops; iter != NULL; iter = iter->next) {
         pcmk_action_t *stop = (pcmk_action_t *) iter->data;
 
         if (!pcmk_is_set(stop->flags, pcmk_action_optional)
             && !pcmk_is_set(action->flags, pcmk_action_optional)
             && !pcmk_is_set(rsc->flags, pcmk_rsc_managed)) {
             pcmk__rsc_trace(rsc, "%s optional on %s: unmanaged",
                             action->uuid, pcmk__node_name(node));
             pcmk__set_action_flags(action, pcmk_action_optional);
         }
 
         if (!pcmk_is_set(stop->flags, pcmk_action_runnable)) {
             crm_debug("%s unrunnable on %s: stop is unrunnable",
                       action->uuid, pcmk__node_name(node));
             pcmk__clear_action_flags(action, pcmk_action_runnable);
         }
 
         if (pcmk_is_set(rsc->flags, pcmk_rsc_managed)) {
             pcmk__new_ordering(rsc, stop_key(rsc), stop,
                                NULL, NULL, action,
                                pcmk__ar_first_implies_then
                                |pcmk__ar_unrunnable_first_blocks,
                                rsc->cluster);
         }
     }
     g_list_free(stop_ops);
 }
 
 /*!
  * \internal
  * \brief Create recurring action from resource history entry for inactive role
  *
  * \param[in,out] rsc    Resource that resource history is for
  * \param[in]     node   Node that resource will be active on (if any)
  * \param[in]     op     Resource history entry
  */
 static void
 recurring_op_for_inactive(pcmk_resource_t *rsc, const pcmk_node_t *node,
                           const struct op_history *op)
 {
     GList *possible_matches = NULL;
 
     // We're only interested in recurring actions for the inactive role
     if (op->role != pcmk_role_stopped) {
         return;
     }
 
     if (!pcmk_is_set(rsc->flags, pcmk_rsc_unique)) {
         crm_notice("Ignoring %s (recurring monitors for " PCMK_ROLE_STOPPED
                    " role are not supported for anonymous clones)", op->id);
         return; // @TODO add support
     }
 
     pcmk__rsc_trace(rsc,
                     "Creating recurring action %s for %s on nodes "
                     "where it should not be running", op->id, rsc->id);
 
     for (GList *iter = rsc->cluster->nodes; iter != NULL; iter = iter->next) {
         pcmk_node_t *stop_node = (pcmk_node_t *) iter->data;
 
         bool is_optional = true;
         pcmk_action_t *stopped_mon = NULL;
 
         // Cancel action on node where resource will be active
         if ((node != NULL)
             && pcmk__str_eq(stop_node->details->uname, node->details->uname,
                             pcmk__str_casei)) {
             cancel_if_running(rsc, node, op->key, op->name, op->interval_ms);
             continue;
         }
 
         // Recurring action on this node is optional if it's already active here
         possible_matches = find_actions_exact(rsc->actions, op->key, stop_node);
         is_optional = (possible_matches != NULL);
         g_list_free(possible_matches);
 
         pcmk__rsc_trace(rsc,
                         "Creating %s recurring action %s for %s (%s "
                         PCMK_ROLE_STOPPED " on %s)",
                         (is_optional? "optional" : "mandatory"),
                         op->key, op->id, rsc->id, pcmk__node_name(stop_node));
 
         stopped_mon = custom_action(rsc, strdup(op->key), op->name, stop_node,
                                     is_optional, rsc->cluster);
 
         pe__add_action_expected_result(stopped_mon, CRM_EX_NOT_RUNNING);
 
         if (pcmk_is_set(rsc->flags, pcmk_rsc_managed)) {
             order_after_probes(rsc, stop_node, stopped_mon);
         }
 
         /* The recurring action is for the inactive role, so it shouldn't be
          * performed until the resource is inactive.
          */
         order_after_stops(rsc, stop_node, stopped_mon);
 
         if (!stop_node->details->online || stop_node->details->unclean) {
             pcmk__rsc_debug(rsc, "%s unrunnable on %s: node unavailable)",
                             stopped_mon->uuid, pcmk__node_name(stop_node));
             pcmk__clear_action_flags(stopped_mon, pcmk_action_runnable);
         }
 
         if (pcmk_is_set(stopped_mon->flags, pcmk_action_runnable)
             && !pcmk_is_set(stopped_mon->flags, pcmk_action_optional)) {
             crm_notice("Start recurring %s-interval %s for "
                        PCMK_ROLE_STOPPED " %s on %s",
                        pcmk__readable_interval(op->interval_ms),
                        stopped_mon->task, rsc->id, pcmk__node_name(stop_node));
         }
     }
 }
 
 /*!
  * \internal
  * \brief Create recurring actions for a resource
  *
  * \param[in,out] rsc  Resource to create recurring actions for
  */
 void
 pcmk__create_recurring_actions(pcmk_resource_t *rsc)
 {
     pcmk_action_t *start = NULL;
 
     if (pcmk_is_set(rsc->flags, pcmk_rsc_blocked)) {
         pcmk__rsc_trace(rsc,
                         "Skipping recurring actions for blocked resource %s",
                         rsc->id);
         return;
     }
 
     if (pcmk_is_set(rsc->flags, pcmk_rsc_maintenance)) {
         pcmk__rsc_trace(rsc,
                         "Skipping recurring actions for %s "
                         "in maintenance mode", rsc->id);
         return;
     }
 
     if (rsc->allocated_to == NULL) {
         // Recurring actions for active roles not needed
 
     } else if (rsc->allocated_to->details->maintenance) {
         pcmk__rsc_trace(rsc,
                         "Skipping recurring actions for %s on %s "
                         "in maintenance mode",
                         rsc->id, pcmk__node_name(rsc->allocated_to));
 
     } else if ((rsc->next_role != pcmk_role_stopped)
         || !pcmk_is_set(rsc->flags, pcmk_rsc_managed)) {
         // Recurring actions for active roles needed
         start = start_action(rsc, rsc->allocated_to, TRUE);
     }
 
     pcmk__rsc_trace(rsc, "Creating any recurring actions needed for %s",
                     rsc->id);
 
     for (xmlNode *op = pcmk__xe_first_child(rsc->ops_xml, PCMK_XE_OP, NULL,
                                             NULL);
          op != NULL; op = pcmk__xe_next_same(op)) {
 
         struct op_history op_history = { NULL, };
 
         if (!is_recurring_history(rsc, op, &op_history)) {
             continue;
         }
 
         if (start != NULL) {
             recurring_op_for_active(rsc, start, rsc->allocated_to, &op_history);
         }
         recurring_op_for_inactive(rsc, rsc->allocated_to, &op_history);
 
         free(op_history.key);
     }
 }
 
 /*!
  * \internal
  * \brief Create an executor cancel action
  *
  * \param[in,out] rsc          Resource of action to cancel
  * \param[in]     task         Name of action to cancel
  * \param[in]     interval_ms  Interval of action to cancel
  * \param[in]     node         Node of action to cancel
  *
  * \return Created op
  */
 pcmk_action_t *
 pcmk__new_cancel_action(pcmk_resource_t *rsc, const char *task,
                         guint interval_ms, const pcmk_node_t *node)
 {
     pcmk_action_t *cancel_op = NULL;
     char *key = NULL;
     char *interval_ms_s = NULL;
 
     CRM_ASSERT((rsc != NULL) && (task != NULL) && (node != NULL));
 
     key = pcmk__op_key(rsc->id, task, interval_ms);
 
     /* This finds an existing action by key, so custom_action() does not change
      * cancel_op->task.
      */
     cancel_op = custom_action(rsc, key, PCMK_ACTION_CANCEL, node, FALSE,
                               rsc->cluster);
 
     pcmk__str_update(&(cancel_op->task), PCMK_ACTION_CANCEL);
     pcmk__str_update(&(cancel_op->cancel_task), task);
 
     interval_ms_s = crm_strdup_printf("%u", interval_ms);
     pcmk__insert_meta(cancel_op, PCMK_XA_OPERATION, task);
     pcmk__insert_meta(cancel_op, PCMK_META_INTERVAL, interval_ms_s);
     free(interval_ms_s);
 
     return cancel_op;
 }
 
 /*!
  * \internal
  * \brief Schedule cancellation of a recurring action
  *
  * \param[in,out] rsc          Resource that action is for
  * \param[in]     call_id      Action's call ID from history
  * \param[in]     task         Action name
  * \param[in]     interval_ms  Action interval
  * \param[in]     node         Node that history entry is for
  * \param[in]     reason       Short description of why action is cancelled
  */
 void
 pcmk__schedule_cancel(pcmk_resource_t *rsc, const char *call_id,
                       const char *task, guint interval_ms,
                       const pcmk_node_t *node, const char *reason)
 {
     pcmk_action_t *cancel = NULL;
 
     CRM_CHECK((rsc != NULL) && (task != NULL)
               && (node != NULL) && (reason != NULL),
               return);
 
     crm_info("Recurring %s-interval %s for %s will be stopped on %s: %s",
              pcmk__readable_interval(interval_ms), task, rsc->id,
              pcmk__node_name(node), reason);
     cancel = pcmk__new_cancel_action(rsc, task, interval_ms, node);
     pcmk__insert_meta(cancel, PCMK__XA_CALL_ID, call_id);
 
     // Cancellations happen after stops
     pcmk__new_ordering(rsc, stop_key(rsc), NULL, rsc, NULL, cancel,
                        pcmk__ar_ordered, rsc->cluster);
 }
 
 /*!
  * \internal
  * \brief Create a recurring action marked as needing rescheduling if active
  *
  * \param[in,out] rsc          Resource that action is for
  * \param[in]     task         Name of action being rescheduled
  * \param[in]     interval_ms  Action interval (in milliseconds)
  * \param[in,out] node         Node where action should be rescheduled
  */
 void
 pcmk__reschedule_recurring(pcmk_resource_t *rsc, const char *task,
                            guint interval_ms, pcmk_node_t *node)
 {
     pcmk_action_t *op = NULL;
 
     trigger_unfencing(rsc, node, "Device parameters changed (reschedule)",
                       NULL, rsc->cluster);
     op = custom_action(rsc, pcmk__op_key(rsc->id, task, interval_ms),
                        task, node, TRUE, rsc->cluster);
     pcmk__set_action_flags(op, pcmk_action_reschedule);
 }
 
 /*!
  * \internal
  * \brief Check whether an action is recurring
  *
  * \param[in] action  Action to check
  *
  * \return true if \p action has a nonzero interval, otherwise false
  */
 bool
 pcmk__action_is_recurring(const pcmk_action_t *action)
 {
     guint interval_ms = 0;
 
     if (pcmk__guint_from_hash(action->meta, PCMK_META_INTERVAL, 0,
                               &interval_ms) != pcmk_rc_ok) {
         return false;
     }
     return (interval_ms > 0);
 }
diff --git a/lib/pacemaker/pcmk_sched_resource.c b/lib/pacemaker/pcmk_sched_resource.c
index 0791994f8f..892719d153 100644
--- a/lib/pacemaker/pcmk_sched_resource.c
+++ b/lib/pacemaker/pcmk_sched_resource.c
@@ -1,774 +1,776 @@
 /*
  * Copyright 2014-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <stdlib.h>
 #include <string.h>
 #include <crm/common/xml.h>
 #include <pacemaker-internal.h>
 
 #include "libpacemaker_private.h"
 
 // Resource assignment methods by resource variant
 static pcmk_assignment_methods_t assignment_methods[] = {
     {
         pcmk__primitive_assign,
         pcmk__primitive_create_actions,
         pcmk__probe_rsc_on_node,
         pcmk__primitive_internal_constraints,
         pcmk__primitive_apply_coloc_score,
         pcmk__colocated_resources,
         pcmk__with_primitive_colocations,
         pcmk__primitive_with_colocations,
         pcmk__add_colocated_node_scores,
         pcmk__apply_location,
         pcmk__primitive_action_flags,
         pcmk__update_ordered_actions,
         pcmk__output_resource_actions,
         pcmk__add_rsc_actions_to_graph,
         pcmk__primitive_add_graph_meta,
         pcmk__primitive_add_utilization,
         pcmk__primitive_shutdown_lock,
     },
     {
         pcmk__group_assign,
         pcmk__group_create_actions,
         pcmk__probe_rsc_on_node,
         pcmk__group_internal_constraints,
         pcmk__group_apply_coloc_score,
         pcmk__group_colocated_resources,
         pcmk__with_group_colocations,
         pcmk__group_with_colocations,
         pcmk__group_add_colocated_node_scores,
         pcmk__group_apply_location,
         pcmk__group_action_flags,
         pcmk__group_update_ordered_actions,
         pcmk__output_resource_actions,
         pcmk__add_rsc_actions_to_graph,
         pcmk__noop_add_graph_meta,
         pcmk__group_add_utilization,
         pcmk__group_shutdown_lock,
     },
     {
         pcmk__clone_assign,
         pcmk__clone_create_actions,
         pcmk__clone_create_probe,
         pcmk__clone_internal_constraints,
         pcmk__clone_apply_coloc_score,
         pcmk__colocated_resources,
         pcmk__with_clone_colocations,
         pcmk__clone_with_colocations,
         pcmk__add_colocated_node_scores,
         pcmk__clone_apply_location,
         pcmk__clone_action_flags,
         pcmk__instance_update_ordered_actions,
         pcmk__output_resource_actions,
         pcmk__clone_add_actions_to_graph,
         pcmk__clone_add_graph_meta,
         pcmk__clone_add_utilization,
         pcmk__clone_shutdown_lock,
     },
     {
         pcmk__bundle_assign,
         pcmk__bundle_create_actions,
         pcmk__bundle_create_probe,
         pcmk__bundle_internal_constraints,
         pcmk__bundle_apply_coloc_score,
         pcmk__colocated_resources,
         pcmk__with_bundle_colocations,
         pcmk__bundle_with_colocations,
         pcmk__add_colocated_node_scores,
         pcmk__bundle_apply_location,
         pcmk__bundle_action_flags,
         pcmk__instance_update_ordered_actions,
         pcmk__output_bundle_actions,
         pcmk__bundle_add_actions_to_graph,
         pcmk__noop_add_graph_meta,
         pcmk__bundle_add_utilization,
         pcmk__bundle_shutdown_lock,
     }
 };
 
 /*!
  * \internal
  * \brief Check whether a resource's agent standard, provider, or type changed
  *
  * \param[in,out] rsc             Resource to check
  * \param[in,out] node            Node needing unfencing if agent changed
  * \param[in]     rsc_entry       XML with previously known agent information
  * \param[in]     active_on_node  Whether \p rsc is active on \p node
  *
  * \return true if agent for \p rsc changed, otherwise false
  */
 bool
 pcmk__rsc_agent_changed(pcmk_resource_t *rsc, pcmk_node_t *node,
                         const xmlNode *rsc_entry, bool active_on_node)
 {
     bool changed = false;
     const char *attr_list[] = {
         PCMK_XA_TYPE,
         PCMK_XA_CLASS,
         PCMK_XA_PROVIDER,
     };
 
     for (int i = 0; i < PCMK__NELEM(attr_list); i++) {
         const char *value = crm_element_value(rsc->xml, attr_list[i]);
         const char *old_value = crm_element_value(rsc_entry, attr_list[i]);
 
         if (!pcmk__str_eq(value, old_value, pcmk__str_none)) {
             changed = true;
             trigger_unfencing(rsc, node, "Device definition changed", NULL,
                               rsc->cluster);
             if (active_on_node) {
                 crm_notice("Forcing restart of %s on %s "
                            "because %s changed from '%s' to '%s'",
                            rsc->id, pcmk__node_name(node), attr_list[i],
                            pcmk__s(old_value, ""), pcmk__s(value, ""));
             }
         }
     }
     if (changed && active_on_node) {
         // Make sure the resource is restarted
         custom_action(rsc, stop_key(rsc), PCMK_ACTION_STOP, node, FALSE,
                       rsc->cluster);
         pcmk__set_rsc_flags(rsc, pcmk_rsc_start_pending);
     }
     return changed;
 }
 
 /*!
  * \internal
  * \brief Add resource (and any matching children) to list if it matches ID
  *
  * \param[in] result  List to add resource to
  * \param[in] rsc     Resource to check
  * \param[in] id      ID to match
  *
  * \return (Possibly new) head of list
  */
 static GList *
 add_rsc_if_matching(GList *result, pcmk_resource_t *rsc, const char *id)
 {
     if ((strcmp(rsc->id, id) == 0)
         || ((rsc->clone_name != NULL) && (strcmp(rsc->clone_name, id) == 0))) {
         result = g_list_prepend(result, rsc);
     }
     for (GList *iter = rsc->children; iter != NULL; iter = iter->next) {
         pcmk_resource_t *child = (pcmk_resource_t *) iter->data;
 
         result = add_rsc_if_matching(result, child, id);
     }
     return result;
 }
 
 /*!
  * \internal
  * \brief Find all resources matching a given ID by either ID or clone name
  *
  * \param[in] id         Resource ID to check
  * \param[in] scheduler  Scheduler data
  *
  * \return List of all resources that match \p id
  * \note The caller is responsible for freeing the return value with
  *       g_list_free().
  */
 GList *
 pcmk__rscs_matching_id(const char *id, const pcmk_scheduler_t *scheduler)
 {
     GList *result = NULL;
 
     CRM_CHECK((id != NULL) && (scheduler != NULL), return NULL);
     for (GList *iter = scheduler->resources; iter != NULL; iter = iter->next) {
         result = add_rsc_if_matching(result, (pcmk_resource_t *) iter->data,
                                      id);
     }
     return result;
 }
 
 /*!
  * \internal
  * \brief Set the variant-appropriate assignment methods for a resource
  *
  * \param[in,out] data       Resource to set assignment methods for
  * \param[in]     user_data  Ignored
  */
 static void
 set_assignment_methods_for_rsc(gpointer data, gpointer user_data)
 {
     pcmk_resource_t *rsc = data;
 
-    rsc->cmds = &assignment_methods[rsc->variant];
+    rsc->private->cmds = &assignment_methods[rsc->variant];
     g_list_foreach(rsc->children, set_assignment_methods_for_rsc, NULL);
 }
 
 /*!
  * \internal
  * \brief Set the variant-appropriate assignment methods for all resources
  *
  * \param[in,out] scheduler  Scheduler data
  */
 void
 pcmk__set_assignment_methods(pcmk_scheduler_t *scheduler)
 {
     g_list_foreach(scheduler->resources, set_assignment_methods_for_rsc, NULL);
 }
 
 /*!
  * \internal
  * \brief Wrapper for colocated_resources() method for readability
  *
  * \param[in]      rsc       Resource to add to colocated list
  * \param[in]      orig_rsc  Resource originally requested
  * \param[in,out]  list      Pointer to list to add to
  *
  * \return (Possibly new) head of list
  */
 static inline void
 add_colocated_resources(const pcmk_resource_t *rsc,
                         const pcmk_resource_t *orig_rsc, GList **list)
 {
-    *list = rsc->cmds->colocated_resources(rsc, orig_rsc, *list);
+    *list = rsc->private->cmds->colocated_resources(rsc, orig_rsc, *list);
 }
 
 // Shared implementation of pcmk_assignment_methods_t:colocated_resources()
 GList *
 pcmk__colocated_resources(const pcmk_resource_t *rsc,
                           const pcmk_resource_t *orig_rsc,
                           GList *colocated_rscs)
 {
     const GList *iter = NULL;
     GList *colocations = NULL;
 
     if (orig_rsc == NULL) {
         orig_rsc = rsc;
     }
 
     if ((rsc == NULL) || (g_list_find(colocated_rscs, rsc) != NULL)) {
         return colocated_rscs;
     }
 
     pcmk__rsc_trace(orig_rsc, "%s is in colocation chain with %s",
                     rsc->id, orig_rsc->id);
     colocated_rscs = g_list_prepend(colocated_rscs, (gpointer) rsc);
 
     // Follow colocations where this resource is the dependent resource
     colocations = pcmk__this_with_colocations(rsc);
     for (iter = colocations; iter != NULL; iter = iter->next) {
         const pcmk__colocation_t *constraint = iter->data;
         const pcmk_resource_t *primary = constraint->primary;
 
         if (primary == orig_rsc) {
             continue; // Break colocation loop
         }
 
         if ((constraint->score == PCMK_SCORE_INFINITY) &&
             (pcmk__colocation_affects(rsc, primary, constraint,
                                       true) == pcmk__coloc_affects_location)) {
             add_colocated_resources(primary, orig_rsc, &colocated_rscs);
         }
     }
     g_list_free(colocations);
 
     // Follow colocations where this resource is the primary resource
     colocations = pcmk__with_this_colocations(rsc);
     for (iter = colocations; iter != NULL; iter = iter->next) {
         const pcmk__colocation_t *constraint = iter->data;
         const pcmk_resource_t *dependent = constraint->dependent;
 
         if (dependent == orig_rsc) {
             continue; // Break colocation loop
         }
 
         if (pcmk__is_clone(rsc) && !pcmk__is_clone(dependent)) {
             continue; // We can't be sure whether dependent will be colocated
         }
 
         if ((constraint->score == PCMK_SCORE_INFINITY) &&
             (pcmk__colocation_affects(dependent, rsc, constraint,
                                       true) == pcmk__coloc_affects_location)) {
             add_colocated_resources(dependent, orig_rsc, &colocated_rscs);
         }
     }
     g_list_free(colocations);
 
     return colocated_rscs;
 }
 
 // No-op function for variants that don't need to implement add_graph_meta()
 void
 pcmk__noop_add_graph_meta(const pcmk_resource_t *rsc, xmlNode *xml)
 {
 }
 
 /*!
  * \internal
  * \brief Output a summary of scheduled actions for a resource
  *
  * \param[in,out] rsc  Resource to output actions for
  */
 void
 pcmk__output_resource_actions(pcmk_resource_t *rsc)
 {
     pcmk_node_t *next = NULL;
     pcmk_node_t *current = NULL;
     pcmk__output_t *out = NULL;
 
     CRM_ASSERT(rsc != NULL);
 
     out = rsc->cluster->priv;
     if (rsc->children != NULL) {
         for (GList *iter = rsc->children; iter != NULL; iter = iter->next) {
             pcmk_resource_t *child = (pcmk_resource_t *) iter->data;
 
-            child->cmds->output_actions(child);
+            child->private->cmds->output_actions(child);
         }
         return;
     }
 
     next = rsc->allocated_to;
     if (rsc->running_on) {
         current = pcmk__current_node(rsc);
         if (rsc->role == pcmk_role_stopped) {
             /* This can occur when resources are being recovered because
              * the current role can change in pcmk__primitive_create_actions()
              */
             rsc->role = pcmk_role_started;
         }
     }
 
     if ((current == NULL) && pcmk_is_set(rsc->flags, pcmk_rsc_removed)) {
         /* Don't log stopped orphans */
         return;
     }
 
     out->message(out, "rsc-action", rsc, current, next);
 }
 
 /*!
  * \internal
  * \brief Add a resource to a node's list of assigned resources
  *
  * \param[in,out] node  Node to add resource to
  * \param[in]     rsc   Resource to add
  */
 static inline void
 add_assigned_resource(pcmk_node_t *node, pcmk_resource_t *rsc)
 {
     node->details->allocated_rsc = g_list_prepend(node->details->allocated_rsc,
                                                   rsc);
 }
 
 /*!
  * \internal
  * \brief Assign a specified resource (of any variant) to a node
  *
  * Assign a specified resource and its children (if any) to a specified node, if
  * the node can run the resource (or unconditionally, if \p force is true). Mark
  * the resources as no longer provisional.
  *
  * If a resource can't be assigned (or \p node is \c NULL), unassign any
  * previous assignment. If \p stop_if_fail is \c true, set next role to stopped
  * and update any existing actions scheduled for the resource.
  *
  * \param[in,out] rsc           Resource to assign
  * \param[in,out] node          Node to assign \p rsc to
  * \param[in]     force         If true, assign to \p node even if unavailable
  * \param[in]     stop_if_fail  If \c true and either \p rsc can't be assigned
  *                              or \p chosen is \c NULL, set next role to
  *                              stopped and update existing actions (if \p rsc
  *                              is not a primitive, this applies to its
  *                              primitive descendants instead)
  *
  * \return \c true if the assignment of \p rsc changed, or \c false otherwise
  *
  * \note Assigning a resource to the NULL node using this function is different
  *       from calling pcmk__unassign_resource(), in that it may also update any
  *       actions created for the resource.
  * \note The \c pcmk_assignment_methods_t:assign() method is preferred, unless
  *       a resource should be assigned to the \c NULL node or every resource in
  *       a tree should be assigned to the same node.
  * \note If \p stop_if_fail is \c false, then \c pcmk__unassign_resource() can
  *       completely undo the assignment. A successful assignment can be either
  *       undone or left alone as final. A failed assignment has the same effect
  *       as calling pcmk__unassign_resource(); there are no side effects on
  *       roles or actions.
  */
 bool
 pcmk__assign_resource(pcmk_resource_t *rsc, pcmk_node_t *node, bool force,
                       bool stop_if_fail)
 {
     bool changed = false;
 
     CRM_ASSERT(rsc != NULL);
 
     if (rsc->children != NULL) {
         for (GList *iter = rsc->children; iter != NULL; iter = iter->next) {
             pcmk_resource_t *child_rsc = iter->data;
 
             changed |= pcmk__assign_resource(child_rsc, node, force,
                                              stop_if_fail);
         }
         return changed;
     }
 
     // Assigning a primitive
 
     if (!force && (node != NULL)
         && ((node->weight < 0)
             // Allow graph to assume that guest node connections will come up
             || (!pcmk__node_available(node, true, false)
                 && !pcmk__is_guest_or_bundle_node(node)))) {
 
         pcmk__rsc_debug(rsc,
                         "All nodes for resource %s are unavailable, unclean or "
                         "shutting down (%s can%s run resources, with score %s)",
                         rsc->id, pcmk__node_name(node),
                         (pcmk__node_available(node, true, false)? "" : "not"),
                         pcmk_readable_score(node->weight));
 
         if (stop_if_fail) {
             pe__set_next_role(rsc, pcmk_role_stopped, "node availability");
         }
         node = NULL;
     }
 
     if (rsc->allocated_to != NULL) {
         changed = !pcmk__same_node(rsc->allocated_to, node);
     } else {
         changed = (node != NULL);
     }
     pcmk__unassign_resource(rsc);
     pcmk__clear_rsc_flags(rsc, pcmk_rsc_unassigned);
 
     if (node == NULL) {
         char *rc_stopped = NULL;
 
         pcmk__rsc_debug(rsc, "Could not assign %s to a node", rsc->id);
 
         if (!stop_if_fail) {
             return changed;
         }
         pe__set_next_role(rsc, pcmk_role_stopped, "unable to assign");
 
         for (GList *iter = rsc->actions; iter != NULL; iter = iter->next) {
             pcmk_action_t *op = (pcmk_action_t *) iter->data;
 
             pcmk__rsc_debug(rsc, "Updating %s for %s assignment failure",
                             op->uuid, rsc->id);
 
             if (pcmk__str_eq(op->task, PCMK_ACTION_STOP, pcmk__str_none)) {
                 pcmk__clear_action_flags(op, pcmk_action_optional);
 
             } else if (pcmk__str_eq(op->task, PCMK_ACTION_START,
                                     pcmk__str_none)) {
                 pcmk__clear_action_flags(op, pcmk_action_runnable);
 
             } else {
                 // Cancel recurring actions, unless for stopped state
                 const char *interval_ms_s = NULL;
                 const char *target_rc_s = NULL;
 
                 interval_ms_s = g_hash_table_lookup(op->meta,
                                                     PCMK_META_INTERVAL);
                 target_rc_s = g_hash_table_lookup(op->meta,
                                                   PCMK__META_OP_TARGET_RC);
                 if (rc_stopped == NULL) {
                     rc_stopped = pcmk__itoa(PCMK_OCF_NOT_RUNNING);
                 }
 
                 if (!pcmk__str_eq(interval_ms_s, "0", pcmk__str_null_matches)
                     && !pcmk__str_eq(rc_stopped, target_rc_s, pcmk__str_none)) {
 
                     pcmk__clear_action_flags(op, pcmk_action_runnable);
                 }
             }
         }
         free(rc_stopped);
         return changed;
     }
 
     pcmk__rsc_debug(rsc, "Assigning %s to %s", rsc->id, pcmk__node_name(node));
     rsc->allocated_to = pe__copy_node(node);
 
     add_assigned_resource(node, rsc);
     node->details->num_resources++;
     node->count++;
     pcmk__consume_node_capacity(node->details->utilization, rsc);
 
     if (pcmk_is_set(rsc->cluster->flags, pcmk_sched_show_utilization)) {
         pcmk__output_t *out = rsc->cluster->priv;
 
         out->message(out, "resource-util", rsc, node, __func__);
     }
     return changed;
 }
 
 /*!
  * \internal
  * \brief Remove any node assignment from a specified resource and its children
  *
  * If a specified resource has been assigned to a node, remove that assignment
  * and mark the resource as provisional again.
  *
  * \param[in,out] rsc  Resource to unassign
  *
  * \note This function is called recursively on \p rsc and its children.
  */
 void
 pcmk__unassign_resource(pcmk_resource_t *rsc)
 {
     pcmk_node_t *old = rsc->allocated_to;
 
     if (old == NULL) {
         crm_info("Unassigning %s", rsc->id);
     } else {
         crm_info("Unassigning %s from %s", rsc->id, pcmk__node_name(old));
     }
 
     pcmk__set_rsc_flags(rsc, pcmk_rsc_unassigned);
 
     if (rsc->children == NULL) {
         if (old == NULL) {
             return;
         }
         rsc->allocated_to = NULL;
 
         /* We're going to free the pcmk_node_t, but its details member is shared
          * and will remain, so update that appropriately first.
          */
         old->details->allocated_rsc = g_list_remove(old->details->allocated_rsc,
                                                     rsc);
         old->details->num_resources--;
         pcmk__release_node_capacity(old->details->utilization, rsc);
         free(old);
         return;
     }
 
     for (GList *iter = rsc->children; iter != NULL; iter = iter->next) {
         pcmk__unassign_resource((pcmk_resource_t *) iter->data);
     }
 }
 
 /*!
  * \internal
  * \brief Check whether a resource has reached its migration threshold on a node
  *
  * \param[in,out] rsc       Resource to check
  * \param[in]     node      Node to check
  * \param[out]    failed    If threshold has been reached, this will be set to
  *                          resource that failed (possibly a parent of \p rsc)
  *
  * \return true if the migration threshold has been reached, false otherwise
  */
 bool
 pcmk__threshold_reached(pcmk_resource_t *rsc, const pcmk_node_t *node,
                         pcmk_resource_t **failed)
 {
     int fail_count, remaining_tries;
     pcmk_resource_t *rsc_to_ban = rsc;
 
     // Migration threshold of 0 means never force away
     if (rsc->migration_threshold == 0) {
         return false;
     }
 
     // If we're ignoring failures, also ignore the migration threshold
     if (pcmk_is_set(rsc->flags, pcmk_rsc_ignore_failure)) {
         return false;
     }
 
     // If there are no failures, there's no need to force away
     fail_count = pe_get_failcount(node, rsc, NULL,
                                   pcmk__fc_effective|pcmk__fc_fillers, NULL);
     if (fail_count <= 0) {
         return false;
     }
 
     // If failed resource is anonymous clone instance, we'll force clone away
     if (!pcmk_is_set(rsc->flags, pcmk_rsc_unique)) {
         rsc_to_ban = uber_parent(rsc);
     }
 
     // How many more times recovery will be tried on this node
     remaining_tries = rsc->migration_threshold - fail_count;
 
     if (remaining_tries <= 0) {
         pcmk__sched_warn("%s cannot run on %s due to reaching migration "
                          "threshold (clean up resource to allow again)"
                          CRM_XS " failures=%d "
                          PCMK_META_MIGRATION_THRESHOLD "=%d",
                          rsc_to_ban->id, pcmk__node_name(node), fail_count,
                          rsc->migration_threshold);
         if (failed != NULL) {
             *failed = rsc_to_ban;
         }
         return true;
     }
 
     crm_info("%s can fail %d more time%s on "
              "%s before reaching migration threshold (%d)",
              rsc_to_ban->id, remaining_tries, pcmk__plural_s(remaining_tries),
              pcmk__node_name(node), rsc->migration_threshold);
     return false;
 }
 
 /*!
  * \internal
  * \brief Get a node's score
  *
  * \param[in] node     Node with ID to check
  * \param[in] nodes    List of nodes to look for \p node score in
  *
  * \return Node's score, or -INFINITY if not found
  */
 static int
 get_node_score(const pcmk_node_t *node, GHashTable *nodes)
 {
     pcmk_node_t *found_node = NULL;
 
     if ((node != NULL) && (nodes != NULL)) {
         found_node = g_hash_table_lookup(nodes, node->details->id);
     }
     return (found_node == NULL)? -PCMK_SCORE_INFINITY : found_node->weight;
 }
 
 /*!
  * \internal
  * \brief Compare two resources according to which should be assigned first
  *
  * \param[in] a     First resource to compare
  * \param[in] b     Second resource to compare
  * \param[in] data  Sorted list of all nodes in cluster
  *
  * \return -1 if \p a should be assigned before \b, 0 if they are equal,
  *         or +1 if \p a should be assigned after \b
  */
 static gint
 cmp_resources(gconstpointer a, gconstpointer b, gpointer data)
 {
     /* GLib insists that this function require gconstpointer arguments, but we
      * make a small, temporary change to each argument (setting the
      * pe_rsc_merging flag) during comparison
      */
     pcmk_resource_t *resource1 = (pcmk_resource_t *) a;
     pcmk_resource_t *resource2 = (pcmk_resource_t *) b;
     const GList *nodes = data;
 
     int rc = 0;
     int r1_score = -PCMK_SCORE_INFINITY;
     int r2_score = -PCMK_SCORE_INFINITY;
     pcmk_node_t *r1_node = NULL;
     pcmk_node_t *r2_node = NULL;
     GHashTable *r1_nodes = NULL;
     GHashTable *r2_nodes = NULL;
     const char *reason = NULL;
 
     // Resources with highest priority should be assigned first
     reason = "priority";
     r1_score = resource1->priority;
     r2_score = resource2->priority;
     if (r1_score > r2_score) {
         rc = -1;
         goto done;
     }
     if (r1_score < r2_score) {
         rc = 1;
         goto done;
     }
 
     // We need nodes to make any other useful comparisons
     reason = "no node list";
     if (nodes == NULL) {
         goto done;
     }
 
     // Calculate and log node scores
-    resource1->cmds->add_colocated_node_scores(resource1, NULL, resource1->id,
-                                               &r1_nodes, NULL, 1,
-                                               pcmk__coloc_select_this_with);
-    resource2->cmds->add_colocated_node_scores(resource2, NULL, resource2->id,
-                                               &r2_nodes, NULL, 1,
-                                               pcmk__coloc_select_this_with);
+    resource1->private->cmds->add_colocated_node_scores(resource1, NULL,
+                                                        resource1->id,
+                                                        &r1_nodes, NULL, 1,
+                                                        pcmk__coloc_select_this_with);
+    resource2->private->cmds->add_colocated_node_scores(resource2, NULL,
+                                                        resource2->id,
+                                                        &r2_nodes, NULL, 1,
+                                                        pcmk__coloc_select_this_with);
     pe__show_node_scores(true, NULL, resource1->id, r1_nodes,
                          resource1->cluster);
     pe__show_node_scores(true, NULL, resource2->id, r2_nodes,
                          resource2->cluster);
 
     // The resource with highest score on its current node goes first
     reason = "current location";
     if (resource1->running_on != NULL) {
         r1_node = pcmk__current_node(resource1);
     }
     if (resource2->running_on != NULL) {
         r2_node = pcmk__current_node(resource2);
     }
     r1_score = get_node_score(r1_node, r1_nodes);
     r2_score = get_node_score(r2_node, r2_nodes);
     if (r1_score > r2_score) {
         rc = -1;
         goto done;
     }
     if (r1_score < r2_score) {
         rc = 1;
         goto done;
     }
 
     // Otherwise a higher score on any node will do
     reason = "score";
     for (const GList *iter = nodes; iter != NULL; iter = iter->next) {
         const pcmk_node_t *node = (const pcmk_node_t *) iter->data;
 
         r1_score = get_node_score(node, r1_nodes);
         r2_score = get_node_score(node, r2_nodes);
         if (r1_score > r2_score) {
             rc = -1;
             goto done;
         }
         if (r1_score < r2_score) {
             rc = 1;
             goto done;
         }
     }
 
 done:
     crm_trace("%s (%d)%s%s %c %s (%d)%s%s: %s",
               resource1->id, r1_score,
               ((r1_node == NULL)? "" : " on "),
               ((r1_node == NULL)? "" : r1_node->details->id),
               ((rc < 0)? '>' : ((rc > 0)? '<' : '=')),
               resource2->id, r2_score,
               ((r2_node == NULL)? "" : " on "),
               ((r2_node == NULL)? "" : r2_node->details->id),
               reason);
     if (r1_nodes != NULL) {
         g_hash_table_destroy(r1_nodes);
     }
     if (r2_nodes != NULL) {
         g_hash_table_destroy(r2_nodes);
     }
     return rc;
 }
 
 /*!
  * \internal
  * \brief Sort resources in the order they should be assigned to nodes
  *
  * \param[in,out] scheduler  Scheduler data
  */
 void
 pcmk__sort_resources(pcmk_scheduler_t *scheduler)
 {
     GList *nodes = g_list_copy(scheduler->nodes);
 
     nodes = pcmk__sort_nodes(nodes, NULL);
     scheduler->resources = g_list_sort_with_data(scheduler->resources,
                                                  cmp_resources, nodes);
     g_list_free(nodes);
 }
diff --git a/lib/pacemaker/pcmk_sched_utilization.c b/lib/pacemaker/pcmk_sched_utilization.c
index 05e3cf6bae..fbaad739db 100644
--- a/lib/pacemaker/pcmk_sched_utilization.c
+++ b/lib/pacemaker/pcmk_sched_utilization.c
@@ -1,469 +1,469 @@
 /*
  * Copyright 2014-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 #include <crm/common/xml.h>
 #include <pacemaker-internal.h>
 
 #include "libpacemaker_private.h"
 
 /*!
  * \internal
  * \brief Get integer utilization from a string
  *
  * \param[in] s  String representation of a node utilization value
  *
  * \return Integer equivalent of \p s
  * \todo It would make sense to restrict utilization values to nonnegative
  *       integers, but the documentation just says "integers" and we didn't
  *       restrict them initially, so for backward compatibility, allow any
  *       integer.
  */
 static int
 utilization_value(const char *s)
 {
     int value = 0;
 
     if ((s != NULL) && (pcmk__scan_min_int(s, &value, INT_MIN) == EINVAL)) {
         pcmk__config_warn("Using 0 for utilization instead of "
                           "invalid value '%s'", value);
         value = 0;
     }
     return value;
 }
 
 
 /*
  * Functions for comparing node capacities
  */
 
 struct compare_data {
     const pcmk_node_t *node1;
     const pcmk_node_t *node2;
     bool node2_only;
     int result;
 };
 
 /*!
  * \internal
  * \brief Compare a single utilization attribute for two nodes
  *
  * Compare one utilization attribute for two nodes, decrementing the result if
  * the first node has greater capacity, and incrementing it if the second node
  * has greater capacity.
  *
  * \param[in]     key        Utilization attribute name to compare
  * \param[in]     value      Utilization attribute value to compare
  * \param[in,out] user_data  Comparison data (as struct compare_data*)
  */
 static void
 compare_utilization_value(gpointer key, gpointer value, gpointer user_data)
 {
     int node1_capacity = 0;
     int node2_capacity = 0;
     struct compare_data *data = user_data;
     const char *node2_value = NULL;
 
     if (data->node2_only) {
         if (g_hash_table_lookup(data->node1->details->utilization, key)) {
             return; // We've already compared this attribute
         }
     } else {
         node1_capacity = utilization_value((const char *) value);
     }
 
     node2_value = g_hash_table_lookup(data->node2->details->utilization, key);
     node2_capacity = utilization_value(node2_value);
 
     if (node1_capacity > node2_capacity) {
         data->result--;
     } else if (node1_capacity < node2_capacity) {
         data->result++;
     }
 }
 
 /*!
  * \internal
  * \brief Compare utilization capacities of two nodes
  *
  * \param[in] node1  First node to compare
  * \param[in] node2  Second node to compare
  *
  * \return Negative integer if node1 has more free capacity,
  *         0 if the capacities are equal, or a positive integer
  *         if node2 has more free capacity
  */
 int
 pcmk__compare_node_capacities(const pcmk_node_t *node1,
                               const pcmk_node_t *node2)
 {
     struct compare_data data = {
         .node1      = node1,
         .node2      = node2,
         .node2_only = false,
         .result     = 0,
     };
 
     // Compare utilization values that node1 and maybe node2 have
     g_hash_table_foreach(node1->details->utilization, compare_utilization_value,
                          &data);
 
     // Compare utilization values that only node2 has
     data.node2_only = true;
     g_hash_table_foreach(node2->details->utilization, compare_utilization_value,
                          &data);
 
     return data.result;
 }
 
 
 /*
  * Functions for updating node capacities
  */
 
 struct calculate_data {
     GHashTable *current_utilization;
     bool plus;
 };
 
 /*!
  * \internal
  * \brief Update a single utilization attribute with a new value
  *
  * \param[in]     key        Name of utilization attribute to update
  * \param[in]     value      Value to add or substract
  * \param[in,out] user_data  Calculation data (as struct calculate_data *)
  */
 static void
 update_utilization_value(gpointer key, gpointer value, gpointer user_data)
 {
     int result = 0;
     const char *current = NULL;
     struct calculate_data *data = user_data;
 
     current = g_hash_table_lookup(data->current_utilization, key);
     if (data->plus) {
         result = utilization_value(current) + utilization_value(value);
     } else if (current) {
         result = utilization_value(current) - utilization_value(value);
     }
     g_hash_table_replace(data->current_utilization,
                          strdup(key), pcmk__itoa(result));
 }
 
 /*!
  * \internal
  * \brief Subtract a resource's utilization from node capacity
  *
  * \param[in,out] current_utilization  Current node utilization attributes
  * \param[in]     rsc                  Resource with utilization to subtract
  */
 void
 pcmk__consume_node_capacity(GHashTable *current_utilization,
                             const pcmk_resource_t *rsc)
 {
     struct calculate_data data = {
         .current_utilization = current_utilization,
         .plus = false,
     };
 
     g_hash_table_foreach(rsc->utilization, update_utilization_value, &data);
 }
 
 /*!
  * \internal
  * \brief Add a resource's utilization to node capacity
  *
  * \param[in,out] current_utilization  Current node utilization attributes
  * \param[in]     rsc                  Resource with utilization to add
  */
 void
 pcmk__release_node_capacity(GHashTable *current_utilization,
                             const pcmk_resource_t *rsc)
 {
     struct calculate_data data = {
         .current_utilization = current_utilization,
         .plus = true,
     };
 
     g_hash_table_foreach(rsc->utilization, update_utilization_value, &data);
 }
 
 
 /*
  * Functions for checking for sufficient node capacity
  */
 
 struct capacity_data {
     const pcmk_node_t *node;
     const char *rsc_id;
     bool is_enough;
 };
 
 /*!
  * \internal
  * \brief Check whether a single utilization attribute has sufficient capacity
  *
  * \param[in]     key        Name of utilization attribute to check
  * \param[in]     value      Amount of utilization required
  * \param[in,out] user_data  Capacity data (as struct capacity_data *)
  */
 static void
 check_capacity(gpointer key, gpointer value, gpointer user_data)
 {
     int required = 0;
     int remaining = 0;
     const char *node_value_s = NULL;
     struct capacity_data *data = user_data;
 
     node_value_s = g_hash_table_lookup(data->node->details->utilization, key);
 
     required = utilization_value(value);
     remaining = utilization_value(node_value_s);
 
     if (required > remaining) {
         crm_debug("Remaining capacity for %s on %s (%d) is insufficient "
                   "for resource %s usage (%d)",
                   (const char *) key, pcmk__node_name(data->node), remaining,
                   data->rsc_id, required);
         data->is_enough = false;
     }
 }
 
 /*!
  * \internal
  * \brief Check whether a node has sufficient capacity for a resource
  *
  * \param[in] node         Node to check
  * \param[in] rsc_id       ID of resource to check (for debug logs only)
  * \param[in] utilization  Required utilization amounts
  *
  * \return true if node has sufficient capacity for resource, otherwise false
  */
 static bool
 have_enough_capacity(const pcmk_node_t *node, const char *rsc_id,
                      GHashTable *utilization)
 {
     struct capacity_data data = {
         .node = node,
         .rsc_id = rsc_id,
         .is_enough = true,
     };
 
     g_hash_table_foreach(utilization, check_capacity, &data);
     return data.is_enough;
 }
 
 /*!
  * \internal
  * \brief Sum the utilization requirements of a list of resources
  *
  * \param[in] orig_rsc  Resource being assigned (for logging purposes)
  * \param[in] rscs      Resources whose utilization should be summed
  *
  * \return Newly allocated hash table with sum of all utilization values
  * \note It is the caller's responsibility to free the return value using
  *       g_hash_table_destroy().
  */
 static GHashTable *
 sum_resource_utilization(const pcmk_resource_t *orig_rsc, GList *rscs)
 {
     GHashTable *utilization = pcmk__strkey_table(free, free);
 
     for (GList *iter = rscs; iter != NULL; iter = iter->next) {
         pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data;
 
-        rsc->cmds->add_utilization(rsc, orig_rsc, rscs, utilization);
+        rsc->private->cmds->add_utilization(rsc, orig_rsc, rscs, utilization);
     }
     return utilization;
 }
 
 /*!
  * \internal
  * \brief Ban resource from nodes with insufficient utilization capacity
  *
  * \param[in,out] rsc  Resource to check
  *
  * \return Allowed node for \p rsc with most spare capacity, if there are no
  *         nodes with enough capacity for \p rsc and all its colocated resources
  */
 const pcmk_node_t *
 pcmk__ban_insufficient_capacity(pcmk_resource_t *rsc)
 {
     bool any_capable = false;
     char *rscs_id = NULL;
     pcmk_node_t *node = NULL;
     const pcmk_node_t *most_capable_node = NULL;
     GList *colocated_rscs = NULL;
     GHashTable *unassigned_utilization = NULL;
     GHashTableIter iter;
 
     CRM_CHECK(rsc != NULL, return NULL);
 
     // The default placement strategy ignores utilization
     if (pcmk__str_eq(rsc->cluster->placement_strategy, PCMK_VALUE_DEFAULT,
                      pcmk__str_casei)) {
         return NULL;
     }
 
     // Check whether any resources are colocated with this one
-    colocated_rscs = rsc->cmds->colocated_resources(rsc, NULL, NULL);
+    colocated_rscs = rsc->private->cmds->colocated_resources(rsc, NULL, NULL);
     if (colocated_rscs == NULL) {
         return NULL;
     }
 
     rscs_id = crm_strdup_printf("%s and its colocated resources", rsc->id);
 
     // If rsc isn't in the list, add it so we include its utilization
     if (g_list_find(colocated_rscs, rsc) == NULL) {
         colocated_rscs = g_list_append(colocated_rscs, rsc);
     }
 
     // Sum utilization of colocated resources that haven't been assigned yet
     unassigned_utilization = sum_resource_utilization(rsc, colocated_rscs);
 
     // Check whether any node has enough capacity for all the resources
     g_hash_table_iter_init(&iter, rsc->allowed_nodes);
     while (g_hash_table_iter_next(&iter, NULL, (void **) &node)) {
         if (!pcmk__node_available(node, true, false)) {
             continue;
         }
 
         if (have_enough_capacity(node, rscs_id, unassigned_utilization)) {
             any_capable = true;
         }
 
         // Keep track of node with most free capacity
         if ((most_capable_node == NULL)
             || (pcmk__compare_node_capacities(node, most_capable_node) < 0)) {
             most_capable_node = node;
         }
     }
 
     if (any_capable) {
         // If so, ban resource from any node with insufficient capacity
         g_hash_table_iter_init(&iter, rsc->allowed_nodes);
         while (g_hash_table_iter_next(&iter, NULL, (void **) &node)) {
             if (pcmk__node_available(node, true, false)
                 && !have_enough_capacity(node, rscs_id,
                                          unassigned_utilization)) {
                 pcmk__rsc_debug(rsc, "%s does not have enough capacity for %s",
                                 pcmk__node_name(node), rscs_id);
                 resource_location(rsc, node, -PCMK_SCORE_INFINITY,
                                   "__limit_utilization__", rsc->cluster);
             }
         }
         most_capable_node = NULL;
 
     } else {
         // Otherwise, ban from nodes with insufficient capacity for rsc alone
         g_hash_table_iter_init(&iter, rsc->allowed_nodes);
         while (g_hash_table_iter_next(&iter, NULL, (void **) &node)) {
             if (pcmk__node_available(node, true, false)
                 && !have_enough_capacity(node, rsc->id, rsc->utilization)) {
                 pcmk__rsc_debug(rsc, "%s does not have enough capacity for %s",
                                 pcmk__node_name(node), rsc->id);
                 resource_location(rsc, node, -PCMK_SCORE_INFINITY,
                                   "__limit_utilization__", rsc->cluster);
             }
         }
     }
 
     g_hash_table_destroy(unassigned_utilization);
     g_list_free(colocated_rscs);
     free(rscs_id);
 
     pe__show_node_scores(true, rsc, "Post-utilization", rsc->allowed_nodes,
                          rsc->cluster);
     return most_capable_node;
 }
 
 /*!
  * \internal
  * \brief Create a new load_stopped pseudo-op for a node
  *
  * \param[in,out] node  Node to create op for
  *
  * \return Newly created load_stopped op
  */
 static pcmk_action_t *
 new_load_stopped_op(pcmk_node_t *node)
 {
     char *load_stopped_task = crm_strdup_printf(PCMK_ACTION_LOAD_STOPPED "_%s",
                                                 node->details->uname);
     pcmk_action_t *load_stopped = get_pseudo_op(load_stopped_task,
                                               node->details->data_set);
 
     if (load_stopped->node == NULL) {
         load_stopped->node = pe__copy_node(node);
         pcmk__clear_action_flags(load_stopped, pcmk_action_optional);
     }
     free(load_stopped_task);
     return load_stopped;
 }
 
 /*!
  * \internal
  * \brief Create utilization-related internal constraints for a resource
  *
  * \param[in,out] rsc            Resource to create constraints for
  * \param[in]     allowed_nodes  List of allowed next nodes for \p rsc
  */
 void
 pcmk__create_utilization_constraints(pcmk_resource_t *rsc,
                                      const GList *allowed_nodes)
 {
     const GList *iter = NULL;
     pcmk_action_t *load_stopped = NULL;
 
     pcmk__rsc_trace(rsc,
                     "Creating utilization constraints for %s - strategy: %s",
                     rsc->id, rsc->cluster->placement_strategy);
 
     // "stop rsc then load_stopped" constraints for current nodes
     for (iter = rsc->running_on; iter != NULL; iter = iter->next) {
         load_stopped = new_load_stopped_op(iter->data);
         pcmk__new_ordering(rsc, stop_key(rsc), NULL, NULL, NULL, load_stopped,
                            pcmk__ar_if_on_same_node_or_target, rsc->cluster);
     }
 
     // "load_stopped then start/migrate_to rsc" constraints for allowed nodes
     for (iter = allowed_nodes; iter; iter = iter->next) {
         load_stopped = new_load_stopped_op(iter->data);
         pcmk__new_ordering(NULL, NULL, load_stopped, rsc, start_key(rsc), NULL,
                            pcmk__ar_if_on_same_node_or_target, rsc->cluster);
         pcmk__new_ordering(NULL, NULL, load_stopped,
                            rsc,
                            pcmk__op_key(rsc->id, PCMK_ACTION_MIGRATE_TO, 0),
                            NULL,
                            pcmk__ar_if_on_same_node_or_target, rsc->cluster);
     }
 }
 
 /*!
  * \internal
  * \brief Output node capacities if enabled
  *
  * \param[in]     desc       Prefix for output
  * \param[in,out] scheduler  Scheduler data
  */
 void
 pcmk__show_node_capacities(const char *desc, pcmk_scheduler_t *scheduler)
 {
     if (!pcmk_is_set(scheduler->flags, pcmk_sched_show_utilization)) {
         return;
     }
     for (const GList *iter = scheduler->nodes;
          iter != NULL; iter = iter->next) {
         const pcmk_node_t *node = (const pcmk_node_t *) iter->data;
         pcmk__output_t *out = scheduler->priv;
 
         out->message(out, "node-capacity", node, desc);
     }
 }
diff --git a/lib/pacemaker/pcmk_scheduler.c b/lib/pacemaker/pcmk_scheduler.c
index 35bfc9d29d..7eb0477cd2 100644
--- a/lib/pacemaker/pcmk_scheduler.c
+++ b/lib/pacemaker/pcmk_scheduler.c
@@ -1,893 +1,894 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <crm/crm.h>
 #include <crm/cib.h>
 #include <crm/cib/internal.h>
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>
 #include <crm/common/scheduler_internal.h>
 
 #include <glib.h>
 
 #include <crm/pengine/status.h>
 #include <pacemaker-internal.h>
 #include "libpacemaker_private.h"
 
 CRM_TRACE_INIT_DATA(pacemaker);
 
 /*!
  * \internal
  * \brief Do deferred action checks after assignment
  *
  * When unpacking the resource history, the scheduler checks for resource
  * configurations that have changed since an action was run. However, at that
  * time, bundles using the REMOTE_CONTAINER_HACK don't have their final
  * parameter information, so instead they add a deferred check to a list. This
  * function processes one entry in that list.
  *
  * \param[in,out] rsc     Resource that action history is for
  * \param[in,out] node    Node that action history is for
  * \param[in]     rsc_op  Action history entry
  * \param[in]     check   Type of deferred check to do
  */
 static void
 check_params(pcmk_resource_t *rsc, pcmk_node_t *node, const xmlNode *rsc_op,
              enum pcmk__check_parameters check)
 {
     const char *reason = NULL;
     pcmk__op_digest_t *digest_data = NULL;
 
     switch (check) {
         case pcmk__check_active:
             if (pcmk__check_action_config(rsc, node, rsc_op)
                 && pe_get_failcount(node, rsc, NULL, pcmk__fc_effective,
                                     NULL)) {
                 reason = "action definition changed";
             }
             break;
 
         case pcmk__check_last_failure:
             digest_data = rsc_action_digest_cmp(rsc, rsc_op, node,
                                                 rsc->cluster);
             switch (digest_data->rc) {
                 case pcmk__digest_unknown:
                     crm_trace("Resource %s history entry %s on %s has "
                               "no digest to compare",
                               rsc->id, pcmk__xe_id(rsc_op), node->details->id);
                     break;
                 case pcmk__digest_match:
                     break;
                 default:
                     reason = "resource parameters have changed";
                     break;
             }
             break;
     }
     if (reason != NULL) {
         pe__clear_failcount(rsc, node, reason, rsc->cluster);
     }
 }
 
 /*!
  * \internal
  * \brief Check whether a resource has failcount clearing scheduled on a node
  *
  * \param[in] node  Node to check
  * \param[in] rsc   Resource to check
  *
  * \return true if \p rsc has failcount clearing scheduled on \p node,
  *         otherwise false
  */
 static bool
 failcount_clear_action_exists(const pcmk_node_t *node,
                               const pcmk_resource_t *rsc)
 {
     GList *list = pe__resource_actions(rsc, node, PCMK_ACTION_CLEAR_FAILCOUNT,
                                        TRUE);
 
     if (list != NULL) {
         g_list_free(list);
         return true;
     }
     return false;
 }
 
 /*!
  * \internal
  * \brief Ban a resource from a node if it reached its failure threshold there
  *
  * \param[in,out] data       Resource to check failure threshold for
  * \param[in]     user_data  Node to check resource on
  */
 static void
 check_failure_threshold(gpointer data, gpointer user_data)
 {
     pcmk_resource_t *rsc = data;
     const pcmk_node_t *node = user_data;
 
     // If this is a collective resource, apply recursively to children instead
     if (rsc->children != NULL) {
         g_list_foreach(rsc->children, check_failure_threshold, user_data);
         return;
     }
 
     if (!failcount_clear_action_exists(node, rsc)) {
         /* Don't force the resource away from this node due to a failcount
          * that's going to be cleared.
          *
          * @TODO Failcount clearing can be scheduled in
          * pcmk__handle_rsc_config_changes() via process_rsc_history(), or in
          * schedule_resource_actions() via check_params(). This runs well before
          * then, so it cannot detect those, meaning we might check the migration
          * threshold when we shouldn't. Worst case, we stop or move the
          * resource, then move it back in the next transition.
          */
         pcmk_resource_t *failed = NULL;
 
         if (pcmk__threshold_reached(rsc, node, &failed)) {
             resource_location(failed, node, -PCMK_SCORE_INFINITY,
                               "__fail_limit__", rsc->cluster);
         }
     }
 }
 
 /*!
  * \internal
  * \brief If resource has exclusive discovery, ban node if not allowed
  *
  * Location constraints have a PCMK_XA_RESOURCE_DISCOVERY option that allows
  * users to specify where probes are done for the affected resource. If this is
  * set to \c exclusive, probes will only be done on nodes listed in exclusive
  * constraints. This function bans the resource from the node if the node is not
  * listed.
  *
  * \param[in,out] data       Resource to check
  * \param[in]     user_data  Node to check resource on
  */
 static void
 apply_exclusive_discovery(gpointer data, gpointer user_data)
 {
     pcmk_resource_t *rsc = data;
     const pcmk_node_t *node = user_data;
 
     if (rsc->exclusive_discover
         || pe__const_top_resource(rsc, false)->exclusive_discover) {
         pcmk_node_t *match = NULL;
 
         // If this is a collective resource, apply recursively to children
         g_list_foreach(rsc->children, apply_exclusive_discovery, user_data);
 
         match = g_hash_table_lookup(rsc->allowed_nodes, node->details->id);
         if ((match != NULL)
             && (match->rsc_discover_mode != pcmk_probe_exclusive)) {
             match->weight = -PCMK_SCORE_INFINITY;
         }
     }
 }
 
 /*!
  * \internal
  * \brief Apply stickiness to a resource if appropriate
  *
  * \param[in,out] data       Resource to check for stickiness
  * \param[in]     user_data  Ignored
  */
 static void
 apply_stickiness(gpointer data, gpointer user_data)
 {
     pcmk_resource_t *rsc = data;
     pcmk_node_t *node = NULL;
 
     // If this is a collective resource, apply recursively to children instead
     if (rsc->children != NULL) {
         g_list_foreach(rsc->children, apply_stickiness, NULL);
         return;
     }
 
     /* A resource is sticky if it is managed, has stickiness configured, and is
      * active on a single node.
      */
     if (!pcmk_is_set(rsc->flags, pcmk_rsc_managed)
         || (rsc->stickiness < 1) || !pcmk__list_of_1(rsc->running_on)) {
         return;
     }
 
     node = rsc->running_on->data;
 
     /* In a symmetric cluster, stickiness can always be used. In an
      * asymmetric cluster, we have to check whether the resource is still
      * allowed on the node, so we don't keep the resource somewhere it is no
      * longer explicitly enabled.
      */
     if (!pcmk_is_set(rsc->cluster->flags, pcmk_sched_symmetric_cluster)
         && (g_hash_table_lookup(rsc->allowed_nodes,
                                 node->details->id) == NULL)) {
         pcmk__rsc_debug(rsc,
                         "Ignoring %s stickiness because the cluster is "
                         "asymmetric and %s is not explicitly allowed",
                         rsc->id, pcmk__node_name(node));
         return;
     }
 
     pcmk__rsc_debug(rsc, "Resource %s has %d stickiness on %s",
                     rsc->id, rsc->stickiness, pcmk__node_name(node));
     resource_location(rsc, node, rsc->stickiness, "stickiness", rsc->cluster);
 }
 
 /*!
  * \internal
  * \brief Apply shutdown locks for all resources as appropriate
  *
  * \param[in,out] scheduler  Scheduler data
  */
 static void
 apply_shutdown_locks(pcmk_scheduler_t *scheduler)
 {
     if (!pcmk_is_set(scheduler->flags, pcmk_sched_shutdown_lock)) {
         return;
     }
     for (GList *iter = scheduler->resources; iter != NULL; iter = iter->next) {
         pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data;
 
-        rsc->cmds->shutdown_lock(rsc);
+        rsc->private->cmds->shutdown_lock(rsc);
     }
 }
 
 /*!
  * \internal
  * \brief Calculate the number of available nodes in the cluster
  *
  * \param[in,out] scheduler  Scheduler data
  */
 static void
 count_available_nodes(pcmk_scheduler_t *scheduler)
 {
     if (pcmk_is_set(scheduler->flags, pcmk_sched_no_compat)) {
         return;
     }
 
     // @COMPAT for API backward compatibility only (cluster does not use value)
     for (GList *iter = scheduler->nodes; iter != NULL; iter = iter->next) {
         pcmk_node_t *node = (pcmk_node_t *) iter->data;
 
         if ((node != NULL) && (node->weight >= 0) && node->details->online
             && (node->details->type != node_ping)) {
             scheduler->max_valid_nodes++;
         }
     }
     crm_trace("Online node count: %d", scheduler->max_valid_nodes);
 }
 
 /*
  * \internal
  * \brief Apply node-specific scheduling criteria
  *
  * After the CIB has been unpacked, process node-specific scheduling criteria
  * including shutdown locks, location constraints, resource stickiness,
  * migration thresholds, and exclusive resource discovery.
  */
 static void
 apply_node_criteria(pcmk_scheduler_t *scheduler)
 {
     crm_trace("Applying node-specific scheduling criteria");
     apply_shutdown_locks(scheduler);
     count_available_nodes(scheduler);
     pcmk__apply_locations(scheduler);
     g_list_foreach(scheduler->resources, apply_stickiness, NULL);
 
     for (GList *node_iter = scheduler->nodes; node_iter != NULL;
          node_iter = node_iter->next) {
         for (GList *rsc_iter = scheduler->resources; rsc_iter != NULL;
              rsc_iter = rsc_iter->next) {
             check_failure_threshold(rsc_iter->data, node_iter->data);
             apply_exclusive_discovery(rsc_iter->data, node_iter->data);
         }
     }
 }
 
 /*!
  * \internal
  * \brief Assign resources to nodes
  *
  * \param[in,out] scheduler  Scheduler data
  */
 static void
 assign_resources(pcmk_scheduler_t *scheduler)
 {
     GList *iter = NULL;
 
     crm_trace("Assigning resources to nodes");
 
     if (!pcmk__str_eq(scheduler->placement_strategy, PCMK_VALUE_DEFAULT,
                       pcmk__str_casei)) {
         pcmk__sort_resources(scheduler);
     }
     pcmk__show_node_capacities("Original", scheduler);
 
     if (pcmk_is_set(scheduler->flags, pcmk_sched_have_remote_nodes)) {
         /* Assign remote connection resources first (which will also assign any
          * colocation dependencies). If the connection is migrating, always
          * prefer the partial migration target.
          */
         for (iter = scheduler->resources; iter != NULL; iter = iter->next) {
             pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data;
 
             if (rsc->is_remote_node) {
                 pcmk__rsc_trace(rsc, "Assigning remote connection resource '%s'",
                                 rsc->id);
-                rsc->cmds->assign(rsc, rsc->partial_migration_target, true);
+                rsc->private->cmds->assign(rsc, rsc->partial_migration_target,
+                                           true);
             }
         }
     }
 
     /* now do the rest of the resources */
     for (iter = scheduler->resources; iter != NULL; iter = iter->next) {
         pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data;
 
         if (!rsc->is_remote_node) {
             pcmk__rsc_trace(rsc, "Assigning %s resource '%s'",
                             rsc->xml->name, rsc->id);
-            rsc->cmds->assign(rsc, NULL, true);
+            rsc->private->cmds->assign(rsc, NULL, true);
         }
     }
 
     pcmk__show_node_capacities("Remaining", scheduler);
 }
 
 /*!
  * \internal
  * \brief Schedule fail count clearing on online nodes if resource is orphaned
  *
  * \param[in,out] data       Resource to check
  * \param[in]     user_data  Ignored
  */
 static void
 clear_failcounts_if_orphaned(gpointer data, gpointer user_data)
 {
     pcmk_resource_t *rsc = data;
 
     if (!pcmk_is_set(rsc->flags, pcmk_rsc_removed)) {
         return;
     }
     crm_trace("Clear fail counts for orphaned resource %s", rsc->id);
 
     /* There's no need to recurse into rsc->children because those
      * should just be unassigned clone instances.
      */
 
     for (GList *iter = rsc->cluster->nodes; iter != NULL; iter = iter->next) {
         pcmk_node_t *node = (pcmk_node_t *) iter->data;
         pcmk_action_t *clear_op = NULL;
 
         if (!node->details->online) {
             continue;
         }
         if (pe_get_failcount(node, rsc, NULL, pcmk__fc_effective, NULL) == 0) {
             continue;
         }
 
         clear_op = pe__clear_failcount(rsc, node, "it is orphaned",
                                        rsc->cluster);
 
         /* We can't use order_action_then_stop() here because its
          * pcmk__ar_guest_allowed breaks things
          */
         pcmk__new_ordering(clear_op->rsc, NULL, clear_op, rsc, stop_key(rsc),
                            NULL, pcmk__ar_ordered, rsc->cluster);
     }
 }
 
 /*!
  * \internal
  * \brief Schedule any resource actions needed
  *
  * \param[in,out] scheduler  Scheduler data
  */
 static void
 schedule_resource_actions(pcmk_scheduler_t *scheduler)
 {
     // Process deferred action checks
     pe__foreach_param_check(scheduler, check_params);
     pe__free_param_checks(scheduler);
 
     if (pcmk_is_set(scheduler->flags, pcmk_sched_probe_resources)) {
         crm_trace("Scheduling probes");
         pcmk__schedule_probes(scheduler);
     }
 
     if (pcmk_is_set(scheduler->flags, pcmk_sched_stop_removed_resources)) {
         g_list_foreach(scheduler->resources, clear_failcounts_if_orphaned,
                        NULL);
     }
 
     crm_trace("Scheduling resource actions");
     for (GList *iter = scheduler->resources; iter != NULL; iter = iter->next) {
         pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data;
 
-        rsc->cmds->create_actions(rsc);
+        rsc->private->cmds->create_actions(rsc);
     }
 }
 
 /*!
  * \internal
  * \brief Check whether a resource or any of its descendants are managed
  *
  * \param[in] rsc  Resource to check
  *
  * \return true if resource or any descendant is managed, otherwise false
  */
 static bool
 is_managed(const pcmk_resource_t *rsc)
 {
     if (pcmk_is_set(rsc->flags, pcmk_rsc_managed)) {
         return true;
     }
     for (GList *iter = rsc->children; iter != NULL; iter = iter->next) {
         if (is_managed((pcmk_resource_t *) iter->data)) {
             return true;
         }
     }
     return false;
 }
 
 /*!
  * \internal
  * \brief Check whether any resources in the cluster are managed
  *
  * \param[in] scheduler  Scheduler data
  *
  * \return true if any resource is managed, otherwise false
  */
 static bool
 any_managed_resources(const pcmk_scheduler_t *scheduler)
 {
     for (const GList *iter = scheduler->resources;
          iter != NULL; iter = iter->next) {
         if (is_managed((const pcmk_resource_t *) iter->data)) {
             return true;
         }
     }
     return false;
 }
 
 /*!
  * \internal
  * \brief Check whether a node requires fencing
  *
  * \param[in] node          Node to check
  * \param[in] have_managed  Whether any resource in cluster is managed
  *
  * \return true if \p node should be fenced, otherwise false
  */
 static bool
 needs_fencing(const pcmk_node_t *node, bool have_managed)
 {
     return have_managed && node->details->unclean
            && pe_can_fence(node->details->data_set, node);
 }
 
 /*!
  * \internal
  * \brief Check whether a node requires shutdown
  *
  * \param[in] node          Node to check
  *
  * \return true if \p node should be shut down, otherwise false
  */
 static bool
 needs_shutdown(const pcmk_node_t *node)
 {
     if (pcmk__is_pacemaker_remote_node(node)) {
        /* Do not send shutdown actions for Pacemaker Remote nodes.
         * @TODO We might come up with a good use for this in the future.
         */
         return false;
     }
     return node->details->online && node->details->shutdown;
 }
 
 /*!
  * \internal
  * \brief Track and order non-DC fencing
  *
  * \param[in,out] list       List of existing non-DC fencing actions
  * \param[in,out] action     Fencing action to prepend to \p list
  * \param[in]     scheduler  Scheduler data
  *
  * \return (Possibly new) head of \p list
  */
 static GList *
 add_nondc_fencing(GList *list, pcmk_action_t *action,
                   const pcmk_scheduler_t *scheduler)
 {
     if (!pcmk_is_set(scheduler->flags, pcmk_sched_concurrent_fencing)
         && (list != NULL)) {
         /* Concurrent fencing is disabled, so order each non-DC
          * fencing in a chain. If there is any DC fencing or
          * shutdown, it will be ordered after the last action in the
          * chain later.
          */
         order_actions((pcmk_action_t *) list->data, action, pcmk__ar_ordered);
     }
     return g_list_prepend(list, action);
 }
 
 /*!
  * \internal
  * \brief Schedule a node for fencing
  *
  * \param[in,out] node      Node that requires fencing
  */
 static pcmk_action_t *
 schedule_fencing(pcmk_node_t *node)
 {
     pcmk_action_t *fencing = pe_fence_op(node, NULL, FALSE, "node is unclean",
                                        FALSE, node->details->data_set);
 
     pcmk__sched_warn("Scheduling node %s for fencing", pcmk__node_name(node));
     pcmk__order_vs_fence(fencing, node->details->data_set);
     return fencing;
 }
 
 /*!
  * \internal
  * \brief Create and order node fencing and shutdown actions
  *
  * \param[in,out] scheduler  Scheduler data
  */
 static void
 schedule_fencing_and_shutdowns(pcmk_scheduler_t *scheduler)
 {
     pcmk_action_t *dc_down = NULL;
     bool integrity_lost = false;
     bool have_managed = any_managed_resources(scheduler);
     GList *fencing_ops = NULL;
     GList *shutdown_ops = NULL;
 
     crm_trace("Scheduling fencing and shutdowns as needed");
     if (!have_managed) {
         crm_notice("No fencing will be done until there are resources "
                    "to manage");
     }
 
     // Check each node for whether it needs fencing or shutdown
     for (GList *iter = scheduler->nodes; iter != NULL; iter = iter->next) {
         pcmk_node_t *node = (pcmk_node_t *) iter->data;
         pcmk_action_t *fencing = NULL;
 
         /* Guest nodes are "fenced" by recovering their container resource,
          * so handle them separately.
          */
         if (pcmk__is_guest_or_bundle_node(node)) {
             if (node->details->remote_requires_reset && have_managed
                 && pe_can_fence(scheduler, node)) {
                 pcmk__fence_guest(node);
             }
             continue;
         }
 
         if (needs_fencing(node, have_managed)) {
             fencing = schedule_fencing(node);
 
             // Track DC and non-DC fence actions separately
             if (node->details->is_dc) {
                 dc_down = fencing;
             } else {
                 fencing_ops = add_nondc_fencing(fencing_ops, fencing,
                                                 scheduler);
             }
 
         } else if (needs_shutdown(node)) {
             pcmk_action_t *down_op = pcmk__new_shutdown_action(node);
 
             // Track DC and non-DC shutdown actions separately
             if (node->details->is_dc) {
                 dc_down = down_op;
             } else {
                 shutdown_ops = g_list_prepend(shutdown_ops, down_op);
             }
         }
 
         if ((fencing == NULL) && node->details->unclean) {
             integrity_lost = true;
             pcmk__config_warn("Node %s is unclean but cannot be fenced",
                               pcmk__node_name(node));
         }
     }
 
     if (integrity_lost) {
         if (!pcmk_is_set(scheduler->flags, pcmk_sched_fencing_enabled)) {
             pcmk__config_warn("Resource functionality and data integrity "
                               "cannot be guaranteed (configure, enable, "
                               "and test fencing to correct this)");
 
         } else if (!pcmk_is_set(scheduler->flags, pcmk_sched_quorate)) {
             crm_notice("Unclean nodes will not be fenced until quorum is "
                        "attained or " PCMK_OPT_NO_QUORUM_POLICY " is set to "
                        PCMK_VALUE_IGNORE);
         }
     }
 
     if (dc_down != NULL) {
         /* Order any non-DC shutdowns before any DC shutdown, to avoid repeated
          * DC elections. However, we don't want to order non-DC shutdowns before
          * a DC *fencing*, because even though we don't want a node that's
          * shutting down to become DC, the DC fencing could be ordered before a
          * clone stop that's also ordered before the shutdowns, thus leading to
          * a graph loop.
          */
         if (pcmk__str_eq(dc_down->task, PCMK_ACTION_DO_SHUTDOWN,
                          pcmk__str_none)) {
             pcmk__order_after_each(dc_down, shutdown_ops);
         }
 
         // Order any non-DC fencing before any DC fencing or shutdown
 
         if (pcmk_is_set(scheduler->flags, pcmk_sched_concurrent_fencing)) {
             /* With concurrent fencing, order each non-DC fencing action
              * separately before any DC fencing or shutdown.
              */
             pcmk__order_after_each(dc_down, fencing_ops);
         } else if (fencing_ops != NULL) {
             /* Without concurrent fencing, the non-DC fencing actions are
              * already ordered relative to each other, so we just need to order
              * the DC fencing after the last action in the chain (which is the
              * first item in the list).
              */
             order_actions((pcmk_action_t *) fencing_ops->data, dc_down,
                           pcmk__ar_ordered);
         }
     }
     g_list_free(fencing_ops);
     g_list_free(shutdown_ops);
 }
 
 static void
 log_resource_details(pcmk_scheduler_t *scheduler)
 {
     pcmk__output_t *out = scheduler->priv;
     GList *all = NULL;
 
     /* Due to the `crm_mon --node=` feature, out->message() for all the
      * resource-related messages expects a list of nodes that we are allowed to
      * output information for. Here, we create a wildcard to match all nodes.
      */
     all = g_list_prepend(all, (gpointer) "*");
 
     for (GList *item = scheduler->resources; item != NULL; item = item->next) {
         pcmk_resource_t *rsc = (pcmk_resource_t *) item->data;
 
         // Log all resources except inactive orphans
         if (!pcmk_is_set(rsc->flags, pcmk_rsc_removed)
             || (rsc->role != pcmk_role_stopped)) {
             out->message(out, pcmk__map_element_name(rsc->xml), 0UL, rsc, all,
                          all);
         }
     }
 
     g_list_free(all);
 }
 
 static void
 log_all_actions(pcmk_scheduler_t *scheduler)
 {
     /* This only ever outputs to the log, so ignore whatever output object was
      * previously set and just log instead.
      */
     pcmk__output_t *prev_out = scheduler->priv;
     pcmk__output_t *out = NULL;
 
     if (pcmk__log_output_new(&out) != pcmk_rc_ok) {
         return;
     }
 
     pe__register_messages(out);
     pcmk__register_lib_messages(out);
     pcmk__output_set_log_level(out, LOG_NOTICE);
     scheduler->priv = out;
 
     out->begin_list(out, NULL, NULL, "Actions");
     pcmk__output_actions(scheduler);
     out->end_list(out);
     out->finish(out, CRM_EX_OK, true, NULL);
     pcmk__output_free(out);
 
     scheduler->priv = prev_out;
 }
 
 /*!
  * \internal
  * \brief Log all required but unrunnable actions at trace level
  *
  * \param[in] scheduler  Scheduler data
  */
 static void
 log_unrunnable_actions(const pcmk_scheduler_t *scheduler)
 {
     const uint64_t flags = pcmk_action_optional
                            |pcmk_action_runnable
                            |pcmk_action_pseudo;
 
     crm_trace("Required but unrunnable actions:");
     for (const GList *iter = scheduler->actions;
          iter != NULL; iter = iter->next) {
 
         const pcmk_action_t *action = (const pcmk_action_t *) iter->data;
 
         if (!pcmk_any_flags_set(action->flags, flags)) {
             pcmk__log_action("\t", action, true);
         }
     }
 }
 
 /*!
  * \internal
  * \brief Unpack the CIB for scheduling
  *
  * \param[in,out] cib        CIB XML to unpack (may be NULL if already unpacked)
  * \param[in]     flags      Scheduler flags to set in addition to defaults
  * \param[in,out] scheduler  Scheduler data
  */
 static void
 unpack_cib(xmlNode *cib, unsigned long long flags, pcmk_scheduler_t *scheduler)
 {
     const char* localhost_save = NULL;
 
     if (pcmk_is_set(scheduler->flags, pcmk_sched_have_status)) {
         crm_trace("Reusing previously calculated cluster status");
         pcmk__set_scheduler_flags(scheduler, flags);
         return;
     }
 
     if (scheduler->localhost) {
         localhost_save = scheduler->localhost;
     }
 
     CRM_ASSERT(cib != NULL);
     crm_trace("Calculating cluster status");
 
     /* This will zero the entire struct without freeing anything first, so
      * callers should never call pcmk__schedule_actions() with a populated data
      * set unless pcmk_sched_have_status is set (i.e. cluster_status() was
      * previously called, whether directly or via pcmk__schedule_actions()).
      */
     set_working_set_defaults(scheduler);
 
     if (localhost_save) {
         scheduler->localhost = localhost_save;
     }
 
     pcmk__set_scheduler_flags(scheduler, flags);
     scheduler->input = cib;
     cluster_status(scheduler); // Sets pcmk_sched_have_status
 }
 
 /*!
  * \internal
  * \brief Run the scheduler for a given CIB
  *
  * \param[in,out] cib        CIB XML to use as scheduler input
  * \param[in]     flags      Scheduler flags to set in addition to defaults
  * \param[in,out] scheduler  Scheduler data
  */
 void
 pcmk__schedule_actions(xmlNode *cib, unsigned long long flags,
                        pcmk_scheduler_t *scheduler)
 {
     unpack_cib(cib, flags, scheduler);
     pcmk__set_assignment_methods(scheduler);
     pcmk__apply_node_health(scheduler);
     pcmk__unpack_constraints(scheduler);
     if (pcmk_is_set(scheduler->flags, pcmk_sched_validate_only)) {
         return;
     }
 
     if (!pcmk_is_set(scheduler->flags, pcmk_sched_location_only)
         && pcmk__is_daemon) {
         log_resource_details(scheduler);
     }
 
     apply_node_criteria(scheduler);
 
     if (pcmk_is_set(scheduler->flags, pcmk_sched_location_only)) {
         return;
     }
 
     pcmk__create_internal_constraints(scheduler);
     pcmk__handle_rsc_config_changes(scheduler);
     assign_resources(scheduler);
     schedule_resource_actions(scheduler);
 
     /* Remote ordering constraints need to happen prior to calculating fencing
      * because it is one more place we can mark nodes as needing fencing.
      */
     pcmk__order_remote_connection_actions(scheduler);
 
     schedule_fencing_and_shutdowns(scheduler);
     pcmk__apply_orderings(scheduler);
     log_all_actions(scheduler);
     pcmk__create_graph(scheduler);
 
     if (get_crm_log_level() == LOG_TRACE) {
         log_unrunnable_actions(scheduler);
     }
 }
 
 /*!
  * \internal
  * \brief Initialize scheduler data
  *
  * Make our own copies of the CIB XML and date/time object, if they're not
  * \c NULL. This way we don't have to take ownership of the objects passed via
  * the API.
  *
  * This function is most useful for public API functions that want the caller
  * to retain ownership of the CIB object
  *
  * \param[in,out] out        Output object
  * \param[in]     input      The CIB XML to check (if \c NULL, use current CIB)
  * \param[in]     date       Date and time to use in the scheduler (if \c NULL,
  *                           use current date and time).  This can be used for
  *                           checking whether a rule is in effect at a certa
  *                           date and time.
  * \param[out]    scheduler  Where to store initialized scheduler data
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__init_scheduler(pcmk__output_t *out, xmlNodePtr input, const crm_time_t *date,
                      pcmk_scheduler_t **scheduler)
 {
     // Allows for cleaner syntax than dereferencing the scheduler argument
     pcmk_scheduler_t *new_scheduler = NULL;
 
     new_scheduler = pe_new_working_set();
     if (new_scheduler == NULL) {
         return ENOMEM;
     }
 
     pcmk__set_scheduler_flags(new_scheduler,
                               pcmk_sched_no_counts|pcmk_sched_no_compat);
 
     // Populate the scheduler data
 
     // Make our own copy of the given input or fetch the CIB and use that
     if (input != NULL) {
         new_scheduler->input = pcmk__xml_copy(NULL, input);
         if (new_scheduler->input == NULL) {
             out->err(out, "Failed to copy input XML");
             pe_free_working_set(new_scheduler);
             return ENOMEM;
         }
 
     } else {
         int rc = cib__signon_query(out, NULL, &(new_scheduler->input));
 
         if (rc != pcmk_rc_ok) {
             pe_free_working_set(new_scheduler);
             return rc;
         }
     }
 
     // Make our own copy of the given crm_time_t object; otherwise
     // cluster_status() populates with the current time
     if (date != NULL) {
         // pcmk_copy_time() guarantees non-NULL
         new_scheduler->now = pcmk_copy_time(date);
     }
 
     // Unpack everything
     cluster_status(new_scheduler);
     *scheduler = new_scheduler;
 
     return pcmk_rc_ok;
 }