diff --git a/include/crm/common/nvpair_internal.h b/include/crm/common/nvpair_internal.h index d73bbcf050..11d2f862f4 100644 --- a/include/crm/common/nvpair_internal.h +++ b/include/crm/common/nvpair_internal.h @@ -1,67 +1,70 @@ /* * 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_NVPAIR_INTERNAL__H #define PCMK__CRM_COMMON_NVPAIR_INTERNAL__H #include // gboolean #include // xmlNode #include // pcmk_rule_input_t #include // crm_time_t #include // pcmk__str_eq(), etc. #ifdef __cplusplus extern "C" { #endif // Data needed to sort XML blocks of name/value pairs typedef struct unpack_data_s { GHashTable *values; // Where to put name/value pairs const char *first_id; // Block with this XML ID should sort first pcmk_rule_input_t rule_input; // Data used to evaluate rules /* Whether each block's values should overwrite any existing ones * * @COMPAT Only external call paths set this to true. Drop it when we drop * pe_eval_nvpairs() and pe_unpack_nvpairs() after replacing with a new * public API that doesn't overwrite. */ bool overwrite; // If not NULL, this will be set to when rule evaluations will change next crm_time_t *next_change; } pcmk__nvpair_unpack_t; +gint pcmk__cmp_nvpair_blocks(gconstpointer a, gconstpointer b, + gpointer user_data); + /*! * \internal * \brief Insert a meta-attribute into a hash table * * \param[in] obj Resource (pcmk__resource_private_t) * or action (pcmk_action_t) to add to * \param[in] name Meta-attribute name * \param[in] value Value to add */ #define pcmk__insert_meta(obj, name, value) do { \ if (pcmk__str_eq((value), "#default", pcmk__str_casei)) { \ /* @COMPAT Deprecated since 2.1.8 */ \ pcmk__config_warn("Support for setting meta-attributes " \ "(such as %s) to the explicit value " \ "'#default' is deprecated and will be " \ "removed in a future release", (name)); \ } else if ((value) != NULL) { \ pcmk__insert_dup((obj)->meta, (name), (value)); \ } \ } while (0) #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_NVPAIR_INTERNAL__H diff --git a/lib/common/nvpair.c b/lib/common/nvpair.c index cfe2a93618..16b7285248 100644 --- a/lib/common/nvpair.c +++ b/lib/common/nvpair.c @@ -1,517 +1,610 @@ /* * 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 // UINT32_MAX #include // PRIu32 #include #include #include #include #include #include #include #include #include "crmcommon_private.h" /* * This file isolates handling of various kinds of name/value pairs: * * - pcmk_nvpair_t data type + * - name=value strings * - XML nvpair elements () - * - Meta-attributes (for resources and actions) + * - Instance attributes and meta-attributes (for resources and actions) */ // pcmk_nvpair_t handling /*! * \internal * \brief Allocate a new name/value pair * * \param[in] name New name (required) * \param[in] value New value * * \return Newly allocated name/value pair * \note The caller is responsible for freeing the result with * \c pcmk__free_nvpair(). */ static pcmk_nvpair_t * pcmk__new_nvpair(const char *name, const char *value) { pcmk_nvpair_t *nvpair = NULL; pcmk__assert(name); nvpair = pcmk__assert_alloc(1, sizeof(pcmk_nvpair_t)); nvpair->name = pcmk__str_copy(name); nvpair->value = pcmk__str_copy(value); return nvpair; } /*! * \internal * \brief Free a name/value pair * * \param[in,out] nvpair Name/value pair to free */ static void pcmk__free_nvpair(gpointer data) { if (data) { pcmk_nvpair_t *nvpair = data; free(nvpair->name); free(nvpair->value); free(nvpair); } } /*! * \brief Prepend a name/value pair to a list * * \param[in,out] nvpairs List to modify * \param[in] name New entry's name * \param[in] value New entry's value * * \return New head of list * \note The caller is responsible for freeing the list with * \c pcmk_free_nvpairs(). */ GSList * pcmk_prepend_nvpair(GSList *nvpairs, const char *name, const char *value) { return g_slist_prepend(nvpairs, pcmk__new_nvpair(name, value)); } /*! * \brief Free a list of name/value pairs * * \param[in,out] list List to free */ void pcmk_free_nvpairs(GSList *nvpairs) { g_slist_free_full(nvpairs, pcmk__free_nvpair); } -// convenience function for name=value strings + +// name=value string handling /*! * \internal * \brief Extract the name and value from an input string formatted as "name=value". * If unable to extract them, they are returned as NULL. * * \param[in] input The input string, likely from the command line * \param[out] name Everything before the first '=' in the input string * \param[out] value Everything after the first '=' in the input string * * \return 2 if both name and value could be extracted, 1 if only one could, and * and error code otherwise */ int pcmk__scan_nvpair(const char *input, char **name, char **value) { #ifdef HAVE_SSCANF_M *name = NULL; *value = NULL; if (sscanf(input, "%m[^=]=%m[^\n]", name, value) <= 0) { return -pcmk_err_bad_nvpair; } #else char *sep = NULL; *name = NULL; *value = NULL; sep = strstr(optarg, "="); if (sep == NULL) { return -pcmk_err_bad_nvpair; } *name = strndup(input, sep-input); if (*name == NULL) { return -ENOMEM; } /* If the last char in optarg is =, the user gave no * value for the option. Leave it as NULL. */ if (*(sep+1) != '\0') { *value = strdup(sep+1); if (*value == NULL) { return -ENOMEM; } } #endif if (*name != NULL && *value != NULL) { return 2; } else if (*name != NULL || *value != NULL) { return 1; } else { return -pcmk_err_bad_nvpair; } } /*! * \internal * \brief Format a name/value pair. * * Units can optionally be provided for the value. Note that unlike most * formatting functions, this one returns the formatted string. It is * assumed that the most common use of this function will be to build up * a string to be output as part of other functions. * * \note The caller is responsible for freeing the return value after use. * * \param[in] name The name of the nvpair. * \param[in] value The value of the nvpair. * \param[in] units Optional units for the value, or NULL. * * \return Newly allocated string with name/value pair */ char * pcmk__format_nvpair(const char *name, const char *value, const char *units) { return crm_strdup_printf("%s=\"%s%s\"", name, value, units ? units : ""); } /*! * \brief Safely add hash table entry to XML as attribute or name-value pair * * Suitable for \c g_hash_table_foreach(), this function takes a hash table key * and value, with an XML node passed as user data, and adds an XML attribute * with the specified name and value if it does not already exist. If the key * name starts with a digit, then it's not a valid XML attribute name. In that * case, this will instead add a child * to the XML. * * \param[in] key Key of hash table entry * \param[in] value Value of hash table entry * \param[in,out] user_data XML node */ void hash2smartfield(gpointer key, gpointer value, gpointer user_data) { /* @TODO Generate PCMK__XE_PARAM nodes for all keys that aren't valid XML * attribute names (not just those that start with digits), or possibly for * all keys to simplify parsing. * * Consider either deprecating as public API or exposing PCMK__XE_PARAM. * PCMK__XE_PARAM is currently private because it doesn't appear in any * output that Pacemaker generates. */ const char *name = key; const char *s_value = value; xmlNode *xml_node = user_data; if (isdigit(name[0])) { xmlNode *tmp = pcmk__xe_create(xml_node, PCMK__XE_PARAM); crm_xml_add(tmp, PCMK_XA_NAME, name); crm_xml_add(tmp, PCMK_XA_VALUE, s_value); } else if (crm_element_value(xml_node, name) == NULL) { crm_xml_add(xml_node, name, s_value); crm_trace("dumped: %s=%s", name, s_value); } else { crm_trace("duplicate: %s=%s", name, s_value); } } /*! * \brief Set XML attribute based on hash table entry * * Suitable for \c g_hash_table_foreach(), this function takes a hash table key * and value, with an XML node passed as user data, and adds an XML attribute * with the specified name and value if it does not already exist. * * \param[in] key Key of hash table entry * \param[in] value Value of hash table entry * \param[in,out] user_data XML node */ void hash2field(gpointer key, gpointer value, gpointer user_data) { const char *name = key; const char *s_value = value; xmlNode *xml_node = user_data; if (crm_element_value(xml_node, name) == NULL) { crm_xml_add(xml_node, name, s_value); } else { crm_trace("duplicate: %s=%s", name, s_value); } } /*! * \brief Set XML attribute based on hash table entry, as meta-attribute name * * Suitable for \c g_hash_table_foreach(), this function takes a hash table key * and value, with an XML node passed as user data, and adds an XML attribute * with the meta-attribute version of the specified name and value if it does * not already exist and if the name does not appear to be cluster-internal. * * \param[in] key Key of hash table entry * \param[in] value Value of hash table entry * \param[in,out] user_data XML node */ void hash2metafield(gpointer key, gpointer value, gpointer user_data) { char *crm_name = NULL; if (key == NULL || value == NULL) { return; } /* Filter out cluster-generated attributes that contain a '#' or ':' * (like fail-count and last-failure). */ for (crm_name = key; *crm_name; ++crm_name) { if ((*crm_name == '#') || (*crm_name == ':')) { return; } } crm_name = crm_meta_name(key); hash2field(crm_name, value, user_data); free(crm_name); } // nvpair handling /*! * \brief Create an XML name/value pair * * \param[in,out] parent If not \c NULL, make new XML node a child of this one * \param[in] id Set this as XML ID (or NULL to auto-generate) * \param[in] name Name to use * \param[in] value Value to use * * \return New XML object on success, \c NULL otherwise */ xmlNode * crm_create_nvpair_xml(xmlNode *parent, const char *id, const char *name, const char *value) { xmlNode *nvp; /* id can be NULL so we auto-generate one, and name can be NULL if this * will be used to delete a name/value pair by ID, but both can't be NULL */ CRM_CHECK(id || name, return NULL); nvp = pcmk__xe_create(parent, PCMK_XE_NVPAIR); if (id) { crm_xml_add(nvp, PCMK_XA_ID, id); } else { pcmk__xe_set_id(nvp, "%s-%s", pcmk__s(pcmk__xe_id(parent), PCMK_XE_NVPAIR), name); } crm_xml_add(nvp, PCMK_XA_NAME, name); crm_xml_add(nvp, PCMK_XA_VALUE, value); return nvp; } /*! * \brief Retrieve XML attributes as a hash table * * Given an XML element, this will look for any \ element child, * creating a hash table of (newly allocated string) name/value pairs taken * first from the attributes element's NAME=VALUE XML attributes, and then * from any \ children of attributes. * * \param[in] XML node to parse * * \return Hash table with name/value pairs * \note It is the caller's responsibility to free the result using * \c g_hash_table_destroy(). */ GHashTable * xml2list(const xmlNode *parent) { xmlNode *child = NULL; xmlAttrPtr pIter = NULL; xmlNode *nvpair_list = NULL; GHashTable *nvpair_hash = pcmk__strkey_table(free, free); CRM_CHECK(parent != NULL, return nvpair_hash); nvpair_list = pcmk__xe_first_child(parent, PCMK__XE_ATTRIBUTES, NULL, NULL); if (nvpair_list == NULL) { crm_trace("No attributes in %s", parent->name); crm_log_xml_trace(parent, "No attributes for resource op"); } crm_log_xml_trace(nvpair_list, "Unpacking"); for (pIter = pcmk__xe_first_attr(nvpair_list); pIter != NULL; pIter = pIter->next) { const char *p_name = (const char *)pIter->name; const char *p_value = pcmk__xml_attr_value(pIter); crm_trace("Added %s=%s", p_name, p_value); pcmk__insert_dup(nvpair_hash, p_name, p_value); } for (child = pcmk__xe_first_child(nvpair_list, PCMK__XE_PARAM, NULL, NULL); child != NULL; child = pcmk__xe_next_same(child)) { const char *key = crm_element_value(child, PCMK_XA_NAME); const char *value = crm_element_value(child, PCMK_XA_VALUE); crm_trace("Added %s=%s", key, value); if (key != NULL && value != NULL) { pcmk__insert_dup(nvpair_hash, key, value); } } return nvpair_hash; } // Meta-attribute handling /*! * \brief Get the environment variable equivalent of a meta-attribute name * * \param[in] attr_name Name of meta-attribute * * \return Newly allocated string for \p attr_name with "CRM_meta_" prefix and * underbars instead of dashes * \note This asserts on an invalid argument or memory allocation error, so * callers can assume the result is non-NULL. The caller is responsible * for freeing the result using free(). */ char * crm_meta_name(const char *attr_name) { char *env_name = NULL; pcmk__assert(!pcmk__str_empty(attr_name)); env_name = crm_strdup_printf(CRM_META "_%s", attr_name); for (char *c = env_name; *c != '\0'; ++c) { if (*c == '-') { *c = '_'; } } return env_name; } /*! * \brief Get the value of a meta-attribute * * Get the value of a meta-attribute from a hash table whose keys are * meta-attribute environment variable names (as crm_meta_name() would * create, like pcmk__graph_action_t:params, not pcmk_resource_t:meta). * * \param[in] meta Hash table of meta-attributes * \param[in] attr_name Name of meta-attribute to get * * \return Value of given meta-attribute */ const char * crm_meta_value(GHashTable *meta, const char *attr_name) { if ((meta != NULL) && (attr_name != NULL)) { char *key = crm_meta_name(attr_name); const char *value = g_hash_table_lookup(meta, key); free(key); return value; } return NULL; } +/*! + * \internal + * \brief Compare processing order of two XML blocks of name/value pairs + * + * \param[in] a First XML block to compare + * \param[in] b Second XML block to compare + * \param[in] user_data pcmk__nvpair_unpack_t with first_id (whether a + * particular XML ID should have priority) and overwrite + * (whether later-processed blocks will overwrite values + * from earlier ones) set as desired + * + * \return Standard comparison return code (a negative value if \p a should sort + * first, a positive value if \p b should sort first, and 0 if they + * should sort equally) + * \note This is suitable for use as a GList sorting function. + */ +gint +pcmk__cmp_nvpair_blocks(gconstpointer a, gconstpointer b, gpointer user_data) +{ + const xmlNode *pair_a = a; + const xmlNode *pair_b = b; + const pcmk__nvpair_unpack_t *unpack_data = user_data; + + int score_a = 0; + int score_b = 0; + int rc = pcmk_rc_ok; + + /* If we're overwriting values, we want to process blocks from + * lowest priority to highest, so higher-priority values overwrite + * lower-priority ones. If we're not overwriting values, we want to process + * from highest priority to lowest. + */ + const gint a_is_higher = ((unpack_data != NULL) + && unpack_data->overwrite)? 1 : -1; + const gint b_is_higher = -a_is_higher; + + /* NULL values have lowest priority, regardless of the other's score + * (it won't be possible in practice anyway, this is just a failsafe) + */ + if (a == NULL) { + return (b == NULL)? 0 : b_is_higher; + + } else if (b == NULL) { + return a_is_higher; + } + + /* A particular XML ID can be specified as having highest priority + * regardless of score (schema validation, if enabled, prevents two blocks + * from having the same ID, so we can ignore handling that case + * specifically) + */ + if ((unpack_data != NULL) && (unpack_data->first_id != NULL)) { + if (pcmk__str_eq(pcmk__xe_id(pair_a), unpack_data->first_id, + pcmk__str_none)) { + return a_is_higher; + + } else if (pcmk__str_eq(pcmk__xe_id(pair_b), unpack_data->first_id, + pcmk__str_none)) { + return b_is_higher; + } + } + + // Otherwise, check the scores + + rc = pcmk__xe_get_score(pair_a, PCMK_XA_SCORE, &score_a, 0); + if (rc != pcmk_rc_ok) { // Not possible with schema validation enabled + pcmk__config_warn("Using 0 as %s score because '%s' " + "is not a valid score: %s", + pcmk__xe_id(pair_a), + crm_element_value(pair_a, PCMK_XA_SCORE), + pcmk_rc_str(rc)); + } + + rc = pcmk__xe_get_score(pair_b, PCMK_XA_SCORE, &score_b, 0); + if (rc != pcmk_rc_ok) { // Not possible with schema validation enabled + pcmk__config_warn("Using 0 as %s score because '%s' " + "is not a valid score: %s", + pcmk__xe_id(pair_b), + crm_element_value(pair_b, PCMK_XA_SCORE), + pcmk_rc_str(rc)); + } + + if (score_a < score_b) { + return b_is_higher; + + } else if (score_a > score_b) { + return a_is_higher; + } + return 0; +} + // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include static gint pcmk__compare_nvpair(gconstpointer a, gconstpointer b) { int rc = 0; const pcmk_nvpair_t *pair_a = a; const pcmk_nvpair_t *pair_b = b; pcmk__assert((pair_a != NULL) && (pair_a->name != NULL) && (pair_b != NULL) && (pair_b->name != NULL)); rc = strcmp(pair_a->name, pair_b->name); if (rc < 0) { return -1; } else if (rc > 0) { return 1; } return 0; } GSList * pcmk_sort_nvpairs(GSList *list) { return g_slist_sort(list, pcmk__compare_nvpair); } GSList * pcmk_xml_attrs2nvpairs(const xmlNode *xml) { GSList *result = NULL; for (xmlAttrPtr iter = pcmk__xe_first_attr(xml); iter != NULL; iter = iter->next) { result = pcmk_prepend_nvpair(result, (const char *) iter->name, (const char *) pcmk__xml_attr_value(iter)); } return result; } static void pcmk__nvpair_add_xml_attr(gpointer data, gpointer user_data) { pcmk_nvpair_t *pair = data; xmlNode *parent = user_data; crm_xml_add(parent, pair->name, pair->value); } void pcmk_nvpairs2xml_attrs(GSList *list, xmlNode *xml) { g_slist_foreach(list, pcmk__nvpair_add_xml_attr, xml); } void hash2nvpair(gpointer key, gpointer value, gpointer user_data) { const char *name = key; const char *s_value = value; xmlNode *xml_node = user_data; crm_create_nvpair_xml(xml_node, name, name, s_value); crm_trace("dumped: name=%s value=%s", name, s_value); } // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/common/tests/nvpair/Makefile.am b/lib/common/tests/nvpair/Makefile.am index 935460199d..cfd800a7e1 100644 --- a/lib/common/tests/nvpair/Makefile.am +++ b/lib/common/tests/nvpair/Makefile.am @@ -1,23 +1,24 @@ # # Copyright 2021-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/common.mk 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 = crm_meta_name_test \ crm_meta_value_test \ + pcmk__cmp_nvpair_blocks_test \ pcmk__xe_attr_is_true_test \ pcmk__xe_get_bool_attr_test \ pcmk__xe_get_datetime_test \ pcmk__xe_get_flags_test \ pcmk__xe_set_bool_attr_test TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/nvpair/pcmk__cmp_nvpair_blocks_test.c b/lib/common/tests/nvpair/pcmk__cmp_nvpair_blocks_test.c new file mode 100644 index 0000000000..2a2050551c --- /dev/null +++ b/lib/common/tests/nvpair/pcmk__cmp_nvpair_blocks_test.c @@ -0,0 +1,154 @@ +/* + * 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 + +#define FIRST_ID "foo" + +#define XML_FIRST_ID "" +#define XML_NO_ID "" +#define XML_LOW "" +#define XML_HIGH "" +#define XML_BAD "" + +static pcmk__nvpair_unpack_t unpack_data = { + .first_id = FIRST_ID, +}; + +static void +null_lowest(void **state) +{ + xmlNode *block = pcmk__xml_parse(XML_LOW); + + assert_non_null(block); + + assert_int_equal(pcmk__cmp_nvpair_blocks(NULL, block, NULL), 1); + assert_int_equal(pcmk__cmp_nvpair_blocks(block, NULL, NULL), -1); + assert_int_equal(pcmk__cmp_nvpair_blocks(NULL, NULL, NULL), 0); + + unpack_data.overwrite = false; + assert_int_equal(pcmk__cmp_nvpair_blocks(NULL, block, &unpack_data), 1); + assert_int_equal(pcmk__cmp_nvpair_blocks(block, NULL, &unpack_data), -1); + assert_int_equal(pcmk__cmp_nvpair_blocks(NULL, NULL, &unpack_data), 0); + + unpack_data.overwrite = true; + assert_int_equal(pcmk__cmp_nvpair_blocks(NULL, block, &unpack_data), -1); + assert_int_equal(pcmk__cmp_nvpair_blocks(block, NULL, &unpack_data), 1); + assert_int_equal(pcmk__cmp_nvpair_blocks(NULL, NULL, &unpack_data), 0); + + pcmk__xml_free(block); +} + +static void +special_id_highest(void **state) +{ + xmlNode *first_id = pcmk__xml_parse(XML_FIRST_ID); + xmlNode *not_first_id = pcmk__xml_parse(XML_HIGH); + xmlNode *no_id = pcmk__xml_parse(XML_NO_ID); + + assert_non_null(first_id); + assert_non_null(not_first_id); + assert_non_null(no_id); + + unpack_data.overwrite = false; + assert_int_equal(pcmk__cmp_nvpair_blocks(first_id, not_first_id, + &unpack_data), -1); + assert_int_equal(pcmk__cmp_nvpair_blocks(not_first_id, first_id, + &unpack_data), 1); + assert_int_equal(pcmk__cmp_nvpair_blocks(first_id, no_id, + &unpack_data), -1); + assert_int_equal(pcmk__cmp_nvpair_blocks(no_id, first_id, + &unpack_data), 1); + + unpack_data.overwrite = true; + assert_int_equal(pcmk__cmp_nvpair_blocks(first_id, not_first_id, + &unpack_data), 1); + assert_int_equal(pcmk__cmp_nvpair_blocks(not_first_id, first_id, + &unpack_data), -1); + assert_int_equal(pcmk__cmp_nvpair_blocks(first_id, no_id, + &unpack_data), 1); + assert_int_equal(pcmk__cmp_nvpair_blocks(no_id, first_id, + &unpack_data), -1); + + pcmk__xml_free(first_id); + pcmk__xml_free(not_first_id); + pcmk__xml_free(no_id); +} + +static void +null_special_id_ignored(void **state) +{ + xmlNode *no_id = pcmk__xml_parse(XML_NO_ID); + xmlNode *high = pcmk__xml_parse(XML_HIGH); + + assert_non_null(no_id); + assert_non_null(high); + + unpack_data.first_id = NULL; + + unpack_data.overwrite = false; + assert_int_equal(pcmk__cmp_nvpair_blocks(no_id, high, &unpack_data), 1); + + unpack_data.overwrite = true; + assert_int_equal(pcmk__cmp_nvpair_blocks(no_id, high, &unpack_data), -1); + + unpack_data.first_id = FIRST_ID; + + pcmk__xml_free(no_id); + pcmk__xml_free(high); +} + +static void +highest_score_wins(void **state) +{ + xmlNode *low = pcmk__xml_parse(XML_LOW); + xmlNode *low2 = pcmk__xml_parse(XML_LOW); + xmlNode *high = pcmk__xml_parse(XML_HIGH); + + assert_non_null(low); + assert_non_null(high); + + unpack_data.overwrite = false; + assert_int_equal(pcmk__cmp_nvpair_blocks(low, high, &unpack_data), 1); + assert_int_equal(pcmk__cmp_nvpair_blocks(low, low2, &unpack_data), 0); + + unpack_data.overwrite = true; + assert_int_equal(pcmk__cmp_nvpair_blocks(low, high, &unpack_data), -1); + assert_int_equal(pcmk__cmp_nvpair_blocks(low, low2, &unpack_data), 0); + + pcmk__xml_free(low); + pcmk__xml_free(high); +} + +static void +invalid_score_is_0(void **state) +{ + xmlNode *zero = pcmk__xml_parse(XML_FIRST_ID); + xmlNode *bad = pcmk__xml_parse(XML_BAD); + + assert_non_null(zero); + assert_non_null(bad); + + assert_int_equal(pcmk__cmp_nvpair_blocks(zero, bad, NULL), 0); + assert_int_equal(pcmk__cmp_nvpair_blocks(bad, zero, NULL), 0); + + pcmk__xml_free(zero); + pcmk__xml_free(bad); +} + +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, pcmk__xml_test_teardown_group, + cmocka_unit_test(null_lowest), + cmocka_unit_test(special_id_highest), + cmocka_unit_test(null_special_id_ignored), + cmocka_unit_test(highest_score_wins), + cmocka_unit_test(invalid_score_is_0)) diff --git a/lib/pengine/rules.c b/lib/pengine/rules.c index 1314978dd0..46c7bb75eb 100644 --- a/lib/pengine/rules.c +++ b/lib/pengine/rules.c @@ -1,315 +1,257 @@ /* * 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; } } -static gint -sort_pairs(gconstpointer a, gconstpointer b, gpointer user_data) -{ - const xmlNode *pair_a = a; - const xmlNode *pair_b = b; - pcmk__nvpair_unpack_t *unpack_data = user_data; - - int score_a = 0; - int score_b = 0; - int rc = pcmk_rc_ok; - - if (a == NULL && b == NULL) { - return 0; - } else if (a == NULL) { - return 1; - } else if (b == NULL) { - return -1; - } - - if (pcmk__str_eq(pcmk__xe_id(pair_a), unpack_data->first_id, - pcmk__str_none)) { - return -1; - - } else if (pcmk__str_eq(pcmk__xe_id(pair_b), unpack_data->first_id, - pcmk__str_none)) { - return 1; - } - - rc = pcmk__xe_get_score(pair_a, PCMK_XA_SCORE, &score_a, 0); - if (rc != pcmk_rc_ok) { // Not possible with schema validation enabled - pcmk__config_warn("Using 0 as %s score because '%s' " - "is not a valid score: %s", - pcmk__xe_id(pair_a), - crm_element_value(pair_a, PCMK_XA_SCORE), - pcmk_rc_str(rc)); - } - - rc = pcmk__xe_get_score(pair_b, PCMK_XA_SCORE, &score_b, 0); - if (rc != pcmk_rc_ok) { // Not possible with schema validation enabled - pcmk__config_warn("Using 0 as %s score because '%s' " - "is not a valid score: %s", - pcmk__xe_id(pair_b), - crm_element_value(pair_b, PCMK_XA_SCORE), - pcmk_rc_str(rc)); - } - - /* 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 (score_a < score_b) { - return unpack_data->overwrite? -1 : 1; - } else if (score_a > score_b) { - return unpack_data->overwrite? 1 : -1; - } - return 0; -} - static void populate_hash(xmlNode *nvpair_list, GHashTable *hash, bool overwrite) { if (pcmk__xe_is(nvpair_list->children, PCMK__XE_ATTRIBUTES)) { nvpair_list = nvpair_list->children; } for (xmlNode *nvpair = pcmk__xe_first_child(nvpair_list, PCMK_XE_NVPAIR, NULL, NULL); nvpair != NULL; nvpair = pcmk__xe_next_same(nvpair)) { xmlNode *ref_nvpair = pcmk__xe_resolve_idref(nvpair, NULL); const char *name = NULL; const char *value = NULL; const char *old_value = NULL; if (ref_nvpair == NULL) { /* Not possible with schema validation enabled (error already * logged) */ continue; } name = crm_element_value(ref_nvpair, PCMK_XA_NAME); 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)) { // @COMPAT Deprecated since 2.1.8 pcmk__config_warn("Support for setting meta-attributes (such as " "%s) to the explicit value '#default' is " "deprecated and will be removed in a future " "release", name); if (old_value != NULL) { crm_trace("Letting %s default (removing explicit value \"%s\")", name, value); g_hash_table_remove(hash, name); } } 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); } } } static void unpack_attr_set(gpointer data, gpointer user_data) { xmlNode *pair = data; pcmk__nvpair_unpack_t *unpack_data = user_data; xmlNode *rule_xml = pcmk__xe_first_child(pair, PCMK_XE_RULE, NULL, NULL); if ((rule_xml != NULL) && (pcmk_evaluate_rule(rule_xml, &(unpack_data->rule_input), unpack_data->next_change) != pcmk_rc_ok)) { return; } crm_trace("Adding name/value pairs from %s %s overwrite", pcmk__xe_id(pair), (unpack_data->overwrite? "with" : "without")); populate_hash(pair, unpack_data->values, unpack_data->overwrite); } /*! * \internal * \brief Create a sorted list of nvpair blocks * * \param[in] xml_obj XML element containing blocks of nvpair elements * \param[in] set_name If not NULL, only get blocks of this element * * \return List of XML blocks of name/value pairs */ static GList * make_pairs(const xmlNode *xml_obj, const char *set_name) { 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)) { xmlNode *expanded_attr_set = pcmk__xe_resolve_idref(attr_set, NULL); if (expanded_attr_set == NULL) { continue; // Not possible with schema validation enabled } unsorted = g_list_prepend(unsorted, expanded_attr_set); } } return unsorted; } /*! * \brief Extract nvpair blocks contained by an XML element into a hash table * * \param[in,out] top Ignored * \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 (all internal callers pass \c FALSE) * \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(xml_obj, set_name); if (pairs) { pcmk__nvpair_unpack_t data = { .values = hash, .first_id = always_first, .overwrite = overwrite, .next_change = next_change, }; map_rule_input(&(data.rule_input), rule_data); - pairs = g_list_sort_with_data(pairs, sort_pairs, &data); + pairs = g_list_sort_with_data(pairs, pcmk__cmp_nvpair_blocks, &data); g_list_foreach(pairs, unpack_attr_set, &data); g_list_free(pairs); } } /*! * \brief Extract nvpair blocks contained by an XML element into a hash table * * \param[in,out] top Ignored * \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 (all internal callers pass \c FALSE) * \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(NULL, xml_obj, set_name, &rule_data, hash, always_first, overwrite, next_change); } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include gboolean test_rule(xmlNode * rule, GHashTable * node_hash, enum rsc_role_e role, crm_time_t * now) { pcmk_rule_input_t rule_input = { .node_attrs = node_hash, .now = now, }; return pcmk_evaluate_rule(rule, &rule_input, NULL) == pcmk_rc_ok; } // LCOV_EXCL_STOP // End deprecated API