diff --git a/include/crm/common/rules.h b/include/crm/common/rules.h index 2f3e3c9491..0ea9ccb554 100644 --- a/include/crm/common/rules.h +++ b/include/crm/common/rules.h @@ -1,108 +1,112 @@ /* - * Copyright 2004-2023 the Pacemaker project contributors + * 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__H # define PCMK__CRM_COMMON_RULES__H #include // guint, GHashTable #include // regmatch_t +#include // xmlNode #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 + pcmk__condition_unknown = 0, // Unknown or invalid condition + pcmk__condition_rule = 1, // Nested rule + pcmk__condition_attribute = 2, // Node attribute expression + pcmk__condition_location = 3, // Node location expression + pcmk__condition_datetime = 5, // Date/time expression + pcmk__condition_resource = 7, // Resource agent expression + pcmk__condition_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, + not_expr = pcmk__condition_unknown, + nested_rule = pcmk__condition_rule, + attr_expr = pcmk__condition_attribute, + loc_expr = pcmk__condition_location, role_expr = 4, - time_expr = pcmk__subexpr_datetime, + time_expr = pcmk__condition_datetime, version_expr = 6, - rsc_expr = pcmk__subexpr_resource, - op_expr = pcmk__subexpr_operation, + rsc_expr = pcmk__condition_resource, + op_expr = pcmk__condition_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; +int pcmk_evaluate_rule(xmlNode *rule, const pcmk_rule_input_t *rule_input, + crm_time_t *next_change); + #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 c27539d37f..d536b857eb 100644 --- a/include/crm/common/rules_internal.h +++ b/include/crm/common/rules_internal.h @@ -1,33 +1,36 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__CRM_COMMON_RULES_INTERNAL__H #define PCMK__CRM_COMMON_RULES_INTERNAL__H #include // regmatch_t #include // xmlNode #include // enum expression_type, etc. #include // crm_time_t -enum expression_type pcmk__expression_type(const xmlNode *expr); +enum pcmk__combine { + pcmk__combine_unknown, + pcmk__combine_and, + pcmk__combine_or, +}; + +enum expression_type pcmk__condition_type(const xmlNode *condition); char *pcmk__replace_submatches(const char *string, const char *match, const regmatch_t submatches[], int nmatches); +enum pcmk__combine pcmk__parse_combine(const char *combine); 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); -int pcmk__evaluate_rsc_expression(const xmlNode *expr, - const pcmk_rule_input_t *rule_input); -int pcmk__evaluate_op_expression(const xmlNode *expr, - const pcmk_rule_input_t *rule_input); +int pcmk__evaluate_condition(xmlNode *expr, const pcmk_rule_input_t *rule_input, + crm_time_t *next_change); #endif // PCMK__CRM_COMMON_RULES_INTERNAL__H diff --git a/include/crm/pengine/rules.h b/include/crm/pengine/rules.h index 1e87d2de4e..0337e12cc8 100644 --- a/include/crm/pengine/rules.h +++ b/include/crm/pengine/rules.h @@ -1,61 +1,48 @@ /* * 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_PENGINE_RULES__H # define PCMK__CRM_PENGINE_RULES__H # include # include # include # include # include #ifdef __cplusplus extern "C" { #endif gboolean pe_evaluate_rules(xmlNode *ruleset, GHashTable *node_hash, crm_time_t *now, crm_time_t *next_change); -gboolean pe_test_rule(xmlNode *rule, GHashTable *node_hash, - enum rsc_role_e role, crm_time_t *now, - crm_time_t *next_change, pe_match_data_t *match_data); - -gboolean pe_test_expression(xmlNode *expr, GHashTable *node_hash, - enum rsc_role_e role, crm_time_t *now, - crm_time_t *next_change, - pe_match_data_t *match_data); - void pe_eval_nvpairs(xmlNode *top, 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); 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); gboolean pe_eval_rules(xmlNode *ruleset, const pe_rule_eval_data_t *rule_data, crm_time_t *next_change); -gboolean pe_eval_expr(xmlNode *rule, const pe_rule_eval_data_t *rule_data, - crm_time_t *next_change); -gboolean pe_eval_subexpr(xmlNode *expr, const pe_rule_eval_data_t *rule_data, - crm_time_t *next_change); #if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1) #include #endif #ifdef __cplusplus } #endif #endif diff --git a/include/crm/pengine/rules_compat.h b/include/crm/pengine/rules_compat.h index 76d8e8c564..b0a99174cf 100644 --- a/include/crm/pengine/rules_compat.h +++ b/include/crm/pengine/rules_compat.h @@ -1,79 +1,98 @@ /* * 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_PENGINE_RULES_COMPAT__H # define PCMK__CRM_PENGINE_RULES_COMPAT__H #include #include // xmlNode #include // crm_time_t #include #ifdef __cplusplus extern "C" { #endif /** * \file * \brief Deprecated Pacemaker rule API * \ingroup pengine * \deprecated Do not include this header directly. The rule APIs in this * header, and the header itself, will be removed in a future * release. */ //! \deprecated Use pe_evaluate_rules() instead gboolean test_ruleset(xmlNode *ruleset, GHashTable *node_hash, crm_time_t *now); -//! \deprecated Use pe_test_rule() instead +//! \deprecated Use pcmk_evaluate_rule() instead gboolean test_rule(xmlNode *rule, GHashTable *node_hash, enum rsc_role_e role, crm_time_t *now); -//! \deprecated Use pe_test_rule() instead +//! \deprecated Use pcmk_evaluate_rule() instead +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); + +//! \deprecated Use pcmk_evaluate_rule() instead 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); -//! \deprecated Use pe_test_rule() instead +//! \deprecated Use pcmk_evaluate_rule() instead 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); -//! \deprecated Use pe_test_expression() instead +//! \deprecated Use pcmk_evaluate_rule() on parent rule instead +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); + +//! \deprecated Use pcmk_evaluate_rule() on parent rule instead gboolean test_expression(xmlNode *expr, GHashTable *node_hash, enum rsc_role_e role, crm_time_t *now); -//! \deprecated Use pe_test_expression() instead +//! \deprecated Use pcmk_evaluate_rule() on parent rule instead 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); -//! \deprecated Use pe_test_expression() instead +//! \deprecated Use pcmk_evaluate_rule() on parent rule instead 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); +//! \deprecated Use pcmk_evaluate_rule() on parent rule instead +gboolean pe_eval_expr(xmlNode *rule, const pe_rule_eval_data_t *rule_data, + crm_time_t *next_change); + +//! \deprecated Use pcmk_evaluate_rule() on parent rule instead +gboolean pe_eval_subexpr(xmlNode *expr, const pe_rule_eval_data_t *rule_data, + crm_time_t *next_change); + //! \deprecated Use pe_unpack_nvpairs() instead 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); //! \deprecated Do not use enum expression_type find_expression_type(xmlNode *expr); //! \deprecated Do not use char *pe_expand_re_matches(const char *string, const pe_re_match_data_t *match_data); #ifdef __cplusplus } #endif #endif // PCMK__CRM_PENGINE_RULES_COMPAT__H diff --git a/include/crm/pengine/rules_internal.h b/include/crm/pengine/rules_internal.h index 4d6741b298..d659ee043b 100644 --- a/include/crm/pengine/rules_internal.h +++ b/include/crm/pengine/rules_internal.h @@ -1,31 +1,22 @@ /* * Copyright 2015-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 RULES_INTERNAL_H #define RULES_INTERNAL_H #include #include #include #include #include GList *pe_unpack_alerts(const xmlNode *alerts); void pe_free_alert_list(GList *alert_list); -gboolean pe__eval_attr_expr(const xmlNode *expr, - const pe_rule_eval_data_t *rule_data); -gboolean pe__eval_op_expr(const xmlNode *expr, - const pe_rule_eval_data_t *rule_data); -gboolean pe__eval_role_expr(const xmlNode *expr, - const pe_rule_eval_data_t *rule_data); -gboolean pe__eval_rsc_expr(const xmlNode *expr, - const pe_rule_eval_data_t *rule_data); - #endif diff --git a/lib/common/crmcommon_private.h b/lib/common/crmcommon_private.h index dc598673a5..4b3f984177 100644 --- a/lib/common/crmcommon_private.h +++ b/lib/common/crmcommon_private.h @@ -1,409 +1,422 @@ /* * 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 // gchar, 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 { gchar *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; // XML entity references #define PCMK__XML_ENTITY_AMP "&" #define PCMK__XML_ENTITY_GT ">" #define PCMK__XML_ENTITY_LT "<" #define PCMK__XML_ENTITY_QUOT """ //! 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__xml_tree_foreach(xmlNode *xml, bool (*fn)(xmlNode *, void *), void *user_data); 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); +G_GNUC_INTERNAL +int pcmk__evaluate_attr_expression(const xmlNode *expression, + const pcmk_rule_input_t *rule_input); + +G_GNUC_INTERNAL +int pcmk__evaluate_rsc_expression(const xmlNode *expr, + const pcmk_rule_input_t *rule_input); + +G_GNUC_INTERNAL +int pcmk__evaluate_op_expression(const xmlNode *expr, + const pcmk_rule_input_t *rule_input); + + /* * 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 f13c3deade..e719df3fba 100644 --- a/lib/common/rules.c +++ b/lib/common/rules.c @@ -1,1295 +1,1465 @@ /* * 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 + * \brief Get the condition type corresponding to given condition XML * - * \param[in] expr Rule expression XML + * \param[in] condition Rule condition XML * - * \return Expression type corresponding to \p expr + * \return Condition type corresponding to \p condition */ enum expression_type -pcmk__expression_type(const xmlNode *expr) +pcmk__condition_type(const xmlNode *condition) { const char *name = NULL; // Expression types based on element name - if (pcmk__xe_is(expr, PCMK_XE_DATE_EXPRESSION)) { - return pcmk__subexpr_datetime; + if (pcmk__xe_is(condition, PCMK_XE_DATE_EXPRESSION)) { + return pcmk__condition_datetime; - } else if (pcmk__xe_is(expr, PCMK_XE_RSC_EXPRESSION)) { - return pcmk__subexpr_resource; + } else if (pcmk__xe_is(condition, PCMK_XE_RSC_EXPRESSION)) { + return pcmk__condition_resource; - } else if (pcmk__xe_is(expr, PCMK_XE_OP_EXPRESSION)) { - return pcmk__subexpr_operation; + } else if (pcmk__xe_is(condition, PCMK_XE_OP_EXPRESSION)) { + return pcmk__condition_operation; - } else if (pcmk__xe_is(expr, PCMK_XE_RULE)) { - return pcmk__subexpr_rule; + } else if (pcmk__xe_is(condition, PCMK_XE_RULE)) { + return pcmk__condition_rule; - } else if (!pcmk__xe_is(expr, PCMK_XE_EXPRESSION)) { - return pcmk__subexpr_unknown; + } else if (!pcmk__xe_is(condition, PCMK_XE_EXPRESSION)) { + return pcmk__condition_unknown; } // Expression types based on node attribute name - name = crm_element_value(expr, PCMK_XA_ATTRIBUTE); + name = crm_element_value(condition, PCMK_XA_ATTRIBUTE); if (pcmk__str_any_of(name, CRM_ATTR_UNAME, CRM_ATTR_KIND, CRM_ATTR_ID, NULL)) { - return pcmk__subexpr_location; + return pcmk__condition_location; } - return pcmk__subexpr_attribute; + return pcmk__condition_attribute; } /*! * \internal * \brief Get parent XML element's ID for logging purposes * * \param[in] xml XML of a subelement * * \return ID of \p xml's parent for logging purposes (guaranteed non-NULL) */ static const char * loggable_parent_id(const xmlNode *xml) { // Default if called without parent (likely for unit testing) const char *parent_id = "implied"; if ((xml != NULL) && (xml->parent != NULL)) { parent_id = pcmk__xe_id(xml->parent); if (parent_id == NULL) { // Not possible with schema validation enabled parent_id = "without ID"; } } return parent_id; } /*! * \internal * \brief Get the moon phase corresponding to a given date/time * * \param[in] now Date/time to get moon phase for * * \return Phase of the moon corresponding to \p now, where 0 is the new moon * and 7 is the full moon * \deprecated This feature has been deprecated since 2.1.6. */ static int phase_of_the_moon(const crm_time_t *now) { /* As per the nethack rules: * - A moon period is 29.53058 days ~= 30 * - A year is 365.2422 days * - Number of days moon phase advances on first day of year compared to * preceding year is (365.2422 - 12 * 29.53058) ~= 11 * - Number of years until same phases fall on the same days of the month * is 18.6 ~= 19 * - Moon phase on first day of year (epact) ~= (11 * (year%19) + 29) % 30 * (29 as initial condition) * - Current phase in days = first day phase + days elapsed in year * - 6 moons ~= 177 days ~= 8 reported phases * 22 (+ 11/22 for rounding) */ uint32_t epact, diy, goldn; uint32_t y; crm_time_get_ordinal(now, &y, &diy); goldn = (y % 19) + 1; epact = (11 * goldn + 18) % 30; if (((epact == 25) && (goldn > 11)) || (epact == 24)) { epact++; } return (((((diy + epact) * 6) + 11) % 177) / 22) & 7; } /*! * \internal * \brief Check an integer value against a range from a date specification * * \param[in] date_spec XML of PCMK_XE_DATE_SPEC element to check * \param[in] id XML ID for logging purposes * \param[in] attr Name of XML attribute with range to check against * \param[in] value Value to compare against range * * \return Standard Pacemaker return code (specifically, pcmk_rc_before_range, * pcmk_rc_after_range, or pcmk_rc_ok to indicate that result is either * within range or undetermined) * \note We return pcmk_rc_ok for an undetermined result so we can continue * checking the next range attribute. */ static int check_range(const xmlNode *date_spec, const char *id, const char *attr, uint32_t value) { int rc = pcmk_rc_ok; const char *range = crm_element_value(date_spec, attr); long long low, high; if (range == NULL) { // Attribute not present goto bail; } if (pcmk__parse_ll_range(range, &low, &high) != pcmk_rc_ok) { // Invalid range /* @COMPAT When we can break behavioral backward compatibility, treat * the entire rule as not passing. */ pcmk__config_err("Ignoring " PCMK_XE_DATE_SPEC " %s attribute %s because '%s' is not a valid range", id, attr, range); } else if ((low != -1) && (value < low)) { rc = pcmk_rc_before_range; } else if ((high != -1) && (value > high)) { rc = pcmk_rc_after_range; } bail: crm_trace("Checked " PCMK_XE_DATE_SPEC " %s %s='%s' for %" PRIu32 ": %s", id, attr, pcmk__s(range, ""), value, pcmk_rc_str(rc)); return rc; } /*! * \internal * \brief Evaluate a date specification for a given date/time * * \param[in] date_spec XML of PCMK_XE_DATE_SPEC element to evaluate * \param[in] now Time to check * * \return Standard Pacemaker return code (specifically, EINVAL for NULL * arguments, pcmk_rc_ok if time matches specification, or * pcmk_rc_before_range, pcmk_rc_after_range, or pcmk_rc_op_unsatisfied * as appropriate to how time relates to specification) */ int pcmk__evaluate_date_spec(const xmlNode *date_spec, const crm_time_t *now) { const char *id = NULL; const char *parent_id = loggable_parent_id(date_spec); // Range attributes that can be specified for a PCMK_XE_DATE_SPEC element struct range { const char *attr; uint32_t value; } ranges[] = { { PCMK_XA_YEARS, 0U }, { PCMK_XA_MONTHS, 0U }, { PCMK_XA_MONTHDAYS, 0U }, { PCMK_XA_HOURS, 0U }, { PCMK_XA_MINUTES, 0U }, { PCMK_XA_SECONDS, 0U }, { PCMK_XA_YEARDAYS, 0U }, { PCMK_XA_WEEKYEARS, 0U }, { PCMK_XA_WEEKS, 0U }, { PCMK_XA_WEEKDAYS, 0U }, { PCMK__XA_MOON, 0U }, }; if ((date_spec == NULL) || (now == NULL)) { return EINVAL; } // Get specification ID (for logging) id = pcmk__xe_id(date_spec); if (pcmk__str_empty(id)) { // Not possible with schema validation enabled /* @COMPAT When we can break behavioral backward compatibility, * fail the specification */ pcmk__config_warn(PCMK_XE_DATE_SPEC " subelement of " PCMK_XE_DATE_EXPRESSION " %s has no " PCMK_XA_ID, parent_id); id = "without ID"; // for logging } // Year, month, day crm_time_get_gregorian(now, &(ranges[0].value), &(ranges[1].value), &(ranges[2].value)); // Hour, minute, second crm_time_get_timeofday(now, &(ranges[3].value), &(ranges[4].value), &(ranges[5].value)); // Year (redundant) and day of year crm_time_get_ordinal(now, &(ranges[0].value), &(ranges[6].value)); // Week year, week of week year, day of week crm_time_get_isoweek(now, &(ranges[7].value), &(ranges[8].value), &(ranges[9].value)); // Moon phase (deprecated) ranges[10].value = phase_of_the_moon(now); if (crm_element_value(date_spec, PCMK__XA_MOON) != NULL) { pcmk__config_warn("Support for '" PCMK__XA_MOON "' in " PCMK_XE_DATE_SPEC " elements (such as %s) is " "deprecated and will be removed in a future release " "of Pacemaker", id); } for (int i = 0; i < PCMK__NELEM(ranges); ++i) { int rc = check_range(date_spec, id, ranges[i].attr, ranges[i].value); if (rc != pcmk_rc_ok) { return rc; } } // All specified ranges passed, or none were given (also considered a pass) return pcmk_rc_ok; } #define ADD_COMPONENT(component) do { \ int sub_rc = pcmk__add_time_from_xml(*end, component, duration); \ if (sub_rc != pcmk_rc_ok) { \ /* @COMPAT return sub_rc when we can break compatibility */ \ pcmk__config_warn("Ignoring %s in " PCMK_XE_DURATION " %s " \ "because it is invalid", \ pcmk__time_component_attr(component), id); \ rc = sub_rc; \ } \ } while (0) /*! * \internal * \brief Given a duration and a start time, calculate the end time * * \param[in] duration XML of PCMK_XE_DURATION element * \param[in] start Start time * \param[out] end Where to store end time (\p *end must be NULL * initially) * * \return Standard Pacemaker return code * \note The caller is responsible for freeing \p *end using crm_time_free(). */ int pcmk__unpack_duration(const xmlNode *duration, const crm_time_t *start, crm_time_t **end) { int rc = pcmk_rc_ok; const char *id = NULL; const char *parent_id = loggable_parent_id(duration); if ((start == NULL) || (duration == NULL) || (end == NULL) || (*end != NULL)) { return EINVAL; } // Get duration ID (for logging) id = pcmk__xe_id(duration); if (pcmk__str_empty(id)) { // Not possible with schema validation enabled /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_unpack_error instead */ pcmk__config_warn(PCMK_XE_DURATION " subelement of " PCMK_XE_DATE_EXPRESSION " %s has no " PCMK_XA_ID, parent_id); id = "without ID"; } *end = pcmk_copy_time(start); ADD_COMPONENT(pcmk__time_years); ADD_COMPONENT(pcmk__time_months); ADD_COMPONENT(pcmk__time_weeks); ADD_COMPONENT(pcmk__time_days); ADD_COMPONENT(pcmk__time_hours); ADD_COMPONENT(pcmk__time_minutes); ADD_COMPONENT(pcmk__time_seconds); return rc; } /*! * \internal * \brief Evaluate a range check for a given date/time * * \param[in] date_expression XML of PCMK_XE_DATE_EXPRESSION element * \param[in] id Expression ID for logging purposes * \param[in] now Date/time to compare * \param[in,out] next_change If not NULL, set this to when the evaluation * will change, if known and earlier than the * original value * * \return Standard Pacemaker return code */ static int evaluate_in_range(const xmlNode *date_expression, const char *id, const crm_time_t *now, crm_time_t *next_change) { crm_time_t *start = NULL; crm_time_t *end = NULL; if (pcmk__xe_get_datetime(date_expression, PCMK_XA_START, &start) != pcmk_rc_ok) { /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_unpack_error */ pcmk__config_warn("Ignoring " PCMK_XA_START " in " PCMK_XE_DATE_EXPRESSION " %s because it is invalid", id); } if (pcmk__xe_get_datetime(date_expression, PCMK_XA_END, &end) != pcmk_rc_ok) { /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_unpack_error */ pcmk__config_warn("Ignoring " PCMK_XA_END " in " PCMK_XE_DATE_EXPRESSION " %s because it is invalid", id); } if ((start == NULL) && (end == NULL)) { // Not possible with schema validation enabled /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_unpack_error */ pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " "passing because in_range requires at least one of " PCMK_XA_START " or " PCMK_XA_END, id); return pcmk_rc_undetermined; } if (end == NULL) { xmlNode *duration = pcmk__xe_first_child(date_expression, PCMK_XE_DURATION, NULL, NULL); if (duration != NULL) { /* @COMPAT When we can break behavioral backward compatibility, * return the result of this if not OK */ pcmk__unpack_duration(duration, start, &end); } } if ((start != NULL) && (crm_time_compare(now, start) < 0)) { pcmk__set_time_if_earlier(next_change, start); crm_time_free(start); crm_time_free(end); return pcmk_rc_before_range; } if (end != NULL) { if (crm_time_compare(now, end) > 0) { crm_time_free(start); crm_time_free(end); return pcmk_rc_after_range; } // Evaluation doesn't change until second after end if (next_change != NULL) { crm_time_add_seconds(end, 1); pcmk__set_time_if_earlier(next_change, end); } } crm_time_free(start); crm_time_free(end); return pcmk_rc_within_range; } /*! * \internal * \brief Evaluate a greater-than check for a given date/time * * \param[in] date_expression XML of PCMK_XE_DATE_EXPRESSION element * \param[in] id Expression ID for logging purposes * \param[in] now Date/time to compare * \param[in,out] next_change If not NULL, set this to when the evaluation * will change, if known and earlier than the * original value * * \return Standard Pacemaker return code */ static int evaluate_gt(const xmlNode *date_expression, const char *id, const crm_time_t *now, crm_time_t *next_change) { crm_time_t *start = NULL; if (pcmk__xe_get_datetime(date_expression, PCMK_XA_START, &start) != pcmk_rc_ok) { /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_unpack_error */ pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " "passing because " PCMK_XA_START " is invalid", id); return pcmk_rc_undetermined; } if (start == NULL) { // Not possible with schema validation enabled /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_unpack_error */ pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " "passing because " PCMK_VALUE_GT " requires " PCMK_XA_START, id); return pcmk_rc_undetermined; } if (crm_time_compare(now, start) > 0) { crm_time_free(start); return pcmk_rc_within_range; } // Evaluation doesn't change until second after start time crm_time_add_seconds(start, 1); pcmk__set_time_if_earlier(next_change, start); crm_time_free(start); return pcmk_rc_before_range; } /*! * \internal * \brief Evaluate a less-than check for a given date/time * * \param[in] date_expression XML of PCMK_XE_DATE_EXPRESSION element * \param[in] id Expression ID for logging purposes * \param[in] now Date/time to compare * \param[in,out] next_change If not NULL, set this to when the evaluation * will change, if known and earlier than the * original value * * \return Standard Pacemaker return code */ static int evaluate_lt(const xmlNode *date_expression, const char *id, const crm_time_t *now, crm_time_t *next_change) { crm_time_t *end = NULL; if (pcmk__xe_get_datetime(date_expression, PCMK_XA_END, &end) != pcmk_rc_ok) { /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_unpack_error */ pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " "passing because " PCMK_XA_END " is invalid", id); return pcmk_rc_undetermined; } if (end == NULL) { // Not possible with schema validation enabled /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_unpack_error */ pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " "passing because " PCMK_VALUE_GT " requires " PCMK_XA_END, id); return pcmk_rc_undetermined; } if (crm_time_compare(now, end) < 0) { pcmk__set_time_if_earlier(next_change, end); crm_time_free(end); return pcmk_rc_within_range; } crm_time_free(end); return pcmk_rc_after_range; } /*! * \internal * \brief Evaluate a rule's date expression for a given date/time * * \param[in] date_expression XML of a PCMK_XE_DATE_EXPRESSION element * \param[in] now Time to use for evaluation * \param[in,out] next_change If not NULL, set this to when the evaluation * will change, if known and earlier than the * original value * * \return Standard Pacemaker return code (unlike most other evaluation * functions, this can return either pcmk_rc_ok or pcmk_rc_within_range * on success) */ int pcmk__evaluate_date_expression(const xmlNode *date_expression, const crm_time_t *now, crm_time_t *next_change) { const char *id = NULL; const char *op = NULL; int rc = pcmk_rc_undetermined; if ((date_expression == NULL) || (now == NULL)) { return EINVAL; } // Get expression ID (for logging) id = pcmk__xe_id(date_expression); if (pcmk__str_empty(id)) { // Not possible with schema validation enabled /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_unpack_error */ pcmk__config_warn(PCMK_XE_DATE_EXPRESSION " element has no " PCMK_XA_ID); id = "without ID"; // for logging } op = crm_element_value(date_expression, PCMK_XA_OPERATION); if (pcmk__str_eq(op, PCMK_VALUE_IN_RANGE, pcmk__str_null_matches|pcmk__str_casei)) { rc = evaluate_in_range(date_expression, id, now, next_change); } else if (pcmk__str_eq(op, PCMK_VALUE_DATE_SPEC, pcmk__str_casei)) { xmlNode *date_spec = pcmk__xe_first_child(date_expression, PCMK_XE_DATE_SPEC, NULL, NULL); if (date_spec == NULL) { // Not possible with schema validation enabled /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_unpack_error */ pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s " "as not passing because " PCMK_VALUE_DATE_SPEC " operations require a " PCMK_XE_DATE_SPEC " subelement", id); } else { // @TODO set next_change appropriately rc = pcmk__evaluate_date_spec(date_spec, now); } } else if (pcmk__str_eq(op, PCMK_VALUE_GT, pcmk__str_casei)) { rc = evaluate_gt(date_expression, id, now, next_change); } else if (pcmk__str_eq(op, PCMK_VALUE_LT, pcmk__str_casei)) { rc = evaluate_lt(date_expression, id, now, next_change); } else { // Not possible with schema validation enabled /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_unpack_error */ pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not passing because '%s' is not a valid " PCMK_XE_OPERATION, op); } crm_trace(PCMK_XE_DATE_EXPRESSION " %s (%s): %s (%d)", id, op, pcmk_rc_str(rc), rc); return rc; } /*! * \internal * \brief Go through submatches in a string, either counting how many bytes * would be needed for the expansion, or performing the expansion, * as requested * * \param[in] string String possibly containing submatch variables * \param[in] match String that matched the regular expression * \param[in] submatches Regular expression submatches (as set by regexec()) * \param[in] nmatches Number of entries in \p submatches[] * \param[out] expansion If not NULL, expand string here (must be * pre-allocated to appropriate size) * \param[out] nbytes If not NULL, set to size needed for expansion * * \return true if any expansion is needed, otherwise false */ static bool process_submatches(const char *string, const char *match, const regmatch_t submatches[], int nmatches, char *expansion, size_t *nbytes) { bool expanded = false; const char *src = string; if (nbytes != NULL) { *nbytes = 1; // Include space for terminator } while (*src != '\0') { int submatch = 0; size_t match_len = 0; if ((src[0] != '%') || !isdigit(src[1])) { /* src does not point to the first character of a %N sequence, * so expand this character as-is */ if (expansion != NULL) { *expansion++ = *src; } if (nbytes != NULL) { ++(*nbytes); } ++src; continue; } submatch = src[1] - '0'; src += 2; // Skip over %N sequence in source string expanded = true; // Expansion will be different from source // Omit sequence from expansion unless it has a non-empty match if ((nmatches <= submatch) // Not enough submatches || (submatches[submatch].rm_so < 0) // Pattern did not match || (submatches[submatch].rm_eo <= submatches[submatch].rm_so)) { // Match was empty continue; } match_len = submatches[submatch].rm_eo - submatches[submatch].rm_so; if (nbytes != NULL) { *nbytes += match_len; } if (expansion != NULL) { memcpy(expansion, match + submatches[submatch].rm_so, match_len); expansion += match_len; } } return expanded; } /*! * \internal * \brief Expand any regular expression submatches (%0-%9) in a string * * \param[in] string String possibly containing submatch variables * \param[in] match String that matched the regular expression * \param[in] submatches Regular expression submatches (as set by regexec()) * \param[in] nmatches Number of entries in \p submatches[] * * \return Newly allocated string identical to \p string with submatches * expanded on success, or NULL if no expansions were needed * \note The caller is responsible for freeing the result with free() */ char * pcmk__replace_submatches(const char *string, const char *match, const regmatch_t submatches[], int nmatches) { size_t nbytes = 0; char *result = NULL; if (pcmk__str_empty(string) || pcmk__str_empty(match)) { return NULL; // Nothing to expand } // Calculate how much space will be needed for expanded string if (!process_submatches(string, match, submatches, nmatches, NULL, &nbytes)) { return NULL; // No expansions needed } // Allocate enough space for expanded string result = pcmk__assert_alloc(nbytes, sizeof(char)); // Expand submatches (void) process_submatches(string, match, submatches, nmatches, result, NULL); return result; } /*! * \internal * \brief Parse a comparison type from a string * * \param[in] op String with comparison type (valid values are * \c PCMK_VALUE_DEFINED, \c PCMK_VALUE_NOT_DEFINED, * \c PCMK_VALUE_EQ, \c PCMK_VALUE_NE, * \c PCMK_VALUE_LT, \c PCMK_VALUE_LTE, * \c PCMK_VALUE_GT, or \c PCMK_VALUE_GTE) * * \return Comparison type corresponding to \p op */ enum pcmk__comparison pcmk__parse_comparison(const char *op) { if (pcmk__str_eq(op, PCMK_VALUE_DEFINED, pcmk__str_casei)) { return pcmk__comparison_defined; } else if (pcmk__str_eq(op, PCMK_VALUE_NOT_DEFINED, pcmk__str_casei)) { return pcmk__comparison_undefined; } else if (pcmk__str_eq(op, PCMK_VALUE_EQ, pcmk__str_casei)) { return pcmk__comparison_eq; } else if (pcmk__str_eq(op, PCMK_VALUE_NE, pcmk__str_casei)) { return pcmk__comparison_ne; } else if (pcmk__str_eq(op, PCMK_VALUE_LT, pcmk__str_casei)) { return pcmk__comparison_lt; } else if (pcmk__str_eq(op, PCMK_VALUE_LTE, pcmk__str_casei)) { return pcmk__comparison_lte; } else if (pcmk__str_eq(op, PCMK_VALUE_GT, pcmk__str_casei)) { return pcmk__comparison_gt; } else if (pcmk__str_eq(op, PCMK_VALUE_GTE, pcmk__str_casei)) { return pcmk__comparison_gte; } return pcmk__comparison_unknown; } /*! * \internal * \brief Parse a value type from a string * * \param[in] type String with value type (valid values are NULL, * \c PCMK_VALUE_STRING, \c PCMK_VALUE_INTEGER, * \c PCMK_VALUE_NUMBER, and \c PCMK_VALUE_VERSION) * \param[in] op Operation type (used only to select default) * \param[in] value1 First value being compared (used only to select default) * \param[in] value2 Second value being compared (used only to select default) */ enum pcmk__type pcmk__parse_type(const char *type, enum pcmk__comparison op, const char *value1, const char *value2) { if (type == NULL) { switch (op) { case pcmk__comparison_lt: case pcmk__comparison_lte: case pcmk__comparison_gt: case pcmk__comparison_gte: if (((value1 != NULL) && (strchr(value1, '.') != NULL)) || ((value2 != NULL) && (strchr(value2, '.') != NULL))) { return pcmk__type_number; } return pcmk__type_integer; default: return pcmk__type_string; } } if (pcmk__str_eq(type, PCMK_VALUE_STRING, pcmk__str_casei)) { return pcmk__type_string; } else if (pcmk__str_eq(type, PCMK_VALUE_INTEGER, pcmk__str_casei)) { return pcmk__type_integer; } else if (pcmk__str_eq(type, PCMK_VALUE_NUMBER, pcmk__str_casei)) { return pcmk__type_number; } else if (pcmk__str_eq(type, PCMK_VALUE_VERSION, pcmk__str_casei)) { return pcmk__type_version; } return pcmk__type_unknown; } /*! * \internal * \brief Compare two strings according to a given type * * \param[in] value1 String with first value to compare * \param[in] value2 String with second value to compare * \param[in] type How to interpret the values * * \return Standard comparison result (a negative integer if \p value1 is * lesser, 0 if the values are equal, and a positive integer if * \p value1 is greater) */ int pcmk__cmp_by_type(const char *value1, const char *value2, enum pcmk__type type) { // NULL compares as less than non-NULL if (value2 == NULL) { return (value1 == NULL)? 0 : 1; } if (value1 == NULL) { return -1; } switch (type) { case pcmk__type_string: return strcasecmp(value1, value2); case pcmk__type_integer: { long long integer1; long long integer2; if ((pcmk__scan_ll(value1, &integer1, 0LL) != pcmk_rc_ok) || (pcmk__scan_ll(value2, &integer2, 0LL) != pcmk_rc_ok)) { crm_warn("Comparing '%s' and '%s' as strings because " "invalid as integers", value1, value2); return strcasecmp(value1, value2); } return (integer1 < integer2)? -1 : (integer1 > integer2)? 1 : 0; } break; case pcmk__type_number: { double num1; double num2; if ((pcmk__scan_double(value1, &num1, NULL, NULL) != pcmk_rc_ok) || (pcmk__scan_double(value2, &num2, NULL, NULL) != pcmk_rc_ok)) { crm_warn("Comparing '%s' and '%s' as strings because invalid as " "numbers", value1, value2); return strcasecmp(value1, value2); } return (num1 < num2)? -1 : (num1 > num2)? 1 : 0; } break; case pcmk__type_version: return compare_version(value1, value2); default: // Invalid type return 0; } } /*! * \internal * \brief Parse a reference value source from a string * * \param[in] source String indicating reference value source * * \return Reference value source corresponding to \p source */ enum pcmk__reference_source pcmk__parse_source(const char *source) { if (pcmk__str_eq(source, PCMK_VALUE_LITERAL, pcmk__str_casei|pcmk__str_null_matches)) { return pcmk__source_literal; } else if (pcmk__str_eq(source, PCMK_VALUE_PARAM, pcmk__str_casei)) { return pcmk__source_instance_attrs; } else if (pcmk__str_eq(source, PCMK_VALUE_META, pcmk__str_casei)) { return pcmk__source_meta_attrs; } else { return pcmk__source_unknown; } } +/*! + * \internal + * \brief Parse a boolean operator from a string + * + * \param[in] combine String indicating boolean operator + * + * \return Enumeration value corresponding to \p combine + */ +enum pcmk__combine +pcmk__parse_combine(const char *combine) +{ + if (pcmk__str_eq(combine, PCMK_VALUE_AND, + pcmk__str_null_matches|pcmk__str_casei)) { + return pcmk__combine_and; + + } else if (pcmk__str_eq(combine, PCMK_VALUE_OR, pcmk__str_casei)) { + return pcmk__combine_or; + + } else { + return pcmk__combine_unknown; + } +} + /*! * \internal * \brief Get the result of a node attribute comparison for rule evaluation * * \param[in] actual Actual node attribute value * \param[in] reference Node attribute value from rule (ignored for * \p comparison of \c pcmk__comparison_defined or * \c pcmk__comparison_undefined) * \param[in] type How to interpret the values * \param[in] comparison How to compare the values * * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok if the * comparison passes, and some other value if it does not) */ static int evaluate_attr_comparison(const char *actual, const char *reference, enum pcmk__type type, enum pcmk__comparison comparison) { int cmp = 0; switch (comparison) { case pcmk__comparison_defined: return (actual != NULL)? pcmk_rc_ok : pcmk_rc_op_unsatisfied; case pcmk__comparison_undefined: return (actual == NULL)? pcmk_rc_ok : pcmk_rc_op_unsatisfied; default: break; } cmp = pcmk__cmp_by_type(actual, reference, type); switch (comparison) { case pcmk__comparison_eq: return (cmp == 0)? pcmk_rc_ok : pcmk_rc_op_unsatisfied; case pcmk__comparison_ne: return (cmp != 0)? pcmk_rc_ok : pcmk_rc_op_unsatisfied; default: break; } if ((actual == NULL) || (reference == NULL)) { return pcmk_rc_op_unsatisfied; // Comparison would be meaningless } switch (comparison) { case pcmk__comparison_lt: return (cmp < 0)? pcmk_rc_ok : pcmk_rc_after_range; case pcmk__comparison_lte: return (cmp <= 0)? pcmk_rc_ok : pcmk_rc_after_range; case pcmk__comparison_gt: return (cmp > 0)? pcmk_rc_ok : pcmk_rc_before_range; case pcmk__comparison_gte: return (cmp >= 0)? pcmk_rc_ok : pcmk_rc_before_range; default: // Not possible with schema validation enabled return pcmk_rc_op_unsatisfied; } } /*! * \internal * \brief Get a reference value from a configured source * * \param[in] value Value given in rule expression * \param[in] source Reference value source * \param[in] rule_input Values used to evaluate rule criteria */ static const char * value_from_source(const char *value, enum pcmk__reference_source source, const pcmk_rule_input_t *rule_input) { GHashTable *table = NULL; if (pcmk__str_empty(value)) { /* @COMPAT When we can break backward compatibility, drop this block so * empty strings are treated as such (there should never be an empty * string as an instance attribute or meta-attribute name, so those will * get NULL anyway, but it could matter for literal comparisons) */ return NULL; } switch (source) { case pcmk__source_literal: return value; case pcmk__source_instance_attrs: table = rule_input->rsc_params; break; case pcmk__source_meta_attrs: table = rule_input->rsc_meta; break; default: return NULL; // Not possible } if (table == NULL) { return NULL; } return (const char *) g_hash_table_lookup(table, value); } /*! * \internal * \brief Evaluate a node attribute rule expression * * \param[in] expression XML of a rule's PCMK_XE_EXPRESSION subelement * \param[in] rule_input Values used to evaluate rule criteria * * \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression * passes, some other value if it does not) */ int pcmk__evaluate_attr_expression(const xmlNode *expression, const pcmk_rule_input_t *rule_input) { const char *id = NULL; const char *op = NULL; const char *attr = NULL; const char *type_s = NULL; const char *value = NULL; const char *actual = NULL; const char *source_s = NULL; const char *reference = NULL; char *expanded_attr = NULL; int rc = pcmk_rc_ok; enum pcmk__type type = pcmk__type_unknown; enum pcmk__reference_source source = pcmk__source_unknown; enum pcmk__comparison comparison = pcmk__comparison_unknown; if ((expression == NULL) || (rule_input == NULL)) { return EINVAL; } // Get expression ID (for logging) id = pcmk__xe_id(expression); if (pcmk__str_empty(id)) { /* @COMPAT When we can break behavioral backward compatibility, * fail the expression */ pcmk__config_warn(PCMK_XE_EXPRESSION " element has no " PCMK_XA_ID); id = "without ID"; // for logging } /* Get name of node attribute to compare (expanding any %0-%9 to * regular expression submatches) */ attr = crm_element_value(expression, PCMK_XA_ATTRIBUTE); if (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; } /*! * \internal * \brief Evaluate a resource rule expression * * \param[in] rsc_expression XML of rule's \c PCMK_XE_RSC_EXPRESSION subelement * \param[in] rule_input Values used to evaluate rule criteria * * \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression * passes, some other value if it does not) */ int pcmk__evaluate_rsc_expression(const xmlNode *rsc_expression, const pcmk_rule_input_t *rule_input) { const char *id = NULL; const char *standard = NULL; const char *provider = NULL; const char *type = NULL; if ((rsc_expression == NULL) || (rule_input == NULL)) { return EINVAL; } // Validate XML ID id = pcmk__xe_id(rsc_expression); if (pcmk__str_empty(id)) { // Not possible with schema validation enabled /* @COMPAT When we can break behavioral backward compatibility, * fail the expression */ pcmk__config_warn(PCMK_XE_RSC_EXPRESSION " has no " PCMK_XA_ID); id = "without ID"; // for logging } // Compare resource standard standard = crm_element_value(rsc_expression, PCMK_XA_CLASS); if ((standard != NULL) && !pcmk__str_eq(standard, rule_input->rsc_standard, pcmk__str_none)) { crm_trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because " "actual standard '%s' doesn't match '%s'", id, pcmk__s(rule_input->rsc_standard, ""), standard); return pcmk_rc_op_unsatisfied; } // Compare resource provider provider = crm_element_value(rsc_expression, PCMK_XA_PROVIDER); if ((provider != NULL) && !pcmk__str_eq(provider, rule_input->rsc_provider, pcmk__str_none)) { crm_trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because " "actual provider '%s' doesn't match '%s'", id, pcmk__s(rule_input->rsc_provider, ""), provider); return pcmk_rc_op_unsatisfied; } // Compare resource agent type type = crm_element_value(rsc_expression, PCMK_XA_TYPE); if ((type != NULL) && !pcmk__str_eq(type, rule_input->rsc_agent, pcmk__str_none)) { crm_trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because " "actual agent '%s' doesn't match '%s'", id, pcmk__s(rule_input->rsc_agent, ""), type); return pcmk_rc_op_unsatisfied; } crm_trace(PCMK_XE_RSC_EXPRESSION " %s is satisfied by %s%s%s:%s", id, pcmk__s(standard, ""), ((provider == NULL)? "" : ":"), pcmk__s(provider, ""), pcmk__s(type, "")); return pcmk_rc_ok; } /*! * \internal * \brief Evaluate an operation rule expression * * \param[in] op_expression XML of a rule's \c PCMK_XE_OP_EXPRESSION subelement * \param[in] rule_input Values used to evaluate rule criteria * * \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression * is satisfied, some other value if it is not) */ int pcmk__evaluate_op_expression(const xmlNode *op_expression, const pcmk_rule_input_t *rule_input) { const char *id = NULL; const char *name = NULL; const char *interval_s = NULL; guint interval_ms = 0U; if ((op_expression == NULL) || (rule_input == NULL)) { return EINVAL; } // Get operation expression ID (for logging) id = pcmk__xe_id(op_expression); if (pcmk__str_empty(id)) { // Not possible with schema validation enabled /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_op_unsatisfied */ pcmk__config_warn(PCMK_XE_OP_EXPRESSION " element has no " PCMK_XA_ID); id = "without ID"; // for logging } // Validate operation name name = crm_element_value(op_expression, PCMK_XA_NAME); if (name == NULL) { // Not possible with schema validation enabled pcmk__config_warn("Treating " PCMK_XE_OP_EXPRESSION " %s as not " "passing because it has no " PCMK_XA_NAME, id); return pcmk_rc_unpack_error; } // Validate operation interval interval_s = crm_element_value(op_expression, PCMK_META_INTERVAL); if (pcmk_parse_interval_spec(interval_s, &interval_ms) != pcmk_rc_ok) { pcmk__config_warn("Treating " PCMK_XE_OP_EXPRESSION " %s as not " "passing because '%s' is not a valid interval", id, interval_s); return pcmk_rc_unpack_error; } // Compare operation name if (!pcmk__str_eq(name, rule_input->op_name, pcmk__str_none)) { crm_trace(PCMK_XE_OP_EXPRESSION " %s is unsatisfied because " "actual name '%s' doesn't match '%s'", id, pcmk__s(rule_input->op_name, ""), name); return pcmk_rc_op_unsatisfied; } // Compare operation interval (unspecified interval matches all) if ((interval_s != NULL) && (interval_ms != rule_input->op_interval_ms)) { crm_trace(PCMK_XE_OP_EXPRESSION " %s is unsatisfied because " "actual interval %s doesn't match %s", id, pcmk__readable_interval(rule_input->op_interval_ms), pcmk__readable_interval(interval_ms)); return pcmk_rc_op_unsatisfied; } crm_trace(PCMK_XE_OP_EXPRESSION " %s is satisfied (name %s, interval %s)", id, name, pcmk__readable_interval(rule_input->op_interval_ms)); return pcmk_rc_ok; } + +/*! + * \internal + * \brief Evaluate a rule condition + * + * \param[in,out] condition XML containing a rule condition (a subrule, or an + * expression of any type) + * \param[in] rule_input Values used to evaluate rule criteria + * \param[out] next_change If not NULL, set to when evaluation will change + * + * \return Standard Pacemaker return code (\c pcmk_rc_ok if the condition + * passes, some other value if it does not) + */ +int +pcmk__evaluate_condition(xmlNode *condition, + const pcmk_rule_input_t *rule_input, + crm_time_t *next_change) +{ + + if ((condition == NULL) || (rule_input == NULL)) { + return EINVAL; + } + + switch (pcmk__condition_type(condition)) { + case pcmk__condition_rule: + return pcmk_evaluate_rule(condition, rule_input, next_change); + + case pcmk__condition_attribute: + case pcmk__condition_location: + return pcmk__evaluate_attr_expression(condition, rule_input); + + case pcmk__condition_datetime: + { + int rc = pcmk__evaluate_date_expression(condition, + rule_input->now, + next_change); + + return (rc == pcmk_rc_within_range)? pcmk_rc_ok : rc; + } + + case pcmk__condition_resource: + return pcmk__evaluate_rsc_expression(condition, rule_input); + + case pcmk__condition_operation: + return pcmk__evaluate_op_expression(condition, rule_input); + + default: // Not possible with schema validation enabled + pcmk__config_err("Treating rule condition %s as not passing " + "because %s is not a valid condition type", + pcmk__s(pcmk__xe_id(condition), "without ID"), + (const char *) condition->name); + return pcmk_rc_unpack_error; + } +} + +/*! + * \brief Evaluate a single rule, including all its conditions + * + * \param[in,out] rule XML containing a rule definition or its id-ref + * \param[in] rule_input Values used to evaluate rule criteria + * \param[out] next_change If not NULL, set to when evaluation will change + * + * \return Standard Pacemaker return code (\c pcmk_rc_ok if the rule is + * satisfied, some other value if it is not) + */ +int +pcmk_evaluate_rule(xmlNode *rule, const pcmk_rule_input_t *rule_input, + crm_time_t *next_change) +{ + bool empty = true; + int rc = pcmk_rc_ok; + const char *id = NULL; + const char *value = NULL; + enum pcmk__combine combine = pcmk__combine_unknown; + + if ((rule == NULL) || (rule_input == NULL)) { + return EINVAL; + } + + rule = expand_idref(rule, NULL); + if (rule == NULL) { + // Not possible with schema validation enabled; message already logged + return pcmk_rc_unpack_error; + } + + // Validate XML ID + id = pcmk__xe_id(rule); + if (pcmk__str_empty(id)) { + /* @COMPAT When we can break behavioral backward compatibility, + * fail the rule + */ + pcmk__config_warn(PCMK_XE_RULE " has no " PCMK_XA_ID); + id = "without ID"; // for logging + } + + value = crm_element_value(rule, PCMK_XA_BOOLEAN_OP); + combine = pcmk__parse_combine(value); + switch (combine) { + case pcmk__combine_and: + // For "and", rc defaults to success (reset on failure below) + break; + + case pcmk__combine_or: + // For "or", rc defaults to failure (reset on success below) + rc = pcmk_rc_op_unsatisfied; + break; + + default: + /* @COMPAT When we can break behavioral backward compatibility, + * return pcmk_rc_unpack_error + */ + pcmk__config_warn("Rule %s has invalid " PCMK_XA_BOOLEAN_OP + " value '%s', using default '" PCMK_VALUE_AND "'", + pcmk__xe_id(rule), value); + combine = pcmk__combine_and; + break; + } + + // Evaluate each condition + for (xmlNode *condition = pcmk__xe_first_child(rule, NULL, NULL, NULL); + condition != NULL; condition = pcmk__xe_next(condition)) { + + empty = false; + if (pcmk__evaluate_condition(condition, rule_input, + next_change) == pcmk_rc_ok) { + if (combine == pcmk__combine_or) { + rc = pcmk_rc_ok; // Any pass is final for "or" + break; + } + } else if (combine == pcmk__combine_and) { + rc = pcmk_rc_op_unsatisfied; // Any failure is final for "and" + break; + } + } + + if (empty) { // Not possible with schema validation enabled + /* @COMPAT Currently, we don't actually ignore "or" rules because + * rc is initialized to failure above in that case. When we can break + * backward compatibility, reset rc to pcmk_rc_ok here. + */ + pcmk__config_warn("Ignoring rule %s because it contains no conditions", + id); + } + + crm_trace("Rule %s is %ssatisfied", id, ((rc == pcmk_rc_ok)? "" : "not ")); + return rc; +} diff --git a/lib/common/tests/rules/Makefile.am b/lib/common/tests/rules/Makefile.am index 0062706d88..41630371da 100644 --- a/lib/common/tests/rules/Makefile.am +++ b/lib/common/tests/rules/Makefile.am @@ -1,26 +1,29 @@ # # 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__evaluate_condition_test \ pcmk__evaluate_op_expression_test \ pcmk__evaluate_rsc_expression_test \ + pcmk__parse_combine_test \ pcmk__parse_comparison_test \ pcmk__parse_source_test \ pcmk__parse_type_test \ pcmk__replace_submatches_test \ - pcmk__unpack_duration_test + pcmk__unpack_duration_test \ + pcmk_evaluate_rule_test TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/rules/pcmk__evaluate_condition_test.c b/lib/common/tests/rules/pcmk__evaluate_condition_test.c new file mode 100644 index 0000000000..025908b0c0 --- /dev/null +++ b/lib/common/tests/rules/pcmk__evaluate_condition_test.c @@ -0,0 +1,197 @@ +/* + * 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 + +/* + * Shared data + */ + +static pcmk_rule_input_t rule_input = { + .rsc_standard = PCMK_RESOURCE_CLASS_OCF, + .rsc_provider = "heartbeat", + .rsc_agent = "IPaddr2", + .op_name = PCMK_ACTION_MONITOR, + .op_interval_ms = 10000, +}; + + +/* + * Test invalid arguments + */ + +#define EXPR_ATTRIBUTE \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_ATTRIBUTE "='foo' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_VALUE "='bar' />" + +static void +null_invalid(void **state) +{ + xmlNode *xml = NULL; + crm_time_t *next_change = crm_time_new_undefined(); + + assert_int_equal(pcmk__evaluate_condition(NULL, NULL, next_change), EINVAL); + + xml = pcmk__xml_parse(EXPR_ATTRIBUTE); + assert_int_equal(pcmk__evaluate_condition(xml, NULL, next_change), EINVAL); + free_xml(xml); + + assert_int_equal(pcmk__evaluate_condition(NULL, &rule_input, next_change), + EINVAL); + + crm_time_free(next_change); +} + + +#define EXPR_INVALID "" + +static void +invalid_expression(void **state) +{ + xmlNode *xml = pcmk__xml_parse(EXPR_INVALID); + crm_time_t *next_change = crm_time_new_undefined(); + + assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, next_change), + pcmk_rc_unpack_error); + + crm_time_free(next_change); + free_xml(xml); +} + + +/* Each expression type function already has unit tests, so we just need to test + * that they are called correctly (essentially, one of each one's own tests). + */ + +static void +attribute_expression(void **state) +{ + xmlNode *xml = pcmk__xml_parse(EXPR_ATTRIBUTE); + + rule_input.node_attrs = pcmk__strkey_table(free, free); + pcmk__insert_dup(rule_input.node_attrs, "foo", "bar"); + + assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, NULL), + pcmk_rc_ok); + + g_hash_table_destroy(rule_input.node_attrs); + rule_input.node_attrs = NULL; + free_xml(xml); +} + +#define EXPR_LOCATION \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_ATTRIBUTE "='" CRM_ATTR_UNAME "' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_VALUE "='node1' />" + +static void +location_expression(void **state) +{ + xmlNode *xml = pcmk__xml_parse(EXPR_LOCATION); + + rule_input.node_attrs = pcmk__strkey_table(free, free); + pcmk__insert_dup(rule_input.node_attrs, CRM_ATTR_UNAME, "node1"); + + assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, NULL), + pcmk_rc_ok); + + g_hash_table_destroy(rule_input.node_attrs); + rule_input.node_attrs = NULL; + free_xml(xml); +} + +#define EXPR_DATE \ + "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \ + PCMK_XA_START "='2024-02-01 12:00:00' " \ + PCMK_XA_END "='2024-02-01 15:00:00' />" + +static void +date_expression(void **state) +{ + xmlNode *xml = pcmk__xml_parse(EXPR_DATE); + crm_time_t *now = crm_time_new("2024-02-01 11:59:59"); + crm_time_t *next_change = crm_time_new("2024-02-01 14:00:00"); + crm_time_t *reference = crm_time_new("2024-02-01 12:00:00"); + + rule_input.now = now; + assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, next_change), + pcmk_rc_before_range); + assert_int_equal(crm_time_compare(next_change, reference), 0); + rule_input.now = NULL; + + crm_time_free(reference); + crm_time_free(next_change); + crm_time_free(now); +} + +#define EXPR_RESOURCE \ + "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \ + PCMK_XA_TYPE "='IPaddr2' />" + +static void +resource_expression(void **state) +{ + xmlNode *xml = pcmk__xml_parse(EXPR_RESOURCE); + + assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, NULL), + pcmk_rc_ok); + free_xml(xml); +} + +#define EXPR_OP \ + "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ + PCMK_XA_INTERVAL "='10s' />" + +static void +op_expression(void **state) +{ + xmlNode *xml = pcmk__xml_parse(EXPR_OP); + + assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, NULL), + pcmk_rc_ok); + free_xml(xml); +} + +#define EXPR_SUBRULE \ + "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \ + " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ + PCMK_XA_INTERVAL "='10s' /> />" + +static void +subrule(void **state) +{ + xmlNode *xml = pcmk__xml_parse(EXPR_SUBRULE); + assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, NULL), + pcmk_rc_ok); + free_xml(xml); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(null_invalid), + cmocka_unit_test(invalid_expression), + cmocka_unit_test(attribute_expression), + cmocka_unit_test(location_expression), + cmocka_unit_test(date_expression), + cmocka_unit_test(resource_expression), + cmocka_unit_test(op_expression), + cmocka_unit_test(subrule)) diff --git a/lib/common/tests/rules/pcmk__evaluate_rsc_expression_test.c b/lib/common/tests/rules/pcmk__evaluate_rsc_expression_test.c index a95c1ef80f..71b9fe4ae5 100644 --- a/lib/common/tests/rules/pcmk__evaluate_rsc_expression_test.c +++ b/lib/common/tests/rules/pcmk__evaluate_rsc_expression_test.c @@ -1,226 +1,227 @@ /* * 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 */ static pcmk_rule_input_t rule_input = { // These are the only members used to evaluate resource expressions .rsc_standard = PCMK_RESOURCE_CLASS_OCF, .rsc_provider = "heartbeat", .rsc_agent = "IPaddr2", }; /*! * \internal * \brief Run one test, comparing return value * * \param[in] xml_string Resource expression XML as string * \param[in] reference_rc Assert that evaluation result equals this */ static void assert_rsc_expression(const char *xml_string, int reference_rc) { xmlNode *xml = pcmk__xml_parse(xml_string); assert_int_equal(pcmk__evaluate_rsc_expression(xml, &rule_input), reference_rc); free_xml(xml); } /* * Invalid arguments */ #define EXPR_ALL_MATCH \ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \ PCMK_XA_PROVIDER "='heartbeat' " \ PCMK_XA_TYPE "='IPaddr2' />" static void null_invalid(void **state) { xmlNode *xml = NULL; assert_int_equal(pcmk__evaluate_rsc_expression(NULL, NULL), EINVAL); xml = pcmk__xml_parse(EXPR_ALL_MATCH); assert_int_equal(pcmk__evaluate_rsc_expression(xml, NULL), EINVAL); free_xml(xml); assert_rsc_expression(NULL, EINVAL); } /* * Test PCMK_XA_ID */ #define EXPR_ID_MISSING \ "<" PCMK_XE_RSC_EXPRESSION " " \ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \ PCMK_XA_PROVIDER "='heartbeat' " \ PCMK_XA_TYPE "='IPaddr2' />" #define EXPR_ID_EMPTY \ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='' " \ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \ PCMK_XA_PROVIDER "='heartbeat' " \ PCMK_XA_TYPE "='IPaddr2' />" static void id_missing(void **state) { // Currently acceptable assert_rsc_expression(EXPR_ID_MISSING, pcmk_rc_ok); assert_rsc_expression(EXPR_ID_EMPTY, pcmk_rc_ok); } /* * Test standard, provider, and agent */ #define EXPR_FAIL_STANDARD \ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_LSB "' />" #define EXPR_EMPTY_STANDARD \ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_CLASS "='' />" static void fail_standard(void **state) { assert_rsc_expression(EXPR_FAIL_STANDARD, pcmk_rc_op_unsatisfied); assert_rsc_expression(EXPR_EMPTY_STANDARD, pcmk_rc_op_unsatisfied); rule_input.rsc_standard = NULL; assert_rsc_expression(EXPR_FAIL_STANDARD, pcmk_rc_op_unsatisfied); assert_rsc_expression(EXPR_EMPTY_STANDARD, pcmk_rc_op_unsatisfied); rule_input.rsc_standard = PCMK_RESOURCE_CLASS_OCF; } #define EXPR_FAIL_PROVIDER \ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \ PCMK_XA_PROVIDER "='pacemaker' " \ PCMK_XA_TYPE "='IPaddr2' />" #define EXPR_EMPTY_PROVIDER \ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \ PCMK_XA_PROVIDER "='' " PCMK_XA_TYPE "='IPaddr2' />" static void fail_provider(void **state) { assert_rsc_expression(EXPR_FAIL_PROVIDER, pcmk_rc_op_unsatisfied); assert_rsc_expression(EXPR_EMPTY_PROVIDER, pcmk_rc_op_unsatisfied); rule_input.rsc_provider = NULL; assert_rsc_expression(EXPR_FAIL_PROVIDER, pcmk_rc_op_unsatisfied); assert_rsc_expression(EXPR_EMPTY_PROVIDER, pcmk_rc_op_unsatisfied); rule_input.rsc_provider = "heartbeat"; } #define EXPR_FAIL_AGENT \ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \ PCMK_XA_PROVIDER "='heartbeat' " \ PCMK_XA_TYPE "='IPaddr3' />" #define EXPR_EMPTY_AGENT \ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \ PCMK_XA_PROVIDER "='heartbeat' " PCMK_XA_TYPE "='' />" static void fail_agent(void **state) { assert_rsc_expression(EXPR_FAIL_AGENT, pcmk_rc_op_unsatisfied); assert_rsc_expression(EXPR_EMPTY_AGENT, pcmk_rc_op_unsatisfied); rule_input.rsc_agent = NULL; assert_rsc_expression(EXPR_FAIL_AGENT, pcmk_rc_op_unsatisfied); assert_rsc_expression(EXPR_EMPTY_AGENT, pcmk_rc_op_unsatisfied); rule_input.rsc_agent = "IPaddr2"; } #define EXPR_NO_STANDARD_MATCHES \ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_PROVIDER "='heartbeat' " \ PCMK_XA_TYPE "='IPaddr2' />" static void no_standard_matches(void **state) { assert_rsc_expression(EXPR_NO_STANDARD_MATCHES, pcmk_rc_ok); } #define EXPR_NO_PROVIDER_MATCHES \ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \ PCMK_XA_TYPE "='IPaddr2' />" static void no_provider_matches(void **state) { assert_rsc_expression(EXPR_NO_PROVIDER_MATCHES, pcmk_rc_ok); } #define EXPR_NO_AGENT_MATCHES \ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \ PCMK_XA_PROVIDER "='heartbeat' />" static void no_agent_matches(void **state) { assert_rsc_expression(EXPR_NO_AGENT_MATCHES, pcmk_rc_ok); } #define EXPR_NO_CRITERIA_MATCHES \ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' />" static void no_criteria_matches(void **state) { assert_rsc_expression(EXPR_NO_CRITERIA_MATCHES, pcmk_rc_ok); } static void all_match(void **state) { assert_rsc_expression(EXPR_ALL_MATCH, pcmk_rc_ok); } PCMK__UNIT_TEST(NULL, NULL, cmocka_unit_test(null_invalid), cmocka_unit_test(id_missing), cmocka_unit_test(fail_standard), cmocka_unit_test(fail_provider), cmocka_unit_test(fail_agent), cmocka_unit_test(no_standard_matches), cmocka_unit_test(no_provider_matches), cmocka_unit_test(no_agent_matches), cmocka_unit_test(no_criteria_matches), cmocka_unit_test(all_match)) diff --git a/lib/common/tests/rules/pcmk__parse_combine_test.c b/lib/common/tests/rules/pcmk__parse_combine_test.c new file mode 100644 index 0000000000..afebcf8f3f --- /dev/null +++ b/lib/common/tests/rules/pcmk__parse_combine_test.c @@ -0,0 +1,52 @@ +/* + * 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 + +static void +default_and(void **state) +{ + assert_int_equal(pcmk__parse_combine(NULL), pcmk__combine_and); +} + +static void +invalid(void **state) +{ + assert_int_equal(pcmk__parse_combine(""), pcmk__combine_unknown); + assert_int_equal(pcmk__parse_combine(" "), pcmk__combine_unknown); + assert_int_equal(pcmk__parse_combine("but"), pcmk__combine_unknown); +} + +static void +valid(void **state) +{ + assert_int_equal(pcmk__parse_combine(PCMK_VALUE_AND), pcmk__combine_and); + assert_int_equal(pcmk__parse_combine(PCMK_VALUE_OR), pcmk__combine_or); +} + +static void +case_insensitive(void **state) +{ + assert_int_equal(pcmk__parse_combine("And"), + pcmk__combine_and); + + assert_int_equal(pcmk__parse_combine("OR"), + pcmk__combine_or); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(default_and), + cmocka_unit_test(invalid), + cmocka_unit_test(valid), + cmocka_unit_test(case_insensitive)) diff --git a/lib/common/tests/rules/pcmk_evaluate_rule_test.c b/lib/common/tests/rules/pcmk_evaluate_rule_test.c new file mode 100644 index 0000000000..ac2cf8b522 --- /dev/null +++ b/lib/common/tests/rules/pcmk_evaluate_rule_test.c @@ -0,0 +1,379 @@ +/* + * 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 + +/* + * Shared data + */ + +static pcmk_rule_input_t rule_input = { + .rsc_standard = PCMK_RESOURCE_CLASS_OCF, + .rsc_provider = "heartbeat", + .rsc_agent = "IPaddr2", + .op_name = PCMK_ACTION_MONITOR, + .op_interval_ms = 10000, +}; + + +/* + * Test invalid arguments + */ + +#define RULE_OP \ + "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' > " \ + " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ + PCMK_XA_INTERVAL "='10s' />" \ + "" + +static void +null_invalid(void **state) +{ + xmlNode *xml = NULL; + crm_time_t *next_change = crm_time_new_undefined(); + + assert_int_equal(pcmk_evaluate_rule(NULL, NULL, next_change), + EINVAL); + + xml = pcmk__xml_parse(RULE_OP); + assert_int_equal(pcmk_evaluate_rule(xml, NULL, next_change), EINVAL); + free_xml(xml); + + assert_int_equal(pcmk_evaluate_rule(NULL, &rule_input, next_change), + EINVAL); + + crm_time_free(next_change); +} + +#define RULE_OP_MISSING_ID \ + "<" PCMK_XE_RULE "> " \ + " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ + PCMK_XA_INTERVAL "='10s' />" \ + "" + +static void +id_missing(void **state) +{ + // Currently acceptable + xmlNode *xml = pcmk__xml_parse(RULE_OP_MISSING_ID); + crm_time_t *next_change = crm_time_new_undefined(); + + assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, next_change), + pcmk_rc_ok); + + crm_time_free(next_change); + free_xml(xml); +} + +#define RULE_IDREF_PARENT "<" PCMK_XE_CIB ">" RULE_OP "" + +static void +good_idref(void **state) +{ + xmlNode *parent_xml = pcmk__xml_parse(RULE_IDREF_PARENT); + xmlNode *rule_xml = pcmk__xe_create(parent_xml, PCMK_XE_RULE); + crm_time_t *next_change = crm_time_new_undefined(); + + crm_xml_add(rule_xml, PCMK_XA_ID_REF, "r"); + assert_int_equal(pcmk_evaluate_rule(rule_xml, &rule_input, next_change), + pcmk_rc_ok); + + crm_time_free(next_change); + free_xml(parent_xml); +} + +static void +bad_idref(void **state) +{ + xmlNode *parent_xml = pcmk__xml_parse(RULE_IDREF_PARENT); + xmlNode *rule_xml = pcmk__xe_create(parent_xml, PCMK_XE_RULE); + crm_time_t *next_change = crm_time_new_undefined(); + + crm_xml_add(rule_xml, PCMK_XA_ID_REF, "x"); + assert_int_equal(pcmk_evaluate_rule(rule_xml, &rule_input, next_change), + pcmk_rc_unpack_error); + + crm_time_free(next_change); + free_xml(parent_xml); +} + +#define RULE_EMPTY "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' />" + +static void +empty_default(void **state) +{ + // Currently acceptable + xmlNode *xml = pcmk__xml_parse(RULE_EMPTY); + + assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), + pcmk_rc_ok); + + free_xml(xml); +} + +#define RULE_EMPTY_AND \ + "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \ + PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' />" + +static void +empty_and(void **state) +{ + // Currently acceptable + xmlNode *xml = pcmk__xml_parse(RULE_EMPTY_AND); + + assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), + pcmk_rc_ok); + + free_xml(xml); +} + +#define RULE_EMPTY_OR \ + "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \ + PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' />" + +static void +empty_or(void **state) +{ + // Currently treated as unsatisfied + xmlNode *xml = pcmk__xml_parse(RULE_EMPTY_OR); + + assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), + pcmk_rc_op_unsatisfied); + + free_xml(xml); +} + +#define RULE_DEFAULT_BOOLEAN_OP \ + "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' >" \ + " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \ + PCMK_XA_TYPE "='Dummy' />" \ + " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \ + PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ + PCMK_XA_INTERVAL "='10s' />" \ + "" + +static void +default_boolean_op(void **state) +{ + // Defaults to PCMK_VALUE_AND + xmlNode *xml = pcmk__xml_parse(RULE_DEFAULT_BOOLEAN_OP); + + assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), + pcmk_rc_op_unsatisfied); + + free_xml(xml); +} + +#define RULE_INVALID_BOOLEAN_OP \ + "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \ + PCMK_XA_BOOLEAN_OP "='not-an-op' >" \ + " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \ + PCMK_XA_TYPE "='Dummy' />" \ + " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \ + PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ + PCMK_XA_INTERVAL "='10s' />" \ + "" + +static void +invalid_boolean_op(void **state) +{ + // Currently defaults to PCMK_VALUE_AND + xmlNode *xml = pcmk__xml_parse(RULE_INVALID_BOOLEAN_OP); + + assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), + pcmk_rc_op_unsatisfied); + + free_xml(xml); +} + +#define RULE_AND_PASSES \ + "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \ + PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' >" \ + " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \ + PCMK_XA_TYPE "='IPaddr2' />" \ + " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \ + PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ + PCMK_XA_INTERVAL "='10s' />" \ + "" + +static void +and_passes(void **state) +{ + xmlNode *xml = pcmk__xml_parse(RULE_AND_PASSES); + + assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok); + + free_xml(xml); +} + +#define RULE_LONELY_AND \ + "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \ + PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' >" \ + " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \ + PCMK_XA_TYPE "='IPaddr2' />" \ + "" + +static void +lonely_and_passes(void **state) +{ + xmlNode *xml = pcmk__xml_parse(RULE_LONELY_AND); + + assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok); + + free_xml(xml); +} + +#define RULE_AND_ONE_FAILS \ + "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \ + PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' >" \ + " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \ + PCMK_XA_TYPE "='Dummy' />" \ + " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \ + PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ + PCMK_XA_INTERVAL "='10s' />" \ + "" + +static void +and_one_fails(void **state) +{ + xmlNode *xml = pcmk__xml_parse(RULE_AND_ONE_FAILS); + + assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), + pcmk_rc_op_unsatisfied); + + free_xml(xml); +} + +#define RULE_AND_TWO_FAIL \ + "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \ + PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' >" \ + " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \ + PCMK_XA_TYPE "='Dummy' />" \ + " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \ + PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ + PCMK_XA_INTERVAL "='9s' />" \ + "" + +static void +and_two_fail(void **state) +{ + xmlNode *xml = pcmk__xml_parse(RULE_AND_TWO_FAIL); + + assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), + pcmk_rc_op_unsatisfied); + + free_xml(xml); +} + +#define RULE_OR_ONE_PASSES \ + "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \ + PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' >" \ + " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \ + PCMK_XA_TYPE "='Dummy' />" \ + " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \ + PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ + PCMK_XA_INTERVAL "='10s' />" \ + "" + +static void +or_one_passes(void **state) +{ + xmlNode *xml = pcmk__xml_parse(RULE_OR_ONE_PASSES); + + assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok); + + free_xml(xml); +} + +#define RULE_OR_TWO_PASS \ + "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \ + PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' >" \ + " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \ + PCMK_XA_TYPE "='IPAddr2' />" \ + " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \ + PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ + PCMK_XA_INTERVAL "='10s' />" \ + "" + +static void +or_two_pass(void **state) +{ + xmlNode *xml = pcmk__xml_parse(RULE_OR_TWO_PASS); + + assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok); + + free_xml(xml); +} + +#define RULE_LONELY_OR \ + "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \ + PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' >" \ + " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \ + PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ + PCMK_XA_INTERVAL "='10s' />" \ + "" + +static void +lonely_or_passes(void **state) +{ + xmlNode *xml = pcmk__xml_parse(RULE_LONELY_OR); + + assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok); + + free_xml(xml); +} + +#define RULE_OR_FAILS \ + "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \ + PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' >" \ + " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \ + PCMK_XA_TYPE "='Dummy' />" \ + " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \ + PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ + PCMK_XA_INTERVAL "='20s' />" \ + "" + +static void +or_fails(void **state) +{ + xmlNode *xml = pcmk__xml_parse(RULE_OR_FAILS); + + assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), + pcmk_rc_op_unsatisfied); + + free_xml(xml); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(null_invalid), + cmocka_unit_test(id_missing), + cmocka_unit_test(good_idref), + cmocka_unit_test(bad_idref), + cmocka_unit_test(empty_default), + cmocka_unit_test(empty_and), + cmocka_unit_test(empty_or), + cmocka_unit_test(default_boolean_op), + cmocka_unit_test(invalid_boolean_op), + cmocka_unit_test(and_passes), + cmocka_unit_test(lonely_and_passes), + cmocka_unit_test(and_one_fails), + cmocka_unit_test(and_two_fail), + cmocka_unit_test(or_one_passes), + cmocka_unit_test(or_two_pass), + cmocka_unit_test(lonely_or_passes), + cmocka_unit_test(or_fails)) diff --git a/lib/pacemaker/pcmk_rule.c b/lib/pacemaker/pcmk_rule.c index 0795d81e41..cda8774cc0 100644 --- a/lib/pacemaker/pcmk_rule.c +++ b/lib/pacemaker/pcmk_rule.c @@ -1,214 +1,214 @@ /* * Copyright 2022-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 "libpacemaker_private.h" #define XPATH_NODE_RULE "//" PCMK_XE_RULE "[@" PCMK_XA_ID "='%s']" /*! * \internal * \brief Check whether a given rule is in effect * * \param[in] scheduler Scheduler data * \param[in] rule_id The ID of the rule to check * \param[out] error Where to store a rule evaluation error message * * \return Standard Pacemaker return code */ static int eval_rule(pcmk_scheduler_t *scheduler, const char *rule_id, const char **error) { xmlNodePtr cib_constraints = NULL; xmlNodePtr match = NULL; xmlXPathObjectPtr xpath_obj = NULL; char *xpath = NULL; int rc = pcmk_rc_ok; int num_results = 0; *error = NULL; /* Rules are under the constraints node in the XML, so first find that. */ cib_constraints = pcmk_find_cib_element(scheduler->input, PCMK_XE_CONSTRAINTS); /* Get all rules matching the given ID that are also simple enough for us * to check. For the moment, these rules must only have a single * date_expression child and: * - Do not have a date_spec operation, or * - Have a date_spec operation that contains years= but does not contain * moon=. * * We do this in steps to provide better error messages. First, check that * there's any rule with the given ID. */ xpath = crm_strdup_printf(XPATH_NODE_RULE, rule_id); xpath_obj = xpath_search(cib_constraints, xpath); num_results = numXpathResults(xpath_obj); free(xpath); freeXpathObject(xpath_obj); if (num_results == 0) { *error = "Rule not found"; return ENXIO; } if (num_results > 1) { // Should not be possible; schema prevents this *error = "Found more than one rule with matching ID"; return pcmk_rc_duplicate_id; } /* Next, make sure it has exactly one date_expression. */ xpath = crm_strdup_printf(XPATH_NODE_RULE "//date_expression", rule_id); xpath_obj = xpath_search(cib_constraints, xpath); num_results = numXpathResults(xpath_obj); free(xpath); freeXpathObject(xpath_obj); if (num_results != 1) { if (num_results == 0) { *error = "Rule does not have a date expression"; } else { *error = "Rule has more than one date expression"; } return EOPNOTSUPP; } /* Then, check that it's something we actually support. */ xpath = crm_strdup_printf(XPATH_NODE_RULE "//" PCMK_XE_DATE_EXPRESSION "[@" PCMK_XA_OPERATION "!='" PCMK_VALUE_DATE_SPEC "']", rule_id); xpath_obj = xpath_search(cib_constraints, xpath); num_results = numXpathResults(xpath_obj); free(xpath); if (num_results == 0) { freeXpathObject(xpath_obj); xpath = crm_strdup_printf(XPATH_NODE_RULE "//" PCMK_XE_DATE_EXPRESSION "[@" PCMK_XA_OPERATION "='" PCMK_VALUE_DATE_SPEC "' " "and " PCMK_XE_DATE_SPEC "/@" PCMK_XA_YEARS " " "and not(" PCMK_XE_DATE_SPEC "/@" PCMK__XA_MOON ")]", rule_id); xpath_obj = xpath_search(cib_constraints, xpath); num_results = numXpathResults(xpath_obj); free(xpath); if (num_results == 0) { freeXpathObject(xpath_obj); *error = "Rule must either not use " PCMK_XE_DATE_SPEC ", or use " PCMK_XE_DATE_SPEC " with " PCMK_XA_YEARS "= but not " PCMK__XA_MOON "="; return EOPNOTSUPP; } } match = getXpathResult(xpath_obj, 0); /* We should have ensured this with the xpath query above, but double- * checking can't hurt. */ CRM_ASSERT(match != NULL); - CRM_ASSERT(pcmk__expression_type(match) == pcmk__subexpr_datetime); + CRM_ASSERT(pcmk__condition_type(match) == pcmk__condition_datetime); rc = pcmk__evaluate_date_expression(match, scheduler->now, NULL); if (rc == pcmk_rc_undetermined) { // Malformed or missing *error = "Error parsing rule"; } freeXpathObject(xpath_obj); return rc; } /*! * \internal * \brief Check whether each rule in a list is in effect * * \param[in,out] out Output object * \param[in] input The CIB XML to check (if \c NULL, use current CIB) * \param[in] date Check whether the rule is in effect at this date and * time (if \c NULL, use current date and time) * \param[in] rule_ids The IDs of the rules to check, as a NULL- * terminated list. * * \return Standard Pacemaker return code */ int pcmk__check_rules(pcmk__output_t *out, xmlNodePtr input, const crm_time_t *date, const char **rule_ids) { pcmk_scheduler_t *scheduler = NULL; int rc = pcmk_rc_ok; CRM_ASSERT(out != NULL); if (rule_ids == NULL) { // Trivial case; every rule specified is in effect return pcmk_rc_ok; } rc = pcmk__init_scheduler(out, input, date, &scheduler); if (rc != pcmk_rc_ok) { return rc; } for (const char **rule_id = rule_ids; *rule_id != NULL; rule_id++) { const char *error = NULL; int last_rc = eval_rule(scheduler, *rule_id, &error); out->message(out, "rule-check", *rule_id, last_rc, error); if (last_rc != pcmk_rc_ok) { rc = last_rc; } } pe_free_working_set(scheduler); return rc; } // Documented in pacemaker.h int pcmk_check_rules(xmlNodePtr *xml, xmlNodePtr input, const crm_time_t *date, const char **rule_ids) { pcmk__output_t *out = NULL; int rc = pcmk_rc_ok; rc = pcmk__xml_output_new(&out, xml); if (rc != pcmk_rc_ok) { return rc; } pcmk__register_lib_messages(out); rc = pcmk__check_rules(out, input, date, rule_ids); pcmk__xml_output_finish(out, pcmk_rc2exitc(rc), xml); return rc; } diff --git a/lib/pacemaker/pcmk_sched_location.c b/lib/pacemaker/pcmk_sched_location.c index f31126ee9a..00dce25532 100644 --- a/lib/pacemaker/pcmk_sched_location.c +++ b/lib/pacemaker/pcmk_sched_location.c @@ -1,735 +1,750 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include #include +#include #include #include #include #include "libpacemaker_private.h" static int get_node_score(const char *rule, const char *score, bool raw, pcmk_node_t *node, pcmk_resource_t *rsc) { int score_f = 0; if (score == NULL) { pcmk__config_warn("Rule %s: no score specified (assuming 0)", rule); } else if (raw) { score_f = char2score(score); } else { const char *target = NULL; const char *attr_score = NULL; target = g_hash_table_lookup(rsc->meta, PCMK_META_CONTAINER_ATTRIBUTE_TARGET); attr_score = pcmk__node_attr(node, score, target, pcmk__rsc_node_current); if (attr_score == NULL) { crm_debug("Rule %s: %s did not have a value for %s", rule, pcmk__node_name(node), score); score_f = -PCMK_SCORE_INFINITY; } else { crm_debug("Rule %s: %s had value %s for %s", rule, pcmk__node_name(node), attr_score, score); score_f = char2score(attr_score); } } return score_f; } /*! * \internal * \brief Parse a role configuration for a location constraint * * \param[in] role_spec Role specification * \param[out] role Where to store parsed role * * \return true if role specification is valid, otherwise false */ static bool parse_location_role(const char *role_spec, enum rsc_role_e *role) { if (role_spec == NULL) { *role = pcmk_role_unknown; return true; } *role = pcmk_parse_role(role_spec); switch (*role) { case pcmk_role_unknown: return false; case pcmk_role_started: case pcmk_role_unpromoted: /* Any promotable clone instance cannot be promoted without being in * the unpromoted role first. Therefore, any constraint for the * started or unpromoted role applies to every role. */ *role = pcmk_role_unknown; break; default: break; } return true; } /*! * \internal * \brief Generate a location constraint from a rule * * \param[in,out] rsc Resource that constraint is for * \param[in] rule_xml Rule XML (sub-element of location constraint) * \param[in] discovery Value of \c PCMK_XA_RESOURCE_DISCOVERY for * constraint * \param[out] next_change Where to set when rule evaluation will change * \param[in] re_match_data Regular expression submatches * * \return New location constraint if rule is valid, otherwise NULL */ static pcmk__location_t * generate_location_rule(pcmk_resource_t *rsc, xmlNode *rule_xml, const char *discovery, crm_time_t *next_change, pe_re_match_data_t *re_match_data) { const char *rule_id = NULL; const char *score = NULL; const char *boolean = NULL; const char *role_spec = NULL; GList *iter = NULL; GList *nodes = NULL; - bool do_and = true; - bool accept = true; bool raw_score = true; bool score_allocated = false; pcmk__location_t *location_rule = NULL; enum rsc_role_e role = pcmk_role_unknown; + enum pcmk__combine combine = pcmk__combine_unknown; rule_xml = expand_idref(rule_xml, rsc->cluster->input); if (rule_xml == NULL) { return NULL; // Error already logged } rule_id = crm_element_value(rule_xml, PCMK_XA_ID); boolean = crm_element_value(rule_xml, PCMK_XA_BOOLEAN_OP); role_spec = crm_element_value(rule_xml, PCMK_XA_ROLE); if (parse_location_role(role_spec, &role)) { crm_trace("Setting rule %s role filter to %s", rule_id, role_spec); } else { pcmk__config_err("Ignoring rule %s: Invalid " PCMK_XA_ROLE " '%s'", rule_id, role_spec); return NULL; } crm_trace("Processing location constraint rule %s", rule_id); score = crm_element_value(rule_xml, PCMK_XA_SCORE); if (score == NULL) { score = crm_element_value(rule_xml, PCMK_XA_SCORE_ATTRIBUTE); if (score != NULL) { raw_score = false; } } - if (pcmk__str_eq(boolean, PCMK_VALUE_OR, pcmk__str_casei)) { - do_and = false; + combine = pcmk__parse_combine(boolean); + switch (combine) { + case pcmk__combine_and: + case pcmk__combine_or: + break; - } else if (!pcmk__str_eq(boolean, PCMK_VALUE_AND, - pcmk__str_null_matches|pcmk__str_casei)) { - pcmk__config_warn("Location constraint rule %s has invalid " - PCMK_XA_BOOLEAN_OP " value '%s', using default " - "('" PCMK_VALUE_AND "')", - rule_id, boolean); + default: + /* @COMPAT When we can break behavioral backward compatibility, + * return NULL + */ + pcmk__config_warn("Location constraint rule %s has invalid " + PCMK_XA_BOOLEAN_OP " value '%s', using default " + "'" PCMK_VALUE_AND "'", + rule_id, boolean); + combine = pcmk__combine_and; + break; } location_rule = pcmk__new_location(rule_id, rsc, 0, discovery, NULL); if (location_rule == NULL) { return NULL; // Error already logged } location_rule->role_filter = role; if ((re_match_data != NULL) && (re_match_data->nregs > 0) && (re_match_data->pmatch[0].rm_so != -1) && !raw_score) { char *result = pcmk__replace_submatches(score, re_match_data->string, re_match_data->pmatch, re_match_data->nregs); if (result != NULL) { score = result; score_allocated = true; } } - if (do_and) { + if (combine == pcmk__combine_and) { nodes = pcmk__copy_node_list(rsc->cluster->nodes, true); for (iter = nodes; iter != NULL; iter = iter->next) { pcmk_node_t *node = iter->data; node->weight = get_node_score(rule_id, score, raw_score, node, rsc); } } for (iter = rsc->cluster->nodes; iter != NULL; iter = iter->next) { + int rc = pcmk_rc_ok; int score_f = 0; pcmk_node_t *node = iter->data; - pe_match_data_t match_data = { - .re = re_match_data, - .params = pe_rsc_params(rsc, node, rsc->cluster), - .meta = rsc->meta, + pcmk_rule_input_t rule_input = { + .now = rsc->cluster->now, + .node_attrs = node->details->attrs, + .rsc_params = pe_rsc_params(rsc, node, rsc->cluster), + .rsc_meta = rsc->meta, }; - accept = pe_test_rule(rule_xml, node->details->attrs, pcmk_role_unknown, - rsc->cluster->now, next_change, &match_data); + if (re_match_data != NULL) { + rule_input.rsc_id = re_match_data->string; + rule_input.rsc_id_submatches = re_match_data->pmatch; + rule_input.rsc_id_nmatches = re_match_data->nregs; + } + + rc = pcmk_evaluate_rule(rule_xml, &rule_input, next_change); crm_trace("Rule %s %s on %s", - pcmk__xe_id(rule_xml), (accept? "passed" : "failed"), + pcmk__xe_id(rule_xml), + ((rc == pcmk_rc_ok)? "passed" : "failed"), pcmk__node_name(node)); score_f = get_node_score(rule_id, score, raw_score, node, rsc); - if (accept) { + if (rc == pcmk_rc_ok) { pcmk_node_t *local = pe_find_node_id(nodes, node->details->id); - if ((local == NULL) && do_and) { + if ((local == NULL) && (combine == pcmk__combine_and)) { continue; } else if (local == NULL) { local = pe__copy_node(node); nodes = g_list_append(nodes, local); } - if (!do_and) { + if (combine == pcmk__combine_or) { local->weight = pcmk__add_scores(local->weight, score_f); } crm_trace("%s has score %s after %s", pcmk__node_name(node), pcmk_readable_score(local->weight), rule_id); - } else if (do_and && !accept) { + } else if (combine == pcmk__combine_and) { // Remove it pcmk_node_t *delete = pe_find_node_id(nodes, node->details->id); if (delete != NULL) { nodes = g_list_remove(nodes, delete); crm_trace("%s did not match", pcmk__node_name(node)); } free(delete); } } if (score_allocated) { free((char *)score); } location_rule->nodes = nodes; if (location_rule->nodes == NULL) { crm_trace("No matching nodes for location constraint rule %s", rule_id); return NULL; } else { crm_trace("Location constraint rule %s matched %d nodes", rule_id, g_list_length(location_rule->nodes)); } return location_rule; } static void unpack_rsc_location(xmlNode *xml_obj, pcmk_resource_t *rsc, const char *role_spec, const char *score, pe_re_match_data_t *re_match_data) { const char *rsc_id = crm_element_value(xml_obj, PCMK_XA_RSC); const char *id = crm_element_value(xml_obj, PCMK_XA_ID); const char *node = crm_element_value(xml_obj, PCMK_XE_NODE); const char *discovery = crm_element_value(xml_obj, PCMK_XA_RESOURCE_DISCOVERY); if (rsc == NULL) { pcmk__config_warn("Ignoring constraint '%s' because resource '%s' " "does not exist", id, rsc_id); return; } if (score == NULL) { score = crm_element_value(xml_obj, PCMK_XA_SCORE); } if ((node != NULL) && (score != NULL)) { int score_i = char2score(score); pcmk_node_t *match = pe_find_node(rsc->cluster->nodes, node); enum rsc_role_e role = pcmk_role_unknown; pcmk__location_t *location = NULL; if (!match) { return; } if (role_spec == NULL) { role_spec = crm_element_value(xml_obj, PCMK_XA_ROLE); } if (parse_location_role(role_spec, &role)) { crm_trace("Setting location constraint %s role filter: %s", id, role_spec); } else { /* @COMPAT The previous behavior of creating the constraint ignoring * the role is retained for now, but we should ignore the entire * constraint when we can break backward compatibility. */ pcmk__config_err("Ignoring role in constraint %s: " "Invalid value '%s'", id, role_spec); } location = pcmk__new_location(id, rsc, score_i, discovery, match); if (location == NULL) { return; // Error already logged } location->role_filter = role; } else { bool empty = true; crm_time_t *next_change = crm_time_new_undefined(); /* This loop is logically parallel to pe_evaluate_rules(), except * instead of checking whether any rule is active, we set up location * constraints for each active rule. */ for (xmlNode *rule_xml = pcmk__xe_first_child(xml_obj, PCMK_XE_RULE, NULL, NULL); rule_xml != NULL; rule_xml = pcmk__xe_next_same(rule_xml)) { empty = false; crm_trace("Unpacking %s/%s", id, pcmk__xe_id(rule_xml)); generate_location_rule(rsc, rule_xml, discovery, next_change, re_match_data); } if (empty) { pcmk__config_err("Ignoring constraint '%s' because it contains " "no rules", id); } /* If there is a point in the future when the evaluation of a rule will * change, make sure the scheduler is re-run by that time. */ if (crm_time_is_defined(next_change)) { time_t t = (time_t) crm_time_get_seconds_since_epoch(next_change); pe__update_recheck_time(t, rsc->cluster, "location rule evaluation"); } crm_time_free(next_change); } } static void unpack_simple_location(xmlNode *xml_obj, pcmk_scheduler_t *scheduler) { const char *id = crm_element_value(xml_obj, PCMK_XA_ID); const char *value = crm_element_value(xml_obj, PCMK_XA_RSC); if (value) { pcmk_resource_t *rsc; rsc = pcmk__find_constraint_resource(scheduler->resources, value); unpack_rsc_location(xml_obj, rsc, NULL, NULL, NULL); } value = crm_element_value(xml_obj, PCMK_XA_RSC_PATTERN); if (value) { regex_t *r_patt = pcmk__assert_alloc(1, sizeof(regex_t)); bool invert = false; if (value[0] == '!') { value++; invert = true; } if (regcomp(r_patt, value, REG_EXTENDED) != 0) { pcmk__config_err("Ignoring constraint '%s' because " PCMK_XA_RSC_PATTERN " has invalid value '%s'", id, value); free(r_patt); return; } for (GList *iter = scheduler->resources; iter != NULL; iter = iter->next) { pcmk_resource_t *r = iter->data; int nregs = 0; regmatch_t *pmatch = NULL; int status; if (r_patt->re_nsub > 0) { nregs = r_patt->re_nsub + 1; } else { nregs = 1; } pmatch = pcmk__assert_alloc(nregs, sizeof(regmatch_t)); status = regexec(r_patt, r->id, nregs, pmatch, 0); if (!invert && (status == 0)) { pe_re_match_data_t re_match_data = { .string = r->id, .nregs = nregs, .pmatch = pmatch }; crm_debug("'%s' matched '%s' for %s", r->id, value, id); unpack_rsc_location(xml_obj, r, NULL, NULL, &re_match_data); } else if (invert && (status != 0)) { crm_debug("'%s' is an inverted match of '%s' for %s", r->id, value, id); unpack_rsc_location(xml_obj, r, NULL, NULL, NULL); } else { crm_trace("'%s' does not match '%s' for %s", r->id, value, id); } free(pmatch); } regfree(r_patt); free(r_patt); } } // \return Standard Pacemaker return code static int unpack_location_tags(xmlNode *xml_obj, xmlNode **expanded_xml, pcmk_scheduler_t *scheduler) { const char *id = NULL; const char *rsc_id = NULL; const char *state = NULL; pcmk_resource_t *rsc = NULL; pcmk_tag_t *tag = NULL; xmlNode *rsc_set = NULL; *expanded_xml = NULL; CRM_CHECK(xml_obj != NULL, return EINVAL); id = pcmk__xe_id(xml_obj); if (id == NULL) { pcmk__config_err("Ignoring <%s> constraint without " PCMK_XA_ID, xml_obj->name); return pcmk_rc_unpack_error; } // Check whether there are any resource sets with template or tag references *expanded_xml = pcmk__expand_tags_in_sets(xml_obj, scheduler); if (*expanded_xml != NULL) { crm_log_xml_trace(*expanded_xml, "Expanded " PCMK_XE_RSC_LOCATION); return pcmk_rc_ok; } rsc_id = crm_element_value(xml_obj, PCMK_XA_RSC); if (rsc_id == NULL) { return pcmk_rc_ok; } if (!pcmk__valid_resource_or_tag(scheduler, rsc_id, &rsc, &tag)) { pcmk__config_err("Ignoring constraint '%s' because '%s' is not a " "valid resource or tag", id, rsc_id); return pcmk_rc_unpack_error; } else if (rsc != NULL) { // No template is referenced return pcmk_rc_ok; } state = crm_element_value(xml_obj, PCMK_XA_ROLE); *expanded_xml = pcmk__xml_copy(NULL, xml_obj); /* Convert any template or tag reference into constraint * PCMK_XE_RESOURCE_SET */ if (!pcmk__tag_to_set(*expanded_xml, &rsc_set, PCMK_XA_RSC, false, scheduler)) { free_xml(*expanded_xml); *expanded_xml = NULL; return pcmk_rc_unpack_error; } if (rsc_set != NULL) { if (state != NULL) { /* Move PCMK_XA_RSC_ROLE into converted PCMK_XE_RESOURCE_SET as * PCMK_XA_ROLE attribute */ crm_xml_add(rsc_set, PCMK_XA_ROLE, state); pcmk__xe_remove_attr(*expanded_xml, PCMK_XA_ROLE); } crm_log_xml_trace(*expanded_xml, "Expanded " PCMK_XE_RSC_LOCATION); } else { // No sets free_xml(*expanded_xml); *expanded_xml = NULL; } return pcmk_rc_ok; } // \return Standard Pacemaker return code static int unpack_location_set(xmlNode *location, xmlNode *set, pcmk_scheduler_t *scheduler) { xmlNode *xml_rsc = NULL; pcmk_resource_t *resource = NULL; const char *set_id; const char *role; const char *local_score; CRM_CHECK(set != NULL, return EINVAL); set_id = pcmk__xe_id(set); if (set_id == NULL) { pcmk__config_err("Ignoring " PCMK_XE_RESOURCE_SET " without " PCMK_XA_ID " in constraint '%s'", pcmk__s(pcmk__xe_id(location), "(missing ID)")); return pcmk_rc_unpack_error; } role = crm_element_value(set, PCMK_XA_ROLE); local_score = crm_element_value(set, PCMK_XA_SCORE); for (xml_rsc = pcmk__xe_first_child(set, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { resource = pcmk__find_constraint_resource(scheduler->resources, pcmk__xe_id(xml_rsc)); if (resource == NULL) { pcmk__config_err("%s: No resource found for %s", set_id, pcmk__xe_id(xml_rsc)); return pcmk_rc_unpack_error; } unpack_rsc_location(location, resource, role, local_score, NULL); } return pcmk_rc_ok; } void pcmk__unpack_location(xmlNode *xml_obj, pcmk_scheduler_t *scheduler) { xmlNode *set = NULL; bool any_sets = false; xmlNode *orig_xml = NULL; xmlNode *expanded_xml = NULL; if (unpack_location_tags(xml_obj, &expanded_xml, scheduler) != pcmk_rc_ok) { return; } if (expanded_xml) { orig_xml = xml_obj; xml_obj = expanded_xml; } for (set = pcmk__xe_first_child(xml_obj, PCMK_XE_RESOURCE_SET, NULL, NULL); set != NULL; set = pcmk__xe_next_same(set)) { any_sets = true; set = expand_idref(set, scheduler->input); if ((set == NULL) // Configuration error, message already logged || (unpack_location_set(xml_obj, set, scheduler) != pcmk_rc_ok)) { if (expanded_xml) { free_xml(expanded_xml); } return; } } if (expanded_xml) { free_xml(expanded_xml); xml_obj = orig_xml; } if (!any_sets) { unpack_simple_location(xml_obj, scheduler); } } /*! * \internal * \brief Add a new location constraint to scheduler data * * \param[in] id XML ID of location constraint * \param[in,out] rsc Resource in location constraint * \param[in] node_score Constraint score * \param[in] discover_mode Resource discovery option for constraint * \param[in] node Node in constraint (or NULL if rule-based) * * \return Newly allocated location constraint * \note The result will be added to the cluster (via \p rsc) and should not be * freed separately. */ pcmk__location_t * pcmk__new_location(const char *id, pcmk_resource_t *rsc, int node_score, const char *discover_mode, pcmk_node_t *node) { pcmk__location_t *new_con = NULL; if (id == NULL) { pcmk__config_err("Invalid constraint: no ID specified"); return NULL; } else if (rsc == NULL) { pcmk__config_err("Invalid constraint %s: no resource specified", id); return NULL; } else if (node == NULL) { CRM_CHECK(node_score == 0, return NULL); } new_con = calloc(1, sizeof(pcmk__location_t)); if (new_con != NULL) { new_con->id = strdup(id); new_con->rsc = rsc; new_con->nodes = NULL; new_con->role_filter = pcmk_role_unknown; if (pcmk__str_eq(discover_mode, PCMK_VALUE_ALWAYS, pcmk__str_null_matches|pcmk__str_casei)) { new_con->discover_mode = pcmk_probe_always; } else if (pcmk__str_eq(discover_mode, PCMK_VALUE_NEVER, pcmk__str_casei)) { new_con->discover_mode = pcmk_probe_never; } else if (pcmk__str_eq(discover_mode, PCMK_VALUE_EXCLUSIVE, pcmk__str_casei)) { new_con->discover_mode = pcmk_probe_exclusive; rsc->exclusive_discover = TRUE; } else { pcmk__config_err("Invalid " PCMK_XA_RESOURCE_DISCOVERY " value %s " "in location constraint", discover_mode); } if (node != NULL) { pcmk_node_t *copy = pe__copy_node(node); copy->weight = node_score; new_con->nodes = g_list_prepend(NULL, copy); } rsc->cluster->placement_constraints = g_list_prepend( rsc->cluster->placement_constraints, new_con); rsc->rsc_location = g_list_prepend(rsc->rsc_location, new_con); } return new_con; } /*! * \internal * \brief Apply all location constraints * * \param[in,out] scheduler Scheduler data */ void pcmk__apply_locations(pcmk_scheduler_t *scheduler) { for (GList *iter = scheduler->placement_constraints; iter != NULL; iter = iter->next) { pcmk__location_t *location = iter->data; location->rsc->cmds->apply_location(location->rsc, location); } } /*! * \internal * \brief Apply a location constraint to a resource's allowed node scores * * \param[in,out] rsc Resource to apply constraint to * \param[in,out] location Location constraint to apply * * \note This does not consider the resource's children, so the resource's * apply_location() method should be used instead in most cases. */ void pcmk__apply_location(pcmk_resource_t *rsc, pcmk__location_t *location) { bool need_role = false; CRM_ASSERT((rsc != NULL) && (location != NULL)); // If a role was specified, ensure constraint is applicable need_role = (location->role_filter > pcmk_role_unknown); if (need_role && (location->role_filter != rsc->next_role)) { pcmk__rsc_trace(rsc, "Not applying %s to %s because role will be %s not %s", location->id, rsc->id, pcmk_role_text(rsc->next_role), pcmk_role_text(location->role_filter)); return; } if (location->nodes == NULL) { pcmk__rsc_trace(rsc, "Not applying %s to %s because no nodes match", location->id, rsc->id); return; } pcmk__rsc_trace(rsc, "Applying %s%s%s to %s", location->id, (need_role? " for role " : ""), (need_role? pcmk_role_text(location->role_filter) : ""), rsc->id); for (GList *iter = location->nodes; iter != NULL; iter = iter->next) { pcmk_node_t *node = iter->data; pcmk_node_t *allowed_node = g_hash_table_lookup(rsc->allowed_nodes, node->details->id); if (allowed_node == NULL) { pcmk__rsc_trace(rsc, "* = %d on %s", node->weight, pcmk__node_name(node)); allowed_node = pe__copy_node(node); g_hash_table_insert(rsc->allowed_nodes, (gpointer) allowed_node->details->id, allowed_node); } else { pcmk__rsc_trace(rsc, "* + %d on %s", node->weight, pcmk__node_name(node)); allowed_node->weight = pcmk__add_scores(allowed_node->weight, node->weight); } if (allowed_node->rsc_discover_mode < location->discover_mode) { if (location->discover_mode == pcmk_probe_exclusive) { rsc->exclusive_discover = TRUE; } /* exclusive > never > always... always is default */ allowed_node->rsc_discover_mode = location->discover_mode; } } } diff --git a/lib/pengine/rules.c b/lib/pengine/rules.c index 31c5a0912d..64f9bdba0c 100644 --- a/lib/pengine/rules.c +++ b/lib/pengine/rules.c @@ -1,671 +1,524 @@ /* * 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, NULL, NULL, NULL); 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, NULL, NULL, NULL); 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 = pcmk__xe_first_child(ruleset, PCMK_XE_RULE, NULL, NULL); rule != NULL; rule = pcmk__xe_next_same(rule)) { + pcmk_rule_input_t rule_input = { NULL, }; + + map_rule_input(&rule_input, rule_data); ruleset_default = FALSE; - if (pe_eval_expr(rule, rule_data, next_change)) { + if (pcmk_evaluate_rule(rule, &rule_input, next_change) == pcmk_rc_ok) { /* 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, NULL, NULL, NULL); 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)); - } +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START - crm_trace("Rule %s %s", pcmk__xe_id(rule), passed ? "passed" : "failed"); - return passed; -} +#include -/*! - * \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) +pe_test_rule(xmlNode *rule, GHashTable *node_hash, enum rsc_role_e role, + crm_time_t *now, crm_time_t *next_change, + pe_match_data_t *match_data) { - gboolean 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; + pcmk_rule_input_t rule_input = { + .node_attrs = node_hash, + .now = now, + }; - 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); + if (match_data != NULL) { + rule_input.rsc_params = match_data->params; + rule_input.rsc_meta = match_data->meta; + if (match_data->re != NULL) { + rule_input.rsc_id = match_data->re->string; + rule_input.rsc_id_submatches = match_data->re->pmatch; + rule_input.rsc_id_nmatches = match_data->re->nregs; + } } - - crm_trace("Expression %s %s on %s", - pcmk__xe_id(expr), (accept? "passed" : "failed"), - pcmk__s(uname, "all nodes")); - return accept; -} - -/*! - * \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) -{ - pcmk_rule_input_t rule_input = { NULL, }; - - 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) -{ - pcmk_rule_input_t rule_input = { NULL, }; - - map_rule_input(&rule_input, rule_data); - return pcmk__evaluate_op_expression(expr, &rule_input) == pcmk_rc_ok; -} - -gboolean -pe__eval_rsc_expr(const xmlNode *expr, const pe_rule_eval_data_t *rule_data) -{ - pcmk_rule_input_t rule_input = { NULL, }; - - map_rule_input(&rule_input, rule_data); - return pcmk__evaluate_rsc_expression(expr, &rule_input) == pcmk_rc_ok; + return pcmk_evaluate_rule(rule, &rule_input, next_change) == pcmk_rc_ok; } -// 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 +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) +{ + pcmk_rule_input_t rule_input = { + .now = now, + .node_attrs = node_hash, + }; + + if (match_data != NULL) { + rule_input.rsc_params = match_data->params; + rule_input.rsc_meta = match_data->meta; + if (match_data->re != NULL) { + rule_input.rsc_id = match_data->re->string; + rule_input.rsc_id_submatches = match_data->re->pmatch; + rule_input.rsc_id_nmatches = match_data->re->nregs; + } + } + return pcmk__evaluate_condition(expr, &rule_input, + next_change) == pcmk_rc_ok; +} + 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); } +gboolean +pe_eval_expr(xmlNode *rule, const pe_rule_eval_data_t *rule_data, + crm_time_t *next_change) +{ + pcmk_rule_input_t rule_input = { NULL, }; + + map_rule_input(&rule_input, rule_data); + return pcmk_evaluate_rule(rule, &rule_input, next_change) == pcmk_rc_ok; +} + +gboolean +pe_eval_subexpr(xmlNode *expr, const pe_rule_eval_data_t *rule_data, + crm_time_t *next_change) +{ + pcmk_rule_input_t rule_input = { NULL, }; + + map_rule_input(&rule_input, rule_data); + return pcmk__evaluate_condition(expr, &rule_input, + next_change) == pcmk_rc_ok; +} + 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); + return pcmk__condition_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