diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 06:53:20 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 06:53:20 +0000 |
commit | e5a812082ae033afb1eed82c0f2df3d0f6bdc93f (patch) | |
tree | a6716c9275b4b413f6c9194798b34b91affb3cc7 /lib/pengine/unpack.c | |
parent | Initial commit. (diff) | |
download | pacemaker-e5a812082ae033afb1eed82c0f2df3d0f6bdc93f.tar.xz pacemaker-e5a812082ae033afb1eed82c0f2df3d0f6bdc93f.zip |
Adding upstream version 2.1.6.upstream/2.1.6
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'lib/pengine/unpack.c')
-rw-r--r-- | lib/pengine/unpack.c | 4829 |
1 files changed, 4829 insertions, 0 deletions
diff --git a/lib/pengine/unpack.c b/lib/pengine/unpack.c new file mode 100644 index 0000000..2bd6707 --- /dev/null +++ b/lib/pengine/unpack.c @@ -0,0 +1,4829 @@ +/* + * 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 <stdio.h> +#include <string.h> +#include <glib.h> +#include <time.h> + +#include <crm/crm.h> +#include <crm/services.h> +#include <crm/msg_xml.h> +#include <crm/common/xml.h> +#include <crm/common/xml_internal.h> + +#include <crm/common/util.h> +#include <crm/pengine/rules.h> +#include <crm/pengine/internal.h> +#include <pe_status_private.h> + +CRM_TRACE_INIT_DATA(pe_status); + +// A (parsed) resource action history entry +struct action_history { + pe_resource_t *rsc; // Resource that history is for + pe_node_t *node; // Node that history is for + xmlNode *xml; // History entry XML + + // Parsed from entry XML + const char *id; // XML ID of history entry + const char *key; // Operation key of action + const char *task; // Action name + const char *exit_reason; // Exit reason given for result + guint interval_ms; // Action interval + int call_id; // Call ID of action + int expected_exit_status; // Expected exit status of action + int exit_status; // Actual exit status of action + int execution_status; // Execution status of action +}; + +/* This uses pcmk__set_flags_as()/pcmk__clear_flags_as() directly rather than + * use pe__set_working_set_flags()/pe__clear_working_set_flags() so that the + * flag is stringified more readably in log messages. + */ +#define set_config_flag(data_set, option, flag) do { \ + const char *scf_value = pe_pref((data_set)->config_hash, (option)); \ + if (scf_value != NULL) { \ + if (crm_is_true(scf_value)) { \ + (data_set)->flags = pcmk__set_flags_as(__func__, __LINE__, \ + LOG_TRACE, "Working set", \ + crm_system_name, (data_set)->flags, \ + (flag), #flag); \ + } else { \ + (data_set)->flags = pcmk__clear_flags_as(__func__, __LINE__,\ + LOG_TRACE, "Working set", \ + crm_system_name, (data_set)->flags, \ + (flag), #flag); \ + } \ + } \ + } while(0) + +static void unpack_rsc_op(pe_resource_t *rsc, pe_node_t *node, xmlNode *xml_op, + xmlNode **last_failure, + enum action_fail_response *failed); +static void determine_remote_online_status(pe_working_set_t *data_set, + pe_node_t *this_node); +static void add_node_attrs(const xmlNode *xml_obj, pe_node_t *node, + bool overwrite, pe_working_set_t *data_set); +static void determine_online_status(const xmlNode *node_state, + pe_node_t *this_node, + pe_working_set_t *data_set); + +static void unpack_node_lrm(pe_node_t *node, const xmlNode *xml, + pe_working_set_t *data_set); + + +// Bitmask for warnings we only want to print once +uint32_t pe_wo = 0; + +static gboolean +is_dangling_guest_node(pe_node_t *node) +{ + /* we are looking for a remote-node that was supposed to be mapped to a + * container resource, but all traces of that container have disappeared + * from both the config and the status section. */ + if (pe__is_guest_or_remote_node(node) && + node->details->remote_rsc && + node->details->remote_rsc->container == NULL && + pcmk_is_set(node->details->remote_rsc->flags, + pe_rsc_orphan_container_filler)) { + return TRUE; + } + + return FALSE; +} + +/*! + * \brief Schedule a fence action for a node + * + * \param[in,out] data_set Current working set of cluster + * \param[in,out] node Node to fence + * \param[in] reason Text description of why fencing is needed + * \param[in] priority_delay Whether to consider `priority-fencing-delay` + */ +void +pe_fence_node(pe_working_set_t * data_set, pe_node_t * node, + const char *reason, bool priority_delay) +{ + CRM_CHECK(node, return); + + /* A guest node is fenced by marking its container as failed */ + if (pe__is_guest_node(node)) { + pe_resource_t *rsc = node->details->remote_rsc->container; + + if (!pcmk_is_set(rsc->flags, pe_rsc_failed)) { + if (!pcmk_is_set(rsc->flags, pe_rsc_managed)) { + crm_notice("Not fencing guest node %s " + "(otherwise would because %s): " + "its guest resource %s is unmanaged", + pe__node_name(node), reason, rsc->id); + } else { + crm_warn("Guest node %s will be fenced " + "(by recovering its guest resource %s): %s", + pe__node_name(node), rsc->id, reason); + + /* We don't mark the node as unclean because that would prevent the + * node from running resources. We want to allow it to run resources + * in this transition if the recovery succeeds. + */ + node->details->remote_requires_reset = TRUE; + pe__set_resource_flags(rsc, pe_rsc_failed|pe_rsc_stop); + } + } + + } else if (is_dangling_guest_node(node)) { + crm_info("Cleaning up dangling connection for guest node %s: " + "fencing was already done because %s, " + "and guest resource no longer exists", + pe__node_name(node), reason); + pe__set_resource_flags(node->details->remote_rsc, + pe_rsc_failed|pe_rsc_stop); + + } else if (pe__is_remote_node(node)) { + pe_resource_t *rsc = node->details->remote_rsc; + + if ((rsc != NULL) && !pcmk_is_set(rsc->flags, pe_rsc_managed)) { + crm_notice("Not fencing remote node %s " + "(otherwise would because %s): connection is unmanaged", + pe__node_name(node), reason); + } else if(node->details->remote_requires_reset == FALSE) { + node->details->remote_requires_reset = TRUE; + crm_warn("Remote node %s %s: %s", + pe__node_name(node), + pe_can_fence(data_set, node)? "will be fenced" : "is unclean", + reason); + } + node->details->unclean = TRUE; + // No need to apply `priority-fencing-delay` for remote nodes + pe_fence_op(node, NULL, TRUE, reason, FALSE, data_set); + + } else if (node->details->unclean) { + crm_trace("Cluster node %s %s because %s", + pe__node_name(node), + pe_can_fence(data_set, node)? "would also be fenced" : "also is unclean", + reason); + + } else { + crm_warn("Cluster node %s %s: %s", + pe__node_name(node), + pe_can_fence(data_set, node)? "will be fenced" : "is unclean", + reason); + node->details->unclean = TRUE; + pe_fence_op(node, NULL, TRUE, reason, priority_delay, data_set); + } +} + +// @TODO xpaths can't handle templates, rules, or id-refs + +// nvpair with provides or requires set to unfencing +#define XPATH_UNFENCING_NVPAIR XML_CIB_TAG_NVPAIR \ + "[(@" XML_NVPAIR_ATTR_NAME "='" PCMK_STONITH_PROVIDES "'" \ + "or @" XML_NVPAIR_ATTR_NAME "='" XML_RSC_ATTR_REQUIRES "') " \ + "and @" XML_NVPAIR_ATTR_VALUE "='" PCMK__VALUE_UNFENCING "']" + +// unfencing in rsc_defaults or any resource +#define XPATH_ENABLE_UNFENCING \ + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_RESOURCES \ + "//" XML_TAG_META_SETS "/" XPATH_UNFENCING_NVPAIR \ + "|/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_RSCCONFIG \ + "/" XML_TAG_META_SETS "/" XPATH_UNFENCING_NVPAIR + +static void +set_if_xpath(uint64_t flag, const char *xpath, pe_working_set_t *data_set) +{ + xmlXPathObjectPtr result = NULL; + + if (!pcmk_is_set(data_set->flags, flag)) { + result = xpath_search(data_set->input, xpath); + if (result && (numXpathResults(result) > 0)) { + pe__set_working_set_flags(data_set, flag); + } + freeXpathObject(result); + } +} + +gboolean +unpack_config(xmlNode * config, pe_working_set_t * data_set) +{ + const char *value = NULL; + GHashTable *config_hash = pcmk__strkey_table(free, free); + + pe_rule_eval_data_t rule_data = { + .node_hash = NULL, + .role = RSC_ROLE_UNKNOWN, + .now = data_set->now, + .match_data = NULL, + .rsc_data = NULL, + .op_data = NULL + }; + + data_set->config_hash = config_hash; + + pe__unpack_dataset_nvpairs(config, XML_CIB_TAG_PROPSET, &rule_data, config_hash, + CIB_OPTIONS_FIRST, FALSE, data_set); + + verify_pe_options(data_set->config_hash); + + set_config_flag(data_set, "enable-startup-probes", pe_flag_startup_probes); + if (!pcmk_is_set(data_set->flags, pe_flag_startup_probes)) { + crm_info("Startup probes: disabled (dangerous)"); + } + + value = pe_pref(data_set->config_hash, XML_ATTR_HAVE_WATCHDOG); + if (value && crm_is_true(value)) { + crm_info("Watchdog-based self-fencing will be performed via SBD if " + "fencing is required and stonith-watchdog-timeout is nonzero"); + pe__set_working_set_flags(data_set, pe_flag_have_stonith_resource); + } + + /* Set certain flags via xpath here, so they can be used before the relevant + * configuration sections are unpacked. + */ + set_if_xpath(pe_flag_enable_unfencing, XPATH_ENABLE_UNFENCING, data_set); + + value = pe_pref(data_set->config_hash, "stonith-timeout"); + data_set->stonith_timeout = (int) crm_parse_interval_spec(value); + crm_debug("STONITH timeout: %d", data_set->stonith_timeout); + + set_config_flag(data_set, "stonith-enabled", pe_flag_stonith_enabled); + crm_debug("STONITH of failed nodes is %s", + pcmk_is_set(data_set->flags, pe_flag_stonith_enabled)? "enabled" : "disabled"); + + data_set->stonith_action = pe_pref(data_set->config_hash, "stonith-action"); + if (!strcmp(data_set->stonith_action, "poweroff")) { + pe_warn_once(pe_wo_poweroff, + "Support for stonith-action of 'poweroff' is deprecated " + "and will be removed in a future release (use 'off' instead)"); + data_set->stonith_action = "off"; + } + crm_trace("STONITH will %s nodes", data_set->stonith_action); + + set_config_flag(data_set, "concurrent-fencing", pe_flag_concurrent_fencing); + crm_debug("Concurrent fencing is %s", + pcmk_is_set(data_set->flags, pe_flag_concurrent_fencing)? "enabled" : "disabled"); + + value = pe_pref(data_set->config_hash, + XML_CONFIG_ATTR_PRIORITY_FENCING_DELAY); + if (value) { + data_set->priority_fencing_delay = crm_parse_interval_spec(value) / 1000; + crm_trace("Priority fencing delay is %ds", data_set->priority_fencing_delay); + } + + set_config_flag(data_set, "stop-all-resources", pe_flag_stop_everything); + crm_debug("Stop all active resources: %s", + pcmk__btoa(pcmk_is_set(data_set->flags, pe_flag_stop_everything))); + + set_config_flag(data_set, "symmetric-cluster", pe_flag_symmetric_cluster); + if (pcmk_is_set(data_set->flags, pe_flag_symmetric_cluster)) { + crm_debug("Cluster is symmetric" " - resources can run anywhere by default"); + } + + value = pe_pref(data_set->config_hash, "no-quorum-policy"); + + if (pcmk__str_eq(value, "ignore", pcmk__str_casei)) { + data_set->no_quorum_policy = no_quorum_ignore; + + } else if (pcmk__str_eq(value, "freeze", pcmk__str_casei)) { + data_set->no_quorum_policy = no_quorum_freeze; + + } else if (pcmk__str_eq(value, "demote", pcmk__str_casei)) { + data_set->no_quorum_policy = no_quorum_demote; + + } else if (pcmk__str_eq(value, "suicide", pcmk__str_casei)) { + if (pcmk_is_set(data_set->flags, pe_flag_stonith_enabled)) { + int do_panic = 0; + + crm_element_value_int(data_set->input, XML_ATTR_QUORUM_PANIC, + &do_panic); + if (do_panic || pcmk_is_set(data_set->flags, pe_flag_have_quorum)) { + data_set->no_quorum_policy = no_quorum_suicide; + } else { + crm_notice("Resetting no-quorum-policy to 'stop': cluster has never had quorum"); + data_set->no_quorum_policy = no_quorum_stop; + } + } else { + pcmk__config_err("Resetting no-quorum-policy to 'stop' because " + "fencing is disabled"); + data_set->no_quorum_policy = no_quorum_stop; + } + + } else { + data_set->no_quorum_policy = no_quorum_stop; + } + + switch (data_set->no_quorum_policy) { + case no_quorum_freeze: + crm_debug("On loss of quorum: Freeze resources"); + break; + case no_quorum_stop: + crm_debug("On loss of quorum: Stop ALL resources"); + break; + case no_quorum_demote: + crm_debug("On loss of quorum: " + "Demote promotable resources and stop other resources"); + break; + case no_quorum_suicide: + crm_notice("On loss of quorum: Fence all remaining nodes"); + break; + case no_quorum_ignore: + crm_notice("On loss of quorum: Ignore"); + break; + } + + set_config_flag(data_set, "stop-orphan-resources", pe_flag_stop_rsc_orphans); + crm_trace("Orphan resources are %s", + pcmk_is_set(data_set->flags, pe_flag_stop_rsc_orphans)? "stopped" : "ignored"); + + set_config_flag(data_set, "stop-orphan-actions", pe_flag_stop_action_orphans); + crm_trace("Orphan resource actions are %s", + pcmk_is_set(data_set->flags, pe_flag_stop_action_orphans)? "stopped" : "ignored"); + + value = pe_pref(data_set->config_hash, "remove-after-stop"); + if (value != NULL) { + if (crm_is_true(value)) { + pe__set_working_set_flags(data_set, pe_flag_remove_after_stop); +#ifndef PCMK__COMPAT_2_0 + pe_warn_once(pe_wo_remove_after, + "Support for the remove-after-stop cluster property is" + " deprecated and will be removed in a future release"); +#endif + } else { + pe__clear_working_set_flags(data_set, pe_flag_remove_after_stop); + } + } + + set_config_flag(data_set, "maintenance-mode", pe_flag_maintenance_mode); + crm_trace("Maintenance mode: %s", + pcmk__btoa(pcmk_is_set(data_set->flags, pe_flag_maintenance_mode))); + + set_config_flag(data_set, "start-failure-is-fatal", pe_flag_start_failure_fatal); + crm_trace("Start failures are %s", + pcmk_is_set(data_set->flags, pe_flag_start_failure_fatal)? "always fatal" : "handled by failcount"); + + if (pcmk_is_set(data_set->flags, pe_flag_stonith_enabled)) { + set_config_flag(data_set, "startup-fencing", pe_flag_startup_fencing); + } + if (pcmk_is_set(data_set->flags, pe_flag_startup_fencing)) { + crm_trace("Unseen nodes will be fenced"); + } else { + pe_warn_once(pe_wo_blind, "Blind faith: not fencing unseen nodes"); + } + + pe__unpack_node_health_scores(data_set); + + data_set->placement_strategy = pe_pref(data_set->config_hash, "placement-strategy"); + crm_trace("Placement strategy: %s", data_set->placement_strategy); + + set_config_flag(data_set, "shutdown-lock", pe_flag_shutdown_lock); + crm_trace("Resources will%s be locked to cleanly shut down nodes", + (pcmk_is_set(data_set->flags, pe_flag_shutdown_lock)? "" : " not")); + if (pcmk_is_set(data_set->flags, pe_flag_shutdown_lock)) { + value = pe_pref(data_set->config_hash, + XML_CONFIG_ATTR_SHUTDOWN_LOCK_LIMIT); + data_set->shutdown_lock = crm_parse_interval_spec(value) / 1000; + crm_trace("Shutdown locks expire after %us", data_set->shutdown_lock); + } + + return TRUE; +} + +pe_node_t * +pe_create_node(const char *id, const char *uname, const char *type, + const char *score, pe_working_set_t * data_set) +{ + pe_node_t *new_node = NULL; + + if (pe_find_node(data_set->nodes, uname) != NULL) { + pcmk__config_warn("More than one node entry has name '%s'", uname); + } + + new_node = calloc(1, sizeof(pe_node_t)); + if (new_node == NULL) { + return NULL; + } + + new_node->weight = char2score(score); + new_node->details = calloc(1, sizeof(struct pe_node_shared_s)); + + if (new_node->details == NULL) { + free(new_node); + return NULL; + } + + crm_trace("Creating node for entry %s/%s", uname, id); + new_node->details->id = id; + new_node->details->uname = uname; + new_node->details->online = FALSE; + new_node->details->shutdown = FALSE; + new_node->details->rsc_discovery_enabled = TRUE; + new_node->details->running_rsc = NULL; + new_node->details->data_set = data_set; + + if (pcmk__str_eq(type, "member", pcmk__str_null_matches | pcmk__str_casei)) { + new_node->details->type = node_member; + + } else if (pcmk__str_eq(type, "remote", pcmk__str_casei)) { + new_node->details->type = node_remote; + pe__set_working_set_flags(data_set, pe_flag_have_remote_nodes); + + } else { + /* @COMPAT 'ping' is the default for backward compatibility, but it + * should be changed to 'member' at a compatibility break + */ + if (!pcmk__str_eq(type, "ping", pcmk__str_casei)) { + pcmk__config_warn("Node %s has unrecognized type '%s', " + "assuming 'ping'", pcmk__s(uname, "without name"), + type); + } + pe_warn_once(pe_wo_ping_node, + "Support for nodes of type 'ping' (such as %s) is " + "deprecated and will be removed in a future release", + pcmk__s(uname, "unnamed node")); + new_node->details->type = node_ping; + } + + new_node->details->attrs = pcmk__strkey_table(free, free); + + if (pe__is_guest_or_remote_node(new_node)) { + g_hash_table_insert(new_node->details->attrs, strdup(CRM_ATTR_KIND), + strdup("remote")); + } else { + g_hash_table_insert(new_node->details->attrs, strdup(CRM_ATTR_KIND), + strdup("cluster")); + } + + new_node->details->utilization = pcmk__strkey_table(free, free); + new_node->details->digest_cache = pcmk__strkey_table(free, + pe__free_digests); + + data_set->nodes = g_list_insert_sorted(data_set->nodes, new_node, + pe__cmp_node_name); + return new_node; +} + +static const char * +expand_remote_rsc_meta(xmlNode *xml_obj, xmlNode *parent, pe_working_set_t *data) +{ + xmlNode *attr_set = NULL; + xmlNode *attr = NULL; + + const char *container_id = ID(xml_obj); + const char *remote_name = NULL; + const char *remote_server = NULL; + const char *remote_port = NULL; + const char *connect_timeout = "60s"; + const char *remote_allow_migrate=NULL; + const char *is_managed = NULL; + + for (attr_set = pcmk__xe_first_child(xml_obj); attr_set != NULL; + attr_set = pcmk__xe_next(attr_set)) { + + if (!pcmk__str_eq((const char *)attr_set->name, XML_TAG_META_SETS, + pcmk__str_casei)) { + continue; + } + + for (attr = pcmk__xe_first_child(attr_set); attr != NULL; + attr = pcmk__xe_next(attr)) { + const char *value = crm_element_value(attr, XML_NVPAIR_ATTR_VALUE); + const char *name = crm_element_value(attr, XML_NVPAIR_ATTR_NAME); + + if (pcmk__str_eq(name, XML_RSC_ATTR_REMOTE_NODE, pcmk__str_casei)) { + remote_name = value; + } else if (pcmk__str_eq(name, "remote-addr", pcmk__str_casei)) { + remote_server = value; + } else if (pcmk__str_eq(name, "remote-port", pcmk__str_casei)) { + remote_port = value; + } else if (pcmk__str_eq(name, "remote-connect-timeout", pcmk__str_casei)) { + connect_timeout = value; + } else if (pcmk__str_eq(name, "remote-allow-migrate", pcmk__str_casei)) { + remote_allow_migrate=value; + } else if (pcmk__str_eq(name, XML_RSC_ATTR_MANAGED, pcmk__str_casei)) { + is_managed = value; + } + } + } + + if (remote_name == NULL) { + return NULL; + } + + if (pe_find_resource(data->resources, remote_name) != NULL) { + return NULL; + } + + pe_create_remote_xml(parent, remote_name, container_id, + remote_allow_migrate, is_managed, + connect_timeout, remote_server, remote_port); + return remote_name; +} + +static void +handle_startup_fencing(pe_working_set_t *data_set, pe_node_t *new_node) +{ + if ((new_node->details->type == node_remote) && (new_node->details->remote_rsc == NULL)) { + /* Ignore fencing for remote nodes that don't have a connection resource + * associated with them. This happens when remote node entries get left + * in the nodes section after the connection resource is removed. + */ + return; + } + + if (pcmk_is_set(data_set->flags, pe_flag_startup_fencing)) { + // All nodes are unclean until we've seen their status entry + new_node->details->unclean = TRUE; + + } else { + // Blind faith ... + new_node->details->unclean = FALSE; + } + + /* We need to be able to determine if a node's status section + * exists or not separate from whether the node is unclean. */ + new_node->details->unseen = TRUE; +} + +gboolean +unpack_nodes(xmlNode * xml_nodes, pe_working_set_t * data_set) +{ + xmlNode *xml_obj = NULL; + pe_node_t *new_node = NULL; + const char *id = NULL; + const char *uname = NULL; + const char *type = NULL; + const char *score = NULL; + + for (xml_obj = pcmk__xe_first_child(xml_nodes); xml_obj != NULL; + xml_obj = pcmk__xe_next(xml_obj)) { + + if (pcmk__str_eq((const char *)xml_obj->name, XML_CIB_TAG_NODE, pcmk__str_none)) { + new_node = NULL; + + id = crm_element_value(xml_obj, XML_ATTR_ID); + uname = crm_element_value(xml_obj, XML_ATTR_UNAME); + type = crm_element_value(xml_obj, XML_ATTR_TYPE); + score = crm_element_value(xml_obj, XML_RULE_ATTR_SCORE); + crm_trace("Processing node %s/%s", uname, id); + + if (id == NULL) { + pcmk__config_err("Ignoring <" XML_CIB_TAG_NODE + "> entry in configuration without id"); + continue; + } + new_node = pe_create_node(id, uname, type, score, data_set); + + if (new_node == NULL) { + return FALSE; + } + + handle_startup_fencing(data_set, new_node); + + add_node_attrs(xml_obj, new_node, FALSE, data_set); + + crm_trace("Done with node %s", crm_element_value(xml_obj, XML_ATTR_UNAME)); + } + } + + if (data_set->localhost && pe_find_node(data_set->nodes, data_set->localhost) == NULL) { + crm_info("Creating a fake local node"); + pe_create_node(data_set->localhost, data_set->localhost, NULL, 0, + data_set); + } + + return TRUE; +} + +static void +setup_container(pe_resource_t * rsc, pe_working_set_t * data_set) +{ + const char *container_id = NULL; + + if (rsc->children) { + g_list_foreach(rsc->children, (GFunc) setup_container, data_set); + return; + } + + container_id = g_hash_table_lookup(rsc->meta, XML_RSC_ATTR_CONTAINER); + if (container_id && !pcmk__str_eq(container_id, rsc->id, pcmk__str_casei)) { + pe_resource_t *container = pe_find_resource(data_set->resources, container_id); + + if (container) { + rsc->container = container; + pe__set_resource_flags(container, pe_rsc_is_container); + container->fillers = g_list_append(container->fillers, rsc); + pe_rsc_trace(rsc, "Resource %s's container is %s", rsc->id, container_id); + } else { + pe_err("Resource %s: Unknown resource container (%s)", rsc->id, container_id); + } + } +} + +gboolean +unpack_remote_nodes(xmlNode * xml_resources, pe_working_set_t * data_set) +{ + xmlNode *xml_obj = NULL; + + /* Create remote nodes and guest nodes from the resource configuration + * before unpacking resources. + */ + for (xml_obj = pcmk__xe_first_child(xml_resources); xml_obj != NULL; + xml_obj = pcmk__xe_next(xml_obj)) { + + const char *new_node_id = NULL; + + /* Check for remote nodes, which are defined by ocf:pacemaker:remote + * primitives. + */ + if (xml_contains_remote_node(xml_obj)) { + new_node_id = ID(xml_obj); + /* The "pe_find_node" check is here to make sure we don't iterate over + * an expanded node that has already been added to the node list. */ + if (new_node_id && pe_find_node(data_set->nodes, new_node_id) == NULL) { + crm_trace("Found remote node %s defined by resource %s", + new_node_id, ID(xml_obj)); + pe_create_node(new_node_id, new_node_id, "remote", NULL, + data_set); + } + continue; + } + + /* Check for guest nodes, which are defined by special meta-attributes + * of a primitive of any type (for example, VirtualDomain or Xen). + */ + if (pcmk__str_eq((const char *)xml_obj->name, XML_CIB_TAG_RESOURCE, pcmk__str_none)) { + /* This will add an ocf:pacemaker:remote primitive to the + * configuration for the guest node's connection, to be unpacked + * later. + */ + new_node_id = expand_remote_rsc_meta(xml_obj, xml_resources, data_set); + if (new_node_id && pe_find_node(data_set->nodes, new_node_id) == NULL) { + crm_trace("Found guest node %s in resource %s", + new_node_id, ID(xml_obj)); + pe_create_node(new_node_id, new_node_id, "remote", NULL, + data_set); + } + continue; + } + + /* Check for guest nodes inside a group. Clones are currently not + * supported as guest nodes. + */ + if (pcmk__str_eq((const char *)xml_obj->name, XML_CIB_TAG_GROUP, pcmk__str_none)) { + xmlNode *xml_obj2 = NULL; + for (xml_obj2 = pcmk__xe_first_child(xml_obj); xml_obj2 != NULL; + xml_obj2 = pcmk__xe_next(xml_obj2)) { + + new_node_id = expand_remote_rsc_meta(xml_obj2, xml_resources, data_set); + + if (new_node_id && pe_find_node(data_set->nodes, new_node_id) == NULL) { + crm_trace("Found guest node %s in resource %s inside group %s", + new_node_id, ID(xml_obj2), ID(xml_obj)); + pe_create_node(new_node_id, new_node_id, "remote", NULL, + data_set); + } + } + } + } + return TRUE; +} + +/* Call this after all the nodes and resources have been + * unpacked, but before the status section is read. + * + * A remote node's online status is reflected by the state + * of the remote node's connection resource. We need to link + * the remote node to this connection resource so we can have + * easy access to the connection resource during the scheduler calculations. + */ +static void +link_rsc2remotenode(pe_working_set_t *data_set, pe_resource_t *new_rsc) +{ + pe_node_t *remote_node = NULL; + + if (new_rsc->is_remote_node == FALSE) { + return; + } + + if (pcmk_is_set(data_set->flags, pe_flag_quick_location)) { + /* remote_nodes and remote_resources are not linked in quick location calculations */ + return; + } + + remote_node = pe_find_node(data_set->nodes, new_rsc->id); + CRM_CHECK(remote_node != NULL, return); + + pe_rsc_trace(new_rsc, "Linking remote connection resource %s to %s", + new_rsc->id, pe__node_name(remote_node)); + remote_node->details->remote_rsc = new_rsc; + + if (new_rsc->container == NULL) { + /* Handle start-up fencing for remote nodes (as opposed to guest nodes) + * the same as is done for cluster nodes. + */ + handle_startup_fencing(data_set, remote_node); + + } else { + /* pe_create_node() marks the new node as "remote" or "cluster"; now + * that we know the node is a guest node, update it correctly. + */ + g_hash_table_replace(remote_node->details->attrs, strdup(CRM_ATTR_KIND), + strdup("container")); + } +} + +static void +destroy_tag(gpointer data) +{ + pe_tag_t *tag = data; + + if (tag) { + free(tag->id); + g_list_free_full(tag->refs, free); + free(tag); + } +} + +/*! + * \internal + * \brief Parse configuration XML for resource information + * + * \param[in] xml_resources Top of resource configuration XML + * \param[in,out] data_set Where to put resource information + * + * \return TRUE + * + * \note unpack_remote_nodes() MUST be called before this, so that the nodes can + * be used when pe__unpack_resource() calls resource_location() + */ +gboolean +unpack_resources(const xmlNode *xml_resources, pe_working_set_t * data_set) +{ + xmlNode *xml_obj = NULL; + GList *gIter = NULL; + + data_set->template_rsc_sets = pcmk__strkey_table(free, destroy_tag); + + for (xml_obj = pcmk__xe_first_child(xml_resources); xml_obj != NULL; + xml_obj = pcmk__xe_next(xml_obj)) { + + pe_resource_t *new_rsc = NULL; + const char *id = ID(xml_obj); + + if (pcmk__str_empty(id)) { + pcmk__config_err("Ignoring <%s> resource without ID", + crm_element_name(xml_obj)); + continue; + } + + if (pcmk__str_eq((const char *) xml_obj->name, XML_CIB_TAG_RSC_TEMPLATE, + pcmk__str_none)) { + if (g_hash_table_lookup_extended(data_set->template_rsc_sets, id, + NULL, NULL) == FALSE) { + /* Record the template's ID for the knowledge of its existence anyway. */ + g_hash_table_insert(data_set->template_rsc_sets, strdup(id), NULL); + } + continue; + } + + crm_trace("Unpacking <%s " XML_ATTR_ID "='%s'>", + crm_element_name(xml_obj), id); + if (pe__unpack_resource(xml_obj, &new_rsc, NULL, + data_set) == pcmk_rc_ok) { + data_set->resources = g_list_append(data_set->resources, new_rsc); + pe_rsc_trace(new_rsc, "Added resource %s", new_rsc->id); + + } else { + pcmk__config_err("Ignoring <%s> resource '%s' " + "because configuration is invalid", + crm_element_name(xml_obj), id); + } + } + + for (gIter = data_set->resources; gIter != NULL; gIter = gIter->next) { + pe_resource_t *rsc = (pe_resource_t *) gIter->data; + + setup_container(rsc, data_set); + link_rsc2remotenode(data_set, rsc); + } + + data_set->resources = g_list_sort(data_set->resources, + pe__cmp_rsc_priority); + if (pcmk_is_set(data_set->flags, pe_flag_quick_location)) { + /* Ignore */ + + } else if (pcmk_is_set(data_set->flags, pe_flag_stonith_enabled) + && !pcmk_is_set(data_set->flags, pe_flag_have_stonith_resource)) { + + pcmk__config_err("Resource start-up disabled since no STONITH resources have been defined"); + pcmk__config_err("Either configure some or disable STONITH with the stonith-enabled option"); + pcmk__config_err("NOTE: Clusters with shared data need STONITH to ensure data integrity"); + } + + return TRUE; +} + +gboolean +unpack_tags(xmlNode * xml_tags, pe_working_set_t * data_set) +{ + xmlNode *xml_tag = NULL; + + data_set->tags = pcmk__strkey_table(free, destroy_tag); + + for (xml_tag = pcmk__xe_first_child(xml_tags); xml_tag != NULL; + xml_tag = pcmk__xe_next(xml_tag)) { + + xmlNode *xml_obj_ref = NULL; + const char *tag_id = ID(xml_tag); + + if (!pcmk__str_eq((const char *)xml_tag->name, XML_CIB_TAG_TAG, pcmk__str_none)) { + continue; + } + + if (tag_id == NULL) { + pcmk__config_err("Ignoring <%s> without " XML_ATTR_ID, + crm_element_name(xml_tag)); + continue; + } + + for (xml_obj_ref = pcmk__xe_first_child(xml_tag); xml_obj_ref != NULL; + xml_obj_ref = pcmk__xe_next(xml_obj_ref)) { + + const char *obj_ref = ID(xml_obj_ref); + + if (!pcmk__str_eq((const char *)xml_obj_ref->name, XML_CIB_TAG_OBJ_REF, pcmk__str_none)) { + continue; + } + + if (obj_ref == NULL) { + pcmk__config_err("Ignoring <%s> for tag '%s' without " XML_ATTR_ID, + crm_element_name(xml_obj_ref), tag_id); + continue; + } + + if (add_tag_ref(data_set->tags, tag_id, obj_ref) == FALSE) { + return FALSE; + } + } + } + + return TRUE; +} + +/* The ticket state section: + * "/cib/status/tickets/ticket_state" */ +static gboolean +unpack_ticket_state(xmlNode * xml_ticket, pe_working_set_t * data_set) +{ + const char *ticket_id = NULL; + const char *granted = NULL; + const char *last_granted = NULL; + const char *standby = NULL; + xmlAttrPtr xIter = NULL; + + pe_ticket_t *ticket = NULL; + + ticket_id = ID(xml_ticket); + if (pcmk__str_empty(ticket_id)) { + return FALSE; + } + + crm_trace("Processing ticket state for %s", ticket_id); + + ticket = g_hash_table_lookup(data_set->tickets, ticket_id); + if (ticket == NULL) { + ticket = ticket_new(ticket_id, data_set); + if (ticket == NULL) { + return FALSE; + } + } + + for (xIter = xml_ticket->properties; xIter; xIter = xIter->next) { + const char *prop_name = (const char *)xIter->name; + const char *prop_value = crm_element_value(xml_ticket, prop_name); + + if (pcmk__str_eq(prop_name, XML_ATTR_ID, pcmk__str_none)) { + continue; + } + g_hash_table_replace(ticket->state, strdup(prop_name), strdup(prop_value)); + } + + granted = g_hash_table_lookup(ticket->state, "granted"); + if (granted && crm_is_true(granted)) { + ticket->granted = TRUE; + crm_info("We have ticket '%s'", ticket->id); + } else { + ticket->granted = FALSE; + crm_info("We do not have ticket '%s'", ticket->id); + } + + last_granted = g_hash_table_lookup(ticket->state, "last-granted"); + if (last_granted) { + long long last_granted_ll; + + pcmk__scan_ll(last_granted, &last_granted_ll, 0LL); + ticket->last_granted = (time_t) last_granted_ll; + } + + standby = g_hash_table_lookup(ticket->state, "standby"); + if (standby && crm_is_true(standby)) { + ticket->standby = TRUE; + if (ticket->granted) { + crm_info("Granted ticket '%s' is in standby-mode", ticket->id); + } + } else { + ticket->standby = FALSE; + } + + crm_trace("Done with ticket state for %s", ticket_id); + + return TRUE; +} + +static gboolean +unpack_tickets_state(xmlNode * xml_tickets, pe_working_set_t * data_set) +{ + xmlNode *xml_obj = NULL; + + for (xml_obj = pcmk__xe_first_child(xml_tickets); xml_obj != NULL; + xml_obj = pcmk__xe_next(xml_obj)) { + + if (!pcmk__str_eq((const char *)xml_obj->name, XML_CIB_TAG_TICKET_STATE, pcmk__str_none)) { + continue; + } + unpack_ticket_state(xml_obj, data_set); + } + + return TRUE; +} + +static void +unpack_handle_remote_attrs(pe_node_t *this_node, const xmlNode *state, + pe_working_set_t *data_set) +{ + const char *resource_discovery_enabled = NULL; + const xmlNode *attrs = NULL; + pe_resource_t *rsc = NULL; + + if (!pcmk__str_eq((const char *)state->name, XML_CIB_TAG_STATE, pcmk__str_none)) { + return; + } + + if ((this_node == NULL) || !pe__is_guest_or_remote_node(this_node)) { + return; + } + crm_trace("Processing Pacemaker Remote node %s", pe__node_name(this_node)); + + pcmk__scan_min_int(crm_element_value(state, XML_NODE_IS_MAINTENANCE), + &(this_node->details->remote_maintenance), 0); + + rsc = this_node->details->remote_rsc; + if (this_node->details->remote_requires_reset == FALSE) { + this_node->details->unclean = FALSE; + this_node->details->unseen = FALSE; + } + attrs = find_xml_node(state, XML_TAG_TRANSIENT_NODEATTRS, FALSE); + add_node_attrs(attrs, this_node, TRUE, data_set); + + if (pe__shutdown_requested(this_node)) { + crm_info("%s is shutting down", pe__node_name(this_node)); + this_node->details->shutdown = TRUE; + } + + if (crm_is_true(pe_node_attribute_raw(this_node, "standby"))) { + crm_info("%s is in standby mode", pe__node_name(this_node)); + this_node->details->standby = TRUE; + } + + if (crm_is_true(pe_node_attribute_raw(this_node, "maintenance")) || + ((rsc != NULL) && !pcmk_is_set(rsc->flags, pe_rsc_managed))) { + crm_info("%s is in maintenance mode", pe__node_name(this_node)); + this_node->details->maintenance = TRUE; + } + + resource_discovery_enabled = pe_node_attribute_raw(this_node, XML_NODE_ATTR_RSC_DISCOVERY); + if (resource_discovery_enabled && !crm_is_true(resource_discovery_enabled)) { + if (pe__is_remote_node(this_node) + && !pcmk_is_set(data_set->flags, pe_flag_stonith_enabled)) { + crm_warn("Ignoring " XML_NODE_ATTR_RSC_DISCOVERY + " attribute on Pacemaker Remote node %s" + " because fencing is disabled", + pe__node_name(this_node)); + } else { + /* This is either a remote node with fencing enabled, or a guest + * node. We don't care whether fencing is enabled when fencing guest + * nodes, because they are "fenced" by recovering their containing + * resource. + */ + crm_info("%s has resource discovery disabled", + pe__node_name(this_node)); + this_node->details->rsc_discovery_enabled = FALSE; + } + } +} + +/*! + * \internal + * \brief Unpack a cluster node's transient attributes + * + * \param[in] state CIB node state XML + * \param[in,out] node Cluster node whose attributes are being unpacked + * \param[in,out] data_set Cluster working set + */ +static void +unpack_transient_attributes(const xmlNode *state, pe_node_t *node, + pe_working_set_t *data_set) +{ + const char *discovery = NULL; + const xmlNode *attrs = find_xml_node(state, XML_TAG_TRANSIENT_NODEATTRS, + FALSE); + + add_node_attrs(attrs, node, TRUE, data_set); + + if (crm_is_true(pe_node_attribute_raw(node, "standby"))) { + crm_info("%s is in standby mode", pe__node_name(node)); + node->details->standby = TRUE; + } + + if (crm_is_true(pe_node_attribute_raw(node, "maintenance"))) { + crm_info("%s is in maintenance mode", pe__node_name(node)); + node->details->maintenance = TRUE; + } + + discovery = pe_node_attribute_raw(node, XML_NODE_ATTR_RSC_DISCOVERY); + if ((discovery != NULL) && !crm_is_true(discovery)) { + crm_warn("Ignoring " XML_NODE_ATTR_RSC_DISCOVERY + " attribute for %s because disabling resource discovery " + "is not allowed for cluster nodes", pe__node_name(node)); + } +} + +/*! + * \internal + * \brief Unpack a node state entry (first pass) + * + * Unpack one node state entry from status. This unpacks information from the + * node_state element itself and node attributes inside it, but not the + * resource history inside it. Multiple passes through the status are needed to + * fully unpack everything. + * + * \param[in] state CIB node state XML + * \param[in,out] data_set Cluster working set + */ +static void +unpack_node_state(const xmlNode *state, pe_working_set_t *data_set) +{ + const char *id = NULL; + const char *uname = NULL; + pe_node_t *this_node = NULL; + + id = crm_element_value(state, XML_ATTR_ID); + if (id == NULL) { + crm_warn("Ignoring malformed " XML_CIB_TAG_STATE " entry without " + XML_ATTR_ID); + return; + } + + uname = crm_element_value(state, XML_ATTR_UNAME); + if (uname == NULL) { + crm_warn("Ignoring malformed " XML_CIB_TAG_STATE " entry without " + XML_ATTR_UNAME); + return; + } + + this_node = pe_find_node_any(data_set->nodes, id, uname); + if (this_node == NULL) { + pcmk__config_warn("Ignoring recorded node state for '%s' because " + "it is no longer in the configuration", uname); + return; + } + + if (pe__is_guest_or_remote_node(this_node)) { + /* We can't determine the online status of Pacemaker Remote nodes until + * after all resource history has been unpacked. In this first pass, we + * do need to mark whether the node has been fenced, as this plays a + * role during unpacking cluster node resource state. + */ + pcmk__scan_min_int(crm_element_value(state, XML_NODE_IS_FENCED), + &(this_node->details->remote_was_fenced), 0); + return; + } + + unpack_transient_attributes(state, this_node, data_set); + + /* Provisionally mark this cluster node as clean. We have at least seen it + * in the current cluster's lifetime. + */ + this_node->details->unclean = FALSE; + this_node->details->unseen = FALSE; + + crm_trace("Determining online status of cluster node %s (id %s)", + pe__node_name(this_node), id); + determine_online_status(state, this_node, data_set); + + if (!pcmk_is_set(data_set->flags, pe_flag_have_quorum) + && this_node->details->online + && (data_set->no_quorum_policy == no_quorum_suicide)) { + /* Everything else should flow from this automatically + * (at least until the scheduler becomes able to migrate off + * healthy resources) + */ + pe_fence_node(data_set, this_node, "cluster does not have quorum", + FALSE); + } +} + +/*! + * \internal + * \brief Unpack nodes' resource history as much as possible + * + * Unpack as many nodes' resource history as possible in one pass through the + * status. We need to process Pacemaker Remote nodes' connections/containers + * before unpacking their history; the connection/container history will be + * in another node's history, so it might take multiple passes to unpack + * everything. + * + * \param[in] status CIB XML status section + * \param[in] fence If true, treat any not-yet-unpacked nodes as unseen + * \param[in,out] data_set Cluster working set + * + * \return Standard Pacemaker return code (specifically pcmk_rc_ok if done, + * or EAGAIN if more unpacking remains to be done) + */ +static int +unpack_node_history(const xmlNode *status, bool fence, + pe_working_set_t *data_set) +{ + int rc = pcmk_rc_ok; + + // Loop through all node_state entries in CIB status + for (const xmlNode *state = first_named_child(status, XML_CIB_TAG_STATE); + state != NULL; state = crm_next_same_xml(state)) { + + const char *id = ID(state); + const char *uname = crm_element_value(state, XML_ATTR_UNAME); + pe_node_t *this_node = NULL; + + if ((id == NULL) || (uname == NULL)) { + // Warning already logged in first pass through status section + crm_trace("Not unpacking resource history from malformed " + XML_CIB_TAG_STATE " without id and/or uname"); + continue; + } + + this_node = pe_find_node_any(data_set->nodes, id, uname); + if (this_node == NULL) { + // Warning already logged in first pass through status section + crm_trace("Not unpacking resource history for node %s because " + "no longer in configuration", id); + continue; + } + + if (this_node->details->unpacked) { + crm_trace("Not unpacking resource history for node %s because " + "already unpacked", id); + continue; + } + + if (fence) { + // We're processing all remaining nodes + + } else if (pe__is_guest_node(this_node)) { + /* We can unpack a guest node's history only after we've unpacked + * other resource history to the point that we know that the node's + * connection and containing resource are both up. + */ + pe_resource_t *rsc = this_node->details->remote_rsc; + + if ((rsc == NULL) || (rsc->role != RSC_ROLE_STARTED) + || (rsc->container->role != RSC_ROLE_STARTED)) { + crm_trace("Not unpacking resource history for guest node %s " + "because container and connection are not known to " + "be up", id); + continue; + } + + } else if (pe__is_remote_node(this_node)) { + /* We can unpack a remote node's history only after we've unpacked + * other resource history to the point that we know that the node's + * connection is up, with the exception of when shutdown locks are + * in use. + */ + pe_resource_t *rsc = this_node->details->remote_rsc; + + if ((rsc == NULL) + || (!pcmk_is_set(data_set->flags, pe_flag_shutdown_lock) + && (rsc->role != RSC_ROLE_STARTED))) { + crm_trace("Not unpacking resource history for remote node %s " + "because connection is not known to be up", id); + continue; + } + + /* If fencing and shutdown locks are disabled and we're not processing + * unseen nodes, then we don't want to unpack offline nodes until online + * nodes have been unpacked. This allows us to number active clone + * instances first. + */ + } else if (!pcmk_any_flags_set(data_set->flags, pe_flag_stonith_enabled + |pe_flag_shutdown_lock) + && !this_node->details->online) { + crm_trace("Not unpacking resource history for offline " + "cluster node %s", id); + continue; + } + + if (pe__is_guest_or_remote_node(this_node)) { + determine_remote_online_status(data_set, this_node); + unpack_handle_remote_attrs(this_node, state, data_set); + } + + crm_trace("Unpacking resource history for %snode %s", + (fence? "unseen " : ""), id); + + this_node->details->unpacked = TRUE; + unpack_node_lrm(this_node, state, data_set); + + rc = EAGAIN; // Other node histories might depend on this one + } + return rc; +} + +/* remove nodes that are down, stopping */ +/* create positive rsc_to_node constraints between resources and the nodes they are running on */ +/* anything else? */ +gboolean +unpack_status(xmlNode * status, pe_working_set_t * data_set) +{ + xmlNode *state = NULL; + + crm_trace("Beginning unpack"); + + if (data_set->tickets == NULL) { + data_set->tickets = pcmk__strkey_table(free, destroy_ticket); + } + + for (state = pcmk__xe_first_child(status); state != NULL; + state = pcmk__xe_next(state)) { + + if (pcmk__str_eq((const char *)state->name, XML_CIB_TAG_TICKETS, pcmk__str_none)) { + unpack_tickets_state((xmlNode *) state, data_set); + + } else if (pcmk__str_eq((const char *)state->name, XML_CIB_TAG_STATE, pcmk__str_none)) { + unpack_node_state(state, data_set); + } + } + + while (unpack_node_history(status, FALSE, data_set) == EAGAIN) { + crm_trace("Another pass through node resource histories is needed"); + } + + // Now catch any nodes we didn't see + unpack_node_history(status, + pcmk_is_set(data_set->flags, pe_flag_stonith_enabled), + data_set); + + /* Now that we know where resources are, we can schedule stops of containers + * with failed bundle connections + */ + if (data_set->stop_needed != NULL) { + for (GList *item = data_set->stop_needed; item; item = item->next) { + pe_resource_t *container = item->data; + pe_node_t *node = pe__current_node(container); + + if (node) { + stop_action(container, node, FALSE); + } + } + g_list_free(data_set->stop_needed); + data_set->stop_needed = NULL; + } + + /* Now that we know status of all Pacemaker Remote connections and nodes, + * we can stop connections for node shutdowns, and check the online status + * of remote/guest nodes that didn't have any node history to unpack. + */ + for (GList *gIter = data_set->nodes; gIter != NULL; gIter = gIter->next) { + pe_node_t *this_node = gIter->data; + + if (!pe__is_guest_or_remote_node(this_node)) { + continue; + } + if (this_node->details->shutdown + && (this_node->details->remote_rsc != NULL)) { + pe__set_next_role(this_node->details->remote_rsc, RSC_ROLE_STOPPED, + "remote shutdown"); + } + if (!this_node->details->unpacked) { + determine_remote_online_status(data_set, this_node); + } + } + + return TRUE; +} + +static gboolean +determine_online_status_no_fencing(pe_working_set_t *data_set, + const xmlNode *node_state, + pe_node_t *this_node) +{ + gboolean online = FALSE; + const char *join = crm_element_value(node_state, XML_NODE_JOIN_STATE); + const char *is_peer = crm_element_value(node_state, XML_NODE_IS_PEER); + const char *in_cluster = crm_element_value(node_state, XML_NODE_IN_CLUSTER); + const char *exp_state = crm_element_value(node_state, XML_NODE_EXPECTED); + + if (!crm_is_true(in_cluster)) { + crm_trace("Node is down: in_cluster=%s", + pcmk__s(in_cluster, "<null>")); + + } else if (pcmk__str_eq(is_peer, ONLINESTATUS, pcmk__str_casei)) { + if (pcmk__str_eq(join, CRMD_JOINSTATE_MEMBER, pcmk__str_casei)) { + online = TRUE; + } else { + crm_debug("Node is not ready to run resources: %s", join); + } + + } else if (this_node->details->expected_up == FALSE) { + crm_trace("Controller is down: " + "in_cluster=%s is_peer=%s join=%s expected=%s", + pcmk__s(in_cluster, "<null>"), pcmk__s(is_peer, "<null>"), + pcmk__s(join, "<null>"), pcmk__s(exp_state, "<null>")); + + } else { + /* mark it unclean */ + pe_fence_node(data_set, this_node, "peer is unexpectedly down", FALSE); + crm_info("in_cluster=%s is_peer=%s join=%s expected=%s", + pcmk__s(in_cluster, "<null>"), pcmk__s(is_peer, "<null>"), + pcmk__s(join, "<null>"), pcmk__s(exp_state, "<null>")); + } + return online; +} + +static gboolean +determine_online_status_fencing(pe_working_set_t *data_set, + const xmlNode *node_state, pe_node_t *this_node) +{ + gboolean online = FALSE; + gboolean do_terminate = FALSE; + bool crmd_online = FALSE; + const char *join = crm_element_value(node_state, XML_NODE_JOIN_STATE); + const char *is_peer = crm_element_value(node_state, XML_NODE_IS_PEER); + const char *in_cluster = crm_element_value(node_state, XML_NODE_IN_CLUSTER); + const char *exp_state = crm_element_value(node_state, XML_NODE_EXPECTED); + const char *terminate = pe_node_attribute_raw(this_node, "terminate"); + +/* + - XML_NODE_IN_CLUSTER ::= true|false + - XML_NODE_IS_PEER ::= online|offline + - XML_NODE_JOIN_STATE ::= member|down|pending|banned + - XML_NODE_EXPECTED ::= member|down +*/ + + if (crm_is_true(terminate)) { + do_terminate = TRUE; + + } else if (terminate != NULL && strlen(terminate) > 0) { + /* could be a time() value */ + char t = terminate[0]; + + if (t != '0' && isdigit(t)) { + do_terminate = TRUE; + } + } + + crm_trace("%s: in_cluster=%s is_peer=%s join=%s expected=%s term=%d", + pe__node_name(this_node), pcmk__s(in_cluster, "<null>"), + pcmk__s(is_peer, "<null>"), pcmk__s(join, "<null>"), + pcmk__s(exp_state, "<null>"), do_terminate); + + online = crm_is_true(in_cluster); + crmd_online = pcmk__str_eq(is_peer, ONLINESTATUS, pcmk__str_casei); + if (exp_state == NULL) { + exp_state = CRMD_JOINSTATE_DOWN; + } + + if (this_node->details->shutdown) { + crm_debug("%s is shutting down", pe__node_name(this_node)); + + /* Slightly different criteria since we can't shut down a dead peer */ + online = crmd_online; + + } else if (in_cluster == NULL) { + pe_fence_node(data_set, this_node, "peer has not been seen by the cluster", FALSE); + + } else if (pcmk__str_eq(join, CRMD_JOINSTATE_NACK, pcmk__str_casei)) { + pe_fence_node(data_set, this_node, + "peer failed Pacemaker membership criteria", FALSE); + + } else if (do_terminate == FALSE && pcmk__str_eq(exp_state, CRMD_JOINSTATE_DOWN, pcmk__str_casei)) { + + if (crm_is_true(in_cluster) || crmd_online) { + crm_info("- %s is not ready to run resources", + pe__node_name(this_node)); + this_node->details->standby = TRUE; + this_node->details->pending = TRUE; + + } else { + crm_trace("%s is down or still coming up", + pe__node_name(this_node)); + } + + } else if (do_terminate && pcmk__str_eq(join, CRMD_JOINSTATE_DOWN, pcmk__str_casei) + && crm_is_true(in_cluster) == FALSE && !crmd_online) { + crm_info("%s was just shot", pe__node_name(this_node)); + online = FALSE; + + } else if (crm_is_true(in_cluster) == FALSE) { + // Consider `priority-fencing-delay` for lost nodes + pe_fence_node(data_set, this_node, "peer is no longer part of the cluster", TRUE); + + } else if (!crmd_online) { + pe_fence_node(data_set, this_node, "peer process is no longer available", FALSE); + + /* Everything is running at this point, now check join state */ + } else if (do_terminate) { + pe_fence_node(data_set, this_node, "termination was requested", FALSE); + + } else if (pcmk__str_eq(join, CRMD_JOINSTATE_MEMBER, pcmk__str_casei)) { + crm_info("%s is active", pe__node_name(this_node)); + + } else if (pcmk__strcase_any_of(join, CRMD_JOINSTATE_PENDING, CRMD_JOINSTATE_DOWN, NULL)) { + crm_info("%s is not ready to run resources", pe__node_name(this_node)); + this_node->details->standby = TRUE; + this_node->details->pending = TRUE; + + } else { + pe_fence_node(data_set, this_node, "peer was in an unknown state", FALSE); + crm_warn("%s: in-cluster=%s is-peer=%s join=%s expected=%s term=%d shutdown=%d", + pe__node_name(this_node), pcmk__s(in_cluster, "<null>"), + pcmk__s(is_peer, "<null>"), pcmk__s(join, "<null>"), + pcmk__s(exp_state, "<null>"), do_terminate, + this_node->details->shutdown); + } + + return online; +} + +static void +determine_remote_online_status(pe_working_set_t * data_set, pe_node_t * this_node) +{ + pe_resource_t *rsc = this_node->details->remote_rsc; + pe_resource_t *container = NULL; + pe_node_t *host = NULL; + + /* If there is a node state entry for a (former) Pacemaker Remote node + * but no resource creating that node, the node's connection resource will + * be NULL. Consider it an offline remote node in that case. + */ + if (rsc == NULL) { + this_node->details->online = FALSE; + goto remote_online_done; + } + + container = rsc->container; + + if (container && pcmk__list_of_1(rsc->running_on)) { + host = rsc->running_on->data; + } + + /* If the resource is currently started, mark it online. */ + if (rsc->role == RSC_ROLE_STARTED) { + crm_trace("%s node %s presumed ONLINE because connection resource is started", + (container? "Guest" : "Remote"), this_node->details->id); + this_node->details->online = TRUE; + } + + /* consider this node shutting down if transitioning start->stop */ + if (rsc->role == RSC_ROLE_STARTED && rsc->next_role == RSC_ROLE_STOPPED) { + crm_trace("%s node %s shutting down because connection resource is stopping", + (container? "Guest" : "Remote"), this_node->details->id); + this_node->details->shutdown = TRUE; + } + + /* Now check all the failure conditions. */ + if(container && pcmk_is_set(container->flags, pe_rsc_failed)) { + crm_trace("Guest node %s UNCLEAN because guest resource failed", + this_node->details->id); + this_node->details->online = FALSE; + this_node->details->remote_requires_reset = TRUE; + + } else if (pcmk_is_set(rsc->flags, pe_rsc_failed)) { + crm_trace("%s node %s OFFLINE because connection resource failed", + (container? "Guest" : "Remote"), this_node->details->id); + this_node->details->online = FALSE; + + } else if (rsc->role == RSC_ROLE_STOPPED + || (container && container->role == RSC_ROLE_STOPPED)) { + + crm_trace("%s node %s OFFLINE because its resource is stopped", + (container? "Guest" : "Remote"), this_node->details->id); + this_node->details->online = FALSE; + this_node->details->remote_requires_reset = FALSE; + + } else if (host && (host->details->online == FALSE) + && host->details->unclean) { + crm_trace("Guest node %s UNCLEAN because host is unclean", + this_node->details->id); + this_node->details->online = FALSE; + this_node->details->remote_requires_reset = TRUE; + } + +remote_online_done: + crm_trace("Remote node %s online=%s", + this_node->details->id, this_node->details->online ? "TRUE" : "FALSE"); +} + +static void +determine_online_status(const xmlNode *node_state, pe_node_t *this_node, + pe_working_set_t *data_set) +{ + gboolean online = FALSE; + const char *exp_state = crm_element_value(node_state, XML_NODE_EXPECTED); + + CRM_CHECK(this_node != NULL, return); + + this_node->details->shutdown = FALSE; + this_node->details->expected_up = FALSE; + + if (pe__shutdown_requested(this_node)) { + this_node->details->shutdown = TRUE; + + } else if (pcmk__str_eq(exp_state, CRMD_JOINSTATE_MEMBER, pcmk__str_casei)) { + this_node->details->expected_up = TRUE; + } + + if (this_node->details->type == node_ping) { + this_node->details->unclean = FALSE; + online = FALSE; /* As far as resource management is concerned, + * the node is safely offline. + * Anyone caught abusing this logic will be shot + */ + + } else if (!pcmk_is_set(data_set->flags, pe_flag_stonith_enabled)) { + online = determine_online_status_no_fencing(data_set, node_state, this_node); + + } else { + online = determine_online_status_fencing(data_set, node_state, this_node); + } + + if (online) { + this_node->details->online = TRUE; + + } else { + /* remove node from contention */ + this_node->fixed = TRUE; // @COMPAT deprecated and unused + this_node->weight = -INFINITY; + } + + if (online && this_node->details->shutdown) { + /* don't run resources here */ + this_node->fixed = TRUE; // @COMPAT deprecated and unused + this_node->weight = -INFINITY; + } + + if (this_node->details->type == node_ping) { + crm_info("%s is not a Pacemaker node", pe__node_name(this_node)); + + } else if (this_node->details->unclean) { + pe_proc_warn("%s is unclean", pe__node_name(this_node)); + + } else if (this_node->details->online) { + crm_info("%s is %s", pe__node_name(this_node), + this_node->details->shutdown ? "shutting down" : + this_node->details->pending ? "pending" : + this_node->details->standby ? "standby" : + this_node->details->maintenance ? "maintenance" : "online"); + + } else { + crm_trace("%s is offline", pe__node_name(this_node)); + } +} + +/*! + * \internal + * \brief Find the end of a resource's name, excluding any clone suffix + * + * \param[in] id Resource ID to check + * + * \return Pointer to last character of resource's base name + */ +const char * +pe_base_name_end(const char *id) +{ + if (!pcmk__str_empty(id)) { + const char *end = id + strlen(id) - 1; + + for (const char *s = end; s > id; --s) { + switch (*s) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + break; + case ':': + return (s == end)? s : (s - 1); + default: + return end; + } + } + return end; + } + return NULL; +} + +/*! + * \internal + * \brief Get a resource name excluding any clone suffix + * + * \param[in] last_rsc_id Resource ID to check + * + * \return Pointer to newly allocated string with resource's base name + * \note It is the caller's responsibility to free() the result. + * This asserts on error, so callers can assume result is not NULL. + */ +char * +clone_strip(const char *last_rsc_id) +{ + const char *end = pe_base_name_end(last_rsc_id); + char *basename = NULL; + + CRM_ASSERT(end); + basename = strndup(last_rsc_id, end - last_rsc_id + 1); + CRM_ASSERT(basename); + return basename; +} + +/*! + * \internal + * \brief Get the name of the first instance of a cloned resource + * + * \param[in] last_rsc_id Resource ID to check + * + * \return Pointer to newly allocated string with resource's base name plus :0 + * \note It is the caller's responsibility to free() the result. + * This asserts on error, so callers can assume result is not NULL. + */ +char * +clone_zero(const char *last_rsc_id) +{ + const char *end = pe_base_name_end(last_rsc_id); + size_t base_name_len = end - last_rsc_id + 1; + char *zero = NULL; + + CRM_ASSERT(end); + zero = calloc(base_name_len + 3, sizeof(char)); + CRM_ASSERT(zero); + memcpy(zero, last_rsc_id, base_name_len); + zero[base_name_len] = ':'; + zero[base_name_len + 1] = '0'; + return zero; +} + +static pe_resource_t * +create_fake_resource(const char *rsc_id, const xmlNode *rsc_entry, + pe_working_set_t *data_set) +{ + pe_resource_t *rsc = NULL; + xmlNode *xml_rsc = create_xml_node(NULL, XML_CIB_TAG_RESOURCE); + + copy_in_properties(xml_rsc, rsc_entry); + crm_xml_add(xml_rsc, XML_ATTR_ID, rsc_id); + crm_log_xml_debug(xml_rsc, "Orphan resource"); + + if (pe__unpack_resource(xml_rsc, &rsc, NULL, data_set) != pcmk_rc_ok) { + return NULL; + } + + if (xml_contains_remote_node(xml_rsc)) { + pe_node_t *node; + + crm_debug("Detected orphaned remote node %s", rsc_id); + node = pe_find_node(data_set->nodes, rsc_id); + if (node == NULL) { + node = pe_create_node(rsc_id, rsc_id, "remote", NULL, data_set); + } + link_rsc2remotenode(data_set, rsc); + + if (node) { + crm_trace("Setting node %s as shutting down due to orphaned connection resource", rsc_id); + node->details->shutdown = TRUE; + } + } + + if (crm_element_value(rsc_entry, XML_RSC_ATTR_CONTAINER)) { + /* This orphaned rsc needs to be mapped to a container. */ + crm_trace("Detected orphaned container filler %s", rsc_id); + pe__set_resource_flags(rsc, pe_rsc_orphan_container_filler); + } + pe__set_resource_flags(rsc, pe_rsc_orphan); + data_set->resources = g_list_append(data_set->resources, rsc); + return rsc; +} + +/*! + * \internal + * \brief Create orphan instance for anonymous clone resource history + * + * \param[in,out] parent Clone resource that orphan will be added to + * \param[in] rsc_id Orphan's resource ID + * \param[in] node Where orphan is active (for logging only) + * \param[in,out] data_set Cluster working set + * + * \return Newly added orphaned instance of \p parent + */ +static pe_resource_t * +create_anonymous_orphan(pe_resource_t *parent, const char *rsc_id, + const pe_node_t *node, pe_working_set_t *data_set) +{ + pe_resource_t *top = pe__create_clone_child(parent, data_set); + + // find_rsc() because we might be a cloned group + pe_resource_t *orphan = top->fns->find_rsc(top, rsc_id, NULL, pe_find_clone); + + pe_rsc_debug(parent, "Created orphan %s for %s: %s on %s", + top->id, parent->id, rsc_id, pe__node_name(node)); + return orphan; +} + +/*! + * \internal + * \brief Check a node for an instance of an anonymous clone + * + * Return a child instance of the specified anonymous clone, in order of + * preference: (1) the instance running on the specified node, if any; + * (2) an inactive instance (i.e. within the total of clone-max instances); + * (3) a newly created orphan (i.e. clone-max instances are already active). + * + * \param[in,out] data_set Cluster information + * \param[in] node Node on which to check for instance + * \param[in,out] parent Clone to check + * \param[in] rsc_id Name of cloned resource in history (without instance) + */ +static pe_resource_t * +find_anonymous_clone(pe_working_set_t *data_set, const pe_node_t *node, + pe_resource_t *parent, const char *rsc_id) +{ + GList *rIter = NULL; + pe_resource_t *rsc = NULL; + pe_resource_t *inactive_instance = NULL; + gboolean skip_inactive = FALSE; + + CRM_ASSERT(parent != NULL); + CRM_ASSERT(pe_rsc_is_clone(parent)); + CRM_ASSERT(!pcmk_is_set(parent->flags, pe_rsc_unique)); + + // Check for active (or partially active, for cloned groups) instance + pe_rsc_trace(parent, "Looking for %s on %s in %s", + rsc_id, pe__node_name(node), parent->id); + for (rIter = parent->children; rsc == NULL && rIter; rIter = rIter->next) { + GList *locations = NULL; + pe_resource_t *child = rIter->data; + + /* Check whether this instance is already known to be active or pending + * anywhere, at this stage of unpacking. Because this function is called + * for a resource before the resource's individual operation history + * entries are unpacked, locations will generally not contain the + * desired node. + * + * However, there are three exceptions: + * (1) when child is a cloned group and we have already unpacked the + * history of another member of the group on the same node; + * (2) when we've already unpacked the history of another numbered + * instance on the same node (which can happen if globally-unique + * was flipped from true to false); and + * (3) when we re-run calculations on the same data set as part of a + * simulation. + */ + child->fns->location(child, &locations, 2); + if (locations) { + /* We should never associate the same numbered anonymous clone + * instance with multiple nodes, and clone instances can't migrate, + * so there must be only one location, regardless of history. + */ + CRM_LOG_ASSERT(locations->next == NULL); + + if (((pe_node_t *)locations->data)->details == node->details) { + /* This child instance is active on the requested node, so check + * for a corresponding configured resource. We use find_rsc() + * instead of child because child may be a cloned group, and we + * need the particular member corresponding to rsc_id. + * + * If the history entry is orphaned, rsc will be NULL. + */ + rsc = parent->fns->find_rsc(child, rsc_id, NULL, pe_find_clone); + if (rsc) { + /* If there are multiple instance history entries for an + * anonymous clone in a single node's history (which can + * happen if globally-unique is switched from true to + * false), we want to consider the instances beyond the + * first as orphans, even if there are inactive instance + * numbers available. + */ + if (rsc->running_on) { + crm_notice("Active (now-)anonymous clone %s has " + "multiple (orphan) instance histories on %s", + parent->id, pe__node_name(node)); + skip_inactive = TRUE; + rsc = NULL; + } else { + pe_rsc_trace(parent, "Resource %s, active", rsc->id); + } + } + } + g_list_free(locations); + + } else { + pe_rsc_trace(parent, "Resource %s, skip inactive", child->id); + if (!skip_inactive && !inactive_instance + && !pcmk_is_set(child->flags, pe_rsc_block)) { + // Remember one inactive instance in case we don't find active + inactive_instance = parent->fns->find_rsc(child, rsc_id, NULL, + pe_find_clone); + + /* ... but don't use it if it was already associated with a + * pending action on another node + */ + if (inactive_instance && inactive_instance->pending_node + && (inactive_instance->pending_node->details != node->details)) { + inactive_instance = NULL; + } + } + } + } + + if ((rsc == NULL) && !skip_inactive && (inactive_instance != NULL)) { + pe_rsc_trace(parent, "Resource %s, empty slot", inactive_instance->id); + rsc = inactive_instance; + } + + /* If the resource has "requires" set to "quorum" or "nothing", and we don't + * have a clone instance for every node, we don't want to consume a valid + * instance number for unclean nodes. Such instances may appear to be active + * according to the history, but should be considered inactive, so we can + * start an instance elsewhere. Treat such instances as orphans. + * + * An exception is instances running on guest nodes -- since guest node + * "fencing" is actually just a resource stop, requires shouldn't apply. + * + * @TODO Ideally, we'd use an inactive instance number if it is not needed + * for any clean instances. However, we don't know that at this point. + */ + if ((rsc != NULL) && !pcmk_is_set(rsc->flags, pe_rsc_needs_fencing) + && (!node->details->online || node->details->unclean) + && !pe__is_guest_node(node) + && !pe__is_universal_clone(parent, data_set)) { + + rsc = NULL; + } + + if (rsc == NULL) { + rsc = create_anonymous_orphan(parent, rsc_id, node, data_set); + pe_rsc_trace(parent, "Resource %s, orphan", rsc->id); + } + return rsc; +} + +static pe_resource_t * +unpack_find_resource(pe_working_set_t *data_set, const pe_node_t *node, + const char *rsc_id) +{ + pe_resource_t *rsc = NULL; + pe_resource_t *parent = NULL; + + crm_trace("looking for %s", rsc_id); + rsc = pe_find_resource(data_set->resources, rsc_id); + + if (rsc == NULL) { + /* If we didn't find the resource by its name in the operation history, + * check it again as a clone instance. Even when clone-max=0, we create + * a single :0 orphan to match against here. + */ + char *clone0_id = clone_zero(rsc_id); + pe_resource_t *clone0 = pe_find_resource(data_set->resources, clone0_id); + + if (clone0 && !pcmk_is_set(clone0->flags, pe_rsc_unique)) { + rsc = clone0; + parent = uber_parent(clone0); + crm_trace("%s found as %s (%s)", rsc_id, clone0_id, parent->id); + } else { + crm_trace("%s is not known as %s either (orphan)", + rsc_id, clone0_id); + } + free(clone0_id); + + } else if (rsc->variant > pe_native) { + crm_trace("Resource history for %s is orphaned because it is no longer primitive", + rsc_id); + return NULL; + + } else { + parent = uber_parent(rsc); + } + + if (pe_rsc_is_anon_clone(parent)) { + + if (pe_rsc_is_bundled(parent)) { + rsc = pe__find_bundle_replica(parent->parent, node); + } else { + char *base = clone_strip(rsc_id); + + rsc = find_anonymous_clone(data_set, node, parent, base); + free(base); + CRM_ASSERT(rsc != NULL); + } + } + + if (rsc && !pcmk__str_eq(rsc_id, rsc->id, pcmk__str_casei) + && !pcmk__str_eq(rsc_id, rsc->clone_name, pcmk__str_casei)) { + + pcmk__str_update(&rsc->clone_name, rsc_id); + pe_rsc_debug(rsc, "Internally renamed %s on %s to %s%s", + rsc_id, pe__node_name(node), rsc->id, + (pcmk_is_set(rsc->flags, pe_rsc_orphan)? " (ORPHAN)" : "")); + } + return rsc; +} + +static pe_resource_t * +process_orphan_resource(const xmlNode *rsc_entry, const pe_node_t *node, + pe_working_set_t *data_set) +{ + pe_resource_t *rsc = NULL; + const char *rsc_id = crm_element_value(rsc_entry, XML_ATTR_ID); + + crm_debug("Detected orphan resource %s on %s", rsc_id, pe__node_name(node)); + rsc = create_fake_resource(rsc_id, rsc_entry, data_set); + if (rsc == NULL) { + return NULL; + } + + if (!pcmk_is_set(data_set->flags, pe_flag_stop_rsc_orphans)) { + pe__clear_resource_flags(rsc, pe_rsc_managed); + + } else { + CRM_CHECK(rsc != NULL, return NULL); + pe_rsc_trace(rsc, "Added orphan %s", rsc->id); + resource_location(rsc, NULL, -INFINITY, "__orphan_do_not_run__", data_set); + } + return rsc; +} + +static void +process_rsc_state(pe_resource_t * rsc, pe_node_t * node, + enum action_fail_response on_fail) +{ + pe_node_t *tmpnode = NULL; + char *reason = NULL; + enum action_fail_response save_on_fail = action_fail_ignore; + + CRM_ASSERT(rsc); + pe_rsc_trace(rsc, "Resource %s is %s on %s: on_fail=%s", + rsc->id, role2text(rsc->role), pe__node_name(node), + fail2text(on_fail)); + + /* process current state */ + if (rsc->role != RSC_ROLE_UNKNOWN) { + pe_resource_t *iter = rsc; + + while (iter) { + if (g_hash_table_lookup(iter->known_on, node->details->id) == NULL) { + pe_node_t *n = pe__copy_node(node); + + pe_rsc_trace(rsc, "%s%s%s known on %s", + rsc->id, + ((rsc->clone_name == NULL)? "" : " also known as "), + ((rsc->clone_name == NULL)? "" : rsc->clone_name), + pe__node_name(n)); + g_hash_table_insert(iter->known_on, (gpointer) n->details->id, n); + } + if (pcmk_is_set(iter->flags, pe_rsc_unique)) { + break; + } + iter = iter->parent; + } + } + + /* If a managed resource is believed to be running, but node is down ... */ + if (rsc->role > RSC_ROLE_STOPPED + && node->details->online == FALSE + && node->details->maintenance == FALSE + && pcmk_is_set(rsc->flags, pe_rsc_managed)) { + + gboolean should_fence = FALSE; + + /* If this is a guest node, fence it (regardless of whether fencing is + * enabled, because guest node fencing is done by recovery of the + * container resource rather than by the fencer). Mark the resource + * we're processing as failed. When the guest comes back up, its + * operation history in the CIB will be cleared, freeing the affected + * resource to run again once we are sure we know its state. + */ + if (pe__is_guest_node(node)) { + pe__set_resource_flags(rsc, pe_rsc_failed|pe_rsc_stop); + should_fence = TRUE; + + } else if (pcmk_is_set(rsc->cluster->flags, pe_flag_stonith_enabled)) { + if (pe__is_remote_node(node) && node->details->remote_rsc + && !pcmk_is_set(node->details->remote_rsc->flags, pe_rsc_failed)) { + + /* Setting unseen means that fencing of the remote node will + * occur only if the connection resource is not going to start + * somewhere. This allows connection resources on a failed + * cluster node to move to another node without requiring the + * remote nodes to be fenced as well. + */ + node->details->unseen = TRUE; + reason = crm_strdup_printf("%s is active there (fencing will be" + " revoked if remote connection can " + "be re-established elsewhere)", + rsc->id); + } + should_fence = TRUE; + } + + if (should_fence) { + if (reason == NULL) { + reason = crm_strdup_printf("%s is thought to be active there", rsc->id); + } + pe_fence_node(rsc->cluster, node, reason, FALSE); + } + free(reason); + } + + /* In order to calculate priority_fencing_delay correctly, save the failure information and pass it to native_add_running(). */ + save_on_fail = on_fail; + + if (node->details->unclean) { + /* No extra processing needed + * Also allows resources to be started again after a node is shot + */ + on_fail = action_fail_ignore; + } + + switch (on_fail) { + case action_fail_ignore: + /* nothing to do */ + break; + + case action_fail_demote: + pe__set_resource_flags(rsc, pe_rsc_failed); + demote_action(rsc, node, FALSE); + break; + + case action_fail_fence: + /* treat it as if it is still running + * but also mark the node as unclean + */ + reason = crm_strdup_printf("%s failed there", rsc->id); + pe_fence_node(rsc->cluster, node, reason, FALSE); + free(reason); + break; + + case action_fail_standby: + node->details->standby = TRUE; + node->details->standby_onfail = TRUE; + break; + + case action_fail_block: + /* is_managed == FALSE will prevent any + * actions being sent for the resource + */ + pe__clear_resource_flags(rsc, pe_rsc_managed); + pe__set_resource_flags(rsc, pe_rsc_block); + break; + + case action_fail_migrate: + /* make sure it comes up somewhere else + * or not at all + */ + resource_location(rsc, node, -INFINITY, "__action_migration_auto__", + rsc->cluster); + break; + + case action_fail_stop: + pe__set_next_role(rsc, RSC_ROLE_STOPPED, "on-fail=stop"); + break; + + case action_fail_recover: + if (rsc->role != RSC_ROLE_STOPPED && rsc->role != RSC_ROLE_UNKNOWN) { + pe__set_resource_flags(rsc, pe_rsc_failed|pe_rsc_stop); + stop_action(rsc, node, FALSE); + } + break; + + case action_fail_restart_container: + pe__set_resource_flags(rsc, pe_rsc_failed|pe_rsc_stop); + if (rsc->container && pe_rsc_is_bundled(rsc)) { + /* A bundle's remote connection can run on a different node than + * the bundle's container. We don't necessarily know where the + * container is running yet, so remember it and add a stop + * action for it later. + */ + rsc->cluster->stop_needed = + g_list_prepend(rsc->cluster->stop_needed, rsc->container); + } else if (rsc->container) { + stop_action(rsc->container, node, FALSE); + } else if (rsc->role != RSC_ROLE_STOPPED && rsc->role != RSC_ROLE_UNKNOWN) { + stop_action(rsc, node, FALSE); + } + break; + + case action_fail_reset_remote: + pe__set_resource_flags(rsc, pe_rsc_failed|pe_rsc_stop); + if (pcmk_is_set(rsc->cluster->flags, pe_flag_stonith_enabled)) { + tmpnode = NULL; + if (rsc->is_remote_node) { + tmpnode = pe_find_node(rsc->cluster->nodes, rsc->id); + } + if (tmpnode && + pe__is_remote_node(tmpnode) && + tmpnode->details->remote_was_fenced == 0) { + + /* The remote connection resource failed in a way that + * should result in fencing the remote node. + */ + pe_fence_node(rsc->cluster, tmpnode, + "remote connection is unrecoverable", FALSE); + } + } + + /* require the stop action regardless if fencing is occurring or not. */ + if (rsc->role > RSC_ROLE_STOPPED) { + stop_action(rsc, node, FALSE); + } + + /* if reconnect delay is in use, prevent the connection from exiting the + * "STOPPED" role until the failure is cleared by the delay timeout. */ + if (rsc->remote_reconnect_ms) { + pe__set_next_role(rsc, RSC_ROLE_STOPPED, "remote reset"); + } + break; + } + + /* ensure a remote-node connection failure forces an unclean remote-node + * to be fenced. By setting unseen = FALSE, the remote-node failure will + * result in a fencing operation regardless if we're going to attempt to + * reconnect to the remote-node in this transition or not. */ + if (pcmk_is_set(rsc->flags, pe_rsc_failed) && rsc->is_remote_node) { + tmpnode = pe_find_node(rsc->cluster->nodes, rsc->id); + if (tmpnode && tmpnode->details->unclean) { + tmpnode->details->unseen = FALSE; + } + } + + if (rsc->role != RSC_ROLE_STOPPED && rsc->role != RSC_ROLE_UNKNOWN) { + if (pcmk_is_set(rsc->flags, pe_rsc_orphan)) { + if (pcmk_is_set(rsc->flags, pe_rsc_managed)) { + pcmk__config_warn("Detected active orphan %s running on %s", + rsc->id, pe__node_name(node)); + } else { + pcmk__config_warn("Resource '%s' must be stopped manually on " + "%s because cluster is configured not to " + "stop active orphans", + rsc->id, pe__node_name(node)); + } + } + + native_add_running(rsc, node, rsc->cluster, + (save_on_fail != action_fail_ignore)); + switch (on_fail) { + case action_fail_ignore: + break; + case action_fail_demote: + case action_fail_block: + pe__set_resource_flags(rsc, pe_rsc_failed); + break; + default: + pe__set_resource_flags(rsc, pe_rsc_failed|pe_rsc_stop); + break; + } + + } else if (rsc->clone_name && strchr(rsc->clone_name, ':') != NULL) { + /* Only do this for older status sections that included instance numbers + * Otherwise stopped instances will appear as orphans + */ + pe_rsc_trace(rsc, "Resetting clone_name %s for %s (stopped)", rsc->clone_name, rsc->id); + free(rsc->clone_name); + rsc->clone_name = NULL; + + } else { + GList *possible_matches = pe__resource_actions(rsc, node, RSC_STOP, + FALSE); + GList *gIter = possible_matches; + + for (; gIter != NULL; gIter = gIter->next) { + pe_action_t *stop = (pe_action_t *) gIter->data; + + pe__set_action_flags(stop, pe_action_optional); + } + + g_list_free(possible_matches); + } + + /* A successful stop after migrate_to on the migration source doesn't make + * the partially migrated resource stopped on the migration target. + */ + if (rsc->role == RSC_ROLE_STOPPED + && rsc->partial_migration_source + && rsc->partial_migration_source->details == node->details + && rsc->partial_migration_target + && rsc->running_on) { + + rsc->role = RSC_ROLE_STARTED; + } +} + +/* create active recurring operations as optional */ +static void +process_recurring(pe_node_t * node, pe_resource_t * rsc, + int start_index, int stop_index, + GList *sorted_op_list, pe_working_set_t * data_set) +{ + int counter = -1; + const char *task = NULL; + const char *status = NULL; + GList *gIter = sorted_op_list; + + CRM_ASSERT(rsc); + pe_rsc_trace(rsc, "%s: Start index %d, stop index = %d", rsc->id, start_index, stop_index); + + for (; gIter != NULL; gIter = gIter->next) { + xmlNode *rsc_op = (xmlNode *) gIter->data; + + guint interval_ms = 0; + char *key = NULL; + const char *id = ID(rsc_op); + + counter++; + + if (node->details->online == FALSE) { + pe_rsc_trace(rsc, "Skipping %s on %s: node is offline", + rsc->id, pe__node_name(node)); + break; + + /* Need to check if there's a monitor for role="Stopped" */ + } else if (start_index < stop_index && counter <= stop_index) { + pe_rsc_trace(rsc, "Skipping %s on %s: resource is not active", + id, pe__node_name(node)); + continue; + + } else if (counter < start_index) { + pe_rsc_trace(rsc, "Skipping %s on %s: old %d", + id, pe__node_name(node), counter); + continue; + } + + crm_element_value_ms(rsc_op, XML_LRM_ATTR_INTERVAL_MS, &interval_ms); + if (interval_ms == 0) { + pe_rsc_trace(rsc, "Skipping %s on %s: non-recurring", + id, pe__node_name(node)); + continue; + } + + status = crm_element_value(rsc_op, XML_LRM_ATTR_OPSTATUS); + if (pcmk__str_eq(status, "-1", pcmk__str_casei)) { + pe_rsc_trace(rsc, "Skipping %s on %s: status", + id, pe__node_name(node)); + continue; + } + task = crm_element_value(rsc_op, XML_LRM_ATTR_TASK); + /* create the action */ + key = pcmk__op_key(rsc->id, task, interval_ms); + pe_rsc_trace(rsc, "Creating %s on %s", key, pe__node_name(node)); + custom_action(rsc, key, task, node, TRUE, TRUE, data_set); + } +} + +void +calculate_active_ops(const GList *sorted_op_list, int *start_index, + int *stop_index) +{ + int counter = -1; + int implied_monitor_start = -1; + int implied_clone_start = -1; + const char *task = NULL; + const char *status = NULL; + + *stop_index = -1; + *start_index = -1; + + for (const GList *iter = sorted_op_list; iter != NULL; iter = iter->next) { + const xmlNode *rsc_op = (const xmlNode *) iter->data; + + counter++; + + task = crm_element_value(rsc_op, XML_LRM_ATTR_TASK); + status = crm_element_value(rsc_op, XML_LRM_ATTR_OPSTATUS); + + if (pcmk__str_eq(task, CRMD_ACTION_STOP, pcmk__str_casei) + && pcmk__str_eq(status, "0", pcmk__str_casei)) { + *stop_index = counter; + + } else if (pcmk__strcase_any_of(task, CRMD_ACTION_START, CRMD_ACTION_MIGRATED, NULL)) { + *start_index = counter; + + } else if ((implied_monitor_start <= *stop_index) && pcmk__str_eq(task, CRMD_ACTION_STATUS, pcmk__str_casei)) { + const char *rc = crm_element_value(rsc_op, XML_LRM_ATTR_RC); + + if (pcmk__strcase_any_of(rc, "0", "8", NULL)) { + implied_monitor_start = counter; + } + } else if (pcmk__strcase_any_of(task, CRMD_ACTION_PROMOTE, CRMD_ACTION_DEMOTE, NULL)) { + implied_clone_start = counter; + } + } + + if (*start_index == -1) { + if (implied_clone_start != -1) { + *start_index = implied_clone_start; + } else if (implied_monitor_start != -1) { + *start_index = implied_monitor_start; + } + } +} + +// If resource history entry has shutdown lock, remember lock node and time +static void +unpack_shutdown_lock(const xmlNode *rsc_entry, pe_resource_t *rsc, + const pe_node_t *node, pe_working_set_t *data_set) +{ + time_t lock_time = 0; // When lock started (i.e. node shutdown time) + + if ((crm_element_value_epoch(rsc_entry, XML_CONFIG_ATTR_SHUTDOWN_LOCK, + &lock_time) == pcmk_ok) && (lock_time != 0)) { + + if ((data_set->shutdown_lock > 0) + && (get_effective_time(data_set) + > (lock_time + data_set->shutdown_lock))) { + pe_rsc_info(rsc, "Shutdown lock for %s on %s expired", + rsc->id, pe__node_name(node)); + pe__clear_resource_history(rsc, node, data_set); + } else { + /* @COMPAT I don't like breaking const signatures, but + * rsc->lock_node should really be const -- we just can't change it + * until the next API compatibility break. + */ + rsc->lock_node = (pe_node_t *) node; + rsc->lock_time = lock_time; + } + } +} + +/*! + * \internal + * \brief Unpack one lrm_resource entry from a node's CIB status + * + * \param[in,out] node Node whose status is being unpacked + * \param[in] rsc_entry lrm_resource XML being unpacked + * \param[in,out] data_set Cluster working set + * + * \return Resource corresponding to the entry, or NULL if no operation history + */ +static pe_resource_t * +unpack_lrm_resource(pe_node_t *node, const xmlNode *lrm_resource, + pe_working_set_t *data_set) +{ + GList *gIter = NULL; + int stop_index = -1; + int start_index = -1; + enum rsc_role_e req_role = RSC_ROLE_UNKNOWN; + + const char *rsc_id = ID(lrm_resource); + + pe_resource_t *rsc = NULL; + GList *op_list = NULL; + GList *sorted_op_list = NULL; + + xmlNode *rsc_op = NULL; + xmlNode *last_failure = NULL; + + enum action_fail_response on_fail = action_fail_ignore; + enum rsc_role_e saved_role = RSC_ROLE_UNKNOWN; + + if (rsc_id == NULL) { + crm_warn("Ignoring malformed " XML_LRM_TAG_RESOURCE + " entry without id"); + return NULL; + } + crm_trace("Unpacking " XML_LRM_TAG_RESOURCE " for %s on %s", + rsc_id, pe__node_name(node)); + + // Build a list of individual lrm_rsc_op entries, so we can sort them + for (rsc_op = first_named_child(lrm_resource, XML_LRM_TAG_RSC_OP); + rsc_op != NULL; rsc_op = crm_next_same_xml(rsc_op)) { + + op_list = g_list_prepend(op_list, rsc_op); + } + + if (!pcmk_is_set(data_set->flags, pe_flag_shutdown_lock)) { + if (op_list == NULL) { + // If there are no operations, there is nothing to do + return NULL; + } + } + + /* find the resource */ + rsc = unpack_find_resource(data_set, node, rsc_id); + if (rsc == NULL) { + if (op_list == NULL) { + // If there are no operations, there is nothing to do + return NULL; + } else { + rsc = process_orphan_resource(lrm_resource, node, data_set); + } + } + CRM_ASSERT(rsc != NULL); + + // Check whether the resource is "shutdown-locked" to this node + if (pcmk_is_set(data_set->flags, pe_flag_shutdown_lock)) { + unpack_shutdown_lock(lrm_resource, rsc, node, data_set); + } + + /* process operations */ + saved_role = rsc->role; + rsc->role = RSC_ROLE_UNKNOWN; + sorted_op_list = g_list_sort(op_list, sort_op_by_callid); + + for (gIter = sorted_op_list; gIter != NULL; gIter = gIter->next) { + xmlNode *rsc_op = (xmlNode *) gIter->data; + + unpack_rsc_op(rsc, node, rsc_op, &last_failure, &on_fail); + } + + /* create active recurring operations as optional */ + calculate_active_ops(sorted_op_list, &start_index, &stop_index); + process_recurring(node, rsc, start_index, stop_index, sorted_op_list, data_set); + + /* no need to free the contents */ + g_list_free(sorted_op_list); + + process_rsc_state(rsc, node, on_fail); + + if (get_target_role(rsc, &req_role)) { + if (rsc->next_role == RSC_ROLE_UNKNOWN || req_role < rsc->next_role) { + pe__set_next_role(rsc, req_role, XML_RSC_ATTR_TARGET_ROLE); + + } else if (req_role > rsc->next_role) { + pe_rsc_info(rsc, "%s: Not overwriting calculated next role %s" + " with requested next role %s", + rsc->id, role2text(rsc->next_role), role2text(req_role)); + } + } + + if (saved_role > rsc->role) { + rsc->role = saved_role; + } + + return rsc; +} + +static void +handle_orphaned_container_fillers(const xmlNode *lrm_rsc_list, + pe_working_set_t *data_set) +{ + for (const xmlNode *rsc_entry = pcmk__xe_first_child(lrm_rsc_list); + rsc_entry != NULL; rsc_entry = pcmk__xe_next(rsc_entry)) { + + pe_resource_t *rsc; + pe_resource_t *container; + const char *rsc_id; + const char *container_id; + + if (!pcmk__str_eq((const char *)rsc_entry->name, XML_LRM_TAG_RESOURCE, pcmk__str_casei)) { + continue; + } + + container_id = crm_element_value(rsc_entry, XML_RSC_ATTR_CONTAINER); + rsc_id = crm_element_value(rsc_entry, XML_ATTR_ID); + if (container_id == NULL || rsc_id == NULL) { + continue; + } + + container = pe_find_resource(data_set->resources, container_id); + if (container == NULL) { + continue; + } + + rsc = pe_find_resource(data_set->resources, rsc_id); + if (rsc == NULL || + !pcmk_is_set(rsc->flags, pe_rsc_orphan_container_filler) || + rsc->container != NULL) { + continue; + } + + pe_rsc_trace(rsc, "Mapped container of orphaned resource %s to %s", + rsc->id, container_id); + rsc->container = container; + container->fillers = g_list_append(container->fillers, rsc); + } +} + +/*! + * \internal + * \brief Unpack one node's lrm status section + * + * \param[in,out] node Node whose status is being unpacked + * \param[in] xml CIB node state XML + * \param[in,out] data_set Cluster working set + */ +static void +unpack_node_lrm(pe_node_t *node, const xmlNode *xml, pe_working_set_t *data_set) +{ + bool found_orphaned_container_filler = false; + + // Drill down to lrm_resources section + xml = find_xml_node(xml, XML_CIB_TAG_LRM, FALSE); + if (xml == NULL) { + return; + } + xml = find_xml_node(xml, XML_LRM_TAG_RESOURCES, FALSE); + if (xml == NULL) { + return; + } + + // Unpack each lrm_resource entry + for (const xmlNode *rsc_entry = first_named_child(xml, XML_LRM_TAG_RESOURCE); + rsc_entry != NULL; rsc_entry = crm_next_same_xml(rsc_entry)) { + + pe_resource_t *rsc = unpack_lrm_resource(node, rsc_entry, data_set); + + if ((rsc != NULL) + && pcmk_is_set(rsc->flags, pe_rsc_orphan_container_filler)) { + found_orphaned_container_filler = true; + } + } + + /* Now that all resource state has been unpacked for this node, map any + * orphaned container fillers to their container resource. + */ + if (found_orphaned_container_filler) { + handle_orphaned_container_fillers(xml, data_set); + } +} + +static void +set_active(pe_resource_t * rsc) +{ + const pe_resource_t *top = pe__const_top_resource(rsc, false); + + if (top && pcmk_is_set(top->flags, pe_rsc_promotable)) { + rsc->role = RSC_ROLE_UNPROMOTED; + } else { + rsc->role = RSC_ROLE_STARTED; + } +} + +static void +set_node_score(gpointer key, gpointer value, gpointer user_data) +{ + pe_node_t *node = value; + int *score = user_data; + + node->weight = *score; +} + +#define XPATH_NODE_STATE "/" XML_TAG_CIB "/" XML_CIB_TAG_STATUS \ + "/" XML_CIB_TAG_STATE +#define SUB_XPATH_LRM_RESOURCE "/" XML_CIB_TAG_LRM \ + "/" XML_LRM_TAG_RESOURCES \ + "/" XML_LRM_TAG_RESOURCE +#define SUB_XPATH_LRM_RSC_OP "/" XML_LRM_TAG_RSC_OP + +static xmlNode * +find_lrm_op(const char *resource, const char *op, const char *node, const char *source, + int target_rc, pe_working_set_t *data_set) +{ + GString *xpath = NULL; + xmlNode *xml = NULL; + + CRM_CHECK((resource != NULL) && (op != NULL) && (node != NULL), + return NULL); + + xpath = g_string_sized_new(256); + pcmk__g_strcat(xpath, + XPATH_NODE_STATE "[@" XML_ATTR_UNAME "='", node, "']" + SUB_XPATH_LRM_RESOURCE "[@" XML_ATTR_ID "='", resource, "']" + SUB_XPATH_LRM_RSC_OP "[@" XML_LRM_ATTR_TASK "='", op, "'", + NULL); + + /* Need to check against transition_magic too? */ + if ((source != NULL) && (strcmp(op, CRMD_ACTION_MIGRATE) == 0)) { + pcmk__g_strcat(xpath, + " and @" XML_LRM_ATTR_MIGRATE_TARGET "='", source, "']", + NULL); + + } else if ((source != NULL) && (strcmp(op, CRMD_ACTION_MIGRATED) == 0)) { + pcmk__g_strcat(xpath, + " and @" XML_LRM_ATTR_MIGRATE_SOURCE "='", source, "']", + NULL); + } else { + g_string_append_c(xpath, ']'); + } + + xml = get_xpath_object((const char *) xpath->str, data_set->input, + LOG_DEBUG); + g_string_free(xpath, TRUE); + + if (xml && target_rc >= 0) { + int rc = PCMK_OCF_UNKNOWN_ERROR; + int status = PCMK_EXEC_ERROR; + + crm_element_value_int(xml, XML_LRM_ATTR_RC, &rc); + crm_element_value_int(xml, XML_LRM_ATTR_OPSTATUS, &status); + if ((rc != target_rc) || (status != PCMK_EXEC_DONE)) { + return NULL; + } + } + return xml; +} + +static xmlNode * +find_lrm_resource(const char *rsc_id, const char *node_name, + pe_working_set_t *data_set) +{ + GString *xpath = NULL; + xmlNode *xml = NULL; + + CRM_CHECK((rsc_id != NULL) && (node_name != NULL), return NULL); + + xpath = g_string_sized_new(256); + pcmk__g_strcat(xpath, + XPATH_NODE_STATE "[@" XML_ATTR_UNAME "='", node_name, "']" + SUB_XPATH_LRM_RESOURCE "[@" XML_ATTR_ID "='", rsc_id, "']", + NULL); + + xml = get_xpath_object((const char *) xpath->str, data_set->input, + LOG_DEBUG); + + g_string_free(xpath, TRUE); + return xml; +} + +/*! + * \internal + * \brief Check whether a resource has no completed action history on a node + * + * \param[in,out] rsc Resource to check + * \param[in] node_name Node to check + * + * \return true if \p rsc_id is unknown on \p node_name, otherwise false + */ +static bool +unknown_on_node(pe_resource_t *rsc, const char *node_name) +{ + bool result = false; + xmlXPathObjectPtr search; + GString *xpath = g_string_sized_new(256); + + pcmk__g_strcat(xpath, + XPATH_NODE_STATE "[@" XML_ATTR_UNAME "='", node_name, "']" + SUB_XPATH_LRM_RESOURCE "[@" XML_ATTR_ID "='", rsc->id, "']" + SUB_XPATH_LRM_RSC_OP "[@" XML_LRM_ATTR_RC "!='193']", + NULL); + search = xpath_search(rsc->cluster->input, (const char *) xpath->str); + result = (numXpathResults(search) == 0); + freeXpathObject(search); + g_string_free(xpath, TRUE); + return result; +} + +/*! + * \brief Check whether a probe/monitor indicating the resource was not running + * on a node happened after some event + * + * \param[in] rsc_id Resource being checked + * \param[in] node_name Node being checked + * \param[in] xml_op Event that monitor is being compared to + * \param[in] same_node Whether the operations are on the same node + * \param[in,out] data_set Cluster working set + * + * \return true if such a monitor happened after event, false otherwise + */ +static bool +monitor_not_running_after(const char *rsc_id, const char *node_name, + const xmlNode *xml_op, bool same_node, + pe_working_set_t *data_set) +{ + /* Any probe/monitor operation on the node indicating it was not running + * there + */ + xmlNode *monitor = find_lrm_op(rsc_id, CRMD_ACTION_STATUS, node_name, + NULL, PCMK_OCF_NOT_RUNNING, data_set); + + return (monitor && pe__is_newer_op(monitor, xml_op, same_node) > 0); +} + +/*! + * \brief Check whether any non-monitor operation on a node happened after some + * event + * + * \param[in] rsc_id Resource being checked + * \param[in] node_name Node being checked + * \param[in] xml_op Event that non-monitor is being compared to + * \param[in] same_node Whether the operations are on the same node + * \param[in,out] data_set Cluster working set + * + * \return true if such a operation happened after event, false otherwise + */ +static bool +non_monitor_after(const char *rsc_id, const char *node_name, + const xmlNode *xml_op, bool same_node, + pe_working_set_t *data_set) +{ + xmlNode *lrm_resource = NULL; + + lrm_resource = find_lrm_resource(rsc_id, node_name, data_set); + if (lrm_resource == NULL) { + return false; + } + + for (xmlNode *op = first_named_child(lrm_resource, XML_LRM_TAG_RSC_OP); + op != NULL; op = crm_next_same_xml(op)) { + const char * task = NULL; + + if (op == xml_op) { + continue; + } + + task = crm_element_value(op, XML_LRM_ATTR_TASK); + + if (pcmk__str_any_of(task, CRMD_ACTION_START, CRMD_ACTION_STOP, + CRMD_ACTION_MIGRATE, CRMD_ACTION_MIGRATED, NULL) + && pe__is_newer_op(op, xml_op, same_node) > 0) { + return true; + } + } + + return false; +} + +/*! + * \brief Check whether the resource has newer state on a node after a migration + * attempt + * + * \param[in] rsc_id Resource being checked + * \param[in] node_name Node being checked + * \param[in] migrate_to Any migrate_to event that is being compared to + * \param[in] migrate_from Any migrate_from event that is being compared to + * \param[in,out] data_set Cluster working set + * + * \return true if such a operation happened after event, false otherwise + */ +static bool +newer_state_after_migrate(const char *rsc_id, const char *node_name, + const xmlNode *migrate_to, + const xmlNode *migrate_from, + pe_working_set_t *data_set) +{ + const xmlNode *xml_op = migrate_to; + const char *source = NULL; + const char *target = NULL; + bool same_node = false; + + if (migrate_from) { + xml_op = migrate_from; + } + + source = crm_element_value(xml_op, XML_LRM_ATTR_MIGRATE_SOURCE); + target = crm_element_value(xml_op, XML_LRM_ATTR_MIGRATE_TARGET); + + /* It's preferred to compare to the migrate event on the same node if + * existing, since call ids are more reliable. + */ + if (pcmk__str_eq(node_name, target, pcmk__str_casei)) { + if (migrate_from) { + xml_op = migrate_from; + same_node = true; + + } else { + xml_op = migrate_to; + } + + } else if (pcmk__str_eq(node_name, source, pcmk__str_casei)) { + if (migrate_to) { + xml_op = migrate_to; + same_node = true; + + } else { + xml_op = migrate_from; + } + } + + /* If there's any newer non-monitor operation on the node, or any newer + * probe/monitor operation on the node indicating it was not running there, + * the migration events potentially no longer matter for the node. + */ + return non_monitor_after(rsc_id, node_name, xml_op, same_node, data_set) + || monitor_not_running_after(rsc_id, node_name, xml_op, same_node, + data_set); +} + +/*! + * \internal + * \brief Parse migration source and target node names from history entry + * + * \param[in] entry Resource history entry for a migration action + * \param[in] source_node If not NULL, source must match this node + * \param[in] target_node If not NULL, target must match this node + * \param[out] source_name Where to store migration source node name + * \param[out] target_name Where to store migration target node name + * + * \return Standard Pacemaker return code + */ +static int +get_migration_node_names(const xmlNode *entry, const pe_node_t *source_node, + const pe_node_t *target_node, + const char **source_name, const char **target_name) +{ + *source_name = crm_element_value(entry, XML_LRM_ATTR_MIGRATE_SOURCE); + *target_name = crm_element_value(entry, XML_LRM_ATTR_MIGRATE_TARGET); + if ((*source_name == NULL) || (*target_name == NULL)) { + crm_err("Ignoring resource history entry %s without " + XML_LRM_ATTR_MIGRATE_SOURCE " and " XML_LRM_ATTR_MIGRATE_TARGET, + ID(entry)); + return pcmk_rc_unpack_error; + } + + if ((source_node != NULL) + && !pcmk__str_eq(*source_name, source_node->details->uname, + pcmk__str_casei|pcmk__str_null_matches)) { + crm_err("Ignoring resource history entry %s because " + XML_LRM_ATTR_MIGRATE_SOURCE "='%s' does not match %s", + ID(entry), *source_name, pe__node_name(source_node)); + return pcmk_rc_unpack_error; + } + + if ((target_node != NULL) + && !pcmk__str_eq(*target_name, target_node->details->uname, + pcmk__str_casei|pcmk__str_null_matches)) { + crm_err("Ignoring resource history entry %s because " + XML_LRM_ATTR_MIGRATE_TARGET "='%s' does not match %s", + ID(entry), *target_name, pe__node_name(target_node)); + return pcmk_rc_unpack_error; + } + + return pcmk_rc_ok; +} + +/* + * \internal + * \brief Add a migration source to a resource's list of dangling migrations + * + * If the migrate_to and migrate_from actions in a live migration both + * succeeded, but there is no stop on the source, the migration is considered + * "dangling." Add the source to the resource's dangling migration list, which + * will be used to schedule a stop on the source without affecting the target. + * + * \param[in,out] rsc Resource involved in migration + * \param[in] node Migration source + */ +static void +add_dangling_migration(pe_resource_t *rsc, const pe_node_t *node) +{ + pe_rsc_trace(rsc, "Dangling migration of %s requires stop on %s", + rsc->id, pe__node_name(node)); + rsc->role = RSC_ROLE_STOPPED; + rsc->dangling_migrations = g_list_prepend(rsc->dangling_migrations, + (gpointer) node); +} + +/*! + * \internal + * \brief Update resource role etc. after a successful migrate_to action + * + * \param[in,out] history Parsed action result history + */ +static void +unpack_migrate_to_success(struct action_history *history) +{ + /* A complete migration sequence is: + * 1. migrate_to on source node (which succeeded if we get to this function) + * 2. migrate_from on target node + * 3. stop on source node + * + * If no migrate_from has happened, the migration is considered to be + * "partial". If the migrate_from succeeded but no stop has happened, the + * migration is considered to be "dangling". + * + * If a successful migrate_to and stop have happened on the source node, we + * still need to check for a partial migration, due to scenarios (easier to + * produce with batch-limit=1) like: + * + * - A resource is migrating from node1 to node2, and a migrate_to is + * initiated for it on node1. + * + * - node2 goes into standby mode while the migrate_to is pending, which + * aborts the transition. + * + * - Upon completion of the migrate_to, a new transition schedules a stop + * on both nodes and a start on node1. + * + * - If the new transition is aborted for any reason while the resource is + * stopping on node1, the transition after that stop completes will see + * the migrate_to and stop on the source, but it's still a partial + * migration, and the resource must be stopped on node2 because it is + * potentially active there due to the migrate_to. + * + * We also need to take into account that either node's history may be + * cleared at any point in the migration process. + */ + int from_rc = PCMK_OCF_OK; + int from_status = PCMK_EXEC_PENDING; + pe_node_t *target_node = NULL; + xmlNode *migrate_from = NULL; + const char *source = NULL; + const char *target = NULL; + bool source_newer_op = false; + bool target_newer_state = false; + bool active_on_target = false; + + // Get source and target node names from XML + if (get_migration_node_names(history->xml, history->node, NULL, &source, + &target) != pcmk_rc_ok) { + return; + } + + // Check for newer state on the source + source_newer_op = non_monitor_after(history->rsc->id, source, history->xml, + true, history->rsc->cluster); + + // Check for a migrate_from action from this source on the target + migrate_from = find_lrm_op(history->rsc->id, CRMD_ACTION_MIGRATED, target, + source, -1, history->rsc->cluster); + if (migrate_from != NULL) { + if (source_newer_op) { + /* There's a newer non-monitor operation on the source and a + * migrate_from on the target, so this migrate_to is irrelevant to + * the resource's state. + */ + return; + } + crm_element_value_int(migrate_from, XML_LRM_ATTR_RC, &from_rc); + crm_element_value_int(migrate_from, XML_LRM_ATTR_OPSTATUS, + &from_status); + } + + /* If the resource has newer state on both the source and target after the + * migration events, this migrate_to is irrelevant to the resource's state. + */ + target_newer_state = newer_state_after_migrate(history->rsc->id, target, + history->xml, migrate_from, + history->rsc->cluster); + if (source_newer_op && target_newer_state) { + return; + } + + /* Check for dangling migration (migrate_from succeeded but stop not done). + * We know there's no stop because we already returned if the target has a + * migrate_from and the source has any newer non-monitor operation. + */ + if ((from_rc == PCMK_OCF_OK) && (from_status == PCMK_EXEC_DONE)) { + add_dangling_migration(history->rsc, history->node); + return; + } + + /* Without newer state, this migrate_to implies the resource is active. + * (Clones are not allowed to migrate, so role can't be promoted.) + */ + history->rsc->role = RSC_ROLE_STARTED; + + target_node = pe_find_node(history->rsc->cluster->nodes, target); + active_on_target = !target_newer_state && (target_node != NULL) + && target_node->details->online; + + if (from_status != PCMK_EXEC_PENDING) { // migrate_from failed on target + if (active_on_target) { + native_add_running(history->rsc, target_node, history->rsc->cluster, + TRUE); + } else { + // Mark resource as failed, require recovery, and prevent migration + pe__set_resource_flags(history->rsc, pe_rsc_failed|pe_rsc_stop); + pe__clear_resource_flags(history->rsc, pe_rsc_allow_migrate); + } + return; + } + + // The migrate_from is pending, complete but erased, or to be scheduled + + /* If there is no history at all for the resource on an online target, then + * it was likely cleaned. Just return, and we'll schedule a probe. Once we + * have the probe result, it will be reflected in target_newer_state. + */ + if ((target_node != NULL) && target_node->details->online + && unknown_on_node(history->rsc, target)) { + return; + } + + if (active_on_target) { + pe_node_t *source_node = pe_find_node(history->rsc->cluster->nodes, + source); + + native_add_running(history->rsc, target_node, history->rsc->cluster, + FALSE); + if ((source_node != NULL) && source_node->details->online) { + /* This is a partial migration: the migrate_to completed + * successfully on the source, but the migrate_from has not + * completed. Remember the source and target; if the newly + * chosen target remains the same when we schedule actions + * later, we may continue with the migration. + */ + history->rsc->partial_migration_target = target_node; + history->rsc->partial_migration_source = source_node; + } + + } else if (!source_newer_op) { + // Mark resource as failed, require recovery, and prevent migration + pe__set_resource_flags(history->rsc, pe_rsc_failed|pe_rsc_stop); + pe__clear_resource_flags(history->rsc, pe_rsc_allow_migrate); + } +} + +/*! + * \internal + * \brief Update resource role etc. after a failed migrate_to action + * + * \param[in,out] history Parsed action result history + */ +static void +unpack_migrate_to_failure(struct action_history *history) +{ + xmlNode *target_migrate_from = NULL; + const char *source = NULL; + const char *target = NULL; + + // Get source and target node names from XML + if (get_migration_node_names(history->xml, history->node, NULL, &source, + &target) != pcmk_rc_ok) { + return; + } + + /* If a migration failed, we have to assume the resource is active. Clones + * are not allowed to migrate, so role can't be promoted. + */ + history->rsc->role = RSC_ROLE_STARTED; + + // Check for migrate_from on the target + target_migrate_from = find_lrm_op(history->rsc->id, CRMD_ACTION_MIGRATED, + target, source, PCMK_OCF_OK, + history->rsc->cluster); + + if (/* If the resource state is unknown on the target, it will likely be + * probed there. + * Don't just consider it running there. We will get back here anyway in + * case the probe detects it's running there. + */ + !unknown_on_node(history->rsc, target) + /* If the resource has newer state on the target after the migration + * events, this migrate_to no longer matters for the target. + */ + && !newer_state_after_migrate(history->rsc->id, target, history->xml, + target_migrate_from, + history->rsc->cluster)) { + /* The resource has no newer state on the target, so assume it's still + * active there. + * (if it is up). + */ + pe_node_t *target_node = pe_find_node(history->rsc->cluster->nodes, + target); + + if (target_node && target_node->details->online) { + native_add_running(history->rsc, target_node, history->rsc->cluster, + FALSE); + } + + } else if (!non_monitor_after(history->rsc->id, source, history->xml, true, + history->rsc->cluster)) { + /* We know the resource has newer state on the target, but this + * migrate_to still matters for the source as long as there's no newer + * non-monitor operation there. + */ + + // Mark node as having dangling migration so we can force a stop later + history->rsc->dangling_migrations = + g_list_prepend(history->rsc->dangling_migrations, + (gpointer) history->node); + } +} + +/*! + * \internal + * \brief Update resource role etc. after a failed migrate_from action + * + * \param[in,out] history Parsed action result history + */ +static void +unpack_migrate_from_failure(struct action_history *history) +{ + xmlNode *source_migrate_to = NULL; + const char *source = NULL; + const char *target = NULL; + + // Get source and target node names from XML + if (get_migration_node_names(history->xml, NULL, history->node, &source, + &target) != pcmk_rc_ok) { + return; + } + + /* If a migration failed, we have to assume the resource is active. Clones + * are not allowed to migrate, so role can't be promoted. + */ + history->rsc->role = RSC_ROLE_STARTED; + + // Check for a migrate_to on the source + source_migrate_to = find_lrm_op(history->rsc->id, CRMD_ACTION_MIGRATE, + source, target, PCMK_OCF_OK, + history->rsc->cluster); + + if (/* If the resource state is unknown on the source, it will likely be + * probed there. + * Don't just consider it running there. We will get back here anyway in + * case the probe detects it's running there. + */ + !unknown_on_node(history->rsc, source) + /* If the resource has newer state on the source after the migration + * events, this migrate_from no longer matters for the source. + */ + && !newer_state_after_migrate(history->rsc->id, source, + source_migrate_to, history->xml, + history->rsc->cluster)) { + /* The resource has no newer state on the source, so assume it's still + * active there (if it is up). + */ + pe_node_t *source_node = pe_find_node(history->rsc->cluster->nodes, + source); + + if (source_node && source_node->details->online) { + native_add_running(history->rsc, source_node, history->rsc->cluster, + TRUE); + } + } +} + +/*! + * \internal + * \brief Add an action to cluster's list of failed actions + * + * \param[in,out] history Parsed action result history + */ +static void +record_failed_op(struct action_history *history) +{ + if (!(history->node->details->online)) { + return; + } + + for (const xmlNode *xIter = history->rsc->cluster->failed->children; + xIter != NULL; xIter = xIter->next) { + + const char *key = pe__xe_history_key(xIter); + const char *uname = crm_element_value(xIter, XML_ATTR_UNAME); + + if (pcmk__str_eq(history->key, key, pcmk__str_none) + && pcmk__str_eq(uname, history->node->details->uname, + pcmk__str_casei)) { + crm_trace("Skipping duplicate entry %s on %s", + history->key, pe__node_name(history->node)); + return; + } + } + + crm_trace("Adding entry for %s on %s to failed action list", + history->key, pe__node_name(history->node)); + crm_xml_add(history->xml, XML_ATTR_UNAME, history->node->details->uname); + crm_xml_add(history->xml, XML_LRM_ATTR_RSCID, history->rsc->id); + add_node_copy(history->rsc->cluster->failed, history->xml); +} + +static char * +last_change_str(const xmlNode *xml_op) +{ + time_t when; + char *result = NULL; + + if (crm_element_value_epoch(xml_op, XML_RSC_OP_LAST_CHANGE, + &when) == pcmk_ok) { + char *when_s = pcmk__epoch2str(&when, 0); + const char *p = strchr(when_s, ' '); + + // Skip day of week to make message shorter + if ((p != NULL) && (*(++p) != '\0')) { + result = strdup(p); + CRM_ASSERT(result != NULL); + } + free(when_s); + } + + if (result == NULL) { + result = strdup("unknown time"); + CRM_ASSERT(result != NULL); + } + + return result; +} + +/*! + * \internal + * \brief Compare two on-fail values + * + * \param[in] first One on-fail value to compare + * \param[in] second The other on-fail value to compare + * + * \return A negative number if second is more severe than first, zero if they + * are equal, or a positive number if first is more severe than second. + * \note This is only needed until the action_fail_response values can be + * renumbered at the next API compatibility break. + */ +static int +cmp_on_fail(enum action_fail_response first, enum action_fail_response second) +{ + switch (first) { + case action_fail_demote: + switch (second) { + case action_fail_ignore: + return 1; + case action_fail_demote: + return 0; + default: + return -1; + } + break; + + case action_fail_reset_remote: + switch (second) { + case action_fail_ignore: + case action_fail_demote: + case action_fail_recover: + return 1; + case action_fail_reset_remote: + return 0; + default: + return -1; + } + break; + + case action_fail_restart_container: + switch (second) { + case action_fail_ignore: + case action_fail_demote: + case action_fail_recover: + case action_fail_reset_remote: + return 1; + case action_fail_restart_container: + return 0; + default: + return -1; + } + break; + + default: + break; + } + switch (second) { + case action_fail_demote: + return (first == action_fail_ignore)? -1 : 1; + + case action_fail_reset_remote: + switch (first) { + case action_fail_ignore: + case action_fail_demote: + case action_fail_recover: + return -1; + default: + return 1; + } + break; + + case action_fail_restart_container: + switch (first) { + case action_fail_ignore: + case action_fail_demote: + case action_fail_recover: + case action_fail_reset_remote: + return -1; + default: + return 1; + } + break; + + default: + break; + } + return first - second; +} + +/*! + * \internal + * \brief Ban a resource (or its clone if an anonymous instance) from all nodes + * + * \param[in,out] rsc Resource to ban + */ +static void +ban_from_all_nodes(pe_resource_t *rsc) +{ + int score = -INFINITY; + pe_resource_t *fail_rsc = rsc; + + if (fail_rsc->parent != NULL) { + pe_resource_t *parent = uber_parent(fail_rsc); + + if (pe_rsc_is_anon_clone(parent)) { + /* For anonymous clones, if an operation with on-fail=stop fails for + * any instance, the entire clone must stop. + */ + fail_rsc = parent; + } + } + + // Ban the resource from all nodes + crm_notice("%s will not be started under current conditions", fail_rsc->id); + if (fail_rsc->allowed_nodes != NULL) { + g_hash_table_destroy(fail_rsc->allowed_nodes); + } + fail_rsc->allowed_nodes = pe__node_list2table(rsc->cluster->nodes); + g_hash_table_foreach(fail_rsc->allowed_nodes, set_node_score, &score); +} + +/*! + * \internal + * \brief Update resource role, failure handling, etc., after a failed action + * + * \param[in,out] history Parsed action result history + * \param[out] last_failure Set this to action XML + * \param[in,out] on_fail What should be done about the result + */ +static void +unpack_rsc_op_failure(struct action_history *history, xmlNode **last_failure, + enum action_fail_response *on_fail) +{ + bool is_probe = false; + pe_action_t *action = NULL; + char *last_change_s = NULL; + + *last_failure = history->xml; + + is_probe = pcmk_xe_is_probe(history->xml); + last_change_s = last_change_str(history->xml); + + if (!pcmk_is_set(history->rsc->cluster->flags, pe_flag_symmetric_cluster) + && (history->exit_status == PCMK_OCF_NOT_INSTALLED)) { + crm_trace("Unexpected result (%s%s%s) was recorded for " + "%s of %s on %s at %s " CRM_XS " exit-status=%d id=%s", + services_ocf_exitcode_str(history->exit_status), + (pcmk__str_empty(history->exit_reason)? "" : ": "), + pcmk__s(history->exit_reason, ""), + (is_probe? "probe" : history->task), history->rsc->id, + pe__node_name(history->node), last_change_s, + history->exit_status, history->id); + } else { + crm_warn("Unexpected result (%s%s%s) was recorded for " + "%s of %s on %s at %s " CRM_XS " exit-status=%d id=%s", + services_ocf_exitcode_str(history->exit_status), + (pcmk__str_empty(history->exit_reason)? "" : ": "), + pcmk__s(history->exit_reason, ""), + (is_probe? "probe" : history->task), history->rsc->id, + pe__node_name(history->node), last_change_s, + history->exit_status, history->id); + + if (is_probe && (history->exit_status != PCMK_OCF_OK) + && (history->exit_status != PCMK_OCF_NOT_RUNNING) + && (history->exit_status != PCMK_OCF_RUNNING_PROMOTED)) { + + /* A failed (not just unexpected) probe result could mean the user + * didn't know resources will be probed even where they can't run. + */ + crm_notice("If it is not possible for %s to run on %s, see " + "the resource-discovery option for location constraints", + history->rsc->id, pe__node_name(history->node)); + } + + record_failed_op(history); + } + + free(last_change_s); + + action = custom_action(history->rsc, strdup(history->key), history->task, + NULL, TRUE, FALSE, history->rsc->cluster); + if (cmp_on_fail(*on_fail, action->on_fail) < 0) { + pe_rsc_trace(history->rsc, "on-fail %s -> %s for %s (%s)", + fail2text(*on_fail), fail2text(action->on_fail), + action->uuid, history->key); + *on_fail = action->on_fail; + } + + if (strcmp(history->task, CRMD_ACTION_STOP) == 0) { + resource_location(history->rsc, history->node, -INFINITY, + "__stop_fail__", history->rsc->cluster); + + } else if (strcmp(history->task, CRMD_ACTION_MIGRATE) == 0) { + unpack_migrate_to_failure(history); + + } else if (strcmp(history->task, CRMD_ACTION_MIGRATED) == 0) { + unpack_migrate_from_failure(history); + + } else if (strcmp(history->task, CRMD_ACTION_PROMOTE) == 0) { + history->rsc->role = RSC_ROLE_PROMOTED; + + } else if (strcmp(history->task, CRMD_ACTION_DEMOTE) == 0) { + if (action->on_fail == action_fail_block) { + history->rsc->role = RSC_ROLE_PROMOTED; + pe__set_next_role(history->rsc, RSC_ROLE_STOPPED, + "demote with on-fail=block"); + + } else if (history->exit_status == PCMK_OCF_NOT_RUNNING) { + history->rsc->role = RSC_ROLE_STOPPED; + + } else { + /* Staying in the promoted role would put the scheduler and + * controller into a loop. Setting the role to unpromoted is not + * dangerous because the resource will be stopped as part of + * recovery, and any promotion will be ordered after that stop. + */ + history->rsc->role = RSC_ROLE_UNPROMOTED; + } + } + + if (is_probe && (history->exit_status == PCMK_OCF_NOT_INSTALLED)) { + /* leave stopped */ + pe_rsc_trace(history->rsc, "Leaving %s stopped", history->rsc->id); + history->rsc->role = RSC_ROLE_STOPPED; + + } else if (history->rsc->role < RSC_ROLE_STARTED) { + pe_rsc_trace(history->rsc, "Setting %s active", history->rsc->id); + set_active(history->rsc); + } + + pe_rsc_trace(history->rsc, + "Resource %s: role=%s, unclean=%s, on_fail=%s, fail_role=%s", + history->rsc->id, role2text(history->rsc->role), + pcmk__btoa(history->node->details->unclean), + fail2text(action->on_fail), role2text(action->fail_role)); + + if ((action->fail_role != RSC_ROLE_STARTED) + && (history->rsc->next_role < action->fail_role)) { + pe__set_next_role(history->rsc, action->fail_role, "failure"); + } + + if (action->fail_role == RSC_ROLE_STOPPED) { + ban_from_all_nodes(history->rsc); + } + + pe_free_action(action); +} + +/*! + * \internal + * \brief Block a resource with a failed action if it cannot be recovered + * + * If resource action is a failed stop and fencing is not possible, mark the + * resource as unmanaged and blocked, since recovery cannot be done. + * + * \param[in,out] history Parsed action history entry + */ +static void +block_if_unrecoverable(struct action_history *history) +{ + char *last_change_s = NULL; + + if (strcmp(history->task, CRMD_ACTION_STOP) != 0) { + return; // All actions besides stop are always recoverable + } + if (pe_can_fence(history->node->details->data_set, history->node)) { + return; // Failed stops are recoverable via fencing + } + + last_change_s = last_change_str(history->xml); + pe_proc_err("No further recovery can be attempted for %s " + "because %s on %s failed (%s%s%s) at %s " + CRM_XS " rc=%d id=%s", + history->rsc->id, history->task, pe__node_name(history->node), + services_ocf_exitcode_str(history->exit_status), + (pcmk__str_empty(history->exit_reason)? "" : ": "), + pcmk__s(history->exit_reason, ""), + last_change_s, history->exit_status, history->id); + + free(last_change_s); + + pe__clear_resource_flags(history->rsc, pe_rsc_managed); + pe__set_resource_flags(history->rsc, pe_rsc_block); +} + +/*! + * \internal + * \brief Update action history's execution status and why + * + * \param[in,out] history Parsed action history entry + * \param[out] why Where to store reason for update + * \param[in] value New value + * \param[in] reason Description of why value was changed + */ +static inline void +remap_because(struct action_history *history, const char **why, int value, + const char *reason) +{ + if (history->execution_status != value) { + history->execution_status = value; + *why = reason; + } +} + +/*! + * \internal + * \brief Remap informational monitor results and operation status + * + * For the monitor results, certain OCF codes are for providing extended information + * to the user about services that aren't yet failed but not entirely healthy either. + * These must be treated as the "normal" result by Pacemaker. + * + * For operation status, the action result can be used to determine an appropriate + * status for the purposes of responding to the action. The status provided by the + * executor is not directly usable since the executor does not know what was expected. + * + * \param[in,out] history Parsed action history entry + * \param[in,out] on_fail What should be done about the result + * \param[in] expired Whether result is expired + * + * \note If the result is remapped and the node is not shutting down or failed, + * the operation will be recorded in the data set's list of failed operations + * to highlight it for the user. + * + * \note This may update the resource's current and next role. + */ +static void +remap_operation(struct action_history *history, + enum action_fail_response *on_fail, bool expired) +{ + bool is_probe = false; + int orig_exit_status = history->exit_status; + int orig_exec_status = history->execution_status; + const char *why = NULL; + const char *task = history->task; + + // Remap degraded results to their successful counterparts + history->exit_status = pcmk__effective_rc(history->exit_status); + if (history->exit_status != orig_exit_status) { + why = "degraded result"; + if (!expired && (!history->node->details->shutdown + || history->node->details->online)) { + record_failed_op(history); + } + } + + if (!pe_rsc_is_bundled(history->rsc) + && pcmk_xe_mask_probe_failure(history->xml) + && ((history->execution_status != PCMK_EXEC_DONE) + || (history->exit_status != PCMK_OCF_NOT_RUNNING))) { + history->execution_status = PCMK_EXEC_DONE; + history->exit_status = PCMK_OCF_NOT_RUNNING; + why = "equivalent probe result"; + } + + /* If the executor reported an execution status of anything but done or + * error, consider that final. But for done or error, we know better whether + * it should be treated as a failure or not, because we know the expected + * result. + */ + switch (history->execution_status) { + case PCMK_EXEC_DONE: + case PCMK_EXEC_ERROR: + break; + + // These should be treated as node-fatal + case PCMK_EXEC_NO_FENCE_DEVICE: + case PCMK_EXEC_NO_SECRETS: + remap_because(history, &why, PCMK_EXEC_ERROR_HARD, + "node-fatal error"); + goto remap_done; + + default: + goto remap_done; + } + + is_probe = pcmk_xe_is_probe(history->xml); + if (is_probe) { + task = "probe"; + } + + if (history->expected_exit_status < 0) { + /* Pre-1.0 Pacemaker versions, and Pacemaker 1.1.6 or earlier with + * Heartbeat 2.0.7 or earlier as the cluster layer, did not include the + * expected exit status in the transition key, which (along with the + * similar case of a corrupted transition key in the CIB) will be + * reported to this function as -1. Pacemaker 2.0+ does not support + * rolling upgrades from those versions or processing of saved CIB files + * from those versions, so we do not need to care much about this case. + */ + remap_because(history, &why, PCMK_EXEC_ERROR, + "obsolete history format"); + crm_warn("Expected result not found for %s on %s " + "(corrupt or obsolete CIB?)", + history->key, pe__node_name(history->node)); + + } else if (history->exit_status == history->expected_exit_status) { + remap_because(history, &why, PCMK_EXEC_DONE, "expected result"); + + } else { + remap_because(history, &why, PCMK_EXEC_ERROR, "unexpected result"); + pe_rsc_debug(history->rsc, + "%s on %s: expected %d (%s), got %d (%s%s%s)", + history->key, pe__node_name(history->node), + history->expected_exit_status, + services_ocf_exitcode_str(history->expected_exit_status), + history->exit_status, + services_ocf_exitcode_str(history->exit_status), + (pcmk__str_empty(history->exit_reason)? "" : ": "), + pcmk__s(history->exit_reason, "")); + } + + switch (history->exit_status) { + case PCMK_OCF_OK: + if (is_probe + && (history->expected_exit_status == PCMK_OCF_NOT_RUNNING)) { + char *last_change_s = last_change_str(history->xml); + + remap_because(history, &why, PCMK_EXEC_DONE, "probe"); + pe_rsc_info(history->rsc, "Probe found %s active on %s at %s", + history->rsc->id, pe__node_name(history->node), + last_change_s); + free(last_change_s); + } + break; + + case PCMK_OCF_NOT_RUNNING: + if (is_probe + || (history->expected_exit_status == history->exit_status) + || !pcmk_is_set(history->rsc->flags, pe_rsc_managed)) { + + /* For probes, recurring monitors for the Stopped role, and + * unmanaged resources, "not running" is not considered a + * failure. + */ + remap_because(history, &why, PCMK_EXEC_DONE, "exit status"); + history->rsc->role = RSC_ROLE_STOPPED; + *on_fail = action_fail_ignore; + pe__set_next_role(history->rsc, RSC_ROLE_UNKNOWN, + "not running"); + } + break; + + case PCMK_OCF_RUNNING_PROMOTED: + if (is_probe + && (history->exit_status != history->expected_exit_status)) { + char *last_change_s = last_change_str(history->xml); + + remap_because(history, &why, PCMK_EXEC_DONE, "probe"); + pe_rsc_info(history->rsc, + "Probe found %s active and promoted on %s at %s", + history->rsc->id, pe__node_name(history->node), + last_change_s); + free(last_change_s); + } + if (!expired + || (history->exit_status == history->expected_exit_status)) { + history->rsc->role = RSC_ROLE_PROMOTED; + } + break; + + case PCMK_OCF_FAILED_PROMOTED: + if (!expired) { + history->rsc->role = RSC_ROLE_PROMOTED; + } + remap_because(history, &why, PCMK_EXEC_ERROR, "exit status"); + break; + + case PCMK_OCF_NOT_CONFIGURED: + remap_because(history, &why, PCMK_EXEC_ERROR_FATAL, "exit status"); + break; + + case PCMK_OCF_UNIMPLEMENT_FEATURE: + { + guint interval_ms = 0; + crm_element_value_ms(history->xml, XML_LRM_ATTR_INTERVAL_MS, + &interval_ms); + + if (interval_ms == 0) { + if (!expired) { + block_if_unrecoverable(history); + } + remap_because(history, &why, PCMK_EXEC_ERROR_HARD, + "exit status"); + } else { + remap_because(history, &why, PCMK_EXEC_NOT_SUPPORTED, + "exit status"); + } + } + break; + + case PCMK_OCF_NOT_INSTALLED: + case PCMK_OCF_INVALID_PARAM: + case PCMK_OCF_INSUFFICIENT_PRIV: + if (!expired) { + block_if_unrecoverable(history); + } + remap_because(history, &why, PCMK_EXEC_ERROR_HARD, "exit status"); + break; + + default: + if (history->execution_status == PCMK_EXEC_DONE) { + char *last_change_s = last_change_str(history->xml); + + crm_info("Treating unknown exit status %d from %s of %s " + "on %s at %s as failure", + history->exit_status, task, history->rsc->id, + pe__node_name(history->node), last_change_s); + remap_because(history, &why, PCMK_EXEC_ERROR, + "unknown exit status"); + free(last_change_s); + } + break; + } + +remap_done: + if (why != NULL) { + pe_rsc_trace(history->rsc, + "Remapped %s result from [%s: %s] to [%s: %s] " + "because of %s", + history->key, pcmk_exec_status_str(orig_exec_status), + crm_exit_str(orig_exit_status), + pcmk_exec_status_str(history->execution_status), + crm_exit_str(history->exit_status), why); + } +} + +// return TRUE if start or monitor last failure but parameters changed +static bool +should_clear_for_param_change(const xmlNode *xml_op, const char *task, + pe_resource_t *rsc, pe_node_t *node) +{ + if (!strcmp(task, "start") || !strcmp(task, "monitor")) { + + if (pe__bundle_needs_remote_name(rsc)) { + /* We haven't allocated resources yet, so we can't reliably + * substitute addr parameters for the REMOTE_CONTAINER_HACK. + * When that's needed, defer the check until later. + */ + pe__add_param_check(xml_op, rsc, node, pe_check_last_failure, + rsc->cluster); + + } else { + op_digest_cache_t *digest_data = NULL; + + digest_data = rsc_action_digest_cmp(rsc, xml_op, node, + rsc->cluster); + switch (digest_data->rc) { + case RSC_DIGEST_UNKNOWN: + crm_trace("Resource %s history entry %s on %s" + " has no digest to compare", + rsc->id, pe__xe_history_key(xml_op), + node->details->id); + break; + case RSC_DIGEST_MATCH: + break; + default: + return TRUE; + } + } + } + return FALSE; +} + +// Order action after fencing of remote node, given connection rsc +static void +order_after_remote_fencing(pe_action_t *action, pe_resource_t *remote_conn, + pe_working_set_t *data_set) +{ + pe_node_t *remote_node = pe_find_node(data_set->nodes, remote_conn->id); + + if (remote_node) { + pe_action_t *fence = pe_fence_op(remote_node, NULL, TRUE, NULL, + FALSE, data_set); + + order_actions(fence, action, pe_order_implies_then); + } +} + +static bool +should_ignore_failure_timeout(const pe_resource_t *rsc, const char *task, + guint interval_ms, bool is_last_failure) +{ + /* Clearing failures of recurring monitors has special concerns. The + * executor reports only changes in the monitor result, so if the + * monitor is still active and still getting the same failure result, + * that will go undetected after the failure is cleared. + * + * Also, the operation history will have the time when the recurring + * monitor result changed to the given code, not the time when the + * result last happened. + * + * @TODO We probably should clear such failures only when the failure + * timeout has passed since the last occurrence of the failed result. + * However we don't record that information. We could maybe approximate + * that by clearing only if there is a more recent successful monitor or + * stop result, but we don't even have that information at this point + * since we are still unpacking the resource's operation history. + * + * This is especially important for remote connection resources with a + * reconnect interval, so in that case, we skip clearing failures + * if the remote node hasn't been fenced. + */ + if (rsc->remote_reconnect_ms + && pcmk_is_set(rsc->cluster->flags, pe_flag_stonith_enabled) + && (interval_ms != 0) && pcmk__str_eq(task, CRMD_ACTION_STATUS, pcmk__str_casei)) { + + pe_node_t *remote_node = pe_find_node(rsc->cluster->nodes, rsc->id); + + if (remote_node && !remote_node->details->remote_was_fenced) { + if (is_last_failure) { + crm_info("Waiting to clear monitor failure for remote node %s" + " until fencing has occurred", rsc->id); + } + return TRUE; + } + } + return FALSE; +} + +/*! + * \internal + * \brief Check operation age and schedule failure clearing when appropriate + * + * This function has two distinct purposes. The first is to check whether an + * operation history entry is expired (i.e. the resource has a failure timeout, + * the entry is older than the timeout, and the resource either has no fail + * count or its fail count is entirely older than the timeout). The second is to + * schedule fail count clearing when appropriate (i.e. the operation is expired + * and either the resource has an expired fail count or the operation is a + * last_failure for a remote connection resource with a reconnect interval, + * or the operation is a last_failure for a start or monitor operation and the + * resource's parameters have changed since the operation). + * + * \param[in,out] history Parsed action result history + * + * \return true if operation history entry is expired, otherwise false + */ +static bool +check_operation_expiry(struct action_history *history) +{ + bool expired = false; + bool is_last_failure = pcmk__ends_with(history->id, "_last_failure_0"); + time_t last_run = 0; + int unexpired_fail_count = 0; + const char *clear_reason = NULL; + + if (history->execution_status == PCMK_EXEC_NOT_INSTALLED) { + pe_rsc_trace(history->rsc, + "Resource history entry %s on %s is not expired: " + "Not Installed does not expire", + history->id, pe__node_name(history->node)); + return false; // "Not installed" must always be cleared manually + } + + if ((history->rsc->failure_timeout > 0) + && (crm_element_value_epoch(history->xml, XML_RSC_OP_LAST_CHANGE, + &last_run) == 0)) { + + // Resource has a failure-timeout, and history entry has a timestamp + + time_t now = get_effective_time(history->rsc->cluster); + time_t last_failure = 0; + + // Is this particular operation history older than the failure timeout? + if ((now >= (last_run + history->rsc->failure_timeout)) + && !should_ignore_failure_timeout(history->rsc, history->task, + history->interval_ms, + is_last_failure)) { + expired = true; + } + + // Does the resource as a whole have an unexpired fail count? + unexpired_fail_count = pe_get_failcount(history->node, history->rsc, + &last_failure, pe_fc_effective, + history->xml); + + // Update scheduler recheck time according to *last* failure + crm_trace("%s@%lld is %sexpired @%lld with unexpired_failures=%d timeout=%ds" + " last-failure@%lld", + history->id, (long long) last_run, (expired? "" : "not "), + (long long) now, unexpired_fail_count, + history->rsc->failure_timeout, (long long) last_failure); + last_failure += history->rsc->failure_timeout + 1; + if (unexpired_fail_count && (now < last_failure)) { + pe__update_recheck_time(last_failure, history->rsc->cluster); + } + } + + if (expired) { + if (pe_get_failcount(history->node, history->rsc, NULL, pe_fc_default, + history->xml)) { + // There is a fail count ignoring timeout + + if (unexpired_fail_count == 0) { + // There is no fail count considering timeout + clear_reason = "it expired"; + + } else { + /* This operation is old, but there is an unexpired fail count. + * In a properly functioning cluster, this should only be + * possible if this operation is not a failure (otherwise the + * fail count should be expired too), so this is really just a + * failsafe. + */ + pe_rsc_trace(history->rsc, + "Resource history entry %s on %s is not expired: " + "Unexpired fail count", + history->id, pe__node_name(history->node)); + expired = false; + } + + } else if (is_last_failure + && (history->rsc->remote_reconnect_ms != 0)) { + /* Clear any expired last failure when reconnect interval is set, + * even if there is no fail count. + */ + clear_reason = "reconnect interval is set"; + } + } + + if (!expired && is_last_failure + && should_clear_for_param_change(history->xml, history->task, + history->rsc, history->node)) { + clear_reason = "resource parameters have changed"; + } + + if (clear_reason != NULL) { + // Schedule clearing of the fail count + pe_action_t *clear_op = pe__clear_failcount(history->rsc, history->node, + clear_reason, + history->rsc->cluster); + + if (pcmk_is_set(history->rsc->cluster->flags, pe_flag_stonith_enabled) + && (history->rsc->remote_reconnect_ms != 0)) { + /* If we're clearing a remote connection due to a reconnect + * interval, we want to wait until any scheduled fencing + * completes. + * + * We could limit this to remote_node->details->unclean, but at + * this point, that's always true (it won't be reliable until + * after unpack_node_history() is done). + */ + crm_info("Clearing %s failure will wait until any scheduled " + "fencing of %s completes", + history->task, history->rsc->id); + order_after_remote_fencing(clear_op, history->rsc, + history->rsc->cluster); + } + } + + if (expired && (history->interval_ms == 0) + && pcmk__str_eq(history->task, CRMD_ACTION_STATUS, pcmk__str_none)) { + switch (history->exit_status) { + case PCMK_OCF_OK: + case PCMK_OCF_NOT_RUNNING: + case PCMK_OCF_RUNNING_PROMOTED: + case PCMK_OCF_DEGRADED: + case PCMK_OCF_DEGRADED_PROMOTED: + // Don't expire probes that return these values + pe_rsc_trace(history->rsc, + "Resource history entry %s on %s is not expired: " + "Probe result", + history->id, pe__node_name(history->node)); + expired = false; + break; + } + } + + return expired; +} + +int +pe__target_rc_from_xml(const xmlNode *xml_op) +{ + int target_rc = 0; + const char *key = crm_element_value(xml_op, XML_ATTR_TRANSITION_KEY); + + if (key == NULL) { + return -1; + } + decode_transition_key(key, NULL, NULL, NULL, &target_rc); + return target_rc; +} + +/*! + * \internal + * \brief Get the failure handling for an action + * + * \param[in,out] history Parsed action history entry + * + * \return Failure handling appropriate to action + */ +static enum action_fail_response +get_action_on_fail(struct action_history *history) +{ + enum action_fail_response result = action_fail_recover; + pe_action_t *action = custom_action(history->rsc, strdup(history->key), + history->task, NULL, TRUE, FALSE, + history->rsc->cluster); + + result = action->on_fail; + pe_free_action(action); + return result; +} + +/*! + * \internal + * \brief Update a resource's state for an action result + * + * \param[in,out] history Parsed action history entry + * \param[in] exit_status Exit status to base new state on + * \param[in] last_failure Resource's last_failure entry, if known + * \param[in,out] on_fail Resource's current failure handling + */ +static void +update_resource_state(struct action_history *history, int exit_status, + const xmlNode *last_failure, + enum action_fail_response *on_fail) +{ + bool clear_past_failure = false; + + if ((exit_status == PCMK_OCF_NOT_INSTALLED) + || (!pe_rsc_is_bundled(history->rsc) + && pcmk_xe_mask_probe_failure(history->xml))) { + history->rsc->role = RSC_ROLE_STOPPED; + + } else if (exit_status == PCMK_OCF_NOT_RUNNING) { + clear_past_failure = true; + + } else if (pcmk__str_eq(history->task, CRMD_ACTION_STATUS, + pcmk__str_none)) { + if ((last_failure != NULL) + && pcmk__str_eq(history->key, pe__xe_history_key(last_failure), + pcmk__str_none)) { + clear_past_failure = true; + } + if (history->rsc->role < RSC_ROLE_STARTED) { + set_active(history->rsc); + } + + } else if (pcmk__str_eq(history->task, CRMD_ACTION_START, pcmk__str_none)) { + history->rsc->role = RSC_ROLE_STARTED; + clear_past_failure = true; + + } else if (pcmk__str_eq(history->task, CRMD_ACTION_STOP, pcmk__str_none)) { + history->rsc->role = RSC_ROLE_STOPPED; + clear_past_failure = true; + + } else if (pcmk__str_eq(history->task, CRMD_ACTION_PROMOTE, + pcmk__str_none)) { + history->rsc->role = RSC_ROLE_PROMOTED; + clear_past_failure = true; + + } else if (pcmk__str_eq(history->task, CRMD_ACTION_DEMOTE, + pcmk__str_none)) { + if (*on_fail == action_fail_demote) { + // Demote clears an error only if on-fail=demote + clear_past_failure = true; + } + history->rsc->role = RSC_ROLE_UNPROMOTED; + + } else if (pcmk__str_eq(history->task, CRMD_ACTION_MIGRATED, + pcmk__str_none)) { + history->rsc->role = RSC_ROLE_STARTED; + clear_past_failure = true; + + } else if (pcmk__str_eq(history->task, CRMD_ACTION_MIGRATE, + pcmk__str_none)) { + unpack_migrate_to_success(history); + + } else if (history->rsc->role < RSC_ROLE_STARTED) { + pe_rsc_trace(history->rsc, "%s active on %s", + history->rsc->id, pe__node_name(history->node)); + set_active(history->rsc); + } + + if (!clear_past_failure) { + return; + } + + switch (*on_fail) { + case action_fail_stop: + case action_fail_fence: + case action_fail_migrate: + case action_fail_standby: + pe_rsc_trace(history->rsc, + "%s (%s) is not cleared by a completed %s", + history->rsc->id, fail2text(*on_fail), history->task); + break; + + case action_fail_block: + case action_fail_ignore: + case action_fail_demote: + case action_fail_recover: + case action_fail_restart_container: + *on_fail = action_fail_ignore; + pe__set_next_role(history->rsc, RSC_ROLE_UNKNOWN, + "clear past failures"); + break; + + case action_fail_reset_remote: + if (history->rsc->remote_reconnect_ms == 0) { + /* With no reconnect interval, the connection is allowed to + * start again after the remote node is fenced and + * completely stopped. (With a reconnect interval, we wait + * for the failure to be cleared entirely before attempting + * to reconnect.) + */ + *on_fail = action_fail_ignore; + pe__set_next_role(history->rsc, RSC_ROLE_UNKNOWN, + "clear past failures and reset remote"); + } + break; + } +} + +/*! + * \internal + * \brief Check whether a given history entry matters for resource state + * + * \param[in] history Parsed action history entry + * + * \return true if action can affect resource state, otherwise false + */ +static inline bool +can_affect_state(struct action_history *history) +{ +#if 0 + /* @COMPAT It might be better to parse only actions we know we're interested + * in, rather than exclude a couple we don't. However that would be a + * behavioral change that should be done at a major or minor series release. + * Currently, unknown operations can affect whether a resource is considered + * active and/or failed. + */ + return pcmk__str_any_of(history->task, CRMD_ACTION_STATUS, + CRMD_ACTION_START, CRMD_ACTION_STOP, + CRMD_ACTION_PROMOTE, CRMD_ACTION_DEMOTE, + CRMD_ACTION_MIGRATE, CRMD_ACTION_MIGRATED, + "asyncmon", NULL); +#else + return !pcmk__str_any_of(history->task, CRMD_ACTION_NOTIFY, + CRMD_ACTION_METADATA, NULL); +#endif +} + +/*! + * \internal + * \brief Unpack execution/exit status and exit reason from a history entry + * + * \param[in,out] history Action history entry to unpack + * + * \return Standard Pacemaker return code + */ +static int +unpack_action_result(struct action_history *history) +{ + if ((crm_element_value_int(history->xml, XML_LRM_ATTR_OPSTATUS, + &(history->execution_status)) < 0) + || (history->execution_status < PCMK_EXEC_PENDING) + || (history->execution_status > PCMK_EXEC_MAX) + || (history->execution_status == PCMK_EXEC_CANCELLED)) { + crm_err("Ignoring resource history entry %s for %s on %s " + "with invalid " XML_LRM_ATTR_OPSTATUS " '%s'", + history->id, history->rsc->id, pe__node_name(history->node), + pcmk__s(crm_element_value(history->xml, XML_LRM_ATTR_OPSTATUS), + "")); + return pcmk_rc_unpack_error; + } + if ((crm_element_value_int(history->xml, XML_LRM_ATTR_RC, + &(history->exit_status)) < 0) + || (history->exit_status < 0) || (history->exit_status > CRM_EX_MAX)) { +#if 0 + /* @COMPAT We should ignore malformed entries, but since that would + * change behavior, it should be done at a major or minor series + * release. + */ + crm_err("Ignoring resource history entry %s for %s on %s " + "with invalid " XML_LRM_ATTR_RC " '%s'", + history->id, history->rsc->id, pe__node_name(history->node), + pcmk__s(crm_element_value(history->xml, XML_LRM_ATTR_RC), + "")); + return pcmk_rc_unpack_error; +#else + history->exit_status = CRM_EX_ERROR; +#endif + } + history->exit_reason = crm_element_value(history->xml, + XML_LRM_ATTR_EXIT_REASON); + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Process an action history entry whose result expired + * + * \param[in,out] history Parsed action history entry + * \param[in] orig_exit_status Action exit status before remapping + * + * \return Standard Pacemaker return code (in particular, pcmk_rc_ok means the + * entry needs no further processing) + */ +static int +process_expired_result(struct action_history *history, int orig_exit_status) +{ + if (!pe_rsc_is_bundled(history->rsc) + && pcmk_xe_mask_probe_failure(history->xml) + && (orig_exit_status != history->expected_exit_status)) { + + if (history->rsc->role <= RSC_ROLE_STOPPED) { + history->rsc->role = RSC_ROLE_UNKNOWN; + } + crm_trace("Ignoring resource history entry %s for probe of %s on %s: " + "Masked failure expired", + history->id, history->rsc->id, + pe__node_name(history->node)); + return pcmk_rc_ok; + } + + if (history->exit_status == history->expected_exit_status) { + return pcmk_rc_undetermined; // Only failures expire + } + + if (history->interval_ms == 0) { + crm_notice("Ignoring resource history entry %s for %s of %s on %s: " + "Expired failure", + history->id, history->task, history->rsc->id, + pe__node_name(history->node)); + return pcmk_rc_ok; + } + + if (history->node->details->online && !history->node->details->unclean) { + /* Reschedule the recurring action. schedule_cancel() won't work at + * this stage, so as a hacky workaround, forcibly change the restart + * digest so pcmk__check_action_config() does what we want later. + * + * @TODO We should skip this if there is a newer successful monitor. + * Also, this causes rescheduling only if the history entry + * has an op-digest (which the expire-non-blocked-failure + * scheduler regression test doesn't, but that may not be a + * realistic scenario in production). + */ + crm_notice("Rescheduling %s-interval %s of %s on %s " + "after failure expired", + pcmk__readable_interval(history->interval_ms), history->task, + history->rsc->id, pe__node_name(history->node)); + crm_xml_add(history->xml, XML_LRM_ATTR_RESTART_DIGEST, + "calculated-failure-timeout"); + return pcmk_rc_ok; + } + + return pcmk_rc_undetermined; +} + +/*! + * \internal + * \brief Process a masked probe failure + * + * \param[in,out] history Parsed action history entry + * \param[in] orig_exit_status Action exit status before remapping + * \param[in] last_failure Resource's last_failure entry, if known + * \param[in,out] on_fail Resource's current failure handling + */ +static void +mask_probe_failure(struct action_history *history, int orig_exit_status, + const xmlNode *last_failure, + enum action_fail_response *on_fail) +{ + pe_resource_t *ban_rsc = history->rsc; + + if (!pcmk_is_set(history->rsc->flags, pe_rsc_unique)) { + ban_rsc = uber_parent(history->rsc); + } + + crm_notice("Treating probe result '%s' for %s on %s as 'not running'", + services_ocf_exitcode_str(orig_exit_status), history->rsc->id, + pe__node_name(history->node)); + update_resource_state(history, history->expected_exit_status, last_failure, + on_fail); + crm_xml_add(history->xml, XML_ATTR_UNAME, history->node->details->uname); + + record_failed_op(history); + resource_location(ban_rsc, history->node, -INFINITY, "masked-probe-failure", + history->rsc->cluster); +} + +/*! + * \internal Check whether a given failure is for a given pending action + * + * \param[in] history Parsed history entry for pending action + * \param[in] last_failure Resource's last_failure entry, if known + * + * \return true if \p last_failure is failure of pending action in \p history, + * otherwise false + * \note Both \p history and \p last_failure must come from the same + * lrm_resource block, as node and resource are assumed to be the same. + */ +static bool +failure_is_newer(const struct action_history *history, + const xmlNode *last_failure) +{ + guint failure_interval_ms = 0U; + long long failure_change = 0LL; + long long this_change = 0LL; + + if (last_failure == NULL) { + return false; // Resource has no last_failure entry + } + + if (!pcmk__str_eq(history->task, + crm_element_value(last_failure, XML_LRM_ATTR_TASK), + pcmk__str_none)) { + return false; // last_failure is for different action + } + + if ((crm_element_value_ms(last_failure, XML_LRM_ATTR_INTERVAL_MS, + &failure_interval_ms) != pcmk_ok) + || (history->interval_ms != failure_interval_ms)) { + return false; // last_failure is for action with different interval + } + + if ((pcmk__scan_ll(crm_element_value(history->xml, XML_RSC_OP_LAST_CHANGE), + &this_change, 0LL) != pcmk_rc_ok) + || (pcmk__scan_ll(crm_element_value(last_failure, + XML_RSC_OP_LAST_CHANGE), + &failure_change, 0LL) != pcmk_rc_ok) + || (failure_change < this_change)) { + return false; // Failure is not known to be newer + } + + return true; +} + +/*! + * \internal + * \brief Update a resource's role etc. for a pending action + * + * \param[in,out] history Parsed history entry for pending action + * \param[in] last_failure Resource's last_failure entry, if known + */ +static void +process_pending_action(struct action_history *history, + const xmlNode *last_failure) +{ + /* For recurring monitors, a failure is recorded only in RSC_last_failure_0, + * and there might be a RSC_monitor_INTERVAL entry with the last successful + * or pending result. + * + * If last_failure contains the failure of the pending recurring monitor + * we're processing here, and is newer, the action is no longer pending. + * (Pending results have call ID -1, which sorts last, so the last failure + * if any should be known.) + */ + if (failure_is_newer(history, last_failure)) { + return; + } + + if (strcmp(history->task, CRMD_ACTION_START) == 0) { + pe__set_resource_flags(history->rsc, pe_rsc_start_pending); + set_active(history->rsc); + + } else if (strcmp(history->task, CRMD_ACTION_PROMOTE) == 0) { + history->rsc->role = RSC_ROLE_PROMOTED; + + } else if ((strcmp(history->task, CRMD_ACTION_MIGRATE) == 0) + && history->node->details->unclean) { + /* A migrate_to action is pending on a unclean source, so force a stop + * on the target. + */ + const char *migrate_target = NULL; + pe_node_t *target = NULL; + + migrate_target = crm_element_value(history->xml, + XML_LRM_ATTR_MIGRATE_TARGET); + target = pe_find_node(history->rsc->cluster->nodes, migrate_target); + if (target != NULL) { + stop_action(history->rsc, target, FALSE); + } + } + + if (history->rsc->pending_task != NULL) { + /* There should never be multiple pending actions, but as a failsafe, + * just remember the first one processed for display purposes. + */ + return; + } + + if (pcmk_is_probe(history->task, history->interval_ms)) { + /* Pending probes are currently never displayed, even if pending + * operations are requested. If we ever want to change that, + * enable the below and the corresponding part of + * native.c:native_pending_task(). + */ +#if 0 + history->rsc->pending_task = strdup("probe"); + history->rsc->pending_node = history->node; +#endif + } else { + history->rsc->pending_task = strdup(history->task); + history->rsc->pending_node = history->node; + } +} + +static void +unpack_rsc_op(pe_resource_t *rsc, pe_node_t *node, xmlNode *xml_op, + xmlNode **last_failure, enum action_fail_response *on_fail) +{ + int old_rc = 0; + bool expired = false; + pe_resource_t *parent = rsc; + enum action_fail_response failure_strategy = action_fail_recover; + + struct action_history history = { + .rsc = rsc, + .node = node, + .xml = xml_op, + .execution_status = PCMK_EXEC_UNKNOWN, + }; + + CRM_CHECK(rsc && node && xml_op, return); + + history.id = ID(xml_op); + if (history.id == NULL) { + crm_err("Ignoring resource history entry for %s on %s without ID", + rsc->id, pe__node_name(node)); + return; + } + + // Task and interval + history.task = crm_element_value(xml_op, XML_LRM_ATTR_TASK); + if (history.task == NULL) { + crm_err("Ignoring resource history entry %s for %s on %s without " + XML_LRM_ATTR_TASK, history.id, rsc->id, pe__node_name(node)); + return; + } + crm_element_value_ms(xml_op, XML_LRM_ATTR_INTERVAL_MS, + &(history.interval_ms)); + if (!can_affect_state(&history)) { + pe_rsc_trace(rsc, + "Ignoring resource history entry %s for %s on %s " + "with irrelevant action '%s'", + history.id, rsc->id, pe__node_name(node), history.task); + return; + } + + if (unpack_action_result(&history) != pcmk_rc_ok) { + return; // Error already logged + } + + history.expected_exit_status = pe__target_rc_from_xml(xml_op); + history.key = pe__xe_history_key(xml_op); + crm_element_value_int(xml_op, XML_LRM_ATTR_CALLID, &(history.call_id)); + + pe_rsc_trace(rsc, "Unpacking %s (%s call %d on %s): %s (%s)", + history.id, history.task, history.call_id, pe__node_name(node), + pcmk_exec_status_str(history.execution_status), + crm_exit_str(history.exit_status)); + + if (node->details->unclean) { + pe_rsc_trace(rsc, + "%s is running on %s, which is unclean (further action " + "depends on value of stop's on-fail attribute)", + rsc->id, pe__node_name(node)); + } + + expired = check_operation_expiry(&history); + old_rc = history.exit_status; + + remap_operation(&history, on_fail, expired); + + if (expired && (process_expired_result(&history, old_rc) == pcmk_rc_ok)) { + goto done; + } + + if (!pe_rsc_is_bundled(rsc) && pcmk_xe_mask_probe_failure(xml_op)) { + mask_probe_failure(&history, old_rc, *last_failure, on_fail); + goto done; + } + + if (!pcmk_is_set(rsc->flags, pe_rsc_unique)) { + parent = uber_parent(rsc); + } + + switch (history.execution_status) { + case PCMK_EXEC_PENDING: + process_pending_action(&history, *last_failure); + goto done; + + case PCMK_EXEC_DONE: + update_resource_state(&history, history.exit_status, *last_failure, + on_fail); + goto done; + + case PCMK_EXEC_NOT_INSTALLED: + failure_strategy = get_action_on_fail(&history); + if (failure_strategy == action_fail_ignore) { + crm_warn("Cannot ignore failed %s of %s on %s: " + "Resource agent doesn't exist " + CRM_XS " status=%d rc=%d id=%s", + history.task, rsc->id, pe__node_name(node), + history.execution_status, history.exit_status, + history.id); + /* Also for printing it as "FAILED" by marking it as pe_rsc_failed later */ + *on_fail = action_fail_migrate; + } + resource_location(parent, node, -INFINITY, "hard-error", + rsc->cluster); + unpack_rsc_op_failure(&history, last_failure, on_fail); + goto done; + + case PCMK_EXEC_NOT_CONNECTED: + if (pe__is_guest_or_remote_node(node) + && pcmk_is_set(node->details->remote_rsc->flags, pe_rsc_managed)) { + /* We should never get into a situation where a managed remote + * connection resource is considered OK but a resource action + * behind the connection gets a "not connected" status. But as a + * fail-safe in case a bug or unusual circumstances do lead to + * that, ensure the remote connection is considered failed. + */ + pe__set_resource_flags(node->details->remote_rsc, + pe_rsc_failed|pe_rsc_stop); + } + break; // Not done, do error handling + + case PCMK_EXEC_ERROR: + case PCMK_EXEC_ERROR_HARD: + case PCMK_EXEC_ERROR_FATAL: + case PCMK_EXEC_TIMEOUT: + case PCMK_EXEC_NOT_SUPPORTED: + case PCMK_EXEC_INVALID: + break; // Not done, do error handling + + default: // No other value should be possible at this point + break; + } + + failure_strategy = get_action_on_fail(&history); + if ((failure_strategy == action_fail_ignore) + || (failure_strategy == action_fail_restart_container + && (strcmp(history.task, CRMD_ACTION_STOP) == 0))) { + + char *last_change_s = last_change_str(xml_op); + + crm_warn("Pretending failed %s (%s%s%s) of %s on %s at %s succeeded " + CRM_XS " %s", + history.task, services_ocf_exitcode_str(history.exit_status), + (pcmk__str_empty(history.exit_reason)? "" : ": "), + pcmk__s(history.exit_reason, ""), rsc->id, pe__node_name(node), + last_change_s, history.id); + free(last_change_s); + + update_resource_state(&history, history.expected_exit_status, + *last_failure, on_fail); + crm_xml_add(xml_op, XML_ATTR_UNAME, node->details->uname); + pe__set_resource_flags(rsc, pe_rsc_failure_ignored); + + record_failed_op(&history); + + if ((failure_strategy == action_fail_restart_container) + && cmp_on_fail(*on_fail, action_fail_recover) <= 0) { + *on_fail = failure_strategy; + } + + } else { + unpack_rsc_op_failure(&history, last_failure, on_fail); + + if (history.execution_status == PCMK_EXEC_ERROR_HARD) { + uint8_t log_level = LOG_ERR; + + if (history.exit_status == PCMK_OCF_NOT_INSTALLED) { + log_level = LOG_NOTICE; + } + do_crm_log(log_level, + "Preventing %s from restarting on %s because " + "of hard failure (%s%s%s) " CRM_XS " %s", + parent->id, pe__node_name(node), + services_ocf_exitcode_str(history.exit_status), + (pcmk__str_empty(history.exit_reason)? "" : ": "), + pcmk__s(history.exit_reason, ""), history.id); + resource_location(parent, node, -INFINITY, "hard-error", + rsc->cluster); + + } else if (history.execution_status == PCMK_EXEC_ERROR_FATAL) { + crm_err("Preventing %s from restarting anywhere because " + "of fatal failure (%s%s%s) " CRM_XS " %s", + parent->id, services_ocf_exitcode_str(history.exit_status), + (pcmk__str_empty(history.exit_reason)? "" : ": "), + pcmk__s(history.exit_reason, ""), history.id); + resource_location(parent, NULL, -INFINITY, "fatal-error", + rsc->cluster); + } + } + +done: + pe_rsc_trace(rsc, "%s role on %s after %s is %s (next %s)", + rsc->id, pe__node_name(node), history.id, + role2text(rsc->role), role2text(rsc->next_role)); +} + +static void +add_node_attrs(const xmlNode *xml_obj, pe_node_t *node, bool overwrite, + pe_working_set_t *data_set) +{ + const char *cluster_name = NULL; + + pe_rule_eval_data_t rule_data = { + .node_hash = NULL, + .role = RSC_ROLE_UNKNOWN, + .now = data_set->now, + .match_data = NULL, + .rsc_data = NULL, + .op_data = NULL + }; + + g_hash_table_insert(node->details->attrs, + strdup(CRM_ATTR_UNAME), strdup(node->details->uname)); + + g_hash_table_insert(node->details->attrs, strdup(CRM_ATTR_ID), + strdup(node->details->id)); + if (pcmk__str_eq(node->details->id, data_set->dc_uuid, pcmk__str_casei)) { + data_set->dc_node = node; + node->details->is_dc = TRUE; + g_hash_table_insert(node->details->attrs, + strdup(CRM_ATTR_IS_DC), strdup(XML_BOOLEAN_TRUE)); + } else { + g_hash_table_insert(node->details->attrs, + strdup(CRM_ATTR_IS_DC), strdup(XML_BOOLEAN_FALSE)); + } + + cluster_name = g_hash_table_lookup(data_set->config_hash, "cluster-name"); + if (cluster_name) { + g_hash_table_insert(node->details->attrs, strdup(CRM_ATTR_CLUSTER_NAME), + strdup(cluster_name)); + } + + pe__unpack_dataset_nvpairs(xml_obj, XML_TAG_ATTR_SETS, &rule_data, + node->details->attrs, NULL, overwrite, data_set); + + pe__unpack_dataset_nvpairs(xml_obj, XML_TAG_UTILIZATION, &rule_data, + node->details->utilization, NULL, + FALSE, data_set); + + if (pe_node_attribute_raw(node, CRM_ATTR_SITE_NAME) == NULL) { + const char *site_name = pe_node_attribute_raw(node, "site-name"); + + if (site_name) { + g_hash_table_insert(node->details->attrs, + strdup(CRM_ATTR_SITE_NAME), + strdup(site_name)); + + } else if (cluster_name) { + /* Default to cluster-name if unset */ + g_hash_table_insert(node->details->attrs, + strdup(CRM_ATTR_SITE_NAME), + strdup(cluster_name)); + } + } +} + +static GList * +extract_operations(const char *node, const char *rsc, xmlNode * rsc_entry, gboolean active_filter) +{ + int counter = -1; + int stop_index = -1; + int start_index = -1; + + xmlNode *rsc_op = NULL; + + GList *gIter = NULL; + GList *op_list = NULL; + GList *sorted_op_list = NULL; + + /* extract operations */ + op_list = NULL; + sorted_op_list = NULL; + + for (rsc_op = pcmk__xe_first_child(rsc_entry); + rsc_op != NULL; rsc_op = pcmk__xe_next(rsc_op)) { + + if (pcmk__str_eq((const char *)rsc_op->name, XML_LRM_TAG_RSC_OP, + pcmk__str_none)) { + crm_xml_add(rsc_op, "resource", rsc); + crm_xml_add(rsc_op, XML_ATTR_UNAME, node); + op_list = g_list_prepend(op_list, rsc_op); + } + } + + if (op_list == NULL) { + /* if there are no operations, there is nothing to do */ + return NULL; + } + + sorted_op_list = g_list_sort(op_list, sort_op_by_callid); + + /* create active recurring operations as optional */ + if (active_filter == FALSE) { + return sorted_op_list; + } + + op_list = NULL; + + calculate_active_ops(sorted_op_list, &start_index, &stop_index); + + for (gIter = sorted_op_list; gIter != NULL; gIter = gIter->next) { + xmlNode *rsc_op = (xmlNode *) gIter->data; + + counter++; + + if (start_index < stop_index) { + crm_trace("Skipping %s: not active", ID(rsc_entry)); + break; + + } else if (counter < start_index) { + crm_trace("Skipping %s: old", ID(rsc_op)); + continue; + } + op_list = g_list_append(op_list, rsc_op); + } + + g_list_free(sorted_op_list); + return op_list; +} + +GList * +find_operations(const char *rsc, const char *node, gboolean active_filter, + pe_working_set_t * data_set) +{ + GList *output = NULL; + GList *intermediate = NULL; + + xmlNode *tmp = NULL; + xmlNode *status = find_xml_node(data_set->input, XML_CIB_TAG_STATUS, TRUE); + + pe_node_t *this_node = NULL; + + xmlNode *node_state = NULL; + + for (node_state = pcmk__xe_first_child(status); node_state != NULL; + node_state = pcmk__xe_next(node_state)) { + + if (pcmk__str_eq((const char *)node_state->name, XML_CIB_TAG_STATE, pcmk__str_none)) { + const char *uname = crm_element_value(node_state, XML_ATTR_UNAME); + + if (node != NULL && !pcmk__str_eq(uname, node, pcmk__str_casei)) { + continue; + } + + this_node = pe_find_node(data_set->nodes, uname); + if(this_node == NULL) { + CRM_LOG_ASSERT(this_node != NULL); + continue; + + } else if (pe__is_guest_or_remote_node(this_node)) { + determine_remote_online_status(data_set, this_node); + + } else { + determine_online_status(node_state, this_node, data_set); + } + + if (this_node->details->online + || pcmk_is_set(data_set->flags, pe_flag_stonith_enabled)) { + /* offline nodes run no resources... + * unless stonith is enabled in which case we need to + * make sure rsc start events happen after the stonith + */ + xmlNode *lrm_rsc = NULL; + + tmp = find_xml_node(node_state, XML_CIB_TAG_LRM, FALSE); + tmp = find_xml_node(tmp, XML_LRM_TAG_RESOURCES, FALSE); + + for (lrm_rsc = pcmk__xe_first_child(tmp); lrm_rsc != NULL; + lrm_rsc = pcmk__xe_next(lrm_rsc)) { + + if (pcmk__str_eq((const char *)lrm_rsc->name, + XML_LRM_TAG_RESOURCE, pcmk__str_none)) { + + const char *rsc_id = crm_element_value(lrm_rsc, XML_ATTR_ID); + + if (rsc != NULL && !pcmk__str_eq(rsc_id, rsc, pcmk__str_casei)) { + continue; + } + + intermediate = extract_operations(uname, rsc_id, lrm_rsc, active_filter); + output = g_list_concat(output, intermediate); + } + } + } + } + } + + return output; +} |