diff --git a/include/crm/pengine/internal.h b/include/crm/pengine/internal.h
index 2d6729d09b..e1ac5309f2 100644
--- a/include/crm/pengine/internal.h
+++ b/include/crm/pengine/internal.h
@@ -1,694 +1,699 @@
 /*
  * Copyright 2004-2023 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 PE_INTERNAL__H
 #  define PE_INTERNAL__H
 
 #  include <stdbool.h>
 #  include <stdint.h>
 #  include <string.h>
 #  include <crm/msg_xml.h>
 #  include <crm/pengine/status.h>
 #  include <crm/pengine/remote_internal.h>
 #  include <crm/common/internal.h>
 #  include <crm/common/options_internal.h>
 #  include <crm/common/output_internal.h>
 #  include <crm/common/scheduler_internal.h>
 
 const char *pe__resource_description(const pcmk_resource_t *rsc,
                                      uint32_t show_opts);
 
 bool pe__clone_is_ordered(const pcmk_resource_t *clone);
 int pe__set_clone_flag(pcmk_resource_t *clone, enum pcmk__clone_flags flag);
 bool pe__clone_flag_is_set(const pcmk_resource_t *clone, uint32_t flags);
 
 bool pe__group_flag_is_set(const pcmk_resource_t *group, uint32_t flags);
 pcmk_resource_t *pe__last_group_member(const pcmk_resource_t *group);
 
 
 #  define pe_rsc_info(rsc, fmt, args...)  crm_log_tag(LOG_INFO,  rsc ? rsc->id : "<NULL>", fmt, ##args)
 #  define pe_rsc_debug(rsc, fmt, args...) crm_log_tag(LOG_DEBUG, rsc ? rsc->id : "<NULL>", fmt, ##args)
 #  define pe_rsc_trace(rsc, fmt, args...) crm_log_tag(LOG_TRACE, rsc ? rsc->id : "<NULL>", fmt, ##args)
 
 #  define pe_err(fmt...) do {           \
         was_processing_error = TRUE;    \
         pcmk__config_err(fmt);          \
     } while (0)
 
 #  define pe_warn(fmt...) do {          \
         was_processing_warning = TRUE;  \
         pcmk__config_warn(fmt);         \
     } while (0)
 
 #  define pe_proc_err(fmt...) { was_processing_error = TRUE; crm_err(fmt); }
 #  define pe_proc_warn(fmt...) { was_processing_warning = TRUE; crm_warn(fmt); }
 
 #define pe__set_working_set_flags(scheduler, flags_to_set) do {             \
         (scheduler)->flags = pcmk__set_flags_as(__func__, __LINE__,         \
             LOG_TRACE, "Scheduler", crm_system_name,                        \
             (scheduler)->flags, (flags_to_set), #flags_to_set);             \
     } while (0)
 
 #define pe__clear_working_set_flags(scheduler, flags_to_clear) do {         \
         (scheduler)->flags = pcmk__clear_flags_as(__func__, __LINE__,       \
             LOG_TRACE, "Scheduler", crm_system_name,                        \
             (scheduler)->flags, (flags_to_clear), #flags_to_clear);         \
     } while (0)
 
 #define pe__set_resource_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)
 
 #define pe__clear_resource_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)
 
 #define pe__set_action_flags(action, flags_to_set) do {                     \
         (action)->flags = pcmk__set_flags_as(__func__, __LINE__,            \
                                              LOG_TRACE,                     \
                                              "Action", (action)->uuid,      \
                                              (action)->flags,               \
                                              (flags_to_set),                \
                                              #flags_to_set);                \
     } while (0)
 
 #define pe__clear_action_flags(action, flags_to_clear) do {                 \
         (action)->flags = pcmk__clear_flags_as(__func__, __LINE__,          \
                                                LOG_TRACE,                   \
                                                "Action", (action)->uuid,    \
                                                (action)->flags,             \
                                                (flags_to_clear),            \
                                                #flags_to_clear);            \
     } while (0)
 
 #define pe__set_raw_action_flags(action_flags, action_name, flags_to_set) do { \
         action_flags = pcmk__set_flags_as(__func__, __LINE__,               \
                                           LOG_TRACE, "Action", action_name, \
                                           (action_flags),                   \
                                           (flags_to_set), #flags_to_set);   \
     } while (0)
 
 #define pe__clear_raw_action_flags(action_flags, action_name, flags_to_clear) do { \
         action_flags = pcmk__clear_flags_as(__func__, __LINE__,             \
                                             LOG_TRACE,                      \
                                             "Action", action_name,          \
                                             (action_flags),                 \
                                             (flags_to_clear),               \
                                             #flags_to_clear);               \
     } while (0)
 
 #define pe__set_action_flags_as(function, line, action, flags_to_set) do {  \
         (action)->flags = pcmk__set_flags_as((function), (line),            \
                                              LOG_TRACE,                     \
                                              "Action", (action)->uuid,      \
                                              (action)->flags,               \
                                              (flags_to_set),                \
                                              #flags_to_set);                \
     } while (0)
 
 #define pe__clear_action_flags_as(function, line, action, flags_to_clear) do { \
         (action)->flags = pcmk__clear_flags_as((function), (line),          \
                                                LOG_TRACE,                   \
                                                "Action", (action)->uuid,    \
                                                (action)->flags,             \
                                                (flags_to_clear),            \
                                                #flags_to_clear);            \
     } while (0)
 
 #define pe__set_order_flags(order_flags, flags_to_set) do {                 \
         order_flags = pcmk__set_flags_as(__func__, __LINE__, LOG_TRACE,     \
                                          "Ordering", "constraint",          \
                                          order_flags, (flags_to_set),       \
                                          #flags_to_set);                    \
     } while (0)
 
 #define pe__clear_order_flags(order_flags, flags_to_clear) do {               \
         order_flags = pcmk__clear_flags_as(__func__, __LINE__, LOG_TRACE,     \
                                            "Ordering", "constraint",          \
                                            order_flags, (flags_to_clear),     \
                                            #flags_to_clear);                  \
     } while (0)
 
 #define pe_warn_once(pe_wo_bit, fmt...) do {    \
         if (!pcmk_is_set(pcmk__warnings, pe_wo_bit)) {  \
             if (pe_wo_bit == pcmk__wo_blind) {  \
                 crm_warn(fmt);                  \
             } else {                            \
                 pe_warn(fmt);                   \
             }                                   \
             pcmk__warnings = pcmk__set_flags_as(__func__, __LINE__,         \
                                                LOG_TRACE,                   \
                                                "Warn-once", "logging",      \
                                                pcmk__warnings,              \
                                                (pe_wo_bit), #pe_wo_bit);    \
         }                                                                   \
     } while (0);
 
 
 typedef struct pe__location_constraint_s {
     char *id;                           // Constraint XML ID
     pcmk_resource_t *rsc_lh;            // Resource being located
     enum rsc_role_e role_filter;        // Role to locate
     enum pe_discover_e discover_mode;   // Resource discovery
     GList *node_list_rh;                // List of pcmk_node_t*
 } pe__location_t;
 
 typedef struct pe__order_constraint_s {
     int id;
     uint32_t flags; // Group of enum pcmk__action_relation_flags
 
     void *lh_opaque;
     pcmk_resource_t *lh_rsc;
     pcmk_action_t *lh_action;
     char *lh_action_task;
 
     void *rh_opaque;
     pcmk_resource_t *rh_rsc;
     pcmk_action_t *rh_action;
     char *rh_action_task;
 } pe__ordering_t;
 
 const pcmk_resource_t *pe__const_top_resource(const pcmk_resource_t *rsc,
                                               bool include_bundle);
 
 int pe__clone_max(const pcmk_resource_t *clone);
 int pe__clone_node_max(const pcmk_resource_t *clone);
 int pe__clone_promoted_max(const pcmk_resource_t *clone);
 int pe__clone_promoted_node_max(const pcmk_resource_t *clone);
 void pe__create_clone_notifications(pcmk_resource_t *clone);
 void pe__free_clone_notification_data(pcmk_resource_t *clone);
 void pe__create_clone_notif_pseudo_ops(pcmk_resource_t *clone,
                                        pcmk_action_t *start,
                                        pcmk_action_t *started,
                                        pcmk_action_t *stop,
                                        pcmk_action_t *stopped);
 
 pcmk_action_t *pe__new_rsc_pseudo_action(pcmk_resource_t *rsc, const char *task,
                                          bool optional, bool runnable);
 
 void pe__create_promotable_pseudo_ops(pcmk_resource_t *clone,
                                       bool any_promoting, bool any_demoting);
 
 bool pe_can_fence(const pcmk_scheduler_t *scheduler, const pcmk_node_t *node);
 
 void add_hash_param(GHashTable * hash, const char *name, const char *value);
 
 char *native_parameter(pcmk_resource_t *rsc, pcmk_node_t *node, gboolean create,
                        const char *name, pcmk_scheduler_t *scheduler);
 pcmk_node_t *native_location(const pcmk_resource_t *rsc, GList **list,
                              int current);
 
 void pe_metadata(pcmk__output_t *out);
 void verify_pe_options(GHashTable * options);
 
 void native_add_running(pcmk_resource_t *rsc, pcmk_node_t *node,
                         pcmk_scheduler_t *scheduler, gboolean failed);
 
 gboolean native_unpack(pcmk_resource_t *rsc, pcmk_scheduler_t *scheduler);
 gboolean group_unpack(pcmk_resource_t *rsc, pcmk_scheduler_t *scheduler);
 gboolean clone_unpack(pcmk_resource_t *rsc, pcmk_scheduler_t *scheduler);
 gboolean pe__unpack_bundle(pcmk_resource_t *rsc, pcmk_scheduler_t *scheduler);
 
 pcmk_resource_t *native_find_rsc(pcmk_resource_t *rsc, const char *id,
                                  const pcmk_node_t *node, int flags);
 
 gboolean native_active(pcmk_resource_t *rsc, gboolean all);
 gboolean group_active(pcmk_resource_t *rsc, gboolean all);
 gboolean clone_active(pcmk_resource_t *rsc, gboolean all);
 gboolean pe__bundle_active(pcmk_resource_t *rsc, gboolean all);
 
 //! \deprecated This function will be removed in a future release
 void native_print(pcmk_resource_t *rsc, const char *pre_text, long options,
                   void *print_data);
 
 //! \deprecated This function will be removed in a future release
 void group_print(pcmk_resource_t *rsc, const char *pre_text, long options,
                  void *print_data);
 
 //! \deprecated This function will be removed in a future release
 void clone_print(pcmk_resource_t *rsc, const char *pre_text, long options,
                  void *print_data);
 
 //! \deprecated This function will be removed in a future release
 void pe__print_bundle(pcmk_resource_t *rsc, const char *pre_text, long options,
                       void *print_data);
 
 gchar *pcmk__native_output_string(const pcmk_resource_t *rsc, const char *name,
                                   const pcmk_node_t *node, uint32_t show_opts,
                                   const char *target_role, bool show_nodes);
 
 int pe__name_and_nvpairs_xml(pcmk__output_t *out, bool is_list, const char *tag_name
                          , size_t pairs_count, ...);
 char *pe__node_display_name(pcmk_node_t *node, bool print_detail);
 
 
 // Clone notifications (pe_notif.c)
 void pe__order_notifs_after_fencing(const pcmk_action_t *action,
                                     pcmk_resource_t *rsc,
                                     pcmk_action_t *stonith_op);
 
 
 static inline const char *
 pe__rsc_bool_str(const pcmk_resource_t *rsc, uint64_t rsc_flag)
 {
     return pcmk__btoa(pcmk_is_set(rsc->flags, rsc_flag));
 }
 
 int pe__clone_xml(pcmk__output_t *out, va_list args);
 int pe__clone_default(pcmk__output_t *out, va_list args);
 int pe__group_xml(pcmk__output_t *out, va_list args);
 int pe__group_default(pcmk__output_t *out, va_list args);
 int pe__bundle_xml(pcmk__output_t *out, va_list args);
 int pe__bundle_html(pcmk__output_t *out, va_list args);
 int pe__bundle_text(pcmk__output_t *out, va_list args);
 int pe__node_html(pcmk__output_t *out, va_list args);
 int pe__node_text(pcmk__output_t *out, va_list args);
 int pe__node_xml(pcmk__output_t *out, va_list args);
 int pe__resource_xml(pcmk__output_t *out, va_list args);
 int pe__resource_html(pcmk__output_t *out, va_list args);
 int pe__resource_text(pcmk__output_t *out, va_list args);
 
 void native_free(pcmk_resource_t *rsc);
 void group_free(pcmk_resource_t *rsc);
 void clone_free(pcmk_resource_t *rsc);
 void pe__free_bundle(pcmk_resource_t *rsc);
 
 enum rsc_role_e native_resource_state(const pcmk_resource_t *rsc,
                                       gboolean current);
 enum rsc_role_e group_resource_state(const pcmk_resource_t *rsc,
                                      gboolean current);
 enum rsc_role_e clone_resource_state(const pcmk_resource_t *rsc,
                                      gboolean current);
 enum rsc_role_e pe__bundle_resource_state(const pcmk_resource_t *rsc,
                                           gboolean current);
 
 void pe__count_common(pcmk_resource_t *rsc);
 void pe__count_bundle(pcmk_resource_t *rsc);
 
 void common_free(pcmk_resource_t *rsc);
 
 pcmk_node_t *pe__copy_node(const pcmk_node_t *this_node);
 time_t get_effective_time(pcmk_scheduler_t *scheduler);
 
 /* Failure handling utilities (from failcounts.c) */
 
 int pe_get_failcount(const pcmk_node_t *node, pcmk_resource_t *rsc,
                      time_t *last_failure, uint32_t flags,
                      const xmlNode *xml_op);
 
 pcmk_action_t *pe__clear_failcount(pcmk_resource_t *rsc,
                                    const pcmk_node_t *node, const char *reason,
                                    pcmk_scheduler_t *scheduler);
 
 /* Functions for finding/counting a resource's active nodes */
 
 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);
 
 pcmk_node_t *pe__find_active_requires(const pcmk_resource_t *rsc,
                                     unsigned int *count);
 
 static inline pcmk_node_t *
 pe__current_node(const pcmk_resource_t *rsc)
 {
     return (rsc == NULL)? NULL : rsc->fns->active_node(rsc, NULL, NULL);
 }
 
 
 /* Binary like operators for lists of nodes */
 GHashTable *pe__node_list2table(const GList *list);
 
 pcmk_action_t *get_pseudo_op(const char *name, pcmk_scheduler_t *scheduler);
 gboolean order_actions(pcmk_action_t *lh_action, pcmk_action_t *rh_action,
                        uint32_t flags);
 
 void pe__show_node_scores_as(const char *file, const char *function,
                              int line, bool to_log, const pcmk_resource_t *rsc,
                              const char *comment, GHashTable *nodes,
                              pcmk_scheduler_t *scheduler);
 
 #define pe__show_node_scores(level, rsc, text, nodes, scheduler)    \
         pe__show_node_scores_as(__FILE__, __func__, __LINE__,      \
                                 (level), (rsc), (text), (nodes), (scheduler))
 
 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 *pcmk__unpack_action_rsc_params(const xmlNode *action_xml,
                                            GHashTable *node_attrs,
                                            pcmk_scheduler_t *data_set);
 xmlNode *pcmk__find_action_config(const pcmk_resource_t *rsc, const char *key,
                                   bool include_disabled);
 
 enum rsc_start_requirement pcmk__action_requires(const pcmk_resource_t *rsc,
                                                  const char *action_name);
 
 enum action_fail_response pcmk__parse_on_fail(const pcmk_resource_t *rsc,
                                               const char *action_name,
                                               guint interval_ms,
                                               const char *value);
 
