diff --git a/include/crm/common/resources.h b/include/crm/common/resources.h
index ebd6c1c92e..dab8ac5ff1 100644
--- a/include/crm/common/resources.h
+++ b/include/crm/common/resources.h
@@ -1,339 +1,336 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #ifndef PCMK__CRM_COMMON_RESOURCES__H
 #define PCMK__CRM_COMMON_RESOURCES__H
 
 #include <stdbool.h>                    // bool
 #include <sys/types.h>                  // time_t
 #include <libxml/tree.h>                // xmlNode
 #include <glib.h>                       // gboolean, guint, GList, GHashTable
 
 #include <crm/common/roles.h>           // enum rsc_role_e
 #include <crm/common/scheduler_types.h> // pcmk_resource_t, etc.
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 /*!
  * \file
  * \brief Scheduler API for resources
  * \ingroup core
  */
 
 // Resource variants supported by Pacemaker
 //!@{
 //! \deprecated Do not use
 enum pe_obj_types {
     // Order matters: some code compares greater or lesser than
     pcmk_rsc_variant_unknown    = -1,   // Unknown resource variant
     pcmk_rsc_variant_primitive  = 0,    // Primitive resource
     pcmk_rsc_variant_group      = 1,    // Group resource
     pcmk_rsc_variant_clone      = 2,    // Clone resource
     pcmk_rsc_variant_bundle     = 3,    // Bundle resource
 
 #if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1)
     pe_unknown      = pcmk_rsc_variant_unknown,
     pe_native       = pcmk_rsc_variant_primitive,
     pe_group        = pcmk_rsc_variant_group,
     pe_clone        = pcmk_rsc_variant_clone,
     pe_container    = pcmk_rsc_variant_bundle,
 #endif
 };
 
 // What resource needs before it can be recovered from a failed node
 enum rsc_start_requirement {
     pcmk_requires_nothing   = 0,    // Resource can be recovered immediately
     pcmk_requires_quorum    = 1,    // Resource can be recovered if quorate
     pcmk_requires_fencing   = 2,    // Resource can be recovered after fencing
 
 #if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1)
     rsc_req_nothing         = pcmk_requires_nothing,
     rsc_req_quorum          = pcmk_requires_quorum,
     rsc_req_stonith         = pcmk_requires_fencing,
 #endif
 };
 
 // How to recover a resource that is incorrectly active on multiple nodes
 enum rsc_recovery_type {
     pcmk_multiply_active_restart    = 0,    // Stop on all, start on desired
     pcmk_multiply_active_stop       = 1,    // Stop on all and leave stopped
     pcmk_multiply_active_block      = 2,    // Do nothing to resource
     pcmk_multiply_active_unexpected = 3,    // Stop unexpected instances
 
 #if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1)
     recovery_stop_start             = pcmk_multiply_active_restart,
     recovery_stop_only              = pcmk_multiply_active_stop,
     recovery_block                  = pcmk_multiply_active_block,
     recovery_stop_unexpected        = pcmk_multiply_active_unexpected,
 #endif
 };
 
 // Resource scheduling flags
 enum pcmk_rsc_flags {
     // No resource flags set (compare with equality rather than bit set)
     pcmk_no_rsc_flags               = 0ULL,
 
     // Whether resource has been removed from the configuration
     pcmk_rsc_removed                = (1ULL << 0),
 
     // Whether resource is managed
     pcmk_rsc_managed                = (1ULL << 1),
 
     // Whether resource is blocked from further action
     pcmk_rsc_blocked                = (1ULL << 2),
 
     // Whether resource has been removed but has a container
     pcmk_rsc_removed_filler         = (1ULL << 3),
 
     // Whether resource has clone notifications enabled
     pcmk_rsc_notify                 = (1ULL << 4),
 
     // Whether resource is not an anonymous clone instance
     pcmk_rsc_unique                 = (1ULL << 5),
 
     // Whether resource's class is "stonith"
     pcmk_rsc_fence_device           = (1ULL << 6),
 
     // Whether resource can be promoted and demoted
     pcmk_rsc_promotable             = (1ULL << 7),
 
     // Whether resource has not yet been assigned to a node
     pcmk_rsc_unassigned             = (1ULL << 8),
 
     // Whether resource is in the process of being assigned to a node
     pcmk_rsc_assigning              = (1ULL << 9),
 
     // Whether resource is in the process of modifying allowed node scores
     pcmk_rsc_updating_nodes         = (1ULL << 10),
 
     // Whether resource is in the process of scheduling actions to restart
     pcmk_rsc_restarting             = (1ULL << 11),
 
     // Whether resource must be stopped (instead of demoted) if it is failed
     pcmk_rsc_stop_if_failed         = (1ULL << 12),
 
     // Whether a reload action has been scheduled for resource
     pcmk_rsc_reload                 = (1ULL << 13),
 
     // Whether resource is a remote connection allowed to run on a remote node
     pcmk_rsc_remote_nesting_allowed = (1ULL << 14),
 
     // Whether resource has \c PCMK_META_CRITICAL meta-attribute enabled
     pcmk_rsc_critical               = (1ULL << 15),
 
     // Whether resource is considered failed
     pcmk_rsc_failed                 = (1ULL << 16),
 
     // Flag for non-scheduler code to use to detect recursion loops
     pcmk_rsc_detect_loop            = (1ULL << 17),
 
     // \deprecated Do not use
     pcmk_rsc_runnable               = (1ULL << 18),
 
     // Whether resource has pending start action in history
     pcmk_rsc_start_pending          = (1ULL << 19),
 
     // \deprecated Do not use
     pcmk_rsc_starting               = (1ULL << 20),
 
     // \deprecated Do not use
     pcmk_rsc_stopping               = (1ULL << 21),
 
     /*
      * Whether resource is multiply active with recovery set to
      * \c PCMK_VALUE_STOP_UNEXPECTED
      */
     pcmk_rsc_stop_unexpected        = (1ULL << 22),
 
     // Whether resource is allowed to live-migrate
     pcmk_rsc_migratable             = (1ULL << 23),
 
     // Whether resource has an ignorable failure
     pcmk_rsc_ignore_failure         = (1ULL << 24),
 
     // Whether resource is an implicit container resource for a bundle replica
     pcmk_rsc_replica_container      = (1ULL << 25),
 
     // Whether resource, its node, or entire cluster is in maintenance mode
     pcmk_rsc_maintenance            = (1ULL << 26),
 
     // \deprecated Do not use
     pcmk_rsc_has_filler             = (1ULL << 27),
 
     // Whether resource can be started or promoted only on quorate nodes
     pcmk_rsc_needs_quorum           = (1ULL << 28),
 
     // Whether resource requires fencing before recovery if on unclean node
     pcmk_rsc_needs_fencing          = (1ULL << 29),
 
     // Whether resource can be started or promoted only on unfenced nodes
     pcmk_rsc_needs_unfencing        = (1ULL << 30),
 };
 //!@}
 
 //! Search options for resources (exact resource ID always matches)
 enum pe_find {
     //! Also match clone instance ID from resource history
     pcmk_rsc_match_history          = (1 << 0),
 
     //! Also match anonymous clone instances by base name
     pcmk_rsc_match_anon_basename    = (1 << 1),
 
     //! Match only clones and their instances, by either clone or instance ID
     pcmk_rsc_match_clone_only       = (1 << 2),
 
     //! If matching by node, compare current node instead of assigned node
     pcmk_rsc_match_current_node     = (1 << 3),
 
     //! \deprecated Do not use
     pe_find_inactive                = (1 << 4),
 
     //! Match clone instances (even unique) by base name as well as exact ID
     pcmk_rsc_match_basename         = (1 << 5),
 
 #if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1)
     //! \deprecated Use pcmk_rsc_match_history instead
     pe_find_renamed     = pcmk_rsc_match_history,
 
     //! \deprecated Use pcmk_rsc_match_anon_basename instead
     pe_find_anon        = pcmk_rsc_match_anon_basename,
 
     //! \deprecated Use pcmk_rsc_match_clone_only instead
     pe_find_clone       = pcmk_rsc_match_clone_only,
 
     //! \deprecated Use pcmk_rsc_match_current_node instead
     pe_find_current     = pcmk_rsc_match_current_node,
 
     //! \deprecated Use pcmk_rsc_match_basename instead
     pe_find_any         = pcmk_rsc_match_basename,
 #endif
 };
 
 //! \deprecated Do not use
 enum pe_restart {
     pe_restart_restart,
     pe_restart_ignore,
 };
 
 //! \internal Do not use
 typedef struct pcmk__resource_private pcmk__resource_private_t;
 
 // 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
 
-    // Configuration of resource operations (possibly expanded from template)
-    xmlNode *ops_xml;
-
     pcmk_scheduler_t *cluster;          // Cluster that resource is part of
     pcmk_resource_t *parent;            // Resource's parent resource, if any
     enum pe_obj_types variant;          // Resource variant
     void *variant_opaque;               // Variant-specific (and private) data
 
     enum rsc_recovery_type recovery_type;   // How to recover if failed
 
     enum pe_restart restart_type;   // \deprecated Do not use
     int priority;                   // Configured priority
     int stickiness;                 // Extra preference for current node
     int sort_index;                 // Promotion score on assigned node
     int failure_timeout;            // Failure timeout
     int migration_threshold;        // Migration threshold
     guint remote_reconnect_ms;      // Retry interval for remote connections
     char *pending_task;             // Pending action in history, if any
 
     // NOTE: sbd (as of at least 1.5.2) uses this
     //! \deprecated Call pcmk_resource_is_managed() instead
     unsigned long long flags;       // Group of enum pcmk_rsc_flags
 
     // @TODO Merge these into flags
     gboolean is_remote_node;        // Whether this is a remote connection
     gboolean exclusive_discover;    // Whether exclusive probing is enabled
 
     /* Pay special attention to whether you want to use rsc_cons_lhs and
      * rsc_cons directly, which include only colocations explicitly involving
      * this resource, or call libpacemaker's pcmk__with_this_colocations() and
      * pcmk__this_with_colocations() functions, which may return relevant
      * colocations involving the resource's ancestors as well.
      */
 
     GList *rsc_cons_lhs;      // Colocations of other resources with this one
     GList *rsc_cons;          // Colocations of this resource with others
     GList *rsc_location;      // Location constraints for resource
     GList *actions;           // Actions scheduled for resource
     GList *rsc_tickets;       // Ticket constraints for resource
 
     pcmk_node_t *allocated_to;  // Node resource is assigned to
 
     // The destination node, if migrate_to completed but migrate_from has not
     pcmk_node_t *partial_migration_target;
 
     // The source node, if migrate_to completed but migrate_from has not
     pcmk_node_t *partial_migration_source;
 
     // Nodes where resource may be active
     GList *running_on;
 
     // Nodes where resource has been probed (key is node ID, not name)
     GHashTable *known_on;
 
     // Nodes where resource may run (key is node ID, not name)
     GHashTable *allowed_nodes;
 
     enum rsc_role_e role;           // Resource's current role
     enum rsc_role_e next_role;      // Resource's scheduled next role
 
     GHashTable *meta;               // Resource's meta-attributes
     GHashTable *parameters;         // \deprecated Use pe_rsc_params() instead
     GHashTable *utilization;        // Resource's utilization attributes
 
     GList *children;                // Resource's child resources, if any
 
     // Source nodes where stop is needed after migrate_from and migrate_to
     GList *dangling_migrations;
 
     pcmk_resource_t *container;     // Resource containing this one, if any
     GList *fillers;                 // Resources contained by this one, if any
 
     // @COMPAT These should be made const at next API compatibility break
     pcmk_node_t *pending_node;      // Node on which pending_task is happening
     pcmk_node_t *lock_node;         // Resource shutdown-locked to this node
 
     time_t lock_time;               // When shutdown lock started
 
     /*
      * Resource parameters may have node-attribute-based rules, which means the
      * values can vary by node. This table has node names as keys and parameter
      * name/value tables as values. Use pe_rsc_params() to get the table for a
      * given node rather than use this directly.
      */
     GHashTable *parameter_cache;
 };
 //!@}
 
 const char *pcmk_resource_id(const pcmk_resource_t *rsc);
 bool pcmk_resource_is_managed(const pcmk_resource_t *rsc);
 
 #ifdef __cplusplus
 }
 #endif
 
 #endif // PCMK__CRM_COMMON_RESOURCES__H
diff --git a/include/crm/common/resources_internal.h b/include/crm/common/resources_internal.h
index 1fb7132b04..c298a28711 100644
--- a/include/crm/common/resources_internal.h
+++ b/include/crm/common/resources_internal.h
@@ -1,228 +1,231 @@
 /*
  * Copyright 2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #ifndef PCMK__CRM_COMMON_RESOURCES_INTERNAL__H
 #define PCMK__CRM_COMMON_RESOURCES_INTERNAL__H
 
 #include <glib.h>                       // gboolean, GList
 #include <crm/common/resources.h>       // enum rsc_recovery_type
 #include <crm/common/roles.h>           // enum rsc_role_e
 #include <crm/common/scheduler_types.h> // pcmk_node_t, pcmk_resource_t, etc.
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 /*!
  * \internal
  * \brief Set resource flags
  *
  * \param[in,out] resource      Resource to set flags for
  * \param[in]     flags_to_set  Group of enum pcmk_rsc_flags to set
  */
 #define pcmk__set_rsc_flags(resource, flags_to_set) do {                    \
         (resource)->flags = pcmk__set_flags_as(__func__, __LINE__,          \
             LOG_TRACE, "Resource", (resource)->id, (resource)->flags,       \
             (flags_to_set), #flags_to_set);                                 \
     } while (0)
 
 /*!
  * \internal
  * \brief Clear resource flags
  *
  * \param[in,out] resource        Resource to clear flags for
  * \param[in]     flags_to_clear  Group of enum pcmk_rsc_flags to clear
  */
 #define pcmk__clear_rsc_flags(resource, flags_to_clear) do {                \
         (resource)->flags = pcmk__clear_flags_as(__func__, __LINE__,        \
             LOG_TRACE, "Resource", (resource)->id, (resource)->flags,       \
             (flags_to_clear), #flags_to_clear);                             \
     } while (0)
 
 //! Resource 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 {
     char *history_id;               // Resource instance ID in history
 
     // Resource configuration (possibly expanded from template)
     xmlNode *xml;
 
     // Original resource configuration, if using template
     xmlNode *orig_xml;
 
