diff --git a/include/crm/pengine/rules.h b/include/crm/pengine/rules.h index aaa62625fb..ed793a45a8 100644 --- a/include/crm/pengine/rules.h +++ b/include/crm/pengine/rules.h @@ -1,83 +1,83 @@ /* - * Copyright 2004-2021 the Pacemaker project contributors + * Copyright 2004-2022 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_PENGINE_RULES__H # define PCMK__CRM_PENGINE_RULES__H # include # include # include # include #ifdef __cplusplus extern "C" { #endif enum expression_type { not_expr, nested_rule, attr_expr, loc_expr, role_expr, time_expr, version_expr, rsc_expr, op_expr }; enum expression_type find_expression_type(xmlNode * expr); gboolean pe_evaluate_rules(xmlNode *ruleset, GHashTable *node_hash, crm_time_t *now, crm_time_t *next_change); gboolean pe_test_rule(xmlNode *rule, GHashTable *node_hash, enum rsc_role_e role, crm_time_t *now, crm_time_t *next_change, pe_match_data_t *match_data); gboolean pe_test_expression(xmlNode *expr, GHashTable *node_hash, enum rsc_role_e role, crm_time_t *now, crm_time_t *next_change, pe_match_data_t *match_data); -void pe_eval_nvpairs(xmlNode *top, xmlNode *xml_obj, const char *set_name, +void pe_eval_nvpairs(xmlNode *top, const xmlNode *xml_obj, const char *set_name, pe_rule_eval_data_t *rule_data, GHashTable *hash, const char *always_first, gboolean overwrite, crm_time_t *next_change); void pe_unpack_nvpairs(xmlNode *top, xmlNode *xml_obj, const char *set_name, GHashTable *node_hash, GHashTable *hash, const char *always_first, gboolean overwrite, crm_time_t *now, crm_time_t *next_change); #if ENABLE_VERSIONED_ATTRS void pe_eval_versioned_attributes(xmlNode *top, xmlNode *xml_obj, const char *set_name, pe_rule_eval_data_t *rule_data, xmlNode *hash, crm_time_t *next_change); GHashTable *pe_unpack_versioned_parameters(xmlNode *versioned_params, const char *ra_version); #endif char *pe_expand_re_matches(const char *string, pe_re_match_data_t * match_data); gboolean pe_eval_rules(xmlNode *ruleset, pe_rule_eval_data_t *rule_data, crm_time_t *next_change); gboolean pe_eval_expr(xmlNode *rule, pe_rule_eval_data_t *rule_data, crm_time_t *next_change); gboolean pe_eval_subexpr(xmlNode *expr, pe_rule_eval_data_t *rule_data, crm_time_t *next_change); #if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1) #include #endif #ifdef __cplusplus } #endif #endif diff --git a/lib/pengine/rules.c b/lib/pengine/rules.c index 8b36fb2697..50ecb37ca4 100644 --- a/lib/pengine/rules.c +++ b/lib/pengine/rules.c @@ -1,1420 +1,1420 @@ /* * Copyright 2004-2022 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include #include CRM_TRACE_INIT_DATA(pe_rules); /*! * \brief Evaluate any rules contained by given XML element * * \param[in] xml XML element to check for rules * \param[in] node_hash Node attributes to use when evaluating expressions * \param[in] now Time to use when evaluating expressions * \param[out] next_change If not NULL, set to when evaluation will change * * \return TRUE if no rules, or any of rules present is in effect, else FALSE */ gboolean pe_evaluate_rules(xmlNode *ruleset, GHashTable *node_hash, crm_time_t *now, crm_time_t *next_change) { pe_rule_eval_data_t rule_data = { .node_hash = node_hash, .role = RSC_ROLE_UNKNOWN, .now = now, .match_data = NULL, .rsc_data = NULL, .op_data = NULL }; return pe_eval_rules(ruleset, &rule_data, next_change); } gboolean pe_test_rule(xmlNode *rule, GHashTable *node_hash, enum rsc_role_e role, crm_time_t *now, crm_time_t *next_change, pe_match_data_t *match_data) { pe_rule_eval_data_t rule_data = { .node_hash = node_hash, .role = role, .now = now, .match_data = match_data, .rsc_data = NULL, .op_data = NULL }; return pe_eval_expr(rule, &rule_data, next_change); } /*! * \brief Evaluate one rule subelement (pass/fail) * * A rule element may contain another rule, a node attribute expression, or a * date expression. Given any one of those, evaluate it and return whether it * passed. * * \param[in] expr Rule subelement XML * \param[in] node_hash Node attributes to use when evaluating expression * \param[in] role Resource role to use when evaluating expression * \param[in] now Time to use when evaluating expression * \param[out] next_change If not NULL, set to when evaluation will change * \param[in] match_data If not NULL, resource back-references and params * * \return TRUE if expression is in effect under given conditions, else FALSE */ gboolean pe_test_expression(xmlNode *expr, GHashTable *node_hash, enum rsc_role_e role, crm_time_t *now, crm_time_t *next_change, pe_match_data_t *match_data) { pe_rule_eval_data_t rule_data = { .node_hash = node_hash, .role = role, .now = now, .match_data = match_data, .rsc_data = NULL, .op_data = NULL }; return pe_eval_subexpr(expr, &rule_data, next_change); } enum expression_type find_expression_type(xmlNode * expr) { const char *tag = NULL; const char *attr = NULL; attr = crm_element_value(expr, XML_EXPR_ATTR_ATTRIBUTE); tag = crm_element_name(expr); if (pcmk__str_eq(tag, "date_expression", pcmk__str_casei)) { return time_expr; } else if (pcmk__str_eq(tag, "rsc_expression", pcmk__str_casei)) { return rsc_expr; } else if (pcmk__str_eq(tag, "op_expression", pcmk__str_casei)) { return op_expr; } else if (pcmk__str_eq(tag, XML_TAG_RULE, pcmk__str_casei)) { return nested_rule; } else if (!pcmk__str_eq(tag, "expression", pcmk__str_casei)) { return not_expr; } else if (pcmk__strcase_any_of(attr, CRM_ATTR_UNAME, CRM_ATTR_KIND, CRM_ATTR_ID, NULL)) { return loc_expr; } else if (pcmk__str_eq(attr, CRM_ATTR_ROLE, pcmk__str_casei)) { return role_expr; #if ENABLE_VERSIONED_ATTRS } else if (pcmk__str_eq(attr, CRM_ATTR_RA_VERSION, pcmk__str_casei)) { return version_expr; #endif } return attr_expr; } /* As per the nethack rules: * * moon period = 29.53058 days ~= 30, year = 365.2422 days * days moon phase advances on first day of year compared to preceding year * = 365.2422 - 12*29.53058 ~= 11 * years in Metonic cycle (time until same phases fall on the same days of * the month) = 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 * 177 ~= 8 reported phases * 22 * + 11/22 for rounding * * 0-7, with 0: new, 4: full */ static int phase_of_the_moon(crm_time_t * now) { 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); } static int check_one(xmlNode *cron_spec, const char *xml_field, uint32_t time_field) { int rc = pcmk_rc_undetermined; const char *value = crm_element_value(cron_spec, xml_field); long long low, high; if (value == NULL) { /* Return pe_date_result_undetermined if the field is missing. */ goto bail; } if (pcmk__parse_ll_range(value, &low, &high) == pcmk_rc_unknown_format) { goto bail; } else if (low == high) { /* A single number was given, not a range. */ if (time_field < low) { rc = pcmk_rc_before_range; } else if (time_field > high) { rc = pcmk_rc_after_range; } else { rc = pcmk_rc_within_range; } } else if (low != -1 && high != -1) { /* This is a range with both bounds. */ if (time_field < low) { rc = pcmk_rc_before_range; } else if (time_field > high) { rc = pcmk_rc_after_range; } else { rc = pcmk_rc_within_range; } } else if (low == -1) { /* This is a range with no starting value. */ rc = time_field <= high ? pcmk_rc_within_range : pcmk_rc_after_range; } else if (high == -1) { /* This is a range with no ending value. */ rc = time_field >= low ? pcmk_rc_within_range : pcmk_rc_before_range; } bail: if (rc == pcmk_rc_within_range) { crm_debug("Condition '%s' in %s: passed", value, xml_field); } else { crm_debug("Condition '%s' in %s: failed", value, xml_field); } return rc; } static gboolean check_passes(int rc) { /* _within_range is obvious. _undetermined is a pass because * this is the return value if a field is not given. In this * case, we just want to ignore it and check other fields to * see if they place some restriction on what can pass. */ return rc == pcmk_rc_within_range || rc == pcmk_rc_undetermined; } #define CHECK_ONE(spec, name, var) do { \ int subpart_rc = check_one(spec, name, var); \ if (check_passes(subpart_rc) == FALSE) { \ return subpart_rc; \ } \ } while (0) int pe_cron_range_satisfied(crm_time_t * now, xmlNode * cron_spec) { uint32_t h, m, s, y, d, w; CRM_CHECK(now != NULL, return pcmk_rc_op_unsatisfied); crm_time_get_gregorian(now, &y, &m, &d); CHECK_ONE(cron_spec, "years", y); CHECK_ONE(cron_spec, "months", m); CHECK_ONE(cron_spec, "monthdays", d); crm_time_get_timeofday(now, &h, &m, &s); CHECK_ONE(cron_spec, "hours", h); CHECK_ONE(cron_spec, "minutes", m); CHECK_ONE(cron_spec, "seconds", s); crm_time_get_ordinal(now, &y, &d); CHECK_ONE(cron_spec, "yeardays", d); crm_time_get_isoweek(now, &y, &w, &d); CHECK_ONE(cron_spec, "weekyears", y); CHECK_ONE(cron_spec, "weeks", w); CHECK_ONE(cron_spec, "weekdays", d); CHECK_ONE(cron_spec, "moon", phase_of_the_moon(now)); /* If we get here, either no fields were specified (which is success), or all * the fields that were specified had their conditions met (which is also a * success). Thus, the result is success. */ return pcmk_rc_ok; } static void update_field(crm_time_t *t, xmlNode *xml, const char *attr, void (*time_fn)(crm_time_t *, int)) { long long value; if ((pcmk__scan_ll(crm_element_value(xml, attr), &value, 0LL) == pcmk_rc_ok) && (value != 0LL) && (value >= INT_MIN) && (value <= INT_MAX)) { time_fn(t, (int) value); } } crm_time_t * pe_parse_xml_duration(crm_time_t * start, xmlNode * duration_spec) { crm_time_t *end = crm_time_new_undefined(); crm_time_set(end, start); update_field(end, duration_spec, "years", crm_time_add_years); update_field(end, duration_spec, "months", crm_time_add_months); update_field(end, duration_spec, "weeks", crm_time_add_weeks); update_field(end, duration_spec, "days", crm_time_add_days); update_field(end, duration_spec, "hours", crm_time_add_hours); update_field(end, duration_spec, "minutes", crm_time_add_minutes); update_field(end, duration_spec, "seconds", crm_time_add_seconds); return end; } // Set next_change to t if t is earlier static void crm_time_set_if_earlier(crm_time_t *next_change, crm_time_t *t) { if ((next_change != NULL) && (t != NULL)) { if (!crm_time_is_defined(next_change) || (crm_time_compare(t, next_change) < 0)) { crm_time_set(next_change, t); } } } // Information about a block of nvpair elements typedef struct sorted_set_s { int score; // This block's score for sorting const char *name; // This block's ID const char *special_name; // ID that should sort first xmlNode *attr_set; // This block } sorted_set_t; static gint sort_pairs(gconstpointer a, gconstpointer b) { const sorted_set_t *pair_a = a; const sorted_set_t *pair_b = b; if (a == NULL && b == NULL) { return 0; } else if (a == NULL) { return 1; } else if (b == NULL) { return -1; } if (pcmk__str_eq(pair_a->name, pair_a->special_name, pcmk__str_casei)) { return -1; } else if (pcmk__str_eq(pair_b->name, pair_a->special_name, pcmk__str_casei)) { return 1; } if (pair_a->score < pair_b->score) { return 1; } else if (pair_a->score > pair_b->score) { return -1; } return 0; } static void populate_hash(xmlNode * nvpair_list, GHashTable * hash, gboolean overwrite, xmlNode * top) { const char *name = NULL; const char *value = NULL; const char *old_value = NULL; xmlNode *list = nvpair_list; xmlNode *an_attr = NULL; name = crm_element_name(list->children); if (pcmk__str_eq(XML_TAG_ATTRS, name, pcmk__str_casei)) { list = list->children; } for (an_attr = pcmk__xe_first_child(list); an_attr != NULL; an_attr = pcmk__xe_next(an_attr)) { if (pcmk__str_eq((const char *)an_attr->name, XML_CIB_TAG_NVPAIR, pcmk__str_none)) { xmlNode *ref_nvpair = expand_idref(an_attr, top); name = crm_element_value(an_attr, XML_NVPAIR_ATTR_NAME); if (name == NULL) { name = crm_element_value(ref_nvpair, XML_NVPAIR_ATTR_NAME); } value = crm_element_value(an_attr, XML_NVPAIR_ATTR_VALUE); if (value == NULL) { value = crm_element_value(ref_nvpair, XML_NVPAIR_ATTR_VALUE); } if (name == NULL || value == NULL) { continue; } old_value = g_hash_table_lookup(hash, name); if (pcmk__str_eq(value, "#default", pcmk__str_casei)) { if (old_value) { crm_trace("Letting %s default (removing explicit value \"%s\")", name, value); g_hash_table_remove(hash, name); } continue; } else if (old_value == NULL) { crm_trace("Setting %s=\"%s\"", name, value); g_hash_table_insert(hash, strdup(name), strdup(value)); } else if (overwrite) { crm_trace("Setting %s=\"%s\" (overwriting old value \"%s\")", name, value, old_value); g_hash_table_replace(hash, strdup(name), strdup(value)); } } } } #if ENABLE_VERSIONED_ATTRS static xmlNode* get_versioned_rule(xmlNode * attr_set) { xmlNode * rule = NULL; xmlNode * expr = NULL; for (rule = pcmk__xe_first_child(attr_set); rule != NULL; rule = pcmk__xe_next(rule)) { if (pcmk__str_eq((const char *)rule->name, XML_TAG_RULE, pcmk__str_none)) { for (expr = pcmk__xe_first_child(rule); expr != NULL; expr = pcmk__xe_next(expr)) { if (find_expression_type(expr) == version_expr) { return rule; } } } } return NULL; } static void add_versioned_attributes(xmlNode * attr_set, xmlNode * versioned_attrs) { xmlNode *attr_set_copy = NULL; xmlNode *rule = NULL; xmlNode *expr = NULL; if (!attr_set || !versioned_attrs) { return; } attr_set_copy = copy_xml(attr_set); rule = get_versioned_rule(attr_set_copy); if (!rule) { free_xml(attr_set_copy); return; } expr = pcmk__xe_first_child(rule); while (expr != NULL) { if (find_expression_type(expr) != version_expr) { xmlNode *node = expr; expr = pcmk__xe_next(expr); free_xml(node); } else { expr = pcmk__xe_next(expr); } } add_node_nocopy(versioned_attrs, NULL, attr_set_copy); } #endif typedef struct unpack_data_s { gboolean overwrite; void *hash; crm_time_t *next_change; pe_rule_eval_data_t *rule_data; xmlNode *top; } unpack_data_t; static void unpack_attr_set(gpointer data, gpointer user_data) { sorted_set_t *pair = data; unpack_data_t *unpack_data = user_data; if (!pe_eval_rules(pair->attr_set, unpack_data->rule_data, unpack_data->next_change)) { return; } #if ENABLE_VERSIONED_ATTRS if (get_versioned_rule(pair->attr_set) && !(unpack_data->rule_data->node_hash && g_hash_table_lookup_extended(unpack_data->rule_data->node_hash, CRM_ATTR_RA_VERSION, NULL, NULL))) { // we haven't actually tested versioned expressions yet return; } #endif crm_trace("Adding attributes from %s (score %d) %s overwrite", pair->name, pair->score, (unpack_data->overwrite? "with" : "without")); populate_hash(pair->attr_set, unpack_data->hash, unpack_data->overwrite, unpack_data->top); } #if ENABLE_VERSIONED_ATTRS static void unpack_versioned_attr_set(gpointer data, gpointer user_data) { sorted_set_t *pair = data; unpack_data_t *unpack_data = user_data; if (pe_eval_rules(pair->attr_set, unpack_data->rule_data, unpack_data->next_change)) { add_versioned_attributes(pair->attr_set, unpack_data->hash); } } #endif /*! * \internal * \brief Create a sorted list of nvpair blocks * * \param[in] top XML document root (used to expand id-ref's) * \param[in] xml_obj XML element containing blocks of nvpair elements * \param[in] set_name If not NULL, only get blocks of this element type * \param[in] always_first If not NULL, sort block with this ID as first * * \return List of sorted_set_t entries for nvpair blocks */ static GList * -make_pairs(xmlNode *top, xmlNode *xml_obj, const char *set_name, +make_pairs(xmlNode *top, const xmlNode *xml_obj, const char *set_name, const char *always_first) { GList *unsorted = NULL; if (xml_obj == NULL) { return NULL; } for (xmlNode *attr_set = pcmk__xe_first_child(xml_obj); attr_set != NULL; attr_set = pcmk__xe_next(attr_set)) { if (pcmk__str_eq(set_name, (const char *) attr_set->name, pcmk__str_null_matches)) { const char *score = NULL; sorted_set_t *pair = NULL; xmlNode *expanded_attr_set = expand_idref(attr_set, top); if (expanded_attr_set == NULL) { // Schema (if not "none") prevents this continue; } pair = calloc(1, sizeof(sorted_set_t)); pair->name = ID(expanded_attr_set); pair->special_name = always_first; pair->attr_set = expanded_attr_set; score = crm_element_value(expanded_attr_set, XML_RULE_ATTR_SCORE); pair->score = char2score(score); unsorted = g_list_prepend(unsorted, pair); } } return g_list_sort(unsorted, sort_pairs); } /*! * \internal * \brief Extract nvpair blocks contained by an XML element into a hash table * * \param[in] top XML document root (used to expand id-ref's) * \param[in] xml_obj XML element containing blocks of nvpair elements * \param[in] set_name If not NULL, only use blocks of this element type * \param[out] hash Where to store extracted name/value pairs * \param[in] always_first If not NULL, process block with this ID first * \param[in] overwrite Whether to replace existing values with same name * \param[in] rule_data Matching parameters to use when unpacking * \param[out] next_change If not NULL, set to when rule evaluation will change * \param[in] unpack_func Function to call to unpack each block */ static void -unpack_nvpair_blocks(xmlNode *top, xmlNode *xml_obj, const char *set_name, +unpack_nvpair_blocks(xmlNode *top, const xmlNode *xml_obj, const char *set_name, void *hash, const char *always_first, gboolean overwrite, pe_rule_eval_data_t *rule_data, crm_time_t *next_change, GFunc unpack_func) { GList *pairs = make_pairs(top, xml_obj, set_name, always_first); if (pairs) { unpack_data_t data = { .hash = hash, .overwrite = overwrite, .next_change = next_change, .top = top, .rule_data = rule_data }; g_list_foreach(pairs, unpack_func, &data); g_list_free_full(pairs, free); } } void -pe_eval_nvpairs(xmlNode *top, xmlNode *xml_obj, const char *set_name, +pe_eval_nvpairs(xmlNode *top, const xmlNode *xml_obj, const char *set_name, pe_rule_eval_data_t *rule_data, GHashTable *hash, const char *always_first, gboolean overwrite, crm_time_t *next_change) { unpack_nvpair_blocks(top, xml_obj, set_name, hash, always_first, overwrite, rule_data, next_change, unpack_attr_set); } /*! * \brief Extract nvpair blocks contained by an XML element into a hash table * * \param[in] top XML document root (used to expand id-ref's) * \param[in] xml_obj XML element containing blocks of nvpair elements * \param[in] set_name Element name to identify nvpair blocks * \param[in] node_hash Node attributes to use when evaluating rules * \param[out] hash Where to store extracted name/value pairs * \param[in] always_first If not NULL, process block with this ID first * \param[in] overwrite Whether to replace existing values with same name * \param[in] now Time to use when evaluating rules * \param[out] next_change If not NULL, set to when rule evaluation will change */ void pe_unpack_nvpairs(xmlNode *top, xmlNode *xml_obj, const char *set_name, GHashTable *node_hash, GHashTable *hash, const char *always_first, gboolean overwrite, crm_time_t *now, crm_time_t *next_change) { pe_rule_eval_data_t rule_data = { .node_hash = node_hash, .role = RSC_ROLE_UNKNOWN, .now = now, .match_data = NULL, .rsc_data = NULL, .op_data = NULL }; pe_eval_nvpairs(top, xml_obj, set_name, &rule_data, hash, always_first, overwrite, next_change); } #if ENABLE_VERSIONED_ATTRS void pe_eval_versioned_attributes(xmlNode *top, xmlNode *xml_obj, const char *set_name, pe_rule_eval_data_t *rule_data, xmlNode *hash, crm_time_t *next_change) { unpack_nvpair_blocks(top, xml_obj, set_name, hash, NULL, FALSE, rule_data, next_change, unpack_versioned_attr_set); } #endif char * pe_expand_re_matches(const char *string, pe_re_match_data_t *match_data) { size_t len = 0; int i; const char *p, *last_match_index; char *p_dst, *result = NULL; if (pcmk__str_empty(string) || !match_data) { return NULL; } p = last_match_index = string; while (*p) { if (*p == '%' && *(p + 1) && isdigit(*(p + 1))) { i = *(p + 1) - '0'; if (match_data->nregs >= i && match_data->pmatch[i].rm_so != -1 && match_data->pmatch[i].rm_eo > match_data->pmatch[i].rm_so) { len += p - last_match_index + (match_data->pmatch[i].rm_eo - match_data->pmatch[i].rm_so); last_match_index = p + 2; } p++; } p++; } len += p - last_match_index + 1; /* FIXME: Excessive? */ if (len - 1 <= 0) { return NULL; } p_dst = result = calloc(1, len); p = string; while (*p) { if (*p == '%' && *(p + 1) && isdigit(*(p + 1))) { i = *(p + 1) - '0'; if (match_data->nregs >= i && match_data->pmatch[i].rm_so != -1 && match_data->pmatch[i].rm_eo > match_data->pmatch[i].rm_so) { /* rm_eo can be equal to rm_so, but then there is nothing to do */ int match_len = match_data->pmatch[i].rm_eo - match_data->pmatch[i].rm_so; memcpy(p_dst, match_data->string + match_data->pmatch[i].rm_so, match_len); p_dst += match_len; } p++; } else { *(p_dst) = *(p); p_dst++; } p++; } return result; } #if ENABLE_VERSIONED_ATTRS GHashTable* pe_unpack_versioned_parameters(xmlNode *versioned_params, const char *ra_version) { GHashTable *hash = pcmk__strkey_table(free, free); if (versioned_params && ra_version) { GHashTable *node_hash = pcmk__strkey_table(free, free); xmlNode *attr_set = pcmk__xe_first_child(versioned_params); if (attr_set) { g_hash_table_insert(node_hash, strdup(CRM_ATTR_RA_VERSION), strdup(ra_version)); pe_unpack_nvpairs(NULL, versioned_params, crm_element_name(attr_set), node_hash, hash, NULL, FALSE, NULL, NULL); } g_hash_table_destroy(node_hash); } return hash; } #endif gboolean pe_eval_rules(xmlNode *ruleset, pe_rule_eval_data_t *rule_data, crm_time_t *next_change) { // If there are no rules, pass by default gboolean ruleset_default = TRUE; for (xmlNode *rule = first_named_child(ruleset, XML_TAG_RULE); rule != NULL; rule = crm_next_same_xml(rule)) { ruleset_default = FALSE; if (pe_eval_expr(rule, rule_data, next_change)) { /* Only the deprecated "lifetime" element of location constraints * may contain more than one rule at the top level -- the schema * limits a block of nvpairs to a single top-level rule. So, this * effectively means that a lifetime is active if any rule it * contains is active. */ return TRUE; } } return ruleset_default; } gboolean pe_eval_expr(xmlNode *rule, pe_rule_eval_data_t *rule_data, crm_time_t *next_change) { xmlNode *expr = NULL; gboolean test = TRUE; gboolean empty = TRUE; gboolean passed = TRUE; gboolean do_and = TRUE; const char *value = NULL; rule = expand_idref(rule, NULL); value = crm_element_value(rule, XML_RULE_ATTR_BOOLEAN_OP); if (pcmk__str_eq(value, "or", pcmk__str_casei)) { do_and = FALSE; passed = FALSE; } crm_trace("Testing rule %s", ID(rule)); for (expr = pcmk__xe_first_child(rule); expr != NULL; expr = pcmk__xe_next(expr)) { test = pe_eval_subexpr(expr, rule_data, next_change); empty = FALSE; if (test && do_and == FALSE) { crm_trace("Expression %s/%s passed", ID(rule), ID(expr)); return TRUE; } else if (test == FALSE && do_and) { crm_trace("Expression %s/%s failed", ID(rule), ID(expr)); return FALSE; } } if (empty) { crm_err("Invalid Rule %s: rules must contain at least one expression", ID(rule)); } crm_trace("Rule %s %s", ID(rule), passed ? "passed" : "failed"); return passed; } gboolean pe_eval_subexpr(xmlNode *expr, pe_rule_eval_data_t *rule_data, crm_time_t *next_change) { gboolean accept = FALSE; const char *uname = NULL; switch (find_expression_type(expr)) { case nested_rule: accept = pe_eval_expr(expr, rule_data, next_change); break; case attr_expr: case loc_expr: /* these expressions can never succeed if there is * no node to compare with */ if (rule_data->node_hash != NULL) { accept = pe__eval_attr_expr(expr, rule_data); } break; case time_expr: switch (pe__eval_date_expr(expr, rule_data, next_change)) { case pcmk_rc_within_range: case pcmk_rc_ok: accept = TRUE; break; default: accept = FALSE; break; } break; case role_expr: accept = pe__eval_role_expr(expr, rule_data); break; case rsc_expr: accept = pe__eval_rsc_expr(expr, rule_data); break; case op_expr: accept = pe__eval_op_expr(expr, rule_data); break; #if ENABLE_VERSIONED_ATTRS case version_expr: if (rule_data->node_hash && g_hash_table_lookup_extended(rule_data->node_hash, CRM_ATTR_RA_VERSION, NULL, NULL)) { accept = pe__eval_attr_expr(expr, rule_data); } else { // we are going to test it when we have ra-version accept = TRUE; } break; #endif default: CRM_CHECK(FALSE /* bad type */ , return FALSE); accept = FALSE; } if (rule_data->node_hash) { uname = g_hash_table_lookup(rule_data->node_hash, CRM_ATTR_UNAME); } crm_trace("Expression %s %s on %s", ID(expr), accept ? "passed" : "failed", uname ? uname : "all nodes"); return accept; } /*! * \internal * \brief Compare two values in a rule's node attribute expression * * \param[in] l_val Value on left-hand side of comparison * \param[in] r_val Value on right-hand side of comparison * \param[in] type How to interpret the values (allowed values: * \c "string", \c "integer", \c "number", * \c "version", \c NULL) * \param[in] op Type of comparison * * \return -1 if (l_val < r_val), * 0 if (l_val == r_val), * 1 if (l_val > r_val) */ static int compare_attr_expr_vals(const char *l_val, const char *r_val, const char *type, const char *op) { int cmp = 0; if (l_val != NULL && r_val != NULL) { if (type == NULL) { if (pcmk__strcase_any_of(op, "lt", "lte", "gt", "gte", NULL)) { if (pcmk__char_in_any_str('.', l_val, r_val, NULL)) { type = "number"; } else { type = "integer"; } } else { type = "string"; } crm_trace("Defaulting to %s based comparison for '%s' op", type, op); } if (pcmk__str_eq(type, "string", pcmk__str_casei)) { cmp = strcasecmp(l_val, r_val); } else if (pcmk__str_eq(type, "integer", pcmk__str_casei)) { long long l_val_num; int rc1 = pcmk__scan_ll(l_val, &l_val_num, 0LL); long long r_val_num; int rc2 = pcmk__scan_ll(r_val, &r_val_num, 0LL); if ((rc1 == pcmk_rc_ok) && (rc2 == pcmk_rc_ok)) { if (l_val_num < r_val_num) { cmp = -1; } else if (l_val_num > r_val_num) { cmp = 1; } else { cmp = 0; } } else { crm_debug("Integer parse error. Comparing %s and %s as strings", l_val, r_val); cmp = compare_attr_expr_vals(l_val, r_val, "string", op); } } else if (pcmk__str_eq(type, "number", pcmk__str_casei)) { double l_val_num; double r_val_num; int rc1 = pcmk__scan_double(l_val, &l_val_num, NULL, NULL); int rc2 = pcmk__scan_double(r_val, &r_val_num, NULL, NULL); if (rc1 == pcmk_rc_ok && rc2 == pcmk_rc_ok) { if (l_val_num < r_val_num) { cmp = -1; } else if (l_val_num > r_val_num) { cmp = 1; } else { cmp = 0; } } else { crm_debug("Floating-point parse error. Comparing %s and %s as " "strings", l_val, r_val); cmp = compare_attr_expr_vals(l_val, r_val, "string", op); } } else if (pcmk__str_eq(type, "version", pcmk__str_casei)) { cmp = compare_version(l_val, r_val); } } else if (l_val == NULL && r_val == NULL) { cmp = 0; } else if (r_val == NULL) { cmp = 1; } else { // l_val == NULL && r_val != NULL cmp = -1; } return cmp; } /*! * \internal * \brief Check whether an attribute expression evaluates to \c true * * \param[in] l_val Value on left-hand side of comparison * \param[in] r_val Value on right-hand side of comparison * \param[in] type How to interpret the values (allowed values: * \c "string", \c "integer", \c "number", * \c "version", \c NULL) * \param[in] op Type of comparison. * * \return \c true if expression evaluates to \c true, \c false * otherwise */ static bool accept_attr_expr(const char *l_val, const char *r_val, const char *type, const char *op) { int cmp; if (pcmk__str_eq(op, "defined", pcmk__str_casei)) { return (l_val != NULL); } else if (pcmk__str_eq(op, "not_defined", pcmk__str_casei)) { return (l_val == NULL); } cmp = compare_attr_expr_vals(l_val, r_val, type, op); if (pcmk__str_eq(op, "eq", pcmk__str_casei)) { return (cmp == 0); } else if (pcmk__str_eq(op, "ne", pcmk__str_casei)) { return (cmp != 0); } else if (l_val == NULL || r_val == NULL) { // The comparison is meaningless from this point on return false; } else if (pcmk__str_eq(op, "lt", pcmk__str_casei)) { return (cmp < 0); } else if (pcmk__str_eq(op, "lte", pcmk__str_casei)) { return (cmp <= 0); } else if (pcmk__str_eq(op, "gt", pcmk__str_casei)) { return (cmp > 0); } else if (pcmk__str_eq(op, "gte", pcmk__str_casei)) { return (cmp >= 0); } return false; // Should never reach this point } /*! * \internal * \brief Get correct value according to value-source * * \param[in] value value given in rule expression * \param[in] value_source value-source given in rule expressions * \param[in] match_data If not NULL, resource back-references and params */ static const char * expand_value_source(const char *value, const char *value_source, pe_match_data_t *match_data) { GHashTable *table = NULL; if (pcmk__str_empty(value)) { return NULL; // value_source is irrelevant } else if (pcmk__str_eq(value_source, "param", pcmk__str_casei)) { table = match_data->params; } else if (pcmk__str_eq(value_source, "meta", pcmk__str_casei)) { table = match_data->meta; } else { // literal return value; } if (table == NULL) { return NULL; } return (const char *) g_hash_table_lookup(table, value); } /*! * \internal * \brief Evaluate a node attribute expression based on #uname, #id, #kind, * or a generic node attribute * * \param[in] expr XML of rule expression * \param[in] rule_data The match_data and node_hash members are used * * \return TRUE if rule_data satisfies the expression, FALSE otherwise */ gboolean pe__eval_attr_expr(xmlNodePtr expr, pe_rule_eval_data_t *rule_data) { gboolean attr_allocated = FALSE; const char *h_val = NULL; const char *op = NULL; const char *type = NULL; const char *attr = NULL; const char *value = NULL; const char *value_source = NULL; attr = crm_element_value(expr, XML_EXPR_ATTR_ATTRIBUTE); op = crm_element_value(expr, XML_EXPR_ATTR_OPERATION); value = crm_element_value(expr, XML_EXPR_ATTR_VALUE); type = crm_element_value(expr, XML_EXPR_ATTR_TYPE); value_source = crm_element_value(expr, XML_EXPR_ATTR_VALUE_SOURCE); if (attr == NULL) { pe_err("Expression %s invalid: " XML_EXPR_ATTR_ATTRIBUTE " not specified", pcmk__s(ID(expr), "without ID")); return FALSE; } else if (op == NULL) { pe_err("Expression %s invalid: " XML_EXPR_ATTR_OPERATION " not specified", pcmk__s(ID(expr), "without ID")); } if (rule_data->match_data != NULL) { // Expand any regular expression submatches (%0-%9) in attribute name if (rule_data->match_data->re != NULL) { char *resolved_attr = pe_expand_re_matches(attr, rule_data->match_data->re); if (resolved_attr != NULL) { attr = (const char *) resolved_attr; attr_allocated = TRUE; } } // Get value appropriate to value-source value = expand_value_source(value, value_source, rule_data->match_data); } if (rule_data->node_hash != NULL) { h_val = (const char *)g_hash_table_lookup(rule_data->node_hash, attr); } if (attr_allocated) { free((char *)attr); attr = NULL; } return accept_attr_expr(h_val, value, type, op); } /*! * \internal * \brief Evaluate a date_expression * * \param[in] expr XML of rule expression * \param[in] rule_data Only the now member is used * \param[out] next_change If not NULL, set to when evaluation will change * * \return Standard Pacemaker return code */ int pe__eval_date_expr(xmlNodePtr expr, pe_rule_eval_data_t *rule_data, crm_time_t *next_change) { crm_time_t *start = NULL; crm_time_t *end = NULL; const char *value = NULL; const char *op = crm_element_value(expr, "operation"); xmlNode *duration_spec = NULL; xmlNode *date_spec = NULL; // "undetermined" will also be returned for parsing errors int rc = pcmk_rc_undetermined; crm_trace("Testing expression: %s", ID(expr)); duration_spec = first_named_child(expr, "duration"); date_spec = first_named_child(expr, "date_spec"); value = crm_element_value(expr, "start"); if (value != NULL) { start = crm_time_new(value); } value = crm_element_value(expr, "end"); if (value != NULL) { end = crm_time_new(value); } if (start != NULL && end == NULL && duration_spec != NULL) { end = pe_parse_xml_duration(start, duration_spec); } if (pcmk__str_eq(op, "in_range", pcmk__str_null_matches | pcmk__str_casei)) { if ((start == NULL) && (end == NULL)) { // in_range requires at least one of start or end } else if ((start != NULL) && (crm_time_compare(rule_data->now, start) < 0)) { rc = pcmk_rc_before_range; crm_time_set_if_earlier(next_change, start); } else if ((end != NULL) && (crm_time_compare(rule_data->now, end) > 0)) { rc = pcmk_rc_after_range; } else { rc = pcmk_rc_within_range; if (end && next_change) { // Evaluation doesn't change until second after end crm_time_add_seconds(end, 1); crm_time_set_if_earlier(next_change, end); } } } else if (pcmk__str_eq(op, "date_spec", pcmk__str_casei)) { rc = pe_cron_range_satisfied(rule_data->now, date_spec); // @TODO set next_change appropriately } else if (pcmk__str_eq(op, "gt", pcmk__str_casei)) { if (start == NULL) { // gt requires start } else if (crm_time_compare(rule_data->now, start) > 0) { rc = pcmk_rc_within_range; } else { rc = pcmk_rc_before_range; // Evaluation doesn't change until second after start crm_time_add_seconds(start, 1); crm_time_set_if_earlier(next_change, start); } } else if (pcmk__str_eq(op, "lt", pcmk__str_casei)) { if (end == NULL) { // lt requires end } else if (crm_time_compare(rule_data->now, end) < 0) { rc = pcmk_rc_within_range; crm_time_set_if_earlier(next_change, end); } else { rc = pcmk_rc_after_range; } } crm_time_free(start); crm_time_free(end); return rc; } gboolean pe__eval_op_expr(xmlNodePtr expr, pe_rule_eval_data_t *rule_data) { const char *name = crm_element_value(expr, XML_NVPAIR_ATTR_NAME); const char *interval_s = crm_element_value(expr, XML_LRM_ATTR_INTERVAL); guint interval; crm_trace("Testing op_defaults expression: %s", ID(expr)); if (rule_data->op_data == NULL) { crm_trace("No operations data provided"); return FALSE; } interval = crm_parse_interval_spec(interval_s); if (interval == 0 && errno != 0) { crm_trace("Could not parse interval: %s", interval_s); return FALSE; } if (interval_s != NULL && interval != rule_data->op_data->interval) { crm_trace("Interval doesn't match: %d != %d", interval, rule_data->op_data->interval); return FALSE; } if (!pcmk__str_eq(name, rule_data->op_data->op_name, pcmk__str_none)) { crm_trace("Name doesn't match: %s != %s", name, rule_data->op_data->op_name); return FALSE; } return TRUE; } /*! * \internal * \brief Evaluate a node attribute expression based on #role * * \param[in] expr XML of rule expression * \param[in] rule_data Only the role member is used * * \return TRUE if rule_data->role satisfies the expression, FALSE otherwise */ gboolean pe__eval_role_expr(xmlNodePtr expr, pe_rule_eval_data_t *rule_data) { gboolean accept = FALSE; const char *op = NULL; const char *value = NULL; if (rule_data->role == RSC_ROLE_UNKNOWN) { return accept; } value = crm_element_value(expr, XML_EXPR_ATTR_VALUE); op = crm_element_value(expr, XML_EXPR_ATTR_OPERATION); if (pcmk__str_eq(op, "defined", pcmk__str_casei)) { if (rule_data->role > RSC_ROLE_STARTED) { accept = TRUE; } } else if (pcmk__str_eq(op, "not_defined", pcmk__str_casei)) { if ((rule_data->role > RSC_ROLE_UNKNOWN) && (rule_data->role < RSC_ROLE_UNPROMOTED)) { accept = TRUE; } } else if (pcmk__str_eq(op, "eq", pcmk__str_casei)) { if (text2role(value) == rule_data->role) { accept = TRUE; } } else if (pcmk__str_eq(op, "ne", pcmk__str_casei)) { // Test "ne" only with promotable clone roles if ((rule_data->role > RSC_ROLE_UNKNOWN) && (rule_data->role < RSC_ROLE_UNPROMOTED)) { accept = FALSE; } else if (text2role(value) != rule_data->role) { accept = TRUE; } } return accept; } gboolean pe__eval_rsc_expr(xmlNodePtr expr, pe_rule_eval_data_t *rule_data) { const char *class = crm_element_value(expr, XML_AGENT_ATTR_CLASS); const char *provider = crm_element_value(expr, XML_AGENT_ATTR_PROVIDER); const char *type = crm_element_value(expr, XML_EXPR_ATTR_TYPE); crm_trace("Testing rsc_defaults expression: %s", ID(expr)); if (rule_data->rsc_data == NULL) { crm_trace("No resource data provided"); return FALSE; } if (class != NULL && !pcmk__str_eq(class, rule_data->rsc_data->standard, pcmk__str_none)) { crm_trace("Class doesn't match: %s != %s", class, rule_data->rsc_data->standard); return FALSE; } if ((provider == NULL && rule_data->rsc_data->provider != NULL) || (provider != NULL && rule_data->rsc_data->provider == NULL) || !pcmk__str_eq(provider, rule_data->rsc_data->provider, pcmk__str_none)) { crm_trace("Provider doesn't match: %s != %s", provider, rule_data->rsc_data->provider); return FALSE; } if (type != NULL && !pcmk__str_eq(type, rule_data->rsc_data->agent, pcmk__str_none)) { crm_trace("Agent doesn't match: %s != %s", type, rule_data->rsc_data->agent); return FALSE; } return TRUE; } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include gboolean test_ruleset(xmlNode *ruleset, GHashTable *node_hash, crm_time_t *now) { return pe_evaluate_rules(ruleset, node_hash, now, NULL); } gboolean test_rule(xmlNode * rule, GHashTable * node_hash, enum rsc_role_e role, crm_time_t * now) { return pe_test_rule(rule, node_hash, role, now, NULL, NULL); } gboolean pe_test_rule_re(xmlNode * rule, GHashTable * node_hash, enum rsc_role_e role, crm_time_t * now, pe_re_match_data_t * re_match_data) { pe_match_data_t match_data = { .re = re_match_data, .params = NULL, .meta = NULL, }; return pe_test_rule(rule, node_hash, role, now, NULL, &match_data); } gboolean pe_test_rule_full(xmlNode *rule, GHashTable *node_hash, enum rsc_role_e role, crm_time_t *now, pe_match_data_t *match_data) { return pe_test_rule(rule, node_hash, role, now, NULL, match_data); } gboolean test_expression(xmlNode * expr, GHashTable * node_hash, enum rsc_role_e role, crm_time_t * now) { return pe_test_expression(expr, node_hash, role, now, NULL, NULL); } gboolean pe_test_expression_re(xmlNode * expr, GHashTable * node_hash, enum rsc_role_e role, crm_time_t * now, pe_re_match_data_t * re_match_data) { pe_match_data_t match_data = { .re = re_match_data, .params = NULL, .meta = NULL, }; return pe_test_expression(expr, node_hash, role, now, NULL, &match_data); } gboolean pe_test_expression_full(xmlNode *expr, GHashTable *node_hash, enum rsc_role_e role, crm_time_t *now, pe_match_data_t *match_data) { return pe_test_expression(expr, node_hash, role, now, NULL, match_data); } void unpack_instance_attributes(xmlNode *top, xmlNode *xml_obj, const char *set_name, GHashTable *node_hash, GHashTable *hash, const char *always_first, gboolean overwrite, crm_time_t *now) { pe_rule_eval_data_t rule_data = { .node_hash = node_hash, .role = RSC_ROLE_UNKNOWN, .now = now, .match_data = NULL, .rsc_data = NULL, .op_data = NULL }; unpack_nvpair_blocks(top, xml_obj, set_name, hash, always_first, overwrite, &rule_data, NULL, unpack_attr_set); } // LCOV_EXCL_STOP // End deprecated API