diff --git a/include/crm/common/logging_internal.h b/include/crm/common/logging_internal.h index 4f4336448f..01de48a35d 100644 --- a/include/crm/common/logging_internal.h +++ b/include/crm/common/logging_internal.h @@ -1,251 +1,253 @@ /* * Copyright 2015-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. */ #ifndef PCMK__CRM_COMMON_LOGGING_INTERNAL__H #define PCMK__CRM_COMMON_LOGGING_INTERNAL__H #include #include #include #ifdef __cplusplus extern "C" { #endif /* Some warnings are too noisy when logged every time a given function is called * (for example, using a deprecated feature). As an alternative, we allow * warnings to be logged once per invocation of the calling program. Each of * those warnings needs a flag defined here. */ enum pcmk__warnings { pcmk__wo_blind = (1 << 0), pcmk__wo_restart_type = (1 << 1), pcmk__wo_role_after = (1 << 2), pcmk__wo_poweroff = (1 << 3), pcmk__wo_require_all = (1 << 4), pcmk__wo_order_score = (1 << 5), pcmk__wo_neg_threshold = (1 << 6), pcmk__wo_remove_after = (1 << 7), pcmk__wo_ping_node = (1 << 8), pcmk__wo_order_inst = (1 << 9), pcmk__wo_coloc_inst = (1 << 10), pcmk__wo_group_order = (1 << 11), pcmk__wo_group_coloc = (1 << 12), pcmk__wo_upstart = (1 << 13), pcmk__wo_nagios = (1 << 14), pcmk__wo_set_ordering = (1 << 15), pcmk__wo_rdisc_enabled = (1 << 16), pcmk__wo_rkt = (1 << 17), pcmk__wo_location_rules = (1 << 18), pcmk__wo_op_attr_expr = (1 << 19), pcmk__wo_instance_defaults = (1 << 20), pcmk__wo_multiple_rules = (1 << 21), pcmk__wo_master_element = (1 << 22), pcmk__wo_clone_master_max = (1 << 23), pcmk__wo_clone_master_node_max = (1 << 24), pcmk__wo_bundle_master = (1 << 25), pcmk__wo_master_role = (1 << 26), pcmk__wo_slave_role = (1 << 27), }; /*! * \internal * \brief Log a warning once per invocation of calling program * * \param[in] wo_flag enum pcmk__warnings value for this warning * \param[in] fmt... printf(3)-style format and arguments */ #define pcmk__warn_once(wo_flag, fmt...) do { \ if (!pcmk_is_set(pcmk__warnings, wo_flag)) { \ if (wo_flag == pcmk__wo_blind) { \ crm_warn(fmt); \ } else { \ pcmk__config_warn(fmt); \ } \ pcmk__warnings = pcmk__set_flags_as(__func__, __LINE__, \ LOG_TRACE, \ "Warn-once", "logging", \ pcmk__warnings, \ (wo_flag), #wo_flag); \ } \ } while (0) -typedef void (*pcmk__config_error_func) (void *ctx, const char *msg, ...); -typedef void (*pcmk__config_warning_func) (void *ctx, const char *msg, ...); +typedef void (*pcmk__config_error_func) (void *ctx, const char *msg, ...) + G_GNUC_PRINTF(2, 3); +typedef void (*pcmk__config_warning_func) (void *ctx, const char *msg, ...) + G_GNUC_PRINTF(2, 3); extern pcmk__config_error_func pcmk__config_error_handler; extern pcmk__config_warning_func pcmk__config_warning_handler; extern void *pcmk__config_error_context; extern void *pcmk__config_warning_context; void pcmk__set_config_error_handler(pcmk__config_error_func error_handler, void *error_context); void pcmk__set_config_warning_handler(pcmk__config_warning_func warning_handler, void *warning_context); /* Pacemaker library functions set this when a configuration error is found, * which turns on extra messages at the end of processing. */ extern bool pcmk__config_has_error; /* Pacemaker library functions set this when a configuration warning is found, * which turns on extra messages at the end of processing. */ extern bool pcmk__config_has_warning; /*! * \internal * \brief Log an error and make crm_verify return failure status * * \param[in] fmt... printf(3)-style format string and arguments */ #define pcmk__config_err(fmt...) do { \ pcmk__config_has_error = true; \ if (pcmk__config_error_handler == NULL) { \ crm_err(fmt); \ } else { \ pcmk__config_error_handler(pcmk__config_error_context, fmt); \ } \ } while (0) /*! * \internal * \brief Log a warning and make crm_verify return failure status * * \param[in] fmt... printf(3)-style format string and arguments */ #define pcmk__config_warn(fmt...) do { \ pcmk__config_has_warning = true; \ if (pcmk__config_warning_handler == NULL) { \ crm_warn(fmt); \ } else { \ pcmk__config_warning_handler(pcmk__config_warning_context, fmt);\ } \ } while (0) /*! * \internal * \brief Execute code depending on whether trace logging is enabled * * This is similar to \p do_crm_log_unlikely() except instead of logging, it * selects one of two code blocks to execute. * * \param[in] if_action Code block to execute if trace logging is enabled * \param[in] else_action Code block to execute if trace logging is not enabled * * \note Neither \p if_action nor \p else_action can contain a \p break or * \p continue statement. */ #define pcmk__if_tracing(if_action, else_action) do { \ static struct qb_log_callsite *trace_cs = NULL; \ \ if (trace_cs == NULL) { \ trace_cs = qb_log_callsite_get(__func__, __FILE__, \ "if_tracing", LOG_TRACE, \ __LINE__, crm_trace_nonlog); \ } \ if (crm_is_callsite_active(trace_cs, LOG_TRACE, \ crm_trace_nonlog)) { \ if_action; \ } else { \ else_action; \ } \ } while (0) /*! * \internal * \brief Log XML changes line-by-line in a formatted fashion * * \param[in] level Priority at which to log the messages * \param[in] xml XML to log * * \note This does nothing when \p level is \c LOG_STDOUT. */ #define pcmk__log_xml_changes(level, xml) do { \ uint8_t _level = pcmk__clip_log_level(level); \ static struct qb_log_callsite *xml_cs = NULL; \ \ switch (_level) { \ case LOG_STDOUT: \ case LOG_NEVER: \ break; \ default: \ if (xml_cs == NULL) { \ xml_cs = qb_log_callsite_get(__func__, __FILE__, \ "xml-changes", _level, \ __LINE__, 0); \ } \ if (crm_is_callsite_active(xml_cs, _level, 0)) { \ pcmk__log_xml_changes_as(__FILE__, __func__, __LINE__, \ 0, _level, xml); \ } \ break; \ } \ } while(0) /*! * \internal * \brief Log an XML patchset line-by-line in a formatted fashion * * \param[in] level Priority at which to log the messages * \param[in] patchset XML patchset to log * * \note This does nothing when \p level is \c LOG_STDOUT. */ #define pcmk__log_xml_patchset(level, patchset) do { \ uint8_t _level = pcmk__clip_log_level(level); \ static struct qb_log_callsite *xml_cs = NULL; \ \ switch (_level) { \ case LOG_STDOUT: \ case LOG_NEVER: \ break; \ default: \ if (xml_cs == NULL) { \ xml_cs = qb_log_callsite_get(__func__, __FILE__, \ "xml-patchset", _level, \ __LINE__, 0); \ } \ if (crm_is_callsite_active(xml_cs, _level, 0)) { \ pcmk__log_xml_patchset_as(__FILE__, __func__, __LINE__, \ 0, _level, patchset); \ } \ break; \ } \ } while(0) void pcmk__log_xml_changes_as(const char *file, const char *function, uint32_t line, uint32_t tags, uint8_t level, const xmlNode *xml); void pcmk__log_xml_patchset_as(const char *file, const char *function, uint32_t line, uint32_t tags, uint8_t level, const xmlNode *patchset); /*! * \internal * \brief Initialize logging for command line tools * * \param[in] name The name of the program * \param[in] verbosity How verbose to be in logging * * \note \p verbosity is not the same as the logging level (LOG_ERR, etc.). */ void pcmk__cli_init_logging(const char *name, unsigned int verbosity); int pcmk__add_logfile(const char *filename); void pcmk__add_logfiles(gchar **log_files, pcmk__output_t *out); void pcmk__free_common_logger(void); #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_LOGGING_INTERNAL__H diff --git a/lib/common/rules.c b/lib/common/rules.c index cfe31b303f..9a2546fd00 100644 --- a/lib/common/rules.c +++ b/lib/common/rules.c @@ -1,1512 +1,1476 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include // NULL, size_t #include // bool #include // isdigit() #include // regmatch_t #include // uint32_t #include // PRIu32 #include // gboolean, FALSE #include // xmlNode #include #include #include #include #include "crmcommon_private.h" /*! * \internal * \brief Get the condition type corresponding to given condition XML * * \param[in] condition Rule condition XML * * \return Condition type corresponding to \p condition */ enum expression_type pcmk__condition_type(const xmlNode *condition) { const char *name = NULL; // Expression types based on element name if (pcmk__xe_is(condition, PCMK_XE_DATE_EXPRESSION)) { return pcmk__condition_datetime; } else if (pcmk__xe_is(condition, PCMK_XE_RSC_EXPRESSION)) { return pcmk__condition_resource; } else if (pcmk__xe_is(condition, PCMK_XE_OP_EXPRESSION)) { return pcmk__condition_operation; } else if (pcmk__xe_is(condition, PCMK_XE_RULE)) { return pcmk__condition_rule; } else if (!pcmk__xe_is(condition, PCMK_XE_EXPRESSION)) { return pcmk__condition_unknown; } // Expression types based on node attribute name name = crm_element_value(condition, PCMK_XA_ATTRIBUTE); if (pcmk__str_any_of(name, CRM_ATTR_UNAME, CRM_ATTR_KIND, CRM_ATTR_ID, NULL)) { return pcmk__condition_location; } return pcmk__condition_attribute; } /*! * \internal * \brief Get parent XML element's ID for logging purposes * * \param[in] xml XML of a subelement * * \return ID of \p xml's parent for logging purposes (guaranteed non-NULL) */ static const char * loggable_parent_id(const xmlNode *xml) { // Default if called without parent (likely for unit testing) const char *parent_id = "implied"; if ((xml != NULL) && (xml->parent != NULL)) { parent_id = pcmk__xe_id(xml->parent); if (parent_id == NULL) { // Not possible with schema validation enabled parent_id = "without ID"; } } return parent_id; } /*! * \internal * \brief Get the moon phase corresponding to a given date/time * * \param[in] now Date/time to get moon phase for * * \return Phase of the moon corresponding to \p now, where 0 is the new moon * and 7 is the full moon * \deprecated This feature has been deprecated since 2.1.6. */ static int phase_of_the_moon(const crm_time_t *now) { /* As per the nethack rules: * - A moon period is 29.53058 days ~= 30 * - A year is 365.2422 days * - Number of days moon phase advances on first day of year compared to * preceding year is (365.2422 - 12 * 29.53058) ~= 11 * - Number of years until same phases fall on the same days of the month * is 18.6 ~= 19 * - Moon phase on first day of year (epact) ~= (11 * (year%19) + 29) % 30 * (29 as initial condition) * - Current phase in days = first day phase + days elapsed in year * - 6 moons ~= 177 days ~= 8 reported phases * 22 (+ 11/22 for rounding) */ uint32_t epact, diy, goldn; uint32_t y; crm_time_get_ordinal(now, &y, &diy); goldn = (y % 19) + 1; epact = (11 * goldn + 18) % 30; if (((epact == 25) && (goldn > 11)) || (epact == 24)) { epact++; } return (((((diy + epact) * 6) + 11) % 177) / 22) & 7; } /*! * \internal * \brief Check an integer value against a range from a date specification * * \param[in] date_spec XML of PCMK_XE_DATE_SPEC element to check - * \param[in] id XML ID for logging purposes + * \param[in] id XML ID of parent date expression for logging purposes * \param[in] attr Name of XML attribute with range to check against * \param[in] value Value to compare against range * * \return Standard Pacemaker return code (specifically, pcmk_rc_before_range, * pcmk_rc_after_range, or pcmk_rc_ok to indicate that result is either * within range or undetermined) * \note We return pcmk_rc_ok for an undetermined result so we can continue * checking the next range attribute. */ static int check_range(const xmlNode *date_spec, const char *id, const char *attr, uint32_t value) { int rc = pcmk_rc_ok; const char *range = crm_element_value(date_spec, attr); long long low, high; - if (range == NULL) { // Attribute not present - goto bail; - } + if (range == NULL) { + // Attribute not present - if (pcmk__parse_ll_range(range, &low, &high) != pcmk_rc_ok) { + } else if (pcmk__parse_ll_range(range, &low, &high) != pcmk_rc_ok) { // Invalid range - /* @COMPAT When we can break behavioral backward compatibility, treat - * the entire rule as not passing. - */ - pcmk__config_err("Ignoring " PCMK_XE_DATE_SPEC - " %s attribute %s because '%s' is not a valid range", - id, attr, range); + pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s " + "as not passing because '%s' is not a valid range " + "for " PCMK_XE_DATE_SPEC " attribute %s", + id, range, attr); + rc = pcmk_rc_unpack_error; } else if ((low != -1) && (value < low)) { rc = pcmk_rc_before_range; } else if ((high != -1) && (value > high)) { rc = pcmk_rc_after_range; } -bail: - crm_trace("Checked " PCMK_XE_DATE_SPEC " %s %s='%s' for %" PRIu32 ": %s", + crm_trace(PCMK_XE_DATE_EXPRESSION " %s: " PCMK_XE_DATE_SPEC + " %s='%s' for %" PRIu32 ": %s", id, attr, pcmk__s(range, ""), value, pcmk_rc_str(rc)); return rc; } /*! * \internal * \brief Evaluate a date specification for a given date/time * * \param[in] date_spec XML of PCMK_XE_DATE_SPEC element to evaluate * \param[in] now Time to check * * \return Standard Pacemaker return code (specifically, EINVAL for NULL - * arguments, pcmk_rc_ok if time matches specification, or - * pcmk_rc_before_range, pcmk_rc_after_range, or pcmk_rc_op_unsatisfied - * as appropriate to how time relates to specification) + * arguments, pcmk_rc_unpack_error if the specification XML is invalid, + * \c pcmk_rc_ok if \p now is within the specification's ranges, or + * \c pcmk_rc_before_range or \c pcmk_rc_after_range as appropriate) */ int pcmk__evaluate_date_spec(const xmlNode *date_spec, const crm_time_t *now) { const char *id = NULL; const char *parent_id = loggable_parent_id(date_spec); // Range attributes that can be specified for a PCMK_XE_DATE_SPEC element struct range { const char *attr; uint32_t value; } ranges[] = { { PCMK_XA_YEARS, 0U }, { PCMK_XA_MONTHS, 0U }, { PCMK_XA_MONTHDAYS, 0U }, { PCMK_XA_HOURS, 0U }, { PCMK_XA_MINUTES, 0U }, { PCMK_XA_SECONDS, 0U }, { PCMK_XA_YEARDAYS, 0U }, { PCMK_XA_WEEKYEARS, 0U }, { PCMK_XA_WEEKS, 0U }, { PCMK_XA_WEEKDAYS, 0U }, { PCMK__XA_MOON, 0U }, }; if ((date_spec == NULL) || (now == NULL)) { return EINVAL; } // Get specification ID (for logging) id = pcmk__xe_id(date_spec); if (pcmk__str_empty(id)) { // Not possible with schema validation enabled - /* @COMPAT When we can break behavioral backward compatibility, - * fail the specification - */ - pcmk__config_warn(PCMK_XE_DATE_SPEC " subelement of " - PCMK_XE_DATE_EXPRESSION " %s has no " PCMK_XA_ID, - parent_id); - id = "without ID"; // for logging + pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " + "passing because " PCMK_XE_DATE_SPEC + " subelement has no " PCMK_XA_ID, parent_id); + return pcmk_rc_unpack_error; } // Year, month, day crm_time_get_gregorian(now, &(ranges[0].value), &(ranges[1].value), &(ranges[2].value)); // Hour, minute, second crm_time_get_timeofday(now, &(ranges[3].value), &(ranges[4].value), &(ranges[5].value)); // Year (redundant) and day of year crm_time_get_ordinal(now, &(ranges[0].value), &(ranges[6].value)); // Week year, week of week year, day of week crm_time_get_isoweek(now, &(ranges[7].value), &(ranges[8].value), &(ranges[9].value)); // Moon phase (deprecated) ranges[10].value = phase_of_the_moon(now); if (crm_element_value(date_spec, PCMK__XA_MOON) != NULL) { pcmk__config_warn("Support for '" PCMK__XA_MOON "' in " PCMK_XE_DATE_SPEC " elements (such as %s) is " "deprecated and will be removed in a future release " "of Pacemaker", id); } for (int i = 0; i < PCMK__NELEM(ranges); ++i) { - int rc = check_range(date_spec, id, ranges[i].attr, ranges[i].value); + int rc = check_range(date_spec, parent_id, ranges[i].attr, + ranges[i].value); if (rc != pcmk_rc_ok) { return rc; } } // All specified ranges passed, or none were given (also considered a pass) return pcmk_rc_ok; } #define ADD_COMPONENT(component) do { \ - int sub_rc = pcmk__add_time_from_xml(*end, component, duration); \ - if (sub_rc != pcmk_rc_ok) { \ - /* @COMPAT return sub_rc when we can break compatibility */ \ - pcmk__config_warn("Ignoring %s in " PCMK_XE_DURATION " %s " \ - "because it is invalid", \ - pcmk__time_component_attr(component), id); \ - rc = sub_rc; \ + int rc = pcmk__add_time_from_xml(*end, component, duration); \ + if (rc != pcmk_rc_ok) { \ + pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s " \ + "as not passing because " PCMK_XE_DURATION \ + " %s attribute %s is invalid", \ + parent_id, id, \ + pcmk__time_component_attr(component)); \ + crm_time_free(*end); \ + *end = NULL; \ + return rc; \ } \ } while (0) /*! * \internal * \brief Given a duration and a start time, calculate the end time * * \param[in] duration XML of PCMK_XE_DURATION element * \param[in] start Start time * \param[out] end Where to store end time (\p *end must be NULL * initially) * * \return Standard Pacemaker return code * \note The caller is responsible for freeing \p *end using crm_time_free(). */ int pcmk__unpack_duration(const xmlNode *duration, const crm_time_t *start, crm_time_t **end) { - int rc = pcmk_rc_ok; const char *id = NULL; const char *parent_id = loggable_parent_id(duration); if ((start == NULL) || (duration == NULL) || (end == NULL) || (*end != NULL)) { return EINVAL; } // Get duration ID (for logging) id = pcmk__xe_id(duration); if (pcmk__str_empty(id)) { // Not possible with schema validation enabled - /* @COMPAT When we can break behavioral backward compatibility, - * return pcmk_rc_unpack_error instead - */ - pcmk__config_warn(PCMK_XE_DURATION " subelement of " - PCMK_XE_DATE_EXPRESSION " %s has no " PCMK_XA_ID, - parent_id); - id = "without ID"; + pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s " + "as not passing because " PCMK_XE_DURATION + " subelement has no " PCMK_XA_ID, parent_id); + return pcmk_rc_unpack_error; } *end = pcmk_copy_time(start); ADD_COMPONENT(pcmk__time_years); ADD_COMPONENT(pcmk__time_months); ADD_COMPONENT(pcmk__time_weeks); ADD_COMPONENT(pcmk__time_days); ADD_COMPONENT(pcmk__time_hours); ADD_COMPONENT(pcmk__time_minutes); ADD_COMPONENT(pcmk__time_seconds); - return rc; + return pcmk_rc_ok; } /*! * \internal * \brief Evaluate a range check for a given date/time * * \param[in] date_expression XML of PCMK_XE_DATE_EXPRESSION element * \param[in] id Expression ID for logging purposes * \param[in] now Date/time to compare * \param[in,out] next_change If not NULL, set this to when the evaluation * will change, if known and earlier than the * original value * * \return Standard Pacemaker return code */ static int evaluate_in_range(const xmlNode *date_expression, const char *id, const crm_time_t *now, crm_time_t *next_change) { crm_time_t *start = NULL; crm_time_t *end = NULL; if (pcmk__xe_get_datetime(date_expression, PCMK_XA_START, &start) != pcmk_rc_ok) { - /* @COMPAT When we can break behavioral backward compatibility, - * return pcmk_rc_unpack_error - */ - pcmk__config_warn("Ignoring " PCMK_XA_START " in " - PCMK_XE_DATE_EXPRESSION " %s because it is invalid", - id); + pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " + "passing because " PCMK_XA_START " is invalid", id); + return pcmk_rc_unpack_error; } if (pcmk__xe_get_datetime(date_expression, PCMK_XA_END, &end) != pcmk_rc_ok) { - /* @COMPAT When we can break behavioral backward compatibility, - * return pcmk_rc_unpack_error - */ - pcmk__config_warn("Ignoring " PCMK_XA_END " in " - PCMK_XE_DATE_EXPRESSION " %s because it is invalid", - id); + pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " + "passing because " PCMK_XA_END " is invalid", id); + crm_time_free(start); + return pcmk_rc_unpack_error; } if ((start == NULL) && (end == NULL)) { // Not possible with schema validation enabled - /* @COMPAT When we can break behavioral backward compatibility, - * return pcmk_rc_unpack_error - */ - pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " - "passing because in_range requires at least one of " - PCMK_XA_START " or " PCMK_XA_END, id); - return pcmk_rc_undetermined; + pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " + "passing because " PCMK_VALUE_IN_RANGE + " requires at least one of " PCMK_XA_START " or " + PCMK_XA_END, id); + return pcmk_rc_unpack_error; } if (end == NULL) { xmlNode *duration = pcmk__xe_first_child(date_expression, PCMK_XE_DURATION, NULL, NULL); if (duration != NULL) { - /* @COMPAT When we can break behavioral backward compatibility, - * return the result of this if not OK - */ - pcmk__unpack_duration(duration, start, &end); + int rc = pcmk__unpack_duration(duration, start, &end); + + if (rc != pcmk_rc_ok) { + pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION + " %s as not passing because duration " + "is invalid", id); + crm_time_free(start); + return rc; + } } } if ((start != NULL) && (crm_time_compare(now, start) < 0)) { pcmk__set_time_if_earlier(next_change, start); crm_time_free(start); crm_time_free(end); return pcmk_rc_before_range; } if (end != NULL) { if (crm_time_compare(now, end) > 0) { crm_time_free(start); crm_time_free(end); return pcmk_rc_after_range; } // Evaluation doesn't change until second after end if (next_change != NULL) { crm_time_add_seconds(end, 1); pcmk__set_time_if_earlier(next_change, end); } } crm_time_free(start); crm_time_free(end); return pcmk_rc_within_range; } /*! * \internal * \brief Evaluate a greater-than check for a given date/time * * \param[in] date_expression XML of PCMK_XE_DATE_EXPRESSION element * \param[in] id Expression ID for logging purposes * \param[in] now Date/time to compare * \param[in,out] next_change If not NULL, set this to when the evaluation * will change, if known and earlier than the * original value * * \return Standard Pacemaker return code */ static int evaluate_gt(const xmlNode *date_expression, const char *id, const crm_time_t *now, crm_time_t *next_change) { crm_time_t *start = NULL; if (pcmk__xe_get_datetime(date_expression, PCMK_XA_START, &start) != pcmk_rc_ok) { - /* @COMPAT When we can break behavioral backward compatibility, - * return pcmk_rc_unpack_error - */ - pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " - "passing because " PCMK_XA_START " is invalid", - id); - return pcmk_rc_undetermined; + pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " + "passing because " PCMK_XA_START " is invalid", + id); + return pcmk_rc_unpack_error; } if (start == NULL) { // Not possible with schema validation enabled - /* @COMPAT When we can break behavioral backward compatibility, - * return pcmk_rc_unpack_error - */ - pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " - "passing because " PCMK_VALUE_GT " requires " - PCMK_XA_START, id); - return pcmk_rc_undetermined; + pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " + "passing because " PCMK_VALUE_GT " requires " + PCMK_XA_START, id); + return pcmk_rc_unpack_error; } if (crm_time_compare(now, start) > 0) { crm_time_free(start); return pcmk_rc_within_range; } // Evaluation doesn't change until second after start time crm_time_add_seconds(start, 1); pcmk__set_time_if_earlier(next_change, start); crm_time_free(start); return pcmk_rc_before_range; } /*! * \internal * \brief Evaluate a less-than check for a given date/time * * \param[in] date_expression XML of PCMK_XE_DATE_EXPRESSION element * \param[in] id Expression ID for logging purposes * \param[in] now Date/time to compare * \param[in,out] next_change If not NULL, set this to when the evaluation * will change, if known and earlier than the * original value * * \return Standard Pacemaker return code */ static int evaluate_lt(const xmlNode *date_expression, const char *id, const crm_time_t *now, crm_time_t *next_change) { crm_time_t *end = NULL; if (pcmk__xe_get_datetime(date_expression, PCMK_XA_END, &end) != pcmk_rc_ok) { - /* @COMPAT When we can break behavioral backward compatibility, - * return pcmk_rc_unpack_error - */ - pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " - "passing because " PCMK_XA_END " is invalid", id); - return pcmk_rc_undetermined; + pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " + "passing because " PCMK_XA_END " is invalid", id); + return pcmk_rc_unpack_error; } if (end == NULL) { // Not possible with schema validation enabled - /* @COMPAT When we can break behavioral backward compatibility, - * return pcmk_rc_unpack_error - */ - pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " - "passing because " PCMK_VALUE_GT " requires " - PCMK_XA_END, id); - return pcmk_rc_undetermined; + pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " + "passing because " PCMK_VALUE_GT " requires " + PCMK_XA_END, id); + return pcmk_rc_unpack_error; } if (crm_time_compare(now, end) < 0) { pcmk__set_time_if_earlier(next_change, end); crm_time_free(end); return pcmk_rc_within_range; } crm_time_free(end); return pcmk_rc_after_range; } /*! * \internal * \brief Evaluate a rule's date expression for a given date/time * * \param[in] date_expression XML of a PCMK_XE_DATE_EXPRESSION element * \param[in] now Time to use for evaluation * \param[in,out] next_change If not NULL, set this to when the evaluation * will change, if known and earlier than the * original value * * \return Standard Pacemaker return code (unlike most other evaluation * functions, this can return either pcmk_rc_ok or pcmk_rc_within_range * on success) */ int pcmk__evaluate_date_expression(const xmlNode *date_expression, const crm_time_t *now, crm_time_t *next_change) { const char *id = NULL; const char *op = NULL; - int rc = pcmk_rc_undetermined; + int rc = pcmk_rc_ok; if ((date_expression == NULL) || (now == NULL)) { return EINVAL; } // Get expression ID (for logging) id = pcmk__xe_id(date_expression); if (pcmk__str_empty(id)) { // Not possible with schema validation enabled - /* @COMPAT When we can break behavioral backward compatibility, - * return pcmk_rc_unpack_error - */ - pcmk__config_warn(PCMK_XE_DATE_EXPRESSION " element has no " - PCMK_XA_ID); - id = "without ID"; // for logging + pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " without " + PCMK_XA_ID " as not passing"); + return pcmk_rc_unpack_error; } op = crm_element_value(date_expression, PCMK_XA_OPERATION); if (pcmk__str_eq(op, PCMK_VALUE_IN_RANGE, pcmk__str_null_matches|pcmk__str_casei)) { rc = evaluate_in_range(date_expression, id, now, next_change); } else if (pcmk__str_eq(op, PCMK_VALUE_DATE_SPEC, pcmk__str_casei)) { xmlNode *date_spec = pcmk__xe_first_child(date_expression, PCMK_XE_DATE_SPEC, NULL, NULL); if (date_spec == NULL) { // Not possible with schema validation enabled - /* @COMPAT When we can break behavioral backward compatibility, - * return pcmk_rc_unpack_error - */ - pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s " - "as not passing because " PCMK_VALUE_DATE_SPEC - " operations require a " PCMK_XE_DATE_SPEC - " subelement", id); - } else { - // @TODO set next_change appropriately - rc = pcmk__evaluate_date_spec(date_spec, now); + pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION " %s " + "as not passing because " PCMK_VALUE_DATE_SPEC + " operations require a " PCMK_XE_DATE_SPEC + " subelement", id); + return pcmk_rc_unpack_error; } + // @TODO set next_change appropriately + rc = pcmk__evaluate_date_spec(date_spec, now); + } else if (pcmk__str_eq(op, PCMK_VALUE_GT, pcmk__str_casei)) { rc = evaluate_gt(date_expression, id, now, next_change); } else if (pcmk__str_eq(op, PCMK_VALUE_LT, pcmk__str_casei)) { rc = evaluate_lt(date_expression, id, now, next_change); } else { // Not possible with schema validation enabled - /* @COMPAT When we can break behavioral backward compatibility, - * return pcmk_rc_unpack_error - */ - pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION - " %s as not passing because '%s' is not a valid " - PCMK_XE_OPERATION, op); + pcmk__config_err("Treating " PCMK_XE_DATE_EXPRESSION + " %s as not passing because '%s' is not a valid " + PCMK_XE_OPERATION, id, op); + return pcmk_rc_unpack_error; } crm_trace(PCMK_XE_DATE_EXPRESSION " %s (%s): %s (%d)", id, op, pcmk_rc_str(rc), rc); return rc; } /*! * \internal * \brief Go through submatches in a string, either counting how many bytes * would be needed for the expansion, or performing the expansion, * as requested * * \param[in] string String possibly containing submatch variables * \param[in] match String that matched the regular expression * \param[in] submatches Regular expression submatches (as set by regexec()) * \param[in] nmatches Number of entries in \p submatches[] * \param[out] expansion If not NULL, expand string here (must be * pre-allocated to appropriate size) * \param[out] nbytes If not NULL, set to size needed for expansion * * \return true if any expansion is needed, otherwise false */ static bool process_submatches(const char *string, const char *match, const regmatch_t submatches[], int nmatches, char *expansion, size_t *nbytes) { bool expanded = false; const char *src = string; if (nbytes != NULL) { *nbytes = 1; // Include space for terminator } while (*src != '\0') { int submatch = 0; size_t match_len = 0; if ((src[0] != '%') || !isdigit(src[1])) { /* src does not point to the first character of a %N sequence, * so expand this character as-is */ if (expansion != NULL) { *expansion++ = *src; } if (nbytes != NULL) { ++(*nbytes); } ++src; continue; } submatch = src[1] - '0'; src += 2; // Skip over %N sequence in source string expanded = true; // Expansion will be different from source // Omit sequence from expansion unless it has a non-empty match if ((nmatches <= submatch) // Not enough submatches || (submatches[submatch].rm_so < 0) // Pattern did not match || (submatches[submatch].rm_eo <= submatches[submatch].rm_so)) { // Match was empty continue; } match_len = submatches[submatch].rm_eo - submatches[submatch].rm_so; if (nbytes != NULL) { *nbytes += match_len; } if (expansion != NULL) { memcpy(expansion, match + submatches[submatch].rm_so, match_len); expansion += match_len; } } return expanded; } /*! * \internal * \brief Expand any regular expression submatches (%0-%9) in a string * * \param[in] string String possibly containing submatch variables * \param[in] match String that matched the regular expression * \param[in] submatches Regular expression submatches (as set by regexec()) * \param[in] nmatches Number of entries in \p submatches[] * * \return Newly allocated string identical to \p string with submatches * expanded on success, or NULL if no expansions were needed * \note The caller is responsible for freeing the result with free() */ char * pcmk__replace_submatches(const char *string, const char *match, const regmatch_t submatches[], int nmatches) { size_t nbytes = 0; char *result = NULL; if (pcmk__str_empty(string) || pcmk__str_empty(match)) { return NULL; // Nothing to expand } // Calculate how much space will be needed for expanded string if (!process_submatches(string, match, submatches, nmatches, NULL, &nbytes)) { return NULL; // No expansions needed } // Allocate enough space for expanded string result = pcmk__assert_alloc(nbytes, sizeof(char)); // Expand submatches (void) process_submatches(string, match, submatches, nmatches, result, NULL); return result; } /*! * \internal * \brief Parse a comparison type from a string * * \param[in] op String with comparison type (valid values are * \c PCMK_VALUE_DEFINED, \c PCMK_VALUE_NOT_DEFINED, * \c PCMK_VALUE_EQ, \c PCMK_VALUE_NE, * \c PCMK_VALUE_LT, \c PCMK_VALUE_LTE, * \c PCMK_VALUE_GT, or \c PCMK_VALUE_GTE) * * \return Comparison type corresponding to \p op */ enum pcmk__comparison pcmk__parse_comparison(const char *op) { if (pcmk__str_eq(op, PCMK_VALUE_DEFINED, pcmk__str_casei)) { return pcmk__comparison_defined; } else if (pcmk__str_eq(op, PCMK_VALUE_NOT_DEFINED, pcmk__str_casei)) { return pcmk__comparison_undefined; } else if (pcmk__str_eq(op, PCMK_VALUE_EQ, pcmk__str_casei)) { return pcmk__comparison_eq; } else if (pcmk__str_eq(op, PCMK_VALUE_NE, pcmk__str_casei)) { return pcmk__comparison_ne; } else if (pcmk__str_eq(op, PCMK_VALUE_LT, pcmk__str_casei)) { return pcmk__comparison_lt; } else if (pcmk__str_eq(op, PCMK_VALUE_LTE, pcmk__str_casei)) { return pcmk__comparison_lte; } else if (pcmk__str_eq(op, PCMK_VALUE_GT, pcmk__str_casei)) { return pcmk__comparison_gt; } else if (pcmk__str_eq(op, PCMK_VALUE_GTE, pcmk__str_casei)) { return pcmk__comparison_gte; } return pcmk__comparison_unknown; } /*! * \internal * \brief Parse a value type from a string * * \param[in] type String with value type (valid values are NULL, * \c PCMK_VALUE_STRING, \c PCMK_VALUE_INTEGER, * \c PCMK_VALUE_NUMBER, and \c PCMK_VALUE_VERSION) * \param[in] op Operation type (used only to select default) * \param[in] value1 First value being compared (used only to select default) * \param[in] value2 Second value being compared (used only to select default) */ enum pcmk__type pcmk__parse_type(const char *type, enum pcmk__comparison op, const char *value1, const char *value2) { if (type == NULL) { switch (op) { case pcmk__comparison_lt: case pcmk__comparison_lte: case pcmk__comparison_gt: case pcmk__comparison_gte: if (((value1 != NULL) && (strchr(value1, '.') != NULL)) || ((value2 != NULL) && (strchr(value2, '.') != NULL))) { return pcmk__type_number; } return pcmk__type_integer; default: return pcmk__type_string; } } if (pcmk__str_eq(type, PCMK_VALUE_STRING, pcmk__str_casei)) { return pcmk__type_string; } else if (pcmk__str_eq(type, PCMK_VALUE_INTEGER, pcmk__str_casei)) { return pcmk__type_integer; } else if (pcmk__str_eq(type, PCMK_VALUE_NUMBER, pcmk__str_casei)) { return pcmk__type_number; } else if (pcmk__str_eq(type, PCMK_VALUE_VERSION, pcmk__str_casei)) { return pcmk__type_version; } return pcmk__type_unknown; } /*! * \internal * \brief Compare two strings according to a given type * * \param[in] value1 String with first value to compare * \param[in] value2 String with second value to compare * \param[in] type How to interpret the values * * \return Standard comparison result (a negative integer if \p value1 is * lesser, 0 if the values are equal, and a positive integer if * \p value1 is greater) */ int pcmk__cmp_by_type(const char *value1, const char *value2, enum pcmk__type type) { // NULL compares as less than non-NULL if (value2 == NULL) { return (value1 == NULL)? 0 : 1; } if (value1 == NULL) { return -1; } switch (type) { case pcmk__type_string: return strcasecmp(value1, value2); case pcmk__type_integer: { long long integer1; long long integer2; if ((pcmk__scan_ll(value1, &integer1, 0LL) != pcmk_rc_ok) || (pcmk__scan_ll(value2, &integer2, 0LL) != pcmk_rc_ok)) { crm_warn("Comparing '%s' and '%s' as strings because " "invalid as integers", value1, value2); return strcasecmp(value1, value2); } return (integer1 < integer2)? -1 : (integer1 > integer2)? 1 : 0; } break; case pcmk__type_number: { double num1; double num2; if ((pcmk__scan_double(value1, &num1, NULL, NULL) != pcmk_rc_ok) || (pcmk__scan_double(value2, &num2, NULL, NULL) != pcmk_rc_ok)) { crm_warn("Comparing '%s' and '%s' as strings because invalid as " "numbers", value1, value2); return strcasecmp(value1, value2); } return (num1 < num2)? -1 : (num1 > num2)? 1 : 0; } break; case pcmk__type_version: return compare_version(value1, value2); default: // Invalid type return 0; } } /*! * \internal * \brief Parse a reference value source from a string * * \param[in] source String indicating reference value source * * \return Reference value source corresponding to \p source */ enum pcmk__reference_source pcmk__parse_source(const char *source) { if (pcmk__str_eq(source, PCMK_VALUE_LITERAL, pcmk__str_casei|pcmk__str_null_matches)) { return pcmk__source_literal; } else if (pcmk__str_eq(source, PCMK_VALUE_PARAM, pcmk__str_casei)) { return pcmk__source_instance_attrs; } else if (pcmk__str_eq(source, PCMK_VALUE_META, pcmk__str_casei)) { return pcmk__source_meta_attrs; } else { return pcmk__source_unknown; } } /*! * \internal * \brief Parse a boolean operator from a string * * \param[in] combine String indicating boolean operator * * \return Enumeration value corresponding to \p combine */ enum pcmk__combine pcmk__parse_combine(const char *combine) { if (pcmk__str_eq(combine, PCMK_VALUE_AND, pcmk__str_null_matches|pcmk__str_casei)) { return pcmk__combine_and; } else if (pcmk__str_eq(combine, PCMK_VALUE_OR, pcmk__str_casei)) { return pcmk__combine_or; } else { return pcmk__combine_unknown; } } /*! * \internal * \brief Get the result of a node attribute comparison for rule evaluation * * \param[in] actual Actual node attribute value * \param[in] reference Node attribute value from rule (ignored for * \p comparison of \c pcmk__comparison_defined or * \c pcmk__comparison_undefined) * \param[in] type How to interpret the values * \param[in] comparison How to compare the values * * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok if the * comparison passes, and some other value if it does not) */ static int evaluate_attr_comparison(const char *actual, const char *reference, enum pcmk__type type, enum pcmk__comparison comparison) { int cmp = 0; switch (comparison) { case pcmk__comparison_defined: return (actual != NULL)? pcmk_rc_ok : pcmk_rc_op_unsatisfied; case pcmk__comparison_undefined: return (actual == NULL)? pcmk_rc_ok : pcmk_rc_op_unsatisfied; default: break; } cmp = pcmk__cmp_by_type(actual, reference, type); switch (comparison) { case pcmk__comparison_eq: return (cmp == 0)? pcmk_rc_ok : pcmk_rc_op_unsatisfied; case pcmk__comparison_ne: return (cmp != 0)? pcmk_rc_ok : pcmk_rc_op_unsatisfied; default: break; } if ((actual == NULL) || (reference == NULL)) { return pcmk_rc_op_unsatisfied; // Comparison would be meaningless } switch (comparison) { case pcmk__comparison_lt: return (cmp < 0)? pcmk_rc_ok : pcmk_rc_after_range; case pcmk__comparison_lte: return (cmp <= 0)? pcmk_rc_ok : pcmk_rc_after_range; case pcmk__comparison_gt: return (cmp > 0)? pcmk_rc_ok : pcmk_rc_before_range; case pcmk__comparison_gte: return (cmp >= 0)? pcmk_rc_ok : pcmk_rc_before_range; default: // Not possible with schema validation enabled return pcmk_rc_op_unsatisfied; } } /*! * \internal * \brief Get a reference value from a configured source * * \param[in] value Value given in rule expression * \param[in] source Reference value source * \param[in] rule_input Values used to evaluate rule criteria */ static const char * value_from_source(const char *value, enum pcmk__reference_source source, const pcmk_rule_input_t *rule_input) { GHashTable *table = NULL; if (pcmk__str_empty(value)) { /* @COMPAT When we can break backward compatibility, drop this block so * empty strings are treated as such (there should never be an empty * string as an instance attribute or meta-attribute name, so those will * get NULL anyway, but it could matter for literal comparisons) */ return NULL; } switch (source) { case pcmk__source_literal: return value; case pcmk__source_instance_attrs: table = rule_input->rsc_params; break; case pcmk__source_meta_attrs: table = rule_input->rsc_meta; break; default: return NULL; // Not possible } if (table == NULL) { return NULL; } return (const char *) g_hash_table_lookup(table, value); } /*! * \internal * \brief Evaluate a node attribute rule expression * * \param[in] expression XML of a rule's PCMK_XE_EXPRESSION subelement * \param[in] rule_input Values used to evaluate rule criteria * * \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression * passes, some other value if it does not) */ int pcmk__evaluate_attr_expression(const xmlNode *expression, const pcmk_rule_input_t *rule_input) { const char *id = NULL; const char *op = NULL; const char *attr = NULL; const char *type_s = NULL; const char *value = NULL; const char *actual = NULL; const char *source_s = NULL; const char *reference = NULL; char *expanded_attr = NULL; int rc = pcmk_rc_ok; enum pcmk__type type = pcmk__type_unknown; enum pcmk__reference_source source = pcmk__source_unknown; enum pcmk__comparison comparison = pcmk__comparison_unknown; if ((expression == NULL) || (rule_input == NULL)) { return EINVAL; } // Get expression ID (for logging) id = pcmk__xe_id(expression); if (pcmk__str_empty(id)) { - /* @COMPAT When we can break behavioral backward compatibility, - * fail the expression - */ - pcmk__config_warn(PCMK_XE_EXPRESSION " element has no " PCMK_XA_ID); - id = "without ID"; // for logging + pcmk__config_err("Treating " PCMK_XE_EXPRESSION " without " PCMK_XA_ID + " as not passing"); + return pcmk_rc_unpack_error; } /* Get name of node attribute to compare (expanding any %0-%9 to * regular expression submatches) */ attr = crm_element_value(expression, PCMK_XA_ATTRIBUTE); if (attr == NULL) { pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not passing " "because " PCMK_XA_ATTRIBUTE " was not specified", id); return pcmk_rc_unpack_error; } expanded_attr = pcmk__replace_submatches(attr, rule_input->rsc_id, rule_input->rsc_id_submatches, rule_input->rsc_id_nmatches); if (expanded_attr != NULL) { attr = expanded_attr; } // Get and validate operation op = crm_element_value(expression, PCMK_XA_OPERATION); comparison = pcmk__parse_comparison(op); if (comparison == pcmk__comparison_unknown) { // Not possible with schema validation enabled if (op == NULL) { pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not " "passing because it has no " PCMK_XA_OPERATION, id); } else { pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not " "passing because '%s' is not a valid " PCMK_XA_OPERATION, id, op); } rc = pcmk_rc_unpack_error; goto done; } // How reference value is obtained (literal, resource meta-attribute, etc.) source_s = crm_element_value(expression, PCMK_XA_VALUE_SOURCE); source = pcmk__parse_source(source_s); if (source == pcmk__source_unknown) { // Not possible with schema validation enabled - // @COMPAT Fail expression once we can break backward compatibility - pcmk__config_warn("Expression %s has invalid " PCMK_XA_VALUE_SOURCE - " value '%s', using default " - "('" PCMK_VALUE_LITERAL "')", id, source_s); - source = pcmk__source_literal; + pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not passing " + "because '%s' is not a valid " PCMK_XA_VALUE_SOURCE, + id, source_s); + rc = pcmk_rc_unpack_error; + goto done; } // Get and validate reference value value = crm_element_value(expression, PCMK_XA_VALUE); switch (comparison) { case pcmk__comparison_defined: case pcmk__comparison_undefined: if (value != NULL) { - pcmk__config_warn("Ignoring " PCMK_XA_VALUE " in " - PCMK_XE_EXPRESSION " %s because it is unused " - "when " PCMK_XA_BOOLEAN_OP " is %s", id, op); + pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not " + "passing because " PCMK_XA_VALUE " is not " + "allowed when " PCMK_XA_OPERATION " is %s", + id, op); + rc = pcmk_rc_unpack_error; + goto done; } break; default: if (value == NULL) { - pcmk__config_warn(PCMK_XE_EXPRESSION " %s has no " - PCMK_XA_VALUE, id); + pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not " + "passing because " PCMK_XA_VALUE " is " + "required when " PCMK_XA_OPERATION " is %s", + id, op); + rc = pcmk_rc_unpack_error; + goto done; } break; } reference = value_from_source(value, source, rule_input); // Get actual value of node attribute if (rule_input->node_attrs != NULL) { actual = g_hash_table_lookup(rule_input->node_attrs, attr); } // Get and validate value type (after expanding reference value) type_s = crm_element_value(expression, PCMK_XA_TYPE); type = pcmk__parse_type(type_s, comparison, actual, reference); if (type == pcmk__type_unknown) { - /* Not possible with schema validation enabled - * - * @COMPAT When we can break behavioral backward compatibility, treat - * the expression as not passing. - */ - pcmk__config_warn("Non-empty node attribute values will be treated as " - "equal for " PCMK_XE_EXPRESSION " %s because '%s' " - "is not a valid type", id, type); + // Not possible with schema validation enabled + pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not passing " + "because '%s' is not a valid type", id, type_s); + rc = pcmk_rc_unpack_error; + goto done; } rc = evaluate_attr_comparison(actual, reference, type, comparison); switch (comparison) { case pcmk__comparison_defined: case pcmk__comparison_undefined: crm_trace(PCMK_XE_EXPRESSION " %s result: %s (for attribute %s %s)", id, pcmk_rc_str(rc), attr, op); break; default: crm_trace(PCMK_XE_EXPRESSION " %s result: " "%s (attribute %s %s '%s' via %s source as %s type)", id, pcmk_rc_str(rc), attr, op, pcmk__s(reference, ""), pcmk__s(source_s, "default"), pcmk__s(type_s, "default")); break; } done: free(expanded_attr); return rc; } /*! * \internal * \brief Evaluate a resource rule expression * * \param[in] rsc_expression XML of rule's \c PCMK_XE_RSC_EXPRESSION subelement * \param[in] rule_input Values used to evaluate rule criteria * * \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression * passes, some other value if it does not) */ int pcmk__evaluate_rsc_expression(const xmlNode *rsc_expression, const pcmk_rule_input_t *rule_input) { const char *id = NULL; const char *standard = NULL; const char *provider = NULL; const char *type = NULL; if ((rsc_expression == NULL) || (rule_input == NULL)) { return EINVAL; } // Validate XML ID id = pcmk__xe_id(rsc_expression); if (pcmk__str_empty(id)) { // Not possible with schema validation enabled - /* @COMPAT When we can break behavioral backward compatibility, - * fail the expression - */ - pcmk__config_warn(PCMK_XE_RSC_EXPRESSION " has no " PCMK_XA_ID); - id = "without ID"; // for logging + pcmk__config_err("Treating " PCMK_XE_RSC_EXPRESSION " without " + PCMK_XA_ID " as not passing"); + return pcmk_rc_unpack_error; } // Compare resource standard standard = crm_element_value(rsc_expression, PCMK_XA_CLASS); if ((standard != NULL) && !pcmk__str_eq(standard, rule_input->rsc_standard, pcmk__str_none)) { crm_trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because " "actual standard '%s' doesn't match '%s'", id, pcmk__s(rule_input->rsc_standard, ""), standard); return pcmk_rc_op_unsatisfied; } // Compare resource provider provider = crm_element_value(rsc_expression, PCMK_XA_PROVIDER); if ((provider != NULL) && !pcmk__str_eq(provider, rule_input->rsc_provider, pcmk__str_none)) { crm_trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because " "actual provider '%s' doesn't match '%s'", id, pcmk__s(rule_input->rsc_provider, ""), provider); return pcmk_rc_op_unsatisfied; } // Compare resource agent type type = crm_element_value(rsc_expression, PCMK_XA_TYPE); if ((type != NULL) && !pcmk__str_eq(type, rule_input->rsc_agent, pcmk__str_none)) { crm_trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because " "actual agent '%s' doesn't match '%s'", id, pcmk__s(rule_input->rsc_agent, ""), type); return pcmk_rc_op_unsatisfied; } crm_trace(PCMK_XE_RSC_EXPRESSION " %s is satisfied by %s%s%s:%s", id, pcmk__s(standard, ""), ((provider == NULL)? "" : ":"), pcmk__s(provider, ""), pcmk__s(type, "")); return pcmk_rc_ok; } /*! * \internal * \brief Evaluate an operation rule expression * * \param[in] op_expression XML of a rule's \c PCMK_XE_OP_EXPRESSION subelement * \param[in] rule_input Values used to evaluate rule criteria * * \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression * is satisfied, some other value if it is not) */ int pcmk__evaluate_op_expression(const xmlNode *op_expression, const pcmk_rule_input_t *rule_input) { const char *id = NULL; const char *name = NULL; const char *interval_s = NULL; guint interval_ms = 0U; if ((op_expression == NULL) || (rule_input == NULL)) { return EINVAL; } // Get operation expression ID (for logging) id = pcmk__xe_id(op_expression); if (pcmk__str_empty(id)) { // Not possible with schema validation enabled - /* @COMPAT When we can break behavioral backward compatibility, - * return pcmk_rc_op_unsatisfied - */ - pcmk__config_warn(PCMK_XE_OP_EXPRESSION " element has no " PCMK_XA_ID); - id = "without ID"; // for logging + pcmk__config_err("Treating " PCMK_XE_OP_EXPRESSION " without " + PCMK_XA_ID " as not passing"); + return pcmk_rc_unpack_error; } // Validate operation name name = crm_element_value(op_expression, PCMK_XA_NAME); if (name == NULL) { // Not possible with schema validation enabled - pcmk__config_warn("Treating " PCMK_XE_OP_EXPRESSION " %s as not " - "passing because it has no " PCMK_XA_NAME, id); + pcmk__config_err("Treating " PCMK_XE_OP_EXPRESSION " %s as not " + "passing because it has no " PCMK_XA_NAME, id); return pcmk_rc_unpack_error; } // Validate operation interval interval_s = crm_element_value(op_expression, PCMK_META_INTERVAL); if (pcmk_parse_interval_spec(interval_s, &interval_ms) != pcmk_rc_ok) { - pcmk__config_warn("Treating " PCMK_XE_OP_EXPRESSION " %s as not " - "passing because '%s' is not a valid interval", - id, interval_s); + pcmk__config_err("Treating " PCMK_XE_OP_EXPRESSION " %s as not " + "passing because '%s' is not a valid " + PCMK_META_INTERVAL, + id, interval_s); return pcmk_rc_unpack_error; } // Compare operation name if (!pcmk__str_eq(name, rule_input->op_name, pcmk__str_none)) { crm_trace(PCMK_XE_OP_EXPRESSION " %s is unsatisfied because " "actual name '%s' doesn't match '%s'", id, pcmk__s(rule_input->op_name, ""), name); return pcmk_rc_op_unsatisfied; } // Compare operation interval (unspecified interval matches all) if ((interval_s != NULL) && (interval_ms != rule_input->op_interval_ms)) { crm_trace(PCMK_XE_OP_EXPRESSION " %s is unsatisfied because " "actual interval %s doesn't match %s", id, pcmk__readable_interval(rule_input->op_interval_ms), pcmk__readable_interval(interval_ms)); return pcmk_rc_op_unsatisfied; } crm_trace(PCMK_XE_OP_EXPRESSION " %s is satisfied (name %s, interval %s)", id, name, pcmk__readable_interval(rule_input->op_interval_ms)); return pcmk_rc_ok; } /*! * \internal * \brief Evaluate a rule condition * * \param[in,out] condition XML containing a rule condition (a subrule, or an * expression of any type) * \param[in] rule_input Values used to evaluate rule criteria * \param[out] next_change If not NULL, set to when evaluation will change * * \return Standard Pacemaker return code (\c pcmk_rc_ok if the condition * passes, some other value if it does not) */ int pcmk__evaluate_condition(xmlNode *condition, const pcmk_rule_input_t *rule_input, crm_time_t *next_change) { if ((condition == NULL) || (rule_input == NULL)) { return EINVAL; } switch (pcmk__condition_type(condition)) { case pcmk__condition_rule: return pcmk_evaluate_rule(condition, rule_input, next_change); case pcmk__condition_attribute: case pcmk__condition_location: return pcmk__evaluate_attr_expression(condition, rule_input); case pcmk__condition_datetime: { int rc = pcmk__evaluate_date_expression(condition, rule_input->now, next_change); return (rc == pcmk_rc_within_range)? pcmk_rc_ok : rc; } case pcmk__condition_resource: return pcmk__evaluate_rsc_expression(condition, rule_input); case pcmk__condition_operation: return pcmk__evaluate_op_expression(condition, rule_input); default: // Not possible with schema validation enabled pcmk__config_err("Treating rule condition %s as not passing " "because %s is not a valid condition type", pcmk__s(pcmk__xe_id(condition), "without ID"), (const char *) condition->name); return pcmk_rc_unpack_error; } } /*! * \brief Evaluate a single rule, including all its conditions * * \param[in,out] rule XML containing a rule definition or its id-ref * \param[in] rule_input Values used to evaluate rule criteria * \param[out] next_change If not NULL, set to when evaluation will change * * \return Standard Pacemaker return code (\c pcmk_rc_ok if the rule is * satisfied, some other value if it is not) */ int pcmk_evaluate_rule(xmlNode *rule, const pcmk_rule_input_t *rule_input, crm_time_t *next_change) { bool empty = true; int rc = pcmk_rc_ok; const char *id = NULL; const char *value = NULL; enum pcmk__combine combine = pcmk__combine_unknown; if ((rule == NULL) || (rule_input == NULL)) { return EINVAL; } rule = pcmk__xe_resolve_idref(rule, NULL); if (rule == NULL) { // Not possible with schema validation enabled; message already logged return pcmk_rc_unpack_error; } // Validate XML ID id = pcmk__xe_id(rule); - if (pcmk__str_empty(id)) { - /* @COMPAT When we can break behavioral backward compatibility, - * fail the rule - */ - pcmk__config_warn(PCMK_XE_RULE " has no " PCMK_XA_ID); - id = "without ID"; // for logging + if (pcmk__str_empty(id)) { // Not possible with schema validation enabled + pcmk__config_err("Treating " PCMK_XE_RULE " without " PCMK_XA_ID + " as not passing"); + return pcmk_rc_unpack_error; } value = crm_element_value(rule, PCMK_XA_BOOLEAN_OP); combine = pcmk__parse_combine(value); switch (combine) { case pcmk__combine_and: // For "and", rc defaults to success (reset on failure below) break; case pcmk__combine_or: // For "or", rc defaults to failure (reset on success below) rc = pcmk_rc_op_unsatisfied; break; - default: - /* @COMPAT When we can break behavioral backward compatibility, - * return pcmk_rc_unpack_error - */ - pcmk__config_warn("Rule %s has invalid " PCMK_XA_BOOLEAN_OP - " value '%s', using default '" PCMK_VALUE_AND "'", - pcmk__xe_id(rule), value); - combine = pcmk__combine_and; - break; + default: // Not possible with schema validation enabled + pcmk__config_err("Treating " PCMK_XE_RULE " %s as not passing " + "because '%s' is not a valid " PCMK_XA_BOOLEAN_OP, + id, value); + return pcmk_rc_unpack_error; } // Evaluate each condition for (xmlNode *condition = pcmk__xe_first_child(rule, NULL, NULL, NULL); condition != NULL; condition = pcmk__xe_next(condition)) { empty = false; if (pcmk__evaluate_condition(condition, rule_input, next_change) == pcmk_rc_ok) { if (combine == pcmk__combine_or) { rc = pcmk_rc_ok; // Any pass is final for "or" break; } } else if (combine == pcmk__combine_and) { rc = pcmk_rc_op_unsatisfied; // Any failure is final for "and" break; } } if (empty) { // Not possible with schema validation enabled - /* @COMPAT Currently, we don't actually ignore "or" rules because - * rc is initialized to failure above in that case. When we can break - * backward compatibility, reset rc to pcmk_rc_ok here. - */ pcmk__config_warn("Ignoring rule %s because it contains no conditions", id); + rc = pcmk_rc_ok; } crm_trace("Rule %s is %ssatisfied", id, ((rc == pcmk_rc_ok)? "" : "not ")); return rc; } /*! * \internal * \brief Evaluate all rules contained within an element * * \param[in,out] xml XML element possibly containing rule subelements * \param[in] rule_input Values used to evaluate rule criteria * \param[out] next_change If not NULL, set to when evaluation will change * * \return Standard Pacemaker return code (pcmk_rc_ok if there are no contained * rules or any contained rule passes, otherwise the result of the last * rule) * \deprecated On code paths leading to this function, the schema allows * multiple top-level rules only in the deprecated lifetime element * of location constraints. The code also allows multiple top-level * rules when unpacking attribute sets, but this is deprecated and * already prevented by schema validation. This function can be * dropped when support for those is dropped. */ int pcmk__evaluate_rules(xmlNode *xml, const pcmk_rule_input_t *rule_input, crm_time_t *next_change) { // If there are no rules, pass by default int rc = pcmk_rc_ok; bool have_rule = false; for (xmlNode *rule = pcmk__xe_first_child(xml, PCMK_XE_RULE, NULL, NULL); rule != NULL; rule = pcmk__xe_next_same(rule)) { if (have_rule) { pcmk__warn_once(pcmk__wo_multiple_rules, "Support for multiple top-level rules is " "deprecated (replace with a single rule containing " "the existing rules with " PCMK_XA_BOOLEAN_OP "set to " PCMK_VALUE_OR " instead)"); } else { have_rule = true; } rc = pcmk_evaluate_rule(rule, rule_input, next_change); if (rc == pcmk_rc_ok) { break; } } return rc; } diff --git a/lib/common/tests/rules/pcmk__evaluate_attr_expression_test.c b/lib/common/tests/rules/pcmk__evaluate_attr_expression_test.c index 0f6c6172d4..e88774c226 100644 --- a/lib/common/tests/rules/pcmk__evaluate_attr_expression_test.c +++ b/lib/common/tests/rules/pcmk__evaluate_attr_expression_test.c @@ -1,831 +1,841 @@ /* * Copyright 2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include "crmcommon_private.h" /* * Shared data */ #define MATCHED_STRING "server-north" static const regmatch_t submatches[] = { { .rm_so = 0, .rm_eo = 12 }, // %0 = Entire string { .rm_so = 7, .rm_eo = 12 }, // %1 = "north" }; static pcmk_rule_input_t rule_input = { // These are the only members used to evaluate attribute expressions // Used to replace submatches in attribute name .rsc_id = MATCHED_STRING, .rsc_id_submatches = submatches, .rsc_id_nmatches = 2, // Used when source is instance attributes .rsc_params = NULL, // Used when source is meta-attributes .rsc_meta = NULL, // Used to get actual value of node attribute .node_attrs = NULL, }; static int setup(void **state) { rule_input.rsc_params = pcmk__strkey_table(free, free); pcmk__insert_dup(rule_input.rsc_params, "foo-param", "bar"); pcmk__insert_dup(rule_input.rsc_params, "myparam", "different"); rule_input.rsc_meta = pcmk__strkey_table(free, free); pcmk__insert_dup(rule_input.rsc_meta, "foo-meta", "bar"); pcmk__insert_dup(rule_input.rsc_params, "mymeta", "different"); rule_input.node_attrs = pcmk__strkey_table(free, free); pcmk__insert_dup(rule_input.node_attrs, "foo", "bar"); pcmk__insert_dup(rule_input.node_attrs, "num", "10"); pcmk__insert_dup(rule_input.node_attrs, "ver", "3.5.0"); pcmk__insert_dup(rule_input.node_attrs, "prefer-north", "100"); return 0; } static int teardown(void **state) { g_hash_table_destroy(rule_input.rsc_params); g_hash_table_destroy(rule_input.rsc_meta); g_hash_table_destroy(rule_input.node_attrs); return 0; } /*! * \internal * \brief Run one test, comparing return value * * \param[in] xml_string Node attribute expression XML as string * \param[in] reference_rc Assert that evaluation result equals this */ static void assert_attr_expression(const char *xml_string, int reference_rc) { xmlNode *xml = pcmk__xml_parse(xml_string); assert_int_equal(pcmk__evaluate_attr_expression(xml, &rule_input), reference_rc); pcmk__xml_free(xml); } /* * Invalid arguments */ #define EXPR_SOURCE_LITERAL_PASSES \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_ATTRIBUTE "='foo' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ PCMK_XA_VALUE "='bar' " \ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_LITERAL "' />" static void null_invalid(void **state) { xmlNode *xml = pcmk__xml_parse(EXPR_SOURCE_LITERAL_PASSES); assert_int_equal(pcmk__evaluate_attr_expression(NULL, NULL), EINVAL); assert_int_equal(pcmk__evaluate_attr_expression(xml, NULL), EINVAL); assert_int_equal(pcmk__evaluate_attr_expression(NULL, &rule_input), EINVAL); pcmk__xml_free(xml); } /* * Test PCMK_XA_ID */ #define EXPR_ID_MISSING \ "<" PCMK_XE_EXPRESSION " " \ PCMK_XA_ATTRIBUTE "='foo' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ PCMK_XA_VALUE "='bar' />" static void id_missing(void **state) { - // Currently acceptable - assert_attr_expression(EXPR_ID_MISSING, pcmk_rc_ok); + assert_attr_expression(EXPR_ID_MISSING, pcmk_rc_unpack_error); } /* * Test PCMK_XA_ATTRIBUTE */ #define EXPR_ATTR_MISSING \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ PCMK_XA_VALUE "='bar' />" static void attr_missing(void **state) { assert_attr_expression(EXPR_ATTR_MISSING, pcmk_rc_unpack_error); } #define EXPR_ATTR_SUBMATCH_PASSES \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_ATTRIBUTE "='prefer-%1' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_DEFINED "' />" static void attr_with_submatch_passes(void **state) { assert_attr_expression(EXPR_ATTR_SUBMATCH_PASSES, pcmk_rc_ok); } #define EXPR_ATTR_SUBMATCH_FAILS \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_ATTRIBUTE "='undefined-%1' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_DEFINED "' />" static void attr_with_submatch_fails(void **state) { assert_attr_expression(EXPR_ATTR_SUBMATCH_FAILS, pcmk_rc_op_unsatisfied); } /* * Test PCMK_XA_VALUE_SOURCE */ #define EXPR_SOURCE_MISSING \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ PCMK_XA_ATTRIBUTE "='foo' " \ PCMK_XA_VALUE "='bar' />" static void source_missing(void **state) { // Defaults to literal assert_attr_expression(EXPR_SOURCE_MISSING, pcmk_rc_ok); } #define EXPR_SOURCE_INVALID \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_ATTRIBUTE "='foo' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ PCMK_XA_VALUE "='bar' " \ PCMK_XA_VALUE_SOURCE "='not-a-source' />" static void source_invalid(void **state) { - // Currently treated as literal - assert_attr_expression(EXPR_SOURCE_INVALID, pcmk_rc_ok); + assert_attr_expression(EXPR_SOURCE_INVALID, pcmk_rc_unpack_error); } static void source_literal_passes(void **state) { assert_attr_expression(EXPR_SOURCE_LITERAL_PASSES, pcmk_rc_ok); } #define EXPR_SOURCE_LITERAL_VALUE_FAILS \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_ATTRIBUTE "='foo' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ PCMK_XA_VALUE "='wrong-value' " \ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_LITERAL "' />" static void source_literal_value_fails(void **state) { assert_attr_expression(EXPR_SOURCE_LITERAL_VALUE_FAILS, pcmk_rc_op_unsatisfied); } #define EXPR_SOURCE_LITERAL_ATTR_FAILS \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_ATTRIBUTE "='not-an-attribute' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ PCMK_XA_VALUE "='bar' " \ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_LITERAL "' />" static void source_literal_attr_fails(void **state) { assert_attr_expression(EXPR_SOURCE_LITERAL_ATTR_FAILS, pcmk_rc_op_unsatisfied); } #define EXPR_SOURCE_PARAM_MISSING \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_ATTRIBUTE "='foo' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ PCMK_XA_VALUE "='not-a-param' " \ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_PARAM "' />" static void source_params_missing(void **state) { assert_attr_expression(EXPR_SOURCE_PARAM_MISSING, pcmk_rc_op_unsatisfied); } #define EXPR_SOURCE_PARAM_PASSES \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_ATTRIBUTE "='foo' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ PCMK_XA_VALUE "='foo-param' " \ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_PARAM "' />" static void source_params_passes(void **state) { assert_attr_expression(EXPR_SOURCE_PARAM_PASSES, pcmk_rc_ok); } #define EXPR_SOURCE_PARAM_FAILS \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_ATTRIBUTE "='foo' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ PCMK_XA_VALUE "='myparam' " \ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_PARAM "' />" static void source_params_fails(void **state) { assert_attr_expression(EXPR_SOURCE_PARAM_FAILS, pcmk_rc_op_unsatisfied); } #define EXPR_SOURCE_META_MISSING \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_ATTRIBUTE "='foo' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ PCMK_XA_VALUE "='not-a-meta' " \ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_META "' />" static void source_meta_missing(void **state) { assert_attr_expression(EXPR_SOURCE_META_MISSING, pcmk_rc_op_unsatisfied); } #define EXPR_SOURCE_META_PASSES \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_ATTRIBUTE "='foo' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ PCMK_XA_VALUE "='foo-meta' " \ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_META "' />" static void source_meta_passes(void **state) { assert_attr_expression(EXPR_SOURCE_META_PASSES, pcmk_rc_ok); } #define EXPR_SOURCE_META_FAILS \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_ATTRIBUTE "='foo' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ PCMK_XA_VALUE "='mymeta' " \ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_META "' />" static void source_meta_fails(void **state) { assert_attr_expression(EXPR_SOURCE_META_FAILS, pcmk_rc_op_unsatisfied); } /* * Test PCMK_XA_TYPE */ #define EXPR_TYPE_DEFAULT_NUMBER \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \ PCMK_XA_ATTRIBUTE "='num' " \ PCMK_XA_VALUE "='2.5' />" static void type_default_number(void **state) { // Defaults to number for "gt" if either value contains a decimal point assert_attr_expression(EXPR_TYPE_DEFAULT_NUMBER, pcmk_rc_ok); } #define EXPR_TYPE_DEFAULT_INT \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \ PCMK_XA_ATTRIBUTE "='num' " \ PCMK_XA_VALUE "='2' />" static void type_default_int(void **state) { // Defaults to integer for "gt" if neither value contains a decimal point assert_attr_expression(EXPR_TYPE_DEFAULT_INT, pcmk_rc_ok); } +#define EXPR_TYPE_INVALID \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='not-a-value' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_ATTRIBUTE "='foo' " \ + PCMK_XA_VALUE "='bar' />" + +static void +type_invalid(void **state) +{ + assert_attr_expression(EXPR_TYPE_INVALID, pcmk_rc_unpack_error); +} + #define EXPR_TYPE_STRING_PASSES \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_TYPE "='" PCMK_VALUE_STRING "' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ PCMK_XA_ATTRIBUTE "='foo' " \ PCMK_XA_VALUE "='bar' />" static void type_string_passes(void **state) { assert_attr_expression(EXPR_TYPE_STRING_PASSES, pcmk_rc_ok); } #define EXPR_TYPE_STRING_FAILS \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_TYPE "='" PCMK_VALUE_STRING "' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ PCMK_XA_ATTRIBUTE "='foo' " \ PCMK_XA_VALUE "='bat' />" static void type_string_fails(void **state) { assert_attr_expression(EXPR_TYPE_STRING_FAILS, pcmk_rc_op_unsatisfied); } #define EXPR_TYPE_INTEGER_PASSES \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ PCMK_XA_ATTRIBUTE "='num' " \ PCMK_XA_VALUE "='10' />" static void type_integer_passes(void **state) { assert_attr_expression(EXPR_TYPE_INTEGER_PASSES, pcmk_rc_ok); } #define EXPR_TYPE_INTEGER_FAILS \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ PCMK_XA_ATTRIBUTE "='num' " \ PCMK_XA_VALUE "='11' />" static void type_integer_fails(void **state) { assert_attr_expression(EXPR_TYPE_INTEGER_FAILS, pcmk_rc_op_unsatisfied); } #define EXPR_TYPE_INTEGER_TRUNCATION \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ PCMK_XA_ATTRIBUTE "='num' " \ PCMK_XA_VALUE "='10.5' />" static void type_integer_truncation(void **state) { assert_attr_expression(EXPR_TYPE_INTEGER_TRUNCATION, pcmk_rc_ok); } #define EXPR_TYPE_NUMBER_PASSES \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_TYPE "='" PCMK_VALUE_NUMBER "' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ PCMK_XA_ATTRIBUTE "='num' " \ PCMK_XA_VALUE "='10.0' />" static void type_number_passes(void **state) { assert_attr_expression(EXPR_TYPE_NUMBER_PASSES, pcmk_rc_ok); } #define EXPR_TYPE_NUMBER_FAILS \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_TYPE "='" PCMK_VALUE_NUMBER "' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ PCMK_XA_ATTRIBUTE "='num' " \ PCMK_XA_VALUE "='10.1' />" static void type_number_fails(void **state) { assert_attr_expression(EXPR_TYPE_NUMBER_FAILS, pcmk_rc_op_unsatisfied); } #define EXPR_TYPE_VERSION_PASSES \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_TYPE "='" PCMK_VALUE_VERSION "' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \ PCMK_XA_ATTRIBUTE "='ver' " \ PCMK_XA_VALUE "='3.4.9' />" static void type_version_passes(void **state) { assert_attr_expression(EXPR_TYPE_VERSION_PASSES, pcmk_rc_ok); } #define EXPR_TYPE_VERSION_EQUALITY \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_TYPE "='" PCMK_VALUE_VERSION "' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ PCMK_XA_ATTRIBUTE "='ver' " \ PCMK_XA_VALUE "='3.5' />" static void type_version_equality(void **state) { assert_attr_expression(EXPR_TYPE_VERSION_EQUALITY, pcmk_rc_ok); } #define EXPR_TYPE_VERSION_FAILS \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_TYPE "='" PCMK_VALUE_VERSION "' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_GTE "' " \ PCMK_XA_ATTRIBUTE "='ver' " \ PCMK_XA_VALUE "='4.0' />" static void type_version_fails(void **state) { assert_attr_expression(EXPR_TYPE_VERSION_FAILS, pcmk_rc_before_range); } /* * Test PCMK_XA_OPERATION */ #define EXPR_OP_MISSING \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_ATTRIBUTE "='foo' " \ PCMK_XA_VALUE "='bar' />" static void op_missing(void **state) { assert_attr_expression(EXPR_OP_MISSING, pcmk_rc_unpack_error); } #define EXPR_OP_INVALID \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_ATTRIBUTE "='foo' " \ PCMK_XA_OPERATION "='not-an-operation' " \ PCMK_XA_VALUE "='bar' />" static void op_invalid(void **state) { assert_attr_expression(EXPR_OP_INVALID, pcmk_rc_unpack_error); } #define EXPR_OP_LT_PASSES \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_LT "' " \ PCMK_XA_ATTRIBUTE "='num' " \ PCMK_XA_VALUE "='20' />" static void op_lt_passes(void **state) { assert_attr_expression(EXPR_OP_LT_PASSES, pcmk_rc_ok); } #define EXPR_OP_LT_FAILS \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_LT "' " \ PCMK_XA_ATTRIBUTE "='num' " \ PCMK_XA_VALUE "='2' />" static void op_lt_fails(void **state) { assert_attr_expression(EXPR_OP_LT_FAILS, pcmk_rc_after_range); } #define EXPR_OP_GT_PASSES \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \ PCMK_XA_ATTRIBUTE "='num' " \ PCMK_XA_VALUE "='2' />" static void op_gt_passes(void **state) { assert_attr_expression(EXPR_OP_GT_PASSES, pcmk_rc_ok); } #define EXPR_OP_GT_FAILS \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \ PCMK_XA_ATTRIBUTE "='num' " \ PCMK_XA_VALUE "='20' />" static void op_gt_fails(void **state) { assert_attr_expression(EXPR_OP_GT_FAILS, pcmk_rc_before_range); } #define EXPR_OP_LTE_LT_PASSES \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_LTE "' " \ PCMK_XA_ATTRIBUTE "='num' " \ PCMK_XA_VALUE "='20' />" static void op_lte_lt_passes(void **state) { assert_attr_expression(EXPR_OP_LTE_LT_PASSES, pcmk_rc_ok); } #define EXPR_OP_LTE_EQ_PASSES \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_LTE "' " \ PCMK_XA_ATTRIBUTE "='num' " \ PCMK_XA_VALUE "='10' />" static void op_lte_eq_passes(void **state) { assert_attr_expression(EXPR_OP_LTE_EQ_PASSES, pcmk_rc_ok); } #define EXPR_OP_LTE_FAILS \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_LTE "' " \ PCMK_XA_ATTRIBUTE "='num' " \ PCMK_XA_VALUE "='9' />" static void op_lte_fails(void **state) { assert_attr_expression(EXPR_OP_LTE_FAILS, pcmk_rc_after_range); } #define EXPR_OP_GTE_GT_PASSES \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_GTE "' " \ PCMK_XA_ATTRIBUTE "='num' " \ PCMK_XA_VALUE "='1' />" static void op_gte_gt_passes(void **state) { assert_attr_expression(EXPR_OP_GTE_GT_PASSES, pcmk_rc_ok); } #define EXPR_OP_GTE_EQ_PASSES \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_GTE "' " \ PCMK_XA_ATTRIBUTE "='num' " \ PCMK_XA_VALUE "='10' />" static void op_gte_eq_passes(void **state) { assert_attr_expression(EXPR_OP_GTE_EQ_PASSES, pcmk_rc_ok); } #define EXPR_OP_GTE_FAILS \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_GTE "' " \ PCMK_XA_ATTRIBUTE "='num' " \ PCMK_XA_VALUE "='11' />" static void op_gte_fails(void **state) { assert_attr_expression(EXPR_OP_GTE_FAILS, pcmk_rc_before_range); } // This also tests that string is used if values aren't parseable as numbers #define EXPR_OP_EQ_PASSES \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_TYPE "='" PCMK_VALUE_NUMBER "' " \ PCMK_XA_ATTRIBUTE "='foo' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ PCMK_XA_VALUE "='bar' " \ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_LITERAL "' />" static void op_eq_passes(void **state) { assert_attr_expression(EXPR_OP_EQ_PASSES, pcmk_rc_ok); } #define EXPR_OP_EQ_FAILS \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ PCMK_XA_ATTRIBUTE "='num' " \ PCMK_XA_VALUE "='bar' />" static void op_eq_fails(void **state) { assert_attr_expression(EXPR_OP_EQ_FAILS, pcmk_rc_op_unsatisfied); } #define EXPR_OP_NE_PASSES \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_TYPE "='" PCMK_VALUE_STRING "' " \ PCMK_XA_ATTRIBUTE "='foo' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_NE "' " \ PCMK_XA_VALUE "='bat' " \ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_LITERAL "' />" static void op_ne_passes(void **state) { assert_attr_expression(EXPR_OP_NE_PASSES, pcmk_rc_ok); } #define EXPR_OP_NE_FAILS \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_NE "' " \ PCMK_XA_ATTRIBUTE "='num' " \ PCMK_XA_VALUE "='10' />" static void op_ne_fails(void **state) { assert_attr_expression(EXPR_OP_NE_FAILS, pcmk_rc_op_unsatisfied); } #define EXPR_OP_DEFINED_PASSES \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_ATTRIBUTE "='foo' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_DEFINED "' />" static void op_defined_passes(void **state) { assert_attr_expression(EXPR_OP_DEFINED_PASSES, pcmk_rc_ok); } #define EXPR_OP_DEFINED_FAILS \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_ATTRIBUTE "='boo' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_DEFINED "' />" static void op_defined_fails(void **state) { assert_attr_expression(EXPR_OP_DEFINED_FAILS, pcmk_rc_op_unsatisfied); } #define EXPR_OP_DEFINED_WITH_VALUE \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_ATTRIBUTE "='foo' " \ PCMK_XA_VALUE "='bar' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_DEFINED "' />" static void op_defined_with_value(void **state) { - // Ill-formed but currently accepted - assert_attr_expression(EXPR_OP_DEFINED_WITH_VALUE, pcmk_rc_ok); + assert_attr_expression(EXPR_OP_DEFINED_WITH_VALUE, pcmk_rc_unpack_error); } #define EXPR_OP_UNDEFINED_PASSES \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_ATTRIBUTE "='boo' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_NOT_DEFINED "' />" static void op_undefined_passes(void **state) { assert_attr_expression(EXPR_OP_UNDEFINED_PASSES, pcmk_rc_ok); } #define EXPR_OP_UNDEFINED_FAILS \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_ATTRIBUTE "='foo' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_NOT_DEFINED "' />" static void op_undefined_fails(void **state) { assert_attr_expression(EXPR_OP_DEFINED_FAILS, pcmk_rc_op_unsatisfied); } /* * Test PCMK_XA_VALUE */ #define EXPR_VALUE_MISSING_DEFINED_OK \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_ATTRIBUTE "='num' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_DEFINED "' />" static void value_missing_defined_ok(void **state) { assert_attr_expression(EXPR_VALUE_MISSING_DEFINED_OK, pcmk_rc_ok); } -#define EXPR_VALUE_MISSING_EQ_OK \ +#define EXPR_VALUE_MISSING_EQ_FAILS \ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_ATTRIBUTE "='not-an-attr' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' />" static void -value_missing_eq_ok(void **state) +value_missing_eq_fails(void **state) { - // Currently treated as NULL reference value - assert_attr_expression(EXPR_VALUE_MISSING_EQ_OK, pcmk_rc_ok); + assert_attr_expression(EXPR_VALUE_MISSING_EQ_FAILS, pcmk_rc_unpack_error); } #define expr_test(f) cmocka_unit_test_setup_teardown(f, setup, teardown) PCMK__UNIT_TEST(pcmk__xml_test_setup_group, pcmk__xml_test_teardown_group, cmocka_unit_test(null_invalid), expr_test(id_missing), expr_test(attr_missing), expr_test(attr_with_submatch_passes), expr_test(attr_with_submatch_fails), expr_test(source_missing), expr_test(source_invalid), expr_test(source_literal_passes), expr_test(source_literal_value_fails), expr_test(source_literal_attr_fails), expr_test(source_params_missing), expr_test(source_params_passes), expr_test(source_params_fails), expr_test(source_meta_missing), expr_test(source_meta_passes), expr_test(source_meta_fails), expr_test(type_default_number), expr_test(type_default_int), + expr_test(type_invalid), expr_test(type_string_passes), expr_test(type_string_fails), expr_test(type_integer_passes), expr_test(type_integer_fails), expr_test(type_integer_truncation), expr_test(type_number_passes), expr_test(type_number_fails), expr_test(type_version_passes), expr_test(type_version_equality), expr_test(type_version_fails), expr_test(op_missing), expr_test(op_invalid), expr_test(op_lt_passes), expr_test(op_lt_fails), expr_test(op_gt_passes), expr_test(op_gt_fails), expr_test(op_lte_lt_passes), expr_test(op_lte_eq_passes), expr_test(op_lte_fails), expr_test(op_gte_gt_passes), expr_test(op_gte_eq_passes), expr_test(op_gte_fails), expr_test(op_eq_passes), expr_test(op_eq_fails), expr_test(op_ne_passes), expr_test(op_ne_fails), expr_test(op_defined_passes), expr_test(op_defined_fails), expr_test(op_defined_with_value), expr_test(op_undefined_passes), expr_test(op_undefined_fails), expr_test(value_missing_defined_ok), - expr_test(value_missing_eq_ok)) + expr_test(value_missing_eq_fails)) diff --git a/lib/common/tests/rules/pcmk__evaluate_date_expression_test.c b/lib/common/tests/rules/pcmk__evaluate_date_expression_test.c index 64e2590b34..9d718c4644 100644 --- a/lib/common/tests/rules/pcmk__evaluate_date_expression_test.c +++ b/lib/common/tests/rules/pcmk__evaluate_date_expression_test.c @@ -1,684 +1,597 @@ /* * Copyright 2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include "crmcommon_private.h" /*! * \internal * \brief Run one test, comparing return value and output argument * * \param[in] xml Date expression XML * \param[in] now_s Time to evaluate expression with (as string) * \param[in] next_change_s If this and \p reference_s are not NULL, initialize * next change time with this time (as string), * and assert that its value after evaluation is the * reference * \param[in] reference_s If not NULL, time (as string) that next change * should be after expression evaluation * \param[in] reference_rc Assert that evaluation result equals this */ static void assert_date_expression(const xmlNode *xml, const char *now_s, const char *next_change_s, const char *reference_s, int reference_rc) { crm_time_t *now = NULL; crm_time_t *next_change = NULL; bool check_next_change = (next_change_s != NULL) && (reference_s != NULL); if (check_next_change) { next_change = crm_time_new(next_change_s); } now = crm_time_new(now_s); assert_int_equal(pcmk__evaluate_date_expression(xml, now, next_change), reference_rc); crm_time_free(now); if (check_next_change) { crm_time_t *reference = crm_time_new(reference_s); assert_int_equal(crm_time_compare(next_change, reference), 0); crm_time_free(reference); crm_time_free(next_change); } } #define EXPR_LT_VALID \ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_LT "' " \ PCMK_XA_END "='2024-02-01 15:00:00' />" static void null_invalid(void **state) { xmlNodePtr xml = pcmk__xml_parse(EXPR_LT_VALID); crm_time_t *t = crm_time_new("2024-02-01"); assert_int_equal(pcmk__evaluate_date_expression(NULL, NULL, NULL), EINVAL); assert_int_equal(pcmk__evaluate_date_expression(xml, NULL, NULL), EINVAL); assert_int_equal(pcmk__evaluate_date_expression(NULL, t, NULL), EINVAL); crm_time_free(t); pcmk__xml_free(xml); } static void null_next_change_ok(void **state) { xmlNodePtr xml = pcmk__xml_parse(EXPR_LT_VALID); assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_within_range); pcmk__xml_free(xml); } #define EXPR_ID_MISSING \ "<" PCMK_XE_DATE_EXPRESSION " " \ PCMK_XA_OPERATION "='" PCMK_VALUE_LT "' " \ PCMK_XA_END "='2024-02-01 15:00:00' />" static void id_missing(void **state) { - // Currently acceptable xmlNodePtr xml = pcmk__xml_parse(EXPR_ID_MISSING); - assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_within_range); + assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_unpack_error); pcmk__xml_free(xml); } #define EXPR_OP_INVALID \ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='not-a-choice' />" static void op_invalid(void **state) { xmlNodePtr xml = pcmk__xml_parse(EXPR_OP_INVALID); - assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined); + assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_unpack_error); pcmk__xml_free(xml); } #define EXPR_LT_MISSING_END \ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_LT "' />" static void lt_missing_end(void **state) { xmlNodePtr xml = pcmk__xml_parse(EXPR_LT_MISSING_END); - assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined); + assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_unpack_error); pcmk__xml_free(xml); } #define EXPR_LT_INVALID_END \ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_LT "' " \ PCMK_XA_END "='not-a-datetime' />" static void lt_invalid_end(void **state) { xmlNodePtr xml = pcmk__xml_parse(EXPR_LT_INVALID_END); - assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined); + assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_unpack_error); pcmk__xml_free(xml); } static void lt_valid(void **state) { xmlNodePtr xml = pcmk__xml_parse(EXPR_LT_VALID); // Now and next change are both before end assert_date_expression(xml, "2023-01-01 05:00:00", "2024-02-01 10:00:00", "2024-02-01 10:00:00", pcmk_rc_within_range); // Now is before end, next change is after end assert_date_expression(xml, "2024-02-01 14:59:59", "2024-02-01 18:00:00", "2024-02-01 15:00:00", pcmk_rc_within_range); // Now is equal to end, next change is after end assert_date_expression(xml, "2024-02-01 15:00:00", "2024-02-01 20:00:00", "2024-02-01 20:00:00", pcmk_rc_after_range); // Now and next change are both after end assert_date_expression(xml, "2024-03-01 12:00:00", "2024-02-01 20:00:00", "2024-02-01 20:00:00", pcmk_rc_after_range); pcmk__xml_free(xml); } #define EXPR_GT_MISSING_START \ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' />" static void gt_missing_start(void **state) { xmlNodePtr xml = pcmk__xml_parse(EXPR_GT_MISSING_START); - assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined); + assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_unpack_error); pcmk__xml_free(xml); } #define EXPR_GT_INVALID_START \ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \ PCMK_XA_START "='not-a-datetime' />" static void gt_invalid_start(void **state) { xmlNodePtr xml = pcmk__xml_parse(EXPR_GT_INVALID_START); - assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined); + assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_unpack_error); pcmk__xml_free(xml); } #define EXPR_GT_VALID \ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \ PCMK_XA_START "='2024-02-01 12:00:00' />" static void gt_valid(void **state) { xmlNodePtr xml = pcmk__xml_parse(EXPR_GT_VALID); // Now and next change are both before start assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00", "2024-01-01 11:00:00", pcmk_rc_before_range); // Now is before start, next change is after start assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 18:00:00", "2024-02-01 12:00:01", pcmk_rc_before_range); // Now is equal to start, next change is after start assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 18:00:00", "2024-02-01 12:00:01", pcmk_rc_before_range); // Now is one second after start, next change is after start assert_date_expression(xml, "2024-02-01 12:00:01", "2024-02-01 18:00:00", "2024-02-01 18:00:00", pcmk_rc_within_range); // t is after start, next change is after start assert_date_expression(xml, "2024-03-01 05:03:11", "2024-04-04 04:04:04", "2024-04-04 04:04:04", pcmk_rc_within_range); pcmk__xml_free(xml); } #define EXPR_RANGE_MISSING \ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' />" static void range_missing(void **state) { xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_MISSING); crm_time_t *t = crm_time_new("2024-01-01"); assert_int_equal(pcmk__evaluate_date_expression(xml, t, NULL), - pcmk_rc_undetermined); + pcmk_rc_unpack_error); crm_time_free(t); pcmk__xml_free(xml); } #define EXPR_RANGE_INVALID_START_INVALID_END \ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \ PCMK_XA_START "='not-a-date' " \ PCMK_XA_END "='not-a-date' />" static void range_invalid_start_invalid_end(void **state) { xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_INVALID_START_INVALID_END); - assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined); + assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_unpack_error); pcmk__xml_free(xml); } #define EXPR_RANGE_INVALID_START_ONLY \ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \ PCMK_XA_START "='not-a-date' />" static void range_invalid_start_only(void **state) { xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_INVALID_START_ONLY); - assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined); + assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_unpack_error); pcmk__xml_free(xml); } #define EXPR_RANGE_VALID_START_ONLY \ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \ PCMK_XA_START "='2024-02-01 12:00:00' />" static void range_valid_start_only(void **state) { xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_ONLY); // Now and next change are before start assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00", "2024-01-01 11:00:00", pcmk_rc_before_range); // Now is before start, next change is after start assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 18:00:00", "2024-02-01 12:00:00", pcmk_rc_before_range); // Now is equal to start, next change is after start assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 18:00:00", "2024-02-01 18:00:00", pcmk_rc_within_range); // Now and next change are after start assert_date_expression(xml, "2024-03-01 05:03:11", "2024-04-04 04:04:04", "2024-04-04 04:04:04", pcmk_rc_within_range); pcmk__xml_free(xml); } #define EXPR_RANGE_INVALID_END_ONLY \ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \ PCMK_XA_END "='not-a-date' />" static void range_invalid_end_only(void **state) { xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_INVALID_END_ONLY); - assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined); + assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_unpack_error); pcmk__xml_free(xml); } #define EXPR_RANGE_VALID_END_ONLY \ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \ PCMK_XA_END "='2024-02-01 15:00:00' />" static void range_valid_end_only(void **state) { xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_END_ONLY); // Now and next change are before end assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00", "2024-01-01 11:00:00", pcmk_rc_within_range); // Now is before end, next change is after end assert_date_expression(xml, "2024-02-01 14:59:59", "2024-02-01 18:00:00", "2024-02-01 15:00:01", pcmk_rc_within_range); // Now is equal to end, next change is after end assert_date_expression(xml, "2024-02-01 15:00:00", "2024-02-01 18:00:00", "2024-02-01 15:00:01", pcmk_rc_within_range); // Now and next change are after end assert_date_expression(xml, "2024-02-01 15:00:01", "2024-04-04 04:04:04", "2024-04-04 04:04:04", pcmk_rc_after_range); pcmk__xml_free(xml); } #define EXPR_RANGE_VALID_START_INVALID_END \ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \ PCMK_XA_START "='2024-02-01 12:00:00' " \ PCMK_XA_END "='not-a-date' />" static void range_valid_start_invalid_end(void **state) { - // Currently treated same as start without end xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_INVALID_END); - // Now and next change are before start - assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00", - "2024-01-01 11:00:00", pcmk_rc_before_range); - - // Now is before start, next change is after start - assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 18:00:00", - "2024-02-01 12:00:00", pcmk_rc_before_range); - - // Now is equal to start, next change is after start - assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 18:00:00", - "2024-02-01 18:00:00", pcmk_rc_within_range); - - // Now and next change are after start - assert_date_expression(xml, "2024-03-01 05:03:11", "2024-04-04 04:04:04", - "2024-04-04 04:04:04", pcmk_rc_within_range); - + assert_date_expression(xml, "2024-01-01 04:30:05", NULL, NULL, + pcmk_rc_unpack_error); pcmk__xml_free(xml); } #define EXPR_RANGE_INVALID_START_VALID_END \ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \ PCMK_XA_START "='not-a-date' " \ PCMK_XA_END "='2024-02-01 15:00:00' />" static void range_invalid_start_valid_end(void **state) { - // Currently treated same as end without start xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_INVALID_START_VALID_END); - // Now and next change are before end - assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00", - "2024-01-01 11:00:00", pcmk_rc_within_range); - - // Now is before end, next change is after end - assert_date_expression(xml, "2024-02-01 14:59:59", "2024-02-01 18:00:00", - "2024-02-01 15:00:01", pcmk_rc_within_range); - - // Now is equal to end, next change is after end - assert_date_expression(xml, "2024-02-01 15:00:00", "2024-02-01 18:00:00", - "2024-02-01 15:00:01", pcmk_rc_within_range); - - // Now and next change are after end - assert_date_expression(xml, "2024-02-01 15:00:01", "2024-04-04 04:04:04", - "2024-04-04 04:04:04", pcmk_rc_after_range); - + assert_date_expression(xml, "2024-01-01 04:30:05", NULL, NULL, + pcmk_rc_unpack_error); pcmk__xml_free(xml); } #define EXPR_RANGE_VALID_START_VALID_END \ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \ PCMK_XA_START "='2024-02-01 12:00:00' " \ PCMK_XA_END "='2024-02-01 15:00:00' />" static void range_valid_start_valid_end(void **state) { xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_VALID_END); // Now and next change are before start assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00", "2024-01-01 11:00:00", pcmk_rc_before_range); // Now is before start, next change is between start and end assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 14:00:00", "2024-02-01 12:00:00", pcmk_rc_before_range); // Now is equal to start, next change is between start and end assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 14:30:00", "2024-02-01 14:30:00", pcmk_rc_within_range); // Now is between start and end, next change is after end assert_date_expression(xml, "2024-02-01 14:03:11", "2024-04-04 04:04:04", "2024-02-01 15:00:01", pcmk_rc_within_range); // Now is equal to end, next change is after end assert_date_expression(xml, "2024-02-01 15:00:00", "2028-04-04 04:04:04", "2024-02-01 15:00:01", pcmk_rc_within_range); // Now and next change are after end assert_date_expression(xml, "2024-02-01 15:00:01", "2028-04-04 04:04:04", "2028-04-04 04:04:04", pcmk_rc_after_range); pcmk__xml_free(xml); } #define EXPR_RANGE_VALID_START_INVALID_DURATION \ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \ PCMK_XA_START "='2024-02-01 12:00:00'>" \ "<" PCMK_XE_DURATION " " PCMK_XA_ID "='d' " \ PCMK_XA_HOURS "='not-a-number' />" \ "" static void range_valid_start_invalid_duration(void **state) { - // Currently treated same as end equals start xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_INVALID_DURATION); - // Now and next change are before start - assert_date_expression(xml, "2024-02-01 04:30:05", "2024-01-01 11:00:00", - "2024-01-01 11:00:00", pcmk_rc_before_range); - - // Now is before start, next change is after start - assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 18:00:00", - "2024-02-01 12:00:00", pcmk_rc_before_range); - - // Now is equal to start, next change is after start - assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 14:30:00", - "2024-02-01 12:00:01", pcmk_rc_within_range); - - // Now and next change are after start - assert_date_expression(xml, "2024-02-01 12:00:01", "2024-02-01 14:30:00", - "2024-02-01 14:30:00", pcmk_rc_after_range); - + assert_date_expression(xml, "2024-02-01 04:30:05", NULL, NULL, + pcmk_rc_unpack_error); pcmk__xml_free(xml); } #define EXPR_RANGE_VALID_START_VALID_DURATION \ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \ PCMK_XA_START "='2024-02-01 12:00:00'>" \ "<" PCMK_XE_DURATION " " PCMK_XA_ID "='d' " \ PCMK_XA_HOURS "='3' />" \ "" static void range_valid_start_valid_duration(void **state) { xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_VALID_DURATION); // Now and next change are before start assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00", "2024-01-01 11:00:00", pcmk_rc_before_range); // Now is before start, next change is between start and end assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 14:00:00", "2024-02-01 12:00:00", pcmk_rc_before_range); // Now is equal to start, next change is between start and end assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 14:30:00", "2024-02-01 14:30:00", pcmk_rc_within_range); // Now is between start and end, next change is after end assert_date_expression(xml, "2024-02-01 14:03:11", "2024-04-04 04:04:04", "2024-02-01 15:00:01", pcmk_rc_within_range); // Now is equal to end, next change is after end assert_date_expression(xml, "2024-02-01 15:00:00", "2028-04-04 04:04:04", "2024-02-01 15:00:01", pcmk_rc_within_range); // Now and next change are after end assert_date_expression(xml, "2024-02-01 15:00:01", "2028-04-04 04:04:04", "2028-04-04 04:04:04", pcmk_rc_after_range); pcmk__xml_free(xml); } #define EXPR_RANGE_VALID_START_DURATION_MISSING_ID \ - "<" PCMK_XE_DATE_EXPRESSION " " \ + "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='d' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \ PCMK_XA_START "='2024-02-01 12:00:00'>" \ - "<" PCMK_XE_DURATION " " PCMK_XA_ID "='d' " \ - PCMK_XA_HOURS "='3' />" \ + "<" PCMK_XE_DURATION " " PCMK_XA_HOURS "='3' />" \ "" static void range_valid_start_duration_missing_id(void **state) { - // Currently acceptable xmlNodePtr xml = NULL; xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_DURATION_MISSING_ID); - // Now and next change are before start - assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00", - "2024-01-01 11:00:00", pcmk_rc_before_range); - - // Now is before start, next change is between start and end - assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 14:00:00", - "2024-02-01 12:00:00", pcmk_rc_before_range); - - // Now is equal to start, next change is between start and end - assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 14:30:00", - "2024-02-01 14:30:00", pcmk_rc_within_range); - - // Now is between start and end, next change is after end - assert_date_expression(xml, "2024-02-01 14:03:11", "2024-04-04 04:04:04", - "2024-02-01 15:00:01", pcmk_rc_within_range); - - // Now is equal to end, next change is after end - assert_date_expression(xml, "2024-02-01 15:00:00", "2028-04-04 04:04:04", - "2024-02-01 15:00:01", pcmk_rc_within_range); - - // Now and next change are after end - assert_date_expression(xml, "2024-02-01 15:00:01", "2028-04-04 04:04:04", - "2028-04-04 04:04:04", pcmk_rc_after_range); - + assert_date_expression(xml, "2024-02-01 04:30:05", NULL, NULL, + pcmk_rc_unpack_error); pcmk__xml_free(xml); } #define EXPR_SPEC_MISSING \ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_DATE_SPEC "' />" static void spec_missing(void **state) { xmlNodePtr xml = pcmk__xml_parse(EXPR_SPEC_MISSING); - assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined); + assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_unpack_error); pcmk__xml_free(xml); } #define EXPR_SPEC_INVALID \ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_DATE_SPEC "'>" \ "<" PCMK_XE_DATE_SPEC " " PCMK_XA_ID "='s' " \ PCMK_XA_MONTHS "='not-a-number'/>" \ "" static void spec_invalid(void **state) { - // Currently treated as date_spec with no ranges (which passes) xmlNodePtr xml = pcmk__xml_parse(EXPR_SPEC_INVALID); - assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_ok); + assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_unpack_error); pcmk__xml_free(xml); } #define EXPR_SPEC_VALID \ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_DATE_SPEC "'>" \ "<" PCMK_XE_DATE_SPEC " " PCMK_XA_ID "='s' " \ PCMK_XA_MONTHS "='2'/>" \ "" static void spec_valid(void **state) { // date_spec does not currently support next_change xmlNodePtr xml = pcmk__xml_parse(EXPR_SPEC_VALID); // Now is just before spec start assert_date_expression(xml, "2024-01-01 23:59:59", NULL, NULL, pcmk_rc_before_range); // Now matches spec start assert_date_expression(xml, "2024-02-01 00:00:00", NULL, NULL, pcmk_rc_ok); // Now is within spec range assert_date_expression(xml, "2024-02-22 22:22:22", NULL, NULL, pcmk_rc_ok); // Now matches spec end assert_date_expression(xml, "2024-02-29 23:59:59", NULL, NULL, pcmk_rc_ok); // Now is just past spec end assert_date_expression(xml, "2024-03-01 00:00:00", NULL, NULL, pcmk_rc_after_range); pcmk__xml_free(xml); } #define EXPR_SPEC_MISSING_ID \ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_DATE_SPEC "'>" \ "<" PCMK_XE_DATE_SPEC " " \ PCMK_XA_MONTHS "='2'/>" \ "" static void spec_missing_id(void **state) { - // Currently acceptable; date_spec does not currently support next_change xmlNodePtr xml = pcmk__xml_parse(EXPR_SPEC_MISSING_ID); - // Now is just before spec start assert_date_expression(xml, "2024-01-01 23:59:59", NULL, NULL, - pcmk_rc_before_range); - - // Now matches spec start - assert_date_expression(xml, "2024-02-01 00:00:00", NULL, NULL, pcmk_rc_ok); - - // Now is within spec range - assert_date_expression(xml, "2024-02-22 22:22:22", NULL, NULL, pcmk_rc_ok); - - // Now matches spec end - assert_date_expression(xml, "2024-02-29 23:59:59", NULL, NULL, pcmk_rc_ok); - - // Now is just past spec end - assert_date_expression(xml, "2024-03-01 00:00:00", NULL, NULL, - pcmk_rc_after_range); - + pcmk_rc_unpack_error); pcmk__xml_free(xml); } PCMK__UNIT_TEST(pcmk__xml_test_setup_group, pcmk__xml_test_teardown_group, cmocka_unit_test(null_invalid), cmocka_unit_test(null_next_change_ok), cmocka_unit_test(id_missing), cmocka_unit_test(op_invalid), cmocka_unit_test(lt_missing_end), cmocka_unit_test(lt_invalid_end), cmocka_unit_test(lt_valid), cmocka_unit_test(gt_missing_start), cmocka_unit_test(gt_invalid_start), cmocka_unit_test(gt_valid), cmocka_unit_test(range_missing), cmocka_unit_test(range_invalid_start_invalid_end), cmocka_unit_test(range_invalid_start_only), cmocka_unit_test(range_valid_start_only), cmocka_unit_test(range_invalid_end_only), cmocka_unit_test(range_valid_end_only), cmocka_unit_test(range_valid_start_invalid_end), cmocka_unit_test(range_invalid_start_valid_end), cmocka_unit_test(range_valid_start_valid_end), cmocka_unit_test(range_valid_start_invalid_duration), cmocka_unit_test(range_valid_start_valid_duration), cmocka_unit_test(range_valid_start_duration_missing_id), cmocka_unit_test(spec_missing), cmocka_unit_test(spec_invalid), cmocka_unit_test(spec_valid), cmocka_unit_test(spec_missing_id)) diff --git a/lib/common/tests/rules/pcmk__evaluate_date_spec_test.c b/lib/common/tests/rules/pcmk__evaluate_date_spec_test.c index 5b85eaac43..351d9db67f 100644 --- a/lib/common/tests/rules/pcmk__evaluate_date_spec_test.c +++ b/lib/common/tests/rules/pcmk__evaluate_date_spec_test.c @@ -1,231 +1,230 @@ /* * Copyright 2020-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 "crmcommon_private.h" static void run_one_test(const char *t, const char *x, int expected) { crm_time_t *tm = crm_time_new(t); xmlNodePtr xml = pcmk__xml_parse(x); assert_int_equal(pcmk__evaluate_date_spec(xml, tm), expected); crm_time_free(tm); pcmk__xml_free(xml); } static void null_invalid(void **state) { xmlNodePtr xml = pcmk__xml_parse("<" PCMK_XE_DATE_SPEC " " PCMK_XA_ID "='spec' " PCMK_XA_YEARS "='2019'/>"); crm_time_t *tm = crm_time_new(NULL); assert_int_equal(pcmk__evaluate_date_spec(NULL, NULL), EINVAL); assert_int_equal(pcmk__evaluate_date_spec(xml, NULL), EINVAL); assert_int_equal(pcmk__evaluate_date_spec(NULL, tm), EINVAL); crm_time_free(tm); pcmk__xml_free(xml); } static void spec_id_missing(void **state) { - // Currently acceptable - run_one_test("2020-01-01", "", pcmk_rc_ok); + run_one_test("2020-01-01", "", + pcmk_rc_unpack_error); } static void invalid_range(void **state) { - // Currently acceptable run_one_test("2020-01-01", "", - pcmk_rc_ok); + pcmk_rc_unpack_error); } static void time_satisfies_year_spec(void **state) { run_one_test("2020-01-01", "", pcmk_rc_ok); } static void time_after_year_spec(void **state) { run_one_test("2020-01-01", "<" PCMK_XE_DATE_SPEC " " PCMK_XA_ID "='spec' " PCMK_XA_YEARS "='2019'/>", pcmk_rc_after_range); } static void time_satisfies_year_range(void **state) { run_one_test("2020-01-01", "<" PCMK_XE_DATE_SPEC " " PCMK_XA_ID "='spec' " PCMK_XA_YEARS "='2010-2030'/>", pcmk_rc_ok); } static void time_before_year_range(void **state) { run_one_test("2000-01-01", "<" PCMK_XE_DATE_SPEC " " PCMK_XA_ID "='spec' " PCMK_XA_YEARS "='2010-2030'/>", pcmk_rc_before_range); } static void time_after_year_range(void **state) { run_one_test("2020-01-01", "<" PCMK_XE_DATE_SPEC " " PCMK_XA_ID "='spec' " PCMK_XA_YEARS "='2010-2015'/>", pcmk_rc_after_range); } static void range_without_start_year_passes(void **state) { run_one_test("2010-01-01", "<" PCMK_XE_DATE_SPEC " " PCMK_XA_ID "='spec' " PCMK_XA_YEARS "='-2020'/>", pcmk_rc_ok); } static void range_without_end_year_passes(void **state) { run_one_test("2010-01-01", "<" PCMK_XE_DATE_SPEC " " PCMK_XA_ID "='spec' " PCMK_XA_YEARS "='2000-'/>", pcmk_rc_ok); run_one_test("2000-10-01", "<" PCMK_XE_DATE_SPEC " " PCMK_XA_ID "='spec' " PCMK_XA_YEARS "='2000-'/>", pcmk_rc_ok); } static void yeardays_satisfies(void **state) { run_one_test("2020-01-30", "<" PCMK_XE_DATE_SPEC " " PCMK_XA_ID "='spec' " PCMK_XA_YEARDAYS "='30'/>", pcmk_rc_ok); } static void time_after_yeardays_spec(void **state) { run_one_test("2020-02-15", "<" PCMK_XE_DATE_SPEC " " PCMK_XA_ID "='spec' " PCMK_XA_YEARDAYS "='40'/>", pcmk_rc_after_range); } static void yeardays_feb_29_satisfies(void **state) { run_one_test("2016-02-29", "<" PCMK_XE_DATE_SPEC " " PCMK_XA_ID "='spec' " PCMK_XA_YEARDAYS "='60'/>", pcmk_rc_ok); } static void exact_ymd_satisfies(void **state) { run_one_test("2001-12-31", "<" PCMK_XE_DATE_SPEC " " PCMK_XA_ID "='spec' " PCMK_XA_YEARS "='2001' " PCMK_XA_MONTHS "='12' " PCMK_XA_MONTHDAYS "='31'/>", pcmk_rc_ok); } static void range_in_month_satisfies(void **state) { run_one_test("2001-06-10", "<" PCMK_XE_DATE_SPEC " " PCMK_XA_ID "='spec' " PCMK_XA_YEARS "='2001' " PCMK_XA_MONTHS "='6' " PCMK_XA_MONTHDAYS "='1-10'/>", pcmk_rc_ok); } static void exact_ymd_after_range(void **state) { run_one_test("2001-12-31", "<" PCMK_XE_DATE_SPEC " " PCMK_XA_ID "='spec' " PCMK_XA_YEARS "='2001' " PCMK_XA_MONTHS "='12' " PCMK_XA_MONTHDAYS "='30'/>", pcmk_rc_after_range); } static void time_after_monthdays_range(void **state) { run_one_test("2001-06-10", "<" PCMK_XE_DATE_SPEC " " PCMK_XA_ID "='spec' " PCMK_XA_YEARS "='2001' " PCMK_XA_MONTHS "='6' " PCMK_XA_MONTHDAYS "='11-15'/>", pcmk_rc_before_range); } PCMK__UNIT_TEST(pcmk__xml_test_setup_group, pcmk__xml_test_teardown_group, cmocka_unit_test(null_invalid), cmocka_unit_test(spec_id_missing), cmocka_unit_test(invalid_range), cmocka_unit_test(time_satisfies_year_spec), cmocka_unit_test(time_after_year_spec), cmocka_unit_test(time_satisfies_year_range), cmocka_unit_test(time_before_year_range), cmocka_unit_test(time_after_year_range), cmocka_unit_test(range_without_start_year_passes), cmocka_unit_test(range_without_end_year_passes), cmocka_unit_test(yeardays_satisfies), cmocka_unit_test(time_after_yeardays_spec), cmocka_unit_test(yeardays_feb_29_satisfies), cmocka_unit_test(exact_ymd_satisfies), cmocka_unit_test(range_in_month_satisfies), cmocka_unit_test(exact_ymd_after_range), cmocka_unit_test(time_after_monthdays_range)) diff --git a/lib/common/tests/rules/pcmk__evaluate_op_expression_test.c b/lib/common/tests/rules/pcmk__evaluate_op_expression_test.c index c6608ca8b3..f9906c9152 100644 --- a/lib/common/tests/rules/pcmk__evaluate_op_expression_test.c +++ b/lib/common/tests/rules/pcmk__evaluate_op_expression_test.c @@ -1,207 +1,206 @@ /* * Copyright 2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include "crmcommon_private.h" /* * Shared data */ static pcmk_rule_input_t rule_input = { // These are the only members used to evaluate operation expressions .op_name = PCMK_ACTION_MONITOR, .op_interval_ms = 10000, }; /*! * \internal * \brief Run one test, comparing return value * * \param[in] xml_string Operation expression XML as string * \param[in] reference_rc Assert that evaluation result equals this */ static void assert_op_expression(const char *xml_string, int reference_rc) { xmlNode *xml = pcmk__xml_parse(xml_string); assert_int_equal(pcmk__evaluate_op_expression(xml, &rule_input), reference_rc); pcmk__xml_free(xml); } /* * Invalid arguments */ #define EXPR_FAIL_BOTH \ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_NAME "='" PCMK_ACTION_START "' " \ PCMK_XA_INTERVAL "='0' />" static void null_invalid(void **state) { xmlNode *xml = NULL; assert_int_equal(pcmk__evaluate_op_expression(NULL, NULL), EINVAL); xml = pcmk__xml_parse(EXPR_FAIL_BOTH); assert_int_equal(pcmk__evaluate_op_expression(xml, NULL), EINVAL); pcmk__xml_free(xml); assert_op_expression(NULL, EINVAL); } /* * Test PCMK_XA_ID */ #define EXPR_ID_MISSING \ "<" PCMK_XE_OP_EXPRESSION " " \ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ PCMK_XA_INTERVAL "='10s' />" #define EXPR_ID_EMPTY \ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='' " \ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ PCMK_XA_INTERVAL "='10s' />" static void id_missing(void **state) { - // Currently acceptable - assert_op_expression(EXPR_ID_MISSING, pcmk_rc_ok); - assert_op_expression(EXPR_ID_EMPTY, pcmk_rc_ok); + assert_op_expression(EXPR_ID_MISSING, pcmk_rc_unpack_error); + assert_op_expression(EXPR_ID_EMPTY, pcmk_rc_unpack_error); } /* * Test PCMK_XA_NAME */ #define EXPR_NAME_MISSING \ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_INTERVAL "='10s' />" static void name_missing(void **state) { assert_op_expression(EXPR_NAME_MISSING, pcmk_rc_unpack_error); } #define EXPR_MATCH_BOTH \ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ PCMK_XA_INTERVAL "='10s' />" #define EXPR_EMPTY_NAME \ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_NAME "='' " PCMK_XA_INTERVAL "='10s' />" static void input_name_missing(void **state) { rule_input.op_name = NULL; assert_op_expression(EXPR_MATCH_BOTH, pcmk_rc_op_unsatisfied); assert_op_expression(EXPR_EMPTY_NAME, pcmk_rc_op_unsatisfied); rule_input.op_name = PCMK_ACTION_MONITOR; } #define EXPR_FAIL_NAME \ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_NAME "='" PCMK_ACTION_START "' " \ PCMK_XA_INTERVAL "='10s' />" static void fail_name(void **state) { assert_op_expression(EXPR_FAIL_NAME, pcmk_rc_op_unsatisfied); // An empty name is meaningless but accepted, so not an unpack error assert_op_expression(EXPR_EMPTY_NAME, pcmk_rc_op_unsatisfied); } /* * Test PCMK_XA_INTERVAL */ #define EXPR_EMPTY_INTERVAL \ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ PCMK_XA_INTERVAL "='' />" #define EXPR_INVALID_INTERVAL \ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ PCMK_XA_INTERVAL "='not-an-interval' />" static void invalid_interval(void **state) { assert_op_expression(EXPR_EMPTY_INTERVAL, pcmk_rc_unpack_error); assert_op_expression(EXPR_INVALID_INTERVAL, pcmk_rc_unpack_error); } #define EXPR_DEFAULT_INTERVAL \ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' />" static void default_interval(void **state) { assert_op_expression(EXPR_DEFAULT_INTERVAL, pcmk_rc_ok); } #define EXPR_FAIL_INTERVAL \ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ PCMK_XA_INTERVAL "='9s' />" static void fail_interval(void **state) { assert_op_expression(EXPR_FAIL_INTERVAL, pcmk_rc_op_unsatisfied); } static void match_both(void **state) { assert_op_expression(EXPR_MATCH_BOTH, pcmk_rc_ok); } static void fail_both(void **state) { assert_op_expression(EXPR_FAIL_BOTH, pcmk_rc_op_unsatisfied); } PCMK__UNIT_TEST(pcmk__xml_test_setup_group, pcmk__xml_test_teardown_group, cmocka_unit_test(null_invalid), cmocka_unit_test(id_missing), cmocka_unit_test(name_missing), cmocka_unit_test(input_name_missing), cmocka_unit_test(fail_name), cmocka_unit_test(invalid_interval), cmocka_unit_test(default_interval), cmocka_unit_test(fail_interval), cmocka_unit_test(match_both), cmocka_unit_test(fail_both)) diff --git a/lib/common/tests/rules/pcmk__evaluate_rsc_expression_test.c b/lib/common/tests/rules/pcmk__evaluate_rsc_expression_test.c index fe80dfefdd..74c1f429c2 100644 --- a/lib/common/tests/rules/pcmk__evaluate_rsc_expression_test.c +++ b/lib/common/tests/rules/pcmk__evaluate_rsc_expression_test.c @@ -1,227 +1,226 @@ /* * Copyright 2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include "crmcommon_private.h" /* * Shared data */ static pcmk_rule_input_t rule_input = { // These are the only members used to evaluate resource expressions .rsc_standard = PCMK_RESOURCE_CLASS_OCF, .rsc_provider = "heartbeat", .rsc_agent = "IPaddr2", }; /*! * \internal * \brief Run one test, comparing return value * * \param[in] xml_string Resource expression XML as string * \param[in] reference_rc Assert that evaluation result equals this */ static void assert_rsc_expression(const char *xml_string, int reference_rc) { xmlNode *xml = pcmk__xml_parse(xml_string); assert_int_equal(pcmk__evaluate_rsc_expression(xml, &rule_input), reference_rc); pcmk__xml_free(xml); } /* * Invalid arguments */ #define EXPR_ALL_MATCH \ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \ PCMK_XA_PROVIDER "='heartbeat' " \ PCMK_XA_TYPE "='IPaddr2' />" static void null_invalid(void **state) { xmlNode *xml = NULL; assert_int_equal(pcmk__evaluate_rsc_expression(NULL, NULL), EINVAL); xml = pcmk__xml_parse(EXPR_ALL_MATCH); assert_int_equal(pcmk__evaluate_rsc_expression(xml, NULL), EINVAL); pcmk__xml_free(xml); assert_rsc_expression(NULL, EINVAL); } /* * Test PCMK_XA_ID */ #define EXPR_ID_MISSING \ "<" PCMK_XE_RSC_EXPRESSION " " \ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \ PCMK_XA_PROVIDER "='heartbeat' " \ PCMK_XA_TYPE "='IPaddr2' />" #define EXPR_ID_EMPTY \ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='' " \ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \ PCMK_XA_PROVIDER "='heartbeat' " \ PCMK_XA_TYPE "='IPaddr2' />" static void id_missing(void **state) { - // Currently acceptable - assert_rsc_expression(EXPR_ID_MISSING, pcmk_rc_ok); - assert_rsc_expression(EXPR_ID_EMPTY, pcmk_rc_ok); + assert_rsc_expression(EXPR_ID_MISSING, pcmk_rc_unpack_error); + assert_rsc_expression(EXPR_ID_EMPTY, pcmk_rc_unpack_error); } /* * Test standard, provider, and agent */ #define EXPR_FAIL_STANDARD \ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_LSB "' />" #define EXPR_EMPTY_STANDARD \ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_CLASS "='' />" static void fail_standard(void **state) { assert_rsc_expression(EXPR_FAIL_STANDARD, pcmk_rc_op_unsatisfied); assert_rsc_expression(EXPR_EMPTY_STANDARD, pcmk_rc_op_unsatisfied); rule_input.rsc_standard = NULL; assert_rsc_expression(EXPR_FAIL_STANDARD, pcmk_rc_op_unsatisfied); assert_rsc_expression(EXPR_EMPTY_STANDARD, pcmk_rc_op_unsatisfied); rule_input.rsc_standard = PCMK_RESOURCE_CLASS_OCF; } #define EXPR_FAIL_PROVIDER \ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \ PCMK_XA_PROVIDER "='pacemaker' " \ PCMK_XA_TYPE "='IPaddr2' />" #define EXPR_EMPTY_PROVIDER \ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \ PCMK_XA_PROVIDER "='' " PCMK_XA_TYPE "='IPaddr2' />" static void fail_provider(void **state) { assert_rsc_expression(EXPR_FAIL_PROVIDER, pcmk_rc_op_unsatisfied); assert_rsc_expression(EXPR_EMPTY_PROVIDER, pcmk_rc_op_unsatisfied); rule_input.rsc_provider = NULL; assert_rsc_expression(EXPR_FAIL_PROVIDER, pcmk_rc_op_unsatisfied); assert_rsc_expression(EXPR_EMPTY_PROVIDER, pcmk_rc_op_unsatisfied); rule_input.rsc_provider = "heartbeat"; } #define EXPR_FAIL_AGENT \ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \ PCMK_XA_PROVIDER "='heartbeat' " \ PCMK_XA_TYPE "='IPaddr3' />" #define EXPR_EMPTY_AGENT \ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \ PCMK_XA_PROVIDER "='heartbeat' " PCMK_XA_TYPE "='' />" static void fail_agent(void **state) { assert_rsc_expression(EXPR_FAIL_AGENT, pcmk_rc_op_unsatisfied); assert_rsc_expression(EXPR_EMPTY_AGENT, pcmk_rc_op_unsatisfied); rule_input.rsc_agent = NULL; assert_rsc_expression(EXPR_FAIL_AGENT, pcmk_rc_op_unsatisfied); assert_rsc_expression(EXPR_EMPTY_AGENT, pcmk_rc_op_unsatisfied); rule_input.rsc_agent = "IPaddr2"; } #define EXPR_NO_STANDARD_MATCHES \ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_PROVIDER "='heartbeat' " \ PCMK_XA_TYPE "='IPaddr2' />" static void no_standard_matches(void **state) { assert_rsc_expression(EXPR_NO_STANDARD_MATCHES, pcmk_rc_ok); } #define EXPR_NO_PROVIDER_MATCHES \ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \ PCMK_XA_TYPE "='IPaddr2' />" static void no_provider_matches(void **state) { assert_rsc_expression(EXPR_NO_PROVIDER_MATCHES, pcmk_rc_ok); } #define EXPR_NO_AGENT_MATCHES \ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \ PCMK_XA_PROVIDER "='heartbeat' />" static void no_agent_matches(void **state) { assert_rsc_expression(EXPR_NO_AGENT_MATCHES, pcmk_rc_ok); } #define EXPR_NO_CRITERIA_MATCHES \ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' />" static void no_criteria_matches(void **state) { assert_rsc_expression(EXPR_NO_CRITERIA_MATCHES, pcmk_rc_ok); } static void all_match(void **state) { assert_rsc_expression(EXPR_ALL_MATCH, pcmk_rc_ok); } PCMK__UNIT_TEST(pcmk__xml_test_setup_group, pcmk__xml_test_teardown_group, cmocka_unit_test(null_invalid), cmocka_unit_test(id_missing), cmocka_unit_test(fail_standard), cmocka_unit_test(fail_provider), cmocka_unit_test(fail_agent), cmocka_unit_test(no_standard_matches), cmocka_unit_test(no_provider_matches), cmocka_unit_test(no_agent_matches), cmocka_unit_test(no_criteria_matches), cmocka_unit_test(all_match)) diff --git a/lib/common/tests/rules/pcmk__unpack_duration_test.c b/lib/common/tests/rules/pcmk__unpack_duration_test.c index 01be5846b1..2eba68e24e 100644 --- a/lib/common/tests/rules/pcmk__unpack_duration_test.c +++ b/lib/common/tests/rules/pcmk__unpack_duration_test.c @@ -1,120 +1,115 @@ /* * Copyright 2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include "../../crmcommon_private.h" #define MONTHS_TO_SECONDS "months=\"2\" weeks=\"3\" days=\"-1\" " \ "hours=\"1\" minutes=\"1\" seconds=\"1\" />" #define ALL_VALID " #include #include #include #include #include /* * Shared data */ static pcmk_rule_input_t rule_input = { .rsc_standard = PCMK_RESOURCE_CLASS_OCF, .rsc_provider = "heartbeat", .rsc_agent = "IPaddr2", .op_name = PCMK_ACTION_MONITOR, .op_interval_ms = 10000, }; /* * Test invalid arguments */ #define RULE_OP \ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' > " \ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ PCMK_XA_INTERVAL "='10s' />" \ "" static void null_invalid(void **state) { xmlNode *xml = NULL; crm_time_t *next_change = crm_time_new_undefined(); assert_int_equal(pcmk_evaluate_rule(NULL, NULL, next_change), EINVAL); xml = pcmk__xml_parse(RULE_OP); assert_int_equal(pcmk_evaluate_rule(xml, NULL, next_change), EINVAL); pcmk__xml_free(xml); assert_int_equal(pcmk_evaluate_rule(NULL, &rule_input, next_change), EINVAL); crm_time_free(next_change); } #define RULE_OP_MISSING_ID \ "<" PCMK_XE_RULE "> " \ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ PCMK_XA_INTERVAL "='10s' />" \ "" static void id_missing(void **state) { - // Currently acceptable xmlNode *xml = pcmk__xml_parse(RULE_OP_MISSING_ID); crm_time_t *next_change = crm_time_new_undefined(); assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, next_change), - pcmk_rc_ok); + pcmk_rc_unpack_error); crm_time_free(next_change); pcmk__xml_free(xml); } #define RULE_IDREF_PARENT "<" PCMK_XE_CIB ">" RULE_OP "" static void good_idref(void **state) { xmlNode *parent_xml = pcmk__xml_parse(RULE_IDREF_PARENT); xmlNode *rule_xml = pcmk__xe_create(parent_xml, PCMK_XE_RULE); crm_time_t *next_change = crm_time_new_undefined(); crm_xml_add(rule_xml, PCMK_XA_ID_REF, "r"); assert_int_equal(pcmk_evaluate_rule(rule_xml, &rule_input, next_change), pcmk_rc_ok); crm_time_free(next_change); pcmk__xml_free(parent_xml); } static void bad_idref(void **state) { xmlNode *parent_xml = pcmk__xml_parse(RULE_IDREF_PARENT); xmlNode *rule_xml = pcmk__xe_create(parent_xml, PCMK_XE_RULE); crm_time_t *next_change = crm_time_new_undefined(); crm_xml_add(rule_xml, PCMK_XA_ID_REF, "x"); assert_int_equal(pcmk_evaluate_rule(rule_xml, &rule_input, next_change), pcmk_rc_unpack_error); crm_time_free(next_change); pcmk__xml_free(parent_xml); } #define RULE_EMPTY "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' />" static void empty_default(void **state) { // Currently acceptable xmlNode *xml = pcmk__xml_parse(RULE_EMPTY); assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok); pcmk__xml_free(xml); } #define RULE_EMPTY_AND \ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' />" static void empty_and(void **state) { // Currently acceptable xmlNode *xml = pcmk__xml_parse(RULE_EMPTY_AND); assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok); pcmk__xml_free(xml); } #define RULE_EMPTY_OR \ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' />" static void empty_or(void **state) { - // Currently treated as unsatisfied xmlNode *xml = pcmk__xml_parse(RULE_EMPTY_OR); assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), - pcmk_rc_op_unsatisfied); + pcmk_rc_ok); pcmk__xml_free(xml); } #define RULE_DEFAULT_BOOLEAN_OP \ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' >" \ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \ PCMK_XA_TYPE "='Dummy' />" \ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ PCMK_XA_INTERVAL "='10s' />" \ "" static void default_boolean_op(void **state) { // Defaults to PCMK_VALUE_AND xmlNode *xml = pcmk__xml_parse(RULE_DEFAULT_BOOLEAN_OP); assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_op_unsatisfied); pcmk__xml_free(xml); } #define RULE_INVALID_BOOLEAN_OP \ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \ PCMK_XA_BOOLEAN_OP "='not-an-op' >" \ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \ PCMK_XA_TYPE "='Dummy' />" \ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ PCMK_XA_INTERVAL "='10s' />" \ "" static void invalid_boolean_op(void **state) { - // Currently defaults to PCMK_VALUE_AND xmlNode *xml = pcmk__xml_parse(RULE_INVALID_BOOLEAN_OP); assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), - pcmk_rc_op_unsatisfied); + pcmk_rc_unpack_error); pcmk__xml_free(xml); } #define RULE_AND_PASSES \ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' >" \ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \ PCMK_XA_TYPE "='IPaddr2' />" \ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ PCMK_XA_INTERVAL "='10s' />" \ "" static void and_passes(void **state) { xmlNode *xml = pcmk__xml_parse(RULE_AND_PASSES); assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok); pcmk__xml_free(xml); } #define RULE_LONELY_AND \ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' >" \ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \ PCMK_XA_TYPE "='IPaddr2' />" \ "" static void lonely_and_passes(void **state) { xmlNode *xml = pcmk__xml_parse(RULE_LONELY_AND); assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok); pcmk__xml_free(xml); } #define RULE_AND_ONE_FAILS \ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' >" \ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \ PCMK_XA_TYPE "='Dummy' />" \ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ PCMK_XA_INTERVAL "='10s' />" \ "" static void and_one_fails(void **state) { xmlNode *xml = pcmk__xml_parse(RULE_AND_ONE_FAILS); assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_op_unsatisfied); pcmk__xml_free(xml); } #define RULE_AND_TWO_FAIL \ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' >" \ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \ PCMK_XA_TYPE "='Dummy' />" \ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ PCMK_XA_INTERVAL "='9s' />" \ "" static void and_two_fail(void **state) { xmlNode *xml = pcmk__xml_parse(RULE_AND_TWO_FAIL); assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_op_unsatisfied); pcmk__xml_free(xml); } #define RULE_OR_ONE_PASSES \ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' >" \ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \ PCMK_XA_TYPE "='Dummy' />" \ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ PCMK_XA_INTERVAL "='10s' />" \ "" static void or_one_passes(void **state) { xmlNode *xml = pcmk__xml_parse(RULE_OR_ONE_PASSES); assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok); pcmk__xml_free(xml); } #define RULE_OR_TWO_PASS \ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' >" \ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \ PCMK_XA_TYPE "='IPAddr2' />" \ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ PCMK_XA_INTERVAL "='10s' />" \ "" static void or_two_pass(void **state) { xmlNode *xml = pcmk__xml_parse(RULE_OR_TWO_PASS); assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok); pcmk__xml_free(xml); } #define RULE_LONELY_OR \ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' >" \ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ PCMK_XA_INTERVAL "='10s' />" \ "" static void lonely_or_passes(void **state) { xmlNode *xml = pcmk__xml_parse(RULE_LONELY_OR); assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok); pcmk__xml_free(xml); } #define RULE_OR_FAILS \ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' >" \ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \ PCMK_XA_TYPE "='Dummy' />" \ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ PCMK_XA_INTERVAL "='20s' />" \ "" static void or_fails(void **state) { xmlNode *xml = pcmk__xml_parse(RULE_OR_FAILS); assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_op_unsatisfied); pcmk__xml_free(xml); } PCMK__UNIT_TEST(pcmk__xml_test_setup_group, pcmk__xml_test_teardown_group, cmocka_unit_test(null_invalid), cmocka_unit_test(id_missing), cmocka_unit_test(good_idref), cmocka_unit_test(bad_idref), cmocka_unit_test(empty_default), cmocka_unit_test(empty_and), cmocka_unit_test(empty_or), cmocka_unit_test(default_boolean_op), cmocka_unit_test(invalid_boolean_op), cmocka_unit_test(and_passes), cmocka_unit_test(lonely_and_passes), cmocka_unit_test(and_one_fails), cmocka_unit_test(and_two_fail), cmocka_unit_test(or_one_passes), cmocka_unit_test(or_two_pass), cmocka_unit_test(lonely_or_passes), cmocka_unit_test(or_fails)) diff --git a/lib/pacemaker/pcmk_rule.c b/lib/pacemaker/pcmk_rule.c index ed8c290c58..4fcb922724 100644 --- a/lib/pacemaker/pcmk_rule.c +++ b/lib/pacemaker/pcmk_rule.c @@ -1,214 +1,215 @@ /* * Copyright 2022-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include "libpacemaker_private.h" #define XPATH_NODE_RULE "//" PCMK_XE_RULE "[@" PCMK_XA_ID "='%s']" /*! * \internal * \brief Check whether a given rule is in effect * * \param[in] scheduler Scheduler data * \param[in] rule_id The ID of the rule to check * \param[out] error Where to store a rule evaluation error message * * \return Standard Pacemaker return code */ static int eval_rule(pcmk_scheduler_t *scheduler, const char *rule_id, const char **error) { xmlNodePtr cib_constraints = NULL; xmlNodePtr match = NULL; xmlXPathObjectPtr xpath_obj = NULL; char *xpath = NULL; int rc = pcmk_rc_ok; int num_results = 0; *error = NULL; /* Rules are under the constraints node in the XML, so first find that. */ cib_constraints = pcmk_find_cib_element(scheduler->input, PCMK_XE_CONSTRAINTS); /* Get all rules matching the given ID that are also simple enough for us * to check. For the moment, these rules must only have a single * date_expression child and: * - Do not have a date_spec operation, or * - Have a date_spec operation that contains years= but does not contain * moon=. * * We do this in steps to provide better error messages. First, check that * there's any rule with the given ID. */ xpath = crm_strdup_printf(XPATH_NODE_RULE, rule_id); xpath_obj = xpath_search(cib_constraints, xpath); num_results = numXpathResults(xpath_obj); free(xpath); freeXpathObject(xpath_obj); if (num_results == 0) { *error = "Rule not found"; return ENXIO; } if (num_results > 1) { // Should not be possible; schema prevents this *error = "Found more than one rule with matching ID"; return pcmk_rc_duplicate_id; } /* Next, make sure it has exactly one date_expression. */ xpath = crm_strdup_printf(XPATH_NODE_RULE "//date_expression", rule_id); xpath_obj = xpath_search(cib_constraints, xpath); num_results = numXpathResults(xpath_obj); free(xpath); freeXpathObject(xpath_obj); if (num_results != 1) { if (num_results == 0) { *error = "Rule does not have a date expression"; } else { *error = "Rule has more than one date expression"; } return EOPNOTSUPP; } /* Then, check that it's something we actually support. */ xpath = crm_strdup_printf(XPATH_NODE_RULE "//" PCMK_XE_DATE_EXPRESSION "[@" PCMK_XA_OPERATION "!='" PCMK_VALUE_DATE_SPEC "']", rule_id); xpath_obj = xpath_search(cib_constraints, xpath); num_results = numXpathResults(xpath_obj); free(xpath); if (num_results == 0) { freeXpathObject(xpath_obj); xpath = crm_strdup_printf(XPATH_NODE_RULE "//" PCMK_XE_DATE_EXPRESSION "[@" PCMK_XA_OPERATION "='" PCMK_VALUE_DATE_SPEC "' " "and " PCMK_XE_DATE_SPEC "/@" PCMK_XA_YEARS " " "and not(" PCMK_XE_DATE_SPEC "/@" PCMK__XA_MOON ")]", rule_id); xpath_obj = xpath_search(cib_constraints, xpath); num_results = numXpathResults(xpath_obj); free(xpath); if (num_results == 0) { freeXpathObject(xpath_obj); *error = "Rule must either not use " PCMK_XE_DATE_SPEC ", or use " PCMK_XE_DATE_SPEC " with " PCMK_XA_YEARS "= but not " PCMK__XA_MOON "="; return EOPNOTSUPP; } } match = getXpathResult(xpath_obj, 0); /* We should have ensured this with the xpath query above, but double- * checking can't hurt. */ CRM_ASSERT(match != NULL); CRM_ASSERT(pcmk__condition_type(match) == pcmk__condition_datetime); rc = pcmk__evaluate_date_expression(match, scheduler->priv->now, NULL); - if (rc == pcmk_rc_undetermined) { // Malformed or missing + if ((rc != pcmk_rc_ok) && (rc != pcmk_rc_within_range)) { + // Malformed or missing *error = "Error parsing rule"; } freeXpathObject(xpath_obj); return rc; } /*! * \internal * \brief Check whether each rule in a list is in effect * * \param[in,out] out Output object * \param[in] input The CIB XML to check (if \c NULL, use current CIB) * \param[in] date Check whether the rule is in effect at this date and * time (if \c NULL, use current date and time) * \param[in] rule_ids The IDs of the rules to check, as a NULL- * terminated list. * * \return Standard Pacemaker return code */ int pcmk__check_rules(pcmk__output_t *out, xmlNodePtr input, const crm_time_t *date, const char **rule_ids) { pcmk_scheduler_t *scheduler = NULL; int rc = pcmk_rc_ok; CRM_ASSERT(out != NULL); if (rule_ids == NULL) { // Trivial case; every rule specified is in effect return pcmk_rc_ok; } rc = pcmk__init_scheduler(out, input, date, &scheduler); if (rc != pcmk_rc_ok) { return rc; } for (const char **rule_id = rule_ids; *rule_id != NULL; rule_id++) { const char *error = NULL; int last_rc = eval_rule(scheduler, *rule_id, &error); out->message(out, "rule-check", *rule_id, last_rc, error); if (last_rc != pcmk_rc_ok) { rc = last_rc; } } pe_free_working_set(scheduler); return rc; } // Documented in pacemaker.h int pcmk_check_rules(xmlNodePtr *xml, xmlNodePtr input, const crm_time_t *date, const char **rule_ids) { pcmk__output_t *out = NULL; int rc = pcmk_rc_ok; rc = pcmk__xml_output_new(&out, xml); if (rc != pcmk_rc_ok) { return rc; } pcmk__register_lib_messages(out); rc = pcmk__check_rules(out, input, date, rule_ids); pcmk__xml_output_finish(out, pcmk_rc2exitc(rc), xml); return rc; } diff --git a/lib/pacemaker/pcmk_sched_colocation.c b/lib/pacemaker/pcmk_sched_colocation.c index 27b5e6fc87..f74c6b71fe 100644 --- a/lib/pacemaker/pcmk_sched_colocation.c +++ b/lib/pacemaker/pcmk_sched_colocation.c @@ -1,2065 +1,2065 @@ /* * 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 "crm/common/util.h" #include "crm/common/xml_internal.h" #include "crm/common/xml.h" #include "libpacemaker_private.h" // Used to temporarily mark a node as unusable #define INFINITY_HACK (PCMK_SCORE_INFINITY * -100) /*! * \internal * \brief Get the value of a colocation's node attribute * * \param[in] node Node on which to look up the attribute * \param[in] attr Name of attribute to look up * \param[in] rsc Resource on whose behalf to look up the attribute * * \return Value of \p attr on \p node or on the host of \p node, as appropriate */ const char * pcmk__colocation_node_attr(const pcmk_node_t *node, const char *attr, const pcmk_resource_t *rsc) { const char *target = NULL; /* A resource colocated with a bundle or its primitive can't run on the * bundle node itself (where only the primitive, if any, can run). Instead, * we treat it as a colocation with the bundle's containers, so always look * up colocation node attributes on the container host. */ if (pcmk__is_bundle_node(node) && pcmk__is_bundled(rsc) && (pe__const_top_resource(rsc, false) == pe__bundled_resource(rsc))) { target = PCMK_VALUE_HOST; } else if (rsc != NULL) { target = g_hash_table_lookup(rsc->priv->meta, PCMK_META_CONTAINER_ATTRIBUTE_TARGET); } return pcmk__node_attr(node, attr, target, pcmk__rsc_node_assigned); } /*! * \internal * \brief Compare two colocations according to priority * * Compare two colocations according to the order in which they should be * considered, based on either their dependent resources or their primary * resources -- preferring (in order): * * Colocation that is not \c NULL * * Colocation whose resource has higher priority * * Colocation whose resource is of a higher-level variant * (bundle > clone > group > primitive) * * Colocation whose resource is promotable, if both are clones * * Colocation whose resource has lower ID in lexicographic order * * \param[in] colocation1 First colocation to compare * \param[in] colocation2 Second colocation to compare * \param[in] dependent If \c true, compare colocations by dependent * priority; otherwise compare them by primary priority * * \return A negative number if \p colocation1 should be considered first, * a positive number if \p colocation2 should be considered first, * or 0 if order doesn't matter */ static gint cmp_colocation_priority(const pcmk__colocation_t *colocation1, const pcmk__colocation_t *colocation2, bool dependent) { const pcmk_resource_t *rsc1 = NULL; const pcmk_resource_t *rsc2 = NULL; if (colocation1 == NULL) { return 1; } if (colocation2 == NULL) { return -1; } if (dependent) { rsc1 = colocation1->dependent; rsc2 = colocation2->dependent; CRM_ASSERT(colocation1->primary != NULL); } else { rsc1 = colocation1->primary; rsc2 = colocation2->primary; CRM_ASSERT(colocation1->dependent != NULL); } CRM_ASSERT((rsc1 != NULL) && (rsc2 != NULL)); if (rsc1->priv->priority > rsc2->priv->priority) { return -1; } if (rsc1->priv->priority < rsc2->priv->priority) { return 1; } // Process clones before primitives and groups if (rsc1->priv->variant > rsc2->priv->variant) { return -1; } if (rsc1->priv->variant < rsc2->priv->variant) { return 1; } /* @COMPAT scheduler <2.0.0: Process promotable clones before nonpromotable * clones (probably unnecessary, but avoids having to update regression * tests) */ if (pcmk__is_clone(rsc1)) { if (pcmk_is_set(rsc1->flags, pcmk__rsc_promotable) && !pcmk_is_set(rsc2->flags, pcmk__rsc_promotable)) { return -1; } if (!pcmk_is_set(rsc1->flags, pcmk__rsc_promotable) && pcmk_is_set(rsc2->flags, pcmk__rsc_promotable)) { return 1; } } return strcmp(rsc1->id, rsc2->id); } /*! * \internal * \brief Compare two colocations according to priority based on dependents * * Compare two colocations according to the order in which they should be * considered, based on their dependent resources -- preferring (in order): * * Colocation that is not \c NULL * * Colocation whose resource has higher priority * * Colocation whose resource is of a higher-level variant * (bundle > clone > group > primitive) * * Colocation whose resource is promotable, if both are clones * * Colocation whose resource has lower ID in lexicographic order * * \param[in] a First colocation to compare * \param[in] b Second colocation to compare * * \return A negative number if \p a should be considered first, * a positive number if \p b should be considered first, * or 0 if order doesn't matter */ static gint cmp_dependent_priority(gconstpointer a, gconstpointer b) { return cmp_colocation_priority(a, b, true); } /*! * \internal * \brief Compare two colocations according to priority based on primaries * * Compare two colocations according to the order in which they should be * considered, based on their primary resources -- preferring (in order): * * Colocation that is not \c NULL * * Colocation whose primary has higher priority * * Colocation whose primary is of a higher-level variant * (bundle > clone > group > primitive) * * Colocation whose primary is promotable, if both are clones * * Colocation whose primary has lower ID in lexicographic order * * \param[in] a First colocation to compare * \param[in] b Second colocation to compare * * \return A negative number if \p a should be considered first, * a positive number if \p b should be considered first, * or 0 if order doesn't matter */ static gint cmp_primary_priority(gconstpointer a, gconstpointer b) { return cmp_colocation_priority(a, b, false); } /*! * \internal * \brief Add a "this with" colocation constraint to a sorted list * * \param[in,out] list List of constraints to add \p colocation to * \param[in] colocation Colocation constraint to add to \p list * \param[in] rsc Resource whose colocations we're getting (for * logging only) * * \note The list will be sorted using cmp_primary_priority(). */ void pcmk__add_this_with(GList **list, const pcmk__colocation_t *colocation, const pcmk_resource_t *rsc) { CRM_ASSERT((list != NULL) && (colocation != NULL) && (rsc != NULL)); pcmk__rsc_trace(rsc, "Adding colocation %s (%s with %s using %s @%s) to " "'this with' list for %s", colocation->id, colocation->dependent->id, colocation->primary->id, colocation->node_attribute, pcmk_readable_score(colocation->score), rsc->id); *list = g_list_insert_sorted(*list, (gpointer) colocation, cmp_primary_priority); } /*! * \internal * \brief Add a list of "this with" colocation constraints to a list * * \param[in,out] list List of constraints to add \p addition to * \param[in] addition List of colocation constraints to add to \p list * \param[in] rsc Resource whose colocations we're getting (for * logging only) * * \note The lists must be pre-sorted by cmp_primary_priority(). */ void pcmk__add_this_with_list(GList **list, GList *addition, const pcmk_resource_t *rsc) { CRM_ASSERT((list != NULL) && (rsc != NULL)); pcmk__if_tracing( {}, // Always add each colocation individually if tracing { if (*list == NULL) { // Trivial case for efficiency if not tracing *list = g_list_copy(addition); return; } } ); for (const GList *iter = addition; iter != NULL; iter = iter->next) { pcmk__add_this_with(list, addition->data, rsc); } } /*! * \internal * \brief Add a "with this" colocation constraint to a sorted list * * \param[in,out] list List of constraints to add \p colocation to * \param[in] colocation Colocation constraint to add to \p list * \param[in] rsc Resource whose colocations we're getting (for * logging only) * * \note The list will be sorted using cmp_dependent_priority(). */ void pcmk__add_with_this(GList **list, const pcmk__colocation_t *colocation, const pcmk_resource_t *rsc) { CRM_ASSERT((list != NULL) && (colocation != NULL) && (rsc != NULL)); pcmk__rsc_trace(rsc, "Adding colocation %s (%s with %s using %s @%s) to " "'with this' list for %s", colocation->id, colocation->dependent->id, colocation->primary->id, colocation->node_attribute, pcmk_readable_score(colocation->score), rsc->id); *list = g_list_insert_sorted(*list, (gpointer) colocation, cmp_dependent_priority); } /*! * \internal * \brief Add a list of "with this" colocation constraints to a list * * \param[in,out] list List of constraints to add \p addition to * \param[in] addition List of colocation constraints to add to \p list * \param[in] rsc Resource whose colocations we're getting (for * logging only) * * \note The lists must be pre-sorted by cmp_dependent_priority(). */ void pcmk__add_with_this_list(GList **list, GList *addition, const pcmk_resource_t *rsc) { CRM_ASSERT((list != NULL) && (rsc != NULL)); pcmk__if_tracing( {}, // Always add each colocation individually if tracing { if (*list == NULL) { // Trivial case for efficiency if not tracing *list = g_list_copy(addition); return; } } ); for (const GList *iter = addition; iter != NULL; iter = iter->next) { pcmk__add_with_this(list, addition->data, rsc); } } /*! * \internal * \brief Add orderings necessary for an anti-colocation constraint * * \param[in,out] first_rsc One resource in an anti-colocation * \param[in] first_role Anti-colocation role of \p first_rsc * \param[in] then_rsc Other resource in the anti-colocation * \param[in] then_role Anti-colocation role of \p then_rsc */ static void anti_colocation_order(pcmk_resource_t *first_rsc, int first_role, pcmk_resource_t *then_rsc, int then_role) { const char *first_tasks[] = { NULL, NULL }; const char *then_tasks[] = { NULL, NULL }; /* Actions to make first_rsc lose first_role */ if (first_role == pcmk_role_promoted) { first_tasks[0] = PCMK_ACTION_DEMOTE; } else { first_tasks[0] = PCMK_ACTION_STOP; if (first_role == pcmk_role_unpromoted) { first_tasks[1] = PCMK_ACTION_PROMOTE; } } /* Actions to make then_rsc gain then_role */ if (then_role == pcmk_role_promoted) { then_tasks[0] = PCMK_ACTION_PROMOTE; } else { then_tasks[0] = PCMK_ACTION_START; if (then_role == pcmk_role_unpromoted) { then_tasks[1] = PCMK_ACTION_DEMOTE; } } for (int first_lpc = 0; (first_lpc <= 1) && (first_tasks[first_lpc] != NULL); first_lpc++) { for (int then_lpc = 0; (then_lpc <= 1) && (then_tasks[then_lpc] != NULL); then_lpc++) { pcmk__order_resource_actions(first_rsc, first_tasks[first_lpc], then_rsc, then_tasks[then_lpc], pcmk__ar_if_required_on_same_node); } } } /*! * \internal * \brief Add a new colocation constraint to scheduler data * * \param[in] id XML ID for this constraint * \param[in] node_attr Colocate by this attribute (NULL for #uname) * \param[in] score Constraint score * \param[in,out] dependent Resource to be colocated * \param[in,out] primary Resource to colocate \p dependent with * \param[in] dependent_role Current role of \p dependent * \param[in] primary_role Current role of \p primary * \param[in] flags Group of enum pcmk__coloc_flags */ void pcmk__new_colocation(const char *id, const char *node_attr, int score, pcmk_resource_t *dependent, pcmk_resource_t *primary, const char *dependent_role, const char *primary_role, uint32_t flags) { pcmk__colocation_t *new_con = NULL; CRM_CHECK(id != NULL, return); if ((dependent == NULL) || (primary == NULL)) { pcmk__config_err("Ignoring colocation '%s' because resource " "does not exist", id); return; } if (score == 0) { pcmk__rsc_trace(dependent, "Ignoring colocation '%s' (%s with %s) because score is 0", id, dependent->id, primary->id); return; } new_con = pcmk__assert_alloc(1, sizeof(pcmk__colocation_t)); if (pcmk__str_eq(dependent_role, PCMK_ROLE_STARTED, pcmk__str_null_matches|pcmk__str_casei)) { dependent_role = PCMK__ROLE_UNKNOWN; } if (pcmk__str_eq(primary_role, PCMK_ROLE_STARTED, pcmk__str_null_matches|pcmk__str_casei)) { primary_role = PCMK__ROLE_UNKNOWN; } new_con->id = id; new_con->dependent = dependent; new_con->primary = primary; new_con->score = score; new_con->dependent_role = pcmk_parse_role(dependent_role); new_con->primary_role = pcmk_parse_role(primary_role); new_con->node_attribute = pcmk__s(node_attr, CRM_ATTR_UNAME); new_con->flags = flags; pcmk__add_this_with(&(dependent->priv->this_with_colocations), new_con, dependent); pcmk__add_with_this(&(primary->priv->with_this_colocations), new_con, primary); dependent->priv->scheduler->priv->colocation_constraints = g_list_prepend(dependent->priv->scheduler->priv->colocation_constraints, new_con); if (score <= -PCMK_SCORE_INFINITY) { anti_colocation_order(dependent, new_con->dependent_role, primary, new_con->primary_role); anti_colocation_order(primary, new_con->primary_role, dependent, new_con->dependent_role); } } /*! * \internal * \brief Return the boolean influence corresponding to configuration * * \param[in] coloc_id Colocation XML ID (for error logging) * \param[in] rsc Resource involved in constraint (for default) * \param[in] influence_s String value of \c PCMK_XA_INFLUENCE option * * \return \c pcmk__coloc_influence if string evaluates true, or string is * \c NULL or invalid and resource's \c PCMK_META_CRITICAL option * evaluates true, otherwise \c pcmk__coloc_none */ static uint32_t unpack_influence(const char *coloc_id, const pcmk_resource_t *rsc, const char *influence_s) { if (influence_s != NULL) { int influence_i = 0; if (crm_str_to_boolean(influence_s, &influence_i) < 0) { pcmk__config_err("Constraint '%s' has invalid value for " PCMK_XA_INFLUENCE " (using default)", coloc_id); } else { return (influence_i == 0)? pcmk__coloc_none : pcmk__coloc_influence; } } if (pcmk_is_set(rsc->flags, pcmk__rsc_critical)) { return pcmk__coloc_influence; } return pcmk__coloc_none; } static void unpack_colocation_set(xmlNode *set, int score, const char *coloc_id, const char *influence_s, pcmk_scheduler_t *scheduler) { xmlNode *xml_rsc = NULL; pcmk_resource_t *other = NULL; pcmk_resource_t *resource = NULL; const char *set_id = pcmk__xe_id(set); const char *role = crm_element_value(set, PCMK_XA_ROLE); bool with_previous = false; int local_score = score; bool sequential = false; uint32_t flags = pcmk__coloc_none; const char *xml_rsc_id = NULL; const char *score_s = crm_element_value(set, PCMK_XA_SCORE); if (score_s) { local_score = char2score(score_s); } if (local_score == 0) { crm_trace("Ignoring colocation '%s' for set '%s' because score is 0", coloc_id, set_id); return; } /* @COMPAT The deprecated PCMK__XA_ORDERING attribute specifies whether * resources in a positive-score set are colocated with the previous or next * resource. */ if (pcmk__str_eq(crm_element_value(set, PCMK__XA_ORDERING), PCMK__VALUE_GROUP, pcmk__str_null_matches|pcmk__str_casei)) { with_previous = true; } else { pcmk__warn_once(pcmk__wo_set_ordering, "Support for '" PCMK__XA_ORDERING "' other than" " '" PCMK__VALUE_GROUP "' in " PCMK_XE_RESOURCE_SET " (such as %s) is deprecated and will be removed in a" " future release", set_id); } if ((pcmk__xe_get_bool_attr(set, PCMK_XA_SEQUENTIAL, &sequential) == pcmk_rc_ok) && !sequential) { return; } if (local_score > 0) { for (xml_rsc = pcmk__xe_first_child(set, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { xml_rsc_id = pcmk__xe_id(xml_rsc); resource = pcmk__find_constraint_resource(scheduler->priv->resources, xml_rsc_id); if (resource == NULL) { // Should be possible only with validation disabled pcmk__config_err("Ignoring %s and later resources in set %s: " "No such resource", xml_rsc_id, set_id); return; } if (other != NULL) { flags = pcmk__coloc_explicit | unpack_influence(coloc_id, resource, influence_s); if (with_previous) { pcmk__rsc_trace(resource, "Colocating %s with %s in set %s", resource->id, other->id, set_id); pcmk__new_colocation(set_id, NULL, local_score, resource, other, role, role, flags); } else { pcmk__rsc_trace(resource, "Colocating %s with %s in set %s", other->id, resource->id, set_id); pcmk__new_colocation(set_id, NULL, local_score, other, resource, role, role, flags); } } other = resource; } } else { /* Anti-colocating with every prior resource is * the only way to ensure the intuitive result * (i.e. that no one in the set can run with anyone else in the set) */ for (xml_rsc = pcmk__xe_first_child(set, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { xmlNode *xml_rsc_with = NULL; xml_rsc_id = pcmk__xe_id(xml_rsc); resource = pcmk__find_constraint_resource(scheduler->priv->resources, xml_rsc_id); if (resource == NULL) { // Should be possible only with validation disabled pcmk__config_err("Ignoring %s and later resources in set %s: " "No such resource", xml_rsc_id, set_id); return; } flags = pcmk__coloc_explicit | unpack_influence(coloc_id, resource, influence_s); for (xml_rsc_with = pcmk__xe_first_child(set, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc_with != NULL; xml_rsc_with = pcmk__xe_next_same(xml_rsc_with)) { xml_rsc_id = pcmk__xe_id(xml_rsc_with); if (pcmk__str_eq(resource->id, xml_rsc_id, pcmk__str_none)) { break; } other = pcmk__find_constraint_resource(scheduler->priv->resources, xml_rsc_id); CRM_ASSERT(other != NULL); // We already processed it pcmk__new_colocation(set_id, NULL, local_score, resource, other, role, role, flags); } } } } /*! * \internal * \brief Colocate two resource sets relative to each other * * \param[in] id Colocation XML ID * \param[in] set1 Dependent set * \param[in] set2 Primary set * \param[in] score Colocation score * \param[in] influence_s Value of colocation's \c PCMK_XA_INFLUENCE * attribute * \param[in,out] scheduler Scheduler data */ static void colocate_rsc_sets(const char *id, const xmlNode *set1, const xmlNode *set2, int score, const char *influence_s, pcmk_scheduler_t *scheduler) { xmlNode *xml_rsc = NULL; pcmk_resource_t *rsc_1 = NULL; pcmk_resource_t *rsc_2 = NULL; const char *xml_rsc_id = NULL; const char *role_1 = crm_element_value(set1, PCMK_XA_ROLE); const char *role_2 = crm_element_value(set2, PCMK_XA_ROLE); int rc = pcmk_rc_ok; bool sequential = false; uint32_t flags = pcmk__coloc_none; if (score == 0) { crm_trace("Ignoring colocation '%s' between sets %s and %s " "because score is 0", id, pcmk__xe_id(set1), pcmk__xe_id(set2)); return; } rc = pcmk__xe_get_bool_attr(set1, PCMK_XA_SEQUENTIAL, &sequential); if ((rc != pcmk_rc_ok) || sequential) { // Get the first one xml_rsc = pcmk__xe_first_child(set1, PCMK_XE_RESOURCE_REF, NULL, NULL); if (xml_rsc != NULL) { xml_rsc_id = pcmk__xe_id(xml_rsc); rsc_1 = pcmk__find_constraint_resource(scheduler->priv->resources, xml_rsc_id); if (rsc_1 == NULL) { // Should be possible only with validation disabled pcmk__config_err("Ignoring colocation of set %s with set %s " "because first resource %s not found", pcmk__xe_id(set1), pcmk__xe_id(set2), xml_rsc_id); return; } } } rc = pcmk__xe_get_bool_attr(set2, PCMK_XA_SEQUENTIAL, &sequential); if ((rc != pcmk_rc_ok) || sequential) { // Get the last one for (xml_rsc = pcmk__xe_first_child(set2, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { xml_rsc_id = pcmk__xe_id(xml_rsc); } rsc_2 = pcmk__find_constraint_resource(scheduler->priv->resources, xml_rsc_id); if (rsc_2 == NULL) { // Should be possible only with validation disabled pcmk__config_err("Ignoring colocation of set %s with set %s " "because last resource %s not found", pcmk__xe_id(set1), pcmk__xe_id(set2), xml_rsc_id); return; } } if ((rsc_1 != NULL) && (rsc_2 != NULL)) { // Both sets are sequential flags = pcmk__coloc_explicit | unpack_influence(id, rsc_1, influence_s); pcmk__new_colocation(id, NULL, score, rsc_1, rsc_2, role_1, role_2, flags); } else if (rsc_1 != NULL) { // Only set1 is sequential flags = pcmk__coloc_explicit | unpack_influence(id, rsc_1, influence_s); for (xml_rsc = pcmk__xe_first_child(set2, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { xml_rsc_id = pcmk__xe_id(xml_rsc); rsc_2 = pcmk__find_constraint_resource(scheduler->priv->resources, xml_rsc_id); if (rsc_2 == NULL) { // Should be possible only with validation disabled pcmk__config_err("Ignoring set %s colocation with resource %s " "in set %s: No such resource", pcmk__xe_id(set1), xml_rsc_id, pcmk__xe_id(set2)); continue; } pcmk__new_colocation(id, NULL, score, rsc_1, rsc_2, role_1, role_2, flags); } } else if (rsc_2 != NULL) { // Only set2 is sequential for (xml_rsc = pcmk__xe_first_child(set1, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { xml_rsc_id = pcmk__xe_id(xml_rsc); rsc_1 = pcmk__find_constraint_resource(scheduler->priv->resources, xml_rsc_id); if (rsc_1 == NULL) { // Should be possible only with validation disabled pcmk__config_err("Ignoring colocation of set %s resource %s " "with set %s: No such resource", pcmk__xe_id(set1), xml_rsc_id, pcmk__xe_id(set2)); continue; } flags = pcmk__coloc_explicit | unpack_influence(id, rsc_1, influence_s); pcmk__new_colocation(id, NULL, score, rsc_1, rsc_2, role_1, role_2, flags); } } else { // Neither set is sequential for (xml_rsc = pcmk__xe_first_child(set1, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { xmlNode *xml_rsc_2 = NULL; xml_rsc_id = pcmk__xe_id(xml_rsc); rsc_1 = pcmk__find_constraint_resource(scheduler->priv->resources, xml_rsc_id); if (rsc_1 == NULL) { // Should be possible only with validation disabled pcmk__config_err("Ignoring colocation of set %s resource %s " "with set %s: No such resource", pcmk__xe_id(set1), xml_rsc_id, pcmk__xe_id(set2)); continue; } flags = pcmk__coloc_explicit | unpack_influence(id, rsc_1, influence_s); for (xml_rsc_2 = pcmk__xe_first_child(set2, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc_2 != NULL; xml_rsc_2 = pcmk__xe_next_same(xml_rsc_2)) { xml_rsc_id = pcmk__xe_id(xml_rsc_2); rsc_2 = pcmk__find_constraint_resource(scheduler->priv->resources, xml_rsc_id); if (rsc_2 == NULL) { // Should be possible only with validation disabled pcmk__config_err("Ignoring colocation of set %s resource " "%s with set %s resource %s: No such " "resource", pcmk__xe_id(set1), pcmk__xe_id(xml_rsc), pcmk__xe_id(set2), xml_rsc_id); continue; } pcmk__new_colocation(id, NULL, score, rsc_1, rsc_2, role_1, role_2, flags); } } } } static void unpack_simple_colocation(xmlNode *xml_obj, const char *id, const char *influence_s, pcmk_scheduler_t *scheduler) { int score_i = 0; uint32_t flags = pcmk__coloc_none; const char *score = crm_element_value(xml_obj, PCMK_XA_SCORE); const char *dependent_id = crm_element_value(xml_obj, PCMK_XA_RSC); const char *primary_id = crm_element_value(xml_obj, PCMK_XA_WITH_RSC); const char *dependent_role = crm_element_value(xml_obj, PCMK_XA_RSC_ROLE); const char *primary_role = crm_element_value(xml_obj, PCMK_XA_WITH_RSC_ROLE); const char *attr = crm_element_value(xml_obj, PCMK_XA_NODE_ATTRIBUTE); const char *primary_instance = NULL; const char *dependent_instance = NULL; pcmk_resource_t *primary = NULL; pcmk_resource_t *dependent = NULL; primary = pcmk__find_constraint_resource(scheduler->priv->resources, primary_id); dependent = pcmk__find_constraint_resource(scheduler->priv->resources, dependent_id); // @COMPAT: Deprecated since 2.1.5 primary_instance = crm_element_value(xml_obj, PCMK__XA_WITH_RSC_INSTANCE); dependent_instance = crm_element_value(xml_obj, PCMK__XA_RSC_INSTANCE); if (dependent_instance != NULL) { pcmk__warn_once(pcmk__wo_coloc_inst, "Support for " PCMK__XA_RSC_INSTANCE " is deprecated " "and will be removed in a future release"); } if (primary_instance != NULL) { pcmk__warn_once(pcmk__wo_coloc_inst, "Support for " PCMK__XA_WITH_RSC_INSTANCE " is " "deprecated and will be removed in a future release"); } if (dependent == NULL) { pcmk__config_err("Ignoring constraint '%s' because resource '%s' " "does not exist", id, dependent_id); return; } else if (primary == NULL) { pcmk__config_err("Ignoring constraint '%s' because resource '%s' " "does not exist", id, primary_id); return; } else if ((dependent_instance != NULL) && !pcmk__is_clone(dependent)) { pcmk__config_err("Ignoring constraint '%s' because resource '%s' " "is not a clone but instance '%s' was requested", id, dependent_id, dependent_instance); return; } else if ((primary_instance != NULL) && !pcmk__is_clone(primary)) { pcmk__config_err("Ignoring constraint '%s' because resource '%s' " "is not a clone but instance '%s' was requested", id, primary_id, primary_instance); return; } if (dependent_instance != NULL) { dependent = find_clone_instance(dependent, dependent_instance); if (dependent == NULL) { pcmk__config_warn("Ignoring constraint '%s' because resource '%s' " "does not have an instance '%s'", id, dependent_id, dependent_instance); return; } } if (primary_instance != NULL) { primary = find_clone_instance(primary, primary_instance); if (primary == NULL) { pcmk__config_warn("Ignoring constraint '%s' because resource '%s' " "does not have an instance '%s'", - "'%s'", id, primary_id, primary_instance); + id, primary_id, primary_instance); return; } } if (pcmk__xe_attr_is_true(xml_obj, PCMK_XA_SYMMETRICAL)) { pcmk__config_warn("The colocation constraint " "'" PCMK_XA_SYMMETRICAL "' attribute has been " "removed"); } if (score) { score_i = char2score(score); } flags = pcmk__coloc_explicit | unpack_influence(id, dependent, influence_s); pcmk__new_colocation(id, attr, score_i, dependent, primary, dependent_role, primary_role, flags); } // \return Standard Pacemaker return code static int unpack_colocation_tags(xmlNode *xml_obj, xmlNode **expanded_xml, pcmk_scheduler_t *scheduler) { const char *id = NULL; const char *dependent_id = NULL; const char *primary_id = NULL; const char *dependent_role = NULL; const char *primary_role = NULL; pcmk_resource_t *dependent = NULL; pcmk_resource_t *primary = NULL; pcmk__idref_t *dependent_tag = NULL; pcmk__idref_t *primary_tag = NULL; xmlNode *dependent_set = NULL; xmlNode *primary_set = NULL; bool any_sets = false; *expanded_xml = NULL; CRM_CHECK(xml_obj != NULL, return EINVAL); id = pcmk__xe_id(xml_obj); if (id == NULL) { pcmk__config_err("Ignoring <%s> constraint without " PCMK_XA_ID, xml_obj->name); return pcmk_rc_unpack_error; } // Check whether there are any resource sets with template or tag references *expanded_xml = pcmk__expand_tags_in_sets(xml_obj, scheduler); if (*expanded_xml != NULL) { crm_log_xml_trace(*expanded_xml, "Expanded " PCMK_XE_RSC_COLOCATION); return pcmk_rc_ok; } dependent_id = crm_element_value(xml_obj, PCMK_XA_RSC); primary_id = crm_element_value(xml_obj, PCMK_XA_WITH_RSC); if ((dependent_id == NULL) || (primary_id == NULL)) { return pcmk_rc_ok; } if (!pcmk__valid_resource_or_tag(scheduler, dependent_id, &dependent, &dependent_tag)) { pcmk__config_err("Ignoring constraint '%s' because '%s' is not a " "valid resource or tag", id, dependent_id); return pcmk_rc_unpack_error; } if (!pcmk__valid_resource_or_tag(scheduler, primary_id, &primary, &primary_tag)) { pcmk__config_err("Ignoring constraint '%s' because '%s' is not a " "valid resource or tag", id, primary_id); return pcmk_rc_unpack_error; } if ((dependent != NULL) && (primary != NULL)) { /* Neither side references any template/tag. */ return pcmk_rc_ok; } if ((dependent_tag != NULL) && (primary_tag != NULL)) { // A colocation constraint between two templates/tags makes no sense pcmk__config_err("Ignoring constraint '%s' because two templates or " "tags cannot be colocated", id); return pcmk_rc_unpack_error; } dependent_role = crm_element_value(xml_obj, PCMK_XA_RSC_ROLE); primary_role = crm_element_value(xml_obj, PCMK_XA_WITH_RSC_ROLE); *expanded_xml = pcmk__xml_copy(NULL, xml_obj); /* Convert dependent's template/tag reference into constraint * PCMK_XE_RESOURCE_SET */ if (!pcmk__tag_to_set(*expanded_xml, &dependent_set, PCMK_XA_RSC, true, scheduler)) { pcmk__xml_free(*expanded_xml); *expanded_xml = NULL; return pcmk_rc_unpack_error; } if (dependent_set != NULL) { if (dependent_role != NULL) { /* Move PCMK_XA_RSC_ROLE into converted PCMK_XE_RESOURCE_SET as * PCMK_XA_ROLE */ crm_xml_add(dependent_set, PCMK_XA_ROLE, dependent_role); pcmk__xe_remove_attr(*expanded_xml, PCMK_XA_RSC_ROLE); } any_sets = true; } /* Convert primary's template/tag reference into constraint * PCMK_XE_RESOURCE_SET */ if (!pcmk__tag_to_set(*expanded_xml, &primary_set, PCMK_XA_WITH_RSC, true, scheduler)) { pcmk__xml_free(*expanded_xml); *expanded_xml = NULL; return pcmk_rc_unpack_error; } if (primary_set != NULL) { if (primary_role != NULL) { /* Move PCMK_XA_WITH_RSC_ROLE into converted PCMK_XE_RESOURCE_SET as * PCMK_XA_ROLE */ crm_xml_add(primary_set, PCMK_XA_ROLE, primary_role); pcmk__xe_remove_attr(*expanded_xml, PCMK_XA_WITH_RSC_ROLE); } any_sets = true; } if (any_sets) { crm_log_xml_trace(*expanded_xml, "Expanded " PCMK_XE_RSC_COLOCATION); } else { pcmk__xml_free(*expanded_xml); *expanded_xml = NULL; } return pcmk_rc_ok; } /*! * \internal * \brief Parse a colocation constraint from XML into scheduler data * * \param[in,out] xml_obj Colocation constraint XML to unpack * \param[in,out] scheduler Scheduler data to add constraint to */ void pcmk__unpack_colocation(xmlNode *xml_obj, pcmk_scheduler_t *scheduler) { int score_i = 0; xmlNode *set = NULL; xmlNode *last = NULL; xmlNode *orig_xml = NULL; xmlNode *expanded_xml = NULL; const char *id = crm_element_value(xml_obj, PCMK_XA_ID); const char *score = NULL; const char *influence_s = NULL; if (pcmk__str_empty(id)) { pcmk__config_err("Ignoring " PCMK_XE_RSC_COLOCATION " without " CRM_ATTR_ID); return; } if (unpack_colocation_tags(xml_obj, &expanded_xml, scheduler) != pcmk_rc_ok) { return; } if (expanded_xml != NULL) { orig_xml = xml_obj; xml_obj = expanded_xml; } score = crm_element_value(xml_obj, PCMK_XA_SCORE); if (score != NULL) { score_i = char2score(score); } influence_s = crm_element_value(xml_obj, PCMK_XA_INFLUENCE); for (set = pcmk__xe_first_child(xml_obj, PCMK_XE_RESOURCE_SET, NULL, NULL); set != NULL; set = pcmk__xe_next_same(set)) { set = pcmk__xe_resolve_idref(set, scheduler->input); if (set == NULL) { // Configuration error, message already logged if (expanded_xml != NULL) { pcmk__xml_free(expanded_xml); } return; } if (pcmk__str_empty(pcmk__xe_id(set))) { pcmk__config_err("Ignoring " PCMK_XE_RESOURCE_SET " without " CRM_ATTR_ID); continue; } unpack_colocation_set(set, score_i, id, influence_s, scheduler); if (last != NULL) { colocate_rsc_sets(id, last, set, score_i, influence_s, scheduler); } last = set; } if (expanded_xml) { pcmk__xml_free(expanded_xml); xml_obj = orig_xml; } if (last == NULL) { unpack_simple_colocation(xml_obj, id, influence_s, scheduler); } } /*! * \internal * \brief Check whether colocation's dependent preferences should be considered * * \param[in] colocation Colocation constraint * \param[in] rsc Primary instance (normally this will be * colocation->primary, which NULL will be treated as, * but for clones or bundles with multiple instances * this can be a particular instance) * * \return true if colocation influence should be effective, otherwise false */ bool pcmk__colocation_has_influence(const pcmk__colocation_t *colocation, const pcmk_resource_t *rsc) { if (rsc == NULL) { rsc = colocation->primary; } /* A bundle replica colocates its remote connection with its container, * using a finite score so that the container can run on Pacemaker Remote * nodes. * * Moving a connection is lightweight and does not interrupt the service, * while moving a container is heavyweight and does interrupt the service, * so don't move a clean, active container based solely on the preferences * of its connection. * * This also avoids problematic scenarios where two containers want to * perpetually swap places. */ if (pcmk_is_set(colocation->dependent->flags, pcmk__rsc_remote_nesting_allowed) && !pcmk_is_set(rsc->flags, pcmk__rsc_failed) && pcmk__list_of_1(rsc->priv->active_nodes)) { return false; } /* The dependent in a colocation influences the primary's location * if the PCMK_XA_INFLUENCE option is true or the primary is not yet active. */ return pcmk_is_set(colocation->flags, pcmk__coloc_influence) || (rsc->priv->active_nodes == NULL); } /*! * \internal * \brief Make actions of a given type unrunnable for a given resource * * \param[in,out] rsc Resource whose actions should be blocked * \param[in] task Name of action to block * \param[in] reason Unrunnable start action causing the block */ static void mark_action_blocked(pcmk_resource_t *rsc, const char *task, const pcmk_resource_t *reason) { GList *iter = NULL; char *reason_text = crm_strdup_printf("colocation with %s", reason->id); for (iter = rsc->priv->actions; iter != NULL; iter = iter->next) { pcmk_action_t *action = iter->data; if (pcmk_is_set(action->flags, pcmk__action_runnable) && pcmk__str_eq(action->task, task, pcmk__str_none)) { pcmk__clear_action_flags(action, pcmk__action_runnable); pe_action_set_reason(action, reason_text, false); pcmk__block_colocation_dependents(action); pcmk__update_action_for_orderings(action, rsc->priv->scheduler); } } // If parent resource can't perform an action, neither can any children for (iter = rsc->priv->children; iter != NULL; iter = iter->next) { mark_action_blocked((pcmk_resource_t *) (iter->data), task, reason); } free(reason_text); } /*! * \internal * \brief If an action is unrunnable, block any relevant dependent actions * * If a given action is an unrunnable start or promote, block the start or * promote actions of resources colocated with it, as appropriate to the * colocations' configured roles. * * \param[in,out] action Action to check */ void pcmk__block_colocation_dependents(pcmk_action_t *action) { GList *iter = NULL; GList *colocations = NULL; pcmk_resource_t *rsc = NULL; bool is_start = false; if (pcmk_is_set(action->flags, pcmk__action_runnable)) { return; // Only unrunnable actions block dependents } is_start = pcmk__str_eq(action->task, PCMK_ACTION_START, pcmk__str_none); if (!is_start && !pcmk__str_eq(action->task, PCMK_ACTION_PROMOTE, pcmk__str_none)) { return; // Only unrunnable starts and promotes block dependents } CRM_ASSERT(action->rsc != NULL); // Start and promote are resource actions /* If this resource is part of a collective resource, dependents are blocked * only if all instances of the collective are unrunnable, so check the * collective resource. */ rsc = uber_parent(action->rsc); if (rsc->priv->parent != NULL) { rsc = rsc->priv->parent; // Bundle } // Colocation fails only if entire primary can't reach desired role for (iter = rsc->priv->children; iter != NULL; iter = iter->next) { pcmk_resource_t *child = iter->data; pcmk_action_t *child_action = NULL; child_action = find_first_action(child->priv->actions, NULL, action->task, NULL); if ((child_action == NULL) || pcmk_is_set(child_action->flags, pcmk__action_runnable)) { crm_trace("Not blocking %s colocation dependents because " "at least %s has runnable %s", rsc->id, child->id, action->task); return; // At least one child can reach desired role } } crm_trace("Blocking %s colocation dependents due to unrunnable %s %s", rsc->id, action->rsc->id, action->task); // Check each colocation where this resource is primary colocations = pcmk__with_this_colocations(rsc); for (iter = colocations; iter != NULL; iter = iter->next) { pcmk__colocation_t *colocation = iter->data; if (colocation->score < PCMK_SCORE_INFINITY) { continue; // Only mandatory colocations block dependent } /* If the primary can't start, the dependent can't reach its colocated * role, regardless of what the primary or dependent colocation role is. * * If the primary can't be promoted, the dependent can't reach its * colocated role if the primary's colocation role is promoted. */ if (!is_start && (colocation->primary_role != pcmk_role_promoted)) { continue; } // Block the dependent from reaching its colocated role if (colocation->dependent_role == pcmk_role_promoted) { mark_action_blocked(colocation->dependent, PCMK_ACTION_PROMOTE, action->rsc); } else { mark_action_blocked(colocation->dependent, PCMK_ACTION_START, action->rsc); } } g_list_free(colocations); } /*! * \internal * \brief Get the resource to use for role comparisons * * A bundle replica includes a container and possibly an instance of the bundled * resource. The dependent in a "with bundle" colocation is colocated with a * particular bundle container. However, if the colocation includes a role, then * the role must be checked on the bundled resource instance inside the * container. The container itself will never be promoted; the bundled resource * may be. * * If the given resource is a bundle replica container, return the resource * inside it, if any. Otherwise, return the resource itself. * * \param[in] rsc Resource to check * * \return Resource to use for role comparisons */ static const pcmk_resource_t * get_resource_for_role(const pcmk_resource_t *rsc) { if (pcmk_is_set(rsc->flags, pcmk__rsc_replica_container)) { const pcmk_resource_t *child = pe__get_rsc_in_container(rsc); if (child != NULL) { return child; } } return rsc; } /*! * \internal * \brief Determine how a colocation constraint should affect a resource * * Colocation constraints have different effects at different points in the * scheduler sequence. Initially, they affect a resource's location; once that * is determined, then for promotable clones they can affect a resource * instance's role; after both are determined, the constraints no longer matter. * Given a specific colocation constraint, check what has been done so far to * determine what should be affected at the current point in the scheduler. * * \param[in] dependent Dependent resource in colocation * \param[in] primary Primary resource in colocation * \param[in] colocation Colocation constraint * \param[in] preview If true, pretend resources have already been assigned * * \return How colocation constraint should be applied at this point */ enum pcmk__coloc_affects pcmk__colocation_affects(const pcmk_resource_t *dependent, const pcmk_resource_t *primary, const pcmk__colocation_t *colocation, bool preview) { const pcmk_resource_t *dependent_role_rsc = NULL; const pcmk_resource_t *primary_role_rsc = NULL; CRM_ASSERT((dependent != NULL) && (primary != NULL) && (colocation != NULL)); if (!preview && pcmk_is_set(primary->flags, pcmk__rsc_unassigned)) { // Primary resource has not been assigned yet, so we can't do anything return pcmk__coloc_affects_nothing; } dependent_role_rsc = get_resource_for_role(dependent); primary_role_rsc = get_resource_for_role(primary); if ((colocation->dependent_role >= pcmk_role_unpromoted) && (dependent_role_rsc->priv->parent != NULL) && pcmk_is_set(dependent_role_rsc->priv->parent->flags, pcmk__rsc_promotable) && !pcmk_is_set(dependent_role_rsc->flags, pcmk__rsc_unassigned)) { /* This is a colocation by role, and the dependent is a promotable clone * that has already been assigned, so the colocation should now affect * the role. */ return pcmk__coloc_affects_role; } if (!preview && !pcmk_is_set(dependent->flags, pcmk__rsc_unassigned)) { /* The dependent resource has already been through assignment, so the * constraint no longer matters. */ return pcmk__coloc_affects_nothing; } if ((colocation->dependent_role != pcmk_role_unknown) && (colocation->dependent_role != dependent_role_rsc->priv->next_role)) { crm_trace("Skipping %scolocation '%s': dependent limited to %s role " "but %s next role is %s", ((colocation->score < 0)? "anti-" : ""), colocation->id, pcmk_role_text(colocation->dependent_role), dependent_role_rsc->id, pcmk_role_text(dependent_role_rsc->priv->next_role)); return pcmk__coloc_affects_nothing; } if ((colocation->primary_role != pcmk_role_unknown) && (colocation->primary_role != primary_role_rsc->priv->next_role)) { crm_trace("Skipping %scolocation '%s': primary limited to %s role " "but %s next role is %s", ((colocation->score < 0)? "anti-" : ""), colocation->id, pcmk_role_text(colocation->primary_role), primary_role_rsc->id, pcmk_role_text(primary_role_rsc->priv->next_role)); return pcmk__coloc_affects_nothing; } return pcmk__coloc_affects_location; } /*! * \internal * \brief Apply colocation to dependent for assignment purposes * * Update the allowed node scores of the dependent resource in a colocation, * for the purposes of assigning it to a node. * * \param[in,out] dependent Dependent resource in colocation * \param[in] primary Primary resource in colocation * \param[in] colocation Colocation constraint */ void pcmk__apply_coloc_to_scores(pcmk_resource_t *dependent, const pcmk_resource_t *primary, const pcmk__colocation_t *colocation) { const char *attr = colocation->node_attribute; const char *value = NULL; GHashTable *work = NULL; GHashTableIter iter; pcmk_node_t *node = NULL; if (primary->priv->assigned_node != NULL) { value = pcmk__colocation_node_attr(primary->priv->assigned_node, attr, primary); } else if (colocation->score < 0) { // Nothing to do (anti-colocation with something that is not running) return; } work = pcmk__copy_node_table(dependent->priv->allowed_nodes); g_hash_table_iter_init(&iter, work); while (g_hash_table_iter_next(&iter, NULL, (void **)&node)) { if (primary->priv->assigned_node == NULL) { node->assign->score = pcmk__add_scores(-colocation->score, node->assign->score); pcmk__rsc_trace(dependent, "Applied %s to %s score on %s (now %s after " "subtracting %s because primary %s inactive)", colocation->id, dependent->id, pcmk__node_name(node), pcmk_readable_score(node->assign->score), pcmk_readable_score(colocation->score), primary->id); continue; } if (pcmk__str_eq(pcmk__colocation_node_attr(node, attr, dependent), value, pcmk__str_casei)) { /* Add colocation score only if optional (or minus infinity). A * mandatory colocation is a requirement rather than a preference, * so we don't need to consider it for relative assignment purposes. * The resource will simply be forbidden from running on the node if * the primary isn't active there (via the condition above). */ if (colocation->score < PCMK_SCORE_INFINITY) { node->assign->score = pcmk__add_scores(colocation->score, node->assign->score); pcmk__rsc_trace(dependent, "Applied %s to %s score on %s (now %s after " "adding %s)", colocation->id, dependent->id, pcmk__node_name(node), pcmk_readable_score(node->assign->score), pcmk_readable_score(colocation->score)); } continue; } if (colocation->score >= PCMK_SCORE_INFINITY) { /* Only mandatory colocations are relevant when the colocation * attribute doesn't match, because an attribute not matching is not * a negative preference -- the colocation is simply relevant only * where it matches. */ node->assign->score = -PCMK_SCORE_INFINITY; pcmk__rsc_trace(dependent, "Banned %s from %s because colocation %s attribute %s " "does not match", dependent->id, pcmk__node_name(node), colocation->id, attr); } } if ((colocation->score <= -PCMK_SCORE_INFINITY) || (colocation->score >= PCMK_SCORE_INFINITY) || pcmk__any_node_available(work)) { g_hash_table_destroy(dependent->priv->allowed_nodes); dependent->priv->allowed_nodes = work; work = NULL; } else { pcmk__rsc_info(dependent, "%s: Rolling back scores from %s (no available nodes)", dependent->id, primary->id); } if (work != NULL) { g_hash_table_destroy(work); } } /*! * \internal * \brief Apply colocation to dependent for role purposes * * Update the priority of the dependent resource in a colocation, for the * purposes of selecting its role * * \param[in,out] dependent Dependent resource in colocation * \param[in] primary Primary resource in colocation * \param[in] colocation Colocation constraint * * \return The score added to the dependent's priority */ int pcmk__apply_coloc_to_priority(pcmk_resource_t *dependent, const pcmk_resource_t *primary, const pcmk__colocation_t *colocation) { const char *dependent_value = NULL; const char *primary_value = NULL; const char *attr = colocation->node_attribute; int score_multiplier = 1; int priority_delta = 0; const pcmk_node_t *primary_node = NULL; const pcmk_node_t *dependent_node = NULL; CRM_ASSERT((dependent != NULL) && (primary != NULL) && (colocation != NULL)); primary_node = primary->priv->assigned_node; dependent_node = dependent->priv->assigned_node; if (dependent_node == NULL) { return 0; } if ((primary_node != NULL) && (colocation->primary_role != pcmk_role_unknown)) { /* Colocation applies only if the primary's next role matches. * * If primary_node == NULL, we want to proceed past this block, so that * dependent_node is marked ineligible for promotion. * * @TODO Why ignore a mandatory colocation in this case when we apply * its negation in the mismatched value case? */ const pcmk_resource_t *role_rsc = get_resource_for_role(primary); if (colocation->primary_role != role_rsc->priv->next_role) { return 0; } } dependent_value = pcmk__colocation_node_attr(dependent_node, attr, dependent); primary_value = pcmk__colocation_node_attr(primary_node, attr, primary); if (!pcmk__str_eq(dependent_value, primary_value, pcmk__str_casei)) { if ((colocation->score == PCMK_SCORE_INFINITY) && (colocation->dependent_role == pcmk_role_promoted)) { /* For a mandatory promoted-role colocation, mark the dependent node * ineligible to promote the dependent if its attribute value * doesn't match the primary node's */ score_multiplier = -1; } else { // Otherwise, ignore the colocation if attribute values don't match return 0; } } else if (colocation->dependent_role == pcmk_role_unpromoted) { /* Node attribute values matched, so we want to avoid promoting the * dependent on this node */ score_multiplier = -1; } priority_delta = score_multiplier * colocation->score; dependent->priv->priority = pcmk__add_scores(priority_delta, dependent->priv->priority); pcmk__rsc_trace(dependent, "Applied %s to %s promotion priority (now %s after %s %d)", colocation->id, dependent->id, pcmk_readable_score(dependent->priv->priority), ((score_multiplier == 1)? "adding" : "subtracting"), colocation->score); return priority_delta; } /*! * \internal * \brief Find score of highest-scored node that matches colocation attribute * * \param[in] colocation Colocation constraint being applied * \param[in,out] rsc Resource whose allowed nodes should be searched * \param[in] attr Colocation attribute name (must not be NULL) * \param[in] value Colocation attribute value to require */ static int best_node_score_matching_attr(const pcmk__colocation_t *colocation, pcmk_resource_t *rsc, const char *attr, const char *value) { GHashTable *allowed_nodes_orig = NULL; GHashTableIter iter; pcmk_node_t *node = NULL; int best_score = -PCMK_SCORE_INFINITY; const char *best_node = NULL; if ((colocation != NULL) && (rsc == colocation->dependent) && pcmk_is_set(colocation->flags, pcmk__coloc_explicit) && pcmk__is_group(rsc->priv->parent) && (rsc != rsc->priv->parent->priv->children->data)) { /* The resource is a user-configured colocation's explicit dependent, * and a group member other than the first, which means the group's * location constraint scores were not applied to it (see * pcmk__group_apply_location()). Explicitly consider those scores now. * * @TODO This does leave one suboptimal case: if the group itself or * another member other than the first is explicitly colocated with * the same primary, the primary will count the group's location scores * multiple times. This is much less likely than a single member being * explicitly colocated, so it's an acceptable tradeoff for now. */ allowed_nodes_orig = rsc->priv->allowed_nodes; rsc->priv->allowed_nodes = pcmk__copy_node_table(allowed_nodes_orig); for (GList *loc_iter = rsc->priv->scheduler->priv->location_constraints; loc_iter != NULL; loc_iter = loc_iter->next) { pcmk__location_t *location = loc_iter->data; if (location->rsc == rsc->priv->parent) { rsc->priv->cmds->apply_location(rsc, location); } } } // Find best allowed node with matching attribute g_hash_table_iter_init(&iter, rsc->priv->allowed_nodes); while (g_hash_table_iter_next(&iter, NULL, (void **) &node)) { if ((node->assign->score > best_score) && pcmk__node_available(node, false, false) && pcmk__str_eq(value, pcmk__colocation_node_attr(node, attr, rsc), pcmk__str_casei)) { best_score = node->assign->score; best_node = node->priv->name; } } if (!pcmk__str_eq(attr, CRM_ATTR_UNAME, pcmk__str_none)) { if (best_node == NULL) { crm_info("No allowed node for %s matches node attribute %s=%s", rsc->id, attr, value); } else { crm_info("Allowed node %s for %s had best score (%d) " "of those matching node attribute %s=%s", best_node, rsc->id, best_score, attr, value); } } if (allowed_nodes_orig != NULL) { g_hash_table_destroy(rsc->priv->allowed_nodes); rsc->priv->allowed_nodes = allowed_nodes_orig; } return best_score; } /*! * \internal * \brief Check whether a resource is allowed only on a single node * * \param[in] rsc Resource to check * * \return \c true if \p rsc is allowed only on one node, otherwise \c false */ static bool allowed_on_one(const pcmk_resource_t *rsc) { GHashTableIter iter; pcmk_node_t *allowed_node = NULL; int allowed_nodes = 0; g_hash_table_iter_init(&iter, rsc->priv->allowed_nodes); while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &allowed_node)) { if ((allowed_node->assign->score >= 0) && (++allowed_nodes > 1)) { pcmk__rsc_trace(rsc, "%s is allowed on multiple nodes", rsc->id); return false; } } pcmk__rsc_trace(rsc, "%s is allowed %s", rsc->id, ((allowed_nodes == 1)? "on a single node" : "nowhere")); return (allowed_nodes == 1); } /*! * \internal * \brief Add resource's colocation matches to current node assignment scores * * For each node in a given table, if any of a given resource's allowed nodes * have a matching value for the colocation attribute, add the highest of those * nodes' scores to the node's score. * * \param[in,out] nodes Table of nodes with assignment scores so far * \param[in,out] source_rsc Resource whose node scores to add * \param[in] target_rsc Resource on whose behalf to update \p nodes * \param[in] colocation Original colocation constraint (used to get * configured primary resource's stickiness, and * to get colocation node attribute; pass NULL to * ignore stickiness and use default attribute) * \param[in] factor Factor by which to multiply scores being added * \param[in] only_positive Whether to add only positive scores */ static void add_node_scores_matching_attr(GHashTable *nodes, pcmk_resource_t *source_rsc, const pcmk_resource_t *target_rsc, const pcmk__colocation_t *colocation, float factor, bool only_positive) { GHashTableIter iter; pcmk_node_t *node = NULL; const char *attr = colocation->node_attribute; // Iterate through each node g_hash_table_iter_init(&iter, nodes); while (g_hash_table_iter_next(&iter, NULL, (void **)&node)) { float delta_f = 0; int delta = 0; int score = 0; int new_score = 0; const char *value = pcmk__colocation_node_attr(node, attr, target_rsc); score = best_node_score_matching_attr(colocation, source_rsc, attr, value); if ((factor < 0) && (score < 0)) { /* If the dependent is anti-colocated, we generally don't want the * primary to prefer nodes that the dependent avoids. That could * lead to unnecessary shuffling of the primary when the dependent * hits its migration threshold somewhere, for example. * * However, there are cases when it is desirable. If the dependent * can't run anywhere but where the primary is, it would be * worthwhile to move the primary for the sake of keeping the * dependent active. * * We can't know that exactly at this point since we don't know * where the primary will be assigned, but we can limit considering * the preference to when the dependent is allowed only on one node. * This is less than ideal for multiple reasons: * * - the dependent could be allowed on more than one node but have * anti-colocation primaries on each; * - the dependent could be a clone or bundle with multiple * instances, and the dependent as a whole is allowed on multiple * nodes but some instance still can't run * - the dependent has considered node-specific criteria such as * location constraints and stickiness by this point, but might * have other factors that end up disallowing a node * * but the alternative is making the primary move when it doesn't * need to. * * We also consider the primary's stickiness and influence, so the * user has some say in the matter. (This is the configured primary, * not a particular instance of the primary, but that doesn't matter * unless stickiness uses a rule to vary by node, and that seems * acceptable to ignore.) */ if ((colocation->primary->priv->stickiness >= -score) || !pcmk__colocation_has_influence(colocation, NULL) || !allowed_on_one(colocation->dependent)) { crm_trace("%s: Filtering %d + %f * %d " "(double negative disallowed)", pcmk__node_name(node), node->assign->score, factor, score); continue; } } if (node->assign->score == INFINITY_HACK) { crm_trace("%s: Filtering %d + %f * %d (node was marked unusable)", pcmk__node_name(node), node->assign->score, factor, score); continue; } delta_f = factor * score; // Round the number; see http://c-faq.com/fp/round.html delta = (int) ((delta_f < 0)? (delta_f - 0.5) : (delta_f + 0.5)); /* Small factors can obliterate the small scores that are often actually * used in configurations. If the score and factor are nonzero, ensure * that the result is nonzero as well. */ if ((delta == 0) && (score != 0)) { if (factor > 0.0) { delta = 1; } else if (factor < 0.0) { delta = -1; } } new_score = pcmk__add_scores(delta, node->assign->score); if (only_positive && (new_score < 0) && (node->assign->score > 0)) { crm_trace("%s: Filtering %d + %f * %d = %d " "(negative disallowed, marking node unusable)", pcmk__node_name(node), node->assign->score, factor, score, new_score); node->assign->score = INFINITY_HACK; continue; } if (only_positive && (new_score < 0) && (node->assign->score == 0)) { crm_trace("%s: Filtering %d + %f * %d = %d (negative disallowed)", pcmk__node_name(node), node->assign->score, factor, score, new_score); continue; } crm_trace("%s: %d + %f * %d = %d", pcmk__node_name(node), node->assign->score, factor, score, new_score); node->assign->score = new_score; } } /*! * \internal * \brief Update nodes with scores of colocated resources' nodes * * Given a table of nodes and a resource, update the nodes' scores with the * scores of the best nodes matching the attribute used for each of the * resource's relevant colocations. * * \param[in,out] source_rsc Resource whose node scores to add * \param[in] target_rsc Resource on whose behalf to update \p *nodes * \param[in] log_id Resource ID for logs (if \c NULL, use * \p source_rsc ID) * \param[in,out] nodes Nodes to update (set initial contents to \c NULL * to copy allowed nodes from \p source_rsc) * \param[in] colocation Original colocation constraint (used to get * configured primary resource's stickiness, and * to get colocation node attribute; if \c NULL, * source_rsc's own matching node scores * will not be added, and \p *nodes must be \c NULL * as well) * \param[in] factor Incorporate scores multiplied by this factor * \param[in] flags Bitmask of enum pcmk__coloc_select values * * \note \c NULL \p target_rsc, \c NULL \p *nodes, \c NULL \p colocation, and * the \c pcmk__coloc_select_this_with flag are used together (and only by * \c cmp_resources()). * \note The caller remains responsible for freeing \p *nodes. * \note This is the shared implementation of * \c pcmk__assignment_methods_t:add_colocated_node_scores(). */ void pcmk__add_colocated_node_scores(pcmk_resource_t *source_rsc, const pcmk_resource_t *target_rsc, const char *log_id, GHashTable **nodes, const pcmk__colocation_t *colocation, float factor, uint32_t flags) { GHashTable *work = NULL; CRM_ASSERT((source_rsc != NULL) && (nodes != NULL) && ((colocation != NULL) || ((target_rsc == NULL) && (*nodes == NULL)))); if (log_id == NULL) { log_id = source_rsc->id; } // Avoid infinite recursion if (pcmk_is_set(source_rsc->flags, pcmk__rsc_updating_nodes)) { pcmk__rsc_info(source_rsc, "%s: Breaking dependency loop at %s", log_id, source_rsc->id); return; } pcmk__set_rsc_flags(source_rsc, pcmk__rsc_updating_nodes); if (*nodes == NULL) { work = pcmk__copy_node_table(source_rsc->priv->allowed_nodes); target_rsc = source_rsc; } else { const bool pos = pcmk_is_set(flags, pcmk__coloc_select_nonnegative); pcmk__rsc_trace(source_rsc, "%s: Merging %s scores from %s (at %.6f)", log_id, (pos? "positive" : "all"), source_rsc->id, factor); work = pcmk__copy_node_table(*nodes); add_node_scores_matching_attr(work, source_rsc, target_rsc, colocation, factor, pos); } if (work == NULL) { pcmk__clear_rsc_flags(source_rsc, pcmk__rsc_updating_nodes); return; } if (pcmk__any_node_available(work)) { GList *colocations = NULL; if (pcmk_is_set(flags, pcmk__coloc_select_this_with)) { colocations = pcmk__this_with_colocations(source_rsc); pcmk__rsc_trace(source_rsc, "Checking additional %d optional '%s with' " "constraints", g_list_length(colocations), source_rsc->id); } else { colocations = pcmk__with_this_colocations(source_rsc); pcmk__rsc_trace(source_rsc, "Checking additional %d optional 'with %s' " "constraints", g_list_length(colocations), source_rsc->id); } flags |= pcmk__coloc_select_active; for (GList *iter = colocations; iter != NULL; iter = iter->next) { pcmk__colocation_t *constraint = iter->data; pcmk_resource_t *other = NULL; float other_factor = factor * constraint->score / (float) PCMK_SCORE_INFINITY; if (pcmk_is_set(flags, pcmk__coloc_select_this_with)) { other = constraint->primary; } else if (!pcmk__colocation_has_influence(constraint, NULL)) { continue; } else { other = constraint->dependent; } pcmk__rsc_trace(source_rsc, "Optionally merging score of '%s' constraint " "(%s with %s)", constraint->id, constraint->dependent->id, constraint->primary->id); other->priv->cmds->add_colocated_node_scores(other, target_rsc, log_id, &work, constraint, other_factor, flags); pe__show_node_scores(true, NULL, log_id, work, source_rsc->priv->scheduler); } g_list_free(colocations); } else if (pcmk_is_set(flags, pcmk__coloc_select_active)) { pcmk__rsc_info(source_rsc, "%s: Rolling back optional scores from %s", log_id, source_rsc->id); g_hash_table_destroy(work); pcmk__clear_rsc_flags(source_rsc, pcmk__rsc_updating_nodes); return; } if (pcmk_is_set(flags, pcmk__coloc_select_nonnegative)) { pcmk_node_t *node = NULL; GHashTableIter iter; g_hash_table_iter_init(&iter, work); while (g_hash_table_iter_next(&iter, NULL, (void **)&node)) { if (node->assign->score == INFINITY_HACK) { node->assign->score = 1; } } } if (*nodes != NULL) { g_hash_table_destroy(*nodes); } *nodes = work; pcmk__clear_rsc_flags(source_rsc, pcmk__rsc_updating_nodes); } /*! * \internal * \brief Apply a "with this" colocation to a resource's allowed node scores * * \param[in,out] data Colocation to apply * \param[in,out] user_data Resource being assigned */ void pcmk__add_dependent_scores(gpointer data, gpointer user_data) { pcmk__colocation_t *colocation = data; pcmk_resource_t *primary = user_data; pcmk_resource_t *dependent = colocation->dependent; const float factor = colocation->score / (float) PCMK_SCORE_INFINITY; uint32_t flags = pcmk__coloc_select_active; if (!pcmk__colocation_has_influence(colocation, NULL)) { return; } if (pcmk__is_clone(primary)) { flags |= pcmk__coloc_select_nonnegative; } pcmk__rsc_trace(primary, "%s: Incorporating attenuated %s assignment scores due " "to colocation %s", primary->id, dependent->id, colocation->id); dependent->priv->cmds->add_colocated_node_scores(dependent, primary, dependent->id, &(primary->priv->allowed_nodes), colocation, factor, flags); } /*! * \internal * \brief Exclude nodes from a dependent's node table if not in a given list * * Given a dependent resource in a colocation and a list of nodes where the * primary resource will run, set a node's score to \c -INFINITY in the * dependent's node table if not found in the primary nodes list. * * \param[in,out] dependent Dependent resource * \param[in] primary Primary resource (for logging only) * \param[in] colocation Colocation constraint (for logging only) * \param[in] primary_nodes List of nodes where the primary will have * unblocked instances in a suitable role * \param[in] merge_scores If \c true and a node is found in both \p table * and \p list, add the node's score in \p list to * the node's score in \p table */ void pcmk__colocation_intersect_nodes(pcmk_resource_t *dependent, const pcmk_resource_t *primary, const pcmk__colocation_t *colocation, const GList *primary_nodes, bool merge_scores) { GHashTableIter iter; pcmk_node_t *dependent_node = NULL; CRM_ASSERT((dependent != NULL) && (primary != NULL) && (colocation != NULL)); g_hash_table_iter_init(&iter, dependent->priv->allowed_nodes); while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &dependent_node)) { const pcmk_node_t *primary_node = NULL; primary_node = pe_find_node_id(primary_nodes, dependent_node->priv->id); if (primary_node == NULL) { dependent_node->assign->score = -PCMK_SCORE_INFINITY; pcmk__rsc_trace(dependent, "Banning %s from %s (no primary instance) for %s", dependent->id, pcmk__node_name(dependent_node), colocation->id); } else if (merge_scores) { dependent_node->assign->score = pcmk__add_scores(dependent_node->assign->score, primary_node->assign->score); pcmk__rsc_trace(dependent, "Added %s's score %s to %s's score for %s (now %d) " "for colocation %s", primary->id, pcmk_readable_score(primary_node->assign->score), dependent->id, pcmk__node_name(dependent_node), dependent_node->assign->score, colocation->id); } } } /*! * \internal * \brief Get all colocations affecting a resource as the primary * * \param[in] rsc Resource to get colocations for * * \return Newly allocated list of colocations affecting \p rsc as primary * * \note This is a convenience wrapper for the with_this_colocations() method. */ GList * pcmk__with_this_colocations(const pcmk_resource_t *rsc) { GList *list = NULL; rsc->priv->cmds->with_this_colocations(rsc, rsc, &list); return list; } /*! * \internal * \brief Get all colocations affecting a resource as the dependent * * \param[in] rsc Resource to get colocations for * * \return Newly allocated list of colocations affecting \p rsc as dependent * * \note This is a convenience wrapper for the this_with_colocations() method. */ GList * pcmk__this_with_colocations(const pcmk_resource_t *rsc) { GList *list = NULL; rsc->priv->cmds->this_with_colocations(rsc, rsc, &list); return list; } diff --git a/lib/pacemaker/pcmk_sched_tickets.c b/lib/pacemaker/pcmk_sched_tickets.c index 6a5d032b4d..68cdb5e857 100644 --- a/lib/pacemaker/pcmk_sched_tickets.c +++ b/lib/pacemaker/pcmk_sched_tickets.c @@ -1,550 +1,550 @@ /* * 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 "libpacemaker_private.h" enum loss_ticket_policy { loss_ticket_stop, loss_ticket_demote, loss_ticket_fence, loss_ticket_freeze }; typedef struct { const char *id; pcmk_resource_t *rsc; pcmk__ticket_t *ticket; enum loss_ticket_policy loss_policy; int role; } rsc_ticket_t; /*! * \brief Check whether a ticket constraint matches a resource by role * * \param[in] rsc_ticket Ticket constraint * \param[in] rsc Resource to compare with ticket * * \param[in] true if constraint has no role or resource's role matches * constraint's, otherwise false */ static bool ticket_role_matches(const pcmk_resource_t *rsc, const rsc_ticket_t *rsc_ticket) { if ((rsc_ticket->role == pcmk_role_unknown) || (rsc_ticket->role == rsc->priv->orig_role)) { return true; } pcmk__rsc_trace(rsc, "Skipping constraint: \"%s\" state filter", pcmk_role_text(rsc_ticket->role)); return false; } /*! * \brief Create location constraints and fencing as needed for a ticket * * \param[in,out] rsc Resource affected by ticket * \param[in] rsc_ticket Ticket */ static void constraints_for_ticket(pcmk_resource_t *rsc, const rsc_ticket_t *rsc_ticket) { GList *iter = NULL; CRM_CHECK((rsc != NULL) && (rsc_ticket != NULL), return); if (pcmk_is_set(rsc_ticket->ticket->flags, pcmk__ticket_granted) && !pcmk_is_set(rsc_ticket->ticket->flags, pcmk__ticket_standby)) { return; } if (rsc->priv->children != NULL) { pcmk__rsc_trace(rsc, "Processing ticket dependencies from %s", rsc->id); for (iter = rsc->priv->children; iter != NULL; iter = iter->next) { constraints_for_ticket((pcmk_resource_t *) iter->data, rsc_ticket); } return; } pcmk__rsc_trace(rsc, "%s: Processing ticket dependency on %s (%s, %s)", rsc->id, rsc_ticket->ticket->id, rsc_ticket->id, pcmk_role_text(rsc_ticket->role)); if (!pcmk_is_set(rsc_ticket->ticket->flags, pcmk__ticket_granted) && (rsc->priv->active_nodes != NULL)) { switch (rsc_ticket->loss_policy) { case loss_ticket_stop: resource_location(rsc, NULL, -PCMK_SCORE_INFINITY, "__loss_of_ticket__", rsc->priv->scheduler); break; case loss_ticket_demote: // Promotion score will be set to -INFINITY in promotion_order() if (rsc_ticket->role != pcmk_role_promoted) { resource_location(rsc, NULL, -PCMK_SCORE_INFINITY, "__loss_of_ticket__", rsc->priv->scheduler); } break; case loss_ticket_fence: if (!ticket_role_matches(rsc, rsc_ticket)) { return; } resource_location(rsc, NULL, -PCMK_SCORE_INFINITY, "__loss_of_ticket__", rsc->priv->scheduler); for (iter = rsc->priv->active_nodes; iter != NULL; iter = iter->next) { pe_fence_node(rsc->priv->scheduler, (pcmk_node_t *) iter->data, "deadman ticket was lost", FALSE); } break; case loss_ticket_freeze: if (!ticket_role_matches(rsc, rsc_ticket)) { return; } if (rsc->priv->active_nodes != NULL) { pcmk__clear_rsc_flags(rsc, pcmk__rsc_managed); pcmk__set_rsc_flags(rsc, pcmk__rsc_blocked); } break; } } else if (!pcmk_is_set(rsc_ticket->ticket->flags, pcmk__ticket_granted)) { if ((rsc_ticket->role != pcmk_role_promoted) || (rsc_ticket->loss_policy == loss_ticket_stop)) { resource_location(rsc, NULL, -PCMK_SCORE_INFINITY, "__no_ticket__", rsc->priv->scheduler); } } else if (pcmk_is_set(rsc_ticket->ticket->flags, pcmk__ticket_standby)) { if ((rsc_ticket->role != pcmk_role_promoted) || (rsc_ticket->loss_policy == loss_ticket_stop)) { resource_location(rsc, NULL, -PCMK_SCORE_INFINITY, "__ticket_standby__", rsc->priv->scheduler); } } } static void rsc_ticket_new(const char *id, pcmk_resource_t *rsc, pcmk__ticket_t *ticket, const char *state, const char *loss_policy) { rsc_ticket_t *new_rsc_ticket = NULL; if (rsc == NULL) { pcmk__config_err("Ignoring ticket '%s' because resource " "does not exist", id); return; } new_rsc_ticket = calloc(1, sizeof(rsc_ticket_t)); if (new_rsc_ticket == NULL) { return; } if (pcmk__str_eq(state, PCMK_ROLE_STARTED, pcmk__str_null_matches|pcmk__str_casei)) { state = PCMK__ROLE_UNKNOWN; } new_rsc_ticket->id = id; new_rsc_ticket->ticket = ticket; new_rsc_ticket->rsc = rsc; new_rsc_ticket->role = pcmk_parse_role(state); if (pcmk__str_eq(loss_policy, PCMK_VALUE_FENCE, pcmk__str_casei)) { if (pcmk_is_set(rsc->priv->scheduler->flags, pcmk__sched_fencing_enabled)) { new_rsc_ticket->loss_policy = loss_ticket_fence; } else { pcmk__config_err("Resetting '" PCMK_XA_LOSS_POLICY "' " "for ticket '%s' to '" PCMK_VALUE_STOP "' " "because fencing is not configured", ticket->id); loss_policy = PCMK_VALUE_STOP; } } if (new_rsc_ticket->loss_policy == loss_ticket_fence) { crm_debug("On loss of ticket '%s': Fence the nodes running %s (%s)", new_rsc_ticket->ticket->id, new_rsc_ticket->rsc->id, pcmk_role_text(new_rsc_ticket->role)); } else if (pcmk__str_eq(loss_policy, PCMK_VALUE_FREEZE, pcmk__str_casei)) { crm_debug("On loss of ticket '%s': Freeze %s (%s)", new_rsc_ticket->ticket->id, new_rsc_ticket->rsc->id, pcmk_role_text(new_rsc_ticket->role)); new_rsc_ticket->loss_policy = loss_ticket_freeze; } else if (pcmk__str_eq(loss_policy, PCMK_VALUE_DEMOTE, pcmk__str_casei)) { crm_debug("On loss of ticket '%s': Demote %s (%s)", new_rsc_ticket->ticket->id, new_rsc_ticket->rsc->id, pcmk_role_text(new_rsc_ticket->role)); new_rsc_ticket->loss_policy = loss_ticket_demote; } else if (pcmk__str_eq(loss_policy, PCMK_VALUE_STOP, pcmk__str_casei)) { crm_debug("On loss of ticket '%s': Stop %s (%s)", new_rsc_ticket->ticket->id, new_rsc_ticket->rsc->id, pcmk_role_text(new_rsc_ticket->role)); new_rsc_ticket->loss_policy = loss_ticket_stop; } else { if (new_rsc_ticket->role == pcmk_role_promoted) { crm_debug("On loss of ticket '%s': Default to demote %s (%s)", new_rsc_ticket->ticket->id, new_rsc_ticket->rsc->id, pcmk_role_text(new_rsc_ticket->role)); new_rsc_ticket->loss_policy = loss_ticket_demote; } else { crm_debug("On loss of ticket '%s': Default to stop %s (%s)", new_rsc_ticket->ticket->id, new_rsc_ticket->rsc->id, pcmk_role_text(new_rsc_ticket->role)); new_rsc_ticket->loss_policy = loss_ticket_stop; } } pcmk__rsc_trace(rsc, "%s (%s) ==> %s", rsc->id, pcmk_role_text(new_rsc_ticket->role), ticket->id); rsc->priv->ticket_constraints = g_list_append(rsc->priv->ticket_constraints, new_rsc_ticket); if (!pcmk_is_set(new_rsc_ticket->ticket->flags, pcmk__ticket_granted) || pcmk_is_set(new_rsc_ticket->ticket->flags, pcmk__ticket_standby)) { constraints_for_ticket(rsc, new_rsc_ticket); } } // \return Standard Pacemaker return code static int unpack_rsc_ticket_set(xmlNode *set, pcmk__ticket_t *ticket, const char *loss_policy, pcmk_scheduler_t *scheduler) { const char *set_id = NULL; const char *role = NULL; CRM_CHECK(set != NULL, return EINVAL); CRM_CHECK(ticket != 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); return pcmk_rc_unpack_error; } role = crm_element_value(set, PCMK_XA_ROLE); for (xmlNode *xml_rsc = pcmk__xe_first_child(set, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { pcmk_resource_t *resource = NULL; 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; } pcmk__rsc_trace(resource, "Resource '%s' depends on ticket '%s'", resource->id, ticket->id); rsc_ticket_new(set_id, resource, ticket, role, loss_policy); } return pcmk_rc_ok; } static void unpack_simple_rsc_ticket(xmlNode *xml_obj, pcmk_scheduler_t *scheduler) { const char *id = NULL; const char *ticket_str = crm_element_value(xml_obj, PCMK_XA_TICKET); const char *loss_policy = crm_element_value(xml_obj, PCMK_XA_LOSS_POLICY); pcmk__ticket_t *ticket = NULL; const char *rsc_id = crm_element_value(xml_obj, PCMK_XA_RSC); const char *state = crm_element_value(xml_obj, PCMK_XA_RSC_ROLE); // @COMPAT: Deprecated since 2.1.5 const char *instance = crm_element_value(xml_obj, PCMK__XA_RSC_INSTANCE); pcmk_resource_t *rsc = NULL; if (instance != NULL) { pcmk__warn_once(pcmk__wo_coloc_inst, "Support for " PCMK__XA_RSC_INSTANCE " is deprecated " "and will be removed in a future release"); } CRM_CHECK(xml_obj != NULL, return); id = pcmk__xe_id(xml_obj); if (id == NULL) { pcmk__config_err("Ignoring <%s> constraint without " PCMK_XA_ID, xml_obj->name); return; } if (ticket_str == NULL) { pcmk__config_err("Ignoring constraint '%s' without ticket specified", id); return; } else { ticket = g_hash_table_lookup(scheduler->priv->ticket_constraints, ticket_str); } if (ticket == NULL) { pcmk__config_err("Ignoring constraint '%s' because ticket '%s' " "does not exist", id, ticket_str); return; } if (rsc_id == NULL) { pcmk__config_err("Ignoring constraint '%s' without resource", id); return; } else { rsc = pcmk__find_constraint_resource(scheduler->priv->resources, rsc_id); } if (rsc == NULL) { pcmk__config_err("Ignoring constraint '%s' because resource '%s' " "does not exist", id, rsc_id); return; } else if ((instance != NULL) && !pcmk__is_clone(rsc)) { pcmk__config_err("Ignoring constraint '%s' because resource '%s' " "is not a clone but instance '%s' was requested", id, rsc_id, instance); return; } if (instance != NULL) { rsc = find_clone_instance(rsc, instance); if (rsc == NULL) { pcmk__config_warn("Ignoring constraint '%s' because resource '%s' " "does not have an instance '%s'", - "'%s'", id, rsc_id, instance); + id, rsc_id, instance); return; } } rsc_ticket_new(id, rsc, ticket, state, loss_policy); } // \return Standard Pacemaker return code static int unpack_rsc_ticket_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 rsc_ticket"); 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 or tag is referenced return pcmk_rc_ok; } state = crm_element_value(xml_obj, PCMK_XA_RSC_ROLE); *expanded_xml = pcmk__xml_copy(NULL, xml_obj); /* Convert any template or tag reference in "rsc" into ticket * 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 a * PCMK_XA_ROLE attribute */ crm_xml_add(rsc_set, PCMK_XA_ROLE, state); pcmk__xe_remove_attr(*expanded_xml, PCMK_XA_RSC_ROLE); } } else { pcmk__xml_free(*expanded_xml); *expanded_xml = NULL; } return pcmk_rc_ok; } void pcmk__unpack_rsc_ticket(xmlNode *xml_obj, pcmk_scheduler_t *scheduler) { xmlNode *set = NULL; bool any_sets = false; const char *id = NULL; const char *ticket_str = NULL; pcmk__ticket_t *ticket = NULL; xmlNode *orig_xml = NULL; xmlNode *expanded_xml = NULL; CRM_CHECK(xml_obj != NULL, return); id = pcmk__xe_id(xml_obj); if (id == NULL) { pcmk__config_err("Ignoring <%s> constraint without " PCMK_XA_ID, xml_obj->name); return; } if (scheduler->priv->ticket_constraints == NULL) { scheduler->priv->ticket_constraints = pcmk__strkey_table(free, destroy_ticket); } ticket_str = crm_element_value(xml_obj, PCMK_XA_TICKET); if (ticket_str == NULL) { pcmk__config_err("Ignoring constraint '%s' without ticket", id); return; } else { ticket = g_hash_table_lookup(scheduler->priv->ticket_constraints, ticket_str); } if (ticket == NULL) { ticket = ticket_new(ticket_str, scheduler); if (ticket == NULL) { return; } } if (unpack_rsc_ticket_tags(xml_obj, &expanded_xml, scheduler) != pcmk_rc_ok) { return; } if (expanded_xml != NULL) { 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)) { const char *loss_policy = NULL; any_sets = true; set = pcmk__xe_resolve_idref(set, scheduler->input); loss_policy = crm_element_value(xml_obj, PCMK_XA_LOSS_POLICY); if ((set == NULL) // Configuration error, message already logged || (unpack_rsc_ticket_set(set, ticket, loss_policy, scheduler) != pcmk_rc_ok)) { if (expanded_xml != NULL) { pcmk__xml_free(expanded_xml); } return; } } if (expanded_xml) { pcmk__xml_free(expanded_xml); xml_obj = orig_xml; } if (!any_sets) { unpack_simple_rsc_ticket(xml_obj, scheduler); } } /*! * \internal * \brief Ban resource from a node if it doesn't have a promotion ticket * * If a resource has tickets for the promoted role, and the ticket is either not * granted or set to standby, then ban the resource from all nodes. * * \param[in,out] rsc Resource to check */ void pcmk__require_promotion_tickets(pcmk_resource_t *rsc) { for (GList *item = rsc->priv->ticket_constraints; item != NULL; item = item->next) { rsc_ticket_t *rsc_ticket = (rsc_ticket_t *) item->data; if ((rsc_ticket->role == pcmk_role_promoted) && (!pcmk_is_set(rsc_ticket->ticket->flags, pcmk__ticket_granted) || pcmk_is_set(rsc_ticket->ticket->flags, pcmk__ticket_standby))) { resource_location(rsc, NULL, -PCMK_SCORE_INFINITY, "__stateful_without_ticket__", rsc->priv->scheduler); } } } diff --git a/lib/pacemaker/pcmk_sched_utilization.c b/lib/pacemaker/pcmk_sched_utilization.c index 2bf6352540..162645cedf 100644 --- a/lib/pacemaker/pcmk_sched_utilization.c +++ b/lib/pacemaker/pcmk_sched_utilization.c @@ -1,477 +1,477 @@ /* * Copyright 2014-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include #include "libpacemaker_private.h" /*! * \internal * \brief Get integer utilization from a string * * \param[in] s String representation of a node utilization value * * \return Integer equivalent of \p s * \todo It would make sense to restrict utilization values to nonnegative * integers, but the documentation just says "integers" and we didn't * restrict them initially, so for backward compatibility, allow any * integer. */ static int utilization_value(const char *s) { int value = 0; if ((s != NULL) && (pcmk__scan_min_int(s, &value, INT_MIN) == EINVAL)) { pcmk__config_warn("Using 0 for utilization instead of " - "invalid value '%s'", value); + "invalid value '%s'", s); value = 0; } return value; } /* * Functions for comparing node capacities */ struct compare_data { const pcmk_node_t *node1; const pcmk_node_t *node2; bool node2_only; int result; }; /*! * \internal * \brief Compare a single utilization attribute for two nodes * * Compare one utilization attribute for two nodes, decrementing the result if * the first node has greater capacity, and incrementing it if the second node * has greater capacity. * * \param[in] key Utilization attribute name to compare * \param[in] value Utilization attribute value to compare * \param[in,out] user_data Comparison data (as struct compare_data*) */ static void compare_utilization_value(gpointer key, gpointer value, gpointer user_data) { int node1_capacity = 0; int node2_capacity = 0; struct compare_data *data = user_data; const char *node2_value = NULL; if (data->node2_only) { if (g_hash_table_lookup(data->node1->priv->utilization, key)) { return; // We've already compared this attribute } } else { node1_capacity = utilization_value((const char *) value); } node2_value = g_hash_table_lookup(data->node2->priv->utilization, key); node2_capacity = utilization_value(node2_value); if (node1_capacity > node2_capacity) { data->result--; } else if (node1_capacity < node2_capacity) { data->result++; } } /*! * \internal * \brief Compare utilization capacities of two nodes * * \param[in] node1 First node to compare * \param[in] node2 Second node to compare * * \return Negative integer if node1 has more free capacity, * 0 if the capacities are equal, or a positive integer * if node2 has more free capacity */ int pcmk__compare_node_capacities(const pcmk_node_t *node1, const pcmk_node_t *node2) { struct compare_data data = { .node1 = node1, .node2 = node2, .node2_only = false, .result = 0, }; // Compare utilization values that node1 and maybe node2 have g_hash_table_foreach(node1->priv->utilization, compare_utilization_value, &data); // Compare utilization values that only node2 has data.node2_only = true; g_hash_table_foreach(node2->priv->utilization, compare_utilization_value, &data); return data.result; } /* * Functions for updating node capacities */ struct calculate_data { GHashTable *current_utilization; bool plus; }; /*! * \internal * \brief Update a single utilization attribute with a new value * * \param[in] key Name of utilization attribute to update * \param[in] value Value to add or substract * \param[in,out] user_data Calculation data (as struct calculate_data *) */ static void update_utilization_value(gpointer key, gpointer value, gpointer user_data) { int result = 0; const char *current = NULL; struct calculate_data *data = user_data; current = g_hash_table_lookup(data->current_utilization, key); if (data->plus) { result = utilization_value(current) + utilization_value(value); } else if (current) { result = utilization_value(current) - utilization_value(value); } g_hash_table_replace(data->current_utilization, strdup(key), pcmk__itoa(result)); } /*! * \internal * \brief Subtract a resource's utilization from node capacity * * \param[in,out] current_utilization Current node utilization attributes * \param[in] rsc Resource with utilization to subtract */ void pcmk__consume_node_capacity(GHashTable *current_utilization, const pcmk_resource_t *rsc) { struct calculate_data data = { .current_utilization = current_utilization, .plus = false, }; g_hash_table_foreach(rsc->priv->utilization, update_utilization_value, &data); } /*! * \internal * \brief Add a resource's utilization to node capacity * * \param[in,out] current_utilization Current node utilization attributes * \param[in] rsc Resource with utilization to add */ void pcmk__release_node_capacity(GHashTable *current_utilization, const pcmk_resource_t *rsc) { struct calculate_data data = { .current_utilization = current_utilization, .plus = true, }; g_hash_table_foreach(rsc->priv->utilization, update_utilization_value, &data); } /* * Functions for checking for sufficient node capacity */ struct capacity_data { const pcmk_node_t *node; const char *rsc_id; bool is_enough; }; /*! * \internal * \brief Check whether a single utilization attribute has sufficient capacity * * \param[in] key Name of utilization attribute to check * \param[in] value Amount of utilization required * \param[in,out] user_data Capacity data (as struct capacity_data *) */ static void check_capacity(gpointer key, gpointer value, gpointer user_data) { int required = 0; int remaining = 0; const char *node_value_s = NULL; struct capacity_data *data = user_data; node_value_s = g_hash_table_lookup(data->node->priv->utilization, key); required = utilization_value(value); remaining = utilization_value(node_value_s); if (required > remaining) { crm_debug("Remaining capacity for %s on %s (%d) is insufficient " "for resource %s usage (%d)", (const char *) key, pcmk__node_name(data->node), remaining, data->rsc_id, required); data->is_enough = false; } } /*! * \internal * \brief Check whether a node has sufficient capacity for a resource * * \param[in] node Node to check * \param[in] rsc_id ID of resource to check (for debug logs only) * \param[in] utilization Required utilization amounts * * \return true if node has sufficient capacity for resource, otherwise false */ static bool have_enough_capacity(const pcmk_node_t *node, const char *rsc_id, GHashTable *utilization) { struct capacity_data data = { .node = node, .rsc_id = rsc_id, .is_enough = true, }; g_hash_table_foreach(utilization, check_capacity, &data); return data.is_enough; } /*! * \internal * \brief Sum the utilization requirements of a list of resources * * \param[in] orig_rsc Resource being assigned (for logging purposes) * \param[in] rscs Resources whose utilization should be summed * * \return Newly allocated hash table with sum of all utilization values * \note It is the caller's responsibility to free the return value using * g_hash_table_destroy(). */ static GHashTable * sum_resource_utilization(const pcmk_resource_t *orig_rsc, GList *rscs) { GHashTable *utilization = pcmk__strkey_table(free, free); for (GList *iter = rscs; iter != NULL; iter = iter->next) { pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data; rsc->priv->cmds->add_utilization(rsc, orig_rsc, rscs, utilization); } return utilization; } /*! * \internal * \brief Ban resource from nodes with insufficient utilization capacity * * \param[in,out] rsc Resource to check * * \return Allowed node for \p rsc with most spare capacity, if there are no * nodes with enough capacity for \p rsc and all its colocated resources */ const pcmk_node_t * pcmk__ban_insufficient_capacity(pcmk_resource_t *rsc) { bool any_capable = false; char *rscs_id = NULL; pcmk_node_t *node = NULL; const pcmk_node_t *most_capable_node = NULL; GList *colocated_rscs = NULL; GHashTable *unassigned_utilization = NULL; GHashTableIter iter; CRM_CHECK(rsc != NULL, return NULL); // The default placement strategy ignores utilization if (pcmk__str_eq(rsc->priv->scheduler->priv->placement_strategy, PCMK_VALUE_DEFAULT, pcmk__str_casei)) { return NULL; } // Check whether any resources are colocated with this one colocated_rscs = rsc->priv->cmds->colocated_resources(rsc, NULL, NULL); if (colocated_rscs == NULL) { return NULL; } rscs_id = crm_strdup_printf("%s and its colocated resources", rsc->id); // If rsc isn't in the list, add it so we include its utilization if (g_list_find(colocated_rscs, rsc) == NULL) { colocated_rscs = g_list_append(colocated_rscs, rsc); } // Sum utilization of colocated resources that haven't been assigned yet unassigned_utilization = sum_resource_utilization(rsc, colocated_rscs); // Check whether any node has enough capacity for all the resources g_hash_table_iter_init(&iter, rsc->priv->allowed_nodes); while (g_hash_table_iter_next(&iter, NULL, (void **) &node)) { if (!pcmk__node_available(node, true, false)) { continue; } if (have_enough_capacity(node, rscs_id, unassigned_utilization)) { any_capable = true; } // Keep track of node with most free capacity if ((most_capable_node == NULL) || (pcmk__compare_node_capacities(node, most_capable_node) < 0)) { most_capable_node = node; } } if (any_capable) { // If so, ban resource from any node with insufficient capacity g_hash_table_iter_init(&iter, rsc->priv->allowed_nodes); while (g_hash_table_iter_next(&iter, NULL, (void **) &node)) { if (pcmk__node_available(node, true, false) && !have_enough_capacity(node, rscs_id, unassigned_utilization)) { pcmk__rsc_debug(rsc, "%s does not have enough capacity for %s", pcmk__node_name(node), rscs_id); resource_location(rsc, node, -PCMK_SCORE_INFINITY, "__limit_utilization__", rsc->priv->scheduler); } } most_capable_node = NULL; } else { // Otherwise, ban from nodes with insufficient capacity for rsc alone g_hash_table_iter_init(&iter, rsc->priv->allowed_nodes); while (g_hash_table_iter_next(&iter, NULL, (void **) &node)) { if (pcmk__node_available(node, true, false) && !have_enough_capacity(node, rsc->id, rsc->priv->utilization)) { pcmk__rsc_debug(rsc, "%s does not have enough capacity for %s", pcmk__node_name(node), rsc->id); resource_location(rsc, node, -PCMK_SCORE_INFINITY, "__limit_utilization__", rsc->priv->scheduler); } } } g_hash_table_destroy(unassigned_utilization); g_list_free(colocated_rscs); free(rscs_id); pe__show_node_scores(true, rsc, "Post-utilization", rsc->priv->allowed_nodes, rsc->priv->scheduler); return most_capable_node; } /*! * \internal * \brief Create a new load_stopped pseudo-op for a node * * \param[in,out] node Node to create op for * * \return Newly created load_stopped op */ static pcmk_action_t * new_load_stopped_op(pcmk_node_t *node) { char *load_stopped_task = crm_strdup_printf(PCMK_ACTION_LOAD_STOPPED "_%s", node->priv->name); pcmk_action_t *load_stopped = get_pseudo_op(load_stopped_task, node->priv->scheduler); if (load_stopped->node == NULL) { load_stopped->node = pe__copy_node(node); pcmk__clear_action_flags(load_stopped, pcmk__action_optional); } free(load_stopped_task); return load_stopped; } /*! * \internal * \brief Create utilization-related internal constraints for a resource * * \param[in,out] rsc Resource to create constraints for * \param[in] allowed_nodes List of allowed next nodes for \p rsc */ void pcmk__create_utilization_constraints(pcmk_resource_t *rsc, const GList *allowed_nodes) { const GList *iter = NULL; pcmk_action_t *load_stopped = NULL; pcmk__rsc_trace(rsc, "Creating utilization constraints for %s - strategy: %s", rsc->id, rsc->priv->scheduler->priv->placement_strategy); // "stop rsc then load_stopped" constraints for current nodes for (iter = rsc->priv->active_nodes; iter != NULL; iter = iter->next) { load_stopped = new_load_stopped_op(iter->data); pcmk__new_ordering(rsc, stop_key(rsc), NULL, NULL, NULL, load_stopped, pcmk__ar_if_on_same_node_or_target, rsc->priv->scheduler); } // "load_stopped then start/migrate_to rsc" constraints for allowed nodes for (iter = allowed_nodes; iter; iter = iter->next) { load_stopped = new_load_stopped_op(iter->data); pcmk__new_ordering(NULL, NULL, load_stopped, rsc, start_key(rsc), NULL, pcmk__ar_if_on_same_node_or_target, rsc->priv->scheduler); pcmk__new_ordering(NULL, NULL, load_stopped, rsc, pcmk__op_key(rsc->id, PCMK_ACTION_MIGRATE_TO, 0), NULL, pcmk__ar_if_on_same_node_or_target, rsc->priv->scheduler); } } /*! * \internal * \brief Output node capacities if enabled * * \param[in] desc Prefix for output * \param[in,out] scheduler Scheduler data */ void pcmk__show_node_capacities(const char *desc, pcmk_scheduler_t *scheduler) { if (!pcmk_is_set(scheduler->flags, pcmk__sched_show_utilization)) { return; } for (const GList *iter = scheduler->nodes; iter != NULL; iter = iter->next) { const pcmk_node_t *node = (const pcmk_node_t *) iter->data; pcmk__output_t *out = scheduler->priv->out; out->message(out, "node-capacity", node, desc); } } diff --git a/tools/crm_resource_ban.c b/tools/crm_resource_ban.c index 4910d3fe6e..4c6f13d23d 100644 --- a/tools/crm_resource_ban.c +++ b/tools/crm_resource_ban.c @@ -1,514 +1,515 @@ /* * 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 static char * parse_cli_lifetime(pcmk__output_t *out, const char *move_lifetime) { char *later_s = NULL; crm_time_t *now = NULL; crm_time_t *later = NULL; crm_time_t *duration = NULL; if (move_lifetime == NULL) { return NULL; } duration = crm_time_parse_duration(move_lifetime); if (duration == NULL) { out->err(out, "Invalid duration specified: %s\n" "Please refer to https://en.wikipedia.org/wiki/ISO_8601#Durations " "for examples of valid durations", move_lifetime); return NULL; } now = crm_time_new(NULL); later = crm_time_add(now, duration); if (later == NULL) { out->err(out, "Unable to add %s to current time\n" "Please report to " PACKAGE_BUGREPORT " as possible bug", move_lifetime); crm_time_free(now); crm_time_free(duration); return NULL; } crm_time_log(LOG_INFO, "now ", now, crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone); crm_time_log(LOG_INFO, "later ", later, crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone); crm_time_log(LOG_INFO, "duration", duration, crm_time_log_date | crm_time_log_timeofday); later_s = crm_time_as_string(later, crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone); out->info(out, "Migration will take effect until: %s", later_s); crm_time_free(duration); crm_time_free(later); crm_time_free(now); return later_s; } // \return Standard Pacemaker return code int cli_resource_ban(pcmk__output_t *out, const char *rsc_id, const char *host, const char *move_lifetime, cib_t * cib_conn, int cib_options, gboolean promoted_role_only, const char *promoted_role) { char *later_s = NULL; int rc = pcmk_rc_ok; xmlNode *fragment = NULL; xmlNode *location = NULL; later_s = parse_cli_lifetime(out, move_lifetime); if(move_lifetime && later_s == NULL) { return EINVAL; } fragment = pcmk__xe_create(NULL, PCMK_XE_CONSTRAINTS); location = pcmk__xe_create(fragment, PCMK_XE_RSC_LOCATION); pcmk__xe_set_id(location, "cli-ban-%s-on-%s", rsc_id, host); out->info(out, "WARNING: Creating " PCMK_XE_RSC_LOCATION " constraint '%s' with " "a score of " PCMK_VALUE_MINUS_INFINITY " for resource %s on %s." "\n\tThis will prevent %s from %s on %s until the constraint is " "removed using the clear option or by editing the CIB with an " "appropriate tool.\n" "\tThis will be the case even if %s is the last node in the " "cluster", pcmk__xe_id(location), rsc_id, host, rsc_id, (promoted_role_only? "being promoted" : "running"), host, host); crm_xml_add(location, PCMK_XA_RSC, rsc_id); if(promoted_role_only) { crm_xml_add(location, PCMK_XA_ROLE, promoted_role); } else { crm_xml_add(location, PCMK_XA_ROLE, PCMK_ROLE_STARTED); } if (later_s == NULL) { /* Short form */ crm_xml_add(location, PCMK_XE_NODE, host); crm_xml_add(location, PCMK_XA_SCORE, PCMK_VALUE_MINUS_INFINITY); } else { xmlNode *rule = pcmk__xe_create(location, PCMK_XE_RULE); xmlNode *expr = pcmk__xe_create(rule, PCMK_XE_EXPRESSION); pcmk__xe_set_id(rule, "cli-ban-%s-on-%s-rule", rsc_id, host); crm_xml_add(rule, PCMK_XA_SCORE, PCMK_VALUE_MINUS_INFINITY); crm_xml_add(rule, PCMK_XA_BOOLEAN_OP, PCMK_VALUE_AND); pcmk__xe_set_id(expr, "cli-ban-%s-on-%s-expr", rsc_id, host); crm_xml_add(expr, PCMK_XA_ATTRIBUTE, CRM_ATTR_UNAME); crm_xml_add(expr, PCMK_XA_OPERATION, PCMK_VALUE_EQ); crm_xml_add(expr, PCMK_XA_VALUE, host); crm_xml_add(expr, PCMK_XA_TYPE, PCMK_VALUE_STRING); expr = pcmk__xe_create(rule, PCMK_XE_DATE_EXPRESSION); pcmk__xe_set_id(expr, "cli-ban-%s-on-%s-lifetime", rsc_id, host); crm_xml_add(expr, PCMK_XA_OPERATION, PCMK_VALUE_LT); crm_xml_add(expr, PCMK_XA_END, later_s); } crm_log_xml_notice(fragment, "Modify"); rc = cib_conn->cmds->modify(cib_conn, PCMK_XE_CONSTRAINTS, fragment, cib_options); rc = pcmk_legacy2rc(rc); pcmk__xml_free(fragment); free(later_s); if ((rc != pcmk_rc_ok) && promoted_role_only && (strcmp(promoted_role, PCMK_ROLE_PROMOTED) == 0)) { int banrc = cli_resource_ban(out, rsc_id, host, move_lifetime, cib_conn, cib_options, promoted_role_only, PCMK__ROLE_PROMOTED_LEGACY); if (banrc == pcmk_rc_ok) { rc = banrc; } } return rc; } // \return Standard Pacemaker return code int cli_resource_prefer(pcmk__output_t *out,const char *rsc_id, const char *host, const char *move_lifetime, cib_t *cib_conn, int cib_options, gboolean promoted_role_only, const char *promoted_role) { char *later_s = parse_cli_lifetime(out, move_lifetime); int rc = pcmk_rc_ok; xmlNode *location = NULL; xmlNode *fragment = NULL; if(move_lifetime && later_s == NULL) { return EINVAL; } if(cib_conn == NULL) { free(later_s); return ENOTCONN; } fragment = pcmk__xe_create(NULL, PCMK_XE_CONSTRAINTS); location = pcmk__xe_create(fragment, PCMK_XE_RSC_LOCATION); pcmk__xe_set_id(location, "cli-prefer-%s", rsc_id); crm_xml_add(location, PCMK_XA_RSC, rsc_id); if(promoted_role_only) { crm_xml_add(location, PCMK_XA_ROLE, promoted_role); } else { crm_xml_add(location, PCMK_XA_ROLE, PCMK_ROLE_STARTED); } if (later_s == NULL) { /* Short form */ crm_xml_add(location, PCMK_XE_NODE, host); crm_xml_add(location, PCMK_XA_SCORE, PCMK_VALUE_INFINITY); } else { xmlNode *rule = pcmk__xe_create(location, PCMK_XE_RULE); xmlNode *expr = pcmk__xe_create(rule, PCMK_XE_EXPRESSION); pcmk__xe_set_id(rule, "cli-prefer-rule-%s", rsc_id); crm_xml_add(rule, PCMK_XA_SCORE, PCMK_VALUE_INFINITY); crm_xml_add(rule, PCMK_XA_BOOLEAN_OP, PCMK_VALUE_AND); pcmk__xe_set_id(expr, "cli-prefer-expr-%s", rsc_id); crm_xml_add(expr, PCMK_XA_ATTRIBUTE, CRM_ATTR_UNAME); crm_xml_add(expr, PCMK_XA_OPERATION, PCMK_VALUE_EQ); crm_xml_add(expr, PCMK_XA_VALUE, host); crm_xml_add(expr, PCMK_XA_TYPE, PCMK_VALUE_STRING); expr = pcmk__xe_create(rule, PCMK_XE_DATE_EXPRESSION); pcmk__xe_set_id(expr, "cli-prefer-lifetime-end-%s", rsc_id); crm_xml_add(expr, PCMK_XA_OPERATION, PCMK_VALUE_LT); crm_xml_add(expr, PCMK_XA_END, later_s); } crm_log_xml_info(fragment, "Modify"); rc = cib_conn->cmds->modify(cib_conn, PCMK_XE_CONSTRAINTS, fragment, cib_options); rc = pcmk_legacy2rc(rc); pcmk__xml_free(fragment); free(later_s); if ((rc != pcmk_rc_ok) && promoted_role_only && (strcmp(promoted_role, PCMK_ROLE_PROMOTED) == 0)) { int preferrc = cli_resource_prefer(out, rsc_id, host, move_lifetime, cib_conn, cib_options, promoted_role_only, PCMK__ROLE_PROMOTED_LEGACY); if (preferrc == pcmk_rc_ok) { rc = preferrc; } } return rc; } /* Nodes can be specified two different ways in the CIB, so we have two different * functions to try clearing out any constraints on them: * * (1) The node could be given by attribute=/value= in an expression XML node. * That's what resource_clear_node_in_expr handles. That XML looks like this: * * * * * * * * * (2) The node could be given by node= in a PCMK_XE_RSC_LOCATION XML node. * That's what resource_clear_node_in_location handles. That XML looks like * this: * * * * \return Standard Pacemaker return code */ static int resource_clear_node_in_expr(const char *rsc_id, const char *host, cib_t * cib_conn, int cib_options) { int rc = pcmk_rc_ok; char *xpath_string = NULL; #define XPATH_FMT \ "//" PCMK_XE_RSC_LOCATION "[@" PCMK_XA_ID "='cli-prefer-%s']" \ "[" PCMK_XE_RULE \ "[@" PCMK_XA_ID "='cli-prefer-rule-%s']" \ "/" PCMK_XE_EXPRESSION \ "[@" PCMK_XA_ATTRIBUTE "='" CRM_ATTR_UNAME "' " \ "and @" PCMK_XA_VALUE "='%s']" \ "]" xpath_string = crm_strdup_printf(XPATH_FMT, rsc_id, rsc_id, host); rc = cib_conn->cmds->remove(cib_conn, xpath_string, NULL, cib_xpath | cib_options); if (rc == -ENXIO) { rc = pcmk_rc_ok; } else { rc = pcmk_legacy2rc(rc); } free(xpath_string); return rc; } // \return Standard Pacemaker return code static int resource_clear_node_in_location(const char *rsc_id, const char *host, cib_t * cib_conn, int cib_options, bool clear_ban_constraints, gboolean force) { int rc = pcmk_rc_ok; xmlNode *fragment = NULL; xmlNode *location = NULL; fragment = pcmk__xe_create(NULL, PCMK_XE_CONSTRAINTS); if (clear_ban_constraints == TRUE) { location = pcmk__xe_create(fragment, PCMK_XE_RSC_LOCATION); pcmk__xe_set_id(location, "cli-ban-%s-on-%s", rsc_id, host); } location = pcmk__xe_create(fragment, PCMK_XE_RSC_LOCATION); pcmk__xe_set_id(location, "cli-prefer-%s", rsc_id); if (force == FALSE) { crm_xml_add(location, PCMK_XE_NODE, host); } crm_log_xml_info(fragment, "Delete"); rc = cib_conn->cmds->remove(cib_conn, PCMK_XE_CONSTRAINTS, fragment, cib_options); if (rc == -ENXIO) { rc = pcmk_rc_ok; } else { rc = pcmk_legacy2rc(rc); } pcmk__xml_free(fragment); return rc; } // \return Standard Pacemaker return code int cli_resource_clear(const char *rsc_id, const char *host, GList *allnodes, cib_t * cib_conn, int cib_options, bool clear_ban_constraints, gboolean force) { int rc = pcmk_rc_ok; if(cib_conn == NULL) { return ENOTCONN; } if (host) { rc = resource_clear_node_in_expr(rsc_id, host, cib_conn, cib_options); /* rc does not tell us whether the previous operation did anything, only * whether it failed or not. Thus, as long as it did not fail, we need * to try the second clear method. */ if (rc == pcmk_rc_ok) { rc = resource_clear_node_in_location(rsc_id, host, cib_conn, cib_options, clear_ban_constraints, force); } } else { GList *n = allnodes; /* Iterate over all nodes, attempting to clear the constraint from each. * On the first error, abort. */ for(; n; n = n->next) { pcmk_node_t *target = n->data; rc = cli_resource_clear(rsc_id, target->priv->name, NULL, cib_conn, cib_options, clear_ban_constraints, force); if (rc != pcmk_rc_ok) { break; } } } return rc; } static void build_clear_xpath_string(GString *buf, const xmlNode *constraint_node, const char *rsc, const char *node, bool promoted_role_only) { const char *cons_id = pcmk__xe_id(constraint_node); const char *cons_rsc = crm_element_value(constraint_node, PCMK_XA_RSC); GString *rsc_role_substr = NULL; const char *promoted_role_rule = "@" PCMK_XA_ROLE "='" PCMK_ROLE_PROMOTED "' or @" PCMK_XA_ROLE "='" PCMK__ROLE_PROMOTED_LEGACY "'"; CRM_ASSERT(buf != NULL); g_string_truncate(buf, 0); if (!pcmk__starts_with(cons_id, "cli-ban-") && !pcmk__starts_with(cons_id, "cli-prefer-")) { return; } g_string_append(buf, "//" PCMK_XE_RSC_LOCATION); if ((node != NULL) || (rsc != NULL) || promoted_role_only) { g_string_append_c(buf, '['); if (node != NULL) { pcmk__g_strcat(buf, "@" PCMK_XE_NODE "='", node, "'", NULL); if (promoted_role_only || (rsc != NULL)) { g_string_append(buf, " and "); } } if ((rsc != NULL) && promoted_role_only) { rsc_role_substr = g_string_sized_new(64); pcmk__g_strcat(rsc_role_substr, "@" PCMK_XA_RSC "='", rsc, "' " "and (" , promoted_role_rule, ")", NULL); } else if (rsc != NULL) { rsc_role_substr = g_string_sized_new(64); pcmk__g_strcat(rsc_role_substr, "@" PCMK_XA_RSC "='", rsc, "'", NULL); } else if (promoted_role_only) { rsc_role_substr = g_string_sized_new(64); g_string_append(rsc_role_substr, promoted_role_rule); } if (rsc_role_substr != NULL) { g_string_append(buf, rsc_role_substr->str); } g_string_append_c(buf, ']'); } if (node != NULL) { g_string_append(buf, "|//" PCMK_XE_RSC_LOCATION); if (rsc_role_substr != NULL) { pcmk__g_strcat(buf, "[", rsc_role_substr, "]", NULL); } pcmk__g_strcat(buf, "/" PCMK_XE_RULE "[" PCMK_XE_EXPRESSION "[@" PCMK_XA_ATTRIBUTE "='" CRM_ATTR_UNAME "' " "and @" PCMK_XA_VALUE "='", node, "']]", NULL); } g_string_append(buf, "//" PCMK_XE_DATE_EXPRESSION "[@" PCMK_XA_ID "='"); if (pcmk__starts_with(cons_id, "cli-ban-")) { pcmk__g_strcat(buf, cons_id, "-lifetime']", NULL); } else { // starts with "cli-prefer-" pcmk__g_strcat(buf, "cli-prefer-lifetime-end-", cons_rsc, "']", NULL); } if (rsc_role_substr != NULL) { g_string_free(rsc_role_substr, TRUE); } } // \return Standard Pacemaker return code int cli_resource_clear_all_expired(xmlNode *root, cib_t *cib_conn, int cib_options, const char *rsc, const char *node, gboolean promoted_role_only) { GString *buf = NULL; xmlXPathObject *xpathObj = NULL; xmlNode *cib_constraints = NULL; crm_time_t *now = crm_time_new(NULL); int i; int rc = pcmk_rc_ok; cib_constraints = pcmk_find_cib_element(root, PCMK_XE_CONSTRAINTS); xpathObj = xpath_search(cib_constraints, "//" PCMK_XE_RSC_LOCATION); for (i = 0; i < numXpathResults(xpathObj); i++) { xmlNode *constraint_node = getXpathResult(xpathObj, i); xmlNode *date_expr_node = NULL; crm_time_t *end = NULL; int rc = pcmk_rc_ok; if (buf == NULL) { buf = g_string_sized_new(1024); } build_clear_xpath_string(buf, constraint_node, rsc, node, promoted_role_only); if (buf->len == 0) { continue; } date_expr_node = get_xpath_object((const char *) buf->str, constraint_node, LOG_DEBUG); if (date_expr_node == NULL) { continue; } /* And then finally, see if the date expression is expired. If so, * clear the constraint. - * - * @COMPAT Check for error once we are rejecting rules with invalid end */ rc = pcmk__xe_get_datetime(date_expr_node, PCMK_XA_END, &end); if (rc != pcmk_rc_ok) { - crm_trace("Invalid " PCMK_XA_END ": %s", pcmk_rc_str(rc)); + crm_trace("Date expression %s has invalid " PCMK_XA_END ": %s", + pcmk__s(pcmk__xe_id(date_expr_node), "without ID"), + pcmk_rc_str(rc)); + continue; // Treat as unexpired } if (crm_time_compare(now, end) == 1) { xmlNode *fragment = NULL; xmlNode *location = NULL; fragment = pcmk__xe_create(NULL, PCMK_XE_CONSTRAINTS); location = pcmk__xe_create(fragment, PCMK_XE_RSC_LOCATION); pcmk__xe_set_id(location, "%s", pcmk__xe_id(constraint_node)); crm_log_xml_info(fragment, "Delete"); rc = cib_conn->cmds->remove(cib_conn, PCMK_XE_CONSTRAINTS, fragment, cib_options); rc = pcmk_legacy2rc(rc); if (rc != pcmk_rc_ok) { goto done; } pcmk__xml_free(fragment); } crm_time_free(end); } done: if (buf != NULL) { g_string_free(buf, TRUE); } freeXpathObject(xpathObj); crm_time_free(now); return rc; }