diff --git a/include/crm/common/resources.h b/include/crm/common/resources.h
index 8927365706..d5522ec5f6 100644
--- a/include/crm/common/resources.h
+++ b/include/crm/common/resources.h
@@ -1,163 +1,161 @@
 /*
  * 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
  */
 
 //!@{
 //! \deprecated Do not use
 
 // 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
 };
 //!@}
 
 //! 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
 };
 
 //! \internal Do not use
 typedef struct pcmk__resource_private pcmk__resource_private_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
 
     // 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
 
-    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_action 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 2c03ab5e03..5cdd0b5390 100644
--- a/include/crm/common/resources_internal.h
+++ b/include/crm/common/resources_internal.h
@@ -1,388 +1,396 @@
 /*
  * 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 <libxml/tree.h>                // xmlNode
 #include <crm/common/resources.h>       // pcmk_resource_t
 #include <crm/common/roles.h>           // enum rsc_role_e
 #include <crm/common/scheduler_types.h> // pcmk_node_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 variants supported by Pacemaker
 enum pcmk__rsc_variant {
     // 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
 };
 
 //! How to recover a resource that is incorrectly active on multiple nodes
 enum pcmk__multiply_active {
     pcmk__multiply_active_restart,      //!< Stop on all, start on desired
     pcmk__multiply_active_stop,         //!< Stop on all and leave stopped
     pcmk__multiply_active_block,        //!< Do nothing to resource
     pcmk__multiply_active_unexpected,   //!< Stop unexpected instances
 };
 
 //! \deprecated
 enum pcmk__restart {
     pcmk__restart_restart,
     pcmk__restart_ignore,
 };
 
 //! 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),
 
     /* NOTE: sbd (at least as of 1.5.2) uses pe_rsc_managed which equates to
      * this value, so the value should not be changed
      */
     // 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),
 
     // Whether resource is a Pacemaker Remote connection
     pcmk__rsc_is_remote_connection   = (1ULL << 18),
 
     // Whether resource has pending start action in history
     pcmk__rsc_start_pending          = (1ULL << 19),
 
     // Whether resource is probed only on nodes marked exclusive
     pcmk__rsc_exclusive_probes       = (1ULL << 20),
 
     /*
      * 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),
 
     // 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),
 };
 
 //! Resource assignment methods (implementation defined by libpacemaker)
 typedef struct pcmk__assignment_methods pcmk__assignment_methods_t;
 
 //! 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 {
     enum pcmk__rsc_variant variant; // Resource variant
     void *variant_opaque;           // Variant-specific data
     char *history_id;               // Resource instance ID in history
     int priority;                   // Priority relative other resources
     int promotion_priority;         // Promotion priority on assigned node
     int stickiness;                 // Extra preference for current node
     guint failure_expiration_ms;    // Failures expire after this much time
     int ban_after_failures;         // Ban from node after this many failures
     guint remote_reconnect_ms;      // Retry interval for remote connections
     char *pending_action;           // Pending action in history, if any
     GList *actions;                 // Actions scheduled for resource
     pcmk_resource_t *parent;        // Resource's parent resource, if any
     pcmk_scheduler_t *scheduler;    // Scheduler data containing resource
     enum pcmk__restart restart_type;    // Deprecated
 
     // 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;
 
     // What to do if the resource is incorrectly active on multiple nodes
     enum pcmk__multiply_active multiply_active_policy;
 
+    /* The assigned node (if not NULL) is the one where the resource *should*
+     * be active by the end of the current scheduler transition. Only primitive
+     * resources have an assigned node.
+     *
+     * @TODO This should probably be part of the primitive variant data.
+     */
+    pcmk_node_t *assigned_node;
+
     /* Pay special attention to whether you want to use with_this_colocations
      * and this_with_colocations 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.
      */
 
     // Colocations of other resources with this one
     GList *with_this_colocations;
 
     // Colocations of this resource with others
     GList *this_with_colocations;
 
     GList *location_constraints;        // Location constraints for resource
     GList *ticket_constraints;          // Ticket constraints for resource
 
     const pcmk__rsc_methods_t *fns;         // Resource object methods
     const pcmk__assignment_methods_t *cmds; // Resource assignment methods
 };
 
 const char *pcmk__multiply_active_text(const pcmk_resource_t *rsc);
 
 /*!
  * \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/common/attrs.c b/lib/common/attrs.c
index 35715df91e..9466e6c7aa 100644
--- a/lib/common/attrs.c
+++ b/lib/common/attrs.c
@@ -1,183 +1,183 @@
 /*
  * Copyright 2011-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #ifndef _GNU_SOURCE
 #  define _GNU_SOURCE
 #endif
 
 #include <crm_internal.h>
 
 #include <stdio.h>
 
 #include <crm/common/xml.h>
 #include <crm/common/scheduler.h>
 #include <crm/common/scheduler_internal.h>
 
 #define OCF_RESKEY_PREFIX "OCF_RESKEY_"
 #define LRM_TARGET_ENV OCF_RESKEY_PREFIX CRM_META "_" PCMK__META_ON_NODE
 
 /*!
  * \internal
  * \brief Get the node name that should be used to set node attributes
  *
  * If given NULL, "auto", or "localhost" as an argument, check the environment
  * to detect the node name that should be used to set node attributes. (The
  * caller might not know the correct name, for example if the target is part of
  * a bundle with \c PCMK_META_CONTAINER_ATTRIBUTE_TARGET set to
  * \c PCMK_VALUE_HOST.)
  *
  * \param[in] name  NULL, "auto" or "localhost" to check environment variables,
  *                  or anything else to return NULL
  *
  * \return Node name that should be used for node attributes based on the
  *         environment if known, otherwise NULL
  */
 const char *
 pcmk__node_attr_target(const char *name)
 {
     if (name == NULL || pcmk__strcase_any_of(name, "auto", "localhost", NULL)) {
         char buf[128] = OCF_RESKEY_PREFIX;
         size_t offset = sizeof(OCF_RESKEY_PREFIX) - 1;
         char *target_var = crm_meta_name(PCMK_META_CONTAINER_ATTRIBUTE_TARGET);
         char *phys_var = crm_meta_name(PCMK__META_PHYSICAL_HOST);
         const char *target = NULL;
         const char *host_physical = NULL;
 
         snprintf(buf + offset, sizeof(buf) - offset, "%s", target_var);
         target = getenv(buf);
 
         snprintf(buf + offset, sizeof(buf) - offset, "%s", phys_var);
         host_physical = getenv(buf);
 
         // It is important to use the name by which the scheduler knows us
         if (host_physical
             && pcmk__str_eq(target, PCMK_VALUE_HOST, pcmk__str_casei)) {
             name = host_physical;
 
         } else {
             const char *host_pcmk = getenv(LRM_TARGET_ENV);
 
             if (host_pcmk) {
                 name = host_pcmk;
             }
         }
         free(target_var);
         free(phys_var);
 
         // TODO? Call pcmk__cluster_local_node_name() if name == NULL
         // (currently would require linkage against libcrmcluster)
         return name;
     } else {
         return NULL;
     }
 }
 
 /*!
  * \brief Return the name of the node attribute used as a promotion score
  *
  * \param[in] rsc_id  Resource ID that promotion score is for (or NULL to
  *                    check the OCF_RESOURCE_INSTANCE environment variable)
  *
  * \return Newly allocated string with the node attribute name (or NULL on
  *         error, including no ID or environment variable specified)
  * \note It is the caller's responsibility to free() the result.
  */
 char *
 pcmk_promotion_score_name(const char *rsc_id)
 {
     if (pcmk__str_empty(rsc_id)) {
         rsc_id = getenv("OCF_RESOURCE_INSTANCE");
         if (pcmk__str_empty(rsc_id)) {
             return NULL;
         }
     }
     return crm_strdup_printf("master-%s", rsc_id);
 }
 
 /*!
  * \internal
  * \brief Get the value of a node attribute
  *
  * \param[in] node       Node to get attribute for
  * \param[in] name       Name of node attribute to get
  * \param[in] target     If this is \c PCMK_VALUE_HOST and \p node is a guest
  *                       (bundle) node, get the value from the guest's host,
  *                       otherwise get the value from \p node itself
  * \param[in] node_type  If getting the value from \p node's host, this
  *                       indicates whether to check the current or assigned host
  *
  * \return Value of \p name attribute for \p node
  */
 const char *
 pcmk__node_attr(const pcmk_node_t *node, const char *name, const char *target,
                 enum pcmk__rsc_node node_type)
 {
     const char *value = NULL;       // Attribute value to return
     const char *node_type_s = NULL; // Readable equivalent of node_type
     const pcmk_node_t *host = NULL;
     const pcmk_resource_t *container = NULL;
 
     if ((node == NULL) || (name == NULL)) {
         return NULL;
     }
 
     /* Check the node's own attributes unless this is a guest (bundle) node with
      * the container host as the attribute target.
      */
     if (!pcmk__is_guest_or_bundle_node(node)
         || !pcmk__str_eq(target, PCMK_VALUE_HOST, pcmk__str_casei)) {
         value = g_hash_table_lookup(node->details->attrs, name);
         crm_trace("%s='%s' on %s",
                   name, pcmk__s(value, ""), pcmk__node_name(node));
         return value;
     }
 
     /* This resource needs attributes set for the container's host instead of
      * for the container itself (useful when the container uses the host's
      * storage).
      */
     container = node->details->remote_rsc->container;
 
     switch (node_type) {
         case pcmk__rsc_node_assigned:
-            host = container->allocated_to;
+            host = container->private->assigned_node;
             if (host == NULL) {
                 crm_trace("Skipping %s lookup for %s because "
                           "its container %s is unassigned",
                           name, pcmk__node_name(node), container->id);
                 return NULL;
             }
             node_type_s = "assigned";
             break;
 
         case pcmk__rsc_node_current:
             if (container->running_on != NULL) {
                 host = container->running_on->data;
             }
             if (host == NULL) {
                 crm_trace("Skipping %s lookup for %s because "
                           "its container %s is inactive",
                           name, pcmk__node_name(node), container->id);
                 return NULL;
             }
             node_type_s = "current";
             break;
 
         default:
             // Add support for other enum pcmk__rsc_node values if needed
             CRM_ASSERT(false);
             break;
     }
 
     value = g_hash_table_lookup(host->details->attrs, name);
     crm_trace("%s='%s' for %s on %s container host %s",
               name, pcmk__s(value, ""), pcmk__node_name(node), node_type_s,
               pcmk__node_name(host));
     return value;
 }
diff --git a/lib/pacemaker/pcmk_graph_producer.c b/lib/pacemaker/pcmk_graph_producer.c
index f77fd25caf..75289d4fdb 100644
--- a/lib/pacemaker/pcmk_graph_producer.c
+++ b/lib/pacemaker/pcmk_graph_producer.c
@@ -1,1098 +1,1098 @@
 /*
  * 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 != NULL)
                && pcmk_is_set(action->rsc->flags,
                               pcmk__rsc_is_remote_connection)
                && 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");
 
         return pcmk__notify_key(action->rsc->private->history_id, n_type,
                                 n_task);
     }
     return pcmk__op_key(action->rsc->private->history_id,
                         pcmk__s(action->cancel_task, 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->private->xml->name);
     if (pcmk_is_set(action->rsc->flags, pcmk__rsc_removed)
         && (action->rsc->private->history_id != 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 history ID %s",
                   action->rsc->id, action->rsc->private->history_id);
         crm_xml_add(rsc_xml, PCMK_XA_ID, action->rsc->private->history_id);
         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->private->xml);
 
         crm_debug("Using anonymous clone name %s for %s (aka %s)",
                   xml_id, action->rsc->id, action->rsc->private->history_id);
 
         /* 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->private->history_id != NULL)
             && !pcmk__str_eq(xml_id, action->rsc->private->history_id,
                              pcmk__str_none)) {
             crm_xml_add(rsc_xml, PCMK__XA_LONG_ID,
                         action->rsc->private->history_id);
         } else {
             crm_xml_add(rsc_xml, PCMK__XA_LONG_ID, action->rsc->id);
         }
 
     } else {
         CRM_ASSERT(action->rsc->private->history_id == 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;
     pcmk_resource_t *rsc = action->rsc;
 
     /* 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(action_xml, 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 ((rsc != NULL) && (action->node != NULL)) {
         // Get the resource instance attributes, evaluated properly for node
         GHashTable *params = pe_rsc_params(rsc, action->node,
                                            rsc->private->scheduler);
 
         pcmk__substitute_remote_addr(rsc, params);
 
         g_hash_table_foreach(params, hash2smartfield, args_xml);
 
     } else if ((rsc != NULL)
                && (rsc->private->variant <= pcmk__rsc_variant_primitive)) {
         GHashTable *params = pe_rsc_params(rsc, NULL, rsc->private->scheduler);
 
         g_hash_table_foreach(params, hash2smartfield, args_xml);
     }
 
     g_hash_table_foreach(action->meta, hash2metafield, args_xml);
     if (rsc != NULL) {
         pcmk_resource_t *parent = rsc;
 
         while (parent != NULL) {
             parent->private->cmds->add_graph_meta(parent, args_xml);
             parent = parent->private->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);
     }
 
     pcmk__xe_sort_attrs(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->private->history_id != 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;
+            pcmk_node_t *assigned = action->rsc->private->assigned_node;
 
             /* 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->private->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->private->actions, add_action_to_graph,
                    rsc->private->scheduler);
 
     // 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->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->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_sched_bundle.c b/lib/pacemaker/pcmk_sched_bundle.c
index 1195d2b511..90bdbf6df9 100644
--- a/lib/pacemaker/pcmk_sched_bundle.c
+++ b/lib/pacemaker/pcmk_sched_bundle.c
@@ -1,1065 +1,1065 @@
 /*
  * 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->private->cmds->assign(replica->ip, prefer, stop_if_fail);
     }
 
-    container_host = replica->container->allocated_to;
+    container_host = replica->container->private->assigned_node;
     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->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->private->parent,
                             pcmk__rsc_assigning);
         pcmk__rsc_trace(bundle, "Assigning bundle %s replica child %s",
                         bundle->id, replica->child->id);
         replica->child->private->cmds->assign(replica->child, replica->node,
                                               stop_if_fail);
         pcmk__clear_rsc_flags(replica->child->private->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->private->scheduler->flags,
                                       pcmk_sched_output_scores),
                          rsc, __func__, rsc->allowed_nodes,
                          rsc->private->scheduler);
 
     // 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->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->private->cmds->create_actions(replica->ip);
     }
     if (replica->container != NULL) {
         replica->container->private->cmds->create_actions(replica->container);
     }
     if (replica->remote != NULL) {
         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->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->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->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->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->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) {
         container->private->cmds->apply_coloc_score(coloc_data->dependent,
                                                     container,
                                                     coloc_data->colocation,
                                                     false);
         return true;
     }
 
     chosen = container->private->fns->location(container, NULL, 0);
     if ((chosen == NULL)
         || 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(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->private->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->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->private->with_this_colocations,
                                  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->private->with_this_colocations,
                                      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->private->with_this_colocations,
                                  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->private->this_with_colocations,
                                  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->private->this_with_colocations,
                                      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->private->this_with_colocations,
                                  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;
 
     replica->container->private->cmds->apply_location(replica->container,
                                                       location);
     if (replica->ip != NULL) {
         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->private->cmds->apply_location(bundled_resource,
                                                         location);
         bundled_resource->private->location_constraints =
             g_list_prepend(bundled_resource->private->location_constraints,
                            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)
         && 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->private->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 = NULL;
 
             params = pe_rsc_params(replica->remote, NULL,
                                    replica->remote->private->scheduler);
             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->private->cmds->add_actions_to_graph(replica->ip);
     }
     replica->container->private->cmds->add_actions_to_graph(replica->container);
     if (replica->remote != NULL) {
         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->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->private->scheduler);
     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->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->private->cmds->create_probe(replica->child,
                                                        probe_data->node)) {
         probe_data->any_created = true;
     }
     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->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 = NULL;
 
         probe = find_first_action(replica->remote->private->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->private->scheduler);
         }
     }
     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->private->cmds->output_actions(replica->ip);
     }
     replica->container->private->cmds->output_actions(replica->container);
     if (replica->remote != NULL) {
         replica->remote->private->cmds->output_actions(replica->remote);
     }
     if (replica->child != NULL) {
         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->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_colocation.c b/lib/pacemaker/pcmk_sched_colocation.c
index 9d9fb80a9d..f6822edd04 100644
--- a/lib/pacemaker/pcmk_sched_colocation.c
+++ b/lib/pacemaker/pcmk_sched_colocation.c
@@ -1,1946 +1,1951 @@
 /*
  * 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->private->priority > rsc2->private->priority) {
         return -1;
     }
     if (rsc1->private->priority < rsc2->private->priority) {
         return 1;
     }
 
     // Process clones before primitives and groups
     if (rsc1->private->variant > rsc2->private->variant) {
         return -1;
     }
     if (rsc1->private->variant < rsc2->private->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->private->this_with_colocations), new_con,
                         dependent);
     pcmk__add_with_this(&(primary->private->with_this_colocations), new_con,
                         primary);
 
     dependent->private->scheduler->colocation_constraints =
         g_list_prepend(dependent->private->scheduler->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->private->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->private->scheduler);
         }
     }
 
     // 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->private->parent != NULL) {
         rsc = rsc->private->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 = NULL;
 
         child_action = find_first_action(child->private->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->private->parent != NULL)
         && pcmk_is_set(dependent_role_rsc->private->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;
+        const pcmk_node_t *primary_node = primary->private->assigned_node;
+        const pcmk_node_t *dependent_node = dependent->private->assigned_node;
 
-        if (dependent->allocated_to == NULL) {
+        if (dependent_node == 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)) {
+            if (!pcmk__same_node(primary_node, dependent_node)) {
                 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(dependent_node),
                                 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)) {
+            if (pcmk__same_node(dependent_node, 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);
+    if (primary->private->assigned_node != NULL) {
+        value = pcmk__colocation_node_attr(primary->private->assigned_node,
+                                           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) {
+        if (primary->private->assigned_node == 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_node_t *primary_node = NULL;
+    const pcmk_node_t *dependent_node = NULL;
 
     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)) {
+    primary_node = primary->private->assigned_node;
+    dependent_node = dependent->private->assigned_node;
+
+    if ((primary_node == NULL) || (dependent_node == NULL)) {
         return;
     }
 
-    dependent_value = pcmk__colocation_node_attr(dependent->allocated_to, attr,
+    dependent_value = pcmk__colocation_node_attr(dependent_node, attr,
                                                  dependent);
-    primary_value = pcmk__colocation_node_attr(primary->allocated_to, attr,
-                                               primary);
+    primary_value = pcmk__colocation_node_attr(primary_node, 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->private->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->private->priority =
         pcmk__add_scores(score_multiplier * colocation->score,
                          dependent->private->priority);
     pcmk__rsc_trace(dependent,
                     "Applied %s to %s promotion priority (now %s after %s %s)",
                     colocation->id, dependent->id,
                     pcmk_readable_score(dependent->private->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->private->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->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->private->scheduler);
         }
         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 *primary = user_data;
 
     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(primary)) {
         flags |= pcmk__coloc_select_nonnegative;
     }
     pcmk__rsc_trace(primary,
                     "%s: Incorporating attenuated %s assignment scores due "
                     "to colocation %s",
                     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->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->private->cmds->this_with_colocations(rsc, rsc, &list);
     return list;
 }
diff --git a/lib/pacemaker/pcmk_sched_group.c b/lib/pacemaker/pcmk_sched_group.c
index b4be5b7ac4..7ad47753a4 100644
--- a/lib/pacemaker/pcmk_sched_group.c
+++ b/lib/pacemaker/pcmk_sched_group.c
@@ -1,973 +1,973 @@
 /*
  * 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
+        return rsc->private->assigned_node; // 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->private->scheduler->flags,
                                       pcmk_sched_output_scores),
                          rsc, __func__, rsc->allowed_nodes,
                          rsc->private->scheduler);
 
     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->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->private->scheduler);
 
     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->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->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->private->parent,
                                      PCMK_ACTION_DEMOTE,
                                      member, PCMK_ACTION_DEMOTE, down_flags);
         pcmk__order_resource_actions(member, PCMK_ACTION_DEMOTE,
                                      member->private->parent,
                                      PCMK_ACTION_DEMOTED, post_down_flags);
 
         // Promote group -> promote member -> group is promoted
         pcmk__order_resource_actions(member, PCMK_ACTION_PROMOTE,
                                      member->private->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->private->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->private->parent, member, down_flags);
     pcmk__order_resource_actions(member, PCMK_ACTION_STOP,
                                  member->private->parent, PCMK_ACTION_STOPPED,
                                  post_down_flags);
 
     // Start group -> start member -> group is started
     pcmk__order_starts(member->private->parent, member,
                        pcmk__ar_then_implies_first_graphed);
     pcmk__order_resource_actions(member, PCMK_ACTION_START,
                                  member->private->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->private->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->private->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->private->parent, member, pcmk__ar_none);
         if (member_data->promotable) {
             pcmk__order_resource_actions(member->private->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->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->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->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->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 = NULL;
 
         member_action = find_first_action(member->private->actions, NULL,
                                           task_s, node);
         if (member_action != NULL) {
             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 = NULL;
 
         member_action = find_first_action(member->private->actions, NULL,
                                           then->task, node);
         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->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)
 {
     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->private->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;
 
             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)
 
 {
     const pcmk_resource_t *parent = NULL;
 
     CRM_ASSERT((orig_rsc != NULL) && (list != NULL) && pcmk__is_group(rsc));
     parent = rsc->private->parent;
 
     // 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->private->with_this_colocations,
                              orig_rsc);
 
     // If cloned, add any relevant colocations with the clone
     if (parent != NULL) {
         parent->private->cmds->with_this_colocations(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) {
             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 *parent = NULL;
     const pcmk_resource_t *member = NULL;
 
     CRM_ASSERT((orig_rsc != NULL) && (list != NULL) && pcmk__is_group(rsc));
     parent = rsc->private->parent;
 
     // 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->private->this_with_colocations,
                                  orig_rsc);
 
         // If cloned, add any relevant colocations involving the clone
         if (parent != NULL) {
             parent->private->cmds->this_with_colocations(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) {
                 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->private->this_with_colocations;
                  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->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->private->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->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->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->private->cmds->shutdown_lock(member);
     }
 }
diff --git a/lib/pacemaker/pcmk_sched_migration.c b/lib/pacemaker/pcmk_sched_migration.c
index 95e9a3a2f6..349cae97d5 100644
--- a/lib/pacemaker/pcmk_sched_migration.c
+++ b/lib/pacemaker/pcmk_sched_migration.c
@@ -1,406 +1,409 @@
 /*
  * 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 Add migration source and target meta-attributes to an action
  *
  * \param[in,out] action  Action to add meta-attributes to
  * \param[in]     source  Node to add as migration source
  * \param[in]     target  Node to add as migration target
  */
 static void
 add_migration_meta(pcmk_action_t *action, const pcmk_node_t *source,
                    const pcmk_node_t *target)
 {
     pcmk__insert_meta(action, PCMK__META_MIGRATE_SOURCE,
                       source->details->uname);
 
     pcmk__insert_meta(action, PCMK__META_MIGRATE_TARGET,
                       target->details->uname);
 }
 
 /*!
  * \internal
  * \brief Create internal migration actions for a migrateable resource
  *
  * \param[in,out] rsc      Resource to create migration actions for
  * \param[in]     current  Node that resource is originally active on
  */
 void
 pcmk__create_migration_actions(pcmk_resource_t *rsc, const pcmk_node_t *current)
 {
     pcmk_action_t *migrate_to = NULL;
     pcmk_action_t *migrate_from = NULL;
     pcmk_action_t *start = NULL;
     pcmk_action_t *stop = NULL;
 
     pcmk__rsc_trace(rsc, "Creating actions to %smigrate %s from %s to %s",
                     ((rsc->partial_migration_target == NULL)? "" : "partially "),
                     rsc->id, pcmk__node_name(current),
-                    pcmk__node_name(rsc->allocated_to));
-    start = start_action(rsc, rsc->allocated_to, TRUE);
+                    pcmk__node_name(rsc->private->assigned_node));
+    start = start_action(rsc, rsc->private->assigned_node, TRUE);
     stop = stop_action(rsc, current, TRUE);
 
     if (rsc->partial_migration_target == NULL) {
         migrate_to = custom_action(rsc, pcmk__op_key(rsc->id,
                                                      PCMK_ACTION_MIGRATE_TO, 0),
                                    PCMK_ACTION_MIGRATE_TO, current, TRUE,
                                    rsc->private->scheduler);
     }
     migrate_from = custom_action(rsc, pcmk__op_key(rsc->id,
                                                    PCMK_ACTION_MIGRATE_FROM, 0),
-                                 PCMK_ACTION_MIGRATE_FROM, rsc->allocated_to,
-                                 TRUE, rsc->private->scheduler);
+                                 PCMK_ACTION_MIGRATE_FROM,
+                                 rsc->private->assigned_node, TRUE,
+                                 rsc->private->scheduler);
 
     pcmk__set_action_flags(start, pcmk_action_migratable);
     pcmk__set_action_flags(stop, pcmk_action_migratable);
 
     // This is easier than trying to delete it from the graph
     pcmk__set_action_flags(start, pcmk_action_pseudo);
 
     if (rsc->partial_migration_target == NULL) {
         pcmk__set_action_flags(migrate_from, pcmk_action_migratable);
         pcmk__set_action_flags(migrate_to, pcmk_action_migratable);
         migrate_to->needs = start->needs;
 
         // Probe -> migrate_to -> migrate_from
         pcmk__new_ordering(rsc, pcmk__op_key(rsc->id, PCMK_ACTION_MONITOR, 0),
                            NULL,
                            rsc,
                            pcmk__op_key(rsc->id, PCMK_ACTION_MIGRATE_TO, 0),
                            NULL, pcmk__ar_ordered, rsc->private->scheduler);
         pcmk__new_ordering(rsc, pcmk__op_key(rsc->id, PCMK_ACTION_MIGRATE_TO, 0),
                            NULL,
                            rsc,
                            pcmk__op_key(rsc->id, PCMK_ACTION_MIGRATE_FROM, 0),
                            NULL,
                            pcmk__ar_ordered|pcmk__ar_unmigratable_then_blocks,
                            rsc->private->scheduler);
     } else {
         pcmk__set_action_flags(migrate_from, pcmk_action_migratable);
         migrate_from->needs = start->needs;
 
         // Probe -> migrate_from (migrate_to already completed)
         pcmk__new_ordering(rsc, pcmk__op_key(rsc->id, PCMK_ACTION_MONITOR, 0),
                            NULL,
                            rsc,
                            pcmk__op_key(rsc->id, PCMK_ACTION_MIGRATE_FROM, 0),
                            NULL, pcmk__ar_ordered, rsc->private->scheduler);
     }
 
     // migrate_from before stop or start
     pcmk__new_ordering(rsc, pcmk__op_key(rsc->id, PCMK_ACTION_MIGRATE_FROM, 0),
                        NULL,
                        rsc, pcmk__op_key(rsc->id, PCMK_ACTION_STOP, 0),
                        NULL,
                        pcmk__ar_ordered|pcmk__ar_unmigratable_then_blocks,
                        rsc->private->scheduler);
     pcmk__new_ordering(rsc, pcmk__op_key(rsc->id, PCMK_ACTION_MIGRATE_FROM, 0),
                        NULL,
                        rsc, pcmk__op_key(rsc->id, PCMK_ACTION_START, 0),
                        NULL,
                        pcmk__ar_ordered
                        |pcmk__ar_unmigratable_then_blocks
                        |pcmk__ar_first_else_then,
                        rsc->private->scheduler);
 
     if (migrate_to != NULL) {
-        add_migration_meta(migrate_to, current, rsc->allocated_to);
+        add_migration_meta(migrate_to, current, rsc->private->assigned_node);
 
         if (!pcmk_is_set(rsc->flags, pcmk__rsc_is_remote_connection)) {
             /* migrate_to takes place on the source node, but can affect the
              * target node depending on how the agent is written. Because of
              * this, pending migrate_to actions must be recorded in the CIB,
              * in case the source node loses membership while the migrate_to
              * action is still in flight.
              *
              * However we know Pacemaker Remote connection resources don't
              * require this, so we skip this for them. (Although it wouldn't
              * hurt, and now that PCMK_META_RECORD_PENDING defaults to true,
              * skipping it matters even less.)
              */
             pcmk__insert_meta(migrate_to,
                               PCMK_META_RECORD_PENDING, PCMK_VALUE_TRUE);
         }
     }
 
-    add_migration_meta(migrate_from, current, rsc->allocated_to);
+    add_migration_meta(migrate_from, current, rsc->private->assigned_node);
 }
 
 /*!
  * \internal
  * \brief Abort a dangling migration by scheduling a stop (and possibly cleanup)
  *
  * \param[in]     data       Source node of dangling migration
  * \param[in,out] user_data  Resource involved in dangling migration
  */
 void
 pcmk__abort_dangling_migration(void *data, void *user_data)
 {
     const pcmk_node_t *dangling_source = (const pcmk_node_t *) data;
     pcmk_resource_t *rsc = (pcmk_resource_t *) user_data;
 
     pcmk_action_t *stop = NULL;
     bool cleanup = pcmk_is_set(rsc->private->scheduler->flags,
                                pcmk_sched_remove_after_stop);
 
     pcmk__rsc_trace(rsc,
                     "Scheduling stop%s for %s on %s due to dangling migration",
                     (cleanup? " and cleanup" : ""), rsc->id,
                     pcmk__node_name(dangling_source));
     stop = stop_action(rsc, dangling_source, FALSE);
     pcmk__set_action_flags(stop, pcmk_action_migration_abort);
     if (cleanup) {
         pcmk__schedule_cleanup(rsc, dangling_source, false);
     }
 }
 
 /*!
  * \internal
  * \brief Check whether a resource can migrate
  *
  * \param[in] rsc   Resource to check
  * \param[in] node  Resource's current node
  *
  * \return true if \p rsc can migrate, otherwise false
  */
 bool
 pcmk__rsc_can_migrate(const pcmk_resource_t *rsc, const pcmk_node_t *current)
 {
     CRM_CHECK(rsc != NULL, return false);
 
     if (!pcmk_is_set(rsc->flags, pcmk__rsc_migratable)) {
         pcmk__rsc_trace(rsc,
                         "%s cannot migrate because "
                         "the configuration does not allow it", rsc->id);
         return false;
     }
 
     if (!pcmk_is_set(rsc->flags, pcmk__rsc_managed)) {
         pcmk__rsc_trace(rsc, "%s cannot migrate because it is not managed",
                         rsc->id);
         return false;
     }
 
     if (pcmk_is_set(rsc->flags, pcmk__rsc_failed)) {
         pcmk__rsc_trace(rsc, "%s cannot migrate because it is failed", rsc->id);
         return false;
     }
 
     if (pcmk_is_set(rsc->flags, pcmk__rsc_start_pending)) {
         pcmk__rsc_trace(rsc, "%s cannot migrate because it has a start pending",
                         rsc->id);
         return false;
     }
 
     if ((current == NULL) || current->details->unclean) {
         pcmk__rsc_trace(rsc,
                         "%s cannot migrate because "
                         "its current node (%s) is unclean",
                         rsc->id, pcmk__node_name(current));
         return false;
     }
 
-    if ((rsc->allocated_to == NULL) || rsc->allocated_to->details->unclean) {
+    if ((rsc->private->assigned_node == NULL)
+        || rsc->private->assigned_node->details->unclean) {
+
         pcmk__rsc_trace(rsc,
                         "%s cannot migrate because "
                         "its next node (%s) is unclean",
-                        rsc->id, pcmk__node_name(rsc->allocated_to));
+                        rsc->id, pcmk__node_name(rsc->private->assigned_node));
         return false;
     }
 
     return true;
 }
 
 /*!
  * \internal
  * \brief Get an action name from an action or operation key
  *
  * \param[in] action  If not NULL, get action name from here
  * \param[in] key     If not NULL, get action name from here
  *
  * \return Newly allocated copy of action name (or NULL if none available)
  */
 static char *
 task_from_action_or_key(const pcmk_action_t *action, const char *key)
 {
     char *res = NULL;
 
     if (action != NULL) {
         res = pcmk__str_copy(action->task);
     } else if (key != NULL) {
         parse_op_key(key, NULL, &res, NULL);
     }
     return res;
 }
 
 /*!
  * \internal
  * \brief Order migration actions equivalent to a given ordering
  *
  * Orderings involving start, stop, demote, and promote actions must be honored
  * during a migration as well, so duplicate any such ordering for the
  * corresponding migration actions.
  *
  * \param[in,out] order     Ordering constraint to check
  */
 void
 pcmk__order_migration_equivalents(pcmk__action_relation_t *order)
 {
     char *first_task = NULL;
     char *then_task = NULL;
     bool then_migratable;
     bool first_migratable;
 
     // Only orderings between unrelated resources are relevant
     if ((order->rsc1 == NULL) || (order->rsc2 == NULL)
         || (order->rsc1 == order->rsc2)
         || is_parent(order->rsc1, order->rsc2)
         || is_parent(order->rsc2, order->rsc1)) {
         return;
     }
 
     // Only orderings involving at least one migratable resource are relevant
     first_migratable = pcmk_is_set(order->rsc1->flags, pcmk__rsc_migratable);
     then_migratable = pcmk_is_set(order->rsc2->flags, pcmk__rsc_migratable);
     if (!first_migratable && !then_migratable) {
         return;
     }
 
     // Check which actions are involved
     first_task = task_from_action_or_key(order->action1, order->task1);
     then_task = task_from_action_or_key(order->action2, order->task2);
 
     if (pcmk__str_eq(first_task, PCMK_ACTION_START, pcmk__str_none)
         && pcmk__str_eq(then_task, PCMK_ACTION_START, pcmk__str_none)) {
 
         uint32_t flags = pcmk__ar_ordered;
 
         if (first_migratable && then_migratable) {
             /* A start then B start
              * -> A migrate_from then B migrate_to */
             pcmk__new_ordering(order->rsc1,
                                pcmk__op_key(order->rsc1->id,
                                             PCMK_ACTION_MIGRATE_FROM, 0),
                                NULL, order->rsc2,
                                pcmk__op_key(order->rsc2->id,
                                             PCMK_ACTION_MIGRATE_TO, 0),
                                NULL, flags, order->rsc1->private->scheduler);
         }
 
         if (then_migratable) {
             if (first_migratable) {
                 pcmk__set_relation_flags(flags, pcmk__ar_if_first_unmigratable);
             }
 
             /* A start then B start
              * -> A start then B migrate_to (if start is not part of a
              *    migration)
              */
             pcmk__new_ordering(order->rsc1,
                                pcmk__op_key(order->rsc1->id,
                                             PCMK_ACTION_START, 0),
                                NULL, order->rsc2,
                                pcmk__op_key(order->rsc2->id,
                                             PCMK_ACTION_MIGRATE_TO, 0),
                                NULL, flags, order->rsc1->private->scheduler);
         }
 
     } else if (then_migratable
                && pcmk__str_eq(first_task, PCMK_ACTION_STOP, pcmk__str_none)
                && pcmk__str_eq(then_task, PCMK_ACTION_STOP, pcmk__str_none)) {
 
         uint32_t flags = pcmk__ar_ordered;
 
         if (first_migratable) {
             pcmk__set_relation_flags(flags, pcmk__ar_if_first_unmigratable);
         }
 
         /* For an ordering "stop A then stop B", if A is moving via restart, and
          * B is migrating, enforce that B's migrate_to occurs after A's stop.
          */
         pcmk__new_ordering(order->rsc1,
                            pcmk__op_key(order->rsc1->id, PCMK_ACTION_STOP, 0),
                            NULL,
                            order->rsc2,
                            pcmk__op_key(order->rsc2->id,
                                         PCMK_ACTION_MIGRATE_TO, 0),
                            NULL, flags, order->rsc1->private->scheduler);
 
         // Also order B's migrate_from after A's stop during partial migrations
         if (order->rsc2->partial_migration_target != NULL) {
             pcmk__new_ordering(order->rsc1,
                                pcmk__op_key(order->rsc1->id, PCMK_ACTION_STOP,
                                             0),
                                NULL, order->rsc2,
                                pcmk__op_key(order->rsc2->id,
                                             PCMK_ACTION_MIGRATE_FROM, 0),
                                NULL, flags, order->rsc1->private->scheduler);
         }
 
     } else if (pcmk__str_eq(first_task, PCMK_ACTION_PROMOTE, pcmk__str_none)
                && pcmk__str_eq(then_task, PCMK_ACTION_START, pcmk__str_none)) {
 
         uint32_t flags = pcmk__ar_ordered;
 
         if (then_migratable) {
             /* A promote then B start
              * -> A promote then B migrate_to */
             pcmk__new_ordering(order->rsc1,
                                pcmk__op_key(order->rsc1->id,
                                             PCMK_ACTION_PROMOTE, 0),
                                NULL, order->rsc2,
                                pcmk__op_key(order->rsc2->id,
                                             PCMK_ACTION_MIGRATE_TO, 0),
                                NULL, flags, order->rsc1->private->scheduler);
         }
 
     } else if (pcmk__str_eq(first_task, PCMK_ACTION_DEMOTE, pcmk__str_none)
                && pcmk__str_eq(then_task, PCMK_ACTION_STOP, pcmk__str_none)) {
 
         uint32_t flags = pcmk__ar_ordered;
 
         if (then_migratable) {
             /* A demote then B stop
              * -> A demote then B migrate_to */
             pcmk__new_ordering(order->rsc1,
                                pcmk__op_key(order->rsc1->id,
                                             PCMK_ACTION_DEMOTE, 0),
                                NULL, order->rsc2,
                                pcmk__op_key(order->rsc2->id,
                                             PCMK_ACTION_MIGRATE_TO, 0),
                                NULL, flags, order->rsc1->private->scheduler);
 
             // Order B migrate_from after A demote during partial migrations
             if (order->rsc2->partial_migration_target != NULL) {
                 pcmk__new_ordering(order->rsc1,
                                    pcmk__op_key(order->rsc1->id,
                                                 PCMK_ACTION_DEMOTE, 0),
                                    NULL, order->rsc2,
                                    pcmk__op_key(order->rsc2->id,
                                                 PCMK_ACTION_MIGRATE_FROM, 0),
                                    NULL, flags,
                                    order->rsc1->private->scheduler);
             }
         }
     }
 
     free(first_task);
     free(then_task);
 }
