summaryrefslogtreecommitdiffstats
path: root/lib/common/rules.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--lib/common/rules.c1512
1 files changed, 1512 insertions, 0 deletions
diff --git a/lib/common/rules.c b/lib/common/rules.c
new file mode 100644
index 0000000..32af835
--- /dev/null
+++ b/lib/common/rules.c
@@ -0,0 +1,1512 @@
+/*
+ * 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 <crm_internal.h>
+
+#include <stdio.h> // NULL, size_t
+#include <stdbool.h> // bool
+#include <ctype.h> // isdigit()
+#include <regex.h> // regmatch_t
+#include <stdint.h> // uint32_t
+#include <inttypes.h> // PRIu32
+#include <glib.h> // gboolean, FALSE
+#include <libxml/tree.h> // xmlNode
+
+#include <crm/common/scheduler.h>
+
+#include <crm/common/iso8601_internal.h>
+#include <crm/common/nvpair_internal.h>
+#include <crm/common/scheduler_internal.h>
+#include "crmcommon_private.h"
+
+/*!
+ * \internal
+ * \brief Get the condition type corresponding to given condition XML
+ *
+ * \param[in] condition Rule condition XML
+ *
+ * \return Condition type corresponding to \p condition
+ */
+enum expression_type
+pcmk__condition_type(const xmlNode *condition)
+{
+ const char *name = NULL;
+
+ // Expression types based on element name
+
+ if (pcmk__xe_is(condition, PCMK_XE_DATE_EXPRESSION)) {
+ return pcmk__condition_datetime;
+
+ } else if (pcmk__xe_is(condition, PCMK_XE_RSC_EXPRESSION)) {
+ return pcmk__condition_resource;
+
+ } else if (pcmk__xe_is(condition, PCMK_XE_OP_EXPRESSION)) {
+ return pcmk__condition_operation;
+
+ } else if (pcmk__xe_is(condition, PCMK_XE_RULE)) {
+ return pcmk__condition_rule;
+
+ } else if (!pcmk__xe_is(condition, PCMK_XE_EXPRESSION)) {
+ return pcmk__condition_unknown;
+ }
+
+ // Expression types based on node attribute name
+
+ name = crm_element_value(condition, PCMK_XA_ATTRIBUTE);
+
+ if (pcmk__str_any_of(name, CRM_ATTR_UNAME, CRM_ATTR_KIND, CRM_ATTR_ID,
+ NULL)) {
+ return pcmk__condition_location;
+ }
+
+ return pcmk__condition_attribute;
+}
+
+/*!
+ * \internal
+ * \brief Get parent XML element's ID for logging purposes
+ *
+ * \param[in] xml XML of a subelement
+ *
+ * \return ID of \p xml's parent for logging purposes (guaranteed non-NULL)
+ */
+static const char *
+loggable_parent_id(const xmlNode *xml)
+{
+ // Default if called without parent (likely for unit testing)
+ const char *parent_id = "implied";
+
+ if ((xml != NULL) && (xml->parent != NULL)) {
+ parent_id = pcmk__xe_id(xml->parent);
+ if (parent_id == NULL) { // Not possible with schema validation enabled
+ parent_id = "without ID";
+ }
+ }
+ return parent_id;
+}
+
+/*!
+ * \internal
+ * \brief Get the moon phase corresponding to a given date/time
+ *
+ * \param[in] now Date/time to get moon phase for
+ *
+ * \return Phase of the moon corresponding to \p now, where 0 is the new moon
+ * and 7 is the full moon
+ * \deprecated This feature has been deprecated since 2.1.6.
+ */
+static int
+phase_of_the_moon(const crm_time_t *now)
+{
+ /* As per the nethack rules:
+ * - A moon period is 29.53058 days ~= 30
+ * - A year is 365.2422 days
+ * - Number of days moon phase advances on first day of year compared to
+ * preceding year is (365.2422 - 12 * 29.53058) ~= 11
+ * - Number of years until same phases fall on the same days of the month
+ * is 18.6 ~= 19
+ * - Moon phase on first day of year (epact) ~= (11 * (year%19) + 29) % 30
+ * (29 as initial condition)
+ * - Current phase in days = first day phase + days elapsed in year
+ * - 6 moons ~= 177 days ~= 8 reported phases * 22 (+ 11/22 for rounding)
+ */
+ uint32_t epact, diy, goldn;
+ uint32_t y;
+
+ crm_time_get_ordinal(now, &y, &diy);
+ goldn = (y % 19) + 1;
+ epact = (11 * goldn + 18) % 30;
+ if (((epact == 25) && (goldn > 11)) || (epact == 24)) {
+ epact++;
+ }
+ return (((((diy + epact) * 6) + 11) % 177) / 22) & 7;
+}
+
+/*!
+ * \internal
+ * \brief Check an integer value against a range from a date specification
+ *
+ * \param[in] date_spec XML of PCMK_XE_DATE_SPEC element to check
+ * \param[in] id XML ID for logging purposes
+ * \param[in] 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 = pcmk__xe_first_child(date_expression,
+ PCMK_XE_DURATION, NULL, NULL);
+
+ if (duration != NULL) {
+ /* @COMPAT When we can break behavioral backward compatibility,
+ * return the result of this if not OK
+ */
+ pcmk__unpack_duration(duration, start, &end);
+ }
+ }
+
+ 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 = 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);
+ }
+
+ } 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) || 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 = expand_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;
+}