+    // Configuration of resource operations (possibly expanded from template)
+    xmlNode *ops_xml;
+
     const pcmk__rsc_methods_t *fns;         // Resource object methods
     const pcmk__assignment_methods_t *cmds; // Resource assignment methods
 };
 
 const char *pcmk__multiply_active_text(enum rsc_recovery_type recovery);
 
 /*!
  * \internal
  * \brief Get node where resource is currently active (if any)
  *
  * \param[in] rsc  Resource to check
  *
  * \return Node that \p rsc is active on, if any, otherwise NULL
  */
 static inline pcmk_node_t *
 pcmk__current_node(const pcmk_resource_t *rsc)
 {
     if (rsc == NULL) {
         return NULL;
     }
     return rsc->private->fns->active_node(rsc, NULL, NULL);
 }
 
 #ifdef __cplusplus
 }
 #endif
 
 #endif // PCMK__CRM_COMMON_RESOURCES_INTERNAL__H
diff --git a/lib/pacemaker/pcmk_sched_recurring.c b/lib/pacemaker/pcmk_sched_recurring.c
index 693abaa4af..5f56ace54e 100644
--- a/lib/pacemaker/pcmk_sched_recurring.c
+++ b/lib/pacemaker/pcmk_sched_recurring.c
@@ -1,747 +1,747 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <stdbool.h>
 
 #include <crm/common/xml.h>
 #include <crm/common/scheduler_internal.h>
 #include <pacemaker-internal.h>
 
 #include "libpacemaker_private.h"
 
 // Information parsed from an operation history entry in the CIB
 struct op_history {
     // XML attributes
     const char *id;         // ID of history entry
     const char *name;       // Action name
 
     // Parsed information
     char *key;              // Operation key for action
     enum rsc_role_e role;   // Action role (or pcmk_role_unknown for default)
     guint interval_ms;      // Action interval
 };
 
 /*!
  * \internal
  * \brief Parse an interval from XML
  *
  * \param[in] xml  XML containing an interval attribute
  *
  * \return Interval parsed from XML (or 0 as default)
  */
 static guint
 xe_interval(const xmlNode *xml)
 {
     guint interval_ms = 0U;
 
     pcmk_parse_interval_spec(crm_element_value(xml, PCMK_META_INTERVAL),
                              &interval_ms);
     return interval_ms;
 }
 
 /*!
  * \internal
  * \brief Check whether an operation exists multiple times in resource history
  *
  * \param[in] rsc          Resource with history to search
  * \param[in] name         Name of action to search for
  * \param[in] interval_ms  Interval (in milliseconds) of action to search for
  *
  * \return true if an operation with \p name and \p interval_ms exists more than
  *         once in the operation history of \p rsc, otherwise false
  */
 static bool
 is_op_dup(const pcmk_resource_t *rsc, const char *name, guint interval_ms)
 {
     const char *id = NULL;
 
-    for (xmlNode *op = pcmk__xe_first_child(rsc->ops_xml, PCMK_XE_OP, NULL,
-                                            NULL);
+    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->actions, key, node);
     if (possible_matches == NULL) {
         pcmk__rsc_trace(rsc,
                         "%s will be mandatory because it is not active on %s",
                         key, pcmk__node_name(node));
         return false;
     }
 
     for (const GList *iter = possible_matches;
          iter != NULL; iter = iter->next) {
 
         const pcmk_action_t *op = (const pcmk_action_t *) iter->data;
 
         if (pcmk_is_set(op->flags, pcmk_action_reschedule)) {
             pcmk__rsc_trace(rsc,
                             "%s will be mandatory because "
                             "it needs to be rescheduled", key);
             g_list_free(possible_matches);
             return false;
         }
     }
 
     g_list_free(possible_matches);
     return true;
 }
 
 /*!
  * \internal
  * \brief Create recurring action from resource history entry for an active role
  *
  * \param[in,out] rsc    Resource that resource history is for
  * \param[in,out] start  Start action for \p rsc on \p node
  * \param[in]     node   Node that resource will be active on (if any)
  * \param[in]     op     Resource history entry
  */
 static void
 recurring_op_for_active(pcmk_resource_t *rsc, pcmk_action_t *start,
                         const pcmk_node_t *node, const struct op_history *op)
 {
     pcmk_action_t *mon = NULL;
     bool is_optional = true;
     bool role_match = false;
     enum rsc_role_e monitor_role = op->role;
 
     // We're only interested in recurring actions for active roles
     if (monitor_role == pcmk_role_stopped) {
         return;
     }
 
     is_optional = active_recurring_should_be_optional(rsc, node, op->key,
                                                       start);
 
     // Check whether monitor's role matches role resource will have
     if (monitor_role == pcmk_role_unknown) {
         monitor_role = pcmk_role_unpromoted;
         role_match = (rsc->next_role != pcmk_role_promoted);
     } else {
         role_match = (rsc->next_role == monitor_role);
     }
 
     if (!role_match) {
         if (is_optional) { // It's running, so cancel it
             char *after_key = NULL;
             pcmk_action_t *cancel_op = pcmk__new_cancel_action(rsc, op->name,
                                                                op->interval_ms,
                                                                node);
 
             switch (rsc->role) {
                 case pcmk_role_unpromoted:
                 case pcmk_role_started:
                     if (rsc->next_role == pcmk_role_promoted) {
                         after_key = promote_key(rsc);
 
                     } else if (rsc->next_role == pcmk_role_stopped) {
                         after_key = stop_key(rsc);
                     }
 
                     break;
                 case pcmk_role_promoted:
                     after_key = demote_key(rsc);
                     break;
                 default:
                     break;
             }
 
             if (after_key) {
                 pcmk__new_ordering(rsc, NULL, cancel_op, rsc, after_key, NULL,
                                    pcmk__ar_unrunnable_first_blocks,
                                    rsc->cluster);
             }
         }
 
         do_crm_log((is_optional? LOG_INFO : LOG_TRACE),
                    "%s recurring action %s because %s configured for %s role "
                    "(not %s)",
                    (is_optional? "Cancelling" : "Ignoring"), op->key, op->id,
                    pcmk_role_text(monitor_role),
                    pcmk_role_text(rsc->next_role));
         return;
     }
 
     pcmk__rsc_trace(rsc,
                     "Creating %s recurring action %s for %s (%s %s on %s)",
                     (is_optional? "optional" : "mandatory"), op->key,
                     op->id, rsc->id, pcmk_role_text(rsc->next_role),
                     pcmk__node_name(node));
 
     mon = custom_action(rsc, strdup(op->key), op->name, node, is_optional,
                         rsc->cluster);
 
     if (!pcmk_is_set(start->flags, pcmk_action_runnable)) {
         pcmk__rsc_trace(rsc, "%s is unrunnable because start is", mon->uuid);
         pcmk__clear_action_flags(mon, pcmk_action_runnable);
 
     } else if ((node == NULL) || !node->details->online
                || node->details->unclean) {
         pcmk__rsc_trace(rsc, "%s is unrunnable because no node is available",
                         mon->uuid);
         pcmk__clear_action_flags(mon, pcmk_action_runnable);
 
     } else if (!pcmk_is_set(mon->flags, pcmk_action_optional)) {
         pcmk__rsc_info(rsc, "Start %s-interval %s for %s on %s",
                        pcmk__readable_interval(op->interval_ms), mon->task,
                        rsc->id, pcmk__node_name(node));
     }
 
     if (rsc->next_role == pcmk_role_promoted) {
         pe__add_action_expected_result(mon, CRM_EX_PROMOTED);
     }
 
     // Order monitor relative to other actions
     if ((node == NULL) || pcmk_is_set(rsc->flags, pcmk_rsc_managed)) {
         pcmk__new_ordering(rsc, start_key(rsc), NULL,
                            NULL, strdup(mon->uuid), mon,
                            pcmk__ar_first_implies_then
                            |pcmk__ar_unrunnable_first_blocks,
                            rsc->cluster);
 
         pcmk__new_ordering(rsc, reload_key(rsc), NULL,
                            NULL, strdup(mon->uuid), mon,
                            pcmk__ar_first_implies_then
                            |pcmk__ar_unrunnable_first_blocks,
                            rsc->cluster);
 
         if (rsc->next_role == pcmk_role_promoted) {
             pcmk__new_ordering(rsc, promote_key(rsc), NULL,
                                rsc, NULL, mon,
                                pcmk__ar_ordered
                                |pcmk__ar_unrunnable_first_blocks,
                                rsc->cluster);
 
         } else if (rsc->role == pcmk_role_promoted) {
             pcmk__new_ordering(rsc, demote_key(rsc), NULL,
                                rsc, NULL, mon,
                                pcmk__ar_ordered
                                |pcmk__ar_unrunnable_first_blocks,
                                rsc->cluster);
         }
     }
 }
 
 /*!
  * \internal
  * \brief Cancel a recurring action if running on a node
  *
  * \param[in,out] rsc          Resource that action is for
  * \param[in]     node         Node to cancel action on
  * \param[in]     key          Operation key for action
  * \param[in]     name         Action name
  * \param[in]     interval_ms  Action interval (in milliseconds)
  */
 static void
 cancel_if_running(pcmk_resource_t *rsc, const pcmk_node_t *node,
                   const char *key, const char *name, guint interval_ms)
 {
     GList *possible_matches = find_actions_exact(rsc->actions, key, node);
     pcmk_action_t *cancel_op = NULL;
 
     if (possible_matches == NULL) {
         return; // Recurring action isn't running on this node
     }
     g_list_free(possible_matches);
 
     cancel_op = pcmk__new_cancel_action(rsc, name, interval_ms, node);
 
     switch (rsc->next_role) {
         case pcmk_role_started:
         case pcmk_role_unpromoted:
             /* Order starts after cancel. If the current role is
              * stopped, this cancels the monitor before the resource
              * starts; if the current role is started, then this cancels
              * the monitor on a migration target before starting there.
              */
             pcmk__new_ordering(rsc, NULL, cancel_op,
                                rsc, start_key(rsc), NULL,
                                pcmk__ar_unrunnable_first_blocks, rsc->cluster);
             break;
         default:
             break;
     }
     pcmk__rsc_info(rsc,
                    "Cancelling %s-interval %s action for %s on %s because "
                    "configured for " PCMK_ROLE_STOPPED " role (not %s)",
                    pcmk__readable_interval(interval_ms), name, rsc->id,
                    pcmk__node_name(node), pcmk_role_text(rsc->next_role));
 }
 
 /*!
  * \internal
  * \brief Order an action after all probes of a resource on a node
  *
  * \param[in,out] rsc     Resource to check for probes
  * \param[in]     node    Node to check for probes of \p rsc
  * \param[in,out] action  Action to order after probes of \p rsc on \p node
  */
 static void
 order_after_probes(pcmk_resource_t *rsc, const pcmk_node_t *node,
                    pcmk_action_t *action)
 {
     GList *probes = pe__resource_actions(rsc, node, PCMK_ACTION_MONITOR, FALSE);
 
     for (GList *iter = probes; iter != NULL; iter = iter->next) {
         order_actions((pcmk_action_t *) iter->data, action,
                       pcmk__ar_unrunnable_first_blocks);
     }
     g_list_free(probes);
 }
 
 /*!
  * \internal
  * \brief Order an action after all stops of a resource on a node
  *
  * \param[in,out] rsc     Resource to check for stops
  * \param[in]     node    Node to check for stops of \p rsc
  * \param[in,out] action  Action to order after stops of \p rsc on \p node
  */
 static void
 order_after_stops(pcmk_resource_t *rsc, const pcmk_node_t *node,
                   pcmk_action_t *action)
 {
     GList *stop_ops = pe__resource_actions(rsc, node, PCMK_ACTION_STOP, TRUE);
 
     for (GList *iter = stop_ops; iter != NULL; iter = iter->next) {
         pcmk_action_t *stop = (pcmk_action_t *) iter->data;
 
         if (!pcmk_is_set(stop->flags, pcmk_action_optional)
             && !pcmk_is_set(action->flags, pcmk_action_optional)
             && !pcmk_is_set(rsc->flags, pcmk_rsc_managed)) {
             pcmk__rsc_trace(rsc, "%s optional on %s: unmanaged",
                             action->uuid, pcmk__node_name(node));
             pcmk__set_action_flags(action, pcmk_action_optional);
         }
 
         if (!pcmk_is_set(stop->flags, pcmk_action_runnable)) {
             crm_debug("%s unrunnable on %s: stop is unrunnable",
                       action->uuid, pcmk__node_name(node));
             pcmk__clear_action_flags(action, pcmk_action_runnable);
         }
 
         if (pcmk_is_set(rsc->flags, pcmk_rsc_managed)) {
             pcmk__new_ordering(rsc, stop_key(rsc), stop,
                                NULL, NULL, action,
                                pcmk__ar_first_implies_then
                                |pcmk__ar_unrunnable_first_blocks,
                                rsc->cluster);
         }
     }
     g_list_free(stop_ops);
 }
 
 /*!
  * \internal
  * \brief Create recurring action from resource history entry for inactive role
  *
  * \param[in,out] rsc    Resource that resource history is for
  * \param[in]     node   Node that resource will be active on (if any)
  * \param[in]     op     Resource history entry
  */
 static void
 recurring_op_for_inactive(pcmk_resource_t *rsc, const pcmk_node_t *node,
                           const struct op_history *op)
 {
     GList *possible_matches = NULL;
 
     // We're only interested in recurring actions for the inactive role
     if (op->role != pcmk_role_stopped) {
         return;
     }
 
     if (!pcmk_is_set(rsc->flags, pcmk_rsc_unique)) {
         crm_notice("Ignoring %s (recurring monitors for " PCMK_ROLE_STOPPED
                    " role are not supported for anonymous clones)", op->id);
         return; // @TODO add support
     }
 
     pcmk__rsc_trace(rsc,
                     "Creating recurring action %s for %s on nodes "
                     "where it should not be running", op->id, rsc->id);
 
     for (GList *iter = rsc->cluster->nodes; iter != NULL; iter = iter->next) {
         pcmk_node_t *stop_node = (pcmk_node_t *) iter->data;
 
         bool is_optional = true;
         pcmk_action_t *stopped_mon = NULL;
 
         // Cancel action on node where resource will be active
         if ((node != NULL)
             && pcmk__str_eq(stop_node->details->uname, node->details->uname,
                             pcmk__str_casei)) {
             cancel_if_running(rsc, node, op->key, op->name, op->interval_ms);
             continue;
         }
 
         // Recurring action on this node is optional if it's already active here
         possible_matches = find_actions_exact(rsc->actions, op->key, stop_node);
         is_optional = (possible_matches != NULL);
         g_list_free(possible_matches);
 
         pcmk__rsc_trace(rsc,
                         "Creating %s recurring action %s for %s (%s "
                         PCMK_ROLE_STOPPED " on %s)",
                         (is_optional? "optional" : "mandatory"),
                         op->key, op->id, rsc->id, pcmk__node_name(stop_node));
 
         stopped_mon = custom_action(rsc, strdup(op->key), op->name, stop_node,
                                     is_optional, rsc->cluster);
 
         pe__add_action_expected_result(stopped_mon, CRM_EX_NOT_RUNNING);
 
         if (pcmk_is_set(rsc->flags, pcmk_rsc_managed)) {
             order_after_probes(rsc, stop_node, stopped_mon);
         }
 
         /* The recurring action is for the inactive role, so it shouldn't be
          * performed until the resource is inactive.
          */
         order_after_stops(rsc, stop_node, stopped_mon);
 
         if (!stop_node->details->online || stop_node->details->unclean) {
             pcmk__rsc_debug(rsc, "%s unrunnable on %s: node unavailable)",
                             stopped_mon->uuid, pcmk__node_name(stop_node));
             pcmk__clear_action_flags(stopped_mon, pcmk_action_runnable);
         }
 
         if (pcmk_is_set(stopped_mon->flags, pcmk_action_runnable)
             && !pcmk_is_set(stopped_mon->flags, pcmk_action_optional)) {
             crm_notice("Start recurring %s-interval %s for "
                        PCMK_ROLE_STOPPED " %s on %s",
                        pcmk__readable_interval(op->interval_ms),
                        stopped_mon->task, rsc->id, pcmk__node_name(stop_node));
         }
     }
 }
 
 /*!
  * \internal
  * \brief Create recurring actions for a resource
  *
  * \param[in,out] rsc  Resource to create recurring actions for
  */
 void
 pcmk__create_recurring_actions(pcmk_resource_t *rsc)
 {
     pcmk_action_t *start = NULL;
 
     if (pcmk_is_set(rsc->flags, pcmk_rsc_blocked)) {
         pcmk__rsc_trace(rsc,
                         "Skipping recurring actions for blocked resource %s",
                         rsc->id);
         return;
     }
 
     if (pcmk_is_set(rsc->flags, pcmk_rsc_maintenance)) {
         pcmk__rsc_trace(rsc,
                         "Skipping recurring actions for %s "
                         "in maintenance mode", rsc->id);
         return;
     }
 
     if (rsc->allocated_to == NULL) {
         // Recurring actions for active roles not needed
 
     } else if (rsc->allocated_to->details->maintenance) {
         pcmk__rsc_trace(rsc,
                         "Skipping recurring actions for %s on %s "
                         "in maintenance mode",
                         rsc->id, pcmk__node_name(rsc->allocated_to));
 
     } else if ((rsc->next_role != pcmk_role_stopped)
         || !pcmk_is_set(rsc->flags, pcmk_rsc_managed)) {
         // Recurring actions for active roles needed
         start = start_action(rsc, rsc->allocated_to, TRUE);
     }
 
     pcmk__rsc_trace(rsc, "Creating any recurring actions needed for %s",
                     rsc->id);
 
-    for (xmlNode *op = pcmk__xe_first_child(rsc->ops_xml, PCMK_XE_OP, NULL,
-                                            NULL);
+    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_inactive(rsc, rsc->allocated_to, &op_history);
 
         free(op_history.key);
     }
 }
 
 /*!
  * \internal
  * \brief Create an executor cancel action
  *
  * \param[in,out] rsc          Resource of action to cancel
  * \param[in]     task         Name of action to cancel
  * \param[in]     interval_ms  Interval of action to cancel
  * \param[in]     node         Node of action to cancel
  *
  * \return Created op
  */
 pcmk_action_t *
 pcmk__new_cancel_action(pcmk_resource_t *rsc, const char *task,
                         guint interval_ms, const pcmk_node_t *node)
 {
     pcmk_action_t *cancel_op = NULL;
     char *key = NULL;
     char *interval_ms_s = NULL;
 
     CRM_ASSERT((rsc != NULL) && (task != NULL) && (node != NULL));
 
     key = pcmk__op_key(rsc->id, task, interval_ms);
 
     /* This finds an existing action by key, so custom_action() does not change
      * cancel_op->task.
      */
     cancel_op = custom_action(rsc, key, PCMK_ACTION_CANCEL, node, FALSE,
                               rsc->cluster);
 
     pcmk__str_update(&(cancel_op->task), PCMK_ACTION_CANCEL);
     pcmk__str_update(&(cancel_op->cancel_task), task);
 
     interval_ms_s = crm_strdup_printf("%u", interval_ms);
     pcmk__insert_meta(cancel_op, PCMK_XA_OPERATION, task);
     pcmk__insert_meta(cancel_op, PCMK_META_INTERVAL, interval_ms_s);
     free(interval_ms_s);
 
     return cancel_op;
 }
 
 /*!
  * \internal
  * \brief Schedule cancellation of a recurring action
  *
  * \param[in,out] rsc          Resource that action is for
  * \param[in]     call_id      Action's call ID from history
  * \param[in]     task         Action name
  * \param[in]     interval_ms  Action interval
  * \param[in]     node         Node that history entry is for
  * \param[in]     reason       Short description of why action is cancelled
  */
 void
 pcmk__schedule_cancel(pcmk_resource_t *rsc, const char *call_id,
                       const char *task, guint interval_ms,
                       const pcmk_node_t *node, const char *reason)
 {
     pcmk_action_t *cancel = NULL;
 
     CRM_CHECK((rsc != NULL) && (task != NULL)
               && (node != NULL) && (reason != NULL),
               return);
 
     crm_info("Recurring %s-interval %s for %s will be stopped on %s: %s",
              pcmk__readable_interval(interval_ms), task, rsc->id,
              pcmk__node_name(node), reason);
     cancel = pcmk__new_cancel_action(rsc, task, interval_ms, node);
     pcmk__insert_meta(cancel, PCMK__XA_CALL_ID, call_id);
 
     // Cancellations happen after stops
     pcmk__new_ordering(rsc, stop_key(rsc), NULL, rsc, NULL, cancel,
                        pcmk__ar_ordered, rsc->cluster);
 }
 
 /*!
  * \internal
  * \brief Create a recurring action marked as needing rescheduling if active
  *
  * \param[in,out] rsc          Resource that action is for
  * \param[in]     task         Name of action being rescheduled
  * \param[in]     interval_ms  Action interval (in milliseconds)
  * \param[in,out] node         Node where action should be rescheduled
  */
 void
 pcmk__reschedule_recurring(pcmk_resource_t *rsc, const char *task,
                            guint interval_ms, pcmk_node_t *node)
 {
     pcmk_action_t *op = NULL;
 
     trigger_unfencing(rsc, node, "Device parameters changed (reschedule)",
                       NULL, rsc->cluster);
     op = custom_action(rsc, pcmk__op_key(rsc->id, task, interval_ms),
                        task, node, TRUE, rsc->cluster);
     pcmk__set_action_flags(op, pcmk_action_reschedule);
 }
 
 /*!
  * \internal
  * \brief Check whether an action is recurring
  *
  * \param[in] action  Action to check
  *
  * \return true if \p action has a nonzero interval, otherwise false
  */
 bool
 pcmk__action_is_recurring(const pcmk_action_t *action)
 {
     guint interval_ms = 0;
 
     if (pcmk__guint_from_hash(action->meta, PCMK_META_INTERVAL, 0,
                               &interval_ms) != pcmk_rc_ok) {
         return false;
     }
     return (interval_ms > 0);
 }
