diff options
Diffstat (limited to 'lib/common')
143 files changed, 17299 insertions, 4761 deletions
diff --git a/lib/common/Makefile.am b/lib/common/Makefile.am index f9c43b9..bfa5c1d 100644 --- a/lib/common/Makefile.am +++ b/lib/common/Makefile.am @@ -1,5 +1,5 @@ # -# 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. # @@ -33,7 +33,7 @@ SUBDIRS = . tests noinst_HEADERS = crmcommon_private.h \ mock_private.h -libcrmcommon_la_LDFLAGS = -version-info 46:0:12 +libcrmcommon_la_LDFLAGS = -version-info 47:0:13 libcrmcommon_la_CFLAGS = $(CFLAGS_HARDENED_LIB) libcrmcommon_la_LDFLAGS += $(LDFLAGS_HARDENED_LIB) @@ -70,7 +70,7 @@ libcrmcommon_la_SOURCES += ipc_client.c libcrmcommon_la_SOURCES += ipc_common.c libcrmcommon_la_SOURCES += ipc_controld.c libcrmcommon_la_SOURCES += ipc_pacemakerd.c -libcrmcommon_la_SOURCES += ipc_schedulerd.c +libcrmcommon_la_SOURCES += ipc_schedulerd.c libcrmcommon_la_SOURCES += ipc_server.c libcrmcommon_la_SOURCES += iso8601.c libcrmcommon_la_SOURCES += lists.c @@ -80,6 +80,7 @@ libcrmcommon_la_SOURCES += messages.c libcrmcommon_la_SOURCES += nodes.c libcrmcommon_la_SOURCES += nvpair.c libcrmcommon_la_SOURCES += options.c +libcrmcommon_la_SOURCES += options_display.c libcrmcommon_la_SOURCES += output.c libcrmcommon_la_SOURCES += output_html.c libcrmcommon_la_SOURCES += output_log.c @@ -89,9 +90,13 @@ libcrmcommon_la_SOURCES += output_xml.c libcrmcommon_la_SOURCES += patchset.c libcrmcommon_la_SOURCES += patchset_display.c libcrmcommon_la_SOURCES += pid.c +libcrmcommon_la_SOURCES += probes.c libcrmcommon_la_SOURCES += procfs.c libcrmcommon_la_SOURCES += remote.c +libcrmcommon_la_SOURCES += resources.c libcrmcommon_la_SOURCES += results.c +libcrmcommon_la_SOURCES += roles.c +libcrmcommon_la_SOURCES += rules.c libcrmcommon_la_SOURCES += scheduler.c libcrmcommon_la_SOURCES += schemas.c libcrmcommon_la_SOURCES += scores.c @@ -101,6 +106,7 @@ libcrmcommon_la_SOURCES += watchdog.c libcrmcommon_la_SOURCES += xml.c libcrmcommon_la_SOURCES += xml_attr.c libcrmcommon_la_SOURCES += xml_display.c +libcrmcommon_la_SOURCES += xml_io.c libcrmcommon_la_SOURCES += xpath.c # @@ -112,6 +118,7 @@ include $(top_srcdir)/mk/tap.mk libcrmcommon_test_la_SOURCES = $(libcrmcommon_la_SOURCES) libcrmcommon_test_la_SOURCES += mock.c +libcrmcommon_test_la_SOURCES += unittest.c libcrmcommon_test_la_LDFLAGS = $(libcrmcommon_la_LDFLAGS) \ -rpath $(libdir) \ $(LDFLAGS_WRAP) @@ -126,8 +133,11 @@ libcrmcommon_test_la_CFLAGS = $(libcrmcommon_la_CFLAGS) \ -fno-inline # If -fno-builtin is used, -lm also needs to be added. See the comment at # BUILD_PROFILING above. -libcrmcommon_test_la_LIBADD = $(libcrmcommon_la_LIBADD) \ - -lcmocka \ - -lm +libcrmcommon_test_la_LIBADD = $(libcrmcommon_la_LIBADD) +if BUILD_COVERAGE +libcrmcommon_test_la_LIBADD += -lgcov +endif +libcrmcommon_test_la_LIBADD += -lcmocka +libcrmcommon_test_la_LIBADD += -lm nodist_libcrmcommon_test_la_SOURCES = $(nodist_libcrmcommon_la_SOURCES) diff --git a/lib/common/acl.c b/lib/common/acl.c index 1ebd765..b8914be 100644 --- a/lib/common/acl.c +++ b/lib/common/acl.c @@ -1,5 +1,5 @@ /* - * 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. * @@ -19,7 +19,6 @@ #include <libxml/tree.h> #include <crm/crm.h> -#include <crm/msg_xml.h> #include <crm/common/xml.h> #include <crm/common/xml_internal.h> #include "crmcommon_private.h" @@ -51,18 +50,18 @@ create_acl(const xmlNode *xml, GList *acls, enum xml_private_flags mode) { xml_acl_t *acl = NULL; - const char *tag = crm_element_value(xml, XML_ACL_ATTR_TAG); - const char *ref = crm_element_value(xml, XML_ACL_ATTR_REF); - const char *xpath = crm_element_value(xml, XML_ACL_ATTR_XPATH); - const char *attr = crm_element_value(xml, XML_ACL_ATTR_ATTRIBUTE); + const char *tag = crm_element_value(xml, PCMK_XA_OBJECT_TYPE); + const char *ref = crm_element_value(xml, PCMK_XA_REFERENCE); + const char *xpath = crm_element_value(xml, PCMK_XA_XPATH); + const char *attr = crm_element_value(xml, PCMK_XA_ATTRIBUTE); if (tag == NULL) { - // @COMPAT rolling upgrades <=1.1.11 - tag = crm_element_value(xml, XML_ACL_ATTR_TAGv1); + // @COMPAT Deprecated since 1.1.12 (needed for rolling upgrades) + tag = crm_element_value(xml, PCMK_XA_TAG); } if (ref == NULL) { - // @COMPAT rolling upgrades <=1.1.11 - ref = crm_element_value(xml, XML_ACL_ATTR_REFv1); + // @COMPAT Deprecated since 1.1.12 (needed for rolling upgrades) + ref = crm_element_value(xml, PCMK__XA_REF); } if ((tag == NULL) && (ref == NULL) && (xpath == NULL)) { @@ -72,8 +71,7 @@ create_acl(const xmlNode *xml, GList *acls, enum xml_private_flags mode) return NULL; } - acl = calloc(1, sizeof (xml_acl_t)); - CRM_ASSERT(acl != NULL); + acl = pcmk__assert_alloc(1, sizeof (xml_acl_t)); acl->mode = mode; if (xpath) { @@ -86,11 +84,11 @@ create_acl(const xmlNode *xml, GList *acls, enum xml_private_flags mode) if ((ref != NULL) && (attr != NULL)) { // NOTE: schema currently does not allow this - pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@" XML_ATTR_ID "='", + pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@" PCMK_XA_ID "='", ref, "' and @", attr, "]", NULL); } else if (ref != NULL) { - pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@" XML_ATTR_ID "='", + pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@" PCMK_XA_ID "='", ref, "']", NULL); } else if (attr != NULL) { @@ -127,12 +125,13 @@ parse_acl_entry(const xmlNode *acl_top, const xmlNode *acl_entry, GList *acls) { xmlNode *child = NULL; - for (child = pcmk__xe_first_child(acl_entry); child; - child = pcmk__xe_next(child)) { + for (child = pcmk__xe_first_child(acl_entry, NULL, NULL, NULL); + child != NULL; child = pcmk__xe_next(child)) { + const char *tag = (const char *) child->name; - const char *kind = crm_element_value(child, XML_ACL_ATTR_KIND); + const char *kind = crm_element_value(child, PCMK_XA_KIND); - if (pcmk__xe_is(child, XML_ACL_TAG_PERMISSION)) { + if (pcmk__xe_is(child, PCMK_XE_ACL_PERMISSION)) { CRM_ASSERT(kind != NULL); crm_trace("Unpacking ACL <%s> element of kind '%s'", tag, kind); tag = kind; @@ -140,18 +139,21 @@ parse_acl_entry(const xmlNode *acl_top, const xmlNode *acl_entry, GList *acls) crm_trace("Unpacking ACL <%s> element", tag); } - if (strcmp(XML_ACL_TAG_ROLE_REF, tag) == 0 - || strcmp(XML_ACL_TAG_ROLE_REFv1, tag) == 0) { - const char *ref_role = crm_element_value(child, XML_ATTR_ID); + /* @COMPAT PCMK__XE_ROLE_REF was deprecated in Pacemaker 1.1.12 (needed + * for rolling upgrades) + */ + if (pcmk__str_any_of(tag, PCMK_XE_ROLE, PCMK__XE_ROLE_REF, NULL)) { + const char *ref_role = crm_element_value(child, PCMK_XA_ID); if (ref_role) { xmlNode *role = NULL; - for (role = pcmk__xe_first_child(acl_top); role; - role = pcmk__xe_next(role)) { - if (!strcmp(XML_ACL_TAG_ROLE, (const char *) role->name)) { + for (role = pcmk__xe_first_child(acl_top, NULL, NULL, NULL); + role != NULL; role = pcmk__xe_next(role)) { + + if (!strcmp(PCMK_XE_ACL_ROLE, (const char *) role->name)) { const char *role_id = crm_element_value(role, - XML_ATTR_ID); + PCMK_XA_ID); if (role_id && strcmp(ref_role, role_id) == 0) { crm_trace("Unpacking referenced role '%s' in ACL <%s> element", @@ -163,13 +165,19 @@ parse_acl_entry(const xmlNode *acl_top, const xmlNode *acl_entry, GList *acls) } } - } else if (strcmp(XML_ACL_TAG_READ, tag) == 0) { + /* @COMPAT Use of a tag instead of a PCMK_XA_KIND attribute was + * deprecated in 1.1.12. We still need to look for tags named + * PCMK_VALUE_READ, etc., to support rolling upgrades. However, + * eventually we can clean this up and make the variables more intuitive + * (for example, don't assign a PCMK_XA_KIND value to the tag variable). + */ + } else if (strcmp(tag, PCMK_VALUE_READ) == 0) { acls = create_acl(child, acls, pcmk__xf_acl_read); - } else if (strcmp(XML_ACL_TAG_WRITE, tag) == 0) { + } else if (strcmp(tag, PCMK_VALUE_WRITE) == 0) { acls = create_acl(child, acls, pcmk__xf_acl_write); - } else if (strcmp(XML_ACL_TAG_DENY, tag) == 0) { + } else if (strcmp(tag, PCMK_VALUE_DENY) == 0) { acls = create_acl(child, acls, pcmk__xf_acl_deny); } else { @@ -292,34 +300,36 @@ pcmk__unpack_acl(xmlNode *source, xmlNode *target, const char *user) user); } else if (docpriv->acls == NULL) { - xmlNode *acls = get_xpath_object("//" XML_CIB_TAG_ACLS, - source, LOG_NEVER); + xmlNode *acls = get_xpath_object("//" PCMK_XE_ACLS, source, LOG_NEVER); pcmk__str_update(&docpriv->user, user); if (acls) { xmlNode *child = NULL; - for (child = pcmk__xe_first_child(acls); child; - child = pcmk__xe_next(child)) { + for (child = pcmk__xe_first_child(acls, NULL, NULL, NULL); + child != NULL; child = pcmk__xe_next(child)) { - if (pcmk__xe_is(child, XML_ACL_TAG_USER) - || pcmk__xe_is(child, XML_ACL_TAG_USERv1)) { - const char *id = crm_element_value(child, XML_ATTR_NAME); + /* @COMPAT PCMK__XE_ACL_USER was deprecated in Pacemaker 1.1.12 + * (needed for rolling upgrades) + */ + if (pcmk__xe_is(child, PCMK_XE_ACL_TARGET) + || pcmk__xe_is(child, PCMK__XE_ACL_USER)) { + const char *id = crm_element_value(child, PCMK_XA_NAME); if (id == NULL) { - id = crm_element_value(child, XML_ATTR_ID); + id = crm_element_value(child, PCMK_XA_ID); } if (id && strcmp(id, user) == 0) { crm_debug("Unpacking ACLs for user '%s'", id); docpriv->acls = parse_acl_entry(acls, child, docpriv->acls); } - } else if (pcmk__xe_is(child, XML_ACL_TAG_GROUP)) { - const char *id = crm_element_value(child, XML_ATTR_NAME); + } else if (pcmk__xe_is(child, PCMK_XE_ACL_GROUP)) { + const char *id = crm_element_value(child, PCMK_XA_NAME); if (id == NULL) { - id = crm_element_value(child, XML_ATTR_ID); + id = crm_element_value(child, PCMK_XA_ID); } if (id && pcmk__is_user_in_group(user,id)) { @@ -388,8 +398,8 @@ purge_xml_attributes(xmlNode *xml) xml_node_private_t *nodepriv = xml->_private; if (test_acl_mode(nodepriv->flags, pcmk__xf_acl_read)) { - crm_trace("%s[@" XML_ATTR_ID "=%s] is readable", - xml->name, ID(xml)); + crm_trace("%s[@" PCMK_XA_ID "=%s] is readable", + xml->name, pcmk__xe_id(xml)); return true; } @@ -399,7 +409,7 @@ purge_xml_attributes(xmlNode *xml) const char *prop_name = (const char *)xIter->name; xIter = xIter->next; - if (strcmp(prop_name, XML_ATTR_ID) == 0) { + if (strcmp(prop_name, PCMK_XA_ID) == 0) { continue; } @@ -447,7 +457,7 @@ xml_acl_filtered_copy(const char *user, xmlNode *acl_source, xmlNode *xml, } crm_trace("Filtering XML copy using user '%s' ACLs", user); - target = copy_xml(xml); + target = pcmk__xml_copy(NULL, xml); if (target == NULL) { return true; } @@ -513,7 +523,7 @@ xml_acl_filtered_copy(const char *user, xmlNode *acl_source, xmlNode *xml, * * Check whether XML is a "scaffolding" element whose creation is implicitly * allowed regardless of ACLs (that is, it is not in the ACL section and has - * no attributes other than "id"). + * no attributes other than \c PCMK_XA_ID). * * \param[in] xml XML element to check * @@ -525,7 +535,7 @@ implicitly_allowed(const xmlNode *xml) GString *path = NULL; for (xmlAttr *prop = xml->properties; prop != NULL; prop = prop->next) { - if (strcmp((const char *) prop->name, XML_ATTR_ID) != 0) { + if (strcmp((const char *) prop->name, PCMK_XA_ID) != 0) { return false; } } @@ -533,7 +543,7 @@ implicitly_allowed(const xmlNode *xml) path = pcmk__element_xpath(xml); CRM_ASSERT(path != NULL); - if (strstr((const char *) path->str, "/" XML_CIB_TAG_ACLS "/") != NULL) { + if (strstr((const char *) path->str, "/" PCMK_XE_ACLS "/") != NULL) { g_string_free(path, TRUE); return false; } @@ -542,7 +552,7 @@ implicitly_allowed(const xmlNode *xml) return true; } -#define display_id(xml) (ID(xml)? ID(xml) : "<unset>") +#define display_id(xml) pcmk__s(pcmk__xe_id(xml), "<unset>") /*! * \internal @@ -551,7 +561,7 @@ implicitly_allowed(const xmlNode *xml) * Given an XML element, free all of its descendant nodes created in violation * of ACLs, with the exception of allowing "scaffolding" elements (i.e. those * that aren't in the ACL section and don't have any attributes other than - * "id"). + * \c PCMK_XA_ID). * * \param[in,out] xml XML to check * \param[in] check_top Whether to apply checks to argument itself @@ -566,22 +576,23 @@ pcmk__apply_creation_acl(xmlNode *xml, bool check_top) if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { if (implicitly_allowed(xml)) { - crm_trace("Creation of <%s> scaffolding with id=\"%s\"" + crm_trace("Creation of <%s> scaffolding with " PCMK_XA_ID "=\"%s\"" " is implicitly allowed", xml->name, display_id(xml)); } else if (pcmk__check_acl(xml, NULL, pcmk__xf_acl_write)) { - crm_trace("ACLs allow creation of <%s> with id=\"%s\"", + crm_trace("ACLs allow creation of <%s> with " PCMK_XA_ID "=\"%s\"", xml->name, display_id(xml)); } else if (check_top) { - crm_trace("ACLs disallow creation of <%s> with id=\"%s\"", - xml->name, display_id(xml)); + crm_trace("ACLs disallow creation of <%s> with " + PCMK_XA_ID "=\"%s\"", xml->name, display_id(xml)); pcmk_free_xml_subtree(xml); return; } else { - crm_notice("ACLs would disallow creation of %s<%s> with id=\"%s\"", + crm_notice("ACLs would disallow creation of %s<%s> with " + PCMK_XA_ID "=\"%s\"", ((xml == xmlDocGetRootElement(xml->doc))? "root element " : ""), xml->name, display_id(xml)); } @@ -757,15 +768,13 @@ pcmk_acl_required(const char *user) char * pcmk__uid2username(uid_t uid) { - char *result = NULL; struct passwd *pwent = getpwuid(uid); if (pwent == NULL) { crm_perror(LOG_INFO, "Cannot get user details for user ID %d", uid); return NULL; } - pcmk__str_update(&result, pwent->pw_name); - return result; + return pcmk__str_copy(pwent->pw_name); } /*! @@ -797,25 +806,24 @@ pcmk__update_acl_user(xmlNode *request, const char *field, if (effective_user == NULL) { effective_user = pcmk__uid2username(geteuid()); if (effective_user == NULL) { - effective_user = strdup("#unprivileged"); - CRM_CHECK(effective_user != NULL, return NULL); + effective_user = pcmk__str_copy("#unprivileged"); crm_err("Unable to determine effective user, assuming unprivileged for ACLs"); } } - requested_user = crm_element_value(request, XML_ACL_TAG_USER); + requested_user = crm_element_value(request, PCMK_XE_ACL_TARGET); if (requested_user == NULL) { /* @COMPAT rolling upgrades <=1.1.11 * * field is checked for backward compatibility with older versions that - * did not use XML_ACL_TAG_USER. + * did not use PCMK_XE_ACL_TARGET. */ requested_user = crm_element_value(request, field); } if (!pcmk__is_privileged(effective_user)) { /* We're not running as a privileged user, set or overwrite any existing - * value for $XML_ACL_TAG_USER + * value for PCMK_XE_ACL_TARGET */ user = effective_user; @@ -831,7 +839,7 @@ pcmk__update_acl_user(xmlNode *request, const char *field, } else if (!pcmk__is_privileged(peer_user)) { /* The peer is not a privileged user, set or overwrite any existing - * value for $XML_ACL_TAG_USER + * value for PCMK_XE_ACL_TARGET */ user = peer_user; @@ -845,8 +853,8 @@ pcmk__update_acl_user(xmlNode *request, const char *field, } // This requires pointer comparison, not string comparison - if (user != crm_element_value(request, XML_ACL_TAG_USER)) { - crm_xml_add(request, XML_ACL_TAG_USER, user); + if (user != crm_element_value(request, PCMK_XE_ACL_TARGET)) { + crm_xml_add(request, PCMK_XE_ACL_TARGET, user); } if (field != NULL && user != crm_element_value(request, field)) { diff --git a/lib/common/actions.c b/lib/common/actions.c index e710615..ed1056f 100644 --- a/lib/common/actions.c +++ b/lib/common/actions.c @@ -1,5 +1,5 @@ /* - * 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. * @@ -21,10 +21,164 @@ #include <crm/crm.h> #include <crm/lrmd.h> -#include <crm/msg_xml.h> #include <crm/common/xml.h> #include <crm/common/xml_internal.h> #include <crm/common/util.h> +#include <crm/common/scheduler.h> + +/*! + * \brief Get string equivalent of an action type + * + * \param[in] action Action type + * + * \return Static string describing \p action + */ +const char * +pcmk_action_text(enum action_tasks action) +{ + switch (action) { + case pcmk_action_stop: + return PCMK_ACTION_STOP; + + case pcmk_action_stopped: + return PCMK_ACTION_STOPPED; + + case pcmk_action_start: + return PCMK_ACTION_START; + + case pcmk_action_started: + return PCMK_ACTION_RUNNING; + + case pcmk_action_shutdown: + return PCMK_ACTION_DO_SHUTDOWN; + + case pcmk_action_fence: + return PCMK_ACTION_STONITH; + + case pcmk_action_monitor: + return PCMK_ACTION_MONITOR; + + case pcmk_action_notify: + return PCMK_ACTION_NOTIFY; + + case pcmk_action_notified: + return PCMK_ACTION_NOTIFIED; + + case pcmk_action_promote: + return PCMK_ACTION_PROMOTE; + + case pcmk_action_promoted: + return PCMK_ACTION_PROMOTED; + + case pcmk_action_demote: + return PCMK_ACTION_DEMOTE; + + case pcmk_action_demoted: + return PCMK_ACTION_DEMOTED; + + default: // pcmk_action_unspecified or invalid + return "no_action"; + } +} + +/*! + * \brief Parse an action type from an action name + * + * \param[in] action_name Action name + * + * \return Action type corresponding to \p action_name + */ +enum action_tasks +pcmk_parse_action(const char *action_name) +{ + if (pcmk__str_eq(action_name, PCMK_ACTION_STOP, pcmk__str_none)) { + return pcmk_action_stop; + + } else if (pcmk__str_eq(action_name, PCMK_ACTION_STOPPED, pcmk__str_none)) { + return pcmk_action_stopped; + + } else if (pcmk__str_eq(action_name, PCMK_ACTION_START, pcmk__str_none)) { + return pcmk_action_start; + + } else if (pcmk__str_eq(action_name, PCMK_ACTION_RUNNING, pcmk__str_none)) { + return pcmk_action_started; + + } else if (pcmk__str_eq(action_name, PCMK_ACTION_DO_SHUTDOWN, + pcmk__str_none)) { + return pcmk_action_shutdown; + + } else if (pcmk__str_eq(action_name, PCMK_ACTION_STONITH, pcmk__str_none)) { + return pcmk_action_fence; + + } else if (pcmk__str_eq(action_name, PCMK_ACTION_MONITOR, pcmk__str_none)) { + return pcmk_action_monitor; + + } else if (pcmk__str_eq(action_name, PCMK_ACTION_NOTIFY, pcmk__str_none)) { + return pcmk_action_notify; + + } else if (pcmk__str_eq(action_name, PCMK_ACTION_NOTIFIED, + pcmk__str_none)) { + return pcmk_action_notified; + + } else if (pcmk__str_eq(action_name, PCMK_ACTION_PROMOTE, pcmk__str_none)) { + return pcmk_action_promote; + + } else if (pcmk__str_eq(action_name, PCMK_ACTION_DEMOTE, pcmk__str_none)) { + return pcmk_action_demote; + + } else if (pcmk__str_eq(action_name, PCMK_ACTION_PROMOTED, + pcmk__str_none)) { + return pcmk_action_promoted; + + } else if (pcmk__str_eq(action_name, PCMK_ACTION_DEMOTED, pcmk__str_none)) { + return pcmk_action_demoted; + } + return pcmk_action_unspecified; +} + +/*! + * \brief Get string equivalent of a failure handling type + * + * \param[in] on_fail Failure handling type + * + * \return Static string describing \p on_fail + */ +const char * +pcmk_on_fail_text(enum action_fail_response on_fail) +{ + switch (on_fail) { + case pcmk_on_fail_ignore: + return "ignore"; + + case pcmk_on_fail_demote: + return "demote"; + + case pcmk_on_fail_block: + return "block"; + + case pcmk_on_fail_restart: + return "recover"; + + case pcmk_on_fail_ban: + return "migrate"; + + case pcmk_on_fail_stop: + return "stop"; + + case pcmk_on_fail_fence_node: + return "fence"; + + case pcmk_on_fail_standby_node: + return "standby"; + + case pcmk_on_fail_restart_container: + return "restart-container"; + + case pcmk_on_fail_reset_remote: + return "reset-remote"; + } + return "<unknown>"; +} /*! * \brief Generate an operation key (RESOURCE_ACTION_INTERVAL) @@ -166,12 +320,12 @@ parse_op_key(const char *key, char **rsc_id, char **op_type, guint *interval_ms) // Set output variables if (rsc_id != NULL) { *rsc_id = strndup(key, action_underbar); - CRM_ASSERT(*rsc_id != NULL); + pcmk__mem_assert(*rsc_id); } if (op_type != NULL) { *op_type = strndup(key + action_underbar + 1, interval_underbar - action_underbar - 1); - CRM_ASSERT(*op_type != NULL); + pcmk__mem_assert(*op_type); } if (interval_ms != NULL) { *interval_ms = local_interval_ms; @@ -220,8 +374,8 @@ decode_transition_magic(const char *magic, char **uuid, int *transition_id, int #ifdef HAVE_SSCANF_M res = sscanf(magic, "%d:%d;%ms", &local_op_status, &local_op_rc, &key); #else - key = calloc(1, strlen(magic) - 3); // magic must have >=4 other characters - CRM_ASSERT(key); + // magic must have >=4 other characters + key = pcmk__assert_alloc(1, strlen(magic) - 3); res = sscanf(magic, "%d:%d;%s", &local_op_status, &local_op_rc, key); #endif if (res == EOF) { @@ -301,8 +455,7 @@ decode_transition_key(const char *key, char **uuid, int *transition_id, int *act crm_warn("Invalid UUID '%s' in transition key '%s'", local_uuid, key); } if (uuid) { - *uuid = strdup(local_uuid); - CRM_ASSERT(*uuid); + *uuid = pcmk__str_copy(local_uuid); } if (transition_id) { *transition_id = local_transition_id; @@ -316,66 +469,6 @@ decode_transition_key(const char *key, char **uuid, int *transition_id, int *act return TRUE; } -// Return true if a is an attribute that should be filtered -static bool -should_filter_for_digest(xmlAttrPtr a, void *user_data) -{ - if (strncmp((const char *) a->name, CRM_META "_", - sizeof(CRM_META " ") - 1) == 0) { - return true; - } - return pcmk__str_any_of((const char *) a->name, - XML_ATTR_ID, - XML_ATTR_CRM_VERSION, - XML_LRM_ATTR_OP_DIGEST, - XML_LRM_ATTR_TARGET, - XML_LRM_ATTR_TARGET_UUID, - "pcmk_external_ip", - NULL); -} - -/*! - * \internal - * \brief Remove XML attributes not needed for operation digest - * - * \param[in,out] param_set XML with operation parameters - */ -void -pcmk__filter_op_for_digest(xmlNode *param_set) -{ - char *key = NULL; - char *timeout = NULL; - guint interval_ms = 0; - - if (param_set == NULL) { - return; - } - - /* Timeout is useful for recurring operation digests, so grab it before - * removing meta-attributes - */ - key = crm_meta_name(XML_LRM_ATTR_INTERVAL_MS); - if (crm_element_value_ms(param_set, key, &interval_ms) != pcmk_ok) { - interval_ms = 0; - } - free(key); - key = NULL; - if (interval_ms != 0) { - key = crm_meta_name(XML_ATTR_TIMEOUT); - timeout = crm_element_value_copy(param_set, key); - } - - // Remove all CRM_meta_* attributes and certain other attributes - pcmk__xe_remove_matching_attrs(param_set, should_filter_for_digest, NULL); - - // Add timeout back for recurring operation digests - if (timeout != NULL) { - crm_xml_add(param_set, key, timeout); - } - free(timeout); - free(key); -} - int rsc_op_expected_rc(const lrmd_event_data_t *op) { @@ -432,12 +525,12 @@ crm_create_op_xml(xmlNode *parent, const char *prefix, const char *task, CRM_CHECK(prefix && task && interval_spec, return NULL); - xml_op = create_xml_node(parent, XML_ATTR_OP); + xml_op = pcmk__xe_create(parent, PCMK_XE_OP); crm_xml_set_id(xml_op, "%s-%s-%s", prefix, task, interval_spec); - crm_xml_add(xml_op, XML_LRM_ATTR_INTERVAL, interval_spec); - crm_xml_add(xml_op, "name", task); + crm_xml_add(xml_op, PCMK_META_INTERVAL, interval_spec); + crm_xml_add(xml_op, PCMK_XA_NAME, task); if (timeout) { - crm_xml_add(xml_op, XML_ATTR_TIMEOUT, timeout); + crm_xml_add(xml_op, PCMK_META_TIMEOUT, timeout); } return xml_op; } @@ -483,50 +576,12 @@ crm_op_needs_metadata(const char *rsc_class, const char *op) * * \param[in] action Action name to check * - * \return true if \p action is "off", "reboot", or "poweroff", otherwise false + * \return \c true if \p action is \c PCMK_ACTION_OFF, \c PCMK_ACTION_REBOOT, + * or \c PCMK__ACTION_POWEROFF, otherwise \c false */ bool pcmk__is_fencing_action(const char *action) { return pcmk__str_any_of(action, PCMK_ACTION_OFF, PCMK_ACTION_REBOOT, - "poweroff", NULL); -} - -bool -pcmk_is_probe(const char *task, guint interval) -{ - if (task == NULL) { - return false; - } - - return (interval == 0) - && pcmk__str_eq(task, PCMK_ACTION_MONITOR, pcmk__str_none); -} - -bool -pcmk_xe_is_probe(const xmlNode *xml_op) -{ - const char *task = crm_element_value(xml_op, XML_LRM_ATTR_TASK); - const char *interval_ms_s = crm_element_value(xml_op, XML_LRM_ATTR_INTERVAL_MS); - int interval_ms; - - pcmk__scan_min_int(interval_ms_s, &interval_ms, 0); - return pcmk_is_probe(task, interval_ms); -} - -bool -pcmk_xe_mask_probe_failure(const xmlNode *xml_op) -{ - int status = PCMK_EXEC_UNKNOWN; - int rc = PCMK_OCF_OK; - - if (!pcmk_xe_is_probe(xml_op)) { - return false; - } - - crm_element_value_int(xml_op, XML_LRM_ATTR_OPSTATUS, &status); - crm_element_value_int(xml_op, XML_LRM_ATTR_RC, &rc); - - return rc == PCMK_OCF_NOT_INSTALLED || rc == PCMK_OCF_INVALID_PARAM || - status == PCMK_EXEC_NOT_INSTALLED; + PCMK__ACTION_POWEROFF, NULL); } diff --git a/lib/common/agents.c b/lib/common/agents.c index d2066c0..dd4ba2e 100644 --- a/lib/common/agents.c +++ b/lib/common/agents.c @@ -46,7 +46,8 @@ pcmk_get_ra_caps(const char *standard) * (which were likely never used as real configurations). * * @TODO Remove pcmk_ra_cap_unique at the next major schema version - * bump, with a transform to remove globally-unique from the config. + * bump, with a transform to remove PCMK_META_GLOBALLY_UNIQUE from the + * config. */ return pcmk_ra_cap_params | pcmk_ra_cap_unique | pcmk_ra_cap_stdin | pcmk_ra_cap_fence_params; diff --git a/lib/common/alerts.c b/lib/common/alerts.c index 98b1e3f..eac3e2e 100644 --- a/lib/common/alerts.c +++ b/lib/common/alerts.c @@ -1,5 +1,5 @@ /* - * Copyright 2015-2023 the Pacemaker project contributors + * Copyright 2015-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -10,7 +10,7 @@ #include <crm_internal.h> #include <crm/crm.h> #include <crm/lrmd.h> -#include <crm/msg_xml.h> +#include <crm/common/xml.h> #include <crm/common/alerts_internal.h> #include <crm/common/cib_internal.h> #include <crm/common/xml_internal.h> @@ -94,11 +94,11 @@ const char *pcmk__alert_keys[PCMK__ALERT_INTERNAL_KEY_MAX][3] = pcmk__alert_t * pcmk__alert_new(const char *id, const char *path) { - pcmk__alert_t *entry = calloc(1, sizeof(pcmk__alert_t)); + pcmk__alert_t *entry = pcmk__assert_alloc(1, sizeof(pcmk__alert_t)); - CRM_ASSERT(entry && id && path); - entry->id = strdup(id); - entry->path = strdup(path); + CRM_ASSERT((id != NULL) && (path != NULL)); + entry->id = pcmk__str_copy(id); + entry->path = pcmk__str_copy(path); entry->timeout = PCMK__ALERT_DEFAULT_TIMEOUT_MS; entry->flags = pcmk__alert_default; return entry; @@ -137,8 +137,8 @@ pcmk__dup_alert(const pcmk__alert_t *entry) new_entry->timeout = entry->timeout; new_entry->flags = entry->flags; new_entry->envvars = pcmk__str_table_dup(entry->envvars); - pcmk__str_update(&new_entry->tstamp_format, entry->tstamp_format); - pcmk__str_update(&new_entry->recipient, entry->recipient); + new_entry->tstamp_format = pcmk__str_copy(entry->tstamp_format); + new_entry->recipient = pcmk__str_copy(entry->recipient); if (entry->select_attribute_name) { new_entry->select_attribute_name = g_strdupv(entry->select_attribute_name); } @@ -152,7 +152,7 @@ pcmk__add_alert_key(GHashTable *table, enum pcmk__alert_keys_e name, for (const char **key = pcmk__alert_keys[name]; *key; key++) { crm_trace("Inserting alert key %s = '%s'", *key, value); if (value) { - g_hash_table_insert(table, strdup(*key), strdup(value)); + pcmk__insert_dup(table, *key, value); } else { g_hash_table_remove(table, *key); } @@ -165,6 +165,6 @@ pcmk__add_alert_key_int(GHashTable *table, enum pcmk__alert_keys_e name, { for (const char **key = pcmk__alert_keys[name]; *key; key++) { crm_trace("Inserting alert key %s = %d", *key, value); - g_hash_table_insert(table, strdup(*key), pcmk__itoa(value)); + g_hash_table_insert(table, pcmk__str_copy(*key), pcmk__itoa(value)); } } diff --git a/lib/common/attrs.c b/lib/common/attrs.c index 2be03b4..35715df 100644 --- a/lib/common/attrs.c +++ b/lib/common/attrs.c @@ -1,5 +1,5 @@ /* - * Copyright 2011-2022 the Pacemaker project contributors + * Copyright 2011-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -15,10 +15,12 @@ #include <stdio.h> -#include <crm/msg_xml.h> -#include <crm/common/attrd_internal.h> +#include <crm/common/xml.h> +#include <crm/common/scheduler.h> +#include <crm/common/scheduler_internal.h> -#define LRM_TARGET_ENV "OCF_RESKEY_" CRM_META "_" XML_LRM_ATTR_TARGET +#define OCF_RESKEY_PREFIX "OCF_RESKEY_" +#define LRM_TARGET_ENV OCF_RESKEY_PREFIX CRM_META "_" PCMK__META_ON_NODE /*! * \internal @@ -27,7 +29,8 @@ * If given NULL, "auto", or "localhost" as an argument, check the environment * to detect the node name that should be used to set node attributes. (The * caller might not know the correct name, for example if the target is part of - * a bundle with container-attribute-target set to "host".) + * a bundle with \c PCMK_META_CONTAINER_ATTRIBUTE_TARGET set to + * \c PCMK_VALUE_HOST.) * * \param[in] name NULL, "auto" or "localhost" to check environment variables, * or anything else to return NULL @@ -39,13 +42,22 @@ const char * pcmk__node_attr_target(const char *name) { if (name == NULL || pcmk__strcase_any_of(name, "auto", "localhost", NULL)) { - char *target_var = crm_meta_name(XML_RSC_ATTR_TARGET); - char *phys_var = crm_meta_name(PCMK__ENV_PHYSICAL_HOST); - const char *target = getenv(target_var); - const char *host_physical = getenv(phys_var); + char buf[128] = OCF_RESKEY_PREFIX; + size_t offset = sizeof(OCF_RESKEY_PREFIX) - 1; + char *target_var = crm_meta_name(PCMK_META_CONTAINER_ATTRIBUTE_TARGET); + char *phys_var = crm_meta_name(PCMK__META_PHYSICAL_HOST); + const char *target = NULL; + const char *host_physical = NULL; + + snprintf(buf + offset, sizeof(buf) - offset, "%s", target_var); + target = getenv(buf); + + snprintf(buf + offset, sizeof(buf) - offset, "%s", phys_var); + host_physical = getenv(buf); // It is important to use the name by which the scheduler knows us - if (host_physical && pcmk__str_eq(target, "host", pcmk__str_casei)) { + if (host_physical + && pcmk__str_eq(target, PCMK_VALUE_HOST, pcmk__str_casei)) { name = host_physical; } else { @@ -58,7 +70,7 @@ pcmk__node_attr_target(const char *name) free(target_var); free(phys_var); - // TODO? Call get_local_node_name() if name == NULL + // TODO? Call pcmk__cluster_local_node_name() if name == NULL // (currently would require linkage against libcrmcluster) return name; } else { @@ -87,3 +99,85 @@ pcmk_promotion_score_name(const char *rsc_id) } return crm_strdup_printf("master-%s", rsc_id); } + +/*! + * \internal + * \brief Get the value of a node attribute + * + * \param[in] node Node to get attribute for + * \param[in] name Name of node attribute to get + * \param[in] target If this is \c PCMK_VALUE_HOST and \p node is a guest + * (bundle) node, get the value from the guest's host, + * otherwise get the value from \p node itself + * \param[in] node_type If getting the value from \p node's host, this + * indicates whether to check the current or assigned host + * + * \return Value of \p name attribute for \p node + */ +const char * +pcmk__node_attr(const pcmk_node_t *node, const char *name, const char *target, + enum pcmk__rsc_node node_type) +{ + const char *value = NULL; // Attribute value to return + const char *node_type_s = NULL; // Readable equivalent of node_type + const pcmk_node_t *host = NULL; + const pcmk_resource_t *container = NULL; + + if ((node == NULL) || (name == NULL)) { + return NULL; + } + + /* Check the node's own attributes unless this is a guest (bundle) node with + * the container host as the attribute target. + */ + if (!pcmk__is_guest_or_bundle_node(node) + || !pcmk__str_eq(target, PCMK_VALUE_HOST, pcmk__str_casei)) { + value = g_hash_table_lookup(node->details->attrs, name); + crm_trace("%s='%s' on %s", + name, pcmk__s(value, ""), pcmk__node_name(node)); + return value; + } + + /* This resource needs attributes set for the container's host instead of + * for the container itself (useful when the container uses the host's + * storage). + */ + container = node->details->remote_rsc->container; + + switch (node_type) { + case pcmk__rsc_node_assigned: + host = container->allocated_to; + if (host == NULL) { + crm_trace("Skipping %s lookup for %s because " + "its container %s is unassigned", + name, pcmk__node_name(node), container->id); + return NULL; + } + node_type_s = "assigned"; + break; + + case pcmk__rsc_node_current: + if (container->running_on != NULL) { + host = container->running_on->data; + } + if (host == NULL) { + crm_trace("Skipping %s lookup for %s because " + "its container %s is inactive", + name, pcmk__node_name(node), container->id); + return NULL; + } + node_type_s = "current"; + break; + + default: + // Add support for other enum pcmk__rsc_node values if needed + CRM_ASSERT(false); + break; + } + + value = g_hash_table_lookup(host->details->attrs, name); + crm_trace("%s='%s' for %s on %s container host %s", + name, pcmk__s(value, ""), pcmk__node_name(node), node_type_s, + pcmk__node_name(host)); + return value; +} diff --git a/lib/common/cib.c b/lib/common/cib.c index fee7881..fee962b 100644 --- a/lib/common/cib.c +++ b/lib/common/cib.c @@ -1,6 +1,6 @@ /* * Original copyright 2004 International Business Machines - * Later changes copyright 2008-2023 the Pacemaker project contributors + * Later changes copyright 2008-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -13,7 +13,7 @@ #include <stdio.h> #include <libxml/tree.h> // xmlNode -#include <crm/msg_xml.h> +#include <crm/common/xml.h> #include <crm/common/cib.h> #include <crm/common/cib_internal.h> @@ -29,74 +29,74 @@ static struct { } cib_sections[] = { { // This first entry is also the default if a NULL is compared - XML_TAG_CIB, + PCMK_XE_CIB, NULL, - "//" XML_TAG_CIB + "//" PCMK_XE_CIB }, { - XML_CIB_TAG_STATUS, - "/" XML_TAG_CIB, - "//" XML_TAG_CIB "/" XML_CIB_TAG_STATUS + PCMK_XE_STATUS, + "/" PCMK_XE_CIB, + "//" PCMK_XE_CIB "/" PCMK_XE_STATUS }, { - XML_CIB_TAG_CONFIGURATION, - "/" XML_TAG_CIB, - "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION + PCMK_XE_CONFIGURATION, + "/" PCMK_XE_CIB, + "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION }, { - XML_CIB_TAG_CRMCONFIG, - "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, - "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_CRMCONFIG + PCMK_XE_CRM_CONFIG, + "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION, + "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_CRM_CONFIG }, { - XML_CIB_TAG_NODES, - "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, - "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_NODES + PCMK_XE_NODES, + "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION, + "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_NODES }, { - XML_CIB_TAG_RESOURCES, - "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, - "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_RESOURCES + PCMK_XE_RESOURCES, + "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION, + "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_RESOURCES }, { - XML_CIB_TAG_CONSTRAINTS, - "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, - "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_CONSTRAINTS + PCMK_XE_CONSTRAINTS, + "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION, + "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_CONSTRAINTS }, { - XML_CIB_TAG_OPCONFIG, - "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, - "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_OPCONFIG + PCMK_XE_OP_DEFAULTS, + "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION, + "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_OP_DEFAULTS }, { - XML_CIB_TAG_RSCCONFIG, - "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, - "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_RSCCONFIG + PCMK_XE_RSC_DEFAULTS, + "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION, + "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_RSC_DEFAULTS }, { - XML_CIB_TAG_ACLS, - "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, - "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_ACLS + PCMK_XE_ACLS, + "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION, + "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_ACLS }, { - XML_TAG_FENCING_TOPOLOGY, - "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, - "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_TAG_FENCING_TOPOLOGY + PCMK_XE_FENCING_TOPOLOGY, + "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION, + "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_FENCING_TOPOLOGY }, { - XML_CIB_TAG_TAGS, - "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, - "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_TAGS + PCMK_XE_TAGS, + "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION, + "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_TAGS }, { - XML_CIB_TAG_ALERTS, - "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, - "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_ALERTS + PCMK_XE_ALERTS, + "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION, + "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_ALERTS }, { - XML_CIB_TAG_SECTION_ALL, + PCMK__XE_ALL, NULL, - "//" XML_TAG_CIB + "//" PCMK_XE_CIB }, }; @@ -173,3 +173,19 @@ pcmk_find_cib_element(xmlNode *cib, const char *element_name) { return get_xpath_object(pcmk_cib_xpath_for(element_name), cib, LOG_TRACE); } + +/*! + * \internal + * \brief Check that the feature set in the CIB is supported on this node + * + * \param[in] new_version PCMK_XA_CRM_FEATURE_SET attribute from the CIB + */ +int +pcmk__check_feature_set(const char *cib_version) +{ + if (cib_version && compare_version(cib_version, CRM_FEATURE_SET) > 0) { + return EPROTONOSUPPORT; + } + + return pcmk_rc_ok; +} diff --git a/lib/common/crmcommon_private.h b/lib/common/crmcommon_private.h index 121d663..d90a64e 100644 --- a/lib/common/crmcommon_private.h +++ b/lib/common/crmcommon_private.h @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the Pacemaker project contributors + * Copyright 2018-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -17,7 +17,7 @@ #include <stdint.h> // uint8_t, uint32_t #include <stdbool.h> // bool #include <sys/types.h> // size_t -#include <glib.h> // GList +#include <glib.h> // gchar, GList #include <libxml/tree.h> // xmlNode, xmlAttr #include <qb/qbipcc.h> // struct qb_ipc_response_header @@ -33,23 +33,33 @@ * (e.g. when checking differences) that something was deleted. */ typedef struct pcmk__deleted_xml_s { - char *path; - int position; + gchar *path; + int position; } pcmk__deleted_xml_t; typedef struct xml_node_private_s { - long check; + uint32_t check; uint32_t flags; } xml_node_private_t; typedef struct xml_doc_private_s { - long check; + 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, \ @@ -63,14 +73,10 @@ typedef struct xml_doc_private_s { } while (0) G_GNUC_INTERNAL -void pcmk__xml2text(const xmlNode *data, uint32_t options, GString *buffer, - int depth); - -G_GNUC_INTERNAL bool pcmk__tracking_xml_changes(xmlNode *xml, bool lazy); G_GNUC_INTERNAL -void pcmk__mark_xml_created(xmlNode *xml); +void pcmk__xml_mark_created(xmlNode *xml); G_GNUC_INTERNAL int pcmk__xml_position(const xmlNode *xml, @@ -82,7 +88,7 @@ xmlNode *pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle, G_GNUC_INTERNAL void pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update, - bool as_diff); + uint32_t flags, bool as_diff); G_GNUC_INTERNAL xmlNode *pcmk__xc_match(const xmlNode *root, const xmlNode *search_comment, @@ -125,6 +131,36 @@ bool pcmk__marked_as_deleted(xmlAttrPtr a, void *user_data); G_GNUC_INTERNAL void pcmk__dump_xml_attr(const xmlAttr *attr, GString *buffer); +G_GNUC_INTERNAL +int pcmk__xe_set_score(xmlNode *target, const char *name, const char *value); + +/* + * 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 */ @@ -274,8 +310,81 @@ 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 @@ -283,4 +392,31 @@ void pcmk__register_patchset_messages(pcmk__output_t *out); #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 { + int schema_index; + 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 +GList *pcmk__find_x_0_schema(void); + + #endif // CRMCOMMON_PRIVATE__H diff --git a/lib/common/digest.c b/lib/common/digest.c index 4de6f97..9a06ff4 100644 --- a/lib/common/digest.c +++ b/lib/common/digest.c @@ -1,5 +1,5 @@ /* - * Copyright 2015-2023 the Pacemaker project contributors + * Copyright 2015-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -16,7 +16,6 @@ #include <md5.h> #include <crm/crm.h> -#include <crm/msg_xml.h> #include <crm/common/xml.h> #include "crmcommon_private.h" @@ -37,7 +36,7 @@ dump_xml_for_digest(xmlNodePtr xml) /* for compatibility with the old result which is used for v1 digests */ g_string_append_c(buffer, ' '); - pcmk__xml2text(xml, 0, buffer, 0); + pcmk__xml_string(xml, 0, buffer, 0); g_string_append_c(buffer, '\n'); return buffer; @@ -92,13 +91,12 @@ static char * calculate_xml_digest_v2(const xmlNode *source, gboolean do_filter) { char *digest = NULL; - GString *buffer = g_string_sized_new(1024); + GString *buf = g_string_sized_new(1024); crm_trace("Begin digest %s", do_filter?"filtered":""); - pcmk__xml2text(source, (do_filter? pcmk__xml_fmt_filtered : 0), buffer, 0); - CRM_ASSERT(buffer != NULL); - digest = crm_md5sum((const char *) buffer->str); + pcmk__xml_string(source, (do_filter? pcmk__xml_fmt_filtered : 0), buf, 0); + digest = crm_md5sum(buf->str); pcmk__if_tracing( { @@ -106,17 +104,17 @@ calculate_xml_digest_v2(const xmlNode *source, gboolean do_filter) pcmk__get_tmpdir(), digest); crm_trace("Saving %s.%s.%s to %s", - crm_element_value(source, XML_ATTR_GENERATION_ADMIN), - crm_element_value(source, XML_ATTR_GENERATION), - crm_element_value(source, XML_ATTR_NUMUPDATES), + crm_element_value(source, PCMK_XA_ADMIN_EPOCH), + crm_element_value(source, PCMK_XA_EPOCH), + crm_element_value(source, PCMK_XA_NUM_UPDATES), trace_file); save_xml_to_file(source, "digest input", trace_file); free(trace_file); }, {} ); - g_string_free(buffer, TRUE); crm_trace("End digest"); + g_string_free(buf, TRUE); return digest; } @@ -234,11 +232,11 @@ bool pcmk__xa_filterable(const char *name) { static const char *filter[] = { - XML_ATTR_ORIGIN, - XML_CIB_ATTR_WRITTEN, - XML_ATTR_UPDATE_ORIG, - XML_ATTR_UPDATE_CLIENT, - XML_ATTR_UPDATE_USER, + PCMK_XA_CRM_DEBUG_ORIGIN, + PCMK_XA_CIB_LAST_WRITTEN, + PCMK_XA_UPDATE_ORIGIN, + PCMK_XA_UPDATE_CLIENT, + PCMK_XA_UPDATE_USER, }; for (int i = 0; i < PCMK__NELEM(filter); i++) { @@ -276,3 +274,63 @@ crm_md5sum(const char *buffer) } return digest; } + +// Return true if a is an attribute that should be filtered +static bool +should_filter_for_digest(xmlAttrPtr a, void *user_data) +{ + if (strncmp((const char *) a->name, CRM_META "_", + sizeof(CRM_META " ") - 1) == 0) { + return true; + } + return pcmk__str_any_of((const char *) a->name, + PCMK_XA_ID, + PCMK_XA_CRM_FEATURE_SET, + PCMK__XA_OP_DIGEST, + PCMK__META_ON_NODE, + PCMK__META_ON_NODE_UUID, + "pcmk_external_ip", + NULL); +} + +/*! + * \internal + * \brief Remove XML attributes not needed for operation digest + * + * \param[in,out] param_set XML with operation parameters + */ +void +pcmk__filter_op_for_digest(xmlNode *param_set) +{ + char *key = NULL; + char *timeout = NULL; + guint interval_ms = 0; + + if (param_set == NULL) { + return; + } + + /* Timeout is useful for recurring operation digests, so grab it before + * removing meta-attributes + */ + key = crm_meta_name(PCMK_META_INTERVAL); + if (crm_element_value_ms(param_set, key, &interval_ms) != pcmk_ok) { + interval_ms = 0; + } + free(key); + key = NULL; + if (interval_ms != 0) { + key = crm_meta_name(PCMK_META_TIMEOUT); + timeout = crm_element_value_copy(param_set, key); + } + + // Remove all CRM_meta_* attributes and certain other attributes + pcmk__xe_remove_matching_attrs(param_set, should_filter_for_digest, NULL); + + // Add timeout back for recurring operation digests + if (timeout != NULL) { + crm_xml_add(param_set, key, timeout); + } + free(timeout); + free(key); +} diff --git a/lib/common/health.c b/lib/common/health.c index ee412be..20cf36e 100644 --- a/lib/common/health.c +++ b/lib/common/health.c @@ -1,5 +1,5 @@ /* - * Copyright 2022 the Pacemaker project contributors + * Copyright 2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -21,11 +21,11 @@ bool pcmk__validate_health_strategy(const char *value) { return pcmk__strcase_any_of(value, - PCMK__VALUE_NONE, - PCMK__VALUE_CUSTOM, - PCMK__VALUE_ONLY_GREEN, - PCMK__VALUE_PROGRESSIVE, - PCMK__VALUE_MIGRATE_ON_RED, + PCMK_VALUE_NONE, + PCMK_VALUE_CUSTOM, + PCMK_VALUE_ONLY_GREEN, + PCMK_VALUE_PROGRESSIVE, + PCMK_VALUE_MIGRATE_ON_RED, NULL); } @@ -40,29 +40,24 @@ pcmk__validate_health_strategy(const char *value) enum pcmk__health_strategy pcmk__parse_health_strategy(const char *value) { - if (pcmk__str_eq(value, PCMK__VALUE_NONE, + if (pcmk__str_eq(value, PCMK_VALUE_NONE, pcmk__str_null_matches|pcmk__str_casei)) { return pcmk__health_strategy_none; - - } else if (pcmk__str_eq(value, PCMK__VALUE_MIGRATE_ON_RED, - pcmk__str_casei)) { + } + if (pcmk__str_eq(value, PCMK_VALUE_MIGRATE_ON_RED, pcmk__str_casei)) { return pcmk__health_strategy_no_red; - - } else if (pcmk__str_eq(value, PCMK__VALUE_ONLY_GREEN, - pcmk__str_casei)) { + } + if (pcmk__str_eq(value, PCMK_VALUE_ONLY_GREEN, pcmk__str_casei)) { return pcmk__health_strategy_only_green; - - } else if (pcmk__str_eq(value, PCMK__VALUE_PROGRESSIVE, - pcmk__str_casei)) { + } + if (pcmk__str_eq(value, PCMK_VALUE_PROGRESSIVE, pcmk__str_casei)) { return pcmk__health_strategy_progressive; - - } else if (pcmk__str_eq(value, PCMK__VALUE_CUSTOM, - pcmk__str_casei)) { + } + if (pcmk__str_eq(value, PCMK_VALUE_CUSTOM, pcmk__str_casei)) { return pcmk__health_strategy_custom; - } else { - pcmk__config_err("Using default of \"" PCMK__VALUE_NONE "\" for " - PCMK__OPT_NODE_HEALTH_STRATEGY + pcmk__config_err("Using default of \"" PCMK_VALUE_NONE "\" for " + PCMK_OPT_NODE_HEALTH_STRATEGY " because '%s' is not a valid value", value); return pcmk__health_strategy_none; diff --git a/lib/common/io.c b/lib/common/io.c index 35efbe9..ee71af7 100644 --- a/lib/common/io.c +++ b/lib/common/io.c @@ -1,5 +1,5 @@ /* - * Copyright 2004-2022 the Pacemaker project contributors + * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -633,20 +633,13 @@ pcmk__close_fds_in_child(bool all) char * pcmk__full_path(const char *filename, const char *dirname) { - char *path = NULL; - CRM_ASSERT(filename != NULL); if (filename[0] == '/') { - path = strdup(filename); - CRM_ASSERT(path != NULL); - - } else { - CRM_ASSERT(dirname != NULL); - path = crm_strdup_printf("%s/%s", dirname, filename); + return pcmk__str_copy(filename); } - - return path; + CRM_ASSERT(dirname != NULL); + return crm_strdup_printf("%s/%s", dirname, filename); } // Deprecated functions kept only for backward API compatibility diff --git a/lib/common/ipc_attrd.c b/lib/common/ipc_attrd.c index 9caaabe..5ab0f2d 100644 --- a/lib/common/ipc_attrd.c +++ b/lib/common/ipc_attrd.c @@ -1,5 +1,5 @@ /* - * Copyright 2011-2023 the Pacemaker project contributors + * Copyright 2011-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -16,10 +16,10 @@ #include <stdio.h> #include <crm/crm.h> +#include <crm/common/attrs_internal.h> #include <crm/common/ipc.h> #include <crm/common/ipc_attrd_internal.h> -#include <crm/common/attrd_internal.h> -#include <crm/msg_xml.h> +#include <crm/common/xml.h> #include "crmcommon_private.h" static void @@ -30,13 +30,13 @@ set_pairs_data(pcmk__attrd_api_reply_t *data, xmlNode *msg_data) name = crm_element_value(msg_data, PCMK__XA_ATTR_NAME); - for (xmlNode *node = first_named_child(msg_data, XML_CIB_TAG_NODE); - node != NULL; node = crm_next_same_xml(node)) { - pair = calloc(1, sizeof(pcmk__attrd_query_pair_t)); + for (xmlNode *node = pcmk__xe_first_child(msg_data, PCMK_XE_NODE, NULL, + NULL); + node != NULL; node = pcmk__xe_next_same(node)) { - CRM_ASSERT(pair != NULL); + pair = pcmk__assert_alloc(1, sizeof(pcmk__attrd_query_pair_t)); - pair->node = crm_element_value(node, PCMK__XA_ATTR_NODE_NAME); + pair->node = crm_element_value(node, PCMK__XA_ATTR_HOST); pair->name = name; pair->value = crm_element_value(node, PCMK__XA_ATTR_VALUE); data->data.pairs = g_list_prepend(data->data.pairs, pair); @@ -46,7 +46,7 @@ set_pairs_data(pcmk__attrd_api_reply_t *data, xmlNode *msg_data) static bool reply_expected(pcmk_ipc_api_t *api, const xmlNode *request) { - const char *command = crm_element_value(request, PCMK__XA_TASK); + const char *command = crm_element_value(request, PCMK_XA_TASK); return pcmk__str_any_of(command, PCMK__ATTRD_CMD_CLEAR_FAILURE, @@ -68,21 +68,22 @@ dispatch(pcmk_ipc_api_t *api, xmlNode *reply) pcmk__attrd_reply_unknown }; - if (pcmk__str_eq((const char *) reply->name, "ack", pcmk__str_none)) { + if (pcmk__xe_is(reply, PCMK__XE_ACK)) { return false; } /* Do some basic validation of the reply */ - value = crm_element_value(reply, F_TYPE); + value = crm_element_value(reply, PCMK__XA_T); if (pcmk__str_empty(value) - || !pcmk__str_eq(value, T_ATTRD, pcmk__str_none)) { + || !pcmk__str_eq(value, PCMK__VALUE_ATTRD, pcmk__str_none)) { crm_info("Unrecognizable message from attribute manager: " - "message type '%s' not '" T_ATTRD "'", pcmk__s(value, "")); + "message type '%s' not '" PCMK__VALUE_ATTRD "'", + pcmk__s(value, "")); status = CRM_EX_PROTOCOL; goto done; } - value = crm_element_value(reply, F_SUBTYPE); + value = crm_element_value(reply, PCMK__XA_SUBT); /* Only the query command gets a reply for now. NULL counts as query for * backward compatibility with attribute managers <2.1.3 that didn't set it. @@ -139,61 +140,38 @@ pcmk__attrd_api_methods(void) static xmlNode * create_attrd_op(const char *user_name) { - xmlNode *attrd_op = create_xml_node(NULL, __func__); + xmlNode *attrd_op = pcmk__xe_create(NULL, __func__); - crm_xml_add(attrd_op, F_TYPE, T_ATTRD); - crm_xml_add(attrd_op, F_ORIG, (crm_system_name? crm_system_name: "unknown")); + crm_xml_add(attrd_op, PCMK__XA_T, PCMK__VALUE_ATTRD); + crm_xml_add(attrd_op, PCMK__XA_SRC, pcmk__s(crm_system_name, "unknown")); crm_xml_add(attrd_op, PCMK__XA_ATTR_USER, user_name); return attrd_op; } static int -create_api(pcmk_ipc_api_t **api) -{ - int rc = pcmk_new_ipc_api(api, pcmk_ipc_attrd); - - if (rc != pcmk_rc_ok) { - crm_err("Could not connect to attrd: %s", pcmk_rc_str(rc)); - } - - return rc; -} - -static void -destroy_api(pcmk_ipc_api_t *api) -{ - pcmk_disconnect_ipc(api); - pcmk_free_ipc_api(api); - api = NULL; -} - -static int connect_and_send_attrd_request(pcmk_ipc_api_t *api, const xmlNode *request) { int rc = pcmk_rc_ok; + bool created_api = false; - rc = pcmk__connect_ipc(api, pcmk_ipc_dispatch_sync, 5); - if (rc != pcmk_rc_ok) { - crm_err("Could not connect to %s: %s", - pcmk_ipc_name(api, true), pcmk_rc_str(rc)); - return rc; + if (api == NULL) { + rc = pcmk_new_ipc_api(&api, pcmk_ipc_attrd); + if (rc != pcmk_rc_ok) { + return rc; + } + created_api = true; } - rc = pcmk__send_ipc_request(api, request); - if (rc != pcmk_rc_ok) { - crm_err("Could not send request to %s: %s", - pcmk_ipc_name(api, true), pcmk_rc_str(rc)); - return rc; + rc = pcmk__connect_ipc(api, pcmk_ipc_dispatch_sync, 5); + if (rc == pcmk_rc_ok) { + rc = pcmk__send_ipc_request(api, request); } - return pcmk_rc_ok; -} - -static int -send_attrd_request(pcmk_ipc_api_t *api, const xmlNode *request) -{ - return pcmk__send_ipc_request(api, request); + if (created_api) { + pcmk_free_ipc_api(api); + } + return rc; } int @@ -212,44 +190,28 @@ pcmk__attrd_api_clear_failures(pcmk_ipc_api_t *api, const char *node, node = target; } - crm_xml_add(request, PCMK__XA_TASK, PCMK__ATTRD_CMD_CLEAR_FAILURE); - pcmk__xe_add_node(request, node, 0); - crm_xml_add(request, PCMK__XA_ATTR_RESOURCE, resource); - crm_xml_add(request, PCMK__XA_ATTR_OPERATION, operation); - crm_xml_add(request, PCMK__XA_ATTR_INTERVAL, interval_spec); - crm_xml_add_int(request, PCMK__XA_ATTR_IS_REMOTE, - pcmk_is_set(options, pcmk__node_attr_remote)); - - if (api == NULL) { - rc = create_api(&api); - if (rc != pcmk_rc_ok) { - return rc; - } - - rc = connect_and_send_attrd_request(api, request); - destroy_api(api); - - } else if (!pcmk_ipc_is_connected(api)) { - rc = connect_and_send_attrd_request(api, request); - - } else { - rc = send_attrd_request(api, request); - } - - free_xml(request); - if (operation) { - interval_desc = interval_spec? interval_spec : "nonrecurring"; + interval_desc = pcmk__s(interval_spec, "nonrecurring"); op_desc = operation; } else { interval_desc = "all"; op_desc = "operations"; } + crm_debug("Asking %s to clear failure of %s %s for %s on %s", + pcmk_ipc_name(api, true), interval_desc, op_desc, + pcmk__s(resource, "all resources"), pcmk__s(node, "all nodes")); - crm_debug("Asked pacemaker-attrd to clear failure of %s %s for %s on %s: %s (%d)", - interval_desc, op_desc, (resource? resource : "all resources"), - (node? node : "all nodes"), pcmk_rc_str(rc), rc); + crm_xml_add(request, PCMK_XA_TASK, PCMK__ATTRD_CMD_CLEAR_FAILURE); + pcmk__xe_add_node(request, node, 0); + crm_xml_add(request, PCMK__XA_ATTR_RESOURCE, resource); + crm_xml_add(request, PCMK__XA_ATTR_CLEAR_OPERATION, operation); + crm_xml_add(request, PCMK__XA_ATTR_CLEAR_INTERVAL, interval_spec); + crm_xml_add_int(request, PCMK__XA_ATTR_IS_REMOTE, + pcmk_is_set(options, pcmk__node_attr_remote)); + + rc = connect_and_send_attrd_request(api, request); + free_xml(request); return rc; } @@ -277,43 +239,30 @@ pcmk__attrd_api_delete(pcmk_ipc_api_t *api, const char *node, const char *name, } int -pcmk__attrd_api_purge(pcmk_ipc_api_t *api, const char *node) +pcmk__attrd_api_purge(pcmk_ipc_api_t *api, const char *node, bool reap) { int rc = pcmk_rc_ok; xmlNode *request = NULL; - const char *display_host = (node ? node : "localhost"); const char *target = pcmk__node_attr_target(node); if (target != NULL) { node = target; } + crm_debug("Asking %s to purge transient attributes%s for %s", + pcmk_ipc_name(api, true), + (reap? " and node cache entries" : ""), + pcmk__s(node, "local node")); + request = create_attrd_op(NULL); - crm_xml_add(request, PCMK__XA_TASK, PCMK__ATTRD_CMD_PEER_REMOVE); + crm_xml_add(request, PCMK_XA_TASK, PCMK__ATTRD_CMD_PEER_REMOVE); + pcmk__xe_set_bool_attr(request, PCMK__XA_REAP, reap); pcmk__xe_add_node(request, node, 0); - if (api == NULL) { - rc = create_api(&api); - if (rc != pcmk_rc_ok) { - return rc; - } - - rc = connect_and_send_attrd_request(api, request); - destroy_api(api); - - } else if (!pcmk_ipc_is_connected(api)) { - rc = connect_and_send_attrd_request(api, request); - - } else { - rc = send_attrd_request(api, request); - } + rc = connect_and_send_attrd_request(api, request); free_xml(request); - - crm_debug("Asked pacemaker-attrd to purge %s: %s (%d)", - display_host, pcmk_rc_str(rc), rc); - return rc; } @@ -339,23 +288,18 @@ pcmk__attrd_api_query(pcmk_ipc_api_t *api, const char *node, const char *name, } } + crm_debug("Querying %s for value of '%s'%s%s", + pcmk_ipc_name(api, true), name, + ((node == NULL)? "" : " on "), pcmk__s(node, "")); + request = create_attrd_op(NULL); crm_xml_add(request, PCMK__XA_ATTR_NAME, name); - crm_xml_add(request, PCMK__XA_TASK, PCMK__ATTRD_CMD_QUERY); + crm_xml_add(request, PCMK_XA_TASK, PCMK__ATTRD_CMD_QUERY); pcmk__xe_add_node(request, node, 0); - rc = send_attrd_request(api, request); + rc = connect_and_send_attrd_request(api, request); free_xml(request); - - if (node) { - crm_debug("Queried pacemaker-attrd for %s on %s: %s (%d)", - name, node, pcmk_rc_str(rc), rc); - } else { - crm_debug("Queried pacemaker-attrd for %s: %s (%d)", - name, pcmk_rc_str(rc), rc); - } - return rc; } @@ -364,39 +308,23 @@ pcmk__attrd_api_refresh(pcmk_ipc_api_t *api, const char *node) { int rc = pcmk_rc_ok; xmlNode *request = NULL; - const char *display_host = (node ? node : "localhost"); const char *target = pcmk__node_attr_target(node); if (target != NULL) { node = target; } + crm_debug("Asking %s to write all transient attributes for %s to CIB", + pcmk_ipc_name(api, true), pcmk__s(node, "local node")); + request = create_attrd_op(NULL); - crm_xml_add(request, PCMK__XA_TASK, PCMK__ATTRD_CMD_REFRESH); + crm_xml_add(request, PCMK_XA_TASK, PCMK__ATTRD_CMD_REFRESH); pcmk__xe_add_node(request, node, 0); - if (api == NULL) { - rc = create_api(&api); - if (rc != pcmk_rc_ok) { - return rc; - } - - rc = connect_and_send_attrd_request(api, request); - destroy_api(api); - - } else if (!pcmk_ipc_is_connected(api)) { - rc = connect_and_send_attrd_request(api, request); - - } else { - rc = send_attrd_request(api, request); - } + rc = connect_and_send_attrd_request(api, request); free_xml(request); - - crm_debug("Asked pacemaker-attrd to refresh %s: %s (%d)", - display_host, pcmk_rc_str(rc), rc); - return rc; } @@ -404,11 +332,11 @@ static void add_op_attr(xmlNode *op, uint32_t options) { if (pcmk_all_flags_set(options, pcmk__node_attr_value | pcmk__node_attr_delay)) { - crm_xml_add(op, PCMK__XA_TASK, PCMK__ATTRD_CMD_UPDATE_BOTH); + crm_xml_add(op, PCMK_XA_TASK, PCMK__ATTRD_CMD_UPDATE_BOTH); } else if (pcmk_is_set(options, pcmk__node_attr_value)) { - crm_xml_add(op, PCMK__XA_TASK, PCMK__ATTRD_CMD_UPDATE); + crm_xml_add(op, PCMK_XA_TASK, PCMK__ATTRD_CMD_UPDATE); } else if (pcmk_is_set(options, pcmk__node_attr_delay)) { - crm_xml_add(op, PCMK__XA_TASK, PCMK__ATTRD_CMD_UPDATE_DELAY); + crm_xml_add(op, PCMK_XA_TASK, PCMK__ATTRD_CMD_UPDATE_DELAY); } } @@ -417,15 +345,15 @@ populate_update_op(xmlNode *op, const char *node, const char *name, const char * const char *dampen, const char *set, uint32_t options) { if (pcmk_is_set(options, pcmk__node_attr_pattern)) { - crm_xml_add(op, PCMK__XA_ATTR_PATTERN, name); + crm_xml_add(op, PCMK__XA_ATTR_REGEX, name); } else { crm_xml_add(op, PCMK__XA_ATTR_NAME, name); } if (pcmk_is_set(options, pcmk__node_attr_utilization)) { - crm_xml_add(op, PCMK__XA_ATTR_SET_TYPE, XML_TAG_UTILIZATION); + crm_xml_add(op, PCMK__XA_ATTR_SET_TYPE, PCMK_XE_UTILIZATION); } else { - crm_xml_add(op, PCMK__XA_ATTR_SET_TYPE, XML_TAG_ATTR_SETS); + crm_xml_add(op, PCMK__XA_ATTR_SET_TYPE, PCMK_XE_INSTANCE_ATTRIBUTES); } add_op_attr(op, options); @@ -453,7 +381,6 @@ pcmk__attrd_api_update(pcmk_ipc_api_t *api, const char *node, const char *name, { int rc = pcmk_rc_ok; xmlNode *request = NULL; - const char *display_host = (node ? node : "localhost"); const char *target = NULL; if (name == NULL) { @@ -466,30 +393,16 @@ pcmk__attrd_api_update(pcmk_ipc_api_t *api, const char *node, const char *name, node = target; } + crm_debug("Asking %s to update '%s' to '%s' for %s", + pcmk_ipc_name(api, true), name, pcmk__s(value, "(null)"), + pcmk__s(node, "local node")); + request = create_attrd_op(user_name); populate_update_op(request, node, name, value, dampen, set, options); - if (api == NULL) { - rc = create_api(&api); - if (rc != pcmk_rc_ok) { - return rc; - } - - rc = connect_and_send_attrd_request(api, request); - destroy_api(api); - - } else if (!pcmk_ipc_is_connected(api)) { - rc = connect_and_send_attrd_request(api, request); - - } else { - rc = send_attrd_request(api, request); - } + rc = connect_and_send_attrd_request(api, request); free_xml(request); - - crm_debug("Asked pacemaker-attrd to update %s on %s: %s (%d)", - name, display_host, pcmk_rc_str(rc), rc); - return rc; } @@ -545,7 +458,7 @@ pcmk__attrd_api_update_list(pcmk_ipc_api_t *api, GList *attrs, const char *dampe * then we also add the task to each child node in populate_update_op * so attrd_client_update knows what form of update is taking place. */ - child = create_xml_node(request, XML_ATTR_OP); + child = pcmk__xe_create(request, PCMK_XE_OP); target = pcmk__node_attr_target(pair->node); if (target != NULL) { @@ -564,23 +477,8 @@ pcmk__attrd_api_update_list(pcmk_ipc_api_t *api, GList *attrs, const char *dampe * request. Do that now, creating and destroying the API object if needed. */ if (pcmk__is_daemon) { - bool created_api = false; - - if (api == NULL) { - rc = create_api(&api); - if (rc != pcmk_rc_ok) { - return rc; - } - - created_api = true; - } - rc = connect_and_send_attrd_request(api, request); free_xml(request); - - if (created_api) { - destroy_api(api); - } } return rc; diff --git a/lib/common/ipc_client.c b/lib/common/ipc_client.c index 0d38650..37a0982 100644 --- a/lib/common/ipc_client.c +++ b/lib/common/ipc_client.c @@ -1,5 +1,5 @@ /* - * 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. * @@ -26,7 +26,7 @@ #include <bzlib.h> #include <crm/crm.h> /* indirectly: pcmk_err_generic */ -#include <crm/msg_xml.h> +#include <crm/common/xml.h> #include <crm/common/ipc.h> #include <crm/common/ipc_internal.h> #include "crmcommon_private.h" @@ -251,7 +251,7 @@ pcmk_ipc_name(const pcmk_ipc_api_t *api, bool for_log) } switch (api->server) { case pcmk_ipc_attrd: - return for_log? "attribute manager" : T_ATTRD; + return for_log? "attribute manager" : PCMK__VALUE_ATTRD; case pcmk_ipc_based: return for_log? "CIB manager" : NULL /* PCMK__SERVER_BASED_RW */; @@ -338,7 +338,7 @@ dispatch_ipc_data(const char *buffer, pcmk_ipc_api_t *api) return ENOMSG; } - msg = string2xml(buffer); + msg = pcmk__xml_parse(buffer); if (msg == NULL) { crm_warn("Malformed message received from %s IPC", pcmk_ipc_name(api, true)); @@ -755,10 +755,11 @@ create_purge_node_request(const pcmk_ipc_api_t *api, const char *node_name, switch (api->server) { case pcmk_ipc_attrd: - request = create_xml_node(NULL, __func__); - crm_xml_add(request, F_TYPE, T_ATTRD); - crm_xml_add(request, F_ORIG, crm_system_name); - crm_xml_add(request, PCMK__XA_TASK, PCMK__ATTRD_CMD_PEER_REMOVE); + request = pcmk__xe_create(NULL, __func__); + crm_xml_add(request, PCMK__XA_T, PCMK__VALUE_ATTRD); + crm_xml_add(request, PCMK__XA_SRC, crm_system_name); + crm_xml_add(request, PCMK_XA_TASK, PCMK__ATTRD_CMD_PEER_REMOVE); + pcmk__xe_set_bool_attr(request, PCMK__XA_REAP, true); pcmk__xe_add_node(request, node_name, nodeid); break; @@ -770,7 +771,7 @@ create_purge_node_request(const pcmk_ipc_api_t *api, const char *node_name, if (nodeid > 0) { crm_xml_set_id(request, "%lu", (unsigned long) nodeid); } - crm_xml_add(request, XML_ATTR_UNAME, node_name); + crm_xml_add(request, PCMK_XA_UNAME, node_name); break; case pcmk_ipc_based: @@ -1122,7 +1123,7 @@ crm_ipc_decompress(crm_ipc_t * client) unsigned int size_u = 1 + header->size_uncompressed; /* never let buf size fall below our max size required for ipc reads. */ unsigned int new_buf_size = QB_MAX((sizeof(pcmk__ipc_header_t) + size_u), client->max_buf_size); - char *uncompressed = calloc(1, new_buf_size); + char *uncompressed = pcmk__assert_alloc(1, new_buf_size); crm_trace("Decompressing message data %u bytes into %u bytes", header->size_compressed, size_u); @@ -1264,13 +1265,13 @@ internal_ipc_get_reply(crm_ipc_t *client, int request_id, int ms_timeout, /* Got it */ break; } else if (hdr->qb.id < request_id) { - xmlNode *bad = string2xml(crm_ipc_buffer(client)); + xmlNode *bad = pcmk__xml_parse(crm_ipc_buffer(client)); crm_err("Discarding old reply %d (need %d)", hdr->qb.id, request_id); crm_log_xml_notice(bad, "OldIpcReply"); } else { - xmlNode *bad = string2xml(crm_ipc_buffer(client)); + xmlNode *bad = pcmk__xml_parse(crm_ipc_buffer(client)); crm_err("Discarding newer reply %d (need %d)", hdr->qb.id, request_id); crm_log_xml_notice(bad, "ImpossibleReply"); @@ -1429,7 +1430,7 @@ crm_ipc_send(crm_ipc_t *client, const xmlNode *message, crm_ipc_buffer(client)); if (reply) { - *reply = string2xml(crm_ipc_buffer(client)); + *reply = pcmk__xml_parse(crm_ipc_buffer(client)); } } else { @@ -1622,13 +1623,17 @@ pcmk__ipc_is_authentic_process_active(const char *name, uid_t refuid, do { poll_rc = poll(&pollfd, 1, 2000); } while ((poll_rc == -1) && (errno == EINTR)); - if ((poll_rc <= 0) || (qb_ipcc_connect_continue(c) != 0)) { + + /* If poll() failed, given that disconnect function is not registered yet, + * qb_ipcc_disconnect() won't clean up the socket. In any case, call + * qb_ipcc_connect_continue() here so that it may fail and do the cleanup + * for us. + */ + if (qb_ipcc_connect_continue(c) != 0) { crm_info("Could not connect to %s IPC: %s", name, (poll_rc == 0)?"timeout":strerror(errno)); rc = pcmk_rc_ipc_unresponsive; - if (poll_rc > 0) { - c = NULL; // qb_ipcc_connect_continue cleaned up for us - } + c = NULL; // qb_ipcc_connect_continue cleaned up for us goto bail; } #endif diff --git a/lib/common/ipc_common.c b/lib/common/ipc_common.c index a48b0e9..5bea139 100644 --- a/lib/common/ipc_common.c +++ b/lib/common/ipc_common.c @@ -1,5 +1,5 @@ /* - * Copyright 2004-2021 the Pacemaker project contributors + * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -13,7 +13,7 @@ #include <stdint.h> // uint64_t #include <sys/types.h> -#include <crm/msg_xml.h> +#include <crm/common/xml.h> #include "crmcommon_private.h" #define MIN_MSG_SIZE 12336 // sizeof(struct qb_ipc_connection_response) diff --git a/lib/common/ipc_controld.c b/lib/common/ipc_controld.c index 8e2016e..755c034 100644 --- a/lib/common/ipc_controld.c +++ b/lib/common/ipc_controld.c @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the Pacemaker project contributors + * Copyright 2020-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -15,7 +15,6 @@ #include <libxml/tree.h> #include <crm/crm.h> -#include <crm/msg_xml.h> #include <crm/common/xml.h> #include <crm/common/ipc.h> #include <crm/common/ipc_internal.h> @@ -70,8 +69,8 @@ new_data(pcmk_ipc_api_t *api) /* This is set to the PID because that's how it was always done, but PIDs * are not unique because clients can be remote. The value appears to be - * unused other than as part of F_CRM_SYS_FROM in IPC requests, which is - * only compared against the internal system names (CRM_SYSTEM_TENGINE, + * unused other than as part of PCMK__XA_CRM_SYS_FROM in IPC requests, which + * is only compared against the internal system names (CRM_SYSTEM_TENGINE, * etc.), so it shouldn't be a problem. */ private->client_uuid = pcmk__getpid_s(); @@ -123,19 +122,21 @@ set_node_info_data(pcmk_controld_api_reply_t *data, xmlNode *msg_data) if (msg_data == NULL) { return; } - data->data.node_info.have_quorum = pcmk__xe_attr_is_true(msg_data, XML_ATTR_HAVE_QUORUM); - data->data.node_info.is_remote = pcmk__xe_attr_is_true(msg_data, XML_NODE_IS_REMOTE); + data->data.node_info.have_quorum = + pcmk__xe_attr_is_true(msg_data, PCMK_XA_HAVE_QUORUM); + data->data.node_info.is_remote = + pcmk__xe_attr_is_true(msg_data, PCMK_XA_REMOTE_NODE); /* Integer node_info.id is currently valid only for Corosync nodes. * * @TODO: Improve handling after crm_node_t is refactored to handle layer- * specific data better. */ - crm_element_value_int(msg_data, XML_ATTR_ID, &(data->data.node_info.id)); + crm_element_value_int(msg_data, PCMK_XA_ID, &(data->data.node_info.id)); - data->data.node_info.uuid = crm_element_value(msg_data, XML_ATTR_ID); - data->data.node_info.uname = crm_element_value(msg_data, XML_ATTR_UNAME); - data->data.node_info.state = crm_element_value(msg_data, PCMK__XA_CRMD); + data->data.node_info.uuid = crm_element_value(msg_data, PCMK_XA_ID); + data->data.node_info.uname = crm_element_value(msg_data, PCMK_XA_UNAME); + data->data.node_info.state = crm_element_value(msg_data, PCMK_XA_CRMD); } static void @@ -146,10 +147,10 @@ set_ping_data(pcmk_controld_api_reply_t *data, xmlNode *msg_data) return; } data->data.ping.sys_from = crm_element_value(msg_data, - XML_PING_ATTR_SYSFROM); + PCMK__XA_CRM_SUBSYSTEM); data->data.ping.fsa_state = crm_element_value(msg_data, - XML_PING_ATTR_CRMDSTATE); - data->data.ping.result = crm_element_value(msg_data, XML_PING_ATTR_STATUS); + PCMK__XA_CRMD_STATE); + data->data.ping.result = crm_element_value(msg_data, PCMK_XA_RESULT); } static void @@ -158,17 +159,18 @@ set_nodes_data(pcmk_controld_api_reply_t *data, xmlNode *msg_data) pcmk_controld_api_node_t *node_info; data->reply_type = pcmk_controld_reply_nodes; - for (xmlNode *node = first_named_child(msg_data, XML_CIB_TAG_NODE); - node != NULL; node = crm_next_same_xml(node)) { + for (xmlNode *node = pcmk__xe_first_child(msg_data, PCMK_XE_NODE, NULL, + NULL); + node != NULL; node = pcmk__xe_next_same(node)) { long long id_ll = 0; - node_info = calloc(1, sizeof(pcmk_controld_api_node_t)); - crm_element_value_ll(node, XML_ATTR_ID, &id_ll); + node_info = pcmk__assert_alloc(1, sizeof(pcmk_controld_api_node_t)); + crm_element_value_ll(node, PCMK_XA_ID, &id_ll); if (id_ll > 0) { node_info->id = id_ll; } - node_info->uname = crm_element_value(node, XML_ATTR_UNAME); + node_info->uname = crm_element_value(node, PCMK_XA_UNAME); node_info->state = crm_element_value(node, PCMK__XA_IN_CCM); data->data.nodes = g_list_prepend(data->data.nodes, node_info); } @@ -178,7 +180,7 @@ static bool reply_expected(pcmk_ipc_api_t *api, const xmlNode *request) { // We only need to handle commands that API functions can send - return pcmk__str_any_of(crm_element_value(request, F_CRM_TASK), + return pcmk__str_any_of(crm_element_value(request, PCMK__XA_CRM_TASK), PCMK__CONTROLD_CMD_NODES, CRM_OP_LRM_DELETE, CRM_OP_LRM_FAIL, @@ -194,13 +196,14 @@ dispatch(pcmk_ipc_api_t *api, xmlNode *reply) { struct controld_api_private_s *private = api->api_data; crm_exit_t status = CRM_EX_OK; + xmlNode *wrapper = NULL; xmlNode *msg_data = NULL; const char *value = NULL; pcmk_controld_api_reply_t reply_data = { pcmk_controld_reply_unknown, NULL, NULL, }; - if (pcmk__xe_is(reply, "ack")) { + if (pcmk__xe_is(reply, PCMK__XE_ACK)) { /* ACKs are trivial responses that do not count toward expected replies, * and do not have all the fields that validation requires, so skip that * processing. @@ -219,22 +222,22 @@ dispatch(pcmk_ipc_api_t *api, xmlNode *reply) * if we fix the controller, we'll still need to handle replies from * old versions (feature set could be used to differentiate). */ - value = crm_element_value(reply, F_CRM_MSG_TYPE); - if (pcmk__str_empty(value) - || !pcmk__str_any_of(value, XML_ATTR_REQUEST, XML_ATTR_RESPONSE, NULL)) { + value = crm_element_value(reply, PCMK__XA_SUBT); + if (!pcmk__str_any_of(value, PCMK__VALUE_REQUEST, PCMK__VALUE_RESPONSE, + NULL)) { crm_info("Unrecognizable message from controller: " "invalid message type '%s'", pcmk__s(value, "")); status = CRM_EX_PROTOCOL; goto done; } - if (pcmk__str_empty(crm_element_value(reply, XML_ATTR_REFERENCE))) { + if (pcmk__str_empty(crm_element_value(reply, PCMK_XA_REFERENCE))) { crm_info("Unrecognizable message from controller: no reference"); status = CRM_EX_PROTOCOL; goto done; } - value = crm_element_value(reply, F_CRM_TASK); + value = crm_element_value(reply, PCMK__XA_CRM_TASK); if (pcmk__str_empty(value)) { crm_info("Unrecognizable message from controller: no command name"); status = CRM_EX_PROTOCOL; @@ -243,9 +246,11 @@ dispatch(pcmk_ipc_api_t *api, xmlNode *reply) // Parse useful info from reply - reply_data.feature_set = crm_element_value(reply, XML_ATTR_VERSION); - reply_data.host_from = crm_element_value(reply, F_CRM_HOST_FROM); - msg_data = get_message_xml(reply, F_CRM_DATA); + reply_data.feature_set = crm_element_value(reply, PCMK_XA_VERSION); + reply_data.host_from = crm_element_value(reply, PCMK__XA_SRC); + + wrapper = pcmk__xe_first_child(reply, PCMK__XE_CRM_XML, NULL, NULL); + msg_data = pcmk__xe_first_child(wrapper, NULL, NULL, NULL); if (!strcmp(value, CRM_OP_REPROBE)) { reply_data.reply_type = pcmk_controld_reply_reprobe; @@ -332,7 +337,7 @@ static int send_controller_request(pcmk_ipc_api_t *api, const xmlNode *request, bool reply_is_expected) { - if (crm_element_value(request, XML_ATTR_REFERENCE) == NULL) { + if (crm_element_value(request, PCMK_XA_REFERENCE) == NULL) { return EINVAL; } if (reply_is_expected) { @@ -348,10 +353,10 @@ create_reprobe_message_data(const char *target_node, const char *router_node) { xmlNode *msg_data; - msg_data = create_xml_node(NULL, "data_for_" CRM_OP_REPROBE); - crm_xml_add(msg_data, XML_LRM_ATTR_TARGET, target_node); + msg_data = pcmk__xe_create(NULL, "data_for_" CRM_OP_REPROBE); + crm_xml_add(msg_data, PCMK__META_ON_NODE, target_node); if ((router_node != NULL) && !pcmk__str_eq(router_node, target_node, pcmk__str_casei)) { - crm_xml_add(msg_data, XML_LRM_ATTR_ROUTER_NODE, router_node); + crm_xml_add(msg_data, PCMK__XA_ROUTER_NODE, router_node); } return msg_data; } @@ -486,7 +491,7 @@ controller_resource_op(pcmk_ipc_api_t *api, const char *op, router_node = target_node; } - msg_data = create_xml_node(NULL, XML_GRAPH_TAG_RSC_OP); + msg_data = pcmk__xe_create(NULL, PCMK__XE_RSC_OP); /* The controller logs the transition key from resource op requests, so we * need to have *something* for it. @@ -494,31 +499,31 @@ controller_resource_op(pcmk_ipc_api_t *api, const char *op, */ key = pcmk__transition_key(0, getpid(), 0, "xxxxxxxx-xrsc-opxx-xcrm-resourcexxxx"); - crm_xml_add(msg_data, XML_ATTR_TRANSITION_KEY, key); + crm_xml_add(msg_data, PCMK__XA_TRANSITION_KEY, key); free(key); - crm_xml_add(msg_data, XML_LRM_ATTR_TARGET, target_node); + crm_xml_add(msg_data, PCMK__META_ON_NODE, target_node); if (!pcmk__str_eq(router_node, target_node, pcmk__str_casei)) { - crm_xml_add(msg_data, XML_LRM_ATTR_ROUTER_NODE, router_node); + crm_xml_add(msg_data, PCMK__XA_ROUTER_NODE, router_node); } if (cib_only) { // Indicate that only the CIB needs to be cleaned - crm_xml_add(msg_data, PCMK__XA_MODE, XML_TAG_CIB); + crm_xml_add(msg_data, PCMK__XA_MODE, PCMK__VALUE_CIB); } - xml_rsc = create_xml_node(msg_data, XML_CIB_TAG_RESOURCE); - crm_xml_add(xml_rsc, XML_ATTR_ID, rsc_id); - crm_xml_add(xml_rsc, XML_ATTR_ID_LONG, rsc_long_id); - crm_xml_add(xml_rsc, XML_AGENT_ATTR_CLASS, standard); - crm_xml_add(xml_rsc, XML_AGENT_ATTR_PROVIDER, provider); - crm_xml_add(xml_rsc, XML_ATTR_TYPE, type); + xml_rsc = pcmk__xe_create(msg_data, PCMK_XE_PRIMITIVE); + crm_xml_add(xml_rsc, PCMK_XA_ID, rsc_id); + crm_xml_add(xml_rsc, PCMK__XA_LONG_ID, rsc_long_id); + crm_xml_add(xml_rsc, PCMK_XA_CLASS, standard); + crm_xml_add(xml_rsc, PCMK_XA_PROVIDER, provider); + crm_xml_add(xml_rsc, PCMK_XA_TYPE, type); - params = create_xml_node(msg_data, XML_TAG_ATTRS); - crm_xml_add(params, XML_ATTR_CRM_VERSION, CRM_FEATURE_SET); + params = pcmk__xe_create(msg_data, PCMK__XE_ATTRIBUTES); + crm_xml_add(params, PCMK_XA_CRM_FEATURE_SET, CRM_FEATURE_SET); // The controller parses the timeout from the request - key = crm_meta_name(XML_ATTR_TIMEOUT); + key = crm_meta_name(PCMK_META_TIMEOUT); crm_xml_add(params, key, "60000"); /* 1 minute */ //@TODO pass as arg free(key); @@ -631,17 +636,13 @@ create_hello_message(const char *uuid, const char *client_name, return NULL; } - hello_node = create_xml_node(NULL, XML_TAG_OPTIONS); - if (hello_node == NULL) { - crm_err("Could not create IPC hello message from %s (UUID %s): " - "Message data creation failed", client_name, uuid); - return NULL; - } + hello_node = pcmk__xe_create(NULL, PCMK__XE_OPTIONS); + crm_xml_add(hello_node, PCMK__XA_MAJOR_VERSION, major_version); + crm_xml_add(hello_node, PCMK__XA_MINOR_VERSION, minor_version); + crm_xml_add(hello_node, PCMK__XA_CLIENT_NAME, client_name); - crm_xml_add(hello_node, "major_version", major_version); - crm_xml_add(hello_node, "minor_version", minor_version); - crm_xml_add(hello_node, "client_name", client_name); - crm_xml_add(hello_node, "client_uuid", uuid); + // @TODO Nothing uses this. Drop, or keep for debugging? + crm_xml_add(hello_node, PCMK__XA_CLIENT_UUID, uuid); hello = create_request(CRM_OP_HELLO, hello_node, NULL, NULL, client_name, uuid); if (hello == NULL) { diff --git a/lib/common/ipc_pacemakerd.c b/lib/common/ipc_pacemakerd.c index 2f03709..7557491 100644 --- a/lib/common/ipc_pacemakerd.c +++ b/lib/common/ipc_pacemakerd.c @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the Pacemaker project contributors + * Copyright 2020-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -13,7 +13,6 @@ #include <time.h> #include <crm/crm.h> -#include <crm/msg_xml.h> #include <crm/common/xml.h> #include <crm/common/ipc.h> #include <crm/common/ipc_internal.h> @@ -26,13 +25,13 @@ typedef struct pacemakerd_api_private_s { } pacemakerd_api_private_t; static const char *pacemakerd_state_str[] = { - XML_PING_ATTR_PACEMAKERDSTATE_INIT, - XML_PING_ATTR_PACEMAKERDSTATE_STARTINGDAEMONS, - XML_PING_ATTR_PACEMAKERDSTATE_WAITPING, - XML_PING_ATTR_PACEMAKERDSTATE_RUNNING, - XML_PING_ATTR_PACEMAKERDSTATE_SHUTTINGDOWN, - XML_PING_ATTR_PACEMAKERDSTATE_SHUTDOWNCOMPLETE, - XML_PING_ATTR_PACEMAKERDSTATE_REMOTE, + PCMK__VALUE_INIT, + PCMK__VALUE_STARTING_DAEMONS, + PCMK__VALUE_WAIT_FOR_PING, + PCMK__VALUE_RUNNING, + PCMK__VALUE_SHUTTING_DOWN, + PCMK__VALUE_SHUTDOWN_COMPLETE, + PCMK_VALUE_REMOTE, }; enum pcmk_pacemakerd_state @@ -180,7 +179,7 @@ post_disconnect(pcmk_ipc_api_t *api) static bool reply_expected(pcmk_ipc_api_t *api, const xmlNode *request) { - const char *command = crm_element_value(request, F_CRM_TASK); + const char *command = crm_element_value(request, PCMK__XA_CRM_TASK); if (command == NULL) { return false; @@ -194,6 +193,7 @@ static bool dispatch(pcmk_ipc_api_t *api, xmlNode *reply) { crm_exit_t status = CRM_EX_OK; + xmlNode *wrapper = NULL; xmlNode *msg_data = NULL; pcmk_pacemakerd_api_reply_t reply_data = { pcmk_pacemakerd_reply_unknown @@ -201,51 +201,56 @@ dispatch(pcmk_ipc_api_t *api, xmlNode *reply) const char *value = NULL; long long value_ll = 0; - if (pcmk__str_eq((const char *) reply->name, "ack", pcmk__str_none)) { + if (pcmk__xe_is(reply, PCMK__XE_ACK)) { long long int ack_status = 0; - pcmk__scan_ll(crm_element_value(reply, "status"), &ack_status, CRM_EX_OK); + pcmk__scan_ll(crm_element_value(reply, PCMK_XA_STATUS), &ack_status, + CRM_EX_OK); return ack_status == CRM_EX_INDETERMINATE; } - value = crm_element_value(reply, F_CRM_MSG_TYPE); - if (pcmk__str_empty(value) - || !pcmk__str_eq(value, XML_ATTR_RESPONSE, pcmk__str_none)) { - crm_info("Unrecognizable message from pacemakerd: " - "message type '%s' not '" XML_ATTR_RESPONSE "'", - pcmk__s(value, "")); + value = crm_element_value(reply, PCMK__XA_SUBT); + if (!pcmk__str_eq(value, PCMK__VALUE_RESPONSE, pcmk__str_none)) { + crm_info("Unrecognizable message from %s: " + "message type '%s' not '" PCMK__VALUE_RESPONSE "'", + pcmk_ipc_name(api, true), pcmk__s(value, "")); status = CRM_EX_PROTOCOL; goto done; } - if (pcmk__str_empty(crm_element_value(reply, XML_ATTR_REFERENCE))) { - crm_info("Unrecognizable message from pacemakerd: no reference"); + if (pcmk__str_empty(crm_element_value(reply, PCMK_XA_REFERENCE))) { + crm_info("Unrecognizable message from %s: no reference", + pcmk_ipc_name(api, true)); status = CRM_EX_PROTOCOL; goto done; } - value = crm_element_value(reply, F_CRM_TASK); + value = crm_element_value(reply, PCMK__XA_CRM_TASK); // Parse useful info from reply - msg_data = get_message_xml(reply, F_CRM_DATA); - crm_element_value_ll(msg_data, XML_ATTR_TSTAMP, &value_ll); + wrapper = pcmk__xe_first_child(reply, PCMK__XE_CRM_XML, NULL, NULL); + msg_data = pcmk__xe_first_child(wrapper, NULL, NULL, NULL); + + crm_element_value_ll(msg_data, PCMK_XA_CRM_TIMESTAMP, &value_ll); if (pcmk__str_eq(value, CRM_OP_PING, pcmk__str_none)) { reply_data.reply_type = pcmk_pacemakerd_reply_ping; reply_data.data.ping.state = pcmk_pacemakerd_api_daemon_state_text2enum( - crm_element_value(msg_data, XML_PING_ATTR_PACEMAKERDSTATE)); + crm_element_value(msg_data, PCMK__XA_PACEMAKERD_STATE)); reply_data.data.ping.status = - pcmk__str_eq(crm_element_value(msg_data, XML_PING_ATTR_STATUS), "ok", + pcmk__str_eq(crm_element_value(msg_data, PCMK_XA_RESULT), "ok", pcmk__str_casei)?pcmk_rc_ok:pcmk_rc_error; reply_data.data.ping.last_good = (value_ll < 0)? 0 : (time_t) value_ll; - reply_data.data.ping.sys_from = crm_element_value(msg_data, - XML_PING_ATTR_SYSFROM); + reply_data.data.ping.sys_from = + crm_element_value(msg_data, PCMK__XA_CRM_SUBSYSTEM); } else if (pcmk__str_eq(value, CRM_OP_QUIT, pcmk__str_none)) { + const char *op_status = crm_element_value(msg_data, PCMK__XA_OP_STATUS); + reply_data.reply_type = pcmk_pacemakerd_reply_shutdown; - reply_data.data.shutdown.status = atoi(crm_element_value(msg_data, XML_LRM_ATTR_OPSTATUS)); + reply_data.data.shutdown.status = atoi(op_status); } else { - crm_info("Unrecognizable message from pacemakerd: " - "unknown command '%s'", pcmk__s(value, "")); + crm_info("Unrecognizable message from %s: unknown command '%s'", + pcmk_ipc_name(api, true), pcmk__s(value, "")); status = CRM_EX_PROTOCOL; goto done; } @@ -292,8 +297,8 @@ do_pacemakerd_api_call(pcmk_ipc_api_t *api, const char *ipc_name, const char *ta if (cmd) { rc = pcmk__send_ipc_request(api, cmd); if (rc != pcmk_rc_ok) { - crm_debug("Couldn't send request to pacemakerd: %s rc=%d", - pcmk_rc_str(rc), rc); + crm_debug("Couldn't send request to %s: %s rc=%d", + pcmk_ipc_name(api, true), pcmk_rc_str(rc), rc); } free_xml(cmd); } else { diff --git a/lib/common/ipc_schedulerd.c b/lib/common/ipc_schedulerd.c index cf788e5..45c5803 100644 --- a/lib/common/ipc_schedulerd.c +++ b/lib/common/ipc_schedulerd.c @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the Pacemaker project contributors + * Copyright 2021-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -13,7 +13,6 @@ #include <time.h> #include <crm/crm.h> -#include <crm/msg_xml.h> #include <crm/common/xml.h> #include <crm/common/ipc.h> #include <crm/common/ipc_internal.h> @@ -64,7 +63,7 @@ post_connect(pcmk_ipc_api_t *api) static bool reply_expected(pcmk_ipc_api_t *api, const xmlNode *request) { - const char *command = crm_element_value(request, F_CRM_TASK); + const char *command = crm_element_value(request, PCMK__XA_CRM_TASK); if (command == NULL) { return false; @@ -78,39 +77,44 @@ static bool dispatch(pcmk_ipc_api_t *api, xmlNode *reply) { crm_exit_t status = CRM_EX_OK; + xmlNode *wrapper = NULL; xmlNode *msg_data = NULL; pcmk_schedulerd_api_reply_t reply_data = { pcmk_schedulerd_reply_unknown }; const char *value = NULL; - if (pcmk__str_eq((const char *) reply->name, "ack", pcmk__str_casei)) { + if (pcmk__xe_is(reply, PCMK__XE_ACK)) { return false; } - value = crm_element_value(reply, F_CRM_MSG_TYPE); - if (!pcmk__str_eq(value, XML_ATTR_RESPONSE, pcmk__str_none)) { + value = crm_element_value(reply, PCMK__XA_SUBT); + if (!pcmk__str_eq(value, PCMK__VALUE_RESPONSE, pcmk__str_none)) { crm_info("Unrecognizable message from schedulerd: " - "message type '%s' not '" XML_ATTR_RESPONSE "'", + "message type '%s' not '" PCMK__VALUE_RESPONSE "'", pcmk__s(value, "")); status = CRM_EX_PROTOCOL; goto done; } - if (pcmk__str_empty(crm_element_value(reply, XML_ATTR_REFERENCE))) { + if (pcmk__str_empty(crm_element_value(reply, PCMK_XA_REFERENCE))) { crm_info("Unrecognizable message from schedulerd: no reference"); status = CRM_EX_PROTOCOL; goto done; } // Parse useful info from reply - msg_data = get_message_xml(reply, F_CRM_DATA); - value = crm_element_value(reply, F_CRM_TASK); + wrapper = pcmk__xe_first_child(reply, PCMK__XE_CRM_XML, NULL, NULL); + msg_data = pcmk__xe_first_child(wrapper, NULL, NULL, NULL); + + value = crm_element_value(reply, PCMK__XA_CRM_TASK); if (pcmk__str_eq(value, CRM_OP_PECALC, pcmk__str_none)) { reply_data.reply_type = pcmk_schedulerd_reply_graph; - reply_data.data.graph.reference = crm_element_value(reply, XML_ATTR_REFERENCE); - reply_data.data.graph.input = crm_element_value(reply, F_CRM_TGRAPH_INPUT); + reply_data.data.graph.reference = crm_element_value(reply, + PCMK_XA_REFERENCE); + reply_data.data.graph.input = crm_element_value(reply, + PCMK__XA_CRM_TGRAPH_IN); reply_data.data.graph.tgraph = msg_data; } else { crm_info("Unrecognizable message from schedulerd: " @@ -164,7 +168,7 @@ do_schedulerd_api_call(pcmk_ipc_api_t *api, const char *task, xmlNode *cib, char pcmk_rc_str(rc), rc); } - *ref = strdup(crm_element_value(cmd, F_CRM_REFERENCE)); + *ref = strdup(crm_element_value(cmd, PCMK_XA_REFERENCE)); free_xml(cmd); } else { rc = ENOMSG; diff --git a/lib/common/ipc_server.c b/lib/common/ipc_server.c index 5cd7e70..a9201b9 100644 --- a/lib/common/ipc_server.c +++ b/lib/common/ipc_server.c @@ -1,5 +1,5 @@ /* - * 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. * @@ -16,7 +16,7 @@ #include <sys/types.h> #include <crm/crm.h> -#include <crm/msg_xml.h> +#include <crm/common/xml.h> #include <crm/common/ipc.h> #include <crm/common/ipc_internal.h> #include "crmcommon_private.h" @@ -158,23 +158,17 @@ pcmk__drop_all_clients(qb_ipcs_service_t *service) * \param[in] key Connection table key (NULL to use sane default) * \param[in] uid_client UID corresponding to c (ignored if c is NULL) * - * \return Pointer to new pcmk__client_t (or NULL on error) + * \return Pointer to new pcmk__client_t (guaranteed not to be \c NULL) */ static pcmk__client_t * client_from_connection(qb_ipcs_connection_t *c, void *key, uid_t uid_client) { - pcmk__client_t *client = calloc(1, sizeof(pcmk__client_t)); - - if (client == NULL) { - crm_perror(LOG_ERR, "Allocating client"); - return NULL; - } + pcmk__client_t *client = pcmk__assert_alloc(1, sizeof(pcmk__client_t)); if (c) { client->user = pcmk__uid2username(uid_client); if (client->user == NULL) { - client->user = strdup("#unprivileged"); - CRM_CHECK(client->user != NULL, free(client); return NULL); + client->user = pcmk__str_copy("#unprivileged"); crm_err("Unable to enforce ACLs for user ID %d, assuming unprivileged", uid_client); } @@ -208,10 +202,7 @@ client_from_connection(qb_ipcs_connection_t *c, void *key, uid_t uid_client) pcmk__client_t * pcmk__new_unauth_client(void *key) { - pcmk__client_t *client = client_from_connection(NULL, key, 0); - - CRM_ASSERT(client != NULL); - return client; + return client_from_connection(NULL, key, 0); } pcmk__client_t * @@ -242,9 +233,6 @@ pcmk__new_client(qb_ipcs_connection_t *c, uid_t uid_client, gid_t gid_client) /* TODO: Do our own auth checking, return NULL if unauthorized */ client = client_from_connection(c, NULL, uid_client); - if (client == NULL) { - return NULL; - } if ((uid_client == 0) || (uid_client == uid_cluster)) { /* Remember when a connection came from root or hacluster */ @@ -259,10 +247,7 @@ pcmk__new_client(qb_ipcs_connection_t *c, uid_t uid_client, gid_t gid_client) static struct iovec * pcmk__new_ipc_event(void) { - struct iovec *iov = calloc(2, sizeof(struct iovec)); - - CRM_ASSERT(iov != NULL); - return iov; + return (struct iovec *) pcmk__assert_alloc(2, sizeof(struct iovec)); } /*! @@ -331,6 +316,14 @@ pcmk__free_client(pcmk__client_t *c) if (c->remote->auth_timeout) { g_source_remove(c->remote->auth_timeout); } +#ifdef HAVE_GNUTLS_GNUTLS_H + if (c->remote->tls_session != NULL) { + /* @TODO Reduce duplication at callers. Put here everything + * necessary to tear down and free tls_session. + */ + gnutls_free(c->remote->tls_session); + } +#endif // HAVE_GNUTLS_GNUTLS_H free(c->remote->buffer); free(c->remote); } @@ -413,7 +406,7 @@ pcmk__client_data2xml(pcmk__client_t *c, void *data, uint32_t *id, if (header->size_compressed) { int rc = 0; unsigned int size_u = 1 + header->size_uncompressed; - uncompressed = calloc(1, size_u); + uncompressed = pcmk__assert_alloc(1, size_u); crm_trace("Decompressing message data %u bytes into %u bytes", header->size_compressed, size_u); @@ -433,7 +426,7 @@ pcmk__client_data2xml(pcmk__client_t *c, void *data, uint32_t *id, CRM_ASSERT(text[header->size_uncompressed - 1] == 0); - xml = string2xml(text); + xml = pcmk__xml_parse(text); crm_log_xml_trace(xml, "[IPC received]"); free(uncompressed); @@ -583,23 +576,25 @@ pcmk__ipc_prepare_iov(uint32_t request, const xmlNode *message, uint32_t max_send_size, struct iovec **result, ssize_t *bytes) { - static unsigned int biggest = 0; struct iovec *iov; unsigned int total = 0; - char *compressed = NULL; - char *buffer = NULL; + GString *buffer = NULL; pcmk__ipc_header_t *header = NULL; + int rc = pcmk_rc_ok; if ((message == NULL) || (result == NULL)) { - return EINVAL; + rc = EINVAL; + goto done; } header = calloc(1, sizeof(pcmk__ipc_header_t)); if (header == NULL) { - return ENOMEM; /* errno mightn't be set by allocator */ + rc = ENOMEM; + goto done; } - buffer = dump_xml_unformatted(message); + buffer = g_string_sized_new(1024); + pcmk__xml_string(message, 0, buffer, 0); if (max_send_size == 0) { max_send_size = crm_ipc_default_buffer_size(); @@ -612,17 +607,21 @@ pcmk__ipc_prepare_iov(uint32_t request, const xmlNode *message, iov[0].iov_base = header; header->version = PCMK__IPC_VERSION; - header->size_uncompressed = 1 + strlen(buffer); + header->size_uncompressed = 1 + buffer->len; total = iov[0].iov_len + header->size_uncompressed; if (total < max_send_size) { - iov[1].iov_base = buffer; + iov[1].iov_base = pcmk__str_copy(buffer->str); iov[1].iov_len = header->size_uncompressed; } else { + static unsigned int biggest = 0; + + char *compressed = NULL; unsigned int new_size = 0; - if (pcmk__compress(buffer, (unsigned int) header->size_uncompressed, + if (pcmk__compress(buffer->str, + (unsigned int) header->size_uncompressed, (unsigned int) max_send_size, &compressed, &new_size) == pcmk_rc_ok) { @@ -632,8 +631,6 @@ pcmk__ipc_prepare_iov(uint32_t request, const xmlNode *message, iov[1].iov_len = header->size_compressed; iov[1].iov_base = compressed; - free(buffer); - biggest = QB_MAX(header->size_compressed, biggest); } else { @@ -646,9 +643,9 @@ pcmk__ipc_prepare_iov(uint32_t request, const xmlNode *message, header->size_uncompressed, max_send_size, 4 * biggest); free(compressed); - free(buffer); pcmk_free_ipc_event(iov); - return EMSGSIZE; + rc = EMSGSIZE; + goto done; } } @@ -660,7 +657,12 @@ pcmk__ipc_prepare_iov(uint32_t request, const xmlNode *message, if (bytes != NULL) { *bytes = header->qb.size; } - return pcmk_rc_ok; + +done: + if (buffer != NULL) { + g_string_free(buffer, TRUE); + } + return rc; } int @@ -786,10 +788,10 @@ pcmk__ipc_create_ack_as(const char *function, int line, uint32_t flags, xmlNode *ack = NULL; if (pcmk_is_set(flags, crm_ipc_client_response)) { - ack = create_xml_node(NULL, tag); - crm_xml_add(ack, "function", function); - crm_xml_add_int(ack, "line", line); - crm_xml_add_int(ack, "status", (int) status); + ack = pcmk__xe_create(NULL, tag); + crm_xml_add(ack, PCMK_XA_FUNCTION, function); + crm_xml_add_int(ack, PCMK__XA_LINE, line); + crm_xml_add_int(ack, PCMK_XA_STATUS, (int) status); crm_xml_add(ack, PCMK__XA_IPC_PROTO_VERSION, ver); } return ack; @@ -911,7 +913,7 @@ void pcmk__serve_attrd_ipc(qb_ipcs_service_t **ipcs, struct qb_ipcs_service_handlers *cb) { - *ipcs = mainloop_add_ipc_server(T_ATTRD, QB_IPC_NATIVE, cb); + *ipcs = mainloop_add_ipc_server(PCMK__VALUE_ATTRD, QB_IPC_NATIVE, cb); if (*ipcs == NULL) { crm_err("Failed to create pacemaker-attrd server: exiting and inhibiting respawn"); diff --git a/lib/common/iso8601.c b/lib/common/iso8601.c index 9de018f..d24f268 100644 --- a/lib/common/iso8601.c +++ b/lib/common/iso8601.c @@ -1,5 +1,5 @@ /* - * Copyright 2005-2022 the Pacemaker project contributors + * Copyright 2005-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -18,9 +18,12 @@ #include <time.h> #include <ctype.h> #include <inttypes.h> +#include <limits.h> // INT_MIN, INT_MAX #include <string.h> #include <stdbool.h> #include <crm/common/iso8601.h> +#include <crm/common/iso8601_internal.h> +#include "crmcommon_private.h" /* * Andrew's code was originally written for OSes whose "struct tm" contains: @@ -125,10 +128,7 @@ crm_time_new(const char *date_time) crm_time_t * crm_time_new_undefined(void) { - crm_time_t *result = calloc(1, sizeof(crm_time_t)); - - CRM_ASSERT(result != NULL); - return result; + return (crm_time_t *) pcmk__assert_alloc(1, sizeof(crm_time_t)); } /*! @@ -623,7 +623,9 @@ time_as_string_common(const crm_time_t *dt, int usec, uint32_t flags, if (pcmk_is_set(flags, crm_time_log_date)) { if (pcmk_is_set(flags, crm_time_weeks)) { // YYYY-WW-D - uint32_t y, w, d; + uint32_t y = 0; + uint32_t w = 0; + uint32_t d = 0; if (crm_time_get_isoweek(dt, &y, &w, &d)) { offset += snprintf(result + offset, DATE_MAX - offset, @@ -632,7 +634,8 @@ time_as_string_common(const crm_time_t *dt, int usec, uint32_t flags, } } else if (pcmk_is_set(flags, crm_time_ordinal)) { // YYYY-DDD - uint32_t y, d; + uint32_t y = 0; + uint32_t d = 0; if (crm_time_get_ordinal(dt, &y, &d)) { offset += snprintf(result + offset, DATE_MAX - offset, @@ -640,7 +643,9 @@ time_as_string_common(const crm_time_t *dt, int usec, uint32_t flags, } } else { // YYYY-MM-DD - uint32_t y, m, d; + uint32_t y = 0; + uint32_t m = 0; + uint32_t d = 0; if (crm_time_get_gregorian(dt, &y, &m, &d)) { offset += snprintf(result + offset, DATE_MAX - offset, @@ -694,12 +699,9 @@ char * crm_time_as_string(const crm_time_t *dt, int flags) { char result[DATE_MAX] = { '\0', }; - char *result_copy = NULL; time_as_string_common(dt, 0, flags, result); - - pcmk__str_update(&result_copy, result); - return result_copy; + return pcmk__str_copy(result); } /*! @@ -864,7 +866,8 @@ crm_time_parse(const char *time_str, crm_time_t *a_time) * \internal * \brief Parse a time object from an ISO 8601 date/time specification * - * \param[in] date_str ISO 8601 date/time specification (or "epoch") + * \param[in] date_str ISO 8601 date/time specification (or + * \c PCMK__VALUE_EPOCH) * * \return New time object on success, NULL (and set errno) otherwise */ @@ -898,8 +901,10 @@ parse_date(const char *date_str) dt = crm_time_new_undefined(); - if (!strncasecmp("epoch", date_str, 5) - && ((date_str[5] == '\0') || (date_str[5] == '/') || isspace(date_str[5]))) { + if ((strncasecmp(PCMK__VALUE_EPOCH, date_str, 5) == 0) + && ((date_str[5] == '\0') + || (date_str[5] == '/') + || isspace(date_str[5]))) { dt->days = 1; dt->years = 1970; crm_time_log(LOG_TRACE, "Unpacked", dt, crm_time_log_date | crm_time_log_timeofday); @@ -1211,8 +1216,7 @@ crm_time_parse_period(const char *period_str) } tzset(); - period = calloc(1, sizeof(crm_time_period_t)); - CRM_ASSERT(period != NULL); + period = pcmk__assert_alloc(1, sizeof(crm_time_period_t)); if (period_str[0] == 'P') { period->diff = crm_time_parse_duration(period_str); @@ -1370,6 +1374,23 @@ crm_time_set_timet(crm_time_t *target, const time_t *source) ha_set_tm_time(target, localtime(source)); } +/*! + * \internal + * \brief Set one time object to another if the other is earlier + * + * \param[in,out] target Time object to set + * \param[in] source Time object to use if earlier + */ +void +pcmk__set_time_if_earlier(crm_time_t *target, const crm_time_t *source) +{ + if ((target != NULL) && (source != NULL) + && (!crm_time_is_defined(target) + || (crm_time_compare(source, target) < 0))) { + crm_time_set(target, source); + } +} + crm_time_t * pcmk_copy_time(const crm_time_t *source) { @@ -1424,6 +1445,127 @@ crm_time_add(const crm_time_t *dt, const crm_time_t *value) return answer; } +/*! + * \internal + * \brief Return the XML attribute name corresponding to a time component + * + * \param[in] component Component to check + * + * \return XML attribute name corresponding to \p component, or NULL if + * \p component is invalid + */ +const char * +pcmk__time_component_attr(enum pcmk__time_component component) +{ + switch (component) { + case pcmk__time_years: + return PCMK_XA_YEARS; + + case pcmk__time_months: + return PCMK_XA_MONTHS; + + case pcmk__time_weeks: + return PCMK_XA_WEEKS; + + case pcmk__time_days: + return PCMK_XA_DAYS; + + case pcmk__time_hours: + return PCMK_XA_HOURS; + + case pcmk__time_minutes: + return PCMK_XA_MINUTES; + + case pcmk__time_seconds: + return PCMK_XA_SECONDS; + + default: + return NULL; + } +} + +typedef void (*component_fn_t)(crm_time_t *, int); + +/*! + * \internal + * \brief Get the addition function corresponding to a time component + * \param[in] component Component to check + * + * \return Addition function corresponding to \p component, or NULL if + * \p component is invalid + */ +static component_fn_t +component_fn(enum pcmk__time_component component) +{ + switch (component) { + case pcmk__time_years: + return crm_time_add_years; + + case pcmk__time_months: + return crm_time_add_months; + + case pcmk__time_weeks: + return crm_time_add_weeks; + + case pcmk__time_days: + return crm_time_add_days; + + case pcmk__time_hours: + return crm_time_add_hours; + + case pcmk__time_minutes: + return crm_time_add_minutes; + + case pcmk__time_seconds: + return crm_time_add_seconds; + + default: + return NULL; + } + +} + +/*! + * \internal + * \brief Add the value of an XML attribute to a time object + * + * \param[in,out] t Time object to add to + * \param[in] component Component of \p t to add to + * \param[in] xml XML with value to add + * + * \return Standard Pacemaker return code + */ +int +pcmk__add_time_from_xml(crm_time_t *t, enum pcmk__time_component component, + const xmlNode *xml) +{ + long long value; + const char *attr = pcmk__time_component_attr(component); + component_fn_t add = component_fn(component); + + if ((t == NULL) || (attr == NULL) || (add == NULL)) { + return EINVAL; + } + + if (xml == NULL) { + return pcmk_rc_ok; + } + + if (pcmk__scan_ll(crm_element_value(xml, attr), &value, + 0LL) != pcmk_rc_ok) { + return pcmk_rc_unpack_error; + } + + if ((value < INT_MIN) || (value > INT_MAX)) { + return ERANGE; + } + + if (value != 0LL) { + add(t, (int) value); + } + return pcmk_rc_ok; +} + crm_time_t * crm_time_calculate_duration(const crm_time_t *dt, const crm_time_t *value) { @@ -1690,8 +1832,11 @@ pcmk__time_hr_convert(pcmk__time_hr_t *target, const crm_time_t *dt) pcmk__time_hr_t *hr_dt = NULL; if (dt) { - hr_dt = target?target:calloc(1, sizeof(pcmk__time_hr_t)); - CRM_ASSERT(hr_dt != NULL); + hr_dt = target; + if (hr_dt == NULL) { + hr_dt = pcmk__assert_alloc(1, sizeof(pcmk__time_hr_t)); + } + *hr_dt = (pcmk__time_hr_t) { .years = dt->years, .months = dt->months, @@ -1772,12 +1917,21 @@ pcmk__time_hr_free(pcmk__time_hr_t * hr_dt) char * pcmk__time_format_hr(const char *format, const pcmk__time_hr_t *hr_dt) { - const char *mark_s; - int max = 128, scanned_pos = 0, printed_pos = 0, fmt_pos = 0, - date_len = 0, nano_digits = 0; - char nano_s[10], date_s[max+1], nanofmt_s[5] = "%", *tmp_fmt_s; - struct tm tm; - crm_time_t dt; +#define DATE_LEN_MAX 128 + const char *mark_s = NULL; + int scanned_pos = 0; + int printed_pos = 0; + int fmt_pos = 0; + size_t date_len = 0; + int nano_digits = 0; + + char nano_s[10] = { '\0', }; + char date_s[DATE_LEN_MAX] = { '\0', }; + char nanofmt_s[5] = "%"; + char *tmp_fmt_s = NULL; + + struct tm tm = { 0, }; + crm_time_t dt = { 0, }; if (!format) { return NULL; @@ -1818,7 +1972,8 @@ pcmk__time_format_hr(const char *format, const pcmk__time_hr_t *hr_dt) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-nonliteral" #endif - date_len += strftime(&date_s[date_len], max-date_len, tmp_fmt_s, &tm); + date_len += strftime(&date_s[date_len], DATE_LEN_MAX - date_len, + tmp_fmt_s, &tm); #ifdef HAVE_FORMAT_NONLITERAL #pragma GCC diagnostic pop #endif @@ -1829,7 +1984,7 @@ pcmk__time_format_hr(const char *format, const pcmk__time_hr_t *hr_dt) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-nonliteral" #endif - date_len += snprintf(&date_s[date_len], max-date_len, + date_len += snprintf(&date_s[date_len], DATE_LEN_MAX - date_len, nanofmt_s, nano_s); #ifdef HAVE_FORMAT_NONLITERAL #pragma GCC diagnostic pop @@ -1839,6 +1994,7 @@ pcmk__time_format_hr(const char *format, const pcmk__time_hr_t *hr_dt) } return (date_len == 0)?NULL:strdup(date_s); +#undef DATE_LEN_MAX } /*! @@ -1858,22 +2014,15 @@ char * pcmk__epoch2str(const time_t *source, uint32_t flags) { time_t epoch_time = (source == NULL)? time(NULL) : *source; - char *result = NULL; if (flags == 0) { - const char *buf = pcmk__trim(ctime(&epoch_time)); - - if (buf != NULL) { - result = strdup(buf); - CRM_ASSERT(result != NULL); - } + return pcmk__str_copy(pcmk__trim(ctime(&epoch_time))); } else { crm_time_t dt; crm_time_set_timet(&dt, &epoch_time); - result = crm_time_as_string(&dt, flags); + return crm_time_as_string(&dt, flags); } - return result; } /*! @@ -1899,7 +2048,6 @@ pcmk__timespec2str(const struct timespec *ts, uint32_t flags) struct timespec tmp_ts; crm_time_t dt; char result[DATE_MAX] = { 0 }; - char *result_copy = NULL; if (ts == NULL) { qb_util_timespec_from_epoch_get(&tmp_ts); @@ -1907,8 +2055,7 @@ pcmk__timespec2str(const struct timespec *ts, uint32_t flags) } crm_time_set_timet(&dt, &ts->tv_sec); time_as_string_common(&dt, ts->tv_nsec / QB_TIME_NS_IN_USEC, flags, result); - pcmk__str_update(&result_copy, result); - return result_copy; + return pcmk__str_copy(result); } /*! @@ -1934,24 +2081,24 @@ pcmk__readable_interval(guint interval_ms) int offset = 0; str[0] = '\0'; - if (interval_ms > MS_IN_D) { + if (interval_ms >= MS_IN_D) { offset += snprintf(str + offset, MAXSTR - offset, "%ud", interval_ms / MS_IN_D); interval_ms -= (interval_ms / MS_IN_D) * MS_IN_D; } - if (interval_ms > MS_IN_H) { + if (interval_ms >= MS_IN_H) { offset += snprintf(str + offset, MAXSTR - offset, "%uh", interval_ms / MS_IN_H); interval_ms -= (interval_ms / MS_IN_H) * MS_IN_H; } - if (interval_ms > MS_IN_M) { + if (interval_ms >= MS_IN_M) { offset += snprintf(str + offset, MAXSTR - offset, "%um", interval_ms / MS_IN_M); interval_ms -= (interval_ms / MS_IN_M) * MS_IN_M; } // Ns, N.NNNs, or NNNms - if (interval_ms > MS_IN_S) { + if (interval_ms >= MS_IN_S) { offset += snprintf(str + offset, MAXSTR - offset, "%u", interval_ms / MS_IN_S); interval_ms -= (interval_ms / MS_IN_S) * MS_IN_S; diff --git a/lib/common/logging.c b/lib/common/logging.c index 7768c35..efdbac3 100644 --- a/lib/common/logging.c +++ b/lib/common/logging.c @@ -1,5 +1,5 @@ /* - * 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. * @@ -192,7 +192,7 @@ set_format_string(int method, const char *daemon, pid_t use_pid, static bool logfile_disabled(const char *filename) { - return pcmk__str_eq(filename, PCMK__VALUE_NONE, pcmk__str_casei) + return pcmk__str_eq(filename, PCMK_VALUE_NONE, pcmk__str_casei) || pcmk__str_eq(filename, "/dev/null", pcmk__str_none); } @@ -785,7 +785,7 @@ set_identity(const char *entity, int argc, char *const *argv) } if (entity != NULL) { - crm_system_name = strdup(entity); + crm_system_name = pcmk__str_copy(entity); } else if ((argc > 0) && (argv != NULL)) { char *mutable = strdup(argv[0]); @@ -794,15 +794,13 @@ set_identity(const char *entity, int argc, char *const *argv) if (strstr(modified, "lt-") == modified) { modified += 3; } - crm_system_name = strdup(modified); + crm_system_name = pcmk__str_copy(modified); free(mutable); } else { - crm_system_name = strdup("Unknown"); + crm_system_name = pcmk__str_copy("Unknown"); } - CRM_ASSERT(crm_system_name != NULL); - // Used by fencing.py.py (in fence-agents) pcmk__set_env_option(PCMK__ENV_SERVICE, crm_system_name, false); } @@ -915,12 +913,12 @@ crm_log_init(const char *entity, uint8_t level, gboolean daemon, gboolean to_std if (pcmk__is_daemon) { facility = "daemon"; } else { - facility = PCMK__VALUE_NONE; + facility = PCMK_VALUE_NONE; } pcmk__set_env_option(PCMK__ENV_LOGFACILITY, facility, true); } - if (pcmk__str_eq(facility, PCMK__VALUE_NONE, pcmk__str_casei)) { + if (pcmk__str_eq(facility, PCMK_VALUE_NONE, pcmk__str_casei)) { quiet = TRUE; @@ -957,7 +955,7 @@ crm_log_init(const char *entity, uint8_t level, gboolean daemon, gboolean to_std { const char *logfile = pcmk__env_option(PCMK__ENV_LOGFILE); - if (!pcmk__str_eq(PCMK__VALUE_NONE, logfile, pcmk__str_casei) + if (!pcmk__str_eq(PCMK_VALUE_NONE, logfile, pcmk__str_casei) && (pcmk__is_daemon || (logfile != NULL))) { // Daemons always get a log file, unless explicitly set to "none" pcmk__add_logfile(logfile); @@ -1296,4 +1294,4 @@ void pcmk__set_config_warning_handler(pcmk__config_warning_func warning_handler, { pcmk__config_warning_handler = warning_handler; pcmk__config_warning_context = warning_context; -}
\ No newline at end of file +} diff --git a/lib/common/mainloop.c b/lib/common/mainloop.c index f971713..7626134 100644 --- a/lib/common/mainloop.c +++ b/lib/common/mainloop.c @@ -1,5 +1,5 @@ /* - * 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. * @@ -191,7 +191,6 @@ mainloop_add_trigger(int priority, int (*dispatch) (gpointer user_data), CRM_ASSERT(sizeof(crm_trigger_t) > sizeof(GSource)); source = g_source_new(&crm_trigger_funcs, sizeof(crm_trigger_t)); - CRM_ASSERT(source != NULL); return mainloop_setup_trigger(source, priority, dispatch, userdata); } @@ -1256,7 +1255,7 @@ mainloop_child_add_with_flags(pid_t pid, int timeout, const char *desc, void *pr void (*callback) (mainloop_child_t * p, pid_t pid, int core, int signo, int exitcode)) { static bool need_init = TRUE; - mainloop_child_t *child = calloc(1, sizeof(mainloop_child_t)); + mainloop_child_t *child = pcmk__assert_alloc(1, sizeof(mainloop_child_t)); child->pid = pid; child->timerid = 0; @@ -1264,7 +1263,7 @@ mainloop_child_add_with_flags(pid_t pid, int timeout, const char *desc, void *pr child->privatedata = privatedata; child->callback = callback; child->flags = flags; - pcmk__str_update(&child->desc, desc); + child->desc = pcmk__str_copy(desc); if (timeout) { child->timerid = g_timeout_add(timeout, child_timeout_callback, child); @@ -1368,21 +1367,19 @@ mainloop_timer_set_period(mainloop_timer_t *t, guint period_ms) mainloop_timer_t * mainloop_timer_add(const char *name, guint period_ms, bool repeat, GSourceFunc cb, void *userdata) { - mainloop_timer_t *t = calloc(1, sizeof(mainloop_timer_t)); + mainloop_timer_t *t = pcmk__assert_alloc(1, sizeof(mainloop_timer_t)); - if(t) { - if(name) { - t->name = crm_strdup_printf("%s-%u-%d", name, period_ms, repeat); - } else { - t->name = crm_strdup_printf("%p-%u-%d", t, period_ms, repeat); - } - t->id = 0; - t->period_ms = period_ms; - t->repeat = repeat; - t->cb = cb; - t->userdata = userdata; - crm_trace("Created timer %s with %p %p", t->name, userdata, t->userdata); - } + if (name != NULL) { + t->name = crm_strdup_printf("%s-%u-%d", name, period_ms, repeat); + } else { + t->name = crm_strdup_printf("%p-%u-%d", t, period_ms, repeat); + } + t->id = 0; + t->period_ms = period_ms; + t->repeat = repeat; + t->cb = cb; + t->userdata = userdata; + crm_trace("Created timer %s with %p %p", t->name, userdata, t->userdata); return t; } diff --git a/lib/common/messages.c b/lib/common/messages.c index 2c01eed..fa21169 100644 --- a/lib/common/messages.c +++ b/lib/common/messages.c @@ -1,5 +1,5 @@ /* - * Copyright 2004-2022 the Pacemaker project contributors + * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -15,7 +15,7 @@ #include <glib.h> #include <libxml/tree.h> -#include <crm/msg_xml.h> +#include <crm/common/xml.h> #include <crm/common/xml_internal.h> /*! @@ -61,23 +61,25 @@ create_request_adv(const char *task, xmlNode *msg_data, } // host_from will get set for us if necessary by the controller when routed - request = create_xml_node(NULL, __func__); - crm_xml_add(request, F_CRM_ORIGIN, origin); - crm_xml_add(request, F_TYPE, T_CRM); - crm_xml_add(request, F_CRM_VERSION, CRM_FEATURE_SET); - crm_xml_add(request, F_CRM_MSG_TYPE, XML_ATTR_REQUEST); - crm_xml_add(request, F_CRM_REFERENCE, reference); - crm_xml_add(request, F_CRM_TASK, task); - crm_xml_add(request, F_CRM_SYS_TO, sys_to); - crm_xml_add(request, F_CRM_SYS_FROM, true_from); + request = pcmk__xe_create(NULL, __func__); + crm_xml_add(request, PCMK_XA_ORIGIN, origin); + crm_xml_add(request, PCMK__XA_T, PCMK__VALUE_CRMD); + crm_xml_add(request, PCMK_XA_VERSION, CRM_FEATURE_SET); + crm_xml_add(request, PCMK__XA_SUBT, PCMK__VALUE_REQUEST); + crm_xml_add(request, PCMK_XA_REFERENCE, reference); + crm_xml_add(request, PCMK__XA_CRM_TASK, task); + crm_xml_add(request, PCMK__XA_CRM_SYS_TO, sys_to); + crm_xml_add(request, PCMK__XA_CRM_SYS_FROM, true_from); /* HOSTTO will be ignored if it is to the DC anyway. */ if (host_to != NULL && strlen(host_to) > 0) { - crm_xml_add(request, F_CRM_HOST_TO, host_to); + crm_xml_add(request, PCMK__XA_CRM_HOST_TO, host_to); } if (msg_data != NULL) { - add_message_xml(request, F_CRM_DATA, msg_data); + xmlNode *wrapper = pcmk__xe_create(request, PCMK__XE_CRM_XML); + + pcmk__xml_copy(wrapper, msg_data); } free(reference); free(true_from); @@ -104,67 +106,56 @@ create_reply_adv(const xmlNode *original_request, xmlNode *xml_response_data, { xmlNode *reply = NULL; - const char *host_from = crm_element_value(original_request, F_CRM_HOST_FROM); - const char *sys_from = crm_element_value(original_request, F_CRM_SYS_FROM); - const char *sys_to = crm_element_value(original_request, F_CRM_SYS_TO); - const char *type = crm_element_value(original_request, F_CRM_MSG_TYPE); - const char *operation = crm_element_value(original_request, F_CRM_TASK); - const char *crm_msg_reference = crm_element_value(original_request, F_CRM_REFERENCE); + const char *host_from = crm_element_value(original_request, PCMK__XA_SRC); + const char *sys_from = crm_element_value(original_request, + PCMK__XA_CRM_SYS_FROM); + const char *sys_to = crm_element_value(original_request, + PCMK__XA_CRM_SYS_TO); + const char *type = crm_element_value(original_request, PCMK__XA_SUBT); + const char *operation = crm_element_value(original_request, + PCMK__XA_CRM_TASK); + const char *crm_msg_reference = crm_element_value(original_request, + PCMK_XA_REFERENCE); if (type == NULL) { crm_err("Cannot create new_message, no message type in original message"); CRM_ASSERT(type != NULL); return NULL; -#if 0 - } else if (strcasecmp(XML_ATTR_REQUEST, type) != 0) { - crm_err("Cannot create new_message, original message was not a request"); - return NULL; -#endif } - reply = create_xml_node(NULL, __func__); - if (reply == NULL) { - crm_err("Cannot create new_message, malloc failed"); - return NULL; + + if (strcmp(type, PCMK__VALUE_REQUEST) != 0) { + /* Replies should only be generated for request messages, but it's possible + * we expect replies to other messages right now so this can't be enforced. + */ + crm_trace("Creating a reply for a non-request original message"); } - crm_xml_add(reply, F_CRM_ORIGIN, origin); - crm_xml_add(reply, F_TYPE, T_CRM); - crm_xml_add(reply, F_CRM_VERSION, CRM_FEATURE_SET); - crm_xml_add(reply, F_CRM_MSG_TYPE, XML_ATTR_RESPONSE); - crm_xml_add(reply, F_CRM_REFERENCE, crm_msg_reference); - crm_xml_add(reply, F_CRM_TASK, operation); + reply = pcmk__xe_create(NULL, __func__); + crm_xml_add(reply, PCMK_XA_ORIGIN, origin); + crm_xml_add(reply, PCMK__XA_T, PCMK__VALUE_CRMD); + crm_xml_add(reply, PCMK_XA_VERSION, CRM_FEATURE_SET); + crm_xml_add(reply, PCMK__XA_SUBT, PCMK__VALUE_RESPONSE); + crm_xml_add(reply, PCMK_XA_REFERENCE, crm_msg_reference); + crm_xml_add(reply, PCMK__XA_CRM_TASK, operation); /* since this is a reply, we reverse the from and to */ - crm_xml_add(reply, F_CRM_SYS_TO, sys_from); - crm_xml_add(reply, F_CRM_SYS_FROM, sys_to); + crm_xml_add(reply, PCMK__XA_CRM_SYS_TO, sys_from); + crm_xml_add(reply, PCMK__XA_CRM_SYS_FROM, sys_to); /* HOSTTO will be ignored if it is to the DC anyway. */ if (host_from != NULL && strlen(host_from) > 0) { - crm_xml_add(reply, F_CRM_HOST_TO, host_from); + crm_xml_add(reply, PCMK__XA_CRM_HOST_TO, host_from); } if (xml_response_data != NULL) { - add_message_xml(reply, F_CRM_DATA, xml_response_data); + xmlNode *wrapper = pcmk__xe_create(reply, PCMK__XE_CRM_XML); + + pcmk__xml_copy(wrapper, xml_response_data); } return reply; } -xmlNode * -get_message_xml(const xmlNode *msg, const char *field) -{ - return pcmk__xml_first_child(first_named_child(msg, field)); -} - -gboolean -add_message_xml(xmlNode *msg, const char *field, xmlNode *xml) -{ - xmlNode *holder = create_xml_node(msg, field); - - add_node_copy(holder, xml); - return TRUE; -} - /*! * \brief Get name to be used as identifier for cluster messages * @@ -289,3 +280,28 @@ pcmk__reset_request(pcmk__request_t *request) pcmk__reset_result(&(request->result)); } + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include <crm/common/xml_compat.h> + +gboolean +add_message_xml(xmlNode *msg, const char *field, xmlNode *xml) +{ + xmlNode *holder = pcmk__xe_create(msg, field); + + pcmk__xml_copy(holder, xml); + return TRUE; +} + +xmlNode * +get_message_xml(const xmlNode *msg, const char *field) +{ + xmlNode *child = pcmk__xe_first_child(msg, field, NULL, NULL); + + return pcmk__xe_first_child(child, NULL, NULL, NULL); +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/mock.c b/lib/common/mock.c index 6f837ad..43c6e8f 100644 --- a/lib/common/mock.c +++ b/lib/common/mock.c @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the Pacemaker project contributors + * Copyright 2021-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -25,6 +25,7 @@ #include <grp.h> #include <cmocka.h> +#include <crm/common/unittest_internal.h> #include "mock_private.h" /* This file is only used when running "make check". It is built into @@ -54,6 +55,27 @@ */ // LCOV_EXCL_START + +/* abort() + * + * Always mock abort - there's no pcmk__mock_abort tuneable to control this. + * Because abort calls _exit(), which doesn't run any of the things registered + * with atexit(), coverage numbers do not get written out. This most noticably + * affects places where we are testing that things abort when they should. + * + * The solution is this wrapper that is always enabled when we are running + * unit tests (mock.c does not get included for the regular libcrmcommon.so). + * All it does is dump coverage data and call the real abort(). + */ +_Noreturn void +__wrap_abort(void) +{ +#if (PCMK__WITH_COVERAGE == 1) + __gcov_dump(); +#endif + __real_abort(); +} + /* calloc() * * If pcmk__mock_calloc is set to true, later calls to calloc() will return @@ -103,6 +125,31 @@ __wrap_getenv(const char *name) } +/* realloc() + * + * If pcmk__mock_realloc is set to true, later calls to realloc() will return + * NULL and must be preceded by: + * + * expect_*(__wrap_realloc, ptr[, ...]); + * expect_*(__wrap_realloc, size[, ...]); + * + * expect_* functions: https://api.cmocka.org/group__cmocka__param.html + */ + +bool pcmk__mock_realloc = false; + +void * +__wrap_realloc(void *ptr, size_t size) +{ + if (!pcmk__mock_realloc) { + return __real_realloc(ptr, size); + } + check_expected_ptr(ptr); + check_expected(size); + return NULL; +} + + /* setenv() * * If pcmk__mock_setenv is set to true, later calls to setenv() must be preceded @@ -412,40 +459,4 @@ __wrap_strdup(const char *s) return NULL; } - -/* uname() - * - * If pcmk__mock_uname is set to true, later calls to uname() must be preceded - * by: - * - * expect_*(__wrap_uname, buf[, ...]); - * will_return(__wrap_uname, return_value); - * will_return(__wrap_uname, node_name_for_buf_parameter_to_uname); - * - * expect_* functions: https://api.cmocka.org/group__cmocka__param.html - */ - -bool pcmk__mock_uname = false; - -int -__wrap_uname(struct utsname *buf) -{ - if (pcmk__mock_uname) { - int retval = 0; - char *result = NULL; - - check_expected_ptr(buf); - retval = mock_type(int); - result = mock_ptr_type(char *); - - if (result != NULL) { - strcpy(buf->nodename, result); - } - return retval; - - } else { - return __real_uname(buf); - } -} - // LCOV_EXCL_STOP diff --git a/lib/common/mock_private.h b/lib/common/mock_private.h index b0e0ed2..3beeda4 100644 --- a/lib/common/mock_private.h +++ b/lib/common/mock_private.h @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the Pacemaker project contributors + * Copyright 2021-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -22,6 +22,9 @@ /* This header is for the sole use of libcrmcommon_test and unit tests */ +_Noreturn void __real_abort(void); +_Noreturn void __wrap_abort(void); + extern bool pcmk__mock_calloc; void *__real_calloc(size_t nmemb, size_t size); void *__wrap_calloc(size_t nmemb, size_t size); @@ -38,6 +41,10 @@ extern bool pcmk__mock_getenv; char *__real_getenv(const char *name); char *__wrap_getenv(const char *name); +extern bool pcmk__mock_realloc; +void *__real_realloc(void *ptr, size_t size); +void *__wrap_realloc(void *ptr, size_t size); + extern bool pcmk__mock_setenv; int __real_setenv(const char *name, const char *value, int overwrite); int __wrap_setenv(const char *name, const char *value, int overwrite); @@ -74,8 +81,4 @@ extern bool pcmk__mock_strdup; char *__real_strdup(const char *s); char *__wrap_strdup(const char *s); -extern bool pcmk__mock_uname; -int __real_uname(struct utsname *buf); -int __wrap_uname(struct utsname *buf); - #endif // MOCK_PRIVATE__H diff --git a/lib/common/nodes.c b/lib/common/nodes.c index a17d587..4edeafb 100644 --- a/lib/common/nodes.c +++ b/lib/common/nodes.c @@ -1,5 +1,5 @@ /* - * Copyright 2022 the Pacemaker project contributors + * Copyright 2022-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -9,16 +9,155 @@ #include <crm_internal.h> +#include <libxml/tree.h> // xmlNode #include <crm/common/nvpair.h> +/*! + * \internal + * \brief Check whether a node is online + * + * \param[in] node Node to check + * + * \return true if \p node is online, otherwise false + */ +bool +pcmk_node_is_online(const pcmk_node_t *node) +{ + return (node != NULL) && node->details->online; +} + +/*! + * \internal + * \brief Check whether a node is pending + * + * Check whether a node is pending. A node is pending if it is a member of the + * cluster but not the controller group, which means it is in the process of + * either joining or leaving the cluster. + * + * \param[in] node Node to check + * + * \return true if \p node is pending, otherwise false + */ +bool +pcmk_node_is_pending(const pcmk_node_t *node) +{ + return (node != NULL) && node->details->pending; +} + +/*! + * \internal + * \brief Check whether a node is clean + * + * Check whether a node is clean. A node is clean if it is a cluster node or + * remote node that has been seen by the cluster at least once, or the + * startup-fencing cluster option is false; and the node, and its host if a + * guest or bundle node, are not scheduled to be fenced. + * + * \param[in] node Node to check + * + * \return true if \p node is clean, otherwise false + */ +bool +pcmk_node_is_clean(const pcmk_node_t *node) +{ + return (node != NULL) && !(node->details->unclean); +} + +/*! + * \internal + * \brief Check whether a node is shutting down + * + * \param[in] node Node to check + * + * \return true if \p node is shutting down, otherwise false + */ +bool +pcmk_node_is_shutting_down(const pcmk_node_t *node) +{ + return (node != NULL) && node->details->shutdown; +} + +/*! + * \internal + * \brief Check whether a node is in maintenance mode + * + * \param[in] node Node to check + * + * \return true if \p node is in maintenance mode, otherwise false + */ +bool +pcmk_node_is_in_maintenance(const pcmk_node_t *node) +{ + return (node != NULL) && node->details->maintenance; +} + +/*! + * \internal + * \brief Call a function for each resource active on a node + * + * Call a caller-supplied function with a caller-supplied argument for each + * resource that is active on a given node. If the function returns false, this + * function will return immediately without processing any remaining resources. + * + * \param[in] node Node to check + * + * \return Result of last call of \p fn (or false if none) + */ +bool +pcmk_foreach_active_resource(pcmk_node_t *node, + bool (*fn)(pcmk_resource_t *, void *), + void *user_data) +{ + bool result = false; + + if ((node != NULL) && (fn != NULL)) { + for (GList *item = node->details->running_rsc; item != NULL; + item = item->next) { + + result = fn((pcmk_resource_t *) item->data, user_data); + if (!result) { + break; + } + } + } + return result; +} + void pcmk__xe_add_node(xmlNode *xml, const char *node, int nodeid) { + CRM_ASSERT(xml != NULL); + if (node != NULL) { - crm_xml_add(xml, PCMK__XA_ATTR_NODE_NAME, node); + crm_xml_add(xml, PCMK__XA_ATTR_HOST, node); } if (nodeid > 0) { - crm_xml_add_int(xml, PCMK__XA_ATTR_NODE_ID, nodeid); + crm_xml_add_int(xml, PCMK__XA_ATTR_HOST_ID, nodeid); + } +} + +/*! + * \internal + * \brief Find a node by name in a list of nodes + * + * \param[in] nodes List of nodes (as pcmk_node_t*) + * \param[in] node_name Name of node to find + * + * \return Node from \p nodes that matches \p node_name if any, otherwise NULL + */ +pcmk_node_t * +pcmk__find_node_in_list(const GList *nodes, const char *node_name) +{ + if (node_name != NULL) { + for (const GList *iter = nodes; iter != NULL; iter = iter->next) { + pcmk_node_t *node = (pcmk_node_t *) iter->data; + + if (pcmk__str_eq(node->details->uname, node_name, + pcmk__str_casei)) { + return node; + } + } } + return NULL; } diff --git a/lib/common/nvpair.c b/lib/common/nvpair.c index dbb9c99..295e923 100644 --- a/lib/common/nvpair.c +++ b/lib/common/nvpair.c @@ -1,5 +1,5 @@ /* - * 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. * @@ -17,17 +17,17 @@ #include <libxml/tree.h> #include <crm/crm.h> -#include <crm/msg_xml.h> #include <crm/common/xml.h> #include <crm/common/xml_internal.h> #include "crmcommon_private.h" /* - * This file isolates handling of three types of name/value pairs: + * This file isolates handling of various kinds of name/value pairs: * * - pcmk_nvpair_t data type * - XML attributes (<TAG ... NAME=VALUE ...>) * - XML nvpair elements (<nvpair id=ID name=NAME value=VALUE>) + * - Meta-attributes (for resources and actions) */ // pcmk_nvpair_t handling @@ -50,11 +50,10 @@ pcmk__new_nvpair(const char *name, const char *value) CRM_ASSERT(name); - nvpair = calloc(1, sizeof(pcmk_nvpair_t)); - CRM_ASSERT(nvpair); + nvpair = pcmk__assert_alloc(1, sizeof(pcmk_nvpair_t)); - pcmk__str_update(&nvpair->name, name); - pcmk__str_update(&nvpair->value, value); + nvpair->name = pcmk__str_copy(name); + nvpair->value = pcmk__str_copy(value); return nvpair; } @@ -630,6 +629,37 @@ crm_element_value_timeval(const xmlNode *xml, const char *name_sec, } /*! + * \internal + * \brief Get a date/time object from an XML attribute value + * + * \param[in] xml XML with attribute to parse (from CIB) + * \param[in] attr Name of attribute to parse + * \param[out] t Where to create date/time object + * (\p *t must be NULL initially) + * + * \return Standard Pacemaker return code + * \note The caller is responsible for freeing \p *t using crm_time_free(). + */ +int +pcmk__xe_get_datetime(const xmlNode *xml, const char *attr, crm_time_t **t) +{ + const char *value = NULL; + + if ((t == NULL) || (*t != NULL) || (xml == NULL) || (attr == NULL)) { + return EINVAL; + } + + value = crm_element_value(xml, attr); + if (value != NULL) { + *t = crm_time_new(value); + if (*t == NULL) { + return pcmk_rc_unpack_error; + } + } + return pcmk_rc_ok; +} + +/*! * \brief Retrieve a copy of the value of an XML attribute * * This is like \c crm_element_value() but allocating new memory for the result. @@ -643,20 +673,18 @@ crm_element_value_timeval(const xmlNode *xml, const char *name_sec, char * crm_element_value_copy(const xmlNode *data, const char *name) { - char *value_copy = NULL; - - pcmk__str_update(&value_copy, crm_element_value(data, name)); - return value_copy; + return pcmk__str_copy(crm_element_value(data, name)); } /*! - * \brief Add hash table entry to XML as (possibly legacy) name/value + * \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, this will instead add a \<param name=NAME - * value=VALUE/> child to the XML (for legacy compatibility with heartbeat). + * name starts with a digit, then it's not a valid XML attribute name. In that + * case, this will instead add a <tt><param name=NAME value=VALUE/></tt> child + * to the XML. * * \param[in] key Key of hash table entry * \param[in] value Value of hash table entry @@ -665,16 +693,24 @@ crm_element_value_copy(const xmlNode *data, const char *name) 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 = create_xml_node(xml_node, XML_TAG_PARAM); + xmlNode *tmp = pcmk__xe_create(xml_node, PCMK__XE_PARAM); - crm_xml_add(tmp, XML_NVPAIR_ATTR_NAME, name); - crm_xml_add(tmp, XML_NVPAIR_ATTR_VALUE, s_value); + 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); @@ -770,19 +806,16 @@ crm_create_nvpair_xml(xmlNode *parent, const char *id, const char *name, */ CRM_CHECK(id || name, return NULL); - nvp = create_xml_node(parent, XML_CIB_TAG_NVPAIR); - CRM_CHECK(nvp, return NULL); + nvp = pcmk__xe_create(parent, PCMK_XE_NVPAIR); if (id) { - crm_xml_add(nvp, XML_ATTR_ID, id); + crm_xml_add(nvp, PCMK_XA_ID, id); } else { - const char *parent_id = ID(parent); - crm_xml_set_id(nvp, "%s-%s", - (parent_id? parent_id : XML_CIB_TAG_NVPAIR), name); + pcmk__s(pcmk__xe_id(parent), PCMK_XE_NVPAIR), name); } - crm_xml_add(nvp, XML_NVPAIR_ATTR_NAME, name); - crm_xml_add(nvp, XML_NVPAIR_ATTR_VALUE, value); + crm_xml_add(nvp, PCMK_XA_NAME, name); + crm_xml_add(nvp, PCMK_XA_VALUE, value); return nvp; } @@ -832,7 +865,7 @@ xml2list(const xmlNode *parent) CRM_CHECK(parent != NULL, return nvpair_hash); - nvpair_list = find_xml_node(parent, XML_TAG_ATTRS, FALSE); + 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"); @@ -848,20 +881,18 @@ xml2list(const xmlNode *parent) crm_trace("Added %s=%s", p_name, p_value); - g_hash_table_insert(nvpair_hash, strdup(p_name), strdup(p_value)); + pcmk__insert_dup(nvpair_hash, p_name, p_value); } - for (child = pcmk__xml_first_child(nvpair_list); child != NULL; - child = pcmk__xml_next(child)) { + for (child = pcmk__xe_first_child(nvpair_list, PCMK__XE_PARAM, NULL, NULL); + child != NULL; child = pcmk__xe_next_same(child)) { - if (strcmp((const char *)child->name, XML_TAG_PARAM) == 0) { - const char *key = crm_element_value(child, XML_NVPAIR_ATTR_NAME); - const char *value = crm_element_value(child, XML_NVPAIR_ATTR_VALUE); + 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) { - g_hash_table_insert(nvpair_hash, strdup(key), strdup(value)); - } + crm_trace("Added %s=%s", key, value); + if (key != NULL && value != NULL) { + pcmk__insert_dup(nvpair_hash, key, value); } } @@ -871,7 +902,7 @@ xml2list(const xmlNode *parent) void pcmk__xe_set_bool_attr(xmlNodePtr node, const char *name, bool value) { - crm_xml_add(node, name, value ? XML_BOOLEAN_TRUE : XML_BOOLEAN_FALSE); + crm_xml_add(node, name, pcmk__btoa(value)); } int @@ -911,6 +942,60 @@ pcmk__xe_attr_is_true(const xmlNode *node, const char *name) return rc == pcmk_rc_ok && value == true; } +// 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; + + CRM_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; +} + // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START @@ -960,7 +1045,7 @@ crm_xml_replace(xmlNode *node, const char *name, const char *value) return NULL; } else if (old_value && !value) { - xml_remove_prop(node, name); + pcmk__xe_remove_attr(node, name); return NULL; } diff --git a/lib/common/options.c b/lib/common/options.c index 2d86ebc..ba64959 100644 --- a/lib/common/options.c +++ b/lib/common/options.c @@ -1,5 +1,5 @@ /* - * Copyright 2004-2022 the Pacemaker project contributors + * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -20,6 +20,7 @@ #include <sys/stat.h> #include <crm/crm.h> +#include <crm/common/xml.h> void pcmk__cli_help(char cmd) @@ -39,6 +40,1035 @@ pcmk__cli_help(char cmd) /* + * Option metadata + */ + +static const pcmk__cluster_option_t cluster_options[] = { + /* name, old name, type, allowed values, + * default value, validator, + * flags, + * short description, + * long description + */ + { + PCMK_OPT_DC_VERSION, NULL, PCMK_VALUE_VERSION, NULL, + NULL, NULL, + pcmk__opt_controld|pcmk__opt_generated, + N_("Pacemaker version on cluster node elected Designated Controller " + "(DC)"), + N_("Includes a hash which identifies the exact revision the code was " + "built from. Used for diagnostic purposes."), + }, + { + PCMK_OPT_CLUSTER_INFRASTRUCTURE, NULL, PCMK_VALUE_STRING, NULL, + NULL, NULL, + pcmk__opt_controld|pcmk__opt_generated, + N_("The messaging layer on which Pacemaker is currently running"), + N_("Used for informational and diagnostic purposes."), + }, + { + PCMK_OPT_CLUSTER_NAME, NULL, PCMK_VALUE_STRING, NULL, + NULL, NULL, + pcmk__opt_controld, + N_("An arbitrary name for the cluster"), + N_("This optional value is mostly for users' convenience as desired " + "in administration, but may also be used in Pacemaker " + "configuration rules via the #cluster-name node attribute, and " + "by higher-level tools and resource agents."), + }, + { + PCMK_OPT_DC_DEADTIME, NULL, PCMK_VALUE_DURATION, NULL, + "20s", pcmk__valid_interval_spec, + pcmk__opt_controld, + N_("How long to wait for a response from other nodes during start-up"), + N_("The optimal value will depend on the speed and load of your " + "network and the type of switches used."), + }, + { + PCMK_OPT_CLUSTER_RECHECK_INTERVAL, NULL, PCMK_VALUE_DURATION, NULL, + "15min", pcmk__valid_interval_spec, + pcmk__opt_controld, + N_("Polling interval to recheck cluster state and evaluate rules " + "with date specifications"), + N_("Pacemaker is primarily event-driven, and looks ahead to know when " + "to recheck cluster state for failure-timeout settings and most " + "time-based rules. However, it will also recheck the cluster after " + "this amount of inactivity, to evaluate rules with date " + "specifications and serve as a fail-safe for certain types of " + "scheduler bugs. A value of 0 disables polling. A positive value " + "sets an interval in seconds, unless other units are specified " + "(for example, \"5min\")."), + }, + { + PCMK_OPT_FENCE_REACTION, NULL, PCMK_VALUE_SELECT, + PCMK_VALUE_STOP ", " PCMK_VALUE_PANIC, + PCMK_VALUE_STOP, NULL, + pcmk__opt_controld, + N_("How a cluster node should react if notified of its own fencing"), + N_("A cluster node may receive notification of a \"succeeded\" " + "fencing that targeted it if fencing is misconfigured, or if " + "fabric fencing is in use that doesn't cut cluster communication. " + "Use \"stop\" to attempt to immediately stop Pacemaker and stay " + "stopped, or \"panic\" to attempt to immediately reboot the local " + "node, falling back to stop on failure."), + }, + { + PCMK_OPT_ELECTION_TIMEOUT, NULL, PCMK_VALUE_DURATION, NULL, + "2min", pcmk__valid_interval_spec, + pcmk__opt_controld|pcmk__opt_advanced, + N_("Declare an election failed if it is not decided within this much " + "time. If you need to adjust this value, it probably indicates " + "the presence of a bug."), + NULL, + }, + { + PCMK_OPT_SHUTDOWN_ESCALATION, NULL, PCMK_VALUE_DURATION, NULL, + "20min", pcmk__valid_interval_spec, + pcmk__opt_controld|pcmk__opt_advanced, + N_("Exit immediately if shutdown does not complete within this much " + "time. If you need to adjust this value, it probably indicates " + "the presence of a bug."), + NULL, + }, + { + PCMK_OPT_JOIN_INTEGRATION_TIMEOUT, "crmd-integration-timeout", + PCMK_VALUE_DURATION, NULL, + "3min", pcmk__valid_interval_spec, + pcmk__opt_controld|pcmk__opt_advanced, + N_("If you need to adjust this value, it probably indicates " + "the presence of a bug."), + NULL, + }, + { + PCMK_OPT_JOIN_FINALIZATION_TIMEOUT, "crmd-finalization-timeout", + PCMK_VALUE_DURATION, NULL, + "30min", pcmk__valid_interval_spec, + pcmk__opt_controld|pcmk__opt_advanced, + N_("If you need to adjust this value, it probably indicates " + "the presence of a bug."), + NULL, + }, + { + PCMK_OPT_TRANSITION_DELAY, "crmd-transition-delay", PCMK_VALUE_DURATION, + NULL, + "0s", pcmk__valid_interval_spec, + pcmk__opt_controld|pcmk__opt_advanced, + N_("Enabling this option will slow down cluster recovery under all " + "conditions"), + N_("Delay cluster recovery for this much time to allow for additional " + "events to occur. Useful if your configuration is sensitive to " + "the order in which ping updates arrive."), + }, + { + PCMK_OPT_NO_QUORUM_POLICY, NULL, PCMK_VALUE_SELECT, + PCMK_VALUE_STOP ", " PCMK_VALUE_FREEZE ", " PCMK_VALUE_IGNORE + ", " PCMK_VALUE_DEMOTE ", " PCMK_VALUE_FENCE_LEGACY, + PCMK_VALUE_STOP, pcmk__valid_no_quorum_policy, + pcmk__opt_schedulerd, + N_("What to do when the cluster does not have quorum"), + NULL, + }, + { + PCMK_OPT_SHUTDOWN_LOCK, NULL, PCMK_VALUE_BOOLEAN, NULL, + PCMK_VALUE_FALSE, pcmk__valid_boolean, + pcmk__opt_schedulerd, + N_("Whether to lock resources to a cleanly shut down node"), + N_("When true, resources active on a node when it is cleanly shut down " + "are kept \"locked\" to that node (not allowed to run elsewhere) " + "until they start again on that node after it rejoins (or for at " + "most shutdown-lock-limit, if set). Stonith resources and " + "Pacemaker Remote connections are never locked. Clone and bundle " + "instances and the promoted role of promotable clones are " + "currently never locked, though support could be added in a future " + "release."), + }, + { + PCMK_OPT_SHUTDOWN_LOCK_LIMIT, NULL, PCMK_VALUE_DURATION, NULL, + "0", pcmk__valid_interval_spec, + pcmk__opt_schedulerd, + N_("Do not lock resources to a cleanly shut down node longer than " + "this"), + N_("If shutdown-lock is true and this is set to a nonzero time " + "duration, shutdown locks will expire after this much time has " + "passed since the shutdown was initiated, even if the node has not " + "rejoined."), + }, + { + PCMK_OPT_ENABLE_ACL, NULL, PCMK_VALUE_BOOLEAN, NULL, + PCMK_VALUE_FALSE, pcmk__valid_boolean, + pcmk__opt_based, + N_("Enable Access Control Lists (ACLs) for the CIB"), + NULL, + }, + { + PCMK_OPT_SYMMETRIC_CLUSTER, NULL, PCMK_VALUE_BOOLEAN, NULL, + PCMK_VALUE_TRUE, pcmk__valid_boolean, + pcmk__opt_schedulerd, + N_("Whether resources can run on any node by default"), + NULL, + }, + { + PCMK_OPT_MAINTENANCE_MODE, NULL, PCMK_VALUE_BOOLEAN, NULL, + PCMK_VALUE_FALSE, pcmk__valid_boolean, + pcmk__opt_schedulerd, + N_("Whether the cluster should refrain from monitoring, starting, and " + "stopping resources"), + NULL, + }, + { + PCMK_OPT_START_FAILURE_IS_FATAL, NULL, PCMK_VALUE_BOOLEAN, NULL, + PCMK_VALUE_TRUE, pcmk__valid_boolean, + pcmk__opt_schedulerd, + N_("Whether a start failure should prevent a resource from being " + "recovered on the same node"), + N_("When true, the cluster will immediately ban a resource from a node " + "if it fails to start there. When false, the cluster will instead " + "check the resource's fail count against its migration-threshold.") + }, + { + PCMK_OPT_ENABLE_STARTUP_PROBES, NULL, PCMK_VALUE_BOOLEAN, NULL, + PCMK_VALUE_TRUE, pcmk__valid_boolean, + pcmk__opt_schedulerd, + N_("Whether the cluster should check for active resources during " + "start-up"), + NULL, + }, + + // Fencing-related options + { + PCMK_OPT_STONITH_ENABLED, NULL, PCMK_VALUE_BOOLEAN, NULL, + PCMK_VALUE_TRUE, pcmk__valid_boolean, + pcmk__opt_schedulerd|pcmk__opt_advanced, + N_("Whether nodes may be fenced as part of recovery"), + N_("If false, unresponsive nodes are immediately assumed to be " + "harmless, and resources that were active on them may be recovered " + "elsewhere. This can result in a \"split-brain\" situation, " + "potentially leading to data loss and/or service unavailability."), + }, + { + PCMK_OPT_STONITH_ACTION, NULL, PCMK_VALUE_SELECT, + PCMK_ACTION_REBOOT ", " PCMK_ACTION_OFF ", " PCMK__ACTION_POWEROFF, + PCMK_ACTION_REBOOT, pcmk__is_fencing_action, + pcmk__opt_schedulerd, + N_("Action to send to fence device when a node needs to be fenced " + "(\"poweroff\" is a deprecated alias for \"off\")"), + NULL, + }, + { + PCMK_OPT_STONITH_TIMEOUT, NULL, PCMK_VALUE_DURATION, NULL, + "60s", pcmk__valid_interval_spec, + pcmk__opt_schedulerd, + N_("How long to wait for on, off, and reboot fence actions to complete " + "by default"), + NULL, + }, + { + PCMK_OPT_HAVE_WATCHDOG, NULL, PCMK_VALUE_BOOLEAN, NULL, + PCMK_VALUE_FALSE, pcmk__valid_boolean, + pcmk__opt_schedulerd|pcmk__opt_generated, + N_("Whether watchdog integration is enabled"), + N_("This is set automatically by the cluster according to whether SBD " + "is detected to be in use. User-configured values are ignored. " + "The value `true` is meaningful if diskless SBD is used and " + "`stonith-watchdog-timeout` is nonzero. In that case, if fencing " + "is required, watchdog-based self-fencing will be performed via " + "SBD without requiring a fencing resource explicitly configured."), + }, + { + /* @COMPAT Currently, unparsable values default to -1 (auto-calculate), + * while missing values default to 0 (disable). All values are accepted + * (unless the controller finds that the value conflicts with the + * SBD_WATCHDOG_TIMEOUT). + * + * At a compatibility break: properly validate as a timeout, let + * either negative values or a particular string like "auto" mean auto- + * calculate, and use 0 as the single default for when the option either + * is unset or fails to validate. + */ + PCMK_OPT_STONITH_WATCHDOG_TIMEOUT, NULL, PCMK_VALUE_TIMEOUT, NULL, + "0", NULL, + pcmk__opt_controld, + N_("How long before nodes can be assumed to be safely down when " + "watchdog-based self-fencing via SBD is in use"), + N_("If this is set to a positive value, lost nodes are assumed to " + "achieve self-fencing using watchdog-based SBD within this much " + "time. This does not require a fencing resource to be explicitly " + "configured, though a fence_watchdog resource can be configured, to " + "limit use to specific nodes. If this is set to 0 (the default), " + "the cluster will never assume watchdog-based self-fencing. If this " + "is set to a negative value, the cluster will use twice the local " + "value of the `SBD_WATCHDOG_TIMEOUT` environment variable if that " + "is positive, or otherwise treat this as 0. WARNING: When used, " + "this timeout must be larger than `SBD_WATCHDOG_TIMEOUT` on all " + "nodes that use watchdog-based SBD, and Pacemaker will refuse to " + "start on any of those nodes where this is not true for the local " + "value or SBD is not active. When this is set to a negative value, " + "`SBD_WATCHDOG_TIMEOUT` must be set to the same value on all nodes " + "that use SBD, otherwise data corruption or loss could occur."), + }, + { + PCMK_OPT_STONITH_MAX_ATTEMPTS, NULL, PCMK_VALUE_SCORE, NULL, + "10", pcmk__valid_positive_int, + pcmk__opt_controld, + N_("How many times fencing can fail before it will no longer be " + "immediately re-attempted on a target"), + NULL, + }, + { + PCMK_OPT_CONCURRENT_FENCING, NULL, PCMK_VALUE_BOOLEAN, NULL, + PCMK__CONCURRENT_FENCING_DEFAULT, pcmk__valid_boolean, + pcmk__opt_schedulerd, + N_("Allow performing fencing operations in parallel"), + NULL, + }, + { + PCMK_OPT_STARTUP_FENCING, NULL, PCMK_VALUE_BOOLEAN, NULL, + PCMK_VALUE_TRUE, pcmk__valid_boolean, + pcmk__opt_schedulerd|pcmk__opt_advanced, + N_("Whether to fence unseen nodes at start-up"), + N_("Setting this to false may lead to a \"split-brain\" situation, " + "potentially leading to data loss and/or service unavailability."), + }, + { + PCMK_OPT_PRIORITY_FENCING_DELAY, NULL, PCMK_VALUE_DURATION, NULL, + "0", pcmk__valid_interval_spec, + pcmk__opt_schedulerd, + N_("Apply fencing delay targeting the lost nodes with the highest " + "total resource priority"), + N_("Apply specified delay for the fencings that are targeting the lost " + "nodes with the highest total resource priority in case we don't " + "have the majority of the nodes in our cluster partition, so that " + "the more significant nodes potentially win any fencing match, " + "which is especially meaningful under split-brain of 2-node " + "cluster. A promoted resource instance takes the base priority + 1 " + "on calculation if the base priority is not 0. Any static/random " + "delays that are introduced by `pcmk_delay_base/max` configured " + "for the corresponding fencing resources will be added to this " + "delay. This delay should be significantly greater than, safely " + "twice, the maximum `pcmk_delay_base/max`. By default, priority " + "fencing delay is disabled."), + }, + { + PCMK_OPT_NODE_PENDING_TIMEOUT, NULL, PCMK_VALUE_DURATION, NULL, + "0", pcmk__valid_interval_spec, + pcmk__opt_schedulerd, + N_("How long to wait for a node that has joined the cluster to join " + "the controller process group"), + N_("Fence nodes that do not join the controller process group within " + "this much time after joining the cluster, to allow the cluster " + "to continue managing resources. A value of 0 means never fence " + "pending nodes. Setting the value to 2h means fence nodes after " + "2 hours."), + }, + { + PCMK_OPT_CLUSTER_DELAY, NULL, PCMK_VALUE_DURATION, NULL, + "60s", pcmk__valid_interval_spec, + pcmk__opt_schedulerd, + N_("Maximum time for node-to-node communication"), + N_("The node elected Designated Controller (DC) will consider an action " + "failed if it does not get a response from the node executing the " + "action within this time (after considering the action's own " + "timeout). The \"correct\" value will depend on the speed and " + "load of your network and cluster nodes.") + }, + + // Limits + { + PCMK_OPT_LOAD_THRESHOLD, NULL, PCMK_VALUE_PERCENTAGE, NULL, + "80%", pcmk__valid_percentage, + pcmk__opt_controld, + N_("Maximum amount of system load that should be used by cluster " + "nodes"), + N_("The cluster will slow down its recovery process when the amount of " + "system resources used (currently CPU) approaches this limit"), + }, + { + PCMK_OPT_NODE_ACTION_LIMIT, NULL, PCMK_VALUE_INTEGER, NULL, + "0", pcmk__valid_int, + pcmk__opt_controld, + N_("Maximum number of jobs that can be scheduled per node (defaults to " + "2x cores)"), + NULL, + }, + { + PCMK_OPT_BATCH_LIMIT, NULL, PCMK_VALUE_INTEGER, NULL, + "0", pcmk__valid_int, + pcmk__opt_schedulerd, + N_("Maximum number of jobs that the cluster may execute in parallel " + "across all nodes"), + N_("The \"correct\" value will depend on the speed and load of your " + "network and cluster nodes. If set to 0, the cluster will " + "impose a dynamically calculated limit when any node has a " + "high load."), + }, + { + PCMK_OPT_MIGRATION_LIMIT, NULL, PCMK_VALUE_INTEGER, NULL, + "-1", pcmk__valid_int, + pcmk__opt_schedulerd, + N_("The number of live migration actions that the cluster is allowed " + "to execute in parallel on a node (-1 means no limit)"), + NULL, + }, + { + /* @TODO This is actually ignored if not strictly positive. We should + * overhaul value types in Pacemaker Explained. There are lots of + * inaccurate ranges (assumptions of 32-bit width, "nonnegative" when + * positive is required, etc.). + * + * Maybe a single integer type with the allowed range specified would be + * better. + * + * Drop the PCMK_VALUE_NONNEGATIVE_INTEGER constant if we do this before + * a release. + */ + PCMK_OPT_CLUSTER_IPC_LIMIT, NULL, PCMK_VALUE_NONNEGATIVE_INTEGER, NULL, + "500", pcmk__valid_positive_int, + pcmk__opt_based, + N_("Maximum IPC message backlog before disconnecting a cluster daemon"), + N_("Raise this if log has \"Evicting client\" messages for cluster " + "daemon PIDs (a good value is the number of resources in the " + "cluster multiplied by the number of nodes)."), + }, + + // Orphans and stopping + { + PCMK_OPT_STOP_ALL_RESOURCES, NULL, PCMK_VALUE_BOOLEAN, NULL, + PCMK_VALUE_FALSE, pcmk__valid_boolean, + pcmk__opt_schedulerd, + N_("Whether the cluster should stop all active resources"), + NULL, + }, + { + PCMK_OPT_STOP_ORPHAN_RESOURCES, NULL, PCMK_VALUE_BOOLEAN, NULL, + PCMK_VALUE_TRUE, pcmk__valid_boolean, + pcmk__opt_schedulerd, + N_("Whether to stop resources that were removed from the " + "configuration"), + NULL, + }, + { + PCMK_OPT_STOP_ORPHAN_ACTIONS, NULL, PCMK_VALUE_BOOLEAN, NULL, + PCMK_VALUE_TRUE, pcmk__valid_boolean, + pcmk__opt_schedulerd, + N_("Whether to cancel recurring actions removed from the " + "configuration"), + NULL, + }, + { + PCMK__OPT_REMOVE_AFTER_STOP, NULL, PCMK_VALUE_BOOLEAN, NULL, + PCMK_VALUE_FALSE, pcmk__valid_boolean, + pcmk__opt_schedulerd|pcmk__opt_deprecated, + N_("Whether to remove stopped resources from the executor"), + N_("Values other than default are poorly tested and potentially " + "dangerous."), + }, + + // Storing inputs + { + PCMK_OPT_PE_ERROR_SERIES_MAX, NULL, PCMK_VALUE_INTEGER, NULL, + "-1", pcmk__valid_int, + pcmk__opt_schedulerd, + N_("The number of scheduler inputs resulting in errors to save"), + N_("Zero to disable, -1 to store unlimited."), + }, + { + PCMK_OPT_PE_WARN_SERIES_MAX, NULL, PCMK_VALUE_INTEGER, NULL, + "5000", pcmk__valid_int, + pcmk__opt_schedulerd, + N_("The number of scheduler inputs resulting in warnings to save"), + N_("Zero to disable, -1 to store unlimited."), + }, + { + PCMK_OPT_PE_INPUT_SERIES_MAX, NULL, PCMK_VALUE_INTEGER, NULL, + "4000", pcmk__valid_int, + pcmk__opt_schedulerd, + N_("The number of scheduler inputs without errors or warnings to save"), + N_("Zero to disable, -1 to store unlimited."), + }, + + // Node health + { + PCMK_OPT_NODE_HEALTH_STRATEGY, NULL, PCMK_VALUE_SELECT, + PCMK_VALUE_NONE ", " PCMK_VALUE_MIGRATE_ON_RED ", " + PCMK_VALUE_ONLY_GREEN ", " PCMK_VALUE_PROGRESSIVE ", " + PCMK_VALUE_CUSTOM, + PCMK_VALUE_NONE, pcmk__validate_health_strategy, + pcmk__opt_schedulerd, + N_("How cluster should react to node health attributes"), + N_("Requires external entities to create node attributes (named with " + "the prefix \"#health\") with values \"red\", \"yellow\", or " + "\"green\".") + }, + { + PCMK_OPT_NODE_HEALTH_BASE, NULL, PCMK_VALUE_SCORE, NULL, + "0", pcmk__valid_int, + pcmk__opt_schedulerd, + N_("Base health score assigned to a node"), + N_("Only used when \"node-health-strategy\" is set to " + "\"progressive\"."), + }, + { + PCMK_OPT_NODE_HEALTH_GREEN, NULL, PCMK_VALUE_SCORE, NULL, + "0", pcmk__valid_int, + pcmk__opt_schedulerd, + N_("The score to use for a node health attribute whose value is " + "\"green\""), + N_("Only used when \"node-health-strategy\" is set to \"custom\" or " + "\"progressive\"."), + }, + { + PCMK_OPT_NODE_HEALTH_YELLOW, NULL, PCMK_VALUE_SCORE, NULL, + "0", pcmk__valid_int, + pcmk__opt_schedulerd, + N_("The score to use for a node health attribute whose value is " + "\"yellow\""), + N_("Only used when \"node-health-strategy\" is set to \"custom\" or " + "\"progressive\"."), + }, + { + PCMK_OPT_NODE_HEALTH_RED, NULL, PCMK_VALUE_SCORE, NULL, + "-INFINITY", pcmk__valid_int, + pcmk__opt_schedulerd, + N_("The score to use for a node health attribute whose value is " + "\"red\""), + N_("Only used when \"node-health-strategy\" is set to \"custom\" or " + "\"progressive\".") + }, + + // Placement strategy + { + PCMK_OPT_PLACEMENT_STRATEGY, NULL, PCMK_VALUE_SELECT, + PCMK_VALUE_DEFAULT ", " PCMK_VALUE_UTILIZATION ", " + PCMK_VALUE_MINIMAL ", " PCMK_VALUE_BALANCED, + PCMK_VALUE_DEFAULT, pcmk__valid_placement_strategy, + pcmk__opt_schedulerd, + N_("How the cluster should allocate resources to nodes"), + NULL, + }, + + { NULL, }, +}; + +static const pcmk__cluster_option_t fencing_params[] = { + /* name, old name, type, allowed values, + * default value, validator, + * flags, + * short description, + * long description + */ + { + PCMK_STONITH_HOST_ARGUMENT, NULL, PCMK_VALUE_STRING, NULL, + "port", NULL, + pcmk__opt_advanced, + N_("An alternate parameter to supply instead of 'port'"), + N_("Some devices do not support the standard 'port' parameter or may " + "provide additional ones. Use this to specify an alternate, device-" + "specific, parameter that should indicate the machine to be " + "fenced. A value of \"none\" can be used to tell the cluster not " + "to supply any additional parameters."), + }, + { + PCMK_STONITH_HOST_MAP, NULL, PCMK_VALUE_STRING, NULL, + NULL, NULL, + pcmk__opt_none, + N_("A mapping of node names to port numbers for devices that do not " + "support node names."), + N_("For example, \"node1:1;node2:2,3\" would tell the cluster to use " + "port 1 for node1 and ports 2 and 3 for node2."), + }, + { + PCMK_STONITH_HOST_LIST, NULL, PCMK_VALUE_STRING, NULL, + NULL, NULL, + pcmk__opt_none, + N_("Nodes targeted by this device"), + N_("Comma-separated list of nodes that can be targeted by this device " + "(for example, \"node1,node2,node3\"). If pcmk_host_check is " + "\"static-list\", either this or pcmk_host_map must be set."), + }, + { + PCMK_STONITH_HOST_CHECK, NULL, PCMK_VALUE_SELECT, + PCMK_VALUE_DYNAMIC_LIST ", " PCMK_VALUE_STATIC_LIST ", " + PCMK_VALUE_STATUS ", " PCMK_VALUE_NONE, + NULL, NULL, + pcmk__opt_none, + N_("How to determine which nodes can be targeted by the device"), + N_("Use \"dynamic-list\" to query the device via the 'list' command; " + "\"static-list\" to check the pcmk_host_list attribute; " + "\"status\" to query the device via the 'status' command; or " + "\"none\" to assume every device can fence every node. " + "The default value is \"static-list\" if pcmk_host_map or " + "pcmk_host_list is set; otherwise \"dynamic-list\" if the device " + "supports the list operation; otherwise \"status\" if the device " + "supports the status operation; otherwise \"none\""), + }, + { + PCMK_STONITH_DELAY_MAX, NULL, PCMK_VALUE_DURATION, NULL, + "0s", NULL, + pcmk__opt_none, + N_("Enable a delay of no more than the time specified before executing " + "fencing actions."), + N_("Enable a delay of no more than the time specified before executing " + "fencing actions. Pacemaker derives the overall delay by taking " + "the value of pcmk_delay_base and adding a random delay value such " + "that the sum is kept below this maximum."), + }, + { + PCMK_STONITH_DELAY_BASE, NULL, PCMK_VALUE_STRING, NULL, + "0s", NULL, + pcmk__opt_none, + N_("Enable a base delay for fencing actions and specify base delay " + "value."), + N_("This enables a static delay for fencing actions, which can help " + "avoid \"death matches\" where two nodes try to fence each other " + "at the same time. If pcmk_delay_max is also used, a random delay " + "will be added such that the total delay is kept below that value. " + "This can be set to a single time value to apply to any node " + "targeted by this device (useful if a separate device is " + "configured for each target), or to a node map (for example, " + "\"node1:1s;node2:5\") to set a different value for each target."), + }, + { + PCMK_STONITH_ACTION_LIMIT, NULL, PCMK_VALUE_INTEGER, NULL, + "1", NULL, + pcmk__opt_none, + N_("The maximum number of actions can be performed in parallel on this " + "device"), + N_("Cluster property concurrent-fencing=\"true\" needs to be " + "configured first. Then use this to specify the maximum number of " + "actions can be performed in parallel on this device. A value of " + "-1 means an unlimited number of actions can be performed in " + "parallel."), + }, + { + "pcmk_reboot_action", NULL, PCMK_VALUE_STRING, NULL, + PCMK_ACTION_REBOOT, NULL, + pcmk__opt_advanced, + N_("An alternate command to run instead of 'reboot'"), + N_("Some devices do not support the standard commands or may provide " + "additional ones. Use this to specify an alternate, device-" + "specific, command that implements the 'reboot' action."), + }, + { + "pcmk_reboot_timeout", NULL, PCMK_VALUE_TIMEOUT, NULL, + "60s", NULL, + pcmk__opt_advanced, + N_("Specify an alternate timeout to use for 'reboot' actions instead " + "of stonith-timeout"), + N_("Some devices need much more/less time to complete than normal. " + "Use this to specify an alternate, device-specific, timeout for " + "'reboot' actions."), + }, + { + "pcmk_reboot_retries", NULL, PCMK_VALUE_INTEGER, NULL, + "2", NULL, + pcmk__opt_advanced, + N_("The maximum number of times to try the 'reboot' command within the " + "timeout period"), + N_("Some devices do not support multiple connections. Operations may " + "\"fail\" if the device is busy with another task. In that case, " + "Pacemaker will automatically retry the operation if there is time " + "remaining. Use this option to alter the number of times Pacemaker " + "tries a 'reboot' action before giving up."), + }, + { + "pcmk_off_action", NULL, PCMK_VALUE_STRING, NULL, + PCMK_ACTION_OFF, NULL, + pcmk__opt_advanced, + N_("An alternate command to run instead of 'off'"), + N_("Some devices do not support the standard commands or may provide " + "additional ones. Use this to specify an alternate, device-" + "specific, command that implements the 'off' action."), + }, + { + "pcmk_off_timeout", NULL, PCMK_VALUE_TIMEOUT, NULL, + "60s", NULL, + pcmk__opt_advanced, + N_("Specify an alternate timeout to use for 'off' actions instead of " + "stonith-timeout"), + N_("Some devices need much more/less time to complete than normal. " + "Use this to specify an alternate, device-specific, timeout for " + "'off' actions."), + }, + { + "pcmk_off_retries", NULL, PCMK_VALUE_INTEGER, NULL, + "2", NULL, + pcmk__opt_advanced, + N_("The maximum number of times to try the 'off' command within the " + "timeout period"), + N_("Some devices do not support multiple connections. Operations may " + "\"fail\" if the device is busy with another task. In that case, " + "Pacemaker will automatically retry the operation if there is time " + "remaining. Use this option to alter the number of times Pacemaker " + "tries a 'off' action before giving up."), + }, + { + "pcmk_on_action", NULL, PCMK_VALUE_STRING, NULL, + PCMK_ACTION_ON, NULL, + pcmk__opt_advanced, + N_("An alternate command to run instead of 'on'"), + N_("Some devices do not support the standard commands or may provide " + "additional ones. Use this to specify an alternate, device-" + "specific, command that implements the 'on' action."), + }, + { + "pcmk_on_timeout", NULL, PCMK_VALUE_TIMEOUT, NULL, + "60s", NULL, + pcmk__opt_advanced, + N_("Specify an alternate timeout to use for 'on' actions instead of " + "stonith-timeout"), + N_("Some devices need much more/less time to complete than normal. " + "Use this to specify an alternate, device-specific, timeout for " + "'on' actions."), + }, + { + "pcmk_on_retries", NULL, PCMK_VALUE_INTEGER, NULL, + "2", NULL, + pcmk__opt_advanced, + N_("The maximum number of times to try the 'on' command within the " + "timeout period"), + N_("Some devices do not support multiple connections. Operations may " + "\"fail\" if the device is busy with another task. In that case, " + "Pacemaker will automatically retry the operation if there is time " + "remaining. Use this option to alter the number of times Pacemaker " + "tries a 'on' action before giving up."), + }, + { + "pcmk_list_action", NULL, PCMK_VALUE_STRING, NULL, + PCMK_ACTION_LIST, NULL, + pcmk__opt_advanced, + N_("An alternate command to run instead of 'list'"), + N_("Some devices do not support the standard commands or may provide " + "additional ones. Use this to specify an alternate, device-" + "specific, command that implements the 'list' action."), + }, + { + "pcmk_list_timeout", NULL, PCMK_VALUE_TIMEOUT, NULL, + "60s", NULL, + pcmk__opt_advanced, + N_("Specify an alternate timeout to use for 'list' actions instead of " + "stonith-timeout"), + N_("Some devices need much more/less time to complete than normal. " + "Use this to specify an alternate, device-specific, timeout for " + "'list' actions."), + }, + { + "pcmk_list_retries", NULL, PCMK_VALUE_INTEGER, NULL, + "2", NULL, + pcmk__opt_advanced, + N_("The maximum number of times to try the 'list' command within the " + "timeout period"), + N_("Some devices do not support multiple connections. Operations may " + "\"fail\" if the device is busy with another task. In that case, " + "Pacemaker will automatically retry the operation if there is time " + "remaining. Use this option to alter the number of times Pacemaker " + "tries a 'list' action before giving up."), + }, + { + "pcmk_monitor_action", NULL, PCMK_VALUE_STRING, NULL, + PCMK_ACTION_MONITOR, NULL, + pcmk__opt_advanced, + N_("An alternate command to run instead of 'monitor'"), + N_("Some devices do not support the standard commands or may provide " + "additional ones. Use this to specify an alternate, device-" + "specific, command that implements the 'monitor' action."), + }, + { + "pcmk_monitor_timeout", NULL, PCMK_VALUE_TIMEOUT, NULL, + "60s", NULL, + pcmk__opt_advanced, + N_("Specify an alternate timeout to use for 'monitor' actions instead " + "of stonith-timeout"), + N_("Some devices need much more/less time to complete than normal. " + "Use this to specify an alternate, device-specific, timeout for " + "'monitor' actions."), + }, + { + "pcmk_monitor_retries", NULL, PCMK_VALUE_INTEGER, NULL, + "2", NULL, + pcmk__opt_advanced, + N_("The maximum number of times to try the 'monitor' command within " + "the timeout period"), + N_("Some devices do not support multiple connections. Operations may " + "\"fail\" if the device is busy with another task. In that case, " + "Pacemaker will automatically retry the operation if there is time " + "remaining. Use this option to alter the number of times Pacemaker " + "tries a 'monitor' action before giving up."), + }, + { + "pcmk_status_action", NULL, PCMK_VALUE_STRING, NULL, + PCMK_ACTION_STATUS, NULL, + pcmk__opt_advanced, + N_("An alternate command to run instead of 'status'"), + N_("Some devices do not support the standard commands or may provide " + "additional ones. Use this to specify an alternate, device-" + "specific, command that implements the 'status' action."), + }, + { + "pcmk_status_timeout", NULL, PCMK_VALUE_TIMEOUT, NULL, + "60s", NULL, + pcmk__opt_advanced, + N_("Specify an alternate timeout to use for 'status' actions instead " + "of stonith-timeout"), + N_("Some devices need much more/less time to complete than normal. " + "Use this to specify an alternate, device-specific, timeout for " + "'status' actions."), + }, + { + "pcmk_status_retries", NULL, PCMK_VALUE_INTEGER, NULL, + "2", NULL, + pcmk__opt_advanced, + N_("The maximum number of times to try the 'status' command within " + "the timeout period"), + N_("Some devices do not support multiple connections. Operations may " + "\"fail\" if the device is busy with another task. In that case, " + "Pacemaker will automatically retry the operation if there is time " + "remaining. Use this option to alter the number of times Pacemaker " + "tries a 'status' action before giving up."), + }, + + { NULL, }, +}; + +static const pcmk__cluster_option_t primitive_meta[] = { + /* name, old name, type, allowed values, + * default value, validator, + * flags, + * short description, + * long description + */ + { + PCMK_META_PRIORITY, NULL, PCMK_VALUE_SCORE, NULL, + "0", NULL, + pcmk__opt_none, + N_("Resource assignment priority"), + N_("If not all resources can be active, the cluster will stop " + "lower-priority resources in order to keep higher-priority ones " + "active."), + }, + { + PCMK_META_CRITICAL, NULL, PCMK_VALUE_BOOLEAN, NULL, + PCMK_VALUE_TRUE, NULL, + pcmk__opt_none, + N_("Default value for influence in colocation constraints"), + N_("Use this value as the default for influence in all colocation " + "constraints involving this resource, as well as in the implicit " + "colocation constraints created if this resource is in a group."), + }, + { + PCMK_META_TARGET_ROLE, NULL, PCMK_VALUE_SELECT, + PCMK_ROLE_STOPPED ", " PCMK_ROLE_STARTED ", " + PCMK_ROLE_UNPROMOTED ", " PCMK_ROLE_PROMOTED, + PCMK_ROLE_STARTED, NULL, + pcmk__opt_none, + N_("State the cluster should attempt to keep this resource in"), + N_("\"Stopped\" forces the resource to be stopped. " + "\"Started\" allows the resource to be started (and in the case of " + "promotable clone resources, promoted if appropriate). " + "\"Unpromoted\" allows the resource to be started, but only in the " + "unpromoted role if the resource is promotable. " + "\"Promoted\" is equivalent to \"Started\"."), + }, + { + PCMK_META_IS_MANAGED, NULL, PCMK_VALUE_BOOLEAN, NULL, + PCMK_VALUE_TRUE, NULL, + pcmk__opt_none, + N_("Whether the cluster is allowed to actively change the resource's " + "state"), + N_("If false, the cluster will not start, stop, promote, or demote the " + "resource on any node. Recurring actions for the resource are " + "unaffected. If true, a true value for the maintenance-mode " + "cluster option, the maintenance node attribute, or the " + "maintenance resource meta-attribute overrides this."), + }, + { + PCMK_META_MAINTENANCE, NULL, PCMK_VALUE_BOOLEAN, NULL, + PCMK_VALUE_FALSE, NULL, + pcmk__opt_none, + N_("If true, the cluster will not schedule any actions involving the " + "resource"), + N_("If true, the cluster will not start, stop, promote, or demote the " + "resource on any node, and will pause any recurring monitors " + "(except those specifying role as \"Stopped\"). If false, a true " + "value for the maintenance-mode cluster option or maintenance node " + "attribute overrides this."), + }, + { + PCMK_META_RESOURCE_STICKINESS, NULL, PCMK_VALUE_SCORE, NULL, + NULL, NULL, + pcmk__opt_none, + N_("Score to add to the current node when a resource is already " + "active"), + N_("Score to add to the current node when a resource is already " + "active. This allows running resources to stay where they are, " + "even if they would be placed elsewhere if they were being started " + "from a stopped state. " + "The default is 1 for individual clone instances, and 0 for all " + "other resources."), + }, + { + PCMK_META_REQUIRES, NULL, PCMK_VALUE_SELECT, + PCMK_VALUE_NOTHING ", " PCMK_VALUE_QUORUM ", " + PCMK_VALUE_FENCING ", " PCMK_VALUE_UNFENCING, + NULL, NULL, + pcmk__opt_none, + N_("Conditions under which the resource can be started"), + N_("Conditions under which the resource can be started. " + "\"nothing\" means the cluster can always start this resource. " + "\"quorum\" means the cluster can start this resource only if a " + "majority of the configured nodes are active. " + "\"fencing\" means the cluster can start this resource only if a " + "majority of the configured nodes are active and any failed or " + "unknown nodes have been fenced. " + "\"unfencing\" means the cluster can start this resource only if " + "a majority of the configured nodes are active and any failed or " + "unknown nodes have been fenced, and only on nodes that have been " + "unfenced. " + "The default is \"quorum\" for resources with a class of stonith; " + "otherwise, \"unfencing\" if unfencing is active in the cluster; " + "otherwise, \"fencing\" if the stonith-enabled cluster option is " + "true; " + "otherwise, \"quorum\"."), + }, + { + PCMK_META_MIGRATION_THRESHOLD, NULL, PCMK_VALUE_SCORE, NULL, + PCMK_VALUE_INFINITY, NULL, + pcmk__opt_none, + N_("Number of failures on a node before the resource becomes " + "ineligible to run there."), + N_("Number of failures that may occur for this resource on a node, " + "before that node is marked ineligible to host this resource. A " + "value of 0 indicates that this feature is disabled (the node will " + "never be marked ineligible). By contrast, the cluster treats " + "\"INFINITY\" (the default) as a very large but finite number. " + "This option has an effect only if the failed operation specifies " + "its on-fail attribute as \"restart\" (the default), and " + "additionally for failed start operations, if the " + "start-failure-is-fatal cluster property is set to false."), + }, + { + PCMK_META_FAILURE_TIMEOUT, NULL, PCMK_VALUE_DURATION, NULL, + "0", NULL, + pcmk__opt_none, + N_("Number of seconds before acting as if a failure had not occurred"), + N_("Number of seconds after a failed action for this resource before " + "acting as if the failure had not occurred, and potentially " + "allowing the resource back to the node on which it failed. " + "A value of 0 indicates that this feature is disabled."), + }, + { + PCMK_META_MULTIPLE_ACTIVE, NULL, PCMK_VALUE_SELECT, + PCMK_VALUE_BLOCK ", " PCMK_VALUE_STOP_ONLY ", " + PCMK_VALUE_STOP_START ", " PCMK_VALUE_STOP_UNEXPECTED, + PCMK_VALUE_STOP_START, NULL, + pcmk__opt_none, + N_("What to do if the cluster finds the resource active on more than " + "one node"), + N_("What to do if the cluster finds the resource active on more than " + "one node. " + "\"block\" means to mark the resource as unmanaged. " + "\"stop_only\" means to stop all active instances of this resource " + "and leave them stopped. " + "\"stop_start\" means to stop all active instances of this " + "resource and start the resource in one location only. " + "\"stop_unexpected\" means to stop all active instances of this " + "resource except where the resource should be active. (This should " + "be used only when extra instances are not expected to disrupt " + "existing instances, and the resource agent's monitor of an " + "existing instance is capable of detecting any problems that could " + "be caused. Note that any resources ordered after this one will " + "still need to be restarted.)"), + }, + { + PCMK_META_ALLOW_MIGRATE, NULL, PCMK_VALUE_BOOLEAN, NULL, + NULL, NULL, + pcmk__opt_none, + N_("Whether the cluster should try to \"live migrate\" this resource " + "when it needs to be moved"), + N_("Whether the cluster should try to \"live migrate\" this resource " + "when it needs to be moved. " + "The default is true for ocf:pacemaker:remote resources, and false " + "otherwise."), + }, + { + PCMK_META_ALLOW_UNHEALTHY_NODES, NULL, PCMK_VALUE_BOOLEAN, NULL, + PCMK_VALUE_FALSE, NULL, + pcmk__opt_none, + N_("Whether the resource should be allowed to run on a node even if " + "the node's health score would otherwise prevent it"), + NULL, + }, + { + PCMK_META_CONTAINER_ATTRIBUTE_TARGET, NULL, PCMK_VALUE_STRING, NULL, + NULL, NULL, + pcmk__opt_none, + N_("Where to check user-defined node attributes"), + N_("Whether to check user-defined node attributes on the physical host " + "where a container is running or on the local node. This is " + "usually set for a bundle resource and inherited by the bundle's " + "primitive resource. " + "A value of \"host\" means to check user-defined node attributes " + "on the underlying physical host. Any other value means to check " + "user-defined node attributes on the local node (for a bundled " + "primitive resource, this is the bundle node)."), + }, + { + PCMK_META_REMOTE_NODE, NULL, PCMK_VALUE_STRING, NULL, + NULL, NULL, + pcmk__opt_none, + N_("Name of the Pacemaker Remote guest node this resource is " + "associated with, if any"), + N_("Name of the Pacemaker Remote guest node this resource is " + "associated with, if any. If specified, this both enables the " + "resource as a guest node and defines the unique name used to " + "identify the guest node. The guest must be configured to run the " + "Pacemaker Remote daemon when it is started. " + "WARNING: This value cannot overlap with any resource or node " + "IDs."), + }, + { + PCMK_META_REMOTE_ADDR, NULL, PCMK_VALUE_STRING, NULL, + NULL, NULL, + pcmk__opt_none, + N_("If remote-node is specified, the IP address or hostname used to " + "connect to the guest via Pacemaker Remote"), + N_("If remote-node is specified, the IP address or hostname used to " + "connect to the guest via Pacemaker Remote. The Pacemaker Remote " + "daemon on the guest must be configured to accept connections on " + "this address. " + "The default is the value of the remote-node meta-attribute."), + }, + { + PCMK_META_REMOTE_PORT, NULL, PCMK_VALUE_PORT, NULL, + "3121", NULL, + pcmk__opt_none, + N_("If remote-node is specified, port on the guest used for its " + "Pacemaker Remote connection"), + N_("If remote-node is specified, the port on the guest used for its " + "Pacemaker Remote connection. The Pacemaker Remote daemon on the " + "guest must be configured to listen on this port."), + }, + { + PCMK_META_REMOTE_CONNECT_TIMEOUT, NULL, PCMK_VALUE_TIMEOUT, NULL, + "60s", NULL, + pcmk__opt_none, + N_("If remote-node is specified, how long before a pending Pacemaker " + "Remote guest connection times out."), + NULL, + }, + { + PCMK_META_REMOTE_ALLOW_MIGRATE, NULL, PCMK_VALUE_BOOLEAN, NULL, + PCMK_VALUE_TRUE, NULL, + pcmk__opt_none, + N_("If remote-node is specified, this acts as the allow-migrate " + "meta-attribute for the implicit remote connection resource " + "(ocf:pacemaker:remote)."), + NULL, + }, + + { NULL, }, +}; + +/* * Environment variable option handling */ @@ -176,161 +1206,195 @@ pcmk__env_option_enabled(const char *daemon, const char *option) * Cluster option handling */ +/*! + * \internal + * \brief Check whether a string represents a valid interval specification + * + * \param[in] value String to validate + * + * \return \c true if \p value is a valid interval specification, or \c false + * otherwise + */ bool pcmk__valid_interval_spec(const char *value) { - (void) crm_parse_interval_spec(value); - return errno == 0; + return pcmk_parse_interval_spec(value, NULL) == pcmk_rc_ok; } +/*! + * \internal + * \brief Check whether a string represents a valid boolean value + * + * \param[in] value String to validate + * + * \return \c true if \p value is a valid boolean value, or \c false otherwise + */ bool pcmk__valid_boolean(const char *value) { - int tmp; - - return crm_str_to_boolean(value, &tmp) == 1; + return crm_str_to_boolean(value, NULL) == 1; } +/*! + * \internal + * \brief Check whether a string represents a valid integer + * + * Valid values include \c INFINITY, \c -INFINITY, and all 64-bit integers. + * + * \param[in] value String to validate + * + * \return \c true if \p value is a valid integer, or \c false otherwise + */ bool -pcmk__valid_number(const char *value) +pcmk__valid_int(const char *value) { - if (value == NULL) { - return false; - - } else if (pcmk_str_is_minus_infinity(value) || - pcmk_str_is_infinity(value)) { - return true; - } - - return pcmk__scan_ll(value, NULL, 0LL) == pcmk_rc_ok; + return (value != NULL) + && (pcmk_str_is_infinity(value) + || pcmk_str_is_minus_infinity(value) + || (pcmk__scan_ll(value, NULL, 0LL) == pcmk_rc_ok)); } +/*! + * \internal + * \brief Check whether a string represents a valid positive integer + * + * Valid values include \c INFINITY and all 64-bit positive integers. + * + * \param[in] value String to validate + * + * \return \c true if \p value is a valid positive integer, or \c false + * otherwise + */ bool -pcmk__valid_positive_number(const char *value) +pcmk__valid_positive_int(const char *value) { long long num = 0LL; return pcmk_str_is_infinity(value) - || ((pcmk__scan_ll(value, &num, 0LL) == pcmk_rc_ok) && (num > 0)); + || ((pcmk__scan_ll(value, &num, 0LL) == pcmk_rc_ok) + && (num > 0)); } +/*! + * \internal + * \brief Check whether a string represents a valid + * \c PCMK__OPT_NO_QUORUM_POLICY value + * + * \param[in] value String to validate + * + * \return \c true if \p value is a valid \c PCMK__OPT_NO_QUORUM_POLICY value, + * or \c false otherwise + */ bool -pcmk__valid_quorum(const char *value) +pcmk__valid_no_quorum_policy(const char *value) { - return pcmk__strcase_any_of(value, "stop", "freeze", "ignore", "demote", "suicide", NULL); + return pcmk__strcase_any_of(value, + PCMK_VALUE_STOP, PCMK_VALUE_FREEZE, + PCMK_VALUE_IGNORE, PCMK_VALUE_DEMOTE, + PCMK_VALUE_FENCE_LEGACY, NULL); } +/*! + * \internal + * \brief Check whether a string represents a valid percentage + * + * Valid values include long integers, with an optional trailing string + * beginning with '%'. + * + * \param[in] value String to validate + * + * \return \c true if \p value is a valid percentage value, or \c false + * otherwise + */ bool -pcmk__valid_script(const char *value) +pcmk__valid_percentage(const char *value) { - struct stat st; - - if (pcmk__str_eq(value, "/dev/null", pcmk__str_casei)) { - return true; - } - - if (stat(value, &st) != 0) { - crm_err("Script %s does not exist", value); - return false; - } - - if (S_ISREG(st.st_mode) == 0) { - crm_err("Script %s is not a regular file", value); - return false; - } - - if ((st.st_mode & (S_IXUSR | S_IXGRP)) == 0) { - crm_err("Script %s is not executable", value); - return false; - } + char *end = NULL; + float number = strtof(value, &end); - return true; + return ((end == NULL) || (end[0] == '%')) && (number >= 0); } +/*! + * \internal + * \brief Check whether a string represents a valid placement strategy + * + * \param[in] value String to validate + * + * \return \c true if \p value is a valid placement strategy, or \c false + * otherwise + */ bool -pcmk__valid_percentage(const char *value) +pcmk__valid_placement_strategy(const char *value) { - char *end = NULL; - long number = strtol(value, &end, 10); - - if (end && (end[0] != '%')) { - return false; - } - return number >= 0; + return pcmk__strcase_any_of(value, + PCMK_VALUE_DEFAULT, PCMK_VALUE_UTILIZATION, + PCMK_VALUE_MINIMAL, PCMK_VALUE_BALANCED, NULL); } /*! * \internal * \brief Check a table of configured options for a particular option * - * \param[in,out] options Name/value pairs for configured options - * \param[in] validate If not NULL, validator function for option value - * \param[in] name Option name to look for - * \param[in] old_name Alternative option name to look for - * \param[in] def_value Default to use if option not configured + * \param[in,out] table Name/value pairs for configured options + * \param[in] option Option to look up * * \return Option value (from supplied options table or default value) */ static const char * -cluster_option_value(GHashTable *options, bool (*validate)(const char *), - const char *name, const char *old_name, - const char *def_value) +cluster_option_value(GHashTable *table, const pcmk__cluster_option_t *option) { const char *value = NULL; - char *new_value = NULL; - CRM_ASSERT(name != NULL); + CRM_ASSERT((option != NULL) && (option->name != NULL)); - if (options) { - value = g_hash_table_lookup(options, name); + if (table != NULL) { + value = g_hash_table_lookup(table, option->name); - if ((value == NULL) && old_name) { - value = g_hash_table_lookup(options, old_name); + if ((value == NULL) && (option->alt_name != NULL)) { + value = g_hash_table_lookup(table, option->alt_name); if (value != NULL) { pcmk__config_warn("Support for legacy name '%s' for cluster " "option '%s' is deprecated and will be " "removed in a future release", - old_name, name); + option->alt_name, option->name); // Inserting copy with current name ensures we only warn once - new_value = strdup(value); - g_hash_table_insert(options, strdup(name), new_value); - value = new_value; + pcmk__insert_dup(table, option->name, value); } } - if (value && validate && (validate(value) == FALSE)) { + if ((value != NULL) && (option->is_valid != NULL) + && !option->is_valid(value)) { + pcmk__config_err("Using default value for cluster option '%s' " - "because '%s' is invalid", name, value); + "because '%s' is invalid", option->name, value); value = NULL; } - if (value) { + if (value != NULL) { return value; } } // No value found, use default - value = def_value; + value = option->default_value; if (value == NULL) { crm_trace("No value or default provided for cluster option '%s'", - name); + option->name); return NULL; } - if (validate) { - CRM_CHECK(validate(value) != FALSE, - crm_err("Bug: default value for cluster option '%s' is invalid", name); - return NULL); - } + CRM_CHECK((option->is_valid == NULL) || option->is_valid(value), + crm_err("Bug: default value for cluster option '%s' is invalid", + option->name); + return NULL); crm_trace("Using default value '%s' for cluster option '%s'", - value, name); - if (options) { - new_value = strdup(value); - g_hash_table_insert(options, strdup(name), new_value); - value = new_value; + value, option->name); + if (table != NULL) { + pcmk__insert_dup(table, option->name, value); } return value; } @@ -339,27 +1403,19 @@ cluster_option_value(GHashTable *options, bool (*validate)(const char *), * \internal * \brief Get the value of a cluster option * - * \param[in,out] options Name/value pairs for configured options - * \param[in] option_list Possible cluster options - * \param[in] len Length of \p option_list - * \param[in] name (Primary) option name to look for + * \param[in,out] options Name/value pairs for configured options + * \param[in] name (Primary) option name to look for * * \return Option value */ const char * -pcmk__cluster_option(GHashTable *options, - const pcmk__cluster_option_t *option_list, - int len, const char *name) +pcmk__cluster_option(GHashTable *options, const char *name) { - const char *value = NULL; + for (const pcmk__cluster_option_t *option = cluster_options; + option->name != NULL; option++) { - for (int lpc = 0; lpc < len; lpc++) { - if (pcmk__str_eq(name, option_list[lpc].name, pcmk__str_casei)) { - value = cluster_option_value(options, option_list[lpc].is_valid, - option_list[lpc].name, - option_list[lpc].alt_name, - option_list[lpc].default_value); - return value; + if (pcmk__str_eq(name, option->name, pcmk__str_casei)) { + return cluster_option_value(options, option); } } CRM_CHECK(FALSE, crm_err("Bug: looking for unknown option '%s'", name)); @@ -368,143 +1424,142 @@ pcmk__cluster_option(GHashTable *options, /*! * \internal - * \brief Add a description element to a meta-data string - * - * \param[in,out] s Meta-data string to add to - * \param[in] tag Name of element to add ("longdesc" or "shortdesc") - * \param[in] desc Textual description to add - * \param[in] values If not \p NULL, the allowed values for the parameter - * \param[in] spaces If not \p NULL, spaces to insert at the beginning of - * each line + * \brief Output cluster option metadata as OCF-like XML + * + * \param[in,out] out Output object + * \param[in] name Fake resource agent name for the option list + * \param[in] desc_short Short description of the option list + * \param[in] desc_long Long description of the option list + * \param[in] filter Group of <tt>enum pcmk__opt_flags</tt>; output an + * option only if its \c flags member has all these + * flags set + * \param[in] all If \c true, output all options; otherwise, exclude + * advanced and deprecated options unless + * \c pcmk__opt_advanced and \c pcmk__opt_deprecated + * flags (respectively) are set in \p filter. This is + * always treated as true for XML output objects. + * + * \return Standard Pacemaker return code */ -static void -add_desc(GString *s, const char *tag, const char *desc, const char *values, - const char *spaces) +int +pcmk__output_cluster_options(pcmk__output_t *out, const char *name, + const char *desc_short, const char *desc_long, + uint32_t filter, bool all) { - char *escaped_en = crm_xml_escape(desc); - - if (spaces != NULL) { - g_string_append(s, spaces); - } - pcmk__g_strcat(s, "<", tag, " lang=\"en\">", escaped_en, NULL); - - if (values != NULL) { - pcmk__g_strcat(s, " Allowed values: ", values, NULL); - } - pcmk__g_strcat(s, "</", tag, ">\n", NULL); - -#ifdef ENABLE_NLS - { - static const char *locale = NULL; - - char *localized = crm_xml_escape(_(desc)); - - if (strcmp(escaped_en, localized) != 0) { - if (locale == NULL) { - locale = strtok(setlocale(LC_ALL, NULL), "_"); - } - - if (spaces != NULL) { - g_string_append(s, spaces); - } - pcmk__g_strcat(s, "<", tag, " lang=\"", locale, "\">", localized, - NULL); - - if (values != NULL) { - pcmk__g_strcat(s, _(" Allowed values: "), _(values), NULL); - } - pcmk__g_strcat(s, "</", tag, ">\n", NULL); - } - free(localized); - } -#endif - - free(escaped_en); + return out->message(out, "option-list", name, desc_short, desc_long, filter, + cluster_options, all); } -gchar * -pcmk__format_option_metadata(const char *name, const char *desc_short, - const char *desc_long, - pcmk__cluster_option_t *option_list, int len) +/*! + * \internal + * \brief Output primitive resource meta-attributes as OCF-like XML + * + * \param[in,out] out Output object + * \param[in] name Fake resource agent name for the option list + * \param[in] desc_short Short description of the option list + * \param[in] desc_long Long description of the option list + * \param[in] all If \c true, output all options; otherwise, exclude + * advanced and deprecated options. This is always + * treated as true for XML output objects. + * + * \return Standard Pacemaker return code + */ +int +pcmk__output_primitive_meta(pcmk__output_t *out, const char *name, + const char *desc_short, const char *desc_long, + bool all) { - /* big enough to hold "pacemaker-schedulerd metadata" output */ - GString *s = g_string_sized_new(13000); - - pcmk__g_strcat(s, - "<?xml version=\"1.0\"?>\n" - "<resource-agent name=\"", name, "\" " - "version=\"" PACEMAKER_VERSION "\">\n" - " <version>" PCMK_OCF_VERSION "</version>\n", NULL); - - add_desc(s, "longdesc", desc_long, NULL, " "); - add_desc(s, "shortdesc", desc_short, NULL, " "); - - g_string_append(s, " <parameters>\n"); - - for (int lpc = 0; lpc < len; lpc++) { - const char *opt_name = option_list[lpc].name; - const char *opt_type = option_list[lpc].type; - const char *opt_values = option_list[lpc].values; - const char *opt_default = option_list[lpc].default_value; - const char *opt_desc_short = option_list[lpc].description_short; - const char *opt_desc_long = option_list[lpc].description_long; - - // The standard requires long and short parameter descriptions - CRM_ASSERT((opt_desc_short != NULL) || (opt_desc_long != NULL)); - - if (opt_desc_short == NULL) { - opt_desc_short = opt_desc_long; - } else if (opt_desc_long == NULL) { - opt_desc_long = opt_desc_short; - } + return out->message(out, "option-list", name, desc_short, desc_long, + pcmk__opt_none, primitive_meta, all); +} - // The standard requires a parameter type - CRM_ASSERT(opt_type != NULL); +/*! + * \internal + * \brief Output fence device common parameter metadata as OCF-like XML + * + * These are parameters that are available for all fencing resources, regardless + * of type. They are processed by Pacemaker, rather than by the fence agent or + * the fencing library. + * + * \param[in,out] out Output object + * \param[in] name Fake resource agent name for the option list + * \param[in] desc_short Short description of the option list + * \param[in] desc_long Long description of the option list + * \param[in] all If \c true, output all options; otherwise, exclude + * advanced and deprecated options. This is always + * treated as true for XML output objects. + * + * \return Standard Pacemaker return code + */ +int +pcmk__output_fencing_params(pcmk__output_t *out, const char *name, + const char *desc_short, const char *desc_long, + bool all) +{ + return out->message(out, "option-list", name, desc_short, desc_long, + pcmk__opt_none, fencing_params, all); +} - pcmk__g_strcat(s, " <parameter name=\"", opt_name, "\">\n", NULL); +/*! + * \internal + * \brief Output a list of cluster options for a daemon + * + * \brief[in,out] out Output object + * \brief[in] name Daemon name + * \brief[in] desc_short Short description of the option list + * \brief[in] desc_long Long description of the option list + * \brief[in] filter <tt>enum pcmk__opt_flags</tt> flag corresponding + * to daemon + * + * \return Standard Pacemaker return code + */ +int +pcmk__daemon_metadata(pcmk__output_t *out, const char *name, + const char *desc_short, const char *desc_long, + enum pcmk__opt_flags filter) +{ + // @COMPAT Drop this function when we drop daemon metadata + pcmk__output_t *tmp_out = NULL; + xmlNode *top = NULL; + const xmlNode *metadata = NULL; + GString *metadata_s = NULL; - add_desc(s, "longdesc", opt_desc_long, opt_values, " "); - add_desc(s, "shortdesc", opt_desc_short, NULL, " "); + int rc = pcmk__output_new(&tmp_out, "xml", "/dev/null", NULL); - pcmk__g_strcat(s, " <content type=\"", opt_type, "\"", NULL); - if (opt_default != NULL) { - pcmk__g_strcat(s, " default=\"", opt_default, "\"", NULL); - } + if (rc != pcmk_rc_ok) { + return rc; + } - if ((opt_values != NULL) && (strcmp(opt_type, "select") == 0)) { - char *str = strdup(opt_values); - const char *delim = ", "; - char *ptr = strtok(str, delim); + pcmk__output_set_legacy_xml(tmp_out); - g_string_append(s, ">\n"); + if (filter == pcmk__opt_fencing) { + pcmk__output_fencing_params(tmp_out, name, desc_short, desc_long, true); + } else { + pcmk__output_cluster_options(tmp_out, name, desc_short, desc_long, + (uint32_t) filter, true); + } - while (ptr != NULL) { - pcmk__g_strcat(s, " <option value=\"", ptr, "\" />\n", - NULL); - ptr = strtok(NULL, delim); - } - g_string_append_printf(s, " </content>\n"); - free(str); + tmp_out->finish(tmp_out, CRM_EX_OK, false, (void **) &top); + metadata = pcmk__xe_first_child(top, PCMK_XE_RESOURCE_AGENT, NULL, NULL); - } else { - g_string_append(s, "/>\n"); - } + metadata_s = g_string_sized_new(16384); + pcmk__xml_string(metadata, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, + metadata_s, 0); - g_string_append(s, " </parameter>\n"); - } - g_string_append(s, " </parameters>\n</resource-agent>\n"); + out->output_xml(out, PCMK_XE_METADATA, metadata_s->str); - return g_string_free(s, FALSE); + pcmk__output_free(tmp_out); + free_xml(top); + g_string_free(metadata_s, TRUE); + return pcmk_rc_ok; } void -pcmk__validate_cluster_options(GHashTable *options, - pcmk__cluster_option_t *option_list, int len) +pcmk__validate_cluster_options(GHashTable *options) { - for (int lpc = 0; lpc < len; lpc++) { - cluster_option_value(options, option_list[lpc].is_valid, - option_list[lpc].name, - option_list[lpc].alt_name, - option_list[lpc].default_value); + for (const pcmk__cluster_option_t *option = cluster_options; + option->name != NULL; option++) { + + cluster_option_value(options, option); } } diff --git a/lib/common/options_display.c b/lib/common/options_display.c new file mode 100644 index 0000000..9798b31 --- /dev/null +++ b/lib/common/options_display.c @@ -0,0 +1,493 @@ +/* + * 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 Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <glib.h> // GSList, GString + +#include "crmcommon_private.h" + +/*! + * \internal + * \brief Output an option's possible values + * + * \param[in,out] out Output object + * \param[in] option Option whose possible values to add + */ +static void +add_possible_values_default(pcmk__output_t *out, + const pcmk__cluster_option_t *option) +{ + const char *id = _("Possible values"); + GString *buf = g_string_sized_new(256); + + CRM_ASSERT(option->type != NULL); + + if (pcmk_is_set(option->flags, pcmk__opt_generated)) { + id = _("Possible values (generated by Pacemaker)"); + } + + if ((option->values != NULL) && (strcmp(option->type, "select") == 0)) { + const char *delim = ", "; + bool found_default = (option->default_value == NULL); + char *str = pcmk__str_copy(option->values); + + for (const char *value = strtok(str, delim); value != NULL; + value = strtok(NULL, delim)) { + + if (buf->len > 0) { + g_string_append(buf, delim); + } + g_string_append_c(buf, '"'); + g_string_append(buf, value); + g_string_append_c(buf, '"'); + + if (!found_default && (strcmp(value, option->default_value) == 0)) { + found_default = true; + g_string_append(buf, _(" (default)")); + } + } + free(str); + + } else if (option->default_value != NULL) { + pcmk__g_strcat(buf, + option->type, _(" (default: \""), option->default_value, + "\")", NULL); + + } else { + pcmk__g_strcat(buf, option->type, _(" (no default)"), NULL); + } + + out->list_item(out, id, "%s", buf->str); + g_string_free(buf, TRUE); +} + +/*! + * \internal + * \brief Output a single option's metadata + * + * \param[in,out] out Output object + * \param[in] option Option to add + */ +static void +add_option_metadata_default(pcmk__output_t *out, + const pcmk__cluster_option_t *option) +{ + const char *desc_short = option->description_short; + const char *desc_long = option->description_long; + + CRM_ASSERT((desc_short != NULL) || (desc_long != NULL)); + + if (desc_short == NULL) { + desc_short = desc_long; + desc_long = NULL; + } + + out->list_item(out, option->name, "%s", _(desc_short)); + + out->begin_list(out, NULL, NULL, NULL); + + if (desc_long != NULL) { + out->list_item(out, NULL, "%s", _(desc_long)); + } + add_possible_values_default(out, option); + out->end_list(out); +} + +/*! + * \internal + * \brief Output the metadata for a list of options + * + * \param[in,out] out Output object + * \param[in] args Message-specific arguments + * + * \return Standard Pacemaker return code + * + * \note \p args should contain the following: + * -# Fake resource agent name for the option list (ignored) + * -# Short description of option list + * -# Long description of option list + * -# Filter: Group of <tt>enum pcmk__opt_flags</tt>; output an option + * only if its \c flags member has all these flags set + * -# <tt>NULL</tt>-terminated list of options whose metadata to format + * -# All: If \c true, output all options; otherwise, exclude advanced and + * deprecated options unless \c pcmk__opt_advanced and + * \c pcmk__opt_deprecated flags (respectively) are set in the filter. + */ +PCMK__OUTPUT_ARGS("option-list", "const char *", "const char *", "const char *", + "uint32_t", "const pcmk__cluster_option_t *", "bool") +static int +option_list_default(pcmk__output_t *out, va_list args) +{ + const char *name G_GNUC_UNUSED = va_arg(args, const char *); + const char *desc_short = va_arg(args, const char *); + const char *desc_long = va_arg(args, const char *); + const uint32_t filter = va_arg(args, uint32_t); + const pcmk__cluster_option_t *option_list = + va_arg(args, pcmk__cluster_option_t *); + const bool all = (bool) va_arg(args, int); + + const bool show_deprecated = all + || pcmk_is_set(filter, pcmk__opt_deprecated); + const bool show_advanced = all || pcmk_is_set(filter, pcmk__opt_advanced); + bool old_fancy = false; + + GSList *deprecated = NULL; + GSList *advanced = NULL; + + CRM_ASSERT((out != NULL) && (desc_short != NULL) && (desc_long != NULL) + && (option_list != NULL)); + + old_fancy = pcmk__output_text_get_fancy(out); + pcmk__output_text_set_fancy(out, true); + + out->info(out, "%s", _(desc_short)); + out->spacer(out); + out->info(out, "%s", _(desc_long)); + out->begin_list(out, NULL, NULL, NULL); + + for (const pcmk__cluster_option_t *option = option_list; + option->name != NULL; option++) { + + // Store deprecated and advanced options to display later if appropriate + if (pcmk_all_flags_set(option->flags, filter)) { + if (pcmk_is_set(option->flags, pcmk__opt_deprecated)) { + if (show_deprecated) { + deprecated = g_slist_prepend(deprecated, (gpointer) option); + } + + } else if (pcmk_is_set(option->flags, pcmk__opt_advanced)) { + if (show_advanced) { + advanced = g_slist_prepend(advanced, (gpointer) option); + } + + } else { + out->spacer(out); + add_option_metadata_default(out, option); + } + } + } + + if (advanced != NULL) { + advanced = g_slist_reverse(advanced); + + out->spacer(out); + out->begin_list(out, NULL, NULL, _("ADVANCED OPTIONS")); + for (const GSList *iter = advanced; iter != NULL; iter = iter->next) { + const pcmk__cluster_option_t *option = iter->data; + + out->spacer(out); + add_option_metadata_default(out, option); + } + out->end_list(out); + g_slist_free(advanced); + } + + if (deprecated != NULL) { + deprecated = g_slist_reverse(deprecated); + + out->spacer(out); + out->begin_list(out, NULL, NULL, + _("DEPRECATED OPTIONS (will be removed in a future " + "release)")); + for (const GSList *iter = deprecated; iter != NULL; iter = iter->next) { + const pcmk__cluster_option_t *option = iter->data; + + out->spacer(out); + add_option_metadata_default(out, option); + } + out->end_list(out); + g_slist_free(deprecated); + } + + out->end_list(out); + pcmk__output_text_set_fancy(out, old_fancy); + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Add a description element to an OCF-like metadata XML node + * + * Include a translation based on the current locale if \c ENABLE_NLS is + * defined. + * + * \param[in,out] out Output object + * \param[in] for_long If \c true, add long description; otherwise, add + * short description + * \param[in] desc Textual description to add + */ +static void +add_desc_xml(pcmk__output_t *out, bool for_long, const char *desc) +{ + const char *tag = (for_long? PCMK_XE_LONGDESC : PCMK_XE_SHORTDESC); + xmlNode *node = pcmk__output_create_xml_text_node(out, tag, desc); + + crm_xml_add(node, PCMK_XA_LANG, PCMK__VALUE_EN); + +#ifdef ENABLE_NLS + { + static const char *locale = NULL; + + if (strcmp(desc, _(desc)) == 0) { + return; + } + + if (locale == NULL) { + locale = strtok(setlocale(LC_ALL, NULL), "_"); + } + node = pcmk__output_create_xml_text_node(out, tag, _(desc)); + crm_xml_add(node, PCMK_XA_LANG, locale); + } +#endif +} + +/*! + * \internal + * \brief Output an option's possible values + * + * Add a \c PCMK_XE_OPTION element for each of the option's possible values. + * + * \param[in,out] out Output object + * \param[in] option Option whose possible values to add + */ +static void +add_possible_values_xml(pcmk__output_t *out, + const pcmk__cluster_option_t *option) +{ + if ((option->values != NULL) && (strcmp(option->type, "select") == 0)) { + const char *delim = ", "; + char *str = pcmk__str_copy(option->values); + const char *ptr = strtok(str, delim); + + while (ptr != NULL) { + pcmk__output_create_xml_node(out, PCMK_XE_OPTION, + PCMK_XA_VALUE, ptr, + NULL); + ptr = strtok(NULL, delim); + } + free(str); + } +} + +/*! + * \internal + * \brief Map an option type to one suitable for daemon metadata + * + * \param[in] type Option type to map + * + * \return String suitable for daemon metadata to display as an option type + */ +static const char * +map_legacy_option_type(const char *type) +{ + // @COMPAT Drop this function when we drop daemon metadata commands + if (pcmk__str_any_of(type, PCMK_VALUE_DURATION, PCMK_VALUE_TIMEOUT, NULL)) { + return PCMK__VALUE_TIME; + + } else if (pcmk__str_any_of(type, + PCMK_VALUE_NONNEGATIVE_INTEGER, + PCMK_VALUE_SCORE, NULL)) { + return PCMK_VALUE_INTEGER; + + } else if (pcmk__str_eq(type, PCMK_VALUE_VERSION, pcmk__str_none)) { + return PCMK_VALUE_STRING; + + } else { + return type; + } +} + +/*! + * \internal + * \brief Add a \c PCMK_XE_PARAMETER element to an OCF-like metadata XML node + * + * \param[in,out] out Output object + * \param[in] option Option to add as a \c PCMK_XE_PARAMETER element + */ +static void +add_option_metadata_xml(pcmk__output_t *out, + const pcmk__cluster_option_t *option) +{ + const char *type = option->type; + const char *desc_long = option->description_long; + const char *desc_short = option->description_short; + const bool advanced = pcmk_is_set(option->flags, pcmk__opt_advanced); + const bool deprecated = pcmk_is_set(option->flags, pcmk__opt_deprecated); + const bool generated = pcmk_is_set(option->flags, pcmk__opt_generated); + + // OCF requires "1"/"0" and does not allow "true"/"false + // @COMPAT Variables no longer needed after we drop legacy mode + const char *advanced_s = advanced? "1" : "0"; + const char *generated_s = generated? "1" : "0"; + + // @COMPAT For daemon metadata only; drop when daemon metadata is dropped + const bool legacy = pcmk__output_get_legacy_xml(out); + char *desc_long_legacy = NULL; + GString *desc_short_legacy = NULL; + + // The standard requires a parameter type + CRM_ASSERT(type != NULL); + + // The standard requires long and short parameter descriptions + CRM_ASSERT((desc_long != NULL) || (desc_short != NULL)); + + if (desc_long == NULL) { + desc_long = desc_short; + } else if (desc_short == NULL) { + desc_short = desc_long; + } + + if (legacy) { + // This is ugly but it will go away at a major release bump + type = map_legacy_option_type(type); + + if (option->values != NULL) { + desc_long_legacy = crm_strdup_printf("%s Allowed values: %s", + desc_long, option->values); + desc_long = desc_long_legacy; + } + + if (deprecated || advanced) { + const size_t init_sz = 1023; + + if (desc_long != option->description_long) { + /* desc_long was NULL and got assigned desc_short, which was + * non-empty. Let desc_long have the "real" description, and put + * the flag in desc_short. + */ + desc_short = ""; + } else { + desc_short = pcmk__s(option->description_short, ""); + } + + if (deprecated) { + pcmk__add_separated_word(&desc_short_legacy, init_sz, + "*** Deprecated ***", NULL); + } + if (advanced) { + pcmk__add_separated_word(&desc_short_legacy, init_sz, + "*** Advanced Use Only ***", NULL); + } + pcmk__add_separated_word(&desc_short_legacy, 0, desc_short, NULL); + + desc_short = desc_short_legacy->str; + } + + /* These must be NULL when used as attribute values later. + * PCMK_XA_ADVANCED and PCMK_XA_GENERATED break validation for some + * legacy tools. + */ + advanced_s = NULL; + generated_s = NULL; + } + + pcmk__output_xml_create_parent(out, PCMK_XE_PARAMETER, + PCMK_XA_NAME, option->name, + PCMK_XA_ADVANCED, advanced_s, + PCMK_XA_GENERATED, generated_s, + NULL); + + if (deprecated && !legacy) { + // No need yet to support "replaced-with" or "desc"; add if needed + pcmk__output_create_xml_node(out, PCMK_XE_DEPRECATED, NULL); + } + add_desc_xml(out, true, desc_long); + add_desc_xml(out, false, desc_short); + + pcmk__output_xml_create_parent(out, PCMK_XE_CONTENT, + PCMK_XA_TYPE, type, + PCMK_XA_DEFAULT, option->default_value, + NULL); + + add_possible_values_xml(out, option); + + pcmk__output_xml_pop_parent(out); + pcmk__output_xml_pop_parent(out); + + free(desc_long_legacy); + if (desc_short_legacy != NULL) { + g_string_free(desc_short_legacy, TRUE); + } +} + +/*! + * \internal + * \brief Output the metadata for a list of options as OCF-like XML + * + * \param[in,out] out Output object + * \param[in] args Message-specific arguments + * + * \return Standard Pacemaker return code + * + * \note \p args should contain the following: + * -# Fake resource agent name for the option list + * -# Short description of option list + * -# Long description of option list + * -# Filter: Group of <tt>enum pcmk__opt_flags</tt>; output an option + * only if its \c flags member has all these flags set + * -# <tt>NULL</tt>-terminated list of options whose metadata to format + * -# Whether to output all options (ignored, treated as \c true) + */ +PCMK__OUTPUT_ARGS("option-list", "const char *", "const char *", "const char *", + "uint32_t", "const pcmk__cluster_option_t *", "bool") +static int +option_list_xml(pcmk__output_t *out, va_list args) +{ + const char *name = va_arg(args, const char *); + const char *desc_short = va_arg(args, const char *); + const char *desc_long = va_arg(args, const char *); + const uint32_t filter = va_arg(args, uint32_t); + const pcmk__cluster_option_t *option_list = + va_arg(args, pcmk__cluster_option_t *); + + CRM_ASSERT((out != NULL) && (name != NULL) && (desc_short != NULL) + && (desc_long != NULL) && (option_list != NULL)); + + pcmk__output_xml_create_parent(out, PCMK_XE_RESOURCE_AGENT, + PCMK_XA_NAME, name, + PCMK_XA_VERSION, PACEMAKER_VERSION, + NULL); + + pcmk__output_create_xml_text_node(out, PCMK_XE_VERSION, PCMK_OCF_VERSION); + add_desc_xml(out, true, desc_long); + add_desc_xml(out, false, desc_short); + + pcmk__output_xml_create_parent(out, PCMK_XE_PARAMETERS, NULL); + + for (const pcmk__cluster_option_t *option = option_list; + option->name != NULL; option++) { + + if (pcmk_all_flags_set(option->flags, filter)) { + add_option_metadata_xml(out, option); + } + } + + pcmk__output_xml_pop_parent(out); + pcmk__output_xml_pop_parent(out); + return pcmk_rc_ok; +} + +static pcmk__message_entry_t fmt_functions[] = { + { "option-list", "default", option_list_default }, + { "option-list", "xml", option_list_xml }, + + { NULL, NULL, NULL } +}; + +/*! + * \internal + * \brief Register the formatting functions for option lists + * + * \param[in,out] out Output object + */ +void +pcmk__register_option_messages(pcmk__output_t *out) { + pcmk__register_messages(out, fmt_functions); +} diff --git a/lib/common/output.c b/lib/common/output.c index 2ea9b0b..92fbfda 100644 --- a/lib/common/output.c +++ b/lib/common/output.c @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 the Pacemaker project contributors + * Copyright 2019-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -18,10 +18,12 @@ static GHashTable *formatters = NULL; #if defined(PCMK__UNIT_TESTING) +// LCOV_EXCL_START GHashTable * pcmk__output_formatters(void) { return formatters; } +// LCOV_EXCL_STOP #endif void @@ -114,9 +116,8 @@ pcmk__output_new(pcmk__output_t **out, const char *fmt_name, int rc = pcmk__bare_output_new(out, fmt_name, filename, argv); if (rc == pcmk_rc_ok) { - /* Register libcrmcommon messages (currently they exist only for - * patchset) - */ + // Register libcrmcommon messages + pcmk__register_option_messages(*out); pcmk__register_patchset_messages(*out); } return rc; @@ -127,8 +128,15 @@ pcmk__register_format(GOptionGroup *group, const char *name, pcmk__output_factory_t create, const GOptionEntry *options) { + char *name_copy = NULL; + CRM_ASSERT(create != NULL && !pcmk__str_empty(name)); + name_copy = strdup(name); + if (name_copy == NULL) { + return ENOMEM; + } + if (formatters == NULL) { formatters = pcmk__strkey_table(free, NULL); } @@ -137,7 +145,7 @@ pcmk__register_format(GOptionGroup *group, const char *name, g_option_group_add_entries(group, options); } - g_hash_table_insert(formatters, strdup(name), create); + g_hash_table_insert(formatters, name_copy, create); return pcmk_rc_ok; } @@ -189,7 +197,7 @@ pcmk__register_message(pcmk__output_t *out, const char *message_id, pcmk__message_fn_t fn) { CRM_ASSERT(out != NULL && !pcmk__str_empty(message_id) && fn != NULL); - g_hash_table_replace(out->messages, strdup(message_id), fn); + g_hash_table_replace(out->messages, pcmk__str_copy(message_id), fn); } void @@ -228,7 +236,7 @@ pcmk__output_and_clear_error(GError **error, pcmk__output_t *out) * functions that want to free any previous result supplied by the caller). * * \param[out] out Where to put newly created output object - * \param[in,out] xml If non-NULL, this will be freed + * \param[in,out] xml If \c *xml is non-NULL, this will be freed * * \return Standard Pacemaker return code */ @@ -239,6 +247,10 @@ pcmk__xml_output_new(pcmk__output_t **out, xmlNodePtr *xml) { { NULL, NULL, NULL } }; + if (xml == NULL) { + return EINVAL; + } + if (*xml != NULL) { xmlFreeNode(*xml); *xml = NULL; @@ -251,12 +263,19 @@ pcmk__xml_output_new(pcmk__output_t **out, xmlNodePtr *xml) { * \internal * \brief Finish and free an XML-only output object * - * \param[in,out] out Output object to free - * \param[out] xml If not NULL, where to store XML output + * \param[in,out] out Output object to free + * \param[in] exit_status The exit value of the whole program + * \param[out] xml If not NULL, where to store XML output */ void -pcmk__xml_output_finish(pcmk__output_t *out, xmlNodePtr *xml) { - out->finish(out, 0, FALSE, (void **) xml); +pcmk__xml_output_finish(pcmk__output_t *out, crm_exit_t exit_status, + xmlNodePtr *xml) +{ + if (out == NULL) { + return; + } + + out->finish(out, exit_status, FALSE, (void **) xml); pcmk__output_free(out); } diff --git a/lib/common/output_html.c b/lib/common/output_html.c index 92e9010..afb2609 100644 --- a/lib/common/output_html.c +++ b/lib/common/output_html.c @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 the Pacemaker project contributors + * Copyright 2019-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -19,22 +19,22 @@ #include <crm/common/xml.h> static const char *stylesheet_default = - ".bold { font-weight: bold }\n" + "." PCMK__VALUE_BOLD " { font-weight: bold }\n" - ".online { color: green }\n" - ".offline { color: red }\n" - ".maint { color: blue }\n" - ".standby { color: blue }\n" - ".health_red { color: red }\n" - ".health_yellow { color: GoldenRod }\n" + "." PCMK_VALUE_ONLINE " { color: green }\n" + "." PCMK_VALUE_OFFLINE " { color: red }\n" + "." PCMK__VALUE_MAINT " { color: blue }\n" + "." PCMK_VALUE_STANDBY " { color: blue }\n" + "." PCMK__VALUE_HEALTH_RED " { color: red }\n" + "." PCMK__VALUE_HEALTH_YELLOW " { color: GoldenRod }\n" - ".rsc-failed { color: red }\n" - ".rsc-failure-ignored { color: DarkGreen }\n" - ".rsc-managed { color: blue }\n" - ".rsc-multiple { color: orange }\n" - ".rsc-ok { color: green }\n" + "." PCMK__VALUE_RSC_FAILED " { color: red }\n" + "." PCMK__VALUE_RSC_FAILURE_IGNORED " { color: DarkGreen }\n" + "." PCMK__VALUE_RSC_MANAGED " { color: blue }\n" + "." PCMK__VALUE_RSC_MULTIPLE " { color: orange }\n" + "." PCMK__VALUE_RSC_OK " { color: green }\n" - ".warning { color: red; font-weight: bold }"; + "." PCMK__VALUE_WARNING " { color: red; font-weight: bold }"; static gboolean cgi_output = FALSE; static char *stylesheet_link = NULL; @@ -81,9 +81,13 @@ html_free_priv(pcmk__output_t *out) { priv = out->priv; - xmlFreeNode(priv->root); + free_xml(priv->root); + /* The elements of parent_q are xmlNodes that are a part of the + * priv->root document, so the above line already frees them. Don't + * call g_queue_free_full here. + */ g_queue_free(priv->parent_q); - g_slist_free(priv->errors); + g_slist_free_full(priv->errors, free); free(priv); out->priv = NULL; } @@ -108,10 +112,10 @@ html_init(pcmk__output_t *out) { priv->parent_q = g_queue_new(); - priv->root = create_xml_node(NULL, "html"); + priv->root = pcmk__xe_create(NULL, "html"); xmlCreateIntSubset(priv->root->doc, (pcmkXmlStr) "html", NULL, NULL); - crm_xml_add(priv->root, "lang", "en"); + crm_xml_add(priv->root, PCMK_XA_LANG, PCMK__VALUE_EN); g_queue_push_tail(priv->parent_q, priv->root); priv->errors = NULL; @@ -132,6 +136,7 @@ html_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy private_data_t *priv = NULL; htmlNodePtr head_node = NULL; htmlNodePtr charset_node = NULL; + xmlNode *child_node = NULL; CRM_ASSERT(out != NULL); @@ -155,12 +160,14 @@ html_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy head_node = xmlNewDocRawNode(NULL, NULL, (pcmkXmlStr) "head", NULL); if (title != NULL ) { - pcmk_create_xml_text_node(head_node, "title", title); + child_node = pcmk__xe_create(head_node, "title"); + pcmk__xe_set_content(child_node, "%s", title); } else if (out->request != NULL) { - pcmk_create_xml_text_node(head_node, "title", out->request); + child_node = pcmk__xe_create(head_node, "title"); + pcmk__xe_set_content(child_node, "%s", out->request); } - charset_node = create_xml_node(head_node, "meta"); + charset_node = pcmk__xe_create(head_node, PCMK__XE_META); crm_xml_add(charset_node, "charset", "utf-8"); /* Add any extra header nodes the caller might have created. */ @@ -174,10 +181,11 @@ html_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy * stylesheet. The second can override the first. At least one should be * given. */ - pcmk_create_xml_text_node(head_node, "style", stylesheet_default); + child_node = pcmk__xe_create(head_node, "style"); + pcmk__xe_set_content(child_node, "%s", stylesheet_default); if (stylesheet_link != NULL) { - htmlNodePtr link_node = create_xml_node(head_node, "link"); + htmlNodePtr link_node = pcmk__xe_create(head_node, "link"); pcmk__xe_set_props(link_node, "rel", "stylesheet", "href", stylesheet_link, NULL); @@ -196,7 +204,7 @@ html_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy } if (copy_dest != NULL) { - *copy_dest = copy_xml(priv->root); + *copy_dest = pcmk__xml_copy(NULL, priv->root); } g_slist_free_full(extra_headers, (GDestroyNotify) xmlFreeNode); @@ -224,15 +232,17 @@ html_subprocess_output(pcmk__output_t *out, int exit_status, rc_buf = crm_strdup_printf("Return code: %d", exit_status); pcmk__output_create_xml_text_node(out, "h2", "Command Output"); - pcmk__output_create_html_node(out, "div", NULL, NULL, rc_buf); + pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, rc_buf); if (proc_stdout != NULL) { - pcmk__output_create_html_node(out, "div", NULL, NULL, "Stdout"); - pcmk__output_create_html_node(out, "div", NULL, "output", proc_stdout); + pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, "Stdout"); + pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, + PCMK__VALUE_OUTPUT, proc_stdout); } if (proc_stderr != NULL) { - pcmk__output_create_html_node(out, "div", NULL, NULL, "Stderr"); - pcmk__output_create_html_node(out, "div", NULL, "output", proc_stderr); + pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, "Stderr"); + pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, + PCMK__VALUE_OUTPUT, proc_stderr); } free(rc_buf); @@ -243,13 +253,17 @@ html_version(pcmk__output_t *out, bool extended) { CRM_ASSERT(out != NULL); pcmk__output_create_xml_text_node(out, "h2", "Version Information"); - pcmk__output_create_html_node(out, "div", NULL, NULL, "Program: Pacemaker"); - pcmk__output_create_html_node(out, "div", NULL, NULL, crm_strdup_printf("Version: %s", PACEMAKER_VERSION)); - pcmk__output_create_html_node(out, "div", NULL, NULL, + pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, + "Program: Pacemaker"); + pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, + "Version: " PACEMAKER_VERSION); + pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, "Author: Andrew Beekhof and " "the Pacemaker project contributors"); - pcmk__output_create_html_node(out, "div", NULL, NULL, crm_strdup_printf("Build: %s", BUILD_VERSION)); - pcmk__output_create_html_node(out, "div", NULL, NULL, crm_strdup_printf("Features: %s", CRM_FEATURES)); + pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, + "Build: " BUILD_VERSION); + pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, + "Features: " CRM_FEATURES); } G_GNUC_PRINTF(2, 3) @@ -284,7 +298,7 @@ html_output_xml(pcmk__output_t *out, const char *name, const char *buf) { CRM_ASSERT(out != NULL); node = pcmk__output_create_html_node(out, "pre", NULL, NULL, buf); - crm_xml_add(node, "lang", "xml"); + crm_xml_add(node, PCMK_XA_LANG, "xml"); } G_GNUC_PRINTF(4, 5) @@ -349,7 +363,7 @@ html_list_item(pcmk__output_t *out, const char *name, const char *format, ...) { free(buf); if (name != NULL) { - crm_xml_add(item_node, "class", name); + crm_xml_add(item_node, PCMK_XA_CLASS, name); } } @@ -365,7 +379,9 @@ html_end_list(pcmk__output_t *out) { CRM_ASSERT(out != NULL && out->priv != NULL); priv = out->priv; - /* Remove the <ul> tag. */ + /* Remove the <ul> tag, but do not free this result - it's still + * part of the document. + */ g_queue_pop_tail(priv->parent_q); pcmk__output_xml_pop_parent(out); @@ -441,16 +457,42 @@ pcmk__output_create_html_node(pcmk__output_t *out, const char *element_name, con node = pcmk__output_create_xml_text_node(out, element_name, text); if (class_name != NULL) { - crm_xml_add(node, "class", class_name); + crm_xml_add(node, PCMK_XA_CLASS, class_name); } if (id != NULL) { - crm_xml_add(node, "id", id); + crm_xml_add(node, PCMK_XA_ID, id); } return node; } +/*! + * \internal + * \brief Create a new HTML element under a given parent with ID and class + * + * \param[in,out] parent XML element that will be the new element's parent + * (\c NULL to create a new XML document with the new + * node as root) + * \param[in] name Name of new element + * \param[in] id CSS ID of new element (can be \c NULL) + * \param[in] class CSS class of new element (can be \c NULL) + * + * \return Newly created XML element (guaranteed not to be \c NULL) + */ +xmlNode * +pcmk__html_create(xmlNode *parent, const char *name, const char *id, + const char *class) +{ + xmlNode *node = pcmk__xe_create(parent, name); + + pcmk__xe_set_props(node, + PCMK_XA_CLASS, class, + PCMK_XA_ID, id, + NULL); + return node; +} + void pcmk__html_add_header(const char *name, ...) { htmlNodePtr header_node; diff --git a/lib/common/output_log.c b/lib/common/output_log.c index 54fa37e..81a39c8 100644 --- a/lib/common/output_log.c +++ b/lib/common/output_log.c @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 the Pacemaker project contributors + * Copyright 2019-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -16,10 +16,6 @@ #include <stdlib.h> #include <stdio.h> -GOptionEntry pcmk__log_output_entries[] = { - { NULL } -}; - typedef struct private_data_s { /* gathered in log_begin_list */ GQueue/*<char*>*/ *prefixes; @@ -165,8 +161,8 @@ log_output_xml(pcmk__output_t *out, const char *name, const char *buf) { CRM_ASSERT(out != NULL && out->priv != NULL); priv = out->priv; - node = create_xml_node(NULL, name); - xmlNodeSetContent(node, (pcmkXmlStr) buf); + node = pcmk__xe_create(NULL, name); + pcmk__xe_set_content(node, "%s", buf); do_crm_log_xml(priv->log_level, name, node); free(node); } @@ -353,34 +349,63 @@ pcmk__mk_log_output(char **argv) { return retval; } +/*! + * \internal + * \brief Get the log level for a log output object + * + * This returns 0 if the output object is not of log format. + * + * \param[in] out Output object + * + * \return Current log level for \p out + */ uint8_t pcmk__output_get_log_level(const pcmk__output_t *out) { - private_data_t *priv = NULL; + CRM_ASSERT(out != NULL); - CRM_ASSERT((out != NULL) && (out->priv != NULL)); - CRM_CHECK(pcmk__str_eq(out->fmt_name, "log", pcmk__str_none), return 0); + if (pcmk__str_eq(out->fmt_name, "log", pcmk__str_none)) { + private_data_t *priv = out->priv; - priv = out->priv; - return priv->log_level; + CRM_ASSERT(priv != NULL); + return priv->log_level; + } + return 0; } +/*! + * \internal + * \brief Set the log level for a log output object + * + * This does nothing if the output object is not of log format. + * + * \param[in,out] out Output object + * \param[in] log_level Log level constant (\c LOG_ERR, etc.) to use + * + * \note \c LOG_INFO is used by default for new \c pcmk__output_t objects. + * \note Almost all formatted output messages respect this setting. However, + * <tt>out->err</tt> always logs at \c LOG_ERR. + */ void -pcmk__output_set_log_level(pcmk__output_t *out, uint8_t log_level) { - private_data_t *priv = NULL; +pcmk__output_set_log_level(pcmk__output_t *out, uint8_t log_level) +{ + CRM_ASSERT(out != NULL); - CRM_ASSERT(out != NULL && out->priv != NULL); - CRM_CHECK(pcmk__str_eq(out->fmt_name, "log", pcmk__str_none), return); + if (pcmk__str_eq(out->fmt_name, "log", pcmk__str_none)) { + private_data_t *priv = out->priv; - priv = out->priv; - priv->log_level = log_level; + CRM_ASSERT(priv != NULL); + priv->log_level = log_level; + } } /*! * \internal * \brief Set the file, function, line, and tags used to filter log output * - * \param[in,out] out Logger output object + * This does nothing if the output object is not of log format. + * + * \param[in,out] out Output object * \param[in] file File name to filter with (or NULL for default) * \param[in] function Function name to filter with (or NULL for default) * \param[in] line Line number to filter with (or 0 for default) @@ -394,14 +419,15 @@ void pcmk__output_set_log_filter(pcmk__output_t *out, const char *file, const char *function, uint32_t line, uint32_t tags) { - private_data_t *priv = NULL; + CRM_ASSERT(out != NULL); - CRM_ASSERT((out != NULL) && (out->priv != NULL)); - CRM_CHECK(pcmk__str_eq(out->fmt_name, "log", pcmk__str_none), return); + if (pcmk__str_eq(out->fmt_name, "log", pcmk__str_none)) { + private_data_t *priv = out->priv; - priv = out->priv; - priv->file = file; - priv->function = function; - priv->line = line; - priv->tags = tags; + CRM_ASSERT(priv != NULL); + priv->file = file; + priv->function = function; + priv->line = line; + priv->tags = tags; + } } diff --git a/lib/common/output_none.c b/lib/common/output_none.c index 581a8b4..d1cdacc 100644 --- a/lib/common/output_none.c +++ b/lib/common/output_none.c @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 the Pacemaker project contributors + * Copyright 2019-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -15,10 +15,6 @@ #include <crm/crm.h> #include <crm/common/cmdline_internal.h> -GOptionEntry pcmk__none_output_entries[] = { - { NULL } -}; - static void none_free_priv(pcmk__output_t *out) { /* This function intentionally left blank */ @@ -120,7 +116,7 @@ pcmk__mk_none_output(char **argv) { return NULL; } - retval->fmt_name = PCMK__VALUE_NONE; + retval->fmt_name = PCMK_VALUE_NONE; retval->request = pcmk__quote_cmdline(argv); retval->init = none_init; diff --git a/lib/common/output_text.c b/lib/common/output_text.c index 6bd362d..d43e29f 100644 --- a/lib/common/output_text.c +++ b/lib/common/output_text.c @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 the Pacemaker project contributors + * Copyright 2019-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -15,10 +15,14 @@ #include <glib.h> #include <termios.h> +#include "crmcommon_private.h" + +// @COMPAT Drop at 3.0.0 static gboolean fancy = FALSE; +// @COMPAT Drop at 3.0.0 GOptionEntry pcmk__text_output_entries[] = { - { "text-fancy", 0, 0, G_OPTION_ARG_NONE, &fancy, + { "text-fancy", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &fancy, "Use more highly formatted output (requires --output-as=text)", NULL }, @@ -33,9 +37,18 @@ typedef struct text_list_data_s { typedef struct private_data_s { GQueue *parent_q; + bool fancy; } private_data_t; static void +free_list_data(gpointer data) { + text_list_data_t *list_data = data; + + free(list_data->singular_noun); + free(list_data->plural_noun); +} + +static void text_free_priv(pcmk__output_t *out) { private_data_t *priv = NULL; @@ -45,7 +58,7 @@ text_free_priv(pcmk__output_t *out) { priv = out->priv; - g_queue_free(priv->parent_q); + g_queue_free_full(priv->parent_q, free_list_data); free(priv); out->priv = NULL; } @@ -59,15 +72,14 @@ text_init(pcmk__output_t *out) { /* If text_init was previously called on this output struct, just return. */ if (out->priv != NULL) { return true; - } else { - out->priv = calloc(1, sizeof(private_data_t)); - if (out->priv == NULL) { - return false; - } + } - priv = out->priv; + out->priv = calloc(1, sizeof(private_data_t)); + if (out->priv == NULL) { + return false; } + priv = out->priv; priv->parent_q = g_queue_new(); return true; } @@ -80,6 +92,9 @@ text_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy static void text_reset(pcmk__output_t *out) { + private_data_t *priv = NULL; + bool old_fancy = false; + CRM_ASSERT(out != NULL); if (out->dest != stdout) { @@ -88,8 +103,15 @@ text_reset(pcmk__output_t *out) { CRM_ASSERT(out->dest != NULL); + // Save priv->fancy before free/init sequence overwrites it + priv = out->priv; + old_fancy = priv->fancy; + text_free_priv(out); text_init(out); + + priv = out->priv; + priv->fancy = old_fancy; } static void @@ -192,17 +214,17 @@ text_begin_list(pcmk__output_t *out, const char *singular_noun, const char *plur va_start(ap, format); - if (fancy && format) { + if ((fancy || priv->fancy) && (format != NULL)) { pcmk__indented_vprintf(out, format, ap); fprintf(out->dest, ":\n"); } va_end(ap); - new_list = calloc(1, sizeof(text_list_data_t)); + new_list = pcmk__assert_alloc(1, sizeof(text_list_data_t)); new_list->len = 0; - pcmk__str_update(&new_list->singular_noun, singular_noun); - pcmk__str_update(&new_list->plural_noun, plural_noun); + new_list->singular_noun = pcmk__str_copy(singular_noun); + new_list->plural_noun = pcmk__str_copy(plural_noun); g_queue_push_tail(priv->parent_q, new_list); } @@ -210,13 +232,15 @@ text_begin_list(pcmk__output_t *out, const char *singular_noun, const char *plur G_GNUC_PRINTF(3, 4) static void text_list_item(pcmk__output_t *out, const char *id, const char *format, ...) { + private_data_t *priv = NULL; va_list ap; CRM_ASSERT(out != NULL); + priv = out->priv; va_start(ap, format); - if (fancy) { + if (fancy || priv->fancy) { if (id != NULL) { /* Not really a good way to do this all in one call, so make it two. * The first handles the indentation and list styling. The second @@ -269,7 +293,7 @@ text_end_list(pcmk__output_t *out) { } } - free(node); + free_list_data(node); } static bool @@ -336,6 +360,52 @@ pcmk__mk_text_output(char **argv) { return retval; } +/*! + * \internal + * \brief Check whether fancy output is enabled for a text output object + * + * This returns \c false if the output object is not of text format. + * + * \param[in] out Output object + * + * \return \c true if \p out has fancy output enabled, or \c false otherwise + */ +bool +pcmk__output_text_get_fancy(pcmk__output_t *out) +{ + CRM_ASSERT(out != NULL); + + if (pcmk__str_eq(out->fmt_name, "text", pcmk__str_none)) { + private_data_t *priv = out->priv; + + CRM_ASSERT(priv != NULL); + return priv->fancy; + } + return false; +} + +/*! + * \internal + * \brief Enable or disable fancy output for a text output object + * + * This does nothing if the output object is not of text format. + * + * \param[in,out] out Output object + * \param[in] enabled Whether fancy output should be enabled for \p out + */ +void +pcmk__output_text_set_fancy(pcmk__output_t *out, bool enabled) +{ + CRM_ASSERT(out != NULL); + + if (pcmk__str_eq(out->fmt_name, "text", pcmk__str_none)) { + private_data_t *priv = out->priv; + + CRM_ASSERT(priv != NULL); + priv->fancy = enabled; + } +} + G_GNUC_PRINTF(2, 0) void pcmk__formatted_vprintf(pcmk__output_t *out, const char *format, va_list args) { @@ -363,10 +433,14 @@ pcmk__formatted_printf(pcmk__output_t *out, const char *format, ...) { G_GNUC_PRINTF(2, 0) void pcmk__indented_vprintf(pcmk__output_t *out, const char *format, va_list args) { + private_data_t *priv = NULL; + CRM_ASSERT(out != NULL); CRM_CHECK(pcmk__str_eq(out->fmt_name, "text", pcmk__str_none), return); - if (fancy) { + priv = out->priv; + + if (fancy || priv->fancy) { int level = 0; private_data_t *priv = out->priv; @@ -428,7 +502,7 @@ pcmk__text_prompt(const char *prompt, bool echo, char **dest) #if HAVE_SSCANF_M rc = scanf("%ms", dest); #else - *dest = calloc(1, 1024); + *dest = pcmk__assert_alloc(1, 1024); rc = scanf("%1023s", *dest); #endif fprintf(stderr, "\n"); diff --git a/lib/common/output_xml.c b/lib/common/output_xml.c index ba61145..9b0d417 100644 --- a/lib/common/output_xml.c +++ b/lib/common/output_xml.c @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 the Pacemaker project contributors + * Copyright 2019-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -14,57 +14,59 @@ #include <stdlib.h> #include <stdio.h> #include <crm/crm.h> -#include <crm/common/output.h> -#include <crm/common/xml.h> -#include <crm/common/xml_internal.h> /* pcmk__xml2fd */ #include <glib.h> #include <crm/common/cmdline_internal.h> +#include <crm/common/output.h> #include <crm/common/xml.h> - -static gboolean legacy_xml = FALSE; -static gboolean simple_list = FALSE; -static gboolean substitute = FALSE; - -GOptionEntry pcmk__xml_output_entries[] = { - { "xml-legacy", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &legacy_xml, - NULL, - NULL }, - { "xml-simple-list", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &simple_list, - NULL, - NULL }, - { "xml-substitute", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &substitute, - NULL, - NULL }, - - { NULL } -}; +#include <crm/common/xml_internal.h> // pcmk__xml2fd typedef struct subst_s { const char *from; const char *to; } subst_t; -static subst_t substitutions[] = { - { "Active Resources", "resources" }, - { "Assignment Scores", "allocations" }, - { "Assignment Scores and Utilization Information", "allocations_utilizations" }, - { "Cluster Summary", "summary" }, - { "Current cluster status", "cluster_status" }, - { "Executing Cluster Transition", "transition" }, - { "Failed Resource Actions", "failures" }, - { "Fencing History", "fence_history" }, - { "Full List of Resources", "resources" }, - { "Inactive Resources", "resources" }, - { "Migration Summary", "node_history" }, - { "Negative Location Constraints", "bans" }, - { "Node Attributes", "node_attributes" }, - { "Operations", "node_history" }, - { "Resource Config", "resource_config" }, - { "Resource Operations", "operations" }, - { "Revised Cluster Status", "revised_cluster_status" }, - { "Transition Summary", "actions" }, - { "Utilization Information", "utilizations" }, +static const subst_t substitutions[] = { + { "Active Resources", + PCMK_XE_RESOURCES, }, + { "Assignment Scores", + PCMK_XE_ALLOCATIONS, }, + { "Assignment Scores and Utilization Information", + PCMK_XE_ALLOCATIONS_UTILIZATIONS, }, + { "Cluster Summary", + PCMK_XE_SUMMARY, }, + { "Current cluster status", + PCMK_XE_CLUSTER_STATUS, }, + { "Executing Cluster Transition", + PCMK_XE_TRANSITION, }, + { "Failed Resource Actions", + PCMK_XE_FAILURES, }, + { "Fencing History", + PCMK_XE_FENCE_HISTORY, }, + { "Full List of Resources", + PCMK_XE_RESOURCES, }, + { "Inactive Resources", + PCMK_XE_RESOURCES, }, + { "Migration Summary", + PCMK_XE_NODE_HISTORY, }, + { "Negative Location Constraints", + PCMK_XE_BANS, }, + { "Node Attributes", + PCMK_XE_NODE_ATTRIBUTES, }, + { "Operations", + PCMK_XE_NODE_HISTORY, }, + { "Resource Config", + PCMK_XE_RESOURCE_CONFIG, }, + { "Resource Operations", + PCMK_XE_OPERATIONS, }, + { "Revised Cluster Status", + PCMK_XE_REVISED_CLUSTER_STATUS, }, + { "Timings", + PCMK_XE_TIMINGS, }, + { "Transition Summary", + PCMK_XE_ACTIONS, }, + { "Utilization Information", + PCMK_XE_UTILIZATIONS, }, { NULL, NULL } }; @@ -82,8 +84,46 @@ typedef struct private_data_s { GSList *errors; /* End members that must match the HTML version */ bool legacy_xml; + bool list_element; } private_data_t; +static bool +has_root_node(pcmk__output_t *out) +{ + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL); + + priv = out->priv; + return priv != NULL && priv->root != NULL; +} + +static void +add_root_node(pcmk__output_t *out) +{ + private_data_t *priv = NULL; + + /* has_root_node will assert if out is NULL, so no need to do it here */ + if (has_root_node(out)) { + return; + } + + priv = out->priv; + + if (priv->legacy_xml) { + priv->root = pcmk__xe_create(NULL, PCMK_XE_CRM_MON); + crm_xml_add(priv->root, PCMK_XA_VERSION, PACEMAKER_VERSION); + } else { + priv->root = pcmk__xe_create(NULL, PCMK_XE_PACEMAKER_RESULT); + crm_xml_add(priv->root, PCMK_XA_API_VERSION, PCMK__API_VERSION); + crm_xml_add(priv->root, PCMK_XA_REQUEST, + pcmk__s(out->request, "libpacemaker")); + } + + priv->parent_q = g_queue_new(); + g_queue_push_tail(priv->parent_q, priv->root); +} + static void xml_free_priv(pcmk__output_t *out) { private_data_t *priv = NULL; @@ -94,9 +134,16 @@ xml_free_priv(pcmk__output_t *out) { priv = out->priv; - free_xml(priv->root); - g_queue_free(priv->parent_q); - g_slist_free(priv->errors); + if (has_root_node(out)) { + free_xml(priv->root); + /* The elements of parent_q are xmlNodes that are a part of the + * priv->root document, so the above line already frees them. Don't + * call g_queue_free_full here. + */ + g_queue_free(priv->parent_q); + } + + g_slist_free_full(priv->errors, free); free(priv); out->priv = NULL; } @@ -119,36 +166,18 @@ xml_init(pcmk__output_t *out) { priv = out->priv; } - if (legacy_xml) { - priv->root = create_xml_node(NULL, "crm_mon"); - crm_xml_add(priv->root, "version", PACEMAKER_VERSION); - } else { - priv->root = create_xml_node(NULL, "pacemaker-result"); - crm_xml_add(priv->root, "api-version", PCMK__API_VERSION); - - if (out->request != NULL) { - crm_xml_add(priv->root, "request", out->request); - } - } - - priv->parent_q = g_queue_new(); priv->errors = NULL; - g_queue_push_tail(priv->parent_q, priv->root); - - /* Copy this from the file-level variable. This means that it is only settable - * as a command line option, and that pcmk__output_new must be called after all - * command line processing is completed. - */ - priv->legacy_xml = legacy_xml; return true; } static void add_error_node(gpointer data, gpointer user_data) { - char *str = (char *) data; + const char *str = (const char *) data; xmlNodePtr node = (xmlNodePtr) user_data; - pcmk_create_xml_text_node(node, "error", str); + + node = pcmk__xe_create(node, PCMK_XE_ERROR); + pcmk__xe_set_content(node, "%s", str); } static void @@ -159,14 +188,13 @@ xml_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_ CRM_ASSERT(out != NULL); priv = out->priv; - /* If root is NULL, xml_init failed and we are being called from pcmk__output_free - * in the pcmk__output_new path. - */ - if (priv == NULL || priv->root == NULL) { + if (priv == NULL) { return; } - if (legacy_xml) { + add_root_node(out); + + if (priv->legacy_xml) { GSList *node = priv->errors; if (exit_status != CRM_EX_OK) { @@ -180,13 +208,14 @@ xml_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_ } else { char *rc_as_str = pcmk__itoa(exit_status); - node = create_xml_node(priv->root, "status"); - pcmk__xe_set_props(node, "code", rc_as_str, - "message", crm_exit_str(exit_status), + node = pcmk__xe_create(priv->root, PCMK_XE_STATUS); + pcmk__xe_set_props(node, + PCMK_XA_CODE, rc_as_str, + PCMK_XA_MESSAGE, crm_exit_str(exit_status), NULL); if (g_slist_length(priv->errors) > 0) { - xmlNodePtr errors_node = create_xml_node(node, "errors"); + xmlNodePtr errors_node = pcmk__xe_create(node, PCMK_XE_ERRORS); g_slist_foreach(priv->errors, add_error_node, (gpointer) errors_node); } @@ -198,7 +227,7 @@ xml_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_ } if (copy_dest != NULL) { - *copy_dest = copy_xml(priv->root); + *copy_dest = pcmk__xml_copy(NULL, priv->root); } } @@ -223,18 +252,20 @@ xml_subprocess_output(pcmk__output_t *out, int exit_status, rc_as_str = pcmk__itoa(exit_status); - node = pcmk__output_xml_create_parent(out, "command", - "code", rc_as_str, + node = pcmk__output_xml_create_parent(out, PCMK_XE_COMMAND, + PCMK_XA_CODE, rc_as_str, NULL); if (proc_stdout != NULL) { - child_node = pcmk_create_xml_text_node(node, "output", proc_stdout); - crm_xml_add(child_node, "source", "stdout"); + child_node = pcmk__xe_create(node, PCMK_XE_OUTPUT); + pcmk__xe_set_content(child_node, "%s", proc_stdout); + crm_xml_add(child_node, PCMK_XA_SOURCE, "stdout"); } if (proc_stderr != NULL) { - child_node = pcmk_create_xml_text_node(node, "output", proc_stderr); - crm_xml_add(child_node, "source", "stderr"); + child_node = pcmk__xe_create(node, PCMK_XE_OUTPUT); + pcmk__xe_set_content(child_node, "%s", proc_stderr); + crm_xml_add(child_node, PCMK_XA_SOURCE, "stderr"); } free(rc_as_str); @@ -242,15 +273,16 @@ xml_subprocess_output(pcmk__output_t *out, int exit_status, static void xml_version(pcmk__output_t *out, bool extended) { + const char *author = "Andrew Beekhof and the Pacemaker project " + "contributors"; CRM_ASSERT(out != NULL); - pcmk__output_create_xml_node(out, "version", - "program", "Pacemaker", - "version", PACEMAKER_VERSION, - "author", "Andrew Beekhof and the " - "Pacemaker project contributors", - "build", BUILD_VERSION, - "features", CRM_FEATURES, + pcmk__output_create_xml_node(out, PCMK_XE_VERSION, + PCMK_XA_PROGRAM, "Pacemaker", + PCMK_XA_VERSION, PACEMAKER_VERSION, + PCMK_XA_AUTHOR, author, + PCMK_XA_BUILD, BUILD_VERSION, + PCMK_XA_FEATURES, CRM_FEATURES, NULL); } @@ -265,6 +297,8 @@ xml_err(pcmk__output_t *out, const char *format, ...) { CRM_ASSERT(out != NULL && out->priv != NULL); priv = out->priv; + add_root_node(out); + va_start(ap, format); len = vasprintf(&buf, format, ap); CRM_ASSERT(len > 0); @@ -302,20 +336,20 @@ xml_begin_list(pcmk__output_t *out, const char *singular_noun, const char *plura char *name = NULL; char *buf = NULL; int len; + private_data_t *priv = NULL; - CRM_ASSERT(out != NULL); + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; va_start(ap, format); len = vasprintf(&buf, format, ap); CRM_ASSERT(len >= 0); va_end(ap); - if (substitute) { - for (subst_t *s = substitutions; s->from != NULL; s++) { - if (!strcmp(s->from, buf)) { - name = g_strdup(s->to); - break; - } + for (const subst_t *s = substitutions; s->from != NULL; s++) { + if (strcmp(s->from, buf) == 0) { + name = g_strdup(s->to); + break; } } @@ -323,12 +357,12 @@ xml_begin_list(pcmk__output_t *out, const char *singular_noun, const char *plura name = g_ascii_strdown(buf, -1); } - if (legacy_xml || simple_list) { - pcmk__output_xml_create_parent(out, name, NULL); - } else { - pcmk__output_xml_create_parent(out, "list", - "name", name, + if (priv->list_element) { + pcmk__output_xml_create_parent(out, PCMK_XE_LIST, + PCMK_XA_NAME, name, NULL); + } else { + pcmk__output_xml_create_parent(out, name, NULL); } g_free(name); @@ -350,10 +384,10 @@ xml_list_item(pcmk__output_t *out, const char *name, const char *format, ...) { CRM_ASSERT(len >= 0); va_end(ap); - item_node = pcmk__output_create_xml_text_node(out, "item", buf); + item_node = pcmk__output_create_xml_text_node(out, PCMK_XE_ITEM, buf); if (name != NULL) { - crm_xml_add(item_node, "name", name); + crm_xml_add(item_node, PCMK_XA_NAME, name); } free(buf); @@ -371,16 +405,18 @@ xml_end_list(pcmk__output_t *out) { CRM_ASSERT(out != NULL && out->priv != NULL); priv = out->priv; - if (priv->legacy_xml || simple_list) { - g_queue_pop_tail(priv->parent_q); - } else { + if (priv->list_element) { char *buf = NULL; xmlNodePtr node; + /* Do not free node here - it's still part of the document */ node = g_queue_pop_tail(priv->parent_q); buf = crm_strdup_printf("%lu", xmlChildElementCount(node)); - crm_xml_add(node, "count", buf); + crm_xml_add(node, PCMK_XA_COUNT, buf); free(buf); + } else { + /* Do not free this result - it's still part of the document */ + g_queue_pop_tail(priv->parent_q); } } @@ -465,13 +501,15 @@ pcmk__output_xml_add_node_copy(pcmk__output_t *out, xmlNodePtr node) { CRM_ASSERT(node != NULL); CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return); + add_root_node(out); + priv = out->priv; parent = g_queue_peek_tail(priv->parent_q); // Shouldn't happen unless the caller popped priv->root CRM_CHECK(parent != NULL, return); - add_node_copy(parent, node); + pcmk__xml_copy(parent, node); } xmlNodePtr @@ -483,9 +521,11 @@ pcmk__output_create_xml_node(pcmk__output_t *out, const char *name, ...) { CRM_ASSERT(out != NULL && out->priv != NULL); CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL); + add_root_node(out); + priv = out->priv; - node = create_xml_node(g_queue_peek_tail(priv->parent_q), name); + node = pcmk__xe_create(g_queue_peek_tail(priv->parent_q), name); va_start(args, name); pcmk__xe_set_propv(node, args); va_end(args); @@ -501,7 +541,7 @@ pcmk__output_create_xml_text_node(pcmk__output_t *out, const char *name, const c CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL); node = pcmk__output_create_xml_node(out, name, NULL); - xmlNodeSetContent(node, (pcmkXmlStr) content); + pcmk__xe_set_content(node, "%s", content); return node; } @@ -513,6 +553,8 @@ pcmk__output_xml_push_parent(pcmk__output_t *out, xmlNodePtr parent) { CRM_ASSERT(parent != NULL); CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return); + add_root_node(out); + priv = out->priv; g_queue_push_tail(priv->parent_q, parent); @@ -525,9 +567,12 @@ pcmk__output_xml_pop_parent(pcmk__output_t *out) { CRM_ASSERT(out != NULL && out->priv != NULL); CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return); + add_root_node(out); + priv = out->priv; CRM_ASSERT(g_queue_get_length(priv->parent_q) > 0); + /* Do not free this result - it's still part of the document */ g_queue_pop_tail(priv->parent_q); } @@ -538,8 +583,61 @@ pcmk__output_xml_peek_parent(pcmk__output_t *out) { CRM_ASSERT(out != NULL && out->priv != NULL); CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL); + add_root_node(out); + priv = out->priv; /* If queue is empty NULL will be returned */ return g_queue_peek_tail(priv->parent_q); } + +bool +pcmk__output_get_legacy_xml(pcmk__output_t *out) +{ + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL); + + if (!pcmk__str_eq(out->fmt_name, "xml", pcmk__str_none)) { + return false; + } + + CRM_ASSERT(out->priv != NULL); + + priv = out->priv; + return priv->legacy_xml; +} + +void +pcmk__output_set_legacy_xml(pcmk__output_t *out) +{ + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL); + + if (!pcmk__str_eq(out->fmt_name, "xml", pcmk__str_none)) { + return; + } + + CRM_ASSERT(out->priv != NULL); + + priv = out->priv; + priv->legacy_xml = true; +} + +void +pcmk__output_enable_list_element(pcmk__output_t *out) +{ + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL); + + if (!pcmk__str_eq(out->fmt_name, "xml", pcmk__str_none)) { + return; + } + + CRM_ASSERT(out->priv != NULL); + + priv = out->priv; + priv->list_element = true; +} diff --git a/lib/common/patchset.c b/lib/common/patchset.c index 34e27fb..c12a8ef 100644 --- a/lib/common/patchset.c +++ b/lib/common/patchset.c @@ -1,5 +1,5 @@ /* - * 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. * @@ -21,14 +21,10 @@ #include <libxml/tree.h> #include <crm/crm.h> -#include <crm/msg_xml.h> #include <crm/common/xml.h> #include <crm/common/xml_internal.h> // CRM_XML_LOG_BASE, etc. #include "crmcommon_private.h" -static xmlNode *subtract_xml_comment(xmlNode *parent, xmlNode *left, - xmlNode *right, gboolean *changed); - /* Add changes for specified XML to patchset. * For patchset format, refer to diff schema. */ @@ -56,12 +52,12 @@ add_xml_changes_to_patchset(xmlNode *xml, xmlNode *patchset) if (xpath != NULL) { int position = pcmk__xml_position(xml, pcmk__xf_deleted); - change = create_xml_node(patchset, XML_DIFF_CHANGE); + change = pcmk__xe_create(patchset, PCMK_XE_CHANGE); - crm_xml_add(change, XML_DIFF_OP, "create"); - crm_xml_add(change, XML_DIFF_PATH, (const char *) xpath->str); - crm_xml_add_int(change, XML_DIFF_POSITION, position); - add_node_copy(change, xml); + crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_CREATE); + crm_xml_add(change, PCMK_XA_PATH, (const char *) xpath->str); + crm_xml_add_int(change, PCMK_XE_POSITION, position); + pcmk__xml_copy(change, xml); g_string_free(xpath, TRUE); } @@ -82,35 +78,35 @@ add_xml_changes_to_patchset(xmlNode *xml, xmlNode *patchset) GString *xpath = pcmk__element_xpath(xml); if (xpath != NULL) { - change = create_xml_node(patchset, XML_DIFF_CHANGE); + change = pcmk__xe_create(patchset, PCMK_XE_CHANGE); - crm_xml_add(change, XML_DIFF_OP, "modify"); - crm_xml_add(change, XML_DIFF_PATH, (const char *) xpath->str); + crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_MODIFY); + crm_xml_add(change, PCMK_XA_PATH, (const char *) xpath->str); - change = create_xml_node(change, XML_DIFF_LIST); + change = pcmk__xe_create(change, PCMK_XE_CHANGE_LIST); g_string_free(xpath, TRUE); } } - attr = create_xml_node(change, XML_DIFF_ATTR); + attr = pcmk__xe_create(change, PCMK_XE_CHANGE_ATTR); - crm_xml_add(attr, XML_NVPAIR_ATTR_NAME, (const char *)pIter->name); + crm_xml_add(attr, PCMK_XA_NAME, (const char *) pIter->name); if (nodepriv->flags & pcmk__xf_deleted) { - crm_xml_add(attr, XML_DIFF_OP, "unset"); + crm_xml_add(attr, PCMK_XA_OPERATION, "unset"); } else { - crm_xml_add(attr, XML_DIFF_OP, "set"); + crm_xml_add(attr, PCMK_XA_OPERATION, "set"); value = pcmk__xml_attr_value(pIter); - crm_xml_add(attr, XML_NVPAIR_ATTR_VALUE, value); + crm_xml_add(attr, PCMK_XA_VALUE, value); } } if (change) { xmlNode *result = NULL; - change = create_xml_node(change->parent, XML_DIFF_RESULT); - result = create_xml_node(change, (const char *)xml->name); + change = pcmk__xe_create(change->parent, PCMK_XE_CHANGE_RESULT); + result = pcmk__xe_create(change, (const char *)xml->name); for (pIter = pcmk__xe_first_attr(xml); pIter != NULL; pIter = pIter->next) { @@ -133,14 +129,15 @@ add_xml_changes_to_patchset(xmlNode *xml, xmlNode *patchset) GString *xpath = pcmk__element_xpath(xml); crm_trace("%s.%s moved to position %d", - xml->name, ID(xml), pcmk__xml_position(xml, pcmk__xf_skip)); + xml->name, pcmk__xe_id(xml), + pcmk__xml_position(xml, pcmk__xf_skip)); if (xpath != NULL) { - change = create_xml_node(patchset, XML_DIFF_CHANGE); + change = pcmk__xe_create(patchset, PCMK_XE_CHANGE); - crm_xml_add(change, XML_DIFF_OP, "move"); - crm_xml_add(change, XML_DIFF_PATH, (const char *) xpath->str); - crm_xml_add_int(change, XML_DIFF_POSITION, + crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_MOVE); + crm_xml_add(change, PCMK_XA_PATH, (const char *) xpath->str); + crm_xml_add_int(change, PCMK_XE_POSITION, pcmk__xml_position(xml, pcmk__xf_deleted)); g_string_free(xpath, TRUE); } @@ -153,7 +150,8 @@ is_config_change(xmlNode *xml) GList *gIter = NULL; xml_node_private_t *nodepriv = NULL; xml_doc_private_t *docpriv; - xmlNode *config = first_named_child(xml, XML_CIB_TAG_CONFIGURATION); + xmlNode *config = pcmk__xe_first_child(xml, PCMK_XE_CONFIGURATION, NULL, + NULL); if (config) { nodepriv = config->_private; @@ -168,7 +166,7 @@ is_config_change(xmlNode *xml) pcmk__deleted_xml_t *deleted_obj = gIter->data; if (strstr(deleted_obj->path, - "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION) != NULL) { + "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION) != NULL) { return TRUE; } } @@ -176,6 +174,7 @@ is_config_change(xmlNode *xml) return FALSE; } +// @COMPAT Remove when v1 patchsets are removed static void xml_repair_v1_diff(xmlNode *last, xmlNode *next, xmlNode *local_diff, gboolean changed) @@ -187,9 +186,9 @@ xml_repair_v1_diff(xmlNode *last, xmlNode *next, xmlNode *local_diff, const char *tag = NULL; const char *vfields[] = { - XML_ATTR_GENERATION_ADMIN, - XML_ATTR_GENERATION, - XML_ATTR_NUMUPDATES, + PCMK_XA_ADMIN_EPOCH, + PCMK_XA_EPOCH, + PCMK_XA_NUM_UPDATES, }; if (local_diff == NULL) { @@ -197,16 +196,16 @@ xml_repair_v1_diff(xmlNode *last, xmlNode *next, xmlNode *local_diff, return; } - tag = XML_TAG_DIFF_REMOVED; - diff_child = find_xml_node(local_diff, tag, FALSE); + tag = PCMK__XE_DIFF_REMOVED; + diff_child = pcmk__xe_first_child(local_diff, tag, NULL, NULL); if (diff_child == NULL) { - diff_child = create_xml_node(local_diff, tag); + diff_child = pcmk__xe_create(local_diff, tag); } - tag = XML_TAG_CIB; - cib = find_xml_node(diff_child, tag, FALSE); + tag = PCMK_XE_CIB; + cib = pcmk__xe_first_child(diff_child, tag, NULL, NULL); if (cib == NULL) { - cib = create_xml_node(diff_child, tag); + cib = pcmk__xe_create(diff_child, tag); } for (lpc = 0; (last != NULL) && (lpc < PCMK__NELEM(vfields)); lpc++) { @@ -218,16 +217,16 @@ xml_repair_v1_diff(xmlNode *last, xmlNode *next, xmlNode *local_diff, } } - tag = XML_TAG_DIFF_ADDED; - diff_child = find_xml_node(local_diff, tag, FALSE); + tag = PCMK__XE_DIFF_ADDED; + diff_child = pcmk__xe_first_child(local_diff, tag, NULL, NULL); if (diff_child == NULL) { - diff_child = create_xml_node(local_diff, tag); + diff_child = pcmk__xe_create(local_diff, tag); } - tag = XML_TAG_CIB; - cib = find_xml_node(diff_child, tag, FALSE); + tag = PCMK_XE_CIB; + cib = pcmk__xe_first_child(diff_child, tag, NULL, NULL); if (cib == NULL) { - cib = create_xml_node(diff_child, tag); + cib = pcmk__xe_create(diff_child, tag); } for (lpc = 0; next && lpc < PCMK__NELEM(vfields); lpc++) { @@ -246,11 +245,12 @@ xml_repair_v1_diff(xmlNode *last, xmlNode *next, xmlNode *local_diff, crm_log_xml_explicit(local_diff, "Repaired-diff"); } +// @COMPAT Remove when v1 patchsets are removed static xmlNode * xml_create_patchset_v1(xmlNode *source, xmlNode *target, bool config, bool suppress) { - xmlNode *patchset = diff_xml_object(source, target, suppress); + xmlNode *patchset = pcmk__diff_v1_xml_object(source, target, suppress); if (patchset) { CRM_LOG_ASSERT(xml_document_dirty(target)); @@ -271,9 +271,9 @@ xml_create_patchset_v2(xmlNode *source, xmlNode *target) xmlNode *version = NULL; xmlNode *patchset = NULL; const char *vfields[] = { - XML_ATTR_GENERATION_ADMIN, - XML_ATTR_GENERATION, - XML_ATTR_NUMUPDATES, + PCMK_XA_ADMIN_EPOCH, + PCMK_XA_EPOCH, + PCMK_XA_NUM_UPDATES, }; CRM_ASSERT(target); @@ -284,12 +284,12 @@ xml_create_patchset_v2(xmlNode *source, xmlNode *target) CRM_ASSERT(target->doc); docpriv = target->doc->_private; - patchset = create_xml_node(NULL, XML_TAG_DIFF); + patchset = pcmk__xe_create(NULL, PCMK_XE_DIFF); crm_xml_add_int(patchset, PCMK_XA_FORMAT, 2); - version = create_xml_node(patchset, XML_DIFF_VERSION); + version = pcmk__xe_create(patchset, PCMK_XE_VERSION); - v = create_xml_node(version, XML_DIFF_VSOURCE); + v = pcmk__xe_create(version, PCMK_XE_SOURCE); for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { const char *value = crm_element_value(source, vfields[lpc]); @@ -299,7 +299,7 @@ xml_create_patchset_v2(xmlNode *source, xmlNode *target) crm_xml_add(v, vfields[lpc], value); } - v = create_xml_node(version, XML_DIFF_VTARGET); + v = pcmk__xe_create(version, PCMK_XE_TARGET); for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { const char *value = crm_element_value(target, vfields[lpc]); @@ -311,12 +311,12 @@ xml_create_patchset_v2(xmlNode *source, xmlNode *target) for (gIter = docpriv->deleted_objs; gIter; gIter = gIter->next) { pcmk__deleted_xml_t *deleted_obj = gIter->data; - xmlNode *change = create_xml_node(patchset, XML_DIFF_CHANGE); + xmlNode *change = pcmk__xe_create(patchset, PCMK_XE_CHANGE); - crm_xml_add(change, XML_DIFF_OP, "delete"); - crm_xml_add(change, XML_DIFF_PATH, deleted_obj->path); + crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_DELETE); + crm_xml_add(change, PCMK_XA_PATH, deleted_obj->path); if (deleted_obj->position >= 0) { - crm_xml_add_int(change, XML_DIFF_POSITION, deleted_obj->position); + crm_xml_add_int(change, PCMK_XE_POSITION, deleted_obj->position); } } @@ -331,7 +331,7 @@ xml_create_patchset(int format, xmlNode *source, xmlNode *target, int counter = 0; bool config = FALSE; xmlNode *patch = NULL; - const char *version = crm_element_value(source, XML_ATTR_CRM_VERSION); + const char *version = crm_element_value(source, PCMK_XA_CRM_FEATURE_SET); xml_acl_disable(target); if (!xml_document_dirty(target)) { @@ -346,16 +346,16 @@ xml_create_patchset(int format, xmlNode *source, xmlNode *target, if (manage_version && config) { crm_trace("Config changed %d", format); - crm_xml_add(target, XML_ATTR_NUMUPDATES, "0"); + crm_xml_add(target, PCMK_XA_NUM_UPDATES, "0"); - crm_element_value_int(target, XML_ATTR_GENERATION, &counter); - crm_xml_add_int(target, XML_ATTR_GENERATION, counter+1); + crm_element_value_int(target, PCMK_XA_EPOCH, &counter); + crm_xml_add_int(target, PCMK_XA_EPOCH, counter+1); } else if (manage_version) { - crm_element_value_int(target, XML_ATTR_NUMUPDATES, &counter); + crm_element_value_int(target, PCMK_XA_NUM_UPDATES, &counter); crm_trace("Status changed %d - %d %s", format, counter, - crm_element_value(source, XML_ATTR_NUMUPDATES)); - crm_xml_add_int(target, XML_ATTR_NUMUPDATES, (counter + 1)); + crm_element_value(source, PCMK_XA_NUM_UPDATES)); + crm_xml_add_int(target, PCMK_XA_NUM_UPDATES, (counter + 1)); } if (format == 0) { @@ -369,6 +369,7 @@ xml_create_patchset(int format, xmlNode *source, xmlNode *target, switch (format) { case 1: + // @COMPAT Remove when v1 patchsets are removed patch = xml_create_patchset_v1(source, target, config, FALSE); break; case 2: @@ -403,23 +404,228 @@ patchset_process_digest(xmlNode *patch, xmlNode *source, xmlNode *target, return; } - version = crm_element_value(source, XML_ATTR_CRM_VERSION); + version = crm_element_value(source, PCMK_XA_CRM_FEATURE_SET); digest = calculate_xml_versioned_digest(target, FALSE, TRUE, version); - crm_xml_add(patch, XML_ATTR_DIGEST, digest); + crm_xml_add(patch, PCMK__XA_DIGEST, digest); free(digest); return; } -// Return true if attribute name is not "id" +// @COMPAT Remove when v1 patchsets are removed +static xmlNode * +subtract_v1_xml_comment(xmlNode *parent, xmlNode *left, xmlNode *right, + gboolean *changed) +{ + CRM_CHECK(left != NULL, return NULL); + CRM_CHECK(left->type == XML_COMMENT_NODE, return NULL); + + if ((right == NULL) || !pcmk__str_eq((const char *)left->content, + (const char *)right->content, + pcmk__str_casei)) { + xmlNode *deleted = NULL; + + deleted = pcmk__xml_copy(parent, left); + *changed = TRUE; + + return deleted; + } + + return NULL; +} + +// @COMPAT Remove when v1 patchsets are removed +static xmlNode * +subtract_v1_xml_object(xmlNode *parent, xmlNode *left, xmlNode *right, + bool full, gboolean *changed, const char *marker) +{ + gboolean dummy = FALSE; + xmlNode *diff = NULL; + xmlNode *right_child = NULL; + xmlNode *left_child = NULL; + xmlAttrPtr xIter = NULL; + + const char *id = NULL; + const char *name = NULL; + const char *value = NULL; + const char *right_val = NULL; + + if (changed == NULL) { + changed = &dummy; + } + + if (left == NULL) { + return NULL; + } + + if (left->type == XML_COMMENT_NODE) { + return subtract_v1_xml_comment(parent, left, right, changed); + } + + id = pcmk__xe_id(left); + name = (const char *) left->name; + if (right == NULL) { + xmlNode *deleted = NULL; + + crm_trace("Processing <%s " PCMK_XA_ID "=%s> (complete copy)", + name, id); + deleted = pcmk__xml_copy(parent, left); + crm_xml_add(deleted, PCMK__XA_CRM_DIFF_MARKER, marker); + + *changed = TRUE; + return deleted; + } + + CRM_CHECK(name != NULL, return NULL); + CRM_CHECK(pcmk__xe_is(left, (const char *) right->name), return NULL); + + // Check for PCMK__XA_CRM_DIFF_MARKER in a child + value = crm_element_value(right, PCMK__XA_CRM_DIFF_MARKER); + if ((value != NULL) && (strcmp(value, "removed:top") == 0)) { + crm_trace("We are the root of the deletion: %s.id=%s", name, id); + *changed = TRUE; + return NULL; + } + + // @TODO Avoiding creating the full hierarchy would save work here + diff = pcmk__xe_create(parent, name); + + // Changes to child objects + for (left_child = pcmk__xml_first_child(left); left_child != NULL; + left_child = pcmk__xml_next(left_child)) { + gboolean child_changed = FALSE; + + right_child = pcmk__xml_match(right, left_child, false); + subtract_v1_xml_object(diff, left_child, right_child, full, + &child_changed, marker); + if (child_changed) { + *changed = TRUE; + } + } + + if (!*changed) { + /* Nothing to do */ + + } else if (full) { + xmlAttrPtr pIter = NULL; + + for (pIter = pcmk__xe_first_attr(left); pIter != NULL; + pIter = pIter->next) { + const char *p_name = (const char *)pIter->name; + const char *p_value = pcmk__xml_attr_value(pIter); + + xmlSetProp(diff, (pcmkXmlStr) p_name, (pcmkXmlStr) p_value); + } + + // We have everything we need + goto done; + } + + // Changes to name/value pairs + for (xIter = pcmk__xe_first_attr(left); xIter != NULL; + xIter = xIter->next) { + const char *prop_name = (const char *) xIter->name; + xmlAttrPtr right_attr = NULL; + xml_node_private_t *nodepriv = NULL; + + if (strcmp(prop_name, PCMK_XA_ID) == 0) { + // id already obtained when present ~ this case, so just reuse + xmlSetProp(diff, (pcmkXmlStr) PCMK_XA_ID, (pcmkXmlStr) id); + continue; + } + + if (pcmk__xa_filterable(prop_name)) { + continue; + } + + right_attr = xmlHasProp(right, (pcmkXmlStr) prop_name); + if (right_attr) { + nodepriv = right_attr->_private; + } + + right_val = crm_element_value(right, prop_name); + if ((right_val == NULL) || (nodepriv && pcmk_is_set(nodepriv->flags, pcmk__xf_deleted))) { + /* new */ + *changed = TRUE; + if (full) { + xmlAttrPtr pIter = NULL; + + for (pIter = pcmk__xe_first_attr(left); pIter != NULL; + pIter = pIter->next) { + const char *p_name = (const char *) pIter->name; + const char *p_value = pcmk__xml_attr_value(pIter); + + xmlSetProp(diff, (pcmkXmlStr) p_name, (pcmkXmlStr) p_value); + } + break; + + } else { + const char *left_value = pcmk__xml_attr_value(xIter); + + xmlSetProp(diff, (pcmkXmlStr) prop_name, (pcmkXmlStr) value); + crm_xml_add(diff, prop_name, left_value); + } + + } else { + /* Only now do we need the left value */ + const char *left_value = pcmk__xml_attr_value(xIter); + + if (strcmp(left_value, right_val) == 0) { + /* unchanged */ + + } else { + *changed = TRUE; + if (full) { + xmlAttrPtr pIter = NULL; + + crm_trace("Changes detected to %s in " + "<%s " PCMK_XA_ID "=%s>", prop_name, name, id); + for (pIter = pcmk__xe_first_attr(left); pIter != NULL; + pIter = pIter->next) { + const char *p_name = (const char *) pIter->name; + const char *p_value = pcmk__xml_attr_value(pIter); + + xmlSetProp(diff, (pcmkXmlStr) p_name, + (pcmkXmlStr) p_value); + } + break; + + } else { + crm_trace("Changes detected to %s (%s -> %s) in " + "<%s " PCMK_XA_ID "=%s>", + prop_name, left_value, right_val, name, id); + crm_xml_add(diff, prop_name, left_value); + } + } + } + } + + if (!*changed) { + free_xml(diff); + return NULL; + + } else if (!full && (id != NULL)) { + crm_xml_add(diff, PCMK_XA_ID, id); + } + done: + return diff; +} + +/* @COMPAT Remove when v1 patchsets are removed. + * + * Return true if attribute name is not \c PCMK_XML_ID. + */ static bool not_id(xmlAttrPtr attr, void *user_data) { - return strcmp((const char *) attr->name, XML_ATTR_ID) != 0; + return strcmp((const char *) attr->name, PCMK_XA_ID) != 0; } -// Apply the removals section of an v1 patchset to an XML node +/* @COMPAT Remove when v1 patchsets are removed. + * + * Apply the removals section of a v1 patchset to an XML node. + */ static void process_v1_removals(xmlNode *target, xmlNode *patch) { @@ -436,15 +642,17 @@ process_v1_removals(xmlNode *target, xmlNode *patch) if (target->type == XML_COMMENT_NODE) { gboolean dummy; - subtract_xml_comment(target->parent, target, patch, &dummy); + subtract_v1_xml_comment(target->parent, target, patch, &dummy); } CRM_CHECK(pcmk__xe_is(target, (const char *) patch->name), return); - CRM_CHECK(pcmk__str_eq(ID(target), ID(patch), pcmk__str_casei), return); + CRM_CHECK(pcmk__str_eq(pcmk__xe_id(target), pcmk__xe_id(patch), + pcmk__str_none), + return); - // Check for XML_DIFF_MARKER in a child - id = crm_element_value_copy(target, XML_ATTR_ID); - value = crm_element_value(patch, XML_DIFF_MARKER); + // Check for PCMK__XA_CRM_DIFF_MARKER in a child + id = crm_element_value_copy(target, PCMK_XA_ID); + value = crm_element_value(patch, PCMK__XA_CRM_DIFF_MARKER); if ((value != NULL) && (strcmp(value, "removed:top") == 0)) { crm_trace("We are the root of the deletion: %s.id=%s", target->name, id); @@ -468,7 +676,10 @@ process_v1_removals(xmlNode *target, xmlNode *patch) free(id); } -// Apply the additions section of an v1 patchset to an XML node +/* @COMPAT Remove when v1 patchsets are removed. + * + * Apply the additions section of a v1 patchset to an XML node. + */ static void process_v1_additions(xmlNode *parent, xmlNode *target, xmlNode *patch) { @@ -486,18 +697,18 @@ process_v1_additions(xmlNode *parent, xmlNode *target, xmlNode *patch) return; } - // Check for XML_DIFF_MARKER in a child + // Check for PCMK__XA_CRM_DIFF_MARKER in a child name = (const char *) patch->name; - value = crm_element_value(patch, XML_DIFF_MARKER); + value = crm_element_value(patch, PCMK__XA_CRM_DIFF_MARKER); if ((target == NULL) && (value != NULL) && (strcmp(value, "added:top") == 0)) { - id = ID(patch); + id = pcmk__xe_id(patch); crm_trace("We are the root of the addition: %s.id=%s", name, id); - add_node_copy(parent, patch); + pcmk__xml_copy(parent, patch); return; } else if (target == NULL) { - id = ID(patch); + id = pcmk__xe_id(patch); crm_err("Could not locate: %s.id=%s", name, id); return; } @@ -507,14 +718,16 @@ process_v1_additions(xmlNode *parent, xmlNode *target, xmlNode *patch) } CRM_CHECK(pcmk__xe_is(target, name), return); - CRM_CHECK(pcmk__str_eq(ID(target), ID(patch), pcmk__str_casei), return); + CRM_CHECK(pcmk__str_eq(pcmk__xe_id(target), pcmk__xe_id(patch), + pcmk__str_none), + return); for (xIter = pcmk__xe_first_attr(patch); xIter != NULL; xIter = xIter->next) { const char *p_name = (const char *) xIter->name; const char *p_value = pcmk__xml_attr_value(xIter); - xml_remove_prop(target, p_name); // Preserve patch order + pcmk__xe_remove_attr(target, p_name); // Preserve patch order crm_xml_add(target, p_name, p_value); } @@ -547,17 +760,20 @@ find_patch_xml_node(const xmlNode *patchset, int format, bool added, switch (format) { case 1: - label = added? XML_TAG_DIFF_ADDED : XML_TAG_DIFF_REMOVED; - *patch_node = find_xml_node(patchset, label, FALSE); - cib_node = find_xml_node(*patch_node, "cib", FALSE); + // @COMPAT Remove when v1 patchsets are removed + label = added? PCMK__XE_DIFF_ADDED : PCMK__XE_DIFF_REMOVED; + *patch_node = pcmk__xe_first_child(patchset, label, NULL, NULL); + cib_node = pcmk__xe_first_child(*patch_node, PCMK_XE_CIB, NULL, + NULL); if (cib_node != NULL) { *patch_node = cib_node; } break; case 2: - label = added? "target" : "source"; - *patch_node = find_xml_node(patchset, "version", FALSE); - *patch_node = find_xml_node(*patch_node, label, FALSE); + label = added? PCMK_XE_TARGET : PCMK_XE_SOURCE; + *patch_node = pcmk__xe_first_child(patchset, PCMK_XE_VERSION, NULL, + NULL); + *patch_node = pcmk__xe_first_child(*patch_node, label, NULL, NULL); break; default: crm_warn("Unknown patch format: %d", format); @@ -576,9 +792,9 @@ xml_patch_versions(const xmlNode *patchset, int add[3], int del[3]) xmlNode *tmp = NULL; const char *vfields[] = { - XML_ATTR_GENERATION_ADMIN, - XML_ATTR_GENERATION, - XML_ATTR_NUMUPDATES, + PCMK_XA_ADMIN_EPOCH, + PCMK_XA_EPOCH, + PCMK_XA_NUM_UPDATES, }; @@ -628,9 +844,9 @@ xml_patch_version_check(const xmlNode *xml, const xmlNode *patchset) int del[] = { 0, 0, 0 }; const char *vfields[] = { - XML_ATTR_GENERATION_ADMIN, - XML_ATTR_GENERATION, - XML_ATTR_NUMUPDATES, + PCMK_XA_ADMIN_EPOCH, + PCMK_XA_EPOCH, + PCMK_XA_NUM_UPDATES, }; for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { @@ -684,6 +900,22 @@ xml_patch_version_check(const xmlNode *xml, const xmlNode *patchset) return pcmk_rc_ok; } +// @COMPAT Remove when v1 patchsets are removed +static void +purge_v1_diff_markers(xmlNode *node) +{ + xmlNode *child = NULL; + + CRM_CHECK(node != NULL, return); + + pcmk__xe_remove_attr(node, PCMK__XA_CRM_DIFF_MARKER); + for (child = pcmk__xml_first_child(node); child != NULL; + child = pcmk__xml_next(child)) { + purge_v1_diff_markers(child); + } +} + +// @COMPAT Remove when v1 patchsets are removed /*! * \internal * \brief Apply a version 1 patchset to an XML node @@ -700,9 +932,11 @@ apply_v1_patchset(xmlNode *xml, const xmlNode *patchset) int root_nodes_seen = 0; xmlNode *child_diff = NULL; - xmlNode *added = find_xml_node(patchset, XML_TAG_DIFF_ADDED, FALSE); - xmlNode *removed = find_xml_node(patchset, XML_TAG_DIFF_REMOVED, FALSE); - xmlNode *old = copy_xml(xml); + xmlNode *added = pcmk__xe_first_child(patchset, PCMK__XE_DIFF_ADDED, NULL, + NULL); + xmlNode *removed = pcmk__xe_first_child(patchset, PCMK__XE_DIFF_REMOVED, + NULL, NULL); + xmlNode *old = pcmk__xml_copy(NULL, xml); crm_trace("Subtraction Phase"); for (child_diff = pcmk__xml_first_child(removed); child_diff != NULL; @@ -741,7 +975,7 @@ apply_v1_patchset(xmlNode *xml, const xmlNode *patchset) rc = ENOTUNIQ; } - purge_diff_markers(xml); // Purge prior to checking digest + purge_v1_diff_markers(xml); // Purge prior to checking digest free_xml(old); return rc; @@ -759,7 +993,7 @@ first_matching_xml_child(const xmlNode *parent, const char *name, if (strcmp((const char *) cIter->name, name) != 0) { continue; } else if (id) { - const char *cid = ID(cIter); + const char *cid = pcmk__xe_id(cIter); if ((cid == NULL) || (strcmp(cid, id) != 0)) { continue; @@ -811,24 +1045,17 @@ search_v2_xpath(const xmlNode *top, const char *key, int target_position) * than key_len - 1 characters plus a null terminator. */ - remainder = calloc(key_len, sizeof(char)); - CRM_ASSERT(remainder != NULL); - - section = calloc(key_len, sizeof(char)); - CRM_ASSERT(section != NULL); - - id = calloc(key_len, sizeof(char)); - CRM_ASSERT(id != NULL); - - tag = calloc(key_len, sizeof(char)); - CRM_ASSERT(tag != NULL); + remainder = pcmk__assert_alloc(key_len, sizeof(char)); + section = pcmk__assert_alloc(key_len, sizeof(char)); + id = pcmk__assert_alloc(key_len, sizeof(char)); + tag = pcmk__assert_alloc(key_len, sizeof(char)); do { // Look for /NEXT_COMPONENT/REMAINING_COMPONENTS rc = sscanf(current, "/%[^/]%s", section, remainder); if (rc > 0) { // Separate FIRST_COMPONENT into TAG[@id='ID'] - int f = sscanf(section, "%[^[][@" XML_ATTR_ID "='%[^']", tag, id); + int f = sscanf(section, "%[^[][@" PCMK_XA_ID "='%[^']", tag, id); int current_position = -1; /* The target position is for the final component tag, so only use @@ -840,6 +1067,7 @@ search_v2_xpath(const xmlNode *top, const char *key, int target_position) switch (f) { case 1: + // @COMPAT Remove when v1 patchsets are removed target = first_matching_xml_child(target, tag, NULL, current_position); break; @@ -886,8 +1114,8 @@ sort_change_obj_by_position(gconstpointer a, gconstpointer b) int position_a = -1; int position_b = -1; - crm_element_value_int(change_obj_a->change, XML_DIFF_POSITION, &position_a); - crm_element_value_int(change_obj_b->change, XML_DIFF_POSITION, &position_b); + crm_element_value_int(change_obj_a->change, PCMK_XE_POSITION, &position_a); + crm_element_value_int(change_obj_b->change, PCMK_XE_POSITION, &position_b); if (position_a < position_b) { return -1; @@ -919,8 +1147,8 @@ apply_v2_patchset(xmlNode *xml, const xmlNode *patchset) for (change = pcmk__xml_first_child(patchset); change != NULL; change = pcmk__xml_next(change)) { xmlNode *match = NULL; - const char *op = crm_element_value(change, XML_DIFF_OP); - const char *xpath = crm_element_value(change, XML_DIFF_PATH); + const char *op = crm_element_value(change, PCMK_XA_OPERATION); + const char *xpath = crm_element_value(change, PCMK_XA_PATH); int position = -1; if (op == NULL) { @@ -929,14 +1157,16 @@ apply_v2_patchset(xmlNode *xml, const xmlNode *patchset) crm_trace("Processing %s %s", change->name, op); - // "delete" changes for XML comments are generated with "position" - if (strcmp(op, "delete") == 0) { - crm_element_value_int(change, XML_DIFF_POSITION, &position); + /* PCMK_VALUE_DELETE changes for XML comments are generated with + * PCMK_XE_POSITION + */ + if (strcmp(op, PCMK_VALUE_DELETE) == 0) { + crm_element_value_int(change, PCMK_XE_POSITION, &position); } match = search_v2_xpath(xml, xpath, position); crm_trace("Performing %s on %s with %p", op, xpath, match); - if ((match == NULL) && (strcmp(op, "delete") == 0)) { + if ((match == NULL) && (strcmp(op, PCMK_VALUE_DELETE) == 0)) { crm_debug("No %s match for %s in %p", op, xpath, xml->doc); continue; @@ -945,32 +1175,33 @@ apply_v2_patchset(xmlNode *xml, const xmlNode *patchset) rc = pcmk_rc_diff_failed; continue; - } else if ((strcmp(op, "create") == 0) || (strcmp(op, "move") == 0)) { - // Delay the adding of a "create" object - xml_change_obj_t *change_obj = calloc(1, sizeof(xml_change_obj_t)); - - CRM_ASSERT(change_obj != NULL); + } else if (pcmk__str_any_of(op, + PCMK_VALUE_CREATE, PCMK_VALUE_MOVE, NULL)) { + // Delay the adding of a PCMK_VALUE_CREATE object + xml_change_obj_t *change_obj = + pcmk__assert_alloc(1, sizeof(xml_change_obj_t)); change_obj->change = change; change_obj->match = match; change_objs = g_list_append(change_objs, change_obj); - if (strcmp(op, "move") == 0) { - // Temporarily put the "move" object after the last sibling + if (strcmp(op, PCMK_VALUE_MOVE) == 0) { + // Temporarily put the PCMK_VALUE_MOVE object after the last sibling if ((match->parent != NULL) && (match->parent->last != NULL)) { xmlAddNextSibling(match->parent->last, match); } } - } else if (strcmp(op, "delete") == 0) { + } else if (strcmp(op, PCMK_VALUE_DELETE) == 0) { free_xml(match); - } else if (strcmp(op, "modify") == 0) { - xmlNode *attrs = NULL; + } else if (strcmp(op, PCMK_VALUE_MODIFY) == 0) { + const xmlNode *child = pcmk__xe_first_child(change, + PCMK_XE_CHANGE_RESULT, + NULL, NULL); + const xmlNode *attrs = pcmk__xml_first_child(child); - attrs = pcmk__xml_first_child(first_named_child(change, - XML_DIFF_RESULT)); if (attrs == NULL) { rc = ENOMSG; continue; @@ -1002,18 +1233,18 @@ apply_v2_patchset(xmlNode *xml, const xmlNode *patchset) change = change_obj->change; - op = crm_element_value(change, XML_DIFF_OP); - xpath = crm_element_value(change, XML_DIFF_PATH); + op = crm_element_value(change, PCMK_XA_OPERATION); + xpath = crm_element_value(change, PCMK_XA_PATH); crm_trace("Continue performing %s on %s with %p", op, xpath, match); - if (strcmp(op, "create") == 0) { + if (strcmp(op, PCMK_VALUE_CREATE) == 0) { int position = 0; xmlNode *child = NULL; xmlNode *match_child = NULL; match_child = match->children; - crm_element_value_int(change, XML_DIFF_POSITION, &position); + crm_element_value_int(change, PCMK_XE_POSITION, &position); while ((match_child != NULL) && (position != pcmk__xml_position(match_child, pcmk__xf_skip))) { @@ -1040,12 +1271,12 @@ apply_v2_patchset(xmlNode *xml, const xmlNode *patchset) CRM_LOG_ASSERT(position == 0); xmlAddChild(match, child); } - pcmk__mark_xml_created(child); + pcmk__xml_mark_created(child); - } else if (strcmp(op, "move") == 0) { + } else if (strcmp(op, PCMK_VALUE_MOVE) == 0) { int position = 0; - crm_element_value_int(change, XML_DIFF_POSITION, &position); + crm_element_value_int(change, PCMK_XE_POSITION, &position); if (position != pcmk__xml_position(match, pcmk__xf_skip)) { xmlNode *match_child = NULL; int p = position; @@ -1083,7 +1314,7 @@ apply_v2_patchset(xmlNode *xml, const xmlNode *patchset) if (position != pcmk__xml_position(match, pcmk__xf_skip)) { crm_err("Moved %s.%s to position %d instead of %d (%p)", - match->name, ID(match), + match->name, pcmk__xe_id(match), pcmk__xml_position(match, pcmk__xf_skip), position, match->prev); rc = pcmk_rc_diff_failed; @@ -1116,18 +1347,19 @@ xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version) } } - digest = crm_element_value(patchset, XML_ATTR_DIGEST); + digest = crm_element_value(patchset, PCMK__XA_DIGEST); if (digest != NULL) { /* Make original XML available for logging in case result doesn't have * expected digest */ - pcmk__if_tracing(old = copy_xml(xml), {}); + pcmk__if_tracing(old = pcmk__xml_copy(NULL, xml), {}); } if (rc == pcmk_ok) { crm_element_value_int(patchset, PCMK_XA_FORMAT, &format); switch (format) { case 1: + // @COMPAT Remove when v1 patchsets are removed rc = pcmk_rc2legacy(apply_v1_patchset(xml, patchset)); break; case 2: @@ -1141,7 +1373,7 @@ xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version) if ((rc == pcmk_ok) && (digest != NULL)) { char *new_digest = NULL; - char *version = crm_element_value_copy(xml, XML_ATTR_CRM_VERSION); + char *version = crm_element_value_copy(xml, PCMK_XA_CRM_FEATURE_SET); new_digest = calculate_xml_versioned_digest(xml, FALSE, TRUE, version); if (!pcmk__str_eq(new_digest, digest, pcmk__str_casei)) { @@ -1168,242 +1400,75 @@ xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version) return rc; } -void -purge_diff_markers(xmlNode *a_node) -{ - xmlNode *child = NULL; - - CRM_CHECK(a_node != NULL, return); - - xml_remove_prop(a_node, XML_DIFF_MARKER); - for (child = pcmk__xml_first_child(a_node); child != NULL; - child = pcmk__xml_next(child)) { - purge_diff_markers(child); - } -} - -xmlNode * -diff_xml_object(xmlNode *old, xmlNode *new, gboolean suppress) +// @COMPAT Remove when v1 patchsets are removed +static bool +can_prune_leaf_v1(xmlNode *node) { - xmlNode *tmp1 = NULL; - xmlNode *diff = create_xml_node(NULL, XML_TAG_DIFF); - xmlNode *removed = create_xml_node(diff, XML_TAG_DIFF_REMOVED); - xmlNode *added = create_xml_node(diff, XML_TAG_DIFF_ADDED); + xmlNode *cIter = NULL; + bool can_prune = true; - crm_xml_add(diff, XML_ATTR_CRM_VERSION, CRM_FEATURE_SET); + CRM_CHECK(node != NULL, return false); - tmp1 = subtract_xml_object(removed, old, new, FALSE, NULL, "removed:top"); - if (suppress && (tmp1 != NULL) && can_prune_leaf(tmp1)) { - free_xml(tmp1); + /* @COMPAT PCMK__XE_ROLE_REF was deprecated in Pacemaker 1.1.12 (needed for + * rolling upgrades) + */ + if (pcmk__strcase_any_of((const char *) node->name, + PCMK_XE_RESOURCE_REF, PCMK_XE_OBJ_REF, + PCMK_XE_ROLE, PCMK__XE_ROLE_REF, + NULL)) { + return false; } - tmp1 = subtract_xml_object(added, new, old, TRUE, NULL, "added:top"); - if (suppress && (tmp1 != NULL) && can_prune_leaf(tmp1)) { - free_xml(tmp1); - } + for (xmlAttrPtr a = pcmk__xe_first_attr(node); a != NULL; a = a->next) { + const char *p_name = (const char *) a->name; - if ((added->children == NULL) && (removed->children == NULL)) { - free_xml(diff); - diff = NULL; + if (strcmp(p_name, PCMK_XA_ID) == 0) { + continue; + } + can_prune = false; } - return diff; -} - -static xmlNode * -subtract_xml_comment(xmlNode *parent, xmlNode *left, xmlNode *right, - gboolean *changed) -{ - CRM_CHECK(left != NULL, return NULL); - CRM_CHECK(left->type == XML_COMMENT_NODE, return NULL); - - if ((right == NULL) || !pcmk__str_eq((const char *)left->content, - (const char *)right->content, - pcmk__str_casei)) { - xmlNode *deleted = NULL; - - deleted = add_node_copy(parent, left); - *changed = TRUE; + cIter = pcmk__xml_first_child(node); + while (cIter) { + xmlNode *child = cIter; - return deleted; + cIter = pcmk__xml_next(cIter); + if (can_prune_leaf_v1(child)) { + free_xml(child); + } else { + can_prune = false; + } } - - return NULL; + return can_prune; } +// @COMPAT Remove when v1 patchsets are removed xmlNode * -subtract_xml_object(xmlNode *parent, xmlNode *left, xmlNode *right, - gboolean full, gboolean *changed, const char *marker) +pcmk__diff_v1_xml_object(xmlNode *old, xmlNode *new, bool suppress) { - gboolean dummy = FALSE; - xmlNode *diff = NULL; - xmlNode *right_child = NULL; - xmlNode *left_child = NULL; - xmlAttrPtr xIter = NULL; - - const char *id = NULL; - const char *name = NULL; - const char *value = NULL; - const char *right_val = NULL; - - if (changed == NULL) { - changed = &dummy; - } - - if (left == NULL) { - return NULL; - } - - if (left->type == XML_COMMENT_NODE) { - return subtract_xml_comment(parent, left, right, changed); - } - - id = ID(left); - name = (const char *) left->name; - if (right == NULL) { - xmlNode *deleted = NULL; - - crm_trace("Processing <%s " XML_ATTR_ID "=%s> (complete copy)", - name, id); - deleted = add_node_copy(parent, left); - crm_xml_add(deleted, XML_DIFF_MARKER, marker); - - *changed = TRUE; - return deleted; - } - - CRM_CHECK(name != NULL, return NULL); - CRM_CHECK(pcmk__xe_is(left, (const char *) right->name), return NULL); - - // Check for XML_DIFF_MARKER in a child - value = crm_element_value(right, XML_DIFF_MARKER); - if ((value != NULL) && (strcmp(value, "removed:top") == 0)) { - crm_trace("We are the root of the deletion: %s.id=%s", name, id); - *changed = TRUE; - return NULL; - } - - // @TODO Avoiding creating the full hierarchy would save work here - diff = create_xml_node(parent, name); - - // Changes to child objects - for (left_child = pcmk__xml_first_child(left); left_child != NULL; - left_child = pcmk__xml_next(left_child)) { - gboolean child_changed = FALSE; - - right_child = pcmk__xml_match(right, left_child, false); - subtract_xml_object(diff, left_child, right_child, full, &child_changed, - marker); - if (child_changed) { - *changed = TRUE; - } - } - - if (!*changed) { - /* Nothing to do */ - - } else if (full) { - xmlAttrPtr pIter = NULL; - - for (pIter = pcmk__xe_first_attr(left); pIter != NULL; - pIter = pIter->next) { - const char *p_name = (const char *)pIter->name; - const char *p_value = pcmk__xml_attr_value(pIter); + xmlNode *tmp1 = NULL; + xmlNode *diff = pcmk__xe_create(NULL, PCMK_XE_DIFF); + xmlNode *removed = pcmk__xe_create(diff, PCMK__XE_DIFF_REMOVED); + xmlNode *added = pcmk__xe_create(diff, PCMK__XE_DIFF_ADDED); - xmlSetProp(diff, (pcmkXmlStr) p_name, (pcmkXmlStr) p_value); - } + crm_xml_add(diff, PCMK_XA_CRM_FEATURE_SET, CRM_FEATURE_SET); - // We have everything we need - goto done; + tmp1 = subtract_v1_xml_object(removed, old, new, false, NULL, + "removed:top"); + if (suppress && (tmp1 != NULL) && can_prune_leaf_v1(tmp1)) { + free_xml(tmp1); } - // Changes to name/value pairs - for (xIter = pcmk__xe_first_attr(left); xIter != NULL; - xIter = xIter->next) { - const char *prop_name = (const char *) xIter->name; - xmlAttrPtr right_attr = NULL; - xml_node_private_t *nodepriv = NULL; - - if (strcmp(prop_name, XML_ATTR_ID) == 0) { - // id already obtained when present ~ this case, so just reuse - xmlSetProp(diff, (pcmkXmlStr) XML_ATTR_ID, (pcmkXmlStr) id); - continue; - } - - if (pcmk__xa_filterable(prop_name)) { - continue; - } - - right_attr = xmlHasProp(right, (pcmkXmlStr) prop_name); - if (right_attr) { - nodepriv = right_attr->_private; - } - - right_val = crm_element_value(right, prop_name); - if ((right_val == NULL) || (nodepriv && pcmk_is_set(nodepriv->flags, pcmk__xf_deleted))) { - /* new */ - *changed = TRUE; - if (full) { - xmlAttrPtr pIter = NULL; - - for (pIter = pcmk__xe_first_attr(left); pIter != NULL; - pIter = pIter->next) { - const char *p_name = (const char *) pIter->name; - const char *p_value = pcmk__xml_attr_value(pIter); - - xmlSetProp(diff, (pcmkXmlStr) p_name, (pcmkXmlStr) p_value); - } - break; - - } else { - const char *left_value = pcmk__xml_attr_value(xIter); - - xmlSetProp(diff, (pcmkXmlStr) prop_name, (pcmkXmlStr) value); - crm_xml_add(diff, prop_name, left_value); - } - - } else { - /* Only now do we need the left value */ - const char *left_value = pcmk__xml_attr_value(xIter); - - if (strcmp(left_value, right_val) == 0) { - /* unchanged */ - - } else { - *changed = TRUE; - if (full) { - xmlAttrPtr pIter = NULL; - - crm_trace("Changes detected to %s in " - "<%s " XML_ATTR_ID "=%s>", prop_name, name, id); - for (pIter = pcmk__xe_first_attr(left); pIter != NULL; - pIter = pIter->next) { - const char *p_name = (const char *) pIter->name; - const char *p_value = pcmk__xml_attr_value(pIter); - - xmlSetProp(diff, (pcmkXmlStr) p_name, - (pcmkXmlStr) p_value); - } - break; - - } else { - crm_trace("Changes detected to %s (%s -> %s) in " - "<%s " XML_ATTR_ID "=%s>", - prop_name, left_value, right_val, name, id); - crm_xml_add(diff, prop_name, left_value); - } - } - } + tmp1 = subtract_v1_xml_object(added, new, old, true, NULL, "added:top"); + if (suppress && (tmp1 != NULL) && can_prune_leaf_v1(tmp1)) { + free_xml(tmp1); } - if (!*changed) { + if ((added->children == NULL) && (removed->children == NULL)) { free_xml(diff); - return NULL; - - } else if (!full && (id != NULL)) { - crm_xml_add(diff, XML_ATTR_ID, id); + diff = NULL; } - done: + return diff; } @@ -1417,12 +1482,14 @@ apply_xml_diff(xmlNode *old_xml, xmlNode *diff, xmlNode **new_xml) { gboolean result = TRUE; int root_nodes_seen = 0; - const char *digest = crm_element_value(diff, XML_ATTR_DIGEST); - const char *version = crm_element_value(diff, XML_ATTR_CRM_VERSION); + const char *digest = crm_element_value(diff, PCMK__XA_DIGEST); + const char *version = crm_element_value(diff, PCMK_XA_CRM_FEATURE_SET); xmlNode *child_diff = NULL; - xmlNode *added = find_xml_node(diff, XML_TAG_DIFF_ADDED, FALSE); - xmlNode *removed = find_xml_node(diff, XML_TAG_DIFF_REMOVED, FALSE); + xmlNode *added = pcmk__xe_first_child(diff, PCMK__XE_DIFF_ADDED, NULL, + NULL); + xmlNode *removed = pcmk__xe_first_child(diff, PCMK__XE_DIFF_REMOVED, NULL, + NULL); CRM_CHECK(new_xml != NULL, return FALSE); @@ -1431,14 +1498,14 @@ apply_xml_diff(xmlNode *old_xml, xmlNode *diff, xmlNode **new_xml) child_diff = pcmk__xml_next(child_diff)) { CRM_CHECK(root_nodes_seen == 0, result = FALSE); if (root_nodes_seen == 0) { - *new_xml = subtract_xml_object(NULL, old_xml, child_diff, FALSE, - NULL, NULL); + *new_xml = subtract_v1_xml_object(NULL, old_xml, child_diff, false, + NULL, NULL); } root_nodes_seen++; } if (root_nodes_seen == 0) { - *new_xml = copy_xml(old_xml); + *new_xml = pcmk__xml_copy(NULL, old_xml); } else if (root_nodes_seen > 1) { crm_err("(-) Diffs cannot contain more than one change set... saw %d", @@ -1455,7 +1522,8 @@ apply_xml_diff(xmlNode *old_xml, xmlNode *diff, xmlNode **new_xml) child_diff = pcmk__xml_next(child_diff)) { CRM_CHECK(root_nodes_seen == 0, result = FALSE); if (root_nodes_seen == 0) { - pcmk__xml_update(NULL, *new_xml, child_diff, true); + pcmk__xml_update(NULL, *new_xml, child_diff, pcmk__xaf_none, + true); } root_nodes_seen++; } @@ -1469,7 +1537,7 @@ apply_xml_diff(xmlNode *old_xml, xmlNode *diff, xmlNode **new_xml) } else if (result && (digest != NULL)) { char *new_digest = NULL; - purge_diff_markers(*new_xml); // Purge now so diff is ok + purge_v1_diff_markers(*new_xml); // Purge now so diff is ok new_digest = calculate_xml_versioned_digest(*new_xml, FALSE, TRUE, version); if (!pcmk__str_eq(new_digest, digest, pcmk__str_casei)) { @@ -1493,11 +1561,36 @@ apply_xml_diff(xmlNode *old_xml, xmlNode *diff, xmlNode **new_xml) free(new_digest); } else if (result) { - purge_diff_markers(*new_xml); // Purge now so diff is ok + purge_v1_diff_markers(*new_xml); // Purge now so diff is ok } return result; } +void +purge_diff_markers(xmlNode *a_node) +{ + purge_v1_diff_markers(a_node); +} + +xmlNode * +diff_xml_object(xmlNode *old, xmlNode *new, gboolean suppress) +{ + return pcmk__diff_v1_xml_object(old, new, suppress); +} + +xmlNode * +subtract_xml_object(xmlNode *parent, xmlNode *left, xmlNode *right, + gboolean full, gboolean *changed, const char *marker) +{ + return subtract_v1_xml_object(parent, left, right, full, changed, marker); +} + +gboolean +can_prune_leaf(xmlNode *xml_node) +{ + return can_prune_leaf_v1(xml_node); +} + // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/common/patchset_display.c b/lib/common/patchset_display.c index 5cc0b52..1351c86 100644 --- a/lib/common/patchset_display.c +++ b/lib/common/patchset_display.c @@ -1,5 +1,5 @@ /* - * 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. * @@ -9,7 +9,7 @@ #include <crm_internal.h> -#include <crm/msg_xml.h> +#include <crm/common/xml.h> #include "crmcommon_private.h" @@ -22,9 +22,9 @@ * * All header lines contain three integers separated by dots, of the form * <tt>{0}.{1}.{2}</tt>: - * * \p {0}: \p XML_ATTR_GENERATION_ADMIN - * * \p {1}: \p XML_ATTR_GENERATION - * * \p {2}: \p XML_ATTR_NUMUPDATES + * * \p {0}: \c PCMK_XA_ADMIN_EPOCH + * * \p {1}: \c PCMK_XA_EPOCH + * * \p {2}: \c PCMK_XA_NUM_UPDATES * * Lines containing \p "---" describe removals and end with the patch format * number. Lines containing \p "+++" describe additions and end with the patch @@ -48,7 +48,7 @@ xml_show_patchset_header(pcmk__output_t *out, const xmlNode *patchset) if ((add[0] != del[0]) || (add[1] != del[1]) || (add[2] != del[2])) { const char *fmt = crm_element_value(patchset, PCMK_XA_FORMAT); - const char *digest = crm_element_value(patchset, XML_ATTR_DIGEST); + const char *digest = crm_element_value(patchset, PCMK__XA_DIGEST); out->info(out, "Diff: --- %d.%d.%d %s", del[0], del[1], del[2], fmt); rc = out->info(out, "Diff: +++ %d.%d.%d %s", @@ -81,7 +81,7 @@ xml_show_patchset_v1_recursive(pcmk__output_t *out, const char *prefix, const xmlNode *data, int depth, uint32_t options) { if ((data->children == NULL) - || (crm_element_value(data, XML_DIFF_MARKER) != NULL)) { + || (crm_element_value(data, PCMK__XA_CRM_DIFF_MARKER) != NULL)) { // Found a change; clear the pcmk__xml_fmt_diff_short option if set options &= ~pcmk__xml_fmt_diff_short; @@ -143,7 +143,7 @@ xml_show_patchset_v1(pcmk__output_t *out, const xmlNode *patchset, * However, v1 patchsets can only exist during rolling upgrades from * Pacemaker 1.1.11, so not worth worrying about. */ - removed = find_xml_node(patchset, XML_TAG_DIFF_REMOVED, FALSE); + removed = pcmk__xe_first_child(patchset, PCMK__XE_DIFF_REMOVED, NULL, NULL); for (child = pcmk__xml_first_child(removed); child != NULL; child = pcmk__xml_next(child)) { int temp_rc = xml_show_patchset_v1_recursive(out, "- ", child, 0, @@ -159,7 +159,7 @@ xml_show_patchset_v1(pcmk__output_t *out, const xmlNode *patchset, } is_first = true; - added = find_xml_node(patchset, XML_TAG_DIFF_ADDED, FALSE); + added = pcmk__xe_first_child(patchset, PCMK__XE_DIFF_ADDED, NULL, NULL); for (child = pcmk__xml_first_child(added); child != NULL; child = pcmk__xml_next(child)) { int temp_rc = xml_show_patchset_v1_recursive(out, "+ ", child, 0, @@ -197,16 +197,18 @@ xml_show_patchset_v2(pcmk__output_t *out, const xmlNode *patchset) int rc = xml_show_patchset_header(out, patchset); int temp_rc = pcmk_rc_no_output; - for (const xmlNode *change = pcmk__xml_first_child(patchset); - change != NULL; change = pcmk__xml_next(change)) { - const char *op = crm_element_value(change, XML_DIFF_OP); - const char *xpath = crm_element_value(change, XML_DIFF_PATH); + for (const xmlNode *change = pcmk__xe_first_child(patchset, NULL, NULL, + NULL); + change != NULL; change = pcmk__xe_next(change)) { + + const char *op = crm_element_value(change, PCMK_XA_OPERATION); + const char *xpath = crm_element_value(change, PCMK_XA_PATH); if (op == NULL) { continue; } - if (strcmp(op, "create") == 0) { + if (strcmp(op, PCMK_VALUE_CREATE) == 0) { char *prefix = crm_strdup_printf(PCMK__XML_PREFIX_CREATED " %s: ", xpath); @@ -226,30 +228,33 @@ xml_show_patchset_v2(pcmk__output_t *out, const xmlNode *patchset) rc = pcmk__output_select_rc(rc, temp_rc); free(prefix); - } else if (strcmp(op, "move") == 0) { - const char *position = crm_element_value(change, XML_DIFF_POSITION); + } else if (strcmp(op, PCMK_VALUE_MOVE) == 0) { + const char *position = crm_element_value(change, PCMK_XE_POSITION); temp_rc = out->info(out, PCMK__XML_PREFIX_MOVED " %s moved to offset %s", xpath, position); rc = pcmk__output_select_rc(rc, temp_rc); - } else if (strcmp(op, "modify") == 0) { - xmlNode *clist = first_named_child(change, XML_DIFF_LIST); + } else if (strcmp(op, PCMK_VALUE_MODIFY) == 0) { + xmlNode *clist = pcmk__xe_first_child(change, PCMK_XE_CHANGE_LIST, + NULL, NULL); GString *buffer_set = NULL; GString *buffer_unset = NULL; - for (const xmlNode *child = pcmk__xml_first_child(clist); - child != NULL; child = pcmk__xml_next(child)) { - const char *name = crm_element_value(child, "name"); + for (const xmlNode *child = pcmk__xe_first_child(clist, NULL, NULL, + NULL); + child != NULL; child = pcmk__xe_next(child)) { + + const char *name = crm_element_value(child, PCMK_XA_NAME); - op = crm_element_value(child, XML_DIFF_OP); + op = crm_element_value(child, PCMK_XA_OPERATION); if (op == NULL) { continue; } if (strcmp(op, "set") == 0) { - const char *value = crm_element_value(child, "value"); + const char *value = crm_element_value(child, PCMK_XA_VALUE); pcmk__add_separated_word(&buffer_set, 256, "@", ", "); pcmk__g_strcat(buffer_set, name, "=", value, NULL); @@ -273,10 +278,10 @@ xml_show_patchset_v2(pcmk__output_t *out, const xmlNode *patchset) g_string_free(buffer_unset, TRUE); } - } else if (strcmp(op, "delete") == 0) { + } else if (strcmp(op, PCMK_VALUE_DELETE) == 0) { int position = -1; - crm_element_value_int(change, XML_DIFF_POSITION, &position); + crm_element_value_int(change, PCMK_XE_POSITION, &position); if (position >= 0) { temp_rc = out->info(out, "-- %s (%d)", xpath, position); } else { @@ -301,7 +306,8 @@ xml_show_patchset_v2(pcmk__output_t *out, const xmlNode *patchset) * * \return Standard Pacemaker return code * - * \note \p args should contain only the XML patchset + * \note \p args should contain the following: + * -# XML patchset */ PCMK__OUTPUT_ARGS("xml-patchset", "const xmlNode *") static int @@ -340,7 +346,8 @@ xml_patchset_default(pcmk__output_t *out, va_list args) * * \return Standard Pacemaker return code * - * \note \p args should contain only the XML patchset + * \note \p args should contain the following: + * -# XML patchset */ PCMK__OUTPUT_ARGS("xml-patchset", "const xmlNode *") static int @@ -402,7 +409,8 @@ xml_patchset_log(pcmk__output_t *out, va_list args) * * \return Standard Pacemaker return code * - * \note \p args should contain only the XML patchset + * \note \p args should contain the following: + * -# XML patchset */ PCMK__OUTPUT_ARGS("xml-patchset", "const xmlNode *") static int @@ -411,10 +419,13 @@ xml_patchset_xml(pcmk__output_t *out, va_list args) const xmlNode *patchset = va_arg(args, const xmlNode *); if (patchset != NULL) { - char *buf = dump_xml_formatted_with_text(patchset); + GString *buf = g_string_sized_new(1024); + + pcmk__xml_string(patchset, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, buf, + 0); - out->output_xml(out, "xml-patchset", buf); - free(buf); + out->output_xml(out, PCMK_XE_XML_PATCHSET, buf->str); + g_string_free(buf, TRUE); return pcmk_rc_ok; } crm_trace("Empty patch"); diff --git a/lib/common/probes.c b/lib/common/probes.c new file mode 100644 index 0000000..39a3905 --- /dev/null +++ b/lib/common/probes.c @@ -0,0 +1,84 @@ +/* + * Copyright 2004-2024 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> // pcmk__str_eq(), etc. + +#include <stdio.h> // NULL +#include <stdbool.h> // bool, true, false +#include <glib.h> // guint +#include <libxml/tree.h> // xmlNode + +#include <crm/common/options.h> // PCMK_META_INTERVAL +#include <crm/common/xml.h> // PCMK_XA_OPERATION + +/*! + * \brief Check whether an action name and interval represent a probe + * + * \param[in] task Action name + * \param[in] interval_ms Action interval in milliseconds + * + * \return true if \p task is \c PCMK_ACTION_MONITOR and \p interval_ms is 0, + * otherwise false + */ +bool +pcmk_is_probe(const char *task, guint interval_ms) +{ + // @COMPAT This should be made inline at an API compatibility break + return (interval_ms == 0) + && pcmk__str_eq(task, PCMK_ACTION_MONITOR, pcmk__str_none); +} + +/*! + * \brief Check whether an action history entry represents a probe + * + * \param[in] xml XML of action history entry + * + * \return true if \p xml is for a probe action, otherwise false + */ +bool +pcmk_xe_is_probe(const xmlNode *xml) +{ + int interval_ms = 0; + + if (xml == NULL) { + return false; + } + + pcmk__scan_min_int(crm_element_value(xml, PCMK_META_INTERVAL), + &interval_ms, 0); + + return pcmk_is_probe(crm_element_value(xml, PCMK_XA_OPERATION), + interval_ms); +} + +/*! + * \brief Check whether an action history entry represents a maskable probe + * + * \param[in] xml XML of action history entry + * + * \return true if \p xml is for a failed probe action that should be treated as + * successful, otherwise false + */ +bool +pcmk_xe_mask_probe_failure(const xmlNode *xml) +{ + int exec_status = PCMK_EXEC_UNKNOWN; + int exit_status = PCMK_OCF_OK; + + if (!pcmk_xe_is_probe(xml)) { + return false; + } + + crm_element_value_int(xml, PCMK__XA_OP_STATUS, &exec_status); + crm_element_value_int(xml, PCMK__XA_RC_CODE, &exit_status); + + return (exit_status == PCMK_OCF_NOT_INSTALLED) + || (exit_status == PCMK_OCF_INVALID_PARAM) + || (exec_status == PCMK_EXEC_NOT_INSTALLED); +} diff --git a/lib/common/remote.c b/lib/common/remote.c index fe19296..9f8419b 100644 --- a/lib/common/remote.c +++ b/lib/common/remote.c @@ -1,5 +1,5 @@ /* - * Copyright 2008-2023 the Pacemaker project contributors + * Copyright 2008-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -493,24 +493,25 @@ pcmk__remote_send_xml(pcmk__remote_t *remote, const xmlNode *msg) { int rc = pcmk_rc_ok; static uint64_t id = 0; - char *xml_text = NULL; + GString *xml_text = NULL; struct iovec iov[2]; struct remote_header_v0 *header; CRM_CHECK((remote != NULL) && (msg != NULL), return EINVAL); - xml_text = dump_xml_unformatted(msg); - CRM_CHECK(xml_text != NULL, return EINVAL); + xml_text = g_string_sized_new(1024); + pcmk__xml_string(msg, 0, xml_text, 0); + CRM_CHECK(xml_text->len > 0, + g_string_free(xml_text, TRUE); return EINVAL); - header = calloc(1, sizeof(struct remote_header_v0)); - CRM_ASSERT(header != NULL); + header = pcmk__assert_alloc(1, sizeof(struct remote_header_v0)); iov[0].iov_base = header; iov[0].iov_len = sizeof(struct remote_header_v0); - iov[1].iov_base = xml_text; - iov[1].iov_len = 1 + strlen(xml_text); + iov[1].iov_len = 1 + xml_text->len; + iov[1].iov_base = g_string_free(xml_text, FALSE); id++; header->id = id; @@ -527,7 +528,7 @@ pcmk__remote_send_xml(pcmk__remote_t *remote, const xmlNode *msg) } free(iov[0].iov_base); - free(iov[1].iov_base); + g_free((gchar *) iov[1].iov_base); return rc; } @@ -554,7 +555,8 @@ pcmk__remote_message_xml(pcmk__remote_t *remote) if (header->payload_compressed) { int rc = 0; unsigned int size_u = 1 + header->payload_uncompressed; - char *uncompressed = calloc(1, header->payload_offset + size_u); + char *uncompressed = + pcmk__assert_alloc(1, header->payload_offset + size_u); crm_trace("Decompressing message data %d bytes into %d bytes", header->payload_compressed, size_u); @@ -592,7 +594,7 @@ pcmk__remote_message_xml(pcmk__remote_t *remote) CRM_LOG_ASSERT(remote->buffer[sizeof(struct remote_header_v0) + header->payload_uncompressed - 1] == 0); - xml = string2xml(remote->buffer + header->payload_offset); + xml = pcmk__xml_parse(remote->buffer + header->payload_offset); if (xml == NULL && header->version > REMOTE_MSG_VERSION) { crm_warn("Couldn't parse v%d message, we only understand v%d", header->version, REMOTE_MSG_VERSION); @@ -978,7 +980,7 @@ connect_socket_retry(int sock, const struct sockaddr *addr, socklen_t addrlen, return rc; } - cb_data = calloc(1, sizeof(struct tcp_async_cb_data)); + cb_data = pcmk__assert_alloc(1, sizeof(struct tcp_async_cb_data)); cb_data->userdata = userdata; cb_data->callback = callback; cb_data->sock = sock; @@ -1206,6 +1208,9 @@ pcmk__accept_remote_connection(int ssock, int *csock) struct sockaddr_storage addr; socklen_t laddr = sizeof(addr); char addr_str[INET6_ADDRSTRLEN]; +#ifdef TCP_USER_TIMEOUT + long sbd_timeout = 0; +#endif /* accept the connection */ memset(&addr, 0, sizeof(addr)); @@ -1229,9 +1234,11 @@ pcmk__accept_remote_connection(int ssock, int *csock) } #ifdef TCP_USER_TIMEOUT - if (pcmk__get_sbd_timeout() > 0) { + sbd_timeout = pcmk__get_sbd_watchdog_timeout(); + if (sbd_timeout > 0) { // Time to fail and retry before watchdog - unsigned int optval = (unsigned int) pcmk__get_sbd_timeout() / 2; + long half = sbd_timeout / 2; + unsigned int optval = (half <= UINT_MAX)? half : UINT_MAX; rc = setsockopt(*csock, SOL_TCP, TCP_USER_TIMEOUT, &optval, sizeof(optval)); diff --git a/lib/common/resources.c b/lib/common/resources.c new file mode 100644 index 0000000..46c038b --- /dev/null +++ b/lib/common/resources.c @@ -0,0 +1,67 @@ +/* + * 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 Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <stdio.h> // NULL +#include <stdbool.h> // bool, false + +#include <crm/common/scheduler.h> +#include <crm/common/scheduler_internal.h> + +/*! + * \internal + * \brief Get a resource's ID + * + * \param[in] rsc Resource to check + * + * \return ID of \p rsc (or NULL if \p rsc is NULL) + */ +const char * +pcmk_resource_id(const pcmk_resource_t *rsc) +{ + return (rsc == NULL)? NULL : rsc->id; +} + +/*! + * \internal + * \brief Check whether a resource is managed by the cluster + * + * \param[in] rsc Resource to check + * + * \return true if \p rsc is managed, otherwise false + */ +bool +pcmk_resource_is_managed(const pcmk_resource_t *rsc) +{ + return (rsc == NULL)? false : pcmk_is_set(rsc->flags, pcmk_rsc_managed); +} + +/*! + * \brief Get readable description of a multiply-active recovery type + * + * \param[in] recovery Recovery type + * + * \return Static string describing \p recovery + */ +const char * +pcmk__multiply_active_text(enum rsc_recovery_type recovery) +{ + switch (recovery) { + case pcmk_multiply_active_stop: + return "shutting it down"; + case pcmk_multiply_active_restart: + return "attempting recovery"; + case pcmk_multiply_active_block: + return "waiting for an administrator"; + case pcmk_multiply_active_unexpected: + return "stopping unexpected instances"; + } + return "Unknown"; +} diff --git a/lib/common/results.c b/lib/common/results.c index dde8b27..396ac0d 100644 --- a/lib/common/results.c +++ b/lib/common/results.c @@ -1,5 +1,5 @@ /* - * 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. * @@ -1091,9 +1091,9 @@ pcmk__copy_result(const pcmk__action_result_t *src, pcmk__action_result_t *dst) CRM_CHECK((src != NULL) && (dst != NULL), return); dst->exit_status = src->exit_status; dst->execution_status = src->execution_status; - pcmk__str_update(&dst->exit_reason, src->exit_reason); - pcmk__str_update(&dst->action_stdout, src->action_stdout); - pcmk__str_update(&dst->action_stderr, src->action_stderr); + dst->exit_reason = pcmk__str_copy(src->exit_reason); + dst->action_stdout = pcmk__str_copy(src->action_stdout); + dst->action_stderr = pcmk__str_copy(src->action_stderr); } // Deprecated functions kept only for backward API compatibility diff --git a/lib/common/roles.c b/lib/common/roles.c new file mode 100644 index 0000000..96aa72a --- /dev/null +++ b/lib/common/roles.c @@ -0,0 +1,88 @@ +/* + * Copyright 2004-2024 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <crm/common/scheduler.h> +#include <crm/common/scheduler_internal.h> + +/*! + * \brief Get readable description of a resource role + * + * \param[in] role Resource role + * + * \return Static string describing \p role, suitable for logging or display + */ +const char * +pcmk_role_text(enum rsc_role_e role) +{ + switch (role) { + case pcmk_role_stopped: + return PCMK_ROLE_STOPPED; + + case pcmk_role_started: + return PCMK_ROLE_STARTED; + + case pcmk_role_unpromoted: +#ifdef PCMK__COMPAT_2_0 + return PCMK__ROLE_UNPROMOTED_LEGACY; +#else + return PCMK_ROLE_UNPROMOTED; +#endif + + case pcmk_role_promoted: +#ifdef PCMK__COMPAT_2_0 + return PCMK__ROLE_PROMOTED_LEGACY; +#else + return PCMK_ROLE_PROMOTED; +#endif + + default: // pcmk_role_unknown + return PCMK__ROLE_UNKNOWN; + } +} + +/*! + * \brief Parse a resource role from a string role specification + * + * \param[in] role Role specification + * + * \return Resource role corresponding to \p role + */ +enum rsc_role_e +pcmk_parse_role(const char *role) +{ + if (pcmk__str_eq(role, PCMK__ROLE_UNKNOWN, + pcmk__str_casei|pcmk__str_null_matches)) { + return pcmk_role_unknown; + } else if (pcmk__str_eq(role, PCMK_ROLE_STOPPED, pcmk__str_casei)) { + return pcmk_role_stopped; + } else if (pcmk__str_eq(role, PCMK_ROLE_STARTED, pcmk__str_casei)) { + return pcmk_role_started; + } else if (pcmk__str_eq(role, PCMK__ROLE_UNPROMOTED_LEGACY, pcmk__str_casei)) { + pcmk__warn_once(pcmk__wo_slave_role, + "Support for the " PCMK__ROLE_UNPROMOTED_LEGACY + " role is deprecated and will be removed in a " + "future release. Use " PCMK_ROLE_UNPROMOTED + " instead."); + return pcmk_role_unpromoted; + } else if (pcmk__str_eq(role, PCMK_ROLE_UNPROMOTED, pcmk__str_casei)) { + return pcmk_role_unpromoted; + } else if (pcmk__str_eq(role, PCMK__ROLE_PROMOTED_LEGACY, pcmk__str_casei)) { + pcmk__warn_once(pcmk__wo_master_role, + "Support for the " PCMK__ROLE_PROMOTED_LEGACY + " role is deprecated and will be removed in a " + "future release. Use " PCMK_ROLE_PROMOTED + " instead."); + return pcmk_role_promoted; + } else if (pcmk__str_eq(role, PCMK_ROLE_PROMOTED, pcmk__str_casei)) { + return pcmk_role_promoted; + } + return pcmk_role_unknown; // Invalid role given +} diff --git a/lib/common/rules.c b/lib/common/rules.c new file mode 100644 index 0000000..32af835 --- /dev/null +++ b/lib/common/rules.c @@ -0,0 +1,1512 @@ +/* + * Copyright 2004-2024 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <stdio.h> // NULL, size_t +#include <stdbool.h> // bool +#include <ctype.h> // isdigit() +#include <regex.h> // regmatch_t +#include <stdint.h> // uint32_t +#include <inttypes.h> // PRIu32 +#include <glib.h> // gboolean, FALSE +#include <libxml/tree.h> // xmlNode + +#include <crm/common/scheduler.h> + +#include <crm/common/iso8601_internal.h> +#include <crm/common/nvpair_internal.h> +#include <crm/common/scheduler_internal.h> +#include "crmcommon_private.h" + +/*! + * \internal + * \brief Get the condition type corresponding to given condition XML + * + * \param[in] condition Rule condition XML + * + * \return Condition type corresponding to \p condition + */ +enum expression_type +pcmk__condition_type(const xmlNode *condition) +{ + const char *name = NULL; + + // Expression types based on element name + + if (pcmk__xe_is(condition, PCMK_XE_DATE_EXPRESSION)) { + return pcmk__condition_datetime; + + } else if (pcmk__xe_is(condition, PCMK_XE_RSC_EXPRESSION)) { + return pcmk__condition_resource; + + } else if (pcmk__xe_is(condition, PCMK_XE_OP_EXPRESSION)) { + return pcmk__condition_operation; + + } else if (pcmk__xe_is(condition, PCMK_XE_RULE)) { + return pcmk__condition_rule; + + } else if (!pcmk__xe_is(condition, PCMK_XE_EXPRESSION)) { + return pcmk__condition_unknown; + } + + // Expression types based on node attribute name + + name = crm_element_value(condition, PCMK_XA_ATTRIBUTE); + + if (pcmk__str_any_of(name, CRM_ATTR_UNAME, CRM_ATTR_KIND, CRM_ATTR_ID, + NULL)) { + return pcmk__condition_location; + } + + return pcmk__condition_attribute; +} + +/*! + * \internal + * \brief Get parent XML element's ID for logging purposes + * + * \param[in] xml XML of a subelement + * + * \return ID of \p xml's parent for logging purposes (guaranteed non-NULL) + */ +static const char * +loggable_parent_id(const xmlNode *xml) +{ + // Default if called without parent (likely for unit testing) + const char *parent_id = "implied"; + + if ((xml != NULL) && (xml->parent != NULL)) { + parent_id = pcmk__xe_id(xml->parent); + if (parent_id == NULL) { // Not possible with schema validation enabled + parent_id = "without ID"; + } + } + return parent_id; +} + +/*! + * \internal + * \brief Get the moon phase corresponding to a given date/time + * + * \param[in] now Date/time to get moon phase for + * + * \return Phase of the moon corresponding to \p now, where 0 is the new moon + * and 7 is the full moon + * \deprecated This feature has been deprecated since 2.1.6. + */ +static int +phase_of_the_moon(const crm_time_t *now) +{ + /* As per the nethack rules: + * - A moon period is 29.53058 days ~= 30 + * - A year is 365.2422 days + * - Number of days moon phase advances on first day of year compared to + * preceding year is (365.2422 - 12 * 29.53058) ~= 11 + * - Number of years until same phases fall on the same days of the month + * is 18.6 ~= 19 + * - Moon phase on first day of year (epact) ~= (11 * (year%19) + 29) % 30 + * (29 as initial condition) + * - Current phase in days = first day phase + days elapsed in year + * - 6 moons ~= 177 days ~= 8 reported phases * 22 (+ 11/22 for rounding) + */ + uint32_t epact, diy, goldn; + uint32_t y; + + crm_time_get_ordinal(now, &y, &diy); + goldn = (y % 19) + 1; + epact = (11 * goldn + 18) % 30; + if (((epact == 25) && (goldn > 11)) || (epact == 24)) { + epact++; + } + return (((((diy + epact) * 6) + 11) % 177) / 22) & 7; +} + +/*! + * \internal + * \brief Check an integer value against a range from a date specification + * + * \param[in] date_spec XML of PCMK_XE_DATE_SPEC element to check + * \param[in] id XML ID for logging purposes + * \param[in] attr Name of XML attribute with range to check against + * \param[in] value Value to compare against range + * + * \return Standard Pacemaker return code (specifically, pcmk_rc_before_range, + * pcmk_rc_after_range, or pcmk_rc_ok to indicate that result is either + * within range or undetermined) + * \note We return pcmk_rc_ok for an undetermined result so we can continue + * checking the next range attribute. + */ +static int +check_range(const xmlNode *date_spec, const char *id, const char *attr, + uint32_t value) +{ + int rc = pcmk_rc_ok; + const char *range = crm_element_value(date_spec, attr); + long long low, high; + + if (range == NULL) { // Attribute not present + goto bail; + } + + if (pcmk__parse_ll_range(range, &low, &high) != pcmk_rc_ok) { + // Invalid range + /* @COMPAT When we can break behavioral backward compatibility, treat + * the entire rule as not passing. + */ + pcmk__config_err("Ignoring " PCMK_XE_DATE_SPEC + " %s attribute %s because '%s' is not a valid range", + id, attr, range); + + } else if ((low != -1) && (value < low)) { + rc = pcmk_rc_before_range; + + } else if ((high != -1) && (value > high)) { + rc = pcmk_rc_after_range; + } + +bail: + crm_trace("Checked " PCMK_XE_DATE_SPEC " %s %s='%s' for %" PRIu32 ": %s", + id, attr, pcmk__s(range, ""), value, pcmk_rc_str(rc)); + return rc; +} + +/*! + * \internal + * \brief Evaluate a date specification for a given date/time + * + * \param[in] date_spec XML of PCMK_XE_DATE_SPEC element to evaluate + * \param[in] now Time to check + * + * \return Standard Pacemaker return code (specifically, EINVAL for NULL + * arguments, pcmk_rc_ok if time matches specification, or + * pcmk_rc_before_range, pcmk_rc_after_range, or pcmk_rc_op_unsatisfied + * as appropriate to how time relates to specification) + */ +int +pcmk__evaluate_date_spec(const xmlNode *date_spec, const crm_time_t *now) +{ + const char *id = NULL; + const char *parent_id = loggable_parent_id(date_spec); + + // Range attributes that can be specified for a PCMK_XE_DATE_SPEC element + struct range { + const char *attr; + uint32_t value; + } ranges[] = { + { PCMK_XA_YEARS, 0U }, + { PCMK_XA_MONTHS, 0U }, + { PCMK_XA_MONTHDAYS, 0U }, + { PCMK_XA_HOURS, 0U }, + { PCMK_XA_MINUTES, 0U }, + { PCMK_XA_SECONDS, 0U }, + { PCMK_XA_YEARDAYS, 0U }, + { PCMK_XA_WEEKYEARS, 0U }, + { PCMK_XA_WEEKS, 0U }, + { PCMK_XA_WEEKDAYS, 0U }, + { PCMK__XA_MOON, 0U }, + }; + + if ((date_spec == NULL) || (now == NULL)) { + return EINVAL; + } + + // Get specification ID (for logging) + id = pcmk__xe_id(date_spec); + if (pcmk__str_empty(id)) { // Not possible with schema validation enabled + /* @COMPAT When we can break behavioral backward compatibility, + * fail the specification + */ + pcmk__config_warn(PCMK_XE_DATE_SPEC " subelement of " + PCMK_XE_DATE_EXPRESSION " %s has no " PCMK_XA_ID, + parent_id); + id = "without ID"; // for logging + } + + // Year, month, day + crm_time_get_gregorian(now, &(ranges[0].value), &(ranges[1].value), + &(ranges[2].value)); + + // Hour, minute, second + crm_time_get_timeofday(now, &(ranges[3].value), &(ranges[4].value), + &(ranges[5].value)); + + // Year (redundant) and day of year + crm_time_get_ordinal(now, &(ranges[0].value), &(ranges[6].value)); + + // Week year, week of week year, day of week + crm_time_get_isoweek(now, &(ranges[7].value), &(ranges[8].value), + &(ranges[9].value)); + + // Moon phase (deprecated) + ranges[10].value = phase_of_the_moon(now); + if (crm_element_value(date_spec, PCMK__XA_MOON) != NULL) { + pcmk__config_warn("Support for '" PCMK__XA_MOON "' in " + PCMK_XE_DATE_SPEC " elements (such as %s) is " + "deprecated and will be removed in a future release " + "of Pacemaker", id); + } + + for (int i = 0; i < PCMK__NELEM(ranges); ++i) { + int rc = check_range(date_spec, id, ranges[i].attr, ranges[i].value); + + if (rc != pcmk_rc_ok) { + return rc; + } + } + + // All specified ranges passed, or none were given (also considered a pass) + return pcmk_rc_ok; +} + +#define ADD_COMPONENT(component) do { \ + int sub_rc = pcmk__add_time_from_xml(*end, component, duration); \ + if (sub_rc != pcmk_rc_ok) { \ + /* @COMPAT return sub_rc when we can break compatibility */ \ + pcmk__config_warn("Ignoring %s in " PCMK_XE_DURATION " %s " \ + "because it is invalid", \ + pcmk__time_component_attr(component), id); \ + rc = sub_rc; \ + } \ + } while (0) + +/*! + * \internal + * \brief Given a duration and a start time, calculate the end time + * + * \param[in] duration XML of PCMK_XE_DURATION element + * \param[in] start Start time + * \param[out] end Where to store end time (\p *end must be NULL + * initially) + * + * \return Standard Pacemaker return code + * \note The caller is responsible for freeing \p *end using crm_time_free(). + */ +int +pcmk__unpack_duration(const xmlNode *duration, const crm_time_t *start, + crm_time_t **end) +{ + int rc = pcmk_rc_ok; + const char *id = NULL; + const char *parent_id = loggable_parent_id(duration); + + if ((start == NULL) || (duration == NULL) + || (end == NULL) || (*end != NULL)) { + return EINVAL; + } + + // Get duration ID (for logging) + id = pcmk__xe_id(duration); + if (pcmk__str_empty(id)) { // Not possible with schema validation enabled + /* @COMPAT When we can break behavioral backward compatibility, + * return pcmk_rc_unpack_error instead + */ + pcmk__config_warn(PCMK_XE_DURATION " subelement of " + PCMK_XE_DATE_EXPRESSION " %s has no " PCMK_XA_ID, + parent_id); + id = "without ID"; + } + + *end = pcmk_copy_time(start); + + ADD_COMPONENT(pcmk__time_years); + ADD_COMPONENT(pcmk__time_months); + ADD_COMPONENT(pcmk__time_weeks); + ADD_COMPONENT(pcmk__time_days); + ADD_COMPONENT(pcmk__time_hours); + ADD_COMPONENT(pcmk__time_minutes); + ADD_COMPONENT(pcmk__time_seconds); + + return rc; +} + +/*! + * \internal + * \brief Evaluate a range check for a given date/time + * + * \param[in] date_expression XML of PCMK_XE_DATE_EXPRESSION element + * \param[in] id Expression ID for logging purposes + * \param[in] now Date/time to compare + * \param[in,out] next_change If not NULL, set this to when the evaluation + * will change, if known and earlier than the + * original value + * + * \return Standard Pacemaker return code + */ +static int +evaluate_in_range(const xmlNode *date_expression, const char *id, + const crm_time_t *now, crm_time_t *next_change) +{ + crm_time_t *start = NULL; + crm_time_t *end = NULL; + + if (pcmk__xe_get_datetime(date_expression, PCMK_XA_START, + &start) != pcmk_rc_ok) { + /* @COMPAT When we can break behavioral backward compatibility, + * return pcmk_rc_unpack_error + */ + pcmk__config_warn("Ignoring " PCMK_XA_START " in " + PCMK_XE_DATE_EXPRESSION " %s because it is invalid", + id); + } + + if (pcmk__xe_get_datetime(date_expression, PCMK_XA_END, + &end) != pcmk_rc_ok) { + /* @COMPAT When we can break behavioral backward compatibility, + * return pcmk_rc_unpack_error + */ + pcmk__config_warn("Ignoring " PCMK_XA_END " in " + PCMK_XE_DATE_EXPRESSION " %s because it is invalid", + id); + } + + if ((start == NULL) && (end == NULL)) { + // Not possible with schema validation enabled + /* @COMPAT When we can break behavioral backward compatibility, + * return pcmk_rc_unpack_error + */ + pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " + "passing because in_range requires at least one of " + PCMK_XA_START " or " PCMK_XA_END, id); + return pcmk_rc_undetermined; + } + + if (end == NULL) { + xmlNode *duration = pcmk__xe_first_child(date_expression, + PCMK_XE_DURATION, NULL, NULL); + + if (duration != NULL) { + /* @COMPAT When we can break behavioral backward compatibility, + * return the result of this if not OK + */ + pcmk__unpack_duration(duration, start, &end); + } + } + + if ((start != NULL) && (crm_time_compare(now, start) < 0)) { + pcmk__set_time_if_earlier(next_change, start); + crm_time_free(start); + crm_time_free(end); + return pcmk_rc_before_range; + } + + if (end != NULL) { + if (crm_time_compare(now, end) > 0) { + crm_time_free(start); + crm_time_free(end); + return pcmk_rc_after_range; + } + + // Evaluation doesn't change until second after end + if (next_change != NULL) { + crm_time_add_seconds(end, 1); + pcmk__set_time_if_earlier(next_change, end); + } + } + + crm_time_free(start); + crm_time_free(end); + return pcmk_rc_within_range; +} + +/*! + * \internal + * \brief Evaluate a greater-than check for a given date/time + * + * \param[in] date_expression XML of PCMK_XE_DATE_EXPRESSION element + * \param[in] id Expression ID for logging purposes + * \param[in] now Date/time to compare + * \param[in,out] next_change If not NULL, set this to when the evaluation + * will change, if known and earlier than the + * original value + * + * \return Standard Pacemaker return code + */ +static int +evaluate_gt(const xmlNode *date_expression, const char *id, + const crm_time_t *now, crm_time_t *next_change) +{ + crm_time_t *start = NULL; + + if (pcmk__xe_get_datetime(date_expression, PCMK_XA_START, + &start) != pcmk_rc_ok) { + /* @COMPAT When we can break behavioral backward compatibility, + * return pcmk_rc_unpack_error + */ + pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " + "passing because " PCMK_XA_START " is invalid", + id); + return pcmk_rc_undetermined; + } + + if (start == NULL) { // Not possible with schema validation enabled + /* @COMPAT When we can break behavioral backward compatibility, + * return pcmk_rc_unpack_error + */ + pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " + "passing because " PCMK_VALUE_GT " requires " + PCMK_XA_START, id); + return pcmk_rc_undetermined; + } + + if (crm_time_compare(now, start) > 0) { + crm_time_free(start); + return pcmk_rc_within_range; + } + + // Evaluation doesn't change until second after start time + crm_time_add_seconds(start, 1); + pcmk__set_time_if_earlier(next_change, start); + crm_time_free(start); + return pcmk_rc_before_range; +} + +/*! + * \internal + * \brief Evaluate a less-than check for a given date/time + * + * \param[in] date_expression XML of PCMK_XE_DATE_EXPRESSION element + * \param[in] id Expression ID for logging purposes + * \param[in] now Date/time to compare + * \param[in,out] next_change If not NULL, set this to when the evaluation + * will change, if known and earlier than the + * original value + * + * \return Standard Pacemaker return code + */ +static int +evaluate_lt(const xmlNode *date_expression, const char *id, + const crm_time_t *now, crm_time_t *next_change) +{ + crm_time_t *end = NULL; + + if (pcmk__xe_get_datetime(date_expression, PCMK_XA_END, + &end) != pcmk_rc_ok) { + /* @COMPAT When we can break behavioral backward compatibility, + * return pcmk_rc_unpack_error + */ + pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " + "passing because " PCMK_XA_END " is invalid", id); + return pcmk_rc_undetermined; + } + + if (end == NULL) { // Not possible with schema validation enabled + /* @COMPAT When we can break behavioral backward compatibility, + * return pcmk_rc_unpack_error + */ + pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " + "passing because " PCMK_VALUE_GT " requires " + PCMK_XA_END, id); + return pcmk_rc_undetermined; + } + + if (crm_time_compare(now, end) < 0) { + pcmk__set_time_if_earlier(next_change, end); + crm_time_free(end); + return pcmk_rc_within_range; + } + + crm_time_free(end); + return pcmk_rc_after_range; +} + +/*! + * \internal + * \brief Evaluate a rule's date expression for a given date/time + * + * \param[in] date_expression XML of a PCMK_XE_DATE_EXPRESSION element + * \param[in] now Time to use for evaluation + * \param[in,out] next_change If not NULL, set this to when the evaluation + * will change, if known and earlier than the + * original value + * + * \return Standard Pacemaker return code (unlike most other evaluation + * functions, this can return either pcmk_rc_ok or pcmk_rc_within_range + * on success) + */ +int +pcmk__evaluate_date_expression(const xmlNode *date_expression, + const crm_time_t *now, crm_time_t *next_change) +{ + const char *id = NULL; + const char *op = NULL; + int rc = pcmk_rc_undetermined; + + if ((date_expression == NULL) || (now == NULL)) { + return EINVAL; + } + + // Get expression ID (for logging) + id = pcmk__xe_id(date_expression); + if (pcmk__str_empty(id)) { // Not possible with schema validation enabled + /* @COMPAT When we can break behavioral backward compatibility, + * return pcmk_rc_unpack_error + */ + pcmk__config_warn(PCMK_XE_DATE_EXPRESSION " element has no " + PCMK_XA_ID); + id = "without ID"; // for logging + } + + op = crm_element_value(date_expression, PCMK_XA_OPERATION); + if (pcmk__str_eq(op, PCMK_VALUE_IN_RANGE, + pcmk__str_null_matches|pcmk__str_casei)) { + rc = evaluate_in_range(date_expression, id, now, next_change); + + } else if (pcmk__str_eq(op, PCMK_VALUE_DATE_SPEC, pcmk__str_casei)) { + xmlNode *date_spec = pcmk__xe_first_child(date_expression, + PCMK_XE_DATE_SPEC, NULL, + NULL); + + if (date_spec == NULL) { // Not possible with schema validation enabled + /* @COMPAT When we can break behavioral backward compatibility, + * return pcmk_rc_unpack_error + */ + pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s " + "as not passing because " PCMK_VALUE_DATE_SPEC + " operations require a " PCMK_XE_DATE_SPEC + " subelement", id); + } else { + // @TODO set next_change appropriately + rc = pcmk__evaluate_date_spec(date_spec, now); + } + + } else if (pcmk__str_eq(op, PCMK_VALUE_GT, pcmk__str_casei)) { + rc = evaluate_gt(date_expression, id, now, next_change); + + } else if (pcmk__str_eq(op, PCMK_VALUE_LT, pcmk__str_casei)) { + rc = evaluate_lt(date_expression, id, now, next_change); + + } else { // Not possible with schema validation enabled + /* @COMPAT When we can break behavioral backward compatibility, + * return pcmk_rc_unpack_error + */ + pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION + " %s as not passing because '%s' is not a valid " + PCMK_XE_OPERATION, op); + } + + crm_trace(PCMK_XE_DATE_EXPRESSION " %s (%s): %s (%d)", + id, op, pcmk_rc_str(rc), rc); + return rc; +} + +/*! + * \internal + * \brief Go through submatches in a string, either counting how many bytes + * would be needed for the expansion, or performing the expansion, + * as requested + * + * \param[in] string String possibly containing submatch variables + * \param[in] match String that matched the regular expression + * \param[in] submatches Regular expression submatches (as set by regexec()) + * \param[in] nmatches Number of entries in \p submatches[] + * \param[out] expansion If not NULL, expand string here (must be + * pre-allocated to appropriate size) + * \param[out] nbytes If not NULL, set to size needed for expansion + * + * \return true if any expansion is needed, otherwise false + */ +static bool +process_submatches(const char *string, const char *match, + const regmatch_t submatches[], int nmatches, + char *expansion, size_t *nbytes) +{ + bool expanded = false; + const char *src = string; + + if (nbytes != NULL) { + *nbytes = 1; // Include space for terminator + } + + while (*src != '\0') { + int submatch = 0; + size_t match_len = 0; + + if ((src[0] != '%') || !isdigit(src[1])) { + /* src does not point to the first character of a %N sequence, + * so expand this character as-is + */ + if (expansion != NULL) { + *expansion++ = *src; + } + if (nbytes != NULL) { + ++(*nbytes); + } + ++src; + continue; + } + + submatch = src[1] - '0'; + src += 2; // Skip over %N sequence in source string + expanded = true; // Expansion will be different from source + + // Omit sequence from expansion unless it has a non-empty match + if ((nmatches <= submatch) // Not enough submatches + || (submatches[submatch].rm_so < 0) // Pattern did not match + || (submatches[submatch].rm_eo + <= submatches[submatch].rm_so)) { // Match was empty + continue; + } + + match_len = submatches[submatch].rm_eo - submatches[submatch].rm_so; + if (nbytes != NULL) { + *nbytes += match_len; + } + if (expansion != NULL) { + memcpy(expansion, match + submatches[submatch].rm_so, + match_len); + expansion += match_len; + } + } + + return expanded; +} + +/*! + * \internal + * \brief Expand any regular expression submatches (%0-%9) in a string + * + * \param[in] string String possibly containing submatch variables + * \param[in] match String that matched the regular expression + * \param[in] submatches Regular expression submatches (as set by regexec()) + * \param[in] nmatches Number of entries in \p submatches[] + * + * \return Newly allocated string identical to \p string with submatches + * expanded on success, or NULL if no expansions were needed + * \note The caller is responsible for freeing the result with free() + */ +char * +pcmk__replace_submatches(const char *string, const char *match, + const regmatch_t submatches[], int nmatches) +{ + size_t nbytes = 0; + char *result = NULL; + + if (pcmk__str_empty(string) || pcmk__str_empty(match)) { + return NULL; // Nothing to expand + } + + // Calculate how much space will be needed for expanded string + if (!process_submatches(string, match, submatches, nmatches, NULL, + &nbytes)) { + return NULL; // No expansions needed + } + + // Allocate enough space for expanded string + result = pcmk__assert_alloc(nbytes, sizeof(char)); + + // Expand submatches + (void) process_submatches(string, match, submatches, nmatches, result, + NULL); + return result; +} + +/*! + * \internal + * \brief Parse a comparison type from a string + * + * \param[in] op String with comparison type (valid values are + * \c PCMK_VALUE_DEFINED, \c PCMK_VALUE_NOT_DEFINED, + * \c PCMK_VALUE_EQ, \c PCMK_VALUE_NE, + * \c PCMK_VALUE_LT, \c PCMK_VALUE_LTE, + * \c PCMK_VALUE_GT, or \c PCMK_VALUE_GTE) + * + * \return Comparison type corresponding to \p op + */ +enum pcmk__comparison +pcmk__parse_comparison(const char *op) +{ + if (pcmk__str_eq(op, PCMK_VALUE_DEFINED, pcmk__str_casei)) { + return pcmk__comparison_defined; + + } else if (pcmk__str_eq(op, PCMK_VALUE_NOT_DEFINED, pcmk__str_casei)) { + return pcmk__comparison_undefined; + + } else if (pcmk__str_eq(op, PCMK_VALUE_EQ, pcmk__str_casei)) { + return pcmk__comparison_eq; + + } else if (pcmk__str_eq(op, PCMK_VALUE_NE, pcmk__str_casei)) { + return pcmk__comparison_ne; + + } else if (pcmk__str_eq(op, PCMK_VALUE_LT, pcmk__str_casei)) { + return pcmk__comparison_lt; + + } else if (pcmk__str_eq(op, PCMK_VALUE_LTE, pcmk__str_casei)) { + return pcmk__comparison_lte; + + } else if (pcmk__str_eq(op, PCMK_VALUE_GT, pcmk__str_casei)) { + return pcmk__comparison_gt; + + } else if (pcmk__str_eq(op, PCMK_VALUE_GTE, pcmk__str_casei)) { + return pcmk__comparison_gte; + } + + return pcmk__comparison_unknown; +} + +/*! + * \internal + * \brief Parse a value type from a string + * + * \param[in] type String with value type (valid values are NULL, + * \c PCMK_VALUE_STRING, \c PCMK_VALUE_INTEGER, + * \c PCMK_VALUE_NUMBER, and \c PCMK_VALUE_VERSION) + * \param[in] op Operation type (used only to select default) + * \param[in] value1 First value being compared (used only to select default) + * \param[in] value2 Second value being compared (used only to select default) + */ +enum pcmk__type +pcmk__parse_type(const char *type, enum pcmk__comparison op, + const char *value1, const char *value2) +{ + if (type == NULL) { + switch (op) { + case pcmk__comparison_lt: + case pcmk__comparison_lte: + case pcmk__comparison_gt: + case pcmk__comparison_gte: + if (((value1 != NULL) && (strchr(value1, '.') != NULL)) + || ((value2 != NULL) && (strchr(value2, '.') != NULL))) { + return pcmk__type_number; + } + return pcmk__type_integer; + + default: + return pcmk__type_string; + } + } + + if (pcmk__str_eq(type, PCMK_VALUE_STRING, pcmk__str_casei)) { + return pcmk__type_string; + + } else if (pcmk__str_eq(type, PCMK_VALUE_INTEGER, pcmk__str_casei)) { + return pcmk__type_integer; + + } else if (pcmk__str_eq(type, PCMK_VALUE_NUMBER, pcmk__str_casei)) { + return pcmk__type_number; + + } else if (pcmk__str_eq(type, PCMK_VALUE_VERSION, pcmk__str_casei)) { + return pcmk__type_version; + } + + return pcmk__type_unknown; +} + +/*! + * \internal + * \brief Compare two strings according to a given type + * + * \param[in] value1 String with first value to compare + * \param[in] value2 String with second value to compare + * \param[in] type How to interpret the values + * + * \return Standard comparison result (a negative integer if \p value1 is + * lesser, 0 if the values are equal, and a positive integer if + * \p value1 is greater) + */ +int +pcmk__cmp_by_type(const char *value1, const char *value2, enum pcmk__type type) +{ + // NULL compares as less than non-NULL + if (value2 == NULL) { + return (value1 == NULL)? 0 : 1; + } + if (value1 == NULL) { + return -1; + } + + switch (type) { + case pcmk__type_string: + return strcasecmp(value1, value2); + + case pcmk__type_integer: + { + long long integer1; + long long integer2; + + if ((pcmk__scan_ll(value1, &integer1, 0LL) != pcmk_rc_ok) + || (pcmk__scan_ll(value2, &integer2, 0LL) != pcmk_rc_ok)) { + crm_warn("Comparing '%s' and '%s' as strings because " + "invalid as integers", value1, value2); + return strcasecmp(value1, value2); + } + return (integer1 < integer2)? -1 : (integer1 > integer2)? 1 : 0; + } + break; + + case pcmk__type_number: + { + double num1; + double num2; + + if ((pcmk__scan_double(value1, &num1, NULL, NULL) != pcmk_rc_ok) + || (pcmk__scan_double(value2, &num2, NULL, + NULL) != pcmk_rc_ok)) { + crm_warn("Comparing '%s' and '%s' as strings because invalid as " + "numbers", value1, value2); + return strcasecmp(value1, value2); + } + return (num1 < num2)? -1 : (num1 > num2)? 1 : 0; + } + break; + + case pcmk__type_version: + return compare_version(value1, value2); + + default: // Invalid type + return 0; + } +} + +/*! + * \internal + * \brief Parse a reference value source from a string + * + * \param[in] source String indicating reference value source + * + * \return Reference value source corresponding to \p source + */ +enum pcmk__reference_source +pcmk__parse_source(const char *source) +{ + if (pcmk__str_eq(source, PCMK_VALUE_LITERAL, + pcmk__str_casei|pcmk__str_null_matches)) { + return pcmk__source_literal; + + } else if (pcmk__str_eq(source, PCMK_VALUE_PARAM, pcmk__str_casei)) { + return pcmk__source_instance_attrs; + + } else if (pcmk__str_eq(source, PCMK_VALUE_META, pcmk__str_casei)) { + return pcmk__source_meta_attrs; + + } else { + return pcmk__source_unknown; + } +} + +/*! + * \internal + * \brief Parse a boolean operator from a string + * + * \param[in] combine String indicating boolean operator + * + * \return Enumeration value corresponding to \p combine + */ +enum pcmk__combine +pcmk__parse_combine(const char *combine) +{ + if (pcmk__str_eq(combine, PCMK_VALUE_AND, + pcmk__str_null_matches|pcmk__str_casei)) { + return pcmk__combine_and; + + } else if (pcmk__str_eq(combine, PCMK_VALUE_OR, pcmk__str_casei)) { + return pcmk__combine_or; + + } else { + return pcmk__combine_unknown; + } +} + +/*! + * \internal + * \brief Get the result of a node attribute comparison for rule evaluation + * + * \param[in] actual Actual node attribute value + * \param[in] reference Node attribute value from rule (ignored for + * \p comparison of \c pcmk__comparison_defined or + * \c pcmk__comparison_undefined) + * \param[in] type How to interpret the values + * \param[in] comparison How to compare the values + * + * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok if the + * comparison passes, and some other value if it does not) + */ +static int +evaluate_attr_comparison(const char *actual, const char *reference, + enum pcmk__type type, enum pcmk__comparison comparison) +{ + int cmp = 0; + + switch (comparison) { + case pcmk__comparison_defined: + return (actual != NULL)? pcmk_rc_ok : pcmk_rc_op_unsatisfied; + + case pcmk__comparison_undefined: + return (actual == NULL)? pcmk_rc_ok : pcmk_rc_op_unsatisfied; + + default: + break; + } + + cmp = pcmk__cmp_by_type(actual, reference, type); + + switch (comparison) { + case pcmk__comparison_eq: + return (cmp == 0)? pcmk_rc_ok : pcmk_rc_op_unsatisfied; + + case pcmk__comparison_ne: + return (cmp != 0)? pcmk_rc_ok : pcmk_rc_op_unsatisfied; + + default: + break; + } + + if ((actual == NULL) || (reference == NULL)) { + return pcmk_rc_op_unsatisfied; // Comparison would be meaningless + } + + switch (comparison) { + case pcmk__comparison_lt: + return (cmp < 0)? pcmk_rc_ok : pcmk_rc_after_range; + + case pcmk__comparison_lte: + return (cmp <= 0)? pcmk_rc_ok : pcmk_rc_after_range; + + case pcmk__comparison_gt: + return (cmp > 0)? pcmk_rc_ok : pcmk_rc_before_range; + + case pcmk__comparison_gte: + return (cmp >= 0)? pcmk_rc_ok : pcmk_rc_before_range; + + default: // Not possible with schema validation enabled + return pcmk_rc_op_unsatisfied; + } +} + +/*! + * \internal + * \brief Get a reference value from a configured source + * + * \param[in] value Value given in rule expression + * \param[in] source Reference value source + * \param[in] rule_input Values used to evaluate rule criteria + */ +static const char * +value_from_source(const char *value, enum pcmk__reference_source source, + const pcmk_rule_input_t *rule_input) +{ + GHashTable *table = NULL; + + if (pcmk__str_empty(value)) { + /* @COMPAT When we can break backward compatibility, drop this block so + * empty strings are treated as such (there should never be an empty + * string as an instance attribute or meta-attribute name, so those will + * get NULL anyway, but it could matter for literal comparisons) + */ + return NULL; + } + + switch (source) { + case pcmk__source_literal: + return value; + + case pcmk__source_instance_attrs: + table = rule_input->rsc_params; + break; + + case pcmk__source_meta_attrs: + table = rule_input->rsc_meta; + break; + + default: + return NULL; // Not possible + } + + if (table == NULL) { + return NULL; + } + return (const char *) g_hash_table_lookup(table, value); +} + +/*! + * \internal + * \brief Evaluate a node attribute rule expression + * + * \param[in] expression XML of a rule's PCMK_XE_EXPRESSION subelement + * \param[in] rule_input Values used to evaluate rule criteria + * + * \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression + * passes, some other value if it does not) + */ +int +pcmk__evaluate_attr_expression(const xmlNode *expression, + const pcmk_rule_input_t *rule_input) +{ + const char *id = NULL; + const char *op = NULL; + const char *attr = NULL; + const char *type_s = NULL; + const char *value = NULL; + const char *actual = NULL; + const char *source_s = NULL; + const char *reference = NULL; + char *expanded_attr = NULL; + int rc = pcmk_rc_ok; + + enum pcmk__type type = pcmk__type_unknown; + enum pcmk__reference_source source = pcmk__source_unknown; + enum pcmk__comparison comparison = pcmk__comparison_unknown; + + if ((expression == NULL) || (rule_input == NULL)) { + return EINVAL; + } + + // Get expression ID (for logging) + id = pcmk__xe_id(expression); + if (pcmk__str_empty(id)) { + /* @COMPAT When we can break behavioral backward compatibility, + * fail the expression + */ + pcmk__config_warn(PCMK_XE_EXPRESSION " element has no " PCMK_XA_ID); + id = "without ID"; // for logging + } + + /* Get name of node attribute to compare (expanding any %0-%9 to + * regular expression submatches) + */ + attr = crm_element_value(expression, PCMK_XA_ATTRIBUTE); + if (attr == NULL) { + pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not passing " + "because " PCMK_XA_ATTRIBUTE " was not specified", id); + return pcmk_rc_unpack_error; + } + expanded_attr = pcmk__replace_submatches(attr, rule_input->rsc_id, + rule_input->rsc_id_submatches, + rule_input->rsc_id_nmatches); + if (expanded_attr != NULL) { + attr = expanded_attr; + } + + // Get and validate operation + op = crm_element_value(expression, PCMK_XA_OPERATION); + comparison = pcmk__parse_comparison(op); + if (comparison == pcmk__comparison_unknown) { + // Not possible with schema validation enabled + if (op == NULL) { + pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not " + "passing because it has no " PCMK_XA_OPERATION, + id); + } else { + pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not " + "passing because '%s' is not a valid " + PCMK_XA_OPERATION, id, op); + } + rc = pcmk_rc_unpack_error; + goto done; + } + + // How reference value is obtained (literal, resource meta-attribute, etc.) + source_s = crm_element_value(expression, PCMK_XA_VALUE_SOURCE); + source = pcmk__parse_source(source_s); + if (source == pcmk__source_unknown) { + // Not possible with schema validation enabled + // @COMPAT Fail expression once we can break backward compatibility + pcmk__config_warn("Expression %s has invalid " PCMK_XA_VALUE_SOURCE + " value '%s', using default " + "('" PCMK_VALUE_LITERAL "')", id, source_s); + source = pcmk__source_literal; + } + + // Get and validate reference value + value = crm_element_value(expression, PCMK_XA_VALUE); + switch (comparison) { + case pcmk__comparison_defined: + case pcmk__comparison_undefined: + if (value != NULL) { + pcmk__config_warn("Ignoring " PCMK_XA_VALUE " in " + PCMK_XE_EXPRESSION " %s because it is unused " + "when " PCMK_XA_BOOLEAN_OP " is %s", id, op); + } + break; + + default: + if (value == NULL) { + pcmk__config_warn(PCMK_XE_EXPRESSION " %s has no " + PCMK_XA_VALUE, id); + } + break; + } + reference = value_from_source(value, source, rule_input); + + // Get actual value of node attribute + if (rule_input->node_attrs != NULL) { + actual = g_hash_table_lookup(rule_input->node_attrs, attr); + } + + // Get and validate value type (after expanding reference value) + type_s = crm_element_value(expression, PCMK_XA_TYPE); + type = pcmk__parse_type(type_s, comparison, actual, reference); + if (type == pcmk__type_unknown) { + /* Not possible with schema validation enabled + * + * @COMPAT When we can break behavioral backward compatibility, treat + * the expression as not passing. + */ + pcmk__config_warn("Non-empty node attribute values will be treated as " + "equal for " PCMK_XE_EXPRESSION " %s because '%s' " + "is not a valid type", id, type); + } + + rc = evaluate_attr_comparison(actual, reference, type, comparison); + switch (comparison) { + case pcmk__comparison_defined: + case pcmk__comparison_undefined: + crm_trace(PCMK_XE_EXPRESSION " %s result: %s (for attribute %s %s)", + id, pcmk_rc_str(rc), attr, op); + break; + + default: + crm_trace(PCMK_XE_EXPRESSION " %s result: " + "%s (attribute %s %s '%s' via %s source as %s type)", + id, pcmk_rc_str(rc), attr, op, pcmk__s(reference, ""), + pcmk__s(source_s, "default"), pcmk__s(type_s, "default")); + break; + } + +done: + free(expanded_attr); + return rc; +} + +/*! + * \internal + * \brief Evaluate a resource rule expression + * + * \param[in] rsc_expression XML of rule's \c PCMK_XE_RSC_EXPRESSION subelement + * \param[in] rule_input Values used to evaluate rule criteria + * + * \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression + * passes, some other value if it does not) + */ +int +pcmk__evaluate_rsc_expression(const xmlNode *rsc_expression, + const pcmk_rule_input_t *rule_input) +{ + const char *id = NULL; + const char *standard = NULL; + const char *provider = NULL; + const char *type = NULL; + + if ((rsc_expression == NULL) || (rule_input == NULL)) { + return EINVAL; + } + + // Validate XML ID + id = pcmk__xe_id(rsc_expression); + if (pcmk__str_empty(id)) { + // Not possible with schema validation enabled + /* @COMPAT When we can break behavioral backward compatibility, + * fail the expression + */ + pcmk__config_warn(PCMK_XE_RSC_EXPRESSION " has no " PCMK_XA_ID); + id = "without ID"; // for logging + } + + // Compare resource standard + standard = crm_element_value(rsc_expression, PCMK_XA_CLASS); + if ((standard != NULL) + && !pcmk__str_eq(standard, rule_input->rsc_standard, pcmk__str_none)) { + crm_trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because " + "actual standard '%s' doesn't match '%s'", + id, pcmk__s(rule_input->rsc_standard, ""), standard); + return pcmk_rc_op_unsatisfied; + } + + // Compare resource provider + provider = crm_element_value(rsc_expression, PCMK_XA_PROVIDER); + if ((provider != NULL) + && !pcmk__str_eq(provider, rule_input->rsc_provider, pcmk__str_none)) { + crm_trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because " + "actual provider '%s' doesn't match '%s'", + id, pcmk__s(rule_input->rsc_provider, ""), provider); + return pcmk_rc_op_unsatisfied; + } + + // Compare resource agent type + type = crm_element_value(rsc_expression, PCMK_XA_TYPE); + if ((type != NULL) + && !pcmk__str_eq(type, rule_input->rsc_agent, pcmk__str_none)) { + crm_trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because " + "actual agent '%s' doesn't match '%s'", + id, pcmk__s(rule_input->rsc_agent, ""), type); + return pcmk_rc_op_unsatisfied; + } + + crm_trace(PCMK_XE_RSC_EXPRESSION " %s is satisfied by %s%s%s:%s", + id, pcmk__s(standard, ""), + ((provider == NULL)? "" : ":"), pcmk__s(provider, ""), + pcmk__s(type, "")); + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Evaluate an operation rule expression + * + * \param[in] op_expression XML of a rule's \c PCMK_XE_OP_EXPRESSION subelement + * \param[in] rule_input Values used to evaluate rule criteria + * + * \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression + * is satisfied, some other value if it is not) + */ +int +pcmk__evaluate_op_expression(const xmlNode *op_expression, + const pcmk_rule_input_t *rule_input) +{ + const char *id = NULL; + const char *name = NULL; + const char *interval_s = NULL; + guint interval_ms = 0U; + + if ((op_expression == NULL) || (rule_input == NULL)) { + return EINVAL; + } + + // Get operation expression ID (for logging) + id = pcmk__xe_id(op_expression); + if (pcmk__str_empty(id)) { // Not possible with schema validation enabled + /* @COMPAT When we can break behavioral backward compatibility, + * return pcmk_rc_op_unsatisfied + */ + pcmk__config_warn(PCMK_XE_OP_EXPRESSION " element has no " PCMK_XA_ID); + id = "without ID"; // for logging + } + + // Validate operation name + name = crm_element_value(op_expression, PCMK_XA_NAME); + if (name == NULL) { // Not possible with schema validation enabled + pcmk__config_warn("Treating " PCMK_XE_OP_EXPRESSION " %s as not " + "passing because it has no " PCMK_XA_NAME, id); + return pcmk_rc_unpack_error; + } + + // Validate operation interval + interval_s = crm_element_value(op_expression, PCMK_META_INTERVAL); + if (pcmk_parse_interval_spec(interval_s, &interval_ms) != pcmk_rc_ok) { + pcmk__config_warn("Treating " PCMK_XE_OP_EXPRESSION " %s as not " + "passing because '%s' is not a valid interval", + id, interval_s); + return pcmk_rc_unpack_error; + } + + // Compare operation name + if (!pcmk__str_eq(name, rule_input->op_name, pcmk__str_none)) { + crm_trace(PCMK_XE_OP_EXPRESSION " %s is unsatisfied because " + "actual name '%s' doesn't match '%s'", + id, pcmk__s(rule_input->op_name, ""), name); + return pcmk_rc_op_unsatisfied; + } + + // Compare operation interval (unspecified interval matches all) + if ((interval_s != NULL) && (interval_ms != rule_input->op_interval_ms)) { + crm_trace(PCMK_XE_OP_EXPRESSION " %s is unsatisfied because " + "actual interval %s doesn't match %s", + id, pcmk__readable_interval(rule_input->op_interval_ms), + pcmk__readable_interval(interval_ms)); + return pcmk_rc_op_unsatisfied; + } + + crm_trace(PCMK_XE_OP_EXPRESSION " %s is satisfied (name %s, interval %s)", + id, name, pcmk__readable_interval(rule_input->op_interval_ms)); + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Evaluate a rule condition + * + * \param[in,out] condition XML containing a rule condition (a subrule, or an + * expression of any type) + * \param[in] rule_input Values used to evaluate rule criteria + * \param[out] next_change If not NULL, set to when evaluation will change + * + * \return Standard Pacemaker return code (\c pcmk_rc_ok if the condition + * passes, some other value if it does not) + */ +int +pcmk__evaluate_condition(xmlNode *condition, + const pcmk_rule_input_t *rule_input, + crm_time_t *next_change) +{ + + if ((condition == NULL) || (rule_input == NULL)) { + return EINVAL; + } + + switch (pcmk__condition_type(condition)) { + case pcmk__condition_rule: + return pcmk_evaluate_rule(condition, rule_input, next_change); + + case pcmk__condition_attribute: + case pcmk__condition_location: + return pcmk__evaluate_attr_expression(condition, rule_input); + + case pcmk__condition_datetime: + { + int rc = pcmk__evaluate_date_expression(condition, + rule_input->now, + next_change); + + return (rc == pcmk_rc_within_range)? pcmk_rc_ok : rc; + } + + case pcmk__condition_resource: + return pcmk__evaluate_rsc_expression(condition, rule_input); + + case pcmk__condition_operation: + return pcmk__evaluate_op_expression(condition, rule_input); + + default: // Not possible with schema validation enabled + pcmk__config_err("Treating rule condition %s as not passing " + "because %s is not a valid condition type", + pcmk__s(pcmk__xe_id(condition), "without ID"), + (const char *) condition->name); + return pcmk_rc_unpack_error; + } +} + +/*! + * \brief Evaluate a single rule, including all its conditions + * + * \param[in,out] rule XML containing a rule definition or its id-ref + * \param[in] rule_input Values used to evaluate rule criteria + * \param[out] next_change If not NULL, set to when evaluation will change + * + * \return Standard Pacemaker return code (\c pcmk_rc_ok if the rule is + * satisfied, some other value if it is not) + */ +int +pcmk_evaluate_rule(xmlNode *rule, const pcmk_rule_input_t *rule_input, + crm_time_t *next_change) +{ + bool empty = true; + int rc = pcmk_rc_ok; + const char *id = NULL; + const char *value = NULL; + enum pcmk__combine combine = pcmk__combine_unknown; + + if ((rule == NULL) || (rule_input == NULL)) { + return EINVAL; + } + + rule = expand_idref(rule, NULL); + if (rule == NULL) { + // Not possible with schema validation enabled; message already logged + return pcmk_rc_unpack_error; + } + + // Validate XML ID + id = pcmk__xe_id(rule); + if (pcmk__str_empty(id)) { + /* @COMPAT When we can break behavioral backward compatibility, + * fail the rule + */ + pcmk__config_warn(PCMK_XE_RULE " has no " PCMK_XA_ID); + id = "without ID"; // for logging + } + + value = crm_element_value(rule, PCMK_XA_BOOLEAN_OP); + combine = pcmk__parse_combine(value); + switch (combine) { + case pcmk__combine_and: + // For "and", rc defaults to success (reset on failure below) + break; + + case pcmk__combine_or: + // For "or", rc defaults to failure (reset on success below) + rc = pcmk_rc_op_unsatisfied; + break; + + default: + /* @COMPAT When we can break behavioral backward compatibility, + * return pcmk_rc_unpack_error + */ + pcmk__config_warn("Rule %s has invalid " PCMK_XA_BOOLEAN_OP + " value '%s', using default '" PCMK_VALUE_AND "'", + pcmk__xe_id(rule), value); + combine = pcmk__combine_and; + break; + } + + // Evaluate each condition + for (xmlNode *condition = pcmk__xe_first_child(rule, NULL, NULL, NULL); + condition != NULL; condition = pcmk__xe_next(condition)) { + + empty = false; + if (pcmk__evaluate_condition(condition, rule_input, + next_change) == pcmk_rc_ok) { + if (combine == pcmk__combine_or) { + rc = pcmk_rc_ok; // Any pass is final for "or" + break; + } + } else if (combine == pcmk__combine_and) { + rc = pcmk_rc_op_unsatisfied; // Any failure is final for "and" + break; + } + } + + if (empty) { // Not possible with schema validation enabled + /* @COMPAT Currently, we don't actually ignore "or" rules because + * rc is initialized to failure above in that case. When we can break + * backward compatibility, reset rc to pcmk_rc_ok here. + */ + pcmk__config_warn("Ignoring rule %s because it contains no conditions", + id); + } + + crm_trace("Rule %s is %ssatisfied", id, ((rc == pcmk_rc_ok)? "" : "not ")); + return rc; +} + +/*! + * \internal + * \brief Evaluate all rules contained within an element + * + * \param[in,out] xml XML element possibly containing rule subelements + * \param[in] rule_input Values used to evaluate rule criteria + * \param[out] next_change If not NULL, set to when evaluation will change + * + * \return Standard Pacemaker return code (pcmk_rc_ok if there are no contained + * rules or any contained rule passes, otherwise the result of the last + * rule) + * \deprecated On code paths leading to this function, the schema allows + * multiple top-level rules only in the deprecated lifetime element + * of location constraints. The code also allows multiple top-level + * rules when unpacking attribute sets, but this is deprecated and + * already prevented by schema validation. This function can be + * dropped when support for those is dropped. + */ +int +pcmk__evaluate_rules(xmlNode *xml, const pcmk_rule_input_t *rule_input, + crm_time_t *next_change) +{ + // If there are no rules, pass by default + int rc = pcmk_rc_ok; + bool have_rule = false; + + for (xmlNode *rule = pcmk__xe_first_child(xml, PCMK_XE_RULE, NULL, NULL); + rule != NULL; rule = pcmk__xe_next_same(rule)) { + + if (have_rule) { + pcmk__warn_once(pcmk__wo_multiple_rules, + "Support for multiple top-level rules is " + "deprecated (replace with a single rule containing " + "the existing rules with " PCMK_XA_BOOLEAN_OP + "set to " PCMK_VALUE_OR " instead)"); + } else { + have_rule = true; + } + + rc = pcmk_evaluate_rule(rule, rule_input, next_change); + if (rc == pcmk_rc_ok) { + break; + } + } + return rc; +} diff --git a/lib/common/scheduler.c b/lib/common/scheduler.c index 20e6fdf..dad3de9 100644 --- a/lib/common/scheduler.c +++ b/lib/common/scheduler.c @@ -1,5 +1,5 @@ /* - * 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. * @@ -10,5 +10,100 @@ #include <crm_internal.h> #include <stdint.h> // uint32_t +#include <errno.h> // EINVAL +#include <glib.h> // gboolean, FALSE +#include <libxml/tree.h> // xmlNode + +#include <crm/common/scheduler.h> uint32_t pcmk__warnings = 0; + +gboolean was_processing_error = FALSE; +gboolean was_processing_warning = FALSE; + +/*! + * \internal + * \brief Get the Designated Controller node from scheduler data + * + * \param[in] scheduler Scheduler data + * + * \return Designated Controller node from scheduler data, or NULL if none + */ +pcmk_node_t * +pcmk_get_dc(const pcmk_scheduler_t *scheduler) +{ + return (scheduler == NULL)? NULL : scheduler->dc_node; +} + +/*! + * \internal + * \brief Get the no quorum policy from scheduler data + * + * \param[in] scheduler Scheduler data + * + * \return No quorum policy from scheduler data + */ +enum pe_quorum_policy +pcmk_get_no_quorum_policy(const pcmk_scheduler_t *scheduler) +{ + if (scheduler == NULL) { + return pcmk_no_quorum_stop; // The default + } + return scheduler->no_quorum_policy; +} + +/*! + * \internal + * \brief Set CIB XML as scheduler input in scheduler data + * + * \param[out] scheduler Scheduler data + * \param[in] cib CIB XML to set as scheduler input + * + * \return Standard Pacemaker return code (EINVAL if \p scheduler is NULL, + * otherwise pcmk_rc_ok) + * \note This will not free any previously set scheduler CIB. + */ +int +pcmk_set_scheduler_cib(pcmk_scheduler_t *scheduler, xmlNode *cib) +{ + if (scheduler == NULL) { + return EINVAL; + } + scheduler->input = cib; + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Check whether cluster has quorum + * + * \param[in] scheduler Scheduler data + * + * \return true if cluster has quorum, otherwise false + */ +bool +pcmk_has_quorum(const pcmk_scheduler_t *scheduler) +{ + if (scheduler == NULL) { + return false; + } + return pcmk_is_set(scheduler->flags, pcmk_sched_quorate); +} + +/*! + * \brief Find a node by name in scheduler data + * + * \param[in] scheduler Scheduler data + * \param[in] node_name Name of node to find + * + * \return Node from scheduler data that matches \p node_name if any, + * otherwise NULL + */ +pcmk_node_t * +pcmk_find_node(const pcmk_scheduler_t *scheduler, const char *node_name) +{ + if ((scheduler == NULL) || (node_name == NULL)) { + return NULL; + } + return pcmk__find_node_in_list(scheduler->nodes, node_name); +} diff --git a/lib/common/schemas.c b/lib/common/schemas.c index b3c09eb..16f2ccb 100644 --- a/lib/common/schemas.c +++ b/lib/common/schemas.c @@ -1,5 +1,5 @@ /* - * 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. * @@ -22,19 +22,13 @@ #include <libxslt/security.h> #include <libxslt/xsltutils.h> -#include <crm/msg_xml.h> #include <crm/common/xml.h> #include <crm/common/xml_internal.h> /* PCMK__XML_LOG_BASE */ -typedef struct { - unsigned char v[2]; -} schema_version_t; +#include "crmcommon_private.h" #define SCHEMA_ZERO { .v = { 0, 0 } } -#define schema_scanf(s, prefix, version, suffix) \ - sscanf((s), prefix "%hhu.%hhu" suffix, &((version).v[0]), &((version).v[1])) - #define schema_strdup_printf(prefix, version, suffix) \ crm_strdup_printf(prefix "%u.%u" suffix, (version).v[0], (version).v[1]) @@ -44,31 +38,11 @@ typedef struct { xmlRelaxNGParserCtxtPtr parser; } relaxng_ctx_cache_t; -enum schema_validator_e { - schema_validator_none, - schema_validator_rng -}; - -struct schema_s { - char *name; - char *transform; - void *cache; - enum schema_validator_e validator; - int after_transform; - schema_version_t version; - char *transform_enter; - bool transform_onleave; -}; - -static struct schema_s *known_schemas = NULL; -static int xml_schema_max = 0; +static GList *known_schemas = NULL; +static bool initialized = false; static bool silent_logging = FALSE; -static void -xml_log(int priority, const char *fmt, ...) -G_GNUC_PRINTF(2, 3); - -static void +static void G_GNUC_PRINTF(2, 3) xml_log(int priority, const char *fmt, ...) { va_list ap; @@ -84,50 +58,114 @@ xml_log(int priority, const char *fmt, ...) static int xml_latest_schema_index(void) { - // @COMPAT: pacemaker-next is deprecated since 2.1.5 - return xml_schema_max - 3; // index from 0, ignore "pacemaker-next"/"none" + /* This function assumes that crm_schema_init() has been called beforehand, + * so we have at least three schemas (one real schema, the "pacemaker-next" + * schema, and the "none" schema). + * + * @COMPAT: pacemaker-next is deprecated since 2.1.5 and none since 2.1.8. + * Update this when we drop those. + */ + return g_list_length(known_schemas) - 3; } -static int -xml_minimum_schema_index(void) +/*! + * \internal + * \brief Return the schema entry of the highest-versioned schema + * + * \return Schema entry of highest-versioned schema (or NULL on error) + */ +static GList * +get_highest_schema(void) { - static int best = 0; - if (best == 0) { - int lpc = 0; - - best = xml_latest_schema_index(); - for (lpc = best; lpc > 0; lpc--) { - if (known_schemas[lpc].version.v[0] - < known_schemas[best].version.v[0]) { - return best; - } else { - best = lpc; - } - } - best = xml_latest_schema_index(); - } - return best; + /* The highest numerically versioned schema is the one before pacemaker-next + * + * @COMPAT pacemaker-next is deprecated since 2.1.5 + */ + GList *entry = pcmk__get_schema("pacemaker-next"); + + CRM_ASSERT((entry != NULL) && (entry->prev != NULL)); + return entry->prev; } +/*! + * \internal + * \brief Return the name of the highest-versioned schema + * + * \return Name of highest-versioned schema (or NULL on error) + */ const char * -xml_latest_schema(void) +pcmk__highest_schema_name(void) { - return get_schema_name(xml_latest_schema_index()); + GList *entry = get_highest_schema(); + + return ((pcmk__schema_t *)(entry->data))->name; } -static inline bool -version_from_filename(const char *filename, schema_version_t *version) +/*! + * \internal + * \brief Find first entry of highest major schema version series + * + * \return Schema entry of first schema with highest major version + */ +GList * +pcmk__find_x_0_schema(void) { - int rc = schema_scanf(filename, "pacemaker-", *version, ".rng"); +#if defined(PCMK__UNIT_TESTING) + /* If we're unit testing, this can't be static because it'll stick + * around from one test run to the next. It needs to be cleared out + * every time. + */ + GList *x_0_entry = NULL; +#else + static GList *x_0_entry = NULL; +#endif + + pcmk__schema_t *highest_schema = NULL; + + if (x_0_entry != NULL) { + return x_0_entry; + } + x_0_entry = get_highest_schema(); + highest_schema = x_0_entry->data; + + for (GList *iter = x_0_entry->prev; iter != NULL; iter = iter->prev) { + pcmk__schema_t *schema = iter->data; + + /* We've found a schema in an older major version series. Return + * the index of the first one in the same major version series as + * the highest schema. + */ + if (schema->version.v[0] < highest_schema->version.v[0]) { + x_0_entry = iter->next; + break; + } + + /* We're out of list to examine. This probably means there was only + * one major version series, so return the first schema entry. + */ + if (iter->prev == NULL) { + x_0_entry = known_schemas->data; + break; + } + } + return x_0_entry; +} - return (rc == 2); +static inline bool +version_from_filename(const char *filename, pcmk__schema_version_t *version) +{ + if (pcmk__ends_with(filename, ".rng")) { + return sscanf(filename, "pacemaker-%hhu.%hhu.rng", &(version->v[0]), &(version->v[1])) == 2; + } else { + return sscanf(filename, "pacemaker-%hhu.%hhu", &(version->v[0]), &(version->v[1])) == 2; + } } static int schema_filter(const struct dirent *a) { int rc = 0; - schema_version_t version = SCHEMA_ZERO; + pcmk__schema_version_t version = SCHEMA_ZERO; if (strstr(a->d_name, "pacemaker-") != a->d_name) { /* crm_trace("%s - wrong prefix", a->d_name); */ @@ -147,17 +185,8 @@ schema_filter(const struct dirent *a) } static int -schema_sort(const struct dirent **a, const struct dirent **b) +schema_cmp(pcmk__schema_version_t a_version, pcmk__schema_version_t b_version) { - schema_version_t a_version = SCHEMA_ZERO; - schema_version_t b_version = SCHEMA_ZERO; - - if (!version_from_filename(a[0]->d_name, &a_version) - || !version_from_filename(b[0]->d_name, &b_version)) { - // Shouldn't be possible, but makes static analysis happy - return 0; - } - for (int i = 0; i < 2; ++i) { if (a_version.v[i] < b_version.v[i]) { return -1; @@ -168,6 +197,21 @@ schema_sort(const struct dirent **a, const struct dirent **b) return 0; } +static int +schema_cmp_directory(const struct dirent **a, const struct dirent **b) +{ + pcmk__schema_version_t a_version = SCHEMA_ZERO; + pcmk__schema_version_t b_version = SCHEMA_ZERO; + + if (!version_from_filename(a[0]->d_name, &a_version) + || !version_from_filename(b[0]->d_name, &b_version)) { + // Shouldn't be possible, but makes static analysis happy + return 0; + } + + return schema_cmp(a_version, b_version); +} + /*! * \internal * \brief Add given schema + auxiliary data to internal bookkeeping. @@ -176,63 +220,35 @@ schema_sort(const struct dirent **a, const struct dirent **b) * through \c add_schema_by_version. */ static void -add_schema(enum schema_validator_e validator, const schema_version_t *version, +add_schema(enum pcmk__schema_validator validator, const pcmk__schema_version_t *version, const char *name, const char *transform, - const char *transform_enter, bool transform_onleave, - int after_transform) + const char *transform_enter, bool transform_onleave) { - int last = xml_schema_max; - bool have_version = FALSE; + pcmk__schema_t *schema = NULL; - xml_schema_max++; - known_schemas = pcmk__realloc(known_schemas, - xml_schema_max * sizeof(struct schema_s)); - CRM_ASSERT(known_schemas != NULL); - memset(known_schemas+last, 0, sizeof(struct schema_s)); - known_schemas[last].validator = validator; - known_schemas[last].after_transform = after_transform; + schema = pcmk__assert_alloc(1, sizeof(pcmk__schema_t)); - for (int i = 0; i < 2; ++i) { - known_schemas[last].version.v[i] = version->v[i]; - if (version->v[i]) { - have_version = TRUE; - } - } - if (have_version) { - known_schemas[last].name = schema_strdup_printf("pacemaker-", *version, ""); + schema->validator = validator; + schema->version.v[0] = version->v[0]; + schema->version.v[1] = version->v[1]; + schema->transform_onleave = transform_onleave; + // schema->schema_index is set after all schemas are loaded and sorted + + if (version->v[0] || version->v[1]) { + schema->name = schema_strdup_printf("pacemaker-", *version, ""); } else { - CRM_ASSERT(name); - schema_scanf(name, "%*[^-]-", known_schemas[last].version, ""); - known_schemas[last].name = strdup(name); + schema->name = pcmk__str_copy(name); } if (transform) { - known_schemas[last].transform = strdup(transform); + schema->transform = pcmk__str_copy(transform); } + if (transform_enter) { - known_schemas[last].transform_enter = strdup(transform_enter); - } - known_schemas[last].transform_onleave = transform_onleave; - if (after_transform == 0) { - after_transform = xml_schema_max; /* upgrade is a one-way */ + schema->transform_enter = pcmk__str_copy(transform_enter); } - known_schemas[last].after_transform = after_transform; - if (known_schemas[last].after_transform < 0) { - crm_debug("Added supported schema %d: %s", - last, known_schemas[last].name); - - } else if (known_schemas[last].transform) { - crm_debug("Added supported schema %d: %s (upgrades to %d with %s.xsl)", - last, known_schemas[last].name, - known_schemas[last].after_transform, - known_schemas[last].transform); - - } else { - crm_debug("Added supported schema %d: %s (upgrades to %d)", - last, known_schemas[last].name, - known_schemas[last].after_transform); - } + known_schemas = g_list_prepend(known_schemas, schema); } /*! @@ -264,8 +280,7 @@ add_schema(enum schema_validator_e validator, const schema_version_t *version, * . name convention: (see "upgrade-enter") */ static int -add_schema_by_version(const schema_version_t *version, int next, - bool transform_expected) +add_schema_by_version(const pcmk__schema_version_t *version, bool transform_expected) { bool transform_onleave = FALSE; int rc = pcmk_rc_ok; @@ -321,12 +336,11 @@ add_schema_by_version(const schema_version_t *version, int next, free(xslt); free(transform_upgrade); transform_upgrade = NULL; - next = -1; rc = ENOENT; } - add_schema(schema_validator_rng, version, NULL, - transform_upgrade, transform_enter, transform_onleave, next); + add_schema(pcmk__schema_validator_rng, version, NULL, + transform_upgrade, transform_enter, transform_onleave); free(transform_upgrade); free(transform_enter); @@ -366,6 +380,85 @@ wrap_libxslt(bool finalize) } } +void +pcmk__load_schemas_from_dir(const char *dir) +{ + int lpc, max; + struct dirent **namelist = NULL; + + max = scandir(dir, &namelist, schema_filter, schema_cmp_directory); + if (max < 0) { + crm_warn("Could not load schemas from %s: %s", dir, strerror(errno)); + return; + } + + for (lpc = 0; lpc < max; lpc++) { + bool transform_expected = false; + pcmk__schema_version_t version = SCHEMA_ZERO; + + if (!version_from_filename(namelist[lpc]->d_name, &version)) { + // Shouldn't be possible, but makes static analysis happy + crm_warn("Skipping schema '%s': could not parse version", + namelist[lpc]->d_name); + continue; + } + if ((lpc + 1) < max) { + pcmk__schema_version_t next_version = SCHEMA_ZERO; + + if (version_from_filename(namelist[lpc+1]->d_name, &next_version) + && (version.v[0] < next_version.v[0])) { + transform_expected = true; + } + } + + if (add_schema_by_version(&version, transform_expected) != pcmk_rc_ok) { + break; + } + } + + for (lpc = 0; lpc < max; lpc++) { + free(namelist[lpc]); + } + + free(namelist); +} + +static gint +schema_sort_GCompareFunc(gconstpointer a, gconstpointer b) +{ + const pcmk__schema_t *schema_a = a; + const pcmk__schema_t *schema_b = b; + + // @COMPAT pacemaker-next is deprecated since 2.1.5 and none since 2.1.8 + if (pcmk__str_eq(schema_a->name, "pacemaker-next", pcmk__str_none)) { + if (pcmk__str_eq(schema_b->name, PCMK_VALUE_NONE, pcmk__str_none)) { + return -1; + } else { + return 1; + } + } else if (pcmk__str_eq(schema_a->name, PCMK_VALUE_NONE, pcmk__str_none)) { + return 1; + } else if (pcmk__str_eq(schema_b->name, "pacemaker-next", pcmk__str_none)) { + return -1; + } else { + return schema_cmp(schema_a->version, schema_b->version); + } +} + +/*! + * \internal + * \brief Sort the list of known schemas such that all pacemaker-X.Y are in + * version order, then pacemaker-next, then none + * + * This function should be called whenever additional schemas are loaded using + * pcmk__load_schemas_from_dir(), after the initial sets in crm_schema_init(). + */ +void +pcmk__sort_schemas(void) +{ + known_schemas = g_list_sort(known_schemas, schema_sort_GCompareFunc); +} + /*! * \internal * \brief Load pacemaker schemas into cache @@ -376,79 +469,67 @@ wrap_libxslt(bool finalize) void crm_schema_init(void) { - int lpc, max; - char *base = pcmk__xml_artefact_root(pcmk__xml_artefact_ns_legacy_rng); - struct dirent **namelist = NULL; - const schema_version_t zero = SCHEMA_ZERO; + if (!initialized) { + const char *remote_schema_dir = pcmk__remote_schema_dir(); + char *base = pcmk__xml_artefact_root(pcmk__xml_artefact_ns_legacy_rng); + const pcmk__schema_version_t zero = SCHEMA_ZERO; + int schema_index = 0; - wrap_libxslt(false); + initialized = true; - max = scandir(base, &namelist, schema_filter, schema_sort); - if (max < 0) { - crm_notice("scandir(%s) failed: %s (%d)", base, strerror(errno), errno); - free(base); + wrap_libxslt(false); - } else { + pcmk__load_schemas_from_dir(base); + pcmk__load_schemas_from_dir(remote_schema_dir); free(base); - for (lpc = 0; lpc < max; lpc++) { - bool transform_expected = FALSE; - int next = 0; - schema_version_t version = SCHEMA_ZERO; - - if (!version_from_filename(namelist[lpc]->d_name, &version)) { - // Shouldn't be possible, but makes static analysis happy - crm_err("Skipping schema '%s': could not parse version", - namelist[lpc]->d_name); - continue; - } - if ((lpc + 1) < max) { - schema_version_t next_version = SCHEMA_ZERO; - if (version_from_filename(namelist[lpc+1]->d_name, &next_version) - && (version.v[0] < next_version.v[0])) { - transform_expected = TRUE; - } + // @COMPAT: Deprecated since 2.1.5 + add_schema(pcmk__schema_validator_rng, &zero, "pacemaker-next", NULL, + NULL, FALSE); + // @COMPAT Deprecated since 2.1.8 + add_schema(pcmk__schema_validator_none, &zero, PCMK_VALUE_NONE, NULL, + NULL, FALSE); + + /* add_schema() prepends items to the list, so in the simple case, this + * just reverses the list. However if there were any remote schemas, + * sorting is necessary. + */ + pcmk__sort_schemas(); + + // Now set the schema indexes and log the final result + for (GList *iter = known_schemas; iter != NULL; iter = iter->next) { + pcmk__schema_t *schema = iter->data; + + if (schema->transform == NULL) { + crm_debug("Loaded schema %d: %s", schema_index, schema->name); } else { - next = -1; - } - if (add_schema_by_version(&version, next, transform_expected) - == ENOENT) { - break; + crm_debug("Loaded schema %d: %s (upgrades with %s.xsl)", + schema_index, schema->name, schema->transform); } + schema->schema_index = schema_index++; } - - for (lpc = 0; lpc < max; lpc++) { - free(namelist[lpc]); - } - free(namelist); } - - // @COMPAT: Deprecated since 2.1.5 - add_schema(schema_validator_rng, &zero, "pacemaker-next", - NULL, NULL, FALSE, -1); - - add_schema(schema_validator_none, &zero, PCMK__VALUE_NONE, - NULL, NULL, FALSE, -1); } -static gboolean -validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler, void *error_handler_context, const char *relaxng_file, +static bool +validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler, + void *error_handler_context, const char *relaxng_file, relaxng_ctx_cache_t **cached_ctx) { int rc = 0; - gboolean valid = TRUE; + bool valid = true; relaxng_ctx_cache_t *ctx = NULL; - CRM_CHECK(doc != NULL, return FALSE); - CRM_CHECK(relaxng_file != NULL, return FALSE); + CRM_CHECK(doc != NULL, return false); + CRM_CHECK(relaxng_file != NULL, return false); if (cached_ctx && *cached_ctx) { ctx = *cached_ctx; } else { crm_debug("Creating RNG parser context"); - ctx = calloc(1, sizeof(relaxng_ctx_cache_t)); + ctx = pcmk__assert_alloc(1, sizeof(relaxng_ctx_cache_t)); ctx->parser = xmlRelaxNGNewParserCtxt(relaxng_file); CRM_CHECK(ctx->parser != NULL, goto cleanup); @@ -488,7 +569,7 @@ validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler, rc = xmlRelaxNGValidateDoc(ctx->valid, doc); if (rc > 0) { - valid = FALSE; + valid = false; } else if (rc < 0) { crm_err("Internal libxml error during validation"); @@ -515,6 +596,45 @@ validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler, return valid; } +static void +free_schema(gpointer data) +{ + pcmk__schema_t *schema = data; + relaxng_ctx_cache_t *ctx = NULL; + + switch (schema->validator) { + case pcmk__schema_validator_none: // not cached + break; + + case pcmk__schema_validator_rng: // cached + ctx = (relaxng_ctx_cache_t *) schema->cache; + if (ctx == NULL) { + break; + } + + if (ctx->parser != NULL) { + xmlRelaxNGFreeParserCtxt(ctx->parser); + } + + if (ctx->valid != NULL) { + xmlRelaxNGFreeValidCtxt(ctx->valid); + } + + if (ctx->rng != NULL) { + xmlRelaxNGFree(ctx->rng); + } + + free(ctx); + schema->cache = NULL; + break; + } + + free(schema->name); + free(schema->transform); + free(schema->transform_enter); + free(schema); +} + /*! * \internal * \brief Clean up global memory associated with XML schemas @@ -522,62 +642,86 @@ validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler, void crm_schema_cleanup(void) { - int lpc; - relaxng_ctx_cache_t *ctx = NULL; + if (known_schemas != NULL) { + g_list_free_full(known_schemas, free_schema); + known_schemas = NULL; + } + initialized = false; - for (lpc = 0; lpc < xml_schema_max; lpc++) { + wrap_libxslt(true); +} - switch (known_schemas[lpc].validator) { - case schema_validator_none: // not cached - break; - case schema_validator_rng: // cached - ctx = (relaxng_ctx_cache_t *) known_schemas[lpc].cache; - if (ctx == NULL) { - break; - } - if (ctx->parser != NULL) { - xmlRelaxNGFreeParserCtxt(ctx->parser); - } - if (ctx->valid != NULL) { - xmlRelaxNGFreeValidCtxt(ctx->valid); - } - if (ctx->rng != NULL) { - xmlRelaxNGFree(ctx->rng); - } - free(ctx); - known_schemas[lpc].cache = NULL; - break; +/*! + * \internal + * \brief Get schema list entry corresponding to a schema name + * + * \param[in] name Name of schema to get + * + * \return Schema list entry corresponding to \p name, or NULL if unknown + */ +GList * +pcmk__get_schema(const char *name) +{ + // @COMPAT Not specifying a schema name is deprecated since 2.1.8 + if (name == NULL) { + name = PCMK_VALUE_NONE; + } + for (GList *iter = known_schemas; iter != NULL; iter = iter->next) { + pcmk__schema_t *schema = iter->data; + + if (pcmk__str_eq(name, schema->name, pcmk__str_casei)) { + return iter; } - free(known_schemas[lpc].name); - free(known_schemas[lpc].transform); - free(known_schemas[lpc].transform_enter); } - free(known_schemas); - known_schemas = NULL; + return NULL; +} - wrap_libxslt(true); +/*! + * \internal + * \brief Compare two schema version numbers given the schema names + * + * \param[in] schema1 Name of first schema to compare + * \param[in] schema2 Name of second schema to compare + * + * \return Standard comparison result (negative integer if \p schema1 has the + * lower version number, positive integer if \p schema1 has the higher + * version number, of 0 if the version numbers are equal) + */ +int +pcmk__cmp_schemas_by_name(const char *schema1_name, const char *schema2_name) +{ + GList *entry1 = pcmk__get_schema(schema1_name); + GList *entry2 = pcmk__get_schema(schema2_name); + + if (entry1 == NULL) { + return (entry2 == NULL)? 0 : -1; + + } else if (entry2 == NULL) { + return 1; + + } else { + pcmk__schema_t *schema1 = entry1->data; + pcmk__schema_t *schema2 = entry2->data; + + return schema1->schema_index - schema2->schema_index; + } } -static gboolean -validate_with(xmlNode *xml, int method, xmlRelaxNGValidityErrorFunc error_handler, void* error_handler_context) +static bool +validate_with(xmlNode *xml, pcmk__schema_t *schema, + xmlRelaxNGValidityErrorFunc error_handler, + void *error_handler_context) { - gboolean valid = FALSE; + bool valid = false; char *file = NULL; - struct schema_s *schema = NULL; relaxng_ctx_cache_t **cache = NULL; - if (method < 0) { - return FALSE; - } - - schema = &(known_schemas[method]); - if (schema->validator == schema_validator_none) { - return TRUE; + if (schema == NULL) { + return false; } - if (pcmk__str_eq(schema->name, "pacemaker-next", pcmk__str_none)) { - crm_warn("The pacemaker-next schema is deprecated and will be removed " - "in a future release."); + if (schema->validator == pcmk__schema_validator_none) { + return true; } file = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng, @@ -586,13 +730,12 @@ validate_with(xmlNode *xml, int method, xmlRelaxNGValidityErrorFunc error_handle crm_trace("Validating with %s (type=%d)", pcmk__s(file, "missing schema"), schema->validator); switch (schema->validator) { - case schema_validator_rng: + case pcmk__schema_validator_rng: cache = (relaxng_ctx_cache_t **) &(schema->cache); valid = validate_with_relaxng(xml->doc, error_handler, error_handler_context, file, cache); break; default: - crm_err("Unknown validator type: %d", - known_schemas[method].validator); + crm_err("Unknown validator type: %d", schema->validator); break; } @@ -601,124 +744,74 @@ validate_with(xmlNode *xml, int method, xmlRelaxNGValidityErrorFunc error_handle } static bool -validate_with_silent(xmlNode *xml, int method) +validate_with_silent(xmlNode *xml, pcmk__schema_t *schema) { bool rc, sl_backup = silent_logging; silent_logging = TRUE; - rc = validate_with(xml, method, (xmlRelaxNGValidityErrorFunc) xml_log, GUINT_TO_POINTER(LOG_ERR)); + rc = validate_with(xml, schema, (xmlRelaxNGValidityErrorFunc) xml_log, GUINT_TO_POINTER(LOG_ERR)); silent_logging = sl_backup; return rc; } -static void -dump_file(const char *filename) +bool +pcmk__validate_xml(xmlNode *xml_blob, const char *validation, + xmlRelaxNGValidityErrorFunc error_handler, + void *error_handler_context) { + GList *entry = NULL; + pcmk__schema_t *schema = NULL; - FILE *fp = NULL; - int ch, line = 0; - - CRM_CHECK(filename != NULL, return); - - fp = fopen(filename, "r"); - if (fp == NULL) { - crm_perror(LOG_ERR, "Could not open %s for reading", filename); - return; - } - - fprintf(stderr, "%4d ", ++line); - do { - ch = getc(fp); - if (ch == EOF) { - putc('\n', stderr); - break; - } else if (ch == '\n') { - fprintf(stderr, "\n%4d ", ++line); - } else { - putc(ch, stderr); - } - } while (1); - - fclose(fp); -} - -gboolean -validate_xml_verbose(const xmlNode *xml_blob) -{ - int fd = 0; - xmlDoc *doc = NULL; - xmlNode *xml = NULL; - gboolean rc = FALSE; - char *filename = NULL; - - filename = crm_strdup_printf("%s/cib-invalid.XXXXXX", pcmk__get_tmpdir()); - - umask(S_IWGRP | S_IWOTH | S_IROTH); - fd = mkstemp(filename); - write_xml_fd(xml_blob, filename, fd, FALSE); - - dump_file(filename); - - doc = xmlReadFile(filename, NULL, 0); - xml = xmlDocGetRootElement(doc); - rc = validate_xml(xml, NULL, FALSE); - free_xml(xml); - - unlink(filename); - free(filename); - - return rc; -} - -gboolean -validate_xml(xmlNode *xml_blob, const char *validation, gboolean to_logs) -{ - return pcmk__validate_xml(xml_blob, validation, to_logs ? (xmlRelaxNGValidityErrorFunc) xml_log : NULL, GUINT_TO_POINTER(LOG_ERR)); -} - -gboolean -pcmk__validate_xml(xmlNode *xml_blob, const char *validation, xmlRelaxNGValidityErrorFunc error_handler, void* error_handler_context) -{ - int version = 0; - - CRM_CHECK((xml_blob != NULL) && (xml_blob->doc != NULL), return FALSE); + CRM_CHECK((xml_blob != NULL) && (xml_blob->doc != NULL), return false); if (validation == NULL) { - validation = crm_element_value(xml_blob, XML_ATTR_VALIDATION); + validation = crm_element_value(xml_blob, PCMK_XA_VALIDATE_WITH); } + pcmk__warn_if_schema_deprecated(validation); + // @COMPAT Not specifying a schema name is deprecated since 2.1.8 if (validation == NULL) { - int lpc = 0; - bool valid = FALSE; - - for (lpc = 0; lpc < xml_schema_max; lpc++) { - if (validate_with(xml_blob, lpc, NULL, NULL)) { - valid = TRUE; - crm_xml_add(xml_blob, XML_ATTR_VALIDATION, - known_schemas[lpc].name); - crm_info("XML validated against %s", known_schemas[lpc].name); - if(known_schemas[lpc].after_transform == 0) { - break; - } + bool valid = false; + + for (entry = known_schemas; entry != NULL; entry = entry->next) { + schema = entry->data; + if (validate_with(xml_blob, schema, NULL, NULL)) { + valid = true; + crm_xml_add(xml_blob, PCMK_XA_VALIDATE_WITH, schema->name); + crm_info("XML validated against %s", schema->name); } } - return valid; } - version = get_schema_version(validation); - if (strcmp(validation, PCMK__VALUE_NONE) == 0) { - return TRUE; - } else if (version < xml_schema_max) { - return validate_with(xml_blob, version, error_handler, error_handler_context); + entry = pcmk__get_schema(validation); + if (entry == NULL) { + pcmk__config_err("Cannot validate CIB with " PCMK_XA_VALIDATE_WITH + " set to an unknown schema such as '%s' (manually" + " edit to use a known schema)", + validation); + return false; } - crm_err("Unknown validator: %s", validation); - return FALSE; + schema = entry->data; + return validate_with(xml_blob, schema, error_handler, + error_handler_context); } -static void -cib_upgrade_err(void *ctx, const char *fmt, ...) -G_GNUC_PRINTF(2, 3); +/*! + * \internal + * \brief Validate XML using its configured schema (and send errors to logs) + * + * \param[in] xml XML to validate + * + * \return true if XML validates, otherwise false + */ +bool +pcmk__configured_schema_validates(xmlNode *xml) +{ + return pcmk__validate_xml(xml, NULL, + (xmlRelaxNGValidityErrorFunc) xml_log, + GUINT_TO_POINTER(LOG_ERR)); +} /* With this arrangement, an attempt to identify the message severity as explicitly signalled directly from XSLT is performed in rather @@ -743,7 +836,7 @@ G_GNUC_PRINTF(2, 3); (suspicious, likely internal errors or some runaways) is LOG_WARNING. */ -static void +static void G_GNUC_PRINTF(2, 3) cib_upgrade_err(void *ctx, const char *fmt, ...) { va_list ap, aq; @@ -858,8 +951,20 @@ cib_upgrade_err(void *ctx, const char *fmt, ...) va_end(ap); } +/*! + * \internal + * \brief Apply a single XSL transformation to given XML + * + * \param[in] xml XML to transform + * \param[in] transform XSL name + * \param[in] to_logs If false, certain validation errors will be sent to + * stderr rather than logged + * + * \return Transformed XML on success, otherwise NULL + */ static xmlNode * -apply_transformation(xmlNode *xml, const char *transform, gboolean to_logs) +apply_transformation(const xmlNode *xml, const char *transform, + gboolean to_logs) { char *xform = NULL; xmlNode *out = NULL; @@ -898,52 +1003,79 @@ apply_transformation(xmlNode *xml, const char *transform, gboolean to_logs) /*! * \internal - * \brief Possibly full enter->upgrade->leave trip per internal bookkeeping. + * \brief Perform all transformations needed to upgrade XML to next schema + * + * A schema upgrade can require up to three XSL transformations: an "enter" + * transform, the main upgrade transform, and a "leave" transform. Perform + * all needed transforms to upgrade given XML to the next schema. + * + * \param[in] original_xml XML to transform + * \param[in] schema_index Index of schema that successfully validates + * \p original_xml + * \param[in] to_logs If false, certain validation errors will be sent to + * stderr rather than logged * - * \note Only emits warnings about enter/leave phases in case of issues. + * \return XML result of schema transforms if successful, otherwise NULL */ static xmlNode * -apply_upgrade(xmlNode *xml, const struct schema_s *schema, gboolean to_logs) +apply_upgrade(const xmlNode *original_xml, int schema_index, gboolean to_logs) { - bool transform_onleave = schema->transform_onleave; + pcmk__schema_t *schema = g_list_nth_data(known_schemas, schema_index); + pcmk__schema_t *upgraded_schema = g_list_nth_data(known_schemas, + schema_index + 1); + bool transform_onleave = false; char *transform_leave; - xmlNode *upgrade = NULL, - *final = NULL; + const xmlNode *xml = original_xml; + xmlNode *upgrade = NULL; + xmlNode *final = NULL; + xmlRelaxNGValidityErrorFunc error_handler = NULL; - if (schema->transform_enter) { - crm_debug("Upgrading %s-style configuration, pre-upgrade phase with %s.xsl", - schema->name, schema->transform_enter); + CRM_ASSERT((schema != NULL) && (upgraded_schema != NULL)); + + if (to_logs) { + error_handler = (xmlRelaxNGValidityErrorFunc) xml_log; + } + + transform_onleave = schema->transform_onleave; + if (schema->transform_enter != NULL) { + crm_debug("Upgrading schema from %s to %s: " + "applying pre-upgrade XSL transform %s", + schema->name, upgraded_schema->name, schema->transform_enter); upgrade = apply_transformation(xml, schema->transform_enter, to_logs); if (upgrade == NULL) { - crm_warn("Upgrade-enter transformation %s.xsl failed", + crm_warn("Pre-upgrade XSL transform %s failed, " + "will skip post-upgrade transform", schema->transform_enter); transform_onleave = FALSE; + } else { + xml = upgrade; } } - if (upgrade == NULL) { - upgrade = xml; - } - crm_debug("Upgrading %s-style configuration, main phase with %s.xsl", - schema->name, schema->transform); - final = apply_transformation(upgrade, schema->transform, to_logs); + + crm_debug("Upgrading schema from %s to %s: " + "applying upgrade XSL transform %s", + schema->name, upgraded_schema->name, schema->transform); + final = apply_transformation(xml, schema->transform, to_logs); if (upgrade != xml) { free_xml(upgrade); upgrade = NULL; } - if (final != NULL && transform_onleave) { + if ((final != NULL) && transform_onleave) { upgrade = final; /* following condition ensured in add_schema_by_version */ CRM_ASSERT(schema->transform_enter != NULL); transform_leave = strdup(schema->transform_enter); /* enter -> leave */ memcpy(strrchr(transform_leave, '-') + 1, "leave", sizeof("leave") - 1); - crm_debug("Upgrading %s-style configuration, post-upgrade phase with %s.xsl", - schema->name, transform_leave); + crm_debug("Upgrading schema from %s to %s: " + "applying post-upgrade XSL transform %s", + schema->name, upgraded_schema->name, transform_leave); final = apply_transformation(upgrade, transform_leave, to_logs); if (final == NULL) { - crm_warn("Upgrade-leave transformation %s.xsl failed", transform_leave); + crm_warn("Ignoring failure of post-upgrade XSL transform %s", + transform_leave); final = upgrade; } else { free_xml(upgrade); @@ -951,290 +1083,661 @@ apply_upgrade(xmlNode *xml, const struct schema_s *schema, gboolean to_logs) free(transform_leave); } - return final; -} + if (final == NULL) { + return NULL; + } -const char * -get_schema_name(int version) -{ - if (version < 0 || version >= xml_schema_max) { - return "unknown"; + // Ensure result validates with its new schema + if (!validate_with(final, upgraded_schema, error_handler, + GUINT_TO_POINTER(LOG_ERR))) { + crm_err("Schema upgrade from %s to %s failed: " + "XSL transform %s produced an invalid configuration", + schema->name, upgraded_schema->name, schema->transform); + crm_log_xml_debug(final, "bad-transform-result"); + free_xml(final); + return NULL; } - return known_schemas[version].name; + + crm_info("Schema upgrade from %s to %s succeeded", + schema->name, upgraded_schema->name); + return final; } -int -get_schema_version(const char *name) +/*! + * \internal + * \brief Get the schema list entry corresponding to XML configuration + * + * \param[in] xml CIB XML to check + * + * \return List entry of schema configured in \p xml + */ +static GList * +get_configured_schema(const xmlNode *xml) { - int lpc = 0; + const char *schema_name = crm_element_value(xml, PCMK_XA_VALIDATE_WITH); - if (name == NULL) { - name = PCMK__VALUE_NONE; + pcmk__warn_if_schema_deprecated(schema_name); + if (schema_name == NULL) { + return NULL; } - for (; lpc < xml_schema_max; lpc++) { - if (pcmk__str_eq(name, known_schemas[lpc].name, pcmk__str_casei)) { - return lpc; - } - } - return -1; + return pcmk__get_schema(schema_name); } -/* set which validation to use */ +/*! + * \brief Update CIB XML to latest schema that validates it + * + * \param[in,out] xml XML to update (may be freed and replaced + * after being transformed) + * \param[in] max_schema_name If not NULL, do not update \p xml to any + * schema later than this one + * \param[in] transform If false, do not update \p xml to any schema + * that requires an XSL transform + * \param[in] to_logs If false, certain validation errors will be + * sent to stderr rather than logged + * + * \return Standard Pacemaker return code + */ int -update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, - gboolean to_logs) +pcmk__update_schema(xmlNode **xml, const char *max_schema_name, bool transform, + bool to_logs) { - xmlNode *xml = NULL; - char *value = NULL; int max_stable_schemas = xml_latest_schema_index(); - int lpc = 0, match = -1, rc = pcmk_ok; - int next = -1; /* -1 denotes "inactive" value */ + int max_schema_index = 0; + int rc = pcmk_rc_ok; + GList *entry = NULL; + pcmk__schema_t *best_schema = NULL; + pcmk__schema_t *original_schema = NULL; xmlRelaxNGValidityErrorFunc error_handler = to_logs ? (xmlRelaxNGValidityErrorFunc) xml_log : NULL; - CRM_CHECK(best != NULL, return -EINVAL); - *best = 0; - - CRM_CHECK((xml_blob != NULL) && (*xml_blob != NULL) - && ((*xml_blob)->doc != NULL), - return -EINVAL); - - xml = *xml_blob; - value = crm_element_value_copy(xml, XML_ATTR_VALIDATION); + CRM_CHECK((xml != NULL) && (*xml != NULL) && ((*xml)->doc != NULL), + return EINVAL); - if (value != NULL) { - match = get_schema_version(value); + if (max_schema_name != NULL) { + GList *max_entry = pcmk__get_schema(max_schema_name); - lpc = match; - if (lpc >= 0 && transform == FALSE) { - *best = lpc++; + if (max_entry != NULL) { + pcmk__schema_t *max_schema = max_entry->data; - } else if (lpc < 0) { - crm_debug("Unknown validation schema"); - lpc = 0; + max_schema_index = max_schema->schema_index; } } + if ((max_schema_index < 1) || (max_schema_index > max_stable_schemas)) { + max_schema_index = max_stable_schemas; + } - if (match >= max_stable_schemas) { - /* nothing to do */ - free(value); - *best = match; - return pcmk_ok; + entry = get_configured_schema(*xml); + if (entry == NULL) { + // @COMPAT Not specifying a schema name is deprecated since 2.1.8 + entry = known_schemas; + } else { + original_schema = entry->data; + if (original_schema->schema_index >= max_schema_index) { + return pcmk_rc_ok; + } } - while (lpc <= max_stable_schemas) { - crm_debug("Testing '%s' validation (%d of %d)", - known_schemas[lpc].name ? known_schemas[lpc].name : "<unset>", - lpc, max_stable_schemas); + for (; entry != NULL; entry = entry->next) { + pcmk__schema_t *current_schema = entry->data; + xmlNode *upgrade = NULL; - if (validate_with(xml, lpc, error_handler, GUINT_TO_POINTER(LOG_ERR)) == FALSE) { - if (next != -1) { - crm_info("Configuration not valid for schema: %s", - known_schemas[lpc].name); - next = -1; - } else { - crm_trace("%s validation failed", - known_schemas[lpc].name ? known_schemas[lpc].name : "<unset>"); - } - if (*best) { + if (current_schema->schema_index > max_schema_index) { + break; + } + + if (!validate_with(*xml, current_schema, error_handler, + GUINT_TO_POINTER(LOG_ERR))) { + crm_debug("Schema %s does not validate", current_schema->name); + if (best_schema != NULL) { /* we've satisfied the validation, no need to check further */ break; } - rc = -pcmk_err_schema_validation; - - } else { - if (next != -1) { - crm_debug("Configuration valid for schema: %s", - known_schemas[next].name); - next = -1; - } - rc = pcmk_ok; + rc = pcmk_rc_schema_validation; + continue; // Try again with the next higher schema } - if (rc == pcmk_ok) { - *best = lpc; + crm_debug("Schema %s validates", current_schema->name); + rc = pcmk_rc_ok; + best_schema = current_schema; + if (current_schema->schema_index == max_schema_index) { + break; // No further transformations possible } - if (rc == pcmk_ok && transform) { - xmlNode *upgrade = NULL; - next = known_schemas[lpc].after_transform; - - if (next <= lpc) { - /* There is no next version, or next would regress */ - crm_trace("Stopping at %s", known_schemas[lpc].name); - break; - - } else if (max > 0 && (lpc == max || next > max)) { - crm_trace("Upgrade limit reached at %s (lpc=%d, next=%d, max=%d)", - known_schemas[lpc].name, lpc, next, max); - break; - - } else if (known_schemas[lpc].transform == NULL - /* possibly avoid transforming when readily valid - (in general more restricted when crossing the major - version boundary, as X.0 "transitional" version is - expected to be more strict than it's successors that - may re-allow constructs from previous major line) */ - || validate_with_silent(xml, next)) { - crm_debug("%s-style configuration is also valid for %s", - known_schemas[lpc].name, known_schemas[next].name); - - lpc = next; - - } else { - crm_debug("Upgrading %s-style configuration to %s with %s.xsl", - known_schemas[lpc].name, known_schemas[next].name, - known_schemas[lpc].transform); - - upgrade = apply_upgrade(xml, &known_schemas[lpc], to_logs); - if (upgrade == NULL) { - crm_err("Transformation %s.xsl failed", - known_schemas[lpc].transform); - rc = -pcmk_err_transform_failed; - - } else if (validate_with(upgrade, next, error_handler, GUINT_TO_POINTER(LOG_ERR))) { - crm_info("Transformation %s.xsl successful", - known_schemas[lpc].transform); - lpc = next; - *best = next; - free_xml(xml); - xml = upgrade; - rc = pcmk_ok; - - } else { - crm_err("Transformation %s.xsl did not produce a valid configuration", - known_schemas[lpc].transform); - crm_log_xml_info(upgrade, "transform:bad"); - free_xml(upgrade); - rc = -pcmk_err_schema_validation; - } - next = -1; - } + if (!transform || (current_schema->transform == NULL) + || validate_with_silent(*xml, entry->next->data)) { + /* The next schema either doesn't require a transform or validates + * successfully even without the transform. Skip the transform and + * try the next schema with the same XML. + */ + continue; } - if (transform == FALSE || rc != pcmk_ok) { - /* we need some progress! */ - lpc++; + upgrade = apply_upgrade(*xml, current_schema->schema_index, to_logs); + if (upgrade == NULL) { + /* The transform failed, so this schema can't be used. Later + * schemas are unlikely to validate, but try anyway until we + * run out of options. + */ + rc = pcmk_rc_transform_failed; + } else { + best_schema = current_schema; + free_xml(*xml); + *xml = upgrade; } } - if (*best > match && *best) { - crm_info("%s the configuration from %s to %s", - transform?"Transformed":"Upgraded", - value ? value : "<none>", known_schemas[*best].name); - crm_xml_add(xml, XML_ATTR_VALIDATION, known_schemas[*best].name); + if (best_schema != NULL) { + if ((original_schema == NULL) + || (best_schema->schema_index > original_schema->schema_index)) { + crm_info("%s the configuration schema to %s", + (transform? "Transformed" : "Upgraded"), + best_schema->name); + crm_xml_add(*xml, PCMK_XA_VALIDATE_WITH, best_schema->name); + } } - - *xml_blob = xml; - free(value); return rc; } -gboolean -cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs) +/*! + * \brief Update XML from its configured schema to the latest major series + * + * \param[in,out] xml XML to update + * \param[in] to_logs If false, certain validation errors will be + * sent to stderr rather than logged + * + * \return Standard Pacemaker return code + */ +int +pcmk_update_configured_schema(xmlNode **xml, bool to_logs) { - gboolean rc = TRUE; - const char *value = crm_element_value(*xml, XML_ATTR_VALIDATION); - char *const orig_value = strdup(value == NULL ? "(none)" : value); + int rc = pcmk_rc_ok; + char *original_schema_name = NULL; + + // @COMPAT Not specifying a schema name is deprecated since 2.1.8 + const char *effective_original_name = "the first"; + + int orig_version = -1; + pcmk__schema_t *x_0_schema = pcmk__find_x_0_schema()->data; + GList *entry = NULL; + + CRM_CHECK(xml != NULL, return EINVAL); - int version = get_schema_version(value); - int orig_version = version; - int min_version = xml_minimum_schema_index(); + original_schema_name = crm_element_value_copy(*xml, PCMK_XA_VALIDATE_WITH); + pcmk__warn_if_schema_deprecated(original_schema_name); + entry = pcmk__get_schema(original_schema_name); + if (entry != NULL) { + pcmk__schema_t *original_schema = entry->data; - if (version < min_version) { + effective_original_name = original_schema->name; + orig_version = original_schema->schema_index; + } + + if (orig_version < x_0_schema->schema_index) { // Current configuration schema is not acceptable, try to update xmlNode *converted = NULL; + const char *new_schema_name = NULL; + pcmk__schema_t *schema = NULL; + + entry = NULL; + converted = pcmk__xml_copy(NULL, *xml); + if (pcmk__update_schema(&converted, NULL, true, to_logs) == pcmk_rc_ok) { + new_schema_name = crm_element_value(converted, + PCMK_XA_VALIDATE_WITH); + entry = pcmk__get_schema(new_schema_name); + } + schema = (entry == NULL)? NULL : entry->data; - converted = copy_xml(*xml); - update_validation(&converted, &version, 0, TRUE, to_logs); - - value = crm_element_value(converted, XML_ATTR_VALIDATION); - if (version < min_version) { + if ((schema == NULL) + || (schema->schema_index < x_0_schema->schema_index)) { // Updated configuration schema is still not acceptable - if (version < orig_version || orig_version == -1) { + if ((orig_version == -1) || (schema == NULL) + || (schema->schema_index < orig_version)) { // We couldn't validate any schema at all if (to_logs) { pcmk__config_err("Cannot upgrade configuration (claiming " - "schema %s) to at least %s because it " + "%s schema) to at least %s because it " "does not validate with any schema from " - "%s to %s", - orig_value, - get_schema_name(min_version), - get_schema_name(orig_version), - xml_latest_schema()); + "%s to the latest", + pcmk__s(original_schema_name, "no"), + x_0_schema->name, effective_original_name); } else { fprintf(stderr, "Cannot upgrade configuration (claiming " - "schema %s) to at least %s because it " + "%s schema) to at least %s because it " "does not validate with any schema from " - "%s to %s\n", - orig_value, - get_schema_name(min_version), - get_schema_name(orig_version), - xml_latest_schema()); + "%s to the latest\n", + pcmk__s(original_schema_name, "no"), + x_0_schema->name, effective_original_name); } } else { // We updated configuration successfully, but still too low if (to_logs) { pcmk__config_err("Cannot upgrade configuration (claiming " - "schema %s) to at least %s because it " + "%s schema) to at least %s because it " "would not upgrade past %s", - orig_value, - get_schema_name(min_version), - pcmk__s(value, "unspecified version")); + pcmk__s(original_schema_name, "no"), + x_0_schema->name, + pcmk__s(new_schema_name, "unspecified version")); } else { fprintf(stderr, "Cannot upgrade configuration (claiming " - "schema %s) to at least %s because it " + "%s schema) to at least %s because it " "would not upgrade past %s\n", - orig_value, - get_schema_name(min_version), - pcmk__s(value, "unspecified version")); + pcmk__s(original_schema_name, "no"), + x_0_schema->name, + pcmk__s(new_schema_name, "unspecified version")); } } free_xml(converted); converted = NULL; - rc = FALSE; + rc = pcmk_rc_transform_failed; } else { // Updated configuration schema is acceptable free_xml(*xml); *xml = converted; - if (version < xml_latest_schema_index()) { + if (schema->schema_index < xml_latest_schema_index()) { if (to_logs) { - pcmk__config_warn("Configuration with schema %s was " + pcmk__config_warn("Configuration with %s schema was " "internally upgraded to acceptable (but " "not most recent) %s", - orig_value, get_schema_name(version)); - } - } else { - if (to_logs) { - crm_info("Configuration with schema %s was internally " - "upgraded to latest version %s", - orig_value, get_schema_name(version)); + pcmk__s(original_schema_name, "no"), + schema->name); } + } else if (to_logs) { + crm_info("Configuration with %s schema was internally " + "upgraded to latest version %s", + pcmk__s(original_schema_name, "no"), + schema->name); } } - } else if (version >= get_schema_version(PCMK__VALUE_NONE)) { - // Schema validation is disabled - if (to_logs) { - pcmk__config_warn("Schema validation of configuration is disabled " - "(enabling is encouraged and prevents common " - "misconfigurations)"); + } else { + // @COMPAT the none schema is deprecated since 2.1.8 + pcmk__schema_t *none_schema = NULL; + + entry = pcmk__get_schema(PCMK_VALUE_NONE); + CRM_ASSERT((entry != NULL) && (entry->data != NULL)); + + none_schema = entry->data; + if (!to_logs && (orig_version >= none_schema->schema_index)) { + fprintf(stderr, "Schema validation of configuration is " + "disabled (support for " PCMK_XA_VALIDATE_WITH + " set to \"" PCMK_VALUE_NONE "\" is deprecated" + " and will be removed in a future release)\n"); + } + } - } else { - fprintf(stderr, "Schema validation of configuration is disabled " - "(enabling is encouraged and prevents common " - "misconfigurations)\n"); + free(original_schema_name); + return rc; +} + +/*! + * \internal + * \brief Return a list of all schema files and any associated XSLT files + * later than the given one + * \brief Return a list of all schema versions later than the given one + * + * \param[in] schema The schema to compare against (for example, + * "pacemaker-3.1.rng" or "pacemaker-3.1") + * + * \note The caller is responsible for freeing both the returned list and + * the elements of the list + */ +GList * +pcmk__schema_files_later_than(const char *name) +{ + GList *lst = NULL; + pcmk__schema_version_t ver; + + if (!version_from_filename(name, &ver)) { + return lst; + } + + for (GList *iter = g_list_nth(known_schemas, xml_latest_schema_index()); + iter != NULL; iter = iter->prev) { + pcmk__schema_t *schema = iter->data; + char *s = NULL; + + if (schema_cmp(ver, schema->version) != -1) { + continue; + } + + s = crm_strdup_printf("%s.rng", schema->name); + lst = g_list_prepend(lst, s); + + if (schema->transform != NULL) { + char *xform = crm_strdup_printf("%s.xsl", schema->transform); + lst = g_list_prepend(lst, xform); + } + + if (schema->transform_enter != NULL) { + char *enter = crm_strdup_printf("%s.xsl", schema->transform_enter); + + lst = g_list_prepend(lst, enter); + + if (schema->transform_onleave) { + int last_dash = strrchr(enter, '-') - enter; + char *leave = crm_strdup_printf("%.*s-leave.xsl", last_dash, enter); + + lst = g_list_prepend(lst, leave); + } } } - if (best_version) { - *best_version = version; + return lst; +} + +static void +append_href(xmlNode *xml, void *user_data) +{ + GList **list = user_data; + char *href = crm_element_value_copy(xml, "href"); + + if (href == NULL) { + return; } + *list = g_list_prepend(*list, href); +} + +static void +external_refs_in_schema(GList **list, const char *contents) +{ + /* local-name()= is needed to ignore the xmlns= setting at the top of + * the XML file. Otherwise, the xpath query will always return nothing. + */ + const char *search = "//*[local-name()='externalRef'] | //*[local-name()='include']"; + xmlNode *xml = pcmk__xml_parse(contents); - free(orig_value); + crm_foreach_xpath_result(xml, search, append_href, list); + free_xml(xml); +} + +static int +read_file_contents(const char *file, char **contents) +{ + int rc = pcmk_rc_ok; + char *path = NULL; + + if (pcmk__ends_with(file, ".rng")) { + path = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng, file); + } else { + path = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, file); + } + + rc = pcmk__file_contents(path, contents); + + free(path); return rc; } + +static void +add_schema_file_to_xml(xmlNode *parent, const char *file, GList **already_included) +{ + char *contents = NULL; + char *path = NULL; + xmlNode *file_node = NULL; + GList *includes = NULL; + int rc = pcmk_rc_ok; + + /* If we already included this file, don't do so again. */ + if (g_list_find_custom(*already_included, file, (GCompareFunc) strcmp) != NULL) { + return; + } + + /* Ensure whatever file we were given has a suffix we know about. If not, + * just assume it's an RNG file. + */ + if (!pcmk__ends_with(file, ".rng") && !pcmk__ends_with(file, ".xsl")) { + path = crm_strdup_printf("%s.rng", file); + } else { + path = pcmk__str_copy(file); + } + + rc = read_file_contents(path, &contents); + if (rc != pcmk_rc_ok || contents == NULL) { + crm_warn("Could not read schema file %s: %s", file, pcmk_rc_str(rc)); + free(path); + return; + } + + /* Create a new <file path="..."> node with the contents of the file + * as a CDATA block underneath it. + */ + file_node = pcmk__xe_create(parent, PCMK_XA_FILE); + crm_xml_add(file_node, PCMK_XA_PATH, path); + *already_included = g_list_prepend(*already_included, path); + + xmlAddChild(file_node, xmlNewCDataBlock(parent->doc, (pcmkXmlStr) contents, + strlen(contents))); + + /* Scan the file for any <externalRef> or <include> nodes and build up + * a list of the files they reference. + */ + external_refs_in_schema(&includes, contents); + + /* For each referenced file, recurse to add it (and potentially anything it + * references, ...) to the XML. + */ + for (GList *iter = includes; iter != NULL; iter = iter->next) { + add_schema_file_to_xml(parent, iter->data, already_included); + } + + free(contents); + g_list_free_full(includes, free); +} + +/*! + * \internal + * \brief Add an XML schema file and all the files it references as children + * of a given XML node + * + * \param[in,out] parent The parent XML node + * \param[in] name The schema version to compare against + * (for example, "pacemaker-3.1" or "pacemaker-3.1.rng") + * \param[in,out] already_included A list of names that have already been added + * to the parent node. + * + * \note The caller is responsible for freeing both the returned list and + * the elements of the list + */ +void +pcmk__build_schema_xml_node(xmlNode *parent, const char *name, GList **already_included) +{ + xmlNode *schema_node = pcmk__xe_create(parent, PCMK__XA_SCHEMA); + + crm_xml_add(schema_node, PCMK_XA_VERSION, name); + add_schema_file_to_xml(schema_node, name, already_included); + + if (schema_node->children == NULL) { + // Not needed if empty. May happen if name was invalid, for example. + free_xml(schema_node); + } +} + +/*! + * \internal + * \brief Return the directory containing any extra schema files that a + * Pacemaker Remote node fetched from the cluster + */ +const char * +pcmk__remote_schema_dir(void) +{ + const char *dir = pcmk__env_option(PCMK__ENV_REMOTE_SCHEMA_DIRECTORY); + + if (pcmk__str_empty(dir)) { + return PCMK__REMOTE_SCHEMA_DIR; + } + + return dir; +} + +/*! + * \internal + * \brief Warn if a given validation schema is deprecated + * + * \param[in] Schema name to check + */ +void +pcmk__warn_if_schema_deprecated(const char *schema) +{ + if ((schema == NULL) || + pcmk__strcase_any_of(schema, "pacemaker-next", PCMK_VALUE_NONE, NULL)) { + pcmk__config_warn("Support for " PCMK_XA_VALIDATE_WITH "='%s' is " + "deprecated and will be removed in a future release " + "without the possibility of upgrades (manually edit " + "to use a supported schema)", pcmk__s(schema, "")); + } +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include <crm/common/xml_compat.h> + +const char * +xml_latest_schema(void) +{ + return pcmk__highest_schema_name(); +} + +const char * +get_schema_name(int version) +{ + pcmk__schema_t *schema = g_list_nth_data(known_schemas, version); + + return (schema != NULL)? schema->name : "unknown"; +} + +int +get_schema_version(const char *name) +{ + int lpc = 0; + + if (name == NULL) { + name = PCMK_VALUE_NONE; + } + + for (GList *iter = known_schemas; iter != NULL; iter = iter->next) { + pcmk__schema_t *schema = iter->data; + + if (pcmk__str_eq(name, schema->name, pcmk__str_casei)) { + return lpc; + } + + lpc++; + } + + return -1; +} + +int +update_validation(xmlNode **xml, int *best, int max, gboolean transform, + gboolean to_logs) +{ + int rc = pcmk__update_schema(xml, get_schema_name(max), transform, to_logs); + + if ((best != NULL) && (xml != NULL) && (rc == pcmk_rc_ok)) { + const char *schema_name = crm_element_value(*xml, + PCMK_XA_VALIDATE_WITH); + GList *schema_entry = pcmk__get_schema(schema_name); + + if (schema_entry != NULL) { + *best = ((pcmk__schema_t *)(schema_entry->data))->schema_index; + } + } + + return pcmk_rc2legacy(rc); +} + +gboolean +validate_xml(xmlNode *xml_blob, const char *validation, gboolean to_logs) +{ + bool rc = pcmk__validate_xml(xml_blob, validation, + to_logs? (xmlRelaxNGValidityErrorFunc) xml_log : NULL, + GUINT_TO_POINTER(LOG_ERR)); + return rc? TRUE : FALSE; +} + +static void +dump_file(const char *filename) +{ + + FILE *fp = NULL; + int ch, line = 0; + + CRM_CHECK(filename != NULL, return); + + fp = fopen(filename, "r"); + if (fp == NULL) { + crm_perror(LOG_ERR, "Could not open %s for reading", filename); + return; + } + + fprintf(stderr, "%4d ", ++line); + do { + ch = getc(fp); + if (ch == EOF) { + putc('\n', stderr); + break; + } else if (ch == '\n') { + fprintf(stderr, "\n%4d ", ++line); + } else { + putc(ch, stderr); + } + } while (1); + + fclose(fp); +} + +gboolean +validate_xml_verbose(const xmlNode *xml_blob) +{ + int fd = 0; + xmlDoc *doc = NULL; + xmlNode *xml = NULL; + gboolean rc = FALSE; + char *filename = NULL; + + filename = crm_strdup_printf("%s/cib-invalid.XXXXXX", pcmk__get_tmpdir()); + + umask(S_IWGRP | S_IWOTH | S_IROTH); + fd = mkstemp(filename); + pcmk__xml_write_fd(xml_blob, filename, fd, false, NULL); + + dump_file(filename); + + doc = xmlReadFile(filename, NULL, 0); + xml = xmlDocGetRootElement(doc); + rc = pcmk__validate_xml(xml, NULL, NULL, NULL); + free_xml(xml); + + unlink(filename); + free(filename); + + return rc? TRUE : FALSE; +} + +gboolean +cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs) +{ + int rc = pcmk_update_configured_schema(xml, to_logs); + + if (best_version != NULL) { + const char *name = crm_element_value(*xml, PCMK_XA_VALIDATE_WITH); + + if (name == NULL) { + *best_version = -1; + } else { + GList *entry = pcmk__get_schema(name); + pcmk__schema_t *schema = (entry == NULL)? NULL : entry->data; + + *best_version = (schema == NULL)? -1 : schema->schema_index; + } + } + return (rc == pcmk_rc_ok)? TRUE: FALSE; +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/scores.c b/lib/common/scores.c index 63c314e..39e224e 100644 --- a/lib/common/scores.c +++ b/lib/common/scores.c @@ -1,5 +1,5 @@ /* - * 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. * @@ -39,29 +39,29 @@ char2score(const char *score) return 0; } else if (pcmk_str_is_minus_infinity(score)) { - return -CRM_SCORE_INFINITY; + return -PCMK_SCORE_INFINITY; } else if (pcmk_str_is_infinity(score)) { - return CRM_SCORE_INFINITY; + return PCMK_SCORE_INFINITY; - } else if (pcmk__str_eq(score, PCMK__VALUE_RED, pcmk__str_casei)) { + } else if (pcmk__str_eq(score, PCMK_VALUE_RED, pcmk__str_casei)) { return pcmk__score_red; - } else if (pcmk__str_eq(score, PCMK__VALUE_YELLOW, pcmk__str_casei)) { + } else if (pcmk__str_eq(score, PCMK_VALUE_YELLOW, pcmk__str_casei)) { return pcmk__score_yellow; - } else if (pcmk__str_eq(score, PCMK__VALUE_GREEN, pcmk__str_casei)) { + } else if (pcmk__str_eq(score, PCMK_VALUE_GREEN, pcmk__str_casei)) { return pcmk__score_green; } else { long long score_ll; pcmk__scan_ll(score, &score_ll, 0LL); - if (score_ll > CRM_SCORE_INFINITY) { - return CRM_SCORE_INFINITY; + if (score_ll > PCMK_SCORE_INFINITY) { + return PCMK_SCORE_INFINITY; - } else if (score_ll < -CRM_SCORE_INFINITY) { - return -CRM_SCORE_INFINITY; + } else if (score_ll < -PCMK_SCORE_INFINITY) { + return -PCMK_SCORE_INFINITY; } else { return (int) score_ll; @@ -86,13 +86,13 @@ const char * pcmk_readable_score(int score) { // The longest possible result is "-INFINITY" - static char score_s[sizeof(CRM_MINUS_INFINITY_S)]; + static char score_s[sizeof(PCMK_VALUE_MINUS_INFINITY)]; - if (score >= CRM_SCORE_INFINITY) { - strcpy(score_s, CRM_INFINITY_S); + if (score >= PCMK_SCORE_INFINITY) { + strcpy(score_s, PCMK_VALUE_INFINITY); - } else if (score <= -CRM_SCORE_INFINITY) { - strcpy(score_s, CRM_MINUS_INFINITY_S); + } else if (score <= -PCMK_SCORE_INFINITY) { + strcpy(score_s, PCMK_VALUE_MINUS_INFINITY); } else { // Range is limited to +/-1000000, so no chance of overflow @@ -115,25 +115,25 @@ pcmk_readable_score(int score) int pcmk__add_scores(int score1, int score2) { - /* As long as CRM_SCORE_INFINITY is less than half of the maximum integer, + /* As long as PCMK_SCORE_INFINITY is less than half of the maximum integer, * we can ignore the possibility of integer overflow. */ int result = score1 + score2; // First handle the cases where one or both is infinite - if ((score1 <= -CRM_SCORE_INFINITY) || (score2 <= -CRM_SCORE_INFINITY)) { - return -CRM_SCORE_INFINITY; + if ((score1 <= -PCMK_SCORE_INFINITY) || (score2 <= -PCMK_SCORE_INFINITY)) { + return -PCMK_SCORE_INFINITY; } - if ((score1 >= CRM_SCORE_INFINITY) || (score2 >= CRM_SCORE_INFINITY)) { - return CRM_SCORE_INFINITY; + if ((score1 >= PCMK_SCORE_INFINITY) || (score2 >= PCMK_SCORE_INFINITY)) { + return PCMK_SCORE_INFINITY; } // Bound result to infinity. - if (result >= CRM_SCORE_INFINITY) { - return CRM_SCORE_INFINITY; + if (result >= PCMK_SCORE_INFINITY) { + return PCMK_SCORE_INFINITY; } - if (result <= -CRM_SCORE_INFINITY) { - return -CRM_SCORE_INFINITY; + if (result <= -PCMK_SCORE_INFINITY) { + return -PCMK_SCORE_INFINITY; } return result; @@ -142,21 +142,18 @@ pcmk__add_scores(int score1, int score2) // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START -#include <crm/common/util_compat.h> +#include <crm/common/scores_compat.h> char * score2char(int score) { - char *result = strdup(pcmk_readable_score(score)); - - CRM_ASSERT(result != NULL); - return result; + return pcmk__str_copy(pcmk_readable_score(score)); } char * score2char_stack(int score, char *buf, size_t len) { - CRM_CHECK((buf != NULL) && (len >= sizeof(CRM_MINUS_INFINITY_S)), + CRM_CHECK((buf != NULL) && (len >= sizeof(PCMK_VALUE_MINUS_INFINITY)), return NULL); strcpy(buf, pcmk_readable_score(score)); return buf; diff --git a/lib/common/strings.c b/lib/common/strings.c index d9d2fda..acf174d 100644 --- a/lib/common/strings.c +++ b/lib/common/strings.c @@ -1,5 +1,5 @@ /* - * 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. * @@ -342,74 +342,146 @@ pcmk__guint_from_hash(GHashTable *table, const char *key, guint default_val, return pcmk_rc_ok; } -#ifndef NUMCHARS -# define NUMCHARS "0123456789." -#endif - -#ifndef WHITESPACE -# define WHITESPACE " \t\n\r\f" -#endif - /*! * \brief Parse a time+units string and return milliseconds equivalent * - * \param[in] input String with a number and optional unit (optionally - * with whitespace before and/or after the number). If - * missing, the unit defaults to seconds. + * \param[in] input String with a nonnegative number and optional unit + * (optionally with whitespace before and/or after the + * number). If missing, the unit defaults to seconds. * * \return Milliseconds corresponding to string expression, or - * PCMK__PARSE_INT_DEFAULT on error + * \c PCMK__PARSE_INT_DEFAULT on error */ long long crm_get_msec(const char *input) { - const char *num_start = NULL; - const char *units; + char *units = NULL; // Do not free; will point to part of input long long multiplier = 1000; long long divisor = 1; long long msec = PCMK__PARSE_INT_DEFAULT; - size_t num_len = 0; - char *end_text = NULL; if (input == NULL) { return PCMK__PARSE_INT_DEFAULT; } - num_start = input + strspn(input, WHITESPACE); - num_len = strspn(num_start, NUMCHARS); - if (num_len < 1) { + // Skip initial whitespace + while (isspace(*input)) { + input++; + } + + // Reject negative and unparsable inputs + scan_ll(input, &msec, -1, &units); + if (msec < 0) { return PCMK__PARSE_INT_DEFAULT; } - units = num_start + num_len; - units += strspn(units, WHITESPACE); - if (!strncasecmp(units, "ms", 2) || !strncasecmp(units, "msec", 4)) { + /* If the number is a decimal, scan_ll() reads only the integer part. Skip + * any remaining digits or decimal characters. + * + * @COMPAT Well-formed and malformed decimals are both accepted inputs. For + * example, "3.14 ms" and "3.1.4 ms" are treated the same as "3ms" and + * parsed successfully. At a compatibility break, decide if this is still + * desired. + */ + while (isdigit(*units) || (*units == '.')) { + units++; + } + + // Skip any additional whitespace after the number + while (isspace(*units)) { + units++; + } + + /* @COMPAT Use exact comparisons. Currently, we match too liberally, and the + * second strncasecmp() in each case is redundant. + */ + if ((*units == '\0') + || (strncasecmp(units, "s", 1) == 0) + || (strncasecmp(units, "sec", 3) == 0)) { + multiplier = 1000; + divisor = 1; + + } else if ((strncasecmp(units, "ms", 2) == 0) + || (strncasecmp(units, "msec", 4) == 0)) { multiplier = 1; divisor = 1; - } else if (!strncasecmp(units, "us", 2) || !strncasecmp(units, "usec", 4)) { + + } else if ((strncasecmp(units, "us", 2) == 0) + || (strncasecmp(units, "usec", 4) == 0)) { multiplier = 1; divisor = 1000; - } else if (!strncasecmp(units, "s", 1) || !strncasecmp(units, "sec", 3)) { - multiplier = 1000; - divisor = 1; - } else if (!strncasecmp(units, "m", 1) || !strncasecmp(units, "min", 3)) { + + } else if ((strncasecmp(units, "m", 1) == 0) + || (strncasecmp(units, "min", 3) == 0)) { multiplier = 60 * 1000; divisor = 1; - } else if (!strncasecmp(units, "h", 1) || !strncasecmp(units, "hr", 2)) { + + } else if ((strncasecmp(units, "h", 1) == 0) + || (strncasecmp(units, "hr", 2) == 0)) { multiplier = 60 * 60 * 1000; divisor = 1; - } else if ((*units != '\0') && (*units != '\n') && (*units != '\r')) { + + } else { + // Invalid units return PCMK__PARSE_INT_DEFAULT; } - scan_ll(num_start, &msec, PCMK__PARSE_INT_DEFAULT, &end_text); + // Apply units, capping at LLONG_MAX if (msec > (LLONG_MAX / multiplier)) { - // Arithmetics overflow while multiplier/divisor mutually exclusive return LLONG_MAX; } - msec *= multiplier; - msec /= divisor; - return msec; + return (msec * multiplier) / divisor; +} + +/*! + * \brief Parse milliseconds from a Pacemaker interval specification + * + * \param[in] input Pacemaker time interval specification (a bare number + * of seconds; a number with a unit, optionally with + * whitespace before and/or after the number; or an ISO + * 8601 duration) + * \param[out] result_ms Where to store milliseconds equivalent of \p input on + * success (limited to the range of an unsigned integer), + * or 0 if \p input is \c NULL or invalid + * + * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok if + * \p input is valid or \c NULL, and \c EINVAL otherwise) + */ +int +pcmk_parse_interval_spec(const char *input, guint *result_ms) +{ + long long msec = PCMK__PARSE_INT_DEFAULT; + int rc = pcmk_rc_ok; + + if (input == NULL) { + msec = 0; + goto done; + } + + if (input[0] == 'P') { + crm_time_t *period_s = crm_time_parse_duration(input); + + if (period_s != NULL) { + msec = 1000 * crm_time_get_seconds(period_s); + crm_time_free(period_s); + } + + } else { + msec = crm_get_msec(input); + } + + if (msec == PCMK__PARSE_INT_DEFAULT) { + crm_warn("Using 0 instead of invalid interval specification '%s'", + input); + msec = 0; + rc = EINVAL; + } + +done: + if (result_ms != NULL) { + *result_ms = (msec >= G_MAXUINT)? G_MAXUINT : (guint) msec; + } + return rc; } gboolean @@ -425,17 +497,20 @@ crm_str_to_boolean(const char *s, int *ret) { if (s == NULL) { return -1; + } - } else if (strcasecmp(s, "true") == 0 - || strcasecmp(s, "on") == 0 - || strcasecmp(s, "yes") == 0 || strcasecmp(s, "y") == 0 || strcasecmp(s, "1") == 0) { - *ret = TRUE; + if (pcmk__strcase_any_of(s, PCMK_VALUE_TRUE, "on", "yes", "y", "1", NULL)) { + if (ret != NULL) { + *ret = TRUE; + } return 1; + } - } else if (strcasecmp(s, "false") == 0 - || strcasecmp(s, "off") == 0 - || strcasecmp(s, "no") == 0 || strcasecmp(s, "n") == 0 || strcasecmp(s, "0") == 0) { - *ret = FALSE; + if (pcmk__strcase_any_of(s, PCMK_VALUE_FALSE, "off", "no", "n", "0", + NULL)) { + if (ret != NULL) { + *ret = FALSE; + } return 1; } return -1; @@ -612,6 +687,24 @@ pcmk__strkey_table(GDestroyNotify key_destroy_func, key_destroy_func, value_destroy_func); } +/*! + * \internal + * \brief Insert string copies into a hash table as key and value + * + * \param[in,out] table Hash table to add to + * \param[in] name String to add a copy of as key + * \param[in] value String to add a copy of as value + * + * \note This asserts on invalid arguments or memory allocation failure. + */ +void +pcmk__insert_dup(GHashTable *table, const char *name, const char *value) +{ + CRM_ASSERT((table != NULL) && (name != NULL)); + + g_hash_table_insert(table, pcmk__str_copy(name), pcmk__str_copy(value)); +} + /* used with hash tables where case does not matter */ static gboolean pcmk__strcase_equal(gconstpointer a, gconstpointer b) @@ -654,7 +747,8 @@ static void copy_str_table_entry(gpointer key, gpointer value, gpointer user_data) { if (key && value && user_data) { - g_hash_table_insert((GHashTable*)user_data, strdup(key), strdup(value)); + pcmk__insert_dup((GHashTable *) user_data, + (const char *) key, (const char *) value); } } @@ -759,8 +853,7 @@ pcmk__compress(const char *data, unsigned int length, unsigned int max, clock_gettime(CLOCK_MONOTONIC, &before_t); #endif - compressed = calloc((size_t) max, sizeof(char)); - CRM_ASSERT(compressed); + compressed = pcmk__assert_alloc((size_t) max, sizeof(char)); *result_len = max; rc = BZ2_bzBuffToBuffCompress(compressed, result_len, uncompressed, length, @@ -967,44 +1060,6 @@ pcmk__str_any_of(const char *s, ...) /*! * \internal - * \brief Check whether a character is in any of a list of strings - * - * \param[in] ch Character (ASCII) to search for - * \param[in] ... Strings to search. Final argument must be - * \c NULL. - * - * \return \c true if any of \p ... contain \p ch, \c false otherwise - * \note \p ... must contain at least one argument (\c NULL). - */ -bool -pcmk__char_in_any_str(int ch, ...) -{ - bool rc = false; - va_list ap; - - /* - * Passing a char to va_start() can generate compiler warnings, - * so ch is declared as an int. - */ - va_start(ap, ch); - - while (1) { - const char *ele = va_arg(ap, const char *); - - if (ele == NULL) { - break; - } else if (strchr(ele, ch) != NULL) { - rc = true; - break; - } - } - - va_end(ap); - return rc; -} - -/*! - * \internal * \brief Sort strings, with numeric portions sorted numerically * * Sort two strings case-insensitively like strcasecmp(), but with any numeric @@ -1178,6 +1233,35 @@ pcmk__strcmp(const char *s1, const char *s2, uint32_t flags) /*! * \internal + * \brief Copy a string, asserting on failure + * + * \param[in] file File where \p function is located + * \param[in] function Calling function + * \param[in] line Line within \p file + * \param[in] str String to copy (can be \c NULL) + * + * \return Newly allocated copy of \p str, or \c NULL if \p str is \c NULL + * + * \note The caller is responsible for freeing the return value using \c free(). + */ +char * +pcmk__str_copy_as(const char *file, const char *function, uint32_t line, + const char *str) +{ + if (str != NULL) { + char *result = strdup(str); + + if (result == NULL) { + crm_abort(file, function, line, "Out of memory", FALSE, TRUE); + crm_exit(CRM_EX_OSERR); + } + return result; + } + return NULL; +} + +/*! + * \internal * \brief Update a dynamically allocated string with a new value * * Given a dynamically allocated string and a new value for it, if the string @@ -1194,12 +1278,7 @@ pcmk__str_update(char **str, const char *value) { if ((str != NULL) && !pcmk__str_eq(*str, value, pcmk__str_none)) { free(*str); - if (value == NULL) { - *str = NULL; - } else { - *str = strdup(value); - CRM_ASSERT(*str != NULL); - } + *str = pcmk__str_copy(value); } } diff --git a/lib/common/tests/Makefile.am b/lib/common/tests/Makefile.am index c0407e5..bde6605 100644 --- a/lib/common/tests/Makefile.am +++ b/lib/common/tests/Makefile.am @@ -1,5 +1,5 @@ # -# Copyright 2020-2023 the Pacemaker project contributors +# Copyright 2020-2024 the Pacemaker project contributors # # The version control history for this file may have further details. # @@ -17,10 +17,16 @@ SUBDIRS = \ io \ iso8601 \ lists \ + nodes \ nvpair \ options \ output \ + probes \ + resources \ results \ + rules \ + scheduler \ + schemas \ scores \ strings \ utils \ diff --git a/lib/common/tests/acl/xml_acl_denied_test.c b/lib/common/tests/acl/xml_acl_denied_test.c index faf2a39..7c5457e 100644 --- a/lib/common/tests/acl/xml_acl_denied_test.c +++ b/lib/common/tests/acl/xml_acl_denied_test.c @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the Pacemaker project contributors + * Copyright 2020-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -17,7 +17,7 @@ static void is_xml_acl_denied_without_node(void **state) { - xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + xmlNode *test_xml = pcmk__xe_create(NULL, "test_xml"); assert_false(xml_acl_denied(test_xml)); test_xml->doc->_private = NULL; @@ -35,10 +35,10 @@ is_xml_acl_denied_with_node(void **state) { xml_doc_private_t *docpriv; - xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + xmlNode *test_xml = pcmk__xe_create(NULL, "test_xml"); // allocate memory for _private, which is NULL by default - test_xml->doc->_private = calloc(1, sizeof(xml_doc_private_t)); + test_xml->doc->_private = pcmk__assert_alloc(1, sizeof(xml_doc_private_t)); assert_false(xml_acl_denied(test_xml)); @@ -56,6 +56,6 @@ is_xml_acl_denied_with_node(void **state) assert_true(xml_acl_denied(test_xml)); } -PCMK__UNIT_TEST(NULL, NULL, +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL, cmocka_unit_test(is_xml_acl_denied_without_node), cmocka_unit_test(is_xml_acl_denied_with_node)) diff --git a/lib/common/tests/acl/xml_acl_enabled_test.c b/lib/common/tests/acl/xml_acl_enabled_test.c index 28665f4..97e361e 100644 --- a/lib/common/tests/acl/xml_acl_enabled_test.c +++ b/lib/common/tests/acl/xml_acl_enabled_test.c @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the Pacemaker project contributors + * Copyright 2020-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -17,7 +17,7 @@ static void is_xml_acl_enabled_without_node(void **state) { - xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + xmlNode *test_xml = pcmk__xe_create(NULL, "test_xml"); assert_false(xml_acl_enabled(test_xml)); test_xml->doc->_private = NULL; @@ -35,10 +35,10 @@ is_xml_acl_enabled_with_node(void **state) { xml_doc_private_t *docpriv; - xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + xmlNode *test_xml = pcmk__xe_create(NULL, "test_xml"); // allocate memory for _private, which is NULL by default - test_xml->doc->_private = calloc(1, sizeof(xml_doc_private_t)); + test_xml->doc->_private = pcmk__assert_alloc(1, sizeof(xml_doc_private_t)); assert_false(xml_acl_enabled(test_xml)); @@ -56,6 +56,6 @@ is_xml_acl_enabled_with_node(void **state) assert_true(xml_acl_enabled(test_xml)); } -PCMK__UNIT_TEST(NULL, NULL, +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL, cmocka_unit_test(is_xml_acl_enabled_without_node), cmocka_unit_test(is_xml_acl_enabled_with_node)) diff --git a/lib/common/tests/actions/Makefile.am b/lib/common/tests/actions/Makefile.am index 6890b84..0dfe521 100644 --- a/lib/common/tests/actions/Makefile.am +++ b/lib/common/tests/actions/Makefile.am @@ -1,5 +1,5 @@ # -# Copyright 2020-2023 the Pacemaker project contributors +# Copyright 2020-2024 the Pacemaker project contributors # # The version control history for this file may have further details. # @@ -11,12 +11,6 @@ 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 = copy_in_properties_test \ - expand_plus_plus_test \ - fix_plus_plus_recursive_test \ - parse_op_key_test \ - pcmk_is_probe_test \ - pcmk_xe_is_probe_test \ - pcmk_xe_mask_probe_failure_test +check_PROGRAMS = parse_op_key_test TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/actions/copy_in_properties_test.c b/lib/common/tests/actions/copy_in_properties_test.c deleted file mode 100644 index 7882551..0000000 --- a/lib/common/tests/actions/copy_in_properties_test.c +++ /dev/null @@ -1,62 +0,0 @@ - /* - * Copyright 2022 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 <crm_internal.h> - -#include <crm/common/unittest_internal.h> - -#include <glib.h> - -static void -target_is_NULL(void **state) -{ - xmlNode *test_xml_1 = create_xml_node(NULL, "test_xml_1"); - xmlNode *test_xml_2 = NULL; - - pcmk__xe_set_props(test_xml_1, "test_prop", "test_value", NULL); - - copy_in_properties(test_xml_2, test_xml_1); - - assert_ptr_equal(test_xml_2, NULL); -} - -static void -src_is_NULL(void **state) -{ - xmlNode *test_xml_1 = NULL; - xmlNode *test_xml_2 = create_xml_node(NULL, "test_xml_2"); - - copy_in_properties(test_xml_2, test_xml_1); - - assert_ptr_equal(test_xml_2->properties, NULL); -} - -static void -copying_is_successful(void **state) -{ - const char *xml_1_value; - const char *xml_2_value; - - xmlNode *test_xml_1 = create_xml_node(NULL, "test_xml_1"); - xmlNode *test_xml_2 = create_xml_node(NULL, "test_xml_2"); - - pcmk__xe_set_props(test_xml_1, "test_prop", "test_value", NULL); - - copy_in_properties(test_xml_2, test_xml_1); - - xml_1_value = crm_element_value(test_xml_1, "test_prop"); - xml_2_value = crm_element_value(test_xml_2, "test_prop"); - - assert_string_equal(xml_1_value, xml_2_value); -} - -PCMK__UNIT_TEST(NULL, NULL, - cmocka_unit_test(target_is_NULL), - cmocka_unit_test(src_is_NULL), - cmocka_unit_test(copying_is_successful)) diff --git a/lib/common/tests/actions/expand_plus_plus_test.c b/lib/common/tests/actions/expand_plus_plus_test.c deleted file mode 100644 index 41471f9..0000000 --- a/lib/common/tests/actions/expand_plus_plus_test.c +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright 2022 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 <crm_internal.h> - -#include <crm/common/unittest_internal.h> - -#include <glib.h> - -static void -value_is_name_plus_plus(void **state) -{ - const char *new_value; - xmlNode *test_xml = create_xml_node(NULL, "test_xml"); - crm_xml_add(test_xml, "X", "5"); - expand_plus_plus(test_xml, "X", "X++"); - new_value = crm_element_value(test_xml, "X"); - assert_string_equal(new_value, "6"); -} - -static void -value_is_name_plus_equals_integer(void **state) -{ - const char *new_value; - xmlNode *test_xml = create_xml_node(NULL, "test_xml"); - crm_xml_add(test_xml, "X", "5"); - expand_plus_plus(test_xml, "X", "X+=2"); - new_value = crm_element_value(test_xml, "X"); - assert_string_equal(new_value, "7"); -} - -// NULL input - -static void -target_is_NULL(void **state) -{ - - const char *new_value; - xmlNode *test_xml = create_xml_node(NULL, "test_xml"); - crm_xml_add(test_xml, "X", "5"); - expand_plus_plus(NULL, "X", "X++"); - new_value = crm_element_value(test_xml, "X"); - assert_string_equal(new_value, "5"); -} - -static void -name_is_NULL(void **state) -{ - const char *new_value; - xmlNode *test_xml = create_xml_node(NULL, "test_xml"); - crm_xml_add(test_xml, "X", "5"); - expand_plus_plus(test_xml, NULL, "X++"); - new_value = crm_element_value(test_xml, "X"); - assert_string_equal(new_value, "5"); -} - -static void -value_is_NULL(void **state) -{ - const char *new_value; - xmlNode *test_xml = create_xml_node(NULL, "test_xml"); - crm_xml_add(test_xml, "X", "5"); - expand_plus_plus(test_xml, "X", NULL); - new_value = crm_element_value(test_xml, "X"); - assert_string_equal(new_value, "5"); -} - -// the value input doesn't start with the name input - -static void -value_is_wrong_name(void **state) -{ - const char *new_value; - xmlNode *test_xml = create_xml_node(NULL, "test_xml"); - crm_xml_add(test_xml, "X", "5"); - expand_plus_plus(test_xml, "X", "Y++"); - new_value = crm_element_value(test_xml, "X"); - assert_string_equal(new_value, "Y++"); -} - -static void -value_is_only_an_integer(void **state) -{ - const char *new_value; - xmlNode *test_xml = create_xml_node(NULL, "test_xml"); - crm_xml_add(test_xml, "X", "5"); - expand_plus_plus(test_xml, "X", "2"); - new_value = crm_element_value(test_xml, "X"); - assert_string_equal(new_value, "2"); -} - -// non-integers - -static void -variable_is_initialized_to_be_NULL(void **state) -{ - const char *new_value; - xmlNode *test_xml = create_xml_node(NULL, "test_xml"); - crm_xml_add(test_xml, "X", NULL); - expand_plus_plus(test_xml, "X", "X++"); - new_value = crm_element_value(test_xml, "X"); - assert_string_equal(new_value, "X++"); -} - -static void -variable_is_initialized_to_be_non_numeric(void **state) -{ - const char *new_value; - xmlNode *test_xml = create_xml_node(NULL, "test_xml"); - crm_xml_add(test_xml, "X", "hello"); - expand_plus_plus(test_xml, "X", "X++"); - new_value = crm_element_value(test_xml, "X"); - assert_string_equal(new_value, "1"); -} - -static void -variable_is_initialized_to_be_non_numeric_2(void **state) -{ - const char *new_value; - xmlNode *test_xml = create_xml_node(NULL, "test_xml"); - crm_xml_add(test_xml, "X", "hello"); - expand_plus_plus(test_xml, "X", "X+=2"); - new_value = crm_element_value(test_xml, "X"); - assert_string_equal(new_value, "2"); -} - -static void -variable_is_initialized_to_be_numeric_and_decimal_point_containing(void **state) -{ - const char *new_value; - xmlNode *test_xml = create_xml_node(NULL, "test_xml"); - crm_xml_add(test_xml, "X", "5.01"); - expand_plus_plus(test_xml, "X", "X++"); - new_value = crm_element_value(test_xml, "X"); - assert_string_equal(new_value, "6"); -} - -static void -variable_is_initialized_to_be_numeric_and_decimal_point_containing_2(void **state) -{ - const char *new_value; - xmlNode *test_xml = create_xml_node(NULL, "test_xml"); - crm_xml_add(test_xml, "X", "5.50"); - expand_plus_plus(test_xml, "X", "X++"); - new_value = crm_element_value(test_xml, "X"); - assert_string_equal(new_value, "6"); -} - -static void -variable_is_initialized_to_be_numeric_and_decimal_point_containing_3(void **state) -{ - const char *new_value; - xmlNode *test_xml = create_xml_node(NULL, "test_xml"); - crm_xml_add(test_xml, "X", "5.99"); - expand_plus_plus(test_xml, "X", "X++"); - new_value = crm_element_value(test_xml, "X"); - assert_string_equal(new_value, "6"); -} - -static void -value_is_non_numeric(void **state) -{ - const char *new_value; - xmlNode *test_xml = create_xml_node(NULL, "test_xml"); - crm_xml_add(test_xml, "X", "5"); - expand_plus_plus(test_xml, "X", "X+=hello"); - new_value = crm_element_value(test_xml, "X"); - assert_string_equal(new_value, "5"); -} - -static void -value_is_numeric_and_decimal_point_containing(void **state) -{ - const char *new_value; - xmlNode *test_xml = create_xml_node(NULL, "test_xml"); - crm_xml_add(test_xml, "X", "5"); - expand_plus_plus(test_xml, "X", "X+=2.01"); - new_value = crm_element_value(test_xml, "X"); - assert_string_equal(new_value, "7"); -} - -static void -value_is_numeric_and_decimal_point_containing_2(void **state) -{ - const char *new_value; - xmlNode *test_xml = create_xml_node(NULL, "test_xml"); - crm_xml_add(test_xml, "X", "5"); - expand_plus_plus(test_xml, "X", "X+=1.50"); - new_value = crm_element_value(test_xml, "X"); - assert_string_equal(new_value, "6"); -} - -static void -value_is_numeric_and_decimal_point_containing_3(void **state) -{ - const char *new_value; - xmlNode *test_xml = create_xml_node(NULL, "test_xml"); - crm_xml_add(test_xml, "X", "5"); - expand_plus_plus(test_xml, "X", "X+=1.99"); - new_value = crm_element_value(test_xml, "X"); - assert_string_equal(new_value, "6"); -} - -// undefined input - -static void -name_is_undefined(void **state) -{ - const char *new_value; - xmlNode *test_xml = create_xml_node(NULL, "test_xml"); - crm_xml_add(test_xml, "Y", "5"); - expand_plus_plus(test_xml, "X", "X++"); - new_value = crm_element_value(test_xml, "X"); - assert_string_equal(new_value, "X++"); -} - -// large input - -static void -assignment_result_is_too_large(void **state) -{ - const char *new_value; - xmlNode *test_xml = create_xml_node(NULL, "test_xml"); - crm_xml_add(test_xml, "X", "5"); - expand_plus_plus(test_xml, "X", "X+=100000000000"); - new_value = crm_element_value(test_xml, "X"); - printf("assignment result is too large %s\n", new_value); - assert_string_equal(new_value, "1000000"); -} - -PCMK__UNIT_TEST(NULL, NULL, - cmocka_unit_test(value_is_name_plus_plus), - cmocka_unit_test(value_is_name_plus_equals_integer), - cmocka_unit_test(target_is_NULL), - cmocka_unit_test(name_is_NULL), - cmocka_unit_test(value_is_NULL), - cmocka_unit_test(value_is_wrong_name), - cmocka_unit_test(value_is_only_an_integer), - cmocka_unit_test(variable_is_initialized_to_be_NULL), - cmocka_unit_test(variable_is_initialized_to_be_non_numeric), - cmocka_unit_test(variable_is_initialized_to_be_non_numeric_2), - cmocka_unit_test(variable_is_initialized_to_be_numeric_and_decimal_point_containing), - cmocka_unit_test(variable_is_initialized_to_be_numeric_and_decimal_point_containing_2), - cmocka_unit_test(variable_is_initialized_to_be_numeric_and_decimal_point_containing_3), - cmocka_unit_test(value_is_non_numeric), - cmocka_unit_test(value_is_numeric_and_decimal_point_containing), - cmocka_unit_test(value_is_numeric_and_decimal_point_containing_2), - cmocka_unit_test(value_is_numeric_and_decimal_point_containing_3), - cmocka_unit_test(name_is_undefined), - cmocka_unit_test(assignment_result_is_too_large)) diff --git a/lib/common/tests/actions/fix_plus_plus_recursive_test.c b/lib/common/tests/actions/fix_plus_plus_recursive_test.c deleted file mode 100644 index b3c7cc2..0000000 --- a/lib/common/tests/actions/fix_plus_plus_recursive_test.c +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2022 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 <crm_internal.h> - -#include <crm/common/unittest_internal.h> - -#include <glib.h> - -static void -element_nodes(void **state) -{ - const char *new_value_root; - const char *new_value_child; - const char *new_value_grandchild; - - xmlNode *test_xml_root = create_xml_node(NULL, "test_xml_root"); - xmlNode *test_xml_child = create_xml_node(test_xml_root, "test_xml_child"); - xmlNode *test_xml_grandchild = create_xml_node(test_xml_child, "test_xml_grandchild"); - xmlNode *test_xml_text = pcmk_create_xml_text_node(test_xml_root, "text_xml_text", "content"); - xmlNode *test_xml_comment = string2xml("<!-- a comment -->"); - - crm_xml_add(test_xml_root, "X", "5"); - crm_xml_add(test_xml_child, "X", "X++"); - crm_xml_add(test_xml_grandchild, "X", "X+=2"); - crm_xml_add(test_xml_text, "X", "X++"); - - fix_plus_plus_recursive(test_xml_root); - fix_plus_plus_recursive(test_xml_comment); - - new_value_root = crm_element_value(test_xml_root, "X"); - new_value_child = crm_element_value(test_xml_child, "X"); - new_value_grandchild = crm_element_value(test_xml_grandchild, "X"); - - assert_string_equal(new_value_root, "5"); - assert_string_equal(new_value_child, "1"); - assert_string_equal(new_value_grandchild, "2"); -} - -PCMK__UNIT_TEST(NULL, NULL, - cmocka_unit_test(element_nodes)) diff --git a/lib/common/tests/actions/pcmk_xe_is_probe_test.c b/lib/common/tests/actions/pcmk_xe_is_probe_test.c deleted file mode 100644 index 62b21d9..0000000 --- a/lib/common/tests/actions/pcmk_xe_is_probe_test.c +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2021 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 <crm_internal.h> - -#include <crm/common/unittest_internal.h> - -static void -op_is_probe_test(void **state) -{ - xmlNode *node = NULL; - - assert_false(pcmk_xe_is_probe(NULL)); - - node = string2xml("<lrm_rsc_op/>"); - assert_false(pcmk_xe_is_probe(node)); - free_xml(node); - - node = string2xml("<lrm_rsc_op operation_key=\"blah\" interval=\"30s\"/>"); - assert_false(pcmk_xe_is_probe(node)); - free_xml(node); - - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"30s\"/>"); - assert_false(pcmk_xe_is_probe(node)); - free_xml(node); - - node = string2xml("<lrm_rsc_op operation=\"start\" interval=\"0\"/>"); - assert_false(pcmk_xe_is_probe(node)); - free_xml(node); - - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\"/>"); - assert_true(pcmk_xe_is_probe(node)); - free_xml(node); -} - -PCMK__UNIT_TEST(NULL, NULL, - cmocka_unit_test(op_is_probe_test)) diff --git a/lib/common/tests/actions/pcmk_xe_mask_probe_failure_test.c b/lib/common/tests/actions/pcmk_xe_mask_probe_failure_test.c deleted file mode 100644 index 9e38019..0000000 --- a/lib/common/tests/actions/pcmk_xe_mask_probe_failure_test.c +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2021 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 <crm_internal.h> - -#include <crm/common/unittest_internal.h> - -static void -op_is_not_probe_test(void **state) { - xmlNode *node = NULL; - - /* Not worth testing this thoroughly since it's just a duplicate of whether - * pcmk_op_is_probe works or not. - */ - - node = string2xml("<lrm_rsc_op operation=\"start\" interval=\"0\"/>"); - assert_false(pcmk_xe_mask_probe_failure(node)); - free_xml(node); -} - -static void -op_does_not_have_right_values_test(void **state) { - xmlNode *node = NULL; - - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\"/>"); - assert_false(pcmk_xe_mask_probe_failure(node)); - free_xml(node); - - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"0\" op-status=\"\"/>"); - assert_false(pcmk_xe_mask_probe_failure(node)); - free_xml(node); -} - -static void -check_values_test(void **state) { - xmlNode *node = NULL; - - /* PCMK_EXEC_NOT_SUPPORTED */ - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"0\" op-status=\"3\"/>"); - assert_false(pcmk_xe_mask_probe_failure(node)); - free_xml(node); - - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"5\" op-status=\"3\"/>"); - assert_true(pcmk_xe_mask_probe_failure(node)); - free_xml(node); - - /* PCMK_EXEC_DONE */ - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"0\" op-status=\"0\"/>"); - assert_false(pcmk_xe_mask_probe_failure(node)); - free_xml(node); - - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"2\" op-status=\"0\"/>"); - assert_true(pcmk_xe_mask_probe_failure(node)); - free_xml(node); - - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"5\" op-status=\"0\"/>"); - assert_true(pcmk_xe_mask_probe_failure(node)); - free_xml(node); - - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"6\" op-status=\"0\"/>"); - assert_false(pcmk_xe_mask_probe_failure(node)); - free_xml(node); - - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"7\" op-status=\"0\"/>"); - assert_false(pcmk_xe_mask_probe_failure(node)); - free_xml(node); - - /* PCMK_EXEC_NOT_INSTALLED */ - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"0\" op-status=\"7\"/>"); - assert_true(pcmk_xe_mask_probe_failure(node)); - free_xml(node); - - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"5\" op-status=\"7\"/>"); - assert_true(pcmk_xe_mask_probe_failure(node)); - free_xml(node); - - /* PCMK_EXEC_ERROR */ - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"0\" op-status=\"4\"/>"); - assert_false(pcmk_xe_mask_probe_failure(node)); - free_xml(node); - - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"2\" op-status=\"4\"/>"); - assert_true(pcmk_xe_mask_probe_failure(node)); - free_xml(node); - - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"5\" op-status=\"4\"/>"); - assert_true(pcmk_xe_mask_probe_failure(node)); - free_xml(node); - - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"6\" op-status=\"4\"/>"); - assert_false(pcmk_xe_mask_probe_failure(node)); - free_xml(node); - - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"7\" op-status=\"4\"/>"); - assert_false(pcmk_xe_mask_probe_failure(node)); - free_xml(node); - - /* PCMK_EXEC_ERROR_HARD */ - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"0\" op-status=\"5\"/>"); - assert_false(pcmk_xe_mask_probe_failure(node)); - free_xml(node); - - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"2\" op-status=\"5\"/>"); - assert_true(pcmk_xe_mask_probe_failure(node)); - free_xml(node); - - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"5\" op-status=\"5\"/>"); - assert_true(pcmk_xe_mask_probe_failure(node)); - free_xml(node); - - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"6\" op-status=\"5\"/>"); - assert_false(pcmk_xe_mask_probe_failure(node)); - free_xml(node); - - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"7\" op-status=\"5\"/>"); - assert_false(pcmk_xe_mask_probe_failure(node)); - free_xml(node); - - /* PCMK_EXEC_ERROR_FATAL */ - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"0\" op-status=\"6\"/>"); - assert_false(pcmk_xe_mask_probe_failure(node)); - free_xml(node); - - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"2\" op-status=\"6\"/>"); - assert_true(pcmk_xe_mask_probe_failure(node)); - free_xml(node); - - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"5\" op-status=\"6\"/>"); - assert_true(pcmk_xe_mask_probe_failure(node)); - free_xml(node); - - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"6\" op-status=\"6\"/>"); - assert_false(pcmk_xe_mask_probe_failure(node)); - free_xml(node); - - node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"7\" op-status=\"6\"/>"); - assert_false(pcmk_xe_mask_probe_failure(node)); - free_xml(node); -} - -PCMK__UNIT_TEST(NULL, NULL, - cmocka_unit_test(op_is_not_probe_test), - cmocka_unit_test(op_does_not_have_right_values_test), - cmocka_unit_test(check_values_test)) diff --git a/lib/common/tests/health/pcmk__parse_health_strategy_test.c b/lib/common/tests/health/pcmk__parse_health_strategy_test.c index 28cc702..197ad1f 100644 --- a/lib/common/tests/health/pcmk__parse_health_strategy_test.c +++ b/lib/common/tests/health/pcmk__parse_health_strategy_test.c @@ -16,7 +16,7 @@ valid(void **state) { assert_int_equal(pcmk__parse_health_strategy(NULL), pcmk__health_strategy_none); - assert_int_equal(pcmk__parse_health_strategy("none"), + assert_int_equal(pcmk__parse_health_strategy(PCMK_VALUE_NONE), pcmk__health_strategy_none); assert_int_equal(pcmk__parse_health_strategy("NONE"), diff --git a/lib/common/tests/health/pcmk__validate_health_strategy_test.c b/lib/common/tests/health/pcmk__validate_health_strategy_test.c index c7c60aa..e706185 100644 --- a/lib/common/tests/health/pcmk__validate_health_strategy_test.c +++ b/lib/common/tests/health/pcmk__validate_health_strategy_test.c @@ -15,7 +15,7 @@ static void valid_strategy(void **state) { - assert_true(pcmk__validate_health_strategy("none")); + assert_true(pcmk__validate_health_strategy(PCMK_VALUE_NONE)); assert_true(pcmk__validate_health_strategy("None")); assert_true(pcmk__validate_health_strategy("NONE")); assert_true(pcmk__validate_health_strategy("NoNe")); diff --git a/lib/common/tests/io/pcmk__full_path_test.c b/lib/common/tests/io/pcmk__full_path_test.c index dbbd71b..2f514aa 100644 --- a/lib/common/tests/io/pcmk__full_path_test.c +++ b/lib/common/tests/io/pcmk__full_path_test.c @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the Pacemaker project contributors + * Copyright 2020-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -18,8 +18,13 @@ function_asserts(void **state) { pcmk__assert_asserts(pcmk__full_path(NULL, "/dir")); pcmk__assert_asserts(pcmk__full_path("file", NULL)); +} - pcmk__assert_asserts( +static void +function_exits(void **state) +{ + pcmk__assert_exits( + CRM_EX_OSERR, { pcmk__mock_strdup = true; // strdup() will return NULL expect_string(__wrap_strdup, s, "/full/path"); @@ -49,4 +54,5 @@ full_path(void **state) PCMK__UNIT_TEST(NULL, NULL, cmocka_unit_test(function_asserts), + cmocka_unit_test(function_exits), cmocka_unit_test(full_path)) diff --git a/lib/common/tests/iso8601/Makefile.am b/lib/common/tests/iso8601/Makefile.am index 5187aec..581115a 100644 --- a/lib/common/tests/iso8601/Makefile.am +++ b/lib/common/tests/iso8601/Makefile.am @@ -1,5 +1,5 @@ # -# Copyright 2020-2022 the Pacemaker project contributors +# Copyright 2020-2024 the Pacemaker project contributors # # The version control history for this file may have further details. # @@ -11,6 +11,8 @@ 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__readable_interval_test +check_PROGRAMS = pcmk__add_time_from_xml_test \ + pcmk__readable_interval_test \ + pcmk__set_time_if_earlier_test TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/iso8601/pcmk__add_time_from_xml_test.c b/lib/common/tests/iso8601/pcmk__add_time_from_xml_test.c new file mode 100644 index 0000000..60a71c0 --- /dev/null +++ b/lib/common/tests/iso8601/pcmk__add_time_from_xml_test.c @@ -0,0 +1,243 @@ +/* + * 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 <crm_internal.h> + +#include <libxml/tree.h> // xmlNode + +#include <crm/common/unittest_internal.h> + +#include <crm/common/iso8601.h> +#include <crm/common/iso8601_internal.h> +#include <crm/common/xml.h> +#include "../../crmcommon_private.h" + +#define ALL_VALID "<duration id=\"duration1\" years=\"1\" months=\"2\" " \ + "weeks=\"3\" days=\"-1\" hours=\"1\" minutes=\"1\" " \ + "seconds=\"1\" />" + +#define YEARS_INVALID "<duration id=\"duration1\" years=\"not-a-number\" />" + +#define YEARS_TOO_BIG "<duration id=\"duration1\" years=\"2222222222\" />" + +#define YEARS_TOO_SMALL "<duration id=\"duration1\" years=\"-2222222222\" />" + +static void +null_time_invalid(void **state) +{ + xmlNode *xml = pcmk__xml_parse(ALL_VALID); + + assert_int_equal(pcmk__add_time_from_xml(NULL, pcmk__time_years, xml), + EINVAL); + free_xml(xml); +} + +static void +null_xml_ok(void **state) +{ + crm_time_t *t = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *reference = pcmk_copy_time(t); + + assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_years, NULL), + pcmk_rc_ok); + assert_int_equal(crm_time_compare(t, reference), 0); + + crm_time_free(t); + crm_time_free(reference); +} + +static void +invalid_component(void **state) +{ + xmlNode *xml = pcmk__xml_parse(ALL_VALID); + + assert_int_equal(pcmk__add_time_from_xml(NULL, pcmk__time_unknown, xml), + EINVAL); + free_xml(xml); +} + +static void +missing_attr(void **state) +{ + crm_time_t *t = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *reference = pcmk_copy_time(t); + xmlNode *xml = pcmk__xml_parse(YEARS_INVALID); + + assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_months, xml), + pcmk_rc_ok); + assert_int_equal(crm_time_compare(t, reference), 0); + + crm_time_free(t); + crm_time_free(reference); + free_xml(xml); +} + +static void +invalid_attr(void **state) +{ + crm_time_t *t = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *reference = pcmk_copy_time(t); + xmlNode *xml = pcmk__xml_parse(YEARS_INVALID); + + assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_years, xml), + pcmk_rc_unpack_error); + assert_int_equal(crm_time_compare(t, reference), 0); + + crm_time_free(t); + crm_time_free(reference); + free_xml(xml); +} + +static void +out_of_range_attr(void **state) +{ + crm_time_t *t = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *reference = pcmk_copy_time(t); + xmlNode *xml = NULL; + + xml = pcmk__xml_parse(YEARS_TOO_BIG); + assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_years, xml), ERANGE); + assert_int_equal(crm_time_compare(t, reference), 0); + free_xml(xml); + + xml = pcmk__xml_parse(YEARS_TOO_SMALL); + assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_years, xml), ERANGE); + assert_int_equal(crm_time_compare(t, reference), 0); + free_xml(xml); + + crm_time_free(t); + crm_time_free(reference); +} + +static void +add_years(void **state) +{ + crm_time_t *t = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *reference = crm_time_new("2025-01-01 15:00:00"); + xmlNode *xml = pcmk__xml_parse(ALL_VALID); + + assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_years, xml), + pcmk_rc_ok); + assert_int_equal(crm_time_compare(t, reference), 0); + + crm_time_free(t); + crm_time_free(reference); + free_xml(xml); +} + +static void +add_months(void **state) +{ + crm_time_t *t = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *reference = crm_time_new("2024-03-01 15:00:00"); + xmlNode *xml = pcmk__xml_parse(ALL_VALID); + + assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_months, xml), + pcmk_rc_ok); + assert_int_equal(crm_time_compare(t, reference), 0); + + crm_time_free(t); + crm_time_free(reference); + free_xml(xml); +} + +static void +add_weeks(void **state) +{ + crm_time_t *t = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *reference = crm_time_new("2024-01-22 15:00:00"); + xmlNode *xml = pcmk__xml_parse(ALL_VALID); + + assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_weeks, xml), + pcmk_rc_ok); + assert_int_equal(crm_time_compare(t, reference), 0); + + crm_time_free(t); + crm_time_free(reference); + free_xml(xml); +} + +static void +add_days(void **state) +{ + crm_time_t *t = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *reference = crm_time_new("2023-12-31 15:00:00"); + xmlNode *xml = pcmk__xml_parse(ALL_VALID); + + assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_days, xml), + pcmk_rc_ok); + assert_int_equal(crm_time_compare(t, reference), 0); + + crm_time_free(t); + crm_time_free(reference); + free_xml(xml); +} + +static void +add_hours(void **state) +{ + crm_time_t *t = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *reference = crm_time_new("2024-01-01 16:00:00"); + xmlNode *xml = pcmk__xml_parse(ALL_VALID); + + assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_hours, xml), + pcmk_rc_ok); + assert_int_equal(crm_time_compare(t, reference), 0); + + crm_time_free(t); + crm_time_free(reference); + free_xml(xml); +} + +static void +add_minutes(void **state) +{ + crm_time_t *t = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *reference = crm_time_new("2024-01-01 15:01:00"); + xmlNode *xml = pcmk__xml_parse(ALL_VALID); + + assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_minutes, xml), + pcmk_rc_ok); + assert_int_equal(crm_time_compare(t, reference), 0); + + crm_time_free(t); + crm_time_free(reference); + free_xml(xml); +} + +static void +add_seconds(void **state) +{ + crm_time_t *t = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *reference = crm_time_new("2024-01-01 15:00:01"); + xmlNode *xml = pcmk__xml_parse(ALL_VALID); + + assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_seconds, xml), + pcmk_rc_ok); + assert_int_equal(crm_time_compare(t, reference), 0); + + crm_time_free(t); + crm_time_free(reference); + free_xml(xml); +} + +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL, + cmocka_unit_test(null_time_invalid), + cmocka_unit_test(null_xml_ok), + cmocka_unit_test(invalid_component), + cmocka_unit_test(missing_attr), + cmocka_unit_test(invalid_attr), + cmocka_unit_test(out_of_range_attr), + cmocka_unit_test(add_years), + cmocka_unit_test(add_months), + cmocka_unit_test(add_weeks), + cmocka_unit_test(add_days), + cmocka_unit_test(add_hours), + cmocka_unit_test(add_minutes), + cmocka_unit_test(add_seconds)); diff --git a/lib/common/tests/iso8601/pcmk__readable_interval_test.c b/lib/common/tests/iso8601/pcmk__readable_interval_test.c index 43b5541..d354975 100644 --- a/lib/common/tests/iso8601/pcmk__readable_interval_test.c +++ b/lib/common/tests/iso8601/pcmk__readable_interval_test.c @@ -17,9 +17,11 @@ static void readable_interval(void **state) { assert_string_equal(pcmk__readable_interval(0), "0s"); + assert_string_equal(pcmk__readable_interval(503), "503ms"); + assert_string_equal(pcmk__readable_interval(3333), "3.333s"); assert_string_equal(pcmk__readable_interval(30000), "30s"); + assert_string_equal(pcmk__readable_interval(61000), "1m1s"); assert_string_equal(pcmk__readable_interval(150000), "2m30s"); - assert_string_equal(pcmk__readable_interval(3333), "3.333s"); assert_string_equal(pcmk__readable_interval(UINT_MAX), "49d17h2m47.295s"); } diff --git a/lib/common/tests/iso8601/pcmk__set_time_if_earlier_test.c b/lib/common/tests/iso8601/pcmk__set_time_if_earlier_test.c new file mode 100644 index 0000000..c4bf014 --- /dev/null +++ b/lib/common/tests/iso8601/pcmk__set_time_if_earlier_test.c @@ -0,0 +1,80 @@ +/* + * 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 <crm_internal.h> + +#include <crm/common/unittest_internal.h> + +#include <crm/common/iso8601.h> +#include "../../crmcommon_private.h" + +static void +null_ok(void **state) +{ + crm_time_t *target = crm_time_new("2024-01-01 00:30:00 +01:00"); + crm_time_t *target_copy = pcmk_copy_time(target); + + // Should do nothing (just checking it doesn't assert or crash) + pcmk__set_time_if_earlier(NULL, NULL); + pcmk__set_time_if_earlier(NULL, target); + + // Shouldn't assert, crash, or change target + pcmk__set_time_if_earlier(target, NULL); + assert_int_equal(crm_time_compare(target, target_copy), 0); + + crm_time_free(target); + crm_time_free(target_copy); +} + +static void +target_undefined(void **state) +{ + crm_time_t *source = crm_time_new("2024-01-01 00:29:59 +01:00"); + crm_time_t *target = crm_time_new_undefined(); + + pcmk__set_time_if_earlier(target, source); + assert_int_equal(crm_time_compare(target, source), 0); + + crm_time_free(source); + crm_time_free(target); +} + +static void +source_earlier(void **state) +{ + crm_time_t *source = crm_time_new("2024-01-01 00:29:59 +01:00"); + crm_time_t *target = crm_time_new("2024-01-01 00:30:00 +01:00"); + + pcmk__set_time_if_earlier(target, source); + assert_int_equal(crm_time_compare(target, source), 0); + + crm_time_free(source); + crm_time_free(target); +} + +static void +source_later(void **state) +{ + crm_time_t *source = crm_time_new("2024-01-01 00:31:00 +01:00"); + crm_time_t *target = crm_time_new("2024-01-01 00:30:00 +01:00"); + crm_time_t *target_copy = pcmk_copy_time(target); + + pcmk__set_time_if_earlier(target, source); + assert_int_equal(crm_time_compare(target, target_copy), 0); + + crm_time_free(source); + crm_time_free(target); + crm_time_free(target_copy); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(null_ok), + cmocka_unit_test(target_undefined), + cmocka_unit_test(source_earlier), + cmocka_unit_test(source_later)) diff --git a/lib/common/tests/nodes/Makefile.am b/lib/common/tests/nodes/Makefile.am new file mode 100644 index 0000000..f52c615 --- /dev/null +++ b/lib/common/tests/nodes/Makefile.am @@ -0,0 +1,23 @@ +# +# 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 $(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__find_node_in_list_test \ + pcmk_foreach_active_resource_test \ + pcmk_node_is_clean_test \ + pcmk_node_is_in_maintenance_test \ + pcmk_node_is_online_test \ + pcmk_node_is_pending_test \ + pcmk_node_is_shutting_down_test \ + pcmk__xe_add_node_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/nodes/pcmk__find_node_in_list_test.c b/lib/common/tests/nodes/pcmk__find_node_in_list_test.c new file mode 100644 index 0000000..7726bf5 --- /dev/null +++ b/lib/common/tests/nodes/pcmk__find_node_in_list_test.c @@ -0,0 +1,53 @@ +/* + * 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 General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <crm/common/unittest_internal.h> +#include <crm/pengine/internal.h> + +static void +empty_list(void **state) +{ + assert_null(pcmk__find_node_in_list(NULL, NULL)); + assert_null(pcmk__find_node_in_list(NULL, "cluster1")); +} + +static void +non_null_list(void **state) +{ + GList *nodes = NULL; + + pcmk_node_t *a = pcmk__assert_alloc(1, sizeof(pcmk_node_t)); + pcmk_node_t *b = pcmk__assert_alloc(1, sizeof(pcmk_node_t)); + + a->details = pcmk__assert_alloc(1, sizeof(struct pe_node_shared_s)); + a->details->uname = "cluster1"; + b->details = pcmk__assert_alloc(1, sizeof(struct pe_node_shared_s)); + b->details->uname = "cluster2"; + + nodes = g_list_append(nodes, a); + nodes = g_list_append(nodes, b); + + assert_ptr_equal(a, pcmk__find_node_in_list(nodes, "cluster1")); + assert_null(pcmk__find_node_in_list(nodes, "cluster10")); + assert_null(pcmk__find_node_in_list(nodes, "nodecluster1")); + assert_ptr_equal(b, pcmk__find_node_in_list(nodes, "CLUSTER2")); + assert_null(pcmk__find_node_in_list(nodes, "xyz")); + + free(a->details); + free(a); + free(b->details); + free(b); + g_list_free(nodes); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_list), + cmocka_unit_test(non_null_list)) diff --git a/lib/common/tests/nodes/pcmk__xe_add_node_test.c b/lib/common/tests/nodes/pcmk__xe_add_node_test.c new file mode 100644 index 0000000..dd77527 --- /dev/null +++ b/lib/common/tests/nodes/pcmk__xe_add_node_test.c @@ -0,0 +1,71 @@ +/* + * 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 <crm_internal.h> + +#include <crm/common/xml.h> +#include <crm/common/unittest_internal.h> +#include <crm/common/xml_internal.h> + +static void +bad_input(void **state) { + xmlNode *node = NULL; + + pcmk__assert_asserts(pcmk__xe_add_node(NULL, NULL, 0)); + + node = pcmk__xe_create(NULL, "test"); + + pcmk__xe_add_node(node, NULL, 0); + assert_null(xmlHasProp(node, (pcmkXmlStr) PCMK__XA_ATTR_HOST)); + assert_null(xmlHasProp(node, (pcmkXmlStr) PCMK__XA_ATTR_HOST_ID)); + + pcmk__xe_add_node(node, NULL, -100); + assert_null(xmlHasProp(node, (pcmkXmlStr) PCMK__XA_ATTR_HOST)); + assert_null(xmlHasProp(node, (pcmkXmlStr) PCMK__XA_ATTR_HOST_ID)); + + free_xml(node); +} + +static void +expected_input(void **state) { + xmlNode *node = pcmk__xe_create(NULL, "test"); + int i; + + pcmk__xe_add_node(node, "somenode", 47); + assert_string_equal("somenode", + crm_element_value(node, PCMK__XA_ATTR_HOST)); + assert_int_equal(pcmk_rc_ok, + crm_element_value_int(node, PCMK__XA_ATTR_HOST_ID, &i)); + assert_int_equal(i, 47); + + free_xml(node); +} + +static void +repeated_use(void **state) { + xmlNode *node = pcmk__xe_create(NULL, "test"); + int i; + + /* Later calls override settings from earlier calls. */ + pcmk__xe_add_node(node, "nodeA", 1); + pcmk__xe_add_node(node, "nodeB", 2); + pcmk__xe_add_node(node, "nodeC", 3); + + assert_string_equal("nodeC", crm_element_value(node, PCMK__XA_ATTR_HOST)); + assert_int_equal(pcmk_rc_ok, + crm_element_value_int(node, PCMK__XA_ATTR_HOST_ID, &i)); + assert_int_equal(i, 3); + + free_xml(node); +} + +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL, + cmocka_unit_test(bad_input), + cmocka_unit_test(expected_input), + cmocka_unit_test(repeated_use)) diff --git a/lib/common/tests/nodes/pcmk_foreach_active_resource_test.c b/lib/common/tests/nodes/pcmk_foreach_active_resource_test.c new file mode 100644 index 0000000..2402789 --- /dev/null +++ b/lib/common/tests/nodes/pcmk_foreach_active_resource_test.c @@ -0,0 +1,149 @@ +/* + * 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 <crm_internal.h> + +#include <stdio.h> // NULL +#include <glib.h> // GList, TRUE, FALSE + +#include <crm/common/nodes.h> +#include <crm/common/resources.h> +#include <crm/common/unittest_internal.h> + +static int counter = 1; +static int return_false = -1; + +static char rsc1_id[] = "rsc1"; +static char rsc2_id[] = "rsc2"; +static char rsc3_id[] = "rsc3"; + +static pcmk_resource_t rsc1 = { + .id = rsc1_id, +}; +static pcmk_resource_t rsc2 = { + .id = rsc2_id, +}; +static pcmk_resource_t rsc3 = { + .id = rsc3_id, +}; + +static bool +fn(pcmk_resource_t *rsc, void *user_data) +{ + char *expected_id = crm_strdup_printf("rsc%d", counter); + + assert_string_equal(rsc->id, expected_id); + free(expected_id); + + return counter++ != return_false; +} + +static void +null_args(void **state) +{ + struct pe_node_shared_s shared = { + .running_rsc = NULL, + }; + pcmk_node_t node = { + .details = &shared, + }; + + counter = 1; + + // These just test that it doesn't crash + pcmk_foreach_active_resource(NULL, NULL, NULL); + pcmk_foreach_active_resource(&node, NULL, NULL); + + pcmk_foreach_active_resource(NULL, fn, NULL); + assert_int_equal(counter, 1); +} + +static void +list_of_0(void **state) +{ + struct pe_node_shared_s shared = { + .running_rsc = NULL, + }; + pcmk_node_t node = { + .details = &shared, + }; + + counter = 1; + pcmk_foreach_active_resource(&node, fn, NULL); + assert_int_equal(counter, 1); +} + +static void +list_of_1(void **state) +{ + struct pe_node_shared_s shared = { + .running_rsc = NULL, + }; + pcmk_node_t node = { + .details = &shared, + }; + + shared.running_rsc = g_list_append(shared.running_rsc, &rsc1); + + counter = 1; + pcmk_foreach_active_resource(&node, fn, NULL); + assert_int_equal(counter, 2); + + g_list_free(shared.running_rsc); +} + +static void +list_of_3(void **state) +{ + struct pe_node_shared_s shared = { + .running_rsc = NULL, + }; + pcmk_node_t node = { + .details = &shared, + }; + + shared.running_rsc = g_list_append(shared.running_rsc, &rsc1); + shared.running_rsc = g_list_append(shared.running_rsc, &rsc2); + shared.running_rsc = g_list_append(shared.running_rsc, &rsc3); + + counter = 1; + pcmk_foreach_active_resource(&node, fn, NULL); + assert_int_equal(counter, 4); + + g_list_free(shared.running_rsc); +} + +static void +list_of_3_return_false(void **state) +{ + struct pe_node_shared_s shared = { + .running_rsc = NULL, + }; + pcmk_node_t node = { + .details = &shared, + }; + + shared.running_rsc = g_list_append(shared.running_rsc, &rsc1); + shared.running_rsc = g_list_append(shared.running_rsc, &rsc2); + shared.running_rsc = g_list_append(shared.running_rsc, &rsc3); + + counter = 1; + return_false = 2; + pcmk_foreach_active_resource(&node, fn, NULL); + assert_int_equal(counter, 3); + + g_list_free(shared.running_rsc); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(null_args), + cmocka_unit_test(list_of_0), + cmocka_unit_test(list_of_1), + cmocka_unit_test(list_of_3), + cmocka_unit_test(list_of_3_return_false)) diff --git a/lib/common/tests/nodes/pcmk_node_is_clean_test.c b/lib/common/tests/nodes/pcmk_node_is_clean_test.c new file mode 100644 index 0000000..0534633 --- /dev/null +++ b/lib/common/tests/nodes/pcmk_node_is_clean_test.c @@ -0,0 +1,54 @@ +/* + * 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 <crm_internal.h> + +#include <stdio.h> // NULL +#include <glib.h> // TRUE, FALSE + +#include <crm/common/nodes.h> +#include <crm/common/unittest_internal.h> + +static void +null_is_unclean(void **state) +{ + assert_false(pcmk_node_is_clean(NULL)); +} + +static void +node_is_clean(void **state) +{ + struct pe_node_shared_s shared = { + .unclean = FALSE, + }; + + pcmk_node_t node = { + .details = &shared, + }; + + assert_true(pcmk_node_is_clean(&node)); +} + +static void +node_is_unclean(void **state) +{ + struct pe_node_shared_s shared = { + .unclean = TRUE, + }; + pcmk_node_t node = { + .details = &shared, + }; + + assert_false(pcmk_node_is_clean(&node)); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(null_is_unclean), + cmocka_unit_test(node_is_clean), + cmocka_unit_test(node_is_unclean)) diff --git a/lib/common/tests/nodes/pcmk_node_is_in_maintenance_test.c b/lib/common/tests/nodes/pcmk_node_is_in_maintenance_test.c new file mode 100644 index 0000000..45a3b6f --- /dev/null +++ b/lib/common/tests/nodes/pcmk_node_is_in_maintenance_test.c @@ -0,0 +1,54 @@ +/* + * 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 <crm_internal.h> + +#include <stdio.h> // NULL +#include <glib.h> // TRUE, FALSE + +#include <crm/common/nodes.h> +#include <crm/common/unittest_internal.h> + +static void +null_is_not_in_maintenance(void **state) +{ + assert_false(pcmk_node_is_in_maintenance(NULL)); +} + +static void +node_is_in_maintenance(void **state) +{ + struct pe_node_shared_s shared = { + .maintenance = TRUE, + }; + + pcmk_node_t node = { + .details = &shared, + }; + + assert_true(pcmk_node_is_in_maintenance(&node)); +} + +static void +node_is_not_in_maintenance(void **state) +{ + struct pe_node_shared_s shared = { + .maintenance = FALSE, + }; + pcmk_node_t node = { + .details = &shared, + }; + + assert_false(pcmk_node_is_in_maintenance(&node)); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(null_is_not_in_maintenance), + cmocka_unit_test(node_is_in_maintenance), + cmocka_unit_test(node_is_not_in_maintenance)) diff --git a/lib/common/tests/nodes/pcmk_node_is_online_test.c b/lib/common/tests/nodes/pcmk_node_is_online_test.c new file mode 100644 index 0000000..d22e3b4 --- /dev/null +++ b/lib/common/tests/nodes/pcmk_node_is_online_test.c @@ -0,0 +1,54 @@ +/* + * 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 <crm_internal.h> + +#include <stdio.h> // NULL +#include <glib.h> // TRUE, FALSE + +#include <crm/common/nodes.h> +#include <crm/common/unittest_internal.h> + +static void +null_is_offline(void **state) +{ + assert_false(pcmk_node_is_online(NULL)); +} + +static void +node_is_online(void **state) +{ + struct pe_node_shared_s shared = { + .online = TRUE, + }; + + pcmk_node_t node = { + .details = &shared, + }; + + assert_true(pcmk_node_is_online(&node)); +} + +static void +node_is_offline(void **state) +{ + struct pe_node_shared_s shared = { + .online = FALSE, + }; + pcmk_node_t node = { + .details = &shared, + }; + + assert_false(pcmk_node_is_online(&node)); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(null_is_offline), + cmocka_unit_test(node_is_online), + cmocka_unit_test(node_is_offline)) diff --git a/lib/common/tests/nodes/pcmk_node_is_pending_test.c b/lib/common/tests/nodes/pcmk_node_is_pending_test.c new file mode 100644 index 0000000..9f2abca --- /dev/null +++ b/lib/common/tests/nodes/pcmk_node_is_pending_test.c @@ -0,0 +1,54 @@ +/* + * 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 <crm_internal.h> + +#include <stdio.h> // NULL +#include <glib.h> // TRUE, FALSE + +#include <crm/common/nodes.h> +#include <crm/common/unittest_internal.h> + +static void +null_is_not_pending(void **state) +{ + assert_false(pcmk_node_is_pending(NULL)); +} + +static void +node_is_pending(void **state) +{ + struct pe_node_shared_s shared = { + .pending = TRUE, + }; + + pcmk_node_t node = { + .details = &shared, + }; + + assert_true(pcmk_node_is_pending(&node)); +} + +static void +node_is_not_pending(void **state) +{ + struct pe_node_shared_s shared = { + .pending = FALSE, + }; + pcmk_node_t node = { + .details = &shared, + }; + + assert_false(pcmk_node_is_pending(&node)); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(null_is_not_pending), + cmocka_unit_test(node_is_pending), + cmocka_unit_test(node_is_not_pending)) diff --git a/lib/common/tests/nodes/pcmk_node_is_shutting_down_test.c b/lib/common/tests/nodes/pcmk_node_is_shutting_down_test.c new file mode 100644 index 0000000..b6054b0 --- /dev/null +++ b/lib/common/tests/nodes/pcmk_node_is_shutting_down_test.c @@ -0,0 +1,54 @@ +/* + * 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 <crm_internal.h> + +#include <stdio.h> // NULL +#include <glib.h> // TRUE, FALSE + +#include <crm/common/nodes.h> +#include <crm/common/unittest_internal.h> + +static void +null_is_not_shutting_down(void **state) +{ + assert_false(pcmk_node_is_shutting_down(NULL)); +} + +static void +node_is_shutting_down(void **state) +{ + struct pe_node_shared_s shared = { + .shutdown = TRUE, + }; + + pcmk_node_t node = { + .details = &shared, + }; + + assert_true(pcmk_node_is_shutting_down(&node)); +} + +static void +node_is_not_shutting_down(void **state) +{ + struct pe_node_shared_s shared = { + .shutdown = FALSE, + }; + pcmk_node_t node = { + .details = &shared, + }; + + assert_false(pcmk_node_is_shutting_down(&node)); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(null_is_not_shutting_down), + cmocka_unit_test(node_is_shutting_down), + cmocka_unit_test(node_is_not_shutting_down)) diff --git a/lib/common/tests/nvpair/Makefile.am b/lib/common/tests/nvpair/Makefile.am index 7f406bd..9f762d4 100644 --- a/lib/common/tests/nvpair/Makefile.am +++ b/lib/common/tests/nvpair/Makefile.am @@ -1,5 +1,5 @@ # -# Copyright 2021-2023 the Pacemaker project contributors +# Copyright 2021-2024 the Pacemaker project contributors # # The version control history for this file may have further details. # @@ -11,7 +11,10 @@ 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__xe_attr_is_true_test \ +check_PROGRAMS = crm_meta_name_test \ + crm_meta_value_test \ + pcmk__xe_attr_is_true_test \ + pcmk__xe_get_datetime_test \ pcmk__xe_get_bool_attr_test \ pcmk__xe_set_bool_attr_test diff --git a/lib/common/tests/utils/crm_meta_name_test.c b/lib/common/tests/nvpair/crm_meta_name_test.c index 06fecc5..7c6c32b 100644 --- a/lib/common/tests/utils/crm_meta_name_test.c +++ b/lib/common/tests/nvpair/crm_meta_name_test.c @@ -1,5 +1,5 @@ /* - * Copyright 2022 the Pacemaker project contributors + * Copyright 2022-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -10,12 +10,12 @@ #include <crm_internal.h> #include <crm/common/unittest_internal.h> -#include <crm/msg_xml.h> +#include <crm/common/xml.h> static void empty_params(void **state) { - assert_null(crm_meta_name(NULL)); + pcmk__assert_asserts(crm_meta_name(NULL)); } static void @@ -23,11 +23,11 @@ standard_usage(void **state) { char *s = NULL; - s = crm_meta_name(XML_RSC_ATTR_NOTIFY); + s = crm_meta_name(PCMK_META_NOTIFY); assert_string_equal(s, "CRM_meta_notify"); free(s); - s = crm_meta_name(XML_RSC_ATTR_STICKINESS); + s = crm_meta_name(PCMK_META_RESOURCE_STICKINESS); assert_string_equal(s, "CRM_meta_resource_stickiness"); free(s); diff --git a/lib/common/tests/utils/crm_meta_value_test.c b/lib/common/tests/nvpair/crm_meta_value_test.c index 0bde5c6..ffe9619 100644 --- a/lib/common/tests/utils/crm_meta_value_test.c +++ b/lib/common/tests/nvpair/crm_meta_value_test.c @@ -1,5 +1,5 @@ /* - * Copyright 2022 the Pacemaker project contributors + * Copyright 2022-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -10,7 +10,7 @@ #include <crm_internal.h> #include <crm/common/unittest_internal.h> -#include <crm/msg_xml.h> +#include <crm/common/xml.h> #include <glib.h> @@ -30,8 +30,8 @@ key_not_in_table(void **state) { GHashTable *tbl = pcmk__strkey_table(free, free); - assert_null(crm_meta_value(tbl, XML_RSC_ATTR_NOTIFY)); - assert_null(crm_meta_value(tbl, XML_RSC_ATTR_STICKINESS)); + assert_null(crm_meta_value(tbl, PCMK_META_NOTIFY)); + assert_null(crm_meta_value(tbl, PCMK_META_RESOURCE_STICKINESS)); g_hash_table_destroy(tbl); } @@ -41,11 +41,13 @@ key_in_table(void **state) { GHashTable *tbl = pcmk__strkey_table(free, free); - g_hash_table_insert(tbl, crm_meta_name(XML_RSC_ATTR_NOTIFY), strdup("1")); - g_hash_table_insert(tbl, crm_meta_name(XML_RSC_ATTR_STICKINESS), strdup("2")); + g_hash_table_insert(tbl, crm_meta_name(PCMK_META_NOTIFY), strdup("1")); + g_hash_table_insert(tbl, crm_meta_name(PCMK_META_RESOURCE_STICKINESS), + strdup("2")); - assert_string_equal(crm_meta_value(tbl, XML_RSC_ATTR_NOTIFY), "1"); - assert_string_equal(crm_meta_value(tbl, XML_RSC_ATTR_STICKINESS), "2"); + assert_string_equal(crm_meta_value(tbl, PCMK_META_NOTIFY), "1"); + assert_string_equal(crm_meta_value(tbl, PCMK_META_RESOURCE_STICKINESS), + "2"); g_hash_table_destroy(tbl); } diff --git a/lib/common/tests/nvpair/pcmk__xe_attr_is_true_test.c b/lib/common/tests/nvpair/pcmk__xe_attr_is_true_test.c index 3723707..84187da 100644 --- a/lib/common/tests/nvpair/pcmk__xe_attr_is_true_test.c +++ b/lib/common/tests/nvpair/pcmk__xe_attr_is_true_test.c @@ -1,5 +1,5 @@ /* - * Copyright 2021 the Pacemaker project contributors + * Copyright 2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -15,7 +15,7 @@ static void empty_input(void **state) { - xmlNode *node = string2xml("<node/>"); + xmlNode *node = pcmk__xml_parse("<node/>"); assert_false(pcmk__xe_attr_is_true(NULL, NULL)); assert_false(pcmk__xe_attr_is_true(NULL, "whatever")); @@ -27,7 +27,7 @@ empty_input(void **state) static void attr_missing(void **state) { - xmlNode *node = string2xml("<node a=\"true\" b=\"false\"/>"); + xmlNode *node = pcmk__xml_parse("<node a=\"true\" b=\"false\"/>"); assert_false(pcmk__xe_attr_is_true(node, "c")); free_xml(node); @@ -36,7 +36,7 @@ attr_missing(void **state) static void attr_present(void **state) { - xmlNode *node = string2xml("<node a=\"true\" b=\"false\"/>"); + xmlNode *node = pcmk__xml_parse("<node a=\"true\" b=\"false\"/>"); assert_true(pcmk__xe_attr_is_true(node, "a")); assert_false(pcmk__xe_attr_is_true(node, "b")); @@ -44,7 +44,7 @@ attr_present(void **state) free_xml(node); } -PCMK__UNIT_TEST(NULL, NULL, +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL, cmocka_unit_test(empty_input), cmocka_unit_test(attr_missing), cmocka_unit_test(attr_present)) diff --git a/lib/common/tests/nvpair/pcmk__xe_get_bool_attr_test.c b/lib/common/tests/nvpair/pcmk__xe_get_bool_attr_test.c index 500d8a6..4823f6a 100644 --- a/lib/common/tests/nvpair/pcmk__xe_get_bool_attr_test.c +++ b/lib/common/tests/nvpair/pcmk__xe_get_bool_attr_test.c @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the Pacemaker project contributors + * Copyright 2021-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -15,7 +15,7 @@ static void empty_input(void **state) { - xmlNode *node = string2xml("<node/>"); + xmlNode *node = pcmk__xml_parse("<node/>"); bool value; assert_int_equal(pcmk__xe_get_bool_attr(NULL, NULL, &value), ENODATA); @@ -29,7 +29,7 @@ empty_input(void **state) static void attr_missing(void **state) { - xmlNode *node = string2xml("<node a=\"true\" b=\"false\"/>"); + xmlNode *node = pcmk__xml_parse("<node a=\"true\" b=\"false\"/>"); bool value; assert_int_equal(pcmk__xe_get_bool_attr(node, "c", &value), ENODATA); @@ -39,7 +39,8 @@ attr_missing(void **state) static void attr_present(void **state) { - xmlNode *node = string2xml("<node a=\"true\" b=\"false\" c=\"blah\"/>"); + xmlNode *node = pcmk__xml_parse("<node a=\"true\" b=\"false\" " + "c=\"blah\"/>"); bool value; value = false; @@ -53,7 +54,7 @@ attr_present(void **state) free_xml(node); } -PCMK__UNIT_TEST(NULL, NULL, +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL, cmocka_unit_test(empty_input), cmocka_unit_test(attr_missing), cmocka_unit_test(attr_present)) diff --git a/lib/common/tests/nvpair/pcmk__xe_get_datetime_test.c b/lib/common/tests/nvpair/pcmk__xe_get_datetime_test.c new file mode 100644 index 0000000..6da1e23 --- /dev/null +++ b/lib/common/tests/nvpair/pcmk__xe_get_datetime_test.c @@ -0,0 +1,108 @@ +/* + * 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 <crm_internal.h> + +#include <errno.h> +#include <libxml/tree.h> + +#include <crm/common/unittest_internal.h> + +#include <crm/common/iso8601.h> +#include <crm/common/xml.h> +#include <crm/common/nvpair_internal.h> + +#define REFERENCE_ISO8601 "2024-001" +#define ATTR_PRESENT "start" +#define ATTR_MISSING "end" +#define REFERENCE_XML "<date_expression id=\"id1\" " \ + ATTR_PRESENT "=\"" REFERENCE_ISO8601 "\"" \ + " operation=\"gt\">" +#define BAD_XML "<date_expression id=\"id1\" " \ + ATTR_PRESENT "=\"not_a_time\"" \ + " operation=\"gt\">" + +static void +null_invalid(void **state) +{ + xmlNode *xml = pcmk__xml_parse(REFERENCE_XML); + crm_time_t *t = NULL; + + assert_int_equal(pcmk__xe_get_datetime(NULL, NULL, NULL), EINVAL); + assert_int_equal(pcmk__xe_get_datetime(xml, NULL, NULL), EINVAL); + assert_int_equal(pcmk__xe_get_datetime(xml, ATTR_PRESENT, NULL), EINVAL); + assert_int_equal(pcmk__xe_get_datetime(xml, NULL, &t), EINVAL); + assert_null(t); + assert_int_equal(pcmk__xe_get_datetime(NULL, ATTR_PRESENT, NULL), EINVAL); + assert_int_equal(pcmk__xe_get_datetime(NULL, ATTR_PRESENT, &t), EINVAL); + assert_null(t); + assert_int_equal(pcmk__xe_get_datetime(NULL, NULL, &t), EINVAL); + assert_null(t); + + free_xml(xml); +} + +static void +nonnull_time_invalid(void **state) +{ + xmlNode *xml = pcmk__xml_parse(REFERENCE_XML); + crm_time_t *t = crm_time_new_undefined(); + + assert_int_equal(pcmk__xe_get_datetime(xml, ATTR_PRESENT, &t), EINVAL); + + crm_time_free(t); + free_xml(xml); +} + +static void +attr_missing(void **state) +{ + xmlNode *xml = pcmk__xml_parse(REFERENCE_XML); + crm_time_t *t = NULL; + + assert_int_equal(pcmk__xe_get_datetime(xml, ATTR_MISSING, &t), pcmk_rc_ok); + assert_null(t); + + free_xml(xml); +} + +static void +attr_valid(void **state) +{ + xmlNode *xml = pcmk__xml_parse(REFERENCE_XML); + crm_time_t *t = NULL; + crm_time_t *reference = crm_time_new(REFERENCE_ISO8601); + + assert_int_equal(pcmk__xe_get_datetime(xml, ATTR_PRESENT, &t), pcmk_rc_ok); + assert_int_equal(crm_time_compare(t, reference), 0); + + crm_time_free(t); + crm_time_free(reference); + free_xml(xml); +} + +static void +attr_invalid(void **state) +{ + xmlNode *xml = pcmk__xml_parse(BAD_XML); + crm_time_t *t = NULL; + + assert_int_equal(pcmk__xe_get_datetime(xml, ATTR_PRESENT, &t), + pcmk_rc_unpack_error); + assert_null(t); + + free_xml(xml); +} + +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL, + cmocka_unit_test(null_invalid), + cmocka_unit_test(nonnull_time_invalid), + cmocka_unit_test(attr_missing), + cmocka_unit_test(attr_valid), + cmocka_unit_test(attr_invalid)) diff --git a/lib/common/tests/nvpair/pcmk__xe_set_bool_attr_test.c b/lib/common/tests/nvpair/pcmk__xe_set_bool_attr_test.c index e1fb9c4..dda2878 100644 --- a/lib/common/tests/nvpair/pcmk__xe_set_bool_attr_test.c +++ b/lib/common/tests/nvpair/pcmk__xe_set_bool_attr_test.c @@ -1,5 +1,5 @@ /* - * Copyright 2021 the Pacemaker project contributors + * Copyright 2021-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -11,21 +11,21 @@ #include <crm/common/unittest_internal.h> #include <crm/common/xml_internal.h> -#include <crm/msg_xml.h> +#include <crm/common/xml.h> static void set_attr(void **state) { - xmlNode *node = string2xml("<node/>"); + xmlNode *node = pcmk__xml_parse("<node/>"); pcmk__xe_set_bool_attr(node, "a", true); pcmk__xe_set_bool_attr(node, "b", false); - assert_string_equal(crm_element_value(node, "a"), XML_BOOLEAN_TRUE); - assert_string_equal(crm_element_value(node, "b"), XML_BOOLEAN_FALSE); + assert_string_equal(crm_element_value(node, "a"), PCMK_VALUE_TRUE); + assert_string_equal(crm_element_value(node, "b"), PCMK_VALUE_FALSE); free_xml(node); } -PCMK__UNIT_TEST(NULL, NULL, +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL, cmocka_unit_test(set_attr)) diff --git a/lib/common/tests/probes/Makefile.am b/lib/common/tests/probes/Makefile.am new file mode 100644 index 0000000..f5a3fb4 --- /dev/null +++ b/lib/common/tests/probes/Makefile.am @@ -0,0 +1,18 @@ +# +# 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 $(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_is_probe_test \ + pcmk_xe_is_probe_test \ + pcmk_xe_mask_probe_failure_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/actions/pcmk_is_probe_test.c b/lib/common/tests/probes/pcmk_is_probe_test.c index 4a65e3f..835aac1 100644 --- a/lib/common/tests/actions/pcmk_is_probe_test.c +++ b/lib/common/tests/probes/pcmk_is_probe_test.c @@ -1,5 +1,5 @@ /* - * Copyright 2021 the Pacemaker project contributors + * Copyright 2021-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * diff --git a/lib/common/tests/probes/pcmk_xe_is_probe_test.c b/lib/common/tests/probes/pcmk_xe_is_probe_test.c new file mode 100644 index 0000000..e42476a --- /dev/null +++ b/lib/common/tests/probes/pcmk_xe_is_probe_test.c @@ -0,0 +1,54 @@ +/* + * 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 <crm_internal.h> + +#include <crm/common/unittest_internal.h> + +static void +op_is_probe_test(void **state) +{ + xmlNode *node = NULL; + + assert_false(pcmk_xe_is_probe(NULL)); + + node = pcmk__xml_parse("<" PCMK__XE_LRM_RSC_OP "/>"); + assert_false(pcmk_xe_is_probe(node)); + free_xml(node); + + node = pcmk__xml_parse("<" PCMK__XE_LRM_RSC_OP " " + PCMK__XA_OPERATION_KEY "=\"blah\" " + PCMK_META_INTERVAL "=\"30s\"/>"); + assert_false(pcmk_xe_is_probe(node)); + free_xml(node); + + node = pcmk__xml_parse("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION + "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"30s\"/>"); + assert_false(pcmk_xe_is_probe(node)); + free_xml(node); + + node = pcmk__xml_parse("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION + "=\"" PCMK_ACTION_START "\" " + PCMK_META_INTERVAL "=\"0\"/>"); + assert_false(pcmk_xe_is_probe(node)); + free_xml(node); + + node = pcmk__xml_parse("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION + "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\"/>"); + assert_true(pcmk_xe_is_probe(node)); + free_xml(node); +} + +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL, + cmocka_unit_test(op_is_probe_test)) diff --git a/lib/common/tests/probes/pcmk_xe_mask_probe_failure_test.c b/lib/common/tests/probes/pcmk_xe_mask_probe_failure_test.c new file mode 100644 index 0000000..ad378bf --- /dev/null +++ b/lib/common/tests/probes/pcmk_xe_mask_probe_failure_test.c @@ -0,0 +1,333 @@ +/* + * 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 <crm_internal.h> + +#include <crm/common/unittest_internal.h> + +static void +op_is_not_probe_test(void **state) { + xmlNode *node = NULL; + + /* Not worth testing this thoroughly since it's just a duplicate of whether + * pcmk_op_is_probe works or not. + */ + + node = pcmk__xml_parse("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION + "=\"" PCMK_ACTION_START "\" " + PCMK_META_INTERVAL "=\"0\"/>"); + assert_false(pcmk_xe_mask_probe_failure(node)); + free_xml(node); +} + +static void +op_does_not_have_right_values_test(void **state) { + xmlNode *node = NULL; + char *s = NULL; + + node = pcmk__xml_parse("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION + "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\"/>"); + assert_false(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"\"/>", + PCMK_OCF_OK); + node = pcmk__xml_parse(s); + assert_false(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); +} + +static void +check_values_test(void **state) { + xmlNode *node = NULL; + char *s = NULL; + + /* PCMK_EXEC_NOT_SUPPORTED */ + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"%d\"/>", + PCMK_OCF_OK, PCMK_EXEC_NOT_SUPPORTED); + node = pcmk__xml_parse(s); + assert_false(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); + + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"%d\"/>", + PCMK_OCF_NOT_INSTALLED, PCMK_EXEC_NOT_SUPPORTED); + node = pcmk__xml_parse(s); + assert_true(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); + + /* PCMK_EXEC_DONE */ + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"%d\"/>", + PCMK_OCF_OK, PCMK_EXEC_DONE); + node = pcmk__xml_parse(s); + assert_false(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); + + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"%d\"/>", + PCMK_OCF_INVALID_PARAM, PCMK_EXEC_DONE); + node = pcmk__xml_parse(s); + assert_true(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); + + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"%d\"/>", + PCMK_OCF_NOT_INSTALLED, PCMK_EXEC_DONE); + node = pcmk__xml_parse(s); + assert_true(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); + + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"%d\"/>", + PCMK_OCF_NOT_CONFIGURED, PCMK_EXEC_DONE); + node = pcmk__xml_parse(s); + assert_false(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); + + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"%d\"/>", + PCMK_OCF_NOT_RUNNING, PCMK_EXEC_DONE); + node = pcmk__xml_parse(s); + assert_false(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); + + /* PCMK_EXEC_NOT_INSTALLED */ + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"%d\"/>", + PCMK_OCF_OK, PCMK_EXEC_NOT_INSTALLED); + node = pcmk__xml_parse(s); + assert_true(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); + + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"%d\"/>", + PCMK_OCF_NOT_INSTALLED, PCMK_EXEC_NOT_INSTALLED); + node = pcmk__xml_parse(s); + assert_true(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); + + /* PCMK_EXEC_ERROR */ + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"%d\"/>", + PCMK_OCF_OK, PCMK_EXEC_ERROR); + node = pcmk__xml_parse(s); + assert_false(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); + + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"%d\"/>", + PCMK_OCF_INVALID_PARAM, PCMK_EXEC_ERROR); + node = pcmk__xml_parse(s); + assert_true(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); + + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"%d\"/>", + PCMK_OCF_NOT_INSTALLED, PCMK_EXEC_ERROR); + node = pcmk__xml_parse(s); + assert_true(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); + + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"%d\"/>", + PCMK_OCF_NOT_CONFIGURED, PCMK_EXEC_ERROR); + node = pcmk__xml_parse(s); + assert_false(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); + + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"%d\"/>", + PCMK_OCF_NOT_RUNNING, PCMK_EXEC_ERROR); + node = pcmk__xml_parse(s); + assert_false(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); + + /* PCMK_EXEC_ERROR_HARD */ + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"%d\"/>", + PCMK_OCF_OK, PCMK_EXEC_ERROR_HARD); + node = pcmk__xml_parse(s); + assert_false(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); + + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"%d\"/>", + PCMK_OCF_INVALID_PARAM, PCMK_EXEC_ERROR_HARD); + node = pcmk__xml_parse(s); + assert_true(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); + + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"%d\"/>", + PCMK_OCF_NOT_INSTALLED, PCMK_EXEC_ERROR_HARD); + node = pcmk__xml_parse(s); + assert_true(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); + + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"%d\"/>", + PCMK_OCF_NOT_CONFIGURED, PCMK_EXEC_ERROR_HARD); + node = pcmk__xml_parse(s); + assert_false(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); + + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"%d\"/>", + PCMK_OCF_NOT_RUNNING, PCMK_EXEC_ERROR_HARD); + node = pcmk__xml_parse(s); + assert_false(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); + + /* PCMK_EXEC_ERROR_FATAL */ + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"%d\"/>", + PCMK_OCF_OK, PCMK_EXEC_ERROR_FATAL); + node = pcmk__xml_parse(s); + assert_false(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); + + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"%d\"/>", + PCMK_OCF_INVALID_PARAM, PCMK_EXEC_ERROR_FATAL); + node = pcmk__xml_parse(s); + assert_true(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); + + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"%d\"/>", + PCMK_OCF_NOT_INSTALLED, PCMK_EXEC_ERROR_FATAL); + node = pcmk__xml_parse(s); + assert_true(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); + + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"%d\"/>", + PCMK_OCF_NOT_CONFIGURED, PCMK_EXEC_ERROR_FATAL); + node = pcmk__xml_parse(s); + assert_false(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); + + s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " " + PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" " + PCMK_META_INTERVAL "=\"0\" " + PCMK__XA_RC_CODE "=\"%d\" " + PCMK__XA_OP_STATUS "=\"%d\"/>", + PCMK_OCF_NOT_RUNNING, PCMK_EXEC_ERROR_FATAL); + node = pcmk__xml_parse(s); + assert_false(pcmk_xe_mask_probe_failure(node)); + free(s); + free_xml(node); +} + +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL, + cmocka_unit_test(op_is_not_probe_test), + cmocka_unit_test(op_does_not_have_right_values_test), + cmocka_unit_test(check_values_test)) diff --git a/lib/common/tests/procfs/pcmk__procfs_pid2path_test.c b/lib/common/tests/procfs/pcmk__procfs_pid2path_test.c index 2bae541..52fe006 100644 --- a/lib/common/tests/procfs/pcmk__procfs_pid2path_test.c +++ b/lib/common/tests/procfs/pcmk__procfs_pid2path_test.c @@ -1,5 +1,5 @@ /* - * Copyright 2022 the Pacemaker project contributors + * Copyright 2022-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -21,7 +21,7 @@ static void no_exe_file(void **state) { size_t len = PATH_MAX; - char *path = calloc(len, sizeof(char)); + char *path = pcmk__assert_alloc(len, sizeof(char)); // Set readlink() errno and link contents pcmk__mock_readlink = true; @@ -43,7 +43,7 @@ static void contents_too_long(void **state) { size_t len = 10; - char *path = calloc(len, sizeof(char)); + char *path = pcmk__assert_alloc(len, sizeof(char)); // Set readlink() errno and link contents pcmk__mock_readlink = true; @@ -66,7 +66,7 @@ static void contents_ok(void **state) { size_t len = PATH_MAX; - char *path = calloc(len, sizeof(char)); + char *path = pcmk__assert_alloc(len, sizeof(char)); // Set readlink() errno and link contents pcmk__mock_readlink = true; diff --git a/lib/common/tests/resources/Makefile.am b/lib/common/tests/resources/Makefile.am new file mode 100644 index 0000000..91e29a6 --- /dev/null +++ b/lib/common/tests/resources/Makefile.am @@ -0,0 +1,17 @@ +# +# 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 $(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_resource_id_test \ + pcmk_resource_is_managed_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/resources/pcmk_resource_id_test.c b/lib/common/tests/resources/pcmk_resource_id_test.c new file mode 100644 index 0000000..5676d2a --- /dev/null +++ b/lib/common/tests/resources/pcmk_resource_id_test.c @@ -0,0 +1,36 @@ +/* + * 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 <crm_internal.h> + +#include <stdio.h> // NULL + +#include <crm/common/resources.h> +#include <crm/common/unittest_internal.h> + +static void +null_resource(void **state) +{ + assert_null(pcmk_resource_id(NULL)); +} + +static void +resource_with_id(void **state) +{ + char rsc1_id[] = "rsc1"; + pcmk_resource_t rsc1 = { + .id = rsc1_id, + }; + + assert_string_equal(pcmk_resource_id(&rsc1), "rsc1"); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(null_resource), + cmocka_unit_test(resource_with_id)) diff --git a/lib/common/tests/resources/pcmk_resource_is_managed_test.c b/lib/common/tests/resources/pcmk_resource_is_managed_test.c new file mode 100644 index 0000000..958bdc2 --- /dev/null +++ b/lib/common/tests/resources/pcmk_resource_is_managed_test.c @@ -0,0 +1,46 @@ +/* + * 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 <crm_internal.h> + +#include <stdio.h> // NULL + +#include <crm/common/resources.h> +#include <crm/common/unittest_internal.h> + +static void +null_resource(void **state) +{ + assert_false(pcmk_resource_is_managed(NULL)); +} + +static void +resource_is_managed(void **state) +{ + pcmk_resource_t rsc1 = { + .flags = pcmk_rsc_managed, + }; + + assert_true(pcmk_resource_is_managed(&rsc1)); +} + +static void +resource_is_not_managed(void **state) +{ + pcmk_resource_t rsc1 = { + .flags = 0, + }; + + assert_false(pcmk_resource_is_managed(&rsc1)); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(null_resource), + cmocka_unit_test(resource_is_managed), + cmocka_unit_test(resource_is_not_managed)) diff --git a/lib/common/tests/rules/Makefile.am b/lib/common/tests/rules/Makefile.am new file mode 100644 index 0000000..4163037 --- /dev/null +++ b/lib/common/tests/rules/Makefile.am @@ -0,0 +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_evaluate_rule_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/rules/pcmk__cmp_by_type_test.c b/lib/common/tests/rules/pcmk__cmp_by_type_test.c new file mode 100644 index 0000000..cf468f1 --- /dev/null +++ b/lib/common/tests/rules/pcmk__cmp_by_type_test.c @@ -0,0 +1,102 @@ +/* + * Copyright 2024 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <limits.h> // INT_MIN, INT_MAX + +#include <crm/common/util.h> // crm_strdup_printf() +#include <crm/common/rules_internal.h> +#include <crm/common/unittest_internal.h> +#include "crmcommon_private.h" + +static void +null_compares_lesser(void **state) +{ + assert_int_equal(pcmk__cmp_by_type(NULL, NULL, pcmk__type_string), 0); + assert_true(pcmk__cmp_by_type("0", NULL, pcmk__type_integer) > 0); + assert_true(pcmk__cmp_by_type(NULL, "0", pcmk__type_number) < 0); +} + +static void +invalid_compares_equal(void **state) +{ + assert_int_equal(pcmk__cmp_by_type("0", "1", pcmk__type_unknown), 0); + assert_int_equal(pcmk__cmp_by_type("hi", "bye", pcmk__type_unknown), 0); + assert_int_equal(pcmk__cmp_by_type("-1.0", "2.0", pcmk__type_unknown), 0); +} + +static void +compare_string_type(void **state) +{ + assert_int_equal(pcmk__cmp_by_type("bye", "bye", pcmk__type_string), 0); + assert_int_equal(pcmk__cmp_by_type("bye", "BYE", pcmk__type_string), 0); + assert_true(pcmk__cmp_by_type("bye", "hello", pcmk__type_string) < 0); + assert_true(pcmk__cmp_by_type("bye", "HELLO", pcmk__type_string) < 0); + assert_true(pcmk__cmp_by_type("bye", "boo", pcmk__type_string) > 0); + assert_true(pcmk__cmp_by_type("bye", "Boo", pcmk__type_string) > 0); +} + +static void +compare_integer_type(void **state) +{ + char *int_min = crm_strdup_printf("%d", INT_MIN); + char *int_max = crm_strdup_printf("%d", INT_MAX); + + assert_int_equal(pcmk__cmp_by_type("0", "0", pcmk__type_integer), 0); + assert_true(pcmk__cmp_by_type("0", "1", pcmk__type_integer) < 0); + assert_true(pcmk__cmp_by_type("1", "0", pcmk__type_integer) > 0); + assert_true(pcmk__cmp_by_type("3999", "399", pcmk__type_integer) > 0); + assert_true(pcmk__cmp_by_type(int_min, int_max, pcmk__type_integer) < 0); + assert_true(pcmk__cmp_by_type(int_max, int_min, pcmk__type_integer) > 0); + free(int_min); + free(int_max); + + // Non-integers compare as strings + assert_int_equal(pcmk__cmp_by_type("0", "x", pcmk__type_integer), + pcmk__cmp_by_type("0", "x", pcmk__type_string)); + assert_int_equal(pcmk__cmp_by_type("x", "0", pcmk__type_integer), + pcmk__cmp_by_type("x", "0", pcmk__type_string)); + assert_int_equal(pcmk__cmp_by_type("x", "X", pcmk__type_integer), + pcmk__cmp_by_type("x", "X", pcmk__type_string)); +} + +static void +compare_number_type(void **state) +{ + assert_int_equal(pcmk__cmp_by_type("0", "0.0", pcmk__type_number), 0); + assert_true(pcmk__cmp_by_type("0.345", "0.5", pcmk__type_number) < 0); + assert_true(pcmk__cmp_by_type("5", "3.1", pcmk__type_number) > 0); + assert_true(pcmk__cmp_by_type("3999", "399", pcmk__type_number) > 0); + + // Non-numbers compare as strings + assert_int_equal(pcmk__cmp_by_type("0.0", "x", pcmk__type_number), + pcmk__cmp_by_type("0.0", "x", pcmk__type_string)); + assert_int_equal(pcmk__cmp_by_type("x", "0.0", pcmk__type_number), + pcmk__cmp_by_type("x", "0.0", pcmk__type_string)); + assert_int_equal(pcmk__cmp_by_type("x", "X", pcmk__type_number), + pcmk__cmp_by_type("x", "X", pcmk__type_string)); +} + +static void +compare_version_type(void **state) +{ + assert_int_equal(pcmk__cmp_by_type("1.0", "1.0", pcmk__type_version), 0); + assert_true(pcmk__cmp_by_type("1.0.0", "1.0.1", pcmk__type_version) < 0); + assert_true(pcmk__cmp_by_type("5.0", "3.1.15", pcmk__type_version) > 0); + assert_true(pcmk__cmp_by_type("3999", "399", pcmk__type_version) > 0); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(null_compares_lesser), + cmocka_unit_test(invalid_compares_equal), + cmocka_unit_test(compare_string_type), + cmocka_unit_test(compare_integer_type), + cmocka_unit_test(compare_number_type), + cmocka_unit_test(compare_version_type)) diff --git a/lib/common/tests/rules/pcmk__evaluate_attr_expression_test.c b/lib/common/tests/rules/pcmk__evaluate_attr_expression_test.c new file mode 100644 index 0000000..d28cb11 --- /dev/null +++ b/lib/common/tests/rules/pcmk__evaluate_attr_expression_test.c @@ -0,0 +1,831 @@ +/* + * Copyright 2024 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <stdio.h> +#include <glib.h> + +#include <crm/common/xml.h> +#include <crm/common/rules_internal.h> +#include <crm/common/unittest_internal.h> +#include "crmcommon_private.h" + +/* + * Shared data + */ + +#define MATCHED_STRING "server-north" + +static const regmatch_t submatches[] = { + { .rm_so = 0, .rm_eo = 12 }, // %0 = Entire string + { .rm_so = 7, .rm_eo = 12 }, // %1 = "north" +}; + +static pcmk_rule_input_t rule_input = { + // These are the only members used to evaluate attribute expressions + + // Used to replace submatches in attribute name + .rsc_id = MATCHED_STRING, + .rsc_id_submatches = submatches, + .rsc_id_nmatches = 2, + + // Used when source is instance attributes + .rsc_params = NULL, + + // Used when source is meta-attributes + .rsc_meta = NULL, + + // Used to get actual value of node attribute + .node_attrs = NULL, +}; + +static int +setup(void **state) +{ + rule_input.rsc_params = pcmk__strkey_table(free, free); + pcmk__insert_dup(rule_input.rsc_params, "foo-param", "bar"); + pcmk__insert_dup(rule_input.rsc_params, "myparam", "different"); + + rule_input.rsc_meta = pcmk__strkey_table(free, free); + pcmk__insert_dup(rule_input.rsc_meta, "foo-meta", "bar"); + pcmk__insert_dup(rule_input.rsc_params, "mymeta", "different"); + + rule_input.node_attrs = pcmk__strkey_table(free, free); + pcmk__insert_dup(rule_input.node_attrs, "foo", "bar"); + pcmk__insert_dup(rule_input.node_attrs, "num", "10"); + pcmk__insert_dup(rule_input.node_attrs, "ver", "3.5.0"); + pcmk__insert_dup(rule_input.node_attrs, "prefer-north", "100"); + + return 0; +} + +static int +teardown(void **state) +{ + g_hash_table_destroy(rule_input.rsc_params); + g_hash_table_destroy(rule_input.rsc_meta); + g_hash_table_destroy(rule_input.node_attrs); + return 0; +} + +/*! + * \internal + * \brief Run one test, comparing return value + * + * \param[in] xml_string Node attribute expression XML as string + * \param[in] reference_rc Assert that evaluation result equals this + */ +static void +assert_attr_expression(const char *xml_string, int reference_rc) +{ + xmlNode *xml = pcmk__xml_parse(xml_string); + + assert_int_equal(pcmk__evaluate_attr_expression(xml, &rule_input), + reference_rc); + free_xml(xml); +} + + +/* + * Invalid arguments + */ + +#define EXPR_SOURCE_LITERAL_PASSES \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_ATTRIBUTE "='foo' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_VALUE "='bar' " \ + PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_LITERAL "' />" + +static void +null_invalid(void **state) +{ + xmlNode *xml = pcmk__xml_parse(EXPR_SOURCE_LITERAL_PASSES); + + assert_int_equal(pcmk__evaluate_attr_expression(NULL, NULL), EINVAL); + assert_int_equal(pcmk__evaluate_attr_expression(xml, NULL), EINVAL); + assert_int_equal(pcmk__evaluate_attr_expression(NULL, &rule_input), EINVAL); + + free_xml(xml); +} + + +/* + * Test PCMK_XA_ID + */ + +#define EXPR_ID_MISSING \ + "<" PCMK_XE_EXPRESSION " " \ + PCMK_XA_ATTRIBUTE "='foo' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_VALUE "='bar' />" + +static void +id_missing(void **state) +{ + // Currently acceptable + assert_attr_expression(EXPR_ID_MISSING, pcmk_rc_ok); +} + + +/* + * Test PCMK_XA_ATTRIBUTE + */ + +#define EXPR_ATTR_MISSING \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_VALUE "='bar' />" + +static void +attr_missing(void **state) +{ + assert_attr_expression(EXPR_ATTR_MISSING, pcmk_rc_unpack_error); +} + +#define EXPR_ATTR_SUBMATCH_PASSES \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_ATTRIBUTE "='prefer-%1' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_DEFINED "' />" + +static void +attr_with_submatch_passes(void **state) +{ + assert_attr_expression(EXPR_ATTR_SUBMATCH_PASSES, pcmk_rc_ok); +} + +#define EXPR_ATTR_SUBMATCH_FAILS \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_ATTRIBUTE "='undefined-%1' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_DEFINED "' />" + +static void +attr_with_submatch_fails(void **state) +{ + assert_attr_expression(EXPR_ATTR_SUBMATCH_FAILS, pcmk_rc_op_unsatisfied); +} + + +/* + * Test PCMK_XA_VALUE_SOURCE + */ + +#define EXPR_SOURCE_MISSING \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_ATTRIBUTE "='foo' " \ + PCMK_XA_VALUE "='bar' />" + +static void +source_missing(void **state) +{ + // Defaults to literal + assert_attr_expression(EXPR_SOURCE_MISSING, pcmk_rc_ok); +} + +#define EXPR_SOURCE_INVALID \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_ATTRIBUTE "='foo' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_VALUE "='bar' " \ + PCMK_XA_VALUE_SOURCE "='not-a-source' />" + +static void +source_invalid(void **state) +{ + // Currently treated as literal + assert_attr_expression(EXPR_SOURCE_INVALID, pcmk_rc_ok); +} + +static void +source_literal_passes(void **state) +{ + assert_attr_expression(EXPR_SOURCE_LITERAL_PASSES, pcmk_rc_ok); +} + +#define EXPR_SOURCE_LITERAL_VALUE_FAILS \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_ATTRIBUTE "='foo' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_VALUE "='wrong-value' " \ + PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_LITERAL "' />" + +static void +source_literal_value_fails(void **state) +{ + assert_attr_expression(EXPR_SOURCE_LITERAL_VALUE_FAILS, + pcmk_rc_op_unsatisfied); +} + +#define EXPR_SOURCE_LITERAL_ATTR_FAILS \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_ATTRIBUTE "='not-an-attribute' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_VALUE "='bar' " \ + PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_LITERAL "' />" + +static void +source_literal_attr_fails(void **state) +{ + assert_attr_expression(EXPR_SOURCE_LITERAL_ATTR_FAILS, + pcmk_rc_op_unsatisfied); +} + +#define EXPR_SOURCE_PARAM_MISSING \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_ATTRIBUTE "='foo' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_VALUE "='not-a-param' " \ + PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_PARAM "' />" + +static void +source_params_missing(void **state) +{ + assert_attr_expression(EXPR_SOURCE_PARAM_MISSING, pcmk_rc_op_unsatisfied); +} + +#define EXPR_SOURCE_PARAM_PASSES \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_ATTRIBUTE "='foo' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_VALUE "='foo-param' " \ + PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_PARAM "' />" + +static void +source_params_passes(void **state) +{ + assert_attr_expression(EXPR_SOURCE_PARAM_PASSES, pcmk_rc_ok); +} + +#define EXPR_SOURCE_PARAM_FAILS \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_ATTRIBUTE "='foo' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_VALUE "='myparam' " \ + PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_PARAM "' />" + +static void +source_params_fails(void **state) +{ + assert_attr_expression(EXPR_SOURCE_PARAM_FAILS, pcmk_rc_op_unsatisfied); +} + +#define EXPR_SOURCE_META_MISSING \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_ATTRIBUTE "='foo' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_VALUE "='not-a-meta' " \ + PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_META "' />" + +static void +source_meta_missing(void **state) +{ + assert_attr_expression(EXPR_SOURCE_META_MISSING, pcmk_rc_op_unsatisfied); +} + +#define EXPR_SOURCE_META_PASSES \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_ATTRIBUTE "='foo' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_VALUE "='foo-meta' " \ + PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_META "' />" + +static void +source_meta_passes(void **state) +{ + assert_attr_expression(EXPR_SOURCE_META_PASSES, pcmk_rc_ok); +} + +#define EXPR_SOURCE_META_FAILS \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_ATTRIBUTE "='foo' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_VALUE "='mymeta' " \ + PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_META "' />" + +static void +source_meta_fails(void **state) +{ + assert_attr_expression(EXPR_SOURCE_META_FAILS, pcmk_rc_op_unsatisfied); +} + + +/* + * Test PCMK_XA_TYPE + */ + +#define EXPR_TYPE_DEFAULT_NUMBER \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \ + PCMK_XA_ATTRIBUTE "='num' " \ + PCMK_XA_VALUE "='2.5' />" + +static void +type_default_number(void **state) +{ + // Defaults to number for "gt" if either value contains a decimal point + assert_attr_expression(EXPR_TYPE_DEFAULT_NUMBER, pcmk_rc_ok); +} + +#define EXPR_TYPE_DEFAULT_INT \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \ + PCMK_XA_ATTRIBUTE "='num' " \ + PCMK_XA_VALUE "='2' />" + +static void +type_default_int(void **state) +{ + // Defaults to integer for "gt" if neither value contains a decimal point + assert_attr_expression(EXPR_TYPE_DEFAULT_INT, pcmk_rc_ok); +} + +#define EXPR_TYPE_STRING_PASSES \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='" PCMK_VALUE_STRING "' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_ATTRIBUTE "='foo' " \ + PCMK_XA_VALUE "='bar' />" + +static void +type_string_passes(void **state) +{ + assert_attr_expression(EXPR_TYPE_STRING_PASSES, pcmk_rc_ok); +} + +#define EXPR_TYPE_STRING_FAILS \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='" PCMK_VALUE_STRING "' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_ATTRIBUTE "='foo' " \ + PCMK_XA_VALUE "='bat' />" + +static void +type_string_fails(void **state) +{ + assert_attr_expression(EXPR_TYPE_STRING_FAILS, pcmk_rc_op_unsatisfied); +} + +#define EXPR_TYPE_INTEGER_PASSES \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_ATTRIBUTE "='num' " \ + PCMK_XA_VALUE "='10' />" + +static void +type_integer_passes(void **state) +{ + assert_attr_expression(EXPR_TYPE_INTEGER_PASSES, pcmk_rc_ok); +} + +#define EXPR_TYPE_INTEGER_FAILS \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_ATTRIBUTE "='num' " \ + PCMK_XA_VALUE "='11' />" + +static void +type_integer_fails(void **state) +{ + assert_attr_expression(EXPR_TYPE_INTEGER_FAILS, pcmk_rc_op_unsatisfied); +} + +#define EXPR_TYPE_INTEGER_TRUNCATION \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_ATTRIBUTE "='num' " \ + PCMK_XA_VALUE "='10.5' />" + +static void +type_integer_truncation(void **state) +{ + assert_attr_expression(EXPR_TYPE_INTEGER_TRUNCATION, pcmk_rc_ok); +} + +#define EXPR_TYPE_NUMBER_PASSES \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='" PCMK_VALUE_NUMBER "' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_ATTRIBUTE "='num' " \ + PCMK_XA_VALUE "='10.0' />" + +static void +type_number_passes(void **state) +{ + assert_attr_expression(EXPR_TYPE_NUMBER_PASSES, pcmk_rc_ok); +} + +#define EXPR_TYPE_NUMBER_FAILS \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='" PCMK_VALUE_NUMBER "' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_ATTRIBUTE "='num' " \ + PCMK_XA_VALUE "='10.1' />" + +static void +type_number_fails(void **state) +{ + assert_attr_expression(EXPR_TYPE_NUMBER_FAILS, pcmk_rc_op_unsatisfied); +} + +#define EXPR_TYPE_VERSION_PASSES \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='" PCMK_VALUE_VERSION "' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \ + PCMK_XA_ATTRIBUTE "='ver' " \ + PCMK_XA_VALUE "='3.4.9' />" + +static void +type_version_passes(void **state) +{ + assert_attr_expression(EXPR_TYPE_VERSION_PASSES, pcmk_rc_ok); +} + +#define EXPR_TYPE_VERSION_EQUALITY \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='" PCMK_VALUE_VERSION "' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_ATTRIBUTE "='ver' " \ + PCMK_XA_VALUE "='3.5' />" + +static void +type_version_equality(void **state) +{ + assert_attr_expression(EXPR_TYPE_VERSION_EQUALITY, pcmk_rc_ok); +} + +#define EXPR_TYPE_VERSION_FAILS \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='" PCMK_VALUE_VERSION "' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_GTE "' " \ + PCMK_XA_ATTRIBUTE "='ver' " \ + PCMK_XA_VALUE "='4.0' />" + +static void +type_version_fails(void **state) +{ + assert_attr_expression(EXPR_TYPE_VERSION_FAILS, pcmk_rc_before_range); +} + +/* + * Test PCMK_XA_OPERATION + */ + +#define EXPR_OP_MISSING \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_ATTRIBUTE "='foo' " \ + PCMK_XA_VALUE "='bar' />" + +static void +op_missing(void **state) +{ + assert_attr_expression(EXPR_OP_MISSING, pcmk_rc_unpack_error); +} + +#define EXPR_OP_INVALID \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_ATTRIBUTE "='foo' " \ + PCMK_XA_OPERATION "='not-an-operation' " \ + PCMK_XA_VALUE "='bar' />" + +static void +op_invalid(void **state) +{ + assert_attr_expression(EXPR_OP_INVALID, pcmk_rc_unpack_error); +} + +#define EXPR_OP_LT_PASSES \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_LT "' " \ + PCMK_XA_ATTRIBUTE "='num' " \ + PCMK_XA_VALUE "='20' />" + +static void +op_lt_passes(void **state) +{ + assert_attr_expression(EXPR_OP_LT_PASSES, pcmk_rc_ok); +} + +#define EXPR_OP_LT_FAILS \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_LT "' " \ + PCMK_XA_ATTRIBUTE "='num' " \ + PCMK_XA_VALUE "='2' />" + +static void +op_lt_fails(void **state) +{ + assert_attr_expression(EXPR_OP_LT_FAILS, pcmk_rc_after_range); +} + +#define EXPR_OP_GT_PASSES \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \ + PCMK_XA_ATTRIBUTE "='num' " \ + PCMK_XA_VALUE "='2' />" + +static void +op_gt_passes(void **state) +{ + assert_attr_expression(EXPR_OP_GT_PASSES, pcmk_rc_ok); +} + +#define EXPR_OP_GT_FAILS \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \ + PCMK_XA_ATTRIBUTE "='num' " \ + PCMK_XA_VALUE "='20' />" + +static void +op_gt_fails(void **state) +{ + assert_attr_expression(EXPR_OP_GT_FAILS, pcmk_rc_before_range); +} + +#define EXPR_OP_LTE_LT_PASSES \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_LTE "' " \ + PCMK_XA_ATTRIBUTE "='num' " \ + PCMK_XA_VALUE "='20' />" + +static void +op_lte_lt_passes(void **state) +{ + assert_attr_expression(EXPR_OP_LTE_LT_PASSES, pcmk_rc_ok); +} + +#define EXPR_OP_LTE_EQ_PASSES \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_LTE "' " \ + PCMK_XA_ATTRIBUTE "='num' " \ + PCMK_XA_VALUE "='10' />" + +static void +op_lte_eq_passes(void **state) +{ + assert_attr_expression(EXPR_OP_LTE_EQ_PASSES, pcmk_rc_ok); +} + +#define EXPR_OP_LTE_FAILS \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_LTE "' " \ + PCMK_XA_ATTRIBUTE "='num' " \ + PCMK_XA_VALUE "='9' />" + +static void +op_lte_fails(void **state) +{ + assert_attr_expression(EXPR_OP_LTE_FAILS, pcmk_rc_after_range); +} + +#define EXPR_OP_GTE_GT_PASSES \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_GTE "' " \ + PCMK_XA_ATTRIBUTE "='num' " \ + PCMK_XA_VALUE "='1' />" + +static void +op_gte_gt_passes(void **state) +{ + assert_attr_expression(EXPR_OP_GTE_GT_PASSES, pcmk_rc_ok); +} + +#define EXPR_OP_GTE_EQ_PASSES \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_GTE "' " \ + PCMK_XA_ATTRIBUTE "='num' " \ + PCMK_XA_VALUE "='10' />" + +static void +op_gte_eq_passes(void **state) +{ + assert_attr_expression(EXPR_OP_GTE_EQ_PASSES, pcmk_rc_ok); +} + +#define EXPR_OP_GTE_FAILS \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_GTE "' " \ + PCMK_XA_ATTRIBUTE "='num' " \ + PCMK_XA_VALUE "='11' />" + +static void +op_gte_fails(void **state) +{ + assert_attr_expression(EXPR_OP_GTE_FAILS, pcmk_rc_before_range); +} + +// This also tests that string is used if values aren't parseable as numbers +#define EXPR_OP_EQ_PASSES \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='" PCMK_VALUE_NUMBER "' " \ + PCMK_XA_ATTRIBUTE "='foo' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_VALUE "='bar' " \ + PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_LITERAL "' />" + +static void +op_eq_passes(void **state) +{ + assert_attr_expression(EXPR_OP_EQ_PASSES, pcmk_rc_ok); +} + +#define EXPR_OP_EQ_FAILS \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \ + PCMK_XA_ATTRIBUTE "='num' " \ + PCMK_XA_VALUE "='bar' />" + +static void +op_eq_fails(void **state) +{ + assert_attr_expression(EXPR_OP_EQ_FAILS, pcmk_rc_op_unsatisfied); +} + +#define EXPR_OP_NE_PASSES \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='" PCMK_VALUE_STRING "' " \ + PCMK_XA_ATTRIBUTE "='foo' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_NE "' " \ + PCMK_XA_VALUE "='bat' " \ + PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_LITERAL "' />" + +static void +op_ne_passes(void **state) +{ + assert_attr_expression(EXPR_OP_NE_PASSES, pcmk_rc_ok); +} + +#define EXPR_OP_NE_FAILS \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_NE "' " \ + PCMK_XA_ATTRIBUTE "='num' " \ + PCMK_XA_VALUE "='10' />" + +static void +op_ne_fails(void **state) +{ + assert_attr_expression(EXPR_OP_NE_FAILS, pcmk_rc_op_unsatisfied); +} + +#define EXPR_OP_DEFINED_PASSES \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_ATTRIBUTE "='foo' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_DEFINED "' />" + +static void +op_defined_passes(void **state) +{ + assert_attr_expression(EXPR_OP_DEFINED_PASSES, pcmk_rc_ok); +} + +#define EXPR_OP_DEFINED_FAILS \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_ATTRIBUTE "='boo' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_DEFINED "' />" + +static void +op_defined_fails(void **state) +{ + assert_attr_expression(EXPR_OP_DEFINED_FAILS, pcmk_rc_op_unsatisfied); +} + +#define EXPR_OP_DEFINED_WITH_VALUE \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_ATTRIBUTE "='foo' " \ + PCMK_XA_VALUE "='bar' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_DEFINED "' />" + +static void +op_defined_with_value(void **state) +{ + // Ill-formed but currently accepted + assert_attr_expression(EXPR_OP_DEFINED_WITH_VALUE, pcmk_rc_ok); +} + +#define EXPR_OP_UNDEFINED_PASSES \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_ATTRIBUTE "='boo' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_NOT_DEFINED "' />" + +static void +op_undefined_passes(void **state) +{ + assert_attr_expression(EXPR_OP_UNDEFINED_PASSES, pcmk_rc_ok); +} + +#define EXPR_OP_UNDEFINED_FAILS \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_ATTRIBUTE "='foo' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_NOT_DEFINED "' />" + +static void +op_undefined_fails(void **state) +{ + assert_attr_expression(EXPR_OP_DEFINED_FAILS, pcmk_rc_op_unsatisfied); +} + + +/* + * Test PCMK_XA_VALUE + */ + +#define EXPR_VALUE_MISSING_DEFINED_OK \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_ATTRIBUTE "='num' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_DEFINED "' />" + +static void +value_missing_defined_ok(void **state) +{ + assert_attr_expression(EXPR_VALUE_MISSING_DEFINED_OK, pcmk_rc_ok); +} + +#define EXPR_VALUE_MISSING_EQ_OK \ + "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_ATTRIBUTE "='not-an-attr' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' />" + +static void +value_missing_eq_ok(void **state) +{ + // Currently treated as NULL reference value + assert_attr_expression(EXPR_VALUE_MISSING_EQ_OK, pcmk_rc_ok); +} + + +#define expr_test(f) cmocka_unit_test_setup_teardown(f, setup, teardown) + +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL, + cmocka_unit_test(null_invalid), + expr_test(id_missing), + expr_test(attr_missing), + expr_test(attr_with_submatch_passes), + expr_test(attr_with_submatch_fails), + expr_test(source_missing), + expr_test(source_invalid), + expr_test(source_literal_passes), + expr_test(source_literal_value_fails), + expr_test(source_literal_attr_fails), + expr_test(source_params_missing), + expr_test(source_params_passes), + expr_test(source_params_fails), + expr_test(source_meta_missing), + expr_test(source_meta_passes), + expr_test(source_meta_fails), + expr_test(type_default_number), + expr_test(type_default_int), + expr_test(type_string_passes), + expr_test(type_string_fails), + expr_test(type_integer_passes), + expr_test(type_integer_fails), + expr_test(type_integer_truncation), + expr_test(type_number_passes), + expr_test(type_number_fails), + expr_test(type_version_passes), + expr_test(type_version_equality), + expr_test(type_version_fails), + expr_test(op_missing), + expr_test(op_invalid), + expr_test(op_lt_passes), + expr_test(op_lt_fails), + expr_test(op_gt_passes), + expr_test(op_gt_fails), + expr_test(op_lte_lt_passes), + expr_test(op_lte_eq_passes), + expr_test(op_lte_fails), + expr_test(op_gte_gt_passes), + expr_test(op_gte_eq_passes), + expr_test(op_gte_fails), + expr_test(op_eq_passes), + expr_test(op_eq_fails), + expr_test(op_ne_passes), + expr_test(op_ne_fails), + expr_test(op_defined_passes), + expr_test(op_defined_fails), + expr_test(op_defined_with_value), + expr_test(op_undefined_passes), + expr_test(op_undefined_fails), + expr_test(value_missing_defined_ok), + expr_test(value_missing_eq_ok)) diff --git a/lib/common/tests/rules/pcmk__evaluate_condition_test.c b/lib/common/tests/rules/pcmk__evaluate_condition_test.c new file mode 100644 index 0000000..bcb13a0 --- /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 <crm_internal.h> + +#include <stdio.h> +#include <glib.h> + +#include <crm/common/xml.h> +#include <crm/common/rules_internal.h> +#include <crm/common/unittest_internal.h> + +/* + * 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 "<not_an_expression " PCMK_XA_ID "='e' />" + +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(pcmk__xml_test_setup_group, 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_date_expression_test.c b/lib/common/tests/rules/pcmk__evaluate_date_expression_test.c new file mode 100644 index 0000000..df8dcbf --- /dev/null +++ b/lib/common/tests/rules/pcmk__evaluate_date_expression_test.c @@ -0,0 +1,684 @@ +/* + * 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 <crm_internal.h> + +#include <stdio.h> +#include <glib.h> + +#include <crm/common/xml.h> +#include <crm/common/rules_internal.h> +#include <crm/common/unittest_internal.h> +#include "crmcommon_private.h" + +/*! + * \internal + * \brief Run one test, comparing return value and output argument + * + * \param[in] xml Date expression XML + * \param[in] now_s Time to evaluate expression with (as string) + * \param[in] next_change_s If this and \p reference_s are not NULL, initialize + * next change time with this time (as string), + * and assert that its value after evaluation is the + * reference + * \param[in] reference_s If not NULL, time (as string) that next change + * should be after expression evaluation + * \param[in] reference_rc Assert that evaluation result equals this + */ +static void +assert_date_expression(const xmlNode *xml, const char *now_s, + const char *next_change_s, const char *reference_s, + int reference_rc) +{ + crm_time_t *now = NULL; + crm_time_t *next_change = NULL; + bool check_next_change = (next_change_s != NULL) && (reference_s != NULL); + + if (check_next_change) { + next_change = crm_time_new(next_change_s); + } + + now = crm_time_new(now_s); + assert_int_equal(pcmk__evaluate_date_expression(xml, now, next_change), + reference_rc); + crm_time_free(now); + + if (check_next_change) { + crm_time_t *reference = crm_time_new(reference_s); + + assert_int_equal(crm_time_compare(next_change, reference), 0); + crm_time_free(reference); + crm_time_free(next_change); + } +} + +#define EXPR_LT_VALID \ + "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_LT "' " \ + PCMK_XA_END "='2024-02-01 15:00:00' />" + +static void +null_invalid(void **state) +{ + xmlNodePtr xml = pcmk__xml_parse(EXPR_LT_VALID); + crm_time_t *t = crm_time_new("2024-02-01"); + + assert_int_equal(pcmk__evaluate_date_expression(NULL, NULL, NULL), EINVAL); + assert_int_equal(pcmk__evaluate_date_expression(xml, NULL, NULL), EINVAL); + assert_int_equal(pcmk__evaluate_date_expression(NULL, t, NULL), EINVAL); + + crm_time_free(t); + free_xml(xml); +} + +static void +null_next_change_ok(void **state) +{ + xmlNodePtr xml = pcmk__xml_parse(EXPR_LT_VALID); + + assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_within_range); + free_xml(xml); +} + +#define EXPR_ID_MISSING \ + "<" PCMK_XE_DATE_EXPRESSION " " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_LT "' " \ + PCMK_XA_END "='2024-02-01 15:00:00' />" + +static void +id_missing(void **state) +{ + // Currently acceptable + xmlNodePtr xml = pcmk__xml_parse(EXPR_ID_MISSING); + + assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_within_range); + free_xml(xml); +} + +#define EXPR_OP_INVALID \ + "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_OPERATION "='not-a-choice' />" + +static void +op_invalid(void **state) +{ + xmlNodePtr xml = pcmk__xml_parse(EXPR_OP_INVALID); + + assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined); + free_xml(xml); +} + +#define EXPR_LT_MISSING_END \ + "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_LT "' />" + +static void +lt_missing_end(void **state) +{ + xmlNodePtr xml = pcmk__xml_parse(EXPR_LT_MISSING_END); + + assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined); + free_xml(xml); +} + +#define EXPR_LT_INVALID_END \ + "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_LT "' " \ + PCMK_XA_END "='not-a-datetime' />" + +static void +lt_invalid_end(void **state) +{ + xmlNodePtr xml = pcmk__xml_parse(EXPR_LT_INVALID_END); + + assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined); + free_xml(xml); +} + +static void +lt_valid(void **state) +{ + xmlNodePtr xml = pcmk__xml_parse(EXPR_LT_VALID); + + // Now and next change are both before end + assert_date_expression(xml, "2023-01-01 05:00:00", "2024-02-01 10:00:00", + "2024-02-01 10:00:00", pcmk_rc_within_range); + + // Now is before end, next change is after end + assert_date_expression(xml, "2024-02-01 14:59:59", "2024-02-01 18:00:00", + "2024-02-01 15:00:00", pcmk_rc_within_range); + + // Now is equal to end, next change is after end + assert_date_expression(xml, "2024-02-01 15:00:00", "2024-02-01 20:00:00", + "2024-02-01 20:00:00", pcmk_rc_after_range); + + // Now and next change are both after end + assert_date_expression(xml, "2024-03-01 12:00:00", "2024-02-01 20:00:00", + "2024-02-01 20:00:00", pcmk_rc_after_range); + + free_xml(xml); +} + +#define EXPR_GT_MISSING_START \ + "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' />" + +static void +gt_missing_start(void **state) +{ + xmlNodePtr xml = pcmk__xml_parse(EXPR_GT_MISSING_START); + + assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined); + free_xml(xml); +} + +#define EXPR_GT_INVALID_START \ + "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \ + PCMK_XA_START "='not-a-datetime' />" + +static void +gt_invalid_start(void **state) +{ + xmlNodePtr xml = pcmk__xml_parse(EXPR_GT_INVALID_START); + + assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined); + free_xml(xml); +} + +#define EXPR_GT_VALID \ + "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \ + PCMK_XA_START "='2024-02-01 12:00:00' />" + +static void +gt_valid(void **state) +{ + xmlNodePtr xml = pcmk__xml_parse(EXPR_GT_VALID); + + // Now and next change are both before start + assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00", + "2024-01-01 11:00:00", pcmk_rc_before_range); + + // Now is before start, next change is after start + assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 18:00:00", + "2024-02-01 12:00:01", pcmk_rc_before_range); + + // Now is equal to start, next change is after start + assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 18:00:00", + "2024-02-01 12:00:01", pcmk_rc_before_range); + + // Now is one second after start, next change is after start + assert_date_expression(xml, "2024-02-01 12:00:01", "2024-02-01 18:00:00", + "2024-02-01 18:00:00", pcmk_rc_within_range); + + // t is after start, next change is after start + assert_date_expression(xml, "2024-03-01 05:03:11", "2024-04-04 04:04:04", + "2024-04-04 04:04:04", pcmk_rc_within_range); + + free_xml(xml); +} + +#define EXPR_RANGE_MISSING \ + "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' />" + +static void +range_missing(void **state) +{ + xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_MISSING); + crm_time_t *t = crm_time_new("2024-01-01"); + + assert_int_equal(pcmk__evaluate_date_expression(xml, t, NULL), + pcmk_rc_undetermined); + + crm_time_free(t); + free_xml(xml); +} + +#define EXPR_RANGE_INVALID_START_INVALID_END \ + "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \ + PCMK_XA_START "='not-a-date' " \ + PCMK_XA_END "='not-a-date' />" + +static void +range_invalid_start_invalid_end(void **state) +{ + xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_INVALID_START_INVALID_END); + + assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined); + free_xml(xml); +} + +#define EXPR_RANGE_INVALID_START_ONLY \ + "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \ + PCMK_XA_START "='not-a-date' />" + +static void +range_invalid_start_only(void **state) +{ + xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_INVALID_START_ONLY); + + assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined); + free_xml(xml); +} + +#define EXPR_RANGE_VALID_START_ONLY \ + "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \ + PCMK_XA_START "='2024-02-01 12:00:00' />" + +static void +range_valid_start_only(void **state) +{ + xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_ONLY); + + // Now and next change are before start + assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00", + "2024-01-01 11:00:00", pcmk_rc_before_range); + + // Now is before start, next change is after start + assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 18:00:00", + "2024-02-01 12:00:00", pcmk_rc_before_range); + + // Now is equal to start, next change is after start + assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 18:00:00", + "2024-02-01 18:00:00", pcmk_rc_within_range); + + // Now and next change are after start + assert_date_expression(xml, "2024-03-01 05:03:11", "2024-04-04 04:04:04", + "2024-04-04 04:04:04", pcmk_rc_within_range); + + free_xml(xml); +} + +#define EXPR_RANGE_INVALID_END_ONLY \ + "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \ + PCMK_XA_END "='not-a-date' />" + +static void +range_invalid_end_only(void **state) +{ + xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_INVALID_END_ONLY); + + assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined); + free_xml(xml); +} + +#define EXPR_RANGE_VALID_END_ONLY \ + "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \ + PCMK_XA_END "='2024-02-01 15:00:00' />" + +static void +range_valid_end_only(void **state) +{ + xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_END_ONLY); + + // Now and next change are before end + assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00", + "2024-01-01 11:00:00", pcmk_rc_within_range); + + // Now is before end, next change is after end + assert_date_expression(xml, "2024-02-01 14:59:59", "2024-02-01 18:00:00", + "2024-02-01 15:00:01", pcmk_rc_within_range); + + // Now is equal to end, next change is after end + assert_date_expression(xml, "2024-02-01 15:00:00", "2024-02-01 18:00:00", + "2024-02-01 15:00:01", pcmk_rc_within_range); + + // Now and next change are after end + assert_date_expression(xml, "2024-02-01 15:00:01", "2024-04-04 04:04:04", + "2024-04-04 04:04:04", pcmk_rc_after_range); + + free_xml(xml); +} + +#define EXPR_RANGE_VALID_START_INVALID_END \ + "<" 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 "='not-a-date' />" + +static void +range_valid_start_invalid_end(void **state) +{ + // Currently treated same as start without end + xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_INVALID_END); + + // Now and next change are before start + assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00", + "2024-01-01 11:00:00", pcmk_rc_before_range); + + // Now is before start, next change is after start + assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 18:00:00", + "2024-02-01 12:00:00", pcmk_rc_before_range); + + // Now is equal to start, next change is after start + assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 18:00:00", + "2024-02-01 18:00:00", pcmk_rc_within_range); + + // Now and next change are after start + assert_date_expression(xml, "2024-03-01 05:03:11", "2024-04-04 04:04:04", + "2024-04-04 04:04:04", pcmk_rc_within_range); + + free_xml(xml); +} + +#define EXPR_RANGE_INVALID_START_VALID_END \ + "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \ + PCMK_XA_START "='not-a-date' " \ + PCMK_XA_END "='2024-02-01 15:00:00' />" + +static void +range_invalid_start_valid_end(void **state) +{ + // Currently treated same as end without start + xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_INVALID_START_VALID_END); + + // Now and next change are before end + assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00", + "2024-01-01 11:00:00", pcmk_rc_within_range); + + // Now is before end, next change is after end + assert_date_expression(xml, "2024-02-01 14:59:59", "2024-02-01 18:00:00", + "2024-02-01 15:00:01", pcmk_rc_within_range); + + // Now is equal to end, next change is after end + assert_date_expression(xml, "2024-02-01 15:00:00", "2024-02-01 18:00:00", + "2024-02-01 15:00:01", pcmk_rc_within_range); + + // Now and next change are after end + assert_date_expression(xml, "2024-02-01 15:00:01", "2024-04-04 04:04:04", + "2024-04-04 04:04:04", pcmk_rc_after_range); + + free_xml(xml); +} + +#define EXPR_RANGE_VALID_START_VALID_END \ + "<" 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 +range_valid_start_valid_end(void **state) +{ + xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_VALID_END); + + // Now and next change are before start + assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00", + "2024-01-01 11:00:00", pcmk_rc_before_range); + + // Now is before start, next change is between start and end + assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 14:00:00", + "2024-02-01 12:00:00", pcmk_rc_before_range); + + // Now is equal to start, next change is between start and end + assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 14:30:00", + "2024-02-01 14:30:00", pcmk_rc_within_range); + + // Now is between start and end, next change is after end + assert_date_expression(xml, "2024-02-01 14:03:11", "2024-04-04 04:04:04", + "2024-02-01 15:00:01", pcmk_rc_within_range); + + // Now is equal to end, next change is after end + assert_date_expression(xml, "2024-02-01 15:00:00", "2028-04-04 04:04:04", + "2024-02-01 15:00:01", pcmk_rc_within_range); + + // Now and next change are after end + assert_date_expression(xml, "2024-02-01 15:00:01", "2028-04-04 04:04:04", + "2028-04-04 04:04:04", pcmk_rc_after_range); + + free_xml(xml); +} + +#define EXPR_RANGE_VALID_START_INVALID_DURATION \ + "<" 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_XE_DURATION " " PCMK_XA_ID "='d' " \ + PCMK_XA_HOURS "='not-a-number' />" \ + "</" PCMK_XE_DATE_EXPRESSION ">" + +static void +range_valid_start_invalid_duration(void **state) +{ + // Currently treated same as end equals start + xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_INVALID_DURATION); + + // Now and next change are before start + assert_date_expression(xml, "2024-02-01 04:30:05", "2024-01-01 11:00:00", + "2024-01-01 11:00:00", pcmk_rc_before_range); + + // Now is before start, next change is after start + assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 18:00:00", + "2024-02-01 12:00:00", pcmk_rc_before_range); + + // Now is equal to start, next change is after start + assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 14:30:00", + "2024-02-01 12:00:01", pcmk_rc_within_range); + + // Now and next change are after start + assert_date_expression(xml, "2024-02-01 12:00:01", "2024-02-01 14:30:00", + "2024-02-01 14:30:00", pcmk_rc_after_range); + + free_xml(xml); +} + +#define EXPR_RANGE_VALID_START_VALID_DURATION \ + "<" 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_XE_DURATION " " PCMK_XA_ID "='d' " \ + PCMK_XA_HOURS "='3' />" \ + "</" PCMK_XE_DATE_EXPRESSION ">" + +static void +range_valid_start_valid_duration(void **state) +{ + xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_VALID_DURATION); + + // Now and next change are before start + assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00", + "2024-01-01 11:00:00", pcmk_rc_before_range); + + // Now is before start, next change is between start and end + assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 14:00:00", + "2024-02-01 12:00:00", pcmk_rc_before_range); + + // Now is equal to start, next change is between start and end + assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 14:30:00", + "2024-02-01 14:30:00", pcmk_rc_within_range); + + // Now is between start and end, next change is after end + assert_date_expression(xml, "2024-02-01 14:03:11", "2024-04-04 04:04:04", + "2024-02-01 15:00:01", pcmk_rc_within_range); + + // Now is equal to end, next change is after end + assert_date_expression(xml, "2024-02-01 15:00:00", "2028-04-04 04:04:04", + "2024-02-01 15:00:01", pcmk_rc_within_range); + + // Now and next change are after end + assert_date_expression(xml, "2024-02-01 15:00:01", "2028-04-04 04:04:04", + "2028-04-04 04:04:04", pcmk_rc_after_range); + + free_xml(xml); +} + +#define EXPR_RANGE_VALID_START_DURATION_MISSING_ID \ + "<" PCMK_XE_DATE_EXPRESSION " " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \ + PCMK_XA_START "='2024-02-01 12:00:00'>" \ + "<" PCMK_XE_DURATION " " PCMK_XA_ID "='d' " \ + PCMK_XA_HOURS "='3' />" \ + "</" PCMK_XE_DATE_EXPRESSION ">" + +static void +range_valid_start_duration_missing_id(void **state) +{ + // Currently acceptable + xmlNodePtr xml = NULL; + + xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_DURATION_MISSING_ID); + + // Now and next change are before start + assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00", + "2024-01-01 11:00:00", pcmk_rc_before_range); + + // Now is before start, next change is between start and end + assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 14:00:00", + "2024-02-01 12:00:00", pcmk_rc_before_range); + + // Now is equal to start, next change is between start and end + assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 14:30:00", + "2024-02-01 14:30:00", pcmk_rc_within_range); + + // Now is between start and end, next change is after end + assert_date_expression(xml, "2024-02-01 14:03:11", "2024-04-04 04:04:04", + "2024-02-01 15:00:01", pcmk_rc_within_range); + + // Now is equal to end, next change is after end + assert_date_expression(xml, "2024-02-01 15:00:00", "2028-04-04 04:04:04", + "2024-02-01 15:00:01", pcmk_rc_within_range); + + // Now and next change are after end + assert_date_expression(xml, "2024-02-01 15:00:01", "2028-04-04 04:04:04", + "2028-04-04 04:04:04", pcmk_rc_after_range); + + free_xml(xml); +} + +#define EXPR_SPEC_MISSING \ + "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_DATE_SPEC "' />" + +static void +spec_missing(void **state) +{ + xmlNodePtr xml = pcmk__xml_parse(EXPR_SPEC_MISSING); + + assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined); + free_xml(xml); +} + +#define EXPR_SPEC_INVALID \ + "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_DATE_SPEC "'>" \ + "<" PCMK_XE_DATE_SPEC " " PCMK_XA_ID "='s' " \ + PCMK_XA_MONTHS "='not-a-number'/>" \ + "</" PCMK_XE_DATE_EXPRESSION ">" + +static void +spec_invalid(void **state) +{ + // Currently treated as date_spec with no ranges (which passes) + xmlNodePtr xml = pcmk__xml_parse(EXPR_SPEC_INVALID); + + assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_ok); + free_xml(xml); +} + +#define EXPR_SPEC_VALID \ + "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_DATE_SPEC "'>" \ + "<" PCMK_XE_DATE_SPEC " " PCMK_XA_ID "='s' " \ + PCMK_XA_MONTHS "='2'/>" \ + "</" PCMK_XE_DATE_EXPRESSION ">" + +static void +spec_valid(void **state) +{ + // date_spec does not currently support next_change + xmlNodePtr xml = pcmk__xml_parse(EXPR_SPEC_VALID); + + // Now is just before spec start + assert_date_expression(xml, "2024-01-01 23:59:59", NULL, NULL, + pcmk_rc_before_range); + + // Now matches spec start + assert_date_expression(xml, "2024-02-01 00:00:00", NULL, NULL, pcmk_rc_ok); + + // Now is within spec range + assert_date_expression(xml, "2024-02-22 22:22:22", NULL, NULL, pcmk_rc_ok); + + // Now matches spec end + assert_date_expression(xml, "2024-02-29 23:59:59", NULL, NULL, pcmk_rc_ok); + + // Now is just past spec end + assert_date_expression(xml, "2024-03-01 00:00:00", NULL, NULL, + pcmk_rc_after_range); + + free_xml(xml); +} + +#define EXPR_SPEC_MISSING_ID \ + "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_OPERATION "='" PCMK_VALUE_DATE_SPEC "'>" \ + "<" PCMK_XE_DATE_SPEC " " \ + PCMK_XA_MONTHS "='2'/>" \ + "</" PCMK_XE_DATE_EXPRESSION ">" + +static void +spec_missing_id(void **state) +{ + // Currently acceptable; date_spec does not currently support next_change + xmlNodePtr xml = pcmk__xml_parse(EXPR_SPEC_MISSING_ID); + + // Now is just before spec start + assert_date_expression(xml, "2024-01-01 23:59:59", NULL, NULL, + pcmk_rc_before_range); + + // Now matches spec start + assert_date_expression(xml, "2024-02-01 00:00:00", NULL, NULL, pcmk_rc_ok); + + // Now is within spec range + assert_date_expression(xml, "2024-02-22 22:22:22", NULL, NULL, pcmk_rc_ok); + + // Now matches spec end + assert_date_expression(xml, "2024-02-29 23:59:59", NULL, NULL, pcmk_rc_ok); + + // Now is just past spec end + assert_date_expression(xml, "2024-03-01 00:00:00", NULL, NULL, + pcmk_rc_after_range); + + free_xml(xml); +} + +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL, + cmocka_unit_test(null_invalid), + cmocka_unit_test(null_next_change_ok), + cmocka_unit_test(id_missing), + cmocka_unit_test(op_invalid), + cmocka_unit_test(lt_missing_end), + cmocka_unit_test(lt_invalid_end), + cmocka_unit_test(lt_valid), + cmocka_unit_test(gt_missing_start), + cmocka_unit_test(gt_invalid_start), + cmocka_unit_test(gt_valid), + cmocka_unit_test(range_missing), + cmocka_unit_test(range_invalid_start_invalid_end), + cmocka_unit_test(range_invalid_start_only), + cmocka_unit_test(range_valid_start_only), + cmocka_unit_test(range_invalid_end_only), + cmocka_unit_test(range_valid_end_only), + cmocka_unit_test(range_valid_start_invalid_end), + cmocka_unit_test(range_invalid_start_valid_end), + cmocka_unit_test(range_valid_start_valid_end), + cmocka_unit_test(range_valid_start_invalid_duration), + cmocka_unit_test(range_valid_start_valid_duration), + cmocka_unit_test(range_valid_start_duration_missing_id), + cmocka_unit_test(spec_missing), + cmocka_unit_test(spec_invalid), + cmocka_unit_test(spec_valid), + cmocka_unit_test(spec_missing_id)) diff --git a/lib/common/tests/rules/pcmk__evaluate_date_spec_test.c b/lib/common/tests/rules/pcmk__evaluate_date_spec_test.c new file mode 100644 index 0000000..6048adf --- /dev/null +++ b/lib/common/tests/rules/pcmk__evaluate_date_spec_test.c @@ -0,0 +1,231 @@ +/* + * 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 <crm_internal.h> + +#include <errno.h> +#include <glib.h> + +#include <crm/common/xml.h> +#include <crm/common/rules_internal.h> +#include <crm/common/unittest_internal.h> +#include "crmcommon_private.h" + +static void +run_one_test(const char *t, const char *x, int expected) +{ + crm_time_t *tm = crm_time_new(t); + xmlNodePtr xml = pcmk__xml_parse(x); + + assert_int_equal(pcmk__evaluate_date_spec(xml, tm), expected); + + crm_time_free(tm); + free_xml(xml); +} + +static void +null_invalid(void **state) +{ + xmlNodePtr xml = pcmk__xml_parse("<" PCMK_XE_DATE_SPEC " " + PCMK_XA_ID "='spec' " + PCMK_XA_YEARS "='2019'/>"); + crm_time_t *tm = crm_time_new(NULL); + + assert_int_equal(pcmk__evaluate_date_spec(NULL, NULL), EINVAL); + assert_int_equal(pcmk__evaluate_date_spec(xml, NULL), EINVAL); + assert_int_equal(pcmk__evaluate_date_spec(NULL, tm), EINVAL); + + crm_time_free(tm); + free_xml(xml); +} + +static void +spec_id_missing(void **state) +{ + // Currently acceptable + run_one_test("2020-01-01", "<date_spec years='2020'/>", pcmk_rc_ok); +} + +static void +invalid_range(void **state) +{ + // Currently acceptable + run_one_test("2020-01-01", "<date_spec years='not-a-year' months='1'/>", + pcmk_rc_ok); +} + +static void +time_satisfies_year_spec(void **state) +{ + run_one_test("2020-01-01", + "<date_spec " PCMK_XA_ID "='spec' years='2020'/>", + pcmk_rc_ok); +} + +static void +time_after_year_spec(void **state) +{ + run_one_test("2020-01-01", + "<" PCMK_XE_DATE_SPEC " " + PCMK_XA_ID "='spec' " + PCMK_XA_YEARS "='2019'/>", + pcmk_rc_after_range); +} + +static void +time_satisfies_year_range(void **state) +{ + run_one_test("2020-01-01", + "<" PCMK_XE_DATE_SPEC " " + PCMK_XA_ID "='spec' " + PCMK_XA_YEARS "='2010-2030'/>", + pcmk_rc_ok); +} + +static void +time_before_year_range(void **state) +{ + run_one_test("2000-01-01", + "<" PCMK_XE_DATE_SPEC " " + PCMK_XA_ID "='spec' " + PCMK_XA_YEARS "='2010-2030'/>", + pcmk_rc_before_range); +} + +static void +time_after_year_range(void **state) +{ + run_one_test("2020-01-01", + "<" PCMK_XE_DATE_SPEC " " + PCMK_XA_ID "='spec' " + PCMK_XA_YEARS "='2010-2015'/>", + pcmk_rc_after_range); +} + +static void +range_without_start_year_passes(void **state) +{ + run_one_test("2010-01-01", + "<" PCMK_XE_DATE_SPEC " " + PCMK_XA_ID "='spec' " + PCMK_XA_YEARS "='-2020'/>", + pcmk_rc_ok); +} + +static void +range_without_end_year_passes(void **state) +{ + run_one_test("2010-01-01", + "<" PCMK_XE_DATE_SPEC " " + PCMK_XA_ID "='spec' " + PCMK_XA_YEARS "='2000-'/>", + pcmk_rc_ok); + run_one_test("2000-10-01", + "<" PCMK_XE_DATE_SPEC " " + PCMK_XA_ID "='spec' " + PCMK_XA_YEARS "='2000-'/>", + pcmk_rc_ok); +} + +static void +yeardays_satisfies(void **state) +{ + run_one_test("2020-01-30", + "<" PCMK_XE_DATE_SPEC " " + PCMK_XA_ID "='spec' " + PCMK_XA_YEARDAYS "='30'/>", + pcmk_rc_ok); +} + +static void +time_after_yeardays_spec(void **state) +{ + run_one_test("2020-02-15", + "<" PCMK_XE_DATE_SPEC " " + PCMK_XA_ID "='spec' " + PCMK_XA_YEARDAYS "='40'/>", + pcmk_rc_after_range); +} + +static void +yeardays_feb_29_satisfies(void **state) +{ + run_one_test("2016-02-29", + "<" PCMK_XE_DATE_SPEC " " + PCMK_XA_ID "='spec' " + PCMK_XA_YEARDAYS "='60'/>", + pcmk_rc_ok); +} + +static void +exact_ymd_satisfies(void **state) +{ + run_one_test("2001-12-31", + "<" PCMK_XE_DATE_SPEC " " + PCMK_XA_ID "='spec' " + PCMK_XA_YEARS "='2001' " + PCMK_XA_MONTHS "='12' " + PCMK_XA_MONTHDAYS "='31'/>", + pcmk_rc_ok); +} + +static void +range_in_month_satisfies(void **state) +{ + run_one_test("2001-06-10", + "<" PCMK_XE_DATE_SPEC " " + PCMK_XA_ID "='spec' " + PCMK_XA_YEARS "='2001' " + PCMK_XA_MONTHS "='6' " + PCMK_XA_MONTHDAYS "='1-10'/>", + pcmk_rc_ok); +} + +static void +exact_ymd_after_range(void **state) +{ + run_one_test("2001-12-31", + "<" PCMK_XE_DATE_SPEC " " + PCMK_XA_ID "='spec' " + PCMK_XA_YEARS "='2001' " + PCMK_XA_MONTHS "='12' " + PCMK_XA_MONTHDAYS "='30'/>", + pcmk_rc_after_range); +} + +static void +time_after_monthdays_range(void **state) +{ + run_one_test("2001-06-10", + "<" PCMK_XE_DATE_SPEC " " + PCMK_XA_ID "='spec' " + PCMK_XA_YEARS "='2001' " + PCMK_XA_MONTHS "='6' " + PCMK_XA_MONTHDAYS "='11-15'/>", + pcmk_rc_before_range); +} + +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL, + cmocka_unit_test(null_invalid), + cmocka_unit_test(spec_id_missing), + cmocka_unit_test(invalid_range), + cmocka_unit_test(time_satisfies_year_spec), + cmocka_unit_test(time_after_year_spec), + cmocka_unit_test(time_satisfies_year_range), + cmocka_unit_test(time_before_year_range), + cmocka_unit_test(time_after_year_range), + cmocka_unit_test(range_without_start_year_passes), + cmocka_unit_test(range_without_end_year_passes), + cmocka_unit_test(yeardays_satisfies), + cmocka_unit_test(time_after_yeardays_spec), + cmocka_unit_test(yeardays_feb_29_satisfies), + cmocka_unit_test(exact_ymd_satisfies), + cmocka_unit_test(range_in_month_satisfies), + cmocka_unit_test(exact_ymd_after_range), + cmocka_unit_test(time_after_monthdays_range)) diff --git a/lib/common/tests/rules/pcmk__evaluate_op_expression_test.c b/lib/common/tests/rules/pcmk__evaluate_op_expression_test.c new file mode 100644 index 0000000..d1cb35f --- /dev/null +++ b/lib/common/tests/rules/pcmk__evaluate_op_expression_test.c @@ -0,0 +1,207 @@ +/* + * 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 <crm_internal.h> + +#include <stdio.h> +#include <glib.h> + +#include <crm/common/xml.h> +#include <crm/common/rules_internal.h> +#include <crm/common/unittest_internal.h> +#include "crmcommon_private.h" + +/* + * Shared data + */ + +static pcmk_rule_input_t rule_input = { + // These are the only members used to evaluate operation expressions + .op_name = PCMK_ACTION_MONITOR, + .op_interval_ms = 10000, +}; + +/*! + * \internal + * \brief Run one test, comparing return value + * + * \param[in] xml_string Operation expression XML as string + * \param[in] reference_rc Assert that evaluation result equals this + */ +static void +assert_op_expression(const char *xml_string, int reference_rc) +{ + xmlNode *xml = pcmk__xml_parse(xml_string); + + assert_int_equal(pcmk__evaluate_op_expression(xml, &rule_input), + reference_rc); + free_xml(xml); +} + + +/* + * Invalid arguments + */ + +#define EXPR_FAIL_BOTH \ + "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_NAME "='" PCMK_ACTION_START "' " \ + PCMK_XA_INTERVAL "='0' />" + +static void +null_invalid(void **state) +{ + xmlNode *xml = NULL; + + assert_int_equal(pcmk__evaluate_op_expression(NULL, NULL), EINVAL); + + xml = pcmk__xml_parse(EXPR_FAIL_BOTH); + assert_int_equal(pcmk__evaluate_op_expression(xml, NULL), EINVAL); + free_xml(xml); + + assert_op_expression(NULL, EINVAL); +} + + +/* + * Test PCMK_XA_ID + */ + +#define EXPR_ID_MISSING \ + "<" PCMK_XE_OP_EXPRESSION " " \ + PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ + PCMK_XA_INTERVAL "='10s' />" + +#define EXPR_ID_EMPTY \ + "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='' " \ + PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ + PCMK_XA_INTERVAL "='10s' />" + +static void +id_missing(void **state) +{ + // Currently acceptable + assert_op_expression(EXPR_ID_MISSING, pcmk_rc_ok); + assert_op_expression(EXPR_ID_EMPTY, pcmk_rc_ok); +} + + +/* + * Test PCMK_XA_NAME + */ + +#define EXPR_NAME_MISSING \ + "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_INTERVAL "='10s' />" + +static void +name_missing(void **state) +{ + assert_op_expression(EXPR_NAME_MISSING, pcmk_rc_unpack_error); +} + +#define EXPR_MATCH_BOTH \ + "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ + PCMK_XA_INTERVAL "='10s' />" + +#define EXPR_EMPTY_NAME \ + "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_NAME "='' " PCMK_XA_INTERVAL "='10s' />" + +static void +input_name_missing(void **state) +{ + rule_input.op_name = NULL; + assert_op_expression(EXPR_MATCH_BOTH, pcmk_rc_op_unsatisfied); + assert_op_expression(EXPR_EMPTY_NAME, pcmk_rc_op_unsatisfied); + rule_input.op_name = PCMK_ACTION_MONITOR; +} + +#define EXPR_FAIL_NAME \ + "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_NAME "='" PCMK_ACTION_START "' " \ + PCMK_XA_INTERVAL "='10s' />" + +static void +fail_name(void **state) +{ + assert_op_expression(EXPR_FAIL_NAME, pcmk_rc_op_unsatisfied); + + // An empty name is meaningless but accepted, so not an unpack error + assert_op_expression(EXPR_EMPTY_NAME, pcmk_rc_op_unsatisfied); +} + + +/* + * Test PCMK_XA_INTERVAL + */ + +#define EXPR_EMPTY_INTERVAL \ + "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ + PCMK_XA_INTERVAL "='' />" + +#define EXPR_INVALID_INTERVAL \ + "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ + PCMK_XA_INTERVAL "='not-an-interval' />" + +static void +invalid_interval(void **state) +{ + assert_op_expression(EXPR_EMPTY_INTERVAL, pcmk_rc_unpack_error); + assert_op_expression(EXPR_INVALID_INTERVAL, pcmk_rc_unpack_error); +} + +#define EXPR_DEFAULT_INTERVAL \ + "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' />" + +static void +default_interval(void **state) +{ + assert_op_expression(EXPR_DEFAULT_INTERVAL, pcmk_rc_ok); +} + +#define EXPR_FAIL_INTERVAL \ + "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \ + PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \ + PCMK_XA_INTERVAL "='9s' />" + +static void +fail_interval(void **state) +{ + assert_op_expression(EXPR_FAIL_INTERVAL, pcmk_rc_op_unsatisfied); +} + + +static void +match_both(void **state) +{ + assert_op_expression(EXPR_MATCH_BOTH, pcmk_rc_ok); +} + +static void +fail_both(void **state) +{ + assert_op_expression(EXPR_FAIL_BOTH, pcmk_rc_op_unsatisfied); +} + +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL, + cmocka_unit_test(null_invalid), + cmocka_unit_test(id_missing), + cmocka_unit_test(name_missing), + cmocka_unit_test(input_name_missing), + cmocka_unit_test(fail_name), + cmocka_unit_test(invalid_interval), + cmocka_unit_test(default_interval), + cmocka_unit_test(fail_interval), + cmocka_unit_test(match_both), + cmocka_unit_test(fail_both)) diff --git a/lib/common/tests/rules/pcmk__evaluate_rsc_expression_test.c b/lib/common/tests/rules/pcmk__evaluate_rsc_expression_test.c new file mode 100644 index 0000000..c3a164e --- /dev/null +++ b/lib/common/tests/rules/pcmk__evaluate_rsc_expression_test.c @@ -0,0 +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 <crm_internal.h> + +#include <stdio.h> +#include <glib.h> + +#include <crm/common/xml.h> +#include <crm/common/rules_internal.h> +#include <crm/common/unittest_internal.h> +#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(pcmk__xml_test_setup_group, 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 0000000..afebcf8 --- /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 <crm_internal.h> + +#include <stdio.h> + +#include <crm/common/rules_internal.h> +#include <crm/common/unittest_internal.h> + +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__parse_comparison_test.c b/lib/common/tests/rules/pcmk__parse_comparison_test.c new file mode 100644 index 0000000..a995596 --- /dev/null +++ b/lib/common/tests/rules/pcmk__parse_comparison_test.c @@ -0,0 +1,72 @@ +/* + * Copyright 2024 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <stdio.h> + +#include <crm/common/rules_internal.h> +#include <crm/common/unittest_internal.h> +#include "crmcommon_private.h" + +static void +null_unknown(void **state) +{ + assert_int_equal(pcmk__parse_comparison(NULL), pcmk__comparison_unknown); +} + +static void +invalid(void **state) +{ + assert_int_equal(pcmk__parse_comparison("nope"), pcmk__comparison_unknown); +} + +static void +valid(void **state) +{ + assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_DEFINED), + pcmk__comparison_defined); + + assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_NOT_DEFINED), + pcmk__comparison_undefined); + + assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_EQ), + pcmk__comparison_eq); + + assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_NE), + pcmk__comparison_ne); + + assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_LT), + pcmk__comparison_lt); + + assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_LTE), + pcmk__comparison_lte); + + assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_GT), + pcmk__comparison_gt); + + assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_GTE), + pcmk__comparison_gte); +} + +static void +case_insensitive(void **state) +{ + assert_int_equal(pcmk__parse_comparison("DEFINED"), + pcmk__comparison_defined); + + assert_int_equal(pcmk__parse_comparison("Not_Defined"), + pcmk__comparison_undefined); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(null_unknown), + cmocka_unit_test(invalid), + cmocka_unit_test(valid), + cmocka_unit_test(case_insensitive)) diff --git a/lib/common/tests/rules/pcmk__parse_source_test.c b/lib/common/tests/rules/pcmk__parse_source_test.c new file mode 100644 index 0000000..9cf9b32 --- /dev/null +++ b/lib/common/tests/rules/pcmk__parse_source_test.c @@ -0,0 +1,62 @@ +/* + * Copyright 2024 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <stdio.h> + +#include <crm/common/rules_internal.h> +#include <crm/common/unittest_internal.h> +#include "crmcommon_private.h" + +static void +default_literal(void **state) +{ + assert_int_equal(pcmk__parse_source(NULL), pcmk__source_literal); +} + +static void +invalid(void **state) +{ + assert_int_equal(pcmk__parse_source(""), pcmk__source_unknown); + assert_int_equal(pcmk__parse_source(" "), pcmk__source_unknown); + assert_int_equal(pcmk__parse_source("params"), pcmk__source_unknown); +} + +static void +valid(void **state) +{ + assert_int_equal(pcmk__parse_source(PCMK_VALUE_LITERAL), + pcmk__source_literal); + + assert_int_equal(pcmk__parse_source(PCMK_VALUE_PARAM), + pcmk__source_instance_attrs); + + assert_int_equal(pcmk__parse_source(PCMK_VALUE_META), + pcmk__source_meta_attrs); +} + +static void +case_insensitive(void **state) +{ + assert_int_equal(pcmk__parse_source("LITERAL"), + pcmk__source_literal); + + assert_int_equal(pcmk__parse_source("Param"), + pcmk__source_instance_attrs); + + assert_int_equal(pcmk__parse_source("MeTa"), + pcmk__source_meta_attrs); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(default_literal), + cmocka_unit_test(invalid), + cmocka_unit_test(valid), + cmocka_unit_test(case_insensitive)) diff --git a/lib/common/tests/rules/pcmk__parse_type_test.c b/lib/common/tests/rules/pcmk__parse_type_test.c new file mode 100644 index 0000000..96f02c8 --- /dev/null +++ b/lib/common/tests/rules/pcmk__parse_type_test.c @@ -0,0 +1,127 @@ +/* + * Copyright 2024 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <stdio.h> + +#include <crm/common/rules_internal.h> +#include <crm/common/unittest_internal.h> +#include "crmcommon_private.h" + +static void +invalid(void **state) +{ + assert_int_equal(pcmk__parse_type("nope", pcmk__comparison_unknown, + NULL, NULL), + pcmk__type_unknown); +} + +static void +valid(void **state) +{ + assert_int_equal(pcmk__parse_type(PCMK_VALUE_STRING, + pcmk__comparison_unknown, NULL, NULL), + pcmk__type_string); + + assert_int_equal(pcmk__parse_type(PCMK_VALUE_INTEGER, + pcmk__comparison_unknown, NULL, NULL), + pcmk__type_integer); + + assert_int_equal(pcmk__parse_type(PCMK_VALUE_NUMBER, + pcmk__comparison_unknown, NULL, NULL), + pcmk__type_number); + + assert_int_equal(pcmk__parse_type(PCMK_VALUE_VERSION, + pcmk__comparison_unknown, NULL, NULL), + pcmk__type_version); +} + +static void +case_insensitive(void **state) +{ + assert_int_equal(pcmk__parse_type("STRING", pcmk__comparison_unknown, + NULL, NULL), + pcmk__type_string); + + assert_int_equal(pcmk__parse_type("Integer", pcmk__comparison_unknown, + NULL, NULL), + pcmk__type_integer); +} + +static void +default_number(void **state) +{ + assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_lt, "1.0", "2.5"), + pcmk__type_number); + + assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_lte, "1.", "2"), + pcmk__type_number); + + assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gt, "1", ".5"), + pcmk__type_number); + + assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gte, "1.0", "2"), + pcmk__type_number); +} + +static void +default_integer(void **state) +{ + assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_lt, "1", "2"), + pcmk__type_integer); + + assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_lte, "1", "2"), + pcmk__type_integer); + + assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gt, "1", "2"), + pcmk__type_integer); + + assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gte, "1", "2"), + pcmk__type_integer); + + assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gte, NULL, NULL), + pcmk__type_integer); + + assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gte, "1", NULL), + pcmk__type_integer); + + assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gte, NULL, "2.5"), + pcmk__type_number); +} + +static void +default_string(void **state) +{ + assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_unknown, + NULL, NULL), + pcmk__type_string); + + assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_defined, + NULL, NULL), + pcmk__type_string); + + assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_undefined, + NULL, NULL), + pcmk__type_string); + + assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_eq, NULL, NULL), + pcmk__type_string); + + assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_ne, NULL, NULL), + pcmk__type_string); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(invalid), + cmocka_unit_test(valid), + cmocka_unit_test(case_insensitive), + cmocka_unit_test(default_number), + cmocka_unit_test(default_integer), + cmocka_unit_test(default_string)) diff --git a/lib/common/tests/rules/pcmk__replace_submatches_test.c b/lib/common/tests/rules/pcmk__replace_submatches_test.c new file mode 100644 index 0000000..d404fcc --- /dev/null +++ b/lib/common/tests/rules/pcmk__replace_submatches_test.c @@ -0,0 +1,81 @@ +/* + * 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 <crm_internal.h> + +#include <regex.h> // regmatch_t + +#include <crm/common/rules_internal.h> +#include <crm/common/unittest_internal.h> + +// An example matched string with submatches +static const char *match = "this is a string"; +static const regmatch_t submatches[] = { + { .rm_so = 0, .rm_eo = 16 }, // %0 = entire string + { .rm_so = 5, .rm_eo = 7 }, // %1 = "is" + { .rm_so = 9, .rm_eo = 9 }, // %2 = empty match +}; +static const int nmatches = 3; + +static void +assert_submatch(const char *string, const char *reference) +{ + char *expanded = NULL; + + expanded = pcmk__replace_submatches(string, match, submatches, nmatches); + if ((expanded == NULL) || (reference == NULL)) { + assert_null(expanded); + assert_null(reference); + } else { + assert_int_equal(strcmp(expanded, reference), 0); + } + free(expanded); +} + +static void +no_source(void **state) +{ + assert_null(pcmk__replace_submatches(NULL, NULL, NULL, 0)); + assert_submatch(NULL, NULL); + assert_submatch("", NULL); +} + +static void +source_has_no_variables(void **state) +{ + assert_null(pcmk__replace_submatches("this has no submatch variables", + match, submatches, nmatches)); + assert_null(pcmk__replace_submatches("this ends in a %", + match, submatches, nmatches)); + assert_null(pcmk__replace_submatches("%this starts with one", + match, submatches, nmatches)); +} + +static void +without_matches(void **state) +{ + assert_submatch("this has an empty submatch %2", + "this has an empty submatch "); + assert_submatch("this has a nonexistent submatch %3", + "this has a nonexistent submatch "); +} + +static void +with_matches(void **state) +{ + assert_submatch("%0", match); // %0 matches entire string + assert_submatch("this %1", "this is"); + assert_submatch("%1 this %ok", "is this %ok"); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(no_source), + cmocka_unit_test(source_has_no_variables), + cmocka_unit_test(without_matches), + cmocka_unit_test(with_matches)) diff --git a/lib/common/tests/rules/pcmk__unpack_duration_test.c b/lib/common/tests/rules/pcmk__unpack_duration_test.c new file mode 100644 index 0000000..e82546c --- /dev/null +++ b/lib/common/tests/rules/pcmk__unpack_duration_test.c @@ -0,0 +1,120 @@ +/* + * 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 <crm_internal.h> + +#include <glib.h> + +#include <crm/common/unittest_internal.h> + +#include <crm/common/iso8601.h> +#include <crm/common/xml.h> +#include "../../crmcommon_private.h" + +#define MONTHS_TO_SECONDS "months=\"2\" weeks=\"3\" days=\"-1\" " \ + "hours=\"1\" minutes=\"1\" seconds=\"1\" />" + +#define ALL_VALID "<duration id=\"duration1\" years=\"1\" " MONTHS_TO_SECONDS + +#define NO_ID "<duration years=\"1\" " MONTHS_TO_SECONDS + +#define YEARS_INVALID "<duration id=\"duration1\" years=\"not-a-number\" " \ + MONTHS_TO_SECONDS + +static void +null_invalid(void **state) +{ + xmlNode *duration = pcmk__xml_parse(ALL_VALID); + crm_time_t *start = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *end = NULL; + + assert_int_equal(pcmk__unpack_duration(NULL, NULL, NULL), EINVAL); + assert_int_equal(pcmk__unpack_duration(duration, NULL, NULL), EINVAL); + assert_int_equal(pcmk__unpack_duration(duration, start, NULL), EINVAL); + assert_int_equal(pcmk__unpack_duration(duration, NULL, &end), EINVAL); + assert_int_equal(pcmk__unpack_duration(NULL, start, NULL), EINVAL); + assert_int_equal(pcmk__unpack_duration(NULL, start, &end), EINVAL); + assert_int_equal(pcmk__unpack_duration(NULL, NULL, &end), EINVAL); + + crm_time_free(start); + free_xml(duration); +} + +static void +nonnull_end_invalid(void **state) +{ + xmlNode *duration = pcmk__xml_parse(ALL_VALID); + crm_time_t *start = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *end = crm_time_new("2024-01-01 15:00:01"); + + assert_int_equal(pcmk__unpack_duration(duration, start, &end), EINVAL); + + crm_time_free(start); + crm_time_free(end); + free_xml(duration); +} + +static void +no_id(void **state) +{ + xmlNode *duration = pcmk__xml_parse(NO_ID); + crm_time_t *start = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *end = NULL; + crm_time_t *reference = crm_time_new("2025-03-21 16:01:01"); + + assert_int_equal(pcmk__unpack_duration(duration, start, &end), pcmk_rc_ok); + assert_int_equal(crm_time_compare(end, reference), 0); + + crm_time_free(start); + crm_time_free(end); + crm_time_free(reference); + free_xml(duration); +} + +static void +years_invalid(void **state) +{ + xmlNode *duration = pcmk__xml_parse(YEARS_INVALID); + crm_time_t *start = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *end = NULL; + crm_time_t *reference = crm_time_new("2024-03-21 16:01:01"); + + assert_int_equal(pcmk__unpack_duration(duration, start, &end), + pcmk_rc_unpack_error); + assert_int_equal(crm_time_compare(end, reference), 0); + + crm_time_free(start); + crm_time_free(end); + crm_time_free(reference); + free_xml(duration); +} + +static void +all_valid(void **state) +{ + xmlNode *duration = pcmk__xml_parse(ALL_VALID); + crm_time_t *start = crm_time_new("2024-01-01 15:00:00"); + crm_time_t *end = NULL; + crm_time_t *reference = crm_time_new("2025-03-21 16:01:01"); + + assert_int_equal(pcmk__unpack_duration(duration, start, &end), pcmk_rc_ok); + assert_int_equal(crm_time_compare(end, reference), 0); + + crm_time_free(start); + crm_time_free(end); + crm_time_free(reference); + free_xml(duration); +} + +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL, + cmocka_unit_test(null_invalid), + cmocka_unit_test(nonnull_end_invalid), + cmocka_unit_test(no_id), + cmocka_unit_test(years_invalid), + cmocka_unit_test(all_valid)) 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 0000000..6b6f9eb --- /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 <crm_internal.h> + +#include <stdio.h> +#include <glib.h> + +#include <crm/common/xml.h> +#include <crm/common/rules_internal.h> +#include <crm/common/unittest_internal.h> + +/* + * 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' />" \ + "</" PCMK_XE_RULE ">" + +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' />" \ + "</" PCMK_XE_RULE ">" + +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 "</" PCMK_XE_CIB ">" + +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' />" \ + "</" PCMK_XE_RULE ">" + +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' />" \ + "</" PCMK_XE_RULE ">" + +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' />" \ + "</" PCMK_XE_RULE ">" + +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' />" \ + "</" PCMK_XE_RULE ">" + +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' />" \ + "</" PCMK_XE_RULE ">" + +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' />" \ + "</" PCMK_XE_RULE ">" + +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' />" \ + "</" PCMK_XE_RULE ">" + +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' />" \ + "</" PCMK_XE_RULE ">" + +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' />" \ + "</" PCMK_XE_RULE ">" + +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' />" \ + "</" PCMK_XE_RULE ">" + +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(pcmk__xml_test_setup_group, 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/common/tests/scheduler/Makefile.am b/lib/common/tests/scheduler/Makefile.am new file mode 100644 index 0000000..6d5f4f8 --- /dev/null +++ b/lib/common/tests/scheduler/Makefile.am @@ -0,0 +1,19 @@ +# +# 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 $(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_get_dc_test \ + pcmk_get_no_quorum_policy_test \ + pcmk_has_quorum_test \ + pcmk_set_scheduler_cib_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/scheduler/pcmk_get_dc_test.c b/lib/common/tests/scheduler/pcmk_get_dc_test.c new file mode 100644 index 0000000..5d9d459 --- /dev/null +++ b/lib/common/tests/scheduler/pcmk_get_dc_test.c @@ -0,0 +1,47 @@ +/* + * 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 <crm_internal.h> + +#include <crm/common/scheduler.h> +#include <crm/common/unittest_internal.h> + +static void +null_scheduler(void **state) +{ + assert_null(pcmk_get_dc(NULL)); +} + +static void +null_dc(void **state) +{ + pcmk_scheduler_t scheduler = { + .dc_node = NULL, + }; + + assert_null(pcmk_get_dc(&scheduler)); +} + +static void +valid_dc(void **state) +{ + pcmk_node_t dc = { + .weight = 1, + }; + pcmk_scheduler_t scheduler = { + .dc_node = &dc, + }; + + assert_ptr_equal(&dc, pcmk_get_dc(&scheduler)); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(null_scheduler), + cmocka_unit_test(null_dc), + cmocka_unit_test(valid_dc)) diff --git a/lib/common/tests/scheduler/pcmk_get_no_quorum_policy_test.c b/lib/common/tests/scheduler/pcmk_get_no_quorum_policy_test.c new file mode 100644 index 0000000..61c97e6 --- /dev/null +++ b/lib/common/tests/scheduler/pcmk_get_no_quorum_policy_test.c @@ -0,0 +1,34 @@ +/* + * 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 <crm_internal.h> + +#include <crm/common/scheduler.h> +#include <crm/common/unittest_internal.h> + +static void +null_scheduler(void **state) +{ + assert_int_equal(pcmk_get_no_quorum_policy(NULL), pcmk_no_quorum_stop); +} + +static void +valid_no_quorum_policy(void **state) +{ + pcmk_scheduler_t scheduler = { + .no_quorum_policy = pcmk_no_quorum_fence, + }; + + assert_int_equal(pcmk_get_no_quorum_policy(&scheduler), + pcmk_no_quorum_fence); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(null_scheduler), + cmocka_unit_test(valid_no_quorum_policy)) diff --git a/lib/common/tests/scheduler/pcmk_has_quorum_test.c b/lib/common/tests/scheduler/pcmk_has_quorum_test.c new file mode 100644 index 0000000..51903df --- /dev/null +++ b/lib/common/tests/scheduler/pcmk_has_quorum_test.c @@ -0,0 +1,36 @@ +/* + * 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 <crm_internal.h> + +#include <crm/common/scheduler.h> +#include <crm/common/unittest_internal.h> + +static void +null_scheduler(void **state) +{ + assert_false(pcmk_has_quorum(NULL)); +} + +static void +valid_scheduler(void **state) +{ + pcmk_scheduler_t scheduler = { + .flags = pcmk_sched_quorate, + }; + + assert_true(pcmk_has_quorum(&scheduler)); + + scheduler.flags = pcmk_sched_none; + assert_false(pcmk_has_quorum(&scheduler)); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(null_scheduler), + cmocka_unit_test(valid_scheduler)) diff --git a/lib/common/tests/scheduler/pcmk_set_scheduler_cib_test.c b/lib/common/tests/scheduler/pcmk_set_scheduler_cib_test.c new file mode 100644 index 0000000..71e690b --- /dev/null +++ b/lib/common/tests/scheduler/pcmk_set_scheduler_cib_test.c @@ -0,0 +1,71 @@ +/* + * 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 <crm_internal.h> + +#include <crm/common/scheduler.h> +#include <crm/common/unittest_internal.h> + +static void +null_scheduler(void **state) +{ + xmlNode *cib = pcmk__xe_create(NULL, "test"); + + assert_int_equal(pcmk_set_scheduler_cib(NULL, NULL), EINVAL); + assert_int_equal(pcmk_set_scheduler_cib(NULL, cib), EINVAL); + + free_xml(cib); +} + +static void +null_cib(void **state) +{ + pcmk_scheduler_t scheduler = { + .input = NULL, + }; + + assert_int_equal(pcmk_set_scheduler_cib(&scheduler, NULL), pcmk_rc_ok); + assert_null(scheduler.input); +} + +static void +previous_cib_null(void **state) +{ + pcmk_scheduler_t scheduler = { + .input = NULL, + }; + xmlNode *cib = pcmk__xe_create(NULL, "test"); + + assert_int_equal(pcmk_set_scheduler_cib(&scheduler, cib), pcmk_rc_ok); + assert_ptr_equal(scheduler.input, cib); + + free_xml(cib); +} + +static void +previous_cib_nonnull(void **state) +{ + xmlNode *old_cib = pcmk__xe_create(NULL, "old"); + xmlNode *new_cib = pcmk__xe_create(NULL, "new"); + pcmk_scheduler_t scheduler = { + .input = old_cib, + }; + + assert_int_equal(pcmk_set_scheduler_cib(&scheduler, new_cib), pcmk_rc_ok); + assert_ptr_equal(scheduler.input, new_cib); + + free_xml(old_cib); + free_xml(new_cib); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(null_scheduler), + cmocka_unit_test(null_cib), + cmocka_unit_test(previous_cib_null), + cmocka_unit_test(previous_cib_nonnull)) diff --git a/lib/common/tests/schemas/Makefile.am b/lib/common/tests/schemas/Makefile.am new file mode 100644 index 0000000..ba0f805 --- /dev/null +++ b/lib/common/tests/schemas/Makefile.am @@ -0,0 +1,88 @@ +# +# Copyright 2023-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 + +CFLAGS += -DPCMK__TEST_SCHEMA_DIR='"$(abs_builddir)/schemas"' + +# Add "_test" to the end of all test program names to simplify .gitignore. + +# These tests share a schema subdirectory +SHARED_SCHEMA_TESTS = pcmk__cmp_schemas_by_name_test \ + crm_schema_init_test \ + pcmk__build_schema_xml_node_test \ + pcmk__get_schema_test \ + pcmk__schema_files_later_than_test + +# This test has its own schema directory +FIND_X_0_SCHEMA_TEST = pcmk__find_x_0_schema_test + +check_PROGRAMS = $(SHARED_SCHEMA_TESTS) $(FIND_X_0_SCHEMA_TEST) + +TESTS = $(check_PROGRAMS) + +$(SHARED_SCHEMA_TESTS): setup-schema-dir + +$(FIND_X_0_SCHEMA_TEST): setup-find_x_0-schema-dir + +# Set up a temporary schemas/ directory containing only some of the full set of +# pacemaker schema files. This lets us know exactly how many schemas are present, +# allowing us to write tests without having to make changes when new schemas are +# added. +# +# This directory contains the following: +# +# * pacemaker-next.rng - Used to verify that this sorts before all versions +# * upgrade-*.xsl - Required by various schema versions +# * pacemaker-[0-9]*.rng - We're only pulling in 15 schemas, which is enough +# to get everything through pacemaker-3.0.rng. This +# includes 2.10, needed so we can check that versions +# are compared as numbers instead of strings. +# * other RNG files - This catches everything except the pacemaker-*rng +# files. These files are included by the top-level +# pacemaker-*rng files, so we need them for tests. +# This will glob more than we need, but the extra ones +# won't get in the way. + +LINK_FILES = $(abs_top_builddir)/xml/pacemaker-next.rng \ + $(abs_top_builddir)/xml/upgrade-*.xsl +ROOT_RNGS = $(shell ls -1v $(abs_top_builddir)/xml/pacemaker-[0-9]*.rng | head -15) +INCLUDED_RNGS = $(shell ls -1 $(top_srcdir)/xml/*.rng | grep -v pacemaker-[0-9]) + +# Most tests share a common, read-only schema directory +.PHONY: setup-schema-dir +setup-schema-dir: + $(MKDIR_P) schemas + ( cd schemas ; \ + ln -sf $(LINK_FILES) . ; \ + for f in $(ROOT_RNGS); do \ + ln -sf $$f $$(basename $$f); \ + done ; \ + for f in $(INCLUDED_RNGS); do \ + ln -sf ../$$f $$(basename $$f); \ + done ) + +# pcmk__find_x_0_schema_test moves schema files around, so it needs its +# own directory, otherwise other tests run in parallel could fail. +.PHONY: setup-find_x_0-schema-dir +setup-find_x_0-schema-dir: + $(MKDIR_P) schemas/find_x_0 + ( cd schemas/find_x_0 ; \ + ln -sf $(LINK_FILES) . ; \ + for f in $(ROOT_RNGS); do \ + ln -sf $$f $$(basename $$f); \ + done ; \ + for f in $(INCLUDED_RNGS); do \ + ln -sf ../$$f $$(basename $$f); \ + done ) + +.PHONY: clean-local +clean-local: + -rm -rf schemas diff --git a/lib/common/tests/schemas/crm_schema_init_test.c b/lib/common/tests/schemas/crm_schema_init_test.c new file mode 100644 index 0000000..8da79cb --- /dev/null +++ b/lib/common/tests/schemas/crm_schema_init_test.c @@ -0,0 +1,152 @@ +/* + * Copyright 2023-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 <crm_internal.h> + +#include <ftw.h> +#include <unistd.h> + +#include <crm/common/xml.h> +#include <crm/common/unittest_internal.h> +#include <crm/common/xml_internal.h> +#include "crmcommon_private.h" + +static char *remote_schema_dir = NULL; + +static int +symlink_schema(const char *tmpdir, const char *target_file, const char *link_file) +{ + int rc = 0; + char *oldpath = NULL; + char *newpath = NULL; + + oldpath = crm_strdup_printf("%s/%s", PCMK__TEST_SCHEMA_DIR, target_file); + newpath = crm_strdup_printf("%s/%s", tmpdir, link_file); + + rc = symlink(oldpath, newpath); + + free(oldpath); + free(newpath); + return rc; +} + +static int +rm_files(const char *pathname, const struct stat *sbuf, int type, struct FTW *ftwb) +{ + return remove(pathname); +} + +static int +rmtree(const char *dir) +{ + return nftw(dir, rm_files, 10, FTW_DEPTH|FTW_MOUNT|FTW_PHYS); +} + +static int +setup(void **state) +{ + char *dir = NULL; + + /* Create a directory to hold additional schema files. These don't need + * to be anything special - we can just copy existing schemas but give + * them new names. + */ + dir = crm_strdup_printf("%s/test-schemas.XXXXXX", pcmk__get_tmpdir()); + remote_schema_dir = mkdtemp(dir); + + if (remote_schema_dir == NULL) { + free(dir); + return -1; + } + + /* Add new files to simulate a remote node not being up-to-date. We can't + * add a new major version here without also creating an XSL transform, and + * we can't add an older version (like 1.1 or 2.11 or something) because + * remotes will only ever ask for stuff newer than their newest. + */ + if (symlink_schema(dir, "pacemaker-3.0.rng", "pacemaker-3.1.rng") != 0) { + rmdir(dir); + free(dir); + return -1; + } + + if (symlink_schema(dir, "pacemaker-3.0.rng", "pacemaker-3.2.rng") != 0) { + rmdir(dir); + free(dir); + return -1; + } + + setenv("PCMK_remote_schema_directory", remote_schema_dir, 1); + setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR, 1); + + /* Do not call crm_schema_init here because that is the function we're + * testing. It needs to be called in each unit test. However, we can + * call crm_schema_cleanup in teardown(). + */ + + return 0; +} + +static int +teardown(void **state) +{ + int rc = 0; + char *f = NULL; + + crm_schema_cleanup(); + unsetenv("PCMK_remote_schema_directory"); + unsetenv("PCMK_schema_directory"); + + rc = rmtree(remote_schema_dir); + + free(remote_schema_dir); + free(f); + return rc; +} + +static void +assert_schema(const char *schema_name, int schema_index) +{ + GList *entry = NULL; + pcmk__schema_t *schema = NULL; + + entry = pcmk__get_schema(schema_name); + assert_non_null(entry); + + schema = entry->data; + assert_non_null(schema); + + assert_int_equal(schema_index, schema->schema_index); +} + +static void +extra_schema_files(void **state) +{ + crm_schema_init(); + + /* Just iterate through the list of schemas and make sure everything + * (including the new schemas we loaded from a second directory) is in + * the right order. + */ + assert_schema("pacemaker-1.0", 0); + assert_schema("pacemaker-1.2", 1); + assert_schema("pacemaker-2.0", 3); + assert_schema("pacemaker-3.0", 14); + assert_schema("pacemaker-3.1", 15); + assert_schema("pacemaker-3.2", 16); + + // @COMPAT pacemaker-next is deprecated since 2.1.5 + assert_schema("pacemaker-next", 17); + + // @COMPAT none is deprecated since 2.1.8 + assert_schema(PCMK_VALUE_NONE, 18); +} + +PCMK__UNIT_TEST(setup, teardown, + cmocka_unit_test(extra_schema_files)); diff --git a/lib/common/tests/schemas/pcmk__build_schema_xml_node_test.c b/lib/common/tests/schemas/pcmk__build_schema_xml_node_test.c new file mode 100644 index 0000000..e4454e2 --- /dev/null +++ b/lib/common/tests/schemas/pcmk__build_schema_xml_node_test.c @@ -0,0 +1,158 @@ +/* + * Copyright 2023-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 <crm_internal.h> + +#include <crm/common/xml.h> +#include <crm/common/unittest_internal.h> +#include <crm/common/lists_internal.h> + +#include <glib.h> + +const char *rngs1[] = { "pacemaker-3.0.rng", "status-1.0.rng", "alerts-2.10.rng", + "nvset-2.9.rng", "score.rng", "rule-2.9.rng", + "tags-1.3.rng", "acls-2.0.rng", "fencing-2.4.rng", + "constraints-3.0.rng", "resources-3.0.rng", "nvset-3.0.rng", + "nodes-3.0.rng", "options-3.0.rng", NULL }; + +const char *rngs2[] = { "pacemaker-2.0.rng", "status-1.0.rng", "tags-1.3.rng", + "acls-2.0.rng", "fencing-1.2.rng", "constraints-1.2.rng", + "rule.rng", "score.rng", "resources-1.3.rng", + "nvset-1.3.rng", "nodes-1.3.rng", "options-1.0.rng", + "nvset.rng", "cib-1.2.rng", NULL }; + +const char *rngs3[] = { "pacemaker-2.1.rng", "constraints-2.1.rng", NULL }; + +static int +setup(void **state) +{ + setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR, 1); + crm_schema_init(); + pcmk__xml_test_setup_group(state); + return 0; +} + +static int +teardown(void **state) +{ + crm_schema_cleanup(); + unsetenv("PCMK_schema_directory"); + return 0; +} + +static void +invalid_name(void **state) +{ + GList *already_included = NULL; + xmlNode *parent = pcmk__xe_create(NULL, PCMK__XA_SCHEMAS); + + pcmk__build_schema_xml_node(parent, "pacemaker-9.0", &already_included); + assert_null(parent->children); + assert_null(already_included); + free_xml(parent); +} + +static void +single_schema(void **state) +{ + GList *already_included = NULL; + xmlNode *parent = pcmk__xe_create(NULL, PCMK__XA_SCHEMAS); + xmlNode *schema_node = NULL; + xmlNode *file_node = NULL; + int i = 0; + + pcmk__build_schema_xml_node(parent, "pacemaker-3.0", &already_included); + + assert_non_null(already_included); + assert_non_null(parent->children); + + /* Test that the result looks like this: + * + * <schemas> + * <schema version="pacemaker-3.0"> + * <file path="pacemaker-3.0.rng">CDATA</file> + * <file path="status-1.0.rng">CDATA</file> + * ... + * </schema> + * </schemas> + */ + schema_node = pcmk__xe_first_child(parent, NULL, NULL, NULL); + assert_string_equal("pacemaker-3.0", + crm_element_value(schema_node, PCMK_XA_VERSION)); + + file_node = pcmk__xe_first_child(schema_node, NULL, NULL, NULL); + while (file_node != NULL && rngs1[i] != NULL) { + assert_string_equal(rngs1[i], + crm_element_value(file_node, PCMK_XA_PATH)); + assert_int_equal(pcmk__xml_first_child(file_node)->type, XML_CDATA_SECTION_NODE); + + file_node = pcmk__xe_next(file_node); + i++; + } + + g_list_free_full(already_included, free); + free_xml(parent); +} + +static void +multiple_schemas(void **state) +{ + GList *already_included = NULL; + xmlNode *parent = pcmk__xe_create(NULL, PCMK__XA_SCHEMAS); + xmlNode *schema_node = NULL; + xmlNode *file_node = NULL; + int i = 0; + + pcmk__build_schema_xml_node(parent, "pacemaker-2.0", &already_included); + pcmk__build_schema_xml_node(parent, "pacemaker-2.1", &already_included); + + assert_non_null(already_included); + assert_non_null(parent->children); + + /* Like single_schema, but make sure files aren't included multiple times + * when the function is called repeatedly. + */ + schema_node = pcmk__xe_first_child(parent, NULL, NULL, NULL); + assert_string_equal("pacemaker-2.0", + crm_element_value(schema_node, PCMK_XA_VERSION)); + + file_node = pcmk__xe_first_child(schema_node, NULL, NULL, NULL); + while (file_node != NULL && rngs2[i] != NULL) { + assert_string_equal(rngs2[i], + crm_element_value(file_node, PCMK_XA_PATH)); + assert_int_equal(pcmk__xml_first_child(file_node)->type, XML_CDATA_SECTION_NODE); + + file_node = pcmk__xe_next(file_node); + i++; + } + + schema_node = pcmk__xe_next(schema_node); + assert_string_equal("pacemaker-2.1", + crm_element_value(schema_node, PCMK_XA_VERSION)); + + file_node = pcmk__xe_first_child(schema_node, NULL, NULL, NULL); + i = 0; + + while (file_node != NULL && rngs3[i] != NULL) { + assert_string_equal(rngs3[i], + crm_element_value(file_node, PCMK_XA_PATH)); + assert_int_equal(pcmk__xml_first_child(file_node)->type, XML_CDATA_SECTION_NODE); + + file_node = pcmk__xe_next(file_node); + i++; + } + + g_list_free_full(already_included, free); + free_xml(parent); +} + +PCMK__UNIT_TEST(setup, teardown, + cmocka_unit_test(invalid_name), + cmocka_unit_test(single_schema), + cmocka_unit_test(multiple_schemas)) diff --git a/lib/common/tests/schemas/pcmk__cmp_schemas_by_name_test.c b/lib/common/tests/schemas/pcmk__cmp_schemas_by_name_test.c new file mode 100644 index 0000000..19ec743 --- /dev/null +++ b/lib/common/tests/schemas/pcmk__cmp_schemas_by_name_test.c @@ -0,0 +1,121 @@ +/* + * 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 <crm_internal.h> + +#include <crm/common/xml.h> +#include <crm/common/unittest_internal.h> +#include <crm/common/xml_internal.h> +#include "crmcommon_private.h" + +static int +setup(void **state) +{ + setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR, 1); + crm_schema_init(); + return 0; +} + +static int +teardown(void **state) +{ + crm_schema_cleanup(); + unsetenv("PCMK_schema_directory"); + return 0; +} + +// NULL schema name defaults to the "none" schema +// @COMPAT none is deprecated since 2.1.8 + +static void +unknown_is_lesser(void **state) +{ + assert_true(pcmk__cmp_schemas_by_name("pacemaker-0.1", + "pacemaker-0.2") == 0); + assert_true(pcmk__cmp_schemas_by_name("pacemaker-0.1", + "pacemaker-1.0") < 0); + assert_true(pcmk__cmp_schemas_by_name("pacemaker-1.0", + "pacemaker-0.1") > 0); + assert_true(pcmk__cmp_schemas_by_name("pacemaker-1.1", NULL) < 0); + assert_true(pcmk__cmp_schemas_by_name(NULL, "pacemaker-0.0") > 0); + + /* @COMPAT pacemaker-next is deprecated since 2.1.5, + * and pacemaker-0.6 and pacemaker-0.7 since 2.1.8 + */ + assert_true(pcmk__cmp_schemas_by_name("pacemaker-0.6", + "pacemaker-next") < 0); + assert_true(pcmk__cmp_schemas_by_name("pacemaker-next", + "pacemaker-0.7") > 0); +} + +// @COMPAT none is deprecated since 2.1.8 +static void +none_is_greater(void **state) +{ + assert_true(pcmk__cmp_schemas_by_name(NULL, NULL) == 0); + assert_true(pcmk__cmp_schemas_by_name(NULL, PCMK_VALUE_NONE) == 0); + assert_true(pcmk__cmp_schemas_by_name(PCMK_VALUE_NONE, NULL) == 0); + assert_true(pcmk__cmp_schemas_by_name(PCMK_VALUE_NONE, + PCMK_VALUE_NONE) == 0); + + assert_true(pcmk__cmp_schemas_by_name("pacemaker-3.0", + PCMK_VALUE_NONE) < 0); + assert_true(pcmk__cmp_schemas_by_name(PCMK_VALUE_NONE, + "pacemaker-1.0") > 0); + + // @COMPAT pacemaker-next is deprecated since 2.1.5 + assert_true(pcmk__cmp_schemas_by_name("pacemaker-next", + PCMK_VALUE_NONE) < 0); + assert_true(pcmk__cmp_schemas_by_name(PCMK_VALUE_NONE, + "pacemaker-next") > 0); +} + +// @COMPAT pacemaker-next is deprecated since 2.1.5 +// @COMPAT none is deprecated since 2.1.8 +static void +next_is_before_none(void **state) +{ + assert_true(pcmk__cmp_schemas_by_name("pacemaker-next", + "pacemaker-next") == 0); + assert_true(pcmk__cmp_schemas_by_name(NULL, "pacemaker-next") > 0); + assert_true(pcmk__cmp_schemas_by_name("pacemaker-next", NULL) < 0); + assert_true(pcmk__cmp_schemas_by_name("pacemaker-3.0", + "pacemaker-next") < 0); + assert_true(pcmk__cmp_schemas_by_name("pacemaker-next", + "pacemaker-1.0") > 0); +} + +static void +known_numeric(void **state) +{ + assert_true(pcmk__cmp_schemas_by_name("pacemaker-1.0", + "pacemaker-1.0") == 0); + assert_true(pcmk__cmp_schemas_by_name("pacemaker-1.2", + "pacemaker-1.0") > 0); + assert_true(pcmk__cmp_schemas_by_name("pacemaker-1.2", + "pacemaker-2.0") < 0); +} + +static void +case_insensitive(void **state) +{ + assert_true(pcmk__cmp_schemas_by_name("Pacemaker-1.0", + "pacemaker-1.0") == 0); + assert_true(pcmk__cmp_schemas_by_name("PACEMAKER-1.2", + "pacemaker-1.0") > 0); + assert_true(pcmk__cmp_schemas_by_name("PaceMaker-1.2", + "pacemaker-2.0") < 0); +} + +PCMK__UNIT_TEST(setup, teardown, + cmocka_unit_test(unknown_is_lesser), + cmocka_unit_test(none_is_greater), + cmocka_unit_test(next_is_before_none), + cmocka_unit_test(known_numeric), + cmocka_unit_test(case_insensitive)); diff --git a/lib/common/tests/schemas/pcmk__find_x_0_schema_test.c b/lib/common/tests/schemas/pcmk__find_x_0_schema_test.c new file mode 100644 index 0000000..25ba0f3 --- /dev/null +++ b/lib/common/tests/schemas/pcmk__find_x_0_schema_test.c @@ -0,0 +1,100 @@ +/* + * Copyright 2023-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 <crm_internal.h> + +#include <stdio.h> // NULL, rename() +#include <stdlib.h> // setenv(), unsetenv() +#include <glib.h> + +#include <crm/common/unittest_internal.h> +#include "crmcommon_private.h" + +#define SCHEMA_PREFIX PCMK__TEST_SCHEMA_DIR "/find_x_0/pacemaker-" + +static int +setup(void **state) +{ + // Use a unique schema directory so we can move files around + setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR "/find_x_0", 1); + return 0; +} + +static int +teardown(void **state) +{ + unsetenv("PCMK_schema_directory"); + return 0; +} + +static void +assert_schema_0(int schema_index, const char *schema_name) +{ + GList *entry = NULL; + pcmk__schema_t *schema = NULL; + + entry = pcmk__find_x_0_schema(); + assert_non_null(entry); + + schema = entry->data; + assert_non_null(schema); + + assert_int_equal(schema->schema_index, schema_index); + assert_string_equal(schema->name, schema_name); +} + +static void +last_is_0(void **state) +{ + /* This loads all the schemas normally linked for unit testing, so we have + * many 1.x and 2.x schemas and a single pacemaker-3.0 schema at index 14. + */ + crm_schema_init(); + assert_schema_0(14, "pacemaker-3.0"); + crm_schema_cleanup(); +} + +static void +last_is_not_0(void **state) +{ + /* Disable the pacemaker-3.0 schema, so we now should get pacemaker-2.0 at + * index 3. + */ + assert_int_equal(0, rename(SCHEMA_PREFIX "3.0.rng", + SCHEMA_PREFIX "3.0.bak")); + crm_schema_init(); + assert_schema_0(3, "pacemaker-2.0"); + assert_int_equal(0, rename(SCHEMA_PREFIX "3.0.bak", + SCHEMA_PREFIX "3.0.rng")); + crm_schema_cleanup(); +} + +static void +schema_0_missing(void **state) +{ + /* Disable the pacemaker-3.0 and pacemaker-2.0 schemas, so we now should get + * pacemaker-2.1 at index 3. + */ + assert_int_equal(0, rename(SCHEMA_PREFIX "3.0.rng", + SCHEMA_PREFIX "3.0.bak")); + assert_int_equal(0, rename(SCHEMA_PREFIX "2.0.rng", + SCHEMA_PREFIX "2.0.bak")); + crm_schema_init(); + assert_schema_0(3, "pacemaker-2.1"); + assert_int_equal(0, rename(SCHEMA_PREFIX "2.0.bak", + SCHEMA_PREFIX "2.0.rng")); + assert_int_equal(0, rename(SCHEMA_PREFIX "3.0.bak", + SCHEMA_PREFIX "3.0.rng")); + crm_schema_cleanup(); +} + +PCMK__UNIT_TEST(setup, teardown, + cmocka_unit_test(last_is_0), + cmocka_unit_test(last_is_not_0), + cmocka_unit_test(schema_0_missing)) diff --git a/lib/common/tests/schemas/pcmk__get_schema_test.c b/lib/common/tests/schemas/pcmk__get_schema_test.c new file mode 100644 index 0000000..6513dfc --- /dev/null +++ b/lib/common/tests/schemas/pcmk__get_schema_test.c @@ -0,0 +1,81 @@ +/* + * Copyright 2023-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 <crm_internal.h> + +#include <crm/common/xml.h> +#include <crm/common/unittest_internal.h> +#include <crm/common/xml_internal.h> +#include "crmcommon_private.h" + +static int +setup(void **state) +{ + setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR, 1); + crm_schema_init(); + return 0; +} + +static int +teardown(void **state) +{ + crm_schema_cleanup(); + unsetenv("PCMK_schema_directory"); + return 0; +} + +static void +assert_schema(const char *name, int expected_index) +{ + GList *schema_entry = NULL; + pcmk__schema_t *schema = NULL; + + schema_entry = pcmk__get_schema(name); + assert_non_null(schema_entry); + + schema = schema_entry->data; + assert_non_null(schema); + + assert_int_equal(schema->schema_index, expected_index); +} + +static void +unknown_schema(void **state) +{ + assert_null(pcmk__get_schema("")); + assert_null(pcmk__get_schema("blahblah")); + assert_null(pcmk__get_schema("pacemaker-2.47")); + assert_null(pcmk__get_schema("pacemaker-47.0")); +} + +static void +known_schema(void **state) +{ + // @COMPAT none is deprecated since 2.1.8 + assert_schema(NULL, 16); // defaults to "none" + + assert_schema("pacemaker-1.0", 0); + assert_schema("pacemaker-1.2", 1); + assert_schema("pacemaker-2.0", 3); + assert_schema("pacemaker-2.5", 8); + assert_schema("pacemaker-3.0", 14); +} + +static void +case_insensitive(void **state) +{ + assert_schema("PACEMAKER-1.0", 0); + assert_schema("pAcEmAkEr-2.0", 3); + assert_schema("paceMAKER-3.0", 14); +} + +PCMK__UNIT_TEST(setup, teardown, + cmocka_unit_test(unknown_schema), + cmocka_unit_test(known_schema), + cmocka_unit_test(case_insensitive)); diff --git a/lib/common/tests/schemas/pcmk__schema_files_later_than_test.c b/lib/common/tests/schemas/pcmk__schema_files_later_than_test.c new file mode 100644 index 0000000..68744ef --- /dev/null +++ b/lib/common/tests/schemas/pcmk__schema_files_later_than_test.c @@ -0,0 +1,106 @@ +/* + * Copyright 2023 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 <crm_internal.h> + +#include <crm/common/unittest_internal.h> +#include <crm/common/lists_internal.h> + +#include <glib.h> + +static int +setup(void **state) +{ + setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR, 1); + crm_schema_init(); + return 0; +} + +static int +teardown(void **state) +{ + crm_schema_cleanup(); + unsetenv("PCMK_schema_directory"); + return 0; +} + +static void +invalid_name(void **state) +{ + assert_null(pcmk__schema_files_later_than("xyz")); + assert_null(pcmk__schema_files_later_than("pacemaker-")); +} + +static void +valid_name(void **state) +{ + GList *schemas = NULL; + + schemas = pcmk__schema_files_later_than("pacemaker-1.0"); + assert_int_equal(g_list_length(schemas), 18); + /* There is no "pacemaker-1.1". */ + assert_string_equal("pacemaker-1.2.rng", g_list_nth_data(schemas, 0)); + assert_string_equal("upgrade-1.3.xsl", g_list_nth_data(schemas, 1)); + assert_string_equal("pacemaker-1.3.rng", g_list_nth_data(schemas, 2)); + assert_string_equal("pacemaker-2.0.rng", g_list_nth_data(schemas, 3)); + assert_string_equal("pacemaker-2.1.rng", g_list_nth_data(schemas, 4)); + assert_string_equal("pacemaker-2.2.rng", g_list_nth_data(schemas, 5)); + assert_string_equal("pacemaker-2.3.rng", g_list_nth_data(schemas, 6)); + assert_string_equal("pacemaker-2.4.rng", g_list_nth_data(schemas, 7)); + assert_string_equal("pacemaker-2.5.rng", g_list_nth_data(schemas, 8)); + assert_string_equal("pacemaker-2.6.rng", g_list_nth_data(schemas, 9)); + assert_string_equal("pacemaker-2.7.rng", g_list_nth_data(schemas, 10)); + assert_string_equal("pacemaker-2.8.rng", g_list_nth_data(schemas, 11)); + assert_string_equal("pacemaker-2.9.rng", g_list_nth_data(schemas, 12)); + assert_string_equal("upgrade-2.10-leave.xsl", g_list_nth_data(schemas, 13)); + assert_string_equal("upgrade-2.10-enter.xsl", g_list_nth_data(schemas, 14)); + assert_string_equal("upgrade-2.10.xsl", g_list_nth_data(schemas, 15)); + assert_string_equal("pacemaker-2.10.rng", g_list_nth_data(schemas, 16)); + assert_string_equal("pacemaker-3.0.rng", g_list_nth_data(schemas, 17)); + g_list_free_full(schemas, free); + + /* Adding .rng to the end of the schema we're requesting is also valid. */ + schemas = pcmk__schema_files_later_than("pacemaker-2.0.rng"); + assert_int_equal(g_list_length(schemas), 14); + assert_string_equal("pacemaker-2.1.rng", g_list_nth_data(schemas, 0)); + assert_string_equal("pacemaker-2.2.rng", g_list_nth_data(schemas, 1)); + assert_string_equal("pacemaker-2.3.rng", g_list_nth_data(schemas, 2)); + assert_string_equal("pacemaker-2.4.rng", g_list_nth_data(schemas, 3)); + assert_string_equal("pacemaker-2.5.rng", g_list_nth_data(schemas, 4)); + assert_string_equal("pacemaker-2.6.rng", g_list_nth_data(schemas, 5)); + assert_string_equal("pacemaker-2.7.rng", g_list_nth_data(schemas, 6)); + assert_string_equal("pacemaker-2.8.rng", g_list_nth_data(schemas, 7)); + assert_string_equal("pacemaker-2.9.rng", g_list_nth_data(schemas, 8)); + assert_string_equal("upgrade-2.10-leave.xsl", g_list_nth_data(schemas, 9)); + assert_string_equal("upgrade-2.10-enter.xsl", g_list_nth_data(schemas, 10)); + assert_string_equal("upgrade-2.10.xsl", g_list_nth_data(schemas, 11)); + assert_string_equal("pacemaker-2.10.rng", g_list_nth_data(schemas, 12)); + assert_string_equal("pacemaker-3.0.rng", g_list_nth_data(schemas, 13)); + g_list_free_full(schemas, free); + + /* Check that "pacemaker-2.10" counts as later than "pacemaker-2.9". */ + schemas = pcmk__schema_files_later_than("pacemaker-2.9"); + assert_int_equal(g_list_length(schemas), 5); + assert_string_equal("upgrade-2.10-leave.xsl", g_list_nth_data(schemas, 0)); + assert_string_equal("upgrade-2.10-enter.xsl", g_list_nth_data(schemas, 1)); + assert_string_equal("upgrade-2.10.xsl", g_list_nth_data(schemas, 2)); + assert_string_equal("pacemaker-2.10.rng", g_list_nth_data(schemas, 3)); + assert_string_equal("pacemaker-3.0.rng", g_list_nth_data(schemas, 4)); + g_list_free_full(schemas, free); + + /* And then something way in the future that will never apply due to our + * special schema directory. + */ + schemas = pcmk__schema_files_later_than("pacemaker-9.0"); + assert_null(schemas); +} + +PCMK__UNIT_TEST(setup, teardown, + cmocka_unit_test(invalid_name), + cmocka_unit_test(valid_name)) diff --git a/lib/common/tests/scores/char2score_test.c b/lib/common/tests/scores/char2score_test.c index fbba12a..5d7252f 100644 --- a/lib/common/tests/scores/char2score_test.c +++ b/lib/common/tests/scores/char2score_test.c @@ -1,5 +1,5 @@ /* - * Copyright 2022 the Pacemaker project contributors + * Copyright 2022-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -32,9 +32,9 @@ bad_input(void **state) static void special_values(void **state) { - assert_int_equal(char2score("-INFINITY"), -CRM_SCORE_INFINITY); - assert_int_equal(char2score("INFINITY"), CRM_SCORE_INFINITY); - assert_int_equal(char2score("+INFINITY"), CRM_SCORE_INFINITY); + assert_int_equal(char2score("-INFINITY"), -PCMK_SCORE_INFINITY); + assert_int_equal(char2score("INFINITY"), PCMK_SCORE_INFINITY); + assert_int_equal(char2score("+INFINITY"), PCMK_SCORE_INFINITY); pcmk__score_red = 10; pcmk__score_green = 20; @@ -56,8 +56,10 @@ special_values(void **state) static void outside_limits(void **state) { - assert_int_equal(char2score(B(CRM_SCORE_INFINITY) "00"), CRM_SCORE_INFINITY); - assert_int_equal(char2score("-" B(CRM_SCORE_INFINITY) "00"), -CRM_SCORE_INFINITY); + assert_int_equal(char2score(B(PCMK_SCORE_INFINITY) "00"), + PCMK_SCORE_INFINITY); + assert_int_equal(char2score("-" B(PCMK_SCORE_INFINITY) "00"), + -PCMK_SCORE_INFINITY); } static void diff --git a/lib/common/tests/scores/pcmk__add_scores_test.c b/lib/common/tests/scores/pcmk__add_scores_test.c index 1309659..952cf97 100644 --- a/lib/common/tests/scores/pcmk__add_scores_test.c +++ b/lib/common/tests/scores/pcmk__add_scores_test.c @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the Pacemaker project contributors + * Copyright 2022-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -14,48 +14,71 @@ static void score1_minus_inf(void **state) { - assert_int_equal(pcmk__add_scores(-CRM_SCORE_INFINITY, -CRM_SCORE_INFINITY), -CRM_SCORE_INFINITY); - assert_int_equal(pcmk__add_scores(-CRM_SCORE_INFINITY, -1), -CRM_SCORE_INFINITY); - assert_int_equal(pcmk__add_scores(-CRM_SCORE_INFINITY, 0), -CRM_SCORE_INFINITY); - assert_int_equal(pcmk__add_scores(-CRM_SCORE_INFINITY, 1), -CRM_SCORE_INFINITY); - assert_int_equal(pcmk__add_scores(-CRM_SCORE_INFINITY, CRM_SCORE_INFINITY), -CRM_SCORE_INFINITY); + assert_int_equal(pcmk__add_scores(-PCMK_SCORE_INFINITY, + -PCMK_SCORE_INFINITY), + -PCMK_SCORE_INFINITY); + assert_int_equal(pcmk__add_scores(-PCMK_SCORE_INFINITY, -1), + -PCMK_SCORE_INFINITY); + assert_int_equal(pcmk__add_scores(-PCMK_SCORE_INFINITY, 0), + -PCMK_SCORE_INFINITY); + assert_int_equal(pcmk__add_scores(-PCMK_SCORE_INFINITY, 1), + -PCMK_SCORE_INFINITY); + assert_int_equal(pcmk__add_scores(-PCMK_SCORE_INFINITY, + PCMK_SCORE_INFINITY), + -PCMK_SCORE_INFINITY); } static void score2_minus_inf(void **state) { - assert_int_equal(pcmk__add_scores(-1, -CRM_SCORE_INFINITY), -CRM_SCORE_INFINITY); - assert_int_equal(pcmk__add_scores(0, -CRM_SCORE_INFINITY), -CRM_SCORE_INFINITY); - assert_int_equal(pcmk__add_scores(1, -CRM_SCORE_INFINITY), -CRM_SCORE_INFINITY); - assert_int_equal(pcmk__add_scores(CRM_SCORE_INFINITY, -CRM_SCORE_INFINITY), -CRM_SCORE_INFINITY); + assert_int_equal(pcmk__add_scores(-1, -PCMK_SCORE_INFINITY), + -PCMK_SCORE_INFINITY); + assert_int_equal(pcmk__add_scores(0, -PCMK_SCORE_INFINITY), + -PCMK_SCORE_INFINITY); + assert_int_equal(pcmk__add_scores(1, -PCMK_SCORE_INFINITY), + -PCMK_SCORE_INFINITY); + assert_int_equal(pcmk__add_scores(PCMK_SCORE_INFINITY, + -PCMK_SCORE_INFINITY), + -PCMK_SCORE_INFINITY); } static void score1_pos_inf(void **state) { - assert_int_equal(pcmk__add_scores(CRM_SCORE_INFINITY, CRM_SCORE_INFINITY), CRM_SCORE_INFINITY); - assert_int_equal(pcmk__add_scores(CRM_SCORE_INFINITY, -1), CRM_SCORE_INFINITY); - assert_int_equal(pcmk__add_scores(CRM_SCORE_INFINITY, 0), CRM_SCORE_INFINITY); - assert_int_equal(pcmk__add_scores(CRM_SCORE_INFINITY, 1), CRM_SCORE_INFINITY); + assert_int_equal(pcmk__add_scores(PCMK_SCORE_INFINITY, PCMK_SCORE_INFINITY), + PCMK_SCORE_INFINITY); + assert_int_equal(pcmk__add_scores(PCMK_SCORE_INFINITY, -1), + PCMK_SCORE_INFINITY); + assert_int_equal(pcmk__add_scores(PCMK_SCORE_INFINITY, 0), + PCMK_SCORE_INFINITY); + assert_int_equal(pcmk__add_scores(PCMK_SCORE_INFINITY, 1), + PCMK_SCORE_INFINITY); } static void score2_pos_inf(void **state) { - assert_int_equal(pcmk__add_scores(-1, CRM_SCORE_INFINITY), CRM_SCORE_INFINITY); - assert_int_equal(pcmk__add_scores(0, CRM_SCORE_INFINITY), CRM_SCORE_INFINITY); - assert_int_equal(pcmk__add_scores(1, CRM_SCORE_INFINITY), CRM_SCORE_INFINITY); + assert_int_equal(pcmk__add_scores(-1, PCMK_SCORE_INFINITY), + PCMK_SCORE_INFINITY); + assert_int_equal(pcmk__add_scores(0, PCMK_SCORE_INFINITY), + PCMK_SCORE_INFINITY); + assert_int_equal(pcmk__add_scores(1, PCMK_SCORE_INFINITY), + PCMK_SCORE_INFINITY); } static void result_infinite(void **state) { - assert_int_equal(pcmk__add_scores(INT_MAX, INT_MAX), CRM_SCORE_INFINITY); - assert_int_equal(pcmk__add_scores(INT_MIN, INT_MIN), -CRM_SCORE_INFINITY); - assert_int_equal(pcmk__add_scores(2000000, 50), CRM_SCORE_INFINITY); - assert_int_equal(pcmk__add_scores(CRM_SCORE_INFINITY/2, CRM_SCORE_INFINITY/2), CRM_SCORE_INFINITY); - assert_int_equal(pcmk__add_scores(-CRM_SCORE_INFINITY/2, -CRM_SCORE_INFINITY/2), -CRM_SCORE_INFINITY); - assert_int_equal(pcmk__add_scores(-4000000, 50), -CRM_SCORE_INFINITY); + assert_int_equal(pcmk__add_scores(INT_MAX, INT_MAX), PCMK_SCORE_INFINITY); + assert_int_equal(pcmk__add_scores(INT_MIN, INT_MIN), -PCMK_SCORE_INFINITY); + assert_int_equal(pcmk__add_scores(2000000, 50), PCMK_SCORE_INFINITY); + assert_int_equal(pcmk__add_scores(PCMK_SCORE_INFINITY/2, + PCMK_SCORE_INFINITY/2), + PCMK_SCORE_INFINITY); + assert_int_equal(pcmk__add_scores(-PCMK_SCORE_INFINITY/2, + -PCMK_SCORE_INFINITY/2), + -PCMK_SCORE_INFINITY); + assert_int_equal(pcmk__add_scores(-4000000, 50), -PCMK_SCORE_INFINITY); } static void diff --git a/lib/common/tests/scores/pcmk_readable_score_test.c b/lib/common/tests/scores/pcmk_readable_score_test.c index ae24159..c3d66f6 100644 --- a/lib/common/tests/scores/pcmk_readable_score_test.c +++ b/lib/common/tests/scores/pcmk_readable_score_test.c @@ -1,5 +1,5 @@ /* - * Copyright 2022 the Pacemaker project contributors + * Copyright 2022-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -14,10 +14,10 @@ static void outside_limits(void **state) { - assert_string_equal(pcmk_readable_score(CRM_SCORE_INFINITY * 2), - CRM_INFINITY_S); - assert_string_equal(pcmk_readable_score(-CRM_SCORE_INFINITY * 2), - CRM_MINUS_INFINITY_S); + assert_string_equal(pcmk_readable_score(PCMK_SCORE_INFINITY * 2), + PCMK_VALUE_INFINITY); + assert_string_equal(pcmk_readable_score(-PCMK_SCORE_INFINITY * 2), + PCMK_VALUE_MINUS_INFINITY); } static void diff --git a/lib/common/tests/strings/Makefile.am b/lib/common/tests/strings/Makefile.am index e66af0d..439b6bf 100644 --- a/lib/common/tests/strings/Makefile.am +++ b/lib/common/tests/strings/Makefile.am @@ -1,5 +1,5 @@ # -# Copyright 2020-2023 the Pacemaker project contributors +# Copyright 2020-2024 the Pacemaker project contributors # # The version control history for this file may have further details. # @@ -16,7 +16,6 @@ check_PROGRAMS = crm_get_msec_test \ crm_str_to_boolean_test \ pcmk__add_word_test \ pcmk__btoa_test \ - pcmk__char_in_any_str_test \ pcmk__compress_test \ pcmk__ends_with_test \ pcmk__g_strcat_test \ diff --git a/lib/common/tests/strings/crm_get_msec_test.c b/lib/common/tests/strings/crm_get_msec_test.c index 5da548b..14b87cf 100644 --- a/lib/common/tests/strings/crm_get_msec_test.c +++ b/lib/common/tests/strings/crm_get_msec_test.c @@ -1,5 +1,5 @@ /* - * Copyright 2021 the Pacemaker project contributors + * Copyright 2021-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -19,6 +19,11 @@ bad_input(void **state) { assert_int_equal(crm_get_msec("100xs"), PCMK__PARSE_INT_DEFAULT); assert_int_equal(crm_get_msec(" 100 xs "), PCMK__PARSE_INT_DEFAULT); assert_int_equal(crm_get_msec("-100ms"), PCMK__PARSE_INT_DEFAULT); + + assert_int_equal(crm_get_msec("3.xs"), PCMK__PARSE_INT_DEFAULT); + assert_int_equal(crm_get_msec(" 3. xs "), PCMK__PARSE_INT_DEFAULT); + assert_int_equal(crm_get_msec("3.14xs"), PCMK__PARSE_INT_DEFAULT); + assert_int_equal(crm_get_msec(" 3.14 xs "), PCMK__PARSE_INT_DEFAULT); } static void @@ -28,6 +33,7 @@ good_input(void **state) { assert_int_equal(crm_get_msec("\t100\n"), 100000); assert_int_equal(crm_get_msec("100ms"), 100); + assert_int_equal(crm_get_msec(" 100 ms "), 100); assert_int_equal(crm_get_msec("100 MSEC"), 100); assert_int_equal(crm_get_msec("1000US"), 1); assert_int_equal(crm_get_msec("1000usec"), 1); @@ -37,6 +43,28 @@ good_input(void **state) { assert_int_equal(crm_get_msec("13 min"), 780000); assert_int_equal(crm_get_msec("2\th"), 7200000); assert_int_equal(crm_get_msec("1 hr"), 3600000); + + assert_int_equal(crm_get_msec("3."), 3000); + assert_int_equal(crm_get_msec(" 3. ms "), 3); + assert_int_equal(crm_get_msec("3.14"), 3000); + assert_int_equal(crm_get_msec(" 3.14 ms "), 3); + + // Questionable + assert_int_equal(crm_get_msec("3.14."), 3000); + assert_int_equal(crm_get_msec(" 3.14. ms "), 3); + assert_int_equal(crm_get_msec("3.14.159"), 3000); + assert_int_equal(crm_get_msec(" 3.14.159 "), 3000); + assert_int_equal(crm_get_msec("3.14.159ms"), 3); + assert_int_equal(crm_get_msec(" 3.14.159 ms "), 3); + + // Questionable + assert_int_equal(crm_get_msec(" 100 mshr "), 100); + assert_int_equal(crm_get_msec(" 100 ms hr "), 100); + assert_int_equal(crm_get_msec(" 100 sasdf "), 100000); + assert_int_equal(crm_get_msec(" 100 s asdf "), 100000); + assert_int_equal(crm_get_msec(" 3.14 shour "), 3000); + assert_int_equal(crm_get_msec(" 3.14 s hour "), 3000); + assert_int_equal(crm_get_msec(" 3.14 ms!@#$ "), 3); } static void diff --git a/lib/common/tests/strings/crm_str_to_boolean_test.c b/lib/common/tests/strings/crm_str_to_boolean_test.c index 3bd2e5d..1b0ba45 100644 --- a/lib/common/tests/strings/crm_str_to_boolean_test.c +++ b/lib/common/tests/strings/crm_str_to_boolean_test.c @@ -1,5 +1,5 @@ /* - * Copyright 2021 the Pacemaker project contributors + * Copyright 2021-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -40,6 +40,13 @@ is_true(void **state) { assert_true(ret); assert_int_equal(crm_str_to_boolean("1", &ret), 1); assert_true(ret); + + // Ensure it still validates the string with a NULL result argument + assert_int_equal(crm_str_to_boolean("true", NULL), 1); + assert_int_equal(crm_str_to_boolean("on", NULL), 1); + assert_int_equal(crm_str_to_boolean("yes", NULL), 1); + assert_int_equal(crm_str_to_boolean("y", NULL), 1); + assert_int_equal(crm_str_to_boolean("1", NULL), 1); } static void @@ -73,6 +80,13 @@ is_false(void **state) { assert_false(ret); assert_int_equal(crm_str_to_boolean("0", &ret), 1); assert_false(ret); + + // Ensure it still validates the string with a NULL result argument + assert_int_equal(crm_str_to_boolean("false", NULL), 1); + assert_int_equal(crm_str_to_boolean("off", NULL), 1); + assert_int_equal(crm_str_to_boolean("no", NULL), 1); + assert_int_equal(crm_str_to_boolean("n", NULL), 1); + assert_int_equal(crm_str_to_boolean("0", NULL), 1); } static void diff --git a/lib/common/tests/strings/pcmk__char_in_any_str_test.c b/lib/common/tests/strings/pcmk__char_in_any_str_test.c deleted file mode 100644 index e70dfb4..0000000 --- a/lib/common/tests/strings/pcmk__char_in_any_str_test.c +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2020-2021 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 <crm_internal.h> - -#include <crm/common/unittest_internal.h> - -static void -empty_list(void **state) -{ - assert_false(pcmk__char_in_any_str('x', NULL)); - assert_false(pcmk__char_in_any_str('\0', NULL)); -} - -static void -null_char(void **state) -{ - assert_true(pcmk__char_in_any_str('\0', "xxx", "yyy", NULL)); - assert_true(pcmk__char_in_any_str('\0', "", NULL)); -} - -static void -in_list(void **state) -{ - assert_true(pcmk__char_in_any_str('x', "aaa", "bbb", "xxx", NULL)); -} - -static void -not_in_list(void **state) -{ - assert_false(pcmk__char_in_any_str('x', "aaa", "bbb", NULL)); - assert_false(pcmk__char_in_any_str('A', "aaa", "bbb", NULL)); - assert_false(pcmk__char_in_any_str('x', "", NULL)); -} - -PCMK__UNIT_TEST(NULL, NULL, - cmocka_unit_test(empty_list), - cmocka_unit_test(null_char), - cmocka_unit_test(in_list), - cmocka_unit_test(not_in_list)) diff --git a/lib/common/tests/strings/pcmk__compress_test.c b/lib/common/tests/strings/pcmk__compress_test.c index 7b59d9d..813bcdb 100644 --- a/lib/common/tests/strings/pcmk__compress_test.c +++ b/lib/common/tests/strings/pcmk__compress_test.c @@ -1,5 +1,5 @@ /* - * Copyright 2022 the Pacemaker project contributors + * Copyright 2022-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -20,7 +20,7 @@ const char *SIMPLE_COMPRESSED = "BZh41AY&SYO\x1ai"; static void simple_compress(void **state) { - char *result = calloc(1024, sizeof(char)); + char *result = pcmk__assert_alloc(1024, sizeof(char)); unsigned int len; assert_int_equal(pcmk__compress(SIMPLE_DATA, 40, 0, &result, &len), pcmk_rc_ok); @@ -30,7 +30,7 @@ simple_compress(void **state) static void max_too_small(void **state) { - char *result = calloc(1024, sizeof(char)); + char *result = pcmk__assert_alloc(1024, sizeof(char)); unsigned int len; assert_int_equal(pcmk__compress(SIMPLE_DATA, 40, 10, &result, &len), EFBIG); @@ -38,10 +38,11 @@ max_too_small(void **state) static void calloc_fails(void **state) { - char *result = calloc(1024, sizeof(char)); + char *result = pcmk__assert_alloc(1024, sizeof(char)); unsigned int len; - pcmk__assert_asserts( + pcmk__assert_exits( + CRM_EX_OSERR, { pcmk__mock_calloc = true; // calloc() will return NULL expect_value(__wrap_calloc, nmemb, (size_t) ((40 * 1.01) + 601)); diff --git a/lib/common/tests/strings/pcmk__str_update_test.c b/lib/common/tests/strings/pcmk__str_update_test.c index 571031d..4a44fba 100644 --- a/lib/common/tests/strings/pcmk__str_update_test.c +++ b/lib/common/tests/strings/pcmk__str_update_test.c @@ -59,7 +59,8 @@ strdup_fails(void **state) { str = strdup("hello"); - pcmk__assert_asserts( + pcmk__assert_exits( + CRM_EX_OSERR, { pcmk__mock_strdup = true; // strdup() will return NULL expect_string(__wrap_strdup, s, "world"); diff --git a/lib/common/tests/utils/Makefile.am b/lib/common/tests/utils/Makefile.am index f028ce4..fb9d5c3 100644 --- a/lib/common/tests/utils/Makefile.am +++ b/lib/common/tests/utils/Makefile.am @@ -1,5 +1,5 @@ # -# Copyright 2020-2023 the Pacemaker project contributors +# Copyright 2020-2024 the Pacemaker project contributors # # The version control history for this file may have further details. # @@ -12,8 +12,6 @@ include $(top_srcdir)/mk/unittest.mk # Add "_test" to the end of all test program names to simplify .gitignore. check_PROGRAMS = compare_version_test \ - crm_meta_name_test \ - crm_meta_value_test \ crm_user_lookup_test \ pcmk_daemon_user_test \ pcmk_str_is_infinity_test \ @@ -21,10 +19,7 @@ check_PROGRAMS = compare_version_test \ pcmk__fail_attr_name_test \ pcmk__failcount_name_test \ pcmk__getpid_s_test \ - pcmk__lastfailure_name_test - -if WRAPPABLE_UNAME -check_PROGRAMS += pcmk_hostname_test -endif + pcmk__lastfailure_name_test \ + pcmk__realloc_test TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/utils/compare_version_test.c b/lib/common/tests/utils/compare_version_test.c index 35ebb63..d191f4a 100644 --- a/lib/common/tests/utils/compare_version_test.c +++ b/lib/common/tests/utils/compare_version_test.c @@ -1,5 +1,5 @@ /* - * Copyright 2022 the Pacemaker project contributors + * Copyright 2022-2023 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -46,6 +46,9 @@ shorter_versions(void **state) { assert_int_equal(compare_version("1.0", "1.0.1"), -1); assert_int_equal(compare_version("1.0.1", "1.0"), 1); + assert_int_equal(compare_version("1.0", "1"), 0); + assert_int_equal(compare_version("1", "1.2"), -1); + assert_int_equal(compare_version("1.2", "1"), 1); } PCMK__UNIT_TEST(NULL, NULL, diff --git a/lib/common/tests/utils/pcmk__realloc_test.c b/lib/common/tests/utils/pcmk__realloc_test.c new file mode 100644 index 0000000..62d51df --- /dev/null +++ b/lib/common/tests/utils/pcmk__realloc_test.c @@ -0,0 +1,69 @@ +/* + * 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 <crm_internal.h> + +#include <crm/common/unittest_internal.h> + +#include "mock_private.h" + +static void +bad_size(void **state) +{ + char *ptr = NULL; + + pcmk__assert_asserts(pcmk__realloc(ptr, 0)); +} + +static void +realloc_fails(void **state) +{ + char *ptr = NULL; + + pcmk__assert_aborts( + { + pcmk__mock_realloc = true; // realloc() will return NULL + expect_any(__wrap_realloc, ptr); + expect_value(__wrap_realloc, size, 1000); + pcmk__realloc(ptr, 1000); + pcmk__mock_realloc = false; // Use real realloc() + } + ); +} + +static void +realloc_succeeds(void **state) +{ + char *ptr = NULL; + + /* We can't really test that the resulting pointer is the size we asked + * for - it might be larger if that's what the memory allocator decides + * to do. And anyway, testing realloc isn't really the point. All we + * want to do here is make sure the function works when given good input. + */ + + /* Allocate new memory */ + ptr = pcmk__realloc(ptr, 1000); + assert_non_null(ptr); + + /* Grow previously allocated memory */ + ptr = pcmk__realloc(ptr, 2000); + assert_non_null(ptr); + + /* Shrink previously allocated memory */ + ptr = pcmk__realloc(ptr, 500); + assert_non_null(ptr); + + free(ptr); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(bad_size), + cmocka_unit_test(realloc_fails), + cmocka_unit_test(realloc_succeeds)) diff --git a/lib/common/tests/utils/pcmk_hostname_test.c b/lib/common/tests/utils/pcmk_hostname_test.c deleted file mode 100644 index 7329486..0000000 --- a/lib/common/tests/utils/pcmk_hostname_test.c +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2021 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 <crm_internal.h> - -#include <crm/common/unittest_internal.h> - -#include "mock_private.h" - -#include <sys/utsname.h> - -static void -uname_succeeded_test(void **state) -{ - char *retval; - - // Set uname() return value and buf parameter node name - pcmk__mock_uname = true; - - expect_any(__wrap_uname, buf); - will_return(__wrap_uname, 0); - will_return(__wrap_uname, "somename"); - - retval = pcmk_hostname(); - assert_non_null(retval); - assert_string_equal("somename", retval); - - free(retval); - - pcmk__mock_uname = false; -} - -static void -uname_failed_test(void **state) -{ - // Set uname() return value and buf parameter node name - pcmk__mock_uname = true; - - expect_any(__wrap_uname, buf); - will_return(__wrap_uname, -1); - will_return(__wrap_uname, NULL); - - assert_null(pcmk_hostname()); - - pcmk__mock_uname = false; -} - -PCMK__UNIT_TEST(NULL, NULL, - cmocka_unit_test(uname_succeeded_test), - cmocka_unit_test(uname_failed_test)) diff --git a/lib/common/tests/xml/Makefile.am b/lib/common/tests/xml/Makefile.am index 465c950..9ed1620 100644 --- a/lib/common/tests/xml/Makefile.am +++ b/lib/common/tests/xml/Makefile.am @@ -1,5 +1,5 @@ # -# Copyright 2022-2023 the Pacemaker project contributors +# Copyright 2022-2024 the Pacemaker project contributors # # The version control history for this file may have further details. # @@ -11,7 +11,12 @@ 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__xe_foreach_child_test \ - pcmk__xe_match_test +check_PROGRAMS = crm_xml_init_test \ + pcmk__xe_copy_attrs_test \ + pcmk__xe_first_child_test \ + pcmk__xe_foreach_child_test \ + pcmk__xe_set_score_test \ + pcmk__xml_escape_test \ + pcmk__xml_needs_escape_test TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/xml/crm_xml_init_test.c b/lib/common/tests/xml/crm_xml_init_test.c new file mode 100644 index 0000000..1f78728 --- /dev/null +++ b/lib/common/tests/xml/crm_xml_init_test.c @@ -0,0 +1,230 @@ +/* + * Copyright 2023-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 <crm_internal.h> + +#include <crm/common/xml.h> +#include <crm/common/unittest_internal.h> +#include <crm/common/xml_internal.h> + +#include "crmcommon_private.h" + +/* Copied from lib/common/xml.c */ +#define XML_DOC_PRIVATE_MAGIC 0x81726354UL +#define XML_NODE_PRIVATE_MAGIC 0x54637281UL + +static int +setup(void **state) { + crm_xml_init(); + return 0; +} + +static int +teardown(void **state) { + crm_xml_cleanup(); + return 0; +} + +static void +buffer_scheme_test(void **state) { + assert_int_equal(XML_BUFFER_ALLOC_DOUBLEIT, xmlGetBufferAllocationScheme()); +} + +/* These functions also serve as unit tests of the static new_private_data + * function. We can't test free_private_data because libxml will call that as + * part of freeing everything else. By the time we'd get back into a unit test + * where we could check that private members are NULL, the structure containing + * the private data would have been freed. + * + * This could probably be tested with a lot of function mocking, but that + * doesn't seem worth it. + */ + +static void +create_document_node(void **state) { + xml_doc_private_t *docpriv = NULL; + xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION); + + /* Double check things */ + assert_non_null(doc); + assert_int_equal(doc->type, XML_DOCUMENT_NODE); + + /* Check that the private data is initialized correctly */ + docpriv = doc->_private; + assert_non_null(docpriv); + assert_int_equal(docpriv->check, XML_DOC_PRIVATE_MAGIC); + assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty|pcmk__xf_created)); + + /* Clean up */ + xmlFreeDoc(doc); +} + +static void +create_element_node(void **state) { + xml_doc_private_t *docpriv = NULL; + xml_node_private_t *priv = NULL; + xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION); + xmlNodePtr node = xmlNewDocNode(doc, NULL, (pcmkXmlStr) "test", NULL); + + /* Adding a node to the document marks it as dirty */ + docpriv = doc->_private; + assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty)); + + /* Double check things */ + assert_non_null(node); + assert_int_equal(node->type, XML_ELEMENT_NODE); + + /* Check that the private data is initialized correctly */ + priv = node->_private; + assert_non_null(priv); + assert_int_equal(priv->check, XML_NODE_PRIVATE_MAGIC); + assert_true(pcmk_all_flags_set(priv->flags, pcmk__xf_dirty|pcmk__xf_created)); + + /* Clean up */ + xmlFreeNode(node); + xmlFreeDoc(doc); +} + +static void +create_attr_node(void **state) { + xml_doc_private_t *docpriv = NULL; + xml_node_private_t *priv = NULL; + xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION); + xmlNodePtr node = xmlNewDocNode(doc, NULL, (pcmkXmlStr) "test", NULL); + xmlAttrPtr attr = xmlNewProp(node, (pcmkXmlStr) PCMK_XA_NAME, + (pcmkXmlStr) "dummy-value"); + + /* Adding a node to the document marks it as dirty */ + docpriv = doc->_private; + assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty)); + + /* Double check things */ + assert_non_null(attr); + assert_int_equal(attr->type, XML_ATTRIBUTE_NODE); + + /* Check that the private data is initialized correctly */ + priv = attr->_private; + assert_non_null(priv); + assert_int_equal(priv->check, XML_NODE_PRIVATE_MAGIC); + assert_true(pcmk_all_flags_set(priv->flags, pcmk__xf_dirty|pcmk__xf_created)); + + /* Clean up */ + xmlFreeNode(node); + xmlFreeDoc(doc); +} + +static void +create_comment_node(void **state) { + xml_doc_private_t *docpriv = NULL; + xml_node_private_t *priv = NULL; + xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION); + xmlNodePtr node = xmlNewDocComment(doc, (pcmkXmlStr) "blahblah"); + + /* Adding a node to the document marks it as dirty */ + docpriv = doc->_private; + assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty)); + + /* Double check things */ + assert_non_null(node); + assert_int_equal(node->type, XML_COMMENT_NODE); + + /* Check that the private data is initialized correctly */ + priv = node->_private; + assert_non_null(priv); + assert_int_equal(priv->check, XML_NODE_PRIVATE_MAGIC); + assert_true(pcmk_all_flags_set(priv->flags, pcmk__xf_dirty|pcmk__xf_created)); + + /* Clean up */ + xmlFreeNode(node); + xmlFreeDoc(doc); +} + +static void +create_text_node(void **state) { + xml_doc_private_t *docpriv = NULL; + xml_node_private_t *priv = NULL; + xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION); + xmlNodePtr node = xmlNewDocText(doc, (pcmkXmlStr) "blahblah"); + + /* Adding a node to the document marks it as dirty */ + docpriv = doc->_private; + assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty)); + + /* Double check things */ + assert_non_null(node); + assert_int_equal(node->type, XML_TEXT_NODE); + + /* Check that no private data was created */ + priv = node->_private; + assert_null(priv); + + /* Clean up */ + xmlFreeNode(node); + xmlFreeDoc(doc); +} + +static void +create_dtd_node(void **state) { + xml_doc_private_t *docpriv = NULL; + xml_node_private_t *priv = NULL; + xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION); + xmlDtdPtr dtd = xmlNewDtd(doc, (pcmkXmlStr) PCMK_XA_NAME, + (pcmkXmlStr) "externalId", + (pcmkXmlStr) "systemId"); + + /* Adding a node to the document marks it as dirty */ + docpriv = doc->_private; + assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty)); + + /* Double check things */ + assert_non_null(dtd); + assert_int_equal(dtd->type, XML_DTD_NODE); + + /* Check that no private data was created */ + priv = dtd->_private; + assert_null(priv); + + /* Clean up */ + /* If you call xmlFreeDtd before xmlFreeDoc, you get a segfault */ + xmlFreeDoc(doc); +} + +static void +create_cdata_node(void **state) { + xml_doc_private_t *docpriv = NULL; + xml_node_private_t *priv = NULL; + xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION); + xmlNodePtr node = xmlNewCDataBlock(doc, (pcmkXmlStr) "blahblah", 8); + + /* Adding a node to the document marks it as dirty */ + docpriv = doc->_private; + assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty)); + + /* Double check things */ + assert_non_null(node); + assert_int_equal(node->type, XML_CDATA_SECTION_NODE); + + /* Check that no private data was created */ + priv = node->_private; + assert_null(priv); + + /* Clean up */ + xmlFreeNode(node); + xmlFreeDoc(doc); +} + +PCMK__UNIT_TEST(setup, teardown, + cmocka_unit_test(buffer_scheme_test), + cmocka_unit_test(create_document_node), + cmocka_unit_test(create_element_node), + cmocka_unit_test(create_attr_node), + cmocka_unit_test(create_comment_node), + cmocka_unit_test(create_text_node), + cmocka_unit_test(create_dtd_node), + cmocka_unit_test(create_cdata_node)); diff --git a/lib/common/tests/xml/pcmk__xe_copy_attrs_test.c b/lib/common/tests/xml/pcmk__xe_copy_attrs_test.c new file mode 100644 index 0000000..146317c --- /dev/null +++ b/lib/common/tests/xml/pcmk__xe_copy_attrs_test.c @@ -0,0 +1,188 @@ + /* + * 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 General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <crm/common/unittest_internal.h> + +#include <glib.h> + +static void +null_args(void **state) +{ + // This test dumps core via CRM_CHECK() + xmlNode *xml = pcmk__xe_create(NULL, "test"); + + assert_int_equal(pcmk__xe_copy_attrs(NULL, NULL, pcmk__xaf_none), EINVAL); + assert_int_equal(pcmk__xe_copy_attrs(NULL, xml, pcmk__xaf_none), EINVAL); + assert_int_equal(pcmk__xe_copy_attrs(xml, NULL, pcmk__xaf_none), EINVAL); + assert_ptr_equal(xml->properties, NULL); + + free_xml(xml); +} + +static void +no_source_attrs(void **state) +{ + xmlNode *src = pcmk__xe_create(NULL, "test"); + xmlNode *target = pcmk__xe_create(NULL, "test"); + + // Ensure copying from empty source doesn't create target properties + assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_none), + pcmk_rc_ok); + assert_ptr_equal(target->properties, NULL); + + // Ensure copying from empty source doesn't delete target attributes + crm_xml_add(target, "attr", "value"); + assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_none), + pcmk_rc_ok); + assert_string_equal(crm_element_value(target, "attr"), "value"); + + free_xml(src); + free_xml(target); +} + +static void +copy_one(void **state) +{ + xmlNode *src = pcmk__xe_create(NULL, "test"); + xmlNode *target = pcmk__xe_create(NULL, "test"); + + crm_xml_add(src, "attr", "value"); + + assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_none), + pcmk_rc_ok); + assert_string_equal(crm_element_value(src, "attr"), + crm_element_value(target, "attr")); + + free_xml(src); + free_xml(target); +} + +static void +copy_multiple(void **state) +{ + xmlNode *src = pcmk__xe_create(NULL, "test"); + xmlNode *target = pcmk__xe_create(NULL, "test"); + + pcmk__xe_set_props(src, + "attr1", "value1", + "attr2", "value2", + "attr3", "value3", + NULL); + + assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_none), + pcmk_rc_ok); + assert_string_equal(crm_element_value(src, "attr1"), + crm_element_value(target, "attr1")); + assert_string_equal(crm_element_value(src, "attr2"), + crm_element_value(target, "attr2")); + assert_string_equal(crm_element_value(src, "attr3"), + crm_element_value(target, "attr3")); + + free_xml(src); + free_xml(target); +} + +static void +overwrite(void **state) +{ + xmlNode *src = pcmk__xe_create(NULL, "test"); + xmlNode *target = pcmk__xe_create(NULL, "test"); + + crm_xml_add(src, "attr", "src_value"); + crm_xml_add(target, "attr", "target_value"); + + // Overwrite enabled by default + assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_none), + pcmk_rc_ok); + assert_string_equal(crm_element_value(src, "attr"), + crm_element_value(target, "attr")); + free_xml(src); + free_xml(target); +} + +static void +no_overwrite(void **state) +{ + xmlNode *src = pcmk__xe_create(NULL, "test"); + xmlNode *target = pcmk__xe_create(NULL, "test"); + + crm_xml_add(src, "attr", "src_value"); + crm_xml_add(target, "attr", "target_value"); + + assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_no_overwrite), + pcmk_rc_ok); + assert_string_not_equal(crm_element_value(src, "attr"), + crm_element_value(target, "attr")); + + // no_overwrite doesn't prevent copy if there's no conflict + pcmk__xe_remove_attr(target, "attr"); + + assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_no_overwrite), + pcmk_rc_ok); + assert_string_equal(crm_element_value(src, "attr"), + crm_element_value(target, "attr")); + + free_xml(src); + free_xml(target); +} + +static void +score_update(void **state) +{ + xmlNode *src = pcmk__xe_create(NULL, "test"); + xmlNode *target = pcmk__xe_create(NULL, "test"); + + crm_xml_add(src, "plus_plus_attr", "plus_plus_attr++"); + crm_xml_add(src, "plus_two_attr", "plus_two_attr+=2"); + crm_xml_add(target, "plus_plus_attr", "1"); + crm_xml_add(target, "plus_two_attr", "1"); + + assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_score_update), + pcmk_rc_ok); + assert_string_equal(crm_element_value(target, "plus_plus_attr"), "2"); + assert_string_equal(crm_element_value(target, "plus_two_attr"), "3"); + + free_xml(src); + free_xml(target); +} + +static void +no_score_update(void **state) +{ + xmlNode *src = pcmk__xe_create(NULL, "test"); + xmlNode *target = pcmk__xe_create(NULL, "test"); + + crm_xml_add(src, "plus_plus_attr", "plus_plus_attr++"); + crm_xml_add(src, "plus_two_attr", "plus_two_attr+=2"); + crm_xml_add(target, "plus_plus_attr", "1"); + crm_xml_add(target, "plus_two_attr", "1"); + + // Score update disabled by default + assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_none), + pcmk_rc_ok); + assert_string_equal(crm_element_value(target, "plus_plus_attr"), + "plus_plus_attr++"); + assert_string_equal(crm_element_value(target, "plus_two_attr"), + "plus_two_attr+=2"); + + free_xml(src); + free_xml(target); +} + +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL, + cmocka_unit_test(null_args), + cmocka_unit_test(no_source_attrs), + cmocka_unit_test(copy_one), + cmocka_unit_test(copy_multiple), + cmocka_unit_test(overwrite), + cmocka_unit_test(no_overwrite), + cmocka_unit_test(score_update), + cmocka_unit_test(no_score_update)); diff --git a/lib/common/tests/xml/pcmk__xe_match_test.c b/lib/common/tests/xml/pcmk__xe_first_child_test.c index be2c949..64b90b0 100644 --- a/lib/common/tests/xml/pcmk__xe_match_test.c +++ b/lib/common/tests/xml/pcmk__xe_first_child_test.c @@ -1,5 +1,5 @@ /* - * Copyright 2022 the Pacemaker project contributors + * Copyright 2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -9,97 +9,97 @@ #include <crm_internal.h> -#include <crm/msg_xml.h> +#include <crm/common/xml.h> #include <crm/common/unittest_internal.h> #include <crm/common/xml_internal.h> const char *str1 = "<xml>\n" " <!-- This is an A node -->\n" - " <nodeA attrA=\"123\" " XML_ATTR_ID "=\"1\">\n" + " <nodeA attrA=\"123\" " PCMK_XA_ID "=\"1\">\n" " content\n" " </nodeA>\n" " <!-- This is an A node -->\n" - " <nodeA attrA=\"456\" " XML_ATTR_ID "=\"2\">\n" + " <nodeA attrA=\"456\" " PCMK_XA_ID "=\"2\">\n" " content\n" " </nodeA>\n" " <!-- This is an A node -->\n" - " <nodeA attrB=\"XYZ\" " XML_ATTR_ID "=\"3\">\n" + " <nodeA attrB=\"XYZ\" " PCMK_XA_ID "=\"3\">\n" " content\n" " </nodeA>\n" " <!-- This is a B node -->\n" - " <nodeB attrA=\"123\" " XML_ATTR_ID "=\"4\">\n" + " <nodeB attrA=\"123\" " PCMK_XA_ID "=\"4\">\n" " content\n" " </nodeA>\n" " <!-- This is a B node -->\n" - " <nodeB attrB=\"ABC\" " XML_ATTR_ID "=\"5\">\n" + " <nodeB attrB=\"ABC\" " PCMK_XA_ID "=\"5\">\n" " content\n" " </nodeA>\n" "</xml>"; static void bad_input(void **state) { - xmlNode *xml = string2xml(str1); + xmlNode *xml = pcmk__xml_parse(str1); - assert_null(pcmk__xe_match(NULL, NULL, NULL, NULL)); - assert_null(pcmk__xe_match(NULL, NULL, NULL, "attrX")); + assert_null(pcmk__xe_first_child(NULL, NULL, NULL, NULL)); + assert_null(pcmk__xe_first_child(NULL, NULL, NULL, "attrX")); free_xml(xml); } static void not_found(void **state) { - xmlNode *xml = string2xml(str1); + xmlNode *xml = pcmk__xml_parse(str1); /* No node with an attrX attribute */ - assert_null(pcmk__xe_match(xml, NULL, "attrX", NULL)); + assert_null(pcmk__xe_first_child(xml, NULL, "attrX", NULL)); /* No nodeX node */ - assert_null(pcmk__xe_match(xml, "nodeX", NULL, NULL)); + assert_null(pcmk__xe_first_child(xml, "nodeX", NULL, NULL)); /* No nodeA node with attrX */ - assert_null(pcmk__xe_match(xml, "nodeA", "attrX", NULL)); + assert_null(pcmk__xe_first_child(xml, "nodeA", "attrX", NULL)); /* No nodeA node with attrA=XYZ */ - assert_null(pcmk__xe_match(xml, "nodeA", "attrA", "XYZ")); + assert_null(pcmk__xe_first_child(xml, "nodeA", "attrA", "XYZ")); free_xml(xml); } static void find_attrB(void **state) { - xmlNode *xml = string2xml(str1); + xmlNode *xml = pcmk__xml_parse(str1); xmlNode *result = NULL; /* Find the first node with attrB */ - result = pcmk__xe_match(xml, NULL, "attrB", NULL); + result = pcmk__xe_first_child(xml, NULL, "attrB", NULL); assert_non_null(result); - assert_string_equal(crm_element_value(result, "id"), "3"); + assert_string_equal(crm_element_value(result, PCMK_XA_ID), "3"); /* Find the first nodeB with attrB */ - result = pcmk__xe_match(xml, "nodeB", "attrB", NULL); + result = pcmk__xe_first_child(xml, "nodeB", "attrB", NULL); assert_non_null(result); - assert_string_equal(crm_element_value(result, "id"), "5"); + assert_string_equal(crm_element_value(result, PCMK_XA_ID), "5"); free_xml(xml); } static void find_attrA_matching(void **state) { - xmlNode *xml = string2xml(str1); + xmlNode *xml = pcmk__xml_parse(str1); xmlNode *result = NULL; /* Find attrA=456 */ - result = pcmk__xe_match(xml, NULL, "attrA", "456"); + result = pcmk__xe_first_child(xml, NULL, "attrA", "456"); assert_non_null(result); - assert_string_equal(crm_element_value(result, "id"), "2"); + assert_string_equal(crm_element_value(result, PCMK_XA_ID), "2"); /* Find a nodeB with attrA=123 */ - result = pcmk__xe_match(xml, "nodeB", "attrA", "123"); + result = pcmk__xe_first_child(xml, "nodeB", "attrA", "123"); assert_non_null(result); - assert_string_equal(crm_element_value(result, "id"), "4"); + assert_string_equal(crm_element_value(result, PCMK_XA_ID), "4"); free_xml(xml); } -PCMK__UNIT_TEST(NULL, NULL, +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL, cmocka_unit_test(bad_input), cmocka_unit_test(not_found), cmocka_unit_test(find_attrB), diff --git a/lib/common/tests/xml/pcmk__xe_foreach_child_test.c b/lib/common/tests/xml/pcmk__xe_foreach_child_test.c index ffb9171..a833dde 100644 --- a/lib/common/tests/xml/pcmk__xe_foreach_child_test.c +++ b/lib/common/tests/xml/pcmk__xe_foreach_child_test.c @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the Pacemaker project contributors + * Copyright 2022-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -36,7 +36,7 @@ const char *str1 = static void bad_input(void **state) { - xmlNode *xml = string2xml(str1); + xmlNode *xml = pcmk__xml_parse(str1); pcmk__assert_asserts(pcmk__xe_foreach_child(xml, NULL, NULL, NULL)); @@ -45,7 +45,7 @@ bad_input(void **state) { static void name_given_test(void **state) { - xmlNode *xml = string2xml(str1); + xmlNode *xml = pcmk__xml_parse(str1); /* The handler should be called once for every <level1> node. */ expect_function_call(compare_name_handler); @@ -58,7 +58,7 @@ name_given_test(void **state) { static void no_name_given_test(void **state) { - xmlNode *xml = string2xml(str1); + xmlNode *xml = pcmk__xml_parse(str1); /* The handler should be called once for every <level1> node. */ expect_function_call(compare_name_handler); @@ -71,7 +71,7 @@ no_name_given_test(void **state) { static void name_doesnt_exist_test(void **state) { - xmlNode *xml = string2xml(str1); + xmlNode *xml = pcmk__xml_parse(str1); pcmk__xe_foreach_child(xml, "xxx", compare_name_handler, NULL); free_xml(xml); } @@ -100,7 +100,7 @@ const char *str2 = static void multiple_levels_test(void **state) { - xmlNode *xml = string2xml(str2); + xmlNode *xml = pcmk__xml_parse(str2); /* The handler should be called once for every <level1> node. */ expect_function_call(compare_name_handler); @@ -112,7 +112,7 @@ multiple_levels_test(void **state) { static void multiple_levels_no_name_test(void **state) { - xmlNode *xml = string2xml(str2); + xmlNode *xml = pcmk__xml_parse(str2); /* The handler should be called once for every <level1> node. */ expect_function_call(compare_name_handler); @@ -147,7 +147,7 @@ static int any_of_handler(xmlNode *xml, void *userdata) { static void any_of_test(void **state) { - xmlNode *xml = string2xml(str3); + xmlNode *xml = pcmk__xml_parse(str3); /* The handler should be called once for every <nodeX> node. */ expect_function_call(any_of_handler); @@ -190,7 +190,7 @@ static int stops_on_third_handler(xmlNode *xml, void *userdata) { static void one_of_test(void **state) { - xmlNode *xml = string2xml(str3); + xmlNode *xml = pcmk__xml_parse(str3); /* The handler should be called once. */ expect_function_call(stops_on_first_handler); @@ -205,7 +205,7 @@ one_of_test(void **state) { free_xml(xml); } -PCMK__UNIT_TEST(NULL, NULL, +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL, cmocka_unit_test(bad_input), cmocka_unit_test(name_given_test), cmocka_unit_test(no_name_given_test), diff --git a/lib/common/tests/xml/pcmk__xe_set_score_test.c b/lib/common/tests/xml/pcmk__xe_set_score_test.c new file mode 100644 index 0000000..deb85b0 --- /dev/null +++ b/lib/common/tests/xml/pcmk__xe_set_score_test.c @@ -0,0 +1,188 @@ +/* + * 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 General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <crm/common/unittest_internal.h> + +#include <glib.h> + +#include "crmcommon_private.h" // pcmk__xe_set_score() + +/*! + * \internal + * \brief Update an XML attribute value and check it against a reference value + * + * The attribute name is hard-coded as \c "X". + * + * \param[in] initial Initial value + * \param[in] new Value to set + * \param[in] reference_val Expected attribute value after update + * \param[in] reference_rc Expected return code from \c pcmk__xe_set_score() + */ +static void +assert_set_score(const char *initial, const char *new, + const char *reference_val, int reference_rc) +{ + const char *name = "X"; + xmlNode *test_xml = pcmk__xe_create(NULL, "test_xml"); + + crm_xml_add(test_xml, name, initial); + assert_int_equal(pcmk__xe_set_score(test_xml, name, new), reference_rc); + assert_string_equal(crm_element_value(test_xml, name), reference_val); + + free_xml(test_xml); +} + +static void +value_is_name_plus_plus(void **state) +{ + assert_set_score("5", "X++", "6", pcmk_rc_ok); +} + +static void +value_is_name_plus_equals_integer(void **state) +{ + assert_set_score("5", "X+=2", "7", pcmk_rc_ok); +} + +// NULL input + +static void +target_is_NULL(void **state) +{ + // Dumps core via CRM_CHECK() + assert_int_equal(pcmk__xe_set_score(NULL, "X", "X++"), EINVAL); +} + +static void +name_is_NULL(void **state) +{ + xmlNode *test_xml = pcmk__xe_create(NULL, "test_xml"); + + crm_xml_add(test_xml, "X", "5"); + + // Dumps core via CRM_CHECK() + assert_int_equal(pcmk__xe_set_score(test_xml, NULL, "X++"), EINVAL); + assert_string_equal(crm_element_value(test_xml, "X"), "5"); + + free_xml(test_xml); +} + +static void +value_is_NULL(void **state) +{ + assert_set_score("5", NULL, "5", pcmk_rc_ok); +} + +// the value input doesn't start with the name input + +static void +value_is_wrong_name(void **state) +{ + assert_set_score("5", "Y++", "Y++", pcmk_rc_ok); +} + +static void +value_is_only_an_integer(void **state) +{ + assert_set_score("5", "2", "2", pcmk_rc_ok); +} + +// non-integers + +static void +variable_is_initialized_to_be_non_numeric(void **state) +{ + assert_set_score("hello", "X++", "1", pcmk_rc_ok); +} + +static void +variable_is_initialized_to_be_non_numeric_2(void **state) +{ + assert_set_score("hello", "X+=2", "2", pcmk_rc_ok); +} + +static void +variable_is_initialized_to_be_numeric_and_decimal_point_containing(void **state) +{ + assert_set_score("5.01", "X++", "6", pcmk_rc_ok); +} + +static void +variable_is_initialized_to_be_numeric_and_decimal_point_containing_2(void **state) +{ + assert_set_score("5.50", "X++", "6", pcmk_rc_ok); +} + +static void +variable_is_initialized_to_be_numeric_and_decimal_point_containing_3(void **state) +{ + assert_set_score("5.99", "X++", "6", pcmk_rc_ok); +} + +static void +value_is_non_numeric(void **state) +{ + assert_set_score("5", "X+=hello", "5", pcmk_rc_ok); +} + +static void +value_is_numeric_and_decimal_point_containing(void **state) +{ + assert_set_score("5", "X+=2.01", "7", pcmk_rc_ok); +} + +static void +value_is_numeric_and_decimal_point_containing_2(void **state) +{ + assert_set_score("5", "X+=1.50", "6", pcmk_rc_ok); +} + +static void +value_is_numeric_and_decimal_point_containing_3(void **state) +{ + assert_set_score("5", "X+=1.99", "6", pcmk_rc_ok); +} + +// undefined input + +static void +name_is_undefined(void **state) +{ + assert_set_score(NULL, "X++", "X++", pcmk_rc_ok); +} + +// large input + +static void +assignment_result_is_too_large(void **state) +{ + assert_set_score("5", "X+=100000000000", "1000000", pcmk_rc_ok); +} + +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL, + cmocka_unit_test(value_is_name_plus_plus), + cmocka_unit_test(value_is_name_plus_equals_integer), + cmocka_unit_test(target_is_NULL), + cmocka_unit_test(name_is_NULL), + cmocka_unit_test(value_is_NULL), + cmocka_unit_test(value_is_wrong_name), + cmocka_unit_test(value_is_only_an_integer), + cmocka_unit_test(variable_is_initialized_to_be_non_numeric), + cmocka_unit_test(variable_is_initialized_to_be_non_numeric_2), + cmocka_unit_test(variable_is_initialized_to_be_numeric_and_decimal_point_containing), + cmocka_unit_test(variable_is_initialized_to_be_numeric_and_decimal_point_containing_2), + cmocka_unit_test(variable_is_initialized_to_be_numeric_and_decimal_point_containing_3), + cmocka_unit_test(value_is_non_numeric), + cmocka_unit_test(value_is_numeric_and_decimal_point_containing), + cmocka_unit_test(value_is_numeric_and_decimal_point_containing_2), + cmocka_unit_test(value_is_numeric_and_decimal_point_containing_3), + cmocka_unit_test(name_is_undefined), + cmocka_unit_test(assignment_result_is_too_large)) diff --git a/lib/common/tests/xml/pcmk__xml_escape_test.c b/lib/common/tests/xml/pcmk__xml_escape_test.c new file mode 100644 index 0000000..8c6fd21 --- /dev/null +++ b/lib/common/tests/xml/pcmk__xml_escape_test.c @@ -0,0 +1,213 @@ +/* + * 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 <crm_internal.h> + +#include <crm/common/unittest_internal.h> +#include <crm/common/xml_internal.h> + +#include "crmcommon_private.h" + +static void +assert_escape(const char *str, const char *reference, + enum pcmk__xml_escape_type type) +{ + gchar *buf = pcmk__xml_escape(str, type); + + assert_string_equal(buf, reference); + g_free(buf); +} + +static void +null_empty(void **state) +{ + assert_null(pcmk__xml_escape(NULL, pcmk__xml_escape_text)); + assert_null(pcmk__xml_escape(NULL, pcmk__xml_escape_attr)); + assert_null(pcmk__xml_escape(NULL, pcmk__xml_escape_attr_pretty)); + + assert_escape("", "", pcmk__xml_escape_text); + assert_escape("", "", pcmk__xml_escape_attr); + assert_escape("", "", pcmk__xml_escape_attr_pretty); +} + +static void +invalid_type(void **state) +{ + const enum pcmk__xml_escape_type type = (enum pcmk__xml_escape_type) -1; + + // Easier to ignore invalid type for NULL or empty string + assert_null(pcmk__xml_escape(NULL, type)); + assert_escape("", "", type); + + // Otherwise, assert if we somehow passed an invalid type + pcmk__assert_asserts(pcmk__xml_escape("he<>llo", type)); +} + +static void +escape_unchanged(void **state) +{ + // No escaped characters (note: this string includes single quote at end) + const char *unchanged = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789" + "`~!@#$%^*()-_=+/|\\[]{}?.,'"; + + assert_escape(unchanged, unchanged, pcmk__xml_escape_text); + assert_escape(unchanged, unchanged, pcmk__xml_escape_attr); + assert_escape(unchanged, unchanged, pcmk__xml_escape_attr_pretty); +} + +// Ensure special characters get escaped at start, middle, and end + +static void +escape_left_angle(void **state) +{ + const char *l_angle = "<abc<def<"; + const char *l_angle_esc = PCMK__XML_ENTITY_LT "abc" + PCMK__XML_ENTITY_LT "def" PCMK__XML_ENTITY_LT; + + assert_escape(l_angle, l_angle_esc, pcmk__xml_escape_text); + assert_escape(l_angle, l_angle_esc, pcmk__xml_escape_attr); + assert_escape(l_angle, l_angle, pcmk__xml_escape_attr_pretty); +} + +static void +escape_right_angle(void **state) +{ + const char *r_angle = ">abc>def>"; + const char *r_angle_esc = PCMK__XML_ENTITY_GT "abc" + PCMK__XML_ENTITY_GT "def" PCMK__XML_ENTITY_GT; + + assert_escape(r_angle, r_angle_esc, pcmk__xml_escape_text); + assert_escape(r_angle, r_angle_esc, pcmk__xml_escape_attr); + assert_escape(r_angle, r_angle, pcmk__xml_escape_attr_pretty); +} + +static void +escape_ampersand(void **state) +{ + const char *ampersand = "&abc&def&"; + const char *ampersand_esc = PCMK__XML_ENTITY_AMP "abc" + PCMK__XML_ENTITY_AMP "def" PCMK__XML_ENTITY_AMP; + + assert_escape(ampersand, ampersand_esc, pcmk__xml_escape_text); + assert_escape(ampersand, ampersand_esc, pcmk__xml_escape_attr); + assert_escape(ampersand, ampersand, pcmk__xml_escape_attr_pretty); +} + +static void +escape_double_quote(void **state) +{ + const char *double_quote = "\"abc\"def\""; + const char *double_quote_esc_ref = PCMK__XML_ENTITY_QUOT "abc" + PCMK__XML_ENTITY_QUOT "def" + PCMK__XML_ENTITY_QUOT; + const char *double_quote_esc_backslash = "\\\"abc\\\"def\\\""; + + assert_escape(double_quote, double_quote, pcmk__xml_escape_text); + assert_escape(double_quote, double_quote_esc_ref, pcmk__xml_escape_attr); + assert_escape(double_quote, double_quote_esc_backslash, + pcmk__xml_escape_attr_pretty); +} + +static void +escape_newline(void **state) +{ + const char *newline = "\nabc\ndef\n"; + const char *newline_esc_ref = "
abc
def
"; + const char *newline_esc_backslash = "\\nabc\\ndef\\n"; + + assert_escape(newline, newline, pcmk__xml_escape_text); + assert_escape(newline, newline_esc_ref, pcmk__xml_escape_attr); + assert_escape(newline, newline_esc_backslash, pcmk__xml_escape_attr_pretty); +} + +static void +escape_tab(void **state) +{ + const char *tab = "\tabc\tdef\t"; + const char *tab_esc_ref = "	abc	def	"; + const char *tab_esc_backslash = "\\tabc\\tdef\\t"; + + assert_escape(tab, tab, pcmk__xml_escape_text); + assert_escape(tab, tab_esc_ref, pcmk__xml_escape_attr); + assert_escape(tab, tab_esc_backslash, pcmk__xml_escape_attr_pretty); +} + +static void +escape_carriage_return(void **state) +{ + const char *cr = "\rabc\rdef\r"; + const char *cr_esc_ref = "
abc
def
"; + const char *cr_esc_backslash = "\\rabc\\rdef\\r"; + + assert_escape(cr, cr_esc_ref, pcmk__xml_escape_text); + assert_escape(cr, cr_esc_ref, pcmk__xml_escape_attr); + assert_escape(cr, cr_esc_backslash, pcmk__xml_escape_attr_pretty); +} + +static void +escape_nonprinting(void **state) +{ + const char *nonprinting = "\a\x7F\x1B"; + const char *nonprinting_esc = ""; + + assert_escape(nonprinting, nonprinting_esc, pcmk__xml_escape_text); + assert_escape(nonprinting, nonprinting_esc, pcmk__xml_escape_attr); + assert_escape(nonprinting, nonprinting, pcmk__xml_escape_attr_pretty); +} + +static void +escape_utf8(void **state) +{ + /* Non-ASCII UTF-8 characters may be two, three, or four 8-bit bytes wide + * and should not be escaped. + */ + const char *chinese = "仅高级使用"; + const char *two_byte = "abc""\xCF\xA6""d<ef"; + const char *two_byte_esc = "abc""\xCF\xA6""d" PCMK__XML_ENTITY_LT "ef"; + + const char *three_byte = "abc""\xEF\x98\x98""d<ef"; + const char *three_byte_esc = "abc""\xEF\x98\x98""d" + PCMK__XML_ENTITY_LT "ef"; + + const char *four_byte = "abc""\xF0\x94\x81\x90""d<ef"; + const char *four_byte_esc = "abc""\xF0\x94\x81\x90""d" + PCMK__XML_ENTITY_LT "ef"; + + assert_escape(chinese, chinese, pcmk__xml_escape_text); + assert_escape(chinese, chinese, pcmk__xml_escape_attr); + assert_escape(chinese, chinese, pcmk__xml_escape_attr_pretty); + + assert_escape(two_byte, two_byte_esc, pcmk__xml_escape_text); + assert_escape(two_byte, two_byte_esc, pcmk__xml_escape_attr); + assert_escape(two_byte, two_byte, pcmk__xml_escape_attr_pretty); + + assert_escape(three_byte, three_byte_esc, pcmk__xml_escape_text); + assert_escape(three_byte, three_byte_esc, pcmk__xml_escape_attr); + assert_escape(three_byte, three_byte, pcmk__xml_escape_attr_pretty); + + assert_escape(four_byte, four_byte_esc, pcmk__xml_escape_text); + assert_escape(four_byte, four_byte_esc, pcmk__xml_escape_attr); + assert_escape(four_byte, four_byte, pcmk__xml_escape_attr_pretty); +} + +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL, + cmocka_unit_test(null_empty), + cmocka_unit_test(invalid_type), + cmocka_unit_test(escape_unchanged), + cmocka_unit_test(escape_left_angle), + cmocka_unit_test(escape_right_angle), + cmocka_unit_test(escape_ampersand), + cmocka_unit_test(escape_double_quote), + cmocka_unit_test(escape_newline), + cmocka_unit_test(escape_tab), + cmocka_unit_test(escape_carriage_return), + cmocka_unit_test(escape_nonprinting), + cmocka_unit_test(escape_utf8)); diff --git a/lib/common/tests/xml/pcmk__xml_needs_escape_test.c b/lib/common/tests/xml/pcmk__xml_needs_escape_test.c new file mode 100644 index 0000000..612f61b --- /dev/null +++ b/lib/common/tests/xml/pcmk__xml_needs_escape_test.c @@ -0,0 +1,337 @@ +/* + * 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 <crm_internal.h> + +#include <crm/common/unittest_internal.h> +#include <crm/common/xml_internal.h> + +#include "crmcommon_private.h" + +static void +null_empty(void **state) +{ + assert_false(pcmk__xml_needs_escape(NULL, pcmk__xml_escape_text)); + assert_false(pcmk__xml_needs_escape(NULL, pcmk__xml_escape_attr)); + assert_false(pcmk__xml_needs_escape(NULL, pcmk__xml_escape_attr_pretty)); + + assert_false(pcmk__xml_needs_escape("", pcmk__xml_escape_text)); + assert_false(pcmk__xml_needs_escape("", pcmk__xml_escape_attr)); + assert_false(pcmk__xml_needs_escape("", pcmk__xml_escape_attr_pretty)); +} + +static void +invalid_type(void **state) +{ + const enum pcmk__xml_escape_type type = (enum pcmk__xml_escape_type) -1; + + // Easier to ignore invalid type for NULL or empty string + assert_false(pcmk__xml_needs_escape(NULL, type)); + assert_false(pcmk__xml_needs_escape("", type)); + + // Otherwise, assert if we somehow passed an invalid type + pcmk__assert_asserts(pcmk__xml_needs_escape("he<>llo", type)); +} + +static void +escape_unchanged(void **state) +{ + // No escaped characters (note: this string includes single quote at end) + const char *unchanged = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789" + "`~!@#$%^*()-_=+/|\\[]{}?.,'"; + + assert_false(pcmk__xml_needs_escape(unchanged, pcmk__xml_escape_text)); + assert_false(pcmk__xml_needs_escape(unchanged, pcmk__xml_escape_attr)); + assert_false(pcmk__xml_needs_escape(unchanged, + pcmk__xml_escape_attr_pretty)); +} + +// Ensure special characters get escaped at start, middle, and end + +static void +escape_left_angle(void **state) +{ + const char *l_angle_left = "<abcdef"; + const char *l_angle_mid = "abc<def"; + const char *l_angle_right = "abcdef<"; + + assert_true(pcmk__xml_needs_escape(l_angle_left, pcmk__xml_escape_text)); + assert_true(pcmk__xml_needs_escape(l_angle_mid, pcmk__xml_escape_text)); + assert_true(pcmk__xml_needs_escape(l_angle_right, pcmk__xml_escape_text)); + + assert_true(pcmk__xml_needs_escape(l_angle_left, pcmk__xml_escape_attr)); + assert_true(pcmk__xml_needs_escape(l_angle_mid, pcmk__xml_escape_attr)); + assert_true(pcmk__xml_needs_escape(l_angle_right, pcmk__xml_escape_attr)); + + assert_false(pcmk__xml_needs_escape(l_angle_left, + pcmk__xml_escape_attr_pretty)); + assert_false(pcmk__xml_needs_escape(l_angle_mid, + pcmk__xml_escape_attr_pretty)); + assert_false(pcmk__xml_needs_escape(l_angle_right, + pcmk__xml_escape_attr_pretty)); +} + +static void +escape_right_angle(void **state) +{ + const char *r_angle_left = ">abcdef"; + const char *r_angle_mid = "abc>def"; + const char *r_angle_right = "abcdef>"; + + assert_true(pcmk__xml_needs_escape(r_angle_left, pcmk__xml_escape_text)); + assert_true(pcmk__xml_needs_escape(r_angle_mid, pcmk__xml_escape_text)); + assert_true(pcmk__xml_needs_escape(r_angle_right, pcmk__xml_escape_text)); + + assert_true(pcmk__xml_needs_escape(r_angle_left, pcmk__xml_escape_attr)); + assert_true(pcmk__xml_needs_escape(r_angle_mid, pcmk__xml_escape_attr)); + assert_true(pcmk__xml_needs_escape(r_angle_right, pcmk__xml_escape_attr)); + + assert_false(pcmk__xml_needs_escape(r_angle_left, + pcmk__xml_escape_attr_pretty)); + assert_false(pcmk__xml_needs_escape(r_angle_mid, + pcmk__xml_escape_attr_pretty)); + assert_false(pcmk__xml_needs_escape(r_angle_right, + pcmk__xml_escape_attr_pretty)); +} + +static void +escape_ampersand(void **state) +{ + const char *ampersand_left = "&abcdef"; + const char *ampersand_mid = "abc&def"; + const char *ampersand_right = "abcdef&"; + + assert_true(pcmk__xml_needs_escape(ampersand_left, pcmk__xml_escape_text)); + assert_true(pcmk__xml_needs_escape(ampersand_mid, pcmk__xml_escape_text)); + assert_true(pcmk__xml_needs_escape(ampersand_right, pcmk__xml_escape_text)); + + assert_true(pcmk__xml_needs_escape(ampersand_left, pcmk__xml_escape_attr)); + assert_true(pcmk__xml_needs_escape(ampersand_mid, pcmk__xml_escape_attr)); + assert_true(pcmk__xml_needs_escape(ampersand_right, pcmk__xml_escape_attr)); + + assert_false(pcmk__xml_needs_escape(ampersand_left, + pcmk__xml_escape_attr_pretty)); + assert_false(pcmk__xml_needs_escape(ampersand_mid, + pcmk__xml_escape_attr_pretty)); + assert_false(pcmk__xml_needs_escape(ampersand_right, + pcmk__xml_escape_attr_pretty)); +} + +static void +escape_double_quote(void **state) +{ + const char *double_quote_left = "\"abcdef"; + const char *double_quote_mid = "abc\"def"; + const char *double_quote_right = "abcdef\""; + + assert_false(pcmk__xml_needs_escape(double_quote_left, + pcmk__xml_escape_text)); + assert_false(pcmk__xml_needs_escape(double_quote_mid, + pcmk__xml_escape_text)); + assert_false(pcmk__xml_needs_escape(double_quote_right, + pcmk__xml_escape_text)); + + assert_true(pcmk__xml_needs_escape(double_quote_left, + pcmk__xml_escape_attr)); + assert_true(pcmk__xml_needs_escape(double_quote_mid, + pcmk__xml_escape_attr)); + assert_true(pcmk__xml_needs_escape(double_quote_right, + pcmk__xml_escape_attr)); + + assert_true(pcmk__xml_needs_escape(double_quote_left, + pcmk__xml_escape_attr_pretty)); + assert_true(pcmk__xml_needs_escape(double_quote_mid, + pcmk__xml_escape_attr_pretty)); + assert_true(pcmk__xml_needs_escape(double_quote_right, + pcmk__xml_escape_attr_pretty)); +} + +static void +escape_newline(void **state) +{ + const char *newline_left = "\nabcdef"; + const char *newline_mid = "abc\ndef"; + const char *newline_right = "abcdef\n"; + + assert_false(pcmk__xml_needs_escape(newline_left, pcmk__xml_escape_text)); + assert_false(pcmk__xml_needs_escape(newline_mid, pcmk__xml_escape_text)); + assert_false(pcmk__xml_needs_escape(newline_right, pcmk__xml_escape_text)); + + assert_true(pcmk__xml_needs_escape(newline_left, pcmk__xml_escape_attr)); + assert_true(pcmk__xml_needs_escape(newline_mid, pcmk__xml_escape_attr)); + assert_true(pcmk__xml_needs_escape(newline_right, pcmk__xml_escape_attr)); + + assert_true(pcmk__xml_needs_escape(newline_left, + pcmk__xml_escape_attr_pretty)); + assert_true(pcmk__xml_needs_escape(newline_mid, + pcmk__xml_escape_attr_pretty)); + assert_true(pcmk__xml_needs_escape(newline_right, + pcmk__xml_escape_attr_pretty)); +} + +static void +escape_tab(void **state) +{ + const char *tab_left = "\tabcdef"; + const char *tab_mid = "abc\tdef"; + const char *tab_right = "abcdef\t"; + + assert_false(pcmk__xml_needs_escape(tab_left, pcmk__xml_escape_text)); + assert_false(pcmk__xml_needs_escape(tab_mid, pcmk__xml_escape_text)); + assert_false(pcmk__xml_needs_escape(tab_right, pcmk__xml_escape_text)); + + assert_true(pcmk__xml_needs_escape(tab_left, pcmk__xml_escape_attr)); + assert_true(pcmk__xml_needs_escape(tab_mid, pcmk__xml_escape_attr)); + assert_true(pcmk__xml_needs_escape(tab_right, pcmk__xml_escape_attr)); + + assert_true(pcmk__xml_needs_escape(tab_left, pcmk__xml_escape_attr_pretty)); + assert_true(pcmk__xml_needs_escape(tab_mid, pcmk__xml_escape_attr_pretty)); + assert_true(pcmk__xml_needs_escape(tab_right, + pcmk__xml_escape_attr_pretty)); +} + +static void +escape_carriage_return(void **state) +{ + const char *cr_left = "\rabcdef"; + const char *cr_mid = "abc\rdef"; + const char *cr_right = "abcdef\r"; + + assert_true(pcmk__xml_needs_escape(cr_left, pcmk__xml_escape_text)); + assert_true(pcmk__xml_needs_escape(cr_mid, pcmk__xml_escape_text)); + assert_true(pcmk__xml_needs_escape(cr_right, pcmk__xml_escape_text)); + + assert_true(pcmk__xml_needs_escape(cr_left, pcmk__xml_escape_attr)); + assert_true(pcmk__xml_needs_escape(cr_mid, pcmk__xml_escape_attr)); + assert_true(pcmk__xml_needs_escape(cr_right, pcmk__xml_escape_attr)); + + assert_true(pcmk__xml_needs_escape(cr_left, pcmk__xml_escape_attr_pretty)); + assert_true(pcmk__xml_needs_escape(cr_mid, pcmk__xml_escape_attr_pretty)); + assert_true(pcmk__xml_needs_escape(cr_right, pcmk__xml_escape_attr_pretty)); +} + +static void +escape_nonprinting(void **state) +{ + const char *alert_left = "\aabcdef"; + const char *alert_mid = "abc\adef"; + const char *alert_right = "abcdef\a"; + + const char *delete_left = "\x7F""abcdef"; + const char *delete_mid = "abc\x7F""def"; + const char *delete_right = "abcdef\x7F"; + + const char *nonprinting_all = "\a\x7F\x1B"; + + assert_true(pcmk__xml_needs_escape(alert_left, pcmk__xml_escape_text)); + assert_true(pcmk__xml_needs_escape(alert_mid, pcmk__xml_escape_text)); + assert_true(pcmk__xml_needs_escape(alert_right, pcmk__xml_escape_text)); + + assert_true(pcmk__xml_needs_escape(alert_left, pcmk__xml_escape_attr)); + assert_true(pcmk__xml_needs_escape(alert_mid, pcmk__xml_escape_attr)); + assert_true(pcmk__xml_needs_escape(alert_right, pcmk__xml_escape_attr)); + + assert_false(pcmk__xml_needs_escape(alert_left, + pcmk__xml_escape_attr_pretty)); + assert_false(pcmk__xml_needs_escape(alert_mid, + pcmk__xml_escape_attr_pretty)); + assert_false(pcmk__xml_needs_escape(alert_right, + pcmk__xml_escape_attr_pretty)); + + assert_true(pcmk__xml_needs_escape(delete_left, pcmk__xml_escape_text)); + assert_true(pcmk__xml_needs_escape(delete_mid, pcmk__xml_escape_text)); + assert_true(pcmk__xml_needs_escape(delete_right, pcmk__xml_escape_text)); + + assert_true(pcmk__xml_needs_escape(delete_left, pcmk__xml_escape_attr)); + assert_true(pcmk__xml_needs_escape(delete_mid, pcmk__xml_escape_attr)); + assert_true(pcmk__xml_needs_escape(delete_right, pcmk__xml_escape_attr)); + + assert_false(pcmk__xml_needs_escape(delete_left, + pcmk__xml_escape_attr_pretty)); + assert_false(pcmk__xml_needs_escape(delete_mid, + pcmk__xml_escape_attr_pretty)); + assert_false(pcmk__xml_needs_escape(delete_right, + pcmk__xml_escape_attr_pretty)); + + assert_true(pcmk__xml_needs_escape(nonprinting_all, pcmk__xml_escape_text)); + assert_true(pcmk__xml_needs_escape(nonprinting_all, pcmk__xml_escape_attr)); + assert_false(pcmk__xml_needs_escape(nonprinting_all, + pcmk__xml_escape_attr_pretty)); +} + +static void +escape_utf8(void **state) +{ + /* Non-ASCII UTF-8 characters may be two, three, or four 8-bit bytes wide + * and should not be escaped. + */ + const char *chinese = "仅高级使用"; + const char *two_byte = "abc""\xCF\xA6""def"; + const char *two_byte_special = "abc""\xCF\xA6""d<ef"; + const char *three_byte = "abc""\xEF\x98\x98""def"; + const char *three_byte_special = "abc""\xEF\x98\x98""d<ef"; + const char *four_byte = "abc""\xF0\x94\x81\x90""def"; + const char *four_byte_special = "abc""\xF0\x94\x81\x90""d<ef"; + + assert_false(pcmk__xml_needs_escape(chinese, pcmk__xml_escape_text)); + assert_false(pcmk__xml_needs_escape(chinese, pcmk__xml_escape_attr)); + assert_false(pcmk__xml_needs_escape(chinese, pcmk__xml_escape_attr_pretty)); + + assert_false(pcmk__xml_needs_escape(two_byte, pcmk__xml_escape_text)); + assert_false(pcmk__xml_needs_escape(two_byte, pcmk__xml_escape_attr)); + assert_false(pcmk__xml_needs_escape(two_byte, + pcmk__xml_escape_attr_pretty)); + + assert_true(pcmk__xml_needs_escape(two_byte_special, + pcmk__xml_escape_text)); + assert_true(pcmk__xml_needs_escape(two_byte_special, + pcmk__xml_escape_attr)); + assert_false(pcmk__xml_needs_escape(two_byte_special, + pcmk__xml_escape_attr_pretty)); + + assert_false(pcmk__xml_needs_escape(three_byte, pcmk__xml_escape_text)); + assert_false(pcmk__xml_needs_escape(three_byte, pcmk__xml_escape_attr)); + assert_false(pcmk__xml_needs_escape(three_byte, + pcmk__xml_escape_attr_pretty)); + + assert_true(pcmk__xml_needs_escape(three_byte_special, + pcmk__xml_escape_text)); + assert_true(pcmk__xml_needs_escape(three_byte_special, + pcmk__xml_escape_attr)); + assert_false(pcmk__xml_needs_escape(three_byte_special, + pcmk__xml_escape_attr_pretty)); + + assert_false(pcmk__xml_needs_escape(four_byte, pcmk__xml_escape_text)); + assert_false(pcmk__xml_needs_escape(four_byte, pcmk__xml_escape_attr)); + assert_false(pcmk__xml_needs_escape(four_byte, + pcmk__xml_escape_attr_pretty)); + + assert_true(pcmk__xml_needs_escape(four_byte_special, + pcmk__xml_escape_text)); + assert_true(pcmk__xml_needs_escape(four_byte_special, + pcmk__xml_escape_attr)); + assert_false(pcmk__xml_needs_escape(four_byte_special, + pcmk__xml_escape_attr_pretty)); +} + +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL, + cmocka_unit_test(null_empty), + cmocka_unit_test(invalid_type), + cmocka_unit_test(escape_unchanged), + cmocka_unit_test(escape_left_angle), + cmocka_unit_test(escape_right_angle), + cmocka_unit_test(escape_ampersand), + cmocka_unit_test(escape_double_quote), + cmocka_unit_test(escape_newline), + cmocka_unit_test(escape_tab), + cmocka_unit_test(escape_carriage_return), + cmocka_unit_test(escape_nonprinting), + cmocka_unit_test(escape_utf8)); diff --git a/lib/common/tests/xpath/pcmk__xpath_node_id_test.c b/lib/common/tests/xpath/pcmk__xpath_node_id_test.c index 3922b34..86500c2 100644 --- a/lib/common/tests/xpath/pcmk__xpath_node_id_test.c +++ b/lib/common/tests/xpath/pcmk__xpath_node_id_test.c @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 the Pacemaker project contributors + * Copyright 2021-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -9,14 +9,14 @@ #include <crm_internal.h> -#include <crm/msg_xml.h> +#include <crm/common/xml.h> #include <crm/common/unittest_internal.h> #include <crm/common/xml_internal.h> static void empty_input(void **state) { - assert_null(pcmk__xpath_node_id(NULL, "lrm")); - assert_null(pcmk__xpath_node_id("", "lrm")); + assert_null(pcmk__xpath_node_id(NULL, PCMK__XE_LRM)); + assert_null(pcmk__xpath_node_id("", PCMK__XE_LRM)); assert_null(pcmk__xpath_node_id("/blah/blah", NULL)); assert_null(pcmk__xpath_node_id("/blah/blah", "")); assert_null(pcmk__xpath_node_id(NULL, NULL)); @@ -24,30 +24,31 @@ empty_input(void **state) { static void no_quotes(void **state) { - const char *xpath = "/some/xpath/lrm[@" XML_ATTR_ID "=xyz]"; - pcmk__assert_asserts(pcmk__xpath_node_id(xpath, "lrm")); + const char *xpath = "/some/xpath/" PCMK__XE_LRM "[@" PCMK_XA_ID "=xyz]"; + pcmk__assert_asserts(pcmk__xpath_node_id(xpath, PCMK__XE_LRM)); } static void not_present(void **state) { - const char *xpath = "/some/xpath/string[@" XML_ATTR_ID "='xyz']"; - assert_null(pcmk__xpath_node_id(xpath, "lrm")); + const char *xpath = "/some/xpath/string[@" PCMK_XA_ID "='xyz']"; + assert_null(pcmk__xpath_node_id(xpath, PCMK__XE_LRM)); - xpath = "/some/xpath/containing[@" XML_ATTR_ID "='lrm']"; - assert_null(pcmk__xpath_node_id(xpath, "lrm")); + xpath = "/some/xpath/containing[@" PCMK_XA_ID "='" PCMK__XE_LRM "']"; + assert_null(pcmk__xpath_node_id(xpath, PCMK__XE_LRM)); } static void present(void **state) { char *s = NULL; - const char *xpath = "/some/xpath/containing/lrm[@" XML_ATTR_ID "='xyz']"; + const char *xpath = "/some/xpath/containing" + "/" PCMK__XE_LRM "[@" PCMK_XA_ID "='xyz']"; - s = pcmk__xpath_node_id(xpath, "lrm"); + s = pcmk__xpath_node_id(xpath, PCMK__XE_LRM); assert_int_equal(strcmp(s, "xyz"), 0); free(s); - xpath = "/some/other/lrm[@" XML_ATTR_ID "='xyz']/xpath"; - s = pcmk__xpath_node_id(xpath, "lrm"); + xpath = "/some/other/" PCMK__XE_LRM "[@" PCMK_XA_ID "='xyz']/xpath"; + s = pcmk__xpath_node_id(xpath, PCMK__XE_LRM); assert_int_equal(strcmp(s, "xyz"), 0); free(s); } diff --git a/lib/common/unittest.c b/lib/common/unittest.c new file mode 100644 index 0000000..0e63915 --- /dev/null +++ b/lib/common/unittest.c @@ -0,0 +1,128 @@ +/* + * 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 Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <crm/common/unittest_internal.h> + +#include <stdlib.h> +#include <unistd.h> + +// LCOV_EXCL_START + +void +pcmk__assert_validates(xmlNode *xml) +{ + const char *schema_dir = NULL; + char *cmd = NULL; + gchar *out = NULL; + gchar *err = NULL; + gint status; + GError *gerr = NULL; + char *xmllint_input = crm_strdup_printf("%s/test-xmllint.XXXXXX", + pcmk__get_tmpdir()); + int fd; + int rc; + + fd = mkstemp(xmllint_input); + if (fd < 0) { + fail_msg("Could not create temp file: %s", strerror(errno)); + } + + rc = pcmk__xml2fd(fd, xml); + if (rc != pcmk_rc_ok) { + unlink(xmllint_input); + fail_msg("Could not write temp file: %s", pcmk_rc_str(rc)); + } + + close(fd); + + /* This should be set as part of AM_TESTS_ENVIRONMENT in Makefile.am. */ + schema_dir = getenv("PCMK_schema_directory"); + if (schema_dir == NULL) { + unlink(xmllint_input); + fail_msg("PCMK_schema_directory is not set in test environment"); + } + + cmd = crm_strdup_printf("xmllint --relaxng %s/api/api-result.rng %s", + schema_dir, xmllint_input); + + if (!g_spawn_command_line_sync(cmd, &out, &err, &status, &gerr)) { + unlink(xmllint_input); + fail_msg("Error occurred when performing validation: %s", gerr->message); + } + + if (WIFEXITED(status) && WEXITSTATUS(status) != 0) { + unlink(xmllint_input); + fail_msg("XML validation failed: %s\n%s\n", out, err); + } + + free(cmd); + g_free(out); + g_free(err); + unlink(xmllint_input); + free(xmllint_input); +} + +int +pcmk__xml_test_setup_group(void **state) +{ + /* This needs to be run before we run unit tests that manipulate XML. + * Without it, document private data won't get created, which can cause + * segmentation faults or assertions in functions related to change + * tracking and ACLs. There's no harm in doing this before all tests. + */ + crm_xml_init(); + return 0; +} + +char * +pcmk__cib_test_copy_cib(const char *in_file) +{ + char *in_path = crm_strdup_printf("%s/%s", getenv("PCMK_CTS_CLI_DIR"), in_file); + char *out_path = NULL; + char *contents = NULL; + int fd; + + /* Copy the CIB over to a temp location so we can modify it. */ + out_path = crm_strdup_printf("%s/test-cib.XXXXXX", pcmk__get_tmpdir()); + + fd = mkstemp(out_path); + if (fd < 0) { + free(out_path); + return NULL; + } + + if (pcmk__file_contents(in_path, &contents) != pcmk_rc_ok) { + free(out_path); + close(fd); + return NULL; + } + + if (pcmk__write_sync(fd, contents) != pcmk_rc_ok) { + free(out_path); + free(in_path); + free(contents); + close(fd); + return NULL; + } + + setenv("CIB_file", out_path, 1); + return out_path; +} + +void +pcmk__cib_test_cleanup(char *out_path) +{ + unlink(out_path); + free(out_path); + unsetenv("CIB_file"); +} + +// LCOV_EXCL_STOP diff --git a/lib/common/utils.c b/lib/common/utils.c index e5b9ef0..e8d343e 100644 --- a/lib/common/utils.c +++ b/lib/common/utils.c @@ -1,5 +1,5 @@ /* - * Copyright 2004-2022 the Pacemaker project contributors + * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -33,7 +33,6 @@ #include <crm/crm.h> #include <crm/services.h> -#include <crm/msg_xml.h> #include <crm/cib/internal.h> #include <crm/common/xml.h> #include <crm/common/util.h> @@ -257,46 +256,6 @@ compare_version(const char *version1, const char *version2) } /*! - * \brief Parse milliseconds from a Pacemaker interval specification - * - * \param[in] input Pacemaker time interval specification (a bare number of - * seconds, a number with a unit optionally with whitespace - * before and/or after the number, or an ISO 8601 duration) - * - * \return Milliseconds equivalent of given specification on success (limited - * to the range of an unsigned integer), 0 if input is NULL, - * or 0 (and set errno to EINVAL) on error - */ -guint -crm_parse_interval_spec(const char *input) -{ - long long msec = -1; - - errno = 0; - if (input == NULL) { - return 0; - - } else if (input[0] == 'P') { - crm_time_t *period_s = crm_time_parse_duration(input); - - if (period_s) { - msec = 1000 * crm_time_get_seconds(period_s); - crm_time_free(period_s); - } - - } else { - msec = crm_get_msec(input); - } - - if (msec < 0) { - crm_warn("Using 0 instead of '%s'", input); - errno = EINVAL; - return 0; - } - return (msec >= G_MAXUINT)? G_MAXUINT : (guint) msec; -} - -/*! * \internal * \brief Log a failed assertion * @@ -464,43 +423,6 @@ pcmk__daemonize(const char *name, const char *pidfile) pcmk__open_devnull(O_WRONLY); // stderr (fd 2) } -char * -crm_meta_name(const char *field) -{ - int lpc = 0; - int max = 0; - char *crm_name = NULL; - - CRM_CHECK(field != NULL, return NULL); - crm_name = crm_strdup_printf(CRM_META "_%s", field); - - /* Massage the names so they can be used as shell variables */ - max = strlen(crm_name); - for (; lpc < max; lpc++) { - switch (crm_name[lpc]) { - case '-': - crm_name[lpc] = '_'; - break; - } - } - return crm_name; -} - -const char * -crm_meta_value(GHashTable * hash, const char *field) -{ - char *key = NULL; - const char *value = NULL; - - key = crm_meta_name(field); - if (key) { - value = g_hash_table_lookup(hash, key); - free(key); - } - - return value; -} - #ifdef HAVE_UUID_UUID_H # include <uuid/uuid.h> #endif @@ -511,7 +433,7 @@ crm_generate_uuid(void) unsigned char uuid[16]; char *buffer = malloc(37); /* Including NUL byte */ - CRM_ASSERT(buffer != NULL); + pcmk__mem_assert(buffer); uuid_generate(uuid); uuid_unparse(uuid, buffer); return buffer; @@ -526,27 +448,15 @@ crm_gnutls_global_init(void) } #endif -/*! - * \brief Get the local hostname - * - * \return Newly allocated string with name, or NULL (and set errno) on error - */ -char * -pcmk_hostname(void) -{ - struct utsname hostinfo; - - return (uname(&hostinfo) < 0)? NULL : strdup(hostinfo.nodename); -} - bool pcmk_str_is_infinity(const char *s) { - return pcmk__str_any_of(s, CRM_INFINITY_S, CRM_PLUS_INFINITY_S, NULL); + return pcmk__str_any_of(s, PCMK_VALUE_INFINITY, PCMK_VALUE_PLUS_INFINITY, + NULL); } bool pcmk_str_is_minus_infinity(const char *s) { - return pcmk__str_eq(s, CRM_MINUS_INFINITY_S, pcmk__str_none); + return pcmk__str_eq(s, PCMK_VALUE_MINUS_INFINITY, pcmk__str_none); } /*! @@ -592,3 +502,48 @@ pcmk__sleep_ms(unsigned int ms) } #endif } + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include <crm/common/util_compat.h> + +guint +crm_parse_interval_spec(const char *input) +{ + long long msec = -1; + + errno = 0; + if (input == NULL) { + return 0; + + } else if (input[0] == 'P') { + crm_time_t *period_s = crm_time_parse_duration(input); + + if (period_s) { + msec = 1000 * crm_time_get_seconds(period_s); + crm_time_free(period_s); + } + + } else { + msec = crm_get_msec(input); + } + + if (msec < 0) { + crm_warn("Using 0 instead of '%s'", input); + errno = EINVAL; + return 0; + } + return (msec >= G_MAXUINT)? G_MAXUINT : (guint) msec; +} + +char * +pcmk_hostname(void) +{ + struct utsname hostinfo; + + return (uname(&hostinfo) < 0)? NULL : strdup(hostinfo.nodename); +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/watchdog.c b/lib/common/watchdog.c index e569214..b7921fe 100644 --- a/lib/common/watchdog.c +++ b/lib/common/watchdog.c @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the Pacemaker project contributors + * Copyright 2013-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -112,10 +112,10 @@ panic_local(void) if(ppid > 1) { /* child daemon */ - exit(CRM_EX_PANIC); + crm_exit(CRM_EX_PANIC); } else { /* pacemakerd or orphan child */ - exit(CRM_EX_FATAL); + crm_exit(CRM_EX_FATAL); } } @@ -141,10 +141,10 @@ panic_sbd(void) if(ppid > 1) { /* child daemon */ - exit(CRM_EX_PANIC); + crm_exit(CRM_EX_PANIC); } else { /* pacemakerd or orphan child */ - exit(CRM_EX_FATAL); + crm_exit(CRM_EX_FATAL); } } @@ -232,7 +232,7 @@ pcmk__locate_sbd(void) } long -pcmk__get_sbd_timeout(void) +pcmk__get_sbd_watchdog_timeout(void) { static long sbd_timeout = -2; @@ -266,44 +266,53 @@ pcmk__get_sbd_sync_resource_startup(void) } long -pcmk__auto_watchdog_timeout(void) +pcmk__auto_stonith_watchdog_timeout(void) { - long sbd_timeout = pcmk__get_sbd_timeout(); + long sbd_timeout = pcmk__get_sbd_watchdog_timeout(); return (sbd_timeout <= 0)? 0 : (2 * sbd_timeout); } bool -pcmk__valid_sbd_timeout(const char *value) +pcmk__valid_stonith_watchdog_timeout(const char *value) { + /* @COMPAT At a compatibility break, accept either negative values or a + * specific string like "auto" (but not both) to mean "auto-calculate the + * timeout." Reject other values that aren't parsable as timeouts. + */ long st_timeout = value? crm_get_msec(value) : 0; if (st_timeout < 0) { - st_timeout = pcmk__auto_watchdog_timeout(); - crm_debug("Using calculated value %ld for stonith-watchdog-timeout (%s)", + st_timeout = pcmk__auto_stonith_watchdog_timeout(); + crm_debug("Using calculated value %ld for " + PCMK_OPT_STONITH_WATCHDOG_TIMEOUT " (%s)", st_timeout, value); } if (st_timeout == 0) { - crm_debug("Watchdog may be enabled but stonith-watchdog-timeout is disabled (%s)", + crm_debug("Watchdog may be enabled but " + PCMK_OPT_STONITH_WATCHDOG_TIMEOUT " is disabled (%s)", value? value : "default"); } else if (pcmk__locate_sbd() == 0) { - crm_emerg("Shutting down: stonith-watchdog-timeout configured (%s) " - "but SBD not active", (value? value : "auto")); + crm_emerg("Shutting down: " PCMK_OPT_STONITH_WATCHDOG_TIMEOUT + " configured (%s) but SBD not active", + pcmk__s(value, "auto")); crm_exit(CRM_EX_FATAL); return false; } else { - long sbd_timeout = pcmk__get_sbd_timeout(); + long sbd_timeout = pcmk__get_sbd_watchdog_timeout(); if (st_timeout < sbd_timeout) { - crm_emerg("Shutting down: stonith-watchdog-timeout (%s) too short " - "(must be >%ldms)", value, sbd_timeout); + crm_emerg("Shutting down: " PCMK_OPT_STONITH_WATCHDOG_TIMEOUT + " (%s) too short (must be >%ldms)", + value, sbd_timeout); crm_exit(CRM_EX_FATAL); return false; } - crm_info("Watchdog configured with stonith-watchdog-timeout %s and SBD timeout %ldms", + crm_info("Watchdog configured with " PCMK_OPT_STONITH_WATCHDOG_TIMEOUT + " %s and SBD timeout %ldms", value, sbd_timeout); } return true; diff --git a/lib/common/xml.c b/lib/common/xml.c index 53ebff7..73d6025 100644 --- a/lib/common/xml.c +++ b/lib/common/xml.c @@ -1,5 +1,5 @@ /* - * 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. * @@ -9,41 +9,52 @@ #include <crm_internal.h> +#include <stdarg.h> +#include <stdint.h> // uint32_t #include <stdio.h> -#include <sys/types.h> -#include <unistd.h> -#include <time.h> -#include <string.h> #include <stdlib.h> -#include <stdarg.h> -#include <bzlib.h> +#include <string.h> +#include <sys/stat.h> // stat(), S_ISREG, etc. +#include <sys/types.h> #include <libxml/parser.h> #include <libxml/tree.h> -#include <libxml/xmlIO.h> /* xmlAllocOutputBuffer */ #include <crm/crm.h> -#include <crm/msg_xml.h> #include <crm/common/xml.h> -#include <crm/common/xml_internal.h> // PCMK__XML_LOG_BASE, etc. +#include <crm/common/xml_internal.h> // PCMK__XML_LOG_BASE, etc. #include "crmcommon_private.h" -// Define this as 1 in development to get insanely verbose trace messages -#ifndef XML_PARSER_DEBUG -#define XML_PARSER_DEBUG 0 -#endif - -/* @TODO XML_PARSE_RECOVER allows some XML errors to be silently worked around - * by libxml2, which is potentially ambiguous and dangerous. We should drop it - * when we can break backward compatibility with configurations that might be - * relying on it (i.e. pacemaker 3.0.0). +/*! + * \internal + * \brief Apply a function to each XML node in a tree (pre-order, depth-first) * - * It might be a good idea to have a transitional period where we first try - * parsing without XML_PARSE_RECOVER, and if that fails, try parsing again with - * it, logging a warning if it succeeds. + * \param[in,out] xml XML tree to traverse + * \param[in,out] fn Function to call for each node (returns \c true to + * continue traversing the tree or \c false to stop) + * \param[in,out] user_data Argument to \p fn + * + * \return \c false if any \p fn call returned \c false, or \c true otherwise + * + * \note This function is recursive. */ -#define PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER (XML_PARSE_NOBLANKS) -#define PCMK__XML_PARSE_OPTS_WITH_RECOVER (XML_PARSE_NOBLANKS | XML_PARSE_RECOVER) +bool +pcmk__xml_tree_foreach(xmlNode *xml, bool (*fn)(xmlNode *, void *), + void *user_data) +{ + if (!fn(xml, user_data)) { + return false; + } + + for (xml = pcmk__xml_first_child(xml); xml != NULL; + xml = pcmk__xml_next(xml)) { + + if (!pcmk__xml_tree_foreach(xml, fn, user_data)) { + return false; + } + } + return true; +} bool pcmk__tracking_xml_changes(xmlNode *xml, bool lazy) @@ -93,43 +104,72 @@ pcmk__mark_xml_node_dirty(xmlNode *xml) set_parent_flag(xml, pcmk__xf_dirty); } -// Clear flags on XML node and its children -static void -reset_xml_node_flags(xmlNode *xml) +/*! + * \internal + * \brief Clear flags on an XML node + * + * \param[in,out] xml XML node whose flags to reset + * \param[in,out] user_data Ignored + * + * \return \c true (to continue traversing the tree) + * + * \note This is compatible with \c pcmk__xml_tree_foreach(). + */ +static bool +reset_xml_node_flags(xmlNode *xml, void *user_data) { - xmlNode *cIter = NULL; xml_node_private_t *nodepriv = xml->_private; - if (nodepriv) { - nodepriv->flags = 0; + if (nodepriv != NULL) { + nodepriv->flags = pcmk__xf_none; } + return true; +} - for (cIter = pcmk__xml_first_child(xml); cIter != NULL; - cIter = pcmk__xml_next(cIter)) { - reset_xml_node_flags(cIter); +/*! + * \internal + * \brief Set the \c pcmk__xf_dirty and \c pcmk__xf_created flags on an XML node + * + * \param[in,out] xml Node whose flags to set + * \param[in] user_data Ignored + * + * \return \c true (to continue traversing the tree) + * + * \note This is compatible with \c pcmk__xml_tree_foreach(). + */ +static bool +mark_xml_dirty_created(xmlNode *xml, void *user_data) +{ + xml_node_private_t *nodepriv = xml->_private; + + if (nodepriv != NULL) { + pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created); } + return true; } -// Set xpf_created flag on XML node and any children +/*! + * \internal + * \brief Mark an XML tree as dirty and created, and mark its parents dirty + * + * Also mark the document dirty. + * + * \param[in,out] xml Tree to mark as dirty and created + */ void -pcmk__mark_xml_created(xmlNode *xml) +pcmk__xml_mark_created(xmlNode *xml) { - xmlNode *cIter = NULL; - xml_node_private_t *nodepriv = NULL; - CRM_ASSERT(xml != NULL); - nodepriv = xml->_private; - if (nodepriv && pcmk__tracking_xml_changes(xml, FALSE)) { - if (!pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { - pcmk__set_xml_flags(nodepriv, pcmk__xf_created); - pcmk__mark_xml_node_dirty(xml); - } - for (cIter = pcmk__xml_first_child(xml); cIter != NULL; - cIter = pcmk__xml_next(cIter)) { - pcmk__mark_xml_created(cIter); - } + if (!pcmk__tracking_xml_changes(xml, false)) { + // Tracking is disabled for entire document + return; } + + // Mark all parents and document dirty + pcmk__mark_xml_node_dirty(xml); + + pcmk__xml_tree_foreach(xml, mark_xml_dirty_created, NULL); } #define XML_DOC_PRIVATE_MAGIC 0x81726354UL @@ -142,7 +182,7 @@ free_deleted_object(void *data) if(data) { pcmk__deleted_xml_t *deleted_obj = data; - free(deleted_obj->path); + g_free(deleted_obj->path); free(deleted_obj); } } @@ -220,9 +260,9 @@ new_private_data(xmlNode *node) { switch (node->type) { case XML_DOCUMENT_NODE: { - xml_doc_private_t *docpriv = NULL; - docpriv = calloc(1, sizeof(xml_doc_private_t)); - CRM_ASSERT(docpriv != NULL); + xml_doc_private_t *docpriv = + pcmk__assert_alloc(1, sizeof(xml_doc_private_t)); + docpriv->check = XML_DOC_PRIVATE_MAGIC; /* Flags will be reset if necessary when tracking is enabled */ pcmk__set_xml_flags(docpriv, pcmk__xf_dirty|pcmk__xf_created); @@ -232,9 +272,9 @@ new_private_data(xmlNode *node) case XML_ELEMENT_NODE: case XML_ATTRIBUTE_NODE: case XML_COMMENT_NODE: { - xml_node_private_t *nodepriv = NULL; - nodepriv = calloc(1, sizeof(xml_node_private_t)); - CRM_ASSERT(nodepriv != NULL); + xml_node_private_t *nodepriv = + pcmk__assert_alloc(1, sizeof(xml_node_private_t)); + nodepriv->check = XML_NODE_PRIVATE_MAGIC; /* Flags will be reset if necessary when tracking is enabled */ pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created); @@ -314,21 +354,23 @@ pcmk__xml_position(const xmlNode *xml, enum xml_private_flags ignore_if_set) return position; } -// Remove all attributes marked as deleted from an XML node -static void -accept_attr_deletions(xmlNode *xml) +/*! + * \internal + * \brief Remove all attributes marked as deleted from an XML node + * + * \param[in,out] xml XML node whose deleted attributes to remove + * \param[in,out] user_data Ignored + * + * \return \c true (to continue traversing the tree) + * + * \note This is compatible with \c pcmk__xml_tree_foreach(). + */ +static bool +accept_attr_deletions(xmlNode *xml, void *user_data) { - // Clear XML node's flags - ((xml_node_private_t *) xml->_private)->flags = pcmk__xf_none; - - // Remove this XML node's attributes that were marked as deleted + reset_xml_node_flags(xml, NULL); pcmk__xe_remove_matching_attrs(xml, pcmk__marked_as_deleted, NULL); - - // Recursively do the same for this XML node's children - for (xmlNodePtr cIter = pcmk__xml_first_child(xml); cIter != NULL; - cIter = pcmk__xml_next(cIter)) { - accept_attr_deletions(cIter); - } + return true; } /*! @@ -348,10 +390,11 @@ pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle, bool exact) return pcmk__xc_match(haystack, needle, exact); } else { - const char *id = ID(needle); - const char *attr = (id == NULL)? NULL : XML_ATTR_ID; + const char *id = pcmk__xe_id(needle); + const char *attr = (id == NULL)? NULL : PCMK_XA_ID; - return pcmk__xe_match(haystack, (const char *) needle->name, attr, id); + return pcmk__xe_first_child(haystack, (const char *) needle->name, attr, + id); } } @@ -377,207 +420,263 @@ xml_accept_changes(xmlNode * xml) } docpriv->flags = pcmk__xf_none; - accept_attr_deletions(top); -} - -xmlNode * -find_xml_node(const xmlNode *root, const char *search_path, gboolean must_find) -{ - xmlNode *a_child = NULL; - const char *name = (root == NULL)? "<NULL>" : (const char *) root->name; - - if (search_path == NULL) { - crm_warn("Will never find <NULL>"); - return NULL; - } - - for (a_child = pcmk__xml_first_child(root); a_child != NULL; - a_child = pcmk__xml_next(a_child)) { - if (strcmp((const char *)a_child->name, search_path) == 0) { - return a_child; - } - } - - if (must_find) { - crm_warn("Could not find %s in %s.", search_path, name); - } else if (root != NULL) { - crm_trace("Could not find %s in %s.", search_path, name); - } else { - crm_trace("Could not find %s in <NULL>.", search_path); - } - - return NULL; + pcmk__xml_tree_foreach(top, accept_attr_deletions, NULL); } -#define attr_matches(c, n, v) pcmk__str_eq(crm_element_value((c), (n)), \ - (v), pcmk__str_none) - /*! * \internal * \brief Find first XML child element matching given criteria * - * \param[in] parent XML element to search - * \param[in] node_name If not NULL, only match children of this type - * \param[in] attr_n If not NULL, only match children with an attribute + * \param[in] parent XML element to search (can be \c NULL) + * \param[in] node_name If not \c NULL, only match children of this type + * \param[in] attr_n If not \c NULL, only match children with an attribute * of this name. * \param[in] attr_v If \p attr_n and this are not NULL, only match children * with an attribute named \p attr_n and this value * - * \return Matching XML child element, or NULL if none found + * \return Matching XML child element, or \c NULL if none found */ xmlNode * -pcmk__xe_match(const xmlNode *parent, const char *node_name, - const char *attr_n, const char *attr_v) +pcmk__xe_first_child(const xmlNode *parent, const char *node_name, + const char *attr_n, const char *attr_v) { - CRM_CHECK(parent != NULL, return NULL); - CRM_CHECK(attr_v == NULL || attr_n != NULL, return NULL); + xmlNode *child = NULL; + const char *parent_name = "<null>"; - for (xmlNode *child = pcmk__xml_first_child(parent); child != NULL; - child = pcmk__xml_next(child)) { - if (pcmk__str_eq(node_name, (const char *) (child->name), - pcmk__str_null_matches) - && ((attr_n == NULL) || - (attr_v == NULL && xmlHasProp(child, (pcmkXmlStr) attr_n)) || - (attr_v != NULL && attr_matches(child, attr_n, attr_v)))) { - return child; + CRM_CHECK((attr_v == NULL) || (attr_n != NULL), return NULL); + + if (parent != NULL) { + child = parent->children; + while ((child != NULL) && (child->type != XML_ELEMENT_NODE)) { + child = child->next; } + + parent_name = (const char *) parent->name; } - crm_trace("XML child node <%s%s%s%s%s> not found in %s", - (node_name? node_name : "(any)"), - (attr_n? " " : ""), - (attr_n? attr_n : ""), - (attr_n? "=" : ""), - (attr_n? attr_v : ""), - (const char *) parent->name); - return NULL; -} -void -copy_in_properties(xmlNode *target, const xmlNode *src) -{ - if (src == NULL) { - crm_warn("No node to copy properties from"); + for (; child != NULL; child = pcmk__xe_next(child)) { + const char *value = NULL; - } else if (target == NULL) { - crm_err("No node to copy properties into"); + if ((node_name != NULL) && !pcmk__xe_is(child, node_name)) { + // Node name mismatch + continue; + } + if (attr_n == NULL) { + // No attribute match needed + return child; + } - } else { - for (xmlAttrPtr a = pcmk__xe_first_attr(src); a != NULL; a = a->next) { - const char *p_name = (const char *) a->name; - const char *p_value = pcmk__xml_attr_value(a); + value = crm_element_value(child, attr_n); - expand_plus_plus(target, p_name, p_value); - if (xml_acl_denied(target)) { - crm_trace("Cannot copy %s=%s to %s", p_name, p_value, target->name); - return; - } + if ((attr_v == NULL) && (value != NULL)) { + // attr_v == NULL: Attribute attr_n must be set (to any value) + return child; + } + if ((attr_v != NULL) && (pcmk__str_eq(value, attr_v, pcmk__str_none))) { + // attr_v != NULL: Attribute attr_n must be set to value attr_v + return child; } } - return; + if (node_name == NULL) { + node_name = "(any)"; // For logging + } + if (attr_n != NULL) { + crm_trace("XML child node <%s %s=%s> not found in %s", + node_name, attr_n, attr_v, parent_name); + } else { + crm_trace("XML child node <%s> not found in %s", + node_name, parent_name); + } + return NULL; } /*! - * \brief Parse integer assignment statements on this node and all its child - * nodes + * \internal + * \brief Set an XML attribute, expanding \c ++ and \c += where appropriate + * + * If \p target already has an attribute named \p name set to an integer value + * and \p value is an addition assignment expression on \p name, then expand + * \p value to an integer and set attribute \p name to the expanded value in + * \p target. + * + * Otherwise, set attribute \p name on \p target using the literal \p value. + * + * The original attribute value in \p target and the number in an assignment + * expression in \p value are parsed and added as scores (that is, their values + * are capped at \c INFINITY and \c -INFINITY). For more details, refer to + * \c char2score(). * - * \param[in,out] target Root XML node to be processed + * For example, suppose \p target has an attribute named \c "X" with value + * \c "5", and that \p name is \c "X". + * * If \p value is \c "X++", the new value of \c "X" in \p target is \c "6". + * * If \p value is \c "X+=3", the new value of \c "X" in \p target is \c "8". + * * If \p value is \c "val", the new value of \c "X" in \p target is \c "val". + * * If \p value is \c "Y++", the new value of \c "X" in \p target is \c "Y++". * - * \note This function is recursive + * \param[in,out] target XML node whose attribute to set + * \param[in] name Name of the attribute to set + * \param[in] value New value of attribute to set + * + * \return Standard Pacemaker return code (specifically, \c EINVAL on invalid + * argument, or \c pcmk_rc_ok otherwise) */ -void -fix_plus_plus_recursive(xmlNode *target) +int +pcmk__xe_set_score(xmlNode *target, const char *name, const char *value) { - /* TODO: Remove recursion and use xpath searches for value++ */ - xmlNode *child = NULL; + const char *old_value = NULL; - for (xmlAttrPtr a = pcmk__xe_first_attr(target); a != NULL; a = a->next) { - const char *p_name = (const char *) a->name; - const char *p_value = pcmk__xml_attr_value(a); + CRM_CHECK((target != NULL) && (name != NULL), return EINVAL); - expand_plus_plus(target, p_name, p_value); + if (value == NULL) { + return pcmk_rc_ok; } - for (child = pcmk__xml_first_child(target); child != NULL; - child = pcmk__xml_next(child)) { - fix_plus_plus_recursive(child); + + old_value = crm_element_value(target, name); + + // If no previous value, skip to default case and set the value unexpanded. + if (old_value != NULL) { + const char *n = name; + const char *v = value; + + // Stop at first character that differs between name and value + for (; (*n == *v) && (*n != '\0'); n++, v++); + + // If value begins with name followed by a "++" or "+=" + if ((*n == '\0') + && (*v++ == '+') + && ((*v == '+') || (*v == '='))) { + + // If we're expanding ourselves, no previous value was set; use 0 + int old_value_i = (old_value != value)? char2score(old_value) : 0; + + /* value="X++": new value of X is old_value + 1 + * value="X+=Y": new value of X is old_value + Y (for some number Y) + */ + int add = (*v == '+')? 1 : char2score(++v); + + crm_xml_add_int(target, name, pcmk__add_scores(old_value_i, add)); + return pcmk_rc_ok; + } + } + + // Default case: set the attribute unexpanded (with value treated literally) + if (old_value != value) { + crm_xml_add(target, name, value); } + return pcmk_rc_ok; } /*! - * \brief Update current XML attribute value per parsed integer assignment - statement - * - * \param[in,out] target an XML node, containing a XML attribute that is - * initialized to some numeric value, to be processed - * \param[in] name name of the XML attribute, e.g. X, whose value - * should be updated - * \param[in] value assignment statement, e.g. "X++" or - * "X+=5", to be applied to the initialized value. - * - * \note The original XML attribute value is treated as 0 if non-numeric and - * truncated to be an integer if decimal-point-containing. - * \note The final XML attribute value is truncated to not exceed 1000000. - * \note Undefined behavior if unexpected input. + * \internal + * \brief Copy XML attributes from a source element to a target element + * + * This is similar to \c xmlCopyPropList() except that attributes are marked + * as dirty for change tracking purposes. + * + * \param[in,out] target XML element to receive copied attributes from \p src + * \param[in] src XML element whose attributes to copy to \p target + * \param[in] flags Group of <tt>enum pcmk__xa_flags</tt> + * + * \return Standard Pacemaker return code */ -void -expand_plus_plus(xmlNode * target, const char *name, const char *value) +int +pcmk__xe_copy_attrs(xmlNode *target, const xmlNode *src, uint32_t flags) { - int offset = 1; - int name_len = 0; - int int_value = 0; - int value_len = 0; + CRM_CHECK((src != NULL) && (target != NULL), return EINVAL); - const char *old_value = NULL; + for (xmlAttr *attr = pcmk__xe_first_attr(src); attr != NULL; + attr = attr->next) { - if (target == NULL || value == NULL || name == NULL) { - return; - } + const char *name = (const char *) attr->name; + const char *value = pcmk__xml_attr_value(attr); - old_value = crm_element_value(target, name); - - if (old_value == NULL) { - /* if no previous value, set unexpanded */ - goto set_unexpanded; + if (pcmk_is_set(flags, pcmk__xaf_no_overwrite) + && (crm_element_value(target, name) != NULL)) { + continue; + } - } else if (strstr(value, name) != value) { - goto set_unexpanded; + if (pcmk_is_set(flags, pcmk__xaf_score_update)) { + pcmk__xe_set_score(target, name, value); + } else { + crm_xml_add(target, name, value); + } } - name_len = strlen(name); - value_len = strlen(value); - if (value_len < (name_len + 2) - || value[name_len] != '+' || (value[name_len + 1] != '+' && value[name_len + 1] != '=')) { - goto set_unexpanded; - } + return pcmk_rc_ok; +} - /* if we are expanding ourselves, - * then no previous value was set and leave int_value as 0 - */ - if (old_value != value) { - int_value = char2score(old_value); +/*! + * \internal + * \brief Remove an XML attribute from an element + * + * \param[in,out] element XML element that owns \p attr + * \param[in,out] attr XML attribute to remove from \p element + * + * \return Standard Pacemaker return code (\c EPERM if ACLs prevent removal of + * attributes from \p element, or \c pcmk_rc_ok otherwise) + */ +static int +remove_xe_attr(xmlNode *element, xmlAttr *attr) +{ + if (attr == NULL) { + return pcmk_rc_ok; } - if (value[name_len + 1] != '+') { - const char *offset_s = value + (name_len + 2); + if (!pcmk__check_acl(element, NULL, pcmk__xf_acl_write)) { + // ACLs apply to element, not to particular attributes + crm_trace("ACLs prevent removal of attributes from %s element", + (const char *) element->name); + return EPERM; + } - offset = char2score(offset_s); + if (pcmk__tracking_xml_changes(element, false)) { + // Leave in place (marked for removal) until after diff is calculated + set_parent_flag(element, pcmk__xf_dirty); + pcmk__set_xml_flags((xml_node_private_t *) attr->_private, + pcmk__xf_deleted); + } else { + xmlRemoveProp(attr); } - int_value += offset; + return pcmk_rc_ok; +} - if (int_value > INFINITY) { - int_value = (int)INFINITY; +/*! + * \internal + * \brief Remove a named attribute from an XML element + * + * \param[in,out] element XML element to remove an attribute from + * \param[in] name Name of attribute to remove + */ +void +pcmk__xe_remove_attr(xmlNode *element, const char *name) +{ + if (name != NULL) { + remove_xe_attr(element, xmlHasProp(element, (pcmkXmlStr) name)); } +} - crm_xml_add_int(target, name, int_value); - return; +/*! + * \internal + * \brief Remove a named attribute from an XML element + * + * This is a wrapper for \c pcmk__xe_remove_attr() for use with + * \c pcmk__xml_tree_foreach(). + * + * \param[in,out] xml XML element to remove an attribute from + * \param[in] user_data Name of attribute to remove + * + * \return \c true (to continue traversing the tree) + * + * \note This is compatible with \c pcmk__xml_tree_foreach(). + */ +bool +pcmk__xe_remove_attr_cb(xmlNode *xml, void *user_data) +{ + const char *name = user_data; - set_unexpanded: - if (old_value == value) { - /* the old value is already set, nothing to do */ - return; - } - crm_xml_add(target, name, value); - return; + pcmk__xe_remove_attr(xml, name); + return true; } /*! @@ -599,102 +698,92 @@ pcmk__xe_remove_matching_attrs(xmlNode *element, for (xmlAttrPtr a = pcmk__xe_first_attr(element); a != NULL; a = next) { next = a->next; // Grab now because attribute might get removed if ((match == NULL) || match(a, user_data)) { - if (!pcmk__check_acl(element, NULL, pcmk__xf_acl_write)) { - crm_trace("ACLs prevent removal of attributes (%s and " - "possibly others) from %s element", - (const char *) a->name, (const char *) element->name); - return; // ACLs apply to element, not particular attributes - } - - if (pcmk__tracking_xml_changes(element, false)) { - // Leave (marked for removal) until after diff is calculated - set_parent_flag(element, pcmk__xf_dirty); - pcmk__set_xml_flags((xml_node_private_t *) a->_private, - pcmk__xf_deleted); - } else { - xmlRemoveProp(a); + if (remove_xe_attr(element, a) != pcmk_rc_ok) { + return; } } } } +/*! + * \internal + * \brief Create a new XML element under a given parent + * + * \param[in,out] parent XML element that will be the new element's parent + * (\c NULL to create a new XML document with the new + * node as root) + * \param[in] name Name of new element + * + * \return Newly created XML element (guaranteed not to be \c NULL) + */ xmlNode * -add_node_copy(xmlNode * parent, xmlNode * src_node) -{ - xmlNode *child = NULL; - - CRM_CHECK((parent != NULL) && (src_node != NULL), return NULL); - - child = xmlDocCopyNode(src_node, parent->doc, 1); - if (child == NULL) { - return NULL; - } - xmlAddChild(parent, child); - pcmk__mark_xml_created(child); - return child; -} - -xmlNode * -create_xml_node(xmlNode * parent, const char *name) +pcmk__xe_create(xmlNode *parent, const char *name) { - xmlDoc *doc = NULL; xmlNode *node = NULL; - if (pcmk__str_empty(name)) { - CRM_CHECK(name != NULL && name[0] == 0, return NULL); - return NULL; - } + CRM_ASSERT(!pcmk__str_empty(name)); if (parent == NULL) { - doc = xmlNewDoc((pcmkXmlStr) "1.0"); - if (doc == NULL) { - return NULL; - } + xmlDoc *doc = xmlNewDoc(PCMK__XML_VERSION); + + pcmk__mem_assert(doc); node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL); - if (node == NULL) { - xmlFreeDoc(doc); - return NULL; - } + pcmk__mem_assert(node); + xmlDocSetRootElement(doc, node); } else { node = xmlNewChild(parent, NULL, (pcmkXmlStr) name, NULL); - if (node == NULL) { - return NULL; - } + pcmk__mem_assert(node); } - pcmk__mark_xml_created(node); + + pcmk__xml_mark_created(node); return node; } -xmlNode * -pcmk_create_xml_text_node(xmlNode * parent, const char *name, const char *content) +/*! + * \internal + * \brief Set a formatted string as an XML node's content + * + * \param[in,out] node Node whose content to set + * \param[in] format <tt>printf(3)</tt>-style format string + * \param[in] ... Arguments for \p format + * + * \note This function escapes special characters. \c xmlNodeSetContent() does + * not. + */ +G_GNUC_PRINTF(2, 3) +void +pcmk__xe_set_content(xmlNode *node, const char *format, ...) { - xmlNode *node = create_xml_node(parent, name); - if (node != NULL) { - xmlNodeSetContent(node, (pcmkXmlStr) content); - } + const char *content = NULL; + char *buf = NULL; - return node; -} + if (strchr(format, '%') == NULL) { + // Nothing to format + content = format; -xmlNode * -pcmk_create_html_node(xmlNode * parent, const char *element_name, const char *id, - const char *class_name, const char *text) -{ - xmlNode *node = pcmk_create_xml_text_node(parent, element_name, text); + } else { + va_list ap; - if (class_name != NULL) { - crm_xml_add(node, "class", class_name); - } + va_start(ap, format); - if (id != NULL) { - crm_xml_add(node, "id", id); - } + if (pcmk__str_eq(format, "%s", pcmk__str_none)) { + // No need to make a copy + content = va_arg(ap, const char *); - return node; + } else { + CRM_ASSERT(vasprintf(&buf, format, ap) >= 0); + content = buf; + } + va_end(ap); + } + + xmlNodeSetContent(node, (pcmkXmlStr) content); + free(buf); + } } /*! @@ -710,72 +799,67 @@ pcmk_free_xml_subtree(xmlNode *xml) } static void -free_xml_with_position(xmlNode * child, int position) +free_xml_with_position(xmlNode *child, int position) { - if (child != NULL) { - xmlNode *top = NULL; - xmlDoc *doc = child->doc; - xml_node_private_t *nodepriv = child->_private; - xml_doc_private_t *docpriv = NULL; - - if (doc != NULL) { - top = xmlDocGetRootElement(doc); - } + xmlDoc *doc = NULL; + xml_node_private_t *nodepriv = NULL; - if (doc != NULL && top == child) { - /* Free everything */ - xmlFreeDoc(doc); + if (child == NULL) { + return; + } + doc = child->doc; + nodepriv = child->_private; - } else if (pcmk__check_acl(child, NULL, pcmk__xf_acl_write) == FALSE) { - GString *xpath = NULL; + if ((doc != NULL) && (xmlDocGetRootElement(doc) == child)) { + // Free everything + xmlFreeDoc(doc); + return; + } - pcmk__if_tracing({}, return); - xpath = pcmk__element_xpath(child); - qb_log_from_external_source(__func__, __FILE__, - "Cannot remove %s %x", LOG_TRACE, - __LINE__, 0, (const char *) xpath->str, - nodepriv->flags); - g_string_free(xpath, TRUE); - return; + if (!pcmk__check_acl(child, NULL, pcmk__xf_acl_write)) { + GString *xpath = NULL; - } else { - if (doc && pcmk__tracking_xml_changes(child, FALSE) - && !pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { - - GString *xpath = pcmk__element_xpath(child); + pcmk__if_tracing({}, return); + xpath = pcmk__element_xpath(child); + qb_log_from_external_source(__func__, __FILE__, + "Cannot remove %s %x", LOG_TRACE, + __LINE__, 0, xpath->str, nodepriv->flags); + g_string_free(xpath, TRUE); + return; + } - if (xpath != NULL) { - pcmk__deleted_xml_t *deleted_obj = NULL; + if ((doc != NULL) && pcmk__tracking_xml_changes(child, false) + && !pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { - crm_trace("Deleting %s %p from %p", - (const char *) xpath->str, child, doc); + xml_doc_private_t *docpriv = doc->_private; + GString *xpath = pcmk__element_xpath(child); - deleted_obj = calloc(1, sizeof(pcmk__deleted_xml_t)); - deleted_obj->path = strdup((const char *) xpath->str); + if (xpath != NULL) { + pcmk__deleted_xml_t *deleted_obj = NULL; - CRM_ASSERT(deleted_obj->path != NULL); - g_string_free(xpath, TRUE); + crm_trace("Deleting %s %p from %p", xpath->str, child, doc); - deleted_obj->position = -1; - /* Record the "position" only for XML comments for now */ - if (child->type == XML_COMMENT_NODE) { - if (position >= 0) { - deleted_obj->position = position; + deleted_obj = pcmk__assert_alloc(1, sizeof(pcmk__deleted_xml_t)); + deleted_obj->path = g_string_free(xpath, FALSE); + deleted_obj->position = -1; - } else { - deleted_obj->position = pcmk__xml_position(child, - pcmk__xf_skip); - } - } + // Record the position only for XML comments for now + if (child->type == XML_COMMENT_NODE) { + if (position >= 0) { + deleted_obj->position = position; - docpriv = doc->_private; - docpriv->deleted_objs = g_list_append(docpriv->deleted_objs, deleted_obj); - pcmk__set_xml_doc_flag(child, pcmk__xf_dirty); + } else { + deleted_obj->position = pcmk__xml_position(child, + pcmk__xf_skip); } } - pcmk_free_xml_subtree(child); + + docpriv->deleted_objs = g_list_append(docpriv->deleted_objs, + deleted_obj); + pcmk__set_xml_doc_flag(child, pcmk__xf_dirty); } } + pcmk_free_xml_subtree(child); } @@ -785,171 +869,48 @@ free_xml(xmlNode * child) free_xml_with_position(child, -1); } +/*! + * \internal + * \brief Make a deep copy of an XML node under a given parent + * + * \param[in,out] parent XML element that will be the copy's parent (\c NULL + * to create a new XML document with the copy as root) + * \param[in] src XML node to copy + * + * \return Deep copy of \p src, or \c NULL if \p src is \c NULL + */ xmlNode * -copy_xml(xmlNode * src) -{ - xmlDoc *doc = xmlNewDoc((pcmkXmlStr) "1.0"); - xmlNode *copy = xmlDocCopyNode(src, doc, 1); - - CRM_ASSERT(copy != NULL); - xmlDocSetRootElement(doc, copy); - return copy; -} - -xmlNode * -string2xml(const char *input) -{ - xmlNode *xml = NULL; - xmlDocPtr output = NULL; - xmlParserCtxtPtr ctxt = NULL; - const xmlError *last_error = NULL; - - if (input == NULL) { - crm_err("Can't parse NULL input"); - return NULL; - } - - /* create a parser context */ - ctxt = xmlNewParserCtxt(); - CRM_CHECK(ctxt != NULL, return NULL); - - xmlCtxtResetLastError(ctxt); - xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err); - output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL, - PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER); - - if (output == NULL) { - output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL, - PCMK__XML_PARSE_OPTS_WITH_RECOVER); - if (output) { - crm_warn("Successfully recovered from XML errors " - "(note: a future release will treat this as a fatal failure)"); - } - } - - if (output) { - xml = xmlDocGetRootElement(output); - } - last_error = xmlCtxtGetLastError(ctxt); - if (last_error && last_error->code != XML_ERR_OK) { - /* crm_abort(__FILE__,__func__,__LINE__, "last_error->code != XML_ERR_OK", TRUE, TRUE); */ - /* - * http://xmlsoft.org/html/libxml-xmlerror.html#xmlErrorLevel - * http://xmlsoft.org/html/libxml-xmlerror.html#xmlParserErrors - */ - crm_warn("Parsing failed (domain=%d, level=%d, code=%d): %s", - last_error->domain, last_error->level, last_error->code, last_error->message); - - if (last_error->code == XML_ERR_DOCUMENT_EMPTY) { - CRM_LOG_ASSERT("Cannot parse an empty string"); - - } else if (last_error->code != XML_ERR_DOCUMENT_END) { - crm_err("Couldn't%s parse %d chars: %s", xml ? " fully" : "", (int)strlen(input), - input); - if (xml != NULL) { - crm_log_xml_err(xml, "Partial"); - } - - } else { - int len = strlen(input); - int lpc = 0; - - while(lpc < len) { - crm_warn("Parse error[+%.3d]: %.80s", lpc, input+lpc); - lpc += 80; - } - - CRM_LOG_ASSERT("String parsing error"); - } - } - - xmlFreeParserCtxt(ctxt); - return xml; -} - -xmlNode * -stdin2xml(void) -{ - size_t data_length = 0; - size_t read_chars = 0; - - char *xml_buffer = NULL; - xmlNode *xml_obj = NULL; - - do { - xml_buffer = pcmk__realloc(xml_buffer, data_length + PCMK__BUFFER_SIZE); - read_chars = fread(xml_buffer + data_length, 1, PCMK__BUFFER_SIZE, - stdin); - data_length += read_chars; - } while (read_chars == PCMK__BUFFER_SIZE); - - if (data_length == 0) { - crm_warn("No XML supplied on stdin"); - free(xml_buffer); - return NULL; - } - - xml_buffer[data_length] = '\0'; - xml_obj = string2xml(xml_buffer); - free(xml_buffer); - - crm_log_xml_trace(xml_obj, "Created fragment"); - return xml_obj; -} - -static char * -decompress_file(const char *filename) +pcmk__xml_copy(xmlNode *parent, xmlNode *src) { - char *buffer = NULL; - int rc = 0; - size_t length = 0, read_len = 0; - BZFILE *bz_file = NULL; - FILE *input = fopen(filename, "r"); + xmlNode *copy = NULL; - if (input == NULL) { - crm_perror(LOG_ERR, "Could not open %s for reading", filename); + if (src == NULL) { return NULL; } - bz_file = BZ2_bzReadOpen(&rc, input, 0, 0, NULL, 0); - rc = pcmk__bzlib2rc(rc); - - if (rc != pcmk_rc_ok) { - crm_err("Could not prepare to read compressed %s: %s " - CRM_XS " rc=%d", filename, pcmk_rc_str(rc), rc); - BZ2_bzReadClose(&rc, bz_file); - fclose(input); - return NULL; - } + if (parent == NULL) { + xmlDoc *doc = NULL; - rc = BZ_OK; - // cppcheck seems not to understand the abort-logic in pcmk__realloc - // cppcheck-suppress memleak - while (rc == BZ_OK) { - buffer = pcmk__realloc(buffer, PCMK__BUFFER_SIZE + length + 1); - read_len = BZ2_bzRead(&rc, bz_file, buffer + length, PCMK__BUFFER_SIZE); + // The copy will be the root element of a new document + CRM_ASSERT(src->type == XML_ELEMENT_NODE); - crm_trace("Read %ld bytes from file: %d", (long)read_len, rc); + doc = xmlNewDoc(PCMK__XML_VERSION); + pcmk__mem_assert(doc); - if (rc == BZ_OK || rc == BZ_STREAM_END) { - length += read_len; - } - } + copy = xmlDocCopyNode(src, doc, 1); + pcmk__mem_assert(copy); - buffer[length] = '\0'; + xmlDocSetRootElement(doc, copy); - rc = pcmk__bzlib2rc(rc); + } else { + copy = xmlDocCopyNode(src, parent->doc, 1); + pcmk__mem_assert(copy); - if (rc != pcmk_rc_ok) { - crm_err("Could not read compressed %s: %s " CRM_XS " rc=%d", - filename, pcmk_rc_str(rc), rc); - free(buffer); - buffer = NULL; + xmlAddChild(parent, copy); } - BZ2_bzReadClose(&rc, bz_file); - fclose(input); - return buffer; + pcmk__xml_mark_created(copy); + return copy; } /*! @@ -986,97 +947,6 @@ pcmk__strip_xml_text(xmlNode *xml) } } -xmlNode * -filename2xml(const char *filename) -{ - xmlNode *xml = NULL; - xmlDocPtr output = NULL; - bool uncompressed = true; - xmlParserCtxtPtr ctxt = NULL; - const xmlError *last_error = NULL; - - /* create a parser context */ - ctxt = xmlNewParserCtxt(); - CRM_CHECK(ctxt != NULL, return NULL); - - xmlCtxtResetLastError(ctxt); - xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err); - - if (filename) { - uncompressed = !pcmk__ends_with_ext(filename, ".bz2"); - } - - if (pcmk__str_eq(filename, "-", pcmk__str_null_matches)) { - /* STDIN_FILENO == fileno(stdin) */ - output = xmlCtxtReadFd(ctxt, STDIN_FILENO, "unknown.xml", NULL, - PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER); - - if (output == NULL) { - output = xmlCtxtReadFd(ctxt, STDIN_FILENO, "unknown.xml", NULL, - PCMK__XML_PARSE_OPTS_WITH_RECOVER); - if (output) { - crm_warn("Successfully recovered from XML errors " - "(note: a future release will treat this as a fatal failure)"); - } - } - - } else if (uncompressed) { - output = xmlCtxtReadFile(ctxt, filename, NULL, - PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER); - - if (output == NULL) { - output = xmlCtxtReadFile(ctxt, filename, NULL, - PCMK__XML_PARSE_OPTS_WITH_RECOVER); - if (output) { - crm_warn("Successfully recovered from XML errors " - "(note: a future release will treat this as a fatal failure)"); - } - } - - } else { - char *input = decompress_file(filename); - - output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL, - PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER); - - if (output == NULL) { - output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL, - PCMK__XML_PARSE_OPTS_WITH_RECOVER); - if (output) { - crm_warn("Successfully recovered from XML errors " - "(note: a future release will treat this as a fatal failure)"); - } - } - - free(input); - } - - if (output && (xml = xmlDocGetRootElement(output))) { - pcmk__strip_xml_text(xml); - } - - last_error = xmlCtxtGetLastError(ctxt); - if (last_error && last_error->code != XML_ERR_OK) { - /* crm_abort(__FILE__,__func__,__LINE__, "last_error->code != XML_ERR_OK", TRUE, TRUE); */ - /* - * http://xmlsoft.org/html/libxml-xmlerror.html#xmlErrorLevel - * http://xmlsoft.org/html/libxml-xmlerror.html#xmlParserErrors - */ - crm_err("Parsing failed (domain=%d, level=%d, code=%d): %s", - last_error->domain, last_error->level, last_error->code, last_error->message); - - if (last_error && last_error->code != XML_ERR_OK) { - crm_err("Couldn't%s parse %s", xml ? " fully" : "", filename); - if (xml != NULL) { - crm_log_xml_err(xml, "Partial"); - } - } - } - - xmlFreeParserCtxt(ctxt); - return xml; -} - /*! * \internal * \brief Add a "last written" attribute to an XML element, set to current time @@ -1091,7 +961,7 @@ pcmk__xe_add_last_written(xmlNode *xe) char *now_s = pcmk__epoch2str(NULL, 0); const char *result = NULL; - result = crm_xml_add(xe, XML_CIB_ATTR_WRITTEN, + result = crm_xml_add(xe, PCMK_XA_CIB_LAST_WRITTEN, pcmk__s(now_s, "Could not determine current time")); free(now_s); return result; @@ -1138,598 +1008,202 @@ crm_xml_set_id(xmlNode *xml, const char *format, ...) CRM_ASSERT(len > 0); crm_xml_sanitize_id(id); - crm_xml_add(xml, XML_ATTR_ID, id); + crm_xml_add(xml, PCMK_XA_ID, id); free(id); } /*! * \internal - * \brief Write XML to a file stream + * \brief Check whether a string has XML special characters that must be escaped * - * \param[in] xml XML to write - * \param[in] filename Name of file being written (for logging only) - * \param[in,out] stream Open file stream corresponding to filename - * \param[in] compress Whether to compress XML before writing - * \param[out] nbytes Number of bytes written + * See \c pcmk__xml_escape() and \c pcmk__xml_escape_type for more details. * - * \return Standard Pacemaker return code - */ -static int -write_xml_stream(const xmlNode *xml, const char *filename, FILE *stream, - bool compress, unsigned int *nbytes) -{ - int rc = pcmk_rc_ok; - char *buffer = NULL; - - *nbytes = 0; - crm_log_xml_trace(xml, "writing"); - - buffer = dump_xml_formatted(xml); - CRM_CHECK(buffer && strlen(buffer), - crm_log_xml_warn(xml, "formatting failed"); - rc = pcmk_rc_error; - goto bail); - - if (compress) { - unsigned int in = 0; - BZFILE *bz_file = NULL; - - rc = BZ_OK; - bz_file = BZ2_bzWriteOpen(&rc, stream, 5, 0, 30); - rc = pcmk__bzlib2rc(rc); - - if (rc != pcmk_rc_ok) { - crm_warn("Not compressing %s: could not prepare file stream: %s " - CRM_XS " rc=%d", filename, pcmk_rc_str(rc), rc); - } else { - BZ2_bzWrite(&rc, bz_file, buffer, strlen(buffer)); - rc = pcmk__bzlib2rc(rc); - - if (rc != pcmk_rc_ok) { - crm_warn("Not compressing %s: could not compress data: %s " - CRM_XS " rc=%d errno=%d", - filename, pcmk_rc_str(rc), rc, errno); - } - } - - if (rc == pcmk_rc_ok) { - BZ2_bzWriteClose(&rc, bz_file, 0, &in, nbytes); - rc = pcmk__bzlib2rc(rc); - - if (rc != pcmk_rc_ok) { - crm_warn("Not compressing %s: could not write compressed data: %s " - CRM_XS " rc=%d errno=%d", - filename, pcmk_rc_str(rc), rc, errno); - *nbytes = 0; // retry without compression - } else { - crm_trace("Compressed XML for %s from %u bytes to %u", - filename, in, *nbytes); - } - } - rc = pcmk_rc_ok; // Either true, or we'll retry without compression - } - - if (*nbytes == 0) { - rc = fprintf(stream, "%s", buffer); - if (rc < 0) { - rc = errno; - crm_perror(LOG_ERR, "writing %s", filename); - } else { - *nbytes = (unsigned int) rc; - rc = pcmk_rc_ok; - } - } - - bail: - - if (fflush(stream) != 0) { - rc = errno; - crm_perror(LOG_ERR, "flushing %s", filename); - } - - /* Don't report error if the file does not support synchronization */ - if (fsync(fileno(stream)) < 0 && errno != EROFS && errno != EINVAL) { - rc = errno; - crm_perror(LOG_ERR, "synchronizing %s", filename); - } - - fclose(stream); - - crm_trace("Saved %d bytes to %s as XML", *nbytes, filename); - free(buffer); - - return rc; -} - -/*! - * \brief Write XML to a file descriptor - * - * \param[in] xml XML to write - * \param[in] filename Name of file being written (for logging only) - * \param[in] fd Open file descriptor corresponding to filename - * \param[in] compress Whether to compress XML before writing + * \param[in] text String to check + * \param[in] type Type of escaping * - * \return Number of bytes written on success, -errno otherwise + * \return \c true if \p text has special characters that need to be escaped, or + * \c false otherwise */ -int -write_xml_fd(const xmlNode *xml, const char *filename, int fd, - gboolean compress) -{ - FILE *stream = NULL; - unsigned int nbytes = 0; - int rc = pcmk_rc_ok; - - CRM_CHECK((xml != NULL) && (fd > 0), return -EINVAL); - stream = fdopen(fd, "w"); - if (stream == NULL) { - return -errno; - } - rc = write_xml_stream(xml, filename, stream, compress, &nbytes); - if (rc != pcmk_rc_ok) { - return pcmk_rc2legacy(rc); - } - return (int) nbytes; -} - -/*! - * \brief Write XML to a file - * - * \param[in] xml XML to write - * \param[in] filename Name of file to write - * \param[in] compress Whether to compress XML before writing - * - * \return Number of bytes written on success, -errno otherwise - */ -int -write_xml_file(const xmlNode *xml, const char *filename, gboolean compress) +bool +pcmk__xml_needs_escape(const char *text, enum pcmk__xml_escape_type type) { - FILE *stream = NULL; - unsigned int nbytes = 0; - int rc = pcmk_rc_ok; + if (text == NULL) { + return false; + } + + while (*text != '\0') { + switch (type) { + case pcmk__xml_escape_text: + switch (*text) { + case '<': + case '>': + case '&': + return true; + case '\n': + case '\t': + break; + default: + if (g_ascii_iscntrl(*text)) { + return true; + } + break; + } + break; - CRM_CHECK((xml != NULL) && (filename != NULL), return -EINVAL); - stream = fopen(filename, "w"); - if (stream == NULL) { - return -errno; - } - rc = write_xml_stream(xml, filename, stream, compress, &nbytes); - if (rc != pcmk_rc_ok) { - return pcmk_rc2legacy(rc); - } - return (int) nbytes; -} + case pcmk__xml_escape_attr: + switch (*text) { + case '<': + case '>': + case '&': + case '"': + return true; + default: + if (g_ascii_iscntrl(*text)) { + return true; + } + break; + } + break; -// Replace a portion of a dynamically allocated string (reallocating memory) -static char * -replace_text(char *text, int start, size_t *length, const char *replace) -{ - size_t offset = strlen(replace) - 1; // We have space for 1 char already + case pcmk__xml_escape_attr_pretty: + switch (*text) { + case '\n': + case '\r': + case '\t': + case '"': + return true; + default: + break; + } + break; - *length += offset; - text = pcmk__realloc(text, *length); + default: // Invalid enum value + CRM_ASSERT(false); + break; + } - for (size_t lpc = (*length) - 1; lpc > (start + offset); lpc--) { - text[lpc] = text[lpc - offset]; + text = g_utf8_next_char(text); } - - memcpy(text + start, replace, offset + 1); - return text; + return false; } /*! + * \internal * \brief Replace special characters with their XML escape sequences * * \param[in] text Text to escape + * \param[in] type Type of escaping * * \return Newly allocated string equivalent to \p text but with special - * characters replaced with XML escape sequences (or NULL if \p text - * is NULL) + * characters replaced with XML escape sequences (or \c NULL if \p text + * is \c NULL). If \p text is not \c NULL, the return value is + * guaranteed not to be \c NULL. + * + * \note There are libxml functions that purport to do this: + * \c xmlEncodeEntitiesReentrant() and \c xmlEncodeSpecialChars(). + * However, their escaping is incomplete. See: + * https://discourse.gnome.org/t/intended-use-of-xmlencodeentitiesreentrant-vs-xmlencodespecialchars/19252 + * \note The caller is responsible for freeing the return value using + * \c g_free(). */ -char * -crm_xml_escape(const char *text) +gchar * +pcmk__xml_escape(const char *text, enum pcmk__xml_escape_type type) { - size_t length; - char *copy; - - /* - * When xmlCtxtReadDoc() parses < and friends in a - * value, it converts them to their human readable - * form. - * - * If one uses xmlNodeDump() to convert it back to a - * string, all is well, because special characters are - * converted back to their escape sequences. - * - * However xmlNodeDump() is randomly dog slow, even with the same - * input. So we need to replicate the escaping in our custom - * version so that the result can be re-parsed by xmlCtxtReadDoc() - * when necessary. - */ + GString *copy = NULL; if (text == NULL) { return NULL; } + copy = g_string_sized_new(strlen(text)); - length = 1 + strlen(text); - copy = strdup(text); - CRM_ASSERT(copy != NULL); - for (size_t index = 0; index < length; index++) { - if(copy[index] & 0x80 && copy[index+1] & 0x80){ - index++; - break; - } - switch (copy[index]) { - case 0: - break; - case '<': - copy = replace_text(copy, index, &length, "<"); - break; - case '>': - copy = replace_text(copy, index, &length, ">"); - break; - case '"': - copy = replace_text(copy, index, &length, """); - break; - case '\'': - copy = replace_text(copy, index, &length, "'"); - break; - case '&': - copy = replace_text(copy, index, &length, "&"); - break; - case '\t': - /* Might as well just expand to a few spaces... */ - copy = replace_text(copy, index, &length, " "); - break; - case '\n': - copy = replace_text(copy, index, &length, "\\n"); - break; - case '\r': - copy = replace_text(copy, index, &length, "\\r"); - break; - default: - /* Check for and replace non-printing characters with their octal equivalent */ - if(copy[index] < ' ' || copy[index] > '~') { - char *replace = crm_strdup_printf("\\%.3o", copy[index]); + while (*text != '\0') { + // Don't escape any non-ASCII characters + if ((*text & 0x80) != 0) { + size_t bytes = g_utf8_next_char(text) - text; - copy = replace_text(copy, index, &length, replace); - free(replace); - } - } - } - return copy; -} - -/*! - * \internal - * \brief Append a string representation of an XML element to a buffer - * - * \param[in] data XML whose representation to append - * \param[in] options Group of \p pcmk__xml_fmt_options flags - * \param[in,out] buffer Where to append the content (must not be \p NULL) - * \param[in] depth Current indentation level - */ -static void -dump_xml_element(const xmlNode *data, uint32_t options, GString *buffer, - int depth) -{ - bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); - bool filtered = pcmk_is_set(options, pcmk__xml_fmt_filtered); - int spaces = pretty? (2 * depth) : 0; - - for (int lpc = 0; lpc < spaces; lpc++) { - g_string_append_c(buffer, ' '); - } - - pcmk__g_strcat(buffer, "<", data->name, NULL); - - for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL; - attr = attr->next) { - - if (!filtered || !pcmk__xa_filterable((const char *) (attr->name))) { - pcmk__dump_xml_attr(attr, buffer); + g_string_append_len(copy, text, bytes); + text += bytes; + continue; } - } - - if (data->children == NULL) { - g_string_append(buffer, "/>"); - - } else { - g_string_append_c(buffer, '>'); - } - if (pretty) { - g_string_append_c(buffer, '\n'); - } - - if (data->children) { - for (const xmlNode *child = data->children; child != NULL; - child = child->next) { - pcmk__xml2text(child, options, buffer, depth + 1); - } + switch (type) { + case pcmk__xml_escape_text: + switch (*text) { + case '<': + g_string_append(copy, PCMK__XML_ENTITY_LT); + break; + case '>': + g_string_append(copy, PCMK__XML_ENTITY_GT); + break; + case '&': + g_string_append(copy, PCMK__XML_ENTITY_AMP); + break; + case '\n': + case '\t': + g_string_append_c(copy, *text); + break; + default: + if (g_ascii_iscntrl(*text)) { + g_string_append_printf(copy, "&#x%.2X;", *text); + } else { + g_string_append_c(copy, *text); + } + break; + } + break; - for (int lpc = 0; lpc < spaces; lpc++) { - g_string_append_c(buffer, ' '); - } + case pcmk__xml_escape_attr: + switch (*text) { + case '<': + g_string_append(copy, PCMK__XML_ENTITY_LT); + break; + case '>': + g_string_append(copy, PCMK__XML_ENTITY_GT); + break; + case '&': + g_string_append(copy, PCMK__XML_ENTITY_AMP); + break; + case '"': + g_string_append(copy, PCMK__XML_ENTITY_QUOT); + break; + default: + if (g_ascii_iscntrl(*text)) { + g_string_append_printf(copy, "&#x%.2X;", *text); + } else { + g_string_append_c(copy, *text); + } + break; + } + break; - pcmk__g_strcat(buffer, "</", data->name, ">", NULL); + case pcmk__xml_escape_attr_pretty: + switch (*text) { + case '"': + g_string_append(copy, "\\\""); + break; + case '\n': + g_string_append(copy, "\\n"); + break; + case '\r': + g_string_append(copy, "\\r"); + break; + case '\t': + g_string_append(copy, "\\t"); + break; + default: + g_string_append_c(copy, *text); + break; + } + break; - if (pretty) { - g_string_append_c(buffer, '\n'); + default: // Invalid enum value + CRM_ASSERT(false); + break; } - } -} - -/*! - * \internal - * \brief Append XML text content to a buffer - * - * \param[in] data XML whose content to append - * \param[in] options Group of \p xml_log_options flags - * \param[in,out] buffer Where to append the content (must not be \p NULL) - * \param[in] depth Current indentation level - */ -static void -dump_xml_text(const xmlNode *data, uint32_t options, GString *buffer, - int depth) -{ - /* @COMPAT: Remove when log_data_element() is removed. There are no internal - * code paths to this, except through the deprecated log_data_element(). - */ - bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); - int spaces = pretty? (2 * depth) : 0; - - for (int lpc = 0; lpc < spaces; lpc++) { - g_string_append_c(buffer, ' '); - } - - g_string_append(buffer, (const gchar *) data->content); - - if (pretty) { - g_string_append_c(buffer, '\n'); - } -} - -/*! - * \internal - * \brief Append XML CDATA content to a buffer - * - * \param[in] data XML whose content to append - * \param[in] options Group of \p pcmk__xml_fmt_options flags - * \param[in,out] buffer Where to append the content (must not be \p NULL) - * \param[in] depth Current indentation level - */ -static void -dump_xml_cdata(const xmlNode *data, uint32_t options, GString *buffer, - int depth) -{ - bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); - int spaces = pretty? (2 * depth) : 0; - - for (int lpc = 0; lpc < spaces; lpc++) { - g_string_append_c(buffer, ' '); - } - - pcmk__g_strcat(buffer, "<![CDATA[", (const char *) data->content, "]]>", - NULL); - if (pretty) { - g_string_append_c(buffer, '\n'); + text = g_utf8_next_char(text); } -} - -/*! - * \internal - * \brief Append an XML comment to a buffer - * - * \param[in] data XML whose content to append - * \param[in] options Group of \p pcmk__xml_fmt_options flags - * \param[in,out] buffer Where to append the content (must not be \p NULL) - * \param[in] depth Current indentation level - */ -static void -dump_xml_comment(const xmlNode *data, uint32_t options, GString *buffer, - int depth) -{ - bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); - int spaces = pretty? (2 * depth) : 0; - - for (int lpc = 0; lpc < spaces; lpc++) { - g_string_append_c(buffer, ' '); - } - - pcmk__g_strcat(buffer, "<!--", (const char *) data->content, "-->", NULL); - - if (pretty) { - g_string_append_c(buffer, '\n'); - } -} - -/*! - * \internal - * \brief Get a string representation of an XML element type - * - * \param[in] type XML element type - * - * \return String representation of \p type - */ -static const char * -xml_element_type2str(xmlElementType type) -{ - static const char *const element_type_names[] = { - [XML_ELEMENT_NODE] = "element", - [XML_ATTRIBUTE_NODE] = "attribute", - [XML_TEXT_NODE] = "text", - [XML_CDATA_SECTION_NODE] = "CDATA section", - [XML_ENTITY_REF_NODE] = "entity reference", - [XML_ENTITY_NODE] = "entity", - [XML_PI_NODE] = "PI", - [XML_COMMENT_NODE] = "comment", - [XML_DOCUMENT_NODE] = "document", - [XML_DOCUMENT_TYPE_NODE] = "document type", - [XML_DOCUMENT_FRAG_NODE] = "document fragment", - [XML_NOTATION_NODE] = "notation", - [XML_HTML_DOCUMENT_NODE] = "HTML document", - [XML_DTD_NODE] = "DTD", - [XML_ELEMENT_DECL] = "element declaration", - [XML_ATTRIBUTE_DECL] = "attribute declaration", - [XML_ENTITY_DECL] = "entity declaration", - [XML_NAMESPACE_DECL] = "namespace declaration", - [XML_XINCLUDE_START] = "XInclude start", - [XML_XINCLUDE_END] = "XInclude end", - }; - - if ((type < 0) || (type >= PCMK__NELEM(element_type_names))) { - return "unrecognized type"; - } - return element_type_names[type]; -} - -/*! - * \internal - * \brief Create a text representation of an XML object - * - * \param[in] data XML to convert - * \param[in] options Group of \p pcmk__xml_fmt_options flags - * \param[in,out] buffer Where to store the text (must not be \p NULL) - * \param[in] depth Current indentation level - */ -void -pcmk__xml2text(const xmlNode *data, uint32_t options, GString *buffer, - int depth) -{ - if (data == NULL) { - crm_trace("Nothing to dump"); - return; - } - - CRM_ASSERT(buffer != NULL); - CRM_CHECK(depth >= 0, depth = 0); - - switch(data->type) { - case XML_ELEMENT_NODE: - /* Handle below */ - dump_xml_element(data, options, buffer, depth); - break; - case XML_TEXT_NODE: - if (pcmk_is_set(options, pcmk__xml_fmt_text)) { - dump_xml_text(data, options, buffer, depth); - } - break; - case XML_COMMENT_NODE: - dump_xml_comment(data, options, buffer, depth); - break; - case XML_CDATA_SECTION_NODE: - dump_xml_cdata(data, options, buffer, depth); - break; - default: - crm_warn("Cannot convert XML %s node to text " CRM_XS " type=%d", - xml_element_type2str(data->type), data->type); - break; - } -} - -char * -dump_xml_formatted_with_text(const xmlNode *xml) -{ - /* libxml's xmlNodeDumpOutput() would work here since we're not specifically - * filtering out any nodes. However, use pcmk__xml2text() for consistency, - * to escape attribute values, and to allow a const argument. - */ - char *buffer = NULL; - GString *g_buffer = g_string_sized_new(1024); - - pcmk__xml2text(xml, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, g_buffer, 0); - - pcmk__str_update(&buffer, g_buffer->str); - g_string_free(g_buffer, TRUE); - return buffer; -} - -char * -dump_xml_formatted(const xmlNode *xml) -{ - char *buffer = NULL; - GString *g_buffer = g_string_sized_new(1024); - - pcmk__xml2text(xml, pcmk__xml_fmt_pretty, g_buffer, 0); - - pcmk__str_update(&buffer, g_buffer->str); - g_string_free(g_buffer, TRUE); - return buffer; -} - -char * -dump_xml_unformatted(const xmlNode *xml) -{ - char *buffer = NULL; - GString *g_buffer = g_string_sized_new(1024); - - pcmk__xml2text(xml, 0, g_buffer, 0); - - pcmk__str_update(&buffer, g_buffer->str); - g_string_free(g_buffer, TRUE); - return buffer; -} - -int -pcmk__xml2fd(int fd, xmlNode *cur) -{ - bool success; - - xmlOutputBuffer *fd_out = xmlOutputBufferCreateFd(fd, NULL); - CRM_ASSERT(fd_out != NULL); - xmlNodeDumpOutput(fd_out, cur->doc, cur, 0, pcmk__xml_fmt_pretty, NULL); - - success = xmlOutputBufferWrite(fd_out, sizeof("\n") - 1, "\n") != -1; - - success = xmlOutputBufferClose(fd_out) != -1 && success; - - if (!success) { - return EIO; - } - - fsync(fd); - return pcmk_rc_ok; -} - -void -xml_remove_prop(xmlNode * obj, const char *name) -{ - if (crm_element_value(obj, name) == NULL) { - return; - } - - if (pcmk__check_acl(obj, NULL, pcmk__xf_acl_write) == FALSE) { - crm_trace("Cannot remove %s from %s", name, obj->name); - - } else if (pcmk__tracking_xml_changes(obj, FALSE)) { - /* Leave in place (marked for removal) until after the diff is calculated */ - xmlAttr *attr = xmlHasProp(obj, (pcmkXmlStr) name); - xml_node_private_t *nodepriv = attr->_private; - - set_parent_flag(obj, pcmk__xf_dirty); - pcmk__set_xml_flags(nodepriv, pcmk__xf_deleted); - } else { - xmlUnsetProp(obj, (pcmkXmlStr) name); - } -} - -void -save_xml_to_file(const xmlNode *xml, const char *desc, const char *filename) -{ - char *f = NULL; - - if (filename == NULL) { - char *uuid = crm_generate_uuid(); - - f = crm_strdup_printf("%s/%s", pcmk__get_tmpdir(), uuid); - filename = f; - free(uuid); - } - - crm_info("Saving %s to %s", desc, filename); - write_xml_file(xml, filename, FALSE); - free(f); + return g_string_free(copy, FALSE); } /*! @@ -1781,7 +1255,7 @@ mark_attr_deleted(xmlNode *new_xml, const char *element, const char *attr_name, nodepriv->flags = 0; // Check ACLs and mark restored value for later removal - xml_remove_prop(new_xml, attr_name); + remove_xe_attr(new_xml, attr); crm_trace("XML attribute %s=%s was removed from %s", attr_name, old_value, element); @@ -1955,10 +1429,10 @@ static void mark_child_deleted(xmlNode *old_child, xmlNode *new_parent) { // Re-create the child element so we can check ACLs - xmlNode *candidate = add_node_copy(new_parent, old_child); + xmlNode *candidate = pcmk__xml_copy(new_parent, old_child); // Clear flags on new child and its children - reset_xml_node_flags(candidate); + pcmk__xml_tree_foreach(candidate, reset_xml_node_flags, NULL); // Check whether ACLs allow the deletion pcmk__apply_acl(xmlDocGetRootElement(candidate->doc)); @@ -1979,8 +1453,9 @@ mark_child_moved(xmlNode *old_child, xmlNode *new_parent, xmlNode *new_child, { xml_node_private_t *nodepriv = new_child->_private; - crm_trace("Child element %s with id='%s' moved from position %d to %d under %s", - new_child->name, (ID(new_child)? ID(new_child) : "<no id>"), + crm_trace("Child element %s with " + PCMK_XA_ID "='%s' moved from position %d to %d under %s", + new_child->name, pcmk__s(pcmk__xe_id(new_child), "<no id>"), p_old, p_new, new_parent->name); pcmk__mark_xml_node_dirty(new_parent); pcmk__set_xml_flags(nodepriv, pcmk__xf_moved); @@ -1997,12 +1472,13 @@ mark_child_moved(xmlNode *old_child, xmlNode *new_parent, xmlNode *new_child, static void mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top) { - xmlNode *cIter = NULL; + xmlNode *old_child = NULL; + xmlNode *new_child = NULL; xml_node_private_t *nodepriv = NULL; CRM_CHECK(new_xml != NULL, return); if (old_xml == NULL) { - pcmk__mark_xml_created(new_xml); + pcmk__xml_mark_created(new_xml); pcmk__apply_creation_acl(new_xml, check_top); return; } @@ -2019,13 +1495,13 @@ mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top) xml_diff_attrs(old_xml, new_xml); // Check for differences in the original children - for (cIter = pcmk__xml_first_child(old_xml); cIter != NULL; ) { - xmlNode *old_child = cIter; - xmlNode *new_child = pcmk__xml_match(new_xml, cIter, true); + for (old_child = pcmk__xml_first_child(old_xml); old_child != NULL; + old_child = pcmk__xml_next(old_child)) { + + new_child = pcmk__xml_match(new_xml, old_child, true); - cIter = pcmk__xml_next(cIter); - if(new_child) { - mark_xml_changes(old_child, new_child, TRUE); + if (new_child != NULL) { + mark_xml_changes(old_child, new_child, true); } else { mark_child_deleted(old_child, new_xml); @@ -2033,16 +1509,19 @@ mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top) } // Check for moved or created children - for (cIter = pcmk__xml_first_child(new_xml); cIter != NULL; ) { - xmlNode *new_child = cIter; - xmlNode *old_child = pcmk__xml_match(old_xml, cIter, true); + new_child = pcmk__xml_first_child(new_xml); + while (new_child != NULL) { + xmlNode *next = pcmk__xml_next(new_child); + + old_child = pcmk__xml_match(old_xml, new_child, true); - cIter = pcmk__xml_next(cIter); - if(old_child == NULL) { + if (old_child == NULL) { // This is a newly created child nodepriv = new_child->_private; pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); - mark_xml_changes(old_child, new_child, TRUE); + + // May free new_child + mark_xml_changes(old_child, new_child, true); } else { /* Check for movement, we already checked for differences */ @@ -2053,6 +1532,8 @@ mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top) mark_child_moved(old_child, new_xml, new_child, p_old, p_new); } } + + new_child = next; } } @@ -2069,7 +1550,8 @@ xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml) { CRM_CHECK((old_xml != NULL) && (new_xml != NULL) && pcmk__xe_is(old_xml, (const char *) new_xml->name) - && pcmk__str_eq(ID(old_xml), ID(new_xml), pcmk__str_none), + && pcmk__str_eq(pcmk__xe_id(old_xml), pcmk__xe_id(new_xml), + pcmk__str_none), return); if(xml_tracking_changes(new_xml) == FALSE) { @@ -2079,44 +1561,6 @@ xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml) mark_xml_changes(old_xml, new_xml, FALSE); } -gboolean -can_prune_leaf(xmlNode * xml_node) -{ - xmlNode *cIter = NULL; - gboolean can_prune = TRUE; - - CRM_CHECK(xml_node != NULL, return FALSE); - - if (pcmk__strcase_any_of((const char *) xml_node->name, - XML_TAG_RESOURCE_REF, XML_CIB_TAG_OBJ_REF, - XML_ACL_TAG_ROLE_REF, XML_ACL_TAG_ROLE_REFv1, - NULL)) { - return FALSE; - } - - for (xmlAttrPtr a = pcmk__xe_first_attr(xml_node); a != NULL; a = a->next) { - const char *p_name = (const char *) a->name; - - if (strcmp(p_name, XML_ATTR_ID) == 0) { - continue; - } - can_prune = FALSE; - } - - cIter = pcmk__xml_first_child(xml_node); - while (cIter) { - xmlNode *child = cIter; - - cIter = pcmk__xml_next(cIter); - if (can_prune_leaf(child)) { - free_xml(child); - } else { - can_prune = FALSE; - } - } - return can_prune; -} - /*! * \internal * \brief Find a comment with matching content in specified XML @@ -2185,7 +1629,7 @@ pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update) } if (target == NULL) { - add_node_copy(parent, update); + pcmk__xml_copy(parent, update); } else if (!pcmk__str_eq((const char *)target->content, (const char *)update->content, pcmk__str_casei)) { xmlFree(target->content); @@ -2195,82 +1639,113 @@ pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update) /*! * \internal - * \brief Make one XML tree match another (in children and attributes) + * \brief Merge one XML tree into another + * + * Here, "merge" means: + * 1. Copy attribute values from \p update to the target, overwriting in case of + * conflict. + * 2. Descend through \p update and the target in parallel. At each level, for + * each child of \p update, look for a matching child of the target. + * a. For each child, if a match is found, go to step 1, recursively merging + * the child of \p update into the child of the target. + * b. Otherwise, copy the child of \p update as a child of the target. + * + * A match is defined as the first child of the same type within the target, + * with: + * * the \c PCMK_XA_ID attribute matching, if set in \p update; otherwise, + * * the \c PCMK_XA_ID_REF attribute matching, if set in \p update + * + * This function does not delete any elements or attributes from the target. It + * may add elements or overwrite attributes, as described above. * * \param[in,out] parent If \p target is NULL and this is not, add or update * child of this XML node that matches \p update * \param[in,out] target If not NULL, update this XML - * \param[in] update Make the desired XML match this (must not be NULL) - * \param[in] as_diff If false, expand "++" when making attributes match + * \param[in] update Make the desired XML match this (must not be \c NULL) + * \param[in] flags Group of <tt>enum pcmk__xa_flags</tt> + * \param[in] as_diff If \c true, preserve order of attributes (deprecated + * since 2.0.5) * - * \note At least one of \p parent and \p target must be non-NULL + * \note At least one of \p parent and \p target must be non-<tt>NULL</tt>. + * \note This function is recursive. For the top-level call, \p parent is + * \c NULL and \p target is not \c NULL. For recursive calls, \p target is + * \c NULL and \p parent is not \c NULL. */ void pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update, - bool as_diff) + uint32_t flags, bool as_diff) { - xmlNode *a_child = NULL; - const char *object_name = NULL, - *object_href = NULL, - *object_href_val = NULL; + /* @COMPAT Refactor further and staticize after v1 patchset deprecation. + * + * @COMPAT Drop as_diff argument when apply_xml_diff() is dropped. + */ + const char *update_name = NULL; + const char *update_id_attr = NULL; + const char *update_id_val = NULL; + char *trace_s = NULL; -#if XML_PARSER_DEBUG - crm_log_xml_trace(update, "update:"); - crm_log_xml_trace(target, "target:"); -#endif + crm_log_xml_trace(update, "update"); + crm_log_xml_trace(target, "target"); - CRM_CHECK(update != NULL, return); + CRM_CHECK(update != NULL, goto done); if (update->type == XML_COMMENT_NODE) { pcmk__xc_update(parent, target, update); - return; + goto done; } - object_name = (const char *) update->name; - object_href_val = ID(update); - if (object_href_val != NULL) { - object_href = XML_ATTR_ID; + update_name = (const char *) update->name; + + CRM_CHECK(update_name != NULL, goto done); + CRM_CHECK((target != NULL) || (parent != NULL), goto done); + + update_id_val = pcmk__xe_id(update); + if (update_id_val != NULL) { + update_id_attr = PCMK_XA_ID; + } else { - object_href_val = crm_element_value(update, XML_ATTR_IDREF); - object_href = (object_href_val == NULL) ? NULL : XML_ATTR_IDREF; + update_id_val = crm_element_value(update, PCMK_XA_ID_REF); + if (update_id_val != NULL) { + update_id_attr = PCMK_XA_ID_REF; + } } - CRM_CHECK(object_name != NULL, return); - CRM_CHECK(target != NULL || parent != NULL, return); + pcmk__if_tracing( + { + if (update_id_attr != NULL) { + trace_s = crm_strdup_printf("<%s %s=%s/>", + update_name, update_id_attr, + update_id_val); + } else { + trace_s = crm_strdup_printf("<%s/>", update_name); + } + }, + {} + ); if (target == NULL) { - target = pcmk__xe_match(parent, object_name, - object_href, object_href_val); + // Recursive call + target = pcmk__xe_first_child(parent, update_name, update_id_attr, + update_id_val); } if (target == NULL) { - target = create_xml_node(parent, object_name); - CRM_CHECK(target != NULL, return); -#if XML_PARSER_DEBUG - crm_trace("Added <%s%s%s%s%s/>", pcmk__s(object_name, "<null>"), - object_href ? " " : "", - object_href ? object_href : "", - object_href ? "=" : "", - object_href ? object_href_val : ""); + // Recursive call with no existing matching child + target = pcmk__xe_create(parent, update_name); + crm_trace("Added %s", pcmk__s(trace_s, update_name)); } else { - crm_trace("Found node <%s%s%s%s%s/> to update", - pcmk__s(object_name, "<null>"), - object_href ? " " : "", - object_href ? object_href : "", - object_href ? "=" : "", - object_href ? object_href_val : ""); -#endif + // Either recursive call with match, or top-level call + crm_trace("Found node %s to update", pcmk__s(trace_s, update_name)); } CRM_CHECK(pcmk__xe_is(target, (const char *) update->name), return); - if (as_diff == FALSE) { - /* So that expand_plus_plus() gets called */ - copy_in_properties(target, update); + if (!as_diff) { + pcmk__xe_copy_attrs(target, update, flags); } else { - /* No need for expand_plus_plus(), just raw speed */ + // Preserve order of attributes. Don't use pcmk__xe_copy_attrs(). for (xmlAttrPtr a = pcmk__xe_first_attr(update); a != NULL; a = a->next) { const char *p_value = pcmk__xml_attr_value(a); @@ -2281,175 +1756,316 @@ pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update, } } - for (a_child = pcmk__xml_first_child(update); a_child != NULL; - a_child = pcmk__xml_next(a_child)) { -#if XML_PARSER_DEBUG - crm_trace("Updating child <%s%s%s%s%s/>", - pcmk__s(object_name, "<null>"), - object_href ? " " : "", - object_href ? object_href : "", - object_href ? "=" : "", - object_href ? object_href_val : ""); -#endif - pcmk__xml_update(target, NULL, a_child, as_diff); - } - -#if XML_PARSER_DEBUG - crm_trace("Finished with <%s%s%s%s%s/>", pcmk__s(object_name, "<null>"), - object_href ? " " : "", - object_href ? object_href : "", - object_href ? "=" : "", - object_href ? object_href_val : ""); -#endif -} + for (xmlNode *child = pcmk__xml_first_child(update); child != NULL; + child = pcmk__xml_next(child)) { -gboolean -update_xml_child(xmlNode * child, xmlNode * to_update) -{ - gboolean can_update = TRUE; - xmlNode *child_of_child = NULL; + crm_trace("Updating child of %s", pcmk__s(trace_s, update_name)); + pcmk__xml_update(target, NULL, child, flags, as_diff); + } - CRM_CHECK(child != NULL, return FALSE); - CRM_CHECK(to_update != NULL, return FALSE); + crm_trace("Finished with %s", pcmk__s(trace_s, update_name)); - if (!pcmk__xe_is(to_update, (const char *) child->name)) { - can_update = FALSE; +done: + free(trace_s); +} - } else if (!pcmk__str_eq(ID(to_update), ID(child), pcmk__str_none)) { - can_update = FALSE; +/*! + * \internal + * \brief Delete an XML subtree if it matches a search element + * + * A match is defined as follows: + * * \p xml and \p user_data are both element nodes of the same type. + * * If \p user_data has attributes set, \p xml has those attributes set to the + * same values. (\p xml may have additional attributes set to arbitrary + * values.) + * + * \param[in,out] xml XML subtree to delete upon match + * \param[in] user_data Search element + * + * \return \c true to continue traversing the tree, or \c false to stop (because + * \p xml was deleted) + * + * \note This is compatible with \c pcmk__xml_tree_foreach(). + */ +static bool +delete_xe_if_matching(xmlNode *xml, void *user_data) +{ + xmlNode *search = user_data; - } else if (can_update) { -#if XML_PARSER_DEBUG - crm_log_xml_trace(child, "Update match found..."); -#endif - pcmk__xml_update(NULL, child, to_update, false); + if (!pcmk__xe_is(search, (const char *) xml->name)) { + // No match: either not both elements, or different element types + return true; } - for (child_of_child = pcmk__xml_first_child(child); child_of_child != NULL; - child_of_child = pcmk__xml_next(child_of_child)) { - /* only update the first one */ - if (can_update) { - break; + for (const xmlAttr *attr = pcmk__xe_first_attr(search); attr != NULL; + attr = attr->next) { + + const char *search_val = pcmk__xml_attr_value(attr); + const char *xml_val = crm_element_value(xml, (const char *) attr->name); + + if (!pcmk__str_eq(search_val, xml_val, pcmk__str_casei)) { + // No match: an attr in xml doesn't match the attr in search + return true; } - can_update = update_xml_child(child_of_child, to_update); } - return can_update; + crm_log_xml_trace(xml, "delete-match"); + crm_log_xml_trace(search, "delete-search"); + free_xml(xml); + + // Found a match and deleted it; stop traversing tree + return false; } +/*! + * \internal + * \brief Search an XML tree depth-first and delete the first matching element + * + * This function does not attempt to match the tree root (\p xml). + * + * A match with a node \c node is defined as follows: + * * \c node and \p search are both element nodes of the same type. + * * If \p search has attributes set, \c node has those attributes set to the + * same values. (\c node may have additional attributes set to arbitrary + * values.) + * + * \param[in,out] xml XML subtree to search + * \param[in] search Element to match against + * + * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on + * successful deletion and an error code otherwise) + */ int -find_xml_children(xmlNode ** children, xmlNode * root, - const char *tag, const char *field, const char *value, gboolean search_matches) +pcmk__xe_delete_match(xmlNode *xml, xmlNode *search) { - int match_found = 0; + // See @COMPAT comment in pcmk__xe_replace_match() + CRM_CHECK((xml != NULL) && (search != NULL), return EINVAL); - CRM_CHECK(root != NULL, return FALSE); - CRM_CHECK(children != NULL, return FALSE); - - if ((tag != NULL) && !pcmk__xe_is(root, tag)) { + for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL; + xml = pcmk__xe_next(xml)) { - } else if (value != NULL && !pcmk__str_eq(value, crm_element_value(root, field), pcmk__str_casei)) { - - } else { - if (*children == NULL) { - *children = create_xml_node(NULL, __func__); + if (!pcmk__xml_tree_foreach(xml, delete_xe_if_matching, search)) { + // Found and deleted an element + return pcmk_rc_ok; } - add_node_copy(*children, root); - match_found = 1; } - if (search_matches || match_found == 0) { - xmlNode *child = NULL; + // No match found in this subtree + return ENXIO; +} - for (child = pcmk__xml_first_child(root); child != NULL; - child = pcmk__xml_next(child)) { - match_found += find_xml_children(children, child, tag, field, value, search_matches); - } - } +/*! + * \internal + * \brief Replace one XML node with a copy of another XML node + * + * This function handles change tracking and applies ACLs. + * + * \param[in,out] old XML node to replace + * \param[in] new XML node to copy as replacement for \p old + * + * \note This frees \p old. + */ +static void +replace_node(xmlNode *old, xmlNode *new) +{ + new = xmlCopyNode(new, 1); + pcmk__mem_assert(new); - return match_found; + // May be unnecessary but avoids slight changes to some test outputs + pcmk__xml_tree_foreach(new, reset_xml_node_flags, NULL); + + old = xmlReplaceNode(old, new); + + if (xml_tracking_changes(new)) { + // Replaced sections may have included relevant ACLs + pcmk__apply_acl(new); + } + xml_calculate_changes(old, new); + xmlFreeNode(old); } -gboolean -replace_xml_child(xmlNode * parent, xmlNode * child, xmlNode * update, gboolean delete_only) +/*! + * \internal + * \brief Replace one XML subtree with a copy of another if the two match + * + * A match is defined as follows: + * * \p xml and \p user_data are both element nodes of the same type. + * * If \p user_data has the \c PCMK_XA_ID attribute set, then \p xml has + * \c PCMK_XA_ID set to the same value. + * + * \param[in,out] xml XML subtree to replace with \p user_data upon match + * \param[in] user_data XML to replace \p xml with a copy of upon match + * + * \return \c true to continue traversing the tree, or \c false to stop (because + * \p xml was replaced by \p user_data) + * + * \note This is compatible with \c pcmk__xml_tree_foreach(). + */ +static bool +replace_xe_if_matching(xmlNode *xml, void *user_data) { - gboolean can_delete = FALSE; - xmlNode *child_of_child = NULL; + xmlNode *replace = user_data; + const char *xml_id = NULL; + const char *replace_id = NULL; - const char *up_id = NULL; - const char *child_id = NULL; - const char *right_val = NULL; + xml_id = pcmk__xe_id(xml); + replace_id = pcmk__xe_id(replace); - CRM_CHECK(child != NULL, return FALSE); - CRM_CHECK(update != NULL, return FALSE); + if (!pcmk__xe_is(replace, (const char *) xml->name)) { + // No match: either not both elements, or different element types + return true; + } - up_id = ID(update); - child_id = ID(child); + if ((replace_id != NULL) + && !pcmk__str_eq(replace_id, xml_id, pcmk__str_none)) { - if (up_id == NULL || (child_id && strcmp(child_id, up_id) == 0)) { - can_delete = TRUE; + // No match: ID was provided in replace and doesn't match xml's ID + return true; } - if (!pcmk__xe_is(update, (const char *) child->name)) { - can_delete = FALSE; - } - if (can_delete && delete_only) { - for (xmlAttrPtr a = pcmk__xe_first_attr(update); a != NULL; - a = a->next) { - const char *p_name = (const char *) a->name; - const char *p_value = pcmk__xml_attr_value(a); - right_val = crm_element_value(child, p_name); - if (!pcmk__str_eq(p_value, right_val, pcmk__str_casei)) { - can_delete = FALSE; - } + crm_log_xml_trace(xml, "replace-match"); + crm_log_xml_trace(replace, "replace-with"); + replace_node(xml, replace); + + // Found a match and replaced it; stop traversing tree + return false; +} + +/*! + * \internal + * \brief Search an XML tree depth-first and replace the first matching element + * + * This function does not attempt to match the tree root (\p xml). + * + * A match with a node \c node is defined as follows: + * * \c node and \p replace are both element nodes of the same type. + * * If \p replace has the \c PCMK_XA_ID attribute set, then \c node has + * \c PCMK_XA_ID set to the same value. + * + * \param[in,out] xml XML tree to search + * \param[in] replace XML to replace a matching element with a copy of + * + * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on + * successful replacement and an error code otherwise) + */ +int +pcmk__xe_replace_match(xmlNode *xml, xmlNode *replace) +{ + /* @COMPAT Some of this behavior (like not matching the tree root, which is + * allowed by pcmk__xe_update_match()) is questionable for general use but + * required for backward compatibility by cib_process_replace() and + * cib_process_delete(). Behavior can change at a major version release if + * desired. + */ + CRM_CHECK((xml != NULL) && (replace != NULL), return EINVAL); + + for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL; + xml = pcmk__xe_next(xml)) { + + if (!pcmk__xml_tree_foreach(xml, replace_xe_if_matching, replace)) { + // Found and replaced an element + return pcmk_rc_ok; } } - if (can_delete && parent != NULL) { - crm_log_xml_trace(child, "Delete match found..."); - if (delete_only || update == NULL) { - free_xml(child); + // No match found in this subtree + return ENXIO; +} - } else { - xmlNode *old = child; - xmlNode *new = xmlCopyNode(update, 1); +//! User data for \c update_xe_if_matching() +struct update_data { + xmlNode *update; //!< Update source + uint32_t flags; //!< Group of <tt>enum pcmk__xa_flags</tt> +}; - CRM_ASSERT(new != NULL); +/*! + * \internal + * \brief Update one XML subtree with another if the two match + * + * "Update" means to merge a source subtree into a target subtree (see + * \c pcmk__xml_update()). + * + * A match is defined as follows: + * * \p xml and \p user_data->update are both element nodes of the same type. + * * \p xml and \p user_data->update have the same \c PCMK_XA_ID attribute + * value, or \c PCMK_XA_ID is unset in both + * + * \param[in,out] xml XML subtree to update with \p user_data->update + * upon match + * \param[in] user_data <tt>struct update_data</tt> object + * + * \return \c true to continue traversing the tree, or \c false to stop (because + * \p xml was updated by \p user_data->update) + * + * \note This is compatible with \c pcmk__xml_tree_foreach(). + */ +static bool +update_xe_if_matching(xmlNode *xml, void *user_data) +{ + struct update_data *data = user_data; + xmlNode *update = data->update; - // May be unnecessary but avoids slight changes to some test outputs - reset_xml_node_flags(new); + if (!pcmk__xe_is(update, (const char *) xml->name)) { + // No match: either not both elements, or different element types + return true; + } - old = xmlReplaceNode(old, new); + if (!pcmk__str_eq(pcmk__xe_id(xml), pcmk__xe_id(update), pcmk__str_none)) { + // No match: ID mismatch + return true; + } - if (xml_tracking_changes(new)) { - // Replaced sections may have included relevant ACLs - pcmk__apply_acl(new); - } - xml_calculate_changes(old, new); - xmlFreeNode(old); - } - return TRUE; + crm_log_xml_trace(xml, "update-match"); + crm_log_xml_trace(update, "update-with"); + pcmk__xml_update(NULL, xml, update, data->flags, false); - } else if (can_delete) { - crm_log_xml_debug(child, "Cannot delete the search root"); - can_delete = FALSE; - } + // Found a match and replaced it; stop traversing tree + return false; +} - child_of_child = pcmk__xml_first_child(child); - while (child_of_child) { - xmlNode *next = pcmk__xml_next(child_of_child); +/*! + * \internal + * \brief Search an XML tree depth-first and update the first matching element + * + * "Update" means to merge a source subtree into a target subtree (see + * \c pcmk__xml_update()). + * + * A match with a node \c node is defined as follows: + * * \c node and \p update are both element nodes of the same type. + * * \c node and \p update have the same \c PCMK_XA_ID attribute value, or + * \c PCMK_XA_ID is unset in both + * + * \param[in,out] xml XML tree to search + * \param[in] update XML to update a matching element with + * \param[in] flags Group of <tt>enum pcmk__xa_flags</tt> + * + * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on + * successful update and an error code otherwise) + */ +int +pcmk__xe_update_match(xmlNode *xml, xmlNode *update, uint32_t flags) +{ + /* @COMPAT In pcmk__xe_delete_match() and pcmk__xe_replace_match(), we + * compare IDs only if the equivalent of the update argument has an ID. + * Here, we're stricter: we consider it a mismatch if only one element has + * an ID attribute, or if both elements have IDs but they don't match. + * + * Perhaps we should align the behavior at a major version release. + */ + struct update_data data = { + .update = update, + .flags = flags, + }; - can_delete = replace_xml_child(child, child_of_child, update, delete_only); + CRM_CHECK((xml != NULL) && (update != NULL), return EINVAL); - /* only delete the first one */ - if (can_delete) { - child_of_child = NULL; - } else { - child_of_child = next; - } + if (!pcmk__xml_tree_foreach(xml, update_xe_if_matching, &data)) { + // Found and updated an element + return pcmk_rc_ok; } - return can_delete; + // No match found in this subtree + return ENXIO; } xmlNode * @@ -2461,61 +2077,42 @@ sorted_xml(xmlNode *input, xmlNode *parent, gboolean recursive) CRM_CHECK(input != NULL, return NULL); - result = create_xml_node(parent, (const char *) input->name); + result = pcmk__xe_create(parent, (const char *) input->name); nvpairs = pcmk_xml_attrs2nvpairs(input); nvpairs = pcmk_sort_nvpairs(nvpairs); pcmk_nvpairs2xml_attrs(nvpairs, result); pcmk_free_nvpairs(nvpairs); - for (child = pcmk__xml_first_child(input); child != NULL; - child = pcmk__xml_next(child)) { + for (child = pcmk__xe_first_child(input, NULL, NULL, NULL); child != NULL; + child = pcmk__xe_next(child)) { if (recursive) { sorted_xml(child, result, recursive); } else { - add_node_copy(result, child); + pcmk__xml_copy(result, child); } } return result; } -xmlNode * -first_named_child(const xmlNode *parent, const char *name) -{ - xmlNode *match = NULL; - - for (match = pcmk__xe_first_child(parent); match != NULL; - match = pcmk__xe_next(match)) { - /* - * name == NULL gives first child regardless of name; this is - * semantically incorrect in this function, but may be necessary - * due to prior use of xml_child_iter_filter - */ - if (pcmk__str_eq(name, (const char *)match->name, pcmk__str_null_matches)) { - return match; - } - } - return NULL; -} - /*! - * \brief Get next instance of same XML tag + * \internal + * \brief Get next sibling XML element with the same name as a given element * - * \param[in] sibling XML tag to start from + * \param[in] node XML element to start from * - * \return Next sibling XML tag with same name + * \return Next sibling XML element with same name */ xmlNode * -crm_next_same_xml(const xmlNode *sibling) +pcmk__xe_next_same(const xmlNode *node) { - xmlNode *match = pcmk__xe_next(sibling); + for (xmlNode *match = pcmk__xe_next(node); match != NULL; + match = pcmk__xe_next(match)) { - while (match != NULL) { - if (pcmk__xe_is(match, (const char *) sibling->name)) { + if (pcmk__xe_is(match, (const char *) node->name)) { return match; } - match = pcmk__xe_next(match); } return NULL; } @@ -2554,31 +2151,32 @@ crm_xml_cleanup(void) xmlNode * expand_idref(xmlNode * input, xmlNode * top) { + char *xpath = NULL; const char *ref = NULL; - xmlNode *result = input; + xmlNode *result = NULL; - if (result == NULL) { + if (input == NULL) { return NULL; - - } else if (top == NULL) { - top = input; } - ref = crm_element_value(result, XML_ATTR_IDREF); - if (ref != NULL) { - char *xpath_string = crm_strdup_printf("//%s[@" XML_ATTR_ID "='%s']", - result->name, ref); + ref = crm_element_value(input, PCMK_XA_ID_REF); + if (ref == NULL) { + return input; + } - result = get_xpath_object(xpath_string, top, LOG_ERR); - if (result == NULL) { - char *nodePath = (char *)xmlGetNodePath(top); + if (top == NULL) { + top = input; + } - crm_err("No match for %s found in %s: Invalid configuration", - xpath_string, pcmk__s(nodePath, "unrecognizable path")); - free(nodePath); - } - free(xpath_string); + xpath = crm_strdup_printf("//%s[@" PCMK_XA_ID "='%s']", input->name, ref); + result = get_xpath_object(xpath, top, LOG_DEBUG); + if (result == NULL) { // Not possible with schema validation enabled + pcmk__config_err("Ignoring invalid %s configuration: " + PCMK_XA_ID_REF " '%s' does not reference " + "a valid object " CRM_XS " xpath=%s", + input->name, ref, xpath); } + free(xpath); return result; } @@ -2610,25 +2208,52 @@ pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns) return ret; } -char * -pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec) +static char * +find_artefact(enum pcmk__xml_artefact_ns ns, const char *path, const char *filespec) { - char *base = pcmk__xml_artefact_root(ns), *ret = NULL; + char *ret = NULL; switch (ns) { case pcmk__xml_artefact_ns_legacy_rng: case pcmk__xml_artefact_ns_base_rng: - ret = crm_strdup_printf("%s/%s.rng", base, filespec); + if (pcmk__ends_with(filespec, ".rng")) { + ret = crm_strdup_printf("%s/%s", path, filespec); + } else { + ret = crm_strdup_printf("%s/%s.rng", path, filespec); + } break; case pcmk__xml_artefact_ns_legacy_xslt: case pcmk__xml_artefact_ns_base_xslt: - ret = crm_strdup_printf("%s/%s.xsl", base, filespec); + if (pcmk__ends_with(filespec, ".xsl")) { + ret = crm_strdup_printf("%s/%s", path, filespec); + } else { + ret = crm_strdup_printf("%s/%s.xsl", path, filespec); + } break; default: crm_err("XML artefact family specified as %u not recognized", ns); } + + return ret; +} + +char * +pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec) +{ + struct stat sb; + char *base = pcmk__xml_artefact_root(ns); + char *ret = NULL; + + ret = find_artefact(ns, base, filespec); free(base); + if (stat(ret, &sb) != 0 || !S_ISREG(sb.st_mode)) { + const char *remote_schema_dir = pcmk__remote_schema_dir(); + + free(ret); + ret = find_artefact(ns, remote_schema_dir, filespec); + } + return ret; } @@ -2669,8 +2294,9 @@ pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name, CRM_ASSERT(handler != NULL); for (xmlNode *node = children; node != NULL; node = node->next) { - if (node->type == XML_ELEMENT_NODE && - pcmk__str_eq(child_element_name, (const char *) node->name, pcmk__str_null_matches)) { + if ((node->type == XML_ELEMENT_NODE) + && ((child_element_name == NULL) + || pcmk__xe_is(node, child_element_name))) { int rc = handler(node, userdata); if (rc != pcmk_rc_ok) { @@ -2690,8 +2316,8 @@ pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name, xmlNode * find_entity(xmlNode *parent, const char *node_name, const char *id) { - return pcmk__xe_match(parent, node_name, - ((id == NULL)? id : XML_ATTR_ID), id); + return pcmk__xe_first_child(parent, node_name, + ((id == NULL)? id : PCMK_XA_ID), id); } void @@ -2709,12 +2335,28 @@ getDocPtr(xmlNode *node) doc = node->doc; if (doc == NULL) { - doc = xmlNewDoc((pcmkXmlStr) "1.0"); + doc = xmlNewDoc(PCMK__XML_VERSION); xmlDocSetRootElement(doc, node); } return doc; } +xmlNode * +add_node_copy(xmlNode *parent, xmlNode *src_node) +{ + xmlNode *child = NULL; + + CRM_CHECK((parent != NULL) && (src_node != NULL), return NULL); + + child = xmlDocCopyNode(src_node, parent->doc, 1); + if (child == NULL) { + return NULL; + } + xmlAddChild(parent, child); + pcmk__xml_mark_created(child); + return child; +} + int add_node_nocopy(xmlNode *parent, const char *name, xmlNode *child) { @@ -2732,5 +2374,352 @@ xml_has_children(const xmlNode * xml_root) return FALSE; } +static char * +replace_text(char *text, size_t *index, size_t *length, const char *replace) +{ + // We have space for 1 char already + size_t offset = strlen(replace) - 1; + + if (offset > 0) { + *length += offset; + text = pcmk__realloc(text, *length + 1); + + // Shift characters to the right to make room for the replacement string + for (size_t i = *length; i > (*index + offset); i--) { + text[i] = text[i - offset]; + } + } + + // Replace the character at index by the replacement string + memcpy(text + *index, replace, offset + 1); + + // Reset index to the end of replacement string + *index += offset; + return text; +} + +char * +crm_xml_escape(const char *text) +{ + size_t length = 0; + char *copy = NULL; + + if (text == NULL) { + return NULL; + } + + length = strlen(text); + copy = pcmk__str_copy(text); + for (size_t index = 0; index <= length; index++) { + if(copy[index] & 0x80 && copy[index+1] & 0x80){ + index++; + continue; + } + switch (copy[index]) { + case 0: + // Sanity only; loop should stop at the last non-null byte + break; + case '<': + copy = replace_text(copy, &index, &length, "<"); + break; + case '>': + copy = replace_text(copy, &index, &length, ">"); + break; + case '"': + copy = replace_text(copy, &index, &length, """); + break; + case '\'': + copy = replace_text(copy, &index, &length, "'"); + break; + case '&': + copy = replace_text(copy, &index, &length, "&"); + break; + case '\t': + /* Might as well just expand to a few spaces... */ + copy = replace_text(copy, &index, &length, " "); + break; + case '\n': + copy = replace_text(copy, &index, &length, "\\n"); + break; + case '\r': + copy = replace_text(copy, &index, &length, "\\r"); + break; + default: + /* Check for and replace non-printing characters with their octal equivalent */ + if(copy[index] < ' ' || copy[index] > '~') { + char *replace = crm_strdup_printf("\\%.3o", copy[index]); + + copy = replace_text(copy, &index, &length, replace); + free(replace); + } + } + } + return copy; +} + +xmlNode * +copy_xml(xmlNode *src) +{ + xmlDoc *doc = xmlNewDoc(PCMK__XML_VERSION); + xmlNode *copy = NULL; + + pcmk__mem_assert(doc); + + copy = xmlDocCopyNode(src, doc, 1); + pcmk__mem_assert(copy); + + xmlDocSetRootElement(doc, copy); + return copy; +} + +xmlNode * +create_xml_node(xmlNode *parent, const char *name) +{ + // Like pcmk__xe_create(), but returns NULL on failure + xmlNode *node = NULL; + + CRM_CHECK(!pcmk__str_empty(name), return NULL); + + if (parent == NULL) { + xmlDoc *doc = xmlNewDoc(PCMK__XML_VERSION); + + if (doc == NULL) { + return NULL; + } + + node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL); + if (node == NULL) { + xmlFreeDoc(doc); + return NULL; + } + xmlDocSetRootElement(doc, node); + + } else { + node = xmlNewChild(parent, NULL, (pcmkXmlStr) name, NULL); + if (node == NULL) { + return NULL; + } + } + pcmk__xml_mark_created(node); + return node; +} + +xmlNode * +pcmk_create_xml_text_node(xmlNode *parent, const char *name, + const char *content) +{ + xmlNode *node = pcmk__xe_create(parent, name); + + pcmk__xe_set_content(node, "%s", content); + return node; +} + +xmlNode * +pcmk_create_html_node(xmlNode *parent, const char *element_name, const char *id, + const char *class_name, const char *text) +{ + xmlNode *node = pcmk__html_create(parent, element_name, id, class_name); + + pcmk__xe_set_content(node, "%s", text); + return node; +} + +xmlNode * +first_named_child(const xmlNode *parent, const char *name) +{ + return pcmk__xe_first_child(parent, name, NULL, NULL); +} + +xmlNode * +find_xml_node(const xmlNode *root, const char *search_path, gboolean must_find) +{ + xmlNode *result = NULL; + + if (search_path == NULL) { + crm_warn("Will never find <NULL>"); + return NULL; + } + + result = pcmk__xe_first_child(root, search_path, NULL, NULL); + + if (must_find && (result == NULL)) { + crm_warn("Could not find %s in %s", + search_path, + ((root != NULL)? (const char *) root->name : "<NULL>")); + } + + return result; +} + +xmlNode * +crm_next_same_xml(const xmlNode *sibling) +{ + return pcmk__xe_next_same(sibling); +} + +void +xml_remove_prop(xmlNode * obj, const char *name) +{ + pcmk__xe_remove_attr(obj, name); +} + +gboolean +replace_xml_child(xmlNode * parent, xmlNode * child, xmlNode * update, gboolean delete_only) +{ + bool is_match = false; + const char *child_id = NULL; + const char *update_id = NULL; + + CRM_CHECK(child != NULL, return FALSE); + CRM_CHECK(update != NULL, return FALSE); + + child_id = pcmk__xe_id(child); + update_id = pcmk__xe_id(update); + + /* Match element name and (if provided in update XML) element ID. Don't + * match search root (child is search root if parent == NULL). + */ + is_match = (parent != NULL) + && pcmk__xe_is(update, (const char *) child->name) + && ((update_id == NULL) + || pcmk__str_eq(update_id, child_id, pcmk__str_none)); + + /* For deletion, match all attributes provided in update. A matching node + * can have additional attributes, but values must match for provided ones. + */ + if (is_match && delete_only) { + for (xmlAttr *attr = pcmk__xe_first_attr(update); attr != NULL; + attr = attr->next) { + const char *name = (const char *) attr->name; + const char *update_val = pcmk__xml_attr_value(attr); + const char *child_val = crm_element_value(child, name); + + if (!pcmk__str_eq(update_val, child_val, pcmk__str_casei)) { + is_match = false; + break; + } + } + } + + if (is_match) { + if (delete_only) { + crm_log_xml_trace(child, "delete-match"); + crm_log_xml_trace(update, "delete-search"); + free_xml(child); + + } else { + crm_log_xml_trace(child, "replace-match"); + crm_log_xml_trace(update, "replace-with"); + replace_node(child, update); + } + return TRUE; + } + + // Current node not a match; search the rest of the subtree depth-first + parent = child; + for (child = pcmk__xml_first_child(parent); child != NULL; + child = pcmk__xml_next(child)) { + + // Only delete/replace the first match + if (replace_xml_child(parent, child, update, delete_only)) { + return TRUE; + } + } + + // No match found in this subtree + return FALSE; +} + +gboolean +update_xml_child(xmlNode *child, xmlNode *to_update) +{ + return pcmk__xe_update_match(child, to_update, + pcmk__xaf_score_update) == pcmk_rc_ok; +} + +int +find_xml_children(xmlNode **children, xmlNode *root, const char *tag, + const char *field, const char *value, gboolean search_matches) +{ + int match_found = 0; + + CRM_CHECK(root != NULL, return FALSE); + CRM_CHECK(children != NULL, return FALSE); + + if ((tag != NULL) && !pcmk__xe_is(root, tag)) { + + } else if ((value != NULL) + && !pcmk__str_eq(value, crm_element_value(root, field), + pcmk__str_casei)) { + + } else { + if (*children == NULL) { + *children = pcmk__xe_create(NULL, __func__); + } + pcmk__xml_copy(*children, root); + match_found = 1; + } + + if (search_matches || match_found == 0) { + xmlNode *child = NULL; + + for (child = pcmk__xml_first_child(root); child != NULL; + child = pcmk__xml_next(child)) { + match_found += find_xml_children(children, child, tag, field, value, + search_matches); + } + } + + return match_found; +} + +void +fix_plus_plus_recursive(xmlNode *target) +{ + /* TODO: Remove recursion and use xpath searches for value++ */ + xmlNode *child = NULL; + + for (xmlAttrPtr a = pcmk__xe_first_attr(target); a != NULL; a = a->next) { + const char *p_name = (const char *) a->name; + const char *p_value = pcmk__xml_attr_value(a); + + expand_plus_plus(target, p_name, p_value); + } + for (child = pcmk__xe_first_child(target, NULL, NULL, NULL); child != NULL; + child = pcmk__xe_next(child)) { + + fix_plus_plus_recursive(child); + } +} + +void +copy_in_properties(xmlNode *target, const xmlNode *src) +{ + if (src == NULL) { + crm_warn("No node to copy properties from"); + + } else if (target == NULL) { + crm_err("No node to copy properties into"); + + } else { + for (xmlAttrPtr a = pcmk__xe_first_attr(src); a != NULL; a = a->next) { + const char *p_name = (const char *) a->name; + const char *p_value = pcmk__xml_attr_value(a); + + expand_plus_plus(target, p_name, p_value); + if (xml_acl_denied(target)) { + crm_trace("Cannot copy %s=%s to %s", p_name, p_value, target->name); + return; + } + } + } +} + +void +expand_plus_plus(xmlNode * target, const char *name, const char *value) +{ + pcmk__xe_set_score(target, name, value); +} + // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/common/xml_attr.c b/lib/common/xml_attr.c index 427d267..faed37f 100644 --- a/lib/common/xml_attr.c +++ b/lib/common/xml_attr.c @@ -1,5 +1,5 @@ /* - * 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. * @@ -23,7 +23,6 @@ #include <libxml/xmlIO.h> /* xmlAllocOutputBuffer */ #include <crm/crm.h> -#include <crm/msg_xml.h> #include <crm/common/xml.h> #include <crm/common/xml_internal.h> // PCMK__XML_LOG_BASE, etc. #include "crmcommon_private.h" @@ -62,8 +61,9 @@ pcmk__marked_as_deleted(xmlAttrPtr a, void *user_data) void pcmk__dump_xml_attr(const xmlAttr *attr, GString *buffer) { - char *p_value = NULL; - const char *p_name = NULL; + const char *name = NULL; + const char *value = NULL; + gchar *value_esc = NULL; xml_node_private_t *nodepriv = NULL; if (attr == NULL || attr->children == NULL) { @@ -75,10 +75,21 @@ pcmk__dump_xml_attr(const xmlAttr *attr, GString *buffer) return; } - p_name = (const char *) attr->name; - p_value = crm_xml_escape((const char *)attr->children->content); - pcmk__g_strcat(buffer, " ", p_name, "=\"", pcmk__s(p_value, "<null>"), "\"", - NULL); + name = (const char *) attr->name; + value = (const char *) attr->children->content; + if (value == NULL) { + /* Don't print anything for unset attribute. Any null-indicator value, + * including the empty string, could also be a real value that needs to + * be treated differently from "unset". + */ + return; + } - free(p_value); -}
\ No newline at end of file + if (pcmk__xml_needs_escape(value, pcmk__xml_escape_attr)) { + value_esc = pcmk__xml_escape(value, pcmk__xml_escape_attr); + value = value_esc; + } + + pcmk__g_strcat(buffer, " ", name, "=\"", value, "\"", NULL); + g_free(value_esc); +} diff --git a/lib/common/xml_display.c b/lib/common/xml_display.c index 18cd3b9..b563d3a 100644 --- a/lib/common/xml_display.c +++ b/lib/common/xml_display.c @@ -1,5 +1,5 @@ /* - * 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. * @@ -12,7 +12,6 @@ #include <libxml/tree.h> #include <crm/crm.h> -#include <crm/msg_xml.h> #include <crm/common/xml.h> #include <crm/common/xml_internal.h> // PCMK__XML_LOG_BASE, etc. #include "crmcommon_private.h" @@ -96,7 +95,7 @@ show_xml_element(pcmk__output_t *out, GString *buffer, const char *prefix, int rc = pcmk_rc_no_output; if (pcmk_is_set(options, pcmk__xml_fmt_open)) { - const char *hidden = crm_element_value(data, "hidden"); + const char *hidden = crm_element_value(data, PCMK__XA_HIDDEN); g_string_truncate(buffer, 0); @@ -110,7 +109,7 @@ show_xml_element(pcmk__output_t *out, GString *buffer, const char *prefix, xml_node_private_t *nodepriv = attr->_private; const char *p_name = (const char *) attr->name; const char *p_value = pcmk__xml_attr_value(attr); - char *p_copy = NULL; + gchar *p_copy = NULL; if (pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) { continue; @@ -120,21 +119,23 @@ show_xml_element(pcmk__output_t *out, GString *buffer, const char *prefix, if (pcmk_any_flags_set(options, pcmk__xml_fmt_diff_plus |pcmk__xml_fmt_diff_minus) - && (strcmp(XML_DIFF_MARKER, p_name) == 0)) { + && (strcmp(PCMK__XA_CRM_DIFF_MARKER, p_name) == 0)) { continue; } if ((hidden != NULL) && (p_name[0] != '\0') && (strstr(hidden, p_name) != NULL)) { - pcmk__str_update(&p_copy, "*****"); + + p_value = "*****"; } else { - p_copy = crm_xml_escape(p_value); + p_copy = pcmk__xml_escape(p_value, true); + p_value = p_copy; } pcmk__g_strcat(buffer, " ", p_name, "=\"", - pcmk__s(p_copy, "<null>"), "\"", NULL); - free(p_copy); + pcmk__s(p_value, "<null>"), "\"", NULL); + g_free(p_copy); } if ((data->children != NULL) @@ -477,7 +478,7 @@ log_data_element(int log_level, const char *file, const char *function, if (pcmk_is_set(options, pcmk__xml_fmt_pretty) && ((data->children == NULL) - || (crm_element_value(data, XML_DIFF_MARKER) != NULL))) { + || (crm_element_value(data, PCMK__XA_CRM_DIFF_MARKER) != NULL))) { if (pcmk_is_set(options, pcmk__xml_fmt_diff_plus)) { legacy_options |= xml_log_option_diff_all; diff --git a/lib/common/xml_io.c b/lib/common/xml_io.c new file mode 100644 index 0000000..f88e0b5 --- /dev/null +++ b/lib/common/xml_io.c @@ -0,0 +1,840 @@ +/* + * Copyright 2004-2024 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/types.h> + +#include <bzlib.h> +#include <libxml/parser.h> +#include <libxml/tree.h> +#include <libxml/xmlIO.h> // xmlOutputBuffer* + +#include <crm/crm.h> +#include <crm/common/xml.h> +#include <crm/common/xml_io.h> +#include "crmcommon_private.h" + +/* @COMPAT XML_PARSE_RECOVER allows some XML errors to be silently worked around + * by libxml2, which is potentially ambiguous and dangerous. We should drop it + * when we can break backward compatibility with configurations that might be + * relying on it (i.e. pacemaker 3.0.0). + */ +#define PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER (XML_PARSE_NOBLANKS) +#define PCMK__XML_PARSE_OPTS_WITH_RECOVER (XML_PARSE_NOBLANKS \ + |XML_PARSE_RECOVER) + +/*! + * \internal + * \brief Read from \c stdin until EOF or error + * + * \return Newly allocated string containing the bytes read from \c stdin, or + * \c NULL on error + * + * \note The caller is responsible for freeing the return value using \c free(). + */ +static char * +read_stdin(void) +{ + char *buf = NULL; + size_t length = 0; + + do { + buf = pcmk__realloc(buf, length + PCMK__BUFFER_SIZE + 1); + length += fread(buf + length, 1, PCMK__BUFFER_SIZE, stdin); + } while ((feof(stdin) == 0) && (ferror(stdin) == 0)); + + if (ferror(stdin) != 0) { + crm_err("Error reading input from stdin"); + free(buf); + buf = NULL; + } else { + buf[length] = '\0'; + } + clearerr(stdin); + return buf; +} + +/*! + * \internal + * \brief Decompress a <tt>bzip2</tt>-compressed file into a string buffer + * + * \param[in] filename Name of file to decompress + * + * \return Newly allocated string with the decompressed contents of \p filename, + * or \c NULL on error. + * + * \note The caller is responsible for freeing the return value using \c free(). + */ +static char * +decompress_file(const char *filename) +{ + char *buffer = NULL; + int rc = pcmk_rc_ok; + size_t length = 0; + BZFILE *bz_file = NULL; + FILE *input = fopen(filename, "r"); + + if (input == NULL) { + crm_perror(LOG_ERR, "Could not open %s for reading", filename); + return NULL; + } + + bz_file = BZ2_bzReadOpen(&rc, input, 0, 0, NULL, 0); + rc = pcmk__bzlib2rc(rc); + if (rc != pcmk_rc_ok) { + crm_err("Could not prepare to read compressed %s: %s " + CRM_XS " rc=%d", filename, pcmk_rc_str(rc), rc); + goto done; + } + + // cppcheck seems not to understand the abort-logic in pcmk__realloc + // cppcheck-suppress memleak + do { + int read_len = 0; + + buffer = pcmk__realloc(buffer, length + PCMK__BUFFER_SIZE + 1); + read_len = BZ2_bzRead(&rc, bz_file, buffer + length, PCMK__BUFFER_SIZE); + + if ((rc == BZ_OK) || (rc == BZ_STREAM_END)) { + crm_trace("Read %ld bytes from file: %d", (long) read_len, rc); + length += read_len; + } + } while (rc == BZ_OK); + + rc = pcmk__bzlib2rc(rc); + if (rc != pcmk_rc_ok) { + rc = pcmk__bzlib2rc(rc); + crm_err("Could not read compressed %s: %s " CRM_XS " rc=%d", + filename, pcmk_rc_str(rc), rc); + free(buffer); + buffer = NULL; + } else { + buffer[length] = '\0'; + } + +done: + BZ2_bzReadClose(&rc, bz_file); + fclose(input); + return buffer; +} + +// @COMPAT Remove macro at 3.0.0 when we drop XML_PARSE_RECOVER +/*! + * \internal + * \brief Try to parse XML first without and then with recovery enabled + * + * \param[out] result Where to store the resulting XML doc (<tt>xmlDoc **</tt>) + * \param[in] fn XML parser function + * \param[in] ... All arguments for \p fn except the final one (an + * \c xmlParserOption group) + */ +#define parse_xml_recover(result, fn, ...) do { \ + *result = fn(__VA_ARGS__, PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER); \ + if (*result == NULL) { \ + *result = fn(__VA_ARGS__, PCMK__XML_PARSE_OPTS_WITH_RECOVER); \ + \ + if (*result != NULL) { \ + crm_warn("Successfully recovered from XML errors " \ + "(note: a future release will treat this as a " \ + "fatal failure)"); \ + } \ + } \ + } while (0); + +/*! + * \internal + * \brief Parse XML from a file + * + * \param[in] filename Name of file containing XML (\c NULL or \c "-" for + * \c stdin); if \p filename ends in \c ".bz2", the file + * will be decompressed using \c bzip2 + * + * \return XML tree parsed from the given file; may be \c NULL or only partial + * on error + */ +xmlNode * +pcmk__xml_read(const char *filename) +{ + bool use_stdin = pcmk__str_eq(filename, "-", pcmk__str_null_matches); + xmlNode *xml = NULL; + xmlDoc *output = NULL; + xmlParserCtxt *ctxt = NULL; + const xmlError *last_error = NULL; + + // Create a parser context + ctxt = xmlNewParserCtxt(); + CRM_CHECK(ctxt != NULL, return NULL); + + xmlCtxtResetLastError(ctxt); + xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err); + + if (use_stdin) { + /* @COMPAT After dropping XML_PARSE_RECOVER, we can avoid capturing + * stdin into a buffer and instead call + * xmlCtxtReadFd(ctxt, STDIN_FILENO, NULL, NULL, XML_PARSE_NOBLANKS); + * + * For now we have to save the input so that we can use it twice. + */ + char *input = read_stdin(); + + if (input != NULL) { + parse_xml_recover(&output, xmlCtxtReadDoc, ctxt, (pcmkXmlStr) input, + NULL, NULL); + free(input); + } + + } else if (pcmk__ends_with_ext(filename, ".bz2")) { + char *input = decompress_file(filename); + + if (input != NULL) { + parse_xml_recover(&output, xmlCtxtReadDoc, ctxt, (pcmkXmlStr) input, + NULL, NULL); + free(input); + } + + } else { + parse_xml_recover(&output, xmlCtxtReadFile, ctxt, filename, NULL); + } + + if (output != NULL) { + xml = xmlDocGetRootElement(output); + if (xml != NULL) { + /* @TODO Should we really be stripping out text? This seems like an + * overly broad way to get rid of whitespace, if that's the goal. + * Text nodes may be invalid in most or all Pacemaker inputs, but + * stripping them in a generic "parse XML from file" function may + * not be the best way to ignore them. + */ + pcmk__strip_xml_text(xml); + } + } + + // @COMPAT At 3.0.0, free xml and return NULL if xml != NULL on error + last_error = xmlCtxtGetLastError(ctxt); + if (last_error != NULL) { + if (xml != NULL) { + crm_log_xml_info(xml, "Partial"); + } + } + + xmlFreeParserCtxt(ctxt); + return xml; +} + +/*! + * \internal + * \brief Parse XML from a string + * + * \param[in] input String to parse + * + * \return XML tree parsed from the given string; may be \c NULL or only partial + * on error + */ +xmlNode * +pcmk__xml_parse(const char *input) +{ + xmlNode *xml = NULL; + xmlDoc *output = NULL; + xmlParserCtxt *ctxt = NULL; + const xmlError *last_error = NULL; + + if (input == NULL) { + return NULL; + } + + ctxt = xmlNewParserCtxt(); + if (ctxt == NULL) { + return NULL; + } + + xmlCtxtResetLastError(ctxt); + xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err); + + parse_xml_recover(&output, xmlCtxtReadDoc, ctxt, (pcmkXmlStr) input, NULL, + NULL); + + if (output != NULL) { + xml = xmlDocGetRootElement(output); + } + + // @COMPAT At 3.0.0, free xml and return NULL if xml != NULL; update doxygen + last_error = xmlCtxtGetLastError(ctxt); + if (last_error != NULL) { + if (xml != NULL) { + crm_log_xml_info(xml, "Partial"); + } + } + + xmlFreeParserCtxt(ctxt); + return xml; +} + +/*! + * \internal + * \brief Append a string representation of an XML element to a buffer + * + * \param[in] data XML whose representation to append + * \param[in] options Group of \p pcmk__xml_fmt_options flags + * \param[in,out] buffer Where to append the content (must not be \p NULL) + * \param[in] depth Current indentation level + */ +static void +dump_xml_element(const xmlNode *data, uint32_t options, GString *buffer, + int depth) +{ + bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); + bool filtered = pcmk_is_set(options, pcmk__xml_fmt_filtered); + int spaces = pretty? (2 * depth) : 0; + + for (int lpc = 0; lpc < spaces; lpc++) { + g_string_append_c(buffer, ' '); + } + + pcmk__g_strcat(buffer, "<", data->name, NULL); + + for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL; + attr = attr->next) { + + if (!filtered || !pcmk__xa_filterable((const char *) (attr->name))) { + pcmk__dump_xml_attr(attr, buffer); + } + } + + if (data->children == NULL) { + g_string_append(buffer, "/>"); + + } else { + g_string_append_c(buffer, '>'); + } + + if (pretty) { + g_string_append_c(buffer, '\n'); + } + + if (data->children) { + for (const xmlNode *child = data->children; child != NULL; + child = child->next) { + pcmk__xml_string(child, options, buffer, depth + 1); + } + + for (int lpc = 0; lpc < spaces; lpc++) { + g_string_append_c(buffer, ' '); + } + + pcmk__g_strcat(buffer, "</", data->name, ">", NULL); + + if (pretty) { + g_string_append_c(buffer, '\n'); + } + } +} + +/*! + * \internal + * \brief Append XML text content to a buffer + * + * \param[in] data XML whose content to append + * \param[in] options Group of \p xml_log_options flags + * \param[in,out] buffer Where to append the content (must not be \p NULL) + * \param[in] depth Current indentation level + */ +static void +dump_xml_text(const xmlNode *data, uint32_t options, GString *buffer, + int depth) +{ + bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); + int spaces = pretty? (2 * depth) : 0; + const char *content = (const char *) data->content; + gchar *content_esc = NULL; + + if (pcmk__xml_needs_escape(content, pcmk__xml_escape_text)) { + content_esc = pcmk__xml_escape(content, pcmk__xml_escape_text); + content = content_esc; + } + + for (int lpc = 0; lpc < spaces; lpc++) { + g_string_append_c(buffer, ' '); + } + + g_string_append(buffer, content); + + if (pretty) { + g_string_append_c(buffer, '\n'); + } + g_free(content_esc); +} + +/*! + * \internal + * \brief Append XML CDATA content to a buffer + * + * \param[in] data XML whose content to append + * \param[in] options Group of \p pcmk__xml_fmt_options flags + * \param[in,out] buffer Where to append the content (must not be \p NULL) + * \param[in] depth Current indentation level + */ +static void +dump_xml_cdata(const xmlNode *data, uint32_t options, GString *buffer, + int depth) +{ + bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); + int spaces = pretty? (2 * depth) : 0; + + for (int lpc = 0; lpc < spaces; lpc++) { + g_string_append_c(buffer, ' '); + } + + pcmk__g_strcat(buffer, "<![CDATA[", (const char *) data->content, "]]>", + NULL); + + if (pretty) { + g_string_append_c(buffer, '\n'); + } +} + +/*! + * \internal + * \brief Append an XML comment to a buffer + * + * \param[in] data XML whose content to append + * \param[in] options Group of \p pcmk__xml_fmt_options flags + * \param[in,out] buffer Where to append the content (must not be \p NULL) + * \param[in] depth Current indentation level + */ +static void +dump_xml_comment(const xmlNode *data, uint32_t options, GString *buffer, + int depth) +{ + bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); + int spaces = pretty? (2 * depth) : 0; + + for (int lpc = 0; lpc < spaces; lpc++) { + g_string_append_c(buffer, ' '); + } + + pcmk__g_strcat(buffer, "<!--", (const char *) data->content, "-->", NULL); + + if (pretty) { + g_string_append_c(buffer, '\n'); + } +} + +/*! + * \internal + * \brief Get a string representation of an XML element type + * + * \param[in] type XML element type + * + * \return String representation of \p type + */ +static const char * +xml_element_type_text(xmlElementType type) +{ + static const char *const element_type_names[] = { + [XML_ELEMENT_NODE] = "element", + [XML_ATTRIBUTE_NODE] = "attribute", + [XML_TEXT_NODE] = "text", + [XML_CDATA_SECTION_NODE] = "CDATA section", + [XML_ENTITY_REF_NODE] = "entity reference", + [XML_ENTITY_NODE] = "entity", + [XML_PI_NODE] = "PI", + [XML_COMMENT_NODE] = "comment", + [XML_DOCUMENT_NODE] = "document", + [XML_DOCUMENT_TYPE_NODE] = "document type", + [XML_DOCUMENT_FRAG_NODE] = "document fragment", + [XML_NOTATION_NODE] = "notation", + [XML_HTML_DOCUMENT_NODE] = "HTML document", + [XML_DTD_NODE] = "DTD", + [XML_ELEMENT_DECL] = "element declaration", + [XML_ATTRIBUTE_DECL] = "attribute declaration", + [XML_ENTITY_DECL] = "entity declaration", + [XML_NAMESPACE_DECL] = "namespace declaration", + [XML_XINCLUDE_START] = "XInclude start", + [XML_XINCLUDE_END] = "XInclude end", + }; + + if ((type < 0) || (type >= PCMK__NELEM(element_type_names))) { + return "unrecognized type"; + } + return element_type_names[type]; +} + +/*! + * \internal + * \brief Create a string representation of an XML object + * + * libxml2's \c xmlNodeDumpOutput() doesn't allow filtering, doesn't escape + * special characters thoroughly, and doesn't allow a const argument. + * + * \param[in] data XML to convert + * \param[in] options Group of \p pcmk__xml_fmt_options flags + * \param[in,out] buffer Where to store the text (must not be \p NULL) + * \param[in] depth Current indentation level + * + * \todo Create a wrapper that doesn't require \p depth. Only used with + * recursive calls currently. + */ +void +pcmk__xml_string(const xmlNode *data, uint32_t options, GString *buffer, + int depth) +{ + if (data == NULL) { + crm_trace("Nothing to dump"); + return; + } + + CRM_ASSERT(buffer != NULL); + CRM_CHECK(depth >= 0, depth = 0); + + switch(data->type) { + case XML_ELEMENT_NODE: + /* Handle below */ + dump_xml_element(data, options, buffer, depth); + break; + case XML_TEXT_NODE: + if (pcmk_is_set(options, pcmk__xml_fmt_text)) { + dump_xml_text(data, options, buffer, depth); + } + break; + case XML_COMMENT_NODE: + dump_xml_comment(data, options, buffer, depth); + break; + case XML_CDATA_SECTION_NODE: + dump_xml_cdata(data, options, buffer, depth); + break; + default: + crm_warn("Cannot convert XML %s node to text " CRM_XS " type=%d", + xml_element_type_text(data->type), data->type); + break; + } +} + +/*! + * \internal + * \brief Write a string to a file stream, compressed using \c bzip2 + * + * \param[in] text String to write + * \param[in] filename Name of file being written (for logging only) + * \param[in,out] stream Open file stream to write to + * \param[out] bytes_out Number of bytes written (valid only on success) + * + * \return Standard Pacemaker return code + */ +static int +write_compressed_stream(char *text, const char *filename, FILE *stream, + unsigned int *bytes_out) +{ + unsigned int bytes_in = 0; + int rc = pcmk_rc_ok; + + // (5, 0, 0): (intermediate block size, silent, default workFactor) + BZFILE *bz_file = BZ2_bzWriteOpen(&rc, stream, 5, 0, 0); + + rc = pcmk__bzlib2rc(rc); + if (rc != pcmk_rc_ok) { + crm_warn("Not compressing %s: could not prepare file stream: %s " + CRM_XS " rc=%d", + filename, pcmk_rc_str(rc), rc); + goto done; + } + + BZ2_bzWrite(&rc, bz_file, text, strlen(text)); + rc = pcmk__bzlib2rc(rc); + if (rc != pcmk_rc_ok) { + crm_warn("Not compressing %s: could not compress data: %s " + CRM_XS " rc=%d errno=%d", + filename, pcmk_rc_str(rc), rc, errno); + goto done; + } + + BZ2_bzWriteClose(&rc, bz_file, 0, &bytes_in, bytes_out); + bz_file = NULL; + rc = pcmk__bzlib2rc(rc); + if (rc != pcmk_rc_ok) { + crm_warn("Not compressing %s: could not write compressed data: %s " + CRM_XS " rc=%d errno=%d", + filename, pcmk_rc_str(rc), rc, errno); + goto done; + } + + crm_trace("Compressed XML for %s from %u bytes to %u", + filename, bytes_in, *bytes_out); + +done: + if (bz_file != NULL) { + BZ2_bzWriteClose(&rc, bz_file, 0, NULL, NULL); + } + return rc; +} + +/*! + * \internal + * \brief Write XML to a file stream + * + * \param[in] xml XML to write + * \param[in] filename Name of file being written (for logging only) + * \param[in,out] stream Open file stream corresponding to filename (closed + * when this function returns) + * \param[in] compress Whether to compress XML before writing + * \param[out] nbytes Number of bytes written + * + * \return Standard Pacemaker return code + */ +static int +write_xml_stream(const xmlNode *xml, const char *filename, FILE *stream, + bool compress, unsigned int *nbytes) +{ + // @COMPAT Drop nbytes as arg when we drop write_xml_fd()/write_xml_file() + GString *buffer = g_string_sized_new(1024); + unsigned int bytes_out = 0; + int rc = pcmk_rc_ok; + + pcmk__xml_string(xml, pcmk__xml_fmt_pretty, buffer, 0); + CRM_CHECK(!pcmk__str_empty(buffer->str), + crm_log_xml_info(xml, "dump-failed"); + rc = pcmk_rc_error; + goto done); + + crm_log_xml_trace(xml, "writing"); + + if (compress + && (write_compressed_stream(buffer->str, filename, stream, + &bytes_out) == pcmk_rc_ok)) { + goto done; + } + + rc = fprintf(stream, "%s", buffer->str); + if (rc < 0) { + rc = EIO; + crm_perror(LOG_ERR, "writing %s", filename); + goto done; + } + bytes_out = (unsigned int) rc; + rc = pcmk_rc_ok; + +done: + if (fflush(stream) != 0) { + rc = errno; + crm_perror(LOG_ERR, "flushing %s", filename); + } + + // Don't report error if the file does not support synchronization + if ((fsync(fileno(stream)) < 0) && (errno != EROFS) && (errno != EINVAL)) { + rc = errno; + crm_perror(LOG_ERR, "synchronizing %s", filename); + } + + fclose(stream); + crm_trace("Saved %u bytes to %s as XML", bytes_out, filename); + + if (nbytes != NULL) { + *nbytes = bytes_out; + } + g_string_free(buffer, TRUE); + return rc; +} + +/*! + * \internal + * \brief Write XML to a file descriptor + * + * \param[in] xml XML to write + * \param[in] filename Name of file being written (for logging only) + * \param[in] fd Open file descriptor corresponding to \p filename + * \param[in] compress If \c true, compress XML before writing + * \param[out] nbytes Number of bytes written (can be \c NULL) + * + * \return Standard Pacemaker return code + */ +int +pcmk__xml_write_fd(const xmlNode *xml, const char *filename, int fd, + bool compress, unsigned int *nbytes) +{ + // @COMPAT Drop compress and nbytes arguments when we drop write_xml_fd() + FILE *stream = NULL; + + CRM_CHECK((xml != NULL) && (fd > 0), return EINVAL); + stream = fdopen(fd, "w"); + if (stream == NULL) { + return errno; + } + + return write_xml_stream(xml, pcmk__s(filename, "unnamed file"), stream, + compress, nbytes); +} + +/*! + * \internal + * \brief Write XML to a file + * + * \param[in] xml XML to write + * \param[in] filename Name of file to write + * \param[in] compress If \c true, compress XML before writing + * \param[out] nbytes Number of bytes written (can be \c NULL) + * + * \return Standard Pacemaker return code + */ +int +pcmk__xml_write_file(const xmlNode *xml, const char *filename, bool compress, + unsigned int *nbytes) +{ + // @COMPAT Drop nbytes argument when we drop write_xml_fd() + FILE *stream = NULL; + + CRM_CHECK((xml != NULL) && (filename != NULL), return EINVAL); + stream = fopen(filename, "w"); + if (stream == NULL) { + return errno; + } + + return write_xml_stream(xml, filename, stream, compress, nbytes); +} + +/*! + * \internal + * \brief Serialize XML (using libxml) into provided descriptor + * + * \param[in] fd File descriptor to (piece-wise) write to + * \param[in] cur XML subtree to proceed + * + * \return a standard Pacemaker return code + */ +int +pcmk__xml2fd(int fd, xmlNode *cur) +{ + bool success; + + xmlOutputBuffer *fd_out = xmlOutputBufferCreateFd(fd, NULL); + pcmk__mem_assert(fd_out); + xmlNodeDumpOutput(fd_out, cur->doc, cur, 0, pcmk__xml_fmt_pretty, NULL); + + success = xmlOutputBufferWrite(fd_out, sizeof("\n") - 1, "\n") != -1; + + success = xmlOutputBufferClose(fd_out) != -1 && success; + + if (!success) { + return EIO; + } + + fsync(fd); + return pcmk_rc_ok; +} + +void +save_xml_to_file(const xmlNode *xml, const char *desc, const char *filename) +{ + char *f = NULL; + + if (filename == NULL) { + char *uuid = crm_generate_uuid(); + + f = crm_strdup_printf("%s/%s", pcmk__get_tmpdir(), uuid); + filename = f; + free(uuid); + } + + crm_info("Saving %s to %s", desc, filename); + pcmk__xml_write_file(xml, filename, false, NULL); + free(f); +} + + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include <crm/common/xml_io_compat.h> + +xmlNode * +filename2xml(const char *filename) +{ + return pcmk__xml_read(filename); +} + +xmlNode * +stdin2xml(void) +{ + return pcmk__xml_read(NULL); +} + +xmlNode * +string2xml(const char *input) +{ + return pcmk__xml_parse(input); +} + +char * +dump_xml_formatted(const xmlNode *xml) +{ + char *str = NULL; + GString *buffer = g_string_sized_new(1024); + + pcmk__xml_string(xml, pcmk__xml_fmt_pretty, buffer, 0); + + str = pcmk__str_copy(buffer->str); + g_string_free(buffer, TRUE); + return str; +} + +char * +dump_xml_formatted_with_text(const xmlNode *xml) +{ + char *str = NULL; + GString *buffer = g_string_sized_new(1024); + + pcmk__xml_string(xml, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, buffer, 0); + + str = pcmk__str_copy(buffer->str); + g_string_free(buffer, TRUE); + return str; +} + +char * +dump_xml_unformatted(const xmlNode *xml) +{ + char *str = NULL; + GString *buffer = g_string_sized_new(1024); + + pcmk__xml_string(xml, 0, buffer, 0); + + str = pcmk__str_copy(buffer->str); + g_string_free(buffer, TRUE); + return str; +} + +int +write_xml_fd(const xmlNode *xml, const char *filename, int fd, + gboolean compress) +{ + unsigned int nbytes = 0; + int rc = pcmk__xml_write_fd(xml, filename, fd, compress, &nbytes); + + if (rc != pcmk_rc_ok) { + return pcmk_rc2legacy(rc); + } + return (int) nbytes; +} + +int +write_xml_file(const xmlNode *xml, const char *filename, gboolean compress) +{ + unsigned int nbytes = 0; + int rc = pcmk__xml_write_file(xml, filename, compress, &nbytes); + + if (rc != pcmk_rc_ok) { + return pcmk_rc2legacy(rc); + } + return (int) nbytes; +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/xpath.c b/lib/common/xpath.c index d90f1c5..9fc95c5 100644 --- a/lib/common/xpath.c +++ b/lib/common/xpath.c @@ -1,5 +1,5 @@ /* - * 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. * @@ -10,7 +10,7 @@ #include <crm_internal.h> #include <stdio.h> #include <string.h> -#include <crm/msg_xml.h> +#include <crm/common/xml.h> #include <crm/common/xml_internal.h> #include "crmcommon_private.h" @@ -147,7 +147,7 @@ xpath_search(const xmlNode *xml_top, const char *path) CRM_CHECK(strlen(path) > 0, return NULL); xpathCtx = xmlXPathNewContext(xml_top->doc); - CRM_ASSERT(xpathCtx != NULL); + pcmk__mem_assert(xpathCtx); xpathObj = xmlXPathEvalExpression(xpathExpr, xpathCtx); xmlXPathFreeContext(xpathCtx); @@ -186,28 +186,6 @@ crm_foreach_xpath_result(xmlNode *xml, const char *xpath, } xmlNode * -get_xpath_object_relative(const char *xpath, xmlNode * xml_obj, int error_level) -{ - xmlNode *result = NULL; - char *xpath_full = NULL; - char *xpath_prefix = NULL; - - if (xml_obj == NULL || xpath == NULL) { - return NULL; - } - - xpath_prefix = (char *)xmlGetNodePath(xml_obj); - - xpath_full = crm_strdup_printf("%s%s", xpath_prefix, xpath); - - result = get_xpath_object(xpath_full, xml_obj, error_level); - - free(xpath_prefix); - free(xpath_full); - return result; -} - -xmlNode * get_xpath_object(const char *xpath, xmlNode * xml_obj, int error_level) { int max; @@ -300,9 +278,9 @@ pcmk__element_xpath(const xmlNode *xml) pcmk__g_strcat(xpath, "/", (const char *) xml->name, NULL); } - id = ID(xml); + id = pcmk__xe_id(xml); if (id != NULL) { - pcmk__g_strcat(xpath, "[@" XML_ATTR_ID "='", id, "']", NULL); + pcmk__g_strcat(xpath, "[@" PCMK_XA_ID "='", id, "']", NULL); } return xpath; @@ -320,7 +298,7 @@ pcmk__xpath_node_id(const char *xpath, const char *node) return retval; } - patt = crm_strdup_printf("/%s[@" XML_ATTR_ID "=", node); + patt = crm_strdup_printf("/%s[@" PCMK_XA_ID "=", node); start = strstr(xpath, patt); if (!start) { @@ -339,6 +317,30 @@ pcmk__xpath_node_id(const char *xpath, const char *node) return retval; } +static int +output_attr_child(xmlNode *child, void *userdata) +{ + pcmk__output_t *out = userdata; + + out->info(out, " Value: %s \t(id=%s)", + crm_element_value(child, PCMK_XA_VALUE), + pcmk__s(pcmk__xe_id(child), "<none>")); + return pcmk_rc_ok; +} + +void +pcmk__warn_multiple_name_matches(pcmk__output_t *out, xmlNode *search, + const char *name) +{ + if (out == NULL || name == NULL || search == NULL || + search->children == NULL) { + return; + } + + out->info(out, "Multiple attributes match " PCMK_XA_NAME "=%s", name); + pcmk__xe_foreach_child(search, NULL, output_attr_child, out); +} + // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START @@ -363,13 +365,32 @@ xml_get_path(const xmlNode *xml) if (g_path == NULL) { return NULL; } - - path = strdup((const char *) g_path->str); - CRM_ASSERT(path != NULL); - + path = pcmk__str_copy(g_path->str); g_string_free(g_path, TRUE); return path; } +xmlNode * +get_xpath_object_relative(const char *xpath, xmlNode *xml_obj, int error_level) +{ + xmlNode *result = NULL; + char *xpath_full = NULL; + char *xpath_prefix = NULL; + + if (xml_obj == NULL || xpath == NULL) { + return NULL; + } + + xpath_prefix = (char *)xmlGetNodePath(xml_obj); + + xpath_full = crm_strdup_printf("%s%s", xpath_prefix, xpath); + + result = get_xpath_object(xpath_full, xml_obj, error_level); + + free(xpath_prefix); + free(xpath_full); + return result; +} + // LCOV_EXCL_STOP // End deprecated API |