diff --git a/lib/pacemaker/pcmk_sched_location.c b/lib/pacemaker/pcmk_sched_location.c index 593e7339f5..6aeb344375 100644 --- a/lib/pacemaker/pcmk_sched_location.c +++ b/lib/pacemaker/pcmk_sched_location.c @@ -1,733 +1,726 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include "libpacemaker_private.h" static int get_node_score(const char *rule, const char *score, bool raw, pcmk_node_t *node, pcmk_resource_t *rsc) { int score_f = 0; if (score == NULL) { pcmk__config_warn("Rule %s: no score specified (assuming 0)", rule); } else if (raw) { score_f = char2score(score); } else { const char *target = NULL; const char *attr_score = NULL; target = g_hash_table_lookup(rsc->priv->meta, PCMK_META_CONTAINER_ATTRIBUTE_TARGET); attr_score = pcmk__node_attr(node, score, target, pcmk__rsc_node_current); if (attr_score == NULL) { crm_debug("Rule %s: %s did not have a value for %s", rule, pcmk__node_name(node), score); score_f = -PCMK_SCORE_INFINITY; } else { crm_debug("Rule %s: %s had value %s for %s", rule, pcmk__node_name(node), attr_score, score); score_f = char2score(attr_score); } } return score_f; } /*! * \internal * \brief Parse a role configuration for a location constraint * * \param[in] role_spec Role specification * \param[out] role Where to store parsed role * * \return true if role specification is valid, otherwise false */ static bool parse_location_role(const char *role_spec, enum rsc_role_e *role) { if (role_spec == NULL) { *role = pcmk_role_unknown; return true; } *role = pcmk_parse_role(role_spec); switch (*role) { case pcmk_role_unknown: return false; case pcmk_role_started: case pcmk_role_unpromoted: /* Any promotable clone instance cannot be promoted without being in * the unpromoted role first. Therefore, any constraint for the * started or unpromoted role applies to every role. */ *role = pcmk_role_unknown; break; default: break; } return true; } /*! * \internal * \brief Generate a location constraint from a rule * * \param[in,out] rsc Resource that constraint is for * \param[in] rule_xml Rule XML (sub-element of location constraint) * \param[in] discovery Value of \c PCMK_XA_RESOURCE_DISCOVERY for * constraint * \param[out] next_change Where to set when rule evaluation will change * \param[in,out] rule_input Values used to evaluate rule criteria * (node-specific values will be overwritten by * this function) * * \return true if rule is valid, otherwise false */ static bool generate_location_rule(pcmk_resource_t *rsc, xmlNode *rule_xml, const char *discovery, crm_time_t *next_change, pcmk_rule_input_t *rule_input) { const char *rule_id = NULL; const char *score = NULL; const char *boolean = NULL; const char *role_spec = NULL; GList *iter = NULL; bool raw_score = true; bool score_allocated = false; pcmk__location_t *location_rule = NULL; enum rsc_role_e role = pcmk_role_unknown; enum pcmk__combine combine = pcmk__combine_unknown; rule_xml = pcmk__xe_resolve_idref(rule_xml, rsc->priv->scheduler->input); if (rule_xml == NULL) { return false; // Error already logged } rule_id = crm_element_value(rule_xml, PCMK_XA_ID); if (rule_id == NULL) { pcmk__config_err("Ignoring " PCMK_XE_RULE " without " PCMK_XA_ID " in location constraint"); return false; } boolean = crm_element_value(rule_xml, PCMK_XA_BOOLEAN_OP); role_spec = crm_element_value(rule_xml, PCMK_XA_ROLE); if (parse_location_role(role_spec, &role)) { crm_trace("Setting rule %s role filter to %s", rule_id, role_spec); } else { pcmk__config_err("Ignoring rule %s: Invalid " PCMK_XA_ROLE " '%s'", rule_id, role_spec); return false; } crm_trace("Processing location constraint rule %s", rule_id); score = crm_element_value(rule_xml, PCMK_XA_SCORE); if (score == NULL) { score = crm_element_value(rule_xml, PCMK_XA_SCORE_ATTRIBUTE); if (score != NULL) { raw_score = false; } } combine = pcmk__parse_combine(boolean); switch (combine) { case pcmk__combine_and: case pcmk__combine_or: break; - default: - /* @COMPAT When we can break behavioral backward compatibility, - * return false - */ - pcmk__config_warn("Location constraint rule %s has invalid " - PCMK_XA_BOOLEAN_OP " value '%s', using default " - "'" PCMK_VALUE_AND "'", - rule_id, boolean); - combine = pcmk__combine_and; - break; + default: // Not possible with schema validation enabled + pcmk__config_err("Ignoring location constraint rule %s " + "because '%s' is not a valid " PCMK_XA_BOOLEAN_OP, + rule_id, boolean); + return false; } location_rule = pcmk__new_location(rule_id, rsc, 0, discovery, NULL); CRM_CHECK(location_rule != NULL, return NULL); location_rule->role_filter = role; if ((rule_input->rsc_id != NULL) && (rule_input->rsc_id_nmatches > 0) && !raw_score) { char *result = pcmk__replace_submatches(score, rule_input->rsc_id, rule_input->rsc_id_submatches, rule_input->rsc_id_nmatches); if (result != NULL) { score = result; score_allocated = true; } } for (iter = rsc->priv->scheduler->nodes; iter != NULL; iter = iter->next) { pcmk_node_t *node = iter->data; rule_input->node_attrs = node->priv->attrs; rule_input->rsc_params = pe_rsc_params(rsc, node, rsc->priv->scheduler); if (pcmk_evaluate_rule(rule_xml, rule_input, next_change) == pcmk_rc_ok) { pcmk_node_t *local = pe__copy_node(node); location_rule->nodes = g_list_prepend(location_rule->nodes, local); local->assign->score = get_node_score(rule_id, score, raw_score, node, rsc); crm_trace("%s has score %s after %s", pcmk__node_name(node), pcmk_readable_score(local->assign->score), rule_id); } } if (score_allocated) { free((char *)score); } if (location_rule->nodes == NULL) { crm_trace("No matching nodes for location constraint rule %s", rule_id); } else { crm_trace("Location constraint rule %s matched %d nodes", rule_id, g_list_length(location_rule->nodes)); } return true; } static void unpack_rsc_location(xmlNode *xml_obj, pcmk_resource_t *rsc, const char *role_spec, const char *score, char *rsc_id_match, int rsc_id_nmatches, regmatch_t *rsc_id_submatches) { const char *rsc_id = crm_element_value(xml_obj, PCMK_XA_RSC); const char *id = crm_element_value(xml_obj, PCMK_XA_ID); const char *node = crm_element_value(xml_obj, PCMK_XA_NODE); const char *discovery = crm_element_value(xml_obj, PCMK_XA_RESOURCE_DISCOVERY); if (rsc == NULL) { pcmk__config_warn("Ignoring constraint '%s' because resource '%s' " "does not exist", id, rsc_id); return; } if (score == NULL) { score = crm_element_value(xml_obj, PCMK_XA_SCORE); } if ((node != NULL) && (score != NULL)) { int score_i = char2score(score); pcmk_node_t *match = pcmk_find_node(rsc->priv->scheduler, node); enum rsc_role_e role = pcmk_role_unknown; pcmk__location_t *location = NULL; if (match == NULL) { crm_info("Ignoring location constraint %s " "because '%s' is not a known node", pcmk__s(id, "without ID"), node); return; } if (role_spec == NULL) { role_spec = crm_element_value(xml_obj, PCMK_XA_ROLE); } if (parse_location_role(role_spec, &role)) { crm_trace("Setting location constraint %s role filter: %s", id, role_spec); - } else { - /* @COMPAT The previous behavior of creating the constraint ignoring - * the role is retained for now, but we should ignore the entire - * constraint when we can break backward compatibility. - */ - pcmk__config_err("Ignoring role in constraint %s: " - "Invalid value '%s'", id, role_spec); + } else { // Not possible with schema validation enabled + pcmk__config_err("Ignoring location constraint %s " + "because '%s' is not a valid " PCMK_XA_ROLE, + id, role_spec); + return; } location = pcmk__new_location(id, rsc, score_i, discovery, match); if (location == NULL) { return; // Error already logged } location->role_filter = role; } else { bool empty = true; crm_time_t *next_change = crm_time_new_undefined(); pcmk_rule_input_t rule_input = { .now = rsc->priv->scheduler->priv->now, .rsc_meta = rsc->priv->meta, .rsc_id = rsc_id_match, .rsc_id_submatches = rsc_id_submatches, .rsc_id_nmatches = rsc_id_nmatches, }; /* This loop is logically parallel to pcmk__evaluate_rules(), except * instead of checking whether any rule is active, we set up location * constraints for each active rule. * * @COMPAT When we can break backward compatibility, limit location * constraints to a single rule, for consistency with other contexts. * Since a rule may contain other rules, this does not prohibit any * existing use cases. */ for (xmlNode *rule_xml = pcmk__xe_first_child(xml_obj, PCMK_XE_RULE, NULL, NULL); rule_xml != NULL; rule_xml = pcmk__xe_next_same(rule_xml)) { if (generate_location_rule(rsc, rule_xml, discovery, next_change, &rule_input)) { if (empty) { empty = false; continue; } pcmk__warn_once(pcmk__wo_location_rules, "Support for multiple " PCMK_XE_RULE " elements in a location constraint is " "deprecated and will be removed in a future " "release (use a single new rule combining the " "previous rules with " PCMK_XA_BOOLEAN_OP " set to '" PCMK_VALUE_OR "' instead)"); } } if (empty) { pcmk__config_err("Ignoring constraint '%s' because it contains " "no valid rules", id); } /* If there is a point in the future when the evaluation of a rule will * change, make sure the scheduler is re-run by that time. */ if (crm_time_is_defined(next_change)) { time_t t = (time_t) crm_time_get_seconds_since_epoch(next_change); pe__update_recheck_time(t, rsc->priv->scheduler, "location rule evaluation"); } crm_time_free(next_change); } } static void unpack_simple_location(xmlNode *xml_obj, pcmk_scheduler_t *scheduler) { const char *id = crm_element_value(xml_obj, PCMK_XA_ID); const char *value = crm_element_value(xml_obj, PCMK_XA_RSC); if (value) { pcmk_resource_t *rsc; rsc = pcmk__find_constraint_resource(scheduler->priv->resources, value); unpack_rsc_location(xml_obj, rsc, NULL, NULL, NULL, 0, NULL); } value = crm_element_value(xml_obj, PCMK_XA_RSC_PATTERN); if (value) { regex_t regex; bool invert = false; if (value[0] == '!') { value++; invert = true; } if (regcomp(®ex, value, REG_EXTENDED) != 0) { pcmk__config_err("Ignoring constraint '%s' because " PCMK_XA_RSC_PATTERN " has invalid value '%s'", id, value); return; } for (GList *iter = scheduler->priv->resources; iter != NULL; iter = iter->next) { pcmk_resource_t *r = iter->data; int nregs = 0; regmatch_t *pmatch = NULL; int status; if (regex.re_nsub > 0) { nregs = regex.re_nsub + 1; } else { nregs = 1; } pmatch = pcmk__assert_alloc(nregs, sizeof(regmatch_t)); status = regexec(®ex, r->id, nregs, pmatch, 0); if (!invert && (status == 0)) { crm_debug("'%s' matched '%s' for %s", r->id, value, id); unpack_rsc_location(xml_obj, r, NULL, NULL, r->id, nregs, pmatch); } else if (invert && (status != 0)) { crm_debug("'%s' is an inverted match of '%s' for %s", r->id, value, id); unpack_rsc_location(xml_obj, r, NULL, NULL, NULL, 0, NULL); } else { crm_trace("'%s' does not match '%s' for %s", r->id, value, id); } free(pmatch); } regfree(®ex); } } // \return Standard Pacemaker return code static int unpack_location_tags(xmlNode *xml_obj, xmlNode **expanded_xml, pcmk_scheduler_t *scheduler) { const char *id = NULL; const char *rsc_id = NULL; const char *state = NULL; pcmk_resource_t *rsc = NULL; pcmk__idref_t *tag = NULL; xmlNode *rsc_set = NULL; *expanded_xml = NULL; CRM_CHECK(xml_obj != NULL, return EINVAL); id = pcmk__xe_id(xml_obj); if (id == NULL) { pcmk__config_err("Ignoring <%s> constraint without " PCMK_XA_ID, xml_obj->name); return pcmk_rc_unpack_error; } // Check whether there are any resource sets with template or tag references *expanded_xml = pcmk__expand_tags_in_sets(xml_obj, scheduler); if (*expanded_xml != NULL) { crm_log_xml_trace(*expanded_xml, "Expanded " PCMK_XE_RSC_LOCATION); return pcmk_rc_ok; } rsc_id = crm_element_value(xml_obj, PCMK_XA_RSC); if (rsc_id == NULL) { return pcmk_rc_ok; } if (!pcmk__valid_resource_or_tag(scheduler, rsc_id, &rsc, &tag)) { pcmk__config_err("Ignoring constraint '%s' because '%s' is not a " "valid resource or tag", id, rsc_id); return pcmk_rc_unpack_error; } else if (rsc != NULL) { // No template is referenced return pcmk_rc_ok; } state = crm_element_value(xml_obj, PCMK_XA_ROLE); *expanded_xml = pcmk__xml_copy(NULL, xml_obj); /* Convert any template or tag reference into constraint * PCMK_XE_RESOURCE_SET */ if (!pcmk__tag_to_set(*expanded_xml, &rsc_set, PCMK_XA_RSC, false, scheduler)) { pcmk__xml_free(*expanded_xml); *expanded_xml = NULL; return pcmk_rc_unpack_error; } if (rsc_set != NULL) { if (state != NULL) { /* Move PCMK_XA_RSC_ROLE into converted PCMK_XE_RESOURCE_SET as * PCMK_XA_ROLE attribute */ crm_xml_add(rsc_set, PCMK_XA_ROLE, state); pcmk__xe_remove_attr(*expanded_xml, PCMK_XA_ROLE); } crm_log_xml_trace(*expanded_xml, "Expanded " PCMK_XE_RSC_LOCATION); } else { // No sets pcmk__xml_free(*expanded_xml); *expanded_xml = NULL; } return pcmk_rc_ok; } // \return Standard Pacemaker return code static int unpack_location_set(xmlNode *location, xmlNode *set, pcmk_scheduler_t *scheduler) { xmlNode *xml_rsc = NULL; pcmk_resource_t *resource = NULL; const char *set_id; const char *role; const char *local_score; CRM_CHECK(set != NULL, return EINVAL); set_id = pcmk__xe_id(set); if (set_id == NULL) { pcmk__config_err("Ignoring " PCMK_XE_RESOURCE_SET " without " PCMK_XA_ID " in constraint '%s'", pcmk__s(pcmk__xe_id(location), "(missing ID)")); return pcmk_rc_unpack_error; } role = crm_element_value(set, PCMK_XA_ROLE); local_score = crm_element_value(set, PCMK_XA_SCORE); for (xml_rsc = pcmk__xe_first_child(set, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { resource = pcmk__find_constraint_resource(scheduler->priv->resources, pcmk__xe_id(xml_rsc)); if (resource == NULL) { pcmk__config_err("%s: No resource found for %s", set_id, pcmk__xe_id(xml_rsc)); return pcmk_rc_unpack_error; } unpack_rsc_location(location, resource, role, local_score, NULL, 0, NULL); } return pcmk_rc_ok; } void pcmk__unpack_location(xmlNode *xml_obj, pcmk_scheduler_t *scheduler) { xmlNode *set = NULL; bool any_sets = false; xmlNode *orig_xml = NULL; xmlNode *expanded_xml = NULL; if (unpack_location_tags(xml_obj, &expanded_xml, scheduler) != pcmk_rc_ok) { return; } if (expanded_xml) { orig_xml = xml_obj; xml_obj = expanded_xml; } for (set = pcmk__xe_first_child(xml_obj, PCMK_XE_RESOURCE_SET, NULL, NULL); set != NULL; set = pcmk__xe_next_same(set)) { any_sets = true; set = pcmk__xe_resolve_idref(set, scheduler->input); if ((set == NULL) // Configuration error, message already logged || (unpack_location_set(xml_obj, set, scheduler) != pcmk_rc_ok)) { if (expanded_xml) { pcmk__xml_free(expanded_xml); } return; } } if (expanded_xml) { pcmk__xml_free(expanded_xml); xml_obj = orig_xml; } if (!any_sets) { unpack_simple_location(xml_obj, scheduler); } } /*! * \internal * \brief Add a new location constraint to scheduler data * * \param[in] id XML ID of location constraint * \param[in,out] rsc Resource in location constraint * \param[in] node_score Constraint score * \param[in] probe_mode When resource should be probed on node * \param[in] node Node in constraint (or NULL if rule-based) * * \return Newly allocated location constraint on success, otherwise NULL * \note The result will be added to the cluster (via \p rsc) and should not be * freed separately. */ pcmk__location_t * pcmk__new_location(const char *id, pcmk_resource_t *rsc, int node_score, const char *probe_mode, pcmk_node_t *node) { pcmk__location_t *new_con = NULL; CRM_CHECK((node != NULL) || (node_score == 0), return NULL); if (id == NULL) { pcmk__config_err("Invalid constraint: no ID specified"); return NULL; } if (rsc == NULL) { pcmk__config_err("Invalid constraint %s: no resource specified", id); return NULL; } new_con = pcmk__assert_alloc(1, sizeof(pcmk__location_t)); new_con->id = pcmk__str_copy(id); new_con->rsc = rsc; new_con->nodes = NULL; new_con->role_filter = pcmk_role_unknown; if (pcmk__str_eq(probe_mode, PCMK_VALUE_ALWAYS, pcmk__str_null_matches|pcmk__str_casei)) { new_con->probe_mode = pcmk__probe_always; } else if (pcmk__str_eq(probe_mode, PCMK_VALUE_NEVER, pcmk__str_casei)) { new_con->probe_mode = pcmk__probe_never; } else if (pcmk__str_eq(probe_mode, PCMK_VALUE_EXCLUSIVE, pcmk__str_casei)) { new_con->probe_mode = pcmk__probe_exclusive; pcmk__set_rsc_flags(rsc, pcmk__rsc_exclusive_probes); } else { pcmk__config_err("Invalid " PCMK_XA_RESOURCE_DISCOVERY " value %s " "in location constraint", probe_mode); } if (node != NULL) { pcmk_node_t *copy = pe__copy_node(node); copy->assign->score = node_score; new_con->nodes = g_list_prepend(NULL, copy); } rsc->priv->scheduler->priv->location_constraints = g_list_prepend(rsc->priv->scheduler->priv->location_constraints, new_con); rsc->priv->location_constraints = g_list_prepend(rsc->priv->location_constraints, new_con); return new_con; } /*! * \internal * \brief Apply all location constraints * * \param[in,out] scheduler Scheduler data */ void pcmk__apply_locations(pcmk_scheduler_t *scheduler) { for (GList *iter = scheduler->priv->location_constraints; iter != NULL; iter = iter->next) { pcmk__location_t *location = iter->data; location->rsc->priv->cmds->apply_location(location->rsc, location); } } /*! * \internal * \brief Apply a location constraint to a resource's allowed node scores * * \param[in,out] rsc Resource to apply constraint to * \param[in,out] location Location constraint to apply * * \note This does not consider the resource's children, so the resource's * apply_location() method should be used instead in most cases. */ void pcmk__apply_location(pcmk_resource_t *rsc, pcmk__location_t *location) { bool need_role = false; CRM_ASSERT((rsc != NULL) && (location != NULL)); // If a role was specified, ensure constraint is applicable need_role = (location->role_filter > pcmk_role_unknown); if (need_role && (location->role_filter != rsc->priv->next_role)) { pcmk__rsc_trace(rsc, "Not applying %s to %s because role will be %s not %s", location->id, rsc->id, pcmk_role_text(rsc->priv->next_role), pcmk_role_text(location->role_filter)); return; } if (location->nodes == NULL) { pcmk__rsc_trace(rsc, "Not applying %s to %s because no nodes match", location->id, rsc->id); return; } for (GList *iter = location->nodes; iter != NULL; iter = iter->next) { pcmk_node_t *node = iter->data; pcmk_node_t *allowed_node = NULL; allowed_node = g_hash_table_lookup(rsc->priv->allowed_nodes, node->priv->id); pcmk__rsc_trace(rsc, "Applying %s%s%s to %s score on %s: %c %s", location->id, (need_role? " for role " : ""), (need_role? pcmk_role_text(location->role_filter) : ""), rsc->id, pcmk__node_name(node), ((allowed_node == NULL)? '=' : '+'), pcmk_readable_score(node->assign->score)); if (allowed_node == NULL) { allowed_node = pe__copy_node(node); g_hash_table_insert(rsc->priv->allowed_nodes, (gpointer) allowed_node->priv->id, allowed_node); } else { allowed_node->assign->score = pcmk__add_scores(allowed_node->assign->score, node->assign->score); } if (allowed_node->assign->probe_mode < location->probe_mode) { if (location->probe_mode == pcmk__probe_exclusive) { pcmk__set_rsc_flags(rsc, pcmk__rsc_exclusive_probes); } /* exclusive > never > always... always is default */ allowed_node->assign->probe_mode = location->probe_mode; } } }