+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);
+
 pcmk_action_t *custom_action(pcmk_resource_t *rsc, char *key, const char *task,
                              const pcmk_node_t *on_node, gboolean optional,
                              gboolean foo, pcmk_scheduler_t *scheduler);
 
 #  define delete_key(rsc) pcmk__op_key(rsc->id, PCMK_ACTION_DELETE, 0)
 #  define delete_action(rsc, node, optional) custom_action(		\
 		rsc, delete_key(rsc), PCMK_ACTION_DELETE, node, \
 		optional, TRUE, rsc->cluster);
 
 #  define stop_key(rsc) pcmk__op_key(rsc->id, PCMK_ACTION_STOP, 0)
 #  define stop_action(rsc, node, optional) custom_action(			\
 		rsc, stop_key(rsc), PCMK_ACTION_STOP, node,		\
 		optional, TRUE, rsc->cluster);
 
 #  define reload_key(rsc) pcmk__op_key(rsc->id, PCMK_ACTION_RELOAD_AGENT, 0)
 #  define start_key(rsc) pcmk__op_key(rsc->id, PCMK_ACTION_START, 0)
 #  define start_action(rsc, node, optional) custom_action(		\
 		rsc, start_key(rsc), PCMK_ACTION_START, node,           \
 		optional, TRUE, rsc->cluster)
 
 #  define promote_key(rsc) pcmk__op_key(rsc->id, PCMK_ACTION_PROMOTE, 0)
 #  define promote_action(rsc, node, optional) custom_action(		\
 		rsc, promote_key(rsc), PCMK_ACTION_PROMOTE, node,	\
 		optional, TRUE, rsc->cluster)
 
 #  define demote_key(rsc) pcmk__op_key(rsc->id, PCMK_ACTION_DEMOTE, 0)
 #  define demote_action(rsc, node, optional) custom_action(		\
 		rsc, demote_key(rsc), PCMK_ACTION_DEMOTE, node, \
 		optional, TRUE, rsc->cluster)
 
 extern int pe_get_configured_timeout(pcmk_resource_t *rsc, const char *action,
                                      pcmk_scheduler_t *scheduler);
 
 pcmk_action_t *find_first_action(const GList *input, const char *uuid,
                                  const char *task, const pcmk_node_t *on_node);
 
 enum action_tasks get_complex_task(const pcmk_resource_t *rsc,
                                    const char *name);
 
 GList *find_actions(GList *input, const char *key, const pcmk_node_t *on_node);
 GList *find_actions_exact(GList *input, const char *key,
                           const pcmk_node_t *on_node);
 GList *pe__resource_actions(const pcmk_resource_t *rsc, const pcmk_node_t *node,
                             const char *task, bool require_node);
 
 extern void pe_free_action(pcmk_action_t *action);
 
 void resource_location(pcmk_resource_t *rsc, const pcmk_node_t *node, int score,
                        const char *tag, pcmk_scheduler_t *scheduler);
 
 extern int pe__is_newer_op(const xmlNode *xml_a, const xmlNode *xml_b,
                            bool same_node_default);
 extern gint sort_op_by_callid(gconstpointer a, gconstpointer b);
 gboolean get_target_role(const pcmk_resource_t *rsc, enum rsc_role_e *role);
 void pe__set_next_role(pcmk_resource_t *rsc, enum rsc_role_e role,
                        const char *why);
 
 pcmk_resource_t *find_clone_instance(const pcmk_resource_t *rsc,
                                      const char *sub_id);
 
 extern void destroy_ticket(gpointer data);
 pe_ticket_t *ticket_new(const char *ticket_id, pcmk_scheduler_t *scheduler);
 
 // Resources for manipulating resource names
 const char *pe_base_name_end(const char *id);
 char *clone_strip(const char *last_rsc_id);
 char *clone_zero(const char *last_rsc_id);
 
 static inline bool
 pe_base_name_eq(const pcmk_resource_t *rsc, const char *id)
 {
     if (id && rsc && rsc->id) {
         // Number of characters in rsc->id before any clone suffix
         size_t base_len = pe_base_name_end(rsc->id) - rsc->id + 1;
 
         return (strlen(id) == base_len) && !strncmp(id, rsc->id, base_len);
     }
     return false;
 }
 
 int pe__target_rc_from_xml(const xmlNode *xml_op);
 
 gint pe__cmp_node_name(gconstpointer a, gconstpointer b);
 bool is_set_recursive(const pcmk_resource_t *rsc, long long flag, bool any);
 
 typedef struct op_digest_cache_s {
     enum pcmk__digest_result rc;
     xmlNode *params_all;
     xmlNode *params_secure;
     xmlNode *params_restart;
     char *digest_all_calc;
     char *digest_secure_calc;
     char *digest_restart_calc;
 } op_digest_cache_t;
 
 op_digest_cache_t *pe__calculate_digests(pcmk_resource_t *rsc, const char *task,
                                          guint *interval_ms,
                                          const pcmk_node_t *node,
                                          const xmlNode *xml_op,
                                          GHashTable *overrides,
                                          bool calc_secure,
                                          pcmk_scheduler_t *scheduler);
 
 void pe__free_digests(gpointer ptr);
 
 op_digest_cache_t *rsc_action_digest_cmp(pcmk_resource_t *rsc,
                                          const xmlNode *xml_op,
                                          pcmk_node_t *node,
                                          pcmk_scheduler_t *scheduler);
 
 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);
 void trigger_unfencing(pcmk_resource_t *rsc, pcmk_node_t *node,
                        const char *reason, pcmk_action_t *dependency,
                        pcmk_scheduler_t *scheduler);
 
 char *pe__action2reason(const pcmk_action_t *action, enum pe_action_flags flag);
 void pe_action_set_reason(pcmk_action_t *action, const char *reason,
                           bool overwrite);
 void pe__add_action_expected_result(pcmk_action_t *action, int expected_result);
 
 void pe__set_resource_flags_recursive(pcmk_resource_t *rsc, uint64_t flags);
 void pe__clear_resource_flags_recursive(pcmk_resource_t *rsc, uint64_t flags);
 void pe__clear_resource_flags_on_all(pcmk_scheduler_t *scheduler,
                                      uint64_t flag);
 
 gboolean add_tag_ref(GHashTable * tags, const char * tag_name,  const char * obj_ref);
 
 //! \deprecated This function will be removed in a future release
 void print_rscs_brief(GList *rsc_list, const char * pre_text, long options,
                       void * print_data, gboolean print_all);
 int pe__rscs_brief_output(pcmk__output_t *out, GList *rsc_list, unsigned int options);
 void pe_fence_node(pcmk_scheduler_t *scheduler, pcmk_node_t *node,
                    const char *reason, bool priority_delay);
 
 pcmk_node_t *pe_create_node(const char *id, const char *uname, const char *type,
                             const char *score, pcmk_scheduler_t *scheduler);
 
 //! \deprecated This function will be removed in a future release
 void common_print(pcmk_resource_t *rsc, const char *pre_text, const char *name,
                   const pcmk_node_t *node, long options, void *print_data);
 int pe__common_output_text(pcmk__output_t *out, const pcmk_resource_t *rsc,
                            const char *name, const pcmk_node_t *node,
                            unsigned int options);
 int pe__common_output_html(pcmk__output_t *out, const pcmk_resource_t *rsc,
                            const char *name, const pcmk_node_t *node,
                            unsigned int options);
 
 //! A single instance of a bundle
 typedef struct {
     int offset;                 //!< 0-origin index of this instance in bundle
     char *ipaddr;               //!< IP address associated with this instance
     pcmk_node_t *node;          //!< Node created for this instance
     pcmk_resource_t *ip;        //!< IP address resource for ipaddr
     pcmk_resource_t *child;     //!< Instance of bundled resource
     pcmk_resource_t *container; //!< Container associated with this instance
     pcmk_resource_t *remote;    //!< Pacemaker Remote connection into container
 } pe__bundle_replica_t;
 
 GList *pe__bundle_containers(const pcmk_resource_t *bundle);
 
 int pe__bundle_max(const pcmk_resource_t *rsc);
 bool pe__node_is_bundle_instance(const pcmk_resource_t *bundle,
                                  const pcmk_node_t *node);
 pcmk_resource_t *pe__bundled_resource(const pcmk_resource_t *rsc);
 const pcmk_resource_t *pe__get_rsc_in_container(const pcmk_resource_t *instance);
 pcmk_resource_t *pe__first_container(const pcmk_resource_t *bundle);
 void pe__foreach_bundle_replica(pcmk_resource_t *bundle,
                                 bool (*fn)(pe__bundle_replica_t *, void *),
                                 void *user_data);
 void pe__foreach_const_bundle_replica(const pcmk_resource_t *bundle,
                                       bool (*fn)(const pe__bundle_replica_t *,
                                                  void *),
                                       void *user_data);
 pcmk_resource_t *pe__find_bundle_replica(const pcmk_resource_t *bundle,
                                          const pcmk_node_t *node);
 bool pe__bundle_needs_remote_name(pcmk_resource_t *rsc);
 const char *pe__add_bundle_remote_name(pcmk_resource_t *rsc,
                                        pcmk_scheduler_t *scheduler,
                                        xmlNode *xml, const char *field);
 
 const char *pe__node_attribute_calculated(const pcmk_node_t *node,
                                           const char *name,
                                           const pcmk_resource_t *rsc,
                                           enum pcmk__rsc_node node_type,
                                           bool force_host);
 const char *pe_node_attribute_raw(const pcmk_node_t *node, const char *name);
 bool pe__is_universal_clone(const pcmk_resource_t *rsc,
                             const pcmk_scheduler_t *scheduler);
 void pe__add_param_check(const xmlNode *rsc_op, pcmk_resource_t *rsc,
                          pcmk_node_t *node, enum pcmk__check_parameters,
                          pcmk_scheduler_t *scheduler);
 void pe__foreach_param_check(pcmk_scheduler_t *scheduler,
                              void (*cb)(pcmk_resource_t*, pcmk_node_t*,
                                         const xmlNode*,
                                         enum pcmk__check_parameters));
 void pe__free_param_checks(pcmk_scheduler_t *scheduler);
 
 bool pe__shutdown_requested(const pcmk_node_t *node);
 void pe__update_recheck_time(time_t recheck, pcmk_scheduler_t *scheduler);
 
 /*!
  * \internal
  * \brief Register xml formatting message functions.
  *
  * \param[in,out] out  Output object to register messages with
  */
 void pe__register_messages(pcmk__output_t *out);
 
 void pe__unpack_dataset_nvpairs(const xmlNode *xml_obj, const char *set_name,
                                 const pe_rule_eval_data_t *rule_data,
                                 GHashTable *hash, const char *always_first,
                                 gboolean overwrite,
                                 pcmk_scheduler_t *scheduler);
 
 bool pe__resource_is_disabled(const pcmk_resource_t *rsc);
 void pe__clear_resource_history(pcmk_resource_t *rsc, const pcmk_node_t *node);
 
 GList *pe__rscs_with_tag(pcmk_scheduler_t *scheduler, const char *tag_name);
 GList *pe__unames_with_tag(pcmk_scheduler_t *scheduler, const char *tag_name);
 bool pe__rsc_has_tag(pcmk_scheduler_t *scheduler, const char *rsc,
                      const char *tag);
 bool pe__uname_has_tag(pcmk_scheduler_t *scheduler, const char *node,
                        const char *tag);
 
 bool pe__rsc_running_on_only(const pcmk_resource_t *rsc,
                              const pcmk_node_t *node);
 bool pe__rsc_running_on_any(pcmk_resource_t *rsc, GList *node_list);
 GList *pe__filter_rsc_list(GList *rscs, GList *filter);
 GList * pe__build_node_name_list(pcmk_scheduler_t *scheduler, const char *s);
 GList * pe__build_rsc_list(pcmk_scheduler_t *scheduler, const char *s);
 
 bool pcmk__rsc_filtered_by_node(pcmk_resource_t *rsc, GList *only_node);
 
 gboolean pe__bundle_is_filtered(const pcmk_resource_t *rsc, GList *only_rsc,
                                 gboolean check_parent);
 gboolean pe__clone_is_filtered(const pcmk_resource_t *rsc, GList *only_rsc,
                                gboolean check_parent);
 gboolean pe__group_is_filtered(const pcmk_resource_t *rsc, GList *only_rsc,
                                gboolean check_parent);
 gboolean pe__native_is_filtered(const pcmk_resource_t *rsc, GList *only_rsc,
                                 gboolean check_parent);
 
 xmlNode *pe__failed_probe_for_rsc(const pcmk_resource_t *rsc, const char *name);
 
 const char *pe__clone_child_id(const pcmk_resource_t *rsc);
 
 int pe__sum_node_health_scores(const pcmk_node_t *node, int base_health);
 int pe__node_health(pcmk_node_t *node);
 
 static inline enum pcmk__health_strategy
 pe__health_strategy(pcmk_scheduler_t *scheduler)
 {
     return pcmk__parse_health_strategy(pe_pref(scheduler->config_hash,
                                                PCMK__OPT_NODE_HEALTH_STRATEGY));
 }
 
 static inline int
 pe__health_score(const char *option, pcmk_scheduler_t *scheduler)
 {
     return char2score(pe_pref(scheduler->config_hash, option));
 }
 
 /*!
  * \internal
  * \brief Return a string suitable for logging as a node name
  *
  * \param[in] node  Node to return a node name string for
  *
  * \return Node name if available, otherwise node ID if available,
  *         otherwise "unspecified node" if node is NULL or "unidentified node"
  *         if node has neither a name nor ID.
  */
 static inline const char *
 pe__node_name(const pcmk_node_t *node)
 {
     if (node == NULL) {
         return "unspecified node";
 
     } else if (node->details->uname != NULL) {
         return node->details->uname;
 
     } else if (node->details->id != NULL) {
         return node->details->id;
 
     } else {
         return "unidentified node";
     }
 }
 
 /*!
  * \internal
  * \brief Check whether two node objects refer to the same node
  *
  * \param[in] node1  First node object to compare
  * \param[in] node2  Second node object to compare
  *
  * \return true if \p node1 and \p node2 refer to the same node
  */
 static inline bool
 pe__same_node(const pcmk_node_t *node1, const pcmk_node_t *node2)
 {
     return (node1 != NULL) && (node2 != NULL)
            && (node1->details == node2->details);
 }
 
 /*!
  * \internal
  * \brief Get the operation key from an action history entry
  *
  * \param[in] xml  Action history entry
  *
  * \return Entry's operation key
  */
 static inline const char *
 pe__xe_history_key(const xmlNode *xml)
 {
     if (xml == NULL) {
         return NULL;
     } else {
         /* @COMPAT Pacemaker <= 1.1.5 did not add the key, and used the ID
          * instead. Checking for that allows us to process old saved CIBs,
          * including some regression tests.
          */
         const char *key = crm_element_value(xml, XML_LRM_ATTR_TASK_KEY);
 
         return pcmk__str_empty(key)? ID(xml) : key;
     }
 }
 
 #endif