diff --git a/lib/pacemaker/pcmk_sched_primitive.c b/lib/pacemaker/pcmk_sched_primitive.c
index d4cb1bcb2c..ee63ded450 100644
--- a/lib/pacemaker/pcmk_sched_primitive.c
+++ b/lib/pacemaker/pcmk_sched_primitive.c
@@ -1,1699 +1,1700 @@
 /*
  * 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;
+        return rsc->private->assigned_node != 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->private->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;
+    return rsc->private->assigned_node != 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->private->cmds->assign(other, NULL, true);
     }
 
     // Apply the colocation score to this resource's allowed node scores
     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->private->scheduler,
                                               connection->id);
 
     CRM_CHECK(remote_node != NULL, return);
 
-    if ((connection->allocated_to != NULL)
+    if ((connection->private->assigned_node != 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" : ""),
+                  ((connection->private->assigned_node == 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_resource_t *parent = NULL;
     pcmk__colocation_t *colocation = NULL;
     pcmk_scheduler_t *scheduler = NULL;
 
     CRM_ASSERT(pcmk__is_primitive(rsc));
     scheduler = rsc->private->scheduler;
     parent = rsc->private->parent;
 
     // Never assign a child without parent being assigned first
     if ((parent != NULL) && !pcmk_is_set(parent->flags, pcmk__rsc_assigning)) {
         pcmk__rsc_debug(rsc, "%s: Assigning parent %s first",
                         rsc->id, parent->id);
         parent->private->cmds->assign(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);
+        if (rsc->private->assigned_node != NULL) {
+            node_name = pcmk__node_name(rsc->private->assigned_node);
         }
         pcmk__rsc_debug(rsc, "%s: pre-assigned to %s", rsc->id, node_name);
-        return rsc->allocated_to;
+        return rsc->private->assigned_node;
     }
 
     // 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,
                          scheduler);
 
     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, scheduler);
 
     // 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, scheduler);
 
     } else if ((rsc->next_role > rsc->role)
                && !pcmk_is_set(scheduler->flags, pcmk_sched_quorate)
                && (scheduler->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(scheduler->flags,
                                       pcmk_sched_output_scores),
                          rsc, __func__, rsc->allowed_nodes, scheduler);
 
     // Unmanage resource if fencing is enabled but no device is configured
     if (pcmk_is_set(scheduler->flags, pcmk_sched_fencing_enabled)
         && !pcmk_is_set(scheduler->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(scheduler->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 (pcmk_is_set(rsc->flags, pcmk__rsc_is_remote_connection)) {
         remote_connection_assigned(rsc);
     }
 
-    return rsc->allocated_to;
+    return rsc->private->assigned_node;
 }
 
 /*!
  * \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);
+        fn(rsc, rsc->private->assigned_node, !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) {
+    if (rsc->private->assigned_node == 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);
+    start = start_action(rsc, rsc->private->assigned_node, 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);
+        fn(rsc, rsc->private->assigned_node, 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));
+                    pcmk__node_name(rsc->private->assigned_node));
 
     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)
+    if ((current != NULL) && (rsc->private->assigned_node != NULL)
+        && !pcmk__same_node(current, rsc->private->assigned_node)
         && (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));
+                        pcmk__node_name(rsc->private->assigned_node));
         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)) {
+        && pcmk__same_node(rsc->private->assigned_node,
+                           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->private->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));
         crm_notice("For more information, see \"What are multiply active "
                    "resources?\" at "
                    "https://projects.clusterlabs.org/w/clusterlabs/faq/");
 
         switch (rsc->private->multiply_active_policy) {
             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)) {
+               && (rsc->private->assigned_node != 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);
+        start = start_action(rsc, rsc->private->assigned_node, 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;
     pcmk_scheduler_t *scheduler = NULL;
 
     CRM_ASSERT(pcmk__is_primitive(rsc));
     scheduler = rsc->private->scheduler;
 
     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(scheduler->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(scheduler->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, scheduler);
 
     // 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, scheduler);
 
         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, scheduler);
     }
 
     // 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,
                        scheduler);
 
     // 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 (pcmk_is_set(rsc->flags, pcmk__rsc_is_remote_connection)) {
             // 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 (pcmk_is_set(rsc->container->flags,
                                pcmk__rsc_is_remote_connection)) {
             remote_rsc = rsc->container;
         } else  {
             remote_rsc = pe__resource_contains_guest_node(scheduler,
                                                           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, scheduler);
 
             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, scheduler);
 
             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 (pcmk_is_set(rsc->flags, pcmk__rsc_is_remote_connection)
         || 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->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)
 {
     const pcmk_resource_t *parent = NULL;
 
     CRM_ASSERT(pcmk__is_primitive(rsc) && (list != NULL));
     parent = rsc->private->parent;
 
     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->private->with_this_colocations,
                                  orig_rsc);
         if (parent != NULL) {
             parent->private->cmds->with_this_colocations(parent, orig_rsc,
                                                          list);
         }
     } else {
         // For an ancestor, add only explicitly configured constraints
         for (GList *iter = rsc->private->with_this_colocations;
              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)
 {
     const pcmk_resource_t *parent = NULL;
 
     CRM_ASSERT(pcmk__is_primitive(rsc) && (list != NULL));
     parent = rsc->private->parent;
 
     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->private->this_with_colocations,
                                  orig_rsc);
         if (parent != NULL) {
             parent->private->cmds->this_with_colocations(parent, orig_rsc,
                                                          list);
         }
     } else {
         // For an ancestor, add only explicitly configured constraints
         for (GList *iter = rsc->private->this_with_colocations;
              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);
+           && pcmk__same_node(rsc->private->assigned_node, 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__same_node(current, rsc->private->assigned_node)) {
                 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) {
+        if (rsc->private->assigned_node == 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->private->scheduler->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->private->scheduler);
 
             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->private->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->private->scheduler);
     }
 }
 
 // Primitive implementation of pcmk__assignment_methods_t:shutdown_lock()
 void
 pcmk__primitive_shutdown_lock(pcmk_resource_t *rsc)
 {
     const char *class = NULL;
     pcmk_scheduler_t *scheduler = NULL;
 
     CRM_ASSERT(pcmk__is_primitive(rsc));
     scheduler = rsc->private->scheduler;
 
     class = crm_element_value(rsc->private->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)
         || pcmk_is_set(rsc->flags, pcmk__rsc_is_remote_connection)) {
         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 (scheduler->shutdown_lock > 0) {
         time_t lock_expiration = rsc->lock_time + scheduler->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, scheduler,
                                 "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(scheduler->nodes, ban_if_not_locked, rsc);
 }
diff --git a/lib/pacemaker/pcmk_sched_probes.c b/lib/pacemaker/pcmk_sched_probes.c
index f0e2f6f9b3..d9d7f71622 100644
--- a/lib/pacemaker/pcmk_sched_probes.c
+++ b/lib/pacemaker/pcmk_sched_probes.c
@@ -1,912 +1,914 @@
 /*
  * 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->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)
+    const pcmk_node_t *rsc1_node = rsc1->private->assigned_node;
+
+    if ((rsc1_node != NULL)
         && (g_hash_table_lookup(rsc1->known_on,
-                                rsc1->allocated_to->details->id) == NULL)) {
+                                rsc1_node->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->private->scheduler);
     }
 }
 
 /*!
  * \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;
+    const pcmk_node_t *guest_node = guest_rsc->private->assigned_node;
 
     /* 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));
+           || ((guest_rsc->role > pcmk_role_stopped) && (guest_node != NULL)
+               && pcmk__find_node_in_list(guest_rsc->running_on,
+                                          guest_node->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->private->scheduler);
     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->private->scheduler->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->private->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->private->scheduler,
                                                        rsc)) {
             reason = "guest nodes cannot run resources containing guest nodes";
             goto no_probe;
 
         } else if (pcmk_is_set(rsc->flags, pcmk__rsc_is_remote_connection)) {
             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)
         && !pcmk_is_set(rsc->flags, pcmk__rsc_is_remote_connection)) {
         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 (pcmk_is_set(rsc->flags, pcmk__rsc_exclusive_probes)
         || pcmk_is_set(top->flags, pcmk__rsc_exclusive_probes)) {
         // 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->private->scheduler);
             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->private->scheduler);
     pcmk__new_ordering(rsc, NULL, probe, top, reload_key(rsc), NULL,
                        pcmk__ar_ordered, rsc->private->scheduler);
 
     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->private->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->private->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->private->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;
         const pcmk_resource_t *chained_rsc = NULL;
 
         /* 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->private->variant < pcmk__rsc_variant_group)
                 || (probe->rsc->private->parent == after->rsc)
                 || (after_wrapper->action->rsc == NULL)) {
                 continue;
             }
             chained_rsc = after_wrapper->action->rsc;
 
             if ((chained_rsc->private->variant > pcmk__rsc_variant_group)
                 || (after->rsc != chained_rsc->private->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->private->variant > pcmk__rsc_variant_group)
                 && interleave
                 && ((compatible_rsc == NULL)
                     || (compatible_rsc != chained_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->private->scheduler);
         }
     }
 
     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", first->uuid, start->uuid);
 
             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_recurring.c b/lib/pacemaker/pcmk_sched_recurring.c
index 14db1916c3..33620bba99 100644
--- a/lib/pacemaker/pcmk_sched_recurring.c
+++ b/lib/pacemaker/pcmk_sched_recurring.c
@@ -1,752 +1,754 @@
 /*
  * 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->private->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->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->private->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->private->scheduler);
             }
         }
 
         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->private->scheduler);
 
     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->private->scheduler);
 
         pcmk__new_ordering(rsc, reload_key(rsc), NULL,
                            NULL, strdup(mon->uuid), mon,
                            pcmk__ar_first_implies_then
                            |pcmk__ar_unrunnable_first_blocks,
                            rsc->private->scheduler);
 
         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->private->scheduler);
 
         } 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->private->scheduler);
         }
     }
 }
 
 /*!
  * \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->private->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->private->scheduler);
             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->private->scheduler);
         }
     }
     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->private->scheduler->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->private->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->private->scheduler);
 
         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) {
+    if (rsc->private->assigned_node == NULL) {
         // Recurring actions for active roles not needed
 
-    } else if (rsc->allocated_to->details->maintenance) {
+    } else if (rsc->private->assigned_node->details->maintenance) {
         pcmk__rsc_trace(rsc,
                         "Skipping recurring actions for %s on %s "
                         "in maintenance mode",
-                        rsc->id, pcmk__node_name(rsc->allocated_to));
+                        rsc->id, pcmk__node_name(rsc->private->assigned_node));
 
     } 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);
+        start = start_action(rsc, rsc->private->assigned_node, TRUE);
     }
 
     pcmk__rsc_trace(rsc, "Creating any recurring actions needed for %s",
                     rsc->id);
 
     for (xmlNode *op = pcmk__xe_first_child(rsc->private->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_active(rsc, start, rsc->private->assigned_node,
+                                    &op_history);
         }
-        recurring_op_for_inactive(rsc, rsc->allocated_to, &op_history);
+        recurring_op_for_inactive(rsc, rsc->private->assigned_node,
+                                  &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->private->scheduler);
 
     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->private->scheduler);
 }
 
 /*!
  * \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->private->scheduler);
     op = custom_action(rsc, pcmk__op_key(rsc->id, task, interval_ms),
                        task, node, TRUE, rsc->private->scheduler);
     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_remote.c b/lib/pacemaker/pcmk_sched_remote.c
index 1f61d17f4e..134517c30d 100644
--- a/lib/pacemaker/pcmk_sched_remote.c
+++ b/lib/pacemaker/pcmk_sched_remote.c
@@ -1,736 +1,738 @@
 /*
  * 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 <crm/common/xml_internal.h>
 
 #include <glib.h>
 
 #include <crm/pengine/status.h>
 #include <pacemaker-internal.h>
 #include "libpacemaker_private.h"
 
 enum remote_connection_state {
     remote_state_unknown = 0,
     remote_state_alive = 1,
     remote_state_resting = 2,
     remote_state_failed = 3,
     remote_state_stopped = 4
 };
 
 static const char *
 state2text(enum remote_connection_state state)
 {
     switch (state) {
         case remote_state_unknown:
             return "unknown";
         case remote_state_alive:
             return "alive";
         case remote_state_resting:
             return "resting";
         case remote_state_failed:
             return "failed";
         case remote_state_stopped:
             return "stopped";
     }
 
     return "impossible";
 }
 
 /* We always use pcmk__ar_guest_allowed with these convenience functions to
  * exempt internally generated constraints from the prohibition of user
  * constraints involving remote connection resources.
  *
  * The start ordering additionally uses pcmk__ar_unrunnable_first_blocks so that
  * the specified action is not runnable if the start is not runnable.
  */
 
 static inline void
 order_start_then_action(pcmk_resource_t *first_rsc, pcmk_action_t *then_action,
                         uint32_t extra)
 {
     if ((first_rsc != NULL) && (then_action != NULL)) {
 
         pcmk__new_ordering(first_rsc, start_key(first_rsc), NULL,
                            then_action->rsc, NULL, then_action,
                            pcmk__ar_guest_allowed
                            |pcmk__ar_unrunnable_first_blocks
                            |extra,
                            first_rsc->private->scheduler);
     }
 }
 
 static inline void
 order_action_then_stop(pcmk_action_t *first_action, pcmk_resource_t *then_rsc,
                        uint32_t extra)
 {
     if ((first_action != NULL) && (then_rsc != NULL)) {
         pcmk__new_ordering(first_action->rsc, NULL, first_action,
                            then_rsc, stop_key(then_rsc), NULL,
                            pcmk__ar_guest_allowed|extra,
                            then_rsc->private->scheduler);
     }
 }
 
 static enum remote_connection_state
 get_remote_node_state(const pcmk_node_t *node)
 {
     const pcmk_resource_t *remote_rsc = NULL;
     const pcmk_node_t *cluster_node = NULL;
 
     CRM_ASSERT(node != NULL);
 
     remote_rsc = node->details->remote_rsc;
     CRM_ASSERT(remote_rsc != NULL);
 
     cluster_node = pcmk__current_node(remote_rsc);
 
     /* If the cluster node the remote connection resource resides on
      * is unclean or went offline, we can't process any operations
      * on that remote node until after it starts elsewhere.
      */
     if ((remote_rsc->next_role == pcmk_role_stopped)
-        || (remote_rsc->allocated_to == NULL)) {
+        || (remote_rsc->private->assigned_node == NULL)) {
 
         // The connection resource is not going to run anywhere
 
         if ((cluster_node != NULL) && cluster_node->details->unclean) {
             /* The remote connection is failed because its resource is on a
              * failed node and can't be recovered elsewhere, so we must fence.
              */
             return remote_state_failed;
         }
 
         if (!pcmk_is_set(remote_rsc->flags, pcmk__rsc_failed)) {
             /* Connection resource is cleanly stopped */
             return remote_state_stopped;
         }
 
         /* Connection resource is failed */
 
         if ((remote_rsc->next_role == pcmk_role_stopped)
             && (remote_rsc->private->remote_reconnect_ms > 0U)
             && node->details->remote_was_fenced
             && !pe__shutdown_requested(node)) {
 
             /* We won't know whether the connection is recoverable until the
              * reconnect interval expires and we reattempt connection.
              */
             return remote_state_unknown;
         }
 
         /* The remote connection is in a failed state. If there are any
          * resources known to be active on it (stop) or in an unknown state
          * (probe), we must assume the worst and fence it.
          */
         return remote_state_failed;
 
     } else if (cluster_node == NULL) {
         /* Connection is recoverable but not currently running anywhere, so see
          * if we can recover it first
          */
         return remote_state_unknown;
 
     } else if (cluster_node->details->unclean
                || !(cluster_node->details->online)) {
         // Connection is running on a dead node, see if we can recover it first
         return remote_state_resting;
 
     } else if (pcmk__list_of_multiple(remote_rsc->running_on)
                && (remote_rsc->partial_migration_source != NULL)
                && (remote_rsc->partial_migration_target != NULL)) {
         /* We're in the middle of migrating a connection resource, so wait until
          * after the migration completes before performing any actions.
          */
         return remote_state_resting;
 
     }
     return remote_state_alive;
 }
 
 /*!
  * \internal
  * \brief Order actions on remote node relative to actions for the connection
  *
  * \param[in,out] action    An action scheduled on a Pacemaker Remote node
  */
 static void
 apply_remote_ordering(pcmk_action_t *action)
 {
     pcmk_resource_t *remote_rsc = NULL;
     enum action_tasks task = pcmk_parse_action(action->task);
     enum remote_connection_state state = get_remote_node_state(action->node);
 
     uint32_t order_opts = pcmk__ar_none;
 
     if (action->rsc == NULL) {
         return;
     }
 
     CRM_ASSERT(pcmk__is_pacemaker_remote_node(action->node));
 
     remote_rsc = action->node->details->remote_rsc;
     CRM_ASSERT(remote_rsc != NULL);
 
     crm_trace("Order %s action %s relative to %s%s (state: %s)",
               action->task, action->uuid,
               pcmk_is_set(remote_rsc->flags, pcmk__rsc_failed)? "failed " : "",
               remote_rsc->id, state2text(state));
 
     if (pcmk__strcase_any_of(action->task, PCMK_ACTION_MIGRATE_TO,
                              PCMK_ACTION_MIGRATE_FROM, NULL)) {
         /* Migration ops map to pcmk_action_unspecified, but we need to apply
          * the same ordering as for stop or demote (see get_router_node()).
          */
         task = pcmk_action_stop;
     }
 
     switch (task) {
         case pcmk_action_start:
         case pcmk_action_promote:
             order_opts = pcmk__ar_none;
 
             if (state == remote_state_failed) {
                 /* Force recovery, by making this action required */
                 pcmk__set_relation_flags(order_opts,
                                          pcmk__ar_first_implies_then);
             }
 
             /* Ensure connection is up before running this action */
             order_start_then_action(remote_rsc, action, order_opts);
             break;
 
         case pcmk_action_stop:
             if (state == remote_state_alive) {
                 order_action_then_stop(action, remote_rsc,
                                        pcmk__ar_then_implies_first);
 
             } else if (state == remote_state_failed) {
                 /* The resource is active on the node, but since we don't have a
                  * valid connection, the only way to stop the resource is by
                  * fencing the node. There is no need to order the stop relative
                  * to the remote connection, since the stop will become implied
                  * by the fencing.
                  */
                 pe_fence_node(remote_rsc->private->scheduler, action->node,
                               "resources are active but "
                               "connection is unrecoverable",
                               FALSE);
 
             } else if (remote_rsc->next_role == pcmk_role_stopped) {
                 /* State must be remote_state_unknown or remote_state_stopped.
                  * Since the connection is not coming back up in this
                  * transition, stop this resource first.
                  */
                 order_action_then_stop(action, remote_rsc,
                                        pcmk__ar_then_implies_first);
 
             } else {
                 /* The connection is going to be started somewhere else, so
                  * stop this resource after that completes.
                  */
                 order_start_then_action(remote_rsc, action, pcmk__ar_none);
             }
             break;
 
         case pcmk_action_demote:
             /* Only order this demote relative to the connection start if the
              * connection isn't being torn down. Otherwise, the demote would be
              * blocked because the connection start would not be allowed.
              */
             if ((state == remote_state_resting)
                 || (state == remote_state_unknown)) {
 
                 order_start_then_action(remote_rsc, action, pcmk__ar_none);
             } /* Otherwise we can rely on the stop ordering */
             break;
 
         default:
             /* Wait for the connection resource to be up */
             if (pcmk__action_is_recurring(action)) {
                 /* In case we ever get the recovery logic wrong, force
                  * recurring monitors to be restarted, even if just
                  * the connection was re-established
                  */
                 order_start_then_action(remote_rsc, action,
                                         pcmk__ar_first_implies_then);
 
             } else {
                 pcmk_node_t *cluster_node = pcmk__current_node(remote_rsc);
 
                 if ((task == pcmk_action_monitor) && (state == remote_state_failed)) {
                     /* We would only be here if we do not know the state of the
                      * resource on the remote node. Since we have no way to find
                      * out, it is necessary to fence the node.
                      */
                     pe_fence_node(remote_rsc->private->scheduler, action->node,
                                   "resources are in unknown state "
                                   "and connection is unrecoverable", FALSE);
                 }
 
                 if ((cluster_node != NULL) && (state == remote_state_stopped)) {
                     /* The connection is currently up, but is going down
                      * permanently. Make sure we check services are actually
                      * stopped _before_ we let the connection get closed.
                      */
                     order_action_then_stop(action, remote_rsc,
                                            pcmk__ar_unrunnable_first_blocks);
 
                 } else {
                     order_start_then_action(remote_rsc, action, pcmk__ar_none);
                 }
             }
             break;
     }
 }
 
 static void
 apply_container_ordering(pcmk_action_t *action)
 {
     /* VMs are also classified as containers for these purposes... in
      * that they both involve a 'thing' running on a real or remote
      * cluster node.
      *
      * This allows us to be smarter about the type and extent of
      * recovery actions required in various scenarios
      */
     pcmk_resource_t *remote_rsc = NULL;
     pcmk_resource_t *container = NULL;
     enum action_tasks task = pcmk_parse_action(action->task);
 
     CRM_ASSERT(action->rsc != NULL);
     CRM_ASSERT(pcmk__is_pacemaker_remote_node(action->node));
 
     remote_rsc = action->node->details->remote_rsc;
     CRM_ASSERT(remote_rsc != NULL);
 
     container = remote_rsc->container;
     CRM_ASSERT(container != NULL);
 
     if (pcmk_is_set(container->flags, pcmk__rsc_failed)) {
         pe_fence_node(action->rsc->private->scheduler, action->node,
                       "container failed", FALSE);
     }
 
     crm_trace("Order %s action %s relative to %s%s for %s%s",
               action->task, action->uuid,
               pcmk_is_set(remote_rsc->flags, pcmk__rsc_failed)? "failed " : "",
               remote_rsc->id,
               pcmk_is_set(container->flags, pcmk__rsc_failed)? "failed " : "",
               container->id);
 
     if (pcmk__strcase_any_of(action->task, PCMK_ACTION_MIGRATE_TO,
                              PCMK_ACTION_MIGRATE_FROM, NULL)) {
         /* Migration ops map to pcmk_action_unspecified, but we need to apply
          * the same ordering as for stop or demote (see get_router_node()).
          */
         task = pcmk_action_stop;
     }
 
     switch (task) {
         case pcmk_action_start:
         case pcmk_action_promote:
             // Force resource recovery if the container is recovered
             order_start_then_action(container, action,
                                     pcmk__ar_first_implies_then);
 
             // Wait for the connection resource to be up, too
             order_start_then_action(remote_rsc, action, pcmk__ar_none);
             break;
 
         case pcmk_action_stop:
         case pcmk_action_demote:
             if (pcmk_is_set(container->flags, pcmk__rsc_failed)) {
                 /* When the container representing a guest node fails, any stop
                  * or demote actions for resources running on the guest node
                  * are implied by the container stopping. This is similar to
                  * how fencing operations work for cluster nodes and remote
                  * nodes.
                  */
             } else {
                 /* Ensure the operation happens before the connection is brought
                  * down.
                  *
                  * If we really wanted to, we could order these after the
                  * connection start, IFF the container's current role was
                  * stopped (otherwise we re-introduce an ordering loop when the
                  * connection is restarting).
                  */
                 order_action_then_stop(action, remote_rsc, pcmk__ar_none);
             }
             break;
 
         default:
             /* Wait for the connection resource to be up */
             if (pcmk__action_is_recurring(action)) {
                 /* In case we ever get the recovery logic wrong, force
                  * recurring monitors to be restarted, even if just
                  * the connection was re-established
                  */
                 if (task != pcmk_action_unspecified) {
                     order_start_then_action(remote_rsc, action,
                                             pcmk__ar_first_implies_then);
                 }
             } else {
                 order_start_then_action(remote_rsc, action, pcmk__ar_none);
             }
             break;
     }
 }
 
 /*!
  * \internal
  * \brief Order all relevant actions relative to remote connection actions
  *
  * \param[in,out] scheduler  Scheduler data
  */
 void
 pcmk__order_remote_connection_actions(pcmk_scheduler_t *scheduler)
 {
     if (!pcmk_is_set(scheduler->flags, pcmk_sched_have_remote_nodes)) {
         return;
     }
 
     crm_trace("Creating remote connection orderings");
 
     for (GList *iter = scheduler->actions; iter != NULL; iter = iter->next) {
         pcmk_action_t *action = iter->data;
         pcmk_resource_t *remote = NULL;
 
         // We are only interested in resource actions
         if (action->rsc == NULL) {
             continue;
         }
 
         /* Special case: If we are clearing the failcount of an actual
          * remote connection resource, then make sure this happens before
          * any start of the resource in this transition.
          */
         if (pcmk_is_set(action->rsc->flags, pcmk__rsc_is_remote_connection)
             && pcmk__str_eq(action->task, PCMK_ACTION_CLEAR_FAILCOUNT,
                             pcmk__str_none)) {
 
             pcmk__new_ordering(action->rsc, NULL, action, action->rsc,
                                pcmk__op_key(action->rsc->id, PCMK_ACTION_START,
                                             0),
                                NULL, pcmk__ar_ordered, scheduler);
 
             continue;
         }
 
         // We are only interested in actions assigned to a node
         if (action->node == NULL) {
             continue;
         }
 
         if (!pcmk__is_pacemaker_remote_node(action->node)) {
             continue;
         }
 
         /* We are only interested in real actions.
          *
          * @TODO This is probably wrong; pseudo-actions might be converted to
          * real actions and vice versa later in update_actions() at the end of
          * pcmk__apply_orderings().
          */
         if (pcmk_is_set(action->flags, pcmk_action_pseudo)) {
             continue;
         }
 
         remote = action->node->details->remote_rsc;
         if (remote == NULL) {
             // Orphaned
             continue;
         }
 
         /* Another special case: if a resource is moving to a Pacemaker Remote
          * node, order the stop on the original node after any start of the
          * remote connection. This ensures that if the connection fails to
          * start, we leave the resource running on the original node.
          */
         if (pcmk__str_eq(action->task, PCMK_ACTION_START, pcmk__str_none)) {
             for (GList *item = action->rsc->private->actions; item != NULL;
                  item = item->next) {
                 pcmk_action_t *rsc_action = item->data;
 
                 if (!pcmk__same_node(rsc_action->node, action->node)
                     && pcmk__str_eq(rsc_action->task, PCMK_ACTION_STOP,
                                     pcmk__str_none)) {
                     pcmk__new_ordering(remote, start_key(remote), NULL,
                                        action->rsc, NULL, rsc_action,
                                        pcmk__ar_ordered, scheduler);
                 }
             }
         }
 
         /* The action occurs across a remote connection, so create
          * ordering constraints that guarantee the action occurs while the node
          * is active (after start, before stop ... things like that).
          *
          * This is somewhat brittle in that we need to make sure the results of
          * this ordering are compatible with the result of get_router_node().
          * It would probably be better to add PCMK__XA_ROUTER_NODE as part of
          * this logic rather than create_graph_action().
          */
         if (remote->container) {
             crm_trace("Container ordering for %s", action->uuid);
             apply_container_ordering(action);
 
         } else {
             crm_trace("Remote ordering for %s", action->uuid);
             apply_remote_ordering(action);
         }
     }
 }
 
 /*!
  * \internal
  * \brief Check whether a node is a failed remote node
  *
  * \param[in] node  Node to check
  *
  * \return true if \p node is a failed remote node, false otherwise
  */
 bool
 pcmk__is_failed_remote_node(const pcmk_node_t *node)
 {
     return pcmk__is_remote_node(node) && (node->details->remote_rsc != NULL)
            && (get_remote_node_state(node) == remote_state_failed);
 }
 
 /*!
  * \internal
  * \brief Check whether a given resource corresponds to a given node as guest
  *
  * \param[in] rsc   Resource to check
  * \param[in] node  Node to check
  *
  * \return true if \p node is a guest node and \p rsc is its containing
  *         resource, otherwise false
  */
 bool
 pcmk__rsc_corresponds_to_guest(const pcmk_resource_t *rsc,
                                const pcmk_node_t *node)
 {
     return (rsc != NULL) && (rsc->fillers != NULL) && (node != NULL)
             && (node->details->remote_rsc != NULL)
             && (node->details->remote_rsc->container == rsc);
 }
 
 /*!
  * \internal
  * \brief Get proper connection host that a remote action must be routed through
  *
  * A remote connection resource might be starting, stopping, or migrating in the
  * same transition that an action needs to be executed on its Pacemaker Remote
  * node. Determine the proper node that the remote action should be routed
  * through.
  *
  * \param[in] action  (Potentially remote) action to route
  *
  * \return Connection host that action should be routed through if remote,
  *         otherwise NULL
  */
 pcmk_node_t *
 pcmk__connection_host_for_action(const pcmk_action_t *action)
 {
     pcmk_node_t *began_on = NULL;
     pcmk_node_t *ended_on = NULL;
     bool partial_migration = false;
     const char *task = action->task;
 
     if (pcmk__str_eq(task, PCMK_ACTION_STONITH, pcmk__str_none)
         || !pcmk__is_pacemaker_remote_node(action->node)) {
         return NULL;
     }
 
     CRM_ASSERT(action->node->details->remote_rsc != NULL);
 
     began_on = pcmk__current_node(action->node->details->remote_rsc);
-    ended_on = action->node->details->remote_rsc->allocated_to;
+    ended_on = action->node->details->remote_rsc->private->assigned_node;
     if (action->node->details->remote_rsc
         && (action->node->details->remote_rsc->container == NULL)
         && action->node->details->remote_rsc->partial_migration_target) {
         partial_migration = true;
     }
 
     if (began_on == NULL) {
         crm_trace("Routing %s for %s through remote connection's "
                   "next node %s (starting)%s",
                   action->task, (action->rsc? action->rsc->id : "no resource"),
                   (ended_on? ended_on->details->uname : "none"),
                   partial_migration? " (partial migration)" : "");
         return ended_on;
     }
 
     if (ended_on == NULL) {
         crm_trace("Routing %s for %s through remote connection's "
                   "current node %s (stopping)%s",
                   action->task, (action->rsc? action->rsc->id : "no resource"),
                   (began_on? began_on->details->uname : "none"),
                   partial_migration? " (partial migration)" : "");
         return began_on;
     }
 
     if (pcmk__same_node(began_on, ended_on)) {
         crm_trace("Routing %s for %s through remote connection's "
                   "current node %s (not moving)%s",
                   action->task, (action->rsc? action->rsc->id : "no resource"),
                   (began_on? began_on->details->uname : "none"),
                   partial_migration? " (partial migration)" : "");
         return began_on;
     }
 
     /* If we get here, the remote connection is moving during this transition.
      * This means some actions for resources behind the connection will get
      * routed through the cluster node the connection resource is currently on,
      * and others are routed through the cluster node the connection will end up
      * on.
      */
 
     if (pcmk__str_eq(task, PCMK_ACTION_NOTIFY, pcmk__str_none)) {
         task = g_hash_table_lookup(action->meta, "notify_operation");
     }
 
     /*
      * Stop, demote, and migration actions must occur before the connection can
      * move (these actions are required before the remote resource can stop). In
      * this case, we know these actions have to be routed through the initial
      * cluster node the connection resource lived on before the move takes
      * place.
      *
      * The exception is a partial migration of a (non-guest) remote connection
      * resource; in that case, all actions (even these) will be ordered after
      * the connection's pseudo-start on the migration target, so the target is
      * the router node.
      */
     if (pcmk__strcase_any_of(task, PCMK_ACTION_CANCEL, PCMK_ACTION_STOP,
                              PCMK_ACTION_DEMOTE, PCMK_ACTION_MIGRATE_FROM,
                              PCMK_ACTION_MIGRATE_TO, NULL)
         && !partial_migration) {
         crm_trace("Routing %s for %s through remote connection's "
                   "current node %s (moving)%s",
                   action->task, (action->rsc? action->rsc->id : "no resource"),
                   (began_on? began_on->details->uname : "none"),
                   partial_migration? " (partial migration)" : "");
         return began_on;
     }
 
     /* Everything else (start, promote, monitor, probe, refresh,
      * clear failcount, delete, ...) must occur after the connection starts on
      * the node it is moving to.
      */
     crm_trace("Routing %s for %s through remote connection's "
               "next node %s (moving)%s",
               action->task, (action->rsc? action->rsc->id : "no resource"),
               (ended_on? ended_on->details->uname : "none"),
               partial_migration? " (partial migration)" : "");
     return ended_on;
 }
 
 /*!
  * \internal
  * \brief Replace remote connection's addr="#uname" with actual address
  *
  * REMOTE_CONTAINER_HACK: If a given resource is a remote connection resource
  * with its "addr" parameter set to "#uname", pull the actual value from the
  * parameters evaluated without a node (which was put there earlier in
  * pcmk__create_graph() when the bundle's expand() method was called).
  *
  * \param[in,out] rsc     Resource to check
  * \param[in,out] params  Resource parameters evaluated per node
  */
 void
 pcmk__substitute_remote_addr(pcmk_resource_t *rsc, GHashTable *params)
 {
     const char *remote_addr = g_hash_table_lookup(params, PCMK_REMOTE_RA_ADDR);
 
     if (pcmk__str_eq(remote_addr, "#uname", pcmk__str_none)) {
         GHashTable *base = pe_rsc_params(rsc, NULL, rsc->private->scheduler);
 
         remote_addr = g_hash_table_lookup(base, PCMK_REMOTE_RA_ADDR);
         if (remote_addr != NULL) {
             pcmk__insert_dup(params, PCMK_REMOTE_RA_ADDR, remote_addr);
         }
     }
 }
 
 /*!
  * \brief Add special guest node meta-attributes to XML
  *
  * If a given action will be executed on a guest node, add the following as XML
  * attributes (using meta-attribute naming):
  * * The resource's \c PCMK_META_CONTAINER_ATTRIBUTE_TARGET meta-attribute
  *   (usually set only for bundles), as \c PCMK_META_CONTAINER_ATTRIBUTE_TARGET
  * * The guest's physical host (current host for "down" actions, next host for
  *   "up" actions), as \c PCMK__META_PHYSICAL_HOST
  *
  * If the guest node has no physical host, then don't add either attribute.
  *
  * \param[in,out] args_xml  XML to add attributes to
  * \param[in]     action    Action to check
  */
 void
 pcmk__add_guest_meta_to_xml(xmlNode *args_xml, const pcmk_action_t *action)
 {
     const pcmk_node_t *guest = action->node;
     const pcmk_node_t *host = NULL;
+    const pcmk_resource_t *container = NULL;
     enum action_tasks task;
 
     if (!pcmk__is_guest_or_bundle_node(guest)) {
         return;
     }
+    container = guest->details->remote_rsc->container;
 
     task = pcmk_parse_action(action->task);
     if ((task == pcmk_action_notify) || (task == pcmk_action_notified)) {
         task = pcmk_parse_action(g_hash_table_lookup(action->meta,
                                                      "notify_operation"));
     }
 
     switch (task) {
         case pcmk_action_stop:
         case pcmk_action_stopped:
         case pcmk_action_demote:
         case pcmk_action_demoted:
             // "Down" actions take place on guest's current host
-            host = pcmk__current_node(guest->details->remote_rsc->container);
+            host = pcmk__current_node(container);
             break;
 
         case pcmk_action_start:
         case pcmk_action_started:
         case pcmk_action_monitor:
         case pcmk_action_promote:
         case pcmk_action_promoted:
             // "Up" actions take place on guest's next host
-            host = guest->details->remote_rsc->container->allocated_to;
+            host = container->private->assigned_node;
             break;
 
         default:
             break;
     }
 
     if (host != NULL) {
         gpointer target =
             g_hash_table_lookup(action->rsc->meta,
                                 PCMK_META_CONTAINER_ATTRIBUTE_TARGET);
 
         hash2metafield((gpointer) PCMK_META_CONTAINER_ATTRIBUTE_TARGET,
                        target,
                        (gpointer) args_xml);
         hash2metafield((gpointer) PCMK__META_PHYSICAL_HOST,
                        (gpointer) host->details->uname,
                        (gpointer) args_xml);
     }
 }
