diff --git a/include/crm/common/rules.h b/include/crm/common/rules.h index 5f347e65ef..2f3e3c9491 100644 --- a/include/crm/common/rules.h +++ b/include/crm/common/rules.h @@ -1,55 +1,108 @@ /* * Copyright 2004-2023 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__H # define PCMK__CRM_COMMON_RULES__H +#include // guint, GHashTable +#include // regmatch_t +#include // crm_time_t + #ifdef __cplusplus extern "C" { #endif /*! * \file * \brief Scheduler API for rules * \ingroup core */ /* Allowed subexpressions of a rule * @COMPAT This should be made internal at an API compatibility break */ //!@{ //! \deprecated For Pacemaker use only enum expression_type { pcmk__subexpr_unknown = 0, // Unknown subexpression type pcmk__subexpr_rule = 1, // Nested rule pcmk__subexpr_attribute = 2, // Node attribute expression pcmk__subexpr_location = 3, // Node location expression pcmk__subexpr_datetime = 5, // Date/time expression pcmk__subexpr_resource = 7, // Resource agent expression pcmk__subexpr_operation = 8, // Operation expression #if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1) not_expr = pcmk__subexpr_unknown, nested_rule = pcmk__subexpr_rule, attr_expr = pcmk__subexpr_attribute, loc_expr = pcmk__subexpr_location, role_expr = 4, time_expr = pcmk__subexpr_datetime, version_expr = 6, rsc_expr = pcmk__subexpr_resource, op_expr = pcmk__subexpr_operation, #endif }; //!@} +//! Data used to evaluate a rule (any NULL items are ignored) +typedef struct pcmk_rule_input { + // Used to evaluate date expressions + const crm_time_t *now; //!< Current time for rule evaluation purposes + + // Used to evaluate resource type expressions + const char *rsc_standard; //!< Resource standard that rule applies to + const char *rsc_provider; //!< Resource provider that rule applies to + const char *rsc_agent; //!< Resource agent that rule applies to + + // Used to evaluate operation type expressions + const char *op_name; //! Operation name that rule applies to + guint op_interval_ms; //! Operation interval that rule applies to + + // Remaining members are used to evaluate node attribute expressions + + /*! + * Node attributes for rule evaluation purposes + * + * \note Though not const, this is used only with g_hash_table_lookup(). + */ + GHashTable *node_attrs; + + // Remaining members are used only within location constraint rules + + /*! + * Resource parameters that can be used as the reference value source + * + * \note Though not const, this is used only with g_hash_table_lookup(). + */ + GHashTable *rsc_params; + + /*! + * Resource meta-attributes that can be used as the reference value source + * + * \note Though not const, this is used only with g_hash_table_lookup(). + */ + GHashTable *rsc_meta; + + //! Resource ID to compare against a location constraint's resource pattern + const char *rsc_id; + + //! Resource pattern submatches (as set by regexec()) for rsc_id + const regmatch_t *rsc_id_submatches; + + //! Number of entries in rsc_id_submatches + int rsc_id_nmatches; +} pcmk_rule_input_t; + #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_RULES__H diff --git a/include/crm/common/rules_internal.h b/include/crm/common/rules_internal.h index 9d4e99c5e0..c2d01abcf3 100644 --- a/include/crm/common/rules_internal.h +++ b/include/crm/common/rules_internal.h @@ -1,63 +1,29 @@ /* * 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 +#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 *expression, + const pcmk_rule_input_t *rule_input); #endif // PCMK__CRM_COMMON_RULES_INTERNAL__H diff --git a/lib/common/crmcommon_private.h b/lib/common/crmcommon_private.h index db52662496..4849f2f06c 100644 --- a/lib/common/crmcommon_private.h +++ b/lib/common/crmcommon_private.h @@ -1,354 +1,398 @@ /* * Copyright 2018-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 CRMCOMMON_PRIVATE__H # define CRMCOMMON_PRIVATE__H /* This header is for the sole use of libcrmcommon, so that functions can be * declared with G_GNUC_INTERNAL for efficiency. */ #include // uint8_t, uint32_t #include // bool #include // size_t #include // GList #include // xmlNode, xmlAttr #include // struct qb_ipc_response_header // Decent chunk size for processing large amounts of data #define PCMK__BUFFER_SIZE 4096 #if defined(PCMK__UNIT_TESTING) #undef G_GNUC_INTERNAL #define G_GNUC_INTERNAL #endif /* When deleting portions of an XML tree, we keep a record so we can know later * (e.g. when checking differences) that something was deleted. */ typedef struct pcmk__deleted_xml_s { char *path; int position; } pcmk__deleted_xml_t; typedef struct xml_node_private_s { uint32_t check; uint32_t flags; } xml_node_private_t; typedef struct xml_doc_private_s { uint32_t check; uint32_t flags; char *user; GList *acls; GList *deleted_objs; // List of pcmk__deleted_xml_t } xml_doc_private_t; //! libxml2 supports only XML version 1.0, at least as of libxml2-2.12.5 #define PCMK__XML_VERSION ((pcmkXmlStr) "1.0") #define pcmk__set_xml_flags(xml_priv, flags_to_set) do { \ (xml_priv)->flags = pcmk__set_flags_as(__func__, __LINE__, \ LOG_NEVER, "XML", "XML node", (xml_priv)->flags, \ (flags_to_set), #flags_to_set); \ } while (0) #define pcmk__clear_xml_flags(xml_priv, flags_to_clear) do { \ (xml_priv)->flags = pcmk__clear_flags_as(__func__, __LINE__, \ LOG_NEVER, "XML", "XML node", (xml_priv)->flags, \ (flags_to_clear), #flags_to_clear); \ } while (0) G_GNUC_INTERNAL bool pcmk__tracking_xml_changes(xmlNode *xml, bool lazy); G_GNUC_INTERNAL void pcmk__mark_xml_created(xmlNode *xml); G_GNUC_INTERNAL int pcmk__xml_position(const xmlNode *xml, enum xml_private_flags ignore_if_set); G_GNUC_INTERNAL xmlNode *pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle, bool exact); G_GNUC_INTERNAL void pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update, bool as_diff); G_GNUC_INTERNAL xmlNode *pcmk__xc_match(const xmlNode *root, const xmlNode *search_comment, bool exact); G_GNUC_INTERNAL void pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update); G_GNUC_INTERNAL void pcmk__free_acls(GList *acls); G_GNUC_INTERNAL void pcmk__unpack_acl(xmlNode *source, xmlNode *target, const char *user); G_GNUC_INTERNAL bool pcmk__is_user_in_group(const char *user, const char *group); G_GNUC_INTERNAL void pcmk__apply_acl(xmlNode *xml); G_GNUC_INTERNAL void pcmk__apply_creation_acl(xmlNode *xml, bool check_top); G_GNUC_INTERNAL void pcmk__mark_xml_attr_dirty(xmlAttr *a); G_GNUC_INTERNAL bool pcmk__xa_filterable(const char *name); G_GNUC_INTERNAL void pcmk__log_xmllib_err(void *ctx, const char *fmt, ...) G_GNUC_PRINTF(2, 3); G_GNUC_INTERNAL void pcmk__mark_xml_node_dirty(xmlNode *xml); G_GNUC_INTERNAL bool pcmk__marked_as_deleted(xmlAttrPtr a, void *user_data); G_GNUC_INTERNAL void pcmk__dump_xml_attr(const xmlAttr *attr, GString *buffer); /* * Date/times */ // For use with pcmk__add_time_from_xml() enum pcmk__time_component { pcmk__time_unknown, pcmk__time_years, pcmk__time_months, pcmk__time_weeks, pcmk__time_days, pcmk__time_hours, pcmk__time_minutes, pcmk__time_seconds, }; G_GNUC_INTERNAL const char *pcmk__time_component_attr(enum pcmk__time_component component); G_GNUC_INTERNAL int pcmk__add_time_from_xml(crm_time_t *t, enum pcmk__time_component component, const xmlNode *xml); G_GNUC_INTERNAL void pcmk__set_time_if_earlier(crm_time_t *target, const crm_time_t *source); /* * IPC */ #define PCMK__IPC_VERSION 1 #define PCMK__CONTROLD_API_MAJOR "1" #define PCMK__CONTROLD_API_MINOR "0" // IPC behavior that varies by daemon typedef struct pcmk__ipc_methods_s { /*! * \internal * \brief Allocate any private data needed by daemon IPC * * \param[in,out] api IPC API connection * * \return Standard Pacemaker return code */ int (*new_data)(pcmk_ipc_api_t *api); /*! * \internal * \brief Free any private data used by daemon IPC * * \param[in,out] api_data Data allocated by new_data() method */ void (*free_data)(void *api_data); /*! * \internal * \brief Perform daemon-specific handling after successful connection * * Some daemons require clients to register before sending any other * commands. The controller requires a CRM_OP_HELLO (with no reply), and * the CIB manager, executor, and fencer require a CRM_OP_REGISTER (with a * reply). Ideally this would be consistent across all daemons, but for now * this allows each to do its own authorization. * * \param[in,out] api IPC API connection * * \return Standard Pacemaker return code */ int (*post_connect)(pcmk_ipc_api_t *api); /*! * \internal * \brief Check whether an IPC request results in a reply * * \param[in,out] api IPC API connection * \param[in] request IPC request XML * * \return true if request would result in an IPC reply, false otherwise */ bool (*reply_expected)(pcmk_ipc_api_t *api, const xmlNode *request); /*! * \internal * \brief Perform daemon-specific handling of an IPC message * * \param[in,out] api IPC API connection * \param[in,out] msg Message read from IPC connection * * \return true if more IPC reply messages should be expected */ bool (*dispatch)(pcmk_ipc_api_t *api, xmlNode *msg); /*! * \internal * \brief Perform daemon-specific handling of an IPC disconnect * * \param[in,out] api IPC API connection */ void (*post_disconnect)(pcmk_ipc_api_t *api); } pcmk__ipc_methods_t; // Implementation of pcmk_ipc_api_t struct pcmk_ipc_api_s { enum pcmk_ipc_server server; // Daemon this IPC API instance is for enum pcmk_ipc_dispatch dispatch_type; // How replies should be dispatched size_t ipc_size_max; // maximum IPC buffer size crm_ipc_t *ipc; // IPC connection mainloop_io_t *mainloop_io; // If using mainloop, I/O source for IPC bool free_on_disconnect; // Whether disconnect should free object pcmk_ipc_callback_t cb; // Caller-registered callback (if any) void *user_data; // Caller-registered data (if any) void *api_data; // For daemon-specific use pcmk__ipc_methods_t *cmds; // Behavior that varies by daemon }; typedef struct pcmk__ipc_header_s { struct qb_ipc_response_header qb; uint32_t size_uncompressed; uint32_t size_compressed; uint32_t flags; uint8_t version; } pcmk__ipc_header_t; G_GNUC_INTERNAL int pcmk__send_ipc_request(pcmk_ipc_api_t *api, const xmlNode *request); G_GNUC_INTERNAL void pcmk__call_ipc_callback(pcmk_ipc_api_t *api, enum pcmk_ipc_event event_type, crm_exit_t status, void *event_data); G_GNUC_INTERNAL unsigned int pcmk__ipc_buffer_size(unsigned int max); G_GNUC_INTERNAL bool pcmk__valid_ipc_header(const pcmk__ipc_header_t *header); G_GNUC_INTERNAL pcmk__ipc_methods_t *pcmk__attrd_api_methods(void); G_GNUC_INTERNAL pcmk__ipc_methods_t *pcmk__controld_api_methods(void); G_GNUC_INTERNAL pcmk__ipc_methods_t *pcmk__pacemakerd_api_methods(void); G_GNUC_INTERNAL pcmk__ipc_methods_t *pcmk__schedulerd_api_methods(void); /* * Logging */ //! XML is newly created #define PCMK__XML_PREFIX_CREATED "++" //! XML has been deleted #define PCMK__XML_PREFIX_DELETED "--" //! XML has been modified #define PCMK__XML_PREFIX_MODIFIED "+ " //! XML has been moved #define PCMK__XML_PREFIX_MOVED "+~" /* * Output */ G_GNUC_INTERNAL int pcmk__bare_output_new(pcmk__output_t **out, const char *fmt_name, const char *filename, char **argv); G_GNUC_INTERNAL void pcmk__register_option_messages(pcmk__output_t *out); G_GNUC_INTERNAL void pcmk__register_patchset_messages(pcmk__output_t *out); G_GNUC_INTERNAL bool pcmk__output_text_get_fancy(pcmk__output_t *out); /* * Rules */ +// 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, +}; + +G_GNUC_INTERNAL +enum pcmk__comparison pcmk__parse_comparison(const char *op); + +G_GNUC_INTERNAL +enum pcmk__type pcmk__parse_type(const char *type, enum pcmk__comparison op, + const char *value1, const char *value2); + +G_GNUC_INTERNAL +enum pcmk__reference_source pcmk__parse_source(const char *source); + +G_GNUC_INTERNAL +int pcmk__cmp_by_type(const char *value1, const char *value2, + enum pcmk__type type); + G_GNUC_INTERNAL int pcmk__unpack_duration(const xmlNode *duration, const crm_time_t *start, crm_time_t **end); G_GNUC_INTERNAL int pcmk__evaluate_date_spec(const xmlNode *date_spec, const crm_time_t *now); /* * Utils */ #define PCMK__PW_BUFFER_LEN 500 /* * Schemas */ typedef struct { unsigned char v[2]; } pcmk__schema_version_t; enum pcmk__schema_validator { pcmk__schema_validator_none, pcmk__schema_validator_rng }; typedef struct { char *name; char *transform; void *cache; enum pcmk__schema_validator validator; pcmk__schema_version_t version; char *transform_enter; bool transform_onleave; } pcmk__schema_t; G_GNUC_INTERNAL int pcmk__find_x_0_schema_index(GList *schemas); #endif // CRMCOMMON_PRIVATE__H diff --git a/lib/common/rules.c b/lib/common/rules.c index bf1c87bc79..118709a66f 100644 --- a/lib/common/rules.c +++ b/lib/common/rules.c @@ -1,890 +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)) { + 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 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 (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(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; +} diff --git a/lib/common/tests/rules/Makefile.am b/lib/common/tests/rules/Makefile.am index 084a0fda27..18de1c97e3 100644 --- a/lib/common/tests/rules/Makefile.am +++ b/lib/common/tests/rules/Makefile.am @@ -1,23 +1,24 @@ # # Copyright 2020-2024 the Pacemaker project contributors # # The version control history for this file may have further details. # # This source code is licensed under the GNU General Public License version 2 # or later (GPLv2+) WITHOUT ANY WARRANTY. # include $(top_srcdir)/mk/tap.mk include $(top_srcdir)/mk/unittest.mk # Add "_test" to the end of all test program names to simplify .gitignore. check_PROGRAMS = pcmk__cmp_by_type_test \ + pcmk__evaluate_attr_expression_test \ pcmk__evaluate_date_expression_test \ pcmk__evaluate_date_spec_test \ pcmk__parse_comparison_test \ pcmk__parse_source_test \ pcmk__parse_type_test \ pcmk__replace_submatches_test \ pcmk__unpack_duration_test TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/rules/pcmk__cmp_by_type_test.c b/lib/common/tests/rules/pcmk__cmp_by_type_test.c index d4bd98454f..cf468f1fa5 100644 --- a/lib/common/tests/rules/pcmk__cmp_by_type_test.c +++ b/lib/common/tests/rules/pcmk__cmp_by_type_test.c @@ -1,101 +1,102 @@ /* * 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 // INT_MIN, INT_MAX #include // crm_strdup_printf() #include #include +#include "crmcommon_private.h" static void null_compares_lesser(void **state) { assert_int_equal(pcmk__cmp_by_type(NULL, NULL, pcmk__type_string), 0); assert_true(pcmk__cmp_by_type("0", NULL, pcmk__type_integer) > 0); assert_true(pcmk__cmp_by_type(NULL, "0", pcmk__type_number) < 0); } static void invalid_compares_equal(void **state) { assert_int_equal(pcmk__cmp_by_type("0", "1", pcmk__type_unknown), 0); assert_int_equal(pcmk__cmp_by_type("hi", "bye", pcmk__type_unknown), 0); assert_int_equal(pcmk__cmp_by_type("-1.0", "2.0", pcmk__type_unknown), 0); } static void compare_string_type(void **state) { assert_int_equal(pcmk__cmp_by_type("bye", "bye", pcmk__type_string), 0); assert_int_equal(pcmk__cmp_by_type("bye", "BYE", pcmk__type_string), 0); assert_true(pcmk__cmp_by_type("bye", "hello", pcmk__type_string) < 0); assert_true(pcmk__cmp_by_type("bye", "HELLO", pcmk__type_string) < 0); assert_true(pcmk__cmp_by_type("bye", "boo", pcmk__type_string) > 0); assert_true(pcmk__cmp_by_type("bye", "Boo", pcmk__type_string) > 0); } static void compare_integer_type(void **state) { char *int_min = crm_strdup_printf("%d", INT_MIN); char *int_max = crm_strdup_printf("%d", INT_MAX); assert_int_equal(pcmk__cmp_by_type("0", "0", pcmk__type_integer), 0); assert_true(pcmk__cmp_by_type("0", "1", pcmk__type_integer) < 0); assert_true(pcmk__cmp_by_type("1", "0", pcmk__type_integer) > 0); assert_true(pcmk__cmp_by_type("3999", "399", pcmk__type_integer) > 0); assert_true(pcmk__cmp_by_type(int_min, int_max, pcmk__type_integer) < 0); assert_true(pcmk__cmp_by_type(int_max, int_min, pcmk__type_integer) > 0); free(int_min); free(int_max); // Non-integers compare as strings assert_int_equal(pcmk__cmp_by_type("0", "x", pcmk__type_integer), pcmk__cmp_by_type("0", "x", pcmk__type_string)); assert_int_equal(pcmk__cmp_by_type("x", "0", pcmk__type_integer), pcmk__cmp_by_type("x", "0", pcmk__type_string)); assert_int_equal(pcmk__cmp_by_type("x", "X", pcmk__type_integer), pcmk__cmp_by_type("x", "X", pcmk__type_string)); } static void compare_number_type(void **state) { assert_int_equal(pcmk__cmp_by_type("0", "0.0", pcmk__type_number), 0); assert_true(pcmk__cmp_by_type("0.345", "0.5", pcmk__type_number) < 0); assert_true(pcmk__cmp_by_type("5", "3.1", pcmk__type_number) > 0); assert_true(pcmk__cmp_by_type("3999", "399", pcmk__type_number) > 0); // Non-numbers compare as strings assert_int_equal(pcmk__cmp_by_type("0.0", "x", pcmk__type_number), pcmk__cmp_by_type("0.0", "x", pcmk__type_string)); assert_int_equal(pcmk__cmp_by_type("x", "0.0", pcmk__type_number), pcmk__cmp_by_type("x", "0.0", pcmk__type_string)); assert_int_equal(pcmk__cmp_by_type("x", "X", pcmk__type_number), pcmk__cmp_by_type("x", "X", pcmk__type_string)); } static void compare_version_type(void **state) { assert_int_equal(pcmk__cmp_by_type("1.0", "1.0", pcmk__type_version), 0); assert_true(pcmk__cmp_by_type("1.0.0", "1.0.1", pcmk__type_version) < 0); assert_true(pcmk__cmp_by_type("5.0", "3.1.15", pcmk__type_version) > 0); assert_true(pcmk__cmp_by_type("3999", "399", pcmk__type_version) > 0); } PCMK__UNIT_TEST(NULL, NULL, cmocka_unit_test(null_compares_lesser), cmocka_unit_test(invalid_compares_equal), cmocka_unit_test(compare_string_type), cmocka_unit_test(compare_integer_type), cmocka_unit_test(compare_number_type), cmocka_unit_test(compare_version_type)) diff --git a/lib/common/tests/rules/pcmk__evaluate_attr_expression_test.c b/lib/common/tests/rules/pcmk__evaluate_attr_expression_test.c new file mode 100644 index 0000000000..e37ec439d1 --- /dev/null +++ b/lib/common/tests/rules/pcmk__evaluate_attr_expression_test.c @@ -0,0 +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), 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)) diff --git a/lib/common/tests/rules/pcmk__parse_comparison_test.c b/lib/common/tests/rules/pcmk__parse_comparison_test.c index 2a9426657b..a99559620a 100644 --- a/lib/common/tests/rules/pcmk__parse_comparison_test.c +++ b/lib/common/tests/rules/pcmk__parse_comparison_test.c @@ -1,71 +1,72 @@ /* * 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 "crmcommon_private.h" static void null_unknown(void **state) { assert_int_equal(pcmk__parse_comparison(NULL), pcmk__comparison_unknown); } static void invalid(void **state) { assert_int_equal(pcmk__parse_comparison("nope"), pcmk__comparison_unknown); } static void valid(void **state) { assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_DEFINED), pcmk__comparison_defined); assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_NOT_DEFINED), pcmk__comparison_undefined); assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_EQ), pcmk__comparison_eq); assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_NE), pcmk__comparison_ne); assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_LT), pcmk__comparison_lt); assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_LTE), pcmk__comparison_lte); assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_GT), pcmk__comparison_gt); assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_GTE), pcmk__comparison_gte); } static void case_insensitive(void **state) { assert_int_equal(pcmk__parse_comparison("DEFINED"), pcmk__comparison_defined); assert_int_equal(pcmk__parse_comparison("Not_Defined"), pcmk__comparison_undefined); } PCMK__UNIT_TEST(NULL, NULL, cmocka_unit_test(null_unknown), cmocka_unit_test(invalid), cmocka_unit_test(valid), cmocka_unit_test(case_insensitive)) diff --git a/lib/common/tests/rules/pcmk__parse_source_test.c b/lib/common/tests/rules/pcmk__parse_source_test.c index 72c65bf8f5..9cf9b3253b 100644 --- a/lib/common/tests/rules/pcmk__parse_source_test.c +++ b/lib/common/tests/rules/pcmk__parse_source_test.c @@ -1,61 +1,62 @@ /* * 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 "crmcommon_private.h" static void default_literal(void **state) { assert_int_equal(pcmk__parse_source(NULL), pcmk__source_literal); } static void invalid(void **state) { assert_int_equal(pcmk__parse_source(""), pcmk__source_unknown); assert_int_equal(pcmk__parse_source(" "), pcmk__source_unknown); assert_int_equal(pcmk__parse_source("params"), pcmk__source_unknown); } static void valid(void **state) { assert_int_equal(pcmk__parse_source(PCMK_VALUE_LITERAL), pcmk__source_literal); assert_int_equal(pcmk__parse_source(PCMK_VALUE_PARAM), pcmk__source_instance_attrs); assert_int_equal(pcmk__parse_source(PCMK_VALUE_META), pcmk__source_meta_attrs); } static void case_insensitive(void **state) { assert_int_equal(pcmk__parse_source("LITERAL"), pcmk__source_literal); assert_int_equal(pcmk__parse_source("Param"), pcmk__source_instance_attrs); assert_int_equal(pcmk__parse_source("MeTa"), pcmk__source_meta_attrs); } PCMK__UNIT_TEST(NULL, NULL, cmocka_unit_test(default_literal), cmocka_unit_test(invalid), cmocka_unit_test(valid), cmocka_unit_test(case_insensitive)) diff --git a/lib/common/tests/rules/pcmk__parse_type_test.c b/lib/common/tests/rules/pcmk__parse_type_test.c index f33ab214fa..96f02c87e4 100644 --- a/lib/common/tests/rules/pcmk__parse_type_test.c +++ b/lib/common/tests/rules/pcmk__parse_type_test.c @@ -1,126 +1,127 @@ /* * 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 "crmcommon_private.h" static void invalid(void **state) { assert_int_equal(pcmk__parse_type("nope", pcmk__comparison_unknown, NULL, NULL), pcmk__type_unknown); } static void valid(void **state) { assert_int_equal(pcmk__parse_type(PCMK_VALUE_STRING, pcmk__comparison_unknown, NULL, NULL), pcmk__type_string); assert_int_equal(pcmk__parse_type(PCMK_VALUE_INTEGER, pcmk__comparison_unknown, NULL, NULL), pcmk__type_integer); assert_int_equal(pcmk__parse_type(PCMK_VALUE_NUMBER, pcmk__comparison_unknown, NULL, NULL), pcmk__type_number); assert_int_equal(pcmk__parse_type(PCMK_VALUE_VERSION, pcmk__comparison_unknown, NULL, NULL), pcmk__type_version); } static void case_insensitive(void **state) { assert_int_equal(pcmk__parse_type("STRING", pcmk__comparison_unknown, NULL, NULL), pcmk__type_string); assert_int_equal(pcmk__parse_type("Integer", pcmk__comparison_unknown, NULL, NULL), pcmk__type_integer); } static void default_number(void **state) { assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_lt, "1.0", "2.5"), pcmk__type_number); assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_lte, "1.", "2"), pcmk__type_number); assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gt, "1", ".5"), pcmk__type_number); assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gte, "1.0", "2"), pcmk__type_number); } static void default_integer(void **state) { assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_lt, "1", "2"), pcmk__type_integer); assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_lte, "1", "2"), pcmk__type_integer); assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gt, "1", "2"), pcmk__type_integer); assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gte, "1", "2"), pcmk__type_integer); assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gte, NULL, NULL), pcmk__type_integer); assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gte, "1", NULL), pcmk__type_integer); assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gte, NULL, "2.5"), pcmk__type_number); } static void default_string(void **state) { assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_unknown, NULL, NULL), pcmk__type_string); assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_defined, NULL, NULL), pcmk__type_string); assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_undefined, NULL, NULL), pcmk__type_string); assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_eq, NULL, NULL), pcmk__type_string); assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_ne, NULL, NULL), pcmk__type_string); } PCMK__UNIT_TEST(NULL, NULL, cmocka_unit_test(invalid), cmocka_unit_test(valid), cmocka_unit_test(case_insensitive), cmocka_unit_test(default_number), cmocka_unit_test(default_integer), cmocka_unit_test(default_string)) diff --git a/lib/pengine/rules.c b/lib/pengine/rules.c index 1e4360ec04..e1bbb20013 100644 --- a/lib/pengine/rules.c +++ b/lib/pengine/rules.c @@ -1,883 +1,721 @@ /* * 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 #include #include #include #include #include #include #include #include #include #include #include CRM_TRACE_INIT_DATA(pe_rules); +/*! + * \internal + * \brief Map pe_rule_eval_data_t to pcmk_rule_input_t + * + * \param[out] new New data struct + * \param[in] old Old data struct + */ +static void +map_rule_input(pcmk_rule_input_t *new, const pe_rule_eval_data_t *old) +{ + if (old == NULL) { + return; + } + new->now = old->now; + new->node_attrs = old->node_hash; + if (old->rsc_data != NULL) { + new->rsc_standard = old->rsc_data->standard; + new->rsc_provider = old->rsc_data->provider; + new->rsc_agent = old->rsc_data->agent; + } + if (old->match_data != NULL) { + new->rsc_params = old->match_data->params; + new->rsc_meta = old->match_data->meta; + if (old->match_data->re != NULL) { + new->rsc_id = old->match_data->re->string; + new->rsc_id_submatches = old->match_data->re->pmatch; + new->rsc_id_nmatches = old->match_data->re->nregs; + } + } + if (old->op_data != NULL) { + new->op_name = old->op_data->op_name; + new->op_interval_ms = old->op_data->interval; + } +} + /*! * \brief Evaluate any rules contained by given XML element * * \param[in,out] xml XML element to check for rules * \param[in] node_hash Node attributes to use to evaluate 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, .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, .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,out] expr Rule subelement XML * \param[in] node_hash Node attributes to use when evaluating expression * \param[in] role Ignored (deprecated) * \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, .now = now, .match_data = match_data, .rsc_data = NULL, .op_data = NULL }; return pe_eval_subexpr(expr, &rule_data, next_change); } // 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 gboolean overwrite; // Whether existing values will be overwritten } 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 we're overwriting values, we want lowest score first, so the highest * score is processed last; if we're not overwriting values, we want highest * score first, so nothing else overwrites it. */ if (pair_a->score < pair_b->score) { return pair_a->overwrite? -1 : 1; } else if (pair_a->score > pair_b->score) { return pair_a->overwrite? 1 : -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; if (pcmk__xe_is(list->children, PCMK__XE_ATTRIBUTES)) { list = list->children; } for (an_attr = pcmk__xe_first_child(list); an_attr != NULL; an_attr = pcmk__xe_next(an_attr)) { if (pcmk__xe_is(an_attr, PCMK_XE_NVPAIR)) { xmlNode *ref_nvpair = expand_idref(an_attr, top); name = crm_element_value(an_attr, PCMK_XA_NAME); if ((name == NULL) && (ref_nvpair != NULL)) { name = crm_element_value(ref_nvpair, PCMK_XA_NAME); } value = crm_element_value(an_attr, PCMK_XA_VALUE); if ((value == NULL) && (ref_nvpair != NULL)) { value = crm_element_value(ref_nvpair, PCMK_XA_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); pcmk__insert_dup(hash, name, value); } else if (overwrite) { crm_trace("Setting %s=\"%s\" (overwriting old value \"%s\")", name, value, old_value); pcmk__insert_dup(hash, name, value); } } } } typedef struct unpack_data_s { gboolean overwrite; void *hash; crm_time_t *next_change; const 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; } 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); } /*! * \internal * \brief Create a sorted list of nvpair blocks * * \param[in,out] 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 * \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, const xmlNode *xml_obj, const char *set_name, const char *always_first, gboolean overwrite) { 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 ((set_name == NULL) || pcmk__xe_is(attr_set, set_name)) { const char *score = NULL; sorted_set_t *pair = NULL; xmlNode *expanded_attr_set = expand_idref(attr_set, top); if (expanded_attr_set == NULL) { continue; // Not possible with schema validation enabled } pair = pcmk__assert_alloc(1, sizeof(sorted_set_t)); pair->name = pcmk__xe_id(expanded_attr_set); pair->special_name = always_first; pair->attr_set = expanded_attr_set; pair->overwrite = overwrite; score = crm_element_value(expanded_attr_set, PCMK_XA_SCORE); pair->score = char2score(score); unsorted = g_list_prepend(unsorted, pair); } } return g_list_sort(unsorted, sort_pairs); } /*! * \brief Extract nvpair blocks contained by an XML element into a hash table * * \param[in,out] 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 * \param[in] rule_data Matching parameters to use when unpacking * \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[out] next_change If not NULL, set to when evaluation will change */ void pe_eval_nvpairs(xmlNode *top, const xmlNode *xml_obj, const char *set_name, const pe_rule_eval_data_t *rule_data, GHashTable *hash, const char *always_first, gboolean overwrite, crm_time_t *next_change) { GList *pairs = make_pairs(top, xml_obj, set_name, always_first, overwrite); 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_attr_set, &data); g_list_free_full(pairs, free); } } /*! * \brief Extract nvpair blocks contained by an XML element into a hash table * * \param[in,out] 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 evaluation will change */ void pe_unpack_nvpairs(xmlNode *top, const 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, .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); } /*! * \brief Evaluate rules * * \param[in,out] ruleset XML possibly containing rule sub-elements * \param[in] rule_data * \param[out] next_change If not NULL, set to when evaluation will change * * \return TRUE if there are no rules or */ gboolean pe_eval_rules(xmlNode *ruleset, const 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, PCMK_XE_RULE); rule != NULL; rule = crm_next_same_xml(rule)) { ruleset_default = FALSE; if (pe_eval_expr(rule, rule_data, next_change)) { /* Only the deprecated PCMK__XE_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; } /*! * \brief Evaluate all of a rule's expressions * * \param[in,out] rule XML containing a rule definition or its id-ref * \param[in] rule_data Matching parameters to check against rule * \param[out] next_change If not NULL, set to when evaluation will change * * \return TRUE if \p rule_data passes \p rule, otherwise FALSE */ gboolean pe_eval_expr(xmlNode *rule, const 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); if (rule == NULL) { return FALSE; // Not possible with schema validation enabled } value = crm_element_value(rule, PCMK_XA_BOOLEAN_OP); if (pcmk__str_eq(value, PCMK_VALUE_OR, pcmk__str_casei)) { do_and = FALSE; passed = FALSE; } else if (!pcmk__str_eq(value, PCMK_VALUE_AND, pcmk__str_null_matches|pcmk__str_casei)) { pcmk__config_warn("Rule %s has invalid " PCMK_XA_BOOLEAN_OP " value '%s', using default ('" PCMK_VALUE_AND "')", pcmk__xe_id(rule), value); } crm_trace("Testing rule %s", pcmk__xe_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", pcmk__xe_id(rule), pcmk__xe_id(expr)); return TRUE; } else if (test == FALSE && do_and) { crm_trace("Expression %s/%s failed", pcmk__xe_id(rule), pcmk__xe_id(expr)); return FALSE; } } if (empty) { pcmk__config_err("Ignoring rule %s because it contains no expressions", pcmk__xe_id(rule)); } crm_trace("Rule %s %s", pcmk__xe_id(rule), passed ? "passed" : "failed"); return passed; } /*! * \brief Evaluate a single rule expression, including any subexpressions * * \param[in,out] expr XML containing a rule expression * \param[in] rule_data Matching parameters to check against expression * \param[out] next_change If not NULL, set to when evaluation will change * * \return TRUE if \p rule_data passes \p expr, otherwise FALSE */ gboolean pe_eval_subexpr(xmlNode *expr, const pe_rule_eval_data_t *rule_data, crm_time_t *next_change) { gboolean accept = FALSE; const char *uname = NULL; switch (pcmk__expression_type(expr)) { case pcmk__subexpr_rule: accept = pe_eval_expr(expr, rule_data, next_change); break; case pcmk__subexpr_attribute: case pcmk__subexpr_location: /* 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 pcmk__subexpr_datetime: switch (pcmk__evaluate_date_expression(expr, rule_data->now, next_change)) { case pcmk_rc_within_range: case pcmk_rc_ok: accept = TRUE; break; default: accept = FALSE; break; } break; case pcmk__subexpr_resource: accept = pe__eval_rsc_expr(expr, rule_data); break; case pcmk__subexpr_operation: accept = pe__eval_op_expr(expr, rule_data); break; 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", pcmk__xe_id(expr), (accept? "passed" : "failed"), pcmk__s(uname, "all nodes")); return accept; } -/*! - * \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 - * \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, enum pcmk__type type, - enum pcmk__comparison op) -{ - int cmp; - - switch (op) { - case pcmk__comparison_defined: - return (l_val != NULL); - - case pcmk__comparison_undefined: - return (l_val == NULL); - - default: - break; - } - - cmp = pcmk__cmp_by_type(l_val, r_val, type); - - switch (op) { - case pcmk__comparison_eq: - return (cmp == 0); - - case pcmk__comparison_ne: - return (cmp != 0); - - default: - break; - } - - if ((l_val == NULL) || (r_val == NULL)) { - // The comparison is meaningless from this point on - return false; - } - - switch (op) { - case pcmk__comparison_lt: - return (cmp < 0); - - case pcmk__comparison_lte: - return (cmp <= 0); - - case pcmk__comparison_gt: - return (cmp > 0); - - case pcmk__comparison_gte: - return (cmp >= 0); - - default: // Not possible with schema validation enabled - return false; - } -} - -/*! - * \internal - * \brief Get correct value according to \c PCMK_XA_VALUE_SOURCE - * - * \param[in] value value given in rule expression - * \param[in] source Reference value source - * \param[in] match_data If not NULL, resource back-references and params - */ -static const char * -expand_value_source(const char *value, enum pcmk__reference_source source, - const pe_match_data_t *match_data) -{ - 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 = match_data->params; - break; - - case pcmk__source_meta_attrs: - table = match_data->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 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(const xmlNode *expr, const pe_rule_eval_data_t *rule_data) { - 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 *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); - - enum pcmk__comparison comparison = pcmk__comparison_unknown; - enum pcmk__type type = pcmk__type_unknown; - - if (attr == NULL) { - pcmk__config_err("Expression %s invalid: " PCMK_XA_ATTRIBUTE - " not specified", pcmk__s(id, "without ID")); - return FALSE; - } - - // Get and validate operation - op = crm_element_value(expr, 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")); - } 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); - } - return FALSE; - } - - if (rule_data->match_data != NULL) { - enum pcmk__reference_source source = pcmk__source_unknown; - - // Expand any regular expression submatches (%0-%9) in attribute name - if (rule_data->match_data->re != NULL) { - const char *match = rule_data->match_data->re->string; - const regmatch_t *submatches = rule_data->match_data->re->pmatch; - const int nmatches = rule_data->match_data->re->nregs; - char *resolved_attr = pcmk__replace_submatches(attr, match, - submatches, - nmatches); - - if (resolved_attr != NULL) { - attr = (const char *) resolved_attr; - attr_allocated = TRUE; - } - } - - // 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 = expand_value_source(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; - } - - // Get and validate value type (after expanding value) - type = pcmk__parse_type(type_s, comparison, h_val, value); - 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); - } + pcmk_rule_input_t rule_input = { NULL, }; - return accept_attr_expr(h_val, value, type, comparison); + map_rule_input(&rule_input, rule_data); + return pcmk__evaluate_attr_expression(expr, &rule_input) == pcmk_rc_ok; } gboolean pe__eval_op_expr(const xmlNode *expr, const pe_rule_eval_data_t *rule_data) { const char *name = crm_element_value(expr, PCMK_XA_NAME); const char *interval_s = crm_element_value(expr, PCMK_META_INTERVAL); guint interval_ms = 0U; crm_trace("Testing op_defaults expression: %s", pcmk__xe_id(expr)); if (rule_data->op_data == NULL) { crm_trace("No operations data provided"); return FALSE; } if (pcmk_parse_interval_spec(interval_s, &interval_ms) != pcmk_rc_ok) { crm_trace("Could not parse interval: %s", interval_s); return FALSE; } if ((interval_s != NULL) && (interval_ms != rule_data->op_data->interval)) { crm_trace("Interval doesn't match: %d != %d", interval_ms, 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; } gboolean pe__eval_rsc_expr(const xmlNode *expr, const pe_rule_eval_data_t *rule_data) { const char *class = crm_element_value(expr, PCMK_XA_CLASS); const char *provider = crm_element_value(expr, PCMK_XA_PROVIDER); const char *type = crm_element_value(expr, PCMK_XA_TYPE); crm_trace("Testing rsc_defaults expression: %s", pcmk__xe_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, .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, NULL); } enum expression_type find_expression_type(xmlNode *expr) { return pcmk__expression_type(expr); } char * pe_expand_re_matches(const char *string, const pe_re_match_data_t *match_data) { if (match_data == NULL) { return NULL; } return pcmk__replace_submatches(string, match_data->string, match_data->pmatch, match_data->nregs); } // LCOV_EXCL_STOP // End deprecated API