diff --git a/lib/pengine/pe_actions.c b/lib/pengine/pe_actions.c
index 6f345d7e86..8f913ec618 100644
--- a/lib/pengine/pe_actions.c
+++ b/lib/pengine/pe_actions.c
@@ -1,1892 +1,1915 @@
 /*
  * Copyright 2004-2023 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/msg_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 *key,
                          bool include_disabled)
 {
     xmlNode *action_config = NULL;
 
     for (xmlNode *operation = first_named_child(rsc->ops_xml, XML_ATTR_OP);
          operation != NULL; operation = crm_next_same_xml(operation)) {
 
         bool enabled = false;
         guint interval_ms = 0;
         char *match_key = NULL;
         const char *name = crm_element_value(operation, "name");
         const char *interval_spec = crm_element_value(operation,
                                                       XML_LRM_ATTR_INTERVAL);
 
         // @TODO This does not consider rules, defaults, etc.
         if (!include_disabled
             && (pcmk__xe_get_bool_attr(operation, "enabled",
                                        &enabled) == pcmk_rc_ok) && !enabled) {
             continue;
         }
 
         interval_ms = crm_parse_interval_spec(interval_spec);
 
         // Try first with resource ID
         match_key = pcmk__op_key(rsc->id, name, interval_ms);
         if (pcmk__str_eq(key, match_key, pcmk__str_none)) {
             action_config = operation;
         }
         free(match_key);
 
         // Then again with clone_name in case ID has instance number
         if (rsc->clone_name != NULL) {
             match_key = pcmk__op_key(rsc->clone_name, name, interval_ms);
             if (pcmk__str_eq(key, match_key, pcmk__str_none)) {
                 action_config = operation;
             }
             free(match_key);
         }
 
         if (action_config != NULL) {
             break;
         }
     }
 
     return action_config;
 }
 
 /*!
  * \internal
  * \brief Find the XML configuration of a resource action
  *
  * \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
  */
 xmlNode *
 pcmk__find_action_config(const pcmk_resource_t *rsc, const char *key,
                          bool include_disabled)
 {
     char *retry_key = NULL;
     xmlNode *action_config = NULL;
 
     // Try exact key first
     action_config = find_exact_action_config(rsc, key, include_disabled);
     if (action_config != NULL) {
         return action_config;
     }
 
     // For migrate_to and migrate_from actions, retry with "migrate"
     // @TODO This should be either documented or deprecated
     if (pcmk__ends_with(key, "_" PCMK_ACTION_MIGRATE_TO "_0")
         || pcmk__ends_with(key, "_" PCMK_ACTION_MIGRATE_FROM "_0")) {
         retry_key = pcmk__op_key(rsc->id, "migrate", 0);
         action_config = find_exact_action_config(rsc, retry_key,
                                                  include_disabled);
         free(retry_key);
         if (action_config != NULL) {
             return action_config;
         }
     }
 
     /* If the given key is for one of the many notification pseudo-actions
      * (pre_notify_promote, etc.), the actual action name is "notify"
      */
     if (strstr(key, "_notify_")) {
         retry_key = pcmk__op_key(rsc->id, PCMK_ACTION_NOTIFY, 0);
         action_config = find_exact_action_config(rsc, retry_key,
                                                  include_disabled);
         free(retry_key);
         if (action_config != NULL) {
             return action_config;
         }
     }
 
     return NULL;
 }
 
 /*!
  * \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]     for_graph  Whether action should be recorded in transition graph
  * \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, bool for_graph,
            pcmk_scheduler_t *scheduler)
 {
     pcmk_action_t *action = calloc(1, sizeof(pcmk_action_t));
 
     CRM_ASSERT(action != NULL);
 
     action->rsc = rsc;
     action->task = strdup(task); CRM_ASSERT(action->task != NULL);
     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
         pe__set_action_flags(action, pcmk_action_on_dc);
     }
 
     pe__set_action_flags(action, pcmk_action_runnable);
     if (optional) {
         pe__set_action_flags(action, pcmk_action_optional);
     } else {
         pe__clear_action_flags(action, pcmk_action_optional);
     }
 
     if (rsc == NULL) {
         action->meta = pcmk__strkey_table(free, free);
     } else {
         guint interval_ms = 0;
 
         action->op_entry = pcmk__find_action_config(rsc, key, true);
         parse_op_key(key, NULL, NULL, &interval_ms);
         unpack_operation(action, action->op_entry, interval_ms);
     }
 
     if (for_graph) {
         pe_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),
                      pe__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,
         .role = pcmk_role_unknown,
         .now = scheduler->now,
         .match_data = NULL,
         .rsc_data = NULL,
         .op_data = NULL
     };
 
     pe__unpack_dataset_nvpairs(action_xml, XML_TAG_ATTR_SETS,
                                &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,
                                 XML_LRM_ATTR_INTERVAL_MS) == NULL)) {
             pe_rsc_debug(action->rsc, "%s on %s is optional (%s is unmanaged)",
                          action->uuid, pe__node_name(action->node),
                          action->rsc->id);
             pe__set_action_flags(action, pcmk_action_optional);
             // We shouldn't clear runnable here because ... something
 
     // Otherwise require the action if requested
     } else if (!optional) {
         pe__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,
                                       "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]     for_graph  Whether action should be recorded in transition graph
  * \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, bool for_graph,
                                 pcmk_scheduler_t *scheduler)
 {
     if (pcmk_is_set(action->flags, pcmk_action_pseudo)) {
         return;
     }
 
     if (action->node == NULL) {
         pe_rsc_trace(action->rsc, "%s is unrunnable (unallocated)",
                      action->uuid);
         pe__clear_action_flags(action, pcmk_action_runnable);
 
     } else if (!pcmk_is_set(action->flags, pcmk_action_on_dc)
                && !(action->node->details->online)
                && (!pe__is_guest_node(action->node)
                    || action->node->details->remote_requires_reset)) {
         pe__clear_action_flags(action, pcmk_action_runnable);
         do_crm_log((for_graph? LOG_WARNING: LOG_TRACE),
                    "%s on %s is unrunnable (node is offline)",
                    action->uuid, pe__node_name(action->node));
         if (pcmk_is_set(action->rsc->flags, pcmk_rsc_managed)
             && for_graph
             && 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) {
         pe__clear_action_flags(action, pcmk_action_runnable);
         do_crm_log((for_graph? LOG_WARNING: LOG_TRACE),
                    "Action %s on %s is unrunnable (node is pending)",
                    action->uuid, pe__node_name(action->node));
 
     } else if (action->needs == pcmk_requires_nothing) {
         pe_action_set_reason(action, NULL, TRUE);
         if (pe__is_guest_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.
              */
             pe_rsc_debug(action->rsc, "%s on %s is unrunnable "
                          "(node's host cannot be fenced)",
                          action->uuid, pe__node_name(action->node));
             pe__clear_action_flags(action, pcmk_action_runnable);
         } else {
             pe_rsc_trace(action->rsc,
                          "%s on %s does not require fencing or quorum",
                          action->uuid, pe__node_name(action->node));
             pe__set_action_flags(action, pcmk_action_runnable);
         }
 
     } else {
         switch (effective_quorum_policy(action->rsc, scheduler)) {
             case pcmk_no_quorum_stop:
                 pe_rsc_debug(action->rsc, "%s on %s is unrunnable (no quorum)",
                              action->uuid, pe__node_name(action->node));
                 pe__clear_action_flags(action, pcmk_action_runnable);
                 pe_action_set_reason(action, "no quorum", true);
                 break;
 
             case pcmk_no_quorum_freeze:
                 if (!action->rsc->fns->active(action->rsc, TRUE)
                     || (action->rsc->next_role > action->rsc->role)) {
                     pe_rsc_debug(action->rsc,
                                  "%s on %s is unrunnable (no quorum)",
                                  action->uuid, pe__node_name(action->node));
                     pe__clear_action_flags(action, pcmk_action_runnable);
                     pe_action_set_reason(action, "quorum freeze", true);
                 }
                 break;
 
             default:
                 //pe_action_set_reason(action, NULL, TRUE);
                 pe__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)) {
         pe__set_resource_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)) {
             pe__set_resource_flags(rsc, pcmk_rsc_starting);
         } else {
             pe__clear_resource_flags(rsc, pcmk_rsc_starting);
         }
     }
 }
 
 static bool
 valid_stop_on_fail(const char *value)
 {
     return !pcmk__strcase_any_of(value, "standby", "demote", "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, XML_OP_ATTR_ON_FAIL);
     char *key = NULL;
     char *new_value = NULL;
 
     // 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 '" XML_OP_ATTR_ON_FAIL "' for %s stop "
                          "action to default value because '%s' is not "
                          "allowed for stop", rsc->id, value);
         g_hash_table_remove(meta, XML_OP_ATTR_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 = first_named_child(rsc->ops_xml, XML_ATTR_OP);
              operation != NULL; operation = crm_next_same_xml(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, XML_OP_ATTR_ON_FAIL);
             if (promote_on_fail == NULL) {
                 continue;
             }
 
             // We only care about recurring monitors for the promoted role
             name = crm_element_value(operation, "name");
             role = crm_element_value(operation, "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, XML_LRM_ATTR_INTERVAL);
             if (crm_parse_interval_spec(interval_spec) == 0) {
                 continue;
             }
 
             // We only care about enabled monitors
             if ((pcmk__xe_get_bool_attr(operation, "enabled",
                                         &enabled) == pcmk_rc_ok) && !enabled) {
                 continue;
             }
 
             // Demote actions can't default to on-fail="demote"
             if (pcmk__str_eq(promote_on_fail, "demote", pcmk__str_casei)) {
                 continue;
             }
 
             // Use value from first applicable promote action found
             key = strdup(XML_OP_ATTR_ON_FAIL);
             new_value = strdup(promote_on_fail);
             CRM_ASSERT((key != NULL) && (new_value != NULL));
             g_hash_table_insert(meta, key, new_value);
         }
         return;
     }
 
     if (pcmk__str_eq(action_name, PCMK_ACTION_LRM_DELETE, pcmk__str_none)
         && !pcmk__str_eq(value, "ignore", pcmk__str_casei)) {
         key = strdup(XML_OP_ATTR_ON_FAIL);
         new_value = strdup("ignore");
         CRM_ASSERT((key != NULL) && (new_value != NULL));
         g_hash_table_insert(meta, key, new_value);
         return;
     }
 
     // on-fail="demote" is allowed only for certain actions
     if (pcmk__str_eq(value, "demote", pcmk__str_casei)) {
         name = crm_element_value(action_config, "name");
         role = crm_element_value(action_config, "role");
         interval_spec = crm_element_value(action_config,
                                           XML_LRM_ATTR_INTERVAL);
 
         if (!pcmk__str_eq(name, PCMK_ACTION_PROMOTE, pcmk__str_none)
             && (!pcmk__str_eq(name, PCMK_ACTION_MONITOR, pcmk__str_none)
                 || !pcmk__strcase_any_of(role, PCMK__ROLE_PROMOTED,
                                          PCMK__ROLE_PROMOTED_LEGACY, NULL)
                 || (crm_parse_interval_spec(interval_spec) == 0))) {
             pcmk__config_err("Resetting '" XML_OP_ATTR_ON_FAIL "' for %s %s "
                              "action to default value because 'demote' is not "
                              "allowed for it", rsc->id, name);
             g_hash_table_remove(meta, XML_OP_ATTR_ON_FAIL);
             return;
         }
     }
 }
 
 static int
 unpack_timeout(const char *value)
 {
     int timeout_ms = crm_get_msec(value);
 
     if (timeout_ms < 0) {
         timeout_ms = PCMK_DEFAULT_ACTION_TIMEOUT_MS;
     }
     return timeout_ms;
 }
 
 // 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 '" XML_OP_ATTR_ORIGIN "' for operation "
                          "'%s' because '%s' is not valid",
                          (ID(xml_obj)? ID(xml_obj) : "(missing ID)"), value);
         return false;
     }
 
     // Get seconds since origin (negative if origin is in the future)
     result = crm_time_get_seconds(now) - crm_time_get_seconds(origin);
     crm_time_free(origin);
 
     // Calculate seconds from closest interval to now
     result = result % interval_sec;
 
     // Calculate seconds remaining until next interval
     result = ((result <= 0)? 0 : interval_sec) - result;
     crm_info("Calculated a start delay of %llds for operation '%s'",
              result,
              (ID(xml_obj)? ID(xml_obj) : "(unspecified)"));
 
     if (start_delay != NULL) {
         *start_delay = result * 1000; // milliseconds
     }
     return true;
 }
 
 static int
 unpack_start_delay(const char *value, GHashTable *meta)
 {
     int start_delay = 0;
 
     if (value != NULL) {
         start_delay = crm_get_msec(value);
 
         if (start_delay < 0) {
             start_delay = 0;
         }
 
         if (meta) {
             g_hash_table_replace(meta, strdup(XML_OP_ATTR_START_DELAY),
                                  pcmk__itoa(start_delay));
         }
     }
 
     return start_delay;
 }
 
 /*!
  * \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 = first_named_child(rsc->ops_xml, XML_ATTR_OP);
          operation != NULL; operation = crm_next_same_xml(operation)) {
         bool enabled = false;
         guint interval_ms = 0;
         const char *interval_spec = crm_element_value(operation,
                                                       XML_LRM_ATTR_INTERVAL);
 
         // We only care about enabled recurring monitors
         if (!pcmk__str_eq(crm_element_value(operation, "name"),
                           PCMK_ACTION_MONITOR, pcmk__str_none)) {
             continue;
         }
         interval_ms = crm_parse_interval_spec(interval_spec);
         if (interval_ms == 0) {
             continue;
         }
 
         // @TODO This does not account for rules, defaults, etc.
         if ((pcmk__xe_get_bool_attr(operation, "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;
     char *name = NULL;
     char *value = NULL;
     const char *timeout_spec = NULL;
     const char *str = NULL;
 
     pe_rsc_eval_data_t rsc_rule_data = {
         .standard = crm_element_value(rsc->xml, XML_AGENT_ATTR_CLASS),
         .provider = crm_element_value(rsc->xml, XML_AGENT_ATTR_PROVIDER),
         .agent = crm_element_value(rsc->xml, XML_EXPR_ATTR_TYPE),
     };
 
     pe_op_eval_data_t op_rule_data = {
         .op_name = action_name,
         .interval = interval_ms,
     };
 
     pe_rule_eval_data_t rule_data = {
         .node_hash = (node == NULL)? NULL : node->details->attrs,
         .role = pcmk_role_unknown,
         .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, XML_TAG_META_SETS,
                                &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 meta_attributes
              * blocks (which may also have rules that need to be evaluated).
              */
             timeout_spec = crm_element_value(min_interval_mon,
                                              XML_ATTR_TIMEOUT);
             if (timeout_spec != NULL) {
                 pe_rsc_trace(rsc,
                              "Setting default timeout for %s probe to "
                              "most frequent monitor's timeout '%s'",
                              rsc->id, timeout_spec);
                 name = strdup(XML_ATTR_TIMEOUT);
                 value = strdup(timeout_spec);
                 CRM_ASSERT((name != NULL) && (value != NULL));
                 g_hash_table_insert(meta, name, value);
             }
         }
     }
 
     if (action_config != NULL) {
         // <op> <meta_attributes> take precedence over defaults
         pe__unpack_dataset_nvpairs(action_config, XML_TAG_META_SETS, &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) {
             name = strdup((const char *) attr->name);
             value = strdup(pcmk__xml_attr_value(attr));
 
             CRM_ASSERT((name != NULL) && (value != NULL));
             g_hash_table_insert(meta, name, value);
         }
     }
 
     g_hash_table_remove(meta, XML_ATTR_ID);
 
     // Normalize interval to milliseconds
     if (interval_ms > 0) {
         name = strdup(XML_LRM_ATTR_INTERVAL);
         CRM_ASSERT(name != NULL);
         value = crm_strdup_printf("%u", interval_ms);
         g_hash_table_insert(meta, name, value);
     } else {
         g_hash_table_remove(meta, XML_LRM_ATTR_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) {
             pe_rsc_trace(rsc,
                          "Setting timeout for %s %s to "
                          "pcmk_monitor_timeout (%s)",
                          rsc->id, action_name, timeout_spec);
             name = strdup(XML_ATTR_TIMEOUT);
             value = strdup(timeout_spec);
             CRM_ASSERT((name != NULL) && (value != NULL));
             g_hash_table_insert(meta, name, value);
         }
     }
 
     // Normalize timeout to positive milliseconds
     name = strdup(XML_ATTR_TIMEOUT);
     CRM_ASSERT(name != NULL);
     timeout_spec = g_hash_table_lookup(meta, XML_ATTR_TIMEOUT);
     g_hash_table_insert(meta, name, pcmk__itoa(unpack_timeout(timeout_spec)));
 
     // Ensure on-fail has a valid value
     validate_on_fail(rsc, action_name, action_config, meta);
 
     // Normalize start-delay
     str = g_hash_table_lookup(meta, XML_OP_ATTR_START_DELAY);
     if (str != NULL) {
         unpack_start_delay(str, meta);
     } else {
         long long start_delay = 0;
 
         str = g_hash_table_lookup(meta, XML_OP_ATTR_ORIGIN);
         if (unpack_interval_origin(str, action_config, interval_ms,
                                    rsc->cluster->now, &start_delay)) {
             name = strdup(XML_OP_ATTR_START_DELAY);
             CRM_ASSERT(name != NULL);
             g_hash_table_insert(meta, name,
                                 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";
     }
     pe_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;
 
     if (value == NULL) {
         // Use default
 
     } else if (pcmk__str_eq(value, "block", pcmk__str_casei)) {
         on_fail = pcmk_on_fail_block;
         desc = "block";
 
     } else if (pcmk__str_eq(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 '" XML_OP_ATTR_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, "standby", pcmk__str_casei)) {
         on_fail = pcmk_on_fail_standby_node;
         desc = "node standby";
 
     } else if (pcmk__strcase_any_of(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, "stop", pcmk__str_casei)) {
         on_fail = pcmk_on_fail_stop;
         desc = "stop resource";
 
     } else if (pcmk__str_eq(value, "restart", pcmk__str_casei)) {
         on_fail = pcmk_on_fail_restart;
         desc = "restart (and possibly migrate)";
 
     } else if (pcmk__str_eq(value, "restart-container", pcmk__str_casei)) {
         if (rsc->container == NULL) {
             pe_rsc_debug(rsc,
                          "Using default " XML_OP_ATTR_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, "demote", pcmk__str_casei)) {
         on_fail = pcmk_on_fail_demote;
         desc = "demote instance";
 
     } else {
         pcmk__config_err("Using default '" XML_OP_ATTR_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
      * on-fail="fence" to fence after start failures.
      */
     if (pe__resource_is_remote_conn(rsc)
         && !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)";
     }
 
     pe_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, "role_after_failure");
+    if (value != NULL) {
+        pe_warn_once(pcmk__wo_role_after,
+                    "Support for role_after_failure is deprecated "
+                    "and will be removed in a future release");
+        if (role == pcmk_role_unknown) {
+            role = text2role(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;
+        }
+    }
+    pe_rsc_trace(rsc, "Role after %s %s failure is: %s",
+                 rsc->id, action_name, role2text(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, XML_OP_ATTR_ON_FAIL);
     action->on_fail = pcmk__parse_on_fail(action->rsc, action->task,
                                           interval_ms, value);
 
-    // Set default for role after failure specially in certain circumstances
-    switch (action->on_fail) {
-        case pcmk_on_fail_stop:
-            action->fail_role = pcmk_role_stopped;
-            break;
-
-        case pcmk_on_fail_reset_remote:
-            if (action->rsc->remote_reconnect_ms != 0) {
-                action->fail_role = pcmk_role_stopped;
-            }
-            break;
-
-        default:
-            break;
-    }
-
-    value = NULL;
-    if (xml_obj != NULL) {
-        value = g_hash_table_lookup(action->meta, "role_after_failure");
-        if (value) {
-            pe_warn_once(pcmk__wo_role_after,
-                        "Support for role_after_failure is deprecated and will be removed in a future release");
-        }
-    }
-    if (value != NULL && action->fail_role == pcmk_role_unknown) {
-        action->fail_role = text2role(value);
-    }
-    /* defaults */
-    if (action->fail_role == pcmk_role_unknown) {
-        if (pcmk__str_eq(action->task, PCMK_ACTION_PROMOTE, pcmk__str_casei)) {
-            action->fail_role = pcmk_role_unpromoted;
-        } else {
-            action->fail_role = pcmk_role_started;
-        }
-    }
-    pe_rsc_trace(action->rsc, "%s failure results in: %s",
-                 action->uuid, role2text(action->fail_role));
+    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]     save_action  Whether action should be recorded in transition graph
  * \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. If
  *       \p save_action is true, \p scheduler will own the returned action,
  *       otherwise it is the caller's responsibility to free the return value
  *       with pe_free_action().
  */
 pcmk_action_t *
 custom_action(pcmk_resource_t *rsc, char *key, const char *task,
               const pcmk_node_t *on_node, gboolean optional,
               gboolean save_action, pcmk_scheduler_t *scheduler)
 {
     pcmk_action_t *action = NULL;
 
     CRM_ASSERT((key != NULL) && (task != NULL) && (scheduler != NULL));
 
     if (save_action) {
         action = find_existing_action(key, rsc, on_node, scheduler);
     }
 
     if (action == NULL) {
         action = new_action(key, task, rsc, on_node, optional, save_action,
                             scheduler);
     } else {
         free(key);
     }
 
     update_action_optional(action, optional);
 
     if (rsc != NULL) {
         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);
             pe__set_action_flags(action, pcmk_action_attrs_evaluated);
         }
 
         update_resource_action_runnable(action, save_action, scheduler);
 
         if (save_action) {
             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, TRUE,
                            scheduler);
         pe__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;
 
     // `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, TRUE, scheduler);
 
         add_hash_param(stonith_op->meta, XML_LRM_ATTR_TARGET, node->details->uname);
         add_hash_param(stonith_op->meta, XML_LRM_ATTR_TARGET_UUID, node->details->id);
         add_hash_param(stonith_op->meta, "stonith_action", op);
 
         if (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);
 
             char *key = NULL;
             char *value = 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,
                                                         XML_ATTR_TYPE);
                 op_digest_cache_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", pe__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",
                                   pe__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);
             }
             key = strdup(XML_OP_ATTR_DIGESTS_ALL);
             value = strdup((const char *) digests_all->str);
             CRM_ASSERT((key != NULL) && (value != NULL));
             g_hash_table_insert(stonith_op->meta, key, value);
             g_string_free(digests_all, TRUE);
 
             key = strdup(XML_OP_ATTR_DIGESTS_SECURE);
             value = strdup((const char *) digests_secure->str);
             CRM_ASSERT((key != NULL) && (value != NULL));
             g_hash_table_insert(stonith_op->meta, key, value);
             g_string_free(digests_secure, TRUE);
         }
 
     } else {
         free(op_key);
     }
 
     if (scheduler->priority_fencing_delay > 0
 
             /* It's a suitable case where `priority-fencing-delay` applies.
              * At least add `priority-fencing-delay` field as an indicator. */
         && (priority_delay
 
             /* 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,
                                    XML_CONFIG_ATTR_PRIORITY_FENCING_DELAY) != NULL)) {
 
             /* Add `priority-fencing-delay` to the fencing op even if it's 0 for
              * the targeting node. So that it takes precedence over any possible
              * `pcmk_delay_base/max`.
              */
             char *delay_s = pcmk__itoa(node_priority_fencing_delay(node,
                                                                    scheduler));
 
             g_hash_table_insert(stonith_op->meta,
                                 strdup(XML_CONFIG_ATTR_PRIORITY_FENCING_DELAY),
                                 delay_s);
     }
 
     if(optional == FALSE && pe_can_fence(scheduler, node)) {
         pe__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);     /* pe_action_wrapper_t* */
     g_list_free_full(action->actions_after, free);      /* pe_action_wrapper_t* */
     if (action->extra) {
         g_hash_table_destroy(action->extra);
     }
     if (action->meta) {
         g_hash_table_destroy(action->meta);
     }
     free(action->cancel_task);
     free(action->reason);
     free(action->task);
     free(action->uuid);
     free(action->node);
     free(action);
 }
 
 int
 pe_get_configured_timeout(pcmk_resource_t *rsc, const char *action,
                           pcmk_scheduler_t *scheduler)
 {
     xmlNode *child = NULL;
     GHashTable *action_meta = NULL;
     const char *timeout_spec = NULL;
     int timeout_ms = 0;
 
     pe_rule_eval_data_t rule_data = {
         .node_hash = NULL,
         .role = pcmk_role_unknown,
         .now = scheduler->now,
         .match_data = NULL,
         .rsc_data = NULL,
         .op_data = NULL
     };
 
     for (child = first_named_child(rsc->ops_xml, XML_ATTR_OP);
          child != NULL; child = crm_next_same_xml(child)) {
         if (pcmk__str_eq(action, crm_element_value(child, XML_NVPAIR_ATTR_NAME),
                 pcmk__str_casei)) {
             timeout_spec = crm_element_value(child, XML_ATTR_TIMEOUT);
             break;
         }
     }
 
     if (timeout_spec == NULL && scheduler->op_defaults) {
         action_meta = pcmk__strkey_table(free, free);
         pe__unpack_dataset_nvpairs(scheduler->op_defaults, XML_TAG_META_SETS,
                                    &rule_data, action_meta, NULL, FALSE,
                                    scheduler);
         timeout_spec = g_hash_table_lookup(action_meta, XML_ATTR_TIMEOUT);
     }
 
     // @TODO check meta-attributes
     // @TODO maybe use min-interval monitor timeout as default for monitors
 
     timeout_ms = crm_get_msec(timeout_spec);
     if (timeout_ms < 0) {
         timeout_ms = PCMK_DEFAULT_ACTION_TIMEOUT_MS;
     }
 
     if (action_meta != NULL) {
         g_hash_table_destroy(action_meta);
     }
     return timeout_ms;
 }
 
 enum action_tasks
 get_complex_task(const pcmk_resource_t *rsc, const char *name)
 {
     enum action_tasks task = text2task(name);
 
     if ((rsc != NULL) && (rsc->variant == pcmk_rsc_variant_primitive)) {
         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 (on_node->details == action->node->details) {
             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, pe__node_name(on_node));
 
             action->node = pe__copy_node(on_node);
             result = g_list_prepend(result, action);
 
         } else if (on_node->details == action->node->details) {
             crm_trace("Action %s on %s matches", key, pe__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, pe__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) {
         pe_rsc_trace(action->rsc, "Changing %s reason from '%s' to '%s'",
                      action->uuid, action->reason, pcmk__s(reason, "(none)"));
     } else if (action->reason == NULL) {
         pe_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, TRUE, 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, XML_ATTR_ID);
     const char *b_xml_id = crm_element_value(xml_b, XML_ATTR_ID);
 
     const char *a_node = crm_element_value(xml_a, XML_LRM_ATTR_TARGET);
     const char *b_node = crm_element_value(xml_b, XML_LRM_ATTR_TARGET);
     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 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 lrm_rsc_op entries in the status
          * section which is unlikely to be a good thing
          *    - we can handle it easily enough, but we need to get
          *    to the bottom of why it's happening.
          */
         pe_err("Duplicate lrm_rsc_op entries named %s", a_xml_id);
         sort_return(0, "duplicate");
     }
 
     crm_element_value_int(xml_a, XML_LRM_ATTR_CALLID, &a_call_id);
     crm_element_value_int(xml_b, XML_LRM_ATTR_CALLID, &b_call_id);
 
     if (a_call_id == -1 && b_call_id == -1) {
         /* both are pending ops so it doesn't matter since
          *   stops are never pending
          */
         sort_return(0, "pending");
 
     } else if (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 last-rc-change
          */
         time_t last_a = -1;
         time_t last_b = -1;
 
         crm_element_value_epoch(xml_a, XML_RSC_OP_LAST_CHANGE, &last_a);
         crm_element_value_epoch(xml_b, XML_RSC_OP_LAST_CHANGE, &last_b);
 
         crm_trace("rc-change: %lld vs %lld",
                   (long long) last_a, (long long) last_b);
         if (last_a >= 0 && last_a < last_b) {
             sort_return(-1, "rc-change");
 
         } else if (last_b >= 0 && last_a > last_b) {
             sort_return(1, "rc-change");
         }
         sort_return(0, "rc-change");
 
     } else {
         /* One of the inputs is a pending operation
          * Attempt to use XML_ATTR_TRANSITION_MAGIC to determine its age relative to the other
          */
 
         int a_id = -1;
         int b_id = -1;
 
         const char *a_magic = crm_element_value(xml_a, XML_ATTR_TRANSITION_MAGIC);
         const char *b_magic = crm_element_value(xml_b, XML_ATTR_TRANSITION_MAGIC);
 
         CRM_CHECK(a_magic != NULL && b_magic != NULL, sort_return(0, "No magic"));
         if (!decode_transition_magic(a_magic, &a_uuid, &a_id, NULL, NULL, NULL,
                                      NULL)) {
             sort_return(0, "bad magic a");
         }
         if (!decode_transition_magic(b_magic, &b_uuid, &b_id, NULL, NULL, NULL,
                                      NULL)) {
             sort_return(0, "bad magic b");
         }
         /* try to determine the relative age of the operation...
          * some pending operations (e.g. a start) may have been superseded
          *   by a subsequent stop
          *
          * [a|b]_id == -1 means it's a shutdown operation and _always_ comes last
          */
         if (!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, TRUE, rsc->cluster);
     pe__set_action_flags(action, pcmk_action_pseudo);
     if (runnable) {
         pe__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 add_hash_param().
  */
 void
 pe__add_action_expected_result(pcmk_action_t *action, int expected_result)
 {
     char *name = NULL;
 
     CRM_ASSERT((action != NULL) && (action->meta != NULL));
 
     name = strdup(XML_ATTR_TE_TARGET_RC);
     CRM_ASSERT (name != NULL);
 
     g_hash_table_insert(action->meta, name, pcmk__itoa(expected_result));
 }