diff --git a/include/crm/common/rules_internal.h b/include/crm/common/rules_internal.h index 451d4ba01d..75147ec2c9 100644 --- a/include/crm/common/rules_internal.h +++ b/include/crm/common/rules_internal.h @@ -1,65 +1,65 @@ /* * 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. */ #ifndef PCMK__CRM_COMMON_RULES_INTERNAL__H #define PCMK__CRM_COMMON_RULES_INTERNAL__H #include // regmatch_t #include // xmlNode #include // enum expression_type, etc. #include // crm_time_t // How node attribute values may be compared in rules enum pcmk__comparison { pcmk__comparison_unknown, pcmk__comparison_defined, pcmk__comparison_undefined, pcmk__comparison_eq, pcmk__comparison_ne, pcmk__comparison_lt, pcmk__comparison_lte, pcmk__comparison_gt, pcmk__comparison_gte, }; // How node attribute values may be parsed in rules enum pcmk__type { pcmk__type_unknown, pcmk__type_string, pcmk__type_integer, pcmk__type_number, pcmk__type_version, }; // Where to obtain reference value for a node attribute comparison enum pcmk__reference_source { pcmk__source_unknown, pcmk__source_literal, pcmk__source_instance_attrs, pcmk__source_meta_attrs, }; enum expression_type pcmk__expression_type(const xmlNode *expr); enum pcmk__comparison pcmk__parse_comparison(const char *op); enum pcmk__type pcmk__parse_type(const char *type, enum pcmk__comparison op, const char *value1, const char *value2); enum pcmk__reference_source pcmk__parse_source(const char *source); int pcmk__cmp_by_type(const char *value1, const char *value2, enum pcmk__type type); char *pcmk__replace_submatches(const char *string, const char *match, const regmatch_t submatches[], int nmatches); int pcmk__evaluate_date_expression(const xmlNode *date_expression, const crm_time_t *now, crm_time_t *next_change); -int pcmk__evaluate_attr_expression(const xmlNode *expr, +int pcmk__evaluate_attr_expression(const xmlNode *expression, const pcmk_rule_input_t *rule_input); #endif // PCMK__CRM_COMMON_RULES_INTERNAL__H diff --git a/lib/common/rules.c b/lib/common/rules.c index 848a47c9ee..9320ec51d1 100644 --- a/lib/common/rules.c +++ b/lib/common/rules.c @@ -1,1103 +1,1151 @@ /* * 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 expression type corresponding to given expression XML * * \param[in] expr Rule expression XML * * \return Expression type corresponding to \p expr */ enum expression_type pcmk__expression_type(const xmlNode *expr) { const char *name = NULL; // Expression types based on element name if (pcmk__xe_is(expr, PCMK_XE_DATE_EXPRESSION)) { return pcmk__subexpr_datetime; } else if (pcmk__xe_is(expr, PCMK_XE_RSC_EXPRESSION)) { return pcmk__subexpr_resource; } else if (pcmk__xe_is(expr, PCMK_XE_OP_EXPRESSION)) { return pcmk__subexpr_operation; } else if (pcmk__xe_is(expr, PCMK_XE_RULE)) { return pcmk__subexpr_rule; } else if (!pcmk__xe_is(expr, PCMK_XE_EXPRESSION)) { return pcmk__subexpr_unknown; } // Expression types based on node attribute name name = crm_element_value(expr, PCMK_XA_ATTRIBUTE); if (pcmk__str_any_of(name, CRM_ATTR_UNAME, CRM_ATTR_KIND, CRM_ATTR_ID, NULL)) { return pcmk__subexpr_location; } return pcmk__subexpr_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] 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 (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); } 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", 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) */ 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 } // 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); 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; \ } \ } 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"; } *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; } /*! * \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); } 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); } 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; } if (end == NULL) { xmlNode *duration = first_named_child(date_expression, PCMK_XE_DURATION); 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); } } 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; } 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; } 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; } 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; } 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; 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 } 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 = first_named_child(date_expression, PCMK_XE_DATE_SPEC); 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); } } 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); } 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)) { 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 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] expr XML of a rule's PCMK_XE_EXPRESSION subelement + * \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 + * \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 *expr, +pcmk__evaluate_attr_expression(const xmlNode *expression, const pcmk_rule_input_t *rule_input) { - gboolean attr_allocated = FALSE; - const char *h_val = NULL; - - const char *id = pcmk__xe_id(expr); - const char *attr = crm_element_value(expr, PCMK_XA_ATTRIBUTE); + const char *id = NULL; const char *op = NULL; - const char *type_s = crm_element_value(expr, PCMK_XA_TYPE); - const char *value = crm_element_value(expr, PCMK_XA_VALUE); - const char *value_source = crm_element_value(expr, PCMK_XA_VALUE_SOURCE); + 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__comparison comparison = pcmk__comparison_unknown; enum pcmk__type type = pcmk__type_unknown; + enum pcmk__reference_source source = pcmk__source_unknown; + enum pcmk__comparison comparison = pcmk__comparison_unknown; - if (attr == NULL) { - pcmk__config_err("Expression %s invalid: " PCMK_XA_ATTRIBUTE - " not specified", pcmk__s(id, "without ID")); + 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 (pcmk__str_empty(attr)) { + 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(expr, PCMK_XA_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 expression %s as not passing " - "because it has no " PCMK_XA_OPERATION, - pcmk__s(id, "without ID")); + pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not " + "passing because it has no " PCMK_XA_OPERATION, + id); } else { - pcmk__config_err("Treating expression %s as not passing " - "because '%s' is not a valid " PCMK_XA_OPERATION, - pcmk__s(id, "without ID"), op); + pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not " + "passing because '%s' is not a valid " + PCMK_XA_OPERATION, id, op); } - return pcmk_rc_unpack_error; + rc = pcmk_rc_unpack_error; + goto done; } - if (rule_input != NULL) { - char *resolved_attr = NULL; - enum pcmk__reference_source source = pcmk__source_unknown; - - // Expand any regular expression submatches (%0-%9) in attribute name - resolved_attr = pcmk__replace_submatches(attr, rule_input->rsc_id, - rule_input->rsc_id_submatches, - rule_input->rsc_id_nmatches); - if (resolved_attr != NULL) { - attr = (const char *) resolved_attr; - attr_allocated = TRUE; - } + // 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 value appropriate to PCMK_XA_VALUE_SOURCE - source = pcmk__parse_source(value_source); - 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 "')", - pcmk__s(id, "without ID"), value_source); - source = pcmk__source_literal; - } - value = value_from_source(value, source, rule_input); + // 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; - if (rule_input->node_attrs != NULL) { - h_val = (const char *)g_hash_table_lookup(rule_input->node_attrs, - attr); - } + 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); - if (attr_allocated) { - free((char *)attr); - attr = NULL; + // 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 value) - type = pcmk__parse_type(type_s, comparison, h_val, value); + // 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 expression %s because '%s' is not a " - "valid type", pcmk__s(id, "without ID"), type); + "equal for " PCMK_XE_EXPRESSION " %s because '%s' " + "is not a valid type", id, type); } - return evaluate_attr_comparison(h_val, value, type, comparison); + 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; } 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 8b960daa19..e37ec439d1 100644 --- a/lib/common/tests/rules/pcmk__evaluate_attr_expression_test.c +++ b/lib/common/tests/rules/pcmk__evaluate_attr_expression_test.c @@ -1,834 +1,831 @@ /* * 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); free_xml(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), - pcmk_rc_unpack_error); - assert_int_equal(pcmk__evaluate_attr_expression(xml, NULL), - pcmk_rc_op_unsatisfied); - assert_int_equal(pcmk__evaluate_attr_expression(NULL, &rule_input), - pcmk_rc_unpack_error); + 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); free_xml(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); } /* * 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); } 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_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); } #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 \ "<" 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) { // Currently treated as NULL reference value assert_attr_expression(EXPR_VALUE_MISSING_EQ_OK, pcmk_rc_ok); } #define expr_test(f) cmocka_unit_test_setup_teardown(f, setup, teardown) PCMK__UNIT_TEST(NULL, NULL, 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_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))