diff --git a/lib/common/rules.c b/lib/common/rules.c index 839bffb8ce..6b65198f87 100644 --- a/lib/common/rules.c +++ b/lib/common/rules.c @@ -1,1512 +1,1486 @@ /* * 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 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 } else if (pcmk__parse_ll_range(range, &low, &high) != pcmk_rc_ok) { // Invalid 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; } 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_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 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, 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 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) { 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 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 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) { 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 } /* 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; } // 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); } break; default: if (value == NULL) { pcmk__config_warn(PCMK_XE_EXPRESSION " %s has no " PCMK_XA_VALUE, id); } 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); } 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 } // 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 } // 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); 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); 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 } 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; } // 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); } 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_date_expression_test.c b/lib/common/tests/rules/pcmk__evaluate_date_expression_test.c index 97573c44ad..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,628 +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) { xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_INVALID_DURATION); 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_XA_ID "='d' " \ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \ PCMK_XA_START "='2024-02-01 12:00:00'>" \ "<" PCMK_XE_DURATION " " PCMK_XA_HOURS "='3' />" \ "" static void range_valid_start_duration_missing_id(void **state) { xmlNodePtr xml = NULL; xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_DURATION_MISSING_ID); 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) { xmlNodePtr xml = pcmk__xml_parse(EXPR_SPEC_INVALID); 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) { xmlNodePtr xml = pcmk__xml_parse(EXPR_SPEC_MISSING_ID); assert_date_expression(xml, "2024-01-01 23:59:59", NULL, NULL, 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/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; }