diff --git a/lib/pacemaker/pcmk_sched_resource.c b/lib/pacemaker/pcmk_sched_resource.c
index 1ecb0d4d7b..61c1e5bd51 100644
--- a/lib/pacemaker/pcmk_sched_resource.c
+++ b/lib/pacemaker/pcmk_sched_resource.c
@@ -1,780 +1,780 @@
 /*
  * 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->private->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->private->scheduler);
             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->private->scheduler);
         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 (pcmk__str_eq(id, rsc->id, pcmk__str_none)
         || pcmk__str_eq(id, rsc->private->history_id, pcmk__str_none)) {
         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->private->cmds = &assignment_methods[rsc->private->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->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->private->scheduler->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->private->cmds->output_actions(child);
         }
         return;
     }
 
-    next = rsc->allocated_to;
+    next = rsc->private->assigned_node;
     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;
     pcmk_scheduler_t *scheduler = NULL;
 
     CRM_ASSERT(rsc != NULL);
     scheduler = rsc->private->scheduler;
 
     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);
+    if (rsc->private->assigned_node != NULL) {
+        changed = !pcmk__same_node(rsc->private->assigned_node, 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->private->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);
+    rsc->private->assigned_node = 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(scheduler->flags, pcmk_sched_show_utilization)) {
         pcmk__output_t *out = scheduler->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;
+    pcmk_node_t *old = rsc->private->assigned_node;
 
     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;
+        rsc->private->assigned_node = 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->private->ban_after_failures == 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->private->ban_after_failures - 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)"
                          QB_XS " failures=%d "
                          PCMK_META_MIGRATION_THRESHOLD "=%d",
                          rsc_to_ban->id, pcmk__node_name(node), fail_count,
                          rsc->private->ban_after_failures);
         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->private->ban_after_failures);
     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->private->priority;
     r2_score = resource2->private->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->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->private->scheduler);
     pe__show_node_scores(true, NULL, resource2->id, r2_nodes,
                          resource2->private->scheduler);
 
     // 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/pengine/bundle.c b/lib/pengine/bundle.c
index 29e06c902c..dd25cfbf69 100644
--- a/lib/pengine/bundle.c
+++ b/lib/pengine/bundle.c
@@ -1,2139 +1,2139 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <ctype.h>
 #include <stdint.h>
 
 #include <crm/pengine/rules.h>
 #include <crm/pengine/status.h>
 #include <crm/pengine/internal.h>
 #include <crm/common/xml.h>
 #include <crm/common/output.h>
 #include <crm/common/xml_internal.h>
 #include <pe_status_private.h>
 
 enum pe__bundle_mount_flags {
     pe__bundle_mount_none       = 0x00,
 
     // mount instance-specific subdirectory rather than source directly
     pe__bundle_mount_subdir     = 0x01
 };
 
 typedef struct {
     char *source;
     char *target;
     char *options;
     uint32_t flags; // bitmask of pe__bundle_mount_flags
 } pe__bundle_mount_t;
 
 typedef struct {
     char *source;
     char *target;
 } pe__bundle_port_t;
 
 enum pe__container_agent {
     PE__CONTAINER_AGENT_UNKNOWN,
     PE__CONTAINER_AGENT_DOCKER,
     PE__CONTAINER_AGENT_RKT,
     PE__CONTAINER_AGENT_PODMAN,
 };
 
 #define PE__CONTAINER_AGENT_UNKNOWN_S "unknown"
 #define PE__CONTAINER_AGENT_DOCKER_S  "docker"
 #define PE__CONTAINER_AGENT_RKT_S     "rkt"
 #define PE__CONTAINER_AGENT_PODMAN_S  "podman"
 
 typedef struct pe__bundle_variant_data_s {
         int promoted_max;
         int nreplicas;
         int nreplicas_per_host;
         char *prefix;
         char *image;
         const char *ip_last;
         char *host_network;
         char *host_netmask;
         char *control_port;
         char *container_network;
         char *ip_range_start;
         gboolean add_host;
         gchar *container_host_options;
         char *container_command;
         char *launcher_options;
         const char *attribute_target;
 
         pcmk_resource_t *child;
 
         GList *replicas;    // pcmk__bundle_replica_t *
         GList *ports;       // pe__bundle_port_t *
         GList *mounts;      // pe__bundle_mount_t *
 
         enum pe__container_agent agent_type;
 } pe__bundle_variant_data_t;
 
 #define get_bundle_variant_data(data, rsc) do { \
         CRM_ASSERT(pcmk__is_bundle(rsc));       \
         data = rsc->private->variant_opaque;    \
     } while (0)
 
 /*!
  * \internal
  * \brief Get maximum number of bundle replicas allowed to run
  *
  * \param[in] rsc  Bundle or bundled resource to check
  *
  * \return Maximum replicas for bundle corresponding to \p rsc
  */
 int
 pe__bundle_max(const pcmk_resource_t *rsc)
 {
     const pe__bundle_variant_data_t *bundle_data = NULL;
 
     get_bundle_variant_data(bundle_data, pe__const_top_resource(rsc, true));
     return bundle_data->nreplicas;
 }
 
 /*!
  * \internal
  * \brief Get the resource inside a bundle
  *
  * \param[in] bundle  Bundle to check
  *
  * \return Resource inside \p bundle if any, otherwise NULL
  */
 pcmk_resource_t *
 pe__bundled_resource(const pcmk_resource_t *rsc)
 {
     const pe__bundle_variant_data_t *bundle_data = NULL;
 
     get_bundle_variant_data(bundle_data, pe__const_top_resource(rsc, true));
     return bundle_data->child;
 }
 
 /*!
  * \internal
  * \brief Get containerized resource corresponding to a given bundle container
  *
  * \param[in] instance  Collective instance that might be a bundle container
  *
  * \return Bundled resource instance inside \p instance if it is a bundle
  *         container instance, otherwise NULL
  */
 const pcmk_resource_t *
 pe__get_rsc_in_container(const pcmk_resource_t *instance)
 {
     const pe__bundle_variant_data_t *data = NULL;
     const pcmk_resource_t *top = pe__const_top_resource(instance, true);
 
     if (!pcmk__is_bundle(top)) {
         return NULL;
     }
     get_bundle_variant_data(data, top);
 
     for (const GList *iter = data->replicas; iter != NULL; iter = iter->next) {
         const pcmk__bundle_replica_t *replica = iter->data;
 
         if (instance == replica->container) {
             return replica->child;
         }
     }
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Check whether a given node is created by a bundle
  *
  * \param[in] bundle  Bundle resource to check
  * \param[in] node    Node to check
  *
  * \return true if \p node is an instance of \p bundle, otherwise false
  */
 bool
 pe__node_is_bundle_instance(const pcmk_resource_t *bundle,
                             const pcmk_node_t *node)
 {
     pe__bundle_variant_data_t *bundle_data = NULL;
 
     get_bundle_variant_data(bundle_data, bundle);
     for (GList *iter = bundle_data->replicas; iter != NULL; iter = iter->next) {
         pcmk__bundle_replica_t *replica = iter->data;
 
         if (pcmk__same_node(node, replica->node)) {
             return true;
         }
     }
     return false;
 }
 
 /*!
  * \internal
  * \brief Get the container of a bundle's first replica
  *
  * \param[in] bundle  Bundle resource to get container for
  *
  * \return Container resource from first replica of \p bundle if any,
  *         otherwise NULL
  */
 pcmk_resource_t *
 pe__first_container(const pcmk_resource_t *bundle)
 {
     const pe__bundle_variant_data_t *bundle_data = NULL;
     const pcmk__bundle_replica_t *replica = NULL;
 
     get_bundle_variant_data(bundle_data, bundle);
     if (bundle_data->replicas == NULL) {
         return NULL;
     }
     replica = bundle_data->replicas->data;
     return replica->container;
 }
 
 /*!
  * \internal
  * \brief Iterate over bundle replicas
  *
  * \param[in,out] bundle     Bundle to iterate over
  * \param[in]     fn         Function to call for each replica (its return value
  *                           indicates whether to continue iterating)
  * \param[in,out] user_data  Pointer to pass to \p fn
  */
 void
 pe__foreach_bundle_replica(pcmk_resource_t *bundle,
                            bool (*fn)(pcmk__bundle_replica_t *, void *),
                            void *user_data)
 {
     const pe__bundle_variant_data_t *bundle_data = NULL;
 
     get_bundle_variant_data(bundle_data, bundle);
     for (GList *iter = bundle_data->replicas; iter != NULL; iter = iter->next) {
         if (!fn((pcmk__bundle_replica_t *) iter->data, user_data)) {
             break;
         }
     }
 }
 
 /*!
  * \internal
  * \brief Iterate over const bundle replicas
  *
  * \param[in]     bundle     Bundle to iterate over
  * \param[in]     fn         Function to call for each replica (its return value
  *                           indicates whether to continue iterating)
  * \param[in,out] user_data  Pointer to pass to \p fn
  */
 void
 pe__foreach_const_bundle_replica(const pcmk_resource_t *bundle,
                                  bool (*fn)(const pcmk__bundle_replica_t *,
                                             void *),
                                  void *user_data)
 {
     const pe__bundle_variant_data_t *bundle_data = NULL;
 
     get_bundle_variant_data(bundle_data, bundle);
     for (const GList *iter = bundle_data->replicas; iter != NULL;
          iter = iter->next) {
 
         if (!fn((const pcmk__bundle_replica_t *) iter->data, user_data)) {
             break;
         }
     }
 }
 
 static char *
 next_ip(const char *last_ip)
 {
     unsigned int oct1 = 0;
     unsigned int oct2 = 0;
     unsigned int oct3 = 0;
     unsigned int oct4 = 0;
     int rc = sscanf(last_ip, "%u.%u.%u.%u", &oct1, &oct2, &oct3, &oct4);
 
     if (rc != 4) {
         /*@ TODO check for IPv6 */
         return NULL;
 
     } else if (oct3 > 253) {
         return NULL;
 
     } else if (oct4 > 253) {
         ++oct3;
         oct4 = 1;
 
     } else {
         ++oct4;
     }
 
     return crm_strdup_printf("%u.%u.%u.%u", oct1, oct2, oct3, oct4);
 }
 
 static void
 allocate_ip(pe__bundle_variant_data_t *data, pcmk__bundle_replica_t *replica,
             GString *buffer)
 {
     if(data->ip_range_start == NULL) {
         return;
 
     } else if(data->ip_last) {
         replica->ipaddr = next_ip(data->ip_last);
 
     } else {
         replica->ipaddr = strdup(data->ip_range_start);
     }
 
     data->ip_last = replica->ipaddr;
     switch (data->agent_type) {
         case PE__CONTAINER_AGENT_DOCKER:
         case PE__CONTAINER_AGENT_PODMAN:
             if (data->add_host) {
                 g_string_append_printf(buffer, " --add-host=%s-%d:%s",
                                        data->prefix, replica->offset,
                                        replica->ipaddr);
             } else {
                 g_string_append_printf(buffer, " --hosts-entry=%s=%s-%d",
                                        replica->ipaddr, data->prefix,
                                        replica->offset);
             }
             break;
 
         case PE__CONTAINER_AGENT_RKT:
             g_string_append_printf(buffer, " --hosts-entry=%s=%s-%d",
                                    replica->ipaddr, data->prefix,
                                    replica->offset);
             break;
 
         default: // PE__CONTAINER_AGENT_UNKNOWN
             break;
     }
 }
 
 static xmlNode *
 create_resource(const char *name, const char *provider, const char *kind)
 {
     xmlNode *rsc = pcmk__xe_create(NULL, PCMK_XE_PRIMITIVE);
 
     crm_xml_add(rsc, PCMK_XA_ID, name);
     crm_xml_add(rsc, PCMK_XA_CLASS, PCMK_RESOURCE_CLASS_OCF);
     crm_xml_add(rsc, PCMK_XA_PROVIDER, provider);
     crm_xml_add(rsc, PCMK_XA_TYPE, kind);
 
     return rsc;
 }
 
 /*!
  * \internal
  * \brief Check whether cluster can manage resource inside container
  *
  * \param[in,out] data  Container variant data
  *
  * \return TRUE if networking configuration is acceptable, FALSE otherwise
  *
  * \note The resource is manageable if an IP range or control port has been
  *       specified. If a control port is used without an IP range, replicas per
  *       host must be 1.
  */
 static bool
 valid_network(pe__bundle_variant_data_t *data)
 {
     if(data->ip_range_start) {
         return TRUE;
     }
     if(data->control_port) {
         if(data->nreplicas_per_host > 1) {
             pcmk__config_err("Specifying the '" PCMK_XA_CONTROL_PORT "' for %s "
                              "requires '" PCMK_XA_REPLICAS_PER_HOST "=1'",
                              data->prefix);
             data->nreplicas_per_host = 1;
             // @TODO to be sure:
             // pcmk__clear_rsc_flags(rsc, pcmk__rsc_unique);
         }
         return TRUE;
     }
     return FALSE;
 }
 
 static int
 create_ip_resource(pcmk_resource_t *parent, pe__bundle_variant_data_t *data,
                    pcmk__bundle_replica_t *replica)
 {
     if(data->ip_range_start) {
         char *id = NULL;
         xmlNode *xml_ip = NULL;
         xmlNode *xml_obj = NULL;
 
         id = crm_strdup_printf("%s-ip-%s", data->prefix, replica->ipaddr);
         pcmk__xml_sanitize_id(id);
         xml_ip = create_resource(id, "heartbeat", "IPaddr2");
         free(id);
 
         xml_obj = pcmk__xe_create(xml_ip, PCMK_XE_INSTANCE_ATTRIBUTES);
         pcmk__xe_set_id(xml_obj, "%s-attributes-%d",
                         data->prefix, replica->offset);
 
         crm_create_nvpair_xml(xml_obj, NULL, "ip", replica->ipaddr);
         if(data->host_network) {
             crm_create_nvpair_xml(xml_obj, NULL, "nic", data->host_network);
         }
 
         if(data->host_netmask) {
             crm_create_nvpair_xml(xml_obj, NULL,
                                   "cidr_netmask", data->host_netmask);
 
         } else {
             crm_create_nvpair_xml(xml_obj, NULL, "cidr_netmask", "32");
         }
 
         xml_obj = pcmk__xe_create(xml_ip, PCMK_XE_OPERATIONS);
         crm_create_op_xml(xml_obj, pcmk__xe_id(xml_ip), PCMK_ACTION_MONITOR,
                           "60s", NULL);
 
         // TODO: Other ops? Timeouts and intervals from underlying resource?
 
         if (pe__unpack_resource(xml_ip, &replica->ip, parent,
                                 parent->private->scheduler) != pcmk_rc_ok) {
             return pcmk_rc_unpack_error;
         }
 
         parent->children = g_list_append(parent->children, replica->ip);
     }
     return pcmk_rc_ok;
 }
 
 static const char*
 container_agent_str(enum pe__container_agent t)
 {
     switch (t) {
         case PE__CONTAINER_AGENT_DOCKER: return PE__CONTAINER_AGENT_DOCKER_S;
         case PE__CONTAINER_AGENT_RKT:    return PE__CONTAINER_AGENT_RKT_S;
         case PE__CONTAINER_AGENT_PODMAN: return PE__CONTAINER_AGENT_PODMAN_S;
         default: // PE__CONTAINER_AGENT_UNKNOWN
             break;
     }
     return PE__CONTAINER_AGENT_UNKNOWN_S;
 }
 
 static int
 create_container_resource(pcmk_resource_t *parent,
                           const pe__bundle_variant_data_t *data,
                           pcmk__bundle_replica_t *replica)
 {
     char *id = NULL;
     xmlNode *xml_container = NULL;
     xmlNode *xml_obj = NULL;
 
     // Agent-specific
     const char *hostname_opt = NULL;
     const char *env_opt = NULL;
     const char *agent_str = NULL;
     int volid = 0;  // rkt-only
 
     GString *buffer = NULL;
     GString *dbuffer = NULL;
 
     // Where syntax differences are drop-in replacements, set them now
     switch (data->agent_type) {
         case PE__CONTAINER_AGENT_DOCKER:
         case PE__CONTAINER_AGENT_PODMAN:
             hostname_opt = "-h ";
             env_opt = "-e ";
             break;
         case PE__CONTAINER_AGENT_RKT:
             hostname_opt = "--hostname=";
             env_opt = "--environment=";
             break;
         default:    // PE__CONTAINER_AGENT_UNKNOWN
             return pcmk_rc_unpack_error;
     }
     agent_str = container_agent_str(data->agent_type);
 
     buffer = g_string_sized_new(4096);
 
     id = crm_strdup_printf("%s-%s-%d", data->prefix, agent_str,
                            replica->offset);
     pcmk__xml_sanitize_id(id);
     xml_container = create_resource(id, "heartbeat", agent_str);
     free(id);
 
     xml_obj = pcmk__xe_create(xml_container, PCMK_XE_INSTANCE_ATTRIBUTES);
     pcmk__xe_set_id(xml_obj, "%s-attributes-%d", data->prefix, replica->offset);
 
     crm_create_nvpair_xml(xml_obj, NULL, "image", data->image);
     crm_create_nvpair_xml(xml_obj, NULL, "allow_pull", PCMK_VALUE_TRUE);
     crm_create_nvpair_xml(xml_obj, NULL, "force_kill", PCMK_VALUE_FALSE);
     crm_create_nvpair_xml(xml_obj, NULL, "reuse", PCMK_VALUE_FALSE);
 
     if (data->agent_type == PE__CONTAINER_AGENT_DOCKER) {
         g_string_append(buffer, " --restart=no");
     }
 
     /* Set a container hostname only if we have an IP to map it to. The user can
      * set -h or --uts=host themselves if they want a nicer name for logs, but
      * this makes applications happy who need their  hostname to match the IP
      * they bind to.
      */
     if (data->ip_range_start != NULL) {
         g_string_append_printf(buffer, " %s%s-%d", hostname_opt, data->prefix,
                                replica->offset);
     }
     pcmk__g_strcat(buffer, " ", env_opt, "PCMK_stderr=1", NULL);
 
     if (data->container_network != NULL) {
         pcmk__g_strcat(buffer, " --net=", data->container_network, NULL);
     }
 
     if (data->control_port != NULL) {
         pcmk__g_strcat(buffer, " ", env_opt, "PCMK_" PCMK__ENV_REMOTE_PORT "=",
                        data->control_port, NULL);
     } else {
         g_string_append_printf(buffer, " %sPCMK_" PCMK__ENV_REMOTE_PORT "=%d",
                                env_opt, DEFAULT_REMOTE_PORT);
     }
 
     for (GList *iter = data->mounts; iter != NULL; iter = iter->next) {
         pe__bundle_mount_t *mount = (pe__bundle_mount_t *) iter->data;
         char *source = NULL;
 
         if (pcmk_is_set(mount->flags, pe__bundle_mount_subdir)) {
             source = crm_strdup_printf("%s/%s-%d", mount->source, data->prefix,
                                        replica->offset);
             pcmk__add_separated_word(&dbuffer, 1024, source, ",");
         }
 
         switch (data->agent_type) {
             case PE__CONTAINER_AGENT_DOCKER:
             case PE__CONTAINER_AGENT_PODMAN:
                 pcmk__g_strcat(buffer,
                                " -v ", pcmk__s(source, mount->source),
                                ":", mount->target, NULL);
 
                 if (mount->options != NULL) {
                     pcmk__g_strcat(buffer, ":", mount->options, NULL);
                 }
                 break;
             case PE__CONTAINER_AGENT_RKT:
                 g_string_append_printf(buffer,
                                        " --volume vol%d,kind=host,"
                                        "source=%s%s%s "
                                        "--mount volume=vol%d,target=%s",
                                        volid, pcmk__s(source, mount->source),
                                        (mount->options != NULL)? "," : "",
                                        pcmk__s(mount->options, ""),
                                        volid, mount->target);
                 volid++;
                 break;
             default:
                 break;
         }
         free(source);
     }
 
     for (GList *iter = data->ports; iter != NULL; iter = iter->next) {
         pe__bundle_port_t *port = (pe__bundle_port_t *) iter->data;
 
         switch (data->agent_type) {
             case PE__CONTAINER_AGENT_DOCKER:
             case PE__CONTAINER_AGENT_PODMAN:
                 if (replica->ipaddr != NULL) {
                     pcmk__g_strcat(buffer,
                                    " -p ", replica->ipaddr, ":", port->source,
                                    ":", port->target, NULL);
 
                 } else if (!pcmk__str_eq(data->container_network,
                                          PCMK_VALUE_HOST, pcmk__str_none)) {
                     // No need to do port mapping if net == host
                     pcmk__g_strcat(buffer,
                                    " -p ", port->source, ":", port->target,
                                    NULL);
                 }
                 break;
             case PE__CONTAINER_AGENT_RKT:
                 if (replica->ipaddr != NULL) {
                     pcmk__g_strcat(buffer,
                                    " --port=", port->target,
                                    ":", replica->ipaddr, ":", port->source,
                                    NULL);
                 } else {
                     pcmk__g_strcat(buffer,
                                    " --port=", port->target, ":", port->source,
                                    NULL);
                 }
                 break;
             default:
                 break;
         }
     }
 
     /* @COMPAT: We should use pcmk__add_word() here, but we can't yet, because
      * it would cause restarts during rolling upgrades.
      *
      * In a previous version of the container resource creation logic, if
      * data->launcher_options is not NULL, we append
      * (" %s", data->launcher_options) even if data->launcher_options is an
      * empty string. Likewise for data->container_host_options. Using
      *
      *     pcmk__add_word(buffer, 0, data->launcher_options)
      *
      * removes that extra trailing space, causing a resource definition change.
      */
     if (data->launcher_options != NULL) {
         pcmk__g_strcat(buffer, " ", data->launcher_options, NULL);
     }
 
     if (data->container_host_options != NULL) {
         pcmk__g_strcat(buffer, " ", data->container_host_options, NULL);
     }
 
     crm_create_nvpair_xml(xml_obj, NULL, "run_opts",
                           (const char *) buffer->str);
     g_string_free(buffer, TRUE);
 
     crm_create_nvpair_xml(xml_obj, NULL, "mount_points",
                           (dbuffer != NULL)? (const char *) dbuffer->str : "");
     if (dbuffer != NULL) {
         g_string_free(dbuffer, TRUE);
     }
 
     if (replica->child != NULL) {
         if (data->container_command != NULL) {
             crm_create_nvpair_xml(xml_obj, NULL, "run_cmd",
                                   data->container_command);
         } else {
             crm_create_nvpair_xml(xml_obj, NULL, "run_cmd",
                                   SBIN_DIR "/pacemaker-remoted");
         }
 
         /* TODO: Allow users to specify their own?
          *
          * We just want to know if the container is alive; we'll monitor the
          * child independently.
          */
         crm_create_nvpair_xml(xml_obj, NULL, "monitor_cmd", "/bin/true");
 #if 0
         /* @TODO Consider supporting the use case where we can start and stop
          * resources, but not proxy local commands (such as setting node
          * attributes), by running the local executor in stand-alone mode.
          * However, this would probably be better done via ACLs as with other
          * Pacemaker Remote nodes.
          */
     } else if ((child != NULL) && data->untrusted) {
         crm_create_nvpair_xml(xml_obj, NULL, "run_cmd",
                               CRM_DAEMON_DIR "/pacemaker-execd");
         crm_create_nvpair_xml(xml_obj, NULL, "monitor_cmd",
                               CRM_DAEMON_DIR "/pacemaker/cts-exec-helper -c poke");
 #endif
     } else {
         if (data->container_command != NULL) {
             crm_create_nvpair_xml(xml_obj, NULL, "run_cmd",
                                   data->container_command);
         }
 
         /* TODO: Allow users to specify their own?
          *
          * We don't know what's in the container, so we just want to know if it
          * is alive.
          */
         crm_create_nvpair_xml(xml_obj, NULL, "monitor_cmd", "/bin/true");
     }
 
     xml_obj = pcmk__xe_create(xml_container, PCMK_XE_OPERATIONS);
     crm_create_op_xml(xml_obj, pcmk__xe_id(xml_container), PCMK_ACTION_MONITOR,
                       "60s", NULL);
 
     // TODO: Other ops? Timeouts and intervals from underlying resource?
     if (pe__unpack_resource(xml_container, &replica->container, parent,
                             parent->private->scheduler) != pcmk_rc_ok) {
         return pcmk_rc_unpack_error;
     }
     pcmk__set_rsc_flags(replica->container, pcmk__rsc_replica_container);
     parent->children = g_list_append(parent->children, replica->container);
 
     return pcmk_rc_ok;
 }
 
 /*!
  * \brief Ban a node from a resource's (and its children's) allowed nodes list
  *
  * \param[in,out] rsc    Resource to modify
  * \param[in]     uname  Name of node to ban
  */
 static void
 disallow_node(pcmk_resource_t *rsc, const char *uname)
 {
     gpointer match = g_hash_table_lookup(rsc->allowed_nodes, uname);
 
     if (match) {
         ((pcmk_node_t *) match)->weight = -PCMK_SCORE_INFINITY;
         ((pcmk_node_t *) match)->rsc_discover_mode = pcmk_probe_never;
     }
     if (rsc->children) {
         g_list_foreach(rsc->children, (GFunc) disallow_node, (gpointer) uname);
     }
 }
 
 static int
 create_remote_resource(pcmk_resource_t *parent, pe__bundle_variant_data_t *data,
                        pcmk__bundle_replica_t *replica)
 {
     if (replica->child && valid_network(data)) {
         GHashTableIter gIter;
         pcmk_node_t *node = NULL;
         xmlNode *xml_remote = NULL;
         char *id = crm_strdup_printf("%s-%d", data->prefix, replica->offset);
         char *port_s = NULL;
         const char *uname = NULL;
         const char *connect_name = NULL;
 
         if (pe_find_resource(parent->private->scheduler->resources,
                              id) != NULL) {
             free(id);
             // The biggest hammer we have
             id = crm_strdup_printf("pcmk-internal-%s-remote-%d",
                                    replica->child->id, replica->offset);
             //@TODO return error instead of asserting?
             CRM_ASSERT(pe_find_resource(parent->private->scheduler->resources,
                                         id) == NULL);
         }
 
         /* REMOTE_CONTAINER_HACK: Using "#uname" as the server name when the
          * connection does not have its own IP is a magic string that we use to
          * support nested remotes (i.e. a bundle running on a remote node).
          */
         connect_name = (replica->ipaddr? replica->ipaddr : "#uname");
 
         if (data->control_port == NULL) {
             port_s = pcmk__itoa(DEFAULT_REMOTE_PORT);
         }
 
         /* This sets replica->container as replica->remote's container, which is
          * similar to what happens with guest nodes. This is how the scheduler
          * knows that the bundle node is fenced by recovering the container, and
          * that remote should be ordered relative to the container.
          */
         xml_remote = pe_create_remote_xml(NULL, id, replica->container->id,
                                           NULL, NULL, NULL,
                                           connect_name, (data->control_port?
                                           data->control_port : port_s));
         free(port_s);
 
         /* Abandon our created ID, and pull the copy from the XML, because we
          * need something that will get freed during scheduler data cleanup to
          * use as the node ID and uname.
          */
         free(id);
         id = NULL;
         uname = pcmk__xe_id(xml_remote);
 
         /* Ensure a node has been created for the guest (it may have already
          * been, if it has a permanent node attribute), and ensure its weight is
          * -INFINITY so no other resources can run on it.
          */
         node = pcmk_find_node(parent->private->scheduler, uname);
         if (node == NULL) {
             node = pe_create_node(uname, uname, PCMK_VALUE_REMOTE,
                                   PCMK_VALUE_MINUS_INFINITY,
                                   parent->private->scheduler);
         } else {
             node->weight = -PCMK_SCORE_INFINITY;
         }
         node->rsc_discover_mode = pcmk_probe_never;
 
         /* unpack_remote_nodes() ensures that each remote node and guest node
          * has a pcmk_node_t entry. Ideally, it would do the same for bundle
          * nodes. Unfortunately, a bundle has to be mostly unpacked before it's
          * obvious what nodes will be needed, so we do it just above.
          *
          * Worse, that means that the node may have been utilized while
          * unpacking other resources, without our weight correction. The most
          * likely place for this to happen is when pe__unpack_resource() calls
          * resource_location() to set a default score in symmetric clusters.
          * This adds a node *copy* to each resource's allowed nodes, and these
          * copies will have the wrong weight.
          *
          * As a hacky workaround, fix those copies here.
          *
          * @TODO Possible alternative: ensure bundles are unpacked before other
          * resources, so the weight is correct before any copies are made.
          */
         g_list_foreach(parent->private->scheduler->resources,
                        (GFunc) disallow_node, (gpointer) uname);
 
         replica->node = pe__copy_node(node);
         replica->node->weight = 500;
         replica->node->rsc_discover_mode = pcmk_probe_exclusive;
 
         /* Ensure the node shows up as allowed and with the correct discovery set */
         if (replica->child->allowed_nodes != NULL) {
             g_hash_table_destroy(replica->child->allowed_nodes);
         }
         replica->child->allowed_nodes = pcmk__strkey_table(NULL, free);
         g_hash_table_insert(replica->child->allowed_nodes,
                             (gpointer) replica->node->details->id,
                             pe__copy_node(replica->node));
 
         {
             pcmk_node_t *copy = pe__copy_node(replica->node);
             copy->weight = -PCMK_SCORE_INFINITY;
             g_hash_table_insert(replica->child->private->parent->allowed_nodes,
                                 (gpointer) replica->node->details->id, copy);
         }
         if (pe__unpack_resource(xml_remote, &replica->remote, parent,
                                 parent->private->scheduler) != pcmk_rc_ok) {
             return pcmk_rc_unpack_error;
         }
 
         g_hash_table_iter_init(&gIter, replica->remote->allowed_nodes);
         while (g_hash_table_iter_next(&gIter, NULL, (void **)&node)) {
             if (pcmk__is_pacemaker_remote_node(node)) {
                 /* Remote resources can only run on 'normal' cluster node */
                 node->weight = -PCMK_SCORE_INFINITY;
             }
         }
 
         replica->node->details->remote_rsc = replica->remote;
 
         // Ensure pcmk__is_guest_or_bundle_node() functions correctly
         replica->remote->container = replica->container;
 
         /* A bundle's #kind is closer to "container" (guest node) than the
          * "remote" set by pe_create_node().
          */
         pcmk__insert_dup(replica->node->details->attrs,
                          CRM_ATTR_KIND, "container");
 
         /* One effect of this is that setup_container() will add
          * replica->remote to replica->container's fillers, which will make
          * pe__resource_contains_guest_node() true for replica->container.
          *
          * replica->child does NOT get added to replica->container's fillers.
          * The only noticeable effect if it did would be for its fail count to
          * be taken into account when checking replica->container's migration
          * threshold.
          */
         parent->children = g_list_append(parent->children, replica->remote);
     }
     return pcmk_rc_ok;
 }
 
 static int
 create_replica_resources(pcmk_resource_t *parent,
                          pe__bundle_variant_data_t *data,
                          pcmk__bundle_replica_t *replica)
 {
     int rc = pcmk_rc_ok;
 
     rc = create_container_resource(parent, data, replica);
     if (rc != pcmk_rc_ok) {
         return rc;
     }
 
     rc = create_ip_resource(parent, data, replica);
     if (rc != pcmk_rc_ok) {
         return rc;
     }
 
     rc = create_remote_resource(parent, data, replica);
     if (rc != pcmk_rc_ok) {
         return rc;
     }
 
     if ((replica->child != NULL) && (replica->ipaddr != NULL)) {
         pcmk__insert_meta(replica->child, "external-ip", replica->ipaddr);
     }
 
     if (replica->remote != NULL) {
         /*
          * Allow the remote connection resource to be allocated to a
          * different node than the one on which the container is active.
          *
          * This makes it possible to have Pacemaker Remote nodes running
          * containers with pacemaker-remoted inside in order to start
          * services inside those containers.
          */
         pcmk__set_rsc_flags(replica->remote, pcmk__rsc_remote_nesting_allowed);
     }
     return rc;
 }
 
 static void
 mount_add(pe__bundle_variant_data_t *bundle_data, const char *source,
           const char *target, const char *options, uint32_t flags)
 {
     pe__bundle_mount_t *mount = pcmk__assert_alloc(1,
                                                    sizeof(pe__bundle_mount_t));
 
     mount->source = pcmk__str_copy(source);
     mount->target = pcmk__str_copy(target);
     mount->options = pcmk__str_copy(options);
     mount->flags = flags;
     bundle_data->mounts = g_list_append(bundle_data->mounts, mount);
 }
 
 static void
 mount_free(pe__bundle_mount_t *mount)
 {
     free(mount->source);
     free(mount->target);
     free(mount->options);
     free(mount);
 }
 
 static void
 port_free(pe__bundle_port_t *port)
 {
     free(port->source);
     free(port->target);
     free(port);
 }
 
 static pcmk__bundle_replica_t *
 replica_for_remote(pcmk_resource_t *remote)
 {
     pcmk_resource_t *top = remote;
     pe__bundle_variant_data_t *bundle_data = NULL;
 
     if (top == NULL) {
         return NULL;
     }
     while (top->private->parent != NULL) {
         top = top->private->parent;
     }
 
     get_bundle_variant_data(bundle_data, top);
     for (GList *gIter = bundle_data->replicas; gIter != NULL;
          gIter = gIter->next) {
         pcmk__bundle_replica_t *replica = gIter->data;
 
         if (replica->remote == remote) {
             return replica;
         }
     }
     CRM_LOG_ASSERT(FALSE);
     return NULL;
 }
 
 bool
 pe__bundle_needs_remote_name(pcmk_resource_t *rsc)
 {
     const char *value;
     GHashTable *params = NULL;
 
     if (rsc == NULL) {
         return false;
     }
 
     // Use NULL node since pcmk__bundle_expand() uses that to set value
     params = pe_rsc_params(rsc, NULL, rsc->private->scheduler);
     value = g_hash_table_lookup(params, PCMK_REMOTE_RA_ADDR);
 
     return pcmk__str_eq(value, "#uname", pcmk__str_casei)
            && xml_contains_remote_node(rsc->private->xml);
 }
 
 const char *
 pe__add_bundle_remote_name(pcmk_resource_t *rsc, xmlNode *xml,
                            const char *field)
 {
     // REMOTE_CONTAINER_HACK: Allow remote nodes that start containers with pacemaker remote inside
 
     pcmk_node_t *node = NULL;
     pcmk__bundle_replica_t *replica = NULL;
 
     if (!pe__bundle_needs_remote_name(rsc)) {
         return NULL;
     }
 
     replica = replica_for_remote(rsc);
     if (replica == NULL) {
         return NULL;
     }
 
-    node = replica->container->allocated_to;
+    node = replica->container->private->assigned_node;
     if (node == NULL) {
         /* If it won't be running anywhere after the
          * transition, go with where it's running now.
          */
         node = pcmk__current_node(replica->container);
     }
 
     if(node == NULL) {
         crm_trace("Cannot determine address for bundle connection %s", rsc->id);
         return NULL;
     }
 
     crm_trace("Setting address for bundle connection %s to bundle host %s",
               rsc->id, pcmk__node_name(node));
     if(xml != NULL && field != NULL) {
         crm_xml_add(xml, field, node->details->uname);
     }
 
     return node->details->uname;
 }
 
 #define pe__set_bundle_mount_flags(mount_xml, flags, flags_to_set) do {     \
         flags = pcmk__set_flags_as(__func__, __LINE__, LOG_TRACE,           \
                                    "Bundle mount", pcmk__xe_id(mount_xml),  \
                                    flags, (flags_to_set), #flags_to_set);   \
     } while (0)
 
 gboolean
 pe__unpack_bundle(pcmk_resource_t *rsc, pcmk_scheduler_t *scheduler)
 {
     const char *value = NULL;
     xmlNode *xml_obj = NULL;
     const xmlNode *xml_child = NULL;
     xmlNode *xml_resource = NULL;
     pe__bundle_variant_data_t *bundle_data = NULL;
     bool need_log_mount = TRUE;
 
     CRM_ASSERT(rsc != NULL);
     pcmk__rsc_trace(rsc, "Processing resource %s...", rsc->id);
 
     bundle_data = pcmk__assert_alloc(1, sizeof(pe__bundle_variant_data_t));
     rsc->private->variant_opaque = bundle_data;
     bundle_data->prefix = strdup(rsc->id);
 
     xml_obj = pcmk__xe_first_child(rsc->private->xml, PCMK_XE_DOCKER, NULL,
                                    NULL);
     if (xml_obj != NULL) {
         bundle_data->agent_type = PE__CONTAINER_AGENT_DOCKER;
     } else {
         xml_obj = pcmk__xe_first_child(rsc->private->xml, PCMK__XE_RKT, NULL,
                                        NULL);
         if (xml_obj != NULL) {
             pcmk__warn_once(pcmk__wo_rkt,
                             "Support for " PCMK__XE_RKT " in bundles "
                             "(such as %s) is deprecated and will be "
                             "removed in a future release", rsc->id);
             bundle_data->agent_type = PE__CONTAINER_AGENT_RKT;
         } else {
             xml_obj = pcmk__xe_first_child(rsc->private->xml, PCMK_XE_PODMAN,
                                            NULL, NULL);
             if (xml_obj != NULL) {
                 bundle_data->agent_type = PE__CONTAINER_AGENT_PODMAN;
             } else {
                 return FALSE;
             }
         }
     }
 
     // Use 0 for default, minimum, and invalid PCMK_XA_PROMOTED_MAX
     value = crm_element_value(xml_obj, PCMK_XA_PROMOTED_MAX);
     if (value == NULL) {
         // @COMPAT deprecated since 2.0.0
         value = crm_element_value(xml_obj, PCMK__XA_PROMOTED_MAX_LEGACY);
 
         if (value != NULL) {
             pcmk__warn_once(pcmk__wo_bundle_master,
                             "Support for the " PCMK__XA_PROMOTED_MAX_LEGACY
                             " attribute (such as in %s) is deprecated and "
                             "will be removed in a future release. Use "
                             PCMK_XA_PROMOTED_MAX " instead.",
                             rsc->id);
         }
     }
     pcmk__scan_min_int(value, &bundle_data->promoted_max, 0);
 
     /* Default replicas to PCMK_XA_PROMOTED_MAX if it was specified and 1
      * otherwise
      */
     value = crm_element_value(xml_obj, PCMK_XA_REPLICAS);
     if ((value == NULL) && (bundle_data->promoted_max > 0)) {
         bundle_data->nreplicas = bundle_data->promoted_max;
     } else {
         pcmk__scan_min_int(value, &bundle_data->nreplicas, 1);
     }
 
     /*
      * Communication between containers on the same host via the
      * floating IPs only works if the container is started with:
      *   --userland-proxy=false --ip-masq=false
      */
     value = crm_element_value(xml_obj, PCMK_XA_REPLICAS_PER_HOST);
     pcmk__scan_min_int(value, &bundle_data->nreplicas_per_host, 1);
     if (bundle_data->nreplicas_per_host == 1) {
         pcmk__clear_rsc_flags(rsc, pcmk__rsc_unique);
     }
 
     bundle_data->container_command =
         crm_element_value_copy(xml_obj, PCMK_XA_RUN_COMMAND);
     bundle_data->launcher_options = crm_element_value_copy(xml_obj,
                                                            PCMK_XA_OPTIONS);
     bundle_data->image = crm_element_value_copy(xml_obj, PCMK_XA_IMAGE);
     bundle_data->container_network = crm_element_value_copy(xml_obj,
                                                             PCMK_XA_NETWORK);
 
     xml_obj = pcmk__xe_first_child(rsc->private->xml, PCMK_XE_NETWORK, NULL,
                                    NULL);
     if(xml_obj) {
         bundle_data->ip_range_start =
             crm_element_value_copy(xml_obj, PCMK_XA_IP_RANGE_START);
         bundle_data->host_netmask =
             crm_element_value_copy(xml_obj, PCMK_XA_HOST_NETMASK);
         bundle_data->host_network =
             crm_element_value_copy(xml_obj, PCMK_XA_HOST_INTERFACE);
         bundle_data->control_port =
             crm_element_value_copy(xml_obj, PCMK_XA_CONTROL_PORT);
         value = crm_element_value(xml_obj, PCMK_XA_ADD_HOST);
         if (crm_str_to_boolean(value, &bundle_data->add_host) != 1) {
             bundle_data->add_host = TRUE;
         }
 
         for (xml_child = pcmk__xe_first_child(xml_obj, PCMK_XE_PORT_MAPPING,
                                               NULL, NULL);
              xml_child != NULL; xml_child = pcmk__xe_next_same(xml_child)) {
 
             pe__bundle_port_t *port =
                 pcmk__assert_alloc(1, sizeof(pe__bundle_port_t));
 
             port->source = crm_element_value_copy(xml_child, PCMK_XA_PORT);
 
             if(port->source == NULL) {
                 port->source = crm_element_value_copy(xml_child, PCMK_XA_RANGE);
             } else {
                 port->target = crm_element_value_copy(xml_child,
                                                       PCMK_XA_INTERNAL_PORT);
             }
 
             if(port->source != NULL && strlen(port->source) > 0) {
                 if(port->target == NULL) {
                     port->target = strdup(port->source);
                 }
                 bundle_data->ports = g_list_append(bundle_data->ports, port);
 
             } else {
                 pcmk__config_err("Invalid " PCMK_XA_PORT " directive %s",
                                  pcmk__xe_id(xml_child));
                 port_free(port);
             }
         }
     }
 
     xml_obj = pcmk__xe_first_child(rsc->private->xml, PCMK_XE_STORAGE, NULL,
                                    NULL);
     for (xml_child = pcmk__xe_first_child(xml_obj, PCMK_XE_STORAGE_MAPPING,
                                           NULL, NULL);
          xml_child != NULL; xml_child = pcmk__xe_next_same(xml_child)) {
 
         const char *source = crm_element_value(xml_child, PCMK_XA_SOURCE_DIR);
         const char *target = crm_element_value(xml_child, PCMK_XA_TARGET_DIR);
         const char *options = crm_element_value(xml_child, PCMK_XA_OPTIONS);
         int flags = pe__bundle_mount_none;
 
         if (source == NULL) {
             source = crm_element_value(xml_child, PCMK_XA_SOURCE_DIR_ROOT);
             pe__set_bundle_mount_flags(xml_child, flags,
                                        pe__bundle_mount_subdir);
         }
 
         if (source && target) {
             mount_add(bundle_data, source, target, options, flags);
             if (strcmp(target, "/var/log") == 0) {
                 need_log_mount = FALSE;
             }
         } else {
             pcmk__config_err("Invalid mount directive %s",
                              pcmk__xe_id(xml_child));
         }
     }
 
     xml_obj = pcmk__xe_first_child(rsc->private->xml, PCMK_XE_PRIMITIVE, NULL,
                                    NULL);
     if (xml_obj && valid_network(bundle_data)) {
         const char *suffix = NULL;
         char *value = NULL;
         xmlNode *xml_set = NULL;
 
         xml_resource = pcmk__xe_create(NULL, PCMK_XE_CLONE);
 
         /* @COMPAT We no longer use the <master> tag, but we need to keep it as
          * part of the resource name, so that bundles don't restart in a rolling
          * upgrade. (It also avoids needing to change regression tests.)
          */
         suffix = (const char *) xml_resource->name;
         if (bundle_data->promoted_max > 0) {
             suffix = "master";
         }
 
         pcmk__xe_set_id(xml_resource, "%s-%s", bundle_data->prefix, suffix);
 
         xml_set = pcmk__xe_create(xml_resource, PCMK_XE_META_ATTRIBUTES);
         pcmk__xe_set_id(xml_set, "%s-%s-meta",
                         bundle_data->prefix, xml_resource->name);
 
         crm_create_nvpair_xml(xml_set, NULL,
                               PCMK_META_ORDERED, PCMK_VALUE_TRUE);
 
         value = pcmk__itoa(bundle_data->nreplicas);
         crm_create_nvpair_xml(xml_set, NULL, PCMK_META_CLONE_MAX, value);
         free(value);
 
         value = pcmk__itoa(bundle_data->nreplicas_per_host);
         crm_create_nvpair_xml(xml_set, NULL, PCMK_META_CLONE_NODE_MAX, value);
         free(value);
 
         crm_create_nvpair_xml(xml_set, NULL, PCMK_META_GLOBALLY_UNIQUE,
                               pcmk__btoa(bundle_data->nreplicas_per_host > 1));
 
         if (bundle_data->promoted_max) {
             crm_create_nvpair_xml(xml_set, NULL,
                                   PCMK_META_PROMOTABLE, PCMK_VALUE_TRUE);
 
             value = pcmk__itoa(bundle_data->promoted_max);
             crm_create_nvpair_xml(xml_set, NULL, PCMK_META_PROMOTED_MAX, value);
             free(value);
         }
 
         //crm_xml_add(xml_obj, PCMK_XA_ID, bundle_data->prefix);
         pcmk__xml_copy(xml_resource, xml_obj);
 
     } else if(xml_obj) {
         pcmk__config_err("Cannot control %s inside %s without either "
                          PCMK_XA_IP_RANGE_START " or " PCMK_XA_CONTROL_PORT,
                          rsc->id, pcmk__xe_id(xml_obj));
         return FALSE;
     }
 
     if(xml_resource) {
         int lpc = 0;
         GList *childIter = NULL;
         pe__bundle_port_t *port = NULL;
         GString *buffer = NULL;
 
         if (pe__unpack_resource(xml_resource, &(bundle_data->child), rsc,
                                 scheduler) != pcmk_rc_ok) {
             return FALSE;
         }
 
         /* Currently, we always map the default authentication key location
          * into the same location inside the container.
          *
          * Ideally, we would respect the host's PCMK_authkey_location, but:
          * - it may be different on different nodes;
          * - the actual connection will do extra checking to make sure the key
          *   file exists and is readable, that we can't do here on the DC
          * - tools such as crm_resource and crm_simulate may not have the same
          *   environment variables as the cluster, causing operation digests to
          *   differ
          *
          * Always using the default location inside the container is fine,
          * because we control the pacemaker_remote environment, and it avoids
          * having to pass another environment variable to the container.
          *
          * @TODO A better solution may be to have only pacemaker_remote use the
          * environment variable, and have the cluster nodes use a new
          * cluster option for key location. This would introduce the limitation
          * of the location being the same on all cluster nodes, but that's
          * reasonable.
          */
         mount_add(bundle_data, DEFAULT_REMOTE_KEY_LOCATION,
                   DEFAULT_REMOTE_KEY_LOCATION, NULL, pe__bundle_mount_none);
 
         if (need_log_mount) {
             mount_add(bundle_data, CRM_BUNDLE_DIR, "/var/log", NULL,
                       pe__bundle_mount_subdir);
         }
 
         port = pcmk__assert_alloc(1, sizeof(pe__bundle_port_t));
         if(bundle_data->control_port) {
             port->source = strdup(bundle_data->control_port);
         } else {
             /* If we wanted to respect PCMK_remote_port, we could use
              * crm_default_remote_port() here and elsewhere in this file instead
              * of DEFAULT_REMOTE_PORT.
              *
              * However, it gains nothing, since we control both the container
              * environment and the connection resource parameters, and the user
              * can use a different port if desired by setting
              * PCMK_XA_CONTROL_PORT.
              */
             port->source = pcmk__itoa(DEFAULT_REMOTE_PORT);
         }
         port->target = strdup(port->source);
         bundle_data->ports = g_list_append(bundle_data->ports, port);
 
         buffer = g_string_sized_new(1024);
         for (childIter = bundle_data->child->children; childIter != NULL;
              childIter = childIter->next) {
 
             pcmk__bundle_replica_t *replica = NULL;
 
             replica = pcmk__assert_alloc(1, sizeof(pcmk__bundle_replica_t));
             replica->child = childIter->data;
             pcmk__set_rsc_flags(replica->child, pcmk__rsc_exclusive_probes);
             replica->offset = lpc++;
 
             // Ensure the child's notify gets set based on the underlying primitive's value
             if (pcmk_is_set(replica->child->flags, pcmk__rsc_notify)) {
                 pcmk__set_rsc_flags(bundle_data->child, pcmk__rsc_notify);
             }
 
             allocate_ip(bundle_data, replica, buffer);
             bundle_data->replicas = g_list_append(bundle_data->replicas,
                                                   replica);
             bundle_data->attribute_target =
                 g_hash_table_lookup(replica->child->meta,
                                     PCMK_META_CONTAINER_ATTRIBUTE_TARGET);
         }
         bundle_data->container_host_options = g_string_free(buffer, FALSE);
 
         if (bundle_data->attribute_target) {
             pcmk__insert_dup(rsc->meta, PCMK_META_CONTAINER_ATTRIBUTE_TARGET,
                              bundle_data->attribute_target);
             pcmk__insert_dup(bundle_data->child->meta,
                              PCMK_META_CONTAINER_ATTRIBUTE_TARGET,
                              bundle_data->attribute_target);
         }
 
     } else {
         // Just a naked container, no pacemaker-remote
         GString *buffer = g_string_sized_new(1024);
 
         for (int lpc = 0; lpc < bundle_data->nreplicas; lpc++) {
             pcmk__bundle_replica_t *replica = NULL;
 
             replica = pcmk__assert_alloc(1, sizeof(pcmk__bundle_replica_t));
             replica->offset = lpc;
             allocate_ip(bundle_data, replica, buffer);
             bundle_data->replicas = g_list_append(bundle_data->replicas,
                                                   replica);
         }
         bundle_data->container_host_options = g_string_free(buffer, FALSE);
     }
 
     for (GList *gIter = bundle_data->replicas; gIter != NULL;
          gIter = gIter->next) {
         pcmk__bundle_replica_t *replica = gIter->data;
 
         if (create_replica_resources(rsc, bundle_data, replica) != pcmk_rc_ok) {
             pcmk__config_err("Failed unpacking resource %s", rsc->id);
             rsc->private->fns->free(rsc);
             return FALSE;
         }
 
         /* Utilization needs special handling for bundles. It makes no sense for
          * the inner primitive to have utilization, because it is tied
          * one-to-one to the guest node created by the container resource -- and
          * there's no way to set capacities for that guest node anyway.
          *
          * What the user really wants is to configure utilization for the
          * container. However, the schema only allows utilization for
          * primitives, and the container resource is implicit anyway, so the
          * user can *only* configure utilization for the inner primitive. If
          * they do, move the primitive's utilization values to the container.
          *
          * @TODO This means that bundles without an inner primitive can't have
          * utilization. An alternative might be to allow utilization values in
          * the top-level bundle XML in the schema, and copy those to each
          * container.
          */
         if (replica->child != NULL) {
             GHashTable *empty = replica->container->utilization;
 
             replica->container->utilization = replica->child->utilization;
             replica->child->utilization = empty;
         }
     }
 
     if (bundle_data->child) {
         rsc->children = g_list_append(rsc->children, bundle_data->child);
     }
     return TRUE;
 }
 
 static int
 replica_resource_active(pcmk_resource_t *rsc, gboolean all)
 {
     if (rsc) {
         gboolean child_active = rsc->private->fns->active(rsc, all);
 
         if (child_active && !all) {
             return TRUE;
         } else if (!child_active && all) {
             return FALSE;
         }
     }
     return -1;
 }
 
 gboolean
 pe__bundle_active(pcmk_resource_t *rsc, gboolean all)
 {
     pe__bundle_variant_data_t *bundle_data = NULL;
     GList *iter = NULL;
 
     get_bundle_variant_data(bundle_data, rsc);
     for (iter = bundle_data->replicas; iter != NULL; iter = iter->next) {
         pcmk__bundle_replica_t *replica = iter->data;
         int rsc_active;
 
         rsc_active = replica_resource_active(replica->ip, all);
         if (rsc_active >= 0) {
             return (gboolean) rsc_active;
         }
 
         rsc_active = replica_resource_active(replica->child, all);
         if (rsc_active >= 0) {
             return (gboolean) rsc_active;
         }
 
         rsc_active = replica_resource_active(replica->container, all);
         if (rsc_active >= 0) {
             return (gboolean) rsc_active;
         }
 
         rsc_active = replica_resource_active(replica->remote, all);
         if (rsc_active >= 0) {
             return (gboolean) rsc_active;
         }
     }
 
     /* If "all" is TRUE, we've already checked that no resources were inactive,
      * so return TRUE; if "all" is FALSE, we didn't find any active resources,
      * so return FALSE.
      */
     return all;
 }
 
 /*!
  * \internal
  * \brief Find the bundle replica corresponding to a given node
  *
  * \param[in] bundle  Top-level bundle resource
  * \param[in] node    Node to search for
  *
  * \return Bundle replica if found, NULL otherwise
  */
 pcmk_resource_t *
 pe__find_bundle_replica(const pcmk_resource_t *bundle, const pcmk_node_t *node)
 {
     pe__bundle_variant_data_t *bundle_data = NULL;
     CRM_ASSERT(bundle && node);
 
     get_bundle_variant_data(bundle_data, bundle);
     for (GList *gIter = bundle_data->replicas; gIter != NULL;
          gIter = gIter->next) {
         pcmk__bundle_replica_t *replica = gIter->data;
 
         CRM_ASSERT(replica && replica->node);
         if (pcmk__same_node(replica->node, node)) {
             return replica->child;
         }
     }
     return NULL;
 }
 
 PCMK__OUTPUT_ARGS("bundle", "uint32_t", "pcmk_resource_t *", "GList *",
                   "GList *")
 int
 pe__bundle_xml(pcmk__output_t *out, va_list args)
 {
     uint32_t show_opts = va_arg(args, uint32_t);
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     GList *only_node = va_arg(args, GList *);
     GList *only_rsc = va_arg(args, GList *);
 
     pe__bundle_variant_data_t *bundle_data = NULL;
     int rc = pcmk_rc_no_output;
     gboolean printed_header = FALSE;
     gboolean print_everything = TRUE;
 
     const char *desc = NULL;
 
     CRM_ASSERT(rsc != NULL);
     get_bundle_variant_data(bundle_data, rsc);
 
     if (rsc->private->fns->is_filtered(rsc, only_rsc, TRUE)) {
         return rc;
     }
 
     print_everything = pcmk__str_in_list(rsc->id, only_rsc, pcmk__str_star_matches);
 
     for (GList *gIter = bundle_data->replicas; gIter != NULL;
          gIter = gIter->next) {
         pcmk__bundle_replica_t *replica = gIter->data;
         pcmk_resource_t *ip = replica->ip;
         pcmk_resource_t *child = replica->child;
         pcmk_resource_t *container = replica->container;
         pcmk_resource_t *remote = replica->remote;
         char *id = NULL;
         gboolean print_ip, print_child, print_ctnr, print_remote;
 
         CRM_ASSERT(replica);
 
         if (pcmk__rsc_filtered_by_node(container, only_node)) {
             continue;
         }
 
         print_ip = (ip != NULL)
                    && !ip->private->fns->is_filtered(ip, only_rsc,
                                                      print_everything);
         print_child = (child != NULL)
                       && !child->private->fns->is_filtered(child, only_rsc,
                                                            print_everything);
         print_ctnr = !container->private->fns->is_filtered(container, only_rsc,
                                                            print_everything);
         print_remote = (remote != NULL)
                        && !remote->private->fns->is_filtered(remote, only_rsc,
                                                              print_everything);
 
         if (!print_everything && !print_ip && !print_child && !print_ctnr && !print_remote) {
             continue;
         }
 
         if (!printed_header) {
             const char *type = container_agent_str(bundle_data->agent_type);
             const char *unique = pcmk__flag_text(rsc->flags, pcmk__rsc_unique);
             const char *maintenance = pcmk__flag_text(rsc->flags,
                                                       pcmk__rsc_maintenance);
             const char *managed = pcmk__flag_text(rsc->flags,
                                                   pcmk__rsc_managed);
             const char *failed = pcmk__flag_text(rsc->flags, pcmk__rsc_failed);
 
             printed_header = TRUE;
 
             desc = pe__resource_description(rsc, show_opts);
 
             rc = pe__name_and_nvpairs_xml(out, true, PCMK_XE_BUNDLE,
                                           PCMK_XA_ID, rsc->id,
                                           PCMK_XA_TYPE, type,
                                           PCMK_XA_IMAGE, bundle_data->image,
                                           PCMK_XA_UNIQUE, unique,
                                           PCMK_XA_MAINTENANCE, maintenance,
                                           PCMK_XA_MANAGED, managed,
                                           PCMK_XA_FAILED, failed,
                                           PCMK_XA_DESCRIPTION, desc,
                                           NULL);
             CRM_ASSERT(rc == pcmk_rc_ok);
         }
 
         id = pcmk__itoa(replica->offset);
         rc = pe__name_and_nvpairs_xml(out, true, PCMK_XE_REPLICA,
                                       PCMK_XA_ID, id,
                                       NULL);
         free(id);
         CRM_ASSERT(rc == pcmk_rc_ok);
 
         if (print_ip) {
             out->message(out, (const char *) ip->private->xml->name, show_opts,
                          ip, only_node, only_rsc);
         }
 
         if (print_child) {
             out->message(out, (const char *) child->private->xml->name,
                          show_opts, child, only_node, only_rsc);
         }
 
         if (print_ctnr) {
             out->message(out, (const char *) container->private->xml->name,
                          show_opts, container, only_node, only_rsc);
         }
 
         if (print_remote) {
             out->message(out, (const char *) remote->private->xml->name,
                          show_opts, remote, only_node, only_rsc);
         }
 
         pcmk__output_xml_pop_parent(out); // replica
     }
 
     if (printed_header) {
         pcmk__output_xml_pop_parent(out); // bundle
     }
 
     return rc;
 }
 
 static void
 pe__bundle_replica_output_html(pcmk__output_t *out,
                                pcmk__bundle_replica_t *replica,
                                pcmk_node_t *node, uint32_t show_opts)
 {
     pcmk_resource_t *rsc = replica->child;
 
     int offset = 0;
     char buffer[LINE_MAX];
 
     if(rsc == NULL) {
         rsc = replica->container;
     }
 
     if (replica->remote) {
         offset += snprintf(buffer + offset, LINE_MAX - offset, "%s",
                            rsc_printable_id(replica->remote));
     } else {
         offset += snprintf(buffer + offset, LINE_MAX - offset, "%s",
                            rsc_printable_id(replica->container));
     }
     if (replica->ipaddr) {
         offset += snprintf(buffer + offset, LINE_MAX - offset, " (%s)",
                            replica->ipaddr);
     }
 
     pe__common_output_html(out, rsc, buffer, node, show_opts);
 }
 
 /*!
  * \internal
  * \brief Get a string describing a resource's unmanaged state or lack thereof
  *
  * \param[in] rsc  Resource to describe
  *
  * \return A string indicating that a resource is in maintenance mode or
  *         otherwise unmanaged, or an empty string otherwise
  */
 static const char *
 get_unmanaged_str(const pcmk_resource_t *rsc)
 {
     if (pcmk_is_set(rsc->flags, pcmk__rsc_maintenance)) {
         return " (maintenance)";
     }
     if (!pcmk_is_set(rsc->flags, pcmk__rsc_managed)) {
         return " (unmanaged)";
     }
     return "";
 }
 
 PCMK__OUTPUT_ARGS("bundle", "uint32_t", "pcmk_resource_t *", "GList *",
                   "GList *")
 int
 pe__bundle_html(pcmk__output_t *out, va_list args)
 {
     uint32_t show_opts = va_arg(args, uint32_t);
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     GList *only_node = va_arg(args, GList *);
     GList *only_rsc = va_arg(args, GList *);
 
     const char *desc = NULL;
     pe__bundle_variant_data_t *bundle_data = NULL;
     int rc = pcmk_rc_no_output;
     gboolean print_everything = TRUE;
 
     CRM_ASSERT(rsc != NULL);
     get_bundle_variant_data(bundle_data, rsc);
 
     desc = pe__resource_description(rsc, show_opts);
 
     if (rsc->private->fns->is_filtered(rsc, only_rsc, TRUE)) {
         return rc;
     }
 
     print_everything = pcmk__str_in_list(rsc->id, only_rsc, pcmk__str_star_matches);
 
     for (GList *gIter = bundle_data->replicas; gIter != NULL;
          gIter = gIter->next) {
         pcmk__bundle_replica_t *replica = gIter->data;
         pcmk_resource_t *ip = replica->ip;
         pcmk_resource_t *child = replica->child;
         pcmk_resource_t *container = replica->container;
         pcmk_resource_t *remote = replica->remote;
         gboolean print_ip, print_child, print_ctnr, print_remote;
 
         CRM_ASSERT(replica);
 
         if (pcmk__rsc_filtered_by_node(container, only_node)) {
             continue;
         }
 
         print_ip = (ip != NULL)
                    && !ip->private->fns->is_filtered(ip, only_rsc,
                                                      print_everything);
         print_child = (child != NULL)
                       && !child->private->fns->is_filtered(child, only_rsc,
                                                            print_everything);
         print_ctnr = !container->private->fns->is_filtered(container, only_rsc,
                                                            print_everything);
         print_remote = (remote != NULL)
                        && !remote->private->fns->is_filtered(remote, only_rsc,
                                                              print_everything);
 
         if (pcmk_is_set(show_opts, pcmk_show_implicit_rscs) ||
             (print_everything == FALSE && (print_ip || print_child || print_ctnr || print_remote))) {
             /* The text output messages used below require pe_print_implicit to
              * be set to do anything.
              */
             uint32_t new_show_opts = show_opts | pcmk_show_implicit_rscs;
 
             PCMK__OUTPUT_LIST_HEADER(out, FALSE, rc, "Container bundle%s: %s [%s]%s%s%s%s%s",
                                      (bundle_data->nreplicas > 1)? " set" : "",
                                      rsc->id, bundle_data->image,
                                      pcmk_is_set(rsc->flags, pcmk__rsc_unique)? " (unique)" : "",
                                      desc ? " (" : "", desc ? desc : "", desc ? ")" : "",
                                      get_unmanaged_str(rsc));
 
             if (pcmk__list_of_multiple(bundle_data->replicas)) {
                 out->begin_list(out, NULL, NULL, "Replica[%d]", replica->offset);
             }
 
             if (print_ip) {
                 out->message(out, (const char *) ip->private->xml->name,
                              new_show_opts, ip, only_node, only_rsc);
             }
 
             if (print_child) {
                 out->message(out, (const char *) child->private->xml->name,
                              new_show_opts, child, only_node, only_rsc);
             }
 
             if (print_ctnr) {
                 out->message(out, (const char *) container->private->xml->name,
                              new_show_opts, container, only_node, only_rsc);
             }
 
             if (print_remote) {
                 out->message(out, (const char *) remote->private->xml->name,
                              new_show_opts, remote, only_node, only_rsc);
             }
 
             if (pcmk__list_of_multiple(bundle_data->replicas)) {
                 out->end_list(out);
             }
         } else if (print_everything == FALSE && !(print_ip || print_child || print_ctnr || print_remote)) {
             continue;
         } else {
             PCMK__OUTPUT_LIST_HEADER(out, FALSE, rc, "Container bundle%s: %s [%s]%s%s%s%s%s",
                                      (bundle_data->nreplicas > 1)? " set" : "",
                                      rsc->id, bundle_data->image,
                                      pcmk_is_set(rsc->flags, pcmk__rsc_unique)? " (unique)" : "",
                                      desc ? " (" : "", desc ? desc : "", desc ? ")" : "",
                                      get_unmanaged_str(rsc));
 
             pe__bundle_replica_output_html(out, replica,
                                            pcmk__current_node(container),
                                            show_opts);
         }
     }
 
     PCMK__OUTPUT_LIST_FOOTER(out, rc);
     return rc;
 }
 
 static void
 pe__bundle_replica_output_text(pcmk__output_t *out,
                                pcmk__bundle_replica_t *replica,
                                pcmk_node_t *node, uint32_t show_opts)
 {
     const pcmk_resource_t *rsc = replica->child;
 
     int offset = 0;
     char buffer[LINE_MAX];
 
     if(rsc == NULL) {
         rsc = replica->container;
     }
 
     if (replica->remote) {
         offset += snprintf(buffer + offset, LINE_MAX - offset, "%s",
                            rsc_printable_id(replica->remote));
     } else {
         offset += snprintf(buffer + offset, LINE_MAX - offset, "%s",
                            rsc_printable_id(replica->container));
     }
     if (replica->ipaddr) {
         offset += snprintf(buffer + offset, LINE_MAX - offset, " (%s)",
                            replica->ipaddr);
     }
 
     pe__common_output_text(out, rsc, buffer, node, show_opts);
 }
 
 PCMK__OUTPUT_ARGS("bundle", "uint32_t", "pcmk_resource_t *", "GList *",
                   "GList *")
 int
 pe__bundle_text(pcmk__output_t *out, va_list args)
 {
     uint32_t show_opts = va_arg(args, uint32_t);
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     GList *only_node = va_arg(args, GList *);
     GList *only_rsc = va_arg(args, GList *);
 
     const char *desc = NULL;
     pe__bundle_variant_data_t *bundle_data = NULL;
     int rc = pcmk_rc_no_output;
     gboolean print_everything = TRUE;
 
     desc = pe__resource_description(rsc, show_opts);
 
     CRM_ASSERT(rsc != NULL);
     get_bundle_variant_data(bundle_data, rsc);
 
     if (rsc->private->fns->is_filtered(rsc, only_rsc, TRUE)) {
         return rc;
     }
 
     print_everything = pcmk__str_in_list(rsc->id, only_rsc, pcmk__str_star_matches);
 
     for (GList *gIter = bundle_data->replicas; gIter != NULL;
          gIter = gIter->next) {
         pcmk__bundle_replica_t *replica = gIter->data;
         pcmk_resource_t *ip = replica->ip;
         pcmk_resource_t *child = replica->child;
         pcmk_resource_t *container = replica->container;
         pcmk_resource_t *remote = replica->remote;
         gboolean print_ip, print_child, print_ctnr, print_remote;
 
         CRM_ASSERT(replica);
 
         if (pcmk__rsc_filtered_by_node(container, only_node)) {
             continue;
         }
 
         print_ip = (ip != NULL)
                    && !ip->private->fns->is_filtered(ip, only_rsc,
                                                      print_everything);
         print_child = (child != NULL)
                       && !child->private->fns->is_filtered(child, only_rsc,
                                                            print_everything);
         print_ctnr = !container->private->fns->is_filtered(container, only_rsc,
                                                            print_everything);
         print_remote = (remote != NULL)
                        && !remote->private->fns->is_filtered(remote, only_rsc,
                                                              print_everything);
 
         if (pcmk_is_set(show_opts, pcmk_show_implicit_rscs) ||
             (print_everything == FALSE && (print_ip || print_child || print_ctnr || print_remote))) {
             /* The text output messages used below require pe_print_implicit to
              * be set to do anything.
              */
             uint32_t new_show_opts = show_opts | pcmk_show_implicit_rscs;
 
             PCMK__OUTPUT_LIST_HEADER(out, FALSE, rc, "Container bundle%s: %s [%s]%s%s%s%s%s",
                                      (bundle_data->nreplicas > 1)? " set" : "",
                                      rsc->id, bundle_data->image,
                                      pcmk_is_set(rsc->flags, pcmk__rsc_unique)? " (unique)" : "",
                                      desc ? " (" : "", desc ? desc : "", desc ? ")" : "",
                                      get_unmanaged_str(rsc));
 
             if (pcmk__list_of_multiple(bundle_data->replicas)) {
                 out->list_item(out, NULL, "Replica[%d]", replica->offset);
             }
 
             out->begin_list(out, NULL, NULL, NULL);
 
             if (print_ip) {
                 out->message(out, (const char *) ip->private->xml->name,
                              new_show_opts, ip, only_node, only_rsc);
             }
 
             if (print_child) {
                 out->message(out, (const char *) child->private->xml->name,
                              new_show_opts, child, only_node, only_rsc);
             }
 
             if (print_ctnr) {
                 out->message(out, (const char *) container->private->xml->name,
                              new_show_opts, container, only_node, only_rsc);
             }
 
             if (print_remote) {
                 out->message(out, (const char *) remote->private->xml->name,
                              new_show_opts, remote, only_node, only_rsc);
             }
 
             out->end_list(out);
         } else if (print_everything == FALSE && !(print_ip || print_child || print_ctnr || print_remote)) {
             continue;
         } else {
             PCMK__OUTPUT_LIST_HEADER(out, FALSE, rc, "Container bundle%s: %s [%s]%s%s%s%s%s",
                                      (bundle_data->nreplicas > 1)? " set" : "",
                                      rsc->id, bundle_data->image,
                                      pcmk_is_set(rsc->flags, pcmk__rsc_unique)? " (unique)" : "",
                                      desc ? " (" : "", desc ? desc : "", desc ? ")" : "",
                                      get_unmanaged_str(rsc));
 
             pe__bundle_replica_output_text(out, replica,
                                            pcmk__current_node(container),
                                            show_opts);
         }
     }
 
     PCMK__OUTPUT_LIST_FOOTER(out, rc);
     return rc;
 }
 
 static void
 free_bundle_replica(pcmk__bundle_replica_t *replica)
 {
     if (replica == NULL) {
         return;
     }
 
     if (replica->node) {
         free(replica->node);
         replica->node = NULL;
     }
 
     if (replica->ip) {
         pcmk__xml_free(replica->ip->private->xml);
         replica->ip->private->xml = NULL;
         replica->ip->private->fns->free(replica->ip);
     }
     if (replica->container) {
         pcmk__xml_free(replica->container->private->xml);
         replica->container->private->xml = NULL;
         replica->container->private->fns->free(replica->container);
     }
     if (replica->remote) {
         pcmk__xml_free(replica->remote->private->xml);
         replica->remote->private->xml = NULL;
         replica->remote->private->fns->free(replica->remote);
     }
     free(replica->ipaddr);
     free(replica);
 }
 
 void
 pe__free_bundle(pcmk_resource_t *rsc)
 {
     pe__bundle_variant_data_t *bundle_data = NULL;
     CRM_CHECK(rsc != NULL, return);
 
     get_bundle_variant_data(bundle_data, rsc);
     pcmk__rsc_trace(rsc, "Freeing %s", rsc->id);
 
     free(bundle_data->prefix);
     free(bundle_data->image);
     free(bundle_data->control_port);
     free(bundle_data->host_network);
     free(bundle_data->host_netmask);
     free(bundle_data->ip_range_start);
     free(bundle_data->container_network);
     free(bundle_data->launcher_options);
     free(bundle_data->container_command);
     g_free(bundle_data->container_host_options);
 
     g_list_free_full(bundle_data->replicas,
                      (GDestroyNotify) free_bundle_replica);
     g_list_free_full(bundle_data->mounts, (GDestroyNotify)mount_free);
     g_list_free_full(bundle_data->ports, (GDestroyNotify)port_free);
     g_list_free(rsc->children);
 
     if(bundle_data->child) {
         pcmk__xml_free(bundle_data->child->private->xml);
         bundle_data->child->private->xml = NULL;
         bundle_data->child->private->fns->free(bundle_data->child);
     }
     common_free(rsc);
 }
 
 enum rsc_role_e
 pe__bundle_resource_state(const pcmk_resource_t *rsc, gboolean current)
 {
     enum rsc_role_e container_role = pcmk_role_unknown;
     return container_role;
 }
 
 /*!
  * \brief Get the number of configured replicas in a bundle
  *
  * \param[in] rsc  Bundle resource
  *
  * \return Number of configured replicas, or 0 on error
  */
 int
 pe_bundle_replicas(const pcmk_resource_t *rsc)
 {
     if (pcmk__is_bundle(rsc)) {
         pe__bundle_variant_data_t *bundle_data = NULL;
 
         get_bundle_variant_data(bundle_data, rsc);
         return bundle_data->nreplicas;
     }
     return 0;
 }
 
 void
 pe__count_bundle(pcmk_resource_t *rsc)
 {
     pe__bundle_variant_data_t *bundle_data = NULL;
 
     get_bundle_variant_data(bundle_data, rsc);
     for (GList *item = bundle_data->replicas; item != NULL; item = item->next) {
         pcmk__bundle_replica_t *replica = item->data;
 
         if (replica->ip) {
             replica->ip->private->fns->count(replica->ip);
         }
         if (replica->child) {
             replica->child->private->fns->count(replica->child);
         }
         if (replica->container) {
             replica->container->private->fns->count(replica->container);
         }
         if (replica->remote) {
             replica->remote->private->fns->count(replica->remote);
         }
     }
 }
 
 gboolean
 pe__bundle_is_filtered(const pcmk_resource_t *rsc, GList *only_rsc,
                        gboolean check_parent)
 {
     gboolean passes = FALSE;
     pe__bundle_variant_data_t *bundle_data = NULL;
 
     if (pcmk__str_in_list(rsc_printable_id(rsc), only_rsc, pcmk__str_star_matches)) {
         passes = TRUE;
     } else {
         get_bundle_variant_data(bundle_data, rsc);
 
         for (GList *gIter = bundle_data->replicas; gIter != NULL; gIter = gIter->next) {
             pcmk__bundle_replica_t *replica = gIter->data;
             pcmk_resource_t *ip = replica->ip;
             pcmk_resource_t *child = replica->child;
             pcmk_resource_t *container = replica->container;
             pcmk_resource_t *remote = replica->remote;
 
             if ((ip != NULL)
                 && !ip->private->fns->is_filtered(ip, only_rsc, FALSE)) {
                 passes = TRUE;
                 break;
             }
             if ((child != NULL)
                 && !child->private->fns->is_filtered(child, only_rsc, FALSE)) {
                 passes = TRUE;
                 break;
             }
             if (!container->private->fns->is_filtered(container, only_rsc,
                                                       FALSE)) {
                 passes = TRUE;
                 break;
             }
             if ((remote != NULL)
                 && !remote->private->fns->is_filtered(remote, only_rsc,
                                                       FALSE)) {
                 passes = TRUE;
                 break;
             }
         }
     }
 
     return !passes;
 }
 
 /*!
  * \internal
  * \brief Get a list of a bundle's containers
  *
  * \param[in] bundle  Bundle resource
  *
  * \return Newly created list of \p bundle's containers
  * \note It is the caller's responsibility to free the result with
  *       g_list_free().
  */
 GList *
 pe__bundle_containers(const pcmk_resource_t *bundle)
 {
     GList *containers = NULL;
     const pe__bundle_variant_data_t *data = NULL;
 
     get_bundle_variant_data(data, bundle);
     for (GList *iter = data->replicas; iter != NULL; iter = iter->next) {
         pcmk__bundle_replica_t *replica = iter->data;
 
         containers = g_list_append(containers, replica->container);
     }
     return containers;
 }
 
 // Bundle implementation of pcmk__rsc_methods_t:active_node()
 pcmk_node_t *
 pe__bundle_active_node(const pcmk_resource_t *rsc, unsigned int *count_all,
                        unsigned int *count_clean)
 {
     pcmk_node_t *active = NULL;
     pcmk_node_t *node = NULL;
     pcmk_resource_t *container = NULL;
     GList *containers = NULL;
     GList *iter = NULL;
     GHashTable *nodes = NULL;
     const pe__bundle_variant_data_t *data = NULL;
 
     if (count_all != NULL) {
         *count_all = 0;
     }
     if (count_clean != NULL) {
         *count_clean = 0;
     }
     if (rsc == NULL) {
         return NULL;
     }
 
     /* For the purposes of this method, we only care about where the bundle's
      * containers are active, so build a list of active containers.
      */
     get_bundle_variant_data(data, rsc);
     for (iter = data->replicas; iter != NULL; iter = iter->next) {
         pcmk__bundle_replica_t *replica = iter->data;
 
         if (replica->container->running_on != NULL) {
             containers = g_list_append(containers, replica->container);
         }
     }
     if (containers == NULL) {
         return NULL;
     }
 
     /* If the bundle has only a single active container, just use that
      * container's method. If live migration is ever supported for bundle
      * containers, this will allow us to prefer the migration source when there
      * is only one container and it is migrating. For now, this just lets us
      * avoid creating the nodes table.
      */
     if (pcmk__list_of_1(containers)) {
         container = containers->data;
         node = container->private->fns->active_node(container, count_all,
                                                     count_clean);
         g_list_free(containers);
         return node;
     }
 
     // Add all containers' active nodes to a hash table (for uniqueness)
     nodes = g_hash_table_new(NULL, NULL);
     for (iter = containers; iter != NULL; iter = iter->next) {
         container = iter->data;
 
         for (GList *node_iter = container->running_on; node_iter != NULL;
              node_iter = node_iter->next) {
             node = node_iter->data;
 
             // If insert returns true, we haven't counted this node yet
             if (g_hash_table_insert(nodes, (gpointer) node->details,
                                     (gpointer) node)
                 && !pe__count_active_node(rsc, node, &active, count_all,
                                           count_clean)) {
                 goto done;
             }
         }
     }
 
 done:
     g_list_free(containers);
     g_hash_table_destroy(nodes);
     return active;
 }
 
 /*!
  * \internal
  * \brief Get maximum bundle resource instances per node
  *
  * \param[in] rsc  Bundle resource to check
  *
  * \return Maximum number of \p rsc instances that can be active on one node
  */
 unsigned int
 pe__bundle_max_per_node(const pcmk_resource_t *rsc)
 {
     pe__bundle_variant_data_t *bundle_data = NULL;
 
     get_bundle_variant_data(bundle_data, rsc);
     CRM_ASSERT(bundle_data->nreplicas_per_host >= 0);
     return (unsigned int) bundle_data->nreplicas_per_host;
 }
diff --git a/lib/pengine/complex.c b/lib/pengine/complex.c
index f4426398a6..ca64f9377e 100644
--- a/lib/pengine/complex.c
+++ b/lib/pengine/complex.c
@@ -1,1268 +1,1268 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <crm/pengine/rules.h>
 #include <crm/pengine/internal.h>
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>
 #include <crm/common/scheduler_internal.h>
 
 #include "pe_status_private.h"
 
 void populate_hash(xmlNode * nvpair_list, GHashTable * hash, const char **attrs, int attrs_length);
 
 static pcmk_node_t *active_node(const pcmk_resource_t *rsc,
                                 unsigned int *count_all,
                                 unsigned int *count_clean);
 
 static pcmk__rsc_methods_t resource_class_functions[] = {
     {
          native_unpack,
          native_find_rsc,
          native_parameter,
          native_active,
          native_resource_state,
          native_location,
          native_free,
          pe__count_common,
          pe__native_is_filtered,
          active_node,
          pe__primitive_max_per_node,
     },
     {
          group_unpack,
          native_find_rsc,
          native_parameter,
          group_active,
          group_resource_state,
          native_location,
          group_free,
          pe__count_common,
          pe__group_is_filtered,
          active_node,
          pe__group_max_per_node,
     },
     {
          clone_unpack,
          native_find_rsc,
          native_parameter,
          clone_active,
          clone_resource_state,
          native_location,
          clone_free,
          pe__count_common,
          pe__clone_is_filtered,
          active_node,
          pe__clone_max_per_node,
     },
     {
          pe__unpack_bundle,
          native_find_rsc,
          native_parameter,
          pe__bundle_active,
          pe__bundle_resource_state,
          native_location,
          pe__free_bundle,
          pe__count_bundle,
          pe__bundle_is_filtered,
          pe__bundle_active_node,
          pe__bundle_max_per_node,
     }
 };
 
 static enum pcmk__rsc_variant
 get_resource_type(const char *name)
 {
     if (pcmk__str_eq(name, PCMK_XE_PRIMITIVE, pcmk__str_casei)) {
         return pcmk__rsc_variant_primitive;
 
     } else if (pcmk__str_eq(name, PCMK_XE_GROUP, pcmk__str_casei)) {
         return pcmk__rsc_variant_group;
 
     } else if (pcmk__str_eq(name, PCMK_XE_CLONE, pcmk__str_casei)) {
         return pcmk__rsc_variant_clone;
 
     } else if (pcmk__str_eq(name, PCMK__XE_PROMOTABLE_LEGACY,
                             pcmk__str_casei)) {
         // @COMPAT deprecated since 2.0.0
         return pcmk__rsc_variant_clone;
 
     } else if (pcmk__str_eq(name, PCMK_XE_BUNDLE, pcmk__str_casei)) {
         return pcmk__rsc_variant_bundle;
     }
 
     return pcmk__rsc_variant_unknown;
 }
 
 /*!
  * \internal
  * \brief Insert a meta-attribute if not already present
  *
  * \param[in]     key    Meta-attribute name
  * \param[in]     value  Meta-attribute value to add if not already present
  * \param[in,out] table  Meta-attribute hash table to insert into
  *
  * \note This is like pcmk__insert_meta() except it won't overwrite existing
  *       values.
  */
 static void
 dup_attr(gpointer key, gpointer value, gpointer user_data)
 {
     GHashTable *table = user_data;
 
     CRM_CHECK((key != NULL) && (table != NULL), return);
     if (pcmk__str_eq((const char *) value, "#default", pcmk__str_casei)) {
         // @COMPAT Deprecated since 2.1.8
         pcmk__config_warn("Support for setting meta-attributes (such as %s) to "
                           "the explicit value '#default' is deprecated and "
                           "will be removed in a future release",
                           (const char *) key);
     } else if ((value != NULL) && (g_hash_table_lookup(table, key) == NULL)) {
         pcmk__insert_dup(table, (const char *) key, (const char *) value);
     }
 }
 
 static void
 expand_parents_fixed_nvpairs(pcmk_resource_t *rsc,
                              pe_rule_eval_data_t *rule_data,
                              GHashTable *meta_hash, pcmk_scheduler_t *scheduler)
 {
     GHashTable *parent_orig_meta = pcmk__strkey_table(free, free);
     pcmk_resource_t *p = rsc->private->parent;
 
     if (p == NULL) {
         return ;
     }
 
     /* Search all parent resources, get the fixed value of
      * PCMK_XE_META_ATTRIBUTES set only in the original xml, and stack it in the
      * hash table. The fixed value of the lower parent resource takes precedence
      * and is not overwritten.
      */
     while(p != NULL) {
         /* A hash table for comparison is generated, including the id-ref. */
         pe__unpack_dataset_nvpairs(p->private->xml, PCMK_XE_META_ATTRIBUTES,
                                    rule_data, parent_orig_meta, NULL, FALSE,
                                    scheduler);
         p = p->private->parent;
     }
 
     if (parent_orig_meta != NULL) {
         // This will not overwrite any values already existing for child
         g_hash_table_foreach(parent_orig_meta, dup_attr, meta_hash);
     }
 
     if (parent_orig_meta != NULL) {
         g_hash_table_destroy(parent_orig_meta);
     }
     
     return ;
 
 }
 void
 get_meta_attributes(GHashTable * meta_hash, pcmk_resource_t * rsc,
                     pcmk_node_t *node, pcmk_scheduler_t *scheduler)
 {
     pe_rsc_eval_data_t rsc_rule_data = {
         .standard = crm_element_value(rsc->private->xml, PCMK_XA_CLASS),
         .provider = crm_element_value(rsc->private->xml, PCMK_XA_PROVIDER),
         .agent = crm_element_value(rsc->private->xml, PCMK_XA_TYPE)
     };
 
     pe_rule_eval_data_t rule_data = {
         .node_hash = NULL,
         .now = scheduler->now,
         .match_data = NULL,
         .rsc_data = &rsc_rule_data,
         .op_data = NULL
     };
 
     if (node) {
         /* @COMPAT Support for node attribute expressions in rules for
          * meta-attributes is deprecated. When we can break behavioral backward
          * compatibility, drop this block.
          */
         rule_data.node_hash = node->details->attrs;
     }
 
     for (xmlAttrPtr a = pcmk__xe_first_attr(rsc->private->xml);
          a != NULL; a = a->next) {
 
         if (a->children != NULL) {
             dup_attr((gpointer) a->name, (gpointer) a->children->content,
                      meta_hash);
         }
     }
 
     pe__unpack_dataset_nvpairs(rsc->private->xml, PCMK_XE_META_ATTRIBUTES,
                                &rule_data, meta_hash, NULL, FALSE, scheduler);
 
     /* Set the PCMK_XE_META_ATTRIBUTES explicitly set in the parent resource to
      * the hash table of the child resource. If it is already explicitly set as
      * a child, it will not be overwritten.
      */
     if (rsc->private->parent != NULL) {
         expand_parents_fixed_nvpairs(rsc, &rule_data, meta_hash, scheduler);
     }
 
     /* check the defaults */
     pe__unpack_dataset_nvpairs(scheduler->rsc_defaults, PCMK_XE_META_ATTRIBUTES,
                                &rule_data, meta_hash, NULL, FALSE, scheduler);
 
     /* If there is PCMK_XE_META_ATTRIBUTES that the parent resource has not
      * explicitly set, set a value that is not set from PCMK_XE_RSC_DEFAULTS
      * either. The values already set up to this point will not be overwritten.
      */
     if (rsc->private->parent != NULL) {
         g_hash_table_foreach(rsc->private->parent->meta, dup_attr, meta_hash);
     }
 }
 
 void
 get_rsc_attributes(GHashTable *meta_hash, const pcmk_resource_t *rsc,
                    const pcmk_node_t *node, pcmk_scheduler_t *scheduler)
 {
     pe_rule_eval_data_t rule_data = {
         .node_hash = NULL,
         .now = scheduler->now,
         .match_data = NULL,
         .rsc_data = NULL,
         .op_data = NULL
     };
 
     if (node) {
         rule_data.node_hash = node->details->attrs;
     }
 
     pe__unpack_dataset_nvpairs(rsc->private->xml, PCMK_XE_INSTANCE_ATTRIBUTES,
                                &rule_data, meta_hash, NULL, FALSE, scheduler);
 
     /* set anything else based on the parent */
     if (rsc->private->parent != NULL) {
         get_rsc_attributes(meta_hash, rsc->private->parent, node, scheduler);
 
     } else {
         if (pcmk__xe_first_child(scheduler->rsc_defaults,
                                  PCMK_XE_INSTANCE_ATTRIBUTES, NULL,
                                  NULL) != NULL) {
             /* Not possible with schema validation enabled
              *
              * @COMPAT Drop support when we can break behavioral
              * backward compatibility
              */
             pcmk__warn_once(pcmk__wo_instance_defaults,
                             "Support for " PCMK_XE_INSTANCE_ATTRIBUTES " in "
                             PCMK_XE_RSC_DEFAULTS " is deprecated and will be "
                             "removed in a future release");
         }
 
         /* and finally check the defaults */
         pe__unpack_dataset_nvpairs(scheduler->rsc_defaults,
                                    PCMK_XE_INSTANCE_ATTRIBUTES, &rule_data,
                                    meta_hash, NULL, FALSE, scheduler);
     }
 }
 
 static char *
 template_op_key(xmlNode * op)
 {
     const char *name = crm_element_value(op, PCMK_XA_NAME);
     const char *role = crm_element_value(op, PCMK_XA_ROLE);
     char *key = NULL;
 
     if ((role == NULL)
         || pcmk__strcase_any_of(role, PCMK_ROLE_STARTED, PCMK_ROLE_UNPROMOTED,
                                 PCMK__ROLE_UNPROMOTED_LEGACY, NULL)) {
         role = PCMK__ROLE_UNKNOWN;
     }
 
     key = crm_strdup_printf("%s-%s", name, role);
     return key;
 }
 
 static gboolean
 unpack_template(xmlNode *xml_obj, xmlNode **expanded_xml,
                 pcmk_scheduler_t *scheduler)
 {
     xmlNode *cib_resources = NULL;
     xmlNode *template = NULL;
     xmlNode *new_xml = NULL;
     xmlNode *child_xml = NULL;
     xmlNode *rsc_ops = NULL;
     xmlNode *template_ops = NULL;
     const char *template_ref = NULL;
     const char *id = NULL;
 
     if (xml_obj == NULL) {
         pcmk__config_err("No resource object for template unpacking");
         return FALSE;
     }
 
     template_ref = crm_element_value(xml_obj, PCMK_XA_TEMPLATE);
     if (template_ref == NULL) {
         return TRUE;
     }
 
     id = pcmk__xe_id(xml_obj);
     if (id == NULL) {
         pcmk__config_err("'%s' object must have a id", xml_obj->name);
         return FALSE;
     }
 
     if (pcmk__str_eq(template_ref, id, pcmk__str_none)) {
         pcmk__config_err("The resource object '%s' should not reference itself",
                          id);
         return FALSE;
     }
 
     cib_resources = get_xpath_object("//" PCMK_XE_RESOURCES, scheduler->input,
                                      LOG_TRACE);
     if (cib_resources == NULL) {
         pcmk__config_err("No resources configured");
         return FALSE;
     }
 
     template = pcmk__xe_first_child(cib_resources, PCMK_XE_TEMPLATE,
                                     PCMK_XA_ID, template_ref);
     if (template == NULL) {
         pcmk__config_err("No template named '%s'", template_ref);
         return FALSE;
     }
 
     new_xml = pcmk__xml_copy(NULL, template);
     xmlNodeSetName(new_xml, xml_obj->name);
     crm_xml_add(new_xml, PCMK_XA_ID, id);
     crm_xml_add(new_xml, PCMK__META_CLONE,
                 crm_element_value(xml_obj, PCMK__META_CLONE));
 
     template_ops = pcmk__xe_first_child(new_xml, PCMK_XE_OPERATIONS, NULL,
                                         NULL);
 
     for (child_xml = pcmk__xe_first_child(xml_obj, NULL, NULL, NULL);
          child_xml != NULL; child_xml = pcmk__xe_next(child_xml)) {
 
         xmlNode *new_child = pcmk__xml_copy(new_xml, child_xml);
 
         if (pcmk__xe_is(new_child, PCMK_XE_OPERATIONS)) {
             rsc_ops = new_child;
         }
     }
 
     if (template_ops && rsc_ops) {
         xmlNode *op = NULL;
         GHashTable *rsc_ops_hash = pcmk__strkey_table(free, NULL);
 
         for (op = pcmk__xe_first_child(rsc_ops, NULL, NULL, NULL); op != NULL;
              op = pcmk__xe_next(op)) {
 
             char *key = template_op_key(op);
 
             g_hash_table_insert(rsc_ops_hash, key, op);
         }
 
         for (op = pcmk__xe_first_child(template_ops, NULL, NULL, NULL);
              op != NULL; op = pcmk__xe_next(op)) {
 
             char *key = template_op_key(op);
 
             if (g_hash_table_lookup(rsc_ops_hash, key) == NULL) {
                 pcmk__xml_copy(rsc_ops, op);
             }
 
             free(key);
         }
 
         if (rsc_ops_hash) {
             g_hash_table_destroy(rsc_ops_hash);
         }
 
         pcmk__xml_free(template_ops);
     }
 
     /*pcmk__xml_free(*expanded_xml); */
     *expanded_xml = new_xml;
 
 #if 0 /* Disable multi-level templates for now */
     if (!unpack_template(new_xml, expanded_xml, scheduler)) {
        pcmk__xml_free(*expanded_xml);
        *expanded_xml = NULL;
        return FALSE;
     }
 #endif
 
     return TRUE;
 }
 
 static gboolean
 add_template_rsc(xmlNode *xml_obj, pcmk_scheduler_t *scheduler)
 {
     const char *template_ref = NULL;
     const char *id = NULL;
 
     if (xml_obj == NULL) {
         pcmk__config_err("No resource object for processing resource list "
                          "of template");
         return FALSE;
     }
 
     template_ref = crm_element_value(xml_obj, PCMK_XA_TEMPLATE);
     if (template_ref == NULL) {
         return TRUE;
     }
 
     id = pcmk__xe_id(xml_obj);
     if (id == NULL) {
         pcmk__config_err("'%s' object must have a id", xml_obj->name);
         return FALSE;
     }
 
     if (pcmk__str_eq(template_ref, id, pcmk__str_none)) {
         pcmk__config_err("The resource object '%s' should not reference itself",
                          id);
         return FALSE;
     }
 
     if (add_tag_ref(scheduler->template_rsc_sets, template_ref, id) == FALSE) {
         return FALSE;
     }
 
     return TRUE;
 }
 
 static bool
 detect_promotable(pcmk_resource_t *rsc)
 {
     const char *promotable = g_hash_table_lookup(rsc->meta,
                                                  PCMK_META_PROMOTABLE);
 
     if (crm_is_true(promotable)) {
         return TRUE;
     }
 
     // @COMPAT deprecated since 2.0.0
     if (pcmk__xe_is(rsc->private->xml, PCMK__XE_PROMOTABLE_LEGACY)) {
         pcmk__warn_once(pcmk__wo_master_element,
                         "Support for <" PCMK__XE_PROMOTABLE_LEGACY "> (such "
                         "as in %s) is deprecated and will be removed in a "
                         "future release. Use <" PCMK_XE_CLONE "> with a "
                         PCMK_META_PROMOTABLE " meta-attribute instead.",
                         rsc->id);
         pcmk__insert_dup(rsc->meta, PCMK_META_PROMOTABLE, PCMK_VALUE_TRUE);
         return TRUE;
     }
     return FALSE;
 }
 
 static void
 free_params_table(gpointer data)
 {
     g_hash_table_destroy((GHashTable *) data);
 }
 
 /*!
  * \brief Get a table of resource parameters
  *
  * \param[in,out] rsc        Resource to query
  * \param[in]     node       Node for evaluating rules (NULL for defaults)
  * \param[in,out] scheduler  Scheduler data
  *
  * \return Hash table containing resource parameter names and values
  *         (or NULL if \p rsc or \p scheduler is NULL)
  * \note The returned table will be destroyed when the resource is freed, so
  *       callers should not destroy it.
  */
 GHashTable *
 pe_rsc_params(pcmk_resource_t *rsc, const pcmk_node_t *node,
               pcmk_scheduler_t *scheduler)
 {
     GHashTable *params_on_node = NULL;
 
     /* A NULL node is used to request the resource's default parameters
      * (not evaluated for node), but we always want something non-NULL
      * as a hash table key.
      */
     const char *node_name = "";
 
     // Sanity check
     if ((rsc == NULL) || (scheduler == NULL)) {
         return NULL;
     }
     if ((node != NULL) && (node->details->uname != NULL)) {
         node_name = node->details->uname;
     }
 
     // Find the parameter table for given node
     if (rsc->parameter_cache == NULL) {
         rsc->parameter_cache = pcmk__strikey_table(free, free_params_table);
     } else {
         params_on_node = g_hash_table_lookup(rsc->parameter_cache, node_name);
     }
 
     // If none exists yet, create one with parameters evaluated for node
     if (params_on_node == NULL) {
         params_on_node = pcmk__strkey_table(free, free);
         get_rsc_attributes(params_on_node, rsc, node, scheduler);
         g_hash_table_insert(rsc->parameter_cache, strdup(node_name),
                             params_on_node);
     }
     return params_on_node;
 }
 
 /*!
  * \internal
  * \brief Unpack a resource's \c PCMK_META_REQUIRES meta-attribute
  *
  * \param[in,out] rsc         Resource being unpacked
  * \param[in]     value       Value of \c PCMK_META_REQUIRES meta-attribute
  * \param[in]     is_default  Whether \p value was selected by default
  */
 static void
 unpack_requires(pcmk_resource_t *rsc, const char *value, bool is_default)
 {
     const pcmk_scheduler_t *scheduler = rsc->private->scheduler;
 
     if (pcmk__str_eq(value, PCMK_VALUE_NOTHING, pcmk__str_casei)) {
 
     } else if (pcmk__str_eq(value, PCMK_VALUE_QUORUM, pcmk__str_casei)) {
         pcmk__set_rsc_flags(rsc, pcmk__rsc_needs_quorum);
 
     } else if (pcmk__str_eq(value, PCMK_VALUE_FENCING, pcmk__str_casei)) {
         pcmk__set_rsc_flags(rsc, pcmk__rsc_needs_fencing);
         if (!pcmk_is_set(scheduler->flags, pcmk_sched_fencing_enabled)) {
             pcmk__config_warn("%s requires fencing but fencing is disabled",
                               rsc->id);
         }
 
     } else if (pcmk__str_eq(value, PCMK_VALUE_UNFENCING, pcmk__str_casei)) {
         if (pcmk_is_set(rsc->flags, pcmk__rsc_fence_device)) {
             pcmk__config_warn("Resetting \"" PCMK_META_REQUIRES "\" for %s "
                               "to \"" PCMK_VALUE_QUORUM "\" because fencing "
                               "devices cannot require unfencing", rsc->id);
             unpack_requires(rsc, PCMK_VALUE_QUORUM, true);
             return;
 
         } else if (!pcmk_is_set(scheduler->flags, pcmk_sched_fencing_enabled)) {
             pcmk__config_warn("Resetting \"" PCMK_META_REQUIRES "\" for %s "
                               "to \"" PCMK_VALUE_QUORUM "\" because fencing is "
                               "disabled", rsc->id);
             unpack_requires(rsc, PCMK_VALUE_QUORUM, true);
             return;
 
         } else {
             pcmk__set_rsc_flags(rsc, pcmk__rsc_needs_fencing
                                      |pcmk__rsc_needs_unfencing);
         }
 
     } else {
         const char *orig_value = value;
 
         if (pcmk_is_set(rsc->flags, pcmk__rsc_fence_device)) {
             value = PCMK_VALUE_QUORUM;
 
         } else if (pcmk__is_primitive(rsc)
                    && xml_contains_remote_node(rsc->private->xml)) {
             value = PCMK_VALUE_QUORUM;
 
         } else if (pcmk_is_set(scheduler->flags, pcmk_sched_enable_unfencing)) {
             value = PCMK_VALUE_UNFENCING;
 
         } else if (pcmk_is_set(scheduler->flags, pcmk_sched_fencing_enabled)) {
             value = PCMK_VALUE_FENCING;
 
         } else if (scheduler->no_quorum_policy == pcmk_no_quorum_ignore) {
             value = PCMK_VALUE_NOTHING;
 
         } else {
             value = PCMK_VALUE_QUORUM;
         }
 
         if (orig_value != NULL) {
             pcmk__config_err("Resetting '" PCMK_META_REQUIRES "' for %s "
                              "to '%s' because '%s' is not valid",
                               rsc->id, value, orig_value);
         }
         unpack_requires(rsc, value, true);
         return;
     }
 
     pcmk__rsc_trace(rsc, "\tRequired to start: %s%s", value,
                     (is_default? " (default)" : ""));
 }
 
 static void
 warn_about_deprecated_classes(pcmk_resource_t *rsc)
 {
     const char *std = crm_element_value(rsc->private->xml, PCMK_XA_CLASS);
 
     if (pcmk__str_eq(std, PCMK_RESOURCE_CLASS_UPSTART, pcmk__str_none)) {
         pcmk__warn_once(pcmk__wo_upstart,
                         "Support for Upstart resources (such as %s) is "
                         "deprecated and will be removed in a future release",
                         rsc->id);
 
     } else if (pcmk__str_eq(std, PCMK_RESOURCE_CLASS_NAGIOS, pcmk__str_none)) {
         pcmk__warn_once(pcmk__wo_nagios,
                         "Support for Nagios resources (such as %s) is "
                         "deprecated and will be removed in a future release",
                         rsc->id);
     }
 }
 
 /*!
  * \internal
  * \brief Unpack configuration XML for a given resource
  *
  * Unpack the XML object containing a resource's configuration into a new
  * \c pcmk_resource_t object.
  *
  * \param[in]     xml_obj    XML node containing the resource's configuration
  * \param[out]    rsc        Where to store the unpacked resource information
  * \param[in]     parent     Resource's parent, if any
  * \param[in,out] scheduler  Scheduler data
  *
  * \return Standard Pacemaker return code
  * \note If pcmk_rc_ok is returned, \p *rsc is guaranteed to be non-NULL, and
  *       the caller is responsible for freeing it using its variant-specific
  *       free() method. Otherwise, \p *rsc is guaranteed to be NULL.
  */
 int
 pe__unpack_resource(xmlNode *xml_obj, pcmk_resource_t **rsc,
                     pcmk_resource_t *parent, pcmk_scheduler_t *scheduler)
 {
     xmlNode *expanded_xml = NULL;
     xmlNode *ops = NULL;
     const char *value = NULL;
     const char *id = NULL;
     bool guest_node = false;
     bool remote_node = false;
     pcmk__resource_private_t *rsc_private = NULL;
 
     pe_rule_eval_data_t rule_data = {
         .node_hash = NULL,
         .now = NULL,
         .match_data = NULL,
         .rsc_data = NULL,
         .op_data = NULL
     };
 
     CRM_CHECK(rsc != NULL, return EINVAL);
     CRM_CHECK((xml_obj != NULL) && (scheduler != NULL),
               *rsc = NULL;
               return EINVAL);
 
     rule_data.now = scheduler->now;
 
     crm_log_xml_trace(xml_obj, "[raw XML]");
 
     id = crm_element_value(xml_obj, PCMK_XA_ID);
     if (id == NULL) {
         pcmk__config_err("Ignoring <%s> configuration without " PCMK_XA_ID,
                          xml_obj->name);
         return pcmk_rc_unpack_error;
     }
 
     if (unpack_template(xml_obj, &expanded_xml, scheduler) == FALSE) {
         return pcmk_rc_unpack_error;
     }
 
     *rsc = calloc(1, sizeof(pcmk_resource_t));
     if (*rsc == NULL) {
         pcmk__sched_err("Unable to allocate memory for resource '%s'", id);
         return ENOMEM;
     }
 
     (*rsc)->private = calloc(1, sizeof(pcmk__resource_private_t));
     if ((*rsc)->private == NULL) {
         pcmk__sched_err("Unable to allocate memory for resource '%s'", id);
         free(*rsc);
         return ENOMEM;
     }
     rsc_private = (*rsc)->private;
 
     rsc_private->scheduler = scheduler;
 
     if (expanded_xml) {
         crm_log_xml_trace(expanded_xml, "[expanded XML]");
         rsc_private->xml = expanded_xml;
         rsc_private->orig_xml = xml_obj;
 
     } else {
         rsc_private->xml = xml_obj;
         rsc_private->orig_xml = NULL;
     }
 
     /* Do not use xml_obj from here on, use (*rsc)->xml in case templates are involved */
 
     rsc_private->parent = parent;
 
     ops = pcmk__xe_first_child(rsc_private->xml, PCMK_XE_OPERATIONS, NULL,
                                NULL);
     rsc_private->ops_xml = pcmk__xe_resolve_idref(ops, scheduler->input);
 
     rsc_private->variant = get_resource_type((const char *)
                                              rsc_private->xml->name);
     if (rsc_private->variant == pcmk__rsc_variant_unknown) {
         pcmk__config_err("Ignoring resource '%s' of unknown type '%s'",
                          id, rsc_private->xml->name);
         common_free(*rsc);
         *rsc = NULL;
         return pcmk_rc_unpack_error;
     }
 
     (*rsc)->meta = pcmk__strkey_table(free, free);
     (*rsc)->allowed_nodes = pcmk__strkey_table(NULL, free);
     (*rsc)->known_on = pcmk__strkey_table(NULL, free);
 
     value = crm_element_value(rsc_private->xml, PCMK__META_CLONE);
     if (value) {
         (*rsc)->id = crm_strdup_printf("%s:%s", id, value);
         pcmk__insert_meta(*rsc, PCMK__META_CLONE, value);
 
     } else {
         (*rsc)->id = strdup(id);
     }
 
     warn_about_deprecated_classes(*rsc);
 
     rsc_private->fns = &resource_class_functions[rsc_private->variant];
 
     get_meta_attributes((*rsc)->meta, *rsc, NULL, scheduler);
     (*rsc)->parameters = pe_rsc_params(*rsc, NULL, scheduler); // \deprecated
 
     (*rsc)->flags = 0;
     pcmk__set_rsc_flags(*rsc, pcmk__rsc_unassigned);
 
     if (!pcmk_is_set(scheduler->flags, pcmk_sched_in_maintenance)) {
         pcmk__set_rsc_flags(*rsc, pcmk__rsc_managed);
     }
 
     (*rsc)->role = pcmk_role_stopped;
     (*rsc)->next_role = pcmk_role_unknown;
 
     rsc_private->ban_after_failures = PCMK_SCORE_INFINITY;
 
     value = g_hash_table_lookup((*rsc)->meta, PCMK_META_PRIORITY);
     rsc_private->priority = char2score(value);
 
     value = g_hash_table_lookup((*rsc)->meta, PCMK_META_CRITICAL);
     if ((value == NULL) || crm_is_true(value)) {
         pcmk__set_rsc_flags(*rsc, pcmk__rsc_critical);
     }
 
     value = g_hash_table_lookup((*rsc)->meta, PCMK_META_NOTIFY);
     if (crm_is_true(value)) {
         pcmk__set_rsc_flags(*rsc, pcmk__rsc_notify);
     }
 
     if (xml_contains_remote_node(rsc_private->xml)) {
         pcmk__set_rsc_flags(*rsc, pcmk__rsc_is_remote_connection);
         if (g_hash_table_lookup((*rsc)->meta, PCMK__META_CONTAINER)) {
             guest_node = true;
         } else {
             remote_node = true;
         }
     }
 
     value = g_hash_table_lookup((*rsc)->meta, PCMK_META_ALLOW_MIGRATE);
     if (crm_is_true(value)) {
         pcmk__set_rsc_flags(*rsc, pcmk__rsc_migratable);
     } else if ((value == NULL) && remote_node) {
         /* By default, we want remote nodes to be able
          * to float around the cluster without having to stop all the
          * resources within the remote-node before moving. Allowing
          * migration support enables this feature. If this ever causes
          * problems, migration support can be explicitly turned off with
          * PCMK_META_ALLOW_MIGRATE=false.
          */
         pcmk__set_rsc_flags(*rsc, pcmk__rsc_migratable);
     }
 
     value = g_hash_table_lookup((*rsc)->meta, PCMK_META_IS_MANAGED);
     if (value != NULL) {
         if (pcmk__str_eq(PCMK_VALUE_DEFAULT, value, pcmk__str_casei)) {
             // @COMPAT Deprecated since 2.1.8
             pcmk__config_warn("Support for setting " PCMK_META_IS_MANAGED
                               " to the explicit value '" PCMK_VALUE_DEFAULT
                               "' is deprecated and will be removed in a "
                               "future release (just leave it unset)");
         } else if (crm_is_true(value)) {
             pcmk__set_rsc_flags(*rsc, pcmk__rsc_managed);
         } else {
             pcmk__clear_rsc_flags(*rsc, pcmk__rsc_managed);
         }
     }
 
     value = g_hash_table_lookup((*rsc)->meta, PCMK_META_MAINTENANCE);
     if (crm_is_true(value)) {
         pcmk__clear_rsc_flags(*rsc, pcmk__rsc_managed);
         pcmk__set_rsc_flags(*rsc, pcmk__rsc_maintenance);
     }
     if (pcmk_is_set(scheduler->flags, pcmk_sched_in_maintenance)) {
         pcmk__clear_rsc_flags(*rsc, pcmk__rsc_managed);
         pcmk__set_rsc_flags(*rsc, pcmk__rsc_maintenance);
     }
 
     if (pcmk__is_clone(pe__const_top_resource(*rsc, false))) {
         value = g_hash_table_lookup((*rsc)->meta, PCMK_META_GLOBALLY_UNIQUE);
         if (crm_is_true(value)) {
             pcmk__set_rsc_flags(*rsc, pcmk__rsc_unique);
         }
         if (detect_promotable(*rsc)) {
             pcmk__set_rsc_flags(*rsc, pcmk__rsc_promotable);
         }
     } else {
         pcmk__set_rsc_flags(*rsc, pcmk__rsc_unique);
     }
 
     // @COMPAT Deprecated meta-attribute
     value = g_hash_table_lookup((*rsc)->meta, PCMK__META_RESTART_TYPE);
     if (pcmk__str_eq(value, PCMK_VALUE_RESTART, pcmk__str_casei)) {
         rsc_private->restart_type = pcmk__restart_restart;
         pcmk__rsc_trace(*rsc, "%s dependency restart handling: restart",
                         (*rsc)->id);
         pcmk__warn_once(pcmk__wo_restart_type,
                         "Support for " PCMK__META_RESTART_TYPE " is deprecated "
                         "and will be removed in a future release");
 
     } else {
         rsc_private->restart_type = pcmk__restart_ignore;
         pcmk__rsc_trace(*rsc, "%s dependency restart handling: ignore",
                         (*rsc)->id);
     }
 
     value = g_hash_table_lookup((*rsc)->meta, PCMK_META_MULTIPLE_ACTIVE);
     if (pcmk__str_eq(value, PCMK_VALUE_STOP_ONLY, pcmk__str_casei)) {
         rsc_private->multiply_active_policy = pcmk__multiply_active_stop;
         pcmk__rsc_trace(*rsc, "%s multiple running resource recovery: stop only",
                         (*rsc)->id);
 
     } else if (pcmk__str_eq(value, PCMK_VALUE_BLOCK, pcmk__str_casei)) {
         rsc_private->multiply_active_policy = pcmk__multiply_active_block;
         pcmk__rsc_trace(*rsc, "%s multiple running resource recovery: block",
                         (*rsc)->id);
 
     } else if (pcmk__str_eq(value, PCMK_VALUE_STOP_UNEXPECTED,
                             pcmk__str_casei)) {
         rsc_private->multiply_active_policy = pcmk__multiply_active_unexpected;
         pcmk__rsc_trace(*rsc,
                         "%s multiple running resource recovery: "
                         "stop unexpected instances",
                         (*rsc)->id);
 
     } else { // PCMK_VALUE_STOP_START
         if (!pcmk__str_eq(value, PCMK_VALUE_STOP_START,
                           pcmk__str_casei|pcmk__str_null_matches)) {
             pcmk__config_warn("%s is not a valid value for "
                               PCMK_META_MULTIPLE_ACTIVE
                               ", using default of "
                               "\"" PCMK_VALUE_STOP_START "\"",
                               value);
         }
         rsc_private->multiply_active_policy = pcmk__multiply_active_restart;
         pcmk__rsc_trace(*rsc,
                         "%s multiple running resource recovery: stop/start",
                         (*rsc)->id);
     }
 
     value = g_hash_table_lookup((*rsc)->meta, PCMK_META_RESOURCE_STICKINESS);
     if (value != NULL) {
         if (pcmk__str_eq(PCMK_VALUE_DEFAULT, value, pcmk__str_casei)) {
             // @COMPAT Deprecated since 2.1.8
             pcmk__config_warn("Support for setting "
                               PCMK_META_RESOURCE_STICKINESS
                               " to the explicit value '" PCMK_VALUE_DEFAULT
                               "' is deprecated and will be removed in a "
                               "future release (just leave it unset)");
         } else {
             rsc_private->stickiness = char2score(value);
         }
     }
 
     value = g_hash_table_lookup((*rsc)->meta, PCMK_META_MIGRATION_THRESHOLD);
     if (value != NULL) {
         if (pcmk__str_eq(PCMK_VALUE_DEFAULT, value, pcmk__str_casei)) {
             // @COMPAT Deprecated since 2.1.8
             pcmk__config_warn("Support for setting "
                               PCMK_META_MIGRATION_THRESHOLD
                               " to the explicit value '" PCMK_VALUE_DEFAULT
                               "' is deprecated and will be removed in a "
                               "future release (just leave it unset)");
         } else {
             rsc_private->ban_after_failures = char2score(value);
             if (rsc_private->ban_after_failures < 0) {
                 /* @COMPAT We use 1 here to preserve previous behavior, but this
                  * should probably use the default (INFINITY) or 0 (to disable)
                  * instead.
                  */
                 pcmk__warn_once(pcmk__wo_neg_threshold,
                                 PCMK_META_MIGRATION_THRESHOLD
                                 " must be non-negative, using 1 instead");
                 rsc_private->ban_after_failures = 1;
             }
         }
     }
 
     if (pcmk__str_eq(crm_element_value(rsc_private->xml, PCMK_XA_CLASS),
                      PCMK_RESOURCE_CLASS_STONITH, pcmk__str_casei)) {
         pcmk__set_scheduler_flags(scheduler, pcmk_sched_have_fencing);
         pcmk__set_rsc_flags(*rsc, pcmk__rsc_fence_device);
     }
 
     value = g_hash_table_lookup((*rsc)->meta, PCMK_META_REQUIRES);
     unpack_requires(*rsc, value, false);
 
     value = g_hash_table_lookup((*rsc)->meta, PCMK_META_FAILURE_TIMEOUT);
     if (value != NULL) {
         pcmk_parse_interval_spec(value, &(rsc_private->failure_expiration_ms));
     }
 
     if (remote_node) {
         GHashTable *params = pe_rsc_params(*rsc, NULL, scheduler);
 
         /* Grabbing the value now means that any rules based on node attributes
          * will evaluate to false, so such rules should not be used with
          * PCMK_REMOTE_RA_RECONNECT_INTERVAL.
          *
          * @TODO Evaluate per node before using
          */
         value = g_hash_table_lookup(params, PCMK_REMOTE_RA_RECONNECT_INTERVAL);
         if (value) {
             /* reconnect delay works by setting failure_timeout and preventing the
              * connection from starting until the failure is cleared. */
             pcmk_parse_interval_spec(value,
                                      &(rsc_private->remote_reconnect_ms));
 
             /* We want to override any default failure_timeout in use when remote
              * PCMK_REMOTE_RA_RECONNECT_INTERVAL is in use.
              */
             rsc_private->failure_expiration_ms =
                 rsc_private->remote_reconnect_ms;
         }
     }
 
     get_target_role(*rsc, &((*rsc)->next_role));
     pcmk__rsc_trace(*rsc, "%s desired next state: %s", (*rsc)->id,
                     ((*rsc)->next_role == pcmk_role_unknown)?
                         "default" : pcmk_role_text((*rsc)->next_role));
 
     if (rsc_private->fns->unpack(*rsc, scheduler) == FALSE) {
         rsc_private->fns->free(*rsc);
         *rsc = NULL;
         return pcmk_rc_unpack_error;
     }
 
     if (pcmk_is_set(scheduler->flags, pcmk_sched_symmetric_cluster)) {
         // This tag must stay exactly the same because it is tested elsewhere
         resource_location(*rsc, NULL, 0, "symmetric_default", scheduler);
     } else if (guest_node) {
         /* remote resources tied to a container resource must always be allowed
          * to opt-in to the cluster. Whether the connection resource is actually
          * allowed to be placed on a node is dependent on the container resource */
         resource_location(*rsc, NULL, 0, "remote_connection_default",
                           scheduler);
     }
 
     pcmk__rsc_trace(*rsc, "%s action notification: %s", (*rsc)->id,
                     pcmk_is_set((*rsc)->flags, pcmk__rsc_notify)? "required" : "not required");
 
     (*rsc)->utilization = pcmk__strkey_table(free, free);
 
     pe__unpack_dataset_nvpairs(rsc_private->xml, PCMK_XE_UTILIZATION,
                                &rule_data, (*rsc)->utilization, NULL, FALSE,
                                scheduler);
 
     if (expanded_xml) {
         if (add_template_rsc(xml_obj, scheduler) == FALSE) {
             rsc_private->fns->free(*rsc);
             *rsc = NULL;
             return pcmk_rc_unpack_error;
         }
     }
     return pcmk_rc_ok;
 }
 
 gboolean
 is_parent(pcmk_resource_t *child, pcmk_resource_t *rsc)
 {
     pcmk_resource_t *parent = child;
 
     if (parent == NULL || rsc == NULL) {
         return FALSE;
     }
     while (parent->private->parent != NULL) {
         if (parent->private->parent == rsc) {
             return TRUE;
         }
         parent = parent->private->parent;
     }
     return FALSE;
 }
 
 pcmk_resource_t *
 uber_parent(pcmk_resource_t *rsc)
 {
     pcmk_resource_t *parent = rsc;
 
     if (parent == NULL) {
         return NULL;
     }
     while ((parent->private->parent != NULL)
            && !pcmk__is_bundle(parent->private->parent)) {
         parent = parent->private->parent;
     }
     return parent;
 }
 
 /*!
  * \internal
  * \brief Get the topmost parent of a resource as a const pointer
  *
  * \param[in] rsc             Resource to check
  * \param[in] include_bundle  If true, go all the way to bundle
  *
  * \return \p NULL if \p rsc is NULL, \p rsc if \p rsc has no parent,
  *         the bundle if \p rsc is bundled and \p include_bundle is true,
  *         otherwise the topmost parent of \p rsc up to a clone
  */
 const pcmk_resource_t *
 pe__const_top_resource(const pcmk_resource_t *rsc, bool include_bundle)
 {
     const pcmk_resource_t *parent = rsc;
 
     if (parent == NULL) {
         return NULL;
     }
     while (parent->private->parent != NULL) {
         if (!include_bundle && pcmk__is_bundle(parent->private->parent)) {
             break;
         }
         parent = parent->private->parent;
     }
     return parent;
 }
 
 void
 common_free(pcmk_resource_t * rsc)
 {
     if (rsc == NULL) {
         return;
     }
 
     pcmk__rsc_trace(rsc, "Freeing %s %s",
                     (const char *) rsc->private->xml->name, rsc->id);
 
     g_list_free(rsc->dangling_migrations);
 
     if (rsc->parameter_cache != NULL) {
         g_hash_table_destroy(rsc->parameter_cache);
     }
     if (rsc->meta != NULL) {
         g_hash_table_destroy(rsc->meta);
     }
     if (rsc->utilization != NULL) {
         g_hash_table_destroy(rsc->utilization);
     }
 
     if ((rsc->private->parent == NULL)
         && pcmk_is_set(rsc->flags, pcmk__rsc_removed)) {
 
         pcmk__xml_free(rsc->private->xml);
         rsc->private->xml = NULL;
         pcmk__xml_free(rsc->private->orig_xml);
         rsc->private->orig_xml = NULL;
 
     } else if (rsc->private->orig_xml != NULL) {
         // rsc->private->xml was expanded from a template
         pcmk__xml_free(rsc->private->xml);
         rsc->private->xml = NULL;
     }
     if (rsc->running_on) {
         g_list_free(rsc->running_on);
         rsc->running_on = NULL;
     }
     if (rsc->known_on) {
         g_hash_table_destroy(rsc->known_on);
         rsc->known_on = NULL;
     }
     if (rsc->allowed_nodes) {
         g_hash_table_destroy(rsc->allowed_nodes);
         rsc->allowed_nodes = NULL;
     }
     g_list_free(rsc->fillers);
     free(rsc->id);
-    free(rsc->allocated_to);
 
     free(rsc->private->variant_opaque);
     free(rsc->private->history_id);
     free(rsc->private->pending_action);
+    free(rsc->private->assigned_node);
     g_list_free(rsc->private->actions);
     g_list_free(rsc->private->with_this_colocations);
     g_list_free(rsc->private->this_with_colocations);
     g_list_free(rsc->private->location_constraints);
     g_list_free(rsc->private->ticket_constraints);
     free(rsc->private);
 
     free(rsc);
 }
 
 /*!
  * \internal
  * \brief Count a node and update most preferred to it as appropriate
  *
  * \param[in]     rsc          An active resource
  * \param[in]     node         A node that \p rsc is active on
  * \param[in,out] active       This will be set to \p node if \p node is more
  *                             preferred than the current value
  * \param[in,out] count_all    If not NULL, this will be incremented
  * \param[in,out] count_clean  If not NULL, this will be incremented if \p node
  *                             is online and clean
  *
  * \return true if the count should continue, or false if sufficiently known
  */
 bool
 pe__count_active_node(const pcmk_resource_t *rsc, pcmk_node_t *node,
                       pcmk_node_t **active, unsigned int *count_all,
                       unsigned int *count_clean)
 {
     bool keep_looking = false;
     bool is_happy = false;
 
     CRM_CHECK((rsc != NULL) && (node != NULL) && (active != NULL),
               return false);
 
     is_happy = node->details->online && !node->details->unclean;
 
     if (count_all != NULL) {
         ++*count_all;
     }
     if ((count_clean != NULL) && is_happy) {
         ++*count_clean;
     }
     if ((count_all != NULL) || (count_clean != NULL)) {
         keep_looking = true; // We're counting, so go through entire list
     }
 
     if (rsc->partial_migration_source != NULL) {
         if (pcmk__same_node(node, rsc->partial_migration_source)) {
             *active = node; // This is the migration source
         } else {
             keep_looking = true;
         }
     } else if (!pcmk_is_set(rsc->flags, pcmk__rsc_needs_fencing)) {
         if (is_happy && ((*active == NULL) || !(*active)->details->online
                          || (*active)->details->unclean)) {
             *active = node; // This is the first clean node
         } else {
             keep_looking = true;
         }
     }
     if (*active == NULL) {
         *active = node; // This is the first node checked
     }
     return keep_looking;
 }
 
 // Shared implementation of pcmk__rsc_methods_t:active_node()
 static pcmk_node_t *
 active_node(const pcmk_resource_t *rsc, unsigned int *count_all,
             unsigned int *count_clean)
 {
     pcmk_node_t *active = NULL;
 
     if (count_all != NULL) {
         *count_all = 0;
     }
     if (count_clean != NULL) {
         *count_clean = 0;
     }
     if (rsc == NULL) {
         return NULL;
     }
     for (GList *iter = rsc->running_on; iter != NULL; iter = iter->next) {
         if (!pe__count_active_node(rsc, (pcmk_node_t *) iter->data, &active,
                                    count_all, count_clean)) {
             break; // Don't waste time iterating if we don't have to
         }
     }
     return active;
 }
 
 /*!
  * \brief
  * \internal Find and count active nodes according to \c PCMK_META_REQUIRES
  *
  * \param[in]  rsc    Resource to check
  * \param[out] count  If not NULL, will be set to count of active nodes
  *
  * \return An active node (or NULL if resource is not active anywhere)
  *
  * \note This is a convenience wrapper for active_node() where the count of all
  *       active nodes or only clean active nodes is desired according to the
  *       \c PCMK_META_REQUIRES meta-attribute.
  */
 pcmk_node_t *
 pe__find_active_requires(const pcmk_resource_t *rsc, unsigned int *count)
 {
     if (rsc == NULL) {
         if (count != NULL) {
             *count = 0;
         }
         return NULL;
     }
 
     if (pcmk_is_set(rsc->flags, pcmk__rsc_needs_fencing)) {
         return rsc->private->fns->active_node(rsc, count, NULL);
     } else {
         return rsc->private->fns->active_node(rsc, NULL, count);
     }
 }
 
 void
 pe__count_common(pcmk_resource_t *rsc)
 {
     if (rsc->children != NULL) {
         for (GList *item = rsc->children; item != NULL; item = item->next) {
             pcmk_resource_t *child = item->data;
 
             child->private->fns->count(item->data);
         }
 
     } else if (!pcmk_is_set(rsc->flags, pcmk__rsc_removed)
                || (rsc->role > pcmk_role_stopped)) {
         rsc->private->scheduler->ninstances++;
         if (pe__resource_is_disabled(rsc)) {
             rsc->private->scheduler->disabled_resources++;
         }
         if (pcmk_is_set(rsc->flags, pcmk__rsc_blocked)) {
             rsc->private->scheduler->blocked_resources++;
         }
     }
 }
 
 /*!
  * \internal
  * \brief Update a resource's next role
  *
  * \param[in,out] rsc   Resource to be updated
  * \param[in]     role  Resource's new next role
  * \param[in]     why   Human-friendly reason why role is changing (for logs)
  */
 void
 pe__set_next_role(pcmk_resource_t *rsc, enum rsc_role_e role, const char *why)
 {
     CRM_ASSERT((rsc != NULL) && (why != NULL));
     if (rsc->next_role != role) {
         pcmk__rsc_trace(rsc, "Resetting next role for %s from %s to %s (%s)",
                         rsc->id, pcmk_role_text(rsc->next_role),
                         pcmk_role_text(role), why);
         rsc->next_role = role;
     }
 }
diff --git a/lib/pengine/native.c b/lib/pengine/native.c
index 12a7c2b7ff..b8c9da7c2b 100644
--- a/lib/pengine/native.c
+++ b/lib/pengine/native.c
@@ -1,1170 +1,1170 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <stdint.h>
 
 #include <crm/common/output.h>
 #include <crm/pengine/rules.h>
 #include <crm/pengine/status.h>
 #include <crm/pengine/complex.h>
 #include <crm/pengine/internal.h>
 #include <crm/common/xml.h>
 #include <pe_status_private.h>
 
 /*!
  * \internal
  * \brief Check whether a resource is active on multiple nodes
  */
 static bool
 is_multiply_active(const pcmk_resource_t *rsc)
 {
     unsigned int count = 0;
 
     if (pcmk__is_primitive(rsc)) {
         pe__find_active_requires(rsc, &count);
     }
     return count > 1;
 }
 
 static void
 native_priority_to_node(pcmk_resource_t *rsc, pcmk_node_t *node,
                         gboolean failed)
 {
     int priority = 0;
     if ((rsc->private->priority == 0) || failed) {
         return;
     }
 
     if (rsc->role == pcmk_role_promoted) {
         // Promoted instance takes base priority + 1
         priority = rsc->private->priority + 1;
 
     } else {
         priority = rsc->private->priority;
     }
 
     node->details->priority += priority;
     pcmk__rsc_trace(rsc, "%s now has priority %d with %s'%s' (priority: %d%s)",
                     pcmk__node_name(node), node->details->priority,
                     (rsc->role == pcmk_role_promoted)? "promoted " : "",
                     rsc->id, rsc->private->priority,
                     (rsc->role == pcmk_role_promoted)? " + 1" : "");
 
     /* Priority of a resource running on a guest node is added to the cluster
      * node as well. */
     if (node->details->remote_rsc
         && node->details->remote_rsc->container) {
         GList *gIter = node->details->remote_rsc->container->running_on;
 
         for (; gIter != NULL; gIter = gIter->next) {
             pcmk_node_t *a_node = gIter->data;
 
             a_node->details->priority += priority;
             pcmk__rsc_trace(rsc,
                             "%s now has priority %d with %s'%s' "
                             "(priority: %d%s) from guest node %s",
                             pcmk__node_name(a_node), a_node->details->priority,
                             (rsc->role == pcmk_role_promoted)? "promoted " : "",
                             rsc->id, rsc->private->priority,
                             (rsc->role == pcmk_role_promoted)? " + 1" : "",
                             pcmk__node_name(node));
         }
     }
 }
 
 void
 native_add_running(pcmk_resource_t *rsc, pcmk_node_t *node,
                    pcmk_scheduler_t *scheduler, gboolean failed)
 {
     pcmk_resource_t *parent = rsc->private->parent;
     GList *gIter = rsc->running_on;
 
     CRM_CHECK(node != NULL, return);
     for (; gIter != NULL; gIter = gIter->next) {
         pcmk_node_t *a_node = (pcmk_node_t *) gIter->data;
 
         CRM_CHECK(a_node != NULL, return);
         if (pcmk__str_eq(a_node->details->id, node->details->id, pcmk__str_casei)) {
             return;
         }
     }
 
     pcmk__rsc_trace(rsc, "Adding %s to %s %s", rsc->id, pcmk__node_name(node),
                     pcmk_is_set(rsc->flags, pcmk__rsc_managed)? "" : "(unmanaged)");
 
     rsc->running_on = g_list_append(rsc->running_on, node);
     if (pcmk__is_primitive(rsc)) {
         node->details->running_rsc = g_list_append(node->details->running_rsc, rsc);
         native_priority_to_node(rsc, node, failed);
         if (node->details->maintenance) {
             pcmk__clear_rsc_flags(rsc, pcmk__rsc_managed);
             pcmk__set_rsc_flags(rsc, pcmk__rsc_maintenance);
         }
     }
 
     if (!pcmk_is_set(rsc->flags, pcmk__rsc_managed)) {
         pcmk_resource_t *p = parent;
 
         pcmk__rsc_info(rsc, "resource %s isn't managed", rsc->id);
         resource_location(rsc, node, PCMK_SCORE_INFINITY,
                           "not_managed_default", scheduler);
 
         while(p && node->details->online) {
             /* add without the additional location constraint */
             p->running_on = g_list_append(p->running_on, node);
             p = p->private->parent;
         }
         return;
     }
 
     if (is_multiply_active(rsc)) {
         switch (rsc->private->multiply_active_policy) {
             case pcmk__multiply_active_stop:
                 {
                     GHashTableIter gIter;
                     pcmk_node_t *local_node = NULL;
 
                     /* make sure it doesn't come up again */
                     if (rsc->allowed_nodes != NULL) {
                         g_hash_table_destroy(rsc->allowed_nodes);
                     }
                     rsc->allowed_nodes = pe__node_list2table(scheduler->nodes);
                     g_hash_table_iter_init(&gIter, rsc->allowed_nodes);
                     while (g_hash_table_iter_next(&gIter, NULL, (void **)&local_node)) {
                         local_node->weight = -PCMK_SCORE_INFINITY;
                     }
                 }
                 break;
             case pcmk__multiply_active_block:
                 pcmk__clear_rsc_flags(rsc, pcmk__rsc_managed);
                 pcmk__set_rsc_flags(rsc, pcmk__rsc_blocked);
 
                 /* If the resource belongs to a group or bundle configured with
                  * PCMK_META_MULTIPLE_ACTIVE=PCMK_VALUE_BLOCK, block the entire
                  * entity.
                  */
                 if ((pcmk__is_group(parent) || pcmk__is_bundle(parent))
                     && (parent->private->multiply_active_policy
                         == pcmk__multiply_active_block)) {
 
                     for (GList *gIter = parent->children;
                          gIter != NULL; gIter = gIter->next) {
                         pcmk_resource_t *child = gIter->data;
 
                         pcmk__clear_rsc_flags(child, pcmk__rsc_managed);
                         pcmk__set_rsc_flags(child, pcmk__rsc_blocked);
                     }
                 }
                 break;
 
             // pcmk__multiply_active_restart, pcmk__multiply_active_unexpected
             default:
                 /* The scheduler will do the right thing because the relevant
                  * variables and flags are set when unpacking the history.
                  */
                 break;
         }
         crm_debug("%s is active on multiple nodes including %s: %s",
                   rsc->id, pcmk__node_name(node),
                   pcmk__multiply_active_text(rsc));
 
     } else {
         pcmk__rsc_trace(rsc, "Resource %s is active on %s",
                         rsc->id, pcmk__node_name(node));
     }
 
     if (parent != NULL) {
         native_add_running(parent, node, scheduler, FALSE);
     }
 }
 
 static void
 recursive_clear_unique(pcmk_resource_t *rsc, gpointer user_data)
 {
     pcmk__clear_rsc_flags(rsc, pcmk__rsc_unique);
     pcmk__insert_meta(rsc, PCMK_META_GLOBALLY_UNIQUE, PCMK_VALUE_FALSE);
     g_list_foreach(rsc->children, (GFunc) recursive_clear_unique, NULL);
 }
 
 gboolean
 native_unpack(pcmk_resource_t *rsc, pcmk_scheduler_t *scheduler)
 {
     pcmk_resource_t *parent = uber_parent(rsc);
     const char *standard = crm_element_value(rsc->private->xml, PCMK_XA_CLASS);
     uint32_t ra_caps = pcmk_get_ra_caps(standard);
 
     pcmk__rsc_trace(rsc, "Processing resource %s...", rsc->id);
 
     // Only some agent standards support unique and promotable clones
     if (!pcmk_is_set(ra_caps, pcmk_ra_cap_unique)
         && pcmk_is_set(rsc->flags, pcmk__rsc_unique)
         && pcmk__is_clone(parent)) {
 
         /* @COMPAT We should probably reject this situation as an error (as we
          * do for promotable below) rather than warn and convert, but that would
          * be a backward-incompatible change that we should probably do with a
          * transform at a schema major version bump.
          */
         pe__force_anon(standard, parent, rsc->id, scheduler);
 
         /* Clear PCMK_META_GLOBALLY_UNIQUE on the parent and all its descendants
          * unpacked so far (clearing the parent should make any future children
          * unpacking correct). We have to clear this resource explicitly because
          * it isn't hooked into the parent's children yet.
          */
         recursive_clear_unique(parent, NULL);
         recursive_clear_unique(rsc, NULL);
     }
     if (!pcmk_is_set(ra_caps, pcmk_ra_cap_promotable)
         && pcmk_is_set(parent->flags, pcmk__rsc_promotable)) {
 
         pcmk__config_err("Resource %s is of type %s and therefore "
                          "cannot be used as a promotable clone resource",
                          rsc->id, standard);
         return FALSE;
     }
     return TRUE;
 }
 
 static bool
 rsc_is_on_node(pcmk_resource_t *rsc, const pcmk_node_t *node, int flags)
 {
     pcmk__rsc_trace(rsc, "Checking whether %s is on %s",
                     rsc->id, pcmk__node_name(node));
 
     if (pcmk_is_set(flags, pcmk_rsc_match_current_node)
         && (rsc->running_on != NULL)) {
 
         for (GList *iter = rsc->running_on; iter; iter = iter->next) {
             if (pcmk__same_node((pcmk_node_t *) iter->data, node)) {
                 return true;
             }
         }
 
     } else if (pcmk_is_set(flags, pe_find_inactive) // @COMPAT deprecated
                && (rsc->running_on == NULL)) {
         return true;
 
     } else if (!pcmk_is_set(flags, pcmk_rsc_match_current_node)
-               && (rsc->allocated_to != NULL)
-               && pcmk__same_node(rsc->allocated_to, node)) {
+               && (rsc->private->assigned_node != NULL)
+               && pcmk__same_node(rsc->private->assigned_node, node)) {
         return true;
     }
     return false;
 }
 
 pcmk_resource_t *
 native_find_rsc(pcmk_resource_t *rsc, const char *id,
                 const pcmk_node_t *on_node, int flags)
 {
     bool match = false;
     pcmk_resource_t *result = NULL;
 
     CRM_CHECK(id && rsc && rsc->id, return NULL);
 
     if (pcmk_is_set(flags, pcmk_rsc_match_clone_only)) {
         const char *rid = pcmk__xe_id(rsc->private->xml);
 
         if (!pcmk__is_clone(pe__const_top_resource(rsc, false))) {
             match = false;
 
         } else if (!strcmp(id, rsc->id) || pcmk__str_eq(id, rid, pcmk__str_none)) {
             match = true;
         }
 
     } else if (!strcmp(id, rsc->id)) {
         match = true;
 
     } else if (pcmk_is_set(flags, pcmk_rsc_match_history)
                && pcmk__str_eq(rsc->private->history_id, id, pcmk__str_none)) {
         match = true;
 
     } else if (pcmk_is_set(flags, pcmk_rsc_match_basename)
                || (pcmk_is_set(flags, pcmk_rsc_match_anon_basename)
                    && !pcmk_is_set(rsc->flags, pcmk__rsc_unique))) {
         match = pe_base_name_eq(rsc, id);
     }
 
     if (match && on_node) {
         if (!rsc_is_on_node(rsc, on_node, flags)) {
             match = false;
         }
     }
 
     if (match) {
         return rsc;
     }
 
     for (GList *gIter = rsc->children; gIter != NULL; gIter = gIter->next) {
         pcmk_resource_t *child = (pcmk_resource_t *) gIter->data;
 
         result = rsc->private->fns->find_rsc(child, id, on_node, flags);
         if (result) {
             return result;
         }
     }
     return NULL;
 }
 
 // create is ignored
 char *
 native_parameter(pcmk_resource_t *rsc, pcmk_node_t *node, gboolean create,
                  const char *name, pcmk_scheduler_t *scheduler)
 {
     const char *value = NULL;
     GHashTable *params = NULL;
 
     CRM_CHECK(rsc != NULL, return NULL);
     CRM_CHECK(name != NULL && strlen(name) != 0, return NULL);
 
     pcmk__rsc_trace(rsc, "Looking up %s in %s", name, rsc->id);
     params = pe_rsc_params(rsc, node, scheduler);
     value = g_hash_table_lookup(params, name);
     if (value == NULL) {
         /* try meta attributes instead */
         value = g_hash_table_lookup(rsc->meta, name);
     }
     return pcmk__str_copy(value);
 }
 
 gboolean
 native_active(pcmk_resource_t * rsc, gboolean all)
 {
     for (GList *gIter = rsc->running_on; gIter != NULL; gIter = gIter->next) {
         pcmk_node_t *a_node = (pcmk_node_t *) gIter->data;
 
         if (a_node->details->unclean) {
             pcmk__rsc_trace(rsc, "Resource %s: %s is unclean",
                             rsc->id, pcmk__node_name(a_node));
             return TRUE;
         } else if (!a_node->details->online
                    && pcmk_is_set(rsc->flags, pcmk__rsc_managed)) {
             pcmk__rsc_trace(rsc, "Resource %s: %s is offline",
                             rsc->id, pcmk__node_name(a_node));
         } else {
             pcmk__rsc_trace(rsc, "Resource %s active on %s",
                             rsc->id, pcmk__node_name(a_node));
             return TRUE;
         }
     }
     return FALSE;
 }
 
 struct print_data_s {
     long options;
     void *print_data;
 };
 
 static const char *
 native_pending_state(const pcmk_resource_t *rsc)
 {
     const char *pending_state = NULL;
 
     if (pcmk__str_eq(rsc->private->pending_action, PCMK_ACTION_START,
                      pcmk__str_none)) {
         pending_state = "Starting";
 
     } else if (pcmk__str_eq(rsc->private->pending_action, PCMK_ACTION_STOP,
                             pcmk__str_none)) {
         pending_state = "Stopping";
 
     } else if (pcmk__str_eq(rsc->private->pending_action, PCMK_ACTION_MIGRATE_TO,
                             pcmk__str_none)) {
         pending_state = "Migrating";
 
     } else if (pcmk__str_eq(rsc->private->pending_action,
                             PCMK_ACTION_MIGRATE_FROM, pcmk__str_none)) {
        /* Work might be done in here. */
         pending_state = "Migrating";
 
     } else if (pcmk__str_eq(rsc->private->pending_action, PCMK_ACTION_PROMOTE,
                             pcmk__str_none)) {
         pending_state = "Promoting";
 
     } else if (pcmk__str_eq(rsc->private->pending_action, PCMK_ACTION_DEMOTE,
                             pcmk__str_none)) {
         pending_state = "Demoting";
     }
 
     return pending_state;
 }
 
 static const char *
 native_pending_action(const pcmk_resource_t *rsc)
 {
     const char *pending_action = NULL;
 
     if (pcmk__str_eq(rsc->private->pending_action, PCMK_ACTION_MONITOR,
                      pcmk__str_none)) {
         pending_action = "Monitoring";
 
     /* Pending probes are not printed, even if pending
      * operations are requested. If someone ever requests that
      * behavior, uncomment this and the corresponding part of
      * unpack.c:unpack_rsc_op().
      */
 #if 0
     } else if (pcmk__str_eq(rsc->private->pending_action, "probe",
                             pcmk__str_none)) {
         pending_action = "Checking";
 #endif
     }
 
     return pending_action;
 }
 
 static enum rsc_role_e
 native_displayable_role(const pcmk_resource_t *rsc)
 {
     enum rsc_role_e role = rsc->role;
 
     if ((role == pcmk_role_started)
         && pcmk_is_set(pe__const_top_resource(rsc, false)->flags,
                        pcmk__rsc_promotable)) {
 
         role = pcmk_role_unpromoted;
     }
     return role;
 }
 
 static const char *
 native_displayable_state(const pcmk_resource_t *rsc, bool print_pending)
 {
     const char *rsc_state = NULL;
 
     if (print_pending) {
         rsc_state = native_pending_state(rsc);
     }
     if (rsc_state == NULL) {
         rsc_state = pcmk_role_text(native_displayable_role(rsc));
     }
     return rsc_state;
 }
 
 // Append a flag to resource description string's flags list
 static bool
 add_output_flag(GString *s, const char *flag_desc, bool have_flags)
 {
     g_string_append(s, (have_flags? ", " : " ("));
     g_string_append(s, flag_desc);
     return true;
 }
 
 // Append a node name to resource description string's node list
 static bool
 add_output_node(GString *s, const char *node, bool have_nodes)
 {
     g_string_append(s, (have_nodes? " " : " [ "));
     g_string_append(s, node);
     return true;
 }
 
 /*!
  * \internal
  * \brief Create a string description of a resource
  *
  * \param[in] rsc          Resource to describe
  * \param[in] name         Desired identifier for the resource
  * \param[in] node         If not NULL, node that resource is "on"
  * \param[in] show_opts    Bitmask of pcmk_show_opt_e.
  * \param[in] target_role  Resource's target role
  * \param[in] show_nodes   Whether to display nodes when multiply active
  *
  * \return Newly allocated string description of resource
  * \note Caller must free the result with g_free().
  */
 gchar *
 pcmk__native_output_string(const pcmk_resource_t *rsc, const char *name,
                            const pcmk_node_t *node, uint32_t show_opts,
                            const char *target_role, bool show_nodes)
 {
     const char *class = crm_element_value(rsc->private->xml, PCMK_XA_CLASS);
     const char *provider = NULL;
     const char *kind = crm_element_value(rsc->private->xml, PCMK_XA_TYPE);
     GString *outstr = NULL;
     bool have_flags = false;
 
     if (!pcmk__is_primitive(rsc)) {
         return NULL;
     }
 
     CRM_CHECK(name != NULL, name = "unknown");
     CRM_CHECK(kind != NULL, kind = "unknown");
     CRM_CHECK(class != NULL, class = "unknown");
 
     if (pcmk_is_set(pcmk_get_ra_caps(class), pcmk_ra_cap_provider)) {
         provider = crm_element_value(rsc->private->xml, PCMK_XA_PROVIDER);
     }
 
     if ((node == NULL) && (rsc->lock_node != NULL)) {
         node = rsc->lock_node;
     }
     if (pcmk_any_flags_set(show_opts, pcmk_show_rsc_only)
         || pcmk__list_of_multiple(rsc->running_on)) {
         node = NULL;
     }
 
     outstr = g_string_sized_new(128);
 
     // Resource name and agent
     pcmk__g_strcat(outstr,
                    name, "\t(", class, ((provider == NULL)? "" : ":"),
                    pcmk__s(provider, ""), ":", kind, "):\t", NULL);
 
     // State on node
     if (pcmk_is_set(rsc->flags, pcmk__rsc_removed)) {
         g_string_append(outstr, " ORPHANED");
     }
     if (pcmk_is_set(rsc->flags, pcmk__rsc_failed)) {
         enum rsc_role_e role = native_displayable_role(rsc);
 
         g_string_append(outstr, " FAILED");
         if (role > pcmk_role_unpromoted) {
             pcmk__add_word(&outstr, 0, pcmk_role_text(role));
         }
     } else {
         bool show_pending = pcmk_is_set(show_opts, pcmk_show_pending);
 
         pcmk__add_word(&outstr, 0, native_displayable_state(rsc, show_pending));
     }
     if (node) {
         pcmk__add_word(&outstr, 0, pcmk__node_name(node));
     }
 
     // Failed probe operation
     if (native_displayable_role(rsc) == pcmk_role_stopped) {
         xmlNode *probe_op = pe__failed_probe_for_rsc(rsc, node ? node->details->uname : NULL);
         if (probe_op != NULL) {
             int rc;
 
             pcmk__scan_min_int(crm_element_value(probe_op, PCMK__XA_RC_CODE),
                                &rc, 0);
             pcmk__g_strcat(outstr, " (", services_ocf_exitcode_str(rc), ") ",
                            NULL);
         }
     }
 
     // Flags, as: (<flag> [...])
     if (node && !(node->details->online) && node->details->unclean) {
         have_flags = add_output_flag(outstr, "UNCLEAN", have_flags);
     }
     if (node && (node == rsc->lock_node)) {
         have_flags = add_output_flag(outstr, "LOCKED", have_flags);
     }
     if (pcmk_is_set(show_opts, pcmk_show_pending)) {
         const char *pending_action = native_pending_action(rsc);
 
         if (pending_action != NULL) {
             have_flags = add_output_flag(outstr, pending_action, have_flags);
         }
     }
     if (target_role != NULL) {
         switch (pcmk_parse_role(target_role)) {
             case pcmk_role_unknown:
                 pcmk__config_err("Invalid " PCMK_META_TARGET_ROLE
                                  " %s for resource %s", target_role, rsc->id);
                 break;
 
             case pcmk_role_stopped:
                 have_flags = add_output_flag(outstr, "disabled", have_flags);
                 break;
 
             case pcmk_role_unpromoted:
                 if (pcmk_is_set(pe__const_top_resource(rsc, false)->flags,
                                 pcmk__rsc_promotable)) {
                     have_flags = add_output_flag(outstr,
                                                  PCMK_META_TARGET_ROLE ":",
                                                  have_flags);
                     g_string_append(outstr, target_role);
                 }
                 break;
 
             default:
                 /* Only show target role if it limits our abilities (i.e. ignore
                  * Started, as it is the default anyways, and doesn't prevent
                  * the resource from becoming promoted).
                  */
                 break;
         }
     }
 
     // Blocked or maintenance implies unmanaged
     if (pcmk_any_flags_set(rsc->flags,
                            pcmk__rsc_blocked|pcmk__rsc_maintenance)) {
         if (pcmk_is_set(rsc->flags, pcmk__rsc_blocked)) {
             have_flags = add_output_flag(outstr, "blocked", have_flags);
 
         } else if (pcmk_is_set(rsc->flags, pcmk__rsc_maintenance)) {
             have_flags = add_output_flag(outstr, "maintenance", have_flags);
         }
     } else if (!pcmk_is_set(rsc->flags, pcmk__rsc_managed)) {
         have_flags = add_output_flag(outstr, "unmanaged", have_flags);
     }
 
     if (pcmk_is_set(rsc->flags, pcmk__rsc_ignore_failure)) {
         have_flags = add_output_flag(outstr, "failure ignored", have_flags);
     }
 
 
     if (have_flags) {
         g_string_append_c(outstr, ')');
     }
 
     // User-supplied description
     if (pcmk_any_flags_set(show_opts, pcmk_show_rsc_only|pcmk_show_description)
         || pcmk__list_of_multiple(rsc->running_on)) {
         const char *desc = crm_element_value(rsc->private->xml,
                                              PCMK_XA_DESCRIPTION);
 
         if (desc) {
             g_string_append(outstr, " (");
             g_string_append(outstr, desc);
             g_string_append(outstr, ")");
 
         }
     }
 
     if (show_nodes && !pcmk_is_set(show_opts, pcmk_show_rsc_only)
         && pcmk__list_of_multiple(rsc->running_on)) {
         bool have_nodes = false;
 
         for (GList *iter = rsc->running_on; iter != NULL; iter = iter->next) {
             pcmk_node_t *n = (pcmk_node_t *) iter->data;
 
             have_nodes = add_output_node(outstr, n->details->uname, have_nodes);
         }
         if (have_nodes) {
             g_string_append(outstr, " ]");
         }
     }
 
     return g_string_free(outstr, FALSE);
 }
 
 int
 pe__common_output_html(pcmk__output_t *out, const pcmk_resource_t *rsc,
                        const char *name, const pcmk_node_t *node,
                        uint32_t show_opts)
 {
     const char *kind = crm_element_value(rsc->private->xml, PCMK_XA_TYPE);
     const char *target_role = NULL;
     const char *cl = NULL;
 
     xmlNode *child = NULL;
     gchar *content = NULL;
 
     CRM_ASSERT((kind != NULL) && pcmk__is_primitive(rsc));
 
     if (rsc->meta) {
         const char *is_internal = g_hash_table_lookup(rsc->meta,
                                                       PCMK__META_INTERNAL_RSC);
 
         if (crm_is_true(is_internal)
             && !pcmk_is_set(show_opts, pcmk_show_implicit_rscs)) {
 
             crm_trace("skipping print of internal resource %s", rsc->id);
             return pcmk_rc_no_output;
         }
         target_role = g_hash_table_lookup(rsc->meta, PCMK_META_TARGET_ROLE);
     }
 
     if (!pcmk_is_set(rsc->flags, pcmk__rsc_managed)) {
         cl = PCMK__VALUE_RSC_MANAGED;
 
     } else if (pcmk_is_set(rsc->flags, pcmk__rsc_failed)) {
         cl = PCMK__VALUE_RSC_FAILED;
 
     } else if (pcmk__is_primitive(rsc) && (rsc->running_on == NULL)) {
         cl = PCMK__VALUE_RSC_FAILED;
 
     } else if (pcmk__list_of_multiple(rsc->running_on)) {
         cl = PCMK__VALUE_RSC_MULTIPLE;
 
     } else if (pcmk_is_set(rsc->flags, pcmk__rsc_ignore_failure)) {
         cl = PCMK__VALUE_RSC_FAILURE_IGNORED;
 
     } else {
         cl = PCMK__VALUE_RSC_OK;
     }
 
     child = pcmk__output_create_html_node(out, "li", NULL, NULL, NULL);
     child = pcmk__html_create(child, PCMK__XE_SPAN, NULL, cl);
     content = pcmk__native_output_string(rsc, name, node, show_opts,
                                          target_role, true);
     pcmk__xe_set_content(child, "%s", content);
     g_free(content);
 
     return pcmk_rc_ok;
 }
 
 int
 pe__common_output_text(pcmk__output_t *out, const pcmk_resource_t *rsc,
                        const char *name, const pcmk_node_t *node,
                        uint32_t show_opts)
 {
     const char *target_role = NULL;
 
     CRM_ASSERT(pcmk__is_primitive(rsc));
 
     if (rsc->meta) {
         const char *is_internal = g_hash_table_lookup(rsc->meta,
                                                       PCMK__META_INTERNAL_RSC);
 
         if (crm_is_true(is_internal)
             && !pcmk_is_set(show_opts, pcmk_show_implicit_rscs)) {
 
             crm_trace("skipping print of internal resource %s", rsc->id);
             return pcmk_rc_no_output;
         }
         target_role = g_hash_table_lookup(rsc->meta, PCMK_META_TARGET_ROLE);
     }
 
     {
         gchar *s = pcmk__native_output_string(rsc, name, node, show_opts,
                                               target_role, true);
 
         out->list_item(out, NULL, "%s", s);
         g_free(s);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("primitive", "uint32_t", "pcmk_resource_t *", "GList *",
                   "GList *")
 int
 pe__resource_xml(pcmk__output_t *out, va_list args)
 {
     uint32_t show_opts = va_arg(args, uint32_t);
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     GList *only_node G_GNUC_UNUSED = va_arg(args, GList *);
     GList *only_rsc = va_arg(args, GList *);
 
     int rc = pcmk_rc_no_output;
     bool print_pending = pcmk_is_set(show_opts, pcmk_show_pending);
     const char *class = crm_element_value(rsc->private->xml, PCMK_XA_CLASS);
     const char *prov = crm_element_value(rsc->private->xml, PCMK_XA_PROVIDER);
 
     char ra_name[LINE_MAX];
     const char *rsc_state = native_displayable_state(rsc, print_pending);
     const char *target_role = NULL;
     const char *active = pcmk__btoa(rsc->private->fns->active(rsc, TRUE));
     const char *orphaned = pcmk__flag_text(rsc->flags, pcmk__rsc_removed);
     const char *blocked = pcmk__flag_text(rsc->flags, pcmk__rsc_blocked);
     const char *maintenance = pcmk__flag_text(rsc->flags,
                                               pcmk__rsc_maintenance);
     const char *managed = pcmk__flag_text(rsc->flags, pcmk__rsc_managed);
     const char *failed = pcmk__flag_text(rsc->flags, pcmk__rsc_failed);
     const char *ignored = pcmk__flag_text(rsc->flags, pcmk__rsc_ignore_failure);
     char *nodes_running_on = NULL;
     const char *pending = print_pending? native_pending_action(rsc) : NULL;
     const char *locked_to = NULL;
     const char *desc = pe__resource_description(rsc, show_opts);
 
     CRM_ASSERT(pcmk__is_primitive(rsc));
 
     if (rsc->private->fns->is_filtered(rsc, only_rsc, TRUE)) {
         return pcmk_rc_no_output;
     }
 
     // Resource information
     snprintf(ra_name, LINE_MAX, "%s%s%s:%s", class,
              ((prov == NULL)? "" : ":"), ((prov == NULL)? "" : prov),
              crm_element_value(rsc->private->xml, PCMK_XA_TYPE));
 
     if (rsc->meta != NULL) {
         target_role = g_hash_table_lookup(rsc->meta, PCMK_META_TARGET_ROLE);
     }
 
     nodes_running_on = pcmk__itoa(g_list_length(rsc->running_on));
 
     if (rsc->lock_node != NULL) {
         locked_to = rsc->lock_node->details->uname;
     }
 
     rc = pe__name_and_nvpairs_xml(out, true, PCMK_XE_RESOURCE,
                                   PCMK_XA_ID, rsc_printable_id(rsc),
                                   PCMK_XA_RESOURCE_AGENT, ra_name,
                                   PCMK_XA_ROLE, rsc_state,
                                   PCMK_XA_TARGET_ROLE, target_role,
                                   PCMK_XA_ACTIVE, active,
                                   PCMK_XA_ORPHANED, orphaned,
                                   PCMK_XA_BLOCKED, blocked,
                                   PCMK_XA_MAINTENANCE, maintenance,
                                   PCMK_XA_MANAGED, managed,
                                   PCMK_XA_FAILED, failed,
                                   PCMK_XA_FAILURE_IGNORED, ignored,
                                   PCMK_XA_NODES_RUNNING_ON, nodes_running_on,
                                   PCMK_XA_PENDING, pending,
                                   PCMK_XA_LOCKED_TO, locked_to,
                                   PCMK_XA_DESCRIPTION, desc,
                                   NULL);
     free(nodes_running_on);
 
     CRM_ASSERT(rc == pcmk_rc_ok);
 
     if (rsc->running_on != NULL) {
         GList *gIter = rsc->running_on;
 
         for (; gIter != NULL; gIter = gIter->next) {
             pcmk_node_t *node = (pcmk_node_t *) gIter->data;
             const char *cached = pcmk__btoa(node->details->online);
 
             rc = pe__name_and_nvpairs_xml(out, false, PCMK_XE_NODE,
                                           PCMK_XA_NAME, node->details->uname,
                                           PCMK_XA_ID, node->details->id,
                                           PCMK_XA_CACHED, cached,
                                           NULL);
             CRM_ASSERT(rc == pcmk_rc_ok);
         }
     }
 
     pcmk__output_xml_pop_parent(out);
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("primitive", "uint32_t", "pcmk_resource_t *", "GList *",
                   "GList *")
 int
 pe__resource_html(pcmk__output_t *out, va_list args)
 {
     uint32_t show_opts = va_arg(args, uint32_t);
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     GList *only_node G_GNUC_UNUSED = va_arg(args, GList *);
     GList *only_rsc = va_arg(args, GList *);
 
     const pcmk_node_t *node = pcmk__current_node(rsc);
 
     if (rsc->private->fns->is_filtered(rsc, only_rsc, TRUE)) {
         return pcmk_rc_no_output;
     }
 
     CRM_ASSERT(pcmk__is_primitive(rsc));
 
     if (node == NULL) {
         // This is set only if a non-probe action is pending on this node
         node = rsc->pending_node;
     }
     return pe__common_output_html(out, rsc, rsc_printable_id(rsc), node, show_opts);
 }
 
 PCMK__OUTPUT_ARGS("primitive", "uint32_t", "pcmk_resource_t *", "GList *",
                   "GList *")
 int
 pe__resource_text(pcmk__output_t *out, va_list args)
 {
     uint32_t show_opts = va_arg(args, uint32_t);
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     GList *only_node G_GNUC_UNUSED = va_arg(args, GList *);
     GList *only_rsc = va_arg(args, GList *);
 
     const pcmk_node_t *node = pcmk__current_node(rsc);
 
     CRM_ASSERT(pcmk__is_primitive(rsc));
 
     if (rsc->private->fns->is_filtered(rsc, only_rsc, TRUE)) {
         return pcmk_rc_no_output;
     }
 
     if (node == NULL) {
         // This is set only if a non-probe action is pending on this node
         node = rsc->pending_node;
     }
     return pe__common_output_text(out, rsc, rsc_printable_id(rsc), node, show_opts);
 }
 
 void
 native_free(pcmk_resource_t * rsc)
 {
     pcmk__rsc_trace(rsc, "Freeing resource action list (not the data)");
     common_free(rsc);
 }
 
 enum rsc_role_e
 native_resource_state(const pcmk_resource_t * rsc, gboolean current)
 {
     enum rsc_role_e role = rsc->next_role;
 
     if (current) {
         role = rsc->role;
     }
     pcmk__rsc_trace(rsc, "%s state: %s", rsc->id, pcmk_role_text(role));
     return role;
 }
 
 /*!
  * \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  0 = where allocated, 1 = where running,
  *                      2 = where running or pending
  *
  * \return If list contains only one node, that node, or NULL otherwise
  */
 pcmk_node_t *
 native_location(const pcmk_resource_t *rsc, GList **list, int current)
 {
     // @COMPAT: Accept a pcmk__rsc_node argument instead of int current
     pcmk_node_t *one = NULL;
     GList *result = NULL;
 
     if (rsc->children) {
         GList *gIter = rsc->children;
 
         for (; gIter != NULL; gIter = gIter->next) {
             pcmk_resource_t *child = (pcmk_resource_t *) gIter->data;
 
             child->private->fns->location(child, &result, current);
         }
 
     } else if (current) {
 
         if (rsc->running_on) {
             result = g_list_copy(rsc->running_on);
         }
         if ((current == 2) && rsc->pending_node
             && !pe_find_node_id(result, rsc->pending_node->details->id)) {
                 result = g_list_append(result, rsc->pending_node);
         }
 
-    } else if (current == FALSE && rsc->allocated_to) {
-        result = g_list_append(NULL, rsc->allocated_to);
+    } else if (!current && (rsc->private->assigned_node != NULL)) {
+        result = g_list_append(NULL, rsc->private->assigned_node);
     }
 
     if (result && (result->next == NULL)) {
         one = result->data;
     }
 
     if (list) {
         GList *gIter = result;
 
         for (; gIter != NULL; gIter = gIter->next) {
             pcmk_node_t *node = (pcmk_node_t *) gIter->data;
 
             if (*list == NULL || pe_find_node_id(*list, node->details->id) == NULL) {
                 *list = g_list_append(*list, node);
             }
         }
     }
 
     g_list_free(result);
     return one;
 }
 
 static void
 get_rscs_brief(GList *rsc_list, GHashTable * rsc_table, GHashTable * active_table)
 {
     GList *gIter = rsc_list;
 
     for (; gIter != NULL; gIter = gIter->next) {
         pcmk_resource_t *rsc = (pcmk_resource_t *) gIter->data;
 
         const char *class = crm_element_value(rsc->private->xml, PCMK_XA_CLASS);
         const char *kind = crm_element_value(rsc->private->xml, PCMK_XA_TYPE);
 
         int offset = 0;
         char buffer[LINE_MAX];
 
         int *rsc_counter = NULL;
         int *active_counter = NULL;
 
         if (!pcmk__is_primitive(rsc)) {
             continue;
         }
 
         offset += snprintf(buffer + offset, LINE_MAX - offset, "%s", class);
         if (pcmk_is_set(pcmk_get_ra_caps(class), pcmk_ra_cap_provider)) {
             const char *prov = crm_element_value(rsc->private->xml,
                                                  PCMK_XA_PROVIDER);
 
             if (prov != NULL) {
                 offset += snprintf(buffer + offset, LINE_MAX - offset,
                                    ":%s", prov);
             }
         }
         offset += snprintf(buffer + offset, LINE_MAX - offset, ":%s", kind);
         CRM_LOG_ASSERT(offset > 0);
 
         if (rsc_table) {
             rsc_counter = g_hash_table_lookup(rsc_table, buffer);
             if (rsc_counter == NULL) {
                 rsc_counter = pcmk__assert_alloc(1, sizeof(int));
                 *rsc_counter = 0;
                 g_hash_table_insert(rsc_table, strdup(buffer), rsc_counter);
             }
             (*rsc_counter)++;
         }
 
         if (active_table) {
             GList *gIter2 = rsc->running_on;
 
             for (; gIter2 != NULL; gIter2 = gIter2->next) {
                 pcmk_node_t *node = (pcmk_node_t *) gIter2->data;
                 GHashTable *node_table = NULL;
 
                 if (node->details->unclean == FALSE && node->details->online == FALSE &&
                     pcmk_is_set(rsc->flags, pcmk__rsc_managed)) {
                     continue;
                 }
 
                 node_table = g_hash_table_lookup(active_table, node->details->uname);
                 if (node_table == NULL) {
                     node_table = pcmk__strkey_table(free, free);
                     g_hash_table_insert(active_table, strdup(node->details->uname), node_table);
                 }
 
                 active_counter = g_hash_table_lookup(node_table, buffer);
                 if (active_counter == NULL) {
                     active_counter = pcmk__assert_alloc(1, sizeof(int));
                     *active_counter = 0;
                     g_hash_table_insert(node_table, strdup(buffer), active_counter);
                 }
                 (*active_counter)++;
             }
         }
     }
 }
 
 static void
 destroy_node_table(gpointer data)
 {
     GHashTable *node_table = data;
 
     if (node_table) {
         g_hash_table_destroy(node_table);
     }
 }
 
 int
 pe__rscs_brief_output(pcmk__output_t *out, GList *rsc_list, uint32_t show_opts)
 {
     GHashTable *rsc_table = pcmk__strkey_table(free, free);
     GHashTable *active_table = pcmk__strkey_table(free, destroy_node_table);
     GList *sorted_rscs;
     int rc = pcmk_rc_no_output;
 
     get_rscs_brief(rsc_list, rsc_table, active_table);
 
     /* Make a list of the rsc_table keys so that it can be sorted.  This is to make sure
      * output order stays consistent between systems.
      */
     sorted_rscs = g_hash_table_get_keys(rsc_table);
     sorted_rscs = g_list_sort(sorted_rscs, (GCompareFunc) strcmp);
 
     for (GList *gIter = sorted_rscs; gIter; gIter = gIter->next) {
         char *type = (char *) gIter->data;
         int *rsc_counter = g_hash_table_lookup(rsc_table, type);
 
         GList *sorted_nodes = NULL;
         int active_counter_all = 0;
 
         /* Also make a list of the active_table keys so it can be sorted.  If there's
          * more than one instance of a type of resource running, we need the nodes to
          * be sorted to make sure output order stays consistent between systems.
          */
         sorted_nodes = g_hash_table_get_keys(active_table);
         sorted_nodes = g_list_sort(sorted_nodes, (GCompareFunc) pcmk__numeric_strcasecmp);
 
         for (GList *gIter2 = sorted_nodes; gIter2; gIter2 = gIter2->next) {
             char *node_name = (char *) gIter2->data;
             GHashTable *node_table = g_hash_table_lookup(active_table, node_name);
             int *active_counter = NULL;
 
             if (node_table == NULL) {
                 continue;
             }
 
             active_counter = g_hash_table_lookup(node_table, type);
 
             if (active_counter == NULL || *active_counter == 0) {
                 continue;
 
             } else {
                 active_counter_all += *active_counter;
             }
 
             if (pcmk_is_set(show_opts, pcmk_show_rsc_only)) {
                 node_name = NULL;
             }
 
             if (pcmk_is_set(show_opts, pcmk_show_inactive_rscs)) {
                 out->list_item(out, NULL, "%d/%d\t(%s):\tActive %s",
                                *active_counter,
                                rsc_counter ? *rsc_counter : 0, type,
                                (*active_counter > 0) && node_name ? node_name : "");
             } else {
                 out->list_item(out, NULL, "%d\t(%s):\tActive %s",
                                *active_counter, type,
                                (*active_counter > 0) && node_name ? node_name : "");
             }
 
             rc = pcmk_rc_ok;
         }
 
         if (pcmk_is_set(show_opts, pcmk_show_inactive_rscs) && active_counter_all == 0) {
             out->list_item(out, NULL, "%d/%d\t(%s):\tActive",
                            active_counter_all,
                            rsc_counter ? *rsc_counter : 0, type);
             rc = pcmk_rc_ok;
         }
 
         if (sorted_nodes) {
             g_list_free(sorted_nodes);
         }
     }
 
     if (rsc_table) {
         g_hash_table_destroy(rsc_table);
         rsc_table = NULL;
     }
     if (active_table) {
         g_hash_table_destroy(active_table);
         active_table = NULL;
     }
     if (sorted_rscs) {
         g_list_free(sorted_rscs);
     }
 
     return rc;
 }
 
 gboolean
 pe__native_is_filtered(const pcmk_resource_t *rsc, GList *only_rsc,
                        gboolean check_parent)
 {
     if (pcmk__str_in_list(rsc_printable_id(rsc), only_rsc, pcmk__str_star_matches) ||
         pcmk__str_in_list(rsc->id, only_rsc, pcmk__str_star_matches)) {
         return FALSE;
     } else if (check_parent && (rsc->private->parent != NULL)) {
         const pcmk_resource_t *up = pe__const_top_resource(rsc, true);
 
         return up->private->fns->is_filtered(up, only_rsc, FALSE);
     }
 
     return TRUE;
 }
 
 /*!
  * \internal
  * \brief Get maximum primitive resource instances per node
  *
  * \param[in] rsc  Primitive resource to check
  *
  * \return Maximum number of \p rsc instances that can be active on one node
  */
 unsigned int
 pe__primitive_max_per_node(const pcmk_resource_t *rsc)
 {
     CRM_ASSERT(pcmk__is_primitive(rsc));
     return 1U;
 }
diff --git a/lib/pengine/pe_notif.c b/lib/pengine/pe_notif.c
index f4e68a1fec..e7b3ded4e1 100644
--- a/lib/pengine/pe_notif.c
+++ b/lib/pengine/pe_notif.c
@@ -1,1006 +1,1006 @@
 /*
  * 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 <crm/pengine/internal.h>
 #include <pacemaker-internal.h>
 
 #include "pe_status_private.h"
 
 typedef struct notify_entry_s {
     const pcmk_resource_t *rsc;
     const pcmk_node_t *node;
 } notify_entry_t;
 
 /*!
  * \internal
  * \brief Compare two notification entries
  *
  * Compare two notification entries, where the one with the alphabetically first
  * resource name (or if equal, node name) sorts as first, with NULL sorting as
  * less than non-NULL.
  *
  * \param[in] a  First notification entry to compare
  * \param[in] b  Second notification entry to compare
  *
  * \return -1 if \p a sorts before \p b, 0 if they are equal, otherwise 1
  */
 static gint
 compare_notify_entries(gconstpointer a, gconstpointer b)
 {
     int tmp;
     const notify_entry_t *entry_a = a;
     const notify_entry_t *entry_b = b;
 
     // NULL a or b is not actually possible
     if ((entry_a == NULL) && (entry_b == NULL)) {
         return 0;
     }
     if (entry_a == NULL) {
         return 1;
     }
     if (entry_b == NULL) {
         return -1;
     }
 
     // NULL resources sort first
     if ((entry_a->rsc == NULL) && (entry_b->rsc == NULL)) {
         return 0;
     }
     if (entry_a->rsc == NULL) {
         return 1;
     }
     if (entry_b->rsc == NULL) {
         return -1;
     }
 
     // Compare resource names
     tmp = strcmp(entry_a->rsc->id, entry_b->rsc->id);
     if (tmp != 0) {
         return tmp;
     }
 
     // Otherwise NULL nodes sort first
     if ((entry_a->node == NULL) && (entry_b->node == NULL)) {
         return 0;
     }
     if (entry_a->node == NULL) {
         return 1;
     }
     if (entry_b->node == NULL) {
         return -1;
     }
 
     // Finally, compare node names
     return strcmp(entry_a->node->details->id, entry_b->node->details->id);
 }
 
 /*!
  * \internal
  * \brief Duplicate a notification entry
  *
  * \param[in] entry  Entry to duplicate
  *
  * \return Newly allocated duplicate of \p entry
  * \note It is the caller's responsibility to free the return value.
  */
 static notify_entry_t *
 dup_notify_entry(const notify_entry_t *entry)
 {
     notify_entry_t *dup = pcmk__assert_alloc(1, sizeof(notify_entry_t));
 
     dup->rsc = entry->rsc;
     dup->node = entry->node;
     return dup;
 }
 
 /*!
  * \internal
  * \brief Given a list of nodes, create strings with node names
  *
  * \param[in]  list             List of nodes (as pcmk_node_t *)
  * \param[out] all_node_names   If not NULL, will be set to space-separated list
  *                              of the names of all nodes in \p list
  * \param[out] host_node_names  Same as \p all_node_names, except active
  *                              guest nodes will list the name of their host
  *
  * \note The caller is responsible for freeing the output argument values using
  *       \p g_string_free().
  */
 static void
 get_node_names(const GList *list, GString **all_node_names,
                GString **host_node_names)
 {
     if (all_node_names != NULL) {
         *all_node_names = NULL;
     }
     if (host_node_names != NULL) {
         *host_node_names = NULL;
     }
 
     for (const GList *iter = list; iter != NULL; iter = iter->next) {
         const pcmk_node_t *node = (const pcmk_node_t *) iter->data;
 
         if (node->details->uname == NULL) {
             continue;
         }
 
         // Always add to list of all node names
         if (all_node_names != NULL) {
             pcmk__add_word(all_node_names, 1024, node->details->uname);
         }
 
         // Add to host node name list if appropriate
         if (host_node_names != NULL) {
             if (pcmk__is_guest_or_bundle_node(node)
                 && (node->details->remote_rsc->container->running_on != NULL)) {
                 node = pcmk__current_node(node->details->remote_rsc->container);
                 if (node->details->uname == NULL) {
                     continue;
                 }
             }
             pcmk__add_word(host_node_names, 1024, node->details->uname);
         }
     }
 
     if ((all_node_names != NULL) && (*all_node_names == NULL)) {
         *all_node_names = g_string_new(" ");
     }
     if ((host_node_names != NULL) && (*host_node_names == NULL)) {
         *host_node_names = g_string_new(" ");
     }
 }
 
 /*!
  * \internal
  * \brief Create strings of instance and node names from notification entries
  *
  * \param[in,out] list        List of notification entries (will be sorted here)
  * \param[out]    rsc_names   If not NULL, will be set to space-separated list
  *                            of clone instances from \p list
  * \param[out]    node_names  If not NULL, will be set to space-separated list
  *                            of node names from \p list
  *
  * \return (Possibly new) head of sorted \p list
  * \note The caller is responsible for freeing the output argument values using
  *       \p g_list_free_full() and \p g_string_free().
  */
 static GList *
 notify_entries_to_strings(GList *list, GString **rsc_names,
                           GString **node_names)
 {
     const char *last_rsc_id = NULL;
 
     // Initialize output lists to NULL
     if (rsc_names != NULL) {
         *rsc_names = NULL;
     }
     if (node_names != NULL) {
         *node_names = NULL;
     }
 
     // Sort input list for user-friendliness (and ease of filtering duplicates)
     list = g_list_sort(list, compare_notify_entries);
 
     for (GList *gIter = list; gIter != NULL; gIter = gIter->next) {
         notify_entry_t *entry = (notify_entry_t *) gIter->data;
 
         // Entry must have a resource (with ID)
         CRM_LOG_ASSERT((entry != NULL) && (entry->rsc != NULL)
                        && (entry->rsc->id != NULL));
         if ((entry == NULL) || (entry->rsc == NULL)
             || (entry->rsc->id == NULL)) {
             continue;
         }
 
         // Entry must have a node unless listing inactive resources
         CRM_LOG_ASSERT((node_names == NULL) || (entry->node != NULL));
         if ((node_names != NULL) && (entry->node == NULL)) {
             continue;
         }
 
         // Don't add duplicates of a particular clone instance
         if (pcmk__str_eq(entry->rsc->id, last_rsc_id, pcmk__str_none)) {
             continue;
         }
         last_rsc_id = entry->rsc->id;
 
         if (rsc_names != NULL) {
             pcmk__add_word(rsc_names, 1024, entry->rsc->id);
         }
         if ((node_names != NULL) && (entry->node->details->uname != NULL)) {
             pcmk__add_word(node_names, 1024, entry->node->details->uname);
         }
     }
 
     // If there are no entries, return "empty" lists
     if ((rsc_names != NULL) && (*rsc_names == NULL)) {
         *rsc_names = g_string_new(" ");
     }
     if ((node_names != NULL) && (*node_names == NULL)) {
         *node_names = g_string_new(" ");
     }
 
     return list;
 }
 
 /*!
  * \internal
  * \brief Copy a meta-attribute into a notify action
  *
  * \param[in]     key        Name of meta-attribute to copy
  * \param[in]     value      Value of meta-attribute to copy
  * \param[in,out] user_data  Notify action to copy into
  */
 static void
 copy_meta_to_notify(gpointer key, gpointer value, gpointer user_data)
 {
     pcmk_action_t *notify = (pcmk_action_t *) user_data;
 
     /* Any existing meta-attributes (for example, the action timeout) are for
      * the notify action itself, so don't override those.
      */
     if (g_hash_table_lookup(notify->meta, (const char *) key) != NULL) {
         return;
     }
 
     pcmk__insert_dup(notify->meta, (const char *) key, (const char *) value);
 }
 
 static void
 add_notify_data_to_action_meta(const notify_data_t *n_data,
                                pcmk_action_t *action)
 {
     for (const GSList *item = n_data->keys; item; item = item->next) {
         const pcmk_nvpair_t *nvpair = (const pcmk_nvpair_t *) item->data;
 
         pcmk__insert_meta(action, nvpair->name, nvpair->value);
     }
 }
 
 /*!
  * \internal
  * \brief Create a new notify pseudo-action for a clone resource
  *
  * \param[in,out] rsc           Clone resource that notification is for
  * \param[in]     action        Action to use in notify action key
  * \param[in]     notif_action  PCMK_ACTION_NOTIFY or PCMK_ACTION_NOTIFIED
  * \param[in]     notif_type    "pre", "post", "confirmed-pre", "confirmed-post"
  *
  * \return Newly created notify pseudo-action
  */
 static pcmk_action_t *
 new_notify_pseudo_action(pcmk_resource_t *rsc, const pcmk_action_t *action,
                          const char *notif_action, const char *notif_type)
 {
     pcmk_action_t *notify = NULL;
 
     notify = custom_action(rsc,
                            pcmk__notify_key(rsc->id, notif_type, action->task),
                            notif_action, NULL,
                            pcmk_is_set(action->flags, pcmk_action_optional),
                            rsc->private->scheduler);
     pcmk__set_action_flags(notify, pcmk_action_pseudo);
     pcmk__insert_meta(notify, "notify_key_type", notif_type);
     pcmk__insert_meta(notify, "notify_key_operation", action->task);
     return notify;
 }
 
 /*!
  * \internal
  * \brief Create a new notify action for a clone instance
  *
  * \param[in,out] rsc          Clone instance that notification is for
  * \param[in]     node         Node that notification is for
  * \param[in,out] op           Action that notification is for
  * \param[in,out] notify_done  Parent pseudo-action for notifications complete
  * \param[in]     n_data       Notification values to add to action meta-data
  *
  * \return Newly created notify action
  */
 static pcmk_action_t *
 new_notify_action(pcmk_resource_t *rsc, const pcmk_node_t *node,
                   pcmk_action_t *op, pcmk_action_t *notify_done,
                   const notify_data_t *n_data)
 {
     char *key = NULL;
     pcmk_action_t *notify_action = NULL;
     const char *value = NULL;
     const char *task = NULL;
     const char *skip_reason = NULL;
 
     CRM_CHECK((rsc != NULL) && (node != NULL), return NULL);
 
     // Ensure we have all the info we need
     if (op == NULL) {
         skip_reason = "no action";
     } else if (notify_done == NULL) {
         skip_reason = "no parent notification";
     } else if (!node->details->online) {
         skip_reason = "node offline";
     } else if (!pcmk_is_set(op->flags, pcmk_action_runnable)) {
         skip_reason = "original action not runnable";
     }
     if (skip_reason != NULL) {
         pcmk__rsc_trace(rsc, "Skipping notify action for %s on %s: %s",
                         rsc->id, pcmk__node_name(node), skip_reason);
         return NULL;
     }
 
     value = g_hash_table_lookup(op->meta, "notify_type");     // "pre" or "post"
     task = g_hash_table_lookup(op->meta, "notify_operation"); // original action
 
     pcmk__rsc_trace(rsc, "Creating notify action for %s on %s (%s-%s)",
                     rsc->id, pcmk__node_name(node), value, task);
 
     // Create the notify action
     key = pcmk__notify_key(rsc->id, value, task);
     notify_action = custom_action(rsc, key, op->task, node,
                                   pcmk_is_set(op->flags, pcmk_action_optional),
                                   rsc->private->scheduler);
 
     // Add meta-data to notify action
     g_hash_table_foreach(op->meta, copy_meta_to_notify, notify_action);
     add_notify_data_to_action_meta(n_data, notify_action);
 
     // Order notify after original action and before parent notification
     order_actions(op, notify_action, pcmk__ar_ordered);
     order_actions(notify_action, notify_done, pcmk__ar_ordered);
     return notify_action;
 }
 
 /*!
  * \internal
  * \brief Create a new "post-" notify action for a clone instance
  *
  * \param[in,out] rsc     Clone instance that notification is for
  * \param[in]     node    Node that notification is for
  * \param[in,out] n_data  Notification values to add to action meta-data
  */
 static void
 new_post_notify_action(pcmk_resource_t *rsc, const pcmk_node_t *node,
                        notify_data_t *n_data)
 {
     pcmk_action_t *notify = NULL;
 
     CRM_ASSERT(n_data != NULL);
 
     // Create the "post-" notify action for specified instance
     notify = new_notify_action(rsc, node, n_data->post, n_data->post_done,
                                n_data);
     if (notify != NULL) {
         notify->priority = PCMK_SCORE_INFINITY;
     }
 
     // Order recurring monitors after all "post-" notifications complete
     if (n_data->post_done == NULL) {
         return;
     }
     for (GList *iter = rsc->private->actions; iter != NULL; iter = iter->next) {
         pcmk_action_t *mon = (pcmk_action_t *) iter->data;
         const char *interval_ms_s = NULL;
 
         interval_ms_s = g_hash_table_lookup(mon->meta, PCMK_META_INTERVAL);
         if (pcmk__str_eq(interval_ms_s, "0", pcmk__str_null_matches)
             || pcmk__str_eq(mon->task, PCMK_ACTION_CANCEL, pcmk__str_none)) {
             continue; // Not a recurring monitor
         }
         order_actions(n_data->post_done, mon, pcmk__ar_ordered);
     }
 }
 
 /*!
  * \internal
  * \brief Create and order notification pseudo-actions for a clone action
  *
  * In addition to the actual notify actions needed for each clone instance,
  * clone notifications also require pseudo-actions to provide ordering points
  * in the notification process. This creates the notification data, along with
  * appropriate pseudo-actions and their orderings.
  *
  * For example, the ordering sequence for starting a clone is:
  *
  *     "pre-" notify pseudo-action for clone
  *     -> "pre-" notify actions for each clone instance
  *     -> "pre-" notifications complete pseudo-action for clone
  *     -> start actions for each clone instance
  *     -> "started" pseudo-action for clone
  *     -> "post-" notify pseudo-action for clone
  *     -> "post-" notify actions for each clone instance
  *     -> "post-" notifications complete pseudo-action for clone
  *
  * \param[in,out] rsc       Clone that notifications are for
  * \param[in]     task      Name of action that notifications are for
  * \param[in,out] action    If not NULL, create a "pre-" pseudo-action ordered
  *                          before a "pre-" complete pseudo-action, ordered
  *                          before this action
  * \param[in,out] complete  If not NULL, create a "post-" pseudo-action ordered
  *                          after this action, and a "post-" complete
  *                          pseudo-action ordered after that
  *
  * \return Newly created notification data
  */
 notify_data_t *
 pe__action_notif_pseudo_ops(pcmk_resource_t *rsc, const char *task,
                             pcmk_action_t *action, pcmk_action_t *complete)
 {
     notify_data_t *n_data = NULL;
 
     if (!pcmk_is_set(rsc->flags, pcmk__rsc_notify)) {
         return NULL;
     }
 
     n_data = pcmk__assert_alloc(1, sizeof(notify_data_t));
 
     n_data->action = task;
 
     if (action != NULL) { // Need "pre-" pseudo-actions
 
         // Create "pre-" notify pseudo-action for clone
         n_data->pre = new_notify_pseudo_action(rsc, action, PCMK_ACTION_NOTIFY,
                                                "pre");
         pcmk__set_action_flags(n_data->pre, pcmk_action_runnable);
         pcmk__insert_meta(n_data->pre, "notify_type", "pre");
         pcmk__insert_meta(n_data->pre, "notify_operation", n_data->action);
 
         // Create "pre-" notifications complete pseudo-action for clone
         n_data->pre_done = new_notify_pseudo_action(rsc, action,
                                                     PCMK_ACTION_NOTIFIED,
                                                     "confirmed-pre");
         pcmk__set_action_flags(n_data->pre_done, pcmk_action_runnable);
         pcmk__insert_meta(n_data->pre_done, "notify_type", "pre");
         pcmk__insert_meta(n_data->pre_done, "notify_operation", n_data->action);
 
         // Order "pre-" -> "pre-" complete -> original action
         order_actions(n_data->pre, n_data->pre_done, pcmk__ar_ordered);
         order_actions(n_data->pre_done, action, pcmk__ar_ordered);
     }
 
     if (complete != NULL) { // Need "post-" pseudo-actions
 
         // Create "post-" notify pseudo-action for clone
         n_data->post = new_notify_pseudo_action(rsc, complete,
                                                 PCMK_ACTION_NOTIFY, "post");
         n_data->post->priority = PCMK_SCORE_INFINITY;
         if (pcmk_is_set(complete->flags, pcmk_action_runnable)) {
             pcmk__set_action_flags(n_data->post, pcmk_action_runnable);
         } else {
             pcmk__clear_action_flags(n_data->post, pcmk_action_runnable);
         }
         pcmk__insert_meta(n_data->post, "notify_type", "post");
         pcmk__insert_meta(n_data->post, "notify_operation", n_data->action);
 
         // Create "post-" notifications complete pseudo-action for clone
         n_data->post_done = new_notify_pseudo_action(rsc, complete,
                                                      PCMK_ACTION_NOTIFIED,
                                                      "confirmed-post");
         n_data->post_done->priority = PCMK_SCORE_INFINITY;
         if (pcmk_is_set(complete->flags, pcmk_action_runnable)) {
             pcmk__set_action_flags(n_data->post_done, pcmk_action_runnable);
         } else {
             pcmk__clear_action_flags(n_data->post_done, pcmk_action_runnable);
         }
         pcmk__insert_meta(n_data->post_done, "notify_type", "post");
         pcmk__insert_meta(n_data->post_done,
                           "notify_operation", n_data->action);
 
         // Order original action complete -> "post-" -> "post-" complete
         order_actions(complete, n_data->post, pcmk__ar_first_implies_then);
         order_actions(n_data->post, n_data->post_done,
                       pcmk__ar_first_implies_then);
     }
 
     // If we created both, order "pre-" complete -> "post-"
     if ((action != NULL) && (complete != NULL)) {
         order_actions(n_data->pre_done, n_data->post, pcmk__ar_ordered);
     }
     return n_data;
 }
 
 /*!
  * \internal
  * \brief Create a new notification entry
  *
  * \param[in] rsc   Resource for notification
  * \param[in] node  Node for notification
  *
  * \return Newly allocated notification entry
  * \note The caller is responsible for freeing the return value.
  */
 static notify_entry_t *
 new_notify_entry(const pcmk_resource_t *rsc, const pcmk_node_t *node)
 {
     notify_entry_t *entry = pcmk__assert_alloc(1, sizeof(notify_entry_t));
 
     entry->rsc = rsc;
     entry->node = node;
     return entry;
 }
 
 /*!
  * \internal
  * \brief Add notification data for resource state and optionally actions
  *
  * \param[in]     rsc       Clone or clone instance being notified
  * \param[in]     activity  Whether to add notification entries for actions
  * \param[in,out] n_data    Notification data for clone
  */
 static void
 collect_resource_data(const pcmk_resource_t *rsc, bool activity,
                       notify_data_t *n_data)
 {
     const GList *iter = NULL;
     notify_entry_t *entry = NULL;
     const pcmk_node_t *node = NULL;
 
     if (n_data == NULL) {
         return;
     }
 
     if (n_data->allowed_nodes == NULL) {
         n_data->allowed_nodes = rsc->allowed_nodes;
     }
 
     // If this is a clone, call recursively for each instance
     if (rsc->children != NULL) {
         for (iter = rsc->children; iter != NULL; iter = iter->next) {
             const pcmk_resource_t *child = (const pcmk_resource_t *) iter->data;
 
             collect_resource_data(child, activity, n_data);
         }
         return;
     }
 
     // This is a notification for a single clone instance
 
     if (rsc->running_on != NULL) {
         node = rsc->running_on->data; // First is sufficient
     }
     entry = new_notify_entry(rsc, node);
 
     // Add notification indicating the resource state
     switch (rsc->role) {
         case pcmk_role_stopped:
             n_data->inactive = g_list_prepend(n_data->inactive, entry);
             break;
 
         case pcmk_role_started:
             n_data->active = g_list_prepend(n_data->active, entry);
             break;
 
         case pcmk_role_unpromoted:
             n_data->unpromoted = g_list_prepend(n_data->unpromoted, entry);
             n_data->active = g_list_prepend(n_data->active,
                                             dup_notify_entry(entry));
             break;
 
         case pcmk_role_promoted:
             n_data->promoted = g_list_prepend(n_data->promoted, entry);
             n_data->active = g_list_prepend(n_data->active,
                                             dup_notify_entry(entry));
             break;
 
         default:
             pcmk__sched_err("Resource %s role on %s (%s) is not supported for "
                             "notifications (bug?)",
                             rsc->id, pcmk__node_name(node),
                             pcmk_role_text(rsc->role));
             free(entry);
             break;
     }
 
     if (!activity) {
         return;
     }
 
     // Add notification entries for each of the resource's actions
     for (iter = rsc->private->actions; iter != NULL; iter = iter->next) {
         const pcmk_action_t *op = (const pcmk_action_t *) iter->data;
 
         if (!pcmk_is_set(op->flags, pcmk_action_optional)
             && (op->node != NULL)) {
             enum action_tasks task = pcmk_parse_action(op->task);
 
             if ((task == pcmk_action_stop) && op->node->details->unclean) {
                 // Create anyway (additional noise if node can't be fenced)
             } else if (!pcmk_is_set(op->flags, pcmk_action_runnable)) {
                 continue;
             }
 
             entry = new_notify_entry(rsc, op->node);
 
             switch (task) {
                 case pcmk_action_start:
                     n_data->start = g_list_prepend(n_data->start, entry);
                     break;
                 case pcmk_action_stop:
                     n_data->stop = g_list_prepend(n_data->stop, entry);
                     break;
                 case pcmk_action_promote:
                     n_data->promote = g_list_prepend(n_data->promote, entry);
                     break;
                 case pcmk_action_demote:
                     n_data->demote = g_list_prepend(n_data->demote, entry);
                     break;
                 default:
                     free(entry);
                     break;
             }
         }
     }
 }
 
 // For (char *) value
 #define add_notify_env(n_data, key, value) do {                         \
          n_data->keys = pcmk_prepend_nvpair(n_data->keys, key, value);  \
     } while (0)
 
 // For (GString *) value
 #define add_notify_env_gs(n_data, key, value) do {                      \
          n_data->keys = pcmk_prepend_nvpair(n_data->keys, key,          \
                                             (const char *) value->str); \
     } while (0)
 
 // For (GString *) value
 #define add_notify_env_free_gs(n_data, key, value) do {                 \
          n_data->keys = pcmk_prepend_nvpair(n_data->keys, key,          \
                                             (const char *) value->str); \
          g_string_free(value, TRUE); value = NULL;                      \
     } while (0)
 
 /*!
  * \internal
  * \brief Create notification name/value pairs from structured data
  *
  * \param[in]     rsc       Resource that notification is for
  * \param[in,out] n_data    Notification data
  */
 static void
 add_notif_keys(const pcmk_resource_t *rsc, notify_data_t *n_data)
 {
     bool required = false; // Whether to make notify actions required
     GString *rsc_list = NULL;
     GString *node_list = NULL;
     GString *metal_list = NULL;
     const char *source = NULL;
     GList *nodes = NULL;
 
     n_data->stop = notify_entries_to_strings(n_data->stop,
                                              &rsc_list, &node_list);
     if ((strcmp(" ", (const char *) rsc_list->str) != 0)
         && pcmk__str_eq(n_data->action, PCMK_ACTION_STOP, pcmk__str_none)) {
         required = true;
     }
     add_notify_env_free_gs(n_data, "notify_stop_resource", rsc_list);
     add_notify_env_free_gs(n_data, "notify_stop_uname", node_list);
 
     if ((n_data->start != NULL)
         && pcmk__str_eq(n_data->action, PCMK_ACTION_START, pcmk__str_none)) {
         required = true;
     }
     n_data->start = notify_entries_to_strings(n_data->start,
                                               &rsc_list, &node_list);
     add_notify_env_free_gs(n_data, "notify_start_resource", rsc_list);
     add_notify_env_free_gs(n_data, "notify_start_uname", node_list);
 
     if ((n_data->demote != NULL)
         && pcmk__str_eq(n_data->action, PCMK_ACTION_DEMOTE, pcmk__str_none)) {
         required = true;
     }
     n_data->demote = notify_entries_to_strings(n_data->demote,
                                                &rsc_list, &node_list);
     add_notify_env_free_gs(n_data, "notify_demote_resource", rsc_list);
     add_notify_env_free_gs(n_data, "notify_demote_uname", node_list);
 
     if ((n_data->promote != NULL)
         && pcmk__str_eq(n_data->action, PCMK_ACTION_PROMOTE, pcmk__str_none)) {
         required = true;
     }
     n_data->promote = notify_entries_to_strings(n_data->promote,
                                                 &rsc_list, &node_list);
     add_notify_env_free_gs(n_data, "notify_promote_resource", rsc_list);
     add_notify_env_free_gs(n_data, "notify_promote_uname", node_list);
 
     n_data->active = notify_entries_to_strings(n_data->active,
                                                &rsc_list, &node_list);
     add_notify_env_free_gs(n_data, "notify_active_resource", rsc_list);
     add_notify_env_free_gs(n_data, "notify_active_uname", node_list);
 
     n_data->unpromoted = notify_entries_to_strings(n_data->unpromoted,
                                                    &rsc_list, &node_list);
     add_notify_env_gs(n_data, "notify_unpromoted_resource", rsc_list);
     add_notify_env_gs(n_data, "notify_unpromoted_uname", node_list);
 
     // Deprecated: kept for backward compatibility with older resource agents
     add_notify_env_free_gs(n_data, "notify_slave_resource", rsc_list);
     add_notify_env_free_gs(n_data, "notify_slave_uname", node_list);
 
     n_data->promoted = notify_entries_to_strings(n_data->promoted,
                                                  &rsc_list, &node_list);
     add_notify_env_gs(n_data, "notify_promoted_resource", rsc_list);
     add_notify_env_gs(n_data, "notify_promoted_uname", node_list);
 
     // Deprecated: kept for backward compatibility with older resource agents
     add_notify_env_free_gs(n_data, "notify_master_resource", rsc_list);
     add_notify_env_free_gs(n_data, "notify_master_uname", node_list);
 
     n_data->inactive = notify_entries_to_strings(n_data->inactive,
                                                  &rsc_list, NULL);
     add_notify_env_free_gs(n_data, "notify_inactive_resource", rsc_list);
 
     nodes = g_hash_table_get_values(n_data->allowed_nodes);
     if (!pcmk__is_daemon) {
         /* For display purposes, sort the node list, for consistent
          * regression test output (while avoiding the performance hit
          * for the live cluster).
          */
         nodes = g_list_sort(nodes, pe__cmp_node_name);
     }
     get_node_names(nodes, &node_list, NULL);
     add_notify_env_free_gs(n_data, "notify_available_uname", node_list);
     g_list_free(nodes);
 
     source = g_hash_table_lookup(rsc->meta,
                                  PCMK_META_CONTAINER_ATTRIBUTE_TARGET);
     if (pcmk__str_eq(PCMK_VALUE_HOST, source, pcmk__str_none)) {
         get_node_names(rsc->private->scheduler->nodes, &node_list, &metal_list);
         add_notify_env_free_gs(n_data, "notify_all_hosts", metal_list);
     } else {
         get_node_names(rsc->private->scheduler->nodes, &node_list, NULL);
     }
     add_notify_env_free_gs(n_data, "notify_all_uname", node_list);
 
     if (required && (n_data->pre != NULL)) {
         pcmk__clear_action_flags(n_data->pre, pcmk_action_optional);
         pcmk__clear_action_flags(n_data->pre_done, pcmk_action_optional);
     }
 
     if (required && (n_data->post != NULL)) {
         pcmk__clear_action_flags(n_data->post, pcmk_action_optional);
         pcmk__clear_action_flags(n_data->post_done, pcmk_action_optional);
     }
 }
 
 /*
  * \internal
  * \brief Find any remote connection start relevant to an action
  *
  * \param[in] action  Action to check
  *
  * \return If action is behind a remote connection, connection's start
  */
 static pcmk_action_t *
 find_remote_start(pcmk_action_t *action)
 {
     if ((action != NULL) && (action->node != NULL)) {
         pcmk_resource_t *remote_rsc = action->node->details->remote_rsc;
 
         if (remote_rsc != NULL) {
             return find_first_action(remote_rsc->private->actions, NULL,
                                      PCMK_ACTION_START,
                                      NULL);
         }
     }
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Create notify actions, and add notify data to original actions
  *
  * \param[in,out] rsc     Clone or clone instance that notification is for
  * \param[in,out] n_data  Clone notification data for some action
  */
 static void
 create_notify_actions(pcmk_resource_t *rsc, notify_data_t *n_data)
 {
     GList *iter = NULL;
     pcmk_action_t *stop = NULL;
     pcmk_action_t *start = NULL;
     enum action_tasks task = pcmk_parse_action(n_data->action);
 
     // If this is a clone, call recursively for each instance
     if (rsc->children != NULL) {
         g_list_foreach(rsc->children, (GFunc) create_notify_actions, n_data);
         return;
     }
 
     // Add notification meta-attributes to original actions
     for (iter = rsc->private->actions; iter != NULL; iter = iter->next) {
         pcmk_action_t *op = (pcmk_action_t *) iter->data;
 
         if (!pcmk_is_set(op->flags, pcmk_action_optional)
             && (op->node != NULL)) {
             switch (pcmk_parse_action(op->task)) {
                 case pcmk_action_start:
                 case pcmk_action_stop:
                 case pcmk_action_promote:
                 case pcmk_action_demote:
                     add_notify_data_to_action_meta(n_data, op);
                     break;
                 default:
                     break;
             }
         }
     }
 
     // Skip notify action itself if original action was not needed
     switch (task) {
         case pcmk_action_start:
             if (n_data->start == NULL) {
                 pcmk__rsc_trace(rsc, "No notify action needed for %s %s",
                                 rsc->id, n_data->action);
                 return;
             }
             break;
 
         case pcmk_action_promote:
             if (n_data->promote == NULL) {
                 pcmk__rsc_trace(rsc, "No notify action needed for %s %s",
                                 rsc->id, n_data->action);
                 return;
             }
             break;
 
         case pcmk_action_demote:
             if (n_data->demote == NULL) {
                 pcmk__rsc_trace(rsc, "No notify action needed for %s %s",
                                 rsc->id, n_data->action);
                 return;
             }
             break;
 
         default:
             // We cannot do same for stop because it might be implied by fencing
             break;
     }
 
     pcmk__rsc_trace(rsc, "Creating notify actions for %s %s",
                     rsc->id, n_data->action);
 
     // Create notify actions for stop or demote
     if ((rsc->role != pcmk_role_stopped)
         && ((task == pcmk_action_stop) || (task == pcmk_action_demote))) {
 
         stop = find_first_action(rsc->private->actions, NULL, PCMK_ACTION_STOP,
                                  NULL);
 
         for (iter = rsc->running_on; iter != NULL; iter = iter->next) {
             pcmk_node_t *current_node = (pcmk_node_t *) iter->data;
 
             /* If a stop is a pseudo-action implied by fencing, don't try to
              * notify the node getting fenced.
              */
             if ((stop != NULL)
                 && pcmk_is_set(stop->flags, pcmk_action_pseudo)
                 && (current_node->details->unclean
                     || current_node->details->remote_requires_reset)) {
                 continue;
             }
 
             new_notify_action(rsc, current_node, n_data->pre,
                               n_data->pre_done, n_data);
 
             if ((task == pcmk_action_demote) || (stop == NULL)
                 || pcmk_is_set(stop->flags, pcmk_action_optional)) {
                 new_post_notify_action(rsc, current_node, n_data);
             }
         }
     }
 
     // Create notify actions for start or promote
     if ((rsc->next_role != pcmk_role_stopped)
         && ((task == pcmk_action_start) || (task == pcmk_action_promote))) {
 
         start = find_first_action(rsc->private->actions, NULL,
                                   PCMK_ACTION_START, NULL);
         if (start != NULL) {
             pcmk_action_t *remote_start = find_remote_start(start);
 
             if ((remote_start != NULL)
                 && !pcmk_is_set(remote_start->flags, pcmk_action_runnable)) {
                 /* Start and promote actions for a clone instance behind
                  * a Pacemaker Remote connection happen after the
                  * connection starts. If the connection start is blocked, do
                  * not schedule notifications for these actions.
                  */
                 return;
             }
         }
-        if (rsc->allocated_to == NULL) {
+        if (rsc->private->assigned_node == NULL) {
             pcmk__sched_err("Next role '%s' but %s is not allocated",
                             pcmk_role_text(rsc->next_role), rsc->id);
             return;
         }
         if ((task != pcmk_action_start) || (start == NULL)
             || pcmk_is_set(start->flags, pcmk_action_optional)) {
 
-            new_notify_action(rsc, rsc->allocated_to, n_data->pre,
+            new_notify_action(rsc, rsc->private->assigned_node, n_data->pre,
                               n_data->pre_done, n_data);
         }
-        new_post_notify_action(rsc, rsc->allocated_to, n_data);
+        new_post_notify_action(rsc, rsc->private->assigned_node, n_data);
     }
 }
 
 /*!
  * \internal
  * \brief Create notification data and actions for one clone action
  *
  * \param[in,out] rsc     Clone resource that notification is for
  * \param[in,out] n_data  Clone notification data for some action
  */
 void
 pe__create_action_notifications(pcmk_resource_t *rsc, notify_data_t *n_data)
 {
     if ((rsc == NULL) || (n_data == NULL)) {
         return;
     }
     collect_resource_data(rsc, true, n_data);
     add_notif_keys(rsc, n_data);
     create_notify_actions(rsc, n_data);
 }
 
 /*!
  * \internal
  * \brief Free notification data for one action
  *
  * \param[in,out] n_data  Notification data to free
  */
 void
 pe__free_action_notification_data(notify_data_t *n_data)
 {
     if (n_data == NULL) {
         return;
     }
     g_list_free_full(n_data->stop, free);
     g_list_free_full(n_data->start, free);
     g_list_free_full(n_data->demote, free);
     g_list_free_full(n_data->promote, free);
     g_list_free_full(n_data->promoted, free);
     g_list_free_full(n_data->unpromoted, free);
     g_list_free_full(n_data->active, free);
     g_list_free_full(n_data->inactive, free);
     pcmk_free_nvpairs(n_data->keys);
     free(n_data);
 }
 
 /*!
  * \internal
  * \brief Order clone "notifications complete" pseudo-action after fencing
  *
  * If a stop action is implied by fencing, the usual notification pseudo-actions
  * will not be sufficient to order things properly, or even create all needed
  * notifications if the clone is also stopping on another node, and another
  * clone is ordered after it. This function creates new notification
  * pseudo-actions relative to the fencing to ensure everything works properly.
  *
  * \param[in]     stop        Stop action implied by fencing
  * \param[in,out] rsc         Clone resource that notification is for
  * \param[in,out] stonith_op  Fencing action that implies \p stop
  */
 void
 pe__order_notifs_after_fencing(const pcmk_action_t *stop, pcmk_resource_t *rsc,
                                pcmk_action_t *stonith_op)
 {
     notify_data_t *n_data;
 
     crm_info("Ordering notifications for implied %s after fencing", stop->uuid);
     n_data = pe__action_notif_pseudo_ops(rsc, PCMK_ACTION_STOP, NULL,
                                          stonith_op);
 
     if (n_data != NULL) {
         collect_resource_data(rsc, false, n_data);
         add_notify_env(n_data, "notify_stop_resource", rsc->id);
         add_notify_env(n_data, "notify_stop_uname", stop->node->details->uname);
         create_notify_actions(uber_parent(rsc), n_data);
         pe__free_action_notification_data(n_data);
     }
 }
diff --git a/lib/pengine/utils.c b/lib/pengine/utils.c
index 44f2dc21ea..1acc2b00ad 100644
--- a/lib/pengine/utils.c
+++ b/lib/pengine/utils.c
@@ -1,927 +1,928 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <glib.h>
 #include <stdbool.h>
 
 #include <crm/crm.h>
 #include <crm/common/xml.h>
 #include <crm/pengine/rules.h>
 #include <crm/pengine/internal.h>
 
 #include "pe_status_private.h"
 
 extern bool pcmk__is_daemon;
 
 gboolean ghash_free_str_str(gpointer key, gpointer value, gpointer user_data);
 
 /*!
  * \internal
  * \brief Check whether we can fence a particular node
  *
  * \param[in] scheduler  Scheduler data
  * \param[in] node       Name of node to check
  *
  * \return true if node can be fenced, false otherwise
  */
 bool
 pe_can_fence(const pcmk_scheduler_t *scheduler, const pcmk_node_t *node)
 {
     if (pcmk__is_guest_or_bundle_node(node)) {
         /* Guest nodes are fenced by stopping their container resource. We can
          * do that if the container's host is either online or fenceable.
          */
         pcmk_resource_t *rsc = node->details->remote_rsc->container;
 
         for (GList *n = rsc->running_on; n != NULL; n = n->next) {
             pcmk_node_t *container_node = n->data;
 
             if (!container_node->details->online
                 && !pe_can_fence(scheduler, container_node)) {
                 return false;
             }
         }
         return true;
 
     } else if (!pcmk_is_set(scheduler->flags, pcmk_sched_fencing_enabled)) {
         return false; /* Turned off */
 
     } else if (!pcmk_is_set(scheduler->flags, pcmk_sched_have_fencing)) {
         return false; /* No devices */
 
     } else if (pcmk_is_set(scheduler->flags, pcmk_sched_quorate)) {
         return true;
 
     } else if (scheduler->no_quorum_policy == pcmk_no_quorum_ignore) {
         return true;
 
     } else if(node == NULL) {
         return false;
 
     } else if(node->details->online) {
         crm_notice("We can fence %s without quorum because they're in our membership",
                    pcmk__node_name(node));
         return true;
     }
 
     crm_trace("Cannot fence %s", pcmk__node_name(node));
     return false;
 }
 
 /*!
  * \internal
  * \brief Copy a node object
  *
  * \param[in] this_node  Node object to copy
  *
  * \return Newly allocated shallow copy of this_node
  * \note This function asserts on errors and is guaranteed to return non-NULL.
  */
 pcmk_node_t *
 pe__copy_node(const pcmk_node_t *this_node)
 {
     pcmk_node_t *new_node = NULL;
 
     CRM_ASSERT(this_node != NULL);
 
     new_node = pcmk__assert_alloc(1, sizeof(pcmk_node_t));
 
     new_node->rsc_discover_mode = this_node->rsc_discover_mode;
     new_node->weight = this_node->weight;
     new_node->fixed = this_node->fixed; // @COMPAT deprecated and unused
     new_node->count = this_node->count;
     new_node->details = this_node->details;
 
     return new_node;
 }
 
 /*!
  * \internal
  * \brief Create a node hash table from a node list
  *
  * \param[in] list  Node list
  *
  * \return Hash table equivalent of node list
  */
 GHashTable *
 pe__node_list2table(const GList *list)
 {
     GHashTable *result = NULL;
 
     result = pcmk__strkey_table(NULL, free);
     for (const GList *gIter = list; gIter != NULL; gIter = gIter->next) {
         pcmk_node_t *new_node = NULL;
 
         new_node = pe__copy_node((const pcmk_node_t *) gIter->data);
         g_hash_table_insert(result, (gpointer) new_node->details->id, new_node);
     }
     return result;
 }
 
 /*!
  * \internal
  * \brief Compare two nodes by name, with numeric portions sorted numerically
  *
  * Sort two node names case-insensitively like strcasecmp(), but with any
  * numeric portions of the name sorted numerically. For example, "node10" will
  * sort higher than "node9" but lower than "remotenode9".
  *
  * \param[in] a  First node to compare (can be \c NULL)
  * \param[in] b  Second node to compare (can be \c NULL)
  *
  * \retval -1 \c a comes before \c b (or \c a is \c NULL and \c b is not)
  * \retval  0 \c a and \c b are equal (or both are \c NULL)
  * \retval  1 \c a comes after \c b (or \c b is \c NULL and \c a is not)
  */
 gint
 pe__cmp_node_name(gconstpointer a, gconstpointer b)
 {
     const pcmk_node_t *node1 = (const pcmk_node_t *) a;
     const pcmk_node_t *node2 = (const pcmk_node_t *) b;
 
     if ((node1 == NULL) && (node2 == NULL)) {
         return 0;
     }
 
     if (node1 == NULL) {
         return -1;
     }
 
     if (node2 == NULL) {
         return 1;
     }
 
     return pcmk__numeric_strcasecmp(node1->details->uname,
                                     node2->details->uname);
 }
 
 /*!
  * \internal
  * \brief Output node weights to stdout
  *
  * \param[in]     rsc        Use allowed nodes for this resource
  * \param[in]     comment    Text description to prefix lines with
  * \param[in]     nodes      If rsc is not specified, use these nodes
  * \param[in,out] scheduler  Scheduler data
  */
 static void
 pe__output_node_weights(const pcmk_resource_t *rsc, const char *comment,
                         GHashTable *nodes, pcmk_scheduler_t *scheduler)
 {
     pcmk__output_t *out = scheduler->priv;
 
     // Sort the nodes so the output is consistent for regression tests
     GList *list = g_list_sort(g_hash_table_get_values(nodes),
                               pe__cmp_node_name);
 
     for (const GList *gIter = list; gIter != NULL; gIter = gIter->next) {
         const pcmk_node_t *node = (const pcmk_node_t *) gIter->data;
 
         out->message(out, "node-weight", rsc, comment, node->details->uname,
                      pcmk_readable_score(node->weight));
     }
     g_list_free(list);
 }
 
 /*!
  * \internal
  * \brief Log node weights at trace level
  *
  * \param[in] file      Caller's filename
  * \param[in] function  Caller's function name
  * \param[in] line      Caller's line number
  * \param[in] rsc       If not NULL, include this resource's ID in logs
  * \param[in] comment   Text description to prefix lines with
  * \param[in] nodes     Nodes whose scores should be logged
  */
 static void
 pe__log_node_weights(const char *file, const char *function, int line,
                      const pcmk_resource_t *rsc, const char *comment,
                      GHashTable *nodes)
 {
     GHashTableIter iter;
     pcmk_node_t *node = NULL;
 
     // Don't waste time if we're not tracing at this point
     pcmk__if_tracing({}, return);
 
     g_hash_table_iter_init(&iter, nodes);
     while (g_hash_table_iter_next(&iter, NULL, (void **) &node)) {
         if (rsc) {
             qb_log_from_external_source(function, file,
                                         "%s: %s allocation score on %s: %s",
                                         LOG_TRACE, line, 0,
                                         comment, rsc->id,
                                         pcmk__node_name(node),
                                         pcmk_readable_score(node->weight));
         } else {
             qb_log_from_external_source(function, file, "%s: %s = %s",
                                         LOG_TRACE, line, 0,
                                         comment, pcmk__node_name(node),
                                         pcmk_readable_score(node->weight));
         }
     }
 }
 
 /*!
  * \internal
  * \brief Log or output node weights
  *
  * \param[in]     file       Caller's filename
  * \param[in]     function   Caller's function name
  * \param[in]     line       Caller's line number
  * \param[in]     to_log     Log if true, otherwise output
  * \param[in]     rsc        If not NULL, use this resource's ID in logs,
  *                           and show scores recursively for any children
  * \param[in]     comment    Text description to prefix lines with
  * \param[in]     nodes      Nodes whose scores should be shown
  * \param[in,out] scheduler  Scheduler data
  */
 void
 pe__show_node_scores_as(const char *file, const char *function, int line,
                         bool to_log, const pcmk_resource_t *rsc,
                         const char *comment, GHashTable *nodes,
                         pcmk_scheduler_t *scheduler)
 {
     if ((rsc != NULL) && pcmk_is_set(rsc->flags, pcmk__rsc_removed)) {
         // Don't show allocation scores for orphans
         return;
     }
     if (nodes == NULL) {
         // Nothing to show
         return;
     }
 
     if (to_log) {
         pe__log_node_weights(file, function, line, rsc, comment, nodes);
     } else {
         pe__output_node_weights(rsc, comment, nodes, scheduler);
     }
 
     // If this resource has children, repeat recursively for each
     if (rsc && rsc->children) {
         for (GList *gIter = rsc->children; gIter != NULL; gIter = gIter->next) {
             pcmk_resource_t *child = (pcmk_resource_t *) gIter->data;
 
             pe__show_node_scores_as(file, function, line, to_log, child,
                                     comment, child->allowed_nodes, scheduler);
         }
     }
 }
 
 /*!
  * \internal
  * \brief Compare two resources by priority
  *
  * \param[in] a  First resource to compare (can be \c NULL)
  * \param[in] b  Second resource to compare (can be \c NULL)
  *
  * \retval -1 a's priority > b's priority (or \c b is \c NULL and \c a is not)
  * \retval  0 a's priority == b's priority (or both \c a and \c b are \c NULL)
  * \retval  1 a's priority < b's priority (or \c a is \c NULL and \c b is not)
  */
 gint
 pe__cmp_rsc_priority(gconstpointer a, gconstpointer b)
 {
     const pcmk_resource_t *resource1 = (const pcmk_resource_t *)a;
     const pcmk_resource_t *resource2 = (const pcmk_resource_t *)b;
 
     if (a == NULL && b == NULL) {
         return 0;
     }
     if (a == NULL) {
         return 1;
     }
     if (b == NULL) {
         return -1;
     }
 
     if (resource1->private->priority > resource2->private->priority) {
         return -1;
     }
 
     if (resource1->private->priority < resource2->private->priority) {
         return 1;
     }
 
     return 0;
 }
 
 static void
 resource_node_score(pcmk_resource_t *rsc, const pcmk_node_t *node, int score,
                     const char *tag)
 {
     pcmk_node_t *match = NULL;
 
     if ((pcmk_is_set(rsc->flags, pcmk__rsc_exclusive_probes)
          || (node->rsc_discover_mode == pcmk_probe_never))
         && pcmk__str_eq(tag, "symmetric_default", pcmk__str_casei)) {
         /* This string comparision may be fragile, but exclusive resources and
          * exclusive nodes should not have the symmetric_default constraint
          * applied to them.
          */
         return;
 
     } else if (rsc->children) {
         GList *gIter = rsc->children;
 
         for (; gIter != NULL; gIter = gIter->next) {
             pcmk_resource_t *child_rsc = (pcmk_resource_t *) gIter->data;
 
             resource_node_score(child_rsc, node, score, tag);
         }
     }
 
     match = g_hash_table_lookup(rsc->allowed_nodes, node->details->id);
     if (match == NULL) {
         match = pe__copy_node(node);
         g_hash_table_insert(rsc->allowed_nodes, (gpointer) match->details->id, match);
     }
     match->weight = pcmk__add_scores(match->weight, score);
     pcmk__rsc_trace(rsc,
                     "Enabling %s preference (%s) for %s on %s (now %s)",
                     tag, pcmk_readable_score(score), rsc->id,
                     pcmk__node_name(node), pcmk_readable_score(match->weight));
 }
 
 void
 resource_location(pcmk_resource_t *rsc, const pcmk_node_t *node, int score,
                   const char *tag, pcmk_scheduler_t *scheduler)
 {
     if (node != NULL) {
         resource_node_score(rsc, node, score, tag);
 
     } else if (scheduler != NULL) {
         GList *gIter = scheduler->nodes;
 
         for (; gIter != NULL; gIter = gIter->next) {
             pcmk_node_t *node_iter = (pcmk_node_t *) gIter->data;
 
             resource_node_score(rsc, node_iter, score, tag);
         }
 
     } else {
         GHashTableIter iter;
         pcmk_node_t *node_iter = NULL;
 
         g_hash_table_iter_init(&iter, rsc->allowed_nodes);
         while (g_hash_table_iter_next(&iter, NULL, (void **)&node_iter)) {
             resource_node_score(rsc, node_iter, score, tag);
         }
     }
 
-    if ((node == NULL) && (score == -PCMK_SCORE_INFINITY)) {
-        if (rsc->allocated_to) {
-            crm_info("Deallocating %s from %s",
-                     rsc->id, pcmk__node_name(rsc->allocated_to));
-            free(rsc->allocated_to);
-            rsc->allocated_to = NULL;
-        }
+    if ((node == NULL) && (score == -PCMK_SCORE_INFINITY)
+        && (rsc->private->assigned_node != NULL)) {
+
+        // @TODO Should this be more like pcmk__unassign_resource()?
+        crm_info("Unassigning %s from %s",
+                 rsc->id, pcmk__node_name(rsc->private->assigned_node));
+        free(rsc->private->assigned_node);
+        rsc->private->assigned_node = NULL;
     }
 }
 
 time_t
 get_effective_time(pcmk_scheduler_t *scheduler)
 {
     if(scheduler) {
         if (scheduler->now == NULL) {
             crm_trace("Recording a new 'now'");
             scheduler->now = crm_time_new(NULL);
         }
         return crm_time_get_seconds_since_epoch(scheduler->now);
     }
 
     crm_trace("Defaulting to 'now'");
     return time(NULL);
 }
 
 gboolean
 get_target_role(const pcmk_resource_t *rsc, enum rsc_role_e *role)
 {
     enum rsc_role_e local_role = pcmk_role_unknown;
     const char *value = g_hash_table_lookup(rsc->meta, PCMK_META_TARGET_ROLE);
 
     CRM_CHECK(role != NULL, return FALSE);
 
     if (pcmk__str_eq(value, PCMK_ROLE_STARTED,
                      pcmk__str_null_matches|pcmk__str_casei)) {
         return FALSE;
     }
     if (pcmk__str_eq(PCMK_VALUE_DEFAULT, value, pcmk__str_casei)) {
         // @COMPAT Deprecated since 2.1.8
         pcmk__config_warn("Support for setting " PCMK_META_TARGET_ROLE
                           " to the explicit value '" PCMK_VALUE_DEFAULT
                           "' is deprecated and will be removed in a "
                           "future release (just leave it unset)");
         return FALSE;
     }
 
     local_role = pcmk_parse_role(value);
     if (local_role == pcmk_role_unknown) {
         pcmk__config_err("Ignoring '" PCMK_META_TARGET_ROLE "' for %s "
                          "because '%s' is not valid", rsc->id, value);
         return FALSE;
 
     } else if (local_role > pcmk_role_started) {
         if (pcmk_is_set(pe__const_top_resource(rsc, false)->flags,
                         pcmk__rsc_promotable)) {
             if (local_role > pcmk_role_unpromoted) {
                 /* This is what we'd do anyway, just leave the default to avoid messing up the placement algorithm */
                 return FALSE;
             }
 
         } else {
             pcmk__config_err("Ignoring '" PCMK_META_TARGET_ROLE "' for %s "
                              "because '%s' only makes sense for promotable "
                              "clones", rsc->id, value);
             return FALSE;
         }
     }
 
     *role = local_role;
     return TRUE;
 }
 
 gboolean
 order_actions(pcmk_action_t *lh_action, pcmk_action_t *rh_action,
               uint32_t flags)
 {
     GList *gIter = NULL;
     pcmk__related_action_t *wrapper = NULL;
     GList *list = NULL;
 
     if (flags == pcmk__ar_none) {
         return FALSE;
     }
 
     if (lh_action == NULL || rh_action == NULL) {
         return FALSE;
     }
 
     crm_trace("Creating action wrappers for ordering: %s then %s",
               lh_action->uuid, rh_action->uuid);
 
     /* Ensure we never create a dependency on ourselves... it's happened */
     CRM_ASSERT(lh_action != rh_action);
 
     /* Filter dups, otherwise update_action_states() has too much work to do */
     gIter = lh_action->actions_after;
     for (; gIter != NULL; gIter = gIter->next) {
         pcmk__related_action_t *after = gIter->data;
 
         if (after->action == rh_action && (after->type & flags)) {
             return FALSE;
         }
     }
 
     wrapper = pcmk__assert_alloc(1, sizeof(pcmk__related_action_t));
     wrapper->action = rh_action;
     wrapper->type = flags;
     list = lh_action->actions_after;
     list = g_list_prepend(list, wrapper);
     lh_action->actions_after = list;
 
     wrapper = pcmk__assert_alloc(1, sizeof(pcmk__related_action_t));
     wrapper->action = lh_action;
     wrapper->type = flags;
     list = rh_action->actions_before;
     list = g_list_prepend(list, wrapper);
     rh_action->actions_before = list;
     return TRUE;
 }
 
 void
 destroy_ticket(gpointer data)
 {
     pcmk_ticket_t *ticket = data;
 
     if (ticket->state) {
         g_hash_table_destroy(ticket->state);
     }
     free(ticket->id);
     free(ticket);
 }
 
 pcmk_ticket_t *
 ticket_new(const char *ticket_id, pcmk_scheduler_t *scheduler)
 {
     pcmk_ticket_t *ticket = NULL;
 
     if (pcmk__str_empty(ticket_id)) {
         return NULL;
     }
 
     if (scheduler->tickets == NULL) {
         scheduler->tickets = pcmk__strkey_table(free, destroy_ticket);
     }
 
     ticket = g_hash_table_lookup(scheduler->tickets, ticket_id);
     if (ticket == NULL) {
 
         ticket = calloc(1, sizeof(pcmk_ticket_t));
         if (ticket == NULL) {
             pcmk__sched_err("Cannot allocate ticket '%s'", ticket_id);
             return NULL;
         }
 
         crm_trace("Creating ticket entry for %s", ticket_id);
 
         ticket->id = strdup(ticket_id);
         ticket->granted = FALSE;
         ticket->last_granted = -1;
         ticket->standby = FALSE;
         ticket->state = pcmk__strkey_table(free, free);
 
         g_hash_table_insert(scheduler->tickets, strdup(ticket->id), ticket);
     }
 
     return ticket;
 }
 
 const char *
 rsc_printable_id(const pcmk_resource_t *rsc)
 {
     if (pcmk_is_set(rsc->flags, pcmk__rsc_unique)) {
         return rsc->id;
     }
     return pcmk__xe_id(rsc->private->xml);
 }
 
 void
 pe__clear_resource_flags_recursive(pcmk_resource_t *rsc, uint64_t flags)
 {
     pcmk__clear_rsc_flags(rsc, flags);
     for (GList *gIter = rsc->children; gIter != NULL; gIter = gIter->next) {
         pe__clear_resource_flags_recursive((pcmk_resource_t *) gIter->data,
                                            flags);
     }
 }
 
 void
 pe__clear_resource_flags_on_all(pcmk_scheduler_t *scheduler, uint64_t flag)
 {
     for (GList *lpc = scheduler->resources; lpc != NULL; lpc = lpc->next) {
         pcmk_resource_t *r = (pcmk_resource_t *) lpc->data;
         pe__clear_resource_flags_recursive(r, flag);
     }
 }
 
 void
 pe__set_resource_flags_recursive(pcmk_resource_t *rsc, uint64_t flags)
 {
     pcmk__set_rsc_flags(rsc, flags);
     for (GList *gIter = rsc->children; gIter != NULL; gIter = gIter->next) {
         pe__set_resource_flags_recursive((pcmk_resource_t *) gIter->data,
                                          flags);
     }
 }
 
 void
 trigger_unfencing(pcmk_resource_t *rsc, pcmk_node_t *node, const char *reason,
                   pcmk_action_t *dependency, pcmk_scheduler_t *scheduler)
 {
     if (!pcmk_is_set(scheduler->flags, pcmk_sched_enable_unfencing)) {
         /* No resources require it */
         return;
 
     } else if ((rsc != NULL)
                && !pcmk_is_set(rsc->flags, pcmk__rsc_fence_device)) {
         /* Wasn't a stonith device */
         return;
 
     } else if(node
               && node->details->online
               && node->details->unclean == FALSE
               && node->details->shutdown == FALSE) {
         pcmk_action_t *unfence = pe_fence_op(node, PCMK_ACTION_ON, FALSE,
                                              reason, FALSE, scheduler);
 
         if(dependency) {
             order_actions(unfence, dependency, pcmk__ar_ordered);
         }
 
     } else if(rsc) {
         GHashTableIter iter;
 
         g_hash_table_iter_init(&iter, rsc->allowed_nodes);
         while (g_hash_table_iter_next(&iter, NULL, (void **)&node)) {
             if(node->details->online && node->details->unclean == FALSE && node->details->shutdown == FALSE) {
                 trigger_unfencing(rsc, node, reason, dependency, scheduler);
             }
         }
     }
 }
 
 gboolean
 add_tag_ref(GHashTable * tags, const char * tag_name,  const char * obj_ref)
 {
     pcmk_tag_t *tag = NULL;
     GList *gIter = NULL;
     gboolean is_existing = FALSE;
 
     CRM_CHECK(tags && tag_name && obj_ref, return FALSE);
 
     tag = g_hash_table_lookup(tags, tag_name);
     if (tag == NULL) {
         tag = calloc(1, sizeof(pcmk_tag_t));
         if (tag == NULL) {
             pcmk__sched_err("Could not allocate memory for tag %s", tag_name);
             return FALSE;
         }
         tag->id = strdup(tag_name);
         tag->refs = NULL;
         g_hash_table_insert(tags, strdup(tag_name), tag);
     }
 
     for (gIter = tag->refs; gIter != NULL; gIter = gIter->next) {
         const char *existing_ref = (const char *) gIter->data;
 
         if (pcmk__str_eq(existing_ref, obj_ref, pcmk__str_none)){
             is_existing = TRUE;
             break;
         }
     }
 
     if (is_existing == FALSE) {
         tag->refs = g_list_append(tag->refs, strdup(obj_ref));
         crm_trace("Added: tag=%s ref=%s", tag->id, obj_ref);
     }
 
     return TRUE;
 }
 
 /*!
  * \internal
  * \brief Check whether shutdown has been requested for a node
  *
  * \param[in] node  Node to check
  *
  * \return TRUE if node has shutdown attribute set and nonzero, FALSE otherwise
  * \note This differs from simply using node->details->shutdown in that it can
  *       be used before that has been determined (and in fact to determine it),
  *       and it can also be used to distinguish requested shutdown from implicit
  *       shutdown of remote nodes by virtue of their connection stopping.
  */
 bool
 pe__shutdown_requested(const pcmk_node_t *node)
 {
     const char *shutdown = pcmk__node_attr(node, PCMK__NODE_ATTR_SHUTDOWN, NULL,
                                            pcmk__rsc_node_current);
 
     return !pcmk__str_eq(shutdown, "0", pcmk__str_null_matches);
 }
 
 /*!
  * \internal
  * \brief Update "recheck by" time in scheduler data
  *
  * \param[in]     recheck    Epoch time when recheck should happen
  * \param[in,out] scheduler  Scheduler data
  * \param[in]     reason     What time is being updated for (for logs)
  */
 void
 pe__update_recheck_time(time_t recheck, pcmk_scheduler_t *scheduler,
                         const char *reason)
 {
     if ((recheck > get_effective_time(scheduler))
         && ((scheduler->recheck_by == 0)
             || (scheduler->recheck_by > recheck))) {
         scheduler->recheck_by = recheck;
         crm_debug("Updated next scheduler recheck to %s for %s",
                   pcmk__trim(ctime(&recheck)), reason);
     }
 }
 
 /*!
  * \internal
  * \brief Extract nvpair blocks contained by a CIB XML element into a hash table
  *
  * \param[in]     xml_obj       XML element containing blocks of nvpair elements
  * \param[in]     set_name      If not NULL, only use blocks of this element
  * \param[in]     rule_data     Matching parameters to use when unpacking
  * \param[out]    hash          Where to store extracted name/value pairs
  * \param[in]     always_first  If not NULL, process block with this ID first
  * \param[in]     overwrite     Whether to replace existing values with same name
  * \param[in,out] scheduler     Scheduler data containing \p xml_obj
  */
 void
 pe__unpack_dataset_nvpairs(const xmlNode *xml_obj, const char *set_name,
                            const pe_rule_eval_data_t *rule_data,
                            GHashTable *hash, const char *always_first,
                            gboolean overwrite, pcmk_scheduler_t *scheduler)
 {
     crm_time_t *next_change = crm_time_new_undefined();
 
     pe_eval_nvpairs(scheduler->input, xml_obj, set_name, rule_data, hash,
                     always_first, overwrite, next_change);
     if (crm_time_is_defined(next_change)) {
         time_t recheck = (time_t) crm_time_get_seconds_since_epoch(next_change);
 
         pe__update_recheck_time(recheck, scheduler, "rule evaluation");
     }
     crm_time_free(next_change);
 }
 
 bool
 pe__resource_is_disabled(const pcmk_resource_t *rsc)
 {
     const char *target_role = NULL;
 
     CRM_CHECK(rsc != NULL, return false);
     target_role = g_hash_table_lookup(rsc->meta, PCMK_META_TARGET_ROLE);
     if (target_role) {
         // If invalid, we've already logged an error when unpacking
         enum rsc_role_e target_role_e = pcmk_parse_role(target_role);
 
         if ((target_role_e == pcmk_role_stopped)
             || ((target_role_e == pcmk_role_unpromoted)
                 && pcmk_is_set(pe__const_top_resource(rsc, false)->flags,
                                pcmk__rsc_promotable))) {
             return true;
         }
     }
     return false;
 }
 
 /*!
  * \internal
  * \brief Check whether a resource is running only on given node
  *
  * \param[in] rsc   Resource to check
  * \param[in] node  Node to check
  *
  * \return true if \p rsc is running only on \p node, otherwise false
  */
 bool
 pe__rsc_running_on_only(const pcmk_resource_t *rsc, const pcmk_node_t *node)
 {
     return (rsc != NULL) && pcmk__list_of_1(rsc->running_on)
             && pcmk__same_node((const pcmk_node_t *) rsc->running_on->data,
                                node);
 }
 
 bool
 pe__rsc_running_on_any(pcmk_resource_t *rsc, GList *node_list)
 {
     for (GList *ele = rsc->running_on; ele; ele = ele->next) {
         pcmk_node_t *node = (pcmk_node_t *) ele->data;
         if (pcmk__str_in_list(node->details->uname, node_list,
                               pcmk__str_star_matches|pcmk__str_casei)) {
             return true;
         }
     }
 
     return false;
 }
 
 bool
 pcmk__rsc_filtered_by_node(pcmk_resource_t *rsc, GList *only_node)
 {
     return rsc->private->fns->active(rsc, FALSE)
            && !pe__rsc_running_on_any(rsc, only_node);
 }
 
 GList *
 pe__filter_rsc_list(GList *rscs, GList *filter)
 {
     GList *retval = NULL;
 
     for (GList *gIter = rscs; gIter; gIter = gIter->next) {
         pcmk_resource_t *rsc = (pcmk_resource_t *) gIter->data;
 
         /* I think the second condition is safe here for all callers of this
          * function.  If not, it needs to move into pe__node_text.
          */
         if (pcmk__str_in_list(rsc_printable_id(rsc), filter, pcmk__str_star_matches) ||
             ((rsc->private->parent != NULL)
              && pcmk__str_in_list(rsc_printable_id(rsc->private->parent),
                                   filter, pcmk__str_star_matches))) {
             retval = g_list_prepend(retval, rsc);
         }
     }
 
     return retval;
 }
 
 GList *
 pe__build_node_name_list(pcmk_scheduler_t *scheduler, const char *s)
 {
     GList *nodes = NULL;
 
     if (pcmk__str_eq(s, "*", pcmk__str_null_matches)) {
         /* Nothing was given so return a list of all node names.  Or, '*' was
          * given.  This would normally fall into the pe__unames_with_tag branch
          * where it will return an empty list.  Catch it here instead.
          */
         nodes = g_list_prepend(nodes, strdup("*"));
     } else {
         pcmk_node_t *node = pcmk_find_node(scheduler, s);
 
         if (node) {
             /* The given string was a valid uname for a node.  Return a
              * singleton list containing just that uname.
              */
             nodes = g_list_prepend(nodes, strdup(s));
         } else {
             /* The given string was not a valid uname.  It's either a tag or
              * it's a typo or something.  In the first case, we'll return a
              * list of all the unames of the nodes with the given tag.  In the
              * second case, we'll return a NULL pointer and nothing will
              * get displayed.
              */
             nodes = pe__unames_with_tag(scheduler, s);
         }
     }
 
     return nodes;
 }
 
 GList *
 pe__build_rsc_list(pcmk_scheduler_t *scheduler, const char *s)
 {
     GList *resources = NULL;
 
     if (pcmk__str_eq(s, "*", pcmk__str_null_matches)) {
         resources = g_list_prepend(resources, strdup("*"));
     } else {
         const uint32_t flags = pcmk_rsc_match_history|pcmk_rsc_match_basename;
         pcmk_resource_t *rsc = pe_find_resource_with_flags(scheduler->resources,
                                                            s, flags);
 
         if (rsc) {
             /* A colon in the name we were given means we're being asked to filter
              * on a specific instance of a cloned resource.  Put that exact string
              * into the filter list.  Otherwise, use the printable ID of whatever
              * resource was found that matches what was asked for.
              */
             if (strstr(s, ":") != NULL) {
                 resources = g_list_prepend(resources, strdup(rsc->id));
             } else {
                 resources = g_list_prepend(resources, strdup(rsc_printable_id(rsc)));
             }
         } else {
             /* The given string was not a valid resource name. It's a tag or a
              * typo or something. See pe__build_node_name_list() for more
              * detail.
              */
             resources = pe__rscs_with_tag(scheduler, s);
         }
     }
 
     return resources;
 }
 
 xmlNode *
 pe__failed_probe_for_rsc(const pcmk_resource_t *rsc, const char *name)
 {
     const pcmk_resource_t *parent = pe__const_top_resource(rsc, false);
     const char *rsc_id = rsc->id;
 
     if (pcmk__is_clone(parent)) {
         rsc_id = pe__clone_child_id(parent);
     }
 
     for (xmlNode *xml_op = pcmk__xe_first_child(rsc->private->scheduler->failed,
                                                 NULL, NULL, NULL);
          xml_op != NULL; xml_op = pcmk__xe_next(xml_op)) {
 
         const char *value = NULL;
         char *op_id = NULL;
 
         /* This resource operation is not a failed probe. */
         if (!pcmk_xe_mask_probe_failure(xml_op)) {
             continue;
         }
 
         /* This resource operation was not run on the given node.  Note that if name is
          * NULL, this will always succeed.
          */
         value = crm_element_value(xml_op, PCMK__META_ON_NODE);
         if (value == NULL || !pcmk__str_eq(value, name, pcmk__str_casei|pcmk__str_null_matches)) {
             continue;
         }
 
         if (!parse_op_key(pcmk__xe_history_key(xml_op), &op_id, NULL, NULL)) {
             continue; // This history entry is missing an operation key
         }
 
         /* This resource operation's ID does not match the rsc_id we are looking for. */
         if (!pcmk__str_eq(op_id, rsc_id, pcmk__str_none)) {
             free(op_id);
             continue;
         }
 
         free(op_id);
         return xml_op;
     }
 
     return NULL;
 }