diff --git a/lib/pengine/complex.c b/lib/pengine/complex.c
index 65640d4a5c..bea0ef61ca 100644
--- a/lib/pengine/complex.c
+++ b/lib/pengine/complex.c
@@ -1,1277 +1,1277 @@
 /*
  * 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 pe_obj_types
 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->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->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->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->parent) {
         g_hash_table_foreach(rsc->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->parent != NULL) {
         get_rsc_attributes(meta_hash, rsc->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)
 {
     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(rsc->cluster->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(rsc->cluster->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(rsc->cluster->flags,
                                pcmk_sched_enable_unfencing)) {
             value = PCMK_VALUE_UNFENCING;
 
         } else if (pcmk_is_set(rsc->cluster->flags,
                                pcmk_sched_fencing_enabled)) {
             value = PCMK_VALUE_FENCING;
 
         } else if (rsc->cluster->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)->cluster = 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)->parent = parent;
 
     ops = pcmk__xe_first_child(rsc_private->xml, PCMK_XE_OPERATIONS, NULL,
                                NULL);
-    (*rsc)->ops_xml = pcmk__xe_resolve_idref(ops, scheduler->input);
+    rsc_private->ops_xml = pcmk__xe_resolve_idref(ops, scheduler->input);
 
     (*rsc)->variant = get_resource_type((const char *) rsc_private->xml->name);
     if ((*rsc)->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)->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_runnable|pcmk_rsc_unassigned);
 
     if (!pcmk_is_set(scheduler->flags, pcmk_sched_in_maintenance)) {
         pcmk__set_rsc_flags(*rsc, pcmk_rsc_managed);
     }
 
     (*rsc)->rsc_cons = NULL;
     (*rsc)->rsc_tickets = NULL;
     (*rsc)->actions = NULL;
     (*rsc)->role = pcmk_role_stopped;
     (*rsc)->next_role = pcmk_role_unknown;
 
     (*rsc)->recovery_type = pcmk_multiply_active_restart;
     (*rsc)->stickiness = 0;
     (*rsc)->migration_threshold = PCMK_SCORE_INFINITY;
     (*rsc)->failure_timeout = 0;
 
     value = g_hash_table_lookup((*rsc)->meta, PCMK_META_PRIORITY);
     (*rsc)->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)) {
         (*rsc)->is_remote_node = TRUE;
         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)->restart_type = pe_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)->restart_type = pe_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)->recovery_type = 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)->recovery_type = 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)->recovery_type = 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)->recovery_type = 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)->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)->migration_threshold = char2score(value);
             if ((*rsc)->migration_threshold < 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)->migration_threshold = 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) {
         guint interval_ms = 0U;
 
         // Stored as seconds
         pcmk_parse_interval_spec(value, &interval_ms);
         (*rsc)->failure_timeout = (int) (interval_ms / 1000);
     }
 
     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)->remote_reconnect_ms));
 
             /* We want to override any default failure_timeout in use when remote
              * PCMK_REMOTE_RA_RECONNECT_INTERVAL is in use.
              */
             (*rsc)->failure_timeout = (*rsc)->remote_reconnect_ms / 1000;
         }
     }
 
     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->parent != NULL) {
         if (parent->parent == rsc) {
             return TRUE;
         }
         parent = parent->parent;
     }
     return FALSE;
 }
 
 pcmk_resource_t *
 uber_parent(pcmk_resource_t *rsc)
 {
     pcmk_resource_t *parent = rsc;
 
     if (parent == NULL) {
         return NULL;
     }
     while ((parent->parent != NULL) && !pcmk__is_bundle(parent->parent)) {
         parent = parent->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->parent != NULL) {
         if (!include_bundle && pcmk__is_bundle(parent->parent)) {
             break;
         }
         parent = parent->parent;
     }
     return parent;
 }
 
 void
 common_free(pcmk_resource_t * rsc)
 {
     if (rsc == NULL) {
         return;
     }
 
     pcmk__rsc_trace(rsc, "Freeing %s %d", rsc->id, rsc->variant);
 
     g_list_free(rsc->rsc_cons);
     g_list_free(rsc->rsc_cons_lhs);
     g_list_free(rsc->rsc_tickets);
     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->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->actions) {
         g_list_free(rsc->actions);
         rsc->actions = NULL;
     }
     if (rsc->allowed_nodes) {
         g_hash_table_destroy(rsc->allowed_nodes);
         rsc->allowed_nodes = NULL;
     }
     g_list_free(rsc->fillers);
     g_list_free(rsc->rsc_location);
     free(rsc->id);
     free(rsc->allocated_to);
     free(rsc->variant_opaque);
     free(rsc->pending_task);
 
     free(rsc->private->history_id);
     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->cluster->ninstances++;
         if (pe__resource_is_disabled(rsc)) {
             rsc->cluster->disabled_resources++;
         }
         if (pcmk_is_set(rsc->flags, pcmk_rsc_blocked)) {
             rsc->cluster->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/pe_actions.c b/lib/pengine/pe_actions.c
index f190fdfc56..36625f001c 100644
--- a/lib/pengine/pe_actions.c
+++ b/lib/pengine/pe_actions.c
@@ -1,1833 +1,1833 @@
 /*
  * 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/common/scheduler_internal.h>
 #include <crm/pengine/internal.h>
 #include <crm/common/xml_internal.h>
 #include "pe_status_private.h"
 
 static void unpack_operation(pcmk_action_t *action, const xmlNode *xml_obj,
                              guint interval_ms);
 
 static void
 add_singleton(pcmk_scheduler_t *scheduler, pcmk_action_t *action)
 {
     if (scheduler->singletons == NULL) {
         scheduler->singletons = pcmk__strkey_table(NULL, NULL);
     }
     g_hash_table_insert(scheduler->singletons, action->uuid, action);
 }
 
 static pcmk_action_t *
 lookup_singleton(pcmk_scheduler_t *scheduler, const char *action_uuid)
 {
     if (scheduler->singletons == NULL) {
         return NULL;
     }
     return g_hash_table_lookup(scheduler->singletons, action_uuid);
 }
 
 /*!
  * \internal
  * \brief Find an existing action that matches arguments
  *
  * \param[in] key        Action key to match
  * \param[in] rsc        Resource to match (if any)
  * \param[in] node       Node to match (if any)
  * \param[in] scheduler  Scheduler data
  *
  * \return Existing action that matches arguments (or NULL if none)
  */
 static pcmk_action_t *
 find_existing_action(const char *key, const pcmk_resource_t *rsc,
                      const pcmk_node_t *node, const pcmk_scheduler_t *scheduler)
 {
     GList *matches = NULL;
     pcmk_action_t *action = NULL;
 
     /* When rsc is NULL, it would be quicker to check scheduler->singletons,
      * but checking all scheduler->actions takes the node into account.
      */
     matches = find_actions(((rsc == NULL)? scheduler->actions : rsc->actions),
                            key, node);
     if (matches == NULL) {
         return NULL;
     }
     CRM_LOG_ASSERT(!pcmk__list_of_multiple(matches));
 
     action = matches->data;
     g_list_free(matches);
     return action;
 }
 
 /*!
  * \internal
  * \brief Find the XML configuration corresponding to a specific action key
  *
  * \param[in] rsc               Resource to find action configuration for
  * \param[in] key               "RSC_ACTION_INTERVAL" of action to find
  * \param[in] include_disabled  If false, do not return disabled actions
  *
  * \return XML configuration of desired action if any, otherwise NULL
  */
 static xmlNode *
 find_exact_action_config(const pcmk_resource_t *rsc, const char *action_name,
                          guint interval_ms, bool include_disabled)
 {
-    for (xmlNode *operation = pcmk__xe_first_child(rsc->ops_xml, PCMK_XE_OP,
-                                                   NULL, NULL);
+    for (xmlNode *operation = pcmk__xe_first_child(rsc->private->ops_xml,
+                                                   PCMK_XE_OP, NULL, NULL);
          operation != NULL; operation = pcmk__xe_next_same(operation)) {
 
         bool enabled = false;
         const char *config_name = NULL;
         const char *interval_spec = NULL;
         guint tmp_ms = 0U;
 
         // @TODO This does not consider meta-attributes, rules, defaults, etc.
         if (!include_disabled
             && (pcmk__xe_get_bool_attr(operation, PCMK_META_ENABLED,
                                        &enabled) == pcmk_rc_ok) && !enabled) {
             continue;
         }
 
         interval_spec = crm_element_value(operation, PCMK_META_INTERVAL);
         pcmk_parse_interval_spec(interval_spec, &tmp_ms);
         if (tmp_ms != interval_ms) {
             continue;
         }
 
         config_name = crm_element_value(operation, PCMK_XA_NAME);
         if (pcmk__str_eq(action_name, config_name, pcmk__str_none)) {
             return operation;
         }
     }
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Find the XML configuration of a resource action
  *
  * \param[in] rsc               Resource to find action configuration for
  * \param[in] action_name       Action name to search for
  * \param[in] interval_ms       Action interval (in milliseconds) to search for
  * \param[in] include_disabled  If false, do not return disabled actions
  *
  * \return XML configuration of desired action if any, otherwise NULL
  */
 xmlNode *
 pcmk__find_action_config(const pcmk_resource_t *rsc, const char *action_name,
                          guint interval_ms, bool include_disabled)
 {
     xmlNode *action_config = NULL;
 
     // Try requested action first
     action_config = find_exact_action_config(rsc, action_name, interval_ms,
                                              include_disabled);
 
     // For migrate_to and migrate_from actions, retry with "migrate"
     // @TODO This should be either documented or deprecated
     if ((action_config == NULL)
         && pcmk__str_any_of(action_name, PCMK_ACTION_MIGRATE_TO,
                             PCMK_ACTION_MIGRATE_FROM, NULL)) {
         action_config = find_exact_action_config(rsc, "migrate", 0,
                                                  include_disabled);
     }
 
     return action_config;
 }
 
 /*!
  * \internal
  * \brief Create a new action object
  *
  * \param[in]     key        Action key
  * \param[in]     task       Action name
  * \param[in,out] rsc        Resource that action is for (if any)
  * \param[in]     node       Node that action is on (if any)
  * \param[in]     optional   Whether action should be considered optional
  * \param[in,out] scheduler  Scheduler data
  *
  * \return Newly allocated action
  * \note This function takes ownership of \p key. It is the caller's
  *       responsibility to free the return value with pe_free_action().
  */
 static pcmk_action_t *
 new_action(char *key, const char *task, pcmk_resource_t *rsc,
            const pcmk_node_t *node, bool optional, pcmk_scheduler_t *scheduler)
 {
     pcmk_action_t *action = pcmk__assert_alloc(1, sizeof(pcmk_action_t));
 
     action->rsc = rsc;
     action->task = pcmk__str_copy(task);
     action->uuid = key;
 
     if (node) {
         action->node = pe__copy_node(node);
     }
 
     if (pcmk__str_eq(task, PCMK_ACTION_LRM_DELETE, pcmk__str_casei)) {
         // Resource history deletion for a node can be done on the DC
         pcmk__set_action_flags(action, pcmk_action_on_dc);
     }
 
     pcmk__set_action_flags(action, pcmk_action_runnable);
     if (optional) {
         pcmk__set_action_flags(action, pcmk_action_optional);
     } else {
         pcmk__clear_action_flags(action, pcmk_action_optional);
     }
 
     if (rsc == NULL) {
         action->meta = pcmk__strkey_table(free, free);
     } else {
         guint interval_ms = 0;
 
         parse_op_key(key, NULL, NULL, &interval_ms);
         action->op_entry = pcmk__find_action_config(rsc, task, interval_ms,
                                                     true);
 
         /* If the given key is for one of the many notification pseudo-actions
          * (pre_notify_promote, etc.), the actual action name is "notify"
          */
         if ((action->op_entry == NULL) && (strstr(key, "_notify_") != NULL)) {
             action->op_entry = find_exact_action_config(rsc, PCMK_ACTION_NOTIFY,
                                                         0, true);
         }
 
         unpack_operation(action, action->op_entry, interval_ms);
     }
 
     pcmk__rsc_trace(rsc, "Created %s action %d (%s): %s for %s on %s",
                     (optional? "optional" : "required"),
                     scheduler->action_id, key, task,
                     ((rsc == NULL)? "no resource" : rsc->id),
                     pcmk__node_name(node));
     action->id = scheduler->action_id++;
 
     scheduler->actions = g_list_prepend(scheduler->actions, action);
     if (rsc == NULL) {
         add_singleton(scheduler, action);
     } else {
         rsc->actions = g_list_prepend(rsc->actions, action);
     }
     return action;
 }
 
 /*!
  * \internal
  * \brief Unpack a resource's action-specific instance parameters
  *
  * \param[in]     action_xml  XML of action's configuration in CIB (if any)
  * \param[in,out] node_attrs  Table of node attributes (for rule evaluation)
  * \param[in,out] scheduler   Cluster working set (for rule evaluation)
  *
  * \return Newly allocated hash table of action-specific instance parameters
  */
 GHashTable *
 pcmk__unpack_action_rsc_params(const xmlNode *action_xml,
                                GHashTable *node_attrs,
                                pcmk_scheduler_t *scheduler)
 {
     GHashTable *params = pcmk__strkey_table(free, free);
 
     pe_rule_eval_data_t rule_data = {
         .node_hash = node_attrs,
         .now = scheduler->now,
         .match_data = NULL,
         .rsc_data = NULL,
         .op_data = NULL
     };
 
     pe__unpack_dataset_nvpairs(action_xml, PCMK_XE_INSTANCE_ATTRIBUTES,
                                &rule_data, params, NULL,
                                FALSE, scheduler);
     return params;
 }
 
 /*!
  * \internal
  * \brief Update an action's optional flag
  *
  * \param[in,out] action    Action to update
  * \param[in]     optional  Requested optional status
  */
 static void
 update_action_optional(pcmk_action_t *action, gboolean optional)
 {
     // Force a non-recurring action to be optional if its resource is unmanaged
     if ((action->rsc != NULL) && (action->node != NULL)
         && !pcmk_is_set(action->flags, pcmk_action_pseudo)
         && !pcmk_is_set(action->rsc->flags, pcmk_rsc_managed)
         && (g_hash_table_lookup(action->meta, PCMK_META_INTERVAL) == NULL)) {
             pcmk__rsc_debug(action->rsc,
                             "%s on %s is optional (%s is unmanaged)",
                             action->uuid, pcmk__node_name(action->node),
                             action->rsc->id);
             pcmk__set_action_flags(action, pcmk_action_optional);
             // We shouldn't clear runnable here because ... something
 
     // Otherwise require the action if requested
     } else if (!optional) {
         pcmk__clear_action_flags(action, pcmk_action_optional);
     }
 }
 
 static enum pe_quorum_policy
 effective_quorum_policy(pcmk_resource_t *rsc, pcmk_scheduler_t *scheduler)
 {
     enum pe_quorum_policy policy = scheduler->no_quorum_policy;
 
     if (pcmk_is_set(scheduler->flags, pcmk_sched_quorate)) {
         policy = pcmk_no_quorum_ignore;
 
     } else if (scheduler->no_quorum_policy == pcmk_no_quorum_demote) {
         switch (rsc->role) {
             case pcmk_role_promoted:
             case pcmk_role_unpromoted:
                 if (rsc->next_role > pcmk_role_unpromoted) {
                     pe__set_next_role(rsc, pcmk_role_unpromoted,
                                       PCMK_OPT_NO_QUORUM_POLICY "=demote");
                 }
                 policy = pcmk_no_quorum_ignore;
                 break;
             default:
                 policy = pcmk_no_quorum_stop;
                 break;
         }
     }
     return policy;
 }
 
 /*!
  * \internal
  * \brief Update a resource action's runnable flag
  *
  * \param[in,out] action     Action to update
  * \param[in,out] scheduler  Scheduler data
  *
  * \note This may also schedule fencing if a stop is unrunnable.
  */
 static void
 update_resource_action_runnable(pcmk_action_t *action,
                                 pcmk_scheduler_t *scheduler)
 {
     pcmk_resource_t *rsc = action->rsc;
 
     if (pcmk_is_set(action->flags, pcmk_action_pseudo)) {
         return;
     }
 
     if (action->node == NULL) {
         pcmk__rsc_trace(rsc, "%s is unrunnable (unallocated)", action->uuid);
         pcmk__clear_action_flags(action, pcmk_action_runnable);
 
     } else if (!pcmk_is_set(action->flags, pcmk_action_on_dc)
                && !(action->node->details->online)
                && (!pcmk__is_guest_or_bundle_node(action->node)
                    || action->node->details->remote_requires_reset)) {
         pcmk__clear_action_flags(action, pcmk_action_runnable);
         do_crm_log(LOG_WARNING, "%s on %s is unrunnable (node is offline)",
                    action->uuid, pcmk__node_name(action->node));
         if (pcmk_is_set(rsc->flags, pcmk_rsc_managed)
             && pcmk__str_eq(action->task, PCMK_ACTION_STOP, pcmk__str_casei)
             && !(action->node->details->unclean)) {
             pe_fence_node(scheduler, action->node, "stop is unrunnable", false);
         }
 
     } else if (!pcmk_is_set(action->flags, pcmk_action_on_dc)
                && action->node->details->pending) {
         pcmk__clear_action_flags(action, pcmk_action_runnable);
         do_crm_log(LOG_WARNING,
                    "Action %s on %s is unrunnable (node is pending)",
                    action->uuid, pcmk__node_name(action->node));
 
     } else if (action->needs == pcmk_requires_nothing) {
         pe_action_set_reason(action, NULL, TRUE);
         if (pcmk__is_guest_or_bundle_node(action->node)
             && !pe_can_fence(scheduler, action->node)) {
             /* An action that requires nothing usually does not require any
              * fencing in order to be runnable. However, there is an exception:
              * such an action cannot be completed if it is on a guest node whose
              * host is unclean and cannot be fenced.
              */
             pcmk__rsc_debug(rsc,
                             "%s on %s is unrunnable "
                             "(node's host cannot be fenced)",
                             action->uuid, pcmk__node_name(action->node));
             pcmk__clear_action_flags(action, pcmk_action_runnable);
         } else {
             pcmk__rsc_trace(rsc,
                             "%s on %s does not require fencing or quorum",
                             action->uuid, pcmk__node_name(action->node));
             pcmk__set_action_flags(action, pcmk_action_runnable);
         }
 
     } else {
         switch (effective_quorum_policy(rsc, scheduler)) {
             case pcmk_no_quorum_stop:
                 pcmk__rsc_debug(rsc, "%s on %s is unrunnable (no quorum)",
                                 action->uuid, pcmk__node_name(action->node));
                 pcmk__clear_action_flags(action, pcmk_action_runnable);
                 pe_action_set_reason(action, "no quorum", true);
                 break;
 
             case pcmk_no_quorum_freeze:
                 if (!rsc->private->fns->active(rsc, TRUE)
                     || (rsc->next_role > rsc->role)) {
                     pcmk__rsc_debug(rsc, "%s on %s is unrunnable (no quorum)",
                                     action->uuid,
                                     pcmk__node_name(action->node));
                     pcmk__clear_action_flags(action, pcmk_action_runnable);
                     pe_action_set_reason(action, "quorum freeze", true);
                 }
                 break;
 
             default:
                 //pe_action_set_reason(action, NULL, TRUE);
                 pcmk__set_action_flags(action, pcmk_action_runnable);
                 break;
         }
     }
 }
 
 /*!
  * \internal
  * \brief Update a resource object's flags for a new action on it
  *
  * \param[in,out] rsc     Resource that action is for (if any)
  * \param[in]     action  New action
  */
 static void
 update_resource_flags_for_action(pcmk_resource_t *rsc,
                                  const pcmk_action_t *action)
 {
     /* @COMPAT pcmk_rsc_starting and pcmk_rsc_stopping are deprecated and unused
      * within Pacemaker, and will eventually be removed
      */
     if (pcmk__str_eq(action->task, PCMK_ACTION_STOP, pcmk__str_casei)) {
         pcmk__set_rsc_flags(rsc, pcmk_rsc_stopping);
 
     } else if (pcmk__str_eq(action->task, PCMK_ACTION_START, pcmk__str_casei)) {
         if (pcmk_is_set(action->flags, pcmk_action_runnable)) {
             pcmk__set_rsc_flags(rsc, pcmk_rsc_starting);
         } else {
             pcmk__clear_rsc_flags(rsc, pcmk_rsc_starting);
         }
     }
 }
 
 static bool
 valid_stop_on_fail(const char *value)
 {
     return !pcmk__strcase_any_of(value,
                                  PCMK_VALUE_STANDBY, PCMK_VALUE_DEMOTE,
                                  PCMK_VALUE_STOP, NULL);
 }
 
 /*!
  * \internal
  * \brief Validate (and possibly reset) resource action's on_fail meta-attribute
  *
  * \param[in]     rsc            Resource that action is for
  * \param[in]     action_name    Action name
  * \param[in]     action_config  Action configuration XML from CIB (if any)
  * \param[in,out] meta           Table of action meta-attributes
  */
 static void
 validate_on_fail(const pcmk_resource_t *rsc, const char *action_name,
                  const xmlNode *action_config, GHashTable *meta)
 {
     const char *name = NULL;
     const char *role = NULL;
     const char *interval_spec = NULL;
     const char *value = g_hash_table_lookup(meta, PCMK_META_ON_FAIL);
     guint interval_ms = 0U;
 
     // Stop actions can only use certain on-fail values
     if (pcmk__str_eq(action_name, PCMK_ACTION_STOP, pcmk__str_none)
         && !valid_stop_on_fail(value)) {
 
         pcmk__config_err("Resetting '" PCMK_META_ON_FAIL "' for %s stop "
                          "action to default value because '%s' is not "
                          "allowed for stop", rsc->id, value);
         g_hash_table_remove(meta, PCMK_META_ON_FAIL);
         return;
     }
 
     /* Demote actions default on-fail to the on-fail value for the first
      * recurring monitor for the promoted role (if any).
      */
     if (pcmk__str_eq(action_name, PCMK_ACTION_DEMOTE, pcmk__str_none)
         && (value == NULL)) {
 
         /* @TODO This does not consider promote options set in a meta-attribute
          * block (which may have rules that need to be evaluated) rather than
          * XML properties.
          */
-        for (xmlNode *operation = pcmk__xe_first_child(rsc->ops_xml, PCMK_XE_OP,
-                                                       NULL, NULL);
+        for (xmlNode *operation = pcmk__xe_first_child(rsc->private->ops_xml,
+                                                       PCMK_XE_OP, NULL, NULL);
              operation != NULL; operation = pcmk__xe_next_same(operation)) {
 
             bool enabled = false;
             const char *promote_on_fail = NULL;
 
             /* We only care about explicit on-fail (if promote uses default, so
              * can demote)
              */
             promote_on_fail = crm_element_value(operation, PCMK_META_ON_FAIL);
             if (promote_on_fail == NULL) {
                 continue;
             }
 
             // We only care about recurring monitors for the promoted role
             name = crm_element_value(operation, PCMK_XA_NAME);
             role = crm_element_value(operation, PCMK_XA_ROLE);
             if (!pcmk__str_eq(name, PCMK_ACTION_MONITOR, pcmk__str_none)
                 || !pcmk__strcase_any_of(role, PCMK_ROLE_PROMOTED,
                                          PCMK__ROLE_PROMOTED_LEGACY, NULL)) {
                 continue;
             }
             interval_spec = crm_element_value(operation, PCMK_META_INTERVAL);
             pcmk_parse_interval_spec(interval_spec, &interval_ms);
             if (interval_ms == 0U) {
                 continue;
             }
 
             // We only care about enabled monitors
             if ((pcmk__xe_get_bool_attr(operation, PCMK_META_ENABLED,
                                         &enabled) == pcmk_rc_ok) && !enabled) {
                 continue;
             }
 
             /* Demote actions can't default to
              * PCMK_META_ON_FAIL=PCMK_VALUE_DEMOTE
              */
             if (pcmk__str_eq(promote_on_fail, PCMK_VALUE_DEMOTE,
                              pcmk__str_casei)) {
                 continue;
             }
 
             // Use value from first applicable promote action found
             pcmk__insert_dup(meta, PCMK_META_ON_FAIL, promote_on_fail);
         }
         return;
     }
 
     if (pcmk__str_eq(action_name, PCMK_ACTION_LRM_DELETE, pcmk__str_none)
         && !pcmk__str_eq(value, PCMK_VALUE_IGNORE, pcmk__str_casei)) {
 
         pcmk__insert_dup(meta, PCMK_META_ON_FAIL, PCMK_VALUE_IGNORE);
         return;
     }
 
     // PCMK_META_ON_FAIL=PCMK_VALUE_DEMOTE is allowed only for certain actions
     if (pcmk__str_eq(value, PCMK_VALUE_DEMOTE, pcmk__str_casei)) {
         name = crm_element_value(action_config, PCMK_XA_NAME);
         role = crm_element_value(action_config, PCMK_XA_ROLE);
         interval_spec = crm_element_value(action_config, PCMK_META_INTERVAL);
         pcmk_parse_interval_spec(interval_spec, &interval_ms);
 
         if (!pcmk__str_eq(name, PCMK_ACTION_PROMOTE, pcmk__str_none)
             && ((interval_ms == 0U)
                 || !pcmk__str_eq(name, PCMK_ACTION_MONITOR, pcmk__str_none)
                 || !pcmk__strcase_any_of(role, PCMK_ROLE_PROMOTED,
                                          PCMK__ROLE_PROMOTED_LEGACY, NULL))) {
 
             pcmk__config_err("Resetting '" PCMK_META_ON_FAIL "' for %s %s "
                              "action to default value because 'demote' is not "
                              "allowed for it", rsc->id, name);
             g_hash_table_remove(meta, PCMK_META_ON_FAIL);
             return;
         }
     }
 }
 
 static int
 unpack_timeout(const char *value)
 {
     long long timeout_ms = crm_get_msec(value);
 
     if (timeout_ms <= 0) {
         timeout_ms = PCMK_DEFAULT_ACTION_TIMEOUT_MS;
     }
     return (int) QB_MIN(timeout_ms, INT_MAX);
 }
 
 // true if value contains valid, non-NULL interval origin for recurring op
 static bool
 unpack_interval_origin(const char *value, const xmlNode *xml_obj,
                        guint interval_ms, const crm_time_t *now,
                        long long *start_delay)
 {
     long long result = 0;
     guint interval_sec = interval_ms / 1000;
     crm_time_t *origin = NULL;
 
     // Ignore unspecified values and non-recurring operations
     if ((value == NULL) || (interval_ms == 0) || (now == NULL)) {
         return false;
     }
 
     // Parse interval origin from text
     origin = crm_time_new(value);
     if (origin == NULL) {
         pcmk__config_err("Ignoring '" PCMK_META_INTERVAL_ORIGIN "' for "
                          "operation '%s' because '%s' is not valid",
                          pcmk__s(pcmk__xe_id(xml_obj), "(missing ID)"), value);
         return false;
     }
 
     // Get seconds since origin (negative if origin is in the future)
     result = crm_time_get_seconds(now) - crm_time_get_seconds(origin);
     crm_time_free(origin);
 
     // Calculate seconds from closest interval to now
     result = result % interval_sec;
 
     // Calculate seconds remaining until next interval
     result = ((result <= 0)? 0 : interval_sec) - result;
     crm_info("Calculated a start delay of %llds for operation '%s'",
              result, pcmk__s(pcmk__xe_id(xml_obj), "(unspecified)"));
 
     if (start_delay != NULL) {
         *start_delay = result * 1000; // milliseconds
     }
     return true;
 }
 
 static int
 unpack_start_delay(const char *value, GHashTable *meta)
 {
     long long start_delay_ms = 0;
 
     if (value == NULL) {
         return 0;
     }
 
     start_delay_ms = crm_get_msec(value);
     start_delay_ms = QB_MIN(start_delay_ms, INT_MAX);
     if (start_delay_ms < 0) {
         start_delay_ms = 0;
     }
 
     if (meta != NULL) {
         g_hash_table_replace(meta, strdup(PCMK_META_START_DELAY),
                              pcmk__itoa(start_delay_ms));
     }
 
     return (int) start_delay_ms;
 }
 
 /*!
  * \internal
  * \brief Find a resource's most frequent recurring monitor
  *
  * \param[in] rsc  Resource to check
  *
  * \return Operation XML configured for most frequent recurring monitor for
  *         \p rsc (if any)
  */
 static xmlNode *
 most_frequent_monitor(const pcmk_resource_t *rsc)
 {
     guint min_interval_ms = G_MAXUINT;
     xmlNode *op = NULL;
 
-    for (xmlNode *operation = pcmk__xe_first_child(rsc->ops_xml, PCMK_XE_OP,
-                                                   NULL, NULL);
+    for (xmlNode *operation = pcmk__xe_first_child(rsc->private->ops_xml,
+                                                   PCMK_XE_OP, NULL, NULL);
          operation != NULL; operation = pcmk__xe_next_same(operation)) {
 
         bool enabled = false;
         guint interval_ms = 0U;
         const char *interval_spec = crm_element_value(operation,
                                                       PCMK_META_INTERVAL);
 
         // We only care about enabled recurring monitors
         if (!pcmk__str_eq(crm_element_value(operation, PCMK_XA_NAME),
                           PCMK_ACTION_MONITOR, pcmk__str_none)) {
             continue;
         }
 
         pcmk_parse_interval_spec(interval_spec, &interval_ms);
         if (interval_ms == 0U) {
             continue;
         }
 
         // @TODO This does not consider meta-attributes, rules, defaults, etc.
         if ((pcmk__xe_get_bool_attr(operation, PCMK_META_ENABLED,
                                     &enabled) == pcmk_rc_ok) && !enabled) {
             continue;
         }
 
         if (interval_ms < min_interval_ms) {
             min_interval_ms = interval_ms;
             op = operation;
         }
     }
     return op;
 }
 
 /*!
  * \internal
  * \brief Unpack action meta-attributes
  *
  * \param[in,out] rsc            Resource that action is for
  * \param[in]     node           Node that action is on
  * \param[in]     action_name    Action name
  * \param[in]     interval_ms    Action interval (in milliseconds)
  * \param[in]     action_config  Action XML configuration from CIB (if any)
  *
  * Unpack a resource action's meta-attributes (normalizing the interval,
  * timeout, and start delay values as integer milliseconds) from its CIB XML
  * configuration (including defaults).
  *
  * \return Newly allocated hash table with normalized action meta-attributes
  */
 GHashTable *
 pcmk__unpack_action_meta(pcmk_resource_t *rsc, const pcmk_node_t *node,
                          const char *action_name, guint interval_ms,
                          const xmlNode *action_config)
 {
     GHashTable *meta = NULL;
     const char *timeout_spec = NULL;
     const char *str = NULL;
 
     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_op_eval_data_t op_rule_data = {
         .op_name = action_name,
         .interval = interval_ms,
     };
 
     pe_rule_eval_data_t rule_data = {
         /* @COMPAT Support for node attribute expressions in operation
          * meta-attributes (whether in the operation configuration or operation
          * defaults) is deprecated. When we can break behavioral backward
          * compatibility, drop this line.
          */
         .node_hash = (node == NULL)? NULL : node->details->attrs,
 
         .now = rsc->cluster->now,
         .match_data = NULL,
         .rsc_data = &rsc_rule_data,
         .op_data = &op_rule_data,
     };
 
     meta = pcmk__strkey_table(free, free);
 
     // Cluster-wide <op_defaults> <meta_attributes>
     pe__unpack_dataset_nvpairs(rsc->cluster->op_defaults,
                                PCMK_XE_META_ATTRIBUTES, &rule_data, meta, NULL,
                                FALSE, rsc->cluster);
 
     // Derive default timeout for probes from recurring monitor timeouts
     if (pcmk_is_probe(action_name, interval_ms)) {
         xmlNode *min_interval_mon = most_frequent_monitor(rsc);
 
         if (min_interval_mon != NULL) {
             /* @TODO This does not consider timeouts set in
              * PCMK_XE_META_ATTRIBUTES blocks (which may also have rules that
              * need to be evaluated).
              */
             timeout_spec = crm_element_value(min_interval_mon,
                                              PCMK_META_TIMEOUT);
             if (timeout_spec != NULL) {
                 pcmk__rsc_trace(rsc,
                                 "Setting default timeout for %s probe to "
                                 "most frequent monitor's timeout '%s'",
                                 rsc->id, timeout_spec);
                 pcmk__insert_dup(meta, PCMK_META_TIMEOUT, timeout_spec);
             }
         }
     }
 
     if (action_config != NULL) {
         // <op> <meta_attributes> take precedence over defaults
         pe__unpack_dataset_nvpairs(action_config, PCMK_XE_META_ATTRIBUTES,
                                    &rule_data, meta, NULL, TRUE, rsc->cluster);
 
         /* Anything set as an <op> XML property has highest precedence.
          * This ensures we use the name and interval from the <op> tag.
          * (See below for the only exception, fence device start/probe timeout.)
          */
         for (xmlAttrPtr attr = action_config->properties;
              attr != NULL; attr = attr->next) {
             pcmk__insert_dup(meta, (const char *) attr->name,
                              pcmk__xml_attr_value(attr));
         }
     }
 
     g_hash_table_remove(meta, PCMK_XA_ID);
 
     // Normalize interval to milliseconds
     if (interval_ms > 0) {
         g_hash_table_insert(meta, pcmk__str_copy(PCMK_META_INTERVAL),
                             crm_strdup_printf("%u", interval_ms));
     } else {
         g_hash_table_remove(meta, PCMK_META_INTERVAL);
     }
 
     /* Timeout order of precedence (highest to lowest):
      *   1. pcmk_monitor_timeout resource parameter (only for starts and probes
      *      when rsc has pcmk_ra_cap_fence_params; this gets used for recurring
      *      monitors via the executor instead)
      *   2. timeout configured in <op> (with <op timeout> taking precedence over
      *      <op> <meta_attributes>)
      *   3. timeout configured in <op_defaults> <meta_attributes>
      *   4. PCMK_DEFAULT_ACTION_TIMEOUT_MS
      */
 
     // Check for pcmk_monitor_timeout
     if (pcmk_is_set(pcmk_get_ra_caps(rsc_rule_data.standard),
                     pcmk_ra_cap_fence_params)
         && (pcmk__str_eq(action_name, PCMK_ACTION_START, pcmk__str_none)
             || pcmk_is_probe(action_name, interval_ms))) {
 
         GHashTable *params = pe_rsc_params(rsc, node, rsc->cluster);
 
         timeout_spec = g_hash_table_lookup(params, "pcmk_monitor_timeout");
         if (timeout_spec != NULL) {
             pcmk__rsc_trace(rsc,
                             "Setting timeout for %s %s to "
                             "pcmk_monitor_timeout (%s)",
                             rsc->id, action_name, timeout_spec);
             pcmk__insert_dup(meta, PCMK_META_TIMEOUT, timeout_spec);
         }
     }
 
     // Normalize timeout to positive milliseconds
     timeout_spec = g_hash_table_lookup(meta, PCMK_META_TIMEOUT);
     g_hash_table_insert(meta, pcmk__str_copy(PCMK_META_TIMEOUT),
                         pcmk__itoa(unpack_timeout(timeout_spec)));
 
     // Ensure on-fail has a valid value
     validate_on_fail(rsc, action_name, action_config, meta);
 
     // Normalize PCMK_META_START_DELAY
     str = g_hash_table_lookup(meta, PCMK_META_START_DELAY);
     if (str != NULL) {
         unpack_start_delay(str, meta);
     } else {
         long long start_delay = 0;
 
         str = g_hash_table_lookup(meta, PCMK_META_INTERVAL_ORIGIN);
         if (unpack_interval_origin(str, action_config, interval_ms,
                                    rsc->cluster->now, &start_delay)) {
             g_hash_table_insert(meta, pcmk__str_copy(PCMK_META_START_DELAY),
                                 crm_strdup_printf("%lld", start_delay));
         }
     }
     return meta;
 }
 
 /*!
  * \internal
  * \brief Determine an action's quorum and fencing dependency
  *
  * \param[in] rsc          Resource that action is for
  * \param[in] action_name  Name of action being unpacked
  *
  * \return Quorum and fencing dependency appropriate to action
  */
 enum rsc_start_requirement
 pcmk__action_requires(const pcmk_resource_t *rsc, const char *action_name)
 {
     const char *value = NULL;
     enum rsc_start_requirement requires = pcmk_requires_nothing;
 
     CRM_CHECK((rsc != NULL) && (action_name != NULL), return requires);
 
     if (!pcmk__strcase_any_of(action_name, PCMK_ACTION_START,
                               PCMK_ACTION_PROMOTE, NULL)) {
         value = "nothing (not start or promote)";
 
     } else if (pcmk_is_set(rsc->flags, pcmk_rsc_needs_fencing)) {
         requires = pcmk_requires_fencing;
         value = "fencing";
 
     } else if (pcmk_is_set(rsc->flags, pcmk_rsc_needs_quorum)) {
         requires = pcmk_requires_quorum;
         value = "quorum";
 
     } else {
         value = "nothing";
     }
     pcmk__rsc_trace(rsc, "%s of %s requires %s", action_name, rsc->id, value);
     return requires;
 }
 
 /*!
  * \internal
  * \brief Parse action failure response from a user-provided string
  *
  * \param[in] rsc          Resource that action is for
  * \param[in] action_name  Name of action
  * \param[in] interval_ms  Action interval (in milliseconds)
  * \param[in] value        User-provided configuration value for on-fail
  *
  * \return Action failure response parsed from \p text
  */
 enum action_fail_response
 pcmk__parse_on_fail(const pcmk_resource_t *rsc, const char *action_name,
                     guint interval_ms, const char *value)
 {
     const char *desc = NULL;
     bool needs_remote_reset = false;
     enum action_fail_response on_fail = pcmk_on_fail_ignore;
 
     // There's no enum value for unknown or invalid, so assert
     CRM_ASSERT((rsc != NULL) && (action_name != NULL));
 
     if (value == NULL) {
         // Use default
 
     } else if (pcmk__str_eq(value, PCMK_VALUE_BLOCK, pcmk__str_casei)) {
         on_fail = pcmk_on_fail_block;
         desc = "block";
 
     } else if (pcmk__str_eq(value, PCMK_VALUE_FENCE, pcmk__str_casei)) {
         if (pcmk_is_set(rsc->cluster->flags, pcmk_sched_fencing_enabled)) {
             on_fail = pcmk_on_fail_fence_node;
             desc = "node fencing";
         } else {
             pcmk__config_err("Resetting '" PCMK_META_ON_FAIL "' for "
                              "%s of %s to 'stop' because 'fence' is not "
                              "valid when fencing is disabled",
                              action_name, rsc->id);
             on_fail = pcmk_on_fail_stop;
             desc = "stop resource";
         }
 
     } else if (pcmk__str_eq(value, PCMK_VALUE_STANDBY, pcmk__str_casei)) {
         on_fail = pcmk_on_fail_standby_node;
         desc = "node standby";
 
     } else if (pcmk__strcase_any_of(value,
                                     PCMK_VALUE_IGNORE, PCMK_VALUE_NOTHING,
                                     NULL)) {
         desc = "ignore";
 
     } else if (pcmk__str_eq(value, "migrate", pcmk__str_casei)) {
         on_fail = pcmk_on_fail_ban;
         desc = "force migration";
 
     } else if (pcmk__str_eq(value, PCMK_VALUE_STOP, pcmk__str_casei)) {
         on_fail = pcmk_on_fail_stop;
         desc = "stop resource";
 
     } else if (pcmk__str_eq(value, PCMK_VALUE_RESTART, pcmk__str_casei)) {
         on_fail = pcmk_on_fail_restart;
         desc = "restart (and possibly migrate)";
 
     } else if (pcmk__str_eq(value, PCMK_VALUE_RESTART_CONTAINER,
                             pcmk__str_casei)) {
         if (rsc->container == NULL) {
             pcmk__rsc_debug(rsc,
                             "Using default " PCMK_META_ON_FAIL " for %s "
                             "of %s because it does not have a container",
                             action_name, rsc->id);
         } else {
             on_fail = pcmk_on_fail_restart_container;
             desc = "restart container (and possibly migrate)";
         }
 
     } else if (pcmk__str_eq(value, PCMK_VALUE_DEMOTE, pcmk__str_casei)) {
         on_fail = pcmk_on_fail_demote;
         desc = "demote instance";
 
     } else {
         pcmk__config_err("Using default '" PCMK_META_ON_FAIL "' for "
                          "%s of %s because '%s' is not valid",
                          action_name, rsc->id, value);
     }
 
     /* Remote node connections are handled specially. Failures that result
      * in dropping an active connection must result in fencing. The only
      * failures that don't are probes and starts. The user can explicitly set
      * PCMK_META_ON_FAIL=PCMK_VALUE_FENCE to fence after start failures.
      */
     if (rsc->is_remote_node
         && pcmk__is_remote_node(pcmk_find_node(rsc->cluster, rsc->id))
         && !pcmk_is_probe(action_name, interval_ms)
         && !pcmk__str_eq(action_name, PCMK_ACTION_START, pcmk__str_none)) {
         needs_remote_reset = true;
         if (!pcmk_is_set(rsc->flags, pcmk_rsc_managed)) {
             desc = NULL; // Force default for unmanaged connections
         }
     }
 
     if (desc != NULL) {
         // Explicit value used, default not needed
 
     } else if (rsc->container != NULL) {
         on_fail = pcmk_on_fail_restart_container;
         desc = "restart container (and possibly migrate) (default)";
 
     } else if (needs_remote_reset) {
         if (pcmk_is_set(rsc->flags, pcmk_rsc_managed)) {
             if (pcmk_is_set(rsc->cluster->flags,
                             pcmk_sched_fencing_enabled)) {
                 desc = "fence remote node (default)";
             } else {
                 desc = "recover remote node connection (default)";
             }
             on_fail = pcmk_on_fail_reset_remote;
         } else {
             on_fail = pcmk_on_fail_stop;
             desc = "stop unmanaged remote node (enforcing default)";
         }
 
     } else if (pcmk__str_eq(action_name, PCMK_ACTION_STOP, pcmk__str_none)) {
         if (pcmk_is_set(rsc->cluster->flags, pcmk_sched_fencing_enabled)) {
             on_fail = pcmk_on_fail_fence_node;
             desc = "resource fence (default)";
         } else {
             on_fail = pcmk_on_fail_block;
             desc = "resource block (default)";
         }
 
     } else {
         on_fail = pcmk_on_fail_restart;
         desc = "restart (and possibly migrate) (default)";
     }
 
     pcmk__rsc_trace(rsc, "Failure handling for %s-interval %s of %s: %s",
                     pcmk__readable_interval(interval_ms), action_name,
                     rsc->id, desc);
     return on_fail;
 }
 
 /*!
  * \internal
  * \brief Determine a resource's role after failure of an action
  *
  * \param[in] rsc          Resource that action is for
  * \param[in] action_name  Action name
  * \param[in] on_fail      Failure handling for action
  * \param[in] meta         Unpacked action meta-attributes
  *
  * \return Resource role that results from failure of action
  */
 enum rsc_role_e
 pcmk__role_after_failure(const pcmk_resource_t *rsc, const char *action_name,
                          enum action_fail_response on_fail, GHashTable *meta)
 {
     const char *value = NULL;
     enum rsc_role_e role = pcmk_role_unknown;
 
     // Set default for role after failure specially in certain circumstances
     switch (on_fail) {
         case pcmk_on_fail_stop:
             role = pcmk_role_stopped;
             break;
 
         case pcmk_on_fail_reset_remote:
             if (rsc->remote_reconnect_ms != 0) {
                 role = pcmk_role_stopped;
             }
             break;
 
         default:
             break;
     }
 
     // @COMPAT Check for explicitly configured role (deprecated)
     value = g_hash_table_lookup(meta, PCMK__META_ROLE_AFTER_FAILURE);
     if (value != NULL) {
         pcmk__warn_once(pcmk__wo_role_after,
                         "Support for " PCMK__META_ROLE_AFTER_FAILURE " is "
                         "deprecated and will be removed in a future release");
         if (role == pcmk_role_unknown) {
             role = pcmk_parse_role(value);
             if (role == pcmk_role_unknown) {
                 pcmk__config_err("Ignoring invalid value %s for "
                                  PCMK__META_ROLE_AFTER_FAILURE,
                                  value);
             }
         }
     }
 
     if (role == pcmk_role_unknown) {
         // Use default
         if (pcmk__str_eq(action_name, PCMK_ACTION_PROMOTE, pcmk__str_none)) {
             role = pcmk_role_unpromoted;
         } else {
             role = pcmk_role_started;
         }
     }
     pcmk__rsc_trace(rsc, "Role after %s %s failure is: %s",
                     rsc->id, action_name, pcmk_role_text(role));
     return role;
 }
 
 /*!
  * \internal
  * \brief Unpack action configuration
  *
  * Unpack a resource action's meta-attributes (normalizing the interval,
  * timeout, and start delay values as integer milliseconds), requirements, and
  * failure policy from its CIB XML configuration (including defaults).
  *
  * \param[in,out] action       Resource action to unpack into
  * \param[in]     xml_obj      Action configuration XML (NULL for defaults only)
  * \param[in]     interval_ms  How frequently to perform the operation
  */
 static void
 unpack_operation(pcmk_action_t *action, const xmlNode *xml_obj,
                  guint interval_ms)
 {
     const char *value = NULL;
 
     action->meta = pcmk__unpack_action_meta(action->rsc, action->node,
                                             action->task, interval_ms, xml_obj);
     action->needs = pcmk__action_requires(action->rsc, action->task);
 
     value = g_hash_table_lookup(action->meta, PCMK_META_ON_FAIL);
     action->on_fail = pcmk__parse_on_fail(action->rsc, action->task,
                                           interval_ms, value);
 
     action->fail_role = pcmk__role_after_failure(action->rsc, action->task,
                                                  action->on_fail, action->meta);
 }
 
 /*!
  * \brief Create or update an action object
  *
  * \param[in,out] rsc          Resource that action is for (if any)
  * \param[in,out] key          Action key (must be non-NULL)
  * \param[in]     task         Action name (must be non-NULL)
  * \param[in]     on_node      Node that action is on (if any)
  * \param[in]     optional     Whether action should be considered optional
  * \param[in,out] scheduler    Scheduler data
  *
  * \return Action object corresponding to arguments (guaranteed not to be
  *         \c NULL)
  * \note This function takes ownership of (and might free) \p key, and
  *       \p scheduler takes ownership of the returned action (the caller should
  *       not free it).
  */
 pcmk_action_t *
 custom_action(pcmk_resource_t *rsc, char *key, const char *task,
               const pcmk_node_t *on_node, gboolean optional,
               pcmk_scheduler_t *scheduler)
 {
     pcmk_action_t *action = NULL;
 
     CRM_ASSERT((key != NULL) && (task != NULL) && (scheduler != NULL));
 
     action = find_existing_action(key, rsc, on_node, scheduler);
     if (action == NULL) {
         action = new_action(key, task, rsc, on_node, optional, scheduler);
     } else {
         free(key);
     }
 
     update_action_optional(action, optional);
 
     if (rsc != NULL) {
         /* An action can be initially created with a NULL node, and later have
          * the node added via find_existing_action() (above) -> find_actions().
          * That is why the extra parameters are unpacked here rather than in
          * new_action().
          */
         if ((action->node != NULL) && (action->op_entry != NULL)
             && !pcmk_is_set(action->flags, pcmk_action_attrs_evaluated)) {
 
             GHashTable *attrs = action->node->details->attrs;
 
             if (action->extra != NULL) {
                 g_hash_table_destroy(action->extra);
             }
             action->extra = pcmk__unpack_action_rsc_params(action->op_entry,
                                                            attrs, scheduler);
             pcmk__set_action_flags(action, pcmk_action_attrs_evaluated);
         }
 
         update_resource_action_runnable(action, scheduler);
         update_resource_flags_for_action(rsc, action);
     }
 
     if (action->extra == NULL) {
         action->extra = pcmk__strkey_table(free, free);
     }
 
     return action;
 }
 
 pcmk_action_t *
 get_pseudo_op(const char *name, pcmk_scheduler_t *scheduler)
 {
     pcmk_action_t *op = lookup_singleton(scheduler, name);
 
     if (op == NULL) {
         op = custom_action(NULL, strdup(name), name, NULL, TRUE, scheduler);
         pcmk__set_action_flags(op, pcmk_action_pseudo|pcmk_action_runnable);
     }
     return op;
 }
 
 static GList *
 find_unfencing_devices(GList *candidates, GList *matches) 
 {
     for (GList *gIter = candidates; gIter != NULL; gIter = gIter->next) {
         pcmk_resource_t *candidate = gIter->data;
 
         if (candidate->children != NULL) {
             matches = find_unfencing_devices(candidate->children, matches);
 
         } else if (!pcmk_is_set(candidate->flags, pcmk_rsc_fence_device)) {
             continue;
 
         } else if (pcmk_is_set(candidate->flags, pcmk_rsc_needs_unfencing)) {
             matches = g_list_prepend(matches, candidate);
 
         } else if (pcmk__str_eq(g_hash_table_lookup(candidate->meta,
                                                     PCMK_STONITH_PROVIDES),
                                 PCMK_VALUE_UNFENCING, pcmk__str_casei)) {
             matches = g_list_prepend(matches, candidate);
         }
     }
     return matches;
 }
 
 static int
 node_priority_fencing_delay(const pcmk_node_t *node,
                             const pcmk_scheduler_t *scheduler)
 {
     int member_count = 0;
     int online_count = 0;
     int top_priority = 0;
     int lowest_priority = 0;
     GList *gIter = NULL;
 
     // PCMK_OPT_PRIORITY_FENCING_DELAY is disabled
     if (scheduler->priority_fencing_delay <= 0) {
         return 0;
     }
 
     /* No need to request a delay if the fencing target is not a normal cluster
      * member, for example if it's a remote node or a guest node. */
     if (node->details->type != pcmk_node_variant_cluster) {
         return 0;
     }
 
     // No need to request a delay if the fencing target is in our partition
     if (node->details->online) {
         return 0;
     }
 
     for (gIter = scheduler->nodes; gIter != NULL; gIter = gIter->next) {
         pcmk_node_t *n = gIter->data;
 
         if (n->details->type != pcmk_node_variant_cluster) {
             continue;
         }
 
         member_count ++;
 
         if (n->details->online) {
             online_count++;
         }
 
         if (member_count == 1
             || n->details->priority > top_priority) {
             top_priority = n->details->priority;
         }
 
         if (member_count == 1
             || n->details->priority < lowest_priority) {
             lowest_priority = n->details->priority;
         }
     }
 
     // No need to delay if we have more than half of the cluster members
     if (online_count > member_count / 2) {
         return 0;
     }
 
     /* All the nodes have equal priority.
      * Any configured corresponding `pcmk_delay_base/max` will be applied. */
     if (lowest_priority == top_priority) {
         return 0;
     }
 
     if (node->details->priority < top_priority) {
         return 0;
     }
 
     return scheduler->priority_fencing_delay;
 }
 
 pcmk_action_t *
 pe_fence_op(pcmk_node_t *node, const char *op, bool optional,
             const char *reason, bool priority_delay,
             pcmk_scheduler_t *scheduler)
 {
     char *op_key = NULL;
     pcmk_action_t *stonith_op = NULL;
 
     if(op == NULL) {
         op = scheduler->stonith_action;
     }
 
     op_key = crm_strdup_printf("%s-%s-%s",
                                PCMK_ACTION_STONITH, node->details->uname, op);
 
     stonith_op = lookup_singleton(scheduler, op_key);
     if(stonith_op == NULL) {
         stonith_op = custom_action(NULL, op_key, PCMK_ACTION_STONITH, node,
                                    TRUE, scheduler);
 
         pcmk__insert_meta(stonith_op, PCMK__META_ON_NODE, node->details->uname);
         pcmk__insert_meta(stonith_op, PCMK__META_ON_NODE_UUID,
                           node->details->id);
         pcmk__insert_meta(stonith_op, PCMK__META_STONITH_ACTION, op);
 
         if (pcmk_is_set(scheduler->flags, pcmk_sched_enable_unfencing)) {
             /* Extra work to detect device changes
              */
             GString *digests_all = g_string_sized_new(1024);
             GString *digests_secure = g_string_sized_new(1024);
 
             GList *matches = find_unfencing_devices(scheduler->resources, NULL);
 
             for (GList *gIter = matches; gIter != NULL; gIter = gIter->next) {
                 pcmk_resource_t *match = gIter->data;
                 const char *agent = g_hash_table_lookup(match->meta,
                                                         PCMK_XA_TYPE);
                 pcmk__op_digest_t *data = NULL;
 
                 data = pe__compare_fencing_digest(match, agent, node,
                                                   scheduler);
                 if (data->rc == pcmk__digest_mismatch) {
                     optional = FALSE;
                     crm_notice("Unfencing node %s because the definition of "
                                "%s changed", pcmk__node_name(node), match->id);
                     if (!pcmk__is_daemon && scheduler->priv != NULL) {
                         pcmk__output_t *out = scheduler->priv;
 
                         out->info(out,
                                   "notice: Unfencing node %s because the "
                                   "definition of %s changed",
                                   pcmk__node_name(node), match->id);
                     }
                 }
 
                 pcmk__g_strcat(digests_all,
                                match->id, ":", agent, ":",
                                data->digest_all_calc, ",", NULL);
                 pcmk__g_strcat(digests_secure,
                                match->id, ":", agent, ":",
                                data->digest_secure_calc, ",", NULL);
             }
             pcmk__insert_dup(stonith_op->meta, PCMK__META_DIGESTS_ALL,
                              digests_all->str);
             g_string_free(digests_all, TRUE);
 
             pcmk__insert_dup(stonith_op->meta, PCMK__META_DIGESTS_SECURE,
                              digests_secure->str);
             g_string_free(digests_secure, TRUE);
         }
 
     } else {
         free(op_key);
     }
 
     if (scheduler->priority_fencing_delay > 0
 
             /* It's a suitable case where PCMK_OPT_PRIORITY_FENCING_DELAY
              * applies. At least add PCMK_OPT_PRIORITY_FENCING_DELAY field as
              * an indicator.
              */
         && (priority_delay
 
             /* The priority delay needs to be recalculated if this function has
              * been called by schedule_fencing_and_shutdowns() after node
              * priority has already been calculated by native_add_running().
              */
             || g_hash_table_lookup(stonith_op->meta,
                                    PCMK_OPT_PRIORITY_FENCING_DELAY) != NULL)) {
 
             /* Add PCMK_OPT_PRIORITY_FENCING_DELAY to the fencing op even if
              * it's 0 for the targeting node. So that it takes precedence over
              * any possible `pcmk_delay_base/max`.
              */
             char *delay_s = pcmk__itoa(node_priority_fencing_delay(node,
                                                                    scheduler));
 
             g_hash_table_insert(stonith_op->meta,
                                 strdup(PCMK_OPT_PRIORITY_FENCING_DELAY),
                                 delay_s);
     }
 
     if(optional == FALSE && pe_can_fence(scheduler, node)) {
         pcmk__clear_action_flags(stonith_op, pcmk_action_optional);
         pe_action_set_reason(stonith_op, reason, false);
 
     } else if(reason && stonith_op->reason == NULL) {
         stonith_op->reason = strdup(reason);
     }
 
     return stonith_op;
 }
 
 void
 pe_free_action(pcmk_action_t *action)
 {
     if (action == NULL) {
         return;
     }
     g_list_free_full(action->actions_before, free);
     g_list_free_full(action->actions_after, free);
     if (action->extra) {
         g_hash_table_destroy(action->extra);
     }
     if (action->meta) {
         g_hash_table_destroy(action->meta);
     }
     free(action->cancel_task);
     free(action->reason);
     free(action->task);
     free(action->uuid);
     free(action->node);
     free(action);
 }
 
 enum action_tasks
 get_complex_task(const pcmk_resource_t *rsc, const char *name)
 {
     enum action_tasks task = pcmk_parse_action(name);
 
     if (pcmk__is_primitive(rsc)) {
         switch (task) {
             case pcmk_action_stopped:
             case pcmk_action_started:
             case pcmk_action_demoted:
             case pcmk_action_promoted:
                 crm_trace("Folding %s back into its atomic counterpart for %s",
                           name, rsc->id);
                 --task;
                 break;
             default:
                 break;
         }
     }
     return task;
 }
 
 /*!
  * \internal
  * \brief Find first matching action in a list
  *
  * \param[in] input    List of actions to search
  * \param[in] uuid     If not NULL, action must have this UUID
  * \param[in] task     If not NULL, action must have this action name
  * \param[in] on_node  If not NULL, action must be on this node
  *
  * \return First action in list that matches criteria, or NULL if none
  */
 pcmk_action_t *
 find_first_action(const GList *input, const char *uuid, const char *task,
                   const pcmk_node_t *on_node)
 {
     CRM_CHECK(uuid || task, return NULL);
 
     for (const GList *gIter = input; gIter != NULL; gIter = gIter->next) {
         pcmk_action_t *action = (pcmk_action_t *) gIter->data;
 
         if (uuid != NULL && !pcmk__str_eq(uuid, action->uuid, pcmk__str_casei)) {
             continue;
 
         } else if (task != NULL && !pcmk__str_eq(task, action->task, pcmk__str_casei)) {
             continue;
 
         } else if (on_node == NULL) {
             return action;
 
         } else if (action->node == NULL) {
             continue;
 
         } else if (pcmk__same_node(on_node, action->node)) {
             return action;
         }
     }
 
     return NULL;
 }
 
 GList *
 find_actions(GList *input, const char *key, const pcmk_node_t *on_node)
 {
     GList *gIter = input;
     GList *result = NULL;
 
     CRM_CHECK(key != NULL, return NULL);
 
     for (; gIter != NULL; gIter = gIter->next) {
         pcmk_action_t *action = (pcmk_action_t *) gIter->data;
 
         if (!pcmk__str_eq(key, action->uuid, pcmk__str_casei)) {
             continue;
 
         } else if (on_node == NULL) {
             crm_trace("Action %s matches (ignoring node)", key);
             result = g_list_prepend(result, action);
 
         } else if (action->node == NULL) {
             crm_trace("Action %s matches (unallocated, assigning to %s)",
                       key, pcmk__node_name(on_node));
 
             action->node = pe__copy_node(on_node);
             result = g_list_prepend(result, action);
 
         } else if (pcmk__same_node(on_node, action->node)) {
             crm_trace("Action %s on %s matches", key, pcmk__node_name(on_node));
             result = g_list_prepend(result, action);
         }
     }
 
     return result;
 }
 
 GList *
 find_actions_exact(GList *input, const char *key, const pcmk_node_t *on_node)
 {
     GList *result = NULL;
 
     CRM_CHECK(key != NULL, return NULL);
 
     if (on_node == NULL) {
         return NULL;
     }
 
     for (GList *gIter = input; gIter != NULL; gIter = gIter->next) {
         pcmk_action_t *action = (pcmk_action_t *) gIter->data;
 
         if ((action->node != NULL)
             && pcmk__str_eq(key, action->uuid, pcmk__str_casei)
             && pcmk__str_eq(on_node->details->id, action->node->details->id,
                             pcmk__str_casei)) {
 
             crm_trace("Action %s on %s matches", key, pcmk__node_name(on_node));
             result = g_list_prepend(result, action);
         }
     }
 
     return result;
 }
 
 /*!
  * \brief Find all actions of given type for a resource
  *
  * \param[in] rsc           Resource to search
  * \param[in] node          Find only actions scheduled on this node
  * \param[in] task          Action name to search for
  * \param[in] require_node  If TRUE, NULL node or action node will not match
  *
  * \return List of actions found (or NULL if none)
  * \note If node is not NULL and require_node is FALSE, matching actions
  *       without a node will be assigned to node.
  */
 GList *
 pe__resource_actions(const pcmk_resource_t *rsc, const pcmk_node_t *node,
                      const char *task, bool require_node)
 {
     GList *result = NULL;
     char *key = pcmk__op_key(rsc->id, task, 0);
 
     if (require_node) {
         result = find_actions_exact(rsc->actions, key, node);
     } else {
         result = find_actions(rsc->actions, key, node);
     }
     free(key);
     return result;
 }
 
 /*!
  * \internal
  * \brief Create an action reason string based on the action itself
  *
  * \param[in] action  Action to create reason string for
  * \param[in] flag    Action flag that was cleared
  *
  * \return Newly allocated string suitable for use as action reason
  * \note It is the caller's responsibility to free() the result.
  */
 char *
 pe__action2reason(const pcmk_action_t *action, enum pe_action_flags flag)
 {
     const char *change = NULL;
 
     switch (flag) {
         case pcmk_action_runnable:
             change = "unrunnable";
             break;
         case pcmk_action_migratable:
             change = "unmigrateable";
             break;
         case pcmk_action_optional:
             change = "required";
             break;
         default:
             // Bug: caller passed unsupported flag
             CRM_CHECK(change != NULL, change = "");
             break;
     }
     return crm_strdup_printf("%s%s%s %s", change,
                              (action->rsc == NULL)? "" : " ",
                              (action->rsc == NULL)? "" : action->rsc->id,
                              action->task);
 }
 
 void pe_action_set_reason(pcmk_action_t *action, const char *reason,
                           bool overwrite)
 {
     if (action->reason != NULL && overwrite) {
         pcmk__rsc_trace(action->rsc, "Changing %s reason from '%s' to '%s'",
                         action->uuid, action->reason,
                         pcmk__s(reason, "(none)"));
     } else if (action->reason == NULL) {
         pcmk__rsc_trace(action->rsc, "Set %s reason to '%s'",
                         action->uuid, pcmk__s(reason, "(none)"));
     } else {
         // crm_assert(action->reason != NULL && !overwrite);
         return;
     }
 
     pcmk__str_update(&action->reason, reason);
 }
 
 /*!
  * \internal
  * \brief Create an action to clear a resource's history from CIB
  *
  * \param[in,out] rsc       Resource to clear
  * \param[in]     node      Node to clear history on
  */
 void
 pe__clear_resource_history(pcmk_resource_t *rsc, const pcmk_node_t *node)
 {
     CRM_ASSERT((rsc != NULL) && (node != NULL));
 
     custom_action(rsc, pcmk__op_key(rsc->id, PCMK_ACTION_LRM_DELETE, 0),
                   PCMK_ACTION_LRM_DELETE, node, FALSE, rsc->cluster);
 }
 
 #define sort_return(an_int, why) do {					\
 	free(a_uuid);						\
 	free(b_uuid);						\
 	crm_trace("%s (%d) %c %s (%d) : %s",				\
 		  a_xml_id, a_call_id, an_int>0?'>':an_int<0?'<':'=',	\
 		  b_xml_id, b_call_id, why);				\
 	return an_int;							\
     } while(0)
 
 int
 pe__is_newer_op(const xmlNode *xml_a, const xmlNode *xml_b,
                 bool same_node_default)
 {
     int a_call_id = -1;
     int b_call_id = -1;
 
     char *a_uuid = NULL;
     char *b_uuid = NULL;
 
     const char *a_xml_id = crm_element_value(xml_a, PCMK_XA_ID);
     const char *b_xml_id = crm_element_value(xml_b, PCMK_XA_ID);
 
     const char *a_node = crm_element_value(xml_a, PCMK__META_ON_NODE);
     const char *b_node = crm_element_value(xml_b, PCMK__META_ON_NODE);
     bool same_node = true;
 
     /* @COMPAT The on_node attribute was added to last_failure as of 1.1.13 (via
      * 8b3ca1c) and the other entries as of 1.1.12 (via 0b07b5c).
      *
      * In case that any of the PCMK__XE_LRM_RSC_OP entries doesn't have on_node
      * attribute, we need to explicitly tell whether the two operations are on
      * the same node.
      */
     if (a_node == NULL || b_node == NULL) {
         same_node = same_node_default;
 
     } else {
         same_node = pcmk__str_eq(a_node, b_node, pcmk__str_casei);
     }
 
     if (same_node && pcmk__str_eq(a_xml_id, b_xml_id, pcmk__str_none)) {
         /* We have duplicate PCMK__XE_LRM_RSC_OP entries in the status
          * section which is unlikely to be a good thing
          *    - we can handle it easily enough, but we need to get
          *    to the bottom of why it's happening.
          */
         pcmk__config_err("Duplicate " PCMK__XE_LRM_RSC_OP " entries named %s",
                          a_xml_id);
         sort_return(0, "duplicate");
     }
 
     crm_element_value_int(xml_a, PCMK__XA_CALL_ID, &a_call_id);
     crm_element_value_int(xml_b, PCMK__XA_CALL_ID, &b_call_id);
 
     if (a_call_id == -1 && b_call_id == -1) {
         /* both are pending ops so it doesn't matter since
          *   stops are never pending
          */
         sort_return(0, "pending");
 
     } else if (same_node && a_call_id >= 0 && a_call_id < b_call_id) {
         sort_return(-1, "call id");
 
     } else if (same_node && b_call_id >= 0 && a_call_id > b_call_id) {
         sort_return(1, "call id");
 
     } else if (a_call_id >= 0 && b_call_id >= 0
                && (!same_node || a_call_id == b_call_id)) {
         /* The op and last_failed_op are the same. Order on
          * PCMK_XA_LAST_RC_CHANGE.
          */
         time_t last_a = -1;
         time_t last_b = -1;
 
         crm_element_value_epoch(xml_a, PCMK_XA_LAST_RC_CHANGE, &last_a);
         crm_element_value_epoch(xml_b, PCMK_XA_LAST_RC_CHANGE, &last_b);
 
         crm_trace("rc-change: %lld vs %lld",
                   (long long) last_a, (long long) last_b);
         if (last_a >= 0 && last_a < last_b) {
             sort_return(-1, "rc-change");
 
         } else if (last_b >= 0 && last_a > last_b) {
             sort_return(1, "rc-change");
         }
         sort_return(0, "rc-change");
 
     } else {
         /* One of the inputs is a pending operation.
          * Attempt to use PCMK__XA_TRANSITION_MAGIC to determine its age relative
          * to the other.
          */
 
         int a_id = -1;
         int b_id = -1;
 
         const char *a_magic = crm_element_value(xml_a,
                                                 PCMK__XA_TRANSITION_MAGIC);
         const char *b_magic = crm_element_value(xml_b,
                                                 PCMK__XA_TRANSITION_MAGIC);
 
         CRM_CHECK(a_magic != NULL && b_magic != NULL, sort_return(0, "No magic"));
         if (!decode_transition_magic(a_magic, &a_uuid, &a_id, NULL, NULL, NULL,
                                      NULL)) {
             sort_return(0, "bad magic a");
         }
         if (!decode_transition_magic(b_magic, &b_uuid, &b_id, NULL, NULL, NULL,
                                      NULL)) {
             sort_return(0, "bad magic b");
         }
         /* try to determine the relative age of the operation...
          * some pending operations (e.g. a start) may have been superseded
          *   by a subsequent stop
          *
          * [a|b]_id == -1 means it's a shutdown operation and _always_ comes last
          */
         if (!pcmk__str_eq(a_uuid, b_uuid, pcmk__str_casei) || a_id == b_id) {
             /*
              * some of the logic in here may be redundant...
              *
              * if the UUID from the TE doesn't match then one better
              *   be a pending operation.
              * pending operations don't survive between elections and joins
              *   because we query the LRM directly
              */
 
             if (b_call_id == -1) {
                 sort_return(-1, "transition + call");
 
             } else if (a_call_id == -1) {
                 sort_return(1, "transition + call");
             }
 
         } else if ((a_id >= 0 && a_id < b_id) || b_id == -1) {
             sort_return(-1, "transition");
 
         } else if ((b_id >= 0 && a_id > b_id) || a_id == -1) {
             sort_return(1, "transition");
         }
     }
 
     /* we should never end up here */
     CRM_CHECK(FALSE, sort_return(0, "default"));
 }
 
 gint
 sort_op_by_callid(gconstpointer a, gconstpointer b)
 {
     const xmlNode *xml_a = a;
     const xmlNode *xml_b = b;
 
     return pe__is_newer_op(xml_a, xml_b, true);
 }
 
 /*!
  * \internal
  * \brief Create a new pseudo-action for a resource
  *
  * \param[in,out] rsc       Resource to create action for
  * \param[in]     task      Action name
  * \param[in]     optional  Whether action should be considered optional
  * \param[in]     runnable  Whethe action should be considered runnable
  *
  * \return New action object corresponding to arguments
  */
 pcmk_action_t *
 pe__new_rsc_pseudo_action(pcmk_resource_t *rsc, const char *task, bool optional,
                           bool runnable)
 {
     pcmk_action_t *action = NULL;
 
     CRM_ASSERT((rsc != NULL) && (task != NULL));
 
     action = custom_action(rsc, pcmk__op_key(rsc->id, task, 0), task, NULL,
                            optional, rsc->cluster);
     pcmk__set_action_flags(action, pcmk_action_pseudo);
     if (runnable) {
         pcmk__set_action_flags(action, pcmk_action_runnable);
     }
     return action;
 }
 
 /*!
  * \internal
  * \brief Add the expected result to an action
  *
  * \param[in,out] action           Action to add expected result to
  * \param[in]     expected_result  Expected result to add
  *
  * \note This is more efficient than calling pcmk__insert_meta().
  */
 void
 pe__add_action_expected_result(pcmk_action_t *action, int expected_result)
 {
     CRM_ASSERT((action != NULL) && (action->meta != NULL));
 
     g_hash_table_insert(action->meta, pcmk__str_copy(PCMK__META_OP_TARGET_RC),
                         pcmk__itoa(expected_result));
 }