diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 06:53:20 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 06:53:20 +0000 |
commit | e5a812082ae033afb1eed82c0f2df3d0f6bdc93f (patch) | |
tree | a6716c9275b4b413f6c9194798b34b91affb3cc7 /lib/common | |
parent | Initial commit. (diff) | |
download | pacemaker-e5a812082ae033afb1eed82c0f2df3d0f6bdc93f.tar.xz pacemaker-e5a812082ae033afb1eed82c0f2df3d0f6bdc93f.zip |
Adding upstream version 2.1.6.upstream/2.1.6
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
159 files changed, 37208 insertions, 0 deletions
diff --git a/lib/common/Makefile.am b/lib/common/Makefile.am new file mode 100644 index 0000000..ef729d4 --- /dev/null +++ b/lib/common/Makefile.am @@ -0,0 +1,124 @@ +# +# Copyright 2004-2023 the Pacemaker project contributors +# +# The version control history for this file may have further details. +# +# This source code is licensed under the GNU General Public License version 2 +# or later (GPLv2+) WITHOUT ANY WARRANTY. +# +include $(top_srcdir)/mk/common.mk + +AM_CPPFLAGS += -I$(top_builddir)/lib/gnu -I$(top_srcdir)/lib/gnu + +## libraries +lib_LTLIBRARIES = libcrmcommon.la +check_LTLIBRARIES = libcrmcommon_test.la + +# Disable -Wcast-qual if used, because we do some hacky casting, +# and because libxml2 has some signatures that should be const but aren't +# for backward compatibility reasons. + +# s390 needs -fPIC +# s390-suse-linux/bin/ld: .libs/ipc.o: relocation R_390_PC32DBL against `__stack_chk_fail@@GLIBC_2.4' can not be used when making a shared object; recompile with -fPIC + +CFLAGS = $(CFLAGS_COPY:-Wcast-qual=) -fPIC + +# Without "." here, check-recursive will run through the subdirectories first +# and then run "make check" here. This will fail, because there's things in +# the subdirectories that need check_LTLIBRARIES built first. Adding "." here +# changes the order so the subdirectories are processed afterwards. +SUBDIRS = . tests + +noinst_HEADERS = crmcommon_private.h mock_private.h + +libcrmcommon_la_LDFLAGS = -version-info 45:0:11 + +libcrmcommon_la_CFLAGS = $(CFLAGS_HARDENED_LIB) +libcrmcommon_la_LDFLAGS += $(LDFLAGS_HARDENED_LIB) + +libcrmcommon_la_LIBADD = @LIBADD_DL@ $(top_builddir)/lib/gnu/libgnu.la + +# If configured with --with-profiling or --with-coverage, BUILD_PROFILING will +# be set and -fno-builtin will be added to the CFLAGS. However, libcrmcommon +# uses the fabs() function which is normally supplied by gcc as one of its +# builtins. Therefore we need to explicitly link against libm here or the +# tests won't link. +if BUILD_PROFILING +libcrmcommon_la_LIBADD += -lm +endif + +# Use += rather than backlashed continuation lines for parsing by bumplibs +libcrmcommon_la_SOURCES = +libcrmcommon_la_SOURCES += acl.c +libcrmcommon_la_SOURCES += agents.c +libcrmcommon_la_SOURCES += alerts.c +libcrmcommon_la_SOURCES += attrs.c +libcrmcommon_la_SOURCES += cib.c +if BUILD_CIBSECRETS +libcrmcommon_la_SOURCES += cib_secrets.c +endif +libcrmcommon_la_SOURCES += cmdline.c +libcrmcommon_la_SOURCES += digest.c +libcrmcommon_la_SOURCES += health.c +libcrmcommon_la_SOURCES += io.c +libcrmcommon_la_SOURCES += ipc_attrd.c +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_server.c +libcrmcommon_la_SOURCES += iso8601.c +libcrmcommon_la_SOURCES += lists.c +libcrmcommon_la_SOURCES += logging.c +libcrmcommon_la_SOURCES += mainloop.c +libcrmcommon_la_SOURCES += messages.c +libcrmcommon_la_SOURCES += nodes.c +libcrmcommon_la_SOURCES += nvpair.c +libcrmcommon_la_SOURCES += operations.c +libcrmcommon_la_SOURCES += options.c +libcrmcommon_la_SOURCES += output.c +libcrmcommon_la_SOURCES += output_html.c +libcrmcommon_la_SOURCES += output_log.c +libcrmcommon_la_SOURCES += output_none.c +libcrmcommon_la_SOURCES += output_text.c +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 += procfs.c +libcrmcommon_la_SOURCES += remote.c +libcrmcommon_la_SOURCES += results.c +libcrmcommon_la_SOURCES += schemas.c +libcrmcommon_la_SOURCES += scores.c +libcrmcommon_la_SOURCES += strings.c +libcrmcommon_la_SOURCES += utils.c +libcrmcommon_la_SOURCES += watchdog.c +libcrmcommon_la_SOURCES += xml.c +libcrmcommon_la_SOURCES += xml_display.c +libcrmcommon_la_SOURCES += xpath.c + +# +# libcrmcommon_test is used only with unit tests, so we can mock system calls. +# See mock.c for details. +# + +include $(top_srcdir)/mk/tap.mk + +libcrmcommon_test_la_SOURCES = $(libcrmcommon_la_SOURCES) +libcrmcommon_test_la_SOURCES += mock.c +libcrmcommon_test_la_LDFLAGS = $(libcrmcommon_la_LDFLAGS) -rpath $(libdir) $(LDFLAGS_WRAP) +# If GCC emits a builtin function in place of something we've mocked up, that will +# get used instead of the mocked version which leads to unexpected test results. So +# disable all builtins. Older versions of GCC (at least, on RHEL7) will still emit +# replacement code for strdup (and possibly other functions) unless -fno-inline is +# also added. +libcrmcommon_test_la_CFLAGS = $(libcrmcommon_la_CFLAGS) -DPCMK__UNIT_TESTING -fno-builtin -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 + +nodist_libcrmcommon_test_la_SOURCES = $(nodist_libcrmcommon_la_SOURCES) + +clean-generic: + rm -f *.log *.debug *.xml *~ diff --git a/lib/common/acl.c b/lib/common/acl.c new file mode 100644 index 0000000..33a4e00 --- /dev/null +++ b/lib/common/acl.c @@ -0,0 +1,860 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <stdio.h> +#include <sys/types.h> +#include <pwd.h> +#include <string.h> +#include <stdlib.h> +#include <stdarg.h> + +#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" + +typedef struct xml_acl_s { + enum xml_private_flags mode; + char *xpath; +} xml_acl_t; + +static void +free_acl(void *data) +{ + if (data) { + xml_acl_t *acl = data; + + free(acl->xpath); + free(acl); + } +} + +void +pcmk__free_acls(GList *acls) +{ + g_list_free_full(acls, free_acl); +} + +static GList * +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); + + if (tag == NULL) { + // @COMPAT rolling upgrades <=1.1.11 + tag = crm_element_value(xml, XML_ACL_ATTR_TAGv1); + } + if (ref == NULL) { + // @COMPAT rolling upgrades <=1.1.11 + ref = crm_element_value(xml, XML_ACL_ATTR_REFv1); + } + + if ((tag == NULL) && (ref == NULL) && (xpath == NULL)) { + // Schema should prevent this, but to be safe ... + crm_trace("Ignoring ACL <%s> element without selection criteria", + crm_element_name(xml)); + return NULL; + } + + acl = calloc(1, sizeof (xml_acl_t)); + CRM_ASSERT(acl != NULL); + + acl->mode = mode; + if (xpath) { + acl->xpath = strdup(xpath); + CRM_ASSERT(acl->xpath != NULL); + crm_trace("Unpacked ACL <%s> element using xpath: %s", + crm_element_name(xml), acl->xpath); + + } else { + GString *buf = g_string_sized_new(128); + + if ((ref != NULL) && (attr != NULL)) { + // NOTE: schema currently does not allow this + pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@" XML_ATTR_ID "='", + ref, "' and @", attr, "]", NULL); + + } else if (ref != NULL) { + pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@" XML_ATTR_ID "='", + ref, "']", NULL); + + } else if (attr != NULL) { + pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@", attr, "]", NULL); + + } else { + pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), NULL); + } + + acl->xpath = strdup((const char *) buf->str); + CRM_ASSERT(acl->xpath != NULL); + + g_string_free(buf, TRUE); + crm_trace("Unpacked ACL <%s> element as xpath: %s", + crm_element_name(xml), acl->xpath); + } + + return g_list_append(acls, acl); +} + +/*! + * \internal + * \brief Unpack a user, group, or role subtree of the ACLs section + * + * \param[in] acl_top XML of entire ACLs section + * \param[in] acl_entry XML of ACL element being unpacked + * \param[in,out] acls List of ACLs unpacked so far + * + * \return New head of (possibly modified) acls + * + * \note This function is recursive + */ +static GList * +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)) { + const char *tag = crm_element_name(child); + const char *kind = crm_element_value(child, XML_ACL_ATTR_KIND); + + if (strcmp(XML_ACL_TAG_PERMISSION, tag) == 0){ + CRM_ASSERT(kind != NULL); + crm_trace("Unpacking ACL <%s> element of kind '%s'", tag, kind); + tag = kind; + } else { + 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); + + 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)) { + const char *role_id = crm_element_value(role, + XML_ATTR_ID); + + if (role_id && strcmp(ref_role, role_id) == 0) { + crm_trace("Unpacking referenced role '%s' in ACL <%s> element", + role_id, crm_element_name(acl_entry)); + acls = parse_acl_entry(acl_top, role, acls); + break; + } + } + } + } + + } else if (strcmp(XML_ACL_TAG_READ, tag) == 0) { + acls = create_acl(child, acls, pcmk__xf_acl_read); + + } else if (strcmp(XML_ACL_TAG_WRITE, tag) == 0) { + acls = create_acl(child, acls, pcmk__xf_acl_write); + + } else if (strcmp(XML_ACL_TAG_DENY, tag) == 0) { + acls = create_acl(child, acls, pcmk__xf_acl_deny); + + } else { + crm_warn("Ignoring unknown ACL %s '%s'", + (kind? "kind" : "element"), tag); + } + } + + return acls; +} + +/* + <acls> + <acl_target id="l33t-haxor"><role id="auto-l33t-haxor"/></acl_target> + <acl_role id="auto-l33t-haxor"> + <acl_permission id="crook-nothing" kind="deny" xpath="/cib"/> + </acl_role> + <acl_target id="niceguy"> + <role id="observer"/> + </acl_target> + <acl_role id="observer"> + <acl_permission id="observer-read-1" kind="read" xpath="/cib"/> + <acl_permission id="observer-write-1" kind="write" xpath="//nvpair[@name='stonith-enabled']"/> + <acl_permission id="observer-write-2" kind="write" xpath="//nvpair[@name='target-role']"/> + </acl_role> + <acl_target id="badidea"><role id="auto-badidea"/></acl_target> + <acl_role id="auto-badidea"> + <acl_permission id="badidea-resources" kind="read" xpath="//meta_attributes"/> + <acl_permission id="badidea-resources-2" kind="deny" reference="dummy-meta_attributes"/> + </acl_role> + </acls> +*/ + +static const char * +acl_to_text(enum xml_private_flags flags) +{ + if (pcmk_is_set(flags, pcmk__xf_acl_deny)) { + return "deny"; + + } else if (pcmk_any_flags_set(flags, pcmk__xf_acl_write|pcmk__xf_acl_create)) { + return "read/write"; + + } else if (pcmk_is_set(flags, pcmk__xf_acl_read)) { + return "read"; + } + return "none"; +} + +void +pcmk__apply_acl(xmlNode *xml) +{ + GList *aIter = NULL; + xml_doc_private_t *docpriv = xml->doc->_private; + xml_node_private_t *nodepriv; + xmlXPathObjectPtr xpathObj = NULL; + + if (!xml_acl_enabled(xml)) { + crm_trace("Skipping ACLs for user '%s' because not enabled for this XML", + docpriv->user); + return; + } + + for (aIter = docpriv->acls; aIter != NULL; aIter = aIter->next) { + int max = 0, lpc = 0; + xml_acl_t *acl = aIter->data; + + xpathObj = xpath_search(xml, acl->xpath); + max = numXpathResults(xpathObj); + + for (lpc = 0; lpc < max; lpc++) { + xmlNode *match = getXpathResult(xpathObj, lpc); + + nodepriv = match->_private; + pcmk__set_xml_flags(nodepriv, acl->mode); + + // Build a GString only if tracing is enabled + pcmk__if_tracing( + { + GString *path = pcmk__element_xpath(match); + crm_trace("Applying %s ACL to %s matched by %s", + acl_to_text(acl->mode), path->str, acl->xpath); + g_string_free(path, TRUE); + }, + {} + ); + } + crm_trace("Applied %s ACL %s (%d match%s)", + acl_to_text(acl->mode), acl->xpath, max, + ((max == 1)? "" : "es")); + freeXpathObject(xpathObj); + } +} + +/*! + * \internal + * \brief Unpack ACLs for a given user into the + * metadata of the target XML tree + * + * Taking the description of ACLs from the source XML tree and + * marking up the target XML tree with access information for the + * given user by tacking it onto the relevant nodes + * + * \param[in] source XML with ACL definitions + * \param[in,out] target XML that ACLs will be applied to + * \param[in] user Username whose ACLs need to be unpacked + */ +void +pcmk__unpack_acl(xmlNode *source, xmlNode *target, const char *user) +{ + xml_doc_private_t *docpriv = NULL; + + if ((target == NULL) || (target->doc == NULL) + || (target->doc->_private == NULL)) { + return; + } + + docpriv = target->doc->_private; + if (!pcmk_acl_required(user)) { + crm_trace("Not unpacking ACLs because not required for user '%s'", + user); + + } else if (docpriv->acls == NULL) { + xmlNode *acls = get_xpath_object("//" XML_CIB_TAG_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)) { + const char *tag = crm_element_name(child); + + if (!strcmp(tag, XML_ACL_TAG_USER) + || !strcmp(tag, XML_ACL_TAG_USERv1)) { + const char *id = crm_element_value(child, XML_ATTR_NAME); + + if (id == NULL) { + id = crm_element_value(child, XML_ATTR_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 (!strcmp(tag, XML_ACL_TAG_GROUP)) { + const char *id = crm_element_value(child, XML_ATTR_NAME); + + if (id == NULL) { + id = crm_element_value(child, XML_ATTR_ID); + } + + if (id && pcmk__is_user_in_group(user,id)) { + crm_debug("Unpacking ACLs for group '%s'", id); + docpriv->acls = parse_acl_entry(acls, child, docpriv->acls); + } + } + } + } + } +} + +/*! + * \internal + * \brief Copy source to target and set xf_acl_enabled flag in target + * + * \param[in] acl_source XML with ACL definitions + * \param[in,out] target XML that ACLs will be applied to + * \param[in] user Username whose ACLs need to be set + */ +void +pcmk__enable_acl(xmlNode *acl_source, xmlNode *target, const char *user) +{ + pcmk__unpack_acl(acl_source, target, user); + pcmk__set_xml_doc_flag(target, pcmk__xf_acl_enabled); + pcmk__apply_acl(target); +} + +static inline bool +test_acl_mode(enum xml_private_flags allowed, enum xml_private_flags requested) +{ + if (pcmk_is_set(allowed, pcmk__xf_acl_deny)) { + return false; + + } else if (pcmk_all_flags_set(allowed, requested)) { + return true; + + } else if (pcmk_is_set(requested, pcmk__xf_acl_read) + && pcmk_is_set(allowed, pcmk__xf_acl_write)) { + return true; + + } else if (pcmk_is_set(requested, pcmk__xf_acl_create) + && pcmk_any_flags_set(allowed, pcmk__xf_acl_write|pcmk__xf_created)) { + return true; + } + return false; +} + +/*! + * \internal + * \brief Rid XML tree of all unreadable nodes and node properties + * + * \param[in,out] xml Root XML node to be purged of attributes + * + * \return true if this node or any of its children are readable + * if false is returned, xml will be freed + * + * \note This function is recursive + */ +static bool +purge_xml_attributes(xmlNode *xml) +{ + xmlNode *child = NULL; + xmlAttr *xIter = NULL; + bool readable_children = false; + 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", + crm_element_name(xml), ID(xml)); + return true; + } + + xIter = xml->properties; + while (xIter != NULL) { + xmlAttr *tmp = xIter; + const char *prop_name = (const char *)xIter->name; + + xIter = xIter->next; + if (strcmp(prop_name, XML_ATTR_ID) == 0) { + continue; + } + + xmlUnsetProp(xml, tmp->name); + } + + child = pcmk__xml_first_child(xml); + while ( child != NULL ) { + xmlNode *tmp = child; + + child = pcmk__xml_next(child); + readable_children |= purge_xml_attributes(tmp); + } + + if (!readable_children) { + free_xml(xml); /* Nothing readable under here, purge completely */ + } + return readable_children; +} + +/*! + * \brief Copy ACL-allowed portions of specified XML + * + * \param[in] user Username whose ACLs should be used + * \param[in] acl_source XML containing ACLs + * \param[in] xml XML to be copied + * \param[out] result Copy of XML portions readable via ACLs + * + * \return true if xml exists and ACLs are required for user, false otherwise + * \note If this returns true, caller should use \p result rather than \p xml + */ +bool +xml_acl_filtered_copy(const char *user, xmlNode *acl_source, xmlNode *xml, + xmlNode **result) +{ + GList *aIter = NULL; + xmlNode *target = NULL; + xml_doc_private_t *docpriv = NULL; + + *result = NULL; + if ((xml == NULL) || !pcmk_acl_required(user)) { + crm_trace("Not filtering XML because ACLs not required for user '%s'", + user); + return false; + } + + crm_trace("Filtering XML copy using user '%s' ACLs", user); + target = copy_xml(xml); + if (target == NULL) { + return true; + } + + pcmk__enable_acl(acl_source, target, user); + + docpriv = target->doc->_private; + for(aIter = docpriv->acls; aIter != NULL && target; aIter = aIter->next) { + int max = 0; + xml_acl_t *acl = aIter->data; + + if (acl->mode != pcmk__xf_acl_deny) { + /* Nothing to do */ + + } else if (acl->xpath) { + int lpc = 0; + xmlXPathObjectPtr xpathObj = xpath_search(target, acl->xpath); + + max = numXpathResults(xpathObj); + for(lpc = 0; lpc < max; lpc++) { + xmlNode *match = getXpathResult(xpathObj, lpc); + + if (!purge_xml_attributes(match) && (match == target)) { + crm_trace("ACLs deny user '%s' access to entire XML document", + user); + freeXpathObject(xpathObj); + return true; + } + } + crm_trace("ACLs deny user '%s' access to %s (%d %s)", + user, acl->xpath, max, + pcmk__plural_alt(max, "match", "matches")); + freeXpathObject(xpathObj); + } + } + + if (!purge_xml_attributes(target)) { + crm_trace("ACLs deny user '%s' access to entire XML document", user); + return true; + } + + if (docpriv->acls) { + g_list_free_full(docpriv->acls, free_acl); + docpriv->acls = NULL; + + } else { + crm_trace("User '%s' without ACLs denied access to entire XML document", + user); + free_xml(target); + target = NULL; + } + + if (target) { + *result = target; + } + + return true; +} + +/*! + * \internal + * \brief Check whether creation of an XML element is implicitly allowed + * + * 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"). + * + * \param[in] xml XML element to check + * + * \return true if XML element is implicitly allowed, false otherwise + */ +static bool +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) { + return false; + } + } + + path = pcmk__element_xpath(xml); + CRM_ASSERT(path != NULL); + + if (strstr((const char *) path->str, "/" XML_CIB_TAG_ACLS "/") != NULL) { + g_string_free(path, TRUE); + return false; + } + + g_string_free(path, TRUE); + return true; +} + +#define display_id(xml) (ID(xml)? ID(xml) : "<unset>") + +/*! + * \internal + * \brief Drop XML nodes created in violation of ACLs + * + * 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"). + * + * \param[in,out] xml XML to check + * \param[in] check_top Whether to apply checks to argument itself + * (if true, xml might get freed) + * + * \note This function is recursive + */ +void +pcmk__apply_creation_acl(xmlNode *xml, bool check_top) +{ + xml_node_private_t *nodepriv = xml->_private; + + if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { + if (implicitly_allowed(xml)) { + crm_trace("Creation of <%s> scaffolding with id=\"%s\"" + " is implicitly allowed", + crm_element_name(xml), 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_element_name(xml), display_id(xml)); + + } else if (check_top) { + crm_trace("ACLs disallow creation of <%s> with id=\"%s\"", + crm_element_name(xml), display_id(xml)); + pcmk_free_xml_subtree(xml); + return; + + } else { + crm_notice("ACLs would disallow creation of %s<%s> with id=\"%s\"", + ((xml == xmlDocGetRootElement(xml->doc))? "root element " : ""), + crm_element_name(xml), display_id(xml)); + } + } + + for (xmlNode *cIter = pcmk__xml_first_child(xml); cIter != NULL; ) { + xmlNode *child = cIter; + cIter = pcmk__xml_next(cIter); /* In case it is free'd */ + pcmk__apply_creation_acl(child, true); + } +} + +/*! + * \brief Check whether or not an XML node is ACL-denied + * + * \param[in] xml node to check + * + * \return true if XML node exists and is ACL-denied, false otherwise + */ +bool +xml_acl_denied(const xmlNode *xml) +{ + if (xml && xml->doc && xml->doc->_private){ + xml_doc_private_t *docpriv = xml->doc->_private; + + return pcmk_is_set(docpriv->flags, pcmk__xf_acl_denied); + } + return false; +} + +void +xml_acl_disable(xmlNode *xml) +{ + if (xml_acl_enabled(xml)) { + xml_doc_private_t *docpriv = xml->doc->_private; + + /* Catch anything that was created but shouldn't have been */ + pcmk__apply_acl(xml); + pcmk__apply_creation_acl(xml, false); + pcmk__clear_xml_flags(docpriv, pcmk__xf_acl_enabled); + } +} + +/*! + * \brief Check whether or not an XML node is ACL-enabled + * + * \param[in] xml node to check + * + * \return true if XML node exists and is ACL-enabled, false otherwise + */ +bool +xml_acl_enabled(const xmlNode *xml) +{ + if (xml && xml->doc && xml->doc->_private){ + xml_doc_private_t *docpriv = xml->doc->_private; + + return pcmk_is_set(docpriv->flags, pcmk__xf_acl_enabled); + } + return false; +} + +bool +pcmk__check_acl(xmlNode *xml, const char *name, enum xml_private_flags mode) +{ + CRM_ASSERT(xml); + CRM_ASSERT(xml->doc); + CRM_ASSERT(xml->doc->_private); + + if (pcmk__tracking_xml_changes(xml, false) && xml_acl_enabled(xml)) { + xmlNode *parent = xml; + xml_doc_private_t *docpriv = xml->doc->_private; + GString *xpath = NULL; + + if (docpriv->acls == NULL) { + pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_denied); + + pcmk__if_tracing({}, return false); + xpath = pcmk__element_xpath(xml); + if (name != NULL) { + pcmk__g_strcat(xpath, "[@", name, "]", NULL); + } + + qb_log_from_external_source(__func__, __FILE__, + "User '%s' without ACLs denied %s " + "access to %s", LOG_TRACE, __LINE__, 0, + docpriv->user, acl_to_text(mode), + (const char *) xpath->str); + g_string_free(xpath, TRUE); + return false; + } + + /* Walk the tree upwards looking for xml_acl_* flags + * - Creating an attribute requires write permissions for the node + * - Creating a child requires write permissions for the parent + */ + + if (name) { + xmlAttr *attr = xmlHasProp(xml, (pcmkXmlStr) name); + + if (attr && mode == pcmk__xf_acl_create) { + mode = pcmk__xf_acl_write; + } + } + + while (parent && parent->_private) { + xml_node_private_t *nodepriv = parent->_private; + if (test_acl_mode(nodepriv->flags, mode)) { + return true; + + } else if (pcmk_is_set(nodepriv->flags, pcmk__xf_acl_deny)) { + pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_denied); + + pcmk__if_tracing({}, return false); + xpath = pcmk__element_xpath(xml); + if (name != NULL) { + pcmk__g_strcat(xpath, "[@", name, "]", NULL); + } + + qb_log_from_external_source(__func__, __FILE__, + "%sACL denies user '%s' %s access " + "to %s", LOG_TRACE, __LINE__, 0, + (parent != xml)? "Parent ": "", + docpriv->user, acl_to_text(mode), + (const char *) xpath->str); + g_string_free(xpath, TRUE); + return false; + } + parent = parent->parent; + } + + pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_denied); + + pcmk__if_tracing({}, return false); + xpath = pcmk__element_xpath(xml); + if (name != NULL) { + pcmk__g_strcat(xpath, "[@", name, "]", NULL); + } + + qb_log_from_external_source(__func__, __FILE__, + "Default ACL denies user '%s' %s access to " + "%s", LOG_TRACE, __LINE__, 0, + docpriv->user, acl_to_text(mode), + (const char *) xpath->str); + g_string_free(xpath, TRUE); + return false; + } + + return true; +} + +/*! + * \brief Check whether ACLs are required for a given user + * + * \param[in] User name to check + * + * \return true if the user requires ACLs, false otherwise + */ +bool +pcmk_acl_required(const char *user) +{ + if (pcmk__str_empty(user)) { + crm_trace("ACLs not required because no user set"); + return false; + + } else if (!strcmp(user, CRM_DAEMON_USER) || !strcmp(user, "root")) { + crm_trace("ACLs not required for privileged user %s", user); + return false; + } + crm_trace("ACLs required for %s", user); + return true; +} + +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; +} + +/*! + * \internal + * \brief Set the ACL user field properly on an XML request + * + * Multiple user names are potentially involved in an XML request: the effective + * user of the current process; the user name known from an IPC client + * connection; and the user name obtained from the request itself, whether by + * the current standard XML attribute name or an older legacy attribute name. + * This function chooses the appropriate one that should be used for ACLs, sets + * it in the request (using the standard attribute name, and the legacy name if + * given), and returns it. + * + * \param[in,out] request XML request to update + * \param[in] field Alternate name for ACL user name XML attribute + * \param[in] peer_user User name as known from IPC connection + * + * \return ACL user name actually used + */ +const char * +pcmk__update_acl_user(xmlNode *request, const char *field, + const char *peer_user) +{ + static const char *effective_user = NULL; + const char *requested_user = NULL; + const char *user = NULL; + + if (effective_user == NULL) { + effective_user = pcmk__uid2username(geteuid()); + if (effective_user == NULL) { + effective_user = strdup("#unprivileged"); + CRM_CHECK(effective_user != NULL, return NULL); + crm_err("Unable to determine effective user, assuming unprivileged for ACLs"); + } + } + + requested_user = crm_element_value(request, XML_ACL_TAG_USER); + 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. + */ + 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 + */ + user = effective_user; + + } else if (peer_user == NULL && requested_user == NULL) { + /* No user known or requested, use 'effective_user' and make sure one is + * set for the request + */ + user = effective_user; + + } else if (peer_user == NULL) { + /* No user known, trusting 'requested_user' */ + user = requested_user; + + } 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 + */ + user = peer_user; + + } else if (requested_user == NULL) { + /* Even if we're privileged, make sure there is always a value set */ + user = peer_user; + + } else { + /* Legal delegation to 'requested_user' */ + user = requested_user; + } + + // 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 (field != NULL && user != crm_element_value(request, field)) { + crm_xml_add(request, field, user); + } + + return requested_user; +} diff --git a/lib/common/agents.c b/lib/common/agents.c new file mode 100644 index 0000000..d2066c0 --- /dev/null +++ b/lib/common/agents.c @@ -0,0 +1,213 @@ +/* + * Copyright 2004-2021 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> + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include <stdio.h> +#include <string.h> +#include <strings.h> + +#include <crm/crm.h> +#include <crm/common/util.h> + +/*! + * \brief Get capabilities of a resource agent standard + * + * \param[in] standard Standard name + * + * \return Bitmask of enum pcmk_ra_caps values + */ +uint32_t +pcmk_get_ra_caps(const char *standard) +{ + /* @COMPAT This should probably be case-sensitive, but isn't, + * for backward compatibility. + */ + if (standard == NULL) { + return pcmk_ra_cap_none; + + } else if (!strcasecmp(standard, PCMK_RESOURCE_CLASS_OCF)) { + return pcmk_ra_cap_provider | pcmk_ra_cap_params + | pcmk_ra_cap_unique | pcmk_ra_cap_promotable; + + } else if (!strcasecmp(standard, PCMK_RESOURCE_CLASS_STONITH)) { + /* @COMPAT Stonith resources can't really be unique clones, but we've + * allowed it in the past and have it in some scheduler regression tests + * (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. + */ + return pcmk_ra_cap_params | pcmk_ra_cap_unique | pcmk_ra_cap_stdin + | pcmk_ra_cap_fence_params; + + } else if (!strcasecmp(standard, PCMK_RESOURCE_CLASS_SYSTEMD) + || !strcasecmp(standard, PCMK_RESOURCE_CLASS_SERVICE) + || !strcasecmp(standard, PCMK_RESOURCE_CLASS_LSB) + || !strcasecmp(standard, PCMK_RESOURCE_CLASS_UPSTART)) { + + /* Since service can map to LSB, systemd, or upstart, these should + * have identical capabilities + */ + return pcmk_ra_cap_status; + + } else if (!strcasecmp(standard, PCMK_RESOURCE_CLASS_NAGIOS)) { + return pcmk_ra_cap_params; + } + return pcmk_ra_cap_none; +} + +int +pcmk__effective_rc(int rc) +{ + int remapped_rc = rc; + + switch (rc) { + case PCMK_OCF_DEGRADED: + remapped_rc = PCMK_OCF_OK; + break; + + case PCMK_OCF_DEGRADED_PROMOTED: + remapped_rc = PCMK_OCF_RUNNING_PROMOTED; + break; + + default: + break; + } + + return remapped_rc; +} + +char * +crm_generate_ra_key(const char *standard, const char *provider, + const char *type) +{ + bool std_empty = pcmk__str_empty(standard); + bool prov_empty = pcmk__str_empty(provider); + bool ty_empty = pcmk__str_empty(type); + + if (std_empty || ty_empty) { + return NULL; + } + + return crm_strdup_printf("%s%s%s:%s", + standard, + (prov_empty ? "" : ":"), (prov_empty ? "" : provider), + type); +} + +/*! + * \brief Parse a "standard[:provider]:type" agent specification + * + * \param[in] spec Agent specification + * \param[out] standard Newly allocated memory containing agent standard (or NULL) + * \param[out] provider Newly allocated memory containing agent provider (or NULL) + * \param[put] type Newly allocated memory containing agent type (or NULL) + * + * \return pcmk_ok if the string could be parsed, -EINVAL otherwise + * + * \note It is acceptable for the type to contain a ':' if the standard supports + * that. For example, systemd supports the form "systemd:UNIT@A:B". + * \note It is the caller's responsibility to free the returned values. + */ +int +crm_parse_agent_spec(const char *spec, char **standard, char **provider, + char **type) +{ + char *colon; + + CRM_CHECK(spec && standard && provider && type, return -EINVAL); + *standard = NULL; + *provider = NULL; + *type = NULL; + + colon = strchr(spec, ':'); + if ((colon == NULL) || (colon == spec)) { + return -EINVAL; + } + + *standard = strndup(spec, colon - spec); + spec = colon + 1; + + if (pcmk_is_set(pcmk_get_ra_caps(*standard), pcmk_ra_cap_provider)) { + colon = strchr(spec, ':'); + if ((colon == NULL) || (colon == spec)) { + free(*standard); + return -EINVAL; + } + *provider = strndup(spec, colon - spec); + spec = colon + 1; + } + + if (*spec == '\0') { + free(*standard); + free(*provider); + return -EINVAL; + } + + *type = strdup(spec); + return pcmk_ok; +} + +/*! + * \brief Check whether a given stonith parameter is handled by Pacemaker + * + * Return true if a given string is the name of one of the special resource + * instance attributes interpreted directly by Pacemaker for stonith-class + * resources. + * + * \param[in] param Parameter name to check + * + * \return true if \p param is a special fencing parameter + */ +bool +pcmk_stonith_param(const char *param) +{ + if (param == NULL) { + return false; + } + if (pcmk__str_any_of(param, PCMK_STONITH_PROVIDES, + PCMK_STONITH_STONITH_TIMEOUT, NULL)) { + return true; + } + if (!pcmk__starts_with(param, "pcmk_")) { // Short-circuit common case + return false; + } + if (pcmk__str_any_of(param, + PCMK_STONITH_ACTION_LIMIT, + PCMK_STONITH_DELAY_BASE, + PCMK_STONITH_DELAY_MAX, + PCMK_STONITH_HOST_ARGUMENT, + PCMK_STONITH_HOST_CHECK, + PCMK_STONITH_HOST_LIST, + PCMK_STONITH_HOST_MAP, + NULL)) { + return true; + } + param = strchr(param + 5, '_'); // Skip past "pcmk_ACTION" + return pcmk__str_any_of(param, "_action", "_timeout", "_retries", NULL); +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include <crm/common/agents_compat.h> + +bool +crm_provider_required(const char *standard) +{ + return pcmk_is_set(pcmk_get_ra_caps(standard), pcmk_ra_cap_provider); +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/alerts.c b/lib/common/alerts.c new file mode 100644 index 0000000..abdadef --- /dev/null +++ b/lib/common/alerts.c @@ -0,0 +1,253 @@ +/* + * Copyright 2015-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> +#include <crm/crm.h> +#include <crm/lrmd.h> +#include <crm/msg_xml.h> +#include <crm/common/alerts_internal.h> +#include <crm/common/xml_internal.h> +#include <crm/cib/internal.h> /* for F_CIB_UPDATE_RESULT */ + +/* + * to allow script compatibility we can have more than one + * set of environment variables + */ +const char *pcmk__alert_keys[PCMK__ALERT_INTERNAL_KEY_MAX][3] = +{ + [PCMK__alert_key_recipient] = { + "CRM_notify_recipient", "CRM_alert_recipient", NULL + }, + [PCMK__alert_key_node] = { + "CRM_notify_node", "CRM_alert_node", NULL + }, + [PCMK__alert_key_nodeid] = { + "CRM_notify_nodeid", "CRM_alert_nodeid", NULL + }, + [PCMK__alert_key_rsc] = { + "CRM_notify_rsc", "CRM_alert_rsc", NULL + }, + [PCMK__alert_key_task] = { + "CRM_notify_task", "CRM_alert_task", NULL + }, + [PCMK__alert_key_interval] = { + "CRM_notify_interval", "CRM_alert_interval", NULL + }, + [PCMK__alert_key_desc] = { + "CRM_notify_desc", "CRM_alert_desc", NULL + }, + [PCMK__alert_key_status] = { + "CRM_notify_status", "CRM_alert_status", NULL + }, + [PCMK__alert_key_target_rc] = { + "CRM_notify_target_rc", "CRM_alert_target_rc", NULL + }, + [PCMK__alert_key_rc] = { + "CRM_notify_rc", "CRM_alert_rc", NULL + }, + [PCMK__alert_key_kind] = { + "CRM_notify_kind", "CRM_alert_kind", NULL + }, + [PCMK__alert_key_version] = { + "CRM_notify_version", "CRM_alert_version", NULL + }, + [PCMK__alert_key_node_sequence] = { + "CRM_notify_node_sequence", PCMK__ALERT_NODE_SEQUENCE, NULL + }, + [PCMK__alert_key_timestamp] = { + "CRM_notify_timestamp", "CRM_alert_timestamp", NULL + }, + [PCMK__alert_key_attribute_name] = { + "CRM_notify_attribute_name", "CRM_alert_attribute_name", NULL + }, + [PCMK__alert_key_attribute_value] = { + "CRM_notify_attribute_value", "CRM_alert_attribute_value", NULL + }, + [PCMK__alert_key_timestamp_epoch] = { + "CRM_notify_timestamp_epoch", "CRM_alert_timestamp_epoch", NULL + }, + [PCMK__alert_key_timestamp_usec] = { + "CRM_notify_timestamp_usec", "CRM_alert_timestamp_usec", NULL + }, + [PCMK__alert_key_exec_time] = { + "CRM_notify_exec_time", "CRM_alert_exec_time", NULL + } +}; + +/*! + * \brief Create a new alert entry structure + * + * \param[in] id ID to use + * \param[in] path Path to alert agent executable + * + * \return Pointer to newly allocated alert entry + * \note Non-string fields will be filled in with defaults. + * It is the caller's responsibility to free the result, + * using pcmk__free_alert(). + */ +pcmk__alert_t * +pcmk__alert_new(const char *id, const char *path) +{ + pcmk__alert_t *entry = calloc(1, sizeof(pcmk__alert_t)); + + CRM_ASSERT(entry && id && path); + entry->id = strdup(id); + entry->path = strdup(path); + entry->timeout = PCMK__ALERT_DEFAULT_TIMEOUT_MS; + entry->flags = pcmk__alert_default; + return entry; +} + +void +pcmk__free_alert(pcmk__alert_t *entry) +{ + if (entry) { + free(entry->id); + free(entry->path); + free(entry->tstamp_format); + free(entry->recipient); + + g_strfreev(entry->select_attribute_name); + if (entry->envvars) { + g_hash_table_destroy(entry->envvars); + } + free(entry); + } +} + +/*! + * \internal + * \brief Duplicate an alert entry + * + * \param[in] entry Alert entry to duplicate + * + * \return Duplicate of alert entry + */ +pcmk__alert_t * +pcmk__dup_alert(const pcmk__alert_t *entry) +{ + pcmk__alert_t *new_entry = pcmk__alert_new(entry->id, entry->path); + + 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); + if (entry->select_attribute_name) { + new_entry->select_attribute_name = g_strdupv(entry->select_attribute_name); + } + return new_entry; +} + +void +pcmk__add_alert_key(GHashTable *table, enum pcmk__alert_keys_e name, + const char *value) +{ + 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)); + } else { + g_hash_table_remove(table, *key); + } + } +} + +void +pcmk__add_alert_key_int(GHashTable *table, enum pcmk__alert_keys_e name, + int value) +{ + 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)); + } +} + +#define XPATH_PATCHSET1_DIFF "//" F_CIB_UPDATE_RESULT "//" XML_TAG_DIFF_ADDED + +#define XPATH_PATCHSET1_CRMCONFIG XPATH_PATCHSET1_DIFF "//" XML_CIB_TAG_CRMCONFIG +#define XPATH_PATCHSET1_ALERTS XPATH_PATCHSET1_DIFF "//" XML_CIB_TAG_ALERTS + +#define XPATH_PATCHSET1_EITHER \ + XPATH_PATCHSET1_CRMCONFIG " | " XPATH_PATCHSET1_ALERTS + +#define XPATH_CONFIG "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION + +#define XPATH_CRMCONFIG XPATH_CONFIG "/" XML_CIB_TAG_CRMCONFIG "/" +#define XPATH_ALERTS XPATH_CONFIG "/" XML_CIB_TAG_ALERTS + +/*! + * \internal + * \brief Check whether a CIB update affects alerts + * + * \param[in] msg XML containing CIB update + * \param[in] config Whether to check for crmconfig change as well + * + * \return TRUE if update affects alerts, FALSE otherwise + */ +bool +pcmk__alert_in_patchset(xmlNode *msg, bool config) +{ + int rc = -1; + int format= 1; + xmlNode *patchset = get_message_xml(msg, F_CIB_UPDATE_RESULT); + xmlNode *change = NULL; + xmlXPathObject *xpathObj = NULL; + + CRM_CHECK(msg != NULL, return FALSE); + + crm_element_value_int(msg, F_CIB_RC, &rc); + if (rc < pcmk_ok) { + crm_trace("Ignore failed CIB update: %s (%d)", pcmk_strerror(rc), rc); + return FALSE; + } + + crm_element_value_int(patchset, "format", &format); + if (format == 1) { + const char *diff = (config? XPATH_PATCHSET1_EITHER : XPATH_PATCHSET1_ALERTS); + + if ((xpathObj = xpath_search(msg, diff)) != NULL) { + freeXpathObject(xpathObj); + return TRUE; + } + } else if (format == 2) { + for (change = pcmk__xml_first_child(patchset); change != NULL; + change = pcmk__xml_next(change)) { + const char *xpath = crm_element_value(change, XML_DIFF_PATH); + + if (xpath == NULL) { + continue; + } + + if ((!config || !strstr(xpath, XPATH_CRMCONFIG)) + && !strstr(xpath, XPATH_ALERTS)) { + + /* this is not a change to an existing section ... */ + + xmlNode *section = NULL; + const char *name = NULL; + + if ((strcmp(xpath, XPATH_CONFIG) != 0) || + ((section = pcmk__xml_first_child(change)) == NULL) || + ((name = crm_element_name(section)) == NULL) || + (strcmp(name, XML_CIB_TAG_ALERTS) != 0)) { + + /* ... nor is it a newly added alerts section */ + continue; + } + } + + return TRUE; + } + + } else { + crm_warn("Unknown patch format: %d", format); + } + return FALSE; +} diff --git a/lib/common/attrs.c b/lib/common/attrs.c new file mode 100644 index 0000000..2be03b4 --- /dev/null +++ b/lib/common/attrs.c @@ -0,0 +1,89 @@ +/* + * Copyright 2011-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include <crm_internal.h> + +#include <stdio.h> + +#include <crm/msg_xml.h> +#include <crm/common/attrd_internal.h> + +#define LRM_TARGET_ENV "OCF_RESKEY_" CRM_META "_" XML_LRM_ATTR_TARGET + +/*! + * \internal + * \brief Get the node name that should be used to set node attributes + * + * 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".) + * + * \param[in] name NULL, "auto" or "localhost" to check environment variables, + * or anything else to return NULL + * + * \return Node name that should be used for node attributes based on the + * environment if known, otherwise NULL + */ +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); + + // It is important to use the name by which the scheduler knows us + if (host_physical && pcmk__str_eq(target, "host", pcmk__str_casei)) { + name = host_physical; + + } else { + const char *host_pcmk = getenv(LRM_TARGET_ENV); + + if (host_pcmk) { + name = host_pcmk; + } + } + free(target_var); + free(phys_var); + + // TODO? Call get_local_node_name() if name == NULL + // (currently would require linkage against libcrmcluster) + return name; + } else { + return NULL; + } +} + +/*! + * \brief Return the name of the node attribute used as a promotion score + * + * \param[in] rsc_id Resource ID that promotion score is for (or NULL to + * check the OCF_RESOURCE_INSTANCE environment variable) + * + * \return Newly allocated string with the node attribute name (or NULL on + * error, including no ID or environment variable specified) + * \note It is the caller's responsibility to free() the result. + */ +char * +pcmk_promotion_score_name(const char *rsc_id) +{ + if (pcmk__str_empty(rsc_id)) { + rsc_id = getenv("OCF_RESOURCE_INSTANCE"); + if (pcmk__str_empty(rsc_id)) { + return NULL; + } + } + return crm_strdup_printf("master-%s", rsc_id); +} diff --git a/lib/common/cib.c b/lib/common/cib.c new file mode 100644 index 0000000..b84c5e8 --- /dev/null +++ b/lib/common/cib.c @@ -0,0 +1,156 @@ +/* + * Original copyright 2004 International Business Machines + * Later changes copyright 2008-2021 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 <libxml/tree.h> // xmlNode + +#include <crm/msg_xml.h> + +/* + * Functions to help find particular sections of the CIB + */ + +// Map CIB element names to their parent elements and XPath searches +static struct { + const char *name; // Name of this CIB element + const char *parent; // CIB element that this element is a child of + const char *path; // XPath to find this CIB element +} cib_sections[] = { + { + // This first entry is also the default if a NULL is compared + XML_TAG_CIB, + NULL, + "//" XML_TAG_CIB + }, + { + XML_CIB_TAG_STATUS, + "/" XML_TAG_CIB, + "//" XML_TAG_CIB "/" XML_CIB_TAG_STATUS + }, + { + XML_CIB_TAG_CONFIGURATION, + "/" XML_TAG_CIB, + "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION + }, + { + XML_CIB_TAG_CRMCONFIG, + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, + "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_CRMCONFIG + }, + { + XML_CIB_TAG_NODES, + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, + "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_NODES + }, + { + XML_CIB_TAG_RESOURCES, + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, + "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_RESOURCES + }, + { + XML_CIB_TAG_CONSTRAINTS, + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, + "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_CONSTRAINTS + }, + { + XML_CIB_TAG_OPCONFIG, + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, + "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_OPCONFIG + }, + { + XML_CIB_TAG_RSCCONFIG, + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, + "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_RSCCONFIG + }, + { + XML_CIB_TAG_ACLS, + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, + "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_ACLS + }, + { + XML_TAG_FENCING_TOPOLOGY, + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, + "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_TAG_FENCING_TOPOLOGY + }, + { + XML_CIB_TAG_TAGS, + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, + "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_TAGS + }, + { + XML_CIB_TAG_ALERTS, + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, + "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_ALERTS + }, + { + XML_CIB_TAG_SECTION_ALL, + NULL, + "//" XML_TAG_CIB + }, +}; + +/*! + * \brief Get the XPath needed to find a specified CIB element name + * + * \param[in] element_name Name of CIB element + * + * \return XPath for finding \p element_name in CIB XML (or NULL if unknown) + * \note The return value is constant and should not be freed. + */ +const char * +pcmk_cib_xpath_for(const char *element_name) +{ + for (int lpc = 0; lpc < PCMK__NELEM(cib_sections); lpc++) { + // A NULL element_name will match the first entry + if (pcmk__str_eq(element_name, cib_sections[lpc].name, + pcmk__str_null_matches)) { + return cib_sections[lpc].path; + } + } + return NULL; +} + +/*! + * \brief Get the parent element name of a given CIB element name + * + * \param[in] element_name Name of CIB element + * + * \return Parent element of \p element_name (or NULL if none or unknown) + * \note The return value is constant and should not be freed. + */ +const char * +pcmk_cib_parent_name_for(const char *element_name) +{ + for (int lpc = 0; lpc < PCMK__NELEM(cib_sections); lpc++) { + // A NULL element_name will match the first entry + if (pcmk__str_eq(element_name, cib_sections[lpc].name, + pcmk__str_null_matches)) { + return cib_sections[lpc].parent; + } + } + return NULL; +} + +/*! + * \brief Find an element in the CIB + * + * \param[in,out] cib Top-level CIB XML to search + * \param[in] element_name Name of CIB element to search for + * + * \return XML element in \p cib corresponding to \p element_name + * (or \p cib itself if element is unknown or not found) + */ +xmlNode * +pcmk_find_cib_element(xmlNode *cib, const char *element_name) +{ + return get_xpath_object(pcmk_cib_xpath_for(element_name), cib, LOG_TRACE); +} diff --git a/lib/common/cib_secrets.c b/lib/common/cib_secrets.c new file mode 100644 index 0000000..2f71c98 --- /dev/null +++ b/lib/common/cib_secrets.c @@ -0,0 +1,192 @@ +/* + * Copyright 2011-2020 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 <unistd.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <ctype.h> +#include <errno.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <time.h> + +#include <glib.h> + +#include <crm/common/util.h> + +static int is_magic_value(char *p); +static bool check_md5_hash(char *hash, char *value); +static void add_secret_params(gpointer key, gpointer value, gpointer user_data); +static char *read_local_file(char *local_file); + +#define MAX_VALUE_LEN 255 +#define MAGIC "lrm://" + +static int +is_magic_value(char *p) +{ + return !strcmp(p, MAGIC); +} + +static bool +check_md5_hash(char *hash, char *value) +{ + bool rc = false; + char *hash2 = NULL; + + hash2 = crm_md5sum(value); + crm_debug("hash: %s, calculated hash: %s", hash, hash2); + if (pcmk__str_eq(hash, hash2, pcmk__str_casei)) { + rc = true; + } + free(hash2); + return rc; +} + +static char * +read_local_file(char *local_file) +{ + FILE *fp = fopen(local_file, "r"); + char buf[MAX_VALUE_LEN+1]; + char *p; + + if (!fp) { + if (errno != ENOENT) { + crm_perror(LOG_ERR, "cannot open %s" , local_file); + } + return NULL; + } + + if (!fgets(buf, MAX_VALUE_LEN, fp)) { + crm_perror(LOG_ERR, "cannot read %s", local_file); + fclose(fp); + return NULL; + } + fclose(fp); + + // Strip trailing white space + for (p = buf + strlen(buf) - 1; (p >= buf) && isspace(*p); p--); + *(p+1) = '\0'; + return strdup(buf); +} + +/*! + * \internal + * \brief Read secret parameter values from file + * + * Given a table of resource parameters, if any of their values are the + * magic string indicating a CIB secret, replace that string with the + * secret read from the file appropriate to the given resource. + * + * \param[in] rsc_id Resource whose parameters are being checked + * \param[in,out] params Resource parameters to check + * + * \return Standard Pacemaker return code + */ +int +pcmk__substitute_secrets(const char *rsc_id, GHashTable *params) +{ + char local_file[FILENAME_MAX+1], *start_pname; + char hash_file[FILENAME_MAX+1], *hash; + GList *secret_params = NULL, *l; + char *key, *pvalue, *secret_value; + int rc = pcmk_rc_ok; + + if (params == NULL) { + return pcmk_rc_ok; + } + + /* secret_params could be cached with the resource; + * there are also parameters sent with operations + * which cannot be cached + */ + g_hash_table_foreach(params, add_secret_params, &secret_params); + if (secret_params == NULL) { // No secret parameters found + return pcmk_rc_ok; + } + + crm_debug("Replace secret parameters for resource %s", rsc_id); + + if (snprintf(local_file, FILENAME_MAX, LRM_CIBSECRETS_DIR "/%s/", rsc_id) + > FILENAME_MAX) { + crm_err("Can't replace secret parameters for %s: file name size exceeded", + rsc_id); + return ENAMETOOLONG; + } + start_pname = local_file + strlen(local_file); + + for (l = g_list_first(secret_params); l; l = g_list_next(l)) { + key = (char *)(l->data); + pvalue = g_hash_table_lookup(params, key); + if (!pvalue) { /* this cannot really happen */ + crm_err("odd, no parameter %s for rsc %s found now", key, rsc_id); + continue; + } + + if ((strlen(key) + strlen(local_file)) >= FILENAME_MAX-2) { + crm_err("%s: parameter name %s too big", rsc_id, key); + rc = ENAMETOOLONG; + continue; + } + + strcpy(start_pname, key); + secret_value = read_local_file(local_file); + if (!secret_value) { + crm_err("secret for rsc %s parameter %s not found in %s", + rsc_id, key, LRM_CIBSECRETS_DIR); + rc = ENOENT; + continue; + } + + strcpy(hash_file, local_file); + if (strlen(hash_file) + 5 > FILENAME_MAX) { + crm_err("cannot build such a long name " + "for the sign file: %s.sign", hash_file); + free(secret_value); + rc = ENAMETOOLONG; + continue; + + } else { + strcat(hash_file, ".sign"); + hash = read_local_file(hash_file); + if (hash == NULL) { + crm_err("md5 sum for rsc %s parameter %s " + "cannot be read from %s", rsc_id, key, hash_file); + free(secret_value); + rc = ENOENT; + continue; + + } else if (!check_md5_hash(hash, secret_value)) { + crm_err("md5 sum for rsc %s parameter %s " + "does not match", rsc_id, key); + free(secret_value); + free(hash); + rc = pcmk_rc_cib_corrupt; + continue; + } + free(hash); + } + g_hash_table_replace(params, strdup(key), secret_value); + } + g_list_free(secret_params); + return rc; +} + +static void +add_secret_params(gpointer key, gpointer value, gpointer user_data) +{ + GList **lp = (GList **)user_data; + + if (is_magic_value((char *)value)) { + *lp = g_list_append(*lp, (char *)key); + } +} diff --git a/lib/common/cmdline.c b/lib/common/cmdline.c new file mode 100644 index 0000000..08c43f7 --- /dev/null +++ b/lib/common/cmdline.c @@ -0,0 +1,379 @@ +/* + * Copyright 2019-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <ctype.h> +#include <glib.h> + +#include <crm/crm.h> +#include <crm/common/cmdline_internal.h> +#include <crm/common/strings_internal.h> +#include <crm/common/util.h> + +static gboolean +bump_verbosity(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { + pcmk__common_args_t *common_args = (pcmk__common_args_t *) data; + common_args->verbosity++; + return TRUE; +} + +pcmk__common_args_t * +pcmk__new_common_args(const char *summary) +{ + pcmk__common_args_t *args = NULL; + + args = calloc(1, sizeof(pcmk__common_args_t)); + if (args == NULL) { + crm_exit(CRM_EX_OSERR); + } + + args->summary = strdup(summary); + if (args->summary == NULL) { + free(args); + args = NULL; + crm_exit(CRM_EX_OSERR); + } + + return args; +} + +static void +free_common_args(gpointer data) { + pcmk__common_args_t *common_args = (pcmk__common_args_t *) data; + + free(common_args->summary); + free(common_args->output_ty); + free(common_args->output_dest); + + if (common_args->output_as_descr != NULL) { + free(common_args->output_as_descr); + } + + free(common_args); +} + +GOptionContext * +pcmk__build_arg_context(pcmk__common_args_t *common_args, const char *fmts, + GOptionGroup **output_group, const char *param_string) { + GOptionContext *context; + GOptionGroup *main_group; + + GOptionEntry main_entries[3] = { + { "version", '$', 0, G_OPTION_ARG_NONE, &(common_args->version), + N_("Display software version and exit"), + NULL }, + { "verbose", 'V', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, bump_verbosity, + N_("Increase debug output (may be specified multiple times)"), + NULL }, + + { NULL } + }; + + main_group = g_option_group_new(NULL, "Application Options:", NULL, common_args, free_common_args); + g_option_group_add_entries(main_group, main_entries); + + context = g_option_context_new(param_string); + g_option_context_set_summary(context, common_args->summary); + g_option_context_set_description(context, + "Report bugs to " PCMK__BUG_URL "\n"); + g_option_context_set_main_group(context, main_group); + + if (fmts != NULL) { + GOptionEntry output_entries[3] = { + { "output-as", 0, 0, G_OPTION_ARG_STRING, &(common_args->output_ty), + NULL, + N_("FORMAT") }, + { "output-to", 0, 0, G_OPTION_ARG_STRING, &(common_args->output_dest), + N_( "Specify file name for output (or \"-\" for stdout)"), N_("DEST") }, + + { NULL } + }; + + if (*output_group == NULL) { + *output_group = g_option_group_new("output", N_("Output Options:"), N_("Show output help"), NULL, NULL); + } + + common_args->output_as_descr = crm_strdup_printf("Specify output format as one of: %s", fmts); + output_entries[0].description = common_args->output_as_descr; + g_option_group_add_entries(*output_group, output_entries); + g_option_context_add_group(context, *output_group); + } + + // main_group is now owned by context, we don't free it here + // cppcheck-suppress memleak + return context; +} + +void +pcmk__free_arg_context(GOptionContext *context) { + if (context == NULL) { + return; + } + + g_option_context_free(context); +} + +void +pcmk__add_main_args(GOptionContext *context, const GOptionEntry entries[]) +{ + GOptionGroup *main_group = g_option_context_get_main_group(context); + + g_option_group_add_entries(main_group, entries); +} + +void +pcmk__add_arg_group(GOptionContext *context, const char *name, + const char *header, const char *desc, + const GOptionEntry entries[]) +{ + GOptionGroup *group = NULL; + + group = g_option_group_new(name, header, desc, NULL, NULL); + g_option_group_add_entries(group, entries); + g_option_context_add_group(context, group); + // group is now owned by context, we don't free it here + // cppcheck-suppress memleak +} + +static gchar * +string_replace(gchar *str, const gchar *sub, const gchar *repl) +{ + /* This function just replaces all occurrences of a substring + * with some other string. It doesn't handle cases like overlapping, + * so don't get clever with it. + * + * FIXME: When glib >= 2.68 is supported, we can get rid of this + * function and use g_string_replace instead. + */ + gchar **split = g_strsplit(str, sub, 0); + gchar *retval = g_strjoinv(repl, split); + + g_strfreev(split); + return retval; +} + +gchar * +pcmk__quote_cmdline(gchar **argv) +{ + GString *gs = NULL; + + if (argv == NULL || argv[0] == NULL) { + return NULL; + } + + gs = g_string_sized_new(100); + + for (int i = 0; argv[i] != NULL; i++) { + if (i > 0) { + g_string_append_c(gs, ' '); + } + + if (strchr(argv[i], ' ') == NULL) { + /* The arg does not contain a space. */ + g_string_append(gs, argv[i]); + } else if (strchr(argv[i], '\'') == NULL) { + /* The arg contains a space, but not a single quote. */ + pcmk__g_strcat(gs, "'", argv[i], "'", NULL); + } else { + /* The arg contains both a space and a single quote, which needs to + * be replaced with an escaped version. We do this instead of counting + * on libxml to handle the escaping for various reasons: + * + * (1) This keeps the string as valid shell. + * (2) We don't want to use XML entities in formats besides XML and HTML. + * (3) The string we are feeding to libxml is something like: "a b 'c d' e". + * It won't escape the single quotes around 'c d' here because there is + * no need to escape quotes inside a different form of quote. If we + * change the string to "a b 'c'd' e", we haven't changed anything - it's + * still single quotes inside double quotes. + * + * On the other hand, if we replace the single quote with "'", then + * we have introduced an ampersand which libxml will escape. This leaves + * us with "&apos;" which is not what we want. + * + * It's simplest to just escape with a backslash. + */ + gchar *repl = string_replace(argv[i], "'", "\\\'"); + pcmk__g_strcat(gs, "'", repl, "'", NULL); + g_free(repl); + } + } + + return g_string_free(gs, FALSE); +} + +gchar ** +pcmk__cmdline_preproc(char *const *argv, const char *special) { + GPtrArray *arr = NULL; + bool saw_dash_dash = false; + bool copy_option = false; + + if (argv == NULL) { + return NULL; + } + + if (g_get_prgname() == NULL && argv && *argv) { + gchar *basename = g_path_get_basename(*argv); + + g_set_prgname(basename); + g_free(basename); + } + + arr = g_ptr_array_new(); + + for (int i = 0; argv[i] != NULL; i++) { + /* If this is the first time we saw "--" in the command line, set + * a flag so we know to just copy everything after it over. We also + * want to copy the "--" over so whatever actually parses the command + * line when we're done knows where arguments end. + */ + if (saw_dash_dash == false && strcmp(argv[i], "--") == 0) { + saw_dash_dash = true; + } + + if (saw_dash_dash == true) { + g_ptr_array_add(arr, g_strdup(argv[i])); + continue; + } + + if (copy_option == true) { + g_ptr_array_add(arr, g_strdup(argv[i])); + copy_option = false; + continue; + } + + /* This is just a dash by itself. That could indicate stdin/stdout, or + * it could be user error. Copy it over and let glib figure it out. + */ + if (pcmk__str_eq(argv[i], "-", pcmk__str_casei)) { + g_ptr_array_add(arr, g_strdup(argv[i])); + continue; + } + + /* "-INFINITY" is almost certainly meant as a string, not as an option + * list + */ + if (strcmp(argv[i], "-INFINITY") == 0) { + g_ptr_array_add(arr, g_strdup(argv[i])); + continue; + } + + /* This is a short argument, or perhaps several. Iterate over it + * and explode them out into individual arguments. + */ + if (g_str_has_prefix(argv[i], "-") && !g_str_has_prefix(argv[i], "--")) { + /* Skip over leading dash */ + const char *ch = argv[i]+1; + + /* This looks like the start of a number, which means it is a negative + * number. It's probably the argument to the preceeding option, but + * we can't know that here. Copy it over and let whatever handles + * arguments next figure it out. + */ + if (*ch != '\0' && *ch >= '1' && *ch <= '9') { + bool is_numeric = true; + + while (*ch != '\0') { + if (!isdigit(*ch)) { + is_numeric = false; + break; + } + + ch++; + } + + if (is_numeric) { + g_ptr_array_add(arr, g_strdup_printf("%s", argv[i])); + continue; + } else { + /* This argument wasn't entirely numeric. Reset ch to the + * beginning so we can process it one character at a time. + */ + ch = argv[i]+1; + } + } + + while (*ch != '\0') { + /* This is a special short argument that takes an option. getopt + * allows values to be interspersed with a list of arguments, but + * glib does not. Grab both the argument and its value and + * separate them into a new argument. + */ + if (special != NULL && strchr(special, *ch) != NULL) { + /* The argument does not occur at the end of this string of + * arguments. Take everything through the end as its value. + */ + if (*(ch+1) != '\0') { + fprintf(stderr, "Deprecated argument format '-%c%s' used.\n", *ch, ch+1); + fprintf(stderr, "Please use '-%c %s' instead. " + "Support will be removed in a future release.\n", + *ch, ch+1); + + g_ptr_array_add(arr, g_strdup_printf("-%c", *ch)); + g_ptr_array_add(arr, g_strdup(ch+1)); + break; + + /* The argument occurs at the end of this string. Hopefully + * whatever comes next in argv is its value. It may not be, + * but that is not for us to decide. + */ + } else { + g_ptr_array_add(arr, g_strdup_printf("-%c", *ch)); + copy_option = true; + ch++; + } + + /* This is a regular short argument. Just copy it over. */ + } else { + g_ptr_array_add(arr, g_strdup_printf("-%c", *ch)); + ch++; + } + } + + /* This is a long argument, or an option, or something else. + * Copy it over - everything else is copied, so this keeps it easy for + * the caller to know what to do with the memory when it's done. + */ + } else { + g_ptr_array_add(arr, g_strdup(argv[i])); + } + } + + g_ptr_array_add(arr, NULL); + + return (char **) g_ptr_array_free(arr, FALSE); +} + +G_GNUC_PRINTF(3, 4) +gboolean +pcmk__force_args(GOptionContext *context, GError **error, const char *format, ...) { + int len = 0; + char *buf = NULL; + gchar **extra_args = NULL; + va_list ap; + gboolean retval = TRUE; + + va_start(ap, format); + len = vasprintf(&buf, format, ap); + CRM_ASSERT(len > 0); + va_end(ap); + + if (!g_shell_parse_argv(buf, NULL, &extra_args, error)) { + g_strfreev(extra_args); + free(buf); + return FALSE; + } + + retval = g_option_context_parse_strv(context, &extra_args, error); + + g_strfreev(extra_args); + free(buf); + return retval; +} diff --git a/lib/common/crmcommon_private.h b/lib/common/crmcommon_private.h new file mode 100644 index 0000000..7faccb6 --- /dev/null +++ b/lib/common/crmcommon_private.h @@ -0,0 +1,325 @@ +/* + * Copyright 2018-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#ifndef CRMCOMMON_PRIVATE__H +# define CRMCOMMON_PRIVATE__H + +/* This header is for the sole use of libcrmcommon, so that functions can be + * declared with G_GNUC_INTERNAL for efficiency. + */ + +#include <stdint.h> // uint8_t, uint32_t +#include <stdbool.h> // bool +#include <sys/types.h> // size_t +#include <glib.h> // GList +#include <libxml/tree.h> // xmlNode, xmlAttr +#include <qb/qbipcc.h> // struct qb_ipc_response_header + +// Decent chunk size for processing large amounts of data +#define PCMK__BUFFER_SIZE 4096 + +#if defined(PCMK__UNIT_TESTING) +#undef G_GNUC_INTERNAL +#define G_GNUC_INTERNAL +#endif + +/* When deleting portions of an XML tree, we keep a record so we can know later + * (e.g. when checking differences) that something was deleted. + */ +typedef struct pcmk__deleted_xml_s { + char *path; + int position; +} pcmk__deleted_xml_t; + +typedef struct xml_node_private_s { + long check; + uint32_t flags; +} xml_node_private_t; + +typedef struct xml_doc_private_s { + long check; + uint32_t flags; + char *user; + GList *acls; + GList *deleted_objs; // List of pcmk__deleted_xml_t +} xml_doc_private_t; + +#define pcmk__set_xml_flags(xml_priv, flags_to_set) do { \ + (xml_priv)->flags = pcmk__set_flags_as(__func__, __LINE__, \ + LOG_NEVER, "XML", "XML node", (xml_priv)->flags, \ + (flags_to_set), #flags_to_set); \ + } while (0) + +#define pcmk__clear_xml_flags(xml_priv, flags_to_clear) do { \ + (xml_priv)->flags = pcmk__clear_flags_as(__func__, __LINE__, \ + LOG_NEVER, "XML", "XML node", (xml_priv)->flags, \ + (flags_to_clear), #flags_to_clear); \ + } while (0) + +G_GNUC_INTERNAL +void pcmk__xml2text(xmlNodePtr 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); + +G_GNUC_INTERNAL +int pcmk__xml_position(const xmlNode *xml, + enum xml_private_flags ignore_if_set); + +G_GNUC_INTERNAL +xmlNode *pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle, + bool exact); + +G_GNUC_INTERNAL +void pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update, + bool as_diff); + +G_GNUC_INTERNAL +xmlNode *pcmk__xc_match(const xmlNode *root, const xmlNode *search_comment, + bool exact); + +G_GNUC_INTERNAL +void pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update); + +G_GNUC_INTERNAL +void pcmk__free_acls(GList *acls); + +G_GNUC_INTERNAL +void pcmk__unpack_acl(xmlNode *source, xmlNode *target, const char *user); + +G_GNUC_INTERNAL +bool pcmk__is_user_in_group(const char *user, const char *group); + +G_GNUC_INTERNAL +void pcmk__apply_acl(xmlNode *xml); + +G_GNUC_INTERNAL +void pcmk__apply_creation_acl(xmlNode *xml, bool check_top); + +G_GNUC_INTERNAL +void pcmk__mark_xml_attr_dirty(xmlAttr *a); + +G_GNUC_INTERNAL +bool pcmk__xa_filterable(const char *name); + +G_GNUC_INTERNAL +void pcmk__log_xmllib_err(void *ctx, const char *fmt, ...) +G_GNUC_PRINTF(2, 3); + +static inline const char * +pcmk__xml_attr_value(const xmlAttr *attr) +{ + return ((attr == NULL) || (attr->children == NULL))? NULL + : (const char *) attr->children->content; +} + +/* + * IPC + */ + +#define PCMK__IPC_VERSION 1 + +#define PCMK__CONTROLD_API_MAJOR "1" +#define PCMK__CONTROLD_API_MINOR "0" + +// IPC behavior that varies by daemon +typedef struct pcmk__ipc_methods_s { + /*! + * \internal + * \brief Allocate any private data needed by daemon IPC + * + * \param[in,out] api IPC API connection + * + * \return Standard Pacemaker return code + */ + int (*new_data)(pcmk_ipc_api_t *api); + + /*! + * \internal + * \brief Free any private data used by daemon IPC + * + * \param[in,out] api_data Data allocated by new_data() method + */ + void (*free_data)(void *api_data); + + /*! + * \internal + * \brief Perform daemon-specific handling after successful connection + * + * Some daemons require clients to register before sending any other + * commands. The controller requires a CRM_OP_HELLO (with no reply), and + * the CIB manager, executor, and fencer require a CRM_OP_REGISTER (with a + * reply). Ideally this would be consistent across all daemons, but for now + * this allows each to do its own authorization. + * + * \param[in,out] api IPC API connection + * + * \return Standard Pacemaker return code + */ + int (*post_connect)(pcmk_ipc_api_t *api); + + /*! + * \internal + * \brief Check whether an IPC request results in a reply + * + * \param[in,out] api IPC API connection + * \param[in,out] request IPC request XML + * + * \return true if request would result in an IPC reply, false otherwise + */ + bool (*reply_expected)(pcmk_ipc_api_t *api, xmlNode *request); + + /*! + * \internal + * \brief Perform daemon-specific handling of an IPC message + * + * \param[in,out] api IPC API connection + * \param[in,out] msg Message read from IPC connection + * + * \return true if more IPC reply messages should be expected + */ + bool (*dispatch)(pcmk_ipc_api_t *api, xmlNode *msg); + + /*! + * \internal + * \brief Perform daemon-specific handling of an IPC disconnect + * + * \param[in,out] api IPC API connection + */ + void (*post_disconnect)(pcmk_ipc_api_t *api); +} pcmk__ipc_methods_t; + +// Implementation of pcmk_ipc_api_t +struct pcmk_ipc_api_s { + enum pcmk_ipc_server server; // Daemon this IPC API instance is for + enum pcmk_ipc_dispatch dispatch_type; // How replies should be dispatched + size_t ipc_size_max; // maximum IPC buffer size + crm_ipc_t *ipc; // IPC connection + mainloop_io_t *mainloop_io; // If using mainloop, I/O source for IPC + bool free_on_disconnect; // Whether disconnect should free object + pcmk_ipc_callback_t cb; // Caller-registered callback (if any) + void *user_data; // Caller-registered data (if any) + void *api_data; // For daemon-specific use + pcmk__ipc_methods_t *cmds; // Behavior that varies by daemon +}; + +typedef struct pcmk__ipc_header_s { + struct qb_ipc_response_header qb; + uint32_t size_uncompressed; + uint32_t size_compressed; + uint32_t flags; + uint8_t version; +} pcmk__ipc_header_t; + +G_GNUC_INTERNAL +int pcmk__send_ipc_request(pcmk_ipc_api_t *api, xmlNode *request); + +G_GNUC_INTERNAL +void pcmk__call_ipc_callback(pcmk_ipc_api_t *api, + enum pcmk_ipc_event event_type, + crm_exit_t status, void *event_data); + +G_GNUC_INTERNAL +unsigned int pcmk__ipc_buffer_size(unsigned int max); + +G_GNUC_INTERNAL +bool pcmk__valid_ipc_header(const pcmk__ipc_header_t *header); + +G_GNUC_INTERNAL +pcmk__ipc_methods_t *pcmk__attrd_api_methods(void); + +G_GNUC_INTERNAL +pcmk__ipc_methods_t *pcmk__controld_api_methods(void); + +G_GNUC_INTERNAL +pcmk__ipc_methods_t *pcmk__pacemakerd_api_methods(void); + +G_GNUC_INTERNAL +pcmk__ipc_methods_t *pcmk__schedulerd_api_methods(void); + + +/* + * Logging + */ + +//! XML is newly created +#define PCMK__XML_PREFIX_CREATED "++" + +//! XML has been deleted +#define PCMK__XML_PREFIX_DELETED "--" + +//! XML has been modified +#define PCMK__XML_PREFIX_MODIFIED "+ " + +//! XML has been moved +#define PCMK__XML_PREFIX_MOVED "+~" + +/*! + * \brief Check the authenticity of the IPC socket peer process + * + * If everything goes well, peer's authenticity is verified by the means + * of comparing against provided referential UID and GID (either satisfies), + * and the result of this check can be deduced from the return value. + * As an exception, detected UID of 0 ("root") satisfies arbitrary + * provided referential daemon's credentials. + * + * \param[in] qb_ipc libqb client connection if available + * \param[in] sock IPC related, connected Unix socket to check peer of + * \param[in] refuid referential UID to check against + * \param[in] refgid referential GID to check against + * \param[out] gotpid to optionally store obtained PID of the peer + * (not available on FreeBSD, special value of 1 + * used instead, and the caller is required to + * special case this value respectively) + * \param[out] gotuid to optionally store obtained UID of the peer + * \param[out] gotgid to optionally store obtained GID of the peer + * + * \return Standard Pacemaker return code + * ie: 0 if it the connection is authentic + * pcmk_rc_ipc_unauthorized if the connection is not authentic, + * standard errors. + * + * \note While this function is tolerant on what constitutes authorized + * IPC daemon process (its effective user matches UID=0 or \p refuid, + * or at least its group matches \p refgid), either or both (in case + * of UID=0) mismatches on the expected credentials of such peer + * process \e shall be investigated at the caller when value of 1 + * gets returned there, since higher-than-expected privileges in + * respect to the expected/intended credentials possibly violate + * the least privilege principle and may pose an additional risk + * (i.e. such accidental inconsistency shall be eventually fixed). + */ +int pcmk__crm_ipc_is_authentic_process(qb_ipcc_connection_t *qb_ipc, int sock, + uid_t refuid, gid_t refgid, + pid_t *gotpid, uid_t *gotuid, + gid_t *gotgid); + + +/* + * Output + */ +G_GNUC_INTERNAL +int pcmk__bare_output_new(pcmk__output_t **out, const char *fmt_name, + const char *filename, char **argv); + +G_GNUC_INTERNAL +void pcmk__register_patchset_messages(pcmk__output_t *out); + + +/* + * Utils + */ +#define PCMK__PW_BUFFER_LEN 500 + + +#endif // CRMCOMMON_PRIVATE__H diff --git a/lib/common/digest.c b/lib/common/digest.c new file mode 100644 index 0000000..3bf04bf --- /dev/null +++ b/lib/common/digest.c @@ -0,0 +1,278 @@ +/* + * Copyright 2015-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <stdio.h> +#include <unistd.h> +#include <string.h> +#include <stdlib.h> +#include <md5.h> + +#include <crm/crm.h> +#include <crm/msg_xml.h> +#include <crm/common/xml.h> +#include "crmcommon_private.h" + +#define BEST_EFFORT_STATUS 0 + +/*! + * \internal + * \brief Dump XML in a format used with v1 digests + * + * \param[in] xml Root of XML to dump + * + * \return Newly allocated buffer containing dumped XML + */ +static GString * +dump_xml_for_digest(xmlNodePtr xml) +{ + GString *buffer = g_string_sized_new(1024); + + /* for compatibility with the old result which is used for v1 digests */ + g_string_append_c(buffer, ' '); + pcmk__xml2text(xml, 0, buffer, 0); + g_string_append_c(buffer, '\n'); + + return buffer; +} + +/*! + * \brief Calculate and return v1 digest of XML tree + * + * \param[in] input Root of XML to digest + * \param[in] sort Whether to sort the XML before calculating digest + * \param[in] ignored Not used + * + * \return Newly allocated string containing digest + * \note Example return value: "c048eae664dba840e1d2060f00299e9d" + */ +static char * +calculate_xml_digest_v1(xmlNode *input, gboolean sort, gboolean ignored) +{ + char *digest = NULL; + GString *buffer = NULL; + xmlNode *copy = NULL; + + if (sort) { + crm_trace("Sorting xml..."); + copy = sorted_xml(input, NULL, TRUE); + crm_trace("Done"); + input = copy; + } + + buffer = dump_xml_for_digest(input); + CRM_CHECK(buffer->len > 0, free_xml(copy); + g_string_free(buffer, TRUE); + return NULL); + + digest = crm_md5sum((const char *) buffer->str); + crm_log_xml_trace(input, "digest:source"); + + g_string_free(buffer, TRUE); + free_xml(copy); + return digest; +} + +/*! + * \brief Calculate and return v2 digest of XML tree + * + * \param[in] source Root of XML to digest + * \param[in] do_filter Whether to filter certain XML attributes + * + * \return Newly allocated string containing digest + */ +static char * +calculate_xml_digest_v2(xmlNode *source, gboolean do_filter) +{ + char *digest = NULL; + GString *buffer = 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__if_tracing( + { + char *trace_file = crm_strdup_printf("%s/digest-%s", + 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), + trace_file); + save_xml_to_file(source, "digest input", trace_file); + free(trace_file); + }, + {} + ); + g_string_free(buffer, TRUE); + crm_trace("End digest"); + return digest; +} + +/*! + * \brief Calculate and return digest of XML tree, suitable for storing on disk + * + * \param[in] input Root of XML to digest + * + * \return Newly allocated string containing digest + */ +char * +calculate_on_disk_digest(xmlNode *input) +{ + /* Always use the v1 format for on-disk digests + * a) it's a compatibility nightmare + * b) we only use this once at startup, all other + * invocations are in a separate child process + */ + return calculate_xml_digest_v1(input, FALSE, FALSE); +} + +/*! + * \brief Calculate and return digest of XML operation + * + * \param[in] input Root of XML to digest + * \param[in] version Unused + * + * \return Newly allocated string containing digest + */ +char * +calculate_operation_digest(xmlNode *input, const char *version) +{ + /* We still need the sorting for operation digests */ + return calculate_xml_digest_v1(input, TRUE, FALSE); +} + +/*! + * \brief Calculate and return digest of XML tree + * + * \param[in] input Root of XML to digest + * \param[in] sort Whether to sort XML before calculating digest + * \param[in] do_filter Whether to filter certain XML attributes + * \param[in] version CRM feature set version (used to select v1/v2 digest) + * + * \return Newly allocated string containing digest + */ +char * +calculate_xml_versioned_digest(xmlNode *input, gboolean sort, + gboolean do_filter, const char *version) +{ + /* + * @COMPAT digests (on-disk or in diffs/patchsets) created <1.1.4; + * removing this affects even full-restart upgrades from old versions + * + * The sorting associated with v1 digest creation accounted for 23% of + * the CIB manager's CPU usage on the server. v2 drops this. + * + * The filtering accounts for an additional 2.5% and we may want to + * remove it in future. + * + * v2 also uses the xmlBuffer contents directly to avoid additional copying + */ + if (version == NULL || compare_version("3.0.5", version) > 0) { + crm_trace("Using v1 digest algorithm for %s", + pcmk__s(version, "unknown feature set")); + return calculate_xml_digest_v1(input, sort, do_filter); + } + crm_trace("Using v2 digest algorithm for %s", + pcmk__s(version, "unknown feature set")); + return calculate_xml_digest_v2(input, do_filter); +} + +/*! + * \internal + * \brief Check whether calculated digest of given XML matches expected digest + * + * \param[in] input Root of XML tree to digest + * \param[in] expected Expected digest in on-disk format + * + * \return true if digests match, false on mismatch or error + */ +bool +pcmk__verify_digest(xmlNode *input, const char *expected) +{ + char *calculated = NULL; + bool passed; + + if (input != NULL) { + calculated = calculate_on_disk_digest(input); + if (calculated == NULL) { + crm_perror(LOG_ERR, "Could not calculate digest for comparison"); + return false; + } + } + passed = pcmk__str_eq(expected, calculated, pcmk__str_casei); + if (passed) { + crm_trace("Digest comparison passed: %s", calculated); + } else { + crm_err("Digest comparison failed: expected %s, calculated %s", + expected, calculated); + } + free(calculated); + return passed; +} + +/*! + * \internal + * \brief Check whether an XML attribute should be excluded from CIB digests + * + * \param[in] name XML attribute name + * + * \return true if XML attribute should be excluded from CIB digest calculation + */ +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, + }; + + for (int i = 0; i < PCMK__NELEM(filter); i++) { + if (strcmp(name, filter[i]) == 0) { + return true; + } + } + return false; +} + +char * +crm_md5sum(const char *buffer) +{ + int lpc = 0, len = 0; + char *digest = NULL; + unsigned char raw_digest[MD5_DIGEST_SIZE]; + + if (buffer == NULL) { + buffer = ""; + } + len = strlen(buffer); + + crm_trace("Beginning digest of %d bytes", len); + digest = malloc(2 * MD5_DIGEST_SIZE + 1); + if (digest) { + md5_buffer(buffer, len, raw_digest); + for (lpc = 0; lpc < MD5_DIGEST_SIZE; lpc++) { + sprintf(digest + (2 * lpc), "%02x", raw_digest[lpc]); + } + digest[(2 * MD5_DIGEST_SIZE)] = 0; + crm_trace("Digest %s.", digest); + + } else { + crm_err("Could not create digest"); + } + return digest; +} diff --git a/lib/common/health.c b/lib/common/health.c new file mode 100644 index 0000000..ee412be --- /dev/null +++ b/lib/common/health.c @@ -0,0 +1,70 @@ +/* + * 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 Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +/*! + * \internal + * \brief Ensure a health strategy value is allowed + * + * \param[in] value Configured health strategy + * + * \return true if \p value is an allowed health strategy value, otherwise false + */ +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, + NULL); +} + +/*! + * \internal + * \brief Parse node health strategy from a user-provided string + * + * \param[in] value User-provided configuration value for node-health-strategy + * + * \return Node health strategy corresponding to \p value + */ +enum pcmk__health_strategy +pcmk__parse_health_strategy(const char *value) +{ + 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)) { + return pcmk__health_strategy_no_red; + + } else 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)) { + return pcmk__health_strategy_progressive; + + } else 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 + " 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 new file mode 100644 index 0000000..2264e16 --- /dev/null +++ b/lib/common/io.c @@ -0,0 +1,663 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include <sys/param.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/resource.h> + +#include <stdio.h> +#include <unistd.h> +#include <string.h> +#include <stdlib.h> +#include <fcntl.h> +#include <dirent.h> +#include <errno.h> +#include <limits.h> +#include <pwd.h> +#include <grp.h> + +#include <crm/crm.h> +#include <crm/common/util.h> + +/*! + * \internal + * \brief Create a directory, including any parent directories needed + * + * \param[in] path_c Pathname of the directory to create + * \param[in] mode Permissions to be used (with current umask) when creating + * + * \return Standard Pacemaker return code + */ +int +pcmk__build_path(const char *path_c, mode_t mode) +{ + int offset = 1, len = 0; + int rc = pcmk_rc_ok; + char *path = strdup(path_c); + + // cppcheck seems not to understand the abort logic in CRM_CHECK + // cppcheck-suppress memleak + CRM_CHECK(path != NULL, return -ENOMEM); + for (len = strlen(path); offset < len; offset++) { + if (path[offset] == '/') { + path[offset] = 0; + if ((mkdir(path, mode) < 0) && (errno != EEXIST)) { + rc = errno; + goto done; + } + path[offset] = '/'; + } + } + if ((mkdir(path, mode) < 0) && (errno != EEXIST)) { + rc = errno; + } +done: + free(path); + return rc; +} + +/*! + * \internal + * \brief Return canonicalized form of a path name + * + * \param[in] path Pathname to canonicalize + * \param[out] resolved_path Where to store canonicalized pathname + * + * \return Standard Pacemaker return code + * \note The caller is responsible for freeing \p resolved_path on success. + * \note This function exists because not all C library versions of + * realpath(path, resolved_path) support a NULL resolved_path. + */ +int +pcmk__real_path(const char *path, char **resolved_path) +{ + CRM_CHECK((path != NULL) && (resolved_path != NULL), return EINVAL); + +#if _POSIX_VERSION >= 200809L + /* Recent C libraries can dynamically allocate memory as needed */ + *resolved_path = realpath(path, NULL); + return (*resolved_path == NULL)? errno : pcmk_rc_ok; + +#elif defined(PATH_MAX) + /* Older implementations require pre-allocated memory */ + /* (this is less desirable because PATH_MAX may be huge or not defined) */ + *resolved_path = malloc(PATH_MAX); + if ((*resolved_path == NULL) || (realpath(path, *resolved_path) == NULL)) { + return errno; + } + return pcmk_rc_ok; +#else + *resolved_path = NULL; + return ENOTSUP; +#endif +} + +/*! + * \internal + * \brief Create a file name using a sequence number + * + * \param[in] directory Directory that contains the file series + * \param[in] series Start of file name + * \param[in] sequence Sequence number + * \param[in] bzip Whether to use ".bz2" instead of ".raw" as extension + * + * \return Newly allocated file path (asserts on error, so always non-NULL) + * \note The caller is responsible for freeing the return value. + */ +char * +pcmk__series_filename(const char *directory, const char *series, int sequence, + bool bzip) +{ + CRM_ASSERT((directory != NULL) && (series != NULL)); + return crm_strdup_printf("%s/%s-%d.%s", directory, series, sequence, + (bzip? "bz2" : "raw")); +} + +/*! + * \internal + * \brief Read sequence number stored in a file series' .last file + * + * \param[in] directory Directory that contains the file series + * \param[in] series Start of file name + * \param[out] seq Where to store the sequence number + * + * \return Standard Pacemaker return code + */ +int +pcmk__read_series_sequence(const char *directory, const char *series, + unsigned int *seq) +{ + int rc; + FILE *fp = NULL; + char *series_file = NULL; + + if ((directory == NULL) || (series == NULL) || (seq == NULL)) { + return EINVAL; + } + + series_file = crm_strdup_printf("%s/%s.last", directory, series); + fp = fopen(series_file, "r"); + if (fp == NULL) { + rc = errno; + crm_debug("Could not open series file %s: %s", + series_file, strerror(rc)); + free(series_file); + return rc; + } + errno = 0; + if (fscanf(fp, "%u", seq) != 1) { + rc = (errno == 0)? ENODATA : errno; + crm_debug("Could not read sequence number from series file %s: %s", + series_file, pcmk_rc_str(rc)); + fclose(fp); + return rc; + } + fclose(fp); + crm_trace("Found last sequence number %u in series file %s", + *seq, series_file); + free(series_file); + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Write sequence number to a file series' .last file + * + * \param[in] directory Directory that contains the file series + * \param[in] series Start of file name + * \param[in] sequence Sequence number to write + * \param[in] max Maximum sequence value, after which it is reset to 0 + * + * \note This function logs some errors but does not return any to the caller + */ +void +pcmk__write_series_sequence(const char *directory, const char *series, + unsigned int sequence, int max) +{ + int rc = 0; + FILE *file_strm = NULL; + char *series_file = NULL; + + CRM_CHECK(directory != NULL, return); + CRM_CHECK(series != NULL, return); + + if (max == 0) { + return; + } + if (max > 0 && sequence >= max) { + sequence = 0; + } + + series_file = crm_strdup_printf("%s/%s.last", directory, series); + file_strm = fopen(series_file, "w"); + if (file_strm != NULL) { + rc = fprintf(file_strm, "%u", sequence); + if (rc < 0) { + crm_perror(LOG_ERR, "Cannot write to series file %s", series_file); + } + + } else { + crm_err("Cannot open series file %s for writing", series_file); + } + + if (file_strm != NULL) { + fflush(file_strm); + fclose(file_strm); + } + + crm_trace("Wrote %d to %s", sequence, series_file); + free(series_file); +} + +/*! + * \internal + * \brief Change the owner and group of a file series' .last file + * + * \param[in] directory Directory that contains series + * \param[in] series Series to change + * \param[in] uid User ID of desired file owner + * \param[in] gid Group ID of desired file group + * + * \return Standard Pacemaker return code + * \note The caller must have the appropriate privileges. + */ +int +pcmk__chown_series_sequence(const char *directory, const char *series, + uid_t uid, gid_t gid) +{ + char *series_file = NULL; + int rc = pcmk_rc_ok; + + if ((directory == NULL) || (series == NULL)) { + return EINVAL; + } + series_file = crm_strdup_printf("%s/%s.last", directory, series); + if (chown(series_file, uid, gid) < 0) { + rc = errno; + } + free(series_file); + return rc; +} + +static bool +pcmk__daemon_user_can_write(const char *target_name, struct stat *target_stat) +{ + struct passwd *sys_user = NULL; + + errno = 0; + sys_user = getpwnam(CRM_DAEMON_USER); + if (sys_user == NULL) { + crm_notice("Could not find user %s: %s", + CRM_DAEMON_USER, pcmk_rc_str(errno)); + return FALSE; + } + if (target_stat->st_uid != sys_user->pw_uid) { + crm_notice("%s is not owned by user %s " CRM_XS " uid %d != %d", + target_name, CRM_DAEMON_USER, sys_user->pw_uid, + target_stat->st_uid); + return FALSE; + } + if ((target_stat->st_mode & (S_IRUSR | S_IWUSR)) == 0) { + crm_notice("%s is not readable and writable by user %s " + CRM_XS " st_mode=0%lo", + target_name, CRM_DAEMON_USER, + (unsigned long) target_stat->st_mode); + return FALSE; + } + return TRUE; +} + +static bool +pcmk__daemon_group_can_write(const char *target_name, struct stat *target_stat) +{ + struct group *sys_grp = NULL; + + errno = 0; + sys_grp = getgrnam(CRM_DAEMON_GROUP); + if (sys_grp == NULL) { + crm_notice("Could not find group %s: %s", + CRM_DAEMON_GROUP, pcmk_rc_str(errno)); + return FALSE; + } + + if (target_stat->st_gid != sys_grp->gr_gid) { + crm_notice("%s is not owned by group %s " CRM_XS " uid %d != %d", + target_name, CRM_DAEMON_GROUP, + sys_grp->gr_gid, target_stat->st_gid); + return FALSE; + } + + if ((target_stat->st_mode & (S_IRGRP | S_IWGRP)) == 0) { + crm_notice("%s is not readable and writable by group %s " + CRM_XS " st_mode=0%lo", + target_name, CRM_DAEMON_GROUP, + (unsigned long) target_stat->st_mode); + return FALSE; + } + return TRUE; +} + +/*! + * \internal + * \brief Check whether a directory or file is writable by the cluster daemon + * + * Return true if either the cluster daemon user or cluster daemon group has + * write permission on a specified file or directory. + * + * \param[in] dir Directory to check (this argument must be specified, and + * the directory must exist) + * \param[in] file File to check (only the directory will be checked if this + * argument is not specified or the file does not exist) + * + * \return true if target is writable by cluster daemon, false otherwise + */ +bool +pcmk__daemon_can_write(const char *dir, const char *file) +{ + int s_res = 0; + struct stat buf; + char *full_file = NULL; + const char *target = NULL; + + // Caller must supply directory + CRM_ASSERT(dir != NULL); + + // If file is given, check whether it exists as a regular file + if (file != NULL) { + full_file = crm_strdup_printf("%s/%s", dir, file); + target = full_file; + + s_res = stat(full_file, &buf); + if (s_res < 0) { + crm_notice("%s not found: %s", target, pcmk_rc_str(errno)); + free(full_file); + full_file = NULL; + target = NULL; + + } else if (S_ISREG(buf.st_mode) == FALSE) { + crm_err("%s must be a regular file " CRM_XS " st_mode=0%lo", + target, (unsigned long) buf.st_mode); + free(full_file); + return false; + } + } + + // If file is not given, ensure dir exists as directory + if (target == NULL) { + target = dir; + s_res = stat(dir, &buf); + if (s_res < 0) { + crm_err("%s not found: %s", dir, pcmk_rc_str(errno)); + return false; + + } else if (S_ISDIR(buf.st_mode) == FALSE) { + crm_err("%s must be a directory " CRM_XS " st_mode=0%lo", + dir, (unsigned long) buf.st_mode); + return false; + } + } + + if (!pcmk__daemon_user_can_write(target, &buf) + && !pcmk__daemon_group_can_write(target, &buf)) { + + crm_err("%s must be owned and writable by either user %s or group %s " + CRM_XS " st_mode=0%lo", + target, CRM_DAEMON_USER, CRM_DAEMON_GROUP, + (unsigned long) buf.st_mode); + free(full_file); + return false; + } + + free(full_file); + return true; +} + +/*! + * \internal + * \brief Flush and sync a directory to disk + * + * \param[in] name Directory to flush and sync + * \note This function logs errors but does not return them to the caller + */ +void +pcmk__sync_directory(const char *name) +{ + int fd; + DIR *directory; + + directory = opendir(name); + if (directory == NULL) { + crm_perror(LOG_ERR, "Could not open %s for syncing", name); + return; + } + + fd = dirfd(directory); + if (fd < 0) { + crm_perror(LOG_ERR, "Could not obtain file descriptor for %s", name); + return; + } + + if (fsync(fd) < 0) { + crm_perror(LOG_ERR, "Could not sync %s", name); + } + if (closedir(directory) < 0) { + crm_perror(LOG_ERR, "Could not close %s after fsync", name); + } +} + +/*! + * \internal + * \brief Read the contents of a file + * + * \param[in] filename Name of file to read + * \param[out] contents Where to store file contents + * + * \return Standard Pacemaker return code + * \note On success, the caller is responsible for freeing contents. + */ +int +pcmk__file_contents(const char *filename, char **contents) +{ + FILE *fp; + int length, read_len; + int rc = pcmk_rc_ok; + + if ((filename == NULL) || (contents == NULL)) { + return EINVAL; + } + + fp = fopen(filename, "r"); + if ((fp == NULL) || (fseek(fp, 0L, SEEK_END) < 0)) { + rc = errno; + goto bail; + } + + length = ftell(fp); + if (length < 0) { + rc = errno; + goto bail; + } + + if (length == 0) { + *contents = NULL; + } else { + *contents = calloc(length + 1, sizeof(char)); + if (*contents == NULL) { + rc = errno; + goto bail; + } + rewind(fp); + read_len = fread(*contents, 1, length, fp); /* Coverity: False positive */ + if (read_len != length) { + free(*contents); + *contents = NULL; + rc = EIO; + } + } + +bail: + if (fp != NULL) { + fclose(fp); + } + return rc; +} + +/*! + * \internal + * \brief Write text to a file, flush and sync it to disk, then close the file + * + * \param[in] fd File descriptor opened for writing + * \param[in] contents String to write to file + * + * \return Standard Pacemaker return code + */ +int +pcmk__write_sync(int fd, const char *contents) +{ + int rc = 0; + FILE *fp = fdopen(fd, "w"); + + if (fp == NULL) { + return errno; + } + if ((contents != NULL) && (fprintf(fp, "%s", contents) < 0)) { + rc = EIO; + } + if (fflush(fp) != 0) { + rc = errno; + } + if (fsync(fileno(fp)) < 0) { + rc = errno; + } + fclose(fp); + return rc; +} + +/*! + * \internal + * \brief Set a file descriptor to non-blocking + * + * \param[in] fd File descriptor to use + * + * \return Standard Pacemaker return code + */ +int +pcmk__set_nonblocking(int fd) +{ + int flag = fcntl(fd, F_GETFL); + + if (flag < 0) { + return errno; + } + if (fcntl(fd, F_SETFL, flag | O_NONBLOCK) < 0) { + return errno; + } + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Get directory name for temporary files + * + * Return the value of the TMPDIR environment variable if it is set to a + * full path, otherwise return "/tmp". + * + * \return Name of directory to be used for temporary files + */ +const char * +pcmk__get_tmpdir(void) +{ + const char *dir = getenv("TMPDIR"); + + return (dir && (*dir == '/'))? dir : "/tmp"; +} + +/*! + * \internal + * \brief Close open file descriptors + * + * Close all file descriptors (except optionally stdin, stdout, and stderr), + * which is a best practice for a new child process forked for the purpose of + * executing an external program. + * + * \param[in] bool If true, close stdin, stdout, and stderr as well + */ +void +pcmk__close_fds_in_child(bool all) +{ + DIR *dir; + struct rlimit rlim; + rlim_t max_fd; + int min_fd = (all? 0 : (STDERR_FILENO + 1)); + + /* Find the current process's (soft) limit for open files. getrlimit() + * should always work, but have a fallback just in case. + */ + if (getrlimit(RLIMIT_NOFILE, &rlim) == 0) { + max_fd = rlim.rlim_cur - 1; + } else { + long conf_max = sysconf(_SC_OPEN_MAX); + + max_fd = (conf_max > 0)? conf_max : 1024; + } + + /* /proc/self/fd (on Linux) or /dev/fd (on most OSes) contains symlinks to + * all open files for the current process, named as the file descriptor. + * Use this if available, because it's more efficient than a shotgun + * approach to closing descriptors. + */ +#if HAVE_LINUX_PROCFS + dir = opendir("/proc/self/fd"); + if (dir == NULL) { + dir = opendir("/dev/fd"); + } +#else + dir = opendir("/dev/fd"); +#endif // HAVE_LINUX_PROCFS + if (dir != NULL) { + struct dirent *entry; + int dir_fd = dirfd(dir); + + while ((entry = readdir(dir)) != NULL) { + int lpc = atoi(entry->d_name); + + /* How could one of these entries be higher than max_fd, you ask? + * It isn't possible in normal operation, but when run under + * valgrind, valgrind can open high-numbered file descriptors for + * its own use that are higher than the process's soft limit. + * These will show up in the fd directory but aren't closable. + */ + if ((lpc >= min_fd) && (lpc <= max_fd) && (lpc != dir_fd)) { + close(lpc); + } + } + closedir(dir); + return; + } + + /* If no fd directory is available, iterate over all possible descriptors. + * This is less efficient due to the overhead of many system calls. + */ + for (int lpc = max_fd; lpc >= min_fd; lpc--) { + close(lpc); + } +} + +/*! + * \brief Duplicate a file path, inserting a prefix if not absolute + * + * \param[in] filename File path to duplicate + * \param[in] dirname If filename is not absolute, prefix to add + * + * \return Newly allocated memory with full path (guaranteed non-NULL) + */ +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 path; +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include <crm/common/util_compat.h> + +void +crm_build_path(const char *path_c, mode_t mode) +{ + int rc = pcmk__build_path(path_c, mode); + + if (rc != pcmk_rc_ok) { + crm_err("Could not create directory '%s': %s", + path_c, pcmk_rc_str(rc)); + } +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/ipc_attrd.c b/lib/common/ipc_attrd.c new file mode 100644 index 0000000..7c40aa7 --- /dev/null +++ b/lib/common/ipc_attrd.c @@ -0,0 +1,590 @@ +/* + * Copyright 2011-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include <crm_internal.h> + +#include <stdio.h> + +#include <crm/crm.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 "crmcommon_private.h" + +static void +set_pairs_data(pcmk__attrd_api_reply_t *data, xmlNode *msg_data) +{ + const char *name = NULL; + pcmk__attrd_query_pair_t *pair; + + 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)); + + CRM_ASSERT(pair != NULL); + + pair->node = crm_element_value(node, PCMK__XA_ATTR_NODE_NAME); + pair->name = name; + pair->value = crm_element_value(node, PCMK__XA_ATTR_VALUE); + data->data.pairs = g_list_prepend(data->data.pairs, pair); + } +} + +static bool +reply_expected(pcmk_ipc_api_t *api, xmlNode *request) +{ + const char *command = crm_element_value(request, PCMK__XA_TASK); + + return pcmk__str_any_of(command, + PCMK__ATTRD_CMD_CLEAR_FAILURE, + PCMK__ATTRD_CMD_QUERY, + PCMK__ATTRD_CMD_REFRESH, + PCMK__ATTRD_CMD_UPDATE, + PCMK__ATTRD_CMD_UPDATE_BOTH, + PCMK__ATTRD_CMD_UPDATE_DELAY, + NULL); +} + +static bool +dispatch(pcmk_ipc_api_t *api, xmlNode *reply) +{ + const char *value = NULL; + crm_exit_t status = CRM_EX_OK; + + pcmk__attrd_api_reply_t reply_data = { + pcmk__attrd_reply_unknown + }; + + if (pcmk__str_eq((const char *) reply->name, "ack", pcmk__str_none)) { + return false; + } + + /* Do some basic validation of the reply */ + value = crm_element_value(reply, F_TYPE); + if (pcmk__str_empty(value) + || !pcmk__str_eq(value, T_ATTRD, pcmk__str_none)) { + crm_info("Unrecognizable message from attribute manager: " + "message type '%s' not '" T_ATTRD "'", pcmk__s(value, "")); + status = CRM_EX_PROTOCOL; + goto done; + } + + value = crm_element_value(reply, F_SUBTYPE); + + /* 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. + */ + if (pcmk__str_eq(value, PCMK__ATTRD_CMD_QUERY, pcmk__str_null_matches)) { + if (!xmlHasProp(reply, (pcmkXmlStr) PCMK__XA_ATTR_NAME)) { + status = ENXIO; // Most likely, the attribute doesn't exist + goto done; + } + reply_data.reply_type = pcmk__attrd_reply_query; + set_pairs_data(&reply_data, reply); + + } else { + crm_info("Unrecognizable message from attribute manager: " + "message subtype '%s' unknown", pcmk__s(value, "")); + status = CRM_EX_PROTOCOL; + goto done; + } + +done: + pcmk__call_ipc_callback(api, pcmk_ipc_event_reply, status, &reply_data); + + /* Free any reply data that was allocated */ + if (reply_data.data.pairs) { + g_list_free_full(reply_data.data.pairs, free); + } + + return false; +} + +pcmk__ipc_methods_t * +pcmk__attrd_api_methods(void) +{ + pcmk__ipc_methods_t *cmds = calloc(1, sizeof(pcmk__ipc_methods_t)); + + if (cmds != NULL) { + cmds->new_data = NULL; + cmds->free_data = NULL; + cmds->post_connect = NULL; + cmds->reply_expected = reply_expected; + cmds->dispatch = dispatch; + } + return cmds; +} + +/*! + * \internal + * \brief Create a generic pacemaker-attrd operation + * + * \param[in] user_name If not NULL, ACL user to set for operation + * + * \return XML of pacemaker-attrd operation + */ +static xmlNode * +create_attrd_op(const char *user_name) +{ + xmlNode *attrd_op = create_xml_node(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_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, xmlNode *request) +{ + int rc = pcmk_rc_ok; + int max = 5; + + while (max > 0) { + crm_info("Connecting to cluster... %d retries remaining", max); + rc = pcmk_connect_ipc(api, pcmk_ipc_dispatch_sync); + + if (rc == pcmk_rc_ok) { + rc = pcmk__send_ipc_request(api, request); + break; + } else if (rc == EAGAIN || rc == EALREADY) { + sleep(5 - max); + max--; + } else { + crm_err("Could not connect to attrd: %s", pcmk_rc_str(rc)); + break; + } + } + + return rc; +} + +static int +send_attrd_request(pcmk_ipc_api_t *api, xmlNode *request) +{ + return pcmk__send_ipc_request(api, request); +} + +int +pcmk__attrd_api_clear_failures(pcmk_ipc_api_t *api, const char *node, + const char *resource, const char *operation, + const char *interval_spec, const char *user_name, + uint32_t options) +{ + int rc = pcmk_rc_ok; + xmlNode *request = create_attrd_op(user_name); + const char *interval_desc = NULL; + const char *op_desc = NULL; + const char *target = pcmk__node_attr_target(node); + + if (target != NULL) { + 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"; + op_desc = operation; + } else { + interval_desc = "all"; + op_desc = "operations"; + } + + 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); + + return rc; +} + +int +pcmk__attrd_api_delete(pcmk_ipc_api_t *api, const char *node, const char *name, + uint32_t options) +{ + const char *target = NULL; + + if (name == NULL) { + return EINVAL; + } + + target = pcmk__node_attr_target(node); + + if (target != NULL) { + node = target; + } + + /* Make sure the right update option is set. */ + options &= ~pcmk__node_attr_delay; + options |= pcmk__node_attr_value; + + return pcmk__attrd_api_update(api, node, name, NULL, NULL, NULL, NULL, options); +} + +int +pcmk__attrd_api_purge(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; + } + + request = create_attrd_op(NULL); + + crm_xml_add(request, PCMK__XA_TASK, PCMK__ATTRD_CMD_PEER_REMOVE); + 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); + } + + free_xml(request); + + crm_debug("Asked pacemaker-attrd to purge %s: %s (%d)", + display_host, pcmk_rc_str(rc), rc); + + return rc; +} + +int +pcmk__attrd_api_query(pcmk_ipc_api_t *api, const char *node, const char *name, + uint32_t options) +{ + int rc = pcmk_rc_ok; + xmlNode *request = NULL; + const char *target = NULL; + + if (name == NULL) { + return EINVAL; + } + + if (pcmk_is_set(options, pcmk__node_attr_query_all)) { + node = NULL; + } else { + target = pcmk__node_attr_target(node); + + if (target != NULL) { + node = target; + } + } + + 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); + pcmk__xe_add_node(request, node, 0); + + rc = 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; +} + +int +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; + } + + request = create_attrd_op(NULL); + + 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); + } + + free_xml(request); + + crm_debug("Asked pacemaker-attrd to refresh %s: %s (%d)", + display_host, pcmk_rc_str(rc), rc); + + return rc; +} + +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); + } else if (pcmk_is_set(options, pcmk__node_attr_value)) { + 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); + } +} + +static void +populate_update_op(xmlNode *op, const char *node, const char *name, const char *value, + 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); + } 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); + } else { + crm_xml_add(op, PCMK__XA_ATTR_SET_TYPE, XML_TAG_ATTR_SETS); + } + + add_op_attr(op, options); + + crm_xml_add(op, PCMK__XA_ATTR_VALUE, value); + crm_xml_add(op, PCMK__XA_ATTR_DAMPENING, dampen); + pcmk__xe_add_node(op, node, 0); + crm_xml_add(op, PCMK__XA_ATTR_SET, set); + crm_xml_add_int(op, PCMK__XA_ATTR_IS_REMOTE, + pcmk_is_set(options, pcmk__node_attr_remote)); + crm_xml_add_int(op, PCMK__XA_ATTR_IS_PRIVATE, + pcmk_is_set(options, pcmk__node_attr_private)); + + if (pcmk_is_set(options, pcmk__node_attr_sync_local)) { + crm_xml_add(op, PCMK__XA_ATTR_SYNC_POINT, PCMK__VALUE_LOCAL); + } else if (pcmk_is_set(options, pcmk__node_attr_sync_cluster)) { + crm_xml_add(op, PCMK__XA_ATTR_SYNC_POINT, PCMK__VALUE_CLUSTER); + } +} + +int +pcmk__attrd_api_update(pcmk_ipc_api_t *api, const char *node, const char *name, + const char *value, const char *dampen, const char *set, + const char *user_name, uint32_t options) +{ + int rc = pcmk_rc_ok; + xmlNode *request = NULL; + const char *display_host = (node ? node : "localhost"); + const char *target = NULL; + + if (name == NULL) { + return EINVAL; + } + + target = pcmk__node_attr_target(node); + + if (target != NULL) { + node = target; + } + + 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); + } + + 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; +} + +int +pcmk__attrd_api_update_list(pcmk_ipc_api_t *api, GList *attrs, const char *dampen, + const char *set, const char *user_name, + uint32_t options) +{ + int rc = pcmk_rc_ok; + xmlNode *request = NULL; + + if (attrs == NULL) { + return EINVAL; + } + + /* There are two different ways of handling a list of attributes: + * + * (1) For messages originating from some command line tool, we have to send + * them one at a time. In this loop, we just call pcmk__attrd_api_update + * for each, letting it deal with creating the API object if it doesn't + * already exist. + * + * The reason we can't use a single message in this case is that we can't + * trust that the server supports it. Remote nodes could be involved + * here, and there's no guarantee that a newer client running on a remote + * node is talking to (or proxied through) a cluster node with a newer + * attrd. We also can't just try sending a single message and then falling + * back on multiple. There's no handshake with the attrd server to + * determine its version. And then we would need to do that fallback in the + * dispatch function for this to work for all connection types (mainloop in + * particular), and at that point we won't know what the original message + * was in order to break it apart and resend as individual messages. + * + * (2) For messages between daemons, we can be assured that the local attrd + * will support the new message and that it can send to the other attrds + * as one request or split up according to the minimum supported version. + */ + for (GList *iter = attrs; iter != NULL; iter = iter->next) { + pcmk__attrd_query_pair_t *pair = (pcmk__attrd_query_pair_t *) iter->data; + + if (pcmk__is_daemon) { + const char *target = NULL; + xmlNode *child = NULL; + + /* First time through this loop - create the basic request. */ + if (request == NULL) { + request = create_attrd_op(user_name); + add_op_attr(request, options); + } + + /* Add a child node for this operation. We add the task to the top + * level XML node so attrd_ipc_dispatch doesn't need changes. And + * 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); + target = pcmk__node_attr_target(pair->node); + + if (target != NULL) { + pair->node = target; + } + + populate_update_op(child, pair->node, pair->name, pair->value, dampen, + set, options); + } else { + rc = pcmk__attrd_api_update(api, pair->node, pair->name, pair->value, + dampen, set, user_name, options); + } + } + + /* If we were doing multiple attributes at once, we still need to send the + * 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 new file mode 100644 index 0000000..c6d1645 --- /dev/null +++ b/lib/common/ipc_client.c @@ -0,0 +1,1576 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#if defined(HAVE_UCRED) || defined(HAVE_SOCKPEERCRED) +# ifdef HAVE_UCRED +# ifndef _GNU_SOURCE +# define _GNU_SOURCE +# endif +# endif +# include <sys/socket.h> +#elif defined(HAVE_GETPEERUCRED) +# include <ucred.h> +#endif + +#include <stdio.h> +#include <sys/types.h> +#include <errno.h> +#include <bzlib.h> + +#include <crm/crm.h> /* indirectly: pcmk_err_generic */ +#include <crm/msg_xml.h> +#include <crm/common/ipc.h> +#include <crm/common/ipc_internal.h> +#include "crmcommon_private.h" + +/*! + * \brief Create a new object for using Pacemaker daemon IPC + * + * \param[out] api Where to store new IPC object + * \param[in] server Which Pacemaker daemon the object is for + * + * \return Standard Pacemaker result code + * + * \note The caller is responsible for freeing *api using pcmk_free_ipc_api(). + * \note This is intended to supersede crm_ipc_new() but currently only + * supports the controller, pacemakerd, and schedulerd IPC API. + */ +int +pcmk_new_ipc_api(pcmk_ipc_api_t **api, enum pcmk_ipc_server server) +{ + if (api == NULL) { + return EINVAL; + } + + *api = calloc(1, sizeof(pcmk_ipc_api_t)); + if (*api == NULL) { + return errno; + } + + (*api)->server = server; + if (pcmk_ipc_name(*api, false) == NULL) { + pcmk_free_ipc_api(*api); + *api = NULL; + return EOPNOTSUPP; + } + + (*api)->ipc_size_max = 0; + + // Set server methods and max_size (if not default) + switch (server) { + case pcmk_ipc_attrd: + (*api)->cmds = pcmk__attrd_api_methods(); + break; + + case pcmk_ipc_based: + (*api)->ipc_size_max = 512 * 1024; // 512KB + break; + + case pcmk_ipc_controld: + (*api)->cmds = pcmk__controld_api_methods(); + break; + + case pcmk_ipc_execd: + break; + + case pcmk_ipc_fenced: + break; + + case pcmk_ipc_pacemakerd: + (*api)->cmds = pcmk__pacemakerd_api_methods(); + break; + + case pcmk_ipc_schedulerd: + (*api)->cmds = pcmk__schedulerd_api_methods(); + // @TODO max_size could vary by client, maybe take as argument? + (*api)->ipc_size_max = 5 * 1024 * 1024; // 5MB + break; + } + if ((*api)->cmds == NULL) { + pcmk_free_ipc_api(*api); + *api = NULL; + return ENOMEM; + } + + (*api)->ipc = crm_ipc_new(pcmk_ipc_name(*api, false), + (*api)->ipc_size_max); + if ((*api)->ipc == NULL) { + pcmk_free_ipc_api(*api); + *api = NULL; + return ENOMEM; + } + + // If daemon API has its own data to track, allocate it + if ((*api)->cmds->new_data != NULL) { + if ((*api)->cmds->new_data(*api) != pcmk_rc_ok) { + pcmk_free_ipc_api(*api); + *api = NULL; + return ENOMEM; + } + } + crm_trace("Created %s API IPC object", pcmk_ipc_name(*api, true)); + return pcmk_rc_ok; +} + +static void +free_daemon_specific_data(pcmk_ipc_api_t *api) +{ + if ((api != NULL) && (api->cmds != NULL)) { + if ((api->cmds->free_data != NULL) && (api->api_data != NULL)) { + api->cmds->free_data(api->api_data); + api->api_data = NULL; + } + free(api->cmds); + api->cmds = NULL; + } +} + +/*! + * \internal + * \brief Call an IPC API event callback, if one is registed + * + * \param[in,out] api IPC API connection + * \param[in] event_type The type of event that occurred + * \param[in] status Event status + * \param[in,out] event_data Event-specific data + */ +void +pcmk__call_ipc_callback(pcmk_ipc_api_t *api, enum pcmk_ipc_event event_type, + crm_exit_t status, void *event_data) +{ + if ((api != NULL) && (api->cb != NULL)) { + api->cb(api, event_type, status, event_data, api->user_data); + } +} + +/*! + * \internal + * \brief Clean up after an IPC disconnect + * + * \param[in,out] user_data IPC API connection that disconnected + * + * \note This function can be used as a main loop IPC destroy callback. + */ +static void +ipc_post_disconnect(gpointer user_data) +{ + pcmk_ipc_api_t *api = user_data; + + crm_info("Disconnected from %s IPC API", pcmk_ipc_name(api, true)); + + // Perform any daemon-specific handling needed + if ((api->cmds != NULL) && (api->cmds->post_disconnect != NULL)) { + api->cmds->post_disconnect(api); + } + + // Call client's registered event callback + pcmk__call_ipc_callback(api, pcmk_ipc_event_disconnect, CRM_EX_DISCONNECT, + NULL); + + /* If this is being called from a running main loop, mainloop_gio_destroy() + * will free ipc and mainloop_io immediately after calling this function. + * If this is called from a stopped main loop, these will leak, so the best + * practice is to close the connection before stopping the main loop. + */ + api->ipc = NULL; + api->mainloop_io = NULL; + + if (api->free_on_disconnect) { + /* pcmk_free_ipc_api() has already been called, but did not free api + * or api->cmds because this function needed them. Do that now. + */ + free_daemon_specific_data(api); + crm_trace("Freeing IPC API object after disconnect"); + free(api); + } +} + +/*! + * \brief Free the contents of an IPC API object + * + * \param[in,out] api IPC API object to free + */ +void +pcmk_free_ipc_api(pcmk_ipc_api_t *api) +{ + bool free_on_disconnect = false; + + if (api == NULL) { + return; + } + crm_debug("Releasing %s IPC API", pcmk_ipc_name(api, true)); + + if (api->ipc != NULL) { + if (api->mainloop_io != NULL) { + /* We need to keep the api pointer itself around, because it is the + * user data for the IPC client destroy callback. That will be + * triggered by the pcmk_disconnect_ipc() call below, but it might + * happen later in the main loop (if still running). + * + * This flag tells the destroy callback to free the object. It can't + * do that unconditionally, because the application might call this + * function after a disconnect that happened by other means. + */ + free_on_disconnect = api->free_on_disconnect = true; + } + pcmk_disconnect_ipc(api); // Frees api if free_on_disconnect is true + } + if (!free_on_disconnect) { + free_daemon_specific_data(api); + crm_trace("Freeing IPC API object"); + free(api); + } +} + +/*! + * \brief Get the IPC name used with an IPC API connection + * + * \param[in] api IPC API connection + * \param[in] for_log If true, return human-friendly name instead of IPC name + * + * \return IPC API's human-friendly or connection name, or if none is available, + * "Pacemaker" if for_log is true and NULL if for_log is false + */ +const char * +pcmk_ipc_name(const pcmk_ipc_api_t *api, bool for_log) +{ + if (api == NULL) { + return for_log? "Pacemaker" : NULL; + } + switch (api->server) { + case pcmk_ipc_attrd: + return for_log? "attribute manager" : T_ATTRD; + + case pcmk_ipc_based: + return for_log? "CIB manager" : NULL /* PCMK__SERVER_BASED_RW */; + + case pcmk_ipc_controld: + return for_log? "controller" : CRM_SYSTEM_CRMD; + + case pcmk_ipc_execd: + return for_log? "executor" : NULL /* CRM_SYSTEM_LRMD */; + + case pcmk_ipc_fenced: + return for_log? "fencer" : NULL /* "stonith-ng" */; + + case pcmk_ipc_pacemakerd: + return for_log? "launcher" : CRM_SYSTEM_MCP; + + case pcmk_ipc_schedulerd: + return for_log? "scheduler" : CRM_SYSTEM_PENGINE; + + default: + return for_log? "Pacemaker" : NULL; + } +} + +/*! + * \brief Check whether an IPC API connection is active + * + * \param[in,out] api IPC API connection + * + * \return true if IPC is connected, false otherwise + */ +bool +pcmk_ipc_is_connected(pcmk_ipc_api_t *api) +{ + return (api != NULL) && crm_ipc_connected(api->ipc); +} + +/*! + * \internal + * \brief Call the daemon-specific API's dispatch function + * + * Perform daemon-specific handling of IPC reply dispatch. It is the daemon + * method's responsibility to call the client's registered event callback, as + * well as allocate and free any event data. + * + * \param[in,out] api IPC API connection + * \param[in,out] message IPC reply XML to dispatch + */ +static bool +call_api_dispatch(pcmk_ipc_api_t *api, xmlNode *message) +{ + crm_log_xml_trace(message, "ipc-received"); + if ((api->cmds != NULL) && (api->cmds->dispatch != NULL)) { + return api->cmds->dispatch(api, message); + } + + return false; +} + +/*! + * \internal + * \brief Dispatch previously read IPC data + * + * \param[in] buffer Data read from IPC + * \param[in,out] api IPC object + * + * \return Standard Pacemaker return code. In particular: + * + * pcmk_rc_ok: There are no more messages expected from the server. Quit + * reading. + * EINPROGRESS: There are more messages expected from the server. Keep reading. + * + * All other values indicate an error. + */ +static int +dispatch_ipc_data(const char *buffer, pcmk_ipc_api_t *api) +{ + bool more = false; + xmlNode *msg; + + if (buffer == NULL) { + crm_warn("Empty message received from %s IPC", + pcmk_ipc_name(api, true)); + return ENOMSG; + } + + msg = string2xml(buffer); + if (msg == NULL) { + crm_warn("Malformed message received from %s IPC", + pcmk_ipc_name(api, true)); + return EPROTO; + } + + more = call_api_dispatch(api, msg); + free_xml(msg); + + if (more) { + return EINPROGRESS; + } else { + return pcmk_rc_ok; + } +} + +/*! + * \internal + * \brief Dispatch data read from IPC source + * + * \param[in] buffer Data read from IPC + * \param[in] length Number of bytes of data in buffer (ignored) + * \param[in,out] user_data IPC object + * + * \return Always 0 (meaning connection is still required) + * + * \note This function can be used as a main loop IPC dispatch callback. + */ +static int +dispatch_ipc_source_data(const char *buffer, ssize_t length, gpointer user_data) +{ + pcmk_ipc_api_t *api = user_data; + + CRM_CHECK(api != NULL, return 0); + dispatch_ipc_data(buffer, api); + return 0; +} + +/*! + * \brief Check whether an IPC connection has data available (without main loop) + * + * \param[in] api IPC API connection + * \param[in] timeout_ms If less than 0, poll indefinitely; if 0, poll once + * and return immediately; otherwise, poll for up to + * this many milliseconds + * + * \return Standard Pacemaker return code + * + * \note Callers of pcmk_connect_ipc() using pcmk_ipc_dispatch_poll should call + * this function to check whether IPC data is available. Return values of + * interest include pcmk_rc_ok meaning data is available, and EAGAIN + * meaning no data is available; all other values indicate errors. + * \todo This does not allow the caller to poll multiple file descriptors at + * once. If there is demand for that, we could add a wrapper for + * crm_ipc_get_fd(api->ipc), so the caller can call poll() themselves. + */ +int +pcmk_poll_ipc(const pcmk_ipc_api_t *api, int timeout_ms) +{ + int rc; + struct pollfd pollfd = { 0, }; + + if ((api == NULL) || (api->dispatch_type != pcmk_ipc_dispatch_poll)) { + return EINVAL; + } + pollfd.fd = crm_ipc_get_fd(api->ipc); + pollfd.events = POLLIN; + rc = poll(&pollfd, 1, timeout_ms); + if (rc < 0) { + /* Some UNIX systems return negative and set EAGAIN for failure to + * allocate memory; standardize the return code in that case + */ + return (errno == EAGAIN)? ENOMEM : errno; + } else if (rc == 0) { + return EAGAIN; + } + return pcmk_rc_ok; +} + +/*! + * \brief Dispatch available messages on an IPC connection (without main loop) + * + * \param[in,out] api IPC API connection + * + * \return Standard Pacemaker return code + * + * \note Callers of pcmk_connect_ipc() using pcmk_ipc_dispatch_poll should call + * this function when IPC data is available. + */ +void +pcmk_dispatch_ipc(pcmk_ipc_api_t *api) +{ + if (api == NULL) { + return; + } + while (crm_ipc_ready(api->ipc) > 0) { + if (crm_ipc_read(api->ipc) > 0) { + dispatch_ipc_data(crm_ipc_buffer(api->ipc), api); + } + } +} + +// \return Standard Pacemaker return code +static int +connect_with_main_loop(pcmk_ipc_api_t *api) +{ + int rc; + + struct ipc_client_callbacks callbacks = { + .dispatch = dispatch_ipc_source_data, + .destroy = ipc_post_disconnect, + }; + + rc = pcmk__add_mainloop_ipc(api->ipc, G_PRIORITY_DEFAULT, api, + &callbacks, &(api->mainloop_io)); + if (rc != pcmk_rc_ok) { + return rc; + } + crm_debug("Connected to %s IPC (attached to main loop)", + pcmk_ipc_name(api, true)); + /* After this point, api->mainloop_io owns api->ipc, so api->ipc + * should not be explicitly freed. + */ + return pcmk_rc_ok; +} + +// \return Standard Pacemaker return code +static int +connect_without_main_loop(pcmk_ipc_api_t *api) +{ + int rc; + + if (!crm_ipc_connect(api->ipc)) { + rc = errno; + crm_ipc_close(api->ipc); + return rc; + } + crm_debug("Connected to %s IPC (without main loop)", + pcmk_ipc_name(api, true)); + return pcmk_rc_ok; +} + +/*! + * \brief Connect to a Pacemaker daemon via IPC + * + * \param[in,out] api IPC API instance + * \param[in] dispatch_type How IPC replies should be dispatched + * + * \return Standard Pacemaker return code + */ +int +pcmk_connect_ipc(pcmk_ipc_api_t *api, enum pcmk_ipc_dispatch dispatch_type) +{ + const int n_attempts = 2; + int rc = pcmk_rc_ok; + + if (api == NULL) { + crm_err("Cannot connect to uninitialized API object"); + return EINVAL; + } + + if (api->ipc == NULL) { + api->ipc = crm_ipc_new(pcmk_ipc_name(api, false), + api->ipc_size_max); + if (api->ipc == NULL) { + crm_err("Failed to re-create IPC API"); + return ENOMEM; + } + } + + if (crm_ipc_connected(api->ipc)) { + crm_trace("Already connected to %s IPC API", pcmk_ipc_name(api, true)); + return pcmk_rc_ok; + } + + api->dispatch_type = dispatch_type; + + for (int i = 0; i < n_attempts; i++) { + switch (dispatch_type) { + case pcmk_ipc_dispatch_main: + rc = connect_with_main_loop(api); + break; + + case pcmk_ipc_dispatch_sync: + case pcmk_ipc_dispatch_poll: + rc = connect_without_main_loop(api); + break; + } + + if (rc != EAGAIN) { + break; + } + + /* EAGAIN may occur due to interruption by a signal or due to some + * transient issue. Try one more time to be more resilient. + */ + if (i < (n_attempts - 1)) { + crm_trace("Connection to %s IPC API failed with EAGAIN, retrying", + pcmk_ipc_name(api, true)); + } + } + + if (rc != pcmk_rc_ok) { + return rc; + } + + if ((api->cmds != NULL) && (api->cmds->post_connect != NULL)) { + rc = api->cmds->post_connect(api); + if (rc != pcmk_rc_ok) { + crm_ipc_close(api->ipc); + } + } + return rc; +} + +/*! + * \brief Disconnect an IPC API instance + * + * \param[in,out] api IPC API connection + * + * \return Standard Pacemaker return code + * + * \note If the connection is attached to a main loop, this function should be + * called before quitting the main loop, to ensure that all memory is + * freed. + */ +void +pcmk_disconnect_ipc(pcmk_ipc_api_t *api) +{ + if ((api == NULL) || (api->ipc == NULL)) { + return; + } + switch (api->dispatch_type) { + case pcmk_ipc_dispatch_main: + { + mainloop_io_t *mainloop_io = api->mainloop_io; + + // Make sure no code with access to api can use these again + api->mainloop_io = NULL; + api->ipc = NULL; + + mainloop_del_ipc_client(mainloop_io); + // After this point api might have already been freed + } + break; + + case pcmk_ipc_dispatch_poll: + case pcmk_ipc_dispatch_sync: + { + crm_ipc_t *ipc = api->ipc; + + // Make sure no code with access to api can use ipc again + api->ipc = NULL; + + // This should always be the case already, but to be safe + api->free_on_disconnect = false; + + crm_ipc_close(ipc); + crm_ipc_destroy(ipc); + ipc_post_disconnect(api); + } + break; + } +} + +/*! + * \brief Register a callback for IPC API events + * + * \param[in,out] api IPC API connection + * \param[in] callback Callback to register + * \param[in] userdata Caller data to pass to callback + * + * \note This function may be called multiple times to update the callback + * and/or user data. The caller remains responsible for freeing + * userdata in any case (after the IPC is disconnected, if the + * user data is still registered with the IPC). + */ +void +pcmk_register_ipc_callback(pcmk_ipc_api_t *api, pcmk_ipc_callback_t cb, + void *user_data) +{ + if (api == NULL) { + return; + } + api->cb = cb; + api->user_data = user_data; +} + +/*! + * \internal + * \brief Send an XML request across an IPC API connection + * + * \param[in,out] api IPC API connection + * \param[in,out] request XML request to send + * + * \return Standard Pacemaker return code + * + * \note Daemon-specific IPC API functions should call this function to send + * requests, because it handles different dispatch types appropriately. + */ +int +pcmk__send_ipc_request(pcmk_ipc_api_t *api, xmlNode *request) +{ + int rc; + xmlNode *reply = NULL; + enum crm_ipc_flags flags = crm_ipc_flags_none; + + if ((api == NULL) || (api->ipc == NULL) || (request == NULL)) { + return EINVAL; + } + crm_log_xml_trace(request, "ipc-sent"); + + // Synchronous dispatch requires waiting for a reply + if ((api->dispatch_type == pcmk_ipc_dispatch_sync) + && (api->cmds != NULL) + && (api->cmds->reply_expected != NULL) + && (api->cmds->reply_expected(api, request))) { + flags = crm_ipc_client_response; + } + + // The 0 here means a default timeout of 5 seconds + rc = crm_ipc_send(api->ipc, request, flags, 0, &reply); + + if (rc < 0) { + return pcmk_legacy2rc(rc); + } else if (rc == 0) { + return ENODATA; + } + + // With synchronous dispatch, we dispatch any reply now + if (reply != NULL) { + bool more = call_api_dispatch(api, reply); + + free_xml(reply); + + while (more) { + rc = crm_ipc_read(api->ipc); + + if (rc == -EAGAIN) { + continue; + } else if (rc == -ENOMSG || rc == pcmk_ok) { + return pcmk_rc_ok; + } else if (rc < 0) { + return -rc; + } + + rc = dispatch_ipc_data(crm_ipc_buffer(api->ipc), api); + + if (rc == pcmk_rc_ok) { + more = false; + } else if (rc == EINPROGRESS) { + more = true; + } else { + continue; + } + } + } + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Create the XML for an IPC request to purge a node from the peer cache + * + * \param[in] api IPC API connection + * \param[in] node_name If not NULL, name of node to purge + * \param[in] nodeid If not 0, node ID of node to purge + * + * \return Newly allocated IPC request XML + * + * \note The controller, fencer, and pacemakerd use the same request syntax, but + * the attribute manager uses a different one. The CIB manager doesn't + * have any syntax for it. The executor and scheduler don't connect to the + * cluster layer and thus don't have or need any syntax for it. + * + * \todo Modify the attribute manager to accept the common syntax (as well + * as its current one, for compatibility with older clients). Modify + * the CIB manager to accept and honor the common syntax. Modify the + * executor and scheduler to accept the syntax (immediately returning + * success), just for consistency. Modify this function to use the + * common syntax with all daemons if their version supports it. + */ +static xmlNode * +create_purge_node_request(const pcmk_ipc_api_t *api, const char *node_name, + uint32_t nodeid) +{ + xmlNode *request = NULL; + const char *client = crm_system_name? crm_system_name : "client"; + + 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); + pcmk__xe_add_node(request, node_name, nodeid); + break; + + case pcmk_ipc_controld: + case pcmk_ipc_fenced: + case pcmk_ipc_pacemakerd: + request = create_request(CRM_OP_RM_NODE_CACHE, NULL, NULL, + pcmk_ipc_name(api, false), client, NULL); + if (nodeid > 0) { + crm_xml_set_id(request, "%lu", (unsigned long) nodeid); + } + crm_xml_add(request, XML_ATTR_UNAME, node_name); + break; + + case pcmk_ipc_based: + case pcmk_ipc_execd: + case pcmk_ipc_schedulerd: + break; + } + return request; +} + +/*! + * \brief Ask a Pacemaker daemon to purge a node from its peer cache + * + * \param[in,out] api IPC API connection + * \param[in] node_name If not NULL, name of node to purge + * \param[in] nodeid If not 0, node ID of node to purge + * + * \return Standard Pacemaker return code + * + * \note At least one of node_name or nodeid must be specified. + */ +int +pcmk_ipc_purge_node(pcmk_ipc_api_t *api, const char *node_name, uint32_t nodeid) +{ + int rc = 0; + xmlNode *request = NULL; + + if (api == NULL) { + return EINVAL; + } + if ((node_name == NULL) && (nodeid == 0)) { + return EINVAL; + } + + request = create_purge_node_request(api, node_name, nodeid); + if (request == NULL) { + return EOPNOTSUPP; + } + rc = pcmk__send_ipc_request(api, request); + free_xml(request); + + crm_debug("%s peer cache purge of node %s[%lu]: rc=%d", + pcmk_ipc_name(api, true), node_name, (unsigned long) nodeid, rc); + return rc; +} + +/* + * Generic IPC API (to eventually be deprecated as public API and made internal) + */ + +struct crm_ipc_s { + struct pollfd pfd; + unsigned int max_buf_size; // maximum bytes we can send or receive over IPC + unsigned int buf_size; // size of allocated buffer + int msg_size; + int need_reply; + char *buffer; + char *server_name; // server IPC name being connected to + qb_ipcc_connection_t *ipc; +}; + +/*! + * \brief Create a new (legacy) object for using Pacemaker daemon IPC + * + * \param[in] name IPC system name to connect to + * \param[in] max_size Use a maximum IPC buffer size of at least this size + * + * \return Newly allocated IPC object on success, NULL otherwise + * + * \note The caller is responsible for freeing the result using + * crm_ipc_destroy(). + * \note This should be considered deprecated for use with daemons supported by + * pcmk_new_ipc_api(). + */ +crm_ipc_t * +crm_ipc_new(const char *name, size_t max_size) +{ + crm_ipc_t *client = NULL; + + client = calloc(1, sizeof(crm_ipc_t)); + if (client == NULL) { + crm_err("Could not create IPC connection: %s", strerror(errno)); + return NULL; + } + + client->server_name = strdup(name); + if (client->server_name == NULL) { + crm_err("Could not create %s IPC connection: %s", + name, strerror(errno)); + free(client); + return NULL; + } + client->buf_size = pcmk__ipc_buffer_size(max_size); + client->buffer = malloc(client->buf_size); + if (client->buffer == NULL) { + crm_err("Could not create %s IPC connection: %s", + name, strerror(errno)); + free(client->server_name); + free(client); + return NULL; + } + + /* Clients initiating connection pick the max buf size */ + client->max_buf_size = client->buf_size; + + client->pfd.fd = -1; + client->pfd.events = POLLIN; + client->pfd.revents = 0; + + return client; +} + +/*! + * \brief Establish an IPC connection to a Pacemaker component + * + * \param[in,out] client Connection instance obtained from crm_ipc_new() + * + * \return true on success, false otherwise (in which case errno will be set; + * specifically, in case of discovering the remote side is not + * authentic, its value is set to ECONNABORTED). + */ +bool +crm_ipc_connect(crm_ipc_t *client) +{ + uid_t cl_uid = 0; + gid_t cl_gid = 0; + pid_t found_pid = 0; uid_t found_uid = 0; gid_t found_gid = 0; + int rv; + + if (client == NULL) { + errno = EINVAL; + return false; + } + + client->need_reply = FALSE; + client->ipc = qb_ipcc_connect(client->server_name, client->buf_size); + + if (client->ipc == NULL) { + crm_debug("Could not establish %s IPC connection: %s (%d)", + client->server_name, pcmk_rc_str(errno), errno); + return false; + } + + client->pfd.fd = crm_ipc_get_fd(client); + if (client->pfd.fd < 0) { + rv = errno; + /* message already omitted */ + crm_ipc_close(client); + errno = rv; + return false; + } + + rv = pcmk_daemon_user(&cl_uid, &cl_gid); + if (rv < 0) { + /* message already omitted */ + crm_ipc_close(client); + errno = -rv; + return false; + } + + if ((rv = pcmk__crm_ipc_is_authentic_process(client->ipc, client->pfd.fd, cl_uid, cl_gid, + &found_pid, &found_uid, + &found_gid)) == pcmk_rc_ipc_unauthorized) { + crm_err("%s IPC provider authentication failed: process %lld has " + "uid %lld (expected %lld) and gid %lld (expected %lld)", + client->server_name, + (long long) PCMK__SPECIAL_PID_AS_0(found_pid), + (long long) found_uid, (long long) cl_uid, + (long long) found_gid, (long long) cl_gid); + crm_ipc_close(client); + errno = ECONNABORTED; + return false; + + } else if (rv != pcmk_rc_ok) { + crm_perror(LOG_ERR, "Could not verify authenticity of %s IPC provider", + client->server_name); + crm_ipc_close(client); + if (rv > 0) { + errno = rv; + } else { + errno = ENOTCONN; + } + return false; + } + + qb_ipcc_context_set(client->ipc, client); + + client->max_buf_size = qb_ipcc_get_buffer_size(client->ipc); + if (client->max_buf_size > client->buf_size) { + free(client->buffer); + client->buffer = calloc(1, client->max_buf_size); + client->buf_size = client->max_buf_size; + } + return true; +} + +void +crm_ipc_close(crm_ipc_t * client) +{ + if (client) { + if (client->ipc) { + qb_ipcc_connection_t *ipc = client->ipc; + + client->ipc = NULL; + qb_ipcc_disconnect(ipc); + } + } +} + +void +crm_ipc_destroy(crm_ipc_t * client) +{ + if (client) { + if (client->ipc && qb_ipcc_is_connected(client->ipc)) { + crm_notice("Destroying active %s IPC connection", + client->server_name); + /* The next line is basically unsafe + * + * If this connection was attached to mainloop and mainloop is active, + * the 'disconnected' callback will end up back here and we'll end + * up free'ing the memory twice - something that can still happen + * even without this if we destroy a connection and it closes before + * we call exit + */ + /* crm_ipc_close(client); */ + } else { + crm_trace("Destroying inactive %s IPC connection", + client->server_name); + } + free(client->buffer); + free(client->server_name); + free(client); + } +} + +int +crm_ipc_get_fd(crm_ipc_t * client) +{ + int fd = 0; + + if (client && client->ipc && (qb_ipcc_fd_get(client->ipc, &fd) == 0)) { + return fd; + } + errno = EINVAL; + crm_perror(LOG_ERR, "Could not obtain file descriptor for %s IPC", + (client? client->server_name : "unspecified")); + return -errno; +} + +bool +crm_ipc_connected(crm_ipc_t * client) +{ + bool rc = FALSE; + + if (client == NULL) { + crm_trace("No client"); + return FALSE; + + } else if (client->ipc == NULL) { + crm_trace("No connection"); + return FALSE; + + } else if (client->pfd.fd < 0) { + crm_trace("Bad descriptor"); + return FALSE; + } + + rc = qb_ipcc_is_connected(client->ipc); + if (rc == FALSE) { + client->pfd.fd = -EINVAL; + } + return rc; +} + +/*! + * \brief Check whether an IPC connection is ready to be read + * + * \param[in,out] client Connection to check + * + * \return Positive value if ready to be read, 0 if not ready, -errno on error + */ +int +crm_ipc_ready(crm_ipc_t *client) +{ + int rc; + + CRM_ASSERT(client != NULL); + + if (!crm_ipc_connected(client)) { + return -ENOTCONN; + } + + client->pfd.revents = 0; + rc = poll(&(client->pfd), 1, 0); + return (rc < 0)? -errno : rc; +} + +// \return Standard Pacemaker return code +static int +crm_ipc_decompress(crm_ipc_t * client) +{ + pcmk__ipc_header_t *header = (pcmk__ipc_header_t *)(void*)client->buffer; + + if (header->size_compressed) { + int rc = 0; + 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); + + crm_trace("Decompressing message data %u bytes into %u bytes", + header->size_compressed, size_u); + + rc = BZ2_bzBuffToBuffDecompress(uncompressed + sizeof(pcmk__ipc_header_t), &size_u, + client->buffer + sizeof(pcmk__ipc_header_t), header->size_compressed, 1, 0); + + if (rc != BZ_OK) { + crm_err("Decompression failed: %s " CRM_XS " bzerror=%d", + bz2_strerror(rc), rc); + free(uncompressed); + return EILSEQ; + } + + /* + * This assert no longer holds true. For an identical msg, some clients may + * require compression, and others may not. If that same msg (event) is sent + * to multiple clients, it could result in some clients receiving a compressed + * msg even though compression was not explicitly required for them. + * + * CRM_ASSERT((header->size_uncompressed + sizeof(pcmk__ipc_header_t)) >= ipc_buffer_max); + */ + CRM_ASSERT(size_u == header->size_uncompressed); + + memcpy(uncompressed, client->buffer, sizeof(pcmk__ipc_header_t)); /* Preserve the header */ + header = (pcmk__ipc_header_t *)(void*)uncompressed; + + free(client->buffer); + client->buf_size = new_buf_size; + client->buffer = uncompressed; + } + + CRM_ASSERT(client->buffer[sizeof(pcmk__ipc_header_t) + header->size_uncompressed - 1] == 0); + return pcmk_rc_ok; +} + +long +crm_ipc_read(crm_ipc_t * client) +{ + pcmk__ipc_header_t *header = NULL; + + CRM_ASSERT(client != NULL); + CRM_ASSERT(client->ipc != NULL); + CRM_ASSERT(client->buffer != NULL); + + client->buffer[0] = 0; + client->msg_size = qb_ipcc_event_recv(client->ipc, client->buffer, + client->buf_size, 0); + if (client->msg_size >= 0) { + int rc = crm_ipc_decompress(client); + + if (rc != pcmk_rc_ok) { + return pcmk_rc2legacy(rc); + } + + header = (pcmk__ipc_header_t *)(void*)client->buffer; + if (!pcmk__valid_ipc_header(header)) { + return -EBADMSG; + } + + crm_trace("Received %s IPC event %d size=%u rc=%d text='%.100s'", + client->server_name, header->qb.id, header->qb.size, + client->msg_size, + client->buffer + sizeof(pcmk__ipc_header_t)); + + } else { + crm_trace("No message received from %s IPC: %s", + client->server_name, pcmk_strerror(client->msg_size)); + + if (client->msg_size == -EAGAIN) { + return -EAGAIN; + } + } + + if (!crm_ipc_connected(client) || client->msg_size == -ENOTCONN) { + crm_err("Connection to %s IPC failed", client->server_name); + } + + if (header) { + /* Data excluding the header */ + return header->size_uncompressed; + } + return -ENOMSG; +} + +const char * +crm_ipc_buffer(crm_ipc_t * client) +{ + CRM_ASSERT(client != NULL); + return client->buffer + sizeof(pcmk__ipc_header_t); +} + +uint32_t +crm_ipc_buffer_flags(crm_ipc_t * client) +{ + pcmk__ipc_header_t *header = NULL; + + CRM_ASSERT(client != NULL); + if (client->buffer == NULL) { + return 0; + } + + header = (pcmk__ipc_header_t *)(void*)client->buffer; + return header->flags; +} + +const char * +crm_ipc_name(crm_ipc_t * client) +{ + CRM_ASSERT(client != NULL); + return client->server_name; +} + +// \return Standard Pacemaker return code +static int +internal_ipc_get_reply(crm_ipc_t *client, int request_id, int ms_timeout, + ssize_t *bytes) +{ + time_t timeout = time(NULL) + 1 + (ms_timeout / 1000); + int rc = pcmk_rc_ok; + + /* get the reply */ + crm_trace("Waiting on reply to %s IPC message %d", + client->server_name, request_id); + do { + + *bytes = qb_ipcc_recv(client->ipc, client->buffer, client->buf_size, 1000); + if (*bytes > 0) { + pcmk__ipc_header_t *hdr = NULL; + + rc = crm_ipc_decompress(client); + if (rc != pcmk_rc_ok) { + return rc; + } + + hdr = (pcmk__ipc_header_t *)(void*)client->buffer; + if (hdr->qb.id == request_id) { + /* Got it */ + break; + } else if (hdr->qb.id < request_id) { + xmlNode *bad = string2xml(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)); + + crm_err("Discarding newer reply %d (need %d)", hdr->qb.id, request_id); + crm_log_xml_notice(bad, "ImpossibleReply"); + CRM_ASSERT(hdr->qb.id <= request_id); + } + } else if (!crm_ipc_connected(client)) { + crm_err("%s IPC provider disconnected while waiting for message %d", + client->server_name, request_id); + break; + } + + } while (time(NULL) < timeout); + + if (*bytes < 0) { + rc = (int) -*bytes; // System errno + } + return rc; +} + +/*! + * \brief Send an IPC XML message + * + * \param[in,out] client Connection to IPC server + * \param[in,out] message XML message to send + * \param[in] flags Bitmask of crm_ipc_flags + * \param[in] ms_timeout Give up if not sent within this much time + * (5 seconds if 0, or no timeout if negative) + * \param[out] reply Reply from server (or NULL if none) + * + * \return Negative errno on error, otherwise size of reply received in bytes + * if reply was needed, otherwise number of bytes sent + */ +int +crm_ipc_send(crm_ipc_t * client, xmlNode * message, enum crm_ipc_flags flags, int32_t ms_timeout, + xmlNode ** reply) +{ + int rc = 0; + ssize_t qb_rc = 0; + ssize_t bytes = 0; + struct iovec *iov; + static uint32_t id = 0; + static int factor = 8; + pcmk__ipc_header_t *header; + + if (client == NULL) { + crm_notice("Can't send IPC request without connection (bug?): %.100s", + message); + return -ENOTCONN; + + } else if (!crm_ipc_connected(client)) { + /* Don't even bother */ + crm_notice("Can't send %s IPC requests: Connection closed", + client->server_name); + return -ENOTCONN; + } + + if (ms_timeout == 0) { + ms_timeout = 5000; + } + + if (client->need_reply) { + qb_rc = qb_ipcc_recv(client->ipc, client->buffer, client->buf_size, ms_timeout); + if (qb_rc < 0) { + crm_warn("Sending %s IPC disabled until pending reply received", + client->server_name); + return -EALREADY; + + } else { + crm_notice("Sending %s IPC re-enabled after pending reply received", + client->server_name); + client->need_reply = FALSE; + } + } + + id++; + CRM_LOG_ASSERT(id != 0); /* Crude wrap-around detection */ + rc = pcmk__ipc_prepare_iov(id, message, client->max_buf_size, &iov, &bytes); + if (rc != pcmk_rc_ok) { + crm_warn("Couldn't prepare %s IPC request: %s " CRM_XS " rc=%d", + client->server_name, pcmk_rc_str(rc), rc); + return pcmk_rc2legacy(rc); + } + + header = iov[0].iov_base; + pcmk__set_ipc_flags(header->flags, client->server_name, flags); + + if (pcmk_is_set(flags, crm_ipc_proxied)) { + /* Don't look for a synchronous response */ + pcmk__clear_ipc_flags(flags, "client", crm_ipc_client_response); + } + + if(header->size_compressed) { + if(factor < 10 && (client->max_buf_size / 10) < (bytes / factor)) { + crm_notice("Compressed message exceeds %d0%% of configured IPC " + "limit (%u bytes); consider setting PCMK_ipc_buffer to " + "%u or higher", + factor, client->max_buf_size, 2 * client->max_buf_size); + factor++; + } + } + + crm_trace("Sending %s IPC request %d of %u bytes using %dms timeout", + client->server_name, header->qb.id, header->qb.size, ms_timeout); + + if ((ms_timeout > 0) || !pcmk_is_set(flags, crm_ipc_client_response)) { + + time_t timeout = time(NULL) + 1 + (ms_timeout / 1000); + + do { + /* @TODO Is this check really needed? Won't qb_ipcc_sendv() return + * an error if it's not connected? + */ + if (!crm_ipc_connected(client)) { + goto send_cleanup; + } + + qb_rc = qb_ipcc_sendv(client->ipc, iov, 2); + } while ((qb_rc == -EAGAIN) && (time(NULL) < timeout)); + + rc = (int) qb_rc; // Negative of system errno, or bytes sent + if (qb_rc <= 0) { + goto send_cleanup; + + } else if (!pcmk_is_set(flags, crm_ipc_client_response)) { + crm_trace("Not waiting for reply to %s IPC request %d", + client->server_name, header->qb.id); + goto send_cleanup; + } + + rc = internal_ipc_get_reply(client, header->qb.id, ms_timeout, &bytes); + if (rc != pcmk_rc_ok) { + /* We didn't get the reply in time, so disable future sends for now. + * The only alternative would be to close the connection since we + * don't know how to detect and discard out-of-sequence replies. + * + * @TODO Implement out-of-sequence detection + */ + client->need_reply = TRUE; + } + rc = (int) bytes; // Negative system errno, or size of reply received + + } else { + // No timeout, and client response needed + do { + qb_rc = qb_ipcc_sendv_recv(client->ipc, iov, 2, client->buffer, + client->buf_size, -1); + } while ((qb_rc == -EAGAIN) && crm_ipc_connected(client)); + rc = (int) qb_rc; // Negative system errno, or size of reply received + } + + if (rc > 0) { + pcmk__ipc_header_t *hdr = (pcmk__ipc_header_t *)(void*)client->buffer; + + crm_trace("Received %d-byte reply %d to %s IPC %d: %.100s", + rc, hdr->qb.id, client->server_name, header->qb.id, + crm_ipc_buffer(client)); + + if (reply) { + *reply = string2xml(crm_ipc_buffer(client)); + } + + } else { + crm_trace("No reply to %s IPC %d: rc=%d", + client->server_name, header->qb.id, rc); + } + + send_cleanup: + if (!crm_ipc_connected(client)) { + crm_notice("Couldn't send %s IPC request %d: Connection closed " + CRM_XS " rc=%d", client->server_name, header->qb.id, rc); + + } else if (rc == -ETIMEDOUT) { + crm_warn("%s IPC request %d failed: %s after %dms " CRM_XS " rc=%d", + client->server_name, header->qb.id, pcmk_strerror(rc), + ms_timeout, rc); + crm_write_blackbox(0, NULL); + + } else if (rc <= 0) { + crm_warn("%s IPC request %d failed: %s " CRM_XS " rc=%d", + client->server_name, header->qb.id, + ((rc == 0)? "No bytes sent" : pcmk_strerror(rc)), rc); + } + + pcmk_free_ipc_event(iov); + return rc; +} + +int +pcmk__crm_ipc_is_authentic_process(qb_ipcc_connection_t *qb_ipc, int sock, uid_t refuid, gid_t refgid, + pid_t *gotpid, uid_t *gotuid, gid_t *gotgid) +{ + int ret = 0; + pid_t found_pid = 0; uid_t found_uid = 0; gid_t found_gid = 0; +#if defined(HAVE_UCRED) + struct ucred ucred; + socklen_t ucred_len = sizeof(ucred); +#endif + +#ifdef HAVE_QB_IPCC_AUTH_GET + if (qb_ipc && !qb_ipcc_auth_get(qb_ipc, &found_pid, &found_uid, &found_gid)) { + goto do_checks; + } +#endif + +#if defined(HAVE_UCRED) + if (!getsockopt(sock, SOL_SOCKET, SO_PEERCRED, + &ucred, &ucred_len) + && ucred_len == sizeof(ucred)) { + found_pid = ucred.pid; found_uid = ucred.uid; found_gid = ucred.gid; + +#elif defined(HAVE_SOCKPEERCRED) + struct sockpeercred sockpeercred; + socklen_t sockpeercred_len = sizeof(sockpeercred); + + if (!getsockopt(sock, SOL_SOCKET, SO_PEERCRED, + &sockpeercred, &sockpeercred_len) + && sockpeercred_len == sizeof(sockpeercred_len)) { + found_pid = sockpeercred.pid; + found_uid = sockpeercred.uid; found_gid = sockpeercred.gid; + +#elif defined(HAVE_GETPEEREID) + if (!getpeereid(sock, &found_uid, &found_gid)) { + found_pid = PCMK__SPECIAL_PID; /* cannot obtain PID (FreeBSD) */ + +#elif defined(HAVE_GETPEERUCRED) + ucred_t *ucred; + if (!getpeerucred(sock, &ucred)) { + errno = 0; + found_pid = ucred_getpid(ucred); + found_uid = ucred_geteuid(ucred); found_gid = ucred_getegid(ucred); + ret = -errno; + ucred_free(ucred); + if (ret) { + return (ret < 0) ? ret : -pcmk_err_generic; + } + +#else +# error "No way to authenticate a Unix socket peer" + errno = 0; + if (0) { +#endif +#ifdef HAVE_QB_IPCC_AUTH_GET + do_checks: +#endif + if (gotpid != NULL) { + *gotpid = found_pid; + } + if (gotuid != NULL) { + *gotuid = found_uid; + } + if (gotgid != NULL) { + *gotgid = found_gid; + } + if (found_uid == 0 || found_uid == refuid || found_gid == refgid) { + ret = 0; + } else { + ret = pcmk_rc_ipc_unauthorized; + } + } else { + ret = (errno > 0) ? errno : pcmk_rc_error; + } + return ret; +} + +int +crm_ipc_is_authentic_process(int sock, uid_t refuid, gid_t refgid, + pid_t *gotpid, uid_t *gotuid, gid_t *gotgid) +{ + int ret = pcmk__crm_ipc_is_authentic_process(NULL, sock, refuid, refgid, + gotpid, gotuid, gotgid); + + /* The old function had some very odd return codes*/ + if (ret == 0) { + return 1; + } else if (ret == pcmk_rc_ipc_unauthorized) { + return 0; + } else { + return pcmk_rc2legacy(ret); + } +} + +int +pcmk__ipc_is_authentic_process_active(const char *name, uid_t refuid, + gid_t refgid, pid_t *gotpid) +{ + static char last_asked_name[PATH_MAX / 2] = ""; /* log spam prevention */ + int fd; + int rc = pcmk_rc_ipc_unresponsive; + int auth_rc = 0; + int32_t qb_rc; + pid_t found_pid = 0; uid_t found_uid = 0; gid_t found_gid = 0; + qb_ipcc_connection_t *c; +#ifdef HAVE_QB_IPCC_CONNECT_ASYNC + struct pollfd pollfd = { 0, }; + int poll_rc; + + c = qb_ipcc_connect_async(name, 0, + &(pollfd.fd)); +#else + c = qb_ipcc_connect(name, 0); +#endif + if (c == NULL) { + crm_info("Could not connect to %s IPC: %s", name, strerror(errno)); + rc = pcmk_rc_ipc_unresponsive; + goto bail; + } +#ifdef HAVE_QB_IPCC_CONNECT_ASYNC + pollfd.events = POLLIN; + do { + poll_rc = poll(&pollfd, 1, 2000); + } while ((poll_rc == -1) && (errno == EINTR)); + if ((poll_rc <= 0) || (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 + } + goto bail; + } +#endif + + qb_rc = qb_ipcc_fd_get(c, &fd); + if (qb_rc != 0) { + rc = (int) -qb_rc; // System errno + crm_err("Could not get fd from %s IPC: %s " CRM_XS " rc=%d", + name, pcmk_rc_str(rc), rc); + goto bail; + } + + auth_rc = pcmk__crm_ipc_is_authentic_process(c, fd, refuid, refgid, &found_pid, + &found_uid, &found_gid); + if (auth_rc == pcmk_rc_ipc_unauthorized) { + crm_err("Daemon (IPC %s) effectively blocked with unauthorized" + " process %lld (uid: %lld, gid: %lld)", + name, (long long) PCMK__SPECIAL_PID_AS_0(found_pid), + (long long) found_uid, (long long) found_gid); + rc = pcmk_rc_ipc_unauthorized; + goto bail; + } + + if (auth_rc != pcmk_rc_ok) { + rc = auth_rc; + crm_err("Could not get peer credentials from %s IPC: %s " + CRM_XS " rc=%d", name, pcmk_rc_str(rc), rc); + goto bail; + } + + if (gotpid != NULL) { + *gotpid = found_pid; + } + + rc = pcmk_rc_ok; + if ((found_uid != refuid || found_gid != refgid) + && strncmp(last_asked_name, name, sizeof(last_asked_name))) { + if ((found_uid == 0) && (refuid != 0)) { + crm_warn("Daemon (IPC %s) runs as root, whereas the expected" + " credentials are %lld:%lld, hazard of violating" + " the least privilege principle", + name, (long long) refuid, (long long) refgid); + } else { + crm_notice("Daemon (IPC %s) runs as %lld:%lld, whereas the" + " expected credentials are %lld:%lld, which may" + " mean a different set of privileges than expected", + name, (long long) found_uid, (long long) found_gid, + (long long) refuid, (long long) refgid); + } + memccpy(last_asked_name, name, '\0', sizeof(last_asked_name)); + } + +bail: + if (c != NULL) { + qb_ipcc_disconnect(c); + } + return rc; +} diff --git a/lib/common/ipc_common.c b/lib/common/ipc_common.c new file mode 100644 index 0000000..d0c0636 --- /dev/null +++ b/lib/common/ipc_common.c @@ -0,0 +1,110 @@ +/* + * Copyright 2004-2021 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 <stdint.h> // uint64_t +#include <sys/types.h> + +#include <crm/msg_xml.h> +#include "crmcommon_private.h" + +#define MIN_MSG_SIZE 12336 // sizeof(struct qb_ipc_connection_response) +#define MAX_MSG_SIZE 128*1024 // 128k default + +/*! + * \internal + * \brief Choose an IPC buffer size in bytes + * + * \param[in] max Use this value if environment/default is lower + * + * \return Maximum of max and value of PCMK_ipc_buffer (default 128KB) + */ +unsigned int +pcmk__ipc_buffer_size(unsigned int max) +{ + static unsigned int global_max = 0; + + if (global_max == 0) { + long long global_ll; + + if ((pcmk__scan_ll(getenv("PCMK_ipc_buffer"), &global_ll, + 0LL) != pcmk_rc_ok) + || (global_ll <= 0)) { + global_max = MAX_MSG_SIZE; // Default for unset or invalid + + } else if (global_ll < MIN_MSG_SIZE) { + global_max = MIN_MSG_SIZE; + + } else if (global_ll > UINT_MAX) { + global_max = UINT_MAX; + + } else { + global_max = (unsigned int) global_ll; + } + } + return QB_MAX(max, global_max); +} + +/*! + * \brief Return pacemaker's default IPC buffer size + * + * \return IPC buffer size in bytes + */ +unsigned int +crm_ipc_default_buffer_size(void) +{ + static unsigned int default_size = 0; + + if (default_size == 0) { + default_size = pcmk__ipc_buffer_size(0); + } + return default_size; +} + +/*! + * \internal + * \brief Check whether an IPC header is valid + * + * \param[in] header IPC header to check + * + * \return true if IPC header has a supported version, false otherwise + */ +bool +pcmk__valid_ipc_header(const pcmk__ipc_header_t *header) +{ + if (header == NULL) { + crm_err("IPC message without header"); + return false; + + } else if (header->version > PCMK__IPC_VERSION) { + crm_err("Filtering incompatible v%d IPC message (only versions <= %d supported)", + header->version, PCMK__IPC_VERSION); + return false; + } + return true; +} + +const char * +pcmk__client_type_str(uint64_t client_type) +{ + switch (client_type) { + case pcmk__client_ipc: + return "IPC"; + case pcmk__client_tcp: + return "TCP"; +#ifdef HAVE_GNUTLS_GNUTLS_H + case pcmk__client_tls: + return "TLS"; +#endif + default: + return "unknown"; + } +} diff --git a/lib/common/ipc_controld.c b/lib/common/ipc_controld.c new file mode 100644 index 0000000..9303afd --- /dev/null +++ b/lib/common/ipc_controld.c @@ -0,0 +1,671 @@ +/* + * Copyright 2020-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <stdio.h> +#include <stdbool.h> +#include <errno.h> +#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> +#include <crm/common/ipc_controld.h> +#include "crmcommon_private.h" + +struct controld_api_private_s { + char *client_uuid; + unsigned int replies_expected; +}; + +/*! + * \internal + * \brief Get a string representation of a controller API reply type + * + * \param[in] reply Controller API reply type + * + * \return String representation of a controller API reply type + */ +const char * +pcmk__controld_api_reply2str(enum pcmk_controld_api_reply reply) +{ + switch (reply) { + case pcmk_controld_reply_reprobe: + return "reprobe"; + case pcmk_controld_reply_info: + return "info"; + case pcmk_controld_reply_resource: + return "resource"; + case pcmk_controld_reply_ping: + return "ping"; + case pcmk_controld_reply_nodes: + return "nodes"; + default: + return "unknown"; + } +} + +// \return Standard Pacemaker return code +static int +new_data(pcmk_ipc_api_t *api) +{ + struct controld_api_private_s *private = NULL; + + api->api_data = calloc(1, sizeof(struct controld_api_private_s)); + + if (api->api_data == NULL) { + return errno; + } + + private = api->api_data; + + /* 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, + * etc.), so it shouldn't be a problem. + */ + private->client_uuid = pcmk__getpid_s(); + + /* @TODO Implement a call ID model similar to the CIB, executor, and fencer + * IPC APIs, so that requests and replies can be matched, and + * duplicate replies can be discarded. + */ + return pcmk_rc_ok; +} + +static void +free_data(void *data) +{ + free(((struct controld_api_private_s *) data)->client_uuid); + free(data); +} + +// \return Standard Pacemaker return code +static int +post_connect(pcmk_ipc_api_t *api) +{ + /* The controller currently requires clients to register via a hello + * request, but does not reply back. + */ + struct controld_api_private_s *private = api->api_data; + const char *client_name = crm_system_name? crm_system_name : "client"; + xmlNode *hello; + int rc; + + hello = create_hello_message(private->client_uuid, client_name, + PCMK__CONTROLD_API_MAJOR, + PCMK__CONTROLD_API_MINOR); + rc = pcmk__send_ipc_request(api, hello); + free_xml(hello); + if (rc != pcmk_rc_ok) { + crm_info("Could not send IPC hello to %s: %s " CRM_XS " rc=%s", + pcmk_ipc_name(api, true), pcmk_rc_str(rc), rc); + } else { + crm_debug("Sent IPC hello to %s", pcmk_ipc_name(api, true)); + } + return rc; +} + +static void +set_node_info_data(pcmk_controld_api_reply_t *data, xmlNode *msg_data) +{ + data->reply_type = pcmk_controld_reply_info; + 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); + + /* 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)); + + 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, XML_NODE_IS_PEER); +} + +static void +set_ping_data(pcmk_controld_api_reply_t *data, xmlNode *msg_data) +{ + data->reply_type = pcmk_controld_reply_ping; + if (msg_data == NULL) { + return; + } + data->data.ping.sys_from = crm_element_value(msg_data, + XML_PING_ATTR_SYSFROM); + 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); +} + +static void +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)) { + + 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); + if (id_ll > 0) { + node_info->id = id_ll; + } + node_info->uname = crm_element_value(node, XML_ATTR_UNAME); + node_info->state = crm_element_value(node, XML_NODE_IN_CLUSTER); + data->data.nodes = g_list_prepend(data->data.nodes, node_info); + } +} + +static bool +reply_expected(pcmk_ipc_api_t *api, xmlNode *request) +{ + const char *command = crm_element_value(request, F_CRM_TASK); + + if (command == NULL) { + return false; + } + + // We only need to handle commands that functions in this file can send + return !strcmp(command, CRM_OP_REPROBE) + || !strcmp(command, CRM_OP_NODE_INFO) + || !strcmp(command, CRM_OP_PING) + || !strcmp(command, CRM_OP_LRM_FAIL) + || !strcmp(command, CRM_OP_LRM_DELETE); +} + +static bool +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 *msg_data = NULL; + const char *value = NULL; + pcmk_controld_api_reply_t reply_data = { + pcmk_controld_reply_unknown, NULL, NULL, + }; + + /* If we got an ACK, return true so the caller knows to expect more responses + * from the IPC server. We do this before decrementing replies_expected because + * ACKs are not going to be included in that value. + * + * Note that we cannot do the same kind of status checking here that we do in + * ipc_pacemakerd.c. The ACK message we receive does not necessarily contain + * a status attribute. That is, we may receive this: + * + * <ack function="crmd_remote_proxy_cb" line="556"/> + * + * Instead of this: + * + * <ack function="dispatch_controller_ipc" line="391" status="112"/> + */ + if (pcmk__str_eq(crm_element_name(reply), "ack", pcmk__str_none)) { + return true; // More replies needed + } + + if (private->replies_expected > 0) { + private->replies_expected--; + } + + // Do some basic validation of the reply + + /* @TODO We should be able to verify that value is always a response, but + * currently the controller doesn't always properly set the type. Even + * 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)) { + 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))) { + crm_info("Unrecognizable message from controller: no reference"); + status = CRM_EX_PROTOCOL; + goto done; + } + + value = crm_element_value(reply, F_CRM_TASK); + if (pcmk__str_empty(value)) { + crm_info("Unrecognizable message from controller: no command name"); + status = CRM_EX_PROTOCOL; + goto done; + } + + // 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); + + if (!strcmp(value, CRM_OP_REPROBE)) { + reply_data.reply_type = pcmk_controld_reply_reprobe; + + } else if (!strcmp(value, CRM_OP_NODE_INFO)) { + set_node_info_data(&reply_data, msg_data); + + } else if (!strcmp(value, CRM_OP_INVOKE_LRM)) { + reply_data.reply_type = pcmk_controld_reply_resource; + reply_data.data.resource.node_state = msg_data; + + } else if (!strcmp(value, CRM_OP_PING)) { + set_ping_data(&reply_data, msg_data); + + } else if (!strcmp(value, PCMK__CONTROLD_CMD_NODES)) { + set_nodes_data(&reply_data, msg_data); + + } else { + crm_info("Unrecognizable message from controller: unknown command '%s'", + value); + status = CRM_EX_PROTOCOL; + } + +done: + pcmk__call_ipc_callback(api, pcmk_ipc_event_reply, status, &reply_data); + + // Free any reply data that was allocated + if (pcmk__str_eq(value, PCMK__CONTROLD_CMD_NODES, pcmk__str_casei)) { + g_list_free_full(reply_data.data.nodes, free); + } + + return false; // No further replies needed +} + +pcmk__ipc_methods_t * +pcmk__controld_api_methods(void) +{ + pcmk__ipc_methods_t *cmds = calloc(1, sizeof(pcmk__ipc_methods_t)); + + if (cmds != NULL) { + cmds->new_data = new_data; + cmds->free_data = free_data; + cmds->post_connect = post_connect; + cmds->reply_expected = reply_expected; + cmds->dispatch = dispatch; + } + return cmds; +} + +/*! + * \internal + * \brief Create XML for a controller IPC request + * + * \param[in] api Controller connection + * \param[in] op Controller IPC command name + * \param[in] node Node name to set as destination host + * \param[in] msg_data XML to attach to request as message data + * + * \return Newly allocated XML for request + */ +static xmlNode * +create_controller_request(const pcmk_ipc_api_t *api, const char *op, + const char *node, xmlNode *msg_data) +{ + struct controld_api_private_s *private = NULL; + const char *sys_to = NULL; + + if (api == NULL) { + return NULL; + } + private = api->api_data; + if ((node == NULL) && !strcmp(op, CRM_OP_PING)) { + sys_to = CRM_SYSTEM_DC; + } else { + sys_to = CRM_SYSTEM_CRMD; + } + return create_request(op, msg_data, node, sys_to, + (crm_system_name? crm_system_name : "client"), + private->client_uuid); +} + +// \return Standard Pacemaker return code +static int +send_controller_request(pcmk_ipc_api_t *api, xmlNode *request, + bool reply_is_expected) +{ + int rc; + + if (crm_element_value(request, XML_ATTR_REFERENCE) == NULL) { + return EINVAL; + } + rc = pcmk__send_ipc_request(api, request); + if ((rc == pcmk_rc_ok) && reply_is_expected) { + struct controld_api_private_s *private = api->api_data; + + private->replies_expected++; + } + return rc; +} + +static xmlNode * +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); + 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); + } + return msg_data; +} + +/*! + * \brief Send a reprobe controller operation + * + * \param[in,out] api Controller connection + * \param[in] target_node Name of node to reprobe + * \param[in] router_node Router node for host + * + * \return Standard Pacemaker return code + * \note Event callback will get a reply of type pcmk_controld_reply_reprobe. + */ +int +pcmk_controld_api_reprobe(pcmk_ipc_api_t *api, const char *target_node, + const char *router_node) +{ + xmlNode *request; + xmlNode *msg_data; + int rc = pcmk_rc_ok; + + if (api == NULL) { + return EINVAL; + } + if (router_node == NULL) { + router_node = target_node; + } + crm_debug("Sending %s IPC request to reprobe %s via %s", + pcmk_ipc_name(api, true), pcmk__s(target_node, "local node"), + pcmk__s(router_node, "local node")); + msg_data = create_reprobe_message_data(target_node, router_node); + request = create_controller_request(api, CRM_OP_REPROBE, router_node, + msg_data); + rc = send_controller_request(api, request, true); + free_xml(msg_data); + free_xml(request); + return rc; +} + +/*! + * \brief Send a "node info" controller operation + * + * \param[in,out] api Controller connection + * \param[in] nodeid ID of node to get info for (or 0 for local node) + * + * \return Standard Pacemaker return code + * \note Event callback will get a reply of type pcmk_controld_reply_info. + */ +int +pcmk_controld_api_node_info(pcmk_ipc_api_t *api, uint32_t nodeid) +{ + xmlNode *request; + int rc = pcmk_rc_ok; + + request = create_controller_request(api, CRM_OP_NODE_INFO, NULL, NULL); + if (request == NULL) { + return EINVAL; + } + if (nodeid > 0) { + crm_xml_set_id(request, "%lu", (unsigned long) nodeid); + } + + rc = send_controller_request(api, request, true); + free_xml(request); + return rc; +} + +/*! + * \brief Ask the controller for status + * + * \param[in,out] api Controller connection + * \param[in] node_name Name of node whose status is desired (NULL for DC) + * + * \return Standard Pacemaker return code + * \note Event callback will get a reply of type pcmk_controld_reply_ping. + */ +int +pcmk_controld_api_ping(pcmk_ipc_api_t *api, const char *node_name) +{ + xmlNode *request; + int rc = pcmk_rc_ok; + + request = create_controller_request(api, CRM_OP_PING, node_name, NULL); + if (request == NULL) { + return EINVAL; + } + rc = send_controller_request(api, request, true); + free_xml(request); + return rc; +} + +/*! + * \brief Ask the controller for cluster information + * + * \param[in,out] api Controller connection + * + * \return Standard Pacemaker return code + * \note Event callback will get a reply of type pcmk_controld_reply_nodes. + */ +int +pcmk_controld_api_list_nodes(pcmk_ipc_api_t *api) +{ + xmlNode *request; + int rc = EINVAL; + + request = create_controller_request(api, PCMK__CONTROLD_CMD_NODES, NULL, + NULL); + if (request != NULL) { + rc = send_controller_request(api, request, true); + free_xml(request); + } + return rc; +} + +// \return Standard Pacemaker return code +static int +controller_resource_op(pcmk_ipc_api_t *api, const char *op, + const char *target_node, const char *router_node, + bool cib_only, const char *rsc_id, + const char *rsc_long_id, const char *standard, + const char *provider, const char *type) +{ + int rc = pcmk_rc_ok; + char *key; + xmlNode *request, *msg_data, *xml_rsc, *params; + + if (api == NULL) { + return EINVAL; + } + if (router_node == NULL) { + router_node = target_node; + } + + msg_data = create_xml_node(NULL, XML_GRAPH_TAG_RSC_OP); + + /* The controller logs the transition key from resource op requests, so we + * need to have *something* for it. + * @TODO don't use "crm-resource" + */ + key = pcmk__transition_key(0, getpid(), 0, + "xxxxxxxx-xrsc-opxx-xcrm-resourcexxxx"); + crm_xml_add(msg_data, XML_ATTR_TRANSITION_KEY, key); + free(key); + + crm_xml_add(msg_data, XML_LRM_ATTR_TARGET, 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); + } + + if (cib_only) { + // Indicate that only the CIB needs to be cleaned + crm_xml_add(msg_data, PCMK__XA_MODE, XML_TAG_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); + + params = create_xml_node(msg_data, XML_TAG_ATTRS); + crm_xml_add(params, XML_ATTR_CRM_VERSION, CRM_FEATURE_SET); + + // The controller parses the timeout from the request + key = crm_meta_name(XML_ATTR_TIMEOUT); + crm_xml_add(params, key, "60000"); /* 1 minute */ //@TODO pass as arg + free(key); + + request = create_controller_request(api, op, router_node, msg_data); + rc = send_controller_request(api, request, true); + free_xml(msg_data); + free_xml(request); + return rc; +} + +/*! + * \brief Ask the controller to fail a resource + * + * \param[in,out] api Controller connection + * \param[in] target_node Name of node resource is on + * \param[in] router_node Router node for target + * \param[in] rsc_id ID of resource to fail + * \param[in] rsc_long_id Long ID of resource (if any) + * \param[in] standard Standard of resource + * \param[in] provider Provider of resource (if any) + * \param[in] type Type of resource to fail + * + * \return Standard Pacemaker return code + * \note Event callback will get a reply of type pcmk_controld_reply_resource. + */ +int +pcmk_controld_api_fail(pcmk_ipc_api_t *api, + const char *target_node, const char *router_node, + const char *rsc_id, const char *rsc_long_id, + const char *standard, const char *provider, + const char *type) +{ + crm_debug("Sending %s IPC request to fail %s (a.k.a. %s) on %s via %s", + pcmk_ipc_name(api, true), pcmk__s(rsc_id, "unknown resource"), + pcmk__s(rsc_long_id, "no other names"), + pcmk__s(target_node, "unspecified node"), + pcmk__s(router_node, "unspecified node")); + return controller_resource_op(api, CRM_OP_LRM_FAIL, target_node, + router_node, false, rsc_id, rsc_long_id, + standard, provider, type); +} + +/*! + * \brief Ask the controller to refresh a resource + * + * \param[in,out] api Controller connection + * \param[in] target_node Name of node resource is on + * \param[in] router_node Router node for target + * \param[in] rsc_id ID of resource to refresh + * \param[in] rsc_long_id Long ID of resource (if any) + * \param[in] standard Standard of resource + * \param[in] provider Provider of resource (if any) + * \param[in] type Type of resource + * \param[in] cib_only If true, clean resource from CIB only + * + * \return Standard Pacemaker return code + * \note Event callback will get a reply of type pcmk_controld_reply_resource. + */ +int +pcmk_controld_api_refresh(pcmk_ipc_api_t *api, const char *target_node, + const char *router_node, + const char *rsc_id, const char *rsc_long_id, + const char *standard, const char *provider, + const char *type, bool cib_only) +{ + crm_debug("Sending %s IPC request to refresh %s (a.k.a. %s) on %s via %s", + pcmk_ipc_name(api, true), pcmk__s(rsc_id, "unknown resource"), + pcmk__s(rsc_long_id, "no other names"), + pcmk__s(target_node, "unspecified node"), + pcmk__s(router_node, "unspecified node")); + return controller_resource_op(api, CRM_OP_LRM_DELETE, target_node, + router_node, cib_only, rsc_id, rsc_long_id, + standard, provider, type); +} + +/*! + * \brief Get the number of IPC replies currently expected from the controller + * + * \param[in] api Controller IPC API connection + * + * \return Number of replies expected + */ +unsigned int +pcmk_controld_api_replies_expected(const pcmk_ipc_api_t *api) +{ + struct controld_api_private_s *private = api->api_data; + + return private->replies_expected; +} + +/*! + * \brief Create XML for a controller IPC "hello" message + * + * \deprecated This function is deprecated as part of the public C API. + */ +// \todo make this static to this file when breaking API backward compatibility +xmlNode * +create_hello_message(const char *uuid, const char *client_name, + const char *major_version, const char *minor_version) +{ + xmlNode *hello_node = NULL; + xmlNode *hello = NULL; + + if (pcmk__str_empty(uuid) || pcmk__str_empty(client_name) + || pcmk__str_empty(major_version) || pcmk__str_empty(minor_version)) { + crm_err("Could not create IPC hello message from %s (UUID %s): " + "missing information", + client_name? client_name : "unknown client", + uuid? uuid : "unknown"); + 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; + } + + 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); + + hello = create_request(CRM_OP_HELLO, hello_node, NULL, NULL, client_name, uuid); + if (hello == NULL) { + crm_err("Could not create IPC hello message from %s (UUID %s): " + "Request creation failed", client_name, uuid); + return NULL; + } + free_xml(hello_node); + + crm_trace("Created hello message from %s (UUID %s)", client_name, uuid); + return hello; +} diff --git a/lib/common/ipc_pacemakerd.c b/lib/common/ipc_pacemakerd.c new file mode 100644 index 0000000..91a3143 --- /dev/null +++ b/lib/common/ipc_pacemakerd.c @@ -0,0 +1,316 @@ +/* + * Copyright 2020-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <stdlib.h> +#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> +#include <crm/common/ipc_pacemakerd.h> +#include "crmcommon_private.h" + +typedef struct pacemakerd_api_private_s { + enum pcmk_pacemakerd_state state; + char *client_uuid; +} 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, +}; + +enum pcmk_pacemakerd_state +pcmk_pacemakerd_api_daemon_state_text2enum(const char *state) +{ + int i; + + if (state == NULL) { + return pcmk_pacemakerd_state_invalid; + } + for (i=pcmk_pacemakerd_state_init; i <= pcmk_pacemakerd_state_max; + i++) { + if (pcmk__str_eq(state, pacemakerd_state_str[i], pcmk__str_none)) { + return i; + } + } + return pcmk_pacemakerd_state_invalid; +} + +const char * +pcmk_pacemakerd_api_daemon_state_enum2text( + enum pcmk_pacemakerd_state state) +{ + if ((state >= pcmk_pacemakerd_state_init) && + (state <= pcmk_pacemakerd_state_max)) { + return pacemakerd_state_str[state]; + } + return "invalid"; +} + +/*! + * \internal + * \brief Return a friendly string representation of a \p pacemakerd state + * + * \param[in] state \p pacemakerd state + * + * \return A user-friendly string representation of \p state, or + * <tt>"Invalid pacemakerd state"</tt> + */ +const char * +pcmk__pcmkd_state_enum2friendly(enum pcmk_pacemakerd_state state) +{ + switch (state) { + case pcmk_pacemakerd_state_init: + return "Initializing pacemaker"; + case pcmk_pacemakerd_state_starting_daemons: + return "Pacemaker daemons are starting"; + case pcmk_pacemakerd_state_wait_for_ping: + return "Waiting for startup trigger from SBD"; + case pcmk_pacemakerd_state_running: + return "Pacemaker is running"; + case pcmk_pacemakerd_state_shutting_down: + return "Pacemaker daemons are shutting down"; + case pcmk_pacemakerd_state_shutdown_complete: + /* Assuming pacemakerd won't process messages while in + * shutdown_complete state unless reporting to SBD + */ + return "Pacemaker daemons are shut down (reporting to SBD)"; + case pcmk_pacemakerd_state_remote: + return "pacemaker-remoted is running (on a Pacemaker Remote node)"; + default: + return "Invalid pacemakerd state"; + } +} + +/*! + * \internal + * \brief Get a string representation of a \p pacemakerd API reply type + * + * \param[in] reply \p pacemakerd API reply type + * + * \return String representation of a \p pacemakerd API reply type + */ +const char * +pcmk__pcmkd_api_reply2str(enum pcmk_pacemakerd_api_reply reply) +{ + switch (reply) { + case pcmk_pacemakerd_reply_ping: + return "ping"; + case pcmk_pacemakerd_reply_shutdown: + return "shutdown"; + default: + return "unknown"; + } +} + +// \return Standard Pacemaker return code +static int +new_data(pcmk_ipc_api_t *api) +{ + struct pacemakerd_api_private_s *private = NULL; + + api->api_data = calloc(1, sizeof(struct pacemakerd_api_private_s)); + + if (api->api_data == NULL) { + return errno; + } + + private = api->api_data; + private->state = pcmk_pacemakerd_state_invalid; + /* other as with cib, controld, ... we are addressing pacemakerd just + from the local node -> pid is unique and thus sufficient as an ID + */ + private->client_uuid = pcmk__getpid_s(); + + return pcmk_rc_ok; +} + +static void +free_data(void *data) +{ + free(((struct pacemakerd_api_private_s *) data)->client_uuid); + free(data); +} + +// \return Standard Pacemaker return code +static int +post_connect(pcmk_ipc_api_t *api) +{ + struct pacemakerd_api_private_s *private = NULL; + + if (api->api_data == NULL) { + return EINVAL; + } + private = api->api_data; + private->state = pcmk_pacemakerd_state_invalid; + + return pcmk_rc_ok; +} + +static void +post_disconnect(pcmk_ipc_api_t *api) +{ + struct pacemakerd_api_private_s *private = NULL; + + if (api->api_data == NULL) { + return; + } + private = api->api_data; + private->state = pcmk_pacemakerd_state_invalid; + + return; +} + +static bool +reply_expected(pcmk_ipc_api_t *api, xmlNode *request) +{ + const char *command = crm_element_value(request, F_CRM_TASK); + + if (command == NULL) { + return false; + } + + // We only need to handle commands that functions in this file can send + return pcmk__str_any_of(command, CRM_OP_PING, CRM_OP_QUIT, NULL); +} + +static bool +dispatch(pcmk_ipc_api_t *api, xmlNode *reply) +{ + crm_exit_t status = CRM_EX_OK; + xmlNode *msg_data = NULL; + pcmk_pacemakerd_api_reply_t reply_data = { + pcmk_pacemakerd_reply_unknown + }; + const char *value = NULL; + long long value_ll = 0; + + if (pcmk__str_eq((const char *) reply->name, "ack", pcmk__str_none)) { + long long int ack_status = 0; + pcmk__scan_ll(crm_element_value(reply, "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, "")); + 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"); + status = CRM_EX_PROTOCOL; + goto done; + } + + value = crm_element_value(reply, F_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); + + 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)); + reply_data.data.ping.status = + pcmk__str_eq(crm_element_value(msg_data, XML_PING_ATTR_STATUS), "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); + } else if (pcmk__str_eq(value, CRM_OP_QUIT, pcmk__str_none)) { + reply_data.reply_type = pcmk_pacemakerd_reply_shutdown; + reply_data.data.shutdown.status = atoi(crm_element_value(msg_data, XML_LRM_ATTR_OPSTATUS)); + } else { + crm_info("Unrecognizable message from pacemakerd: " + "unknown command '%s'", pcmk__s(value, "")); + status = CRM_EX_PROTOCOL; + goto done; + } + +done: + pcmk__call_ipc_callback(api, pcmk_ipc_event_reply, status, &reply_data); + return false; +} + +pcmk__ipc_methods_t * +pcmk__pacemakerd_api_methods(void) +{ + pcmk__ipc_methods_t *cmds = calloc(1, sizeof(pcmk__ipc_methods_t)); + + if (cmds != NULL) { + cmds->new_data = new_data; + cmds->free_data = free_data; + cmds->post_connect = post_connect; + cmds->reply_expected = reply_expected; + cmds->dispatch = dispatch; + cmds->post_disconnect = post_disconnect; + } + return cmds; +} + +static int +do_pacemakerd_api_call(pcmk_ipc_api_t *api, const char *ipc_name, const char *task) +{ + pacemakerd_api_private_t *private; + xmlNode *cmd; + int rc; + + if (api == NULL) { + return EINVAL; + } + + private = api->api_data; + CRM_ASSERT(private != NULL); + + cmd = create_request(task, NULL, NULL, CRM_SYSTEM_MCP, + pcmk__ipc_sys_name(ipc_name, "client"), + private->client_uuid); + + 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); + } + free_xml(cmd); + } else { + rc = ENOMSG; + } + + return rc; +} + +int +pcmk_pacemakerd_api_ping(pcmk_ipc_api_t *api, const char *ipc_name) +{ + return do_pacemakerd_api_call(api, ipc_name, CRM_OP_PING); +} + +int +pcmk_pacemakerd_api_shutdown(pcmk_ipc_api_t *api, const char *ipc_name) +{ + return do_pacemakerd_api_call(api, ipc_name, CRM_OP_QUIT); +} diff --git a/lib/common/ipc_schedulerd.c b/lib/common/ipc_schedulerd.c new file mode 100644 index 0000000..c1b81a4 --- /dev/null +++ b/lib/common/ipc_schedulerd.c @@ -0,0 +1,180 @@ +/* + * Copyright 2021-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <stdlib.h> +#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> +#include <crm/common/ipc_schedulerd.h> +#include "crmcommon_private.h" + +typedef struct schedulerd_api_private_s { + char *client_uuid; +} schedulerd_api_private_t; + +// \return Standard Pacemaker return code +static int +new_data(pcmk_ipc_api_t *api) +{ + struct schedulerd_api_private_s *private = NULL; + + api->api_data = calloc(1, sizeof(struct schedulerd_api_private_s)); + + if (api->api_data == NULL) { + return errno; + } + + private = api->api_data; + /* See comments in ipc_pacemakerd.c. */ + private->client_uuid = pcmk__getpid_s(); + + return pcmk_rc_ok; +} + +static void +free_data(void *data) +{ + free(((struct schedulerd_api_private_s *) data)->client_uuid); + free(data); +} + +// \return Standard Pacemaker return code +static int +post_connect(pcmk_ipc_api_t *api) +{ + if (api->api_data == NULL) { + return EINVAL; + } + + return pcmk_rc_ok; +} + +static bool +reply_expected(pcmk_ipc_api_t *api, xmlNode *request) +{ + const char *command = crm_element_value(request, F_CRM_TASK); + + if (command == NULL) { + return false; + } + + // We only need to handle commands that functions in this file can send + return pcmk__str_any_of(command, CRM_OP_PECALC, NULL); +} + +static bool +dispatch(pcmk_ipc_api_t *api, xmlNode *reply) +{ + crm_exit_t status = CRM_EX_OK; + 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)) { + return false; + } + + value = crm_element_value(reply, F_CRM_MSG_TYPE); + if (!pcmk__str_eq(value, XML_ATTR_RESPONSE, pcmk__str_none)) { + crm_info("Unrecognizable message from schedulerd: " + "message type '%s' not '" XML_ATTR_RESPONSE "'", + 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 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); + + 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.tgraph = msg_data; + } else { + crm_info("Unrecognizable message from schedulerd: " + "unknown command '%s'", pcmk__s(value, "")); + status = CRM_EX_PROTOCOL; + goto done; + } + +done: + pcmk__call_ipc_callback(api, pcmk_ipc_event_reply, status, &reply_data); + return false; +} + +pcmk__ipc_methods_t * +pcmk__schedulerd_api_methods(void) +{ + pcmk__ipc_methods_t *cmds = calloc(1, sizeof(pcmk__ipc_methods_t)); + + if (cmds != NULL) { + cmds->new_data = new_data; + cmds->free_data = free_data; + cmds->post_connect = post_connect; + cmds->reply_expected = reply_expected; + cmds->dispatch = dispatch; + } + return cmds; +} + +static int +do_schedulerd_api_call(pcmk_ipc_api_t *api, const char *task, xmlNode *cib, char **ref) +{ + schedulerd_api_private_t *private; + xmlNode *cmd = NULL; + int rc; + + if (!pcmk_ipc_is_connected(api)) { + return ENOTCONN; + } + + private = api->api_data; + CRM_ASSERT(private != NULL); + + cmd = create_request(task, cib, NULL, CRM_SYSTEM_PENGINE, + crm_system_name? crm_system_name : "client", + private->client_uuid); + + if (cmd) { + rc = pcmk__send_ipc_request(api, cmd); + if (rc != pcmk_rc_ok) { + crm_debug("Couldn't send request to schedulerd: %s rc=%d", + pcmk_rc_str(rc), rc); + } + + *ref = strdup(crm_element_value(cmd, F_CRM_REFERENCE)); + free_xml(cmd); + } else { + rc = ENOMSG; + } + + return rc; +} + +int +pcmk_schedulerd_api_graph(pcmk_ipc_api_t *api, xmlNode *cib, char **ref) +{ + return do_schedulerd_api_call(api, CRM_OP_PECALC, cib, ref); +} diff --git a/lib/common/ipc_server.c b/lib/common/ipc_server.c new file mode 100644 index 0000000..60f20fb --- /dev/null +++ b/lib/common/ipc_server.c @@ -0,0 +1,1008 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <stdio.h> +#include <errno.h> +#include <bzlib.h> +#include <sys/stat.h> +#include <sys/types.h> + +#include <crm/crm.h> +#include <crm/msg_xml.h> +#include <crm/common/ipc.h> +#include <crm/common/ipc_internal.h> +#include "crmcommon_private.h" + +/* Evict clients whose event queue grows this large (by default) */ +#define PCMK_IPC_DEFAULT_QUEUE_MAX 500 + +static GHashTable *client_connections = NULL; + +/*! + * \internal + * \brief Count IPC clients + * + * \return Number of active IPC client connections + */ +guint +pcmk__ipc_client_count(void) +{ + return client_connections? g_hash_table_size(client_connections) : 0; +} + +/*! + * \internal + * \brief Execute a function for each active IPC client connection + * + * \param[in] func Function to call + * \param[in,out] user_data Pointer to pass to function + * + * \note The parameters are the same as for g_hash_table_foreach(). + */ +void +pcmk__foreach_ipc_client(GHFunc func, gpointer user_data) +{ + if ((func != NULL) && (client_connections != NULL)) { + g_hash_table_foreach(client_connections, func, user_data); + } +} + +pcmk__client_t * +pcmk__find_client(const qb_ipcs_connection_t *c) +{ + if (client_connections) { + return g_hash_table_lookup(client_connections, c); + } + + crm_trace("No client found for %p", c); + return NULL; +} + +pcmk__client_t * +pcmk__find_client_by_id(const char *id) +{ + if ((client_connections != NULL) && (id != NULL)) { + gpointer key; + pcmk__client_t *client = NULL; + GHashTableIter iter; + + g_hash_table_iter_init(&iter, client_connections); + while (g_hash_table_iter_next(&iter, &key, (gpointer *) & client)) { + if (strcmp(client->id, id) == 0) { + return client; + } + } + } + crm_trace("No client found with id='%s'", pcmk__s(id, "")); + return NULL; +} + +/*! + * \internal + * \brief Get a client identifier for use in log messages + * + * \param[in] c Client + * + * \return Client's name, client's ID, or a string literal, as available + * \note This is intended to be used in format strings like "client %s". + */ +const char * +pcmk__client_name(const pcmk__client_t *c) +{ + if (c == NULL) { + return "(unspecified)"; + + } else if (c->name != NULL) { + return c->name; + + } else if (c->id != NULL) { + return c->id; + + } else { + return "(unidentified)"; + } +} + +void +pcmk__client_cleanup(void) +{ + if (client_connections != NULL) { + int active = g_hash_table_size(client_connections); + + if (active > 0) { + crm_warn("Exiting with %d active IPC client%s", + active, pcmk__plural_s(active)); + } + g_hash_table_destroy(client_connections); + client_connections = NULL; + } +} + +void +pcmk__drop_all_clients(qb_ipcs_service_t *service) +{ + qb_ipcs_connection_t *c = NULL; + + if (service == NULL) { + return; + } + + c = qb_ipcs_connection_first_get(service); + + while (c != NULL) { + qb_ipcs_connection_t *last = c; + + c = qb_ipcs_connection_next_get(service, last); + + /* There really shouldn't be anyone connected at this point */ + crm_notice("Disconnecting client %p, pid=%d...", + last, pcmk__client_pid(last)); + qb_ipcs_disconnect(last); + qb_ipcs_connection_unref(last); + } +} + +/*! + * \internal + * \brief Allocate a new pcmk__client_t object based on an IPC connection + * + * \param[in] c IPC connection (NULL to allocate generic client) + * \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) + */ +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; + } + + 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); + crm_err("Unable to enforce ACLs for user ID %d, assuming unprivileged", + uid_client); + } + client->ipcs = c; + pcmk__set_client_flags(client, pcmk__client_ipc); + client->pid = pcmk__client_pid(c); + if (key == NULL) { + key = c; + } + } + + client->id = crm_generate_uuid(); + if (key == NULL) { + key = client->id; + } + if (client_connections == NULL) { + crm_trace("Creating IPC client table"); + client_connections = g_hash_table_new(g_direct_hash, g_direct_equal); + } + g_hash_table_insert(client_connections, key, client); + return client; +} + +/*! + * \brief Allocate a new pcmk__client_t object and generate its ID + * + * \param[in] key What to use as connections hash table key (NULL to use ID) + * + * \return Pointer to new pcmk__client_t (asserts on failure) + */ +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; +} + +pcmk__client_t * +pcmk__new_client(qb_ipcs_connection_t *c, uid_t uid_client, gid_t gid_client) +{ + gid_t uid_cluster = 0; + gid_t gid_cluster = 0; + + pcmk__client_t *client = NULL; + + CRM_CHECK(c != NULL, return NULL); + + if (pcmk_daemon_user(&uid_cluster, &gid_cluster) < 0) { + static bool need_log = TRUE; + + if (need_log) { + crm_warn("Could not find user and group IDs for user %s", + CRM_DAEMON_USER); + need_log = FALSE; + } + } + + if (uid_client != 0) { + crm_trace("Giving group %u access to new IPC connection", gid_cluster); + /* Passing -1 to chown(2) means don't change */ + qb_ipcs_connection_auth_set(c, -1, gid_cluster, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP); + } + + /* 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 */ + pcmk__set_client_flags(client, pcmk__client_privileged); + } + + crm_debug("New IPC client %s for PID %u with uid %d and gid %d", + client->id, client->pid, uid_client, gid_client); + return client; +} + +static struct iovec * +pcmk__new_ipc_event(void) +{ + struct iovec *iov = calloc(2, sizeof(struct iovec)); + + CRM_ASSERT(iov != NULL); + return iov; +} + +/*! + * \brief Free an I/O vector created by pcmk__ipc_prepare_iov() + * + * \param[in,out] event I/O vector to free + */ +void +pcmk_free_ipc_event(struct iovec *event) +{ + if (event != NULL) { + free(event[0].iov_base); + free(event[1].iov_base); + free(event); + } +} + +static void +free_event(gpointer data) +{ + pcmk_free_ipc_event((struct iovec *) data); +} + +static void +add_event(pcmk__client_t *c, struct iovec *iov) +{ + if (c->event_queue == NULL) { + c->event_queue = g_queue_new(); + } + g_queue_push_tail(c->event_queue, iov); +} + +void +pcmk__free_client(pcmk__client_t *c) +{ + if (c == NULL) { + return; + } + + if (client_connections) { + if (c->ipcs) { + crm_trace("Destroying %p/%p (%d remaining)", + c, c->ipcs, g_hash_table_size(client_connections) - 1); + g_hash_table_remove(client_connections, c->ipcs); + + } else { + crm_trace("Destroying remote connection %p (%d remaining)", + c, g_hash_table_size(client_connections) - 1); + g_hash_table_remove(client_connections, c->id); + } + } + + if (c->event_timer) { + g_source_remove(c->event_timer); + } + + if (c->event_queue) { + crm_debug("Destroying %d events", g_queue_get_length(c->event_queue)); + g_queue_free_full(c->event_queue, free_event); + } + + free(c->id); + free(c->name); + free(c->user); + if (c->remote) { + if (c->remote->auth_timeout) { + g_source_remove(c->remote->auth_timeout); + } + free(c->remote->buffer); + free(c->remote); + } + free(c); +} + +/*! + * \internal + * \brief Raise IPC eviction threshold for a client, if allowed + * + * \param[in,out] client Client to modify + * \param[in] qmax New threshold (as non-NULL string) + * + * \return true if change was allowed, false otherwise + */ +bool +pcmk__set_client_queue_max(pcmk__client_t *client, const char *qmax) +{ + if (pcmk_is_set(client->flags, pcmk__client_privileged)) { + long long qmax_ll; + + if ((pcmk__scan_ll(qmax, &qmax_ll, 0LL) == pcmk_rc_ok) + && (qmax_ll > 0LL) && (qmax_ll <= UINT_MAX)) { + client->queue_max = (unsigned int) qmax_ll; + return true; + } + } + return false; +} + +int +pcmk__client_pid(qb_ipcs_connection_t *c) +{ + struct qb_ipcs_connection_stats stats; + + stats.client_pid = 0; + qb_ipcs_connection_stats_get(c, &stats, 0); + return stats.client_pid; +} + +/*! + * \internal + * \brief Retrieve message XML from data read from client IPC + * + * \param[in,out] c IPC client connection + * \param[in] data Data read from client connection + * \param[out] id Where to store message ID from libqb header + * \param[out] flags Where to store flags from libqb header + * + * \return Message XML on success, NULL otherwise + */ +xmlNode * +pcmk__client_data2xml(pcmk__client_t *c, void *data, uint32_t *id, + uint32_t *flags) +{ + xmlNode *xml = NULL; + char *uncompressed = NULL; + char *text = ((char *)data) + sizeof(pcmk__ipc_header_t); + pcmk__ipc_header_t *header = data; + + if (!pcmk__valid_ipc_header(header)) { + return NULL; + } + + if (id) { + *id = ((struct qb_ipc_response_header *)data)->id; + } + if (flags) { + *flags = header->flags; + } + + if (pcmk_is_set(header->flags, crm_ipc_proxied)) { + /* Mark this client as being the endpoint of a proxy connection. + * Proxy connections responses are sent on the event channel, to avoid + * blocking the controller serving as proxy. + */ + pcmk__set_client_flags(c, pcmk__client_proxied); + } + + if (header->size_compressed) { + int rc = 0; + unsigned int size_u = 1 + header->size_uncompressed; + uncompressed = calloc(1, size_u); + + crm_trace("Decompressing message data %u bytes into %u bytes", + header->size_compressed, size_u); + + rc = BZ2_bzBuffToBuffDecompress(uncompressed, &size_u, text, header->size_compressed, 1, 0); + text = uncompressed; + + if (rc != BZ_OK) { + crm_err("Decompression failed: %s " CRM_XS " bzerror=%d", + bz2_strerror(rc), rc); + free(uncompressed); + return NULL; + } + } + + CRM_ASSERT(text[header->size_uncompressed - 1] == 0); + + xml = string2xml(text); + crm_log_xml_trace(xml, "[IPC received]"); + + free(uncompressed); + return xml; +} + +static int crm_ipcs_flush_events(pcmk__client_t *c); + +static gboolean +crm_ipcs_flush_events_cb(gpointer data) +{ + pcmk__client_t *c = data; + + c->event_timer = 0; + crm_ipcs_flush_events(c); + return FALSE; +} + +/*! + * \internal + * \brief Add progressive delay before next event queue flush + * + * \param[in,out] c Client connection to add delay to + * \param[in] queue_len Current event queue length + */ +static inline void +delay_next_flush(pcmk__client_t *c, unsigned int queue_len) +{ + /* Delay a maximum of 1.5 seconds */ + guint delay = (queue_len < 5)? (1000 + 100 * queue_len) : 1500; + + c->event_timer = g_timeout_add(delay, crm_ipcs_flush_events_cb, c); +} + +/*! + * \internal + * \brief Send client any messages in its queue + * + * \param[in,out] c Client to flush + * + * \return Standard Pacemaker return value + */ +static int +crm_ipcs_flush_events(pcmk__client_t *c) +{ + int rc = pcmk_rc_ok; + ssize_t qb_rc = 0; + unsigned int sent = 0; + unsigned int queue_len = 0; + + if (c == NULL) { + return rc; + + } else if (c->event_timer) { + /* There is already a timer, wait until it goes off */ + crm_trace("Timer active for %p - %d", c->ipcs, c->event_timer); + return rc; + } + + if (c->event_queue) { + queue_len = g_queue_get_length(c->event_queue); + } + while (sent < 100) { + pcmk__ipc_header_t *header = NULL; + struct iovec *event = NULL; + + if (c->event_queue) { + // We don't pop unless send is successful + event = g_queue_peek_head(c->event_queue); + } + if (event == NULL) { // Queue is empty + break; + } + + qb_rc = qb_ipcs_event_sendv(c->ipcs, event, 2); + if (qb_rc < 0) { + rc = (int) -qb_rc; + break; + } + event = g_queue_pop_head(c->event_queue); + + sent++; + header = event[0].iov_base; + if (header->size_compressed) { + crm_trace("Event %d to %p[%d] (%lld compressed bytes) sent", + header->qb.id, c->ipcs, c->pid, (long long) qb_rc); + } else { + crm_trace("Event %d to %p[%d] (%lld bytes) sent: %.120s", + header->qb.id, c->ipcs, c->pid, (long long) qb_rc, + (char *) (event[1].iov_base)); + } + pcmk_free_ipc_event(event); + } + + queue_len -= sent; + if (sent > 0 || queue_len) { + crm_trace("Sent %d events (%d remaining) for %p[%d]: %s (%lld)", + sent, queue_len, c->ipcs, c->pid, + pcmk_rc_str(rc), (long long) qb_rc); + } + + if (queue_len) { + + /* Allow clients to briefly fall behind on processing incoming messages, + * but drop completely unresponsive clients so the connection doesn't + * consume resources indefinitely. + */ + if (queue_len > QB_MAX(c->queue_max, PCMK_IPC_DEFAULT_QUEUE_MAX)) { + if ((c->queue_backlog <= 1) || (queue_len < c->queue_backlog)) { + /* Don't evict for a new or shrinking backlog */ + crm_warn("Client with process ID %u has a backlog of %u messages " + CRM_XS " %p", c->pid, queue_len, c->ipcs); + } else { + crm_err("Evicting client with process ID %u due to backlog of %u messages " + CRM_XS " %p", c->pid, queue_len, c->ipcs); + c->queue_backlog = 0; + qb_ipcs_disconnect(c->ipcs); + return rc; + } + } + + c->queue_backlog = queue_len; + delay_next_flush(c, queue_len); + + } else { + /* Event queue is empty, there is no backlog */ + c->queue_backlog = 0; + } + + return rc; +} + +/*! + * \internal + * \brief Create an I/O vector for sending an IPC XML message + * + * \param[in] request Identifier for libqb response header + * \param[in,out] message XML message to send + * \param[in] max_send_size If 0, default IPC buffer size is used + * \param[out] result Where to store prepared I/O vector + * \param[out] bytes Size of prepared data in bytes + * + * \return Standard Pacemaker return code + */ +int +pcmk__ipc_prepare_iov(uint32_t request, 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; + pcmk__ipc_header_t *header = NULL; + + if ((message == NULL) || (result == NULL)) { + return EINVAL; + } + + header = calloc(1, sizeof(pcmk__ipc_header_t)); + if (header == NULL) { + return ENOMEM; /* errno mightn't be set by allocator */ + } + + buffer = dump_xml_unformatted(message); + + if (max_send_size == 0) { + max_send_size = crm_ipc_default_buffer_size(); + } + CRM_LOG_ASSERT(max_send_size != 0); + + *result = NULL; + iov = pcmk__new_ipc_event(); + iov[0].iov_len = sizeof(pcmk__ipc_header_t); + iov[0].iov_base = header; + + header->version = PCMK__IPC_VERSION; + header->size_uncompressed = 1 + strlen(buffer); + total = iov[0].iov_len + header->size_uncompressed; + + if (total < max_send_size) { + iov[1].iov_base = buffer; + iov[1].iov_len = header->size_uncompressed; + + } else { + unsigned int new_size = 0; + + if (pcmk__compress(buffer, (unsigned int) header->size_uncompressed, + (unsigned int) max_send_size, &compressed, + &new_size) == pcmk_rc_ok) { + + pcmk__set_ipc_flags(header->flags, "send data", crm_ipc_compressed); + header->size_compressed = new_size; + + iov[1].iov_len = header->size_compressed; + iov[1].iov_base = compressed; + + free(buffer); + + biggest = QB_MAX(header->size_compressed, biggest); + + } else { + crm_log_xml_trace(message, "EMSGSIZE"); + biggest = QB_MAX(header->size_uncompressed, biggest); + + crm_err("Could not compress %u-byte message into less than IPC " + "limit of %u bytes; set PCMK_ipc_buffer to higher value " + "(%u bytes suggested)", + header->size_uncompressed, max_send_size, 4 * biggest); + + free(compressed); + free(buffer); + pcmk_free_ipc_event(iov); + return EMSGSIZE; + } + } + + header->qb.size = iov[0].iov_len + iov[1].iov_len; + header->qb.id = (int32_t)request; /* Replying to a specific request */ + + *result = iov; + CRM_ASSERT(header->qb.size > 0); + if (bytes != NULL) { + *bytes = header->qb.size; + } + return pcmk_rc_ok; +} + +int +pcmk__ipc_send_iov(pcmk__client_t *c, struct iovec *iov, uint32_t flags) +{ + int rc = pcmk_rc_ok; + static uint32_t id = 1; + pcmk__ipc_header_t *header = iov[0].iov_base; + + if (c->flags & pcmk__client_proxied) { + /* _ALL_ replies to proxied connections need to be sent as events */ + if (!pcmk_is_set(flags, crm_ipc_server_event)) { + /* The proxied flag lets us know this was originally meant to be a + * response, even though we're sending it over the event channel. + */ + pcmk__set_ipc_flags(flags, "server event", + crm_ipc_server_event + |crm_ipc_proxied_relay_response); + } + } + + pcmk__set_ipc_flags(header->flags, "server event", flags); + if (flags & crm_ipc_server_event) { + header->qb.id = id++; /* We don't really use it, but doesn't hurt to set one */ + + if (flags & crm_ipc_server_free) { + crm_trace("Sending the original to %p[%d]", c->ipcs, c->pid); + add_event(c, iov); + + } else { + struct iovec *iov_copy = pcmk__new_ipc_event(); + + crm_trace("Sending a copy to %p[%d]", c->ipcs, c->pid); + iov_copy[0].iov_len = iov[0].iov_len; + iov_copy[0].iov_base = malloc(iov[0].iov_len); + memcpy(iov_copy[0].iov_base, iov[0].iov_base, iov[0].iov_len); + + iov_copy[1].iov_len = iov[1].iov_len; + iov_copy[1].iov_base = malloc(iov[1].iov_len); + memcpy(iov_copy[1].iov_base, iov[1].iov_base, iov[1].iov_len); + + add_event(c, iov_copy); + } + + } else { + ssize_t qb_rc; + + CRM_LOG_ASSERT(header->qb.id != 0); /* Replying to a specific request */ + + qb_rc = qb_ipcs_response_sendv(c->ipcs, iov, 2); + if (qb_rc < header->qb.size) { + if (qb_rc < 0) { + rc = (int) -qb_rc; + } + crm_notice("Response %d to pid %d failed: %s " + CRM_XS " bytes=%u rc=%lld ipcs=%p", + header->qb.id, c->pid, pcmk_rc_str(rc), + header->qb.size, (long long) qb_rc, c->ipcs); + + } else { + crm_trace("Response %d sent, %lld bytes to %p[%d]", + header->qb.id, (long long) qb_rc, c->ipcs, c->pid); + } + + if (flags & crm_ipc_server_free) { + pcmk_free_ipc_event(iov); + } + } + + if (flags & crm_ipc_server_event) { + rc = crm_ipcs_flush_events(c); + } else { + crm_ipcs_flush_events(c); + } + + if ((rc == EPIPE) || (rc == ENOTCONN)) { + crm_trace("Client %p disconnected", c->ipcs); + } + return rc; +} + +int +pcmk__ipc_send_xml(pcmk__client_t *c, uint32_t request, xmlNode *message, + uint32_t flags) +{ + struct iovec *iov = NULL; + int rc = pcmk_rc_ok; + + if (c == NULL) { + return EINVAL; + } + rc = pcmk__ipc_prepare_iov(request, message, crm_ipc_default_buffer_size(), + &iov, NULL); + if (rc == pcmk_rc_ok) { + pcmk__set_ipc_flags(flags, "send data", crm_ipc_server_free); + rc = pcmk__ipc_send_iov(c, iov, flags); + } else { + pcmk_free_ipc_event(iov); + crm_notice("IPC message to pid %d failed: %s " CRM_XS " rc=%d", + c->pid, pcmk_rc_str(rc), rc); + } + return rc; +} + +/*! + * \internal + * \brief Create an acknowledgement with a status code to send to a client + * + * \param[in] function Calling function + * \param[in] line Source file line within calling function + * \param[in] flags IPC flags to use when sending + * \param[in] tag Element name to use for acknowledgement + * \param[in] ver IPC protocol version (can be NULL) + * \param[in] status Exit status code to add to ack + * + * \return Newly created XML for ack + * \note The caller is responsible for freeing the return value with free_xml(). + */ +xmlNode * +pcmk__ipc_create_ack_as(const char *function, int line, uint32_t flags, + const char *tag, const char *ver, crm_exit_t status) +{ + 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); + crm_xml_add(ack, PCMK__XA_IPC_PROTO_VERSION, ver); + } + return ack; +} + +/*! + * \internal + * \brief Send an acknowledgement with a status code to a client + * + * \param[in] function Calling function + * \param[in] line Source file line within calling function + * \param[in] c Client to send ack to + * \param[in] request Request ID being replied to + * \param[in] flags IPC flags to use when sending + * \param[in] tag Element name to use for acknowledgement + * \param[in] ver IPC protocol version (can be NULL) + * \param[in] status Status code to send with acknowledgement + * + * \return Standard Pacemaker return code + */ +int +pcmk__ipc_send_ack_as(const char *function, int line, pcmk__client_t *c, + uint32_t request, uint32_t flags, const char *tag, + const char *ver, crm_exit_t status) +{ + int rc = pcmk_rc_ok; + xmlNode *ack = pcmk__ipc_create_ack_as(function, line, flags, tag, ver, status); + + if (ack != NULL) { + crm_trace("Ack'ing IPC message from client %s as <%s status=%d>", + pcmk__client_name(c), tag, status); + c->request_id = 0; + rc = pcmk__ipc_send_xml(c, request, ack, flags); + free_xml(ack); + } + return rc; +} + +/*! + * \internal + * \brief Add an IPC server to the main loop for the pacemaker-based API + * + * \param[out] ipcs_ro New IPC server for read-only pacemaker-based API + * \param[out] ipcs_rw New IPC server for read/write pacemaker-based API + * \param[out] ipcs_shm New IPC server for shared-memory pacemaker-based API + * \param[in] ro_cb IPC callbacks for read-only API + * \param[in] rw_cb IPC callbacks for read/write and shared-memory APIs + * + * \note This function exits fatally if unable to create the servers. + */ +void pcmk__serve_based_ipc(qb_ipcs_service_t **ipcs_ro, + qb_ipcs_service_t **ipcs_rw, + qb_ipcs_service_t **ipcs_shm, + struct qb_ipcs_service_handlers *ro_cb, + struct qb_ipcs_service_handlers *rw_cb) +{ + *ipcs_ro = mainloop_add_ipc_server(PCMK__SERVER_BASED_RO, + QB_IPC_NATIVE, ro_cb); + + *ipcs_rw = mainloop_add_ipc_server(PCMK__SERVER_BASED_RW, + QB_IPC_NATIVE, rw_cb); + + *ipcs_shm = mainloop_add_ipc_server(PCMK__SERVER_BASED_SHM, + QB_IPC_SHM, rw_cb); + + if (*ipcs_ro == NULL || *ipcs_rw == NULL || *ipcs_shm == NULL) { + crm_err("Failed to create the CIB manager: exiting and inhibiting respawn"); + crm_warn("Verify pacemaker and pacemaker_remote are not both enabled"); + crm_exit(CRM_EX_FATAL); + } +} + +/*! + * \internal + * \brief Destroy IPC servers for pacemaker-based API + * + * \param[out] ipcs_ro IPC server for read-only pacemaker-based API + * \param[out] ipcs_rw IPC server for read/write pacemaker-based API + * \param[out] ipcs_shm IPC server for shared-memory pacemaker-based API + * + * \note This is a convenience function for calling qb_ipcs_destroy() for each + * argument. + */ +void +pcmk__stop_based_ipc(qb_ipcs_service_t *ipcs_ro, + qb_ipcs_service_t *ipcs_rw, + qb_ipcs_service_t *ipcs_shm) +{ + qb_ipcs_destroy(ipcs_ro); + qb_ipcs_destroy(ipcs_rw); + qb_ipcs_destroy(ipcs_shm); +} + +/*! + * \internal + * \brief Add an IPC server to the main loop for the pacemaker-controld API + * + * \param[in] cb IPC callbacks + * + * \return Newly created IPC server + */ +qb_ipcs_service_t * +pcmk__serve_controld_ipc(struct qb_ipcs_service_handlers *cb) +{ + return mainloop_add_ipc_server(CRM_SYSTEM_CRMD, QB_IPC_NATIVE, cb); +} + +/*! + * \internal + * \brief Add an IPC server to the main loop for the pacemaker-attrd API + * + * \param[out] ipcs Where to store newly created IPC server + * \param[in] cb IPC callbacks + * + * \note This function exits fatally if unable to create the servers. + */ +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); + + if (*ipcs == NULL) { + crm_err("Failed to create pacemaker-attrd server: exiting and inhibiting respawn"); + crm_warn("Verify pacemaker and pacemaker_remote are not both enabled."); + crm_exit(CRM_EX_FATAL); + } +} + +/*! + * \internal + * \brief Add an IPC server to the main loop for the pacemaker-fenced API + * + * \param[out] ipcs Where to store newly created IPC server + * \param[in] cb IPC callbacks + * + * \note This function exits fatally if unable to create the servers. + */ +void +pcmk__serve_fenced_ipc(qb_ipcs_service_t **ipcs, + struct qb_ipcs_service_handlers *cb) +{ + *ipcs = mainloop_add_ipc_server_with_prio("stonith-ng", QB_IPC_NATIVE, cb, + QB_LOOP_HIGH); + + if (*ipcs == NULL) { + crm_err("Failed to create fencer: exiting and inhibiting respawn."); + crm_warn("Verify pacemaker and pacemaker_remote are not both enabled."); + crm_exit(CRM_EX_FATAL); + } +} + +/*! + * \internal + * \brief Add an IPC server to the main loop for the pacemakerd API + * + * \param[out] ipcs Where to store newly created IPC server + * \param[in] cb IPC callbacks + * + * \note This function exits with CRM_EX_OSERR if unable to create the servers. + */ +void +pcmk__serve_pacemakerd_ipc(qb_ipcs_service_t **ipcs, + struct qb_ipcs_service_handlers *cb) +{ + *ipcs = mainloop_add_ipc_server(CRM_SYSTEM_MCP, QB_IPC_NATIVE, cb); + + if (*ipcs == NULL) { + crm_err("Couldn't start pacemakerd IPC server"); + crm_warn("Verify pacemaker and pacemaker_remote are not both enabled."); + /* sub-daemons are observed by pacemakerd. Thus we exit CRM_EX_FATAL + * if we want to prevent pacemakerd from restarting them. + * With pacemakerd we leave the exit-code shown to e.g. systemd + * to what it was prior to moving the code here from pacemakerd.c + */ + crm_exit(CRM_EX_OSERR); + } +} + +/*! + * \internal + * \brief Add an IPC server to the main loop for the pacemaker-schedulerd API + * + * \param[in] cb IPC callbacks + * + * \return Newly created IPC server + * \note This function exits fatally if unable to create the servers. + */ +qb_ipcs_service_t * +pcmk__serve_schedulerd_ipc(struct qb_ipcs_service_handlers *cb) +{ + return mainloop_add_ipc_server(CRM_SYSTEM_PENGINE, QB_IPC_NATIVE, cb); +} + +/*! + * \brief Check whether string represents a client name used by cluster daemons + * + * \param[in] name String to check + * + * \return true if name is standard client name used by daemons, false otherwise + * + * \note This is provided by the client, and so cannot be used by itself as a + * secure means of authentication. + */ +bool +crm_is_daemon_name(const char *name) +{ + name = pcmk__message_name(name); + return (!strcmp(name, CRM_SYSTEM_CRMD) + || !strcmp(name, CRM_SYSTEM_STONITHD) + || !strcmp(name, "stonith-ng") + || !strcmp(name, "attrd") + || !strcmp(name, CRM_SYSTEM_CIB) + || !strcmp(name, CRM_SYSTEM_MCP) + || !strcmp(name, CRM_SYSTEM_DC) + || !strcmp(name, CRM_SYSTEM_TENGINE) + || !strcmp(name, CRM_SYSTEM_LRMD)); +} diff --git a/lib/common/iso8601.c b/lib/common/iso8601.c new file mode 100644 index 0000000..3e000e1 --- /dev/null +++ b/lib/common/iso8601.c @@ -0,0 +1,1970 @@ +/* + * Copyright 2005-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +/* + * References: + * https://en.wikipedia.org/wiki/ISO_8601 + * http://www.staff.science.uu.nl/~gent0113/calendar/isocalendar.htm + */ + +#include <crm_internal.h> +#include <crm/crm.h> +#include <time.h> +#include <ctype.h> +#include <inttypes.h> +#include <string.h> +#include <stdbool.h> +#include <crm/common/iso8601.h> + +/* + * Andrew's code was originally written for OSes whose "struct tm" contains: + * long tm_gmtoff; :: Seconds east of UTC + * const char *tm_zone; :: Timezone abbreviation + * Some OSes lack these, instead having: + * time_t (or long) timezone; + :: "difference between UTC and local standard time" + * char *tzname[2] = { "...", "..." }; + * I (David Lee) confess to not understanding the details. So my attempted + * generalisations for where their use is necessary may be flawed. + * + * 1. Does "difference between ..." subtract the same or opposite way? + * 2. Should it use "altzone" instead of "timezone"? + * 3. Should it use tzname[0] or tzname[1]? Interaction with timezone/altzone? + */ +#if defined(HAVE_STRUCT_TM_TM_GMTOFF) +# define GMTOFF(tm) ((tm)->tm_gmtoff) +#else +/* Note: extern variable; macro argument not actually used. */ +# define GMTOFF(tm) (-timezone+daylight) +#endif + +#define HOUR_SECONDS (60 * 60) +#define DAY_SECONDS (HOUR_SECONDS * 24) + +/*! + * \internal + * \brief Validate a seconds/microseconds tuple + * + * The microseconds value must be in the correct range, and if both are nonzero + * they must have the same sign. + * + * \param[in] sec Seconds + * \param[in] usec Microseconds + * + * \return true if the seconds/microseconds tuple is valid, or false otherwise + */ +#define valid_sec_usec(sec, usec) \ + ((QB_ABS(usec) < QB_TIME_US_IN_SEC) \ + && (((sec) == 0) || ((usec) == 0) || (((sec) < 0) == ((usec) < 0)))) + +// A date/time or duration +struct crm_time_s { + int years; // Calendar year (date/time) or number of years (duration) + int months; // Number of months (duration only) + int days; // Ordinal day of year (date/time) or number of days (duration) + int seconds; // Seconds of day (date/time) or number of seconds (duration) + int offset; // Seconds offset from UTC (date/time only) + bool duration; // True if duration +}; + +static crm_time_t *parse_date(const char *date_str); + +static crm_time_t * +crm_get_utc_time(const crm_time_t *dt) +{ + crm_time_t *utc = NULL; + + if (dt == NULL) { + errno = EINVAL; + return NULL; + } + + utc = crm_time_new_undefined(); + utc->years = dt->years; + utc->days = dt->days; + utc->seconds = dt->seconds; + utc->offset = 0; + + if (dt->offset) { + crm_time_add_seconds(utc, -dt->offset); + } else { + /* Durations (which are the only things that can include months, never have a timezone */ + utc->months = dt->months; + } + + crm_time_log(LOG_TRACE, "utc-source", dt, + crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone); + crm_time_log(LOG_TRACE, "utc-target", utc, + crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone); + return utc; +} + +crm_time_t * +crm_time_new(const char *date_time) +{ + tzset(); + if (date_time == NULL) { + return pcmk__copy_timet(time(NULL)); + } + return parse_date(date_time); +} + +/*! + * \brief Allocate memory for an uninitialized time object + * + * \return Newly allocated time object + * \note The caller is responsible for freeing the return value using + * crm_time_free(). + */ +crm_time_t * +crm_time_new_undefined(void) +{ + crm_time_t *result = calloc(1, sizeof(crm_time_t)); + + CRM_ASSERT(result != NULL); + return result; +} + +/*! + * \brief Check whether a time object has been initialized yet + * + * \param[in] t Time object to check + * + * \return TRUE if time object has been initialized, FALSE otherwise + */ +bool +crm_time_is_defined(const crm_time_t *t) +{ + // Any nonzero member indicates something has been done to t + return (t != NULL) && (t->years || t->months || t->days || t->seconds + || t->offset || t->duration); +} + +void +crm_time_free(crm_time_t * dt) +{ + if (dt == NULL) { + return; + } + free(dt); +} + +static int +year_days(int year) +{ + int d = 365; + + if (crm_time_leapyear(year)) { + d++; + } + return d; +} + +/* From http://myweb.ecu.edu/mccartyr/ISOwdALG.txt : + * + * 5. Find the Jan1Weekday for Y (Monday=1, Sunday=7) + * YY = (Y-1) % 100 + * C = (Y-1) - YY + * G = YY + YY/4 + * Jan1Weekday = 1 + (((((C / 100) % 4) x 5) + G) % 7) + */ +int +crm_time_january1_weekday(int year) +{ + int YY = (year - 1) % 100; + int C = (year - 1) - YY; + int G = YY + YY / 4; + int jan1 = 1 + (((((C / 100) % 4) * 5) + G) % 7); + + crm_trace("YY=%d, C=%d, G=%d", YY, C, G); + crm_trace("January 1 %.4d: %d", year, jan1); + return jan1; +} + +int +crm_time_weeks_in_year(int year) +{ + int weeks = 52; + int jan1 = crm_time_january1_weekday(year); + + /* if jan1 == thursday */ + if (jan1 == 4) { + weeks++; + } else { + jan1 = crm_time_january1_weekday(year + 1); + /* if dec31 == thursday aka. jan1 of next year is a friday */ + if (jan1 == 5) { + weeks++; + } + + } + return weeks; +} + +// Jan-Dec plus Feb of leap years +static int month_days[13] = { + 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 29 +}; + +/*! + * \brief Return number of days in given month of given year + * + * \param[in] Ordinal month (1-12) + * \param[in] Gregorian year + * + * \return Number of days in given month (0 if given month is invalid) + */ +int +crm_time_days_in_month(int month, int year) +{ + if ((month < 1) || (month > 12)) { + return 0; + } + if ((month == 2) && crm_time_leapyear(year)) { + month = 13; + } + return month_days[month - 1]; +} + +bool +crm_time_leapyear(int year) +{ + gboolean is_leap = FALSE; + + if (year % 4 == 0) { + is_leap = TRUE; + } + if (year % 100 == 0 && year % 400 != 0) { + is_leap = FALSE; + } + return is_leap; +} + +static uint32_t +get_ordinal_days(uint32_t y, uint32_t m, uint32_t d) +{ + int lpc; + + for (lpc = 1; lpc < m; lpc++) { + d += crm_time_days_in_month(lpc, y); + } + return d; +} + +void +crm_time_log_alias(int log_level, const char *file, const char *function, + int line, const char *prefix, const crm_time_t *date_time, + int flags) +{ + char *date_s = crm_time_as_string(date_time, flags); + + if (log_level == LOG_STDOUT) { + printf("%s%s%s\n", + (prefix? prefix : ""), (prefix? ": " : ""), date_s); + } else { + do_crm_log_alias(log_level, file, function, line, "%s%s%s", + (prefix? prefix : ""), (prefix? ": " : ""), date_s); + } + free(date_s); +} + +static void +crm_time_get_sec(int sec, uint32_t *h, uint32_t *m, uint32_t *s) +{ + uint32_t hours, minutes, seconds; + + seconds = QB_ABS(sec); + + hours = seconds / HOUR_SECONDS; + seconds -= HOUR_SECONDS * hours; + + minutes = seconds / 60; + seconds -= 60 * minutes; + + crm_trace("%d == %.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32, + sec, hours, minutes, seconds); + + *h = hours; + *m = minutes; + *s = seconds; +} + +int +crm_time_get_timeofday(const crm_time_t *dt, uint32_t *h, uint32_t *m, + uint32_t *s) +{ + crm_time_get_sec(dt->seconds, h, m, s); + return TRUE; +} + +int +crm_time_get_timezone(const crm_time_t *dt, uint32_t *h, uint32_t *m) +{ + uint32_t s; + + crm_time_get_sec(dt->seconds, h, m, &s); + return TRUE; +} + +long long +crm_time_get_seconds(const crm_time_t *dt) +{ + int lpc; + crm_time_t *utc = NULL; + long long in_seconds = 0; + + if (dt == NULL) { + return 0; + } + + utc = crm_get_utc_time(dt); + if (utc == NULL) { + return 0; + } + + for (lpc = 1; lpc < utc->years; lpc++) { + long long dmax = year_days(lpc); + + in_seconds += DAY_SECONDS * dmax; + } + + /* utc->months is an offset that can only be set for a duration. + * By definition, the value is variable depending on the date to + * which it is applied. + * + * Force 30-day months so that something vaguely sane happens + * for anyone that tries to use a month in this way. + */ + if (utc->months > 0) { + in_seconds += DAY_SECONDS * 30 * (long long) (utc->months); + } + + if (utc->days > 0) { + in_seconds += DAY_SECONDS * (long long) (utc->days - 1); + } + in_seconds += utc->seconds; + + crm_time_free(utc); + return in_seconds; +} + +#define EPOCH_SECONDS 62135596800ULL /* Calculated using crm_time_get_seconds() */ +long long +crm_time_get_seconds_since_epoch(const crm_time_t *dt) +{ + return (dt == NULL)? 0 : (crm_time_get_seconds(dt) - EPOCH_SECONDS); +} + +int +crm_time_get_gregorian(const crm_time_t *dt, uint32_t *y, uint32_t *m, + uint32_t *d) +{ + int months = 0; + int days = dt->days; + + if(dt->years != 0) { + for (months = 1; months <= 12 && days > 0; months++) { + int mdays = crm_time_days_in_month(months, dt->years); + + if (mdays >= days) { + break; + } else { + days -= mdays; + } + } + + } else if (dt->months) { + /* This is a duration including months, don't convert the days field */ + months = dt->months; + + } else { + /* This is a duration not including months, still don't convert the days field */ + } + + *y = dt->years; + *m = months; + *d = days; + crm_trace("%.4d-%.3d -> %.4d-%.2d-%.2d", dt->years, dt->days, dt->years, months, days); + return TRUE; +} + +int +crm_time_get_ordinal(const crm_time_t *dt, uint32_t *y, uint32_t *d) +{ + *y = dt->years; + *d = dt->days; + return TRUE; +} + +int +crm_time_get_isoweek(const crm_time_t *dt, uint32_t *y, uint32_t *w, + uint32_t *d) +{ + /* + * Monday 29 December 2008 is written "2009-W01-1" + * Sunday 3 January 2010 is written "2009-W53-7" + */ + int year_num = 0; + int jan1 = crm_time_january1_weekday(dt->years); + int h = -1; + + CRM_CHECK(dt->days > 0, return FALSE); + +/* 6. Find the Weekday for Y M D */ + h = dt->days + jan1 - 1; + *d = 1 + ((h - 1) % 7); + +/* 7. Find if Y M D falls in YearNumber Y-1, WeekNumber 52 or 53 */ + if (dt->days <= (8 - jan1) && jan1 > 4) { + crm_trace("year--, jan1=%d", jan1); + year_num = dt->years - 1; + *w = crm_time_weeks_in_year(year_num); + + } else { + year_num = dt->years; + } + +/* 8. Find if Y M D falls in YearNumber Y+1, WeekNumber 1 */ + if (year_num == dt->years) { + int dmax = year_days(year_num); + int correction = 4 - *d; + + if ((dmax - dt->days) < correction) { + crm_trace("year++, jan1=%d, i=%d vs. %d", jan1, dmax - dt->days, correction); + year_num = dt->years + 1; + *w = 1; + } + } + +/* 9. Find if Y M D falls in YearNumber Y, WeekNumber 1 through 53 */ + if (year_num == dt->years) { + int j = dt->days + (7 - *d) + (jan1 - 1); + + *w = j / 7; + if (jan1 > 4) { + *w -= 1; + } + } + + *y = year_num; + crm_trace("Converted %.4d-%.3d to %.4" PRIu32 "-W%.2" PRIu32 "-%" PRIu32, + dt->years, dt->days, *y, *w, *d); + return TRUE; +} + +#define DATE_MAX 128 + +/*! + * \internal + * \brief Print "<seconds>.<microseconds>" to a buffer + * + * \param[in] sec Seconds + * \param[in] usec Microseconds (must be of same sign as \p sec and of + * absolute value less than \p QB_TIME_US_IN_SEC) + * \param[in,out] buf Result buffer + * \param[in,out] offset Current offset within \p buf + */ +static inline void +sec_usec_as_string(long long sec, int usec, char *buf, size_t *offset) +{ + *offset += snprintf(buf + *offset, DATE_MAX - *offset, "%s%lld.%06d", + ((sec == 0) && (usec < 0))? "-" : "", + sec, QB_ABS(usec)); +} + +/*! + * \internal + * \brief Get a string representation of a duration + * + * \param[in] dt Time object to interpret as a duration + * \param[in] usec Microseconds to add to \p dt + * \param[in] show_usec Whether to include microseconds in \p result + * \param[out] result Where to store the result string + */ +static void +crm_duration_as_string(const crm_time_t *dt, int usec, bool show_usec, + char *result) +{ + size_t offset = 0; + + CRM_ASSERT(valid_sec_usec(dt->seconds, usec)); + + if (dt->years) { + offset += snprintf(result + offset, DATE_MAX - offset, "%4d year%s ", + dt->years, pcmk__plural_s(dt->years)); + } + if (dt->months) { + offset += snprintf(result + offset, DATE_MAX - offset, "%2d month%s ", + dt->months, pcmk__plural_s(dt->months)); + } + if (dt->days) { + offset += snprintf(result + offset, DATE_MAX - offset, "%2d day%s ", + dt->days, pcmk__plural_s(dt->days)); + } + + // At least print seconds (and optionally usecs) + if ((offset == 0) || (dt->seconds != 0) || (show_usec && (usec != 0))) { + if (show_usec) { + sec_usec_as_string(dt->seconds, usec, result, &offset); + } else { + offset += snprintf(result + offset, DATE_MAX - offset, "%d", + dt->seconds); + } + offset += snprintf(result + offset, DATE_MAX - offset, " second%s", + pcmk__plural_s(dt->seconds)); + } + + // More than one minute, so provide a more readable breakdown into units + if (QB_ABS(dt->seconds) >= 60) { + uint32_t h = 0; + uint32_t m = 0; + uint32_t s = 0; + uint32_t u = QB_ABS(usec); + bool print_sec_component = false; + + crm_time_get_sec(dt->seconds, &h, &m, &s); + print_sec_component = ((s != 0) || (show_usec && (u != 0))); + + offset += snprintf(result + offset, DATE_MAX - offset, " ("); + + if (h) { + offset += snprintf(result + offset, DATE_MAX - offset, + "%" PRIu32 " hour%s%s", h, pcmk__plural_s(h), + ((m != 0) || print_sec_component)? " " : ""); + } + + if (m) { + offset += snprintf(result + offset, DATE_MAX - offset, + "%" PRIu32 " minute%s%s", m, pcmk__plural_s(m), + print_sec_component? " " : ""); + } + + if (print_sec_component) { + if (show_usec) { + sec_usec_as_string(s, u, result, &offset); + } else { + offset += snprintf(result + offset, DATE_MAX - offset, + "%" PRIu32, s); + } + offset += snprintf(result + offset, DATE_MAX - offset, " second%s", + pcmk__plural_s(dt->seconds)); + } + + offset += snprintf(result + offset, DATE_MAX - offset, ")"); + } +} + +/*! + * \internal + * \brief Get a string representation of a time object + * + * \param[in] dt Time to convert to string + * \param[in] usec Microseconds to add to \p dt + * \param[in] flags Group of \p crm_time_* string format options + * \param[out] result Where to store the result string + * + * \note \p result must be of size \p DATE_MAX or larger. + */ +static void +time_as_string_common(const crm_time_t *dt, int usec, uint32_t flags, + char *result) +{ + crm_time_t *utc = NULL; + size_t offset = 0; + + if (!crm_time_is_defined(dt)) { + strcpy(result, "<undefined time>"); + return; + } + + CRM_ASSERT(valid_sec_usec(dt->seconds, usec)); + + /* Simple cases: as duration, seconds, or seconds since epoch. + * These never depend on time zone. + */ + + if (pcmk_is_set(flags, crm_time_log_duration)) { + crm_duration_as_string(dt, usec, pcmk_is_set(flags, crm_time_usecs), + result); + return; + } + + if (pcmk_any_flags_set(flags, crm_time_seconds|crm_time_epoch)) { + long long seconds = 0; + + if (pcmk_is_set(flags, crm_time_seconds)) { + seconds = crm_time_get_seconds(dt); + } else { + seconds = crm_time_get_seconds_since_epoch(dt); + } + + if (pcmk_is_set(flags, crm_time_usecs)) { + sec_usec_as_string(seconds, usec, result, &offset); + } else { + snprintf(result, DATE_MAX, "%lld", seconds); + } + return; + } + + // Convert to UTC if local timezone was not requested + if ((dt->offset != 0) && !pcmk_is_set(flags, crm_time_log_with_timezone)) { + crm_trace("UTC conversion"); + utc = crm_get_utc_time(dt); + dt = utc; + } + + // As readable string + + 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; + + if (crm_time_get_isoweek(dt, &y, &w, &d)) { + offset += snprintf(result + offset, DATE_MAX - offset, + "%" PRIu32 "-W%.2" PRIu32 "-%" PRIu32, + y, w, d); + } + + } else if (pcmk_is_set(flags, crm_time_ordinal)) { // YYYY-DDD + uint32_t y, d; + + if (crm_time_get_ordinal(dt, &y, &d)) { + offset += snprintf(result + offset, DATE_MAX - offset, + "%" PRIu32 "-%.3" PRIu32, y, d); + } + + } else { // YYYY-MM-DD + uint32_t y, m, d; + + if (crm_time_get_gregorian(dt, &y, &m, &d)) { + offset += snprintf(result + offset, DATE_MAX - offset, + "%.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, + y, m, d); + } + } + } + + if (pcmk_is_set(flags, crm_time_log_timeofday)) { + uint32_t h = 0, m = 0, s = 0; + + if (offset > 0) { + offset += snprintf(result + offset, DATE_MAX - offset, " "); + } + + if (crm_time_get_timeofday(dt, &h, &m, &s)) { + offset += snprintf(result + offset, DATE_MAX - offset, + "%.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32, + h, m, s); + + if (pcmk_is_set(flags, crm_time_usecs)) { + offset += snprintf(result + offset, DATE_MAX - offset, + ".%06" PRIu32, QB_ABS(usec)); + } + } + + if (pcmk_is_set(flags, crm_time_log_with_timezone) + && (dt->offset != 0)) { + crm_time_get_sec(dt->offset, &h, &m, &s); + offset += snprintf(result + offset, DATE_MAX - offset, + " %c%.2" PRIu32 ":%.2" PRIu32, + ((dt->offset < 0)? '-' : '+'), h, m); + } else { + offset += snprintf(result + offset, DATE_MAX - offset, "Z"); + } + } + + crm_time_free(utc); +} + +/*! + * \brief Get a string representation of a \p crm_time_t object + * + * \param[in] dt Time to convert to string + * \param[in] flags Group of \p crm_time_* string format options + * + * \note The caller is responsible for freeing the return value using \p free(). + */ +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; +} + +/*! + * \internal + * \brief Determine number of seconds from an hour:minute:second string + * + * \param[in] time_str Time specification string + * \param[out] result Number of seconds equivalent to time_str + * + * \return TRUE if specification was valid, FALSE (and set errno) otherwise + * \note This may return the number of seconds in a day (which is out of bounds + * for a time object) if given 24:00:00. + */ +static bool +crm_time_parse_sec(const char *time_str, int *result) +{ + int rc; + uint32_t hour = 0; + uint32_t minute = 0; + uint32_t second = 0; + + *result = 0; + + // Must have at least hour, but minutes and seconds are optional + rc = sscanf(time_str, "%" SCNu32 ":%" SCNu32 ":%" SCNu32, + &hour, &minute, &second); + if (rc == 1) { + rc = sscanf(time_str, "%2" SCNu32 "%2" SCNu32 "%2" SCNu32, + &hour, &minute, &second); + } + if (rc == 0) { + crm_err("%s is not a valid ISO 8601 time specification", time_str); + errno = EINVAL; + return FALSE; + } + + crm_trace("Got valid time: %.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32, + hour, minute, second); + + if ((hour == 24) && (minute == 0) && (second == 0)) { + // Equivalent to 00:00:00 of next day, return number of seconds in day + } else if (hour >= 24) { + crm_err("%s is not a valid ISO 8601 time specification " + "because %" PRIu32 " is not a valid hour", time_str, hour); + errno = EINVAL; + return FALSE; + } + if (minute >= 60) { + crm_err("%s is not a valid ISO 8601 time specification " + "because %" PRIu32 " is not a valid minute", time_str, minute); + errno = EINVAL; + return FALSE; + } + if (second >= 60) { + crm_err("%s is not a valid ISO 8601 time specification " + "because %" PRIu32 " is not a valid second", time_str, second); + errno = EINVAL; + return FALSE; + } + + *result = (hour * HOUR_SECONDS) + (minute * 60) + second; + return TRUE; +} + +static bool +crm_time_parse_offset(const char *offset_str, int *offset) +{ + tzset(); + + if (offset_str == NULL) { + // Use local offset +#if defined(HAVE_STRUCT_TM_TM_GMTOFF) + time_t now = time(NULL); + struct tm *now_tm = localtime(&now); +#endif + int h_offset = GMTOFF(now_tm) / HOUR_SECONDS; + int m_offset = (GMTOFF(now_tm) - (HOUR_SECONDS * h_offset)) / 60; + + if (h_offset < 0 && m_offset < 0) { + m_offset = 0 - m_offset; + } + *offset = (HOUR_SECONDS * h_offset) + (60 * m_offset); + return TRUE; + } + + if (offset_str[0] == 'Z') { // @TODO invalid if anything after? + *offset = 0; + return TRUE; + } + + *offset = 0; + if ((offset_str[0] == '+') || (offset_str[0] == '-') + || isdigit((int)offset_str[0])) { + + gboolean negate = FALSE; + + if (offset_str[0] == '+') { + offset_str++; + } else if (offset_str[0] == '-') { + negate = TRUE; + offset_str++; + } + if (crm_time_parse_sec(offset_str, offset) == FALSE) { + return FALSE; + } + if (negate) { + *offset = 0 - *offset; + } + } // @TODO else invalid? + return TRUE; +} + +/*! + * \internal + * \brief Parse the time portion of an ISO 8601 date/time string + * + * \param[in] time_str Time portion of specification (after any 'T') + * \param[in,out] a_time Time object to parse into + * + * \return TRUE if valid time was parsed, FALSE (and set errno) otherwise + * \note This may add a day to a_time (if the time is 24:00:00). + */ +static bool +crm_time_parse(const char *time_str, crm_time_t *a_time) +{ + uint32_t h, m, s; + char *offset_s = NULL; + + tzset(); + + if (time_str) { + if (crm_time_parse_sec(time_str, &(a_time->seconds)) == FALSE) { + return FALSE; + } + offset_s = strstr(time_str, "Z"); + if (offset_s == NULL) { + offset_s = strstr(time_str, " "); + if (offset_s) { + while (isspace(offset_s[0])) { + offset_s++; + } + } + } + } + + if (crm_time_parse_offset(offset_s, &(a_time->offset)) == FALSE) { + return FALSE; + } + crm_time_get_sec(a_time->offset, &h, &m, &s); + crm_trace("Got tz: %c%2." PRIu32 ":%.2" PRIu32, + (a_time->offset < 0)? '-' : '+', h, m); + + if (a_time->seconds == DAY_SECONDS) { + // 24:00:00 == 00:00:00 of next day + a_time->seconds = 0; + crm_time_add_days(a_time, 1); + } + return TRUE; +} + +/* + * \internal + * \brief Parse a time object from an ISO 8601 date/time specification + * + * \param[in] date_str ISO 8601 date/time specification (or "epoch") + * + * \return New time object on success, NULL (and set errno) otherwise + */ +static crm_time_t * +parse_date(const char *date_str) +{ + const char *time_s = NULL; + crm_time_t *dt = NULL; + + int year = 0; + int month = 0; + int week = 0; + int day = 0; + int rc = 0; + + if (pcmk__str_empty(date_str)) { + crm_err("No ISO 8601 date/time specification given"); + goto invalid; + } + + if ((date_str[0] == 'T') || (date_str[2] == ':')) { + /* Just a time supplied - Infer current date */ + dt = crm_time_new(NULL); + if (date_str[0] == 'T') { + time_s = date_str + 1; + } else { + time_s = date_str; + } + goto parse_time; + } + + dt = crm_time_new_undefined(); + + if (!strncasecmp("epoch", date_str, 5) + && ((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); + return dt; + } + + /* YYYY-MM-DD */ + rc = sscanf(date_str, "%d-%d-%d", &year, &month, &day); + if (rc == 1) { + /* YYYYMMDD */ + rc = sscanf(date_str, "%4d%2d%2d", &year, &month, &day); + } + if (rc == 3) { + if (month > 12) { + crm_err("'%s' is not a valid ISO 8601 date/time specification " + "because '%d' is not a valid month", date_str, month); + goto invalid; + } else if (day > crm_time_days_in_month(month, year)) { + crm_err("'%s' is not a valid ISO 8601 date/time specification " + "because '%d' is not a valid day of the month", + date_str, day); + goto invalid; + } else { + dt->years = year; + dt->days = get_ordinal_days(year, month, day); + crm_trace("Parsed Gregorian date '%.4d-%.3d' from date string '%s'", + year, dt->days, date_str); + } + goto parse_time; + } + + /* YYYY-DDD */ + rc = sscanf(date_str, "%d-%d", &year, &day); + if (rc == 2) { + if (day > year_days(year)) { + crm_err("'%s' is not a valid ISO 8601 date/time specification " + "because '%d' is not a valid day of the year (max %d)", + date_str, day, year_days(year)); + goto invalid; + } + crm_trace("Parsed ordinal year %d and days %d from date string '%s'", + year, day, date_str); + dt->days = day; + dt->years = year; + goto parse_time; + } + + /* YYYY-Www-D */ + rc = sscanf(date_str, "%d-W%d-%d", &year, &week, &day); + if (rc == 3) { + if (week > crm_time_weeks_in_year(year)) { + crm_err("'%s' is not a valid ISO 8601 date/time specification " + "because '%d' is not a valid week of the year (max %d)", + date_str, week, crm_time_weeks_in_year(year)); + goto invalid; + } else if (day < 1 || day > 7) { + crm_err("'%s' is not a valid ISO 8601 date/time specification " + "because '%d' is not a valid day of the week", + date_str, day); + goto invalid; + } else { + /* + * See https://en.wikipedia.org/wiki/ISO_week_date + * + * Monday 29 December 2008 is written "2009-W01-1" + * Sunday 3 January 2010 is written "2009-W53-7" + * Saturday 27 September 2008 is written "2008-W37-6" + * + * If 1 January is on a Monday, Tuesday, Wednesday or Thursday, it is in week 01. + * If 1 January is on a Friday, Saturday or Sunday, it is in week 52 or 53 of the previous year. + */ + int jan1 = crm_time_january1_weekday(year); + + crm_trace("Got year %d (Jan 1 = %d), week %d, and day %d from date string '%s'", + year, jan1, week, day, date_str); + + dt->years = year; + crm_time_add_days(dt, (week - 1) * 7); + + if (jan1 <= 4) { + crm_time_add_days(dt, 1 - jan1); + } else { + crm_time_add_days(dt, 8 - jan1); + } + + crm_time_add_days(dt, day); + } + goto parse_time; + } + + crm_err("'%s' is not a valid ISO 8601 date/time specification", date_str); + goto invalid; + + parse_time: + + if (time_s == NULL) { + time_s = date_str + strspn(date_str, "0123456789-W"); + if ((time_s[0] == ' ') || (time_s[0] == 'T')) { + ++time_s; + } else { + time_s = NULL; + } + } + if ((time_s != NULL) && (crm_time_parse(time_s, dt) == FALSE)) { + goto invalid; + } + + crm_time_log(LOG_TRACE, "Unpacked", dt, crm_time_log_date | crm_time_log_timeofday); + if (crm_time_check(dt) == FALSE) { + crm_err("'%s' is not a valid ISO 8601 date/time specification", + date_str); + goto invalid; + } + return dt; + +invalid: + crm_time_free(dt); + errno = EINVAL; + return NULL; +} + +// Parse an ISO 8601 numeric value and return number of characters consumed +// @TODO This cannot handle >INT_MAX int values +// @TODO Fractions appear to be not working +// @TODO Error out on invalid specifications +static int +parse_int(const char *str, int field_width, int upper_bound, int *result) +{ + int lpc = 0; + int offset = 0; + int intermediate = 0; + gboolean fraction = FALSE; + gboolean negate = FALSE; + + *result = 0; + if (*str == '\0') { + return 0; + } + + if (str[offset] == 'T') { + offset++; + } + + if (str[offset] == '.' || str[offset] == ',') { + fraction = TRUE; + field_width = -1; + offset++; + } else if (str[offset] == '-') { + negate = TRUE; + offset++; + } else if (str[offset] == '+' || str[offset] == ':') { + offset++; + } + + for (; (fraction || lpc < field_width) && isdigit((int)str[offset]); lpc++) { + if (fraction) { + intermediate = (str[offset] - '0') / (10 ^ lpc); + } else { + *result *= 10; + intermediate = str[offset] - '0'; + } + *result += intermediate; + offset++; + } + if (fraction) { + *result = (int)(*result * upper_bound); + + } else if (upper_bound > 0 && *result > upper_bound) { + *result = upper_bound; + } + if (negate) { + *result = 0 - *result; + } + if (lpc > 0) { + crm_trace("Found int: %d. Stopped at str[%d]='%c'", *result, lpc, str[lpc]); + return offset; + } + return 0; +} + +/*! + * \brief Parse a time duration from an ISO 8601 duration specification + * + * \param[in] period_s ISO 8601 duration specification (optionally followed by + * whitespace, after which the rest of the string will be + * ignored) + * + * \return New time object on success, NULL (and set errno) otherwise + * \note It is the caller's responsibility to return the result using + * crm_time_free(). + */ +crm_time_t * +crm_time_parse_duration(const char *period_s) +{ + gboolean is_time = FALSE; + crm_time_t *diff = NULL; + + if (pcmk__str_empty(period_s)) { + crm_err("No ISO 8601 time duration given"); + goto invalid; + } + if (period_s[0] != 'P') { + crm_err("'%s' is not a valid ISO 8601 time duration " + "because it does not start with a 'P'", period_s); + goto invalid; + } + if ((period_s[1] == '\0') || isspace(period_s[1])) { + crm_err("'%s' is not a valid ISO 8601 time duration " + "because nothing follows 'P'", period_s); + goto invalid; + } + + diff = crm_time_new_undefined(); + diff->duration = TRUE; + + for (const char *current = period_s + 1; + current[0] && (current[0] != '/') && !isspace(current[0]); + ++current) { + + int an_int = 0, rc; + + if (current[0] == 'T') { + /* A 'T' separates year/month/day from hour/minute/seconds. We don't + * require it strictly, but just use it to differentiate month from + * minutes. + */ + is_time = TRUE; + continue; + } + + // An integer must be next + rc = parse_int(current, 10, 0, &an_int); + if (rc == 0) { + crm_err("'%s' is not a valid ISO 8601 time duration " + "because no integer at '%s'", period_s, current); + goto invalid; + } + current += rc; + + // A time unit must be next (we're not strict about the order) + switch (current[0]) { + case 'Y': + diff->years = an_int; + break; + case 'M': + if (is_time) { + /* Minutes */ + diff->seconds += an_int * 60; + } else { + diff->months = an_int; + } + break; + case 'W': + diff->days += an_int * 7; + break; + case 'D': + diff->days += an_int; + break; + case 'H': + diff->seconds += an_int * HOUR_SECONDS; + break; + case 'S': + diff->seconds += an_int; + break; + case '\0': + crm_err("'%s' is not a valid ISO 8601 time duration " + "because no units after %d", period_s, an_int); + goto invalid; + default: + crm_err("'%s' is not a valid ISO 8601 time duration " + "because '%c' is not a valid time unit", + period_s, current[0]); + goto invalid; + } + } + + if (!crm_time_is_defined(diff)) { + crm_err("'%s' is not a valid ISO 8601 time duration " + "because no amounts and units given", period_s); + goto invalid; + } + return diff; + +invalid: + crm_time_free(diff); + errno = EINVAL; + return NULL; +} + +/*! + * \brief Parse a time period from an ISO 8601 interval specification + * + * \param[in] period_str ISO 8601 interval specification (start/end, + * start/duration, or duration/end) + * + * \return New time period object on success, NULL (and set errno) otherwise + * \note The caller is responsible for freeing the result using + * crm_time_free_period(). + */ +crm_time_period_t * +crm_time_parse_period(const char *period_str) +{ + const char *original = period_str; + crm_time_period_t *period = NULL; + + if (pcmk__str_empty(period_str)) { + crm_err("No ISO 8601 time period given"); + goto invalid; + } + + tzset(); + period = calloc(1, sizeof(crm_time_period_t)); + CRM_ASSERT(period != NULL); + + if (period_str[0] == 'P') { + period->diff = crm_time_parse_duration(period_str); + if (period->diff == NULL) { + goto error; + } + } else { + period->start = parse_date(period_str); + if (period->start == NULL) { + goto error; + } + } + + period_str = strstr(original, "/"); + if (period_str) { + ++period_str; + if (period_str[0] == 'P') { + if (period->diff != NULL) { + crm_err("'%s' is not a valid ISO 8601 time period " + "because it has two durations", + original); + goto invalid; + } + period->diff = crm_time_parse_duration(period_str); + if (period->diff == NULL) { + goto error; + } + } else { + period->end = parse_date(period_str); + if (period->end == NULL) { + goto error; + } + } + + } else if (period->diff != NULL) { + // Only duration given, assume start is now + period->start = crm_time_new(NULL); + + } else { + // Only start given + crm_err("'%s' is not a valid ISO 8601 time period " + "because it has no duration or ending time", + original); + goto invalid; + } + + if (period->start == NULL) { + period->start = crm_time_subtract(period->end, period->diff); + + } else if (period->end == NULL) { + period->end = crm_time_add(period->start, period->diff); + } + + if (crm_time_check(period->start) == FALSE) { + crm_err("'%s' is not a valid ISO 8601 time period " + "because the start is invalid", period_str); + goto invalid; + } + if (crm_time_check(period->end) == FALSE) { + crm_err("'%s' is not a valid ISO 8601 time period " + "because the end is invalid", period_str); + goto invalid; + } + return period; + +invalid: + errno = EINVAL; +error: + crm_time_free_period(period); + return NULL; +} + +/*! + * \brief Free a dynamically allocated time period object + * + * \param[in,out] period Time period to free + */ +void +crm_time_free_period(crm_time_period_t *period) +{ + if (period) { + crm_time_free(period->start); + crm_time_free(period->end); + crm_time_free(period->diff); + free(period); + } +} + +void +crm_time_set(crm_time_t *target, const crm_time_t *source) +{ + crm_trace("target=%p, source=%p", target, source); + + CRM_CHECK(target != NULL && source != NULL, return); + + target->years = source->years; + target->days = source->days; + target->months = source->months; /* Only for durations */ + target->seconds = source->seconds; + target->offset = source->offset; + + crm_time_log(LOG_TRACE, "source", source, + crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone); + crm_time_log(LOG_TRACE, "target", target, + crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone); +} + +static void +ha_set_tm_time(crm_time_t *target, const struct tm *source) +{ + int h_offset = 0; + int m_offset = 0; + + /* Ensure target is fully initialized */ + target->years = 0; + target->months = 0; + target->days = 0; + target->seconds = 0; + target->offset = 0; + target->duration = FALSE; + + if (source->tm_year > 0) { + /* years since 1900 */ + target->years = 1900 + source->tm_year; + } + + if (source->tm_yday >= 0) { + /* days since January 1 [0-365] */ + target->days = 1 + source->tm_yday; + } + + if (source->tm_hour >= 0) { + target->seconds += HOUR_SECONDS * source->tm_hour; + } + if (source->tm_min >= 0) { + target->seconds += 60 * source->tm_min; + } + if (source->tm_sec >= 0) { + target->seconds += source->tm_sec; + } + + /* tm_gmtoff == offset from UTC in seconds */ + h_offset = GMTOFF(source) / HOUR_SECONDS; + m_offset = (GMTOFF(source) - (HOUR_SECONDS * h_offset)) / 60; + crm_trace("Time offset is %lds (%.2d:%.2d)", + GMTOFF(source), h_offset, m_offset); + + target->offset += HOUR_SECONDS * h_offset; + target->offset += 60 * m_offset; +} + +void +crm_time_set_timet(crm_time_t *target, const time_t *source) +{ + ha_set_tm_time(target, localtime(source)); +} + +crm_time_t * +pcmk_copy_time(const crm_time_t *source) +{ + crm_time_t *target = crm_time_new_undefined(); + + crm_time_set(target, source); + return target; +} + +/*! + * \internal + * \brief Convert a \p time_t time to a \p crm_time_t time + * + * \param[in] source Time to convert + * + * \return A \p crm_time_t object representing \p source + */ +crm_time_t * +pcmk__copy_timet(time_t source) +{ + crm_time_t *target = crm_time_new_undefined(); + + crm_time_set_timet(target, &source); + return target; +} + +crm_time_t * +crm_time_add(const crm_time_t *dt, const crm_time_t *value) +{ + crm_time_t *utc = NULL; + crm_time_t *answer = NULL; + + if ((dt == NULL) || (value == NULL)) { + errno = EINVAL; + return NULL; + } + + answer = pcmk_copy_time(dt); + + utc = crm_get_utc_time(value); + if (utc == NULL) { + crm_time_free(answer); + return NULL; + } + + answer->years += utc->years; + crm_time_add_months(answer, utc->months); + crm_time_add_days(answer, utc->days); + crm_time_add_seconds(answer, utc->seconds); + + crm_time_free(utc); + return answer; +} + +crm_time_t * +crm_time_calculate_duration(const crm_time_t *dt, const crm_time_t *value) +{ + crm_time_t *utc = NULL; + crm_time_t *answer = NULL; + + if ((dt == NULL) || (value == NULL)) { + errno = EINVAL; + return NULL; + } + + utc = crm_get_utc_time(value); + if (utc == NULL) { + return NULL; + } + + answer = crm_get_utc_time(dt); + if (answer == NULL) { + crm_time_free(utc); + return NULL; + } + answer->duration = TRUE; + + answer->years -= utc->years; + if(utc->months != 0) { + crm_time_add_months(answer, -utc->months); + } + crm_time_add_days(answer, -utc->days); + crm_time_add_seconds(answer, -utc->seconds); + + crm_time_free(utc); + return answer; +} + +crm_time_t * +crm_time_subtract(const crm_time_t *dt, const crm_time_t *value) +{ + crm_time_t *utc = NULL; + crm_time_t *answer = NULL; + + if ((dt == NULL) || (value == NULL)) { + errno = EINVAL; + return NULL; + } + + utc = crm_get_utc_time(value); + if (utc == NULL) { + return NULL; + } + + answer = pcmk_copy_time(dt); + answer->years -= utc->years; + if(utc->months != 0) { + crm_time_add_months(answer, -utc->months); + } + crm_time_add_days(answer, -utc->days); + crm_time_add_seconds(answer, -utc->seconds); + crm_time_free(utc); + + return answer; +} + +/*! + * \brief Check whether a time object represents a sensible date/time + * + * \param[in] dt Date/time object to check + * + * \return \c true if years, days, and seconds are sensible, \c false otherwise + */ +bool +crm_time_check(const crm_time_t *dt) +{ + return (dt != NULL) + && (dt->days > 0) && (dt->days <= year_days(dt->years)) + && (dt->seconds >= 0) && (dt->seconds < DAY_SECONDS); +} + +#define do_cmp_field(l, r, field) \ + if(rc == 0) { \ + if(l->field > r->field) { \ + crm_trace("%s: %d > %d", \ + #field, l->field, r->field); \ + rc = 1; \ + } else if(l->field < r->field) { \ + crm_trace("%s: %d < %d", \ + #field, l->field, r->field); \ + rc = -1; \ + } \ + } + +int +crm_time_compare(const crm_time_t *a, const crm_time_t *b) +{ + int rc = 0; + crm_time_t *t1 = crm_get_utc_time(a); + crm_time_t *t2 = crm_get_utc_time(b); + + if ((t1 == NULL) && (t2 == NULL)) { + rc = 0; + } else if (t1 == NULL) { + rc = -1; + } else if (t2 == NULL) { + rc = 1; + } else { + do_cmp_field(t1, t2, years); + do_cmp_field(t1, t2, days); + do_cmp_field(t1, t2, seconds); + } + + crm_time_free(t1); + crm_time_free(t2); + return rc; +} + +/*! + * \brief Add a given number of seconds to a date/time or duration + * + * \param[in,out] a_time Date/time or duration to add seconds to + * \param[in] extra Number of seconds to add + */ +void +crm_time_add_seconds(crm_time_t *a_time, int extra) +{ + int days = 0; + + crm_trace("Adding %d seconds to %d (max=%d)", + extra, a_time->seconds, DAY_SECONDS); + a_time->seconds += extra; + days = a_time->seconds / DAY_SECONDS; + a_time->seconds %= DAY_SECONDS; + + // Don't have negative seconds + if (a_time->seconds < 0) { + a_time->seconds += DAY_SECONDS; + --days; + } + + crm_time_add_days(a_time, days); +} + +void +crm_time_add_days(crm_time_t * a_time, int extra) +{ + int lower_bound = 1; + int ydays = crm_time_leapyear(a_time->years) ? 366 : 365; + + crm_trace("Adding %d days to %.4d-%.3d", extra, a_time->years, a_time->days); + + a_time->days += extra; + while (a_time->days > ydays) { + a_time->years++; + a_time->days -= ydays; + ydays = crm_time_leapyear(a_time->years) ? 366 : 365; + } + + if(a_time->duration) { + lower_bound = 0; + } + + while (a_time->days < lower_bound) { + a_time->years--; + a_time->days += crm_time_leapyear(a_time->years) ? 366 : 365; + } +} + +void +crm_time_add_months(crm_time_t * a_time, int extra) +{ + int lpc; + uint32_t y, m, d, dmax; + + crm_time_get_gregorian(a_time, &y, &m, &d); + crm_trace("Adding %d months to %.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, + extra, y, m, d); + + if (extra > 0) { + for (lpc = extra; lpc > 0; lpc--) { + m++; + if (m == 13) { + m = 1; + y++; + } + } + } else { + for (lpc = -extra; lpc > 0; lpc--) { + m--; + if (m == 0) { + m = 12; + y--; + } + } + } + + dmax = crm_time_days_in_month(m, y); + if (dmax < d) { + /* Preserve day-of-month unless the month doesn't have enough days */ + d = dmax; + } + + crm_trace("Calculated %.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, y, m, d); + + a_time->years = y; + a_time->days = get_ordinal_days(y, m, d); + + crm_time_get_gregorian(a_time, &y, &m, &d); + crm_trace("Got %.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, y, m, d); +} + +void +crm_time_add_minutes(crm_time_t * a_time, int extra) +{ + crm_time_add_seconds(a_time, extra * 60); +} + +void +crm_time_add_hours(crm_time_t * a_time, int extra) +{ + crm_time_add_seconds(a_time, extra * HOUR_SECONDS); +} + +void +crm_time_add_weeks(crm_time_t * a_time, int extra) +{ + crm_time_add_days(a_time, extra * 7); +} + +void +crm_time_add_years(crm_time_t * a_time, int extra) +{ + a_time->years += extra; +} + +static void +ha_get_tm_time(struct tm *target, const crm_time_t *source) +{ + *target = (struct tm) { + .tm_year = source->years - 1900, + .tm_mday = source->days, + .tm_sec = source->seconds % 60, + .tm_min = ( source->seconds / 60 ) % 60, + .tm_hour = source->seconds / HOUR_SECONDS, + .tm_isdst = -1, /* don't adjust */ + +#if defined(HAVE_STRUCT_TM_TM_GMTOFF) + .tm_gmtoff = source->offset +#endif + }; + mktime(target); +} + +/* The high-resolution variant of time object was added to meet an immediate + * need, and is kept internal API. + * + * @TODO The long-term goal is to come up with a clean, unified design for a + * time type (or types) that meets all the various needs, to replace + * crm_time_t, pcmk__time_hr_t, and struct timespec (in lrmd_cmd_t). + * Using glib's GDateTime is a possibility (if we are willing to require + * glib >= 2.26). + */ + +pcmk__time_hr_t * +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 = (pcmk__time_hr_t) { + .years = dt->years, + .months = dt->months, + .days = dt->days, + .seconds = dt->seconds, + .offset = dt->offset, + .duration = dt->duration + }; + } + + return hr_dt; +} + +void +pcmk__time_set_hr_dt(crm_time_t *target, const pcmk__time_hr_t *hr_dt) +{ + CRM_ASSERT((hr_dt) && (target)); + *target = (crm_time_t) { + .years = hr_dt->years, + .months = hr_dt->months, + .days = hr_dt->days, + .seconds = hr_dt->seconds, + .offset = hr_dt->offset, + .duration = hr_dt->duration + }; +} + +/*! + * \internal + * \brief Return the current time as a high-resolution time + * + * \param[out] epoch If not NULL, this will be set to seconds since epoch + * + * \return Newly allocated high-resolution time set to the current time + */ +pcmk__time_hr_t * +pcmk__time_hr_now(time_t *epoch) +{ + struct timespec tv; + crm_time_t dt; + pcmk__time_hr_t *hr; + + qb_util_timespec_from_epoch_get(&tv); + if (epoch != NULL) { + *epoch = tv.tv_sec; + } + crm_time_set_timet(&dt, &(tv.tv_sec)); + hr = pcmk__time_hr_convert(NULL, &dt); + if (hr != NULL) { + hr->useconds = tv.tv_nsec / QB_TIME_NS_IN_USEC; + } + return hr; +} + +pcmk__time_hr_t * +pcmk__time_hr_new(const char *date_time) +{ + pcmk__time_hr_t *hr_dt = NULL; + + if (date_time == NULL) { + hr_dt = pcmk__time_hr_now(NULL); + } else { + crm_time_t *dt; + + dt = parse_date(date_time); + hr_dt = pcmk__time_hr_convert(NULL, dt); + crm_time_free(dt); + } + return hr_dt; +} + +void +pcmk__time_hr_free(pcmk__time_hr_t * hr_dt) +{ + free(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; + + if (!format) { + return NULL; + } + pcmk__time_set_hr_dt(&dt, hr_dt); + ha_get_tm_time(&tm, &dt); + sprintf(nano_s, "%06d000", hr_dt->useconds); + + while ((format[scanned_pos]) != '\0') { + mark_s = strchr(&format[scanned_pos], '%'); + if (mark_s) { + int fmt_len = 1; + + fmt_pos = mark_s - format; + while ((format[fmt_pos+fmt_len] != '\0') && + (format[fmt_pos+fmt_len] >= '0') && + (format[fmt_pos+fmt_len] <= '9')) { + fmt_len++; + } + scanned_pos = fmt_pos + fmt_len + 1; + if (format[fmt_pos+fmt_len] == 'N') { + nano_digits = atoi(&format[fmt_pos+1]); + nano_digits = (nano_digits > 6)?6:nano_digits; + nano_digits = (nano_digits < 0)?0:nano_digits; + sprintf(&nanofmt_s[1], ".%ds", nano_digits); + } else { + if (format[scanned_pos] != '\0') { + continue; + } + fmt_pos = scanned_pos; /* print till end */ + } + } else { + scanned_pos = strlen(format); + fmt_pos = scanned_pos; /* print till end */ + } + tmp_fmt_s = strndup(&format[printed_pos], fmt_pos - printed_pos); +#ifdef HAVE_FORMAT_NONLITERAL +#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); +#ifdef HAVE_FORMAT_NONLITERAL +#pragma GCC diagnostic pop +#endif + printed_pos = scanned_pos; + free(tmp_fmt_s); + if (nano_digits) { +#ifdef HAVE_FORMAT_NONLITERAL +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-nonliteral" +#endif + date_len += snprintf(&date_s[date_len], max-date_len, + nanofmt_s, nano_s); +#ifdef HAVE_FORMAT_NONLITERAL +#pragma GCC diagnostic pop +#endif + nano_digits = 0; + } + } + + return (date_len == 0)?NULL:strdup(date_s); +} + +/*! + * \internal + * \brief Return a human-friendly string corresponding to an epoch time value + * + * \param[in] source Pointer to epoch time value (or \p NULL for current time) + * \param[in] flags Group of \p crm_time_* flags controlling display format + * (0 to use \p ctime() with newline removed) + * + * \return String representation of \p source on success (may be empty depending + * on \p flags; guaranteed not to be \p NULL) + * + * \note The caller is responsible for freeing the return value using \p free(). + */ +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); + } + } else { + crm_time_t dt; + + crm_time_set_timet(&dt, &epoch_time); + result = crm_time_as_string(&dt, flags); + } + return result; +} + +/*! + * \internal + * \brief Return a human-friendly string corresponding to seconds-and- + * nanoseconds value + * + * Time is shown with microsecond resolution if \p crm_time_usecs is in \p + * flags. + * + * \param[in] ts Time in seconds and nanoseconds (or \p NULL for current + * time) + * \param[in] flags Group of \p crm_time_* flags controlling display format + * + * \return String representation of \p ts on success (may be empty depending on + * \p flags; guaranteed not to be \p NULL) + * + * \note The caller is responsible for freeing the return value using \p free(). + */ +char * +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); + ts = &tmp_ts; + } + 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; +} + +/*! + * \internal + * \brief Given a millisecond interval, return a log-friendly string + * + * \param[in] interval_ms Interval in milliseconds + * + * \return Readable version of \p interval_ms + * + * \note The return value is a pointer to static memory that will be + * overwritten by later calls to this function. + */ +const char * +pcmk__readable_interval(guint interval_ms) +{ +#define MS_IN_S (1000) +#define MS_IN_M (MS_IN_S * 60) +#define MS_IN_H (MS_IN_M * 60) +#define MS_IN_D (MS_IN_H * 24) +#define MAXSTR sizeof("..d..h..m..s...ms") + static char str[MAXSTR] = { '\0', }; + int offset = 0; + + 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) { + 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) { + 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) { + offset += snprintf(str + offset, MAXSTR - offset, "%u", + interval_ms / MS_IN_S); + interval_ms -= (interval_ms / MS_IN_S) * MS_IN_S; + if (interval_ms > 0) { + offset += snprintf(str + offset, MAXSTR - offset, ".%03u", + interval_ms); + } + (void) snprintf(str + offset, MAXSTR - offset, "s"); + + } else if (interval_ms > 0) { + (void) snprintf(str + offset, MAXSTR - offset, "%ums", interval_ms); + + } else if (str[0] == '\0') { + strcpy(str, "0s"); + } + return str; +} diff --git a/lib/common/lists.c b/lib/common/lists.c new file mode 100644 index 0000000..9cd88d7 --- /dev/null +++ b/lib/common/lists.c @@ -0,0 +1,27 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> +#include <crm/common/lists_internal.h> + +GList* +pcmk__subtract_lists(GList *from, const GList *items, GCompareFunc cmp) +{ + GList *result = g_list_copy(from); + + for (const GList *item = items; item != NULL; item = item->next) { + GList *match = g_list_find_custom(result, item->data, cmp); + + if (match != NULL) { + result = g_list_remove(result, match->data); + } + } + + return result; +} diff --git a/lib/common/logging.c b/lib/common/logging.c new file mode 100644 index 0000000..dded873 --- /dev/null +++ b/lib/common/logging.c @@ -0,0 +1,1192 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <sys/param.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <sys/stat.h> +#include <sys/utsname.h> + +#include <stdio.h> +#include <unistd.h> +#include <string.h> +#include <stdlib.h> +#include <limits.h> +#include <ctype.h> +#include <pwd.h> +#include <grp.h> +#include <time.h> +#include <libgen.h> +#include <signal.h> +#include <bzlib.h> + +#include <qb/qbdefs.h> + +#include <crm/crm.h> +#include <crm/common/mainloop.h> + +// Use high-resolution (millisecond) timestamps if libqb supports them +#ifdef QB_FEATURE_LOG_HIRES_TIMESTAMPS +#define TIMESTAMP_FORMAT_SPEC "%%T" +typedef struct timespec *log_time_t; +#else +#define TIMESTAMP_FORMAT_SPEC "%%t" +typedef time_t log_time_t; +#endif + +unsigned int crm_log_level = LOG_INFO; +unsigned int crm_trace_nonlog = 0; +bool pcmk__is_daemon = false; +char *pcmk__our_nodename = NULL; + +static unsigned int crm_log_priority = LOG_NOTICE; +static GLogFunc glib_log_default = NULL; +static pcmk__output_t *logger_out = NULL; + +static gboolean crm_tracing_enabled(void); + +static void +crm_glib_handler(const gchar * log_domain, GLogLevelFlags flags, const gchar * message, + gpointer user_data) +{ + int log_level = LOG_WARNING; + GLogLevelFlags msg_level = (flags & G_LOG_LEVEL_MASK); + static struct qb_log_callsite *glib_cs = NULL; + + if (glib_cs == NULL) { + glib_cs = qb_log_callsite_get(__func__, __FILE__, "glib-handler", + LOG_DEBUG, __LINE__, crm_trace_nonlog); + } + + switch (msg_level) { + case G_LOG_LEVEL_CRITICAL: + log_level = LOG_CRIT; + + if (!crm_is_callsite_active(glib_cs, LOG_DEBUG, crm_trace_nonlog)) { + /* log and record how we got here */ + crm_abort(__FILE__, __func__, __LINE__, message, TRUE, TRUE); + } + break; + + case G_LOG_LEVEL_ERROR: + log_level = LOG_ERR; + break; + case G_LOG_LEVEL_MESSAGE: + log_level = LOG_NOTICE; + break; + case G_LOG_LEVEL_INFO: + log_level = LOG_INFO; + break; + case G_LOG_LEVEL_DEBUG: + log_level = LOG_DEBUG; + break; + + case G_LOG_LEVEL_WARNING: + case G_LOG_FLAG_RECURSION: + case G_LOG_FLAG_FATAL: + case G_LOG_LEVEL_MASK: + log_level = LOG_WARNING; + break; + } + + do_crm_log(log_level, "%s: %s", log_domain, message); +} + +#ifndef NAME_MAX +# define NAME_MAX 256 +#endif + +/*! + * \internal + * \brief Write out a blackbox (enabling blackboxes if needed) + * + * \param[in] nsig Signal number that was received + * + * \note This is a true signal handler, and so must be async-safe. + */ +static void +crm_trigger_blackbox(int nsig) +{ + if(nsig == SIGTRAP) { + /* Turn it on if it wasn't already */ + crm_enable_blackbox(nsig); + } + crm_write_blackbox(nsig, NULL); +} + +void +crm_log_deinit(void) +{ + if (glib_log_default != NULL) { + g_log_set_default_handler(glib_log_default, NULL); + } +} + +#define FMT_MAX 256 + +/*! + * \internal + * \brief Set the log format string based on the passed-in method + * + * \param[in] method The detail level of the log output + * \param[in] daemon The daemon ID included in error messages + * \param[in] use_pid Cached result of getpid() call, for efficiency + * \param[in] use_nodename Cached result of uname() call, for efficiency + * + */ + +/* XXX __attribute__((nonnull)) for use_nodename parameter */ +static void +set_format_string(int method, const char *daemon, pid_t use_pid, + const char *use_nodename) +{ + if (method == QB_LOG_SYSLOG) { + // The system log gets a simplified, user-friendly format + crm_extended_logging(method, QB_FALSE); + qb_log_format_set(method, "%g %p: %b"); + + } else { + // Everything else gets more detail, for advanced troubleshooting + + int offset = 0; + char fmt[FMT_MAX]; + + if (method > QB_LOG_STDERR) { + // If logging to file, prefix with timestamp, node name, daemon ID + offset += snprintf(fmt + offset, FMT_MAX - offset, + TIMESTAMP_FORMAT_SPEC " %s %-20s[%lu] ", + use_nodename, daemon, (unsigned long) use_pid); + } + + // Add function name (in parentheses) + offset += snprintf(fmt + offset, FMT_MAX - offset, "(%%n"); + if (crm_tracing_enabled()) { + // When tracing, add file and line number + offset += snprintf(fmt + offset, FMT_MAX - offset, "@%%f:%%l"); + } + offset += snprintf(fmt + offset, FMT_MAX - offset, ")"); + + // Add tag (if any), severity, and actual message + offset += snprintf(fmt + offset, FMT_MAX - offset, " %%g\t%%p: %%b"); + + CRM_LOG_ASSERT(offset > 0); + qb_log_format_set(method, fmt); + } +} + +#define DEFAULT_LOG_FILE CRM_LOG_DIR "/pacemaker.log" + +static bool +logfile_disabled(const char *filename) +{ + return pcmk__str_eq(filename, PCMK__VALUE_NONE, pcmk__str_casei) + || pcmk__str_eq(filename, "/dev/null", pcmk__str_none); +} + +/*! + * \internal + * \brief Fix log file ownership if group is wrong or doesn't have access + * + * \param[in] filename Log file name (for logging only) + * \param[in] logfd Log file descriptor + * + * \return Standard Pacemaker return code + */ +static int +chown_logfile(const char *filename, int logfd) +{ + uid_t pcmk_uid = 0; + gid_t pcmk_gid = 0; + struct stat st; + int rc; + + // Get the log file's current ownership and permissions + if (fstat(logfd, &st) < 0) { + return errno; + } + + // Any other errors don't prevent file from being used as log + + rc = pcmk_daemon_user(&pcmk_uid, &pcmk_gid); + if (rc != pcmk_ok) { + rc = pcmk_legacy2rc(rc); + crm_warn("Not changing '%s' ownership because user information " + "unavailable: %s", filename, pcmk_rc_str(rc)); + return pcmk_rc_ok; + } + if ((st.st_gid == pcmk_gid) + && ((st.st_mode & S_IRWXG) == (S_IRGRP|S_IWGRP))) { + return pcmk_rc_ok; + } + if (fchown(logfd, pcmk_uid, pcmk_gid) < 0) { + crm_warn("Couldn't change '%s' ownership to user %s gid %d: %s", + filename, CRM_DAEMON_USER, pcmk_gid, strerror(errno)); + } + return pcmk_rc_ok; +} + +// Reset log file permissions (using environment variable if set) +static void +chmod_logfile(const char *filename, int logfd) +{ + const char *modestr = getenv("PCMK_logfile_mode"); + mode_t filemode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP; + + if (modestr != NULL) { + long filemode_l = strtol(modestr, NULL, 8); + + if ((filemode_l != LONG_MIN) && (filemode_l != LONG_MAX)) { + filemode = (mode_t) filemode_l; + } + } + if ((filemode != 0) && (fchmod(logfd, filemode) < 0)) { + crm_warn("Couldn't change '%s' mode to %04o: %s", + filename, filemode, strerror(errno)); + } +} + +// If we're root, correct a log file's permissions if needed +static int +set_logfile_permissions(const char *filename, FILE *logfile) +{ + if (geteuid() == 0) { + int logfd = fileno(logfile); + int rc = chown_logfile(filename, logfd); + + if (rc != pcmk_rc_ok) { + return rc; + } + chmod_logfile(filename, logfd); + } + return pcmk_rc_ok; +} + +// Enable libqb logging to a new log file +static void +enable_logfile(int fd) +{ + qb_log_ctl(fd, QB_LOG_CONF_ENABLED, QB_TRUE); +#if 0 + qb_log_ctl(fd, QB_LOG_CONF_FILE_SYNC, 1); // Turn on synchronous writes +#endif + +#ifdef HAVE_qb_log_conf_QB_LOG_CONF_MAX_LINE_LEN + // Longer than default, for logging long XML lines + qb_log_ctl(fd, QB_LOG_CONF_MAX_LINE_LEN, 800); +#endif + + crm_update_callsites(); +} + +static inline void +disable_logfile(int fd) +{ + qb_log_ctl(fd, QB_LOG_CONF_ENABLED, QB_FALSE); +} + +static void +setenv_logfile(const char *filename) +{ + // Some resource agents will log only if environment variable is set + if (pcmk__env_option(PCMK__ENV_LOGFILE) == NULL) { + pcmk__set_env_option(PCMK__ENV_LOGFILE, filename); + } +} + +/*! + * \brief Add a file to be used as a Pacemaker detail log + * + * \param[in] filename Name of log file to use + * + * \return Standard Pacemaker return code + */ +int +pcmk__add_logfile(const char *filename) +{ + /* No log messages from this function will be logged to the new log! + * If another target such as syslog has already been added, the messages + * should show up there. + */ + + int fd = 0; + int rc = pcmk_rc_ok; + FILE *logfile = NULL; + bool is_default = false; + + static int default_fd = -1; + static bool have_logfile = false; + + // Use default if caller didn't specify (and we don't already have one) + if (filename == NULL) { + if (have_logfile) { + return pcmk_rc_ok; + } + filename = DEFAULT_LOG_FILE; + } + + // If the user doesn't want logging, we're done + if (logfile_disabled(filename)) { + return pcmk_rc_ok; + } + + // If the caller wants the default and we already have it, we're done + is_default = pcmk__str_eq(filename, DEFAULT_LOG_FILE, pcmk__str_none); + if (is_default && (default_fd >= 0)) { + return pcmk_rc_ok; + } + + // Check whether we have write access to the file + logfile = fopen(filename, "a"); + if (logfile == NULL) { + rc = errno; + crm_warn("Logging to '%s' is disabled: %s " CRM_XS " uid=%u gid=%u", + filename, strerror(rc), geteuid(), getegid()); + return rc; + } + + rc = set_logfile_permissions(filename, logfile); + if (rc != pcmk_rc_ok) { + crm_warn("Logging to '%s' is disabled: %s " CRM_XS " permissions", + filename, strerror(rc)); + fclose(logfile); + return rc; + } + + // Close and reopen as libqb logging target + fclose(logfile); + fd = qb_log_file_open(filename); + if (fd < 0) { + crm_warn("Logging to '%s' is disabled: %s " CRM_XS " qb_log_file_open", + filename, strerror(-fd)); + return -fd; // == +errno + } + + if (is_default) { + default_fd = fd; + setenv_logfile(filename); + + } else if (default_fd >= 0) { + crm_notice("Switching logging to %s", filename); + disable_logfile(default_fd); + } + + crm_notice("Additional logging available in %s", filename); + enable_logfile(fd); + have_logfile = true; + return pcmk_rc_ok; +} + +/*! + * \brief Add multiple additional log files + * + * \param[in] log_files Array of log files to add + * \param[in] out Output object to use for error reporting + * + * \return Standard Pacemaker return code + */ +void +pcmk__add_logfiles(gchar **log_files, pcmk__output_t *out) +{ + if (log_files == NULL) { + return; + } + + for (gchar **fname = log_files; *fname != NULL; fname++) { + int rc = pcmk__add_logfile(*fname); + + if (rc != pcmk_rc_ok) { + out->err(out, "Logging to %s is disabled: %s", + *fname, pcmk_rc_str(rc)); + } + } +} + +static int blackbox_trigger = 0; +static volatile char *blackbox_file_prefix = NULL; + +static void +blackbox_logger(int32_t t, struct qb_log_callsite *cs, log_time_t timestamp, + const char *msg) +{ + if(cs && cs->priority < LOG_ERR) { + crm_write_blackbox(SIGTRAP, cs); /* Bypass the over-dumping logic */ + } else { + crm_write_blackbox(0, cs); + } +} + +static void +crm_control_blackbox(int nsig, bool enable) +{ + int lpc = 0; + + if (blackbox_file_prefix == NULL) { + pid_t pid = getpid(); + + blackbox_file_prefix = crm_strdup_printf("%s/%s-%lu", + CRM_BLACKBOX_DIR, + crm_system_name, + (unsigned long) pid); + } + + if (enable && qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) { + qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_SIZE, 5 * 1024 * 1024); /* Any size change drops existing entries */ + qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_TRUE); /* Setting the size seems to disable it */ + + /* Enable synchronous logging */ + for (lpc = QB_LOG_BLACKBOX; lpc < QB_LOG_TARGET_MAX; lpc++) { + qb_log_ctl(lpc, QB_LOG_CONF_FILE_SYNC, QB_TRUE); + } + + crm_notice("Initiated blackbox recorder: %s", blackbox_file_prefix); + + /* Save to disk on abnormal termination */ + crm_signal_handler(SIGSEGV, crm_trigger_blackbox); + crm_signal_handler(SIGABRT, crm_trigger_blackbox); + crm_signal_handler(SIGILL, crm_trigger_blackbox); + crm_signal_handler(SIGBUS, crm_trigger_blackbox); + crm_signal_handler(SIGFPE, crm_trigger_blackbox); + + crm_update_callsites(); + + blackbox_trigger = qb_log_custom_open(blackbox_logger, NULL, NULL, NULL); + qb_log_ctl(blackbox_trigger, QB_LOG_CONF_ENABLED, QB_TRUE); + crm_trace("Trigger: %d is %d %d", blackbox_trigger, + qb_log_ctl(blackbox_trigger, QB_LOG_CONF_STATE_GET, 0), QB_LOG_STATE_ENABLED); + + crm_update_callsites(); + + } else if (!enable && qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_STATE_GET, 0) == QB_LOG_STATE_ENABLED) { + qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE); + + /* Disable synchronous logging again when the blackbox is disabled */ + for (lpc = QB_LOG_BLACKBOX; lpc < QB_LOG_TARGET_MAX; lpc++) { + qb_log_ctl(lpc, QB_LOG_CONF_FILE_SYNC, QB_FALSE); + } + } +} + +void +crm_enable_blackbox(int nsig) +{ + crm_control_blackbox(nsig, TRUE); +} + +void +crm_disable_blackbox(int nsig) +{ + crm_control_blackbox(nsig, FALSE); +} + +/*! + * \internal + * \brief Write out a blackbox, if blackboxes are enabled + * + * \param[in] nsig Signal that was received + * \param[in] cs libqb callsite + * + * \note This may be called via a true signal handler and so must be async-safe. + * @TODO actually make this async-safe + */ +void +crm_write_blackbox(int nsig, const struct qb_log_callsite *cs) +{ + static volatile int counter = 1; + static volatile time_t last = 0; + + char buffer[NAME_MAX]; + time_t now = time(NULL); + + if (blackbox_file_prefix == NULL) { + return; + } + + switch (nsig) { + case 0: + case SIGTRAP: + /* The graceful case - such as assertion failure or user request */ + + if (nsig == 0 && now == last) { + /* Prevent over-dumping */ + return; + } + + snprintf(buffer, NAME_MAX, "%s.%d", blackbox_file_prefix, counter++); + if (nsig == SIGTRAP) { + crm_notice("Blackbox dump requested, please see %s for contents", buffer); + + } else if (cs) { + syslog(LOG_NOTICE, + "Problem detected at %s:%d (%s), please see %s for additional details", + cs->function, cs->lineno, cs->filename, buffer); + } else { + crm_notice("Problem detected, please see %s for additional details", buffer); + } + + last = now; + qb_log_blackbox_write_to_file(buffer); + + /* Flush the existing contents + * A size change would also work + */ + qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE); + qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_TRUE); + break; + + default: + /* Do as little as possible, just try to get what we have out + * We logged the filename when the blackbox was enabled + */ + crm_signal_handler(nsig, SIG_DFL); + qb_log_blackbox_write_to_file((const char *)blackbox_file_prefix); + qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE); + raise(nsig); + break; + } +} + +static const char * +crm_quark_to_string(uint32_t tag) +{ + const char *text = g_quark_to_string(tag); + + if (text) { + return text; + } + return ""; +} + +static void +crm_log_filter_source(int source, const char *trace_files, const char *trace_fns, + const char *trace_fmts, const char *trace_tags, const char *trace_blackbox, + struct qb_log_callsite *cs) +{ + if (qb_log_ctl(source, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) { + return; + } else if (cs->tags != crm_trace_nonlog && source == QB_LOG_BLACKBOX) { + /* Blackbox gets everything if enabled */ + qb_bit_set(cs->targets, source); + + } else if (source == blackbox_trigger && blackbox_trigger > 0) { + /* Should this log message result in the blackbox being dumped */ + if (cs->priority <= LOG_ERR) { + qb_bit_set(cs->targets, source); + + } else if (trace_blackbox) { + char *key = crm_strdup_printf("%s:%d", cs->function, cs->lineno); + + if (strstr(trace_blackbox, key) != NULL) { + qb_bit_set(cs->targets, source); + } + free(key); + } + + } else if (source == QB_LOG_SYSLOG) { /* No tracing to syslog */ + if (cs->priority <= crm_log_priority && cs->priority <= crm_log_level) { + qb_bit_set(cs->targets, source); + } + /* Log file tracing options... */ + } else if (cs->priority <= crm_log_level) { + qb_bit_set(cs->targets, source); + } else if (trace_files && strstr(trace_files, cs->filename) != NULL) { + qb_bit_set(cs->targets, source); + } else if (trace_fns && strstr(trace_fns, cs->function) != NULL) { + qb_bit_set(cs->targets, source); + } else if (trace_fmts && strstr(trace_fmts, cs->format) != NULL) { + qb_bit_set(cs->targets, source); + } else if (trace_tags + && cs->tags != 0 + && cs->tags != crm_trace_nonlog && g_quark_to_string(cs->tags) != NULL) { + qb_bit_set(cs->targets, source); + } +} + +static void +crm_log_filter(struct qb_log_callsite *cs) +{ + int lpc = 0; + static int need_init = 1; + static const char *trace_fns = NULL; + static const char *trace_tags = NULL; + static const char *trace_fmts = NULL; + static const char *trace_files = NULL; + static const char *trace_blackbox = NULL; + + if (need_init) { + need_init = 0; + trace_fns = getenv("PCMK_trace_functions"); + trace_fmts = getenv("PCMK_trace_formats"); + trace_tags = getenv("PCMK_trace_tags"); + trace_files = getenv("PCMK_trace_files"); + trace_blackbox = getenv("PCMK_trace_blackbox"); + + if (trace_tags != NULL) { + uint32_t tag; + char token[500]; + const char *offset = NULL; + const char *next = trace_tags; + + do { + offset = next; + next = strchrnul(offset, ','); + snprintf(token, sizeof(token), "%.*s", (int)(next - offset), offset); + + tag = g_quark_from_string(token); + crm_info("Created GQuark %u from token '%s' in '%s'", tag, token, trace_tags); + + if (next[0] != 0) { + next++; + } + + } while (next != NULL && next[0] != 0); + } + } + + cs->targets = 0; /* Reset then find targets to enable */ + for (lpc = QB_LOG_SYSLOG; lpc < QB_LOG_TARGET_MAX; lpc++) { + crm_log_filter_source(lpc, trace_files, trace_fns, trace_fmts, trace_tags, trace_blackbox, + cs); + } +} + +gboolean +crm_is_callsite_active(struct qb_log_callsite *cs, uint8_t level, uint32_t tags) +{ + gboolean refilter = FALSE; + + if (cs == NULL) { + return FALSE; + } + + if (cs->priority != level) { + cs->priority = level; + refilter = TRUE; + } + + if (cs->tags != tags) { + cs->tags = tags; + refilter = TRUE; + } + + if (refilter) { + crm_log_filter(cs); + } + + if (cs->targets == 0) { + return FALSE; + } + return TRUE; +} + +void +crm_update_callsites(void) +{ + static gboolean log = TRUE; + + if (log) { + log = FALSE; + crm_debug + ("Enabling callsites based on priority=%d, files=%s, functions=%s, formats=%s, tags=%s", + crm_log_level, getenv("PCMK_trace_files"), getenv("PCMK_trace_functions"), + getenv("PCMK_trace_formats"), getenv("PCMK_trace_tags")); + } + qb_log_filter_fn_set(crm_log_filter); +} + +static gboolean +crm_tracing_enabled(void) +{ + if (crm_log_level == LOG_TRACE) { + return TRUE; + } else if (getenv("PCMK_trace_files") || getenv("PCMK_trace_functions") + || getenv("PCMK_trace_formats") || getenv("PCMK_trace_tags")) { + return TRUE; + } + return FALSE; +} + +static int +crm_priority2int(const char *name) +{ + struct syslog_names { + const char *name; + int priority; + }; + static struct syslog_names p_names[] = { + {"emerg", LOG_EMERG}, + {"alert", LOG_ALERT}, + {"crit", LOG_CRIT}, + {"error", LOG_ERR}, + {"warning", LOG_WARNING}, + {"notice", LOG_NOTICE}, + {"info", LOG_INFO}, + {"debug", LOG_DEBUG}, + {NULL, -1} + }; + int lpc; + + for (lpc = 0; name != NULL && p_names[lpc].name != NULL; lpc++) { + if (pcmk__str_eq(p_names[lpc].name, name, pcmk__str_none)) { + return p_names[lpc].priority; + } + } + return crm_log_priority; +} + + +/*! + * \internal + * \brief Set the identifier for the current process + * + * If the identifier crm_system_name is not already set, then it is set as follows: + * - it is passed to the function via the "entity" parameter, or + * - it is derived from the executable name + * + * The identifier can be used in logs, IPC, and more. + * + * This method also sets the PCMK_service environment variable. + * + * \param[in] entity If not NULL, will be assigned to the identifier + * \param[in] argc The number of command line parameters + * \param[in] argv The command line parameter values + */ +static void +set_identity(const char *entity, int argc, char *const *argv) +{ + if (crm_system_name != NULL) { + return; // Already set, don't overwrite + } + + if (entity != NULL) { + crm_system_name = strdup(entity); + + } else if ((argc > 0) && (argv != NULL)) { + char *mutable = strdup(argv[0]); + char *modified = basename(mutable); + + if (strstr(modified, "lt-") == modified) { + modified += 3; + } + crm_system_name = strdup(modified); + free(mutable); + + } else { + crm_system_name = strdup("Unknown"); + } + + CRM_ASSERT(crm_system_name != NULL); + + setenv("PCMK_service", crm_system_name, 1); +} + +void +crm_log_preinit(const char *entity, int argc, char *const *argv) +{ + /* Configure libqb logging with nothing turned on */ + + struct utsname res; + int lpc = 0; + int32_t qb_facility = 0; + pid_t pid = getpid(); + const char *nodename = "localhost"; + static bool have_logging = false; + + if (have_logging) { + return; + } + + have_logging = true; + + crm_xml_init(); /* Sets buffer allocation strategy */ + + if (crm_trace_nonlog == 0) { + crm_trace_nonlog = g_quark_from_static_string("Pacemaker non-logging tracepoint"); + } + + umask(S_IWGRP | S_IWOTH | S_IROTH); + + /* Redirect messages from glib functions to our handler */ + glib_log_default = g_log_set_default_handler(crm_glib_handler, NULL); + + /* and for good measure... - this enum is a bit field (!) */ + g_log_set_always_fatal((GLogLevelFlags) 0); /*value out of range */ + + /* Set crm_system_name, which is used as the logging name. It may also + * be used for other purposes such as an IPC client name. + */ + set_identity(entity, argc, argv); + + qb_facility = qb_log_facility2int("local0"); + qb_log_init(crm_system_name, qb_facility, LOG_ERR); + crm_log_level = LOG_CRIT; + + /* Nuke any syslog activity until it's asked for */ + qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_ENABLED, QB_FALSE); +#ifdef HAVE_qb_log_conf_QB_LOG_CONF_MAX_LINE_LEN + // Shorter than default, generous for what we *should* send to syslog + qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_MAX_LINE_LEN, 256); +#endif + if (uname(memset(&res, 0, sizeof(res))) == 0 && *res.nodename != '\0') { + nodename = res.nodename; + } + + /* Set format strings and disable threading + * Pacemaker and threads do not mix well (due to the amount of forking) + */ + qb_log_tags_stringify_fn_set(crm_quark_to_string); + for (lpc = QB_LOG_SYSLOG; lpc < QB_LOG_TARGET_MAX; lpc++) { + qb_log_ctl(lpc, QB_LOG_CONF_THREADED, QB_FALSE); +#ifdef HAVE_qb_log_conf_QB_LOG_CONF_ELLIPSIS + // End truncated lines with '...' + qb_log_ctl(lpc, QB_LOG_CONF_ELLIPSIS, QB_TRUE); +#endif + set_format_string(lpc, crm_system_name, pid, nodename); + } + +#ifdef ENABLE_NLS + /* Enable translations (experimental). Currently we only have a few + * proof-of-concept translations for some option help. The goal would be to + * offer translations for option help and man pages rather than logs or + * documentation, to reduce the burden of maintaining them. + */ + + // Load locale information for the local host from the environment + setlocale(LC_ALL, ""); + + // Tell gettext where to find Pacemaker message catalogs + CRM_ASSERT(bindtextdomain(PACKAGE, PCMK__LOCALE_DIR) != NULL); + + // Tell gettext to use the Pacemaker message catalogs + CRM_ASSERT(textdomain(PACKAGE) != NULL); + + // Tell gettext that the translated strings are stored in UTF-8 + bind_textdomain_codeset(PACKAGE, "UTF-8"); +#endif +} + +gboolean +crm_log_init(const char *entity, uint8_t level, gboolean daemon, gboolean to_stderr, + int argc, char **argv, gboolean quiet) +{ + const char *syslog_priority = NULL; + const char *facility = pcmk__env_option(PCMK__ENV_LOGFACILITY); + const char *f_copy = facility; + + pcmk__is_daemon = daemon; + crm_log_preinit(entity, argc, argv); + + if (level > LOG_TRACE) { + level = LOG_TRACE; + } + if(level > crm_log_level) { + crm_log_level = level; + } + + /* Should we log to syslog */ + if (facility == NULL) { + if (pcmk__is_daemon) { + facility = "daemon"; + } else { + facility = PCMK__VALUE_NONE; + } + pcmk__set_env_option(PCMK__ENV_LOGFACILITY, facility); + } + + if (pcmk__str_eq(facility, PCMK__VALUE_NONE, pcmk__str_casei)) { + quiet = TRUE; + + + } else { + qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_FACILITY, qb_log_facility2int(facility)); + } + + if (pcmk__env_option_enabled(crm_system_name, PCMK__ENV_DEBUG)) { + /* Override the default setting */ + crm_log_level = LOG_DEBUG; + } + + /* What lower threshold do we have for sending to syslog */ + syslog_priority = pcmk__env_option(PCMK__ENV_LOGPRIORITY); + if (syslog_priority) { + crm_log_priority = crm_priority2int(syslog_priority); + } + qb_log_filter_ctl(QB_LOG_SYSLOG, QB_LOG_FILTER_ADD, QB_LOG_FILTER_FILE, "*", + crm_log_priority); + + // Log to syslog unless requested to be quiet + if (!quiet) { + qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_ENABLED, QB_TRUE); + } + + /* Should we log to stderr */ + if (pcmk__env_option_enabled(crm_system_name, PCMK__ENV_STDERR)) { + /* Override the default setting */ + to_stderr = TRUE; + } + crm_enable_stderr(to_stderr); + + // Log to a file if we're a daemon or user asked for one + { + const char *logfile = pcmk__env_option(PCMK__ENV_LOGFILE); + + 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); + } + } + + if (pcmk__is_daemon + && pcmk__env_option_enabled(crm_system_name, PCMK__ENV_BLACKBOX)) { + crm_enable_blackbox(0); + } + + /* Summary */ + crm_trace("Quiet: %d, facility %s", quiet, f_copy); + pcmk__env_option(PCMK__ENV_LOGFILE); + pcmk__env_option(PCMK__ENV_LOGFACILITY); + + crm_update_callsites(); + + /* Ok, now we can start logging... */ + + // Disable daemon request if user isn't root or Pacemaker daemon user + if (pcmk__is_daemon) { + const char *user = getenv("USER"); + + if (user != NULL && !pcmk__strcase_any_of(user, "root", CRM_DAEMON_USER, NULL)) { + crm_trace("Not switching to corefile directory for %s", user); + pcmk__is_daemon = false; + } + } + + if (pcmk__is_daemon) { + int user = getuid(); + struct passwd *pwent = getpwuid(user); + + if (pwent == NULL) { + crm_perror(LOG_ERR, "Cannot get name for uid: %d", user); + + } else if (!pcmk__strcase_any_of(pwent->pw_name, "root", CRM_DAEMON_USER, NULL)) { + crm_trace("Don't change active directory for regular user: %s", pwent->pw_name); + + } else if (chdir(CRM_CORE_DIR) < 0) { + crm_perror(LOG_INFO, "Cannot change active directory to " CRM_CORE_DIR); + + } else { + crm_info("Changed active directory to " CRM_CORE_DIR); + } + + /* Original meanings from signal(7) + * + * Signal Value Action Comment + * SIGTRAP 5 Core Trace/breakpoint trap + * SIGUSR1 30,10,16 Term User-defined signal 1 + * SIGUSR2 31,12,17 Term User-defined signal 2 + * + * Our usage is as similar as possible + */ + mainloop_add_signal(SIGUSR1, crm_enable_blackbox); + mainloop_add_signal(SIGUSR2, crm_disable_blackbox); + mainloop_add_signal(SIGTRAP, crm_trigger_blackbox); + + } else if (!quiet) { + crm_log_args(argc, argv); + } + + return TRUE; +} + +/* returns the old value */ +unsigned int +set_crm_log_level(unsigned int level) +{ + unsigned int old = crm_log_level; + + if (level > LOG_TRACE) { + level = LOG_TRACE; + } + crm_log_level = level; + crm_update_callsites(); + crm_trace("New log level: %d", level); + return old; +} + +void +crm_enable_stderr(int enable) +{ + if (enable && qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) { + qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_ENABLED, QB_TRUE); + crm_update_callsites(); + + } else if (enable == FALSE) { + qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_ENABLED, QB_FALSE); + } +} + +/*! + * \brief Make logging more verbose + * + * If logging to stderr is not already enabled when this function is called, + * enable it. Otherwise, increase the log level by 1. + * + * \param[in] argc Ignored + * \param[in] argv Ignored + */ +void +crm_bump_log_level(int argc, char **argv) +{ + if (qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_STATE_GET, 0) + != QB_LOG_STATE_ENABLED) { + crm_enable_stderr(TRUE); + } else { + set_crm_log_level(crm_log_level + 1); + } +} + +unsigned int +get_crm_log_level(void) +{ + return crm_log_level; +} + +/*! + * \brief Log the command line (once) + * + * \param[in] Number of values in \p argv + * \param[in] Command-line arguments (including command name) + * + * \note This function will only log once, even if called with different + * arguments. + */ +void +crm_log_args(int argc, char **argv) +{ + static bool logged = false; + gchar *arg_string = NULL; + + if ((argc == 0) || (argv == NULL) || logged) { + return; + } + logged = true; + arg_string = g_strjoinv(" ", argv); + crm_notice("Invoked: %s", arg_string); + g_free(arg_string); +} + +void +crm_log_output_fn(const char *file, const char *function, int line, int level, const char *prefix, + const char *output) +{ + const char *next = NULL; + const char *offset = NULL; + + if (level == LOG_NEVER) { + return; + } + + if (output == NULL) { + if (level != LOG_STDOUT) { + level = LOG_TRACE; + } + output = "-- empty --"; + } + + next = output; + do { + offset = next; + next = strchrnul(offset, '\n'); + do_crm_log_alias(level, file, function, line, "%s [ %.*s ]", prefix, + (int)(next - offset), offset); + if (next[0] != 0) { + next++; + } + + } while (next != NULL && next[0] != 0); +} + +void +pcmk__cli_init_logging(const char *name, unsigned int verbosity) +{ + crm_log_init(name, LOG_ERR, FALSE, FALSE, 0, NULL, TRUE); + + for (int i = 0; i < verbosity; i++) { + /* These arguments are ignored, so pass placeholders. */ + crm_bump_log_level(0, NULL); + } +} + +/*! + * \brief Log XML line-by-line in a formatted fashion + * + * \param[in] level Priority at which to log the messages + * \param[in] text Prefix for each line + * \param[in] xml XML to log + * + * \note This does nothing when \p level is \p LOG_STDOUT. + * \note Do not call this function directly. It should be called only from the + * \p do_crm_log_xml() macro. + */ +void +pcmk_log_xml_impl(uint8_t level, const char *text, const xmlNode *xml) +{ + if (xml == NULL) { + do_crm_log(level, "%s%sNo data to dump as XML", + pcmk__s(text, ""), pcmk__str_empty(text)? "" : " "); + + } else { + if (logger_out == NULL) { + CRM_CHECK(pcmk__log_output_new(&logger_out) == pcmk_rc_ok, return); + } + + pcmk__output_set_log_level(logger_out, level); + pcmk__xml_show(logger_out, text, xml, 1, + pcmk__xml_fmt_pretty + |pcmk__xml_fmt_open + |pcmk__xml_fmt_children + |pcmk__xml_fmt_close); + } +} + +/*! + * \internal + * \brief Free the logging library's internal log output object + */ +void +pcmk__free_common_logger(void) +{ + if (logger_out != NULL) { + logger_out->finish(logger_out, CRM_EX_OK, true, NULL); + pcmk__output_free(logger_out); + logger_out = NULL; + } +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include <crm/common/logging_compat.h> + +gboolean +crm_log_cli_init(const char *entity) +{ + pcmk__cli_init_logging(entity, 0); + return TRUE; +} + +gboolean +crm_add_logfile(const char *filename) +{ + return pcmk__add_logfile(filename) == pcmk_rc_ok; +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/mainloop.c b/lib/common/mainloop.c new file mode 100644 index 0000000..3124e43 --- /dev/null +++ b/lib/common/mainloop.c @@ -0,0 +1,1480 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include <stdlib.h> +#include <string.h> +#include <signal.h> +#include <errno.h> + +#include <sys/wait.h> + +#include <crm/crm.h> +#include <crm/common/xml.h> +#include <crm/common/mainloop.h> +#include <crm/common/ipc_internal.h> + +#include <qb/qbarray.h> + +struct mainloop_child_s { + pid_t pid; + char *desc; + unsigned timerid; + gboolean timeout; + void *privatedata; + + enum mainloop_child_flags flags; + + /* Called when a process dies */ + void (*callback) (mainloop_child_t * p, pid_t pid, int core, int signo, int exitcode); +}; + +struct trigger_s { + GSource source; + gboolean running; + gboolean trigger; + void *user_data; + guint id; + +}; + +struct mainloop_timer_s { + guint id; + guint period_ms; + bool repeat; + char *name; + GSourceFunc cb; + void *userdata; +}; + +static gboolean +crm_trigger_prepare(GSource * source, gint * timeout) +{ + crm_trigger_t *trig = (crm_trigger_t *) source; + + /* cluster-glue's FD and IPC related sources make use of + * g_source_add_poll() but do not set a timeout in their prepare + * functions + * + * This means mainloop's poll() will block until an event for one + * of these sources occurs - any /other/ type of source, such as + * this one or g_idle_*, that doesn't use g_source_add_poll() is + * S-O-L and won't be processed until there is something fd-based + * happens. + * + * Luckily the timeout we can set here affects all sources and + * puts an upper limit on how long poll() can take. + * + * So unconditionally set a small-ish timeout, not too small that + * we're in constant motion, which will act as an upper bound on + * how long the signal handling might be delayed for. + */ + *timeout = 500; /* Timeout in ms */ + + return trig->trigger; +} + +static gboolean +crm_trigger_check(GSource * source) +{ + crm_trigger_t *trig = (crm_trigger_t *) source; + + return trig->trigger; +} + +/*! + * \internal + * \brief GSource dispatch function for crm_trigger_t + * + * \param[in] source crm_trigger_t being dispatched + * \param[in] callback Callback passed at source creation + * \param[in,out] userdata User data passed at source creation + * + * \return G_SOURCE_REMOVE to remove source, G_SOURCE_CONTINUE to keep it + */ +static gboolean +crm_trigger_dispatch(GSource *source, GSourceFunc callback, gpointer userdata) +{ + gboolean rc = G_SOURCE_CONTINUE; + crm_trigger_t *trig = (crm_trigger_t *) source; + + if (trig->running) { + /* Wait until the existing job is complete before starting the next one */ + return G_SOURCE_CONTINUE; + } + trig->trigger = FALSE; + + if (callback) { + int callback_rc = callback(trig->user_data); + + if (callback_rc < 0) { + crm_trace("Trigger handler %p not yet complete", trig); + trig->running = TRUE; + } else if (callback_rc == 0) { + rc = G_SOURCE_REMOVE; + } + } + return rc; +} + +static void +crm_trigger_finalize(GSource * source) +{ + crm_trace("Trigger %p destroyed", source); +} + +static GSourceFuncs crm_trigger_funcs = { + crm_trigger_prepare, + crm_trigger_check, + crm_trigger_dispatch, + crm_trigger_finalize, +}; + +static crm_trigger_t * +mainloop_setup_trigger(GSource * source, int priority, int (*dispatch) (gpointer user_data), + gpointer userdata) +{ + crm_trigger_t *trigger = NULL; + + trigger = (crm_trigger_t *) source; + + trigger->id = 0; + trigger->trigger = FALSE; + trigger->user_data = userdata; + + if (dispatch) { + g_source_set_callback(source, dispatch, trigger, NULL); + } + + g_source_set_priority(source, priority); + g_source_set_can_recurse(source, FALSE); + + trigger->id = g_source_attach(source, NULL); + return trigger; +} + +void +mainloop_trigger_complete(crm_trigger_t * trig) +{ + crm_trace("Trigger handler %p complete", trig); + trig->running = FALSE; +} + +/*! + * \brief Create a trigger to be used as a mainloop source + * + * \param[in] priority Relative priority of source (lower number is higher priority) + * \param[in] dispatch Trigger dispatch function (should return 0 to remove the + * trigger from the mainloop, -1 if the trigger should be + * kept but the job is still running and not complete, and + * 1 if the trigger should be kept and the job is complete) + * \param[in] userdata Pointer to pass to \p dispatch + * + * \return Newly allocated mainloop source for trigger + */ +crm_trigger_t * +mainloop_add_trigger(int priority, int (*dispatch) (gpointer user_data), + gpointer userdata) +{ + GSource *source = NULL; + + 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); +} + +void +mainloop_set_trigger(crm_trigger_t * source) +{ + if(source) { + source->trigger = TRUE; + } +} + +gboolean +mainloop_destroy_trigger(crm_trigger_t * source) +{ + GSource *gs = NULL; + + if(source == NULL) { + return TRUE; + } + + gs = (GSource *)source; + + g_source_destroy(gs); /* Remove from mainloop, ref_count-- */ + g_source_unref(gs); /* The caller no longer carries a reference to source + * + * At this point the source should be free'd, + * unless we're currently processing said + * source, in which case mainloop holds an + * additional reference and it will be free'd + * once our processing completes + */ + return TRUE; +} + +// Define a custom glib source for signal handling + +// Data structure for custom glib source +typedef struct signal_s { + crm_trigger_t trigger; // trigger that invoked source (must be first) + void (*handler) (int sig); // signal handler + int signal; // signal that was received +} crm_signal_t; + +// Table to associate signal handlers with signal numbers +static crm_signal_t *crm_signals[NSIG]; + +/*! + * \internal + * \brief Dispatch an event from custom glib source for signals + * + * Given an signal event, clear the event trigger and call any registered + * signal handler. + * + * \param[in] source glib source that triggered this dispatch + * \param[in] callback (ignored) + * \param[in] userdata (ignored) + */ +static gboolean +crm_signal_dispatch(GSource *source, GSourceFunc callback, gpointer userdata) +{ + crm_signal_t *sig = (crm_signal_t *) source; + + if(sig->signal != SIGCHLD) { + crm_notice("Caught '%s' signal "CRM_XS" %d (%s handler)", + strsignal(sig->signal), sig->signal, + (sig->handler? "invoking" : "no")); + } + + sig->trigger.trigger = FALSE; + if (sig->handler) { + sig->handler(sig->signal); + } + return TRUE; +} + +/*! + * \internal + * \brief Handle a signal by setting a trigger for signal source + * + * \param[in] sig Signal number that was received + * + * \note This is the true signal handler for the mainloop signal source, and + * must be async-safe. + */ +static void +mainloop_signal_handler(int sig) +{ + if (sig > 0 && sig < NSIG && crm_signals[sig] != NULL) { + mainloop_set_trigger((crm_trigger_t *) crm_signals[sig]); + } +} + +// Functions implementing our custom glib source for signal handling +static GSourceFuncs crm_signal_funcs = { + crm_trigger_prepare, + crm_trigger_check, + crm_signal_dispatch, + crm_trigger_finalize, +}; + +/*! + * \internal + * \brief Set a true signal handler + * + * signal()-like interface to sigaction() + * + * \param[in] sig Signal number to register handler for + * \param[in] dispatch Signal handler + * + * \return The previous value of the signal handler, or SIG_ERR on error + * \note The dispatch function must be async-safe. + */ +sighandler_t +crm_signal_handler(int sig, sighandler_t dispatch) +{ + sigset_t mask; + struct sigaction sa; + struct sigaction old; + + if (sigemptyset(&mask) < 0) { + crm_err("Could not set handler for signal %d: %s", + sig, pcmk_rc_str(errno)); + return SIG_ERR; + } + + memset(&sa, 0, sizeof(struct sigaction)); + sa.sa_handler = dispatch; + sa.sa_flags = SA_RESTART; + sa.sa_mask = mask; + + if (sigaction(sig, &sa, &old) < 0) { + crm_err("Could not set handler for signal %d: %s", + sig, pcmk_rc_str(errno)); + return SIG_ERR; + } + return old.sa_handler; +} + +static void +mainloop_destroy_signal_entry(int sig) +{ + crm_signal_t *tmp = crm_signals[sig]; + + crm_signals[sig] = NULL; + + crm_trace("Destroying signal %d", sig); + mainloop_destroy_trigger((crm_trigger_t *) tmp); +} + +/*! + * \internal + * \brief Add a signal handler to a mainloop + * + * \param[in] sig Signal number to handle + * \param[in] dispatch Signal handler function + * + * \note The true signal handler merely sets a mainloop trigger to call this + * dispatch function via the mainloop. Therefore, the dispatch function + * does not need to be async-safe. + */ +gboolean +mainloop_add_signal(int sig, void (*dispatch) (int sig)) +{ + GSource *source = NULL; + int priority = G_PRIORITY_HIGH - 1; + + if (sig == SIGTERM) { + /* TERM is higher priority than other signals, + * signals are higher priority than other ipc. + * Yes, minus: smaller is "higher" + */ + priority--; + } + + if (sig >= NSIG || sig < 0) { + crm_err("Signal %d is out of range", sig); + return FALSE; + + } else if (crm_signals[sig] != NULL && crm_signals[sig]->handler == dispatch) { + crm_trace("Signal handler for %d is already installed", sig); + return TRUE; + + } else if (crm_signals[sig] != NULL) { + crm_err("Different signal handler for %d is already installed", sig); + return FALSE; + } + + CRM_ASSERT(sizeof(crm_signal_t) > sizeof(GSource)); + source = g_source_new(&crm_signal_funcs, sizeof(crm_signal_t)); + + crm_signals[sig] = (crm_signal_t *) mainloop_setup_trigger(source, priority, NULL, NULL); + CRM_ASSERT(crm_signals[sig] != NULL); + + crm_signals[sig]->handler = dispatch; + crm_signals[sig]->signal = sig; + + if (crm_signal_handler(sig, mainloop_signal_handler) == SIG_ERR) { + mainloop_destroy_signal_entry(sig); + return FALSE; + } +#if 0 + /* If we want signals to interrupt mainloop's poll(), instead of waiting for + * the timeout, then we should call siginterrupt() below + * + * For now, just enforce a low timeout + */ + if (siginterrupt(sig, 1) < 0) { + crm_perror(LOG_INFO, "Could not enable system call interruptions for signal %d", sig); + } +#endif + + return TRUE; +} + +gboolean +mainloop_destroy_signal(int sig) +{ + if (sig >= NSIG || sig < 0) { + crm_err("Signal %d is out of range", sig); + return FALSE; + + } else if (crm_signal_handler(sig, NULL) == SIG_ERR) { + crm_perror(LOG_ERR, "Could not uninstall signal handler for signal %d", sig); + return FALSE; + + } else if (crm_signals[sig] == NULL) { + return TRUE; + } + mainloop_destroy_signal_entry(sig); + return TRUE; +} + +static qb_array_t *gio_map = NULL; + +void +mainloop_cleanup(void) +{ + if (gio_map) { + qb_array_free(gio_map); + } + + for (int sig = 0; sig < NSIG; ++sig) { + mainloop_destroy_signal_entry(sig); + } +} + +/* + * libqb... + */ +struct gio_to_qb_poll { + int32_t is_used; + guint source; + int32_t events; + void *data; + qb_ipcs_dispatch_fn_t fn; + enum qb_loop_priority p; +}; + +static gboolean +gio_read_socket(GIOChannel * gio, GIOCondition condition, gpointer data) +{ + struct gio_to_qb_poll *adaptor = (struct gio_to_qb_poll *)data; + gint fd = g_io_channel_unix_get_fd(gio); + + crm_trace("%p.%d %d", data, fd, condition); + + /* if this assert get's hit, then there is a race condition between + * when we destroy a fd and when mainloop actually gives it up */ + CRM_ASSERT(adaptor->is_used > 0); + + return (adaptor->fn(fd, condition, adaptor->data) == 0); +} + +static void +gio_poll_destroy(gpointer data) +{ + struct gio_to_qb_poll *adaptor = (struct gio_to_qb_poll *)data; + + adaptor->is_used--; + CRM_ASSERT(adaptor->is_used >= 0); + + if (adaptor->is_used == 0) { + crm_trace("Marking adaptor %p unused", adaptor); + adaptor->source = 0; + } +} + +/*! + * \internal + * \brief Convert libqb's poll priority into GLib's one + * + * \param[in] prio libqb's poll priority (#QB_LOOP_MED assumed as fallback) + * + * \return best matching GLib's priority + */ +static gint +conv_prio_libqb2glib(enum qb_loop_priority prio) +{ + switch (prio) { + case QB_LOOP_LOW: return G_PRIORITY_LOW; + case QB_LOOP_HIGH: return G_PRIORITY_HIGH; + default: return G_PRIORITY_DEFAULT; // QB_LOOP_MED + } +} + +/*! + * \internal + * \brief Convert libqb's poll priority to rate limiting spec + * + * \param[in] prio libqb's poll priority (#QB_LOOP_MED assumed as fallback) + * + * \return best matching rate limiting spec + * \note This is the inverse of libqb's qb_ipcs_request_rate_limit(). + */ +static enum qb_ipcs_rate_limit +conv_libqb_prio2ratelimit(enum qb_loop_priority prio) +{ + switch (prio) { + case QB_LOOP_LOW: return QB_IPCS_RATE_SLOW; + case QB_LOOP_HIGH: return QB_IPCS_RATE_FAST; + default: return QB_IPCS_RATE_NORMAL; // QB_LOOP_MED + } +} + +static int32_t +gio_poll_dispatch_update(enum qb_loop_priority p, int32_t fd, int32_t evts, + void *data, qb_ipcs_dispatch_fn_t fn, int32_t add) +{ + struct gio_to_qb_poll *adaptor; + GIOChannel *channel; + int32_t res = 0; + + res = qb_array_index(gio_map, fd, (void **)&adaptor); + if (res < 0) { + crm_err("Array lookup failed for fd=%d: %d", fd, res); + return res; + } + + crm_trace("Adding fd=%d to mainloop as adaptor %p", fd, adaptor); + + if (add && adaptor->source) { + crm_err("Adaptor for descriptor %d is still in-use", fd); + return -EEXIST; + } + if (!add && !adaptor->is_used) { + crm_err("Adaptor for descriptor %d is not in-use", fd); + return -ENOENT; + } + + /* channel is created with ref_count = 1 */ + channel = g_io_channel_unix_new(fd); + if (!channel) { + crm_err("No memory left to add fd=%d", fd); + return -ENOMEM; + } + + if (adaptor->source) { + g_source_remove(adaptor->source); + adaptor->source = 0; + } + + /* Because unlike the poll() API, glib doesn't tell us about HUPs by default */ + evts |= (G_IO_HUP | G_IO_NVAL | G_IO_ERR); + + adaptor->fn = fn; + adaptor->events = evts; + adaptor->data = data; + adaptor->p = p; + adaptor->is_used++; + adaptor->source = + g_io_add_watch_full(channel, conv_prio_libqb2glib(p), evts, + gio_read_socket, adaptor, gio_poll_destroy); + + /* Now that mainloop now holds a reference to channel, + * thanks to g_io_add_watch_full(), drop ours from g_io_channel_unix_new(). + * + * This means that channel will be free'd by: + * g_main_context_dispatch() + * -> g_source_destroy_internal() + * -> g_source_callback_unref() + * shortly after gio_poll_destroy() completes + */ + g_io_channel_unref(channel); + + crm_trace("Added to mainloop with gsource id=%d", adaptor->source); + if (adaptor->source > 0) { + return 0; + } + + return -EINVAL; +} + +static int32_t +gio_poll_dispatch_add(enum qb_loop_priority p, int32_t fd, int32_t evts, + void *data, qb_ipcs_dispatch_fn_t fn) +{ + return gio_poll_dispatch_update(p, fd, evts, data, fn, QB_TRUE); +} + +static int32_t +gio_poll_dispatch_mod(enum qb_loop_priority p, int32_t fd, int32_t evts, + void *data, qb_ipcs_dispatch_fn_t fn) +{ + return gio_poll_dispatch_update(p, fd, evts, data, fn, QB_FALSE); +} + +static int32_t +gio_poll_dispatch_del(int32_t fd) +{ + struct gio_to_qb_poll *adaptor; + + crm_trace("Looking for fd=%d", fd); + if (qb_array_index(gio_map, fd, (void **)&adaptor) == 0) { + if (adaptor->source) { + g_source_remove(adaptor->source); + adaptor->source = 0; + } + } + return 0; +} + +struct qb_ipcs_poll_handlers gio_poll_funcs = { + .job_add = NULL, + .dispatch_add = gio_poll_dispatch_add, + .dispatch_mod = gio_poll_dispatch_mod, + .dispatch_del = gio_poll_dispatch_del, +}; + +static enum qb_ipc_type +pick_ipc_type(enum qb_ipc_type requested) +{ + const char *env = getenv("PCMK_ipc_type"); + + if (env && strcmp("shared-mem", env) == 0) { + return QB_IPC_SHM; + } else if (env && strcmp("socket", env) == 0) { + return QB_IPC_SOCKET; + } else if (env && strcmp("posix", env) == 0) { + return QB_IPC_POSIX_MQ; + } else if (env && strcmp("sysv", env) == 0) { + return QB_IPC_SYSV_MQ; + } else if (requested == QB_IPC_NATIVE) { + /* We prefer shared memory because the server never blocks on + * send. If part of a message fits into the socket, libqb + * needs to block until the remainder can be sent also. + * Otherwise the client will wait forever for the remaining + * bytes. + */ + return QB_IPC_SHM; + } + return requested; +} + +qb_ipcs_service_t * +mainloop_add_ipc_server(const char *name, enum qb_ipc_type type, + struct qb_ipcs_service_handlers *callbacks) +{ + return mainloop_add_ipc_server_with_prio(name, type, callbacks, QB_LOOP_MED); +} + +qb_ipcs_service_t * +mainloop_add_ipc_server_with_prio(const char *name, enum qb_ipc_type type, + struct qb_ipcs_service_handlers *callbacks, + enum qb_loop_priority prio) +{ + int rc = 0; + qb_ipcs_service_t *server = NULL; + + if (gio_map == NULL) { + gio_map = qb_array_create_2(64, sizeof(struct gio_to_qb_poll), 1); + } + + server = qb_ipcs_create(name, 0, pick_ipc_type(type), callbacks); + + if (server == NULL) { + crm_err("Could not create %s IPC server: %s (%d)", name, pcmk_strerror(rc), rc); + return NULL; + } + + if (prio != QB_LOOP_MED) { + qb_ipcs_request_rate_limit(server, conv_libqb_prio2ratelimit(prio)); + } + + /* All clients should use at least ipc_buffer_max as their buffer size */ + qb_ipcs_enforce_buffer_size(server, crm_ipc_default_buffer_size()); + qb_ipcs_poll_handlers_set(server, &gio_poll_funcs); + + rc = qb_ipcs_run(server); + if (rc < 0) { + crm_err("Could not start %s IPC server: %s (%d)", name, pcmk_strerror(rc), rc); + return NULL; // qb_ipcs_run() destroys server on failure + } + + return server; +} + +void +mainloop_del_ipc_server(qb_ipcs_service_t * server) +{ + if (server) { + qb_ipcs_destroy(server); + } +} + +struct mainloop_io_s { + char *name; + void *userdata; + + int fd; + guint source; + crm_ipc_t *ipc; + GIOChannel *channel; + + int (*dispatch_fn_ipc) (const char *buffer, ssize_t length, gpointer userdata); + int (*dispatch_fn_io) (gpointer userdata); + void (*destroy_fn) (gpointer userdata); + +}; + +/*! + * \internal + * \brief I/O watch callback function (GIOFunc) + * + * \param[in] gio I/O channel being watched + * \param[in] condition I/O condition satisfied + * \param[in] data User data passed when source was created + * + * \return G_SOURCE_REMOVE to remove source, G_SOURCE_CONTINUE to keep it + */ +static gboolean +mainloop_gio_callback(GIOChannel *gio, GIOCondition condition, gpointer data) +{ + gboolean rc = G_SOURCE_CONTINUE; + mainloop_io_t *client = data; + + CRM_ASSERT(client->fd == g_io_channel_unix_get_fd(gio)); + + if (condition & G_IO_IN) { + if (client->ipc) { + long read_rc = 0L; + int max = 10; + + do { + read_rc = crm_ipc_read(client->ipc); + if (read_rc <= 0) { + crm_trace("Could not read IPC message from %s: %s (%ld)", + client->name, pcmk_strerror(read_rc), read_rc); + + } else if (client->dispatch_fn_ipc) { + const char *buffer = crm_ipc_buffer(client->ipc); + + crm_trace("New %ld-byte IPC message from %s " + "after I/O condition %d", + read_rc, client->name, (int) condition); + if (client->dispatch_fn_ipc(buffer, read_rc, client->userdata) < 0) { + crm_trace("Connection to %s no longer required", client->name); + rc = G_SOURCE_REMOVE; + } + } + + } while ((rc == G_SOURCE_CONTINUE) && (read_rc > 0) && --max > 0); + + } else { + crm_trace("New I/O event for %s after I/O condition %d", + client->name, (int) condition); + if (client->dispatch_fn_io) { + if (client->dispatch_fn_io(client->userdata) < 0) { + crm_trace("Connection to %s no longer required", client->name); + rc = G_SOURCE_REMOVE; + } + } + } + } + + if (client->ipc && !crm_ipc_connected(client->ipc)) { + crm_err("Connection to %s closed " CRM_XS "client=%p condition=%d", + client->name, client, condition); + rc = G_SOURCE_REMOVE; + + } else if (condition & (G_IO_HUP | G_IO_NVAL | G_IO_ERR)) { + crm_trace("The connection %s[%p] has been closed (I/O condition=%d)", + client->name, client, condition); + rc = G_SOURCE_REMOVE; + + } else if ((condition & G_IO_IN) == 0) { + /* + #define GLIB_SYSDEF_POLLIN =1 + #define GLIB_SYSDEF_POLLPRI =2 + #define GLIB_SYSDEF_POLLOUT =4 + #define GLIB_SYSDEF_POLLERR =8 + #define GLIB_SYSDEF_POLLHUP =16 + #define GLIB_SYSDEF_POLLNVAL =32 + + typedef enum + { + G_IO_IN GLIB_SYSDEF_POLLIN, + G_IO_OUT GLIB_SYSDEF_POLLOUT, + G_IO_PRI GLIB_SYSDEF_POLLPRI, + G_IO_ERR GLIB_SYSDEF_POLLERR, + G_IO_HUP GLIB_SYSDEF_POLLHUP, + G_IO_NVAL GLIB_SYSDEF_POLLNVAL + } GIOCondition; + + A bitwise combination representing a condition to watch for on an event source. + + G_IO_IN There is data to read. + G_IO_OUT Data can be written (without blocking). + G_IO_PRI There is urgent data to read. + G_IO_ERR Error condition. + G_IO_HUP Hung up (the connection has been broken, usually for pipes and sockets). + G_IO_NVAL Invalid request. The file descriptor is not open. + */ + crm_err("Strange condition: %d", condition); + } + + /* G_SOURCE_REMOVE results in mainloop_gio_destroy() being called + * just before the source is removed from mainloop + */ + return rc; +} + +static void +mainloop_gio_destroy(gpointer c) +{ + mainloop_io_t *client = c; + char *c_name = strdup(client->name); + + /* client->source is valid but about to be destroyed (ref_count == 0) in gmain.c + * client->channel will still have ref_count > 0... should be == 1 + */ + crm_trace("Destroying client %s[%p]", c_name, c); + + if (client->ipc) { + crm_ipc_close(client->ipc); + } + + if (client->destroy_fn) { + void (*destroy_fn) (gpointer userdata) = client->destroy_fn; + + client->destroy_fn = NULL; + destroy_fn(client->userdata); + } + + if (client->ipc) { + crm_ipc_t *ipc = client->ipc; + + client->ipc = NULL; + crm_ipc_destroy(ipc); + } + + crm_trace("Destroyed client %s[%p]", c_name, c); + + free(client->name); client->name = NULL; + free(client); + + free(c_name); +} + +/*! + * \brief Connect to IPC and add it as a main loop source + * + * \param[in,out] ipc IPC connection to add + * \param[in] priority Event source priority to use for connection + * \param[in] userdata Data to register with callbacks + * \param[in] callbacks Dispatch and destroy callbacks for connection + * \param[out] source Newly allocated event source + * + * \return Standard Pacemaker return code + * + * \note On failure, the caller is still responsible for ipc. On success, the + * caller should call mainloop_del_ipc_client() when source is no longer + * needed, which will lead to the disconnection of the IPC later in the + * main loop if it is connected. However the IPC disconnects, + * mainloop_gio_destroy() will free ipc and source after calling the + * destroy callback. + */ +int +pcmk__add_mainloop_ipc(crm_ipc_t *ipc, int priority, void *userdata, + const struct ipc_client_callbacks *callbacks, + mainloop_io_t **source) +{ + CRM_CHECK((ipc != NULL) && (callbacks != NULL), return EINVAL); + + if (!crm_ipc_connect(ipc)) { + int rc = errno; + crm_debug("Connection to %s failed: %d", crm_ipc_name(ipc), errno); + return rc; + } + *source = mainloop_add_fd(crm_ipc_name(ipc), priority, crm_ipc_get_fd(ipc), + userdata, NULL); + if (*source == NULL) { + int rc = errno; + + crm_ipc_close(ipc); + return rc; + } + (*source)->ipc = ipc; + (*source)->destroy_fn = callbacks->destroy; + (*source)->dispatch_fn_ipc = callbacks->dispatch; + return pcmk_rc_ok; +} + +/*! + * \brief Get period for mainloop timer + * + * \param[in] timer Timer + * + * \return Period in ms + */ +guint +pcmk__mainloop_timer_get_period(const mainloop_timer_t *timer) +{ + if (timer) { + return timer->period_ms; + } + return 0; +} + +mainloop_io_t * +mainloop_add_ipc_client(const char *name, int priority, size_t max_size, + void *userdata, struct ipc_client_callbacks *callbacks) +{ + crm_ipc_t *ipc = crm_ipc_new(name, max_size); + mainloop_io_t *source = NULL; + int rc = pcmk__add_mainloop_ipc(ipc, priority, userdata, callbacks, + &source); + + if (rc != pcmk_rc_ok) { + if (crm_log_level == LOG_STDOUT) { + fprintf(stderr, "Connection to %s failed: %s", + name, pcmk_rc_str(rc)); + } + crm_ipc_destroy(ipc); + if (rc > 0) { + errno = rc; + } else { + errno = ENOTCONN; + } + return NULL; + } + return source; +} + +void +mainloop_del_ipc_client(mainloop_io_t * client) +{ + mainloop_del_fd(client); +} + +crm_ipc_t * +mainloop_get_ipc_client(mainloop_io_t * client) +{ + if (client) { + return client->ipc; + } + return NULL; +} + +mainloop_io_t * +mainloop_add_fd(const char *name, int priority, int fd, void *userdata, + struct mainloop_fd_callbacks * callbacks) +{ + mainloop_io_t *client = NULL; + + if (fd >= 0) { + client = calloc(1, sizeof(mainloop_io_t)); + if (client == NULL) { + return NULL; + } + client->name = strdup(name); + client->userdata = userdata; + + if (callbacks) { + client->destroy_fn = callbacks->destroy; + client->dispatch_fn_io = callbacks->dispatch; + } + + client->fd = fd; + client->channel = g_io_channel_unix_new(fd); + client->source = + g_io_add_watch_full(client->channel, priority, + (G_IO_IN | G_IO_HUP | G_IO_NVAL | G_IO_ERR), mainloop_gio_callback, + client, mainloop_gio_destroy); + + /* Now that mainloop now holds a reference to channel, + * thanks to g_io_add_watch_full(), drop ours from g_io_channel_unix_new(). + * + * This means that channel will be free'd by: + * g_main_context_dispatch() or g_source_remove() + * -> g_source_destroy_internal() + * -> g_source_callback_unref() + * shortly after mainloop_gio_destroy() completes + */ + g_io_channel_unref(client->channel); + crm_trace("Added connection %d for %s[%p].%d", client->source, client->name, client, fd); + } else { + errno = EINVAL; + } + + return client; +} + +void +mainloop_del_fd(mainloop_io_t * client) +{ + if (client != NULL) { + crm_trace("Removing client %s[%p]", client->name, client); + if (client->source) { + /* Results in mainloop_gio_destroy() being called just + * before the source is removed from mainloop + */ + g_source_remove(client->source); + } + } +} + +static GList *child_list = NULL; + +pid_t +mainloop_child_pid(mainloop_child_t * child) +{ + return child->pid; +} + +const char * +mainloop_child_name(mainloop_child_t * child) +{ + return child->desc; +} + +int +mainloop_child_timeout(mainloop_child_t * child) +{ + return child->timeout; +} + +void * +mainloop_child_userdata(mainloop_child_t * child) +{ + return child->privatedata; +} + +void +mainloop_clear_child_userdata(mainloop_child_t * child) +{ + child->privatedata = NULL; +} + +/* good function name */ +static void +child_free(mainloop_child_t *child) +{ + if (child->timerid != 0) { + crm_trace("Removing timer %d", child->timerid); + g_source_remove(child->timerid); + child->timerid = 0; + } + free(child->desc); + free(child); +} + +/* terrible function name */ +static int +child_kill_helper(mainloop_child_t *child) +{ + int rc; + if (child->flags & mainloop_leave_pid_group) { + crm_debug("Kill pid %d only. leave group intact.", child->pid); + rc = kill(child->pid, SIGKILL); + } else { + crm_debug("Kill pid %d's group", child->pid); + rc = kill(-child->pid, SIGKILL); + } + + if (rc < 0) { + if (errno != ESRCH) { + crm_perror(LOG_ERR, "kill(%d, KILL) failed", child->pid); + } + return -errno; + } + return 0; +} + +static gboolean +child_timeout_callback(gpointer p) +{ + mainloop_child_t *child = p; + int rc = 0; + + child->timerid = 0; + if (child->timeout) { + crm_warn("%s process (PID %d) will not die!", child->desc, (int)child->pid); + return FALSE; + } + + rc = child_kill_helper(child); + if (rc == -ESRCH) { + /* Nothing left to do. pid doesn't exist */ + return FALSE; + } + + child->timeout = TRUE; + crm_debug("%s process (PID %d) timed out", child->desc, (int)child->pid); + + child->timerid = g_timeout_add(5000, child_timeout_callback, child); + return FALSE; +} + +static bool +child_waitpid(mainloop_child_t *child, int flags) +{ + int rc = 0; + int core = 0; + int signo = 0; + int status = 0; + int exitcode = 0; + bool callback_needed = true; + + rc = waitpid(child->pid, &status, flags); + if (rc == 0) { // WNOHANG in flags, and child status is not available + crm_trace("Child process %d (%s) still active", + child->pid, child->desc); + callback_needed = false; + + } else if (rc != child->pid) { + /* According to POSIX, possible conditions: + * - child->pid was non-positive (process group or any child), + * and rc is specific child + * - errno ECHILD (pid does not exist or is not child) + * - errno EINVAL (invalid flags) + * - errno EINTR (caller interrupted by signal) + * + * @TODO Handle these cases more specifically. + */ + signo = SIGCHLD; + exitcode = 1; + crm_notice("Wait for child process %d (%s) interrupted: %s", + child->pid, child->desc, pcmk_rc_str(errno)); + + } else if (WIFEXITED(status)) { + exitcode = WEXITSTATUS(status); + crm_trace("Child process %d (%s) exited with status %d", + child->pid, child->desc, exitcode); + + } else if (WIFSIGNALED(status)) { + signo = WTERMSIG(status); + crm_trace("Child process %d (%s) exited with signal %d (%s)", + child->pid, child->desc, signo, strsignal(signo)); + +#ifdef WCOREDUMP // AIX, SunOS, maybe others + } else if (WCOREDUMP(status)) { + core = 1; + crm_err("Child process %d (%s) dumped core", + child->pid, child->desc); +#endif + + } else { // flags must contain WUNTRACED and/or WCONTINUED to reach this + crm_trace("Child process %d (%s) stopped or continued", + child->pid, child->desc); + callback_needed = false; + } + + if (callback_needed && child->callback) { + child->callback(child, child->pid, core, signo, exitcode); + } + return callback_needed; +} + +static void +child_death_dispatch(int signal) +{ + for (GList *iter = child_list; iter; ) { + GList *saved = iter; + mainloop_child_t *child = iter->data; + + iter = iter->next; + if (child_waitpid(child, WNOHANG)) { + crm_trace("Removing completed process %d from child list", + child->pid); + child_list = g_list_remove_link(child_list, saved); + g_list_free(saved); + child_free(child); + } + } +} + +static gboolean +child_signal_init(gpointer p) +{ + crm_trace("Installed SIGCHLD handler"); + /* Do NOT use g_child_watch_add() and friends, they rely on pthreads */ + mainloop_add_signal(SIGCHLD, child_death_dispatch); + + /* In case they terminated before the signal handler was installed */ + child_death_dispatch(SIGCHLD); + return FALSE; +} + +gboolean +mainloop_child_kill(pid_t pid) +{ + GList *iter; + mainloop_child_t *child = NULL; + mainloop_child_t *match = NULL; + /* It is impossible to block SIGKILL, this allows us to + * call waitpid without WNOHANG flag.*/ + int waitflags = 0, rc = 0; + + for (iter = child_list; iter != NULL && match == NULL; iter = iter->next) { + child = iter->data; + if (pid == child->pid) { + match = child; + } + } + + if (match == NULL) { + return FALSE; + } + + rc = child_kill_helper(match); + if(rc == -ESRCH) { + /* It's gone, but hasn't shown up in waitpid() yet. Wait until we get + * SIGCHLD and let handler clean it up as normal (so we get the correct + * return code/status). The blocking alternative would be to call + * child_waitpid(match, 0). + */ + crm_trace("Waiting for signal that child process %d completed", + match->pid); + return TRUE; + + } else if(rc != 0) { + /* If KILL for some other reason set the WNOHANG flag since we + * can't be certain what happened. + */ + waitflags = WNOHANG; + } + + if (!child_waitpid(match, waitflags)) { + /* not much we can do if this occurs */ + return FALSE; + } + + child_list = g_list_remove(child_list, match); + child_free(match); + return TRUE; +} + +/* Create/Log a new tracked process + * To track a process group, use -pid + * + * @TODO Using a non-positive pid (i.e. any child, or process group) would + * likely not be useful since we will free the child after the first + * completed process. + */ +void +mainloop_child_add_with_flags(pid_t pid, int timeout, const char *desc, void *privatedata, enum mainloop_child_flags flags, + 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)); + + child->pid = pid; + child->timerid = 0; + child->timeout = FALSE; + child->privatedata = privatedata; + child->callback = callback; + child->flags = flags; + pcmk__str_update(&child->desc, desc); + + if (timeout) { + child->timerid = g_timeout_add(timeout, child_timeout_callback, child); + } + + child_list = g_list_append(child_list, child); + + if(need_init) { + need_init = FALSE; + /* SIGCHLD processing has to be invoked from mainloop. + * We do not want it to be possible to both add a child pid + * to mainloop, and have the pid's exit callback invoked within + * the same callstack. */ + g_timeout_add(1, child_signal_init, NULL); + } +} + +void +mainloop_child_add(pid_t pid, int timeout, const char *desc, void *privatedata, + void (*callback) (mainloop_child_t * p, pid_t pid, int core, int signo, int exitcode)) +{ + mainloop_child_add_with_flags(pid, timeout, desc, privatedata, 0, callback); +} + +static gboolean +mainloop_timer_cb(gpointer user_data) +{ + int id = 0; + bool repeat = FALSE; + struct mainloop_timer_s *t = user_data; + + CRM_ASSERT(t != NULL); + + id = t->id; + t->id = 0; /* Ensure it's unset during callbacks so that + * mainloop_timer_running() works as expected + */ + + if(t->cb) { + crm_trace("Invoking callbacks for timer %s", t->name); + repeat = t->repeat; + if(t->cb(t->userdata) == FALSE) { + crm_trace("Timer %s complete", t->name); + repeat = FALSE; + } + } + + if(repeat) { + /* Restore if repeating */ + t->id = id; + } + + return repeat; +} + +bool +mainloop_timer_running(mainloop_timer_t *t) +{ + if(t && t->id != 0) { + return TRUE; + } + return FALSE; +} + +void +mainloop_timer_start(mainloop_timer_t *t) +{ + mainloop_timer_stop(t); + if(t && t->period_ms > 0) { + crm_trace("Starting timer %s", t->name); + t->id = g_timeout_add(t->period_ms, mainloop_timer_cb, t); + } +} + +void +mainloop_timer_stop(mainloop_timer_t *t) +{ + if(t && t->id != 0) { + crm_trace("Stopping timer %s", t->name); + g_source_remove(t->id); + t->id = 0; + } +} + +guint +mainloop_timer_set_period(mainloop_timer_t *t, guint period_ms) +{ + guint last = 0; + + if(t) { + last = t->period_ms; + t->period_ms = period_ms; + } + + if(t && t->id != 0 && last != t->period_ms) { + mainloop_timer_start(t); + } + return last; +} + +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)); + + 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); + } + return t; +} + +void +mainloop_timer_del(mainloop_timer_t *t) +{ + if(t) { + crm_trace("Destroying timer %s", t->name); + mainloop_timer_stop(t); + free(t->name); + free(t); + } +} + +/* + * Helpers to make sure certain events aren't lost at shutdown + */ + +static gboolean +drain_timeout_cb(gpointer user_data) +{ + bool *timeout_popped = (bool*) user_data; + + *timeout_popped = TRUE; + return FALSE; +} + +/*! + * \brief Drain some remaining main loop events then quit it + * + * \param[in,out] mloop Main loop to drain and quit + * \param[in] n Drain up to this many pending events + */ +void +pcmk_quit_main_loop(GMainLoop *mloop, unsigned int n) +{ + if ((mloop != NULL) && g_main_loop_is_running(mloop)) { + GMainContext *ctx = g_main_loop_get_context(mloop); + + /* Drain up to n events in case some memory clean-up is pending + * (helpful to reduce noise in valgrind output). + */ + for (int i = 0; (i < n) && g_main_context_pending(ctx); ++i) { + g_main_context_dispatch(ctx); + } + g_main_loop_quit(mloop); + } +} + +/*! + * \brief Process main loop events while a certain condition is met + * + * \param[in,out] mloop Main loop to process + * \param[in] timer_ms Don't process longer than this amount of time + * \param[in] check Function that returns true if events should be + * processed + * + * \note This function is intended to be called at shutdown if certain important + * events should not be missed. The caller would likely quit the main loop + * or exit after calling this function. The check() function will be + * passed the remaining timeout in milliseconds. + */ +void +pcmk_drain_main_loop(GMainLoop *mloop, guint timer_ms, bool (*check)(guint)) +{ + bool timeout_popped = FALSE; + guint timer = 0; + GMainContext *ctx = NULL; + + CRM_CHECK(mloop && check, return); + + ctx = g_main_loop_get_context(mloop); + if (ctx) { + time_t start_time = time(NULL); + + timer = g_timeout_add(timer_ms, drain_timeout_cb, &timeout_popped); + while (!timeout_popped + && check(timer_ms - (time(NULL) - start_time) * 1000)) { + g_main_context_iteration(ctx, TRUE); + } + } + if (!timeout_popped && (timer > 0)) { + g_source_remove(timer); + } +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include <crm/common/mainloop_compat.h> + +gboolean +crm_signal(int sig, void (*dispatch) (int sig)) +{ + return crm_signal_handler(sig, dispatch) != SIG_ERR; +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/messages.c b/lib/common/messages.c new file mode 100644 index 0000000..2c01eed --- /dev/null +++ b/lib/common/messages.c @@ -0,0 +1,291 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <stdio.h> +#include <sys/types.h> + +#include <glib.h> +#include <libxml/tree.h> + +#include <crm/msg_xml.h> +#include <crm/common/xml_internal.h> + +/*! + * \brief Create a Pacemaker request (for IPC or cluster layer) + * + * \param[in] task What to set as the request's task + * \param[in] msg_data What to add as the request's data contents + * \param[in] host_to What to set as the request's destination host + * \param[in] sys_to What to set as the request's destination system + * \param[in] sys_from If not NULL, set as request's origin system + * \param[in] uuid_from If not NULL, use in request's origin system + * \param[in] origin Name of function that called this one + * + * \return XML of new request + * + * \note One of sys_from or uuid_from must be non-NULL + * \note This function should not be called directly, but via the + * create_request() wrapper. + * \note The caller is responsible for freeing the result using free_xml(). + */ +xmlNode * +create_request_adv(const char *task, xmlNode *msg_data, + const char *host_to, const char *sys_to, + const char *sys_from, const char *uuid_from, + const char *origin) +{ + static uint ref_counter = 0; + + char *true_from = NULL; + xmlNode *request = NULL; + char *reference = crm_strdup_printf("%s-%s-%lld-%u", + (task? task : "_empty_"), + (sys_from? sys_from : "_empty_"), + (long long) time(NULL), ref_counter++); + + if (uuid_from != NULL) { + true_from = crm_strdup_printf("%s_%s", uuid_from, + (sys_from? sys_from : "none")); + } else if (sys_from != NULL) { + true_from = strdup(sys_from); + } else { + crm_err("Cannot create IPC request: No originating system specified"); + } + + // 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); + + /* 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); + } + + if (msg_data != NULL) { + add_message_xml(request, F_CRM_DATA, msg_data); + } + free(reference); + free(true_from); + + return request; +} + +/*! + * \brief Create a Pacemaker reply (for IPC or cluster layer) + * + * \param[in] original_request XML of request this is a reply to + * \param[in] xml_response_data XML to copy as data section of reply + * \param[in] origin Name of function that called this one + * + * \return XML of new reply + * + * \note This function should not be called directly, but via the + * create_reply() wrapper. + * \note The caller is responsible for freeing the result using free_xml(). + */ +xmlNode * +create_reply_adv(const xmlNode *original_request, xmlNode *xml_response_data, + const char *origin) +{ + 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); + + 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; + } + + 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); + + /* 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); + + /* 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); + } + + if (xml_response_data != NULL) { + add_message_xml(reply, F_CRM_DATA, 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 + * + * \param[in] name Actual system name to check + * + * \return Non-NULL cluster message identifier corresponding to name + * + * \note The Pacemaker daemons were renamed in version 2.0.0, but the old names + * must continue to be used as the identifier for cluster messages, so + * that mixed-version clusters are possible during a rolling upgrade. + */ +const char * +pcmk__message_name(const char *name) +{ + if (name == NULL) { + return "unknown"; + + } else if (!strcmp(name, "pacemaker-attrd")) { + return "attrd"; + + } else if (!strcmp(name, "pacemaker-based")) { + return CRM_SYSTEM_CIB; + + } else if (!strcmp(name, "pacemaker-controld")) { + return CRM_SYSTEM_CRMD; + + } else if (!strcmp(name, "pacemaker-execd")) { + return CRM_SYSTEM_LRMD; + + } else if (!strcmp(name, "pacemaker-fenced")) { + return "stonith-ng"; + + } else if (!strcmp(name, "pacemaker-schedulerd")) { + return CRM_SYSTEM_PENGINE; + + } else { + return name; + } +} + +/*! + * \internal + * \brief Register handlers for server commands + * + * \param[in] handlers Array of handler functions for supported server commands + * (the final entry must have a NULL command name, and if + * it has a handler it will be used as the default handler + * for unrecognized commands) + * + * \return Newly created hash table with commands and handlers + * \note The caller is responsible for freeing the return value with + * g_hash_table_destroy(). + */ +GHashTable * +pcmk__register_handlers(const pcmk__server_command_t handlers[]) +{ + GHashTable *commands = g_hash_table_new(g_str_hash, g_str_equal); + + if (handlers != NULL) { + int i; + + for (i = 0; handlers[i].command != NULL; ++i) { + g_hash_table_insert(commands, (gpointer) handlers[i].command, + handlers[i].handler); + } + if (handlers[i].handler != NULL) { + // g_str_hash() can't handle NULL, so use empty string for default + g_hash_table_insert(commands, (gpointer) "", handlers[i].handler); + } + } + return commands; +} + +/*! + * \internal + * \brief Process an incoming request + * + * \param[in,out] request Request to process + * \param[in] handlers Command table created by pcmk__register_handlers() + * + * \return XML to send as reply (or NULL if no reply is needed) + */ +xmlNode * +pcmk__process_request(pcmk__request_t *request, GHashTable *handlers) +{ + xmlNode *(*handler)(pcmk__request_t *request) = NULL; + + CRM_CHECK((request != NULL) && (request->op != NULL) && (handlers != NULL), + return NULL); + + if (pcmk_is_set(request->flags, pcmk__request_sync) + && (request->ipc_client != NULL)) { + CRM_CHECK(request->ipc_client->request_id == request->ipc_id, + return NULL); + } + + handler = g_hash_table_lookup(handlers, request->op); + if (handler == NULL) { + handler = g_hash_table_lookup(handlers, ""); // Default handler + if (handler == NULL) { + crm_info("Ignoring %s request from %s %s with no handler", + request->op, pcmk__request_origin_type(request), + pcmk__request_origin(request)); + return NULL; + } + } + + return (*handler)(request); +} + +/*! + * \internal + * \brief Free memory used within a request (but not the request itself) + * + * \param[in,out] request Request to reset + */ +void +pcmk__reset_request(pcmk__request_t *request) +{ + free(request->op); + request->op = NULL; + + pcmk__reset_result(&(request->result)); +} diff --git a/lib/common/mock.c b/lib/common/mock.c new file mode 100644 index 0000000..2bd8334 --- /dev/null +++ b/lib/common/mock.c @@ -0,0 +1,427 @@ +/* + * Copyright 2021-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <errno.h> +#include <pwd.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <setjmp.h> +#include <sys/types.h> +#include <sys/utsname.h> +#include <unistd.h> +#include <grp.h> + +#include <cmocka.h> +#include "mock_private.h" + +/* This file is only used when running "make check". It is built into + * libcrmcommon_test.a, not into libcrmcommon.so. It is used to support + * constructing mock versions of library functions for unit testing. + * + * HOW TO ADD A MOCKED FUNCTION: + * + * - In this file, declare a bool pcmk__mock_X variable, and define a __wrap_X + * function with the same prototype as the actual function that performs the + * desired behavior if pcmk__mock_X is true and calls __real_X otherwise. + * You can use cmocka's mock_type() and mock_ptr_type() to pass extra + * information to the mocked function (see existing examples for details). + * + * - In mock_private.h, add declarations for extern bool pcmk__mock_X and the + * __real_X and __wrap_X function prototypes. + * + * - In mk/tap.mk, add the function name to the WRAPPED variable. + * + * HOW TO USE A MOCKED FUNCTION: + * + * - #include "mock_private.h" in your test file. + * + * - Write your test cases using pcmk__mock_X and cmocka's will_return() as + * needed per the comments for the mocked function below. See existing test + * cases for examples. + */ + +// LCOV_EXCL_START +/* calloc() + * + * If pcmk__mock_calloc is set to true, later calls to calloc() will return + * NULL and must be preceded by: + * + * expect_*(__wrap_calloc, nmemb[, ...]); + * expect_*(__wrap_calloc, size[, ...]); + * + * expect_* functions: https://api.cmocka.org/group__cmocka__param.html + */ + +bool pcmk__mock_calloc = false; + +void * +__wrap_calloc(size_t nmemb, size_t size) +{ + if (!pcmk__mock_calloc) { + return __real_calloc(nmemb, size); + } + check_expected(nmemb); + check_expected(size); + return NULL; +} + + +/* getenv() + * + * If pcmk__mock_getenv is set to true, later calls to getenv() must be preceded + * by: + * + * expect_*(__wrap_getenv, name[, ...]); + * will_return(__wrap_getenv, return_value); + * + * expect_* functions: https://api.cmocka.org/group__cmocka__param.html + */ + +bool pcmk__mock_getenv = false; + +char * +__wrap_getenv(const char *name) +{ + if (!pcmk__mock_getenv) { + return __real_getenv(name); + } + check_expected_ptr(name); + return mock_ptr_type(char *); +} + + +/* setenv() + * + * If pcmk__mock_setenv is set to true, later calls to setenv() must be preceded + * by: + * + * expect_*(__wrap_setenv, name[, ...]); + * expect_*(__wrap_setenv, value[, ...]); + * expect_*(__wrap_setenv, overwrite[, ...]); + * will_return(__wrap_setenv, errno_to_set); + * + * expect_* functions: https://api.cmocka.org/group__cmocka__param.html + * + * The mocked function will return 0 if errno_to_set is 0, and -1 otherwise. + */ +bool pcmk__mock_setenv = false; + +int +__wrap_setenv(const char *name, const char *value, int overwrite) +{ + if (!pcmk__mock_setenv) { + return __real_setenv(name, value, overwrite); + } + check_expected_ptr(name); + check_expected_ptr(value); + check_expected(overwrite); + errno = mock_type(int); + return (errno == 0)? 0 : -1; +} + + +/* unsetenv() + * + * If pcmk__mock_unsetenv is set to true, later calls to unsetenv() must be + * preceded by: + * + * expect_*(__wrap_unsetenv, name[, ...]); + * will_return(__wrap_setenv, errno_to_set); + * + * expect_* functions: https://api.cmocka.org/group__cmocka__param.html + * + * The mocked function will return 0 if errno_to_set is 0, and -1 otherwise. + */ +bool pcmk__mock_unsetenv = false; + +int +__wrap_unsetenv(const char *name) +{ + if (!pcmk__mock_unsetenv) { + return __real_unsetenv(name); + } + check_expected_ptr(name); + errno = mock_type(int); + return (errno == 0)? 0 : -1; +} + + +/* getpid() + * + * If pcmk__mock_getpid is set to true, later calls to getpid() must be preceded + * by: + * + * will_return(__wrap_getpid, return_value); + */ + +bool pcmk__mock_getpid = false; + +pid_t +__wrap_getpid(void) +{ + return pcmk__mock_getpid? mock_type(pid_t) : __real_getpid(); +} + + +/* setgrent(), getgrent() and endgrent() + * + * If pcmk__mock_grent is set to true, getgrent() will behave as if the only + * groups on the system are: + * + * - grp0 (user0, user1) + * - grp1 (user1) + * - grp2 (user2, user1) + */ + +bool pcmk__mock_grent = false; + +// Index of group that will be returned next from getgrent() +static int group_idx = 0; + +// Data used for testing +static const char* grp0_members[] = { + "user0", "user1", NULL +}; + +static const char* grp1_members[] = { + "user1", NULL +}; + +static const char* grp2_members[] = { + "user2", "user1", NULL +}; + +/* An array of "groups" (a struct from grp.h) + * + * The members of the groups are initalized here to some testing data, casting + * away the consts to make the compiler happy and simplify initialization. We + * never actually change these variables during the test! + * + * string literal = const char* (cannot be changed b/c ? ) + * vs. char* (it's getting casted to this) + */ +static const int NUM_GROUPS = 3; +static struct group groups[] = { + {(char*)"grp0", (char*)"", 0, (char**)grp0_members}, + {(char*)"grp1", (char*)"", 1, (char**)grp1_members}, + {(char*)"grp2", (char*)"", 2, (char**)grp2_members}, +}; + +// This function resets the group_idx to 0. +void +__wrap_setgrent(void) { + if (pcmk__mock_grent) { + group_idx = 0; + } else { + __real_setgrent(); + } +} + +/* This function returns the next group entry in the list of groups, or + * NULL if there aren't any left. + * group_idx is a global variable which keeps track of where you are in the list + */ +struct group * +__wrap_getgrent(void) { + if (pcmk__mock_grent) { + if (group_idx >= NUM_GROUPS) { + return NULL; + } + return &groups[group_idx++]; + } else { + return __real_getgrent(); + } +} + +void +__wrap_endgrent(void) { + if (!pcmk__mock_grent) { + __real_endgrent(); + } +} + + +/* fopen() + * + * If pcmk__mock_fopen is set to true, later calls to fopen() must be + * preceded by: + * + * expect_*(__wrap_fopen, pathname[, ...]); + * expect_*(__wrap_fopen, mode[, ...]); + * will_return(__wrap_fopen, errno_to_set); + * + * expect_* functions: https://api.cmocka.org/group__cmocka__param.html + */ + +bool pcmk__mock_fopen = false; + +FILE * +__wrap_fopen(const char *pathname, const char *mode) +{ + if (pcmk__mock_fopen) { + check_expected_ptr(pathname); + check_expected_ptr(mode); + errno = mock_type(int); + + if (errno != 0) { + return NULL; + } else { + return __real_fopen(pathname, mode); + } + + } else { + return __real_fopen(pathname, mode); + } +} + + +/* getpwnam_r() + * + * If pcmk__mock_getpwnam_r is set to true, later calls to getpwnam_r() must be + * preceded by: + * + * expect_*(__wrap_getpwnam_r, name[, ...]); + * expect_*(__wrap_getpwnam_r, pwd[, ...]); + * expect_*(__wrap_getpwnam_r, buf[, ...]); + * expect_*(__wrap_getpwnam_r, buflen[, ...]); + * expect_*(__wrap_getpwnam_r, result[, ...]); + * will_return(__wrap_getpwnam_r, return_value); + * will_return(__wrap_getpwnam_r, ptr_to_result_struct); + * + * expect_* functions: https://api.cmocka.org/group__cmocka__param.html + */ + +bool pcmk__mock_getpwnam_r = false; + +int +__wrap_getpwnam_r(const char *name, struct passwd *pwd, char *buf, + size_t buflen, struct passwd **result) +{ + if (pcmk__mock_getpwnam_r) { + int retval = mock_type(int); + + check_expected_ptr(name); + check_expected_ptr(pwd); + check_expected_ptr(buf); + check_expected(buflen); + check_expected_ptr(result); + *result = mock_ptr_type(struct passwd *); + return retval; + + } else { + return __real_getpwnam_r(name, pwd, buf, buflen, result); + } +} + +/* + * If pcmk__mock_readlink is set to true, later calls to readlink() must be + * preceded by: + * + * expect_*(__wrap_readlink, path[, ...]); + * expect_*(__wrap_readlink, buf[, ...]); + * expect_*(__wrap_readlink, bufsize[, ...]); + * will_return(__wrap_readlink, errno_to_set); + * will_return(__wrap_readlink, link_contents); + * + * expect_* functions: https://api.cmocka.org/group__cmocka__param.html + * + * The mocked function will return 0 if errno_to_set is 0, and -1 otherwise. + */ + +bool pcmk__mock_readlink = false; + +ssize_t +__wrap_readlink(const char *restrict path, char *restrict buf, + size_t bufsize) +{ + if (pcmk__mock_readlink) { + const char *contents = NULL; + + check_expected_ptr(path); + check_expected_ptr(buf); + check_expected(bufsize); + errno = mock_type(int); + contents = mock_ptr_type(const char *); + + if (errno == 0) { + strncpy(buf, contents, bufsize - 1); + return strlen(contents); + } + return -1; + + } else { + return __real_readlink(path, buf, bufsize); + } +} + + +/* strdup() + * + * If pcmk__mock_strdup is set to true, later calls to strdup() will return + * NULL and must be preceded by: + * + * expect_*(__wrap_strdup, s[, ...]); + * + * expect_* functions: https://api.cmocka.org/group__cmocka__param.html + */ + +bool pcmk__mock_strdup = false; + +char * +__wrap_strdup(const char *s) +{ + if (!pcmk__mock_strdup) { + return __real_strdup(s); + } + check_expected_ptr(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 new file mode 100644 index 0000000..45207c4 --- /dev/null +++ b/lib/common/mock_private.h @@ -0,0 +1,77 @@ +/* + * Copyright 2021-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#ifndef MOCK_PRIVATE__H +# define MOCK_PRIVATE__H + +#include <pwd.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/types.h> +#include <sys/utsname.h> +#include <unistd.h> +#include <grp.h> + +/* This header is for the sole use of libcrmcommon_test and unit tests */ + +extern bool pcmk__mock_calloc; +void *__real_calloc(size_t nmemb, size_t size); +void *__wrap_calloc(size_t nmemb, size_t size); + +extern bool pcmk__mock_fopen; +FILE *__real_fopen(const char *pathname, const char *mode); +FILE *__wrap_fopen(const char *pathname, const char *mode); + +extern bool pcmk__mock_getenv; +char *__real_getenv(const char *name); +char *__wrap_getenv(const char *name); + +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); + +extern bool pcmk__mock_unsetenv; +int __real_unsetenv(const char *name); +int __wrap_unsetenv(const char *name); + +extern bool pcmk__mock_getpid; +pid_t __real_getpid(void); +pid_t __wrap_getpid(void); + +extern bool pcmk__mock_grent; +void __real_setgrent(void); +void __wrap_setgrent(void); +struct group * __wrap_getgrent(void); +struct group * __real_getgrent(void); +void __wrap_endgrent(void); +void __real_endgrent(void); + +extern bool pcmk__mock_getpwnam_r; +int __real_getpwnam_r(const char *name, struct passwd *pwd, + char *buf, size_t buflen, struct passwd **result); +int __wrap_getpwnam_r(const char *name, struct passwd *pwd, + char *buf, size_t buflen, struct passwd **result); + +extern bool pcmk__mock_readlink; +ssize_t __real_readlink(const char *restrict path, char *restrict buf, + size_t bufsize); +ssize_t __wrap_readlink(const char *restrict path, char *restrict buf, + size_t bufsize); + +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 new file mode 100644 index 0000000..a17d587 --- /dev/null +++ b/lib/common/nodes.c @@ -0,0 +1,24 @@ +/* + * 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 Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <crm/common/nvpair.h> + +void +pcmk__xe_add_node(xmlNode *xml, const char *node, int nodeid) +{ + if (node != NULL) { + crm_xml_add(xml, PCMK__XA_ATTR_NODE_NAME, node); + } + + if (nodeid > 0) { + crm_xml_add_int(xml, PCMK__XA_ATTR_NODE_ID, nodeid); + } +} diff --git a/lib/common/nvpair.c b/lib/common/nvpair.c new file mode 100644 index 0000000..3766c45 --- /dev/null +++ b/lib/common/nvpair.c @@ -0,0 +1,992 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <stdio.h> +#include <sys/types.h> +#include <string.h> +#include <ctype.h> +#include <glib.h> +#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: + * + * - pcmk_nvpair_t data type + * - XML attributes (<TAG ... NAME=VALUE ...>) + * - XML nvpair elements (<nvpair id=ID name=NAME value=VALUE>) + */ + +// pcmk_nvpair_t handling + +/*! + * \internal + * \brief Allocate a new name/value pair + * + * \param[in] name New name (required) + * \param[in] value New value + * + * \return Newly allocated name/value pair + * \note The caller is responsible for freeing the result with + * \c pcmk__free_nvpair(). + */ +static pcmk_nvpair_t * +pcmk__new_nvpair(const char *name, const char *value) +{ + pcmk_nvpair_t *nvpair = NULL; + + CRM_ASSERT(name); + + nvpair = calloc(1, sizeof(pcmk_nvpair_t)); + CRM_ASSERT(nvpair); + + pcmk__str_update(&nvpair->name, name); + pcmk__str_update(&nvpair->value, value); + return nvpair; +} + +/*! + * \internal + * \brief Free a name/value pair + * + * \param[in,out] nvpair Name/value pair to free + */ +static void +pcmk__free_nvpair(gpointer data) +{ + if (data) { + pcmk_nvpair_t *nvpair = data; + + free(nvpair->name); + free(nvpair->value); + free(nvpair); + } +} + +/*! + * \brief Prepend a name/value pair to a list + * + * \param[in,out] nvpairs List to modify + * \param[in] name New entry's name + * \param[in] value New entry's value + * + * \return New head of list + * \note The caller is responsible for freeing the list with + * \c pcmk_free_nvpairs(). + */ +GSList * +pcmk_prepend_nvpair(GSList *nvpairs, const char *name, const char *value) +{ + return g_slist_prepend(nvpairs, pcmk__new_nvpair(name, value)); +} + +/*! + * \brief Free a list of name/value pairs + * + * \param[in,out] list List to free + */ +void +pcmk_free_nvpairs(GSList *nvpairs) +{ + g_slist_free_full(nvpairs, pcmk__free_nvpair); +} + +/*! + * \internal + * \brief Compare two name/value pairs + * + * \param[in] a First name/value pair to compare + * \param[in] b Second name/value pair to compare + * + * \return 0 if a == b, 1 if a > b, -1 if a < b + */ +static gint +pcmk__compare_nvpair(gconstpointer a, gconstpointer b) +{ + int rc = 0; + const pcmk_nvpair_t *pair_a = a; + const pcmk_nvpair_t *pair_b = b; + + CRM_ASSERT(a != NULL); + CRM_ASSERT(pair_a->name != NULL); + + CRM_ASSERT(b != NULL); + CRM_ASSERT(pair_b->name != NULL); + + rc = strcmp(pair_a->name, pair_b->name); + if (rc < 0) { + return -1; + } else if (rc > 0) { + return 1; + } + return 0; +} + +/*! + * \brief Sort a list of name/value pairs + * + * \param[in,out] list List to sort + * + * \return New head of list + */ +GSList * +pcmk_sort_nvpairs(GSList *list) +{ + return g_slist_sort(list, pcmk__compare_nvpair); +} + +/*! + * \brief Create a list of name/value pairs from an XML node's attributes + * + * \param[in] XML to parse + * + * \return New list of name/value pairs + * \note It is the caller's responsibility to free the list with + * \c pcmk_free_nvpairs(). + */ +GSList * +pcmk_xml_attrs2nvpairs(const xmlNode *xml) +{ + GSList *result = NULL; + + for (xmlAttrPtr iter = pcmk__xe_first_attr(xml); iter != NULL; + iter = iter->next) { + + result = pcmk_prepend_nvpair(result, + (const char *) iter->name, + (const char *) pcmk__xml_attr_value(iter)); + } + return result; +} + +/*! + * \internal + * \brief Add an XML attribute corresponding to a name/value pair + * + * Suitable for glib list iterators, this function adds a NAME=VALUE + * XML attribute based on a given name/value pair. + * + * \param[in] data Name/value pair + * \param[out] user_data XML node to add attributes to + */ +static void +pcmk__nvpair_add_xml_attr(gpointer data, gpointer user_data) +{ + pcmk_nvpair_t *pair = data; + xmlNode *parent = user_data; + + crm_xml_add(parent, pair->name, pair->value); +} + +/*! + * \brief Add XML attributes based on a list of name/value pairs + * + * \param[in,out] list List of name/value pairs + * \param[in,out] xml XML node to add attributes to + */ +void +pcmk_nvpairs2xml_attrs(GSList *list, xmlNode *xml) +{ + g_slist_foreach(list, pcmk__nvpair_add_xml_attr, xml); +} + +// convenience function for name=value strings + +/*! + * \internal + * \brief Extract the name and value from an input string formatted as "name=value". + * If unable to extract them, they are returned as NULL. + * + * \param[in] input The input string, likely from the command line + * \param[out] name Everything before the first '=' in the input string + * \param[out] value Everything after the first '=' in the input string + * + * \return 2 if both name and value could be extracted, 1 if only one could, and + * and error code otherwise + */ +int +pcmk__scan_nvpair(const char *input, char **name, char **value) +{ +#ifdef HAVE_SSCANF_M + *name = NULL; + *value = NULL; + if (sscanf(input, "%m[^=]=%m[^\n]", name, value) <= 0) { + return -pcmk_err_bad_nvpair; + } +#else + char *sep = NULL; + *name = NULL; + *value = NULL; + + sep = strstr(optarg, "="); + if (sep == NULL) { + return -pcmk_err_bad_nvpair; + } + + *name = strndup(input, sep-input); + + if (*name == NULL) { + return -ENOMEM; + } + + /* If the last char in optarg is =, the user gave no + * value for the option. Leave it as NULL. + */ + if (*(sep+1) != '\0') { + *value = strdup(sep+1); + + if (*value == NULL) { + return -ENOMEM; + } + } +#endif + + if (*name != NULL && *value != NULL) { + return 2; + } else if (*name != NULL || *value != NULL) { + return 1; + } else { + return -pcmk_err_bad_nvpair; + } +} + +/*! + * \internal + * \brief Format a name/value pair. + * + * Units can optionally be provided for the value. Note that unlike most + * formatting functions, this one returns the formatted string. It is + * assumed that the most common use of this function will be to build up + * a string to be output as part of other functions. + * + * \note The caller is responsible for freeing the return value after use. + * + * \param[in] name The name of the nvpair. + * \param[in] value The value of the nvpair. + * \param[in] units Optional units for the value, or NULL. + * + * \return Newly allocated string with name/value pair + */ +char * +pcmk__format_nvpair(const char *name, const char *value, const char *units) +{ + return crm_strdup_printf("%s=\"%s%s\"", name, value, units ? units : ""); +} + +// XML attribute handling + +/*! + * \brief Create an XML attribute with specified name and value + * + * \param[in,out] node XML node to modify + * \param[in] name Attribute name to set + * \param[in] value Attribute value to set + * + * \return New value on success, \c NULL otherwise + * \note This does nothing if node, name, or value are \c NULL or empty. + */ +const char * +crm_xml_add(xmlNode *node, const char *name, const char *value) +{ + bool dirty = FALSE; + xmlAttr *attr = NULL; + + CRM_CHECK(node != NULL, return NULL); + CRM_CHECK(name != NULL, return NULL); + + if (value == NULL) { + return NULL; + } + + if (pcmk__tracking_xml_changes(node, FALSE)) { + const char *old = crm_element_value(node, name); + + if (old == NULL || value == NULL || strcmp(old, value) != 0) { + dirty = TRUE; + } + } + + if (dirty && (pcmk__check_acl(node, name, pcmk__xf_acl_create) == FALSE)) { + crm_trace("Cannot add %s=%s to %s", name, value, node->name); + return NULL; + } + + attr = xmlSetProp(node, (pcmkXmlStr) name, (pcmkXmlStr) value); + if (dirty) { + pcmk__mark_xml_attr_dirty(attr); + } + + CRM_CHECK(attr && attr->children && attr->children->content, return NULL); + return (char *)attr->children->content; +} + +/*! + * \brief Replace an XML attribute with specified name and (possibly NULL) value + * + * \param[in,out] node XML node to modify + * \param[in] name Attribute name to set + * \param[in] value Attribute value to set + * + * \return New value on success, \c NULL otherwise + * \note This does nothing if node or name is \c NULL or empty. + */ +const char * +crm_xml_replace(xmlNode *node, const char *name, const char *value) +{ + bool dirty = FALSE; + xmlAttr *attr = NULL; + const char *old_value = NULL; + + CRM_CHECK(node != NULL, return NULL); + CRM_CHECK(name != NULL && name[0] != 0, return NULL); + + old_value = crm_element_value(node, name); + + /* Could be re-setting the same value */ + CRM_CHECK(old_value != value, return value); + + if (pcmk__check_acl(node, name, pcmk__xf_acl_write) == FALSE) { + /* Create a fake object linked to doc->_private instead? */ + crm_trace("Cannot replace %s=%s to %s", name, value, node->name); + return NULL; + + } else if (old_value && !value) { + xml_remove_prop(node, name); + return NULL; + } + + if (pcmk__tracking_xml_changes(node, FALSE)) { + if (!old_value || !value || !strcmp(old_value, value)) { + dirty = TRUE; + } + } + + attr = xmlSetProp(node, (pcmkXmlStr) name, (pcmkXmlStr) value); + if (dirty) { + pcmk__mark_xml_attr_dirty(attr); + } + CRM_CHECK(attr && attr->children && attr->children->content, return NULL); + return (char *) attr->children->content; +} + +/*! + * \brief Create an XML attribute with specified name and integer value + * + * This is like \c crm_xml_add() but taking an integer value. + * + * \param[in,out] node XML node to modify + * \param[in] name Attribute name to set + * \param[in] value Attribute value to set + * + * \return New value as string on success, \c NULL otherwise + * \note This does nothing if node or name are \c NULL or empty. + */ +const char * +crm_xml_add_int(xmlNode *node, const char *name, int value) +{ + char *number = pcmk__itoa(value); + const char *added = crm_xml_add(node, name, number); + + free(number); + return added; +} + +/*! + * \brief Create an XML attribute with specified name and unsigned value + * + * This is like \c crm_xml_add() but taking a guint value. + * + * \param[in,out] node XML node to modify + * \param[in] name Attribute name to set + * \param[in] ms Attribute value to set + * + * \return New value as string on success, \c NULL otherwise + * \note This does nothing if node or name are \c NULL or empty. + */ +const char * +crm_xml_add_ms(xmlNode *node, const char *name, guint ms) +{ + char *number = crm_strdup_printf("%u", ms); + const char *added = crm_xml_add(node, name, number); + + free(number); + return added; +} + +// Maximum size of null-terminated string representation of 64-bit integer +// -9223372036854775808 +#define LLSTRSIZE 21 + +/*! + * \brief Create an XML attribute with specified name and long long int value + * + * This is like \c crm_xml_add() but taking a long long int value. It is a + * useful equivalent for defined types like time_t, etc. + * + * \param[in,out] xml XML node to modify + * \param[in] name Attribute name to set + * \param[in] value Attribute value to set + * + * \return New value as string on success, \c NULL otherwise + * \note This does nothing if xml or name are \c NULL or empty. + * This does not support greater than 64-bit values. + */ +const char * +crm_xml_add_ll(xmlNode *xml, const char *name, long long value) +{ + char s[LLSTRSIZE] = { '\0', }; + + if (snprintf(s, LLSTRSIZE, "%lld", (long long) value) == LLSTRSIZE) { + return NULL; + } + return crm_xml_add(xml, name, s); +} + +/*! + * \brief Create XML attributes for seconds and microseconds + * + * This is like \c crm_xml_add() but taking a struct timeval. + * + * \param[in,out] xml XML node to modify + * \param[in] name_sec Name of XML attribute for seconds + * \param[in] name_usec Name of XML attribute for microseconds (or NULL) + * \param[in] value Time value to set + * + * \return New seconds value as string on success, \c NULL otherwise + * \note This does nothing if xml, name_sec, or value is \c NULL. + */ +const char * +crm_xml_add_timeval(xmlNode *xml, const char *name_sec, const char *name_usec, + const struct timeval *value) +{ + const char *added = NULL; + + if (xml && name_sec && value) { + added = crm_xml_add_ll(xml, name_sec, (long long) value->tv_sec); + if (added && name_usec) { + // Any error is ignored (we successfully added seconds) + crm_xml_add_ll(xml, name_usec, (long long) value->tv_usec); + } + } + return added; +} + +/*! + * \brief Retrieve the value of an XML attribute + * + * \param[in] data XML node to check + * \param[in] name Attribute name to check + * + * \return Value of specified attribute (may be \c NULL) + */ +const char * +crm_element_value(const xmlNode *data, const char *name) +{ + xmlAttr *attr = NULL; + + if (data == NULL) { + crm_err("Couldn't find %s in NULL", name ? name : "<null>"); + CRM_LOG_ASSERT(data != NULL); + return NULL; + + } else if (name == NULL) { + crm_err("Couldn't find NULL in %s", crm_element_name(data)); + return NULL; + } + + /* The first argument to xmlHasProp() has always been const, + * but libxml2 <2.9.2 didn't declare that, so cast it + */ + attr = xmlHasProp((xmlNode *) data, (pcmkXmlStr) name); + if (!attr || !attr->children) { + return NULL; + } + return (const char *) attr->children->content; +} + +/*! + * \brief Retrieve the integer value of an XML attribute + * + * This is like \c crm_element_value() but getting the value as an integer. + * + * \param[in] data XML node to check + * \param[in] name Attribute name to check + * \param[out] dest Where to store element value + * + * \return 0 on success, -1 otherwise + */ +int +crm_element_value_int(const xmlNode *data, const char *name, int *dest) +{ + const char *value = NULL; + + CRM_CHECK(dest != NULL, return -1); + value = crm_element_value(data, name); + if (value) { + long long value_ll; + + if ((pcmk__scan_ll(value, &value_ll, 0LL) != pcmk_rc_ok) + || (value_ll < INT_MIN) || (value_ll > INT_MAX)) { + *dest = PCMK__PARSE_INT_DEFAULT; + } else { + *dest = (int) value_ll; + return 0; + } + } + return -1; +} + +/*! + * \brief Retrieve the long long integer value of an XML attribute + * + * This is like \c crm_element_value() but getting the value as a long long int. + * + * \param[in] data XML node to check + * \param[in] name Attribute name to check + * \param[out] dest Where to store element value + * + * \return 0 on success, -1 otherwise + */ +int +crm_element_value_ll(const xmlNode *data, const char *name, long long *dest) +{ + const char *value = NULL; + + CRM_CHECK(dest != NULL, return -1); + value = crm_element_value(data, name); + if ((value != NULL) + && (pcmk__scan_ll(value, dest, PCMK__PARSE_INT_DEFAULT) == pcmk_rc_ok)) { + return 0; + } + return -1; +} + +/*! + * \brief Retrieve the millisecond value of an XML attribute + * + * This is like \c crm_element_value() but returning the value as a guint. + * + * \param[in] data XML node to check + * \param[in] name Attribute name to check + * \param[out] dest Where to store attribute value + * + * \return \c pcmk_ok on success, -1 otherwise + */ +int +crm_element_value_ms(const xmlNode *data, const char *name, guint *dest) +{ + const char *value = NULL; + long long value_ll; + + CRM_CHECK(dest != NULL, return -1); + *dest = 0; + value = crm_element_value(data, name); + if ((pcmk__scan_ll(value, &value_ll, 0LL) != pcmk_rc_ok) + || (value_ll < 0) || (value_ll > G_MAXUINT)) { + return -1; + } + *dest = (guint) value_ll; + return pcmk_ok; +} + +/*! + * \brief Retrieve the seconds-since-epoch value of an XML attribute + * + * This is like \c crm_element_value() but returning the value as a time_t. + * + * \param[in] xml XML node to check + * \param[in] name Attribute name to check + * \param[out] dest Where to store attribute value + * + * \return \c pcmk_ok on success, -1 otherwise + */ +int +crm_element_value_epoch(const xmlNode *xml, const char *name, time_t *dest) +{ + long long value_ll = 0; + + if (crm_element_value_ll(xml, name, &value_ll) < 0) { + return -1; + } + + /* Unfortunately, we can't do any bounds checking, since time_t has neither + * standardized bounds nor constants defined for them. + */ + *dest = (time_t) value_ll; + return pcmk_ok; +} + +/*! + * \brief Retrieve the value of XML second/microsecond attributes as time + * + * This is like \c crm_element_value() but returning value as a struct timeval. + * + * \param[in] xml XML to parse + * \param[in] name_sec Name of XML attribute for seconds + * \param[in] name_usec Name of XML attribute for microseconds + * \param[out] dest Where to store result + * + * \return \c pcmk_ok on success, -errno on error + * \note Values default to 0 if XML or XML attribute does not exist + */ +int +crm_element_value_timeval(const xmlNode *xml, const char *name_sec, + const char *name_usec, struct timeval *dest) +{ + long long value_i = 0; + + CRM_CHECK(dest != NULL, return -EINVAL); + dest->tv_sec = 0; + dest->tv_usec = 0; + + if (xml == NULL) { + return pcmk_ok; + } + + /* Unfortunately, we can't do any bounds checking, since there are no + * constants provided for the bounds of time_t and suseconds_t, and + * calculating them isn't worth the effort. If there are XML values + * beyond the native sizes, there will probably be worse problems anyway. + */ + + // Parse seconds + errno = 0; + if (crm_element_value_ll(xml, name_sec, &value_i) < 0) { + return -errno; + } + dest->tv_sec = (time_t) value_i; + + // Parse microseconds + if (crm_element_value_ll(xml, name_usec, &value_i) < 0) { + return -errno; + } + dest->tv_usec = (suseconds_t) value_i; + + return pcmk_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. + * + * \param[in] data XML node to check + * \param[in] name Attribute name to check + * + * \return Value of specified attribute (may be \c NULL) + * \note The caller is responsible for freeing the result. + */ +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; +} + +/*! + * \brief Add hash table entry to XML as (possibly legacy) name/value + * + * 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). + * + * \param[in] key Key of hash table entry + * \param[in] value Value of hash table entry + * \param[in,out] user_data XML node + */ +void +hash2smartfield(gpointer key, gpointer value, gpointer user_data) +{ + 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); + + crm_xml_add(tmp, XML_NVPAIR_ATTR_NAME, name); + crm_xml_add(tmp, XML_NVPAIR_ATTR_VALUE, s_value); + + } else if (crm_element_value(xml_node, name) == NULL) { + crm_xml_add(xml_node, name, s_value); + crm_trace("dumped: %s=%s", name, s_value); + + } else { + crm_trace("duplicate: %s=%s", name, s_value); + } +} + +/*! + * \brief Set XML attribute based on hash table entry + * + * Suitable for \c g_hash_table_foreach(), this function takes a hash table key + * and value, with an XML node passed as user data, and adds an XML attribute + * with the specified name and value if it does not already exist. + * + * \param[in] key Key of hash table entry + * \param[in] value Value of hash table entry + * \param[in,out] user_data XML node + */ +void +hash2field(gpointer key, gpointer value, gpointer user_data) +{ + const char *name = key; + const char *s_value = value; + + xmlNode *xml_node = user_data; + + if (crm_element_value(xml_node, name) == NULL) { + crm_xml_add(xml_node, name, s_value); + + } else { + crm_trace("duplicate: %s=%s", name, s_value); + } +} + +/*! + * \brief Set XML attribute based on hash table entry, as meta-attribute name + * + * Suitable for \c g_hash_table_foreach(), this function takes a hash table key + * and value, with an XML node passed as user data, and adds an XML attribute + * with the meta-attribute version of the specified name and value if it does + * not already exist and if the name does not appear to be cluster-internal. + * + * \param[in] key Key of hash table entry + * \param[in] value Value of hash table entry + * \param[in,out] user_data XML node + */ +void +hash2metafield(gpointer key, gpointer value, gpointer user_data) +{ + char *crm_name = NULL; + + if (key == NULL || value == NULL) { + return; + } + + /* Filter out cluster-generated attributes that contain a '#' or ':' + * (like fail-count and last-failure). + */ + for (crm_name = key; *crm_name; ++crm_name) { + if ((*crm_name == '#') || (*crm_name == ':')) { + return; + } + } + + crm_name = crm_meta_name(key); + hash2field(crm_name, value, user_data); + free(crm_name); +} + +// nvpair handling + +/*! + * \brief Create an XML name/value pair + * + * \param[in,out] parent If not \c NULL, make new XML node a child of this one + * \param[in] id Set this as XML ID (or NULL to auto-generate) + * \param[in] name Name to use + * \param[in] value Value to use + * + * \return New XML object on success, \c NULL otherwise + */ +xmlNode * +crm_create_nvpair_xml(xmlNode *parent, const char *id, const char *name, + const char *value) +{ + xmlNode *nvp; + + /* id can be NULL so we auto-generate one, and name can be NULL if this + * will be used to delete a name/value pair by ID, but both can't be NULL + */ + CRM_CHECK(id || name, return NULL); + + nvp = create_xml_node(parent, XML_CIB_TAG_NVPAIR); + CRM_CHECK(nvp, return NULL); + + if (id) { + crm_xml_add(nvp, XML_ATTR_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); + } + crm_xml_add(nvp, XML_NVPAIR_ATTR_NAME, name); + crm_xml_add(nvp, XML_NVPAIR_ATTR_VALUE, value); + return nvp; +} + +/*! + * \brief Add XML nvpair element based on hash table entry + * + * Suitable for \c g_hash_table_foreach(), this function takes a hash table key + * and value, with an XML node passed as the user data, and adds an \c nvpair + * XML element with the specified name and value. + * + * \param[in] key Key of hash table entry + * \param[in] value Value of hash table entry + * \param[in,out] user_data XML node + */ +void +hash2nvpair(gpointer key, gpointer value, gpointer user_data) +{ + const char *name = key; + const char *s_value = value; + xmlNode *xml_node = user_data; + + crm_create_nvpair_xml(xml_node, name, name, s_value); + crm_trace("dumped: name=%s value=%s", name, s_value); +} + +/*! + * \brief Retrieve XML attributes as a hash table + * + * Given an XML element, this will look for any \<attributes> element child, + * creating a hash table of (newly allocated string) name/value pairs taken + * first from the attributes element's NAME=VALUE XML attributes, and then + * from any \<param name=NAME value=VALUE> children of attributes. + * + * \param[in] XML node to parse + * + * \return Hash table with name/value pairs + * \note It is the caller's responsibility to free the result using + * \c g_hash_table_destroy(). + */ +GHashTable * +xml2list(const xmlNode *parent) +{ + xmlNode *child = NULL; + xmlAttrPtr pIter = NULL; + xmlNode *nvpair_list = NULL; + GHashTable *nvpair_hash = pcmk__strkey_table(free, free); + + CRM_CHECK(parent != NULL, return nvpair_hash); + + nvpair_list = find_xml_node(parent, XML_TAG_ATTRS, FALSE); + if (nvpair_list == NULL) { + crm_trace("No attributes in %s", crm_element_name(parent)); + crm_log_xml_trace(parent, "No attributes for resource op"); + } + + crm_log_xml_trace(nvpair_list, "Unpacking"); + + for (pIter = pcmk__xe_first_attr(nvpair_list); pIter != NULL; + pIter = pIter->next) { + + const char *p_name = (const char *)pIter->name; + const char *p_value = pcmk__xml_attr_value(pIter); + + crm_trace("Added %s=%s", p_name, p_value); + + g_hash_table_insert(nvpair_hash, strdup(p_name), strdup(p_value)); + } + + for (child = pcmk__xml_first_child(nvpair_list); child != NULL; + child = pcmk__xml_next(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); + + crm_trace("Added %s=%s", key, value); + if (key != NULL && value != NULL) { + g_hash_table_insert(nvpair_hash, strdup(key), strdup(value)); + } + } + } + + return nvpair_hash; +} + +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); +} + +int +pcmk__xe_get_bool_attr(const xmlNode *node, const char *name, bool *value) +{ + const char *xml_value = NULL; + int ret, rc; + + if (node == NULL) { + return ENODATA; + } else if (name == NULL || value == NULL) { + return EINVAL; + } + + xml_value = crm_element_value(node, name); + + if (xml_value == NULL) { + return ENODATA; + } + + rc = crm_str_to_boolean(xml_value, &ret); + if (rc == 1) { + *value = ret; + return pcmk_rc_ok; + } else { + return pcmk_rc_bad_input; + } +} + +bool +pcmk__xe_attr_is_true(const xmlNode *node, const char *name) +{ + bool value = false; + int rc; + + rc = pcmk__xe_get_bool_attr(node, name, &value); + return rc == pcmk_rc_ok && value == true; +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include <crm/common/util_compat.h> + +int +pcmk_scan_nvpair(const char *input, char **name, char **value) +{ + return pcmk__scan_nvpair(input, name, value); +} + +char * +pcmk_format_nvpair(const char *name, const char *value, + const char *units) +{ + return pcmk__format_nvpair(name, value, units); +} + +char * +pcmk_format_named_time(const char *name, time_t epoch_time) +{ + char *now_s = pcmk__epoch2str(&epoch_time, 0); + char *result = crm_strdup_printf("%s=\"%s\"", name, pcmk__s(now_s, "")); + + free(now_s); + return result; +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/operations.c b/lib/common/operations.c new file mode 100644 index 0000000..3db96cd --- /dev/null +++ b/lib/common/operations.c @@ -0,0 +1,530 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <sys/types.h> +#include <ctype.h> + +#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> + +/*! + * \brief Generate an operation key (RESOURCE_ACTION_INTERVAL) + * + * \param[in] rsc_id ID of resource being operated on + * \param[in] op_type Operation name + * \param[in] interval_ms Operation interval + * + * \return Newly allocated memory containing operation key as string + * + * \note This function asserts on errors, so it will never return NULL. + * The caller is responsible for freeing the result with free(). + */ +char * +pcmk__op_key(const char *rsc_id, const char *op_type, guint interval_ms) +{ + CRM_ASSERT(rsc_id != NULL); + CRM_ASSERT(op_type != NULL); + return crm_strdup_printf(PCMK__OP_FMT, rsc_id, op_type, interval_ms); +} + +static inline gboolean +convert_interval(const char *s, guint *interval_ms) +{ + unsigned long l; + + errno = 0; + l = strtoul(s, NULL, 10); + + if (errno != 0) { + return FALSE; + } + + *interval_ms = (guint) l; + return TRUE; +} + +/*! + * \internal + * \brief Check for underbar-separated substring match + * + * \param[in] key Overall string being checked + * \param[in] position Match before underbar at this \p key index + * \param[in] matches Substrings to match (may contain underbars) + * + * \return \p key index of underbar before any matching substring, + * or 0 if none + */ +static size_t +match_before(const char *key, size_t position, const char **matches) +{ + for (int i = 0; matches[i] != NULL; ++i) { + const size_t match_len = strlen(matches[i]); + + // Must have at least X_MATCH before position + if (position > (match_len + 1)) { + const size_t possible = position - match_len - 1; + + if ((key[possible] == '_') + && (strncmp(key + possible + 1, matches[i], match_len) == 0)) { + return possible; + } + } + } + return 0; +} + +gboolean +parse_op_key(const char *key, char **rsc_id, char **op_type, guint *interval_ms) +{ + guint local_interval_ms = 0; + const size_t key_len = (key == NULL)? 0 : strlen(key); + + // Operation keys must be formatted as RSC_ACTION_INTERVAL + size_t action_underbar = 0; // Index in key of underbar before ACTION + size_t interval_underbar = 0; // Index in key of underbar before INTERVAL + size_t possible = 0; + + /* Underbar was a poor choice of separator since both RSC and ACTION can + * contain underbars. Here, list action names and name prefixes that can. + */ + const char *actions_with_underbars[] = { + CRMD_ACTION_MIGRATED, + CRMD_ACTION_MIGRATE, + NULL + }; + const char *action_prefixes_with_underbars[] = { + "pre_" CRMD_ACTION_NOTIFY, + "post_" CRMD_ACTION_NOTIFY, + "confirmed-pre_" CRMD_ACTION_NOTIFY, + "confirmed-post_" CRMD_ACTION_NOTIFY, + NULL, + }; + + // Initialize output variables in case of early return + if (rsc_id) { + *rsc_id = NULL; + } + if (op_type) { + *op_type = NULL; + } + if (interval_ms) { + *interval_ms = 0; + } + + // RSC_ACTION_INTERVAL implies a minimum of 5 characters + if (key_len < 5) { + return FALSE; + } + + // Find, parse, and validate interval + interval_underbar = key_len - 2; + while ((interval_underbar > 2) && (key[interval_underbar] != '_')) { + --interval_underbar; + } + if ((interval_underbar == 2) + || !convert_interval(key + interval_underbar + 1, &local_interval_ms)) { + return FALSE; + } + + // Find the base (OCF) action name, disregarding prefixes + action_underbar = match_before(key, interval_underbar, + actions_with_underbars); + if (action_underbar == 0) { + action_underbar = interval_underbar - 2; + while ((action_underbar > 0) && (key[action_underbar] != '_')) { + --action_underbar; + } + if (action_underbar == 0) { + return FALSE; + } + } + possible = match_before(key, action_underbar, + action_prefixes_with_underbars); + if (possible != 0) { + action_underbar = possible; + } + + // Set output variables + if (rsc_id != NULL) { + *rsc_id = strndup(key, action_underbar); + CRM_ASSERT(*rsc_id != NULL); + } + if (op_type != NULL) { + *op_type = strndup(key + action_underbar + 1, + interval_underbar - action_underbar - 1); + CRM_ASSERT(*op_type != NULL); + } + if (interval_ms != NULL) { + *interval_ms = local_interval_ms; + } + return TRUE; +} + +char * +pcmk__notify_key(const char *rsc_id, const char *notify_type, + const char *op_type) +{ + CRM_CHECK(rsc_id != NULL, return NULL); + CRM_CHECK(op_type != NULL, return NULL); + CRM_CHECK(notify_type != NULL, return NULL); + return crm_strdup_printf("%s_%s_notify_%s_0", + rsc_id, notify_type, op_type); +} + +/*! + * \brief Parse a transition magic string into its constituent parts + * + * \param[in] magic Magic string to parse (must be non-NULL) + * \param[out] uuid If non-NULL, where to store copy of parsed UUID + * \param[out] transition_id If non-NULL, where to store parsed transition ID + * \param[out] action_id If non-NULL, where to store parsed action ID + * \param[out] op_status If non-NULL, where to store parsed result status + * \param[out] op_rc If non-NULL, where to store parsed actual rc + * \param[out] target_rc If non-NULL, where to stored parsed target rc + * + * \return TRUE if key was valid, FALSE otherwise + * \note If uuid is supplied and this returns TRUE, the caller is responsible + * for freeing the memory for *uuid using free(). + */ +gboolean +decode_transition_magic(const char *magic, char **uuid, int *transition_id, int *action_id, + int *op_status, int *op_rc, int *target_rc) +{ + int res = 0; + char *key = NULL; + gboolean result = TRUE; + int local_op_status = -1; + int local_op_rc = -1; + + CRM_CHECK(magic != NULL, return FALSE); + +#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); + res = sscanf(magic, "%d:%d;%s", &local_op_status, &local_op_rc, key); +#endif + if (res == EOF) { + crm_err("Could not decode transition information '%s': %s", + magic, pcmk_rc_str(errno)); + result = FALSE; + } else if (res < 3) { + crm_warn("Transition information '%s' incomplete (%d of 3 expected items)", + magic, res); + result = FALSE; + } else { + if (op_status) { + *op_status = local_op_status; + } + if (op_rc) { + *op_rc = local_op_rc; + } + result = decode_transition_key(key, uuid, transition_id, action_id, + target_rc); + } + free(key); + return result; +} + +char * +pcmk__transition_key(int transition_id, int action_id, int target_rc, + const char *node) +{ + CRM_CHECK(node != NULL, return NULL); + return crm_strdup_printf("%d:%d:%d:%-*s", + action_id, transition_id, target_rc, 36, node); +} + +/*! + * \brief Parse a transition key into its constituent parts + * + * \param[in] key Transition key to parse (must be non-NULL) + * \param[out] uuid If non-NULL, where to store copy of parsed UUID + * \param[out] transition_id If non-NULL, where to store parsed transition ID + * \param[out] action_id If non-NULL, where to store parsed action ID + * \param[out] target_rc If non-NULL, where to stored parsed target rc + * + * \return TRUE if key was valid, FALSE otherwise + * \note If uuid is supplied and this returns TRUE, the caller is responsible + * for freeing the memory for *uuid using free(). + */ +gboolean +decode_transition_key(const char *key, char **uuid, int *transition_id, int *action_id, + int *target_rc) +{ + int local_transition_id = -1; + int local_action_id = -1; + int local_target_rc = -1; + char local_uuid[37] = { '\0' }; + + // Initialize any supplied output arguments + if (uuid) { + *uuid = NULL; + } + if (transition_id) { + *transition_id = -1; + } + if (action_id) { + *action_id = -1; + } + if (target_rc) { + *target_rc = -1; + } + + CRM_CHECK(key != NULL, return FALSE); + if (sscanf(key, "%d:%d:%d:%36s", &local_action_id, &local_transition_id, + &local_target_rc, local_uuid) != 4) { + crm_err("Invalid transition key '%s'", key); + return FALSE; + } + if (strlen(local_uuid) != 36) { + crm_warn("Invalid UUID '%s' in transition key '%s'", local_uuid, key); + } + if (uuid) { + *uuid = strdup(local_uuid); + CRM_ASSERT(*uuid); + } + if (transition_id) { + *transition_id = local_transition_id; + } + if (action_id) { + *action_id = local_action_id; + } + if (target_rc) { + *target_rc = local_target_rc; + } + 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) +{ + int rc = 0; + + if (op && op->user_data) { + decode_transition_key(op->user_data, NULL, NULL, NULL, &rc); + } + return rc; +} + +gboolean +did_rsc_op_fail(lrmd_event_data_t * op, int target_rc) +{ + switch (op->op_status) { + case PCMK_EXEC_CANCELLED: + case PCMK_EXEC_PENDING: + return FALSE; + + case PCMK_EXEC_NOT_SUPPORTED: + case PCMK_EXEC_TIMEOUT: + case PCMK_EXEC_ERROR: + case PCMK_EXEC_NOT_CONNECTED: + case PCMK_EXEC_NO_FENCE_DEVICE: + case PCMK_EXEC_NO_SECRETS: + case PCMK_EXEC_INVALID: + return TRUE; + + default: + if (target_rc != op->rc) { + return TRUE; + } + } + + return FALSE; +} + +/*! + * \brief Create a CIB XML element for an operation + * + * \param[in,out] parent If not NULL, make new XML node a child of this + * \param[in] prefix Generate an ID using this prefix + * \param[in] task Operation task to set + * \param[in] interval_spec Operation interval to set + * \param[in] timeout If not NULL, operation timeout to set + * + * \return New XML object on success, NULL otherwise + */ +xmlNode * +crm_create_op_xml(xmlNode *parent, const char *prefix, const char *task, + const char *interval_spec, const char *timeout) +{ + xmlNode *xml_op; + + CRM_CHECK(prefix && task && interval_spec, return NULL); + + xml_op = create_xml_node(parent, XML_ATTR_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); + if (timeout) { + crm_xml_add(xml_op, XML_ATTR_TIMEOUT, timeout); + } + return xml_op; +} + +/*! + * \brief Check whether an operation requires resource agent meta-data + * + * \param[in] rsc_class Resource agent class (or NULL to skip class check) + * \param[in] op Operation action (or NULL to skip op check) + * + * \return true if operation needs meta-data, false otherwise + * \note At least one of rsc_class and op must be specified. + */ +bool +crm_op_needs_metadata(const char *rsc_class, const char *op) +{ + /* Agent metadata is used to determine whether an agent reload is possible, + * so if this op is not relevant to that feature, we don't need metadata. + */ + + CRM_CHECK((rsc_class != NULL) || (op != NULL), return false); + + if ((rsc_class != NULL) + && !pcmk_is_set(pcmk_get_ra_caps(rsc_class), pcmk_ra_cap_params)) { + // Metadata is needed only for resource classes that use parameters + return false; + } + if (op == NULL) { + return true; + } + + // Metadata is needed only for these actions + return pcmk__str_any_of(op, CRMD_ACTION_START, CRMD_ACTION_STATUS, + CRMD_ACTION_PROMOTE, CRMD_ACTION_DEMOTE, + CRMD_ACTION_RELOAD, CRMD_ACTION_RELOAD_AGENT, + CRMD_ACTION_MIGRATE, CRMD_ACTION_MIGRATED, + CRMD_ACTION_NOTIFY, NULL); +} + +/*! + * \internal + * \brief Check whether an action name is for a fencing action + * + * \param[in] action Action name to check + * + * \return true if \p action is "off", "reboot", or "poweroff", otherwise false + */ +bool +pcmk__is_fencing_action(const char *action) +{ + return pcmk__str_any_of(action, "off", "reboot", "poweroff", NULL); +} + +bool +pcmk_is_probe(const char *task, guint interval) +{ + if (task == NULL) { + return false; + } + + return (interval == 0) && pcmk__str_eq(task, CRMD_ACTION_STATUS, 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; +} diff --git a/lib/common/options.c b/lib/common/options.c new file mode 100644 index 0000000..cb32b3f --- /dev/null +++ b/lib/common/options.c @@ -0,0 +1,497 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include <crm_internal.h> + +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <sys/types.h> +#include <sys/stat.h> + +#include <crm/crm.h> + +void +pcmk__cli_help(char cmd) +{ + if (cmd == 'v' || cmd == '$') { + printf("Pacemaker %s\n", PACEMAKER_VERSION); + printf("Written by Andrew Beekhof and " + "the Pacemaker project contributors\n"); + + } else if (cmd == '!') { + printf("Pacemaker %s (Build: %s): %s\n", PACEMAKER_VERSION, BUILD_VERSION, CRM_FEATURES); + } + + crm_exit(CRM_EX_OK); + while(1); // above does not return +} + + +/* + * Environment variable option handling + */ + +/*! + * \internal + * \brief Get the value of a Pacemaker environment variable option + * + * If an environment variable option is set, with either a PCMK_ or (for + * backward compatibility) HA_ prefix, log and return the value. + * + * \param[in] option Environment variable name (without prefix) + * + * \return Value of environment variable option, or NULL in case of + * option name too long or value not found + */ +const char * +pcmk__env_option(const char *option) +{ + const char *const prefixes[] = {"PCMK_", "HA_"}; + char env_name[NAME_MAX]; + const char *value = NULL; + + CRM_CHECK(!pcmk__str_empty(option), return NULL); + + for (int i = 0; i < PCMK__NELEM(prefixes); i++) { + int rv = snprintf(env_name, NAME_MAX, "%s%s", prefixes[i], option); + + if (rv < 0) { + crm_err("Failed to write %s%s to buffer: %s", prefixes[i], option, + strerror(errno)); + return NULL; + } + + if (rv >= sizeof(env_name)) { + crm_trace("\"%s%s\" is too long", prefixes[i], option); + continue; + } + + value = getenv(env_name); + if (value != NULL) { + crm_trace("Found %s = %s", env_name, value); + return value; + } + } + + crm_trace("Nothing found for %s", option); + return NULL; +} + +/*! + * \brief Set or unset a Pacemaker environment variable option + * + * Set an environment variable option with both a PCMK_ and (for + * backward compatibility) HA_ prefix. + * + * \param[in] option Environment variable name (without prefix) + * \param[in] value New value (or NULL to unset) + */ +void +pcmk__set_env_option(const char *option, const char *value) +{ + const char *const prefixes[] = {"PCMK_", "HA_"}; + char env_name[NAME_MAX]; + + CRM_CHECK(!pcmk__str_empty(option) && (strchr(option, '=') == NULL), + return); + + for (int i = 0; i < PCMK__NELEM(prefixes); i++) { + int rv = snprintf(env_name, NAME_MAX, "%s%s", prefixes[i], option); + + if (rv < 0) { + crm_err("Failed to write %s%s to buffer: %s", prefixes[i], option, + strerror(errno)); + return; + } + + if (rv >= sizeof(env_name)) { + crm_trace("\"%s%s\" is too long", prefixes[i], option); + continue; + } + + if (value != NULL) { + crm_trace("Setting %s to %s", env_name, value); + rv = setenv(env_name, value, 1); + } else { + crm_trace("Unsetting %s", env_name); + rv = unsetenv(env_name); + } + + if (rv < 0) { + crm_err("Failed to %sset %s: %s", (value != NULL)? "" : "un", + env_name, strerror(errno)); + } + } +} + +/*! + * \internal + * \brief Check whether Pacemaker environment variable option is enabled + * + * Given a Pacemaker environment variable option that can either be boolean + * or a list of daemon names, return true if the option is enabled for a given + * daemon. + * + * \param[in] daemon Daemon name (can be NULL) + * \param[in] option Pacemaker environment variable name + * + * \return true if variable is enabled for daemon, otherwise false + */ +bool +pcmk__env_option_enabled(const char *daemon, const char *option) +{ + const char *value = pcmk__env_option(option); + + return (value != NULL) + && (crm_is_true(value) + || ((daemon != NULL) && (strstr(value, daemon) != NULL))); +} + + +/* + * Cluster option handling + */ + +bool +pcmk__valid_interval_spec(const char *value) +{ + (void) crm_parse_interval_spec(value); + return errno == 0; +} + +bool +pcmk__valid_boolean(const char *value) +{ + int tmp; + + return crm_str_to_boolean(value, &tmp) == 1; +} + +bool +pcmk__valid_number(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; +} + +bool +pcmk__valid_positive_number(const char *value) +{ + long long num = 0LL; + + return pcmk_str_is_infinity(value) + || ((pcmk__scan_ll(value, &num, 0LL) == pcmk_rc_ok) && (num > 0)); +} + +bool +pcmk__valid_quorum(const char *value) +{ + return pcmk__strcase_any_of(value, "stop", "freeze", "ignore", "demote", "suicide", NULL); +} + +bool +pcmk__valid_script(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; + } + + return true; +} + +bool +pcmk__valid_percentage(const char *value) +{ + char *end = NULL; + long number = strtol(value, &end, 10); + + if (end && (end[0] != '%')) { + return false; + } + return number >= 0; +} + +/*! + * \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 + * + * \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) +{ + const char *value = NULL; + char *new_value = NULL; + + CRM_ASSERT(name != NULL); + + if (options) { + value = g_hash_table_lookup(options, name); + + if ((value == NULL) && old_name) { + value = g_hash_table_lookup(options, old_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); + + // 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; + } + } + + if (value && validate && (validate(value) == FALSE)) { + pcmk__config_err("Using default value for cluster option '%s' " + "because '%s' is invalid", name, value); + value = NULL; + } + + if (value) { + return value; + } + } + + // No value found, use default + value = def_value; + + if (value == NULL) { + crm_trace("No value or default provided for cluster option '%s'", + 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_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; + } + return value; +} + +/*! + * \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 + * + * \return Option value + */ +const char * +pcmk__cluster_option(GHashTable *options, + const pcmk__cluster_option_t *option_list, + int len, const char *name) +{ + const char *value = NULL; + + 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; + } + } + CRM_CHECK(FALSE, crm_err("Bug: looking for unknown option '%s'", name)); + return NULL; +} + +/*! + * \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 + */ +static void +add_desc(GString *s, const char *tag, const char *desc, const char *values, + const char *spaces) +{ + 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); +} + +gchar * +pcmk__format_option_metadata(const char *name, const char *desc_short, + const char *desc_long, + pcmk__cluster_option_t *option_list, int len) +{ + /* 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; + } + + // The standard requires a parameter type + CRM_ASSERT(opt_type != NULL); + + pcmk__g_strcat(s, " <parameter name=\"", opt_name, "\">\n", NULL); + + add_desc(s, "longdesc", opt_desc_long, opt_values, " "); + add_desc(s, "shortdesc", opt_desc_short, NULL, " "); + + pcmk__g_strcat(s, " <content type=\"", opt_type, "\"", NULL); + if (opt_default != NULL) { + pcmk__g_strcat(s, " default=\"", opt_default, "\"", NULL); + } + + if ((opt_values != NULL) && (strcmp(opt_type, "select") == 0)) { + char *str = strdup(opt_values); + const char *delim = ", "; + char *ptr = strtok(str, delim); + + g_string_append(s, ">\n"); + + 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); + + } else { + g_string_append(s, "/>\n"); + } + + g_string_append(s, " </parameter>\n"); + } + g_string_append(s, " </parameters>\n</resource-agent>\n"); + + return g_string_free(s, FALSE); +} + +void +pcmk__validate_cluster_options(GHashTable *options, + pcmk__cluster_option_t *option_list, int len) +{ + 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); + } +} diff --git a/lib/common/output.c b/lib/common/output.c new file mode 100644 index 0000000..2ea9b0b --- /dev/null +++ b/lib/common/output.c @@ -0,0 +1,318 @@ +/* + * Copyright 2019-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <crm/common/util.h> +#include <crm/common/xml.h> +#include <libxml/tree.h> + +#include "crmcommon_private.h" + +static GHashTable *formatters = NULL; + +#if defined(PCMK__UNIT_TESTING) +GHashTable * +pcmk__output_formatters(void) { + return formatters; +} +#endif + +void +pcmk__output_free(pcmk__output_t *out) { + if (out == NULL) { + return; + } + + out->free_priv(out); + + if (out->messages != NULL) { + g_hash_table_destroy(out->messages); + } + + g_free(out->request); + free(out); +} + +/*! + * \internal + * \brief Create a new \p pcmk__output_t structure + * + * This function does not register any message functions with the newly created + * object. + * + * \param[in,out] out Where to store the new output object + * \param[in] fmt_name How to format output + * \param[in] filename Where to write formatted output. This can be a + * filename (the file will be overwritten if it already + * exists), or \p NULL or \p "-" for stdout. For no + * output, pass a filename of \p "/dev/null". + * \param[in] argv List of command line arguments + * + * \return Standard Pacemaker return code + */ +int +pcmk__bare_output_new(pcmk__output_t **out, const char *fmt_name, + const char *filename, char **argv) +{ + pcmk__output_factory_t create = NULL; + + CRM_ASSERT(formatters != NULL && out != NULL); + + /* If no name was given, just try "text". It's up to each tool to register + * what it supports so this also may not be valid. + */ + if (fmt_name == NULL) { + create = g_hash_table_lookup(formatters, "text"); + } else { + create = g_hash_table_lookup(formatters, fmt_name); + } + + if (create == NULL) { + return pcmk_rc_unknown_format; + } + + *out = create(argv); + if (*out == NULL) { + return ENOMEM; + } + + if (pcmk__str_eq(filename, "-", pcmk__str_null_matches)) { + (*out)->dest = stdout; + } else { + (*out)->dest = fopen(filename, "w"); + if ((*out)->dest == NULL) { + pcmk__output_free(*out); + *out = NULL; + return errno; + } + } + + (*out)->quiet = false; + (*out)->messages = pcmk__strkey_table(free, NULL); + + if ((*out)->init(*out) == false) { + pcmk__output_free(*out); + return ENOMEM; + } + + setenv("OCF_OUTPUT_FORMAT", (*out)->fmt_name, 1); + + return pcmk_rc_ok; +} + +int +pcmk__output_new(pcmk__output_t **out, const char *fmt_name, + const char *filename, char **argv) +{ + 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) + */ + pcmk__register_patchset_messages(*out); + } + return rc; +} + +int +pcmk__register_format(GOptionGroup *group, const char *name, + pcmk__output_factory_t create, + const GOptionEntry *options) +{ + CRM_ASSERT(create != NULL && !pcmk__str_empty(name)); + + if (formatters == NULL) { + formatters = pcmk__strkey_table(free, NULL); + } + + if (options != NULL && group != NULL) { + g_option_group_add_entries(group, options); + } + + g_hash_table_insert(formatters, strdup(name), create); + return pcmk_rc_ok; +} + +void +pcmk__register_formats(GOptionGroup *group, + const pcmk__supported_format_t *formats) +{ + if (formats == NULL) { + return; + } + for (const pcmk__supported_format_t *entry = formats; entry->name != NULL; + entry++) { + pcmk__register_format(group, entry->name, entry->create, entry->options); + } +} + +void +pcmk__unregister_formats(void) { + if (formatters != NULL) { + g_hash_table_destroy(formatters); + formatters = NULL; + } +} + +int +pcmk__call_message(pcmk__output_t *out, const char *message_id, ...) { + va_list args; + int rc = pcmk_rc_ok; + pcmk__message_fn_t fn; + + CRM_ASSERT(out != NULL && !pcmk__str_empty(message_id)); + + fn = g_hash_table_lookup(out->messages, message_id); + if (fn == NULL) { + crm_debug("Called unknown output message '%s' for format '%s'", + message_id, out->fmt_name); + return EINVAL; + } + + va_start(args, message_id); + rc = fn(out, args); + va_end(args); + + return rc; +} + +void +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); +} + +void +pcmk__register_messages(pcmk__output_t *out, const pcmk__message_entry_t *table) +{ + for (const pcmk__message_entry_t *entry = table; entry->message_id != NULL; + entry++) { + if (pcmk__strcase_any_of(entry->fmt_name, "default", out->fmt_name, NULL)) { + pcmk__register_message(out, entry->message_id, entry->fn); + } + } +} + +void +pcmk__output_and_clear_error(GError **error, pcmk__output_t *out) +{ + if (error == NULL || *error == NULL) { + return; + } + + if (out != NULL) { + out->err(out, "%s: %s", g_get_prgname(), (*error)->message); + } else { + fprintf(stderr, "%s: %s\n", g_get_prgname(), (*error)->message); + } + + g_clear_error(error); +} + +/*! + * \internal + * \brief Create an XML-only output object + * + * Create an output object that supports only the XML format, and free + * existing XML if supplied (particularly useful for libpacemaker public API + * 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 + * + * \return Standard Pacemaker return code + */ +int +pcmk__xml_output_new(pcmk__output_t **out, xmlNodePtr *xml) { + pcmk__supported_format_t xml_format[] = { + PCMK__SUPPORTED_FORMAT_XML, + { NULL, NULL, NULL } + }; + + if (*xml != NULL) { + xmlFreeNode(*xml); + *xml = NULL; + } + pcmk__register_formats(NULL, xml_format); + return pcmk__output_new(out, "xml", NULL, NULL); +} + +/*! + * \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 + */ +void +pcmk__xml_output_finish(pcmk__output_t *out, xmlNodePtr *xml) { + out->finish(out, 0, FALSE, (void **) xml); + pcmk__output_free(out); +} + +/*! + * \internal + * \brief Create a new output object using the "log" format + * + * \param[out] out Where to store newly allocated output object + * + * \return Standard Pacemaker return code + */ +int +pcmk__log_output_new(pcmk__output_t **out) +{ + int rc = pcmk_rc_ok; + const char* argv[] = { "", NULL }; + pcmk__supported_format_t formats[] = { + PCMK__SUPPORTED_FORMAT_LOG, + { NULL, NULL, NULL } + }; + + pcmk__register_formats(NULL, formats); + rc = pcmk__output_new(out, "log", NULL, (char **) argv); + if ((rc != pcmk_rc_ok) || (*out == NULL)) { + crm_err("Can't log certain messages due to internal error: %s", + pcmk_rc_str(rc)); + return rc; + } + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Create a new output object using the "text" format + * + * \param[out] out Where to store newly allocated output object + * \param[in] filename Name of output destination file + * + * \return Standard Pacemaker return code + */ +int +pcmk__text_output_new(pcmk__output_t **out, const char *filename) +{ + int rc = pcmk_rc_ok; + const char* argv[] = { "", NULL }; + pcmk__supported_format_t formats[] = { + PCMK__SUPPORTED_FORMAT_TEXT, + { NULL, NULL, NULL } + }; + + pcmk__register_formats(NULL, formats); + rc = pcmk__output_new(out, "text", filename, (char **) argv); + if ((rc != pcmk_rc_ok) || (*out == NULL)) { + crm_err("Can't create text output object to internal error: %s", + pcmk_rc_str(rc)); + return rc; + } + return pcmk_rc_ok; +} diff --git a/lib/common/output_html.c b/lib/common/output_html.c new file mode 100644 index 0000000..47b14c1 --- /dev/null +++ b/lib/common/output_html.c @@ -0,0 +1,477 @@ +/* + * Copyright 2019-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <ctype.h> +#include <libxml/HTMLtree.h> +#include <stdarg.h> +#include <stdlib.h> +#include <stdio.h> + +#include <crm/common/cmdline_internal.h> +#include <crm/common/xml.h> + +static const char *stylesheet_default = + ".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" + + ".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" + + ".warning { color: red; font-weight: bold }"; + +static gboolean cgi_output = FALSE; +static char *stylesheet_link = NULL; +static char *title = NULL; +static GSList *extra_headers = NULL; + +GOptionEntry pcmk__html_output_entries[] = { + { "html-cgi", 0, 0, G_OPTION_ARG_NONE, &cgi_output, + "Add CGI headers (requires --output-as=html)", + NULL }, + + { "html-stylesheet", 0, 0, G_OPTION_ARG_STRING, &stylesheet_link, + "Link to an external stylesheet (requires --output-as=html)", + "URI" }, + + { "html-title", 0, 0, G_OPTION_ARG_STRING, &title, + "Specify a page title (requires --output-as=html)", + "TITLE" }, + + { NULL } +}; + +/* The first several elements of this struct must be the same as the first + * several elements of private_data_s in lib/common/output_xml.c. This + * struct gets passed to a bunch of the pcmk__output_xml_* functions which + * assume an XML private_data_s. Keeping them laid out the same means this + * still works. + */ +typedef struct private_data_s { + /* Begin members that must match the XML version */ + xmlNode *root; + GQueue *parent_q; + GSList *errors; + /* End members that must match the XML version */ +} private_data_t; + +static void +html_free_priv(pcmk__output_t *out) { + private_data_t *priv = NULL; + + if (out == NULL || out->priv == NULL) { + return; + } + + priv = out->priv; + + xmlFreeNode(priv->root); + g_queue_free(priv->parent_q); + g_slist_free(priv->errors); + free(priv); + out->priv = NULL; +} + +static bool +html_init(pcmk__output_t *out) { + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL); + + /* If html_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; + } + + priv->parent_q = g_queue_new(); + + priv->root = create_xml_node(NULL, "html"); + xmlCreateIntSubset(priv->root->doc, (pcmkXmlStr) "html", NULL, NULL); + + crm_xml_add(priv->root, "lang", "en"); + g_queue_push_tail(priv->parent_q, priv->root); + priv->errors = NULL; + + pcmk__output_xml_create_parent(out, "body", NULL); + + return true; +} + +static void +add_error_node(gpointer data, gpointer user_data) { + char *str = (char *) data; + pcmk__output_t *out = (pcmk__output_t *) user_data; + out->list_item(out, NULL, "%s", str); +} + +static void +html_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_dest) { + private_data_t *priv = NULL; + htmlNodePtr head_node = NULL; + htmlNodePtr charset_node = NULL; + + CRM_ASSERT(out != NULL); + + priv = out->priv; + + /* If root is NULL, html_init failed and we are being called from pcmk__output_free + * in the pcmk__output_new path. + */ + if (priv == NULL || priv->root == NULL) { + return; + } + + if (cgi_output && print) { + fprintf(out->dest, "Content-Type: text/html\n\n"); + } + + /* Add the head node last - it's not needed earlier because it doesn't contain + * anything else that the user could add, and we want it done last to pick up + * any options that may have been given. + */ + head_node = xmlNewNode(NULL, (pcmkXmlStr) "head"); + + if (title != NULL ) { + pcmk_create_xml_text_node(head_node, "title", title); + } else if (out->request != NULL) { + pcmk_create_xml_text_node(head_node, "title", out->request); + } + + charset_node = create_xml_node(head_node, "meta"); + crm_xml_add(charset_node, "charset", "utf-8"); + + /* Add any extra header nodes the caller might have created. */ + for (int i = 0; i < g_slist_length(extra_headers); i++) { + xmlAddChild(head_node, xmlCopyNode(g_slist_nth_data(extra_headers, i), 1)); + } + + /* Stylesheets are included two different ways. The first is via a built-in + * default (see the stylesheet_default const above). The second is via the + * html-stylesheet option, and this should obviously be a link to a + * stylesheet. The second can override the first. At least one should be + * given. + */ + pcmk_create_xml_text_node(head_node, "style", stylesheet_default); + + if (stylesheet_link != NULL) { + htmlNodePtr link_node = create_xml_node(head_node, "link"); + pcmk__xe_set_props(link_node, "rel", "stylesheet", + "href", stylesheet_link, + NULL); + } + + xmlAddPrevSibling(priv->root->children, head_node); + + if (g_slist_length(priv->errors) > 0) { + out->begin_list(out, "Errors", NULL, NULL); + g_slist_foreach(priv->errors, add_error_node, (gpointer) out); + out->end_list(out); + } + + if (print) { + htmlDocDump(out->dest, priv->root->doc); + } + + if (copy_dest != NULL) { + *copy_dest = copy_xml(priv->root); + } + + g_slist_free_full(extra_headers, (GDestroyNotify) xmlFreeNode); + extra_headers = NULL; +} + +static void +html_reset(pcmk__output_t *out) { + CRM_ASSERT(out != NULL); + + out->dest = freopen(NULL, "w", out->dest); + CRM_ASSERT(out->dest != NULL); + + html_free_priv(out); + html_init(out); +} + +static void +html_subprocess_output(pcmk__output_t *out, int exit_status, + const char *proc_stdout, const char *proc_stderr) { + char *rc_buf = NULL; + + CRM_ASSERT(out != NULL); + + 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); + + 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); + } + 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); + } + + free(rc_buf); +} + +static void +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, + "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)); +} + +G_GNUC_PRINTF(2, 3) +static void +html_err(pcmk__output_t *out, const char *format, ...) { + private_data_t *priv = NULL; + int len = 0; + char *buf = NULL; + va_list ap; + + 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); + + priv->errors = g_slist_append(priv->errors, buf); +} + +G_GNUC_PRINTF(2, 3) +static int +html_info(pcmk__output_t *out, const char *format, ...) { + return pcmk_rc_no_output; +} + +static void +html_output_xml(pcmk__output_t *out, const char *name, const char *buf) { + htmlNodePtr node = NULL; + + CRM_ASSERT(out != NULL); + + node = pcmk__output_create_html_node(out, "pre", NULL, NULL, buf); + crm_xml_add(node, "lang", "xml"); +} + +G_GNUC_PRINTF(4, 5) +static void +html_begin_list(pcmk__output_t *out, const char *singular_noun, + const char *plural_noun, const char *format, ...) { + int q_len = 0; + private_data_t *priv = NULL; + xmlNodePtr node = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + /* If we are already in a list (the queue depth is always at least + * one because of the <html> element), first create a <li> element + * to hold the <h2> and the new list. + */ + q_len = g_queue_get_length(priv->parent_q); + if (q_len > 2) { + pcmk__output_xml_create_parent(out, "li", NULL); + } + + if (format != NULL) { + va_list ap; + char *buf = NULL; + int len; + + va_start(ap, format); + len = vasprintf(&buf, format, ap); + va_end(ap); + CRM_ASSERT(len >= 0); + + if (q_len > 2) { + pcmk__output_create_xml_text_node(out, "h3", buf); + } else { + pcmk__output_create_xml_text_node(out, "h2", buf); + } + + free(buf); + } + + node = pcmk__output_xml_create_parent(out, "ul", NULL); + g_queue_push_tail(priv->parent_q, node); +} + +G_GNUC_PRINTF(3, 4) +static void +html_list_item(pcmk__output_t *out, const char *name, const char *format, ...) { + htmlNodePtr item_node = NULL; + va_list ap; + char *buf = NULL; + int len; + + CRM_ASSERT(out != NULL); + + va_start(ap, format); + len = vasprintf(&buf, format, ap); + CRM_ASSERT(len >= 0); + va_end(ap); + + item_node = pcmk__output_create_xml_text_node(out, "li", buf); + free(buf); + + if (name != NULL) { + crm_xml_add(item_node, "class", name); + } +} + +static void +html_increment_list(pcmk__output_t *out) { + /* This function intentially left blank */ +} + +static void +html_end_list(pcmk__output_t *out) { + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + /* Remove the <ul> tag. */ + g_queue_pop_tail(priv->parent_q); + pcmk__output_xml_pop_parent(out); + + /* Remove the <li> created for nested lists. */ + if (g_queue_get_length(priv->parent_q) > 2) { + pcmk__output_xml_pop_parent(out); + } +} + +static bool +html_is_quiet(pcmk__output_t *out) { + return false; +} + +static void +html_spacer(pcmk__output_t *out) { + CRM_ASSERT(out != NULL); + pcmk__output_create_xml_node(out, "br", NULL); +} + +static void +html_progress(pcmk__output_t *out, bool end) { + /* This function intentially left blank */ +} + +pcmk__output_t * +pcmk__mk_html_output(char **argv) { + pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t)); + + if (retval == NULL) { + return NULL; + } + + retval->fmt_name = "html"; + retval->request = pcmk__quote_cmdline(argv); + + retval->init = html_init; + retval->free_priv = html_free_priv; + retval->finish = html_finish; + retval->reset = html_reset; + + retval->register_message = pcmk__register_message; + retval->message = pcmk__call_message; + + retval->subprocess_output = html_subprocess_output; + retval->version = html_version; + retval->info = html_info; + retval->transient = html_info; + retval->err = html_err; + retval->output_xml = html_output_xml; + + retval->begin_list = html_begin_list; + retval->list_item = html_list_item; + retval->increment_list = html_increment_list; + retval->end_list = html_end_list; + + retval->is_quiet = html_is_quiet; + retval->spacer = html_spacer; + retval->progress = html_progress; + retval->prompt = pcmk__text_prompt; + + return retval; +} + +xmlNodePtr +pcmk__output_create_html_node(pcmk__output_t *out, const char *element_name, const char *id, + const char *class_name, const char *text) { + htmlNodePtr node = NULL; + + CRM_ASSERT(out != NULL); + CRM_CHECK(pcmk__str_eq(out->fmt_name, "html", pcmk__str_none), return NULL); + + node = pcmk__output_create_xml_text_node(out, element_name, text); + + if (class_name != NULL) { + crm_xml_add(node, "class", class_name); + } + + if (id != NULL) { + crm_xml_add(node, "id", id); + } + + return node; +} + +void +pcmk__html_add_header(const char *name, ...) { + htmlNodePtr header_node; + va_list ap; + + va_start(ap, name); + + header_node = xmlNewNode(NULL, (pcmkXmlStr) name); + while (1) { + char *key = va_arg(ap, char *); + char *value; + + if (key == NULL) { + break; + } + + value = va_arg(ap, char *); + crm_xml_add(header_node, key, value); + } + + extra_headers = g_slist_append(extra_headers, header_node); + + va_end(ap); +} diff --git a/lib/common/output_log.c b/lib/common/output_log.c new file mode 100644 index 0000000..aca168d --- /dev/null +++ b/lib/common/output_log.c @@ -0,0 +1,353 @@ +/* + * Copyright 2019-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> +#include <crm/common/cmdline_internal.h> + +#include <ctype.h> +#include <stdarg.h> +#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; + uint8_t log_level; +} private_data_t; + +static void +log_subprocess_output(pcmk__output_t *out, int exit_status, + const char *proc_stdout, const char *proc_stderr) { + /* This function intentionally left blank */ +} + +static void +log_free_priv(pcmk__output_t *out) { + private_data_t *priv = NULL; + + if (out == NULL || out->priv == NULL) { + return; + } + + priv = out->priv; + + g_queue_free(priv->prefixes); + free(priv); + out->priv = NULL; +} + +static bool +log_init(pcmk__output_t *out) { + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL); + + /* If log_init was previously called on this output struct, just return. */ + if (out->priv != NULL) { + return true; + } + + out->priv = calloc(1, sizeof(private_data_t)); + if (out->priv == NULL) { + return false; + } + + priv = out->priv; + + priv->prefixes = g_queue_new(); + priv->log_level = LOG_INFO; + + return true; +} + +static void +log_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_dest) { + /* This function intentionally left blank */ +} + +static void +log_reset(pcmk__output_t *out) { + CRM_ASSERT(out != NULL); + + out->dest = freopen(NULL, "w", out->dest); + CRM_ASSERT(out->dest != NULL); + + log_free_priv(out); + log_init(out); +} + +static void +log_version(pcmk__output_t *out, bool extended) { + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + if (extended) { + do_crm_log(priv->log_level, "Pacemaker %s (Build: %s): %s", + PACEMAKER_VERSION, BUILD_VERSION, CRM_FEATURES); + } else { + do_crm_log(priv->log_level, "Pacemaker %s", PACEMAKER_VERSION); + do_crm_log(priv->log_level, "Written by Andrew Beekhof and" + "the Pacemaker project contributors"); + } +} + +G_GNUC_PRINTF(2, 3) +static void +log_err(pcmk__output_t *out, const char *format, ...) { + va_list ap; + char* buffer = NULL; + int len = 0; + + CRM_ASSERT(out != NULL); + + va_start(ap, format); + /* Informational output does not get indented, to separate it from other + * potentially indented list output. + */ + len = vasprintf(&buffer, format, ap); + CRM_ASSERT(len >= 0); + va_end(ap); + + crm_err("%s", buffer); + + free(buffer); +} + +static void +log_output_xml(pcmk__output_t *out, const char *name, const char *buf) { + xmlNodePtr node = NULL; + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + node = create_xml_node(NULL, name); + xmlNodeSetContent(node, (pcmkXmlStr) buf); + do_crm_log_xml(priv->log_level, name, node); + free(node); +} + +G_GNUC_PRINTF(4, 5) +static void +log_begin_list(pcmk__output_t *out, const char *singular_noun, const char *plural_noun, + const char *format, ...) { + int len = 0; + va_list ap; + char* buffer = NULL; + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + va_start(ap, format); + len = vasprintf(&buffer, format, ap); + CRM_ASSERT(len >= 0); + va_end(ap); + + /* Don't skip empty prefixes, + * otherwise there will be mismatch + * in the log_end_list */ + if(strcmp(buffer, "") == 0) { + /* nothing */ + } + + g_queue_push_tail(priv->prefixes, buffer); +} + +G_GNUC_PRINTF(3, 4) +static void +log_list_item(pcmk__output_t *out, const char *name, const char *format, ...) { + int len = 0; + va_list ap; + private_data_t *priv = NULL; + char prefix[LINE_MAX] = { 0 }; + int offset = 0; + char* buffer = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + for (GList* gIter = priv->prefixes->head; gIter; gIter = gIter->next) { + if (strcmp(prefix, "") != 0) { + offset += snprintf(prefix + offset, LINE_MAX - offset, ": %s", (char *)gIter->data); + } else { + offset = snprintf(prefix, LINE_MAX, "%s", (char *)gIter->data); + } + } + + va_start(ap, format); + len = vasprintf(&buffer, format, ap); + CRM_ASSERT(len >= 0); + va_end(ap); + + if (strcmp(buffer, "") != 0) { /* We don't want empty messages */ + if ((name != NULL) && (strcmp(name, "") != 0)) { + if (strcmp(prefix, "") != 0) { + do_crm_log(priv->log_level, "%s: %s: %s", prefix, name, buffer); + } else { + do_crm_log(priv->log_level, "%s: %s", name, buffer); + } + } else { + if (strcmp(prefix, "") != 0) { + do_crm_log(priv->log_level, "%s: %s", prefix, buffer); + } else { + do_crm_log(priv->log_level, "%s", buffer); + } + } + } + free(buffer); +} + +static void +log_end_list(pcmk__output_t *out) { + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + if (priv->prefixes == NULL) { + return; + } + CRM_ASSERT(priv->prefixes->tail != NULL); + + free((char *)priv->prefixes->tail->data); + g_queue_pop_tail(priv->prefixes); +} + +G_GNUC_PRINTF(2, 3) +static int +log_info(pcmk__output_t *out, const char *format, ...) { + private_data_t *priv = NULL; + int len = 0; + va_list ap; + char* buffer = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + va_start(ap, format); + len = vasprintf(&buffer, format, ap); + CRM_ASSERT(len >= 0); + va_end(ap); + + do_crm_log(priv->log_level, "%s", buffer); + + free(buffer); + return pcmk_rc_ok; +} + +G_GNUC_PRINTF(2, 3) +static int +log_transient(pcmk__output_t *out, const char *format, ...) +{ + private_data_t *priv = NULL; + int len = 0; + va_list ap; + char *buffer = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + va_start(ap, format); + len = vasprintf(&buffer, format, ap); + CRM_ASSERT(len >= 0); + va_end(ap); + + do_crm_log(QB_MAX(priv->log_level, LOG_DEBUG), "%s", buffer); + + free(buffer); + return pcmk_rc_ok; +} + +static bool +log_is_quiet(pcmk__output_t *out) { + return false; +} + +static void +log_spacer(pcmk__output_t *out) { + /* This function intentionally left blank */ +} + +static void +log_progress(pcmk__output_t *out, bool end) { + /* This function intentionally left blank */ +} + +static void +log_prompt(const char *prompt, bool echo, char **dest) { + /* This function intentionally left blank */ +} + +pcmk__output_t * +pcmk__mk_log_output(char **argv) { + pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t)); + + if (retval == NULL) { + return NULL; + } + + retval->fmt_name = "log"; + retval->request = pcmk__quote_cmdline(argv); + + retval->init = log_init; + retval->free_priv = log_free_priv; + retval->finish = log_finish; + retval->reset = log_reset; + + retval->register_message = pcmk__register_message; + retval->message = pcmk__call_message; + + retval->subprocess_output = log_subprocess_output; + retval->version = log_version; + retval->info = log_info; + retval->transient = log_transient; + retval->err = log_err; + retval->output_xml = log_output_xml; + + retval->begin_list = log_begin_list; + retval->list_item = log_list_item; + retval->end_list = log_end_list; + + retval->is_quiet = log_is_quiet; + retval->spacer = log_spacer; + retval->progress = log_progress; + retval->prompt = log_prompt; + + return retval; +} + +uint8_t +pcmk__output_get_log_level(const pcmk__output_t *out) +{ + private_data_t *priv = NULL; + + CRM_ASSERT((out != NULL) && (out->priv != NULL)); + CRM_CHECK(pcmk__str_eq(out->fmt_name, "log", pcmk__str_none), return 0); + + priv = out->priv; + return priv->log_level; +} + +void +pcmk__output_set_log_level(pcmk__output_t *out, uint8_t log_level) { + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + CRM_CHECK(pcmk__str_eq(out->fmt_name, "log", pcmk__str_none), return); + + priv = out->priv; + priv->log_level = log_level; +} diff --git a/lib/common/output_none.c b/lib/common/output_none.c new file mode 100644 index 0000000..581a8b4 --- /dev/null +++ b/lib/common/output_none.c @@ -0,0 +1,152 @@ +/* + * Copyright 2019-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <stdlib.h> +#include <glib.h> + +#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 */ +} + +static bool +none_init(pcmk__output_t *out) { + return true; +} + +static void +none_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_dest) { + /* This function intentionally left blank */ +} + +static void +none_reset(pcmk__output_t *out) { + CRM_ASSERT(out != NULL); + none_free_priv(out); + none_init(out); +} + +static void +none_subprocess_output(pcmk__output_t *out, int exit_status, + const char *proc_stdout, const char *proc_stderr) { + /* This function intentionally left blank */ +} + +static void +none_version(pcmk__output_t *out, bool extended) { + /* This function intentionally left blank */ +} + +G_GNUC_PRINTF(2, 3) +static void +none_err(pcmk__output_t *out, const char *format, ...) { + /* This function intentionally left blank */ +} + +G_GNUC_PRINTF(2, 3) +static int +none_info(pcmk__output_t *out, const char *format, ...) { + return pcmk_rc_no_output; +} + +static void +none_output_xml(pcmk__output_t *out, const char *name, const char *buf) { + /* This function intentionally left blank */ +} + +G_GNUC_PRINTF(4, 5) +static void +none_begin_list(pcmk__output_t *out, const char *singular_noun, const char *plural_noun, + const char *format, ...) { + /* This function intentionally left blank */ +} + +G_GNUC_PRINTF(3, 4) +static void +none_list_item(pcmk__output_t *out, const char *id, const char *format, ...) { + /* This function intentionally left blank */ +} + +static void +none_increment_list(pcmk__output_t *out) { + /* This function intentionally left blank */ +} + +static void +none_end_list(pcmk__output_t *out) { + /* This function intentionally left blank */ +} + +static bool +none_is_quiet(pcmk__output_t *out) { + return out->quiet; +} + +static void +none_spacer(pcmk__output_t *out) { + /* This function intentionally left blank */ +} + +static void +none_progress(pcmk__output_t *out, bool end) { + /* This function intentionally left blank */ +} + +static void +none_prompt(const char *prompt, bool echo, char **dest) { + /* This function intentionally left blank */ +} + +pcmk__output_t * +pcmk__mk_none_output(char **argv) { + pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t)); + + if (retval == NULL) { + return NULL; + } + + retval->fmt_name = PCMK__VALUE_NONE; + retval->request = pcmk__quote_cmdline(argv); + + retval->init = none_init; + retval->free_priv = none_free_priv; + retval->finish = none_finish; + retval->reset = none_reset; + + retval->register_message = pcmk__register_message; + retval->message = pcmk__call_message; + + retval->subprocess_output = none_subprocess_output; + retval->version = none_version; + retval->info = none_info; + retval->transient = none_info; + retval->err = none_err; + retval->output_xml = none_output_xml; + + retval->begin_list = none_begin_list; + retval->list_item = none_list_item; + retval->increment_list = none_increment_list; + retval->end_list = none_end_list; + + retval->is_quiet = none_is_quiet; + retval->spacer = none_spacer; + retval->progress = none_progress; + retval->prompt = none_prompt; + + return retval; +} diff --git a/lib/common/output_text.c b/lib/common/output_text.c new file mode 100644 index 0000000..6bd362d --- /dev/null +++ b/lib/common/output_text.c @@ -0,0 +1,446 @@ +/* + * Copyright 2019-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> +#include <crm/common/cmdline_internal.h> + +#include <stdarg.h> +#include <stdlib.h> +#include <glib.h> +#include <termios.h> + +static gboolean fancy = FALSE; + +GOptionEntry pcmk__text_output_entries[] = { + { "text-fancy", 0, 0, G_OPTION_ARG_NONE, &fancy, + "Use more highly formatted output (requires --output-as=text)", + NULL }, + + { NULL } +}; + +typedef struct text_list_data_s { + unsigned int len; + char *singular_noun; + char *plural_noun; +} text_list_data_t; + +typedef struct private_data_s { + GQueue *parent_q; +} private_data_t; + +static void +text_free_priv(pcmk__output_t *out) { + private_data_t *priv = NULL; + + if (out == NULL || out->priv == NULL) { + return; + } + + priv = out->priv; + + g_queue_free(priv->parent_q); + free(priv); + out->priv = NULL; +} + +static bool +text_init(pcmk__output_t *out) { + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL); + + /* 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; + } + + priv->parent_q = g_queue_new(); + return true; +} + +static void +text_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_dest) { + CRM_ASSERT(out != NULL && out->dest != NULL); + fflush(out->dest); +} + +static void +text_reset(pcmk__output_t *out) { + CRM_ASSERT(out != NULL); + + if (out->dest != stdout) { + out->dest = freopen(NULL, "w", out->dest); + } + + CRM_ASSERT(out->dest != NULL); + + text_free_priv(out); + text_init(out); +} + +static void +text_subprocess_output(pcmk__output_t *out, int exit_status, + const char *proc_stdout, const char *proc_stderr) { + CRM_ASSERT(out != NULL); + + if (proc_stdout != NULL) { + fprintf(out->dest, "%s\n", proc_stdout); + } + + if (proc_stderr != NULL) { + fprintf(out->dest, "%s\n", proc_stderr); + } +} + +static void +text_version(pcmk__output_t *out, bool extended) { + CRM_ASSERT(out != NULL && out->dest != NULL); + + if (extended) { + fprintf(out->dest, "Pacemaker %s (Build: %s): %s\n", PACEMAKER_VERSION, BUILD_VERSION, CRM_FEATURES); + } else { + fprintf(out->dest, "Pacemaker %s\n", PACEMAKER_VERSION); + fprintf(out->dest, "Written by Andrew Beekhof and " + "the Pacemaker project contributors\n"); + } +} + +G_GNUC_PRINTF(2, 3) +static void +text_err(pcmk__output_t *out, const char *format, ...) { + va_list ap; + int len = 0; + + CRM_ASSERT(out != NULL); + + va_start(ap, format); + + /* Informational output does not get indented, to separate it from other + * potentially indented list output. + */ + len = vfprintf(stderr, format, ap); + CRM_ASSERT(len >= 0); + va_end(ap); + + /* Add a newline. */ + fprintf(stderr, "\n"); +} + +G_GNUC_PRINTF(2, 3) +static int +text_info(pcmk__output_t *out, const char *format, ...) { + va_list ap; + int len = 0; + + CRM_ASSERT(out != NULL); + + if (out->is_quiet(out)) { + return pcmk_rc_no_output; + } + + va_start(ap, format); + + /* Informational output does not get indented, to separate it from other + * potentially indented list output. + */ + len = vfprintf(out->dest, format, ap); + CRM_ASSERT(len >= 0); + va_end(ap); + + /* Add a newline. */ + fprintf(out->dest, "\n"); + return pcmk_rc_ok; +} + +G_GNUC_PRINTF(2, 3) +static int +text_transient(pcmk__output_t *out, const char *format, ...) +{ + return pcmk_rc_no_output; +} + +static void +text_output_xml(pcmk__output_t *out, const char *name, const char *buf) { + CRM_ASSERT(out != NULL); + pcmk__indented_printf(out, "%s", buf); +} + +G_GNUC_PRINTF(4, 5) +static void +text_begin_list(pcmk__output_t *out, const char *singular_noun, const char *plural_noun, + const char *format, ...) { + private_data_t *priv = NULL; + text_list_data_t *new_list = NULL; + va_list ap; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + va_start(ap, format); + + if (fancy && format) { + pcmk__indented_vprintf(out, format, ap); + fprintf(out->dest, ":\n"); + } + + va_end(ap); + + new_list = calloc(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); + + g_queue_push_tail(priv->parent_q, new_list); +} + +G_GNUC_PRINTF(3, 4) +static void +text_list_item(pcmk__output_t *out, const char *id, const char *format, ...) { + va_list ap; + + CRM_ASSERT(out != NULL); + + va_start(ap, format); + + if (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 + * just prints right after that one. + */ + pcmk__indented_printf(out, "%s: ", id); + vfprintf(out->dest, format, ap); + } else { + pcmk__indented_vprintf(out, format, ap); + } + } else { + pcmk__indented_vprintf(out, format, ap); + } + + fputc('\n', out->dest); + fflush(out->dest); + va_end(ap); + + out->increment_list(out); +} + +static void +text_increment_list(pcmk__output_t *out) { + private_data_t *priv = NULL; + gpointer tail; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + tail = g_queue_peek_tail(priv->parent_q); + CRM_ASSERT(tail != NULL); + ((text_list_data_t *) tail)->len++; +} + +static void +text_end_list(pcmk__output_t *out) { + private_data_t *priv = NULL; + text_list_data_t *node = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + node = g_queue_pop_tail(priv->parent_q); + + if (node->singular_noun != NULL && node->plural_noun != NULL) { + if (node->len == 1) { + pcmk__indented_printf(out, "%d %s found\n", node->len, node->singular_noun); + } else { + pcmk__indented_printf(out, "%d %s found\n", node->len, node->plural_noun); + } + } + + free(node); +} + +static bool +text_is_quiet(pcmk__output_t *out) { + CRM_ASSERT(out != NULL); + return out->quiet; +} + +static void +text_spacer(pcmk__output_t *out) { + CRM_ASSERT(out != NULL); + fprintf(out->dest, "\n"); +} + +static void +text_progress(pcmk__output_t *out, bool end) { + CRM_ASSERT(out != NULL); + + if (out->dest == stdout) { + fprintf(out->dest, "."); + + if (end) { + fprintf(out->dest, "\n"); + } + } +} + +pcmk__output_t * +pcmk__mk_text_output(char **argv) { + pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t)); + + if (retval == NULL) { + return NULL; + } + + retval->fmt_name = "text"; + retval->request = pcmk__quote_cmdline(argv); + + retval->init = text_init; + retval->free_priv = text_free_priv; + retval->finish = text_finish; + retval->reset = text_reset; + + retval->register_message = pcmk__register_message; + retval->message = pcmk__call_message; + + retval->subprocess_output = text_subprocess_output; + retval->version = text_version; + retval->info = text_info; + retval->transient = text_transient; + retval->err = text_err; + retval->output_xml = text_output_xml; + + retval->begin_list = text_begin_list; + retval->list_item = text_list_item; + retval->increment_list = text_increment_list; + retval->end_list = text_end_list; + + retval->is_quiet = text_is_quiet; + retval->spacer = text_spacer; + retval->progress = text_progress; + retval->prompt = pcmk__text_prompt; + + return retval; +} + +G_GNUC_PRINTF(2, 0) +void +pcmk__formatted_vprintf(pcmk__output_t *out, const char *format, va_list args) { + int len = 0; + + CRM_ASSERT(out != NULL); + CRM_CHECK(pcmk__str_eq(out->fmt_name, "text", pcmk__str_none), return); + + len = vfprintf(out->dest, format, args); + CRM_ASSERT(len >= 0); +} + +G_GNUC_PRINTF(2, 3) +void +pcmk__formatted_printf(pcmk__output_t *out, const char *format, ...) { + va_list ap; + + CRM_ASSERT(out != NULL); + + va_start(ap, format); + pcmk__formatted_vprintf(out, format, ap); + va_end(ap); +} + +G_GNUC_PRINTF(2, 0) +void +pcmk__indented_vprintf(pcmk__output_t *out, const char *format, va_list args) { + CRM_ASSERT(out != NULL); + CRM_CHECK(pcmk__str_eq(out->fmt_name, "text", pcmk__str_none), return); + + if (fancy) { + int level = 0; + private_data_t *priv = out->priv; + + CRM_ASSERT(priv != NULL); + + level = g_queue_get_length(priv->parent_q); + + for (int i = 0; i < level; i++) { + fprintf(out->dest, " "); + } + + if (level > 0) { + fprintf(out->dest, "* "); + } + } + + pcmk__formatted_vprintf(out, format, args); +} + +G_GNUC_PRINTF(2, 3) +void +pcmk__indented_printf(pcmk__output_t *out, const char *format, ...) { + va_list ap; + + CRM_ASSERT(out != NULL); + + va_start(ap, format); + pcmk__indented_vprintf(out, format, ap); + va_end(ap); +} + +void +pcmk__text_prompt(const char *prompt, bool echo, char **dest) +{ + int rc = 0; + struct termios settings; + tcflag_t orig_c_lflag = 0; + + CRM_ASSERT(prompt != NULL); + CRM_ASSERT(dest != NULL); + + if (!echo) { + rc = tcgetattr(0, &settings); + if (rc == 0) { + orig_c_lflag = settings.c_lflag; + settings.c_lflag &= ~ECHO; + rc = tcsetattr(0, TCSANOW, &settings); + } + } + + if (rc == 0) { + fprintf(stderr, "%s: ", prompt); + + if (*dest != NULL) { + free(*dest); + *dest = NULL; + } + +#if HAVE_SSCANF_M + rc = scanf("%ms", dest); +#else + *dest = calloc(1, 1024); + rc = scanf("%1023s", *dest); +#endif + fprintf(stderr, "\n"); + } + + if (rc < 1) { + free(*dest); + *dest = NULL; + } + + if (orig_c_lflag != 0) { + settings.c_lflag = orig_c_lflag; + /* rc = */ tcsetattr(0, TCSANOW, &settings); + } +} diff --git a/lib/common/output_xml.c b/lib/common/output_xml.c new file mode 100644 index 0000000..0972638 --- /dev/null +++ b/lib/common/output_xml.c @@ -0,0 +1,541 @@ +/* + * Copyright 2019-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <ctype.h> +#include <stdarg.h> +#include <stdlib.h> +#include <stdio.h> +#include <glib.h> + +#include <crm/common/cmdline_internal.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 } +}; + +typedef struct subst_s { + const char *from; + const char *to; +} subst_t; + +static subst_t substitutions[] = { + { "Active Resources", "resources" }, + { "Allocation Scores", "allocations" }, + { "Allocation 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" }, + + { NULL, NULL } +}; + +/* The first several elements of this struct must be the same as the first + * several elements of private_data_s in lib/common/output_html.c. That + * struct gets passed to a bunch of the pcmk__output_xml_* functions which + * assume an XML private_data_s. Keeping them laid out the same means this + * still works. + */ +typedef struct private_data_s { + /* Begin members that must match the HTML version */ + xmlNode *root; + GQueue *parent_q; + GSList *errors; + /* End members that must match the HTML version */ + bool legacy_xml; +} private_data_t; + +static void +xml_free_priv(pcmk__output_t *out) { + private_data_t *priv = NULL; + + if (out == NULL || out->priv == NULL) { + return; + } + + priv = out->priv; + + free_xml(priv->root); + g_queue_free(priv->parent_q); + g_slist_free(priv->errors); + free(priv); + out->priv = NULL; +} + +static bool +xml_init(pcmk__output_t *out) { + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL); + + /* If xml_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; + } + + 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; + xmlNodePtr node = (xmlNodePtr) user_data; + pcmk_create_xml_text_node(node, "error", str); +} + +static void +xml_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_dest) { + private_data_t *priv = NULL; + xmlNodePtr node; + + 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) { + return; + } + + if (legacy_xml) { + GSList *node = priv->errors; + + if (exit_status != CRM_EX_OK) { + fprintf(stderr, "%s\n", crm_exit_str(exit_status)); + } + + while (node != NULL) { + fprintf(stderr, "%s\n", (char *) node->data); + node = node->next; + } + } 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), + NULL); + + if (g_slist_length(priv->errors) > 0) { + xmlNodePtr errors_node = create_xml_node(node, "errors"); + g_slist_foreach(priv->errors, add_error_node, (gpointer) errors_node); + } + + free(rc_as_str); + } + + if (print) { + char *buf = dump_xml_formatted_with_text(priv->root); + fprintf(out->dest, "%s", buf); + fflush(out->dest); + free(buf); + } + + if (copy_dest != NULL) { + *copy_dest = copy_xml(priv->root); + } +} + +static void +xml_reset(pcmk__output_t *out) { + CRM_ASSERT(out != NULL); + + out->dest = freopen(NULL, "w", out->dest); + CRM_ASSERT(out->dest != NULL); + + xml_free_priv(out); + xml_init(out); +} + +static void +xml_subprocess_output(pcmk__output_t *out, int exit_status, + const char *proc_stdout, const char *proc_stderr) { + xmlNodePtr node, child_node; + char *rc_as_str = NULL; + + CRM_ASSERT(out != NULL); + + rc_as_str = pcmk__itoa(exit_status); + + node = pcmk__output_xml_create_parent(out, "command", + "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"); + } + + if (proc_stderr != NULL) { + child_node = pcmk_create_xml_text_node(node, "output", proc_stderr); + crm_xml_add(child_node, "source", "stderr"); + } + + free(rc_as_str); +} + +static void +xml_version(pcmk__output_t *out, bool extended) { + 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, + NULL); +} + +G_GNUC_PRINTF(2, 3) +static void +xml_err(pcmk__output_t *out, const char *format, ...) { + private_data_t *priv = NULL; + int len = 0; + char *buf = NULL; + va_list ap; + + 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); + + priv->errors = g_slist_append(priv->errors, buf); +} + +G_GNUC_PRINTF(2, 3) +static int +xml_info(pcmk__output_t *out, const char *format, ...) { + return pcmk_rc_no_output; +} + +static void +xml_output_xml(pcmk__output_t *out, const char *name, const char *buf) { + xmlNodePtr parent = NULL; + xmlNodePtr cdata_node = NULL; + + CRM_ASSERT(out != NULL); + + parent = pcmk__output_create_xml_node(out, name, NULL); + cdata_node = xmlNewCDataBlock(getDocPtr(parent), (pcmkXmlStr) buf, strlen(buf)); + xmlAddChild(parent, cdata_node); +} + +G_GNUC_PRINTF(4, 5) +static void +xml_begin_list(pcmk__output_t *out, const char *singular_noun, const char *plural_noun, + const char *format, ...) { + va_list ap; + char *name = NULL; + char *buf = NULL; + int len; + + CRM_ASSERT(out != NULL); + + 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; + } + } + } + + if (name == NULL) { + 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, + NULL); + } + + g_free(name); + free(buf); +} + +G_GNUC_PRINTF(3, 4) +static void +xml_list_item(pcmk__output_t *out, const char *name, const char *format, ...) { + xmlNodePtr item_node = NULL; + va_list ap; + char *buf = NULL; + int len; + + CRM_ASSERT(out != NULL); + + va_start(ap, format); + len = vasprintf(&buf, format, ap); + CRM_ASSERT(len >= 0); + va_end(ap); + + item_node = pcmk__output_create_xml_text_node(out, "item", buf); + + if (name != NULL) { + crm_xml_add(item_node, "name", name); + } + + free(buf); +} + +static void +xml_increment_list(pcmk__output_t *out) { + /* This function intentially left blank */ +} + +static void +xml_end_list(pcmk__output_t *out) { + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + if (priv->legacy_xml || simple_list) { + g_queue_pop_tail(priv->parent_q); + } else { + char *buf = NULL; + xmlNodePtr node; + + node = g_queue_pop_tail(priv->parent_q); + buf = crm_strdup_printf("%lu", xmlChildElementCount(node)); + crm_xml_add(node, "count", buf); + free(buf); + } +} + +static bool +xml_is_quiet(pcmk__output_t *out) { + return false; +} + +static void +xml_spacer(pcmk__output_t *out) { + /* This function intentionally left blank */ +} + +static void +xml_progress(pcmk__output_t *out, bool end) { + /* This function intentionally left blank */ +} + +pcmk__output_t * +pcmk__mk_xml_output(char **argv) { + pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t)); + + if (retval == NULL) { + return NULL; + } + + retval->fmt_name = "xml"; + retval->request = pcmk__quote_cmdline(argv); + + retval->init = xml_init; + retval->free_priv = xml_free_priv; + retval->finish = xml_finish; + retval->reset = xml_reset; + + retval->register_message = pcmk__register_message; + retval->message = pcmk__call_message; + + retval->subprocess_output = xml_subprocess_output; + retval->version = xml_version; + retval->info = xml_info; + retval->transient = xml_info; + retval->err = xml_err; + retval->output_xml = xml_output_xml; + + retval->begin_list = xml_begin_list; + retval->list_item = xml_list_item; + retval->increment_list = xml_increment_list; + retval->end_list = xml_end_list; + + retval->is_quiet = xml_is_quiet; + retval->spacer = xml_spacer; + retval->progress = xml_progress; + retval->prompt = pcmk__text_prompt; + + return retval; +} + +xmlNodePtr +pcmk__output_xml_create_parent(pcmk__output_t *out, const char *name, ...) { + va_list args; + xmlNodePtr node = NULL; + + CRM_ASSERT(out != NULL); + CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL); + + node = pcmk__output_create_xml_node(out, name, NULL); + + va_start(args, name); + pcmk__xe_set_propv(node, args); + va_end(args); + + pcmk__output_xml_push_parent(out, node); + return node; +} + +void +pcmk__output_xml_add_node_copy(pcmk__output_t *out, xmlNodePtr node) { + private_data_t *priv = NULL; + xmlNodePtr parent = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + CRM_ASSERT(node != NULL); + CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return); + + 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); +} + +xmlNodePtr +pcmk__output_create_xml_node(pcmk__output_t *out, const char *name, ...) { + xmlNodePtr node = NULL; + private_data_t *priv = NULL; + va_list args; + + CRM_ASSERT(out != NULL && out->priv != NULL); + CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL); + + priv = out->priv; + + node = create_xml_node(g_queue_peek_tail(priv->parent_q), name); + va_start(args, name); + pcmk__xe_set_propv(node, args); + va_end(args); + + return node; +} + +xmlNodePtr +pcmk__output_create_xml_text_node(pcmk__output_t *out, const char *name, const char *content) { + xmlNodePtr node = NULL; + + CRM_ASSERT(out != NULL); + 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); + return node; +} + +void +pcmk__output_xml_push_parent(pcmk__output_t *out, xmlNodePtr parent) { + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + CRM_ASSERT(parent != NULL); + CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return); + + priv = out->priv; + + g_queue_push_tail(priv->parent_q, parent); +} + +void +pcmk__output_xml_pop_parent(pcmk__output_t *out) { + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return); + + priv = out->priv; + + CRM_ASSERT(g_queue_get_length(priv->parent_q) > 0); + g_queue_pop_tail(priv->parent_q); +} + +xmlNodePtr +pcmk__output_xml_peek_parent(pcmk__output_t *out) { + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL); + + priv = out->priv; + + /* If queue is empty NULL will be returned */ + return g_queue_peek_tail(priv->parent_q); +} diff --git a/lib/common/patchset.c b/lib/common/patchset.c new file mode 100644 index 0000000..8c1362d --- /dev/null +++ b/lib/common/patchset.c @@ -0,0 +1,1516 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#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 <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. + */ +static void +add_xml_changes_to_patchset(xmlNode *xml, xmlNode *patchset) +{ + xmlNode *cIter = NULL; + xmlAttr *pIter = NULL; + xmlNode *change = NULL; + xml_node_private_t *nodepriv = xml->_private; + const char *value = NULL; + + // If this XML node is new, just report that + if (patchset && pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { + GString *xpath = pcmk__element_xpath(xml->parent); + + if (xpath != NULL) { + int position = pcmk__xml_position(xml, pcmk__xf_deleted); + + change = create_xml_node(patchset, XML_DIFF_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); + g_string_free(xpath, TRUE); + } + + return; + } + + // Check each of the XML node's attributes for changes + for (pIter = pcmk__xe_first_attr(xml); pIter != NULL; + pIter = pIter->next) { + xmlNode *attr = NULL; + + nodepriv = pIter->_private; + if (!pcmk_any_flags_set(nodepriv->flags, pcmk__xf_deleted|pcmk__xf_dirty)) { + continue; + } + + if (change == NULL) { + GString *xpath = pcmk__element_xpath(xml); + + if (xpath != NULL) { + change = create_xml_node(patchset, XML_DIFF_CHANGE); + + crm_xml_add(change, XML_DIFF_OP, "modify"); + crm_xml_add(change, XML_DIFF_PATH, (const char *) xpath->str); + + change = create_xml_node(change, XML_DIFF_LIST); + g_string_free(xpath, TRUE); + } + } + + attr = create_xml_node(change, XML_DIFF_ATTR); + + crm_xml_add(attr, XML_NVPAIR_ATTR_NAME, (const char *)pIter->name); + if (nodepriv->flags & pcmk__xf_deleted) { + crm_xml_add(attr, XML_DIFF_OP, "unset"); + + } else { + crm_xml_add(attr, XML_DIFF_OP, "set"); + + value = crm_element_value(xml, (const char *) pIter->name); + crm_xml_add(attr, XML_NVPAIR_ATTR_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); + + for (pIter = pcmk__xe_first_attr(xml); pIter != NULL; + pIter = pIter->next) { + nodepriv = pIter->_private; + if (!pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) { + value = crm_element_value(xml, (const char *) pIter->name); + crm_xml_add(result, (const char *)pIter->name, value); + } + } + } + + // Now recursively do the same for each child node of this node + for (cIter = pcmk__xml_first_child(xml); cIter != NULL; + cIter = pcmk__xml_next(cIter)) { + add_xml_changes_to_patchset(cIter, patchset); + } + + nodepriv = xml->_private; + if (patchset && pcmk_is_set(nodepriv->flags, pcmk__xf_moved)) { + 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)); + + if (xpath != NULL) { + change = create_xml_node(patchset, XML_DIFF_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, + pcmk__xml_position(xml, pcmk__xf_deleted)); + g_string_free(xpath, TRUE); + } + } +} + +static bool +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); + + if (config) { + nodepriv = config->_private; + } + if ((nodepriv != NULL) && pcmk_is_set(nodepriv->flags, pcmk__xf_dirty)) { + return TRUE; + } + + if ((xml->doc != NULL) && (xml->doc->_private != NULL)) { + docpriv = xml->doc->_private; + for (gIter = docpriv->deleted_objs; gIter; gIter = gIter->next) { + pcmk__deleted_xml_t *deleted_obj = gIter->data; + + if (strstr(deleted_obj->path, + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION) != NULL) { + return TRUE; + } + } + } + return FALSE; +} + +static void +xml_repair_v1_diff(xmlNode *last, xmlNode *next, xmlNode *local_diff, + gboolean changed) +{ + int lpc = 0; + xmlNode *cib = NULL; + xmlNode *diff_child = NULL; + + const char *tag = NULL; + + const char *vfields[] = { + XML_ATTR_GENERATION_ADMIN, + XML_ATTR_GENERATION, + XML_ATTR_NUMUPDATES, + }; + + if (local_diff == NULL) { + crm_trace("Nothing to do"); + return; + } + + tag = "diff-removed"; + diff_child = find_xml_node(local_diff, tag, FALSE); + if (diff_child == NULL) { + diff_child = create_xml_node(local_diff, tag); + } + + tag = XML_TAG_CIB; + cib = find_xml_node(diff_child, tag, FALSE); + if (cib == NULL) { + cib = create_xml_node(diff_child, tag); + } + + for (lpc = 0; (last != NULL) && (lpc < PCMK__NELEM(vfields)); lpc++) { + const char *value = crm_element_value(last, vfields[lpc]); + + crm_xml_add(diff_child, vfields[lpc], value); + if (changed || lpc == 2) { + crm_xml_add(cib, vfields[lpc], value); + } + } + + tag = "diff-added"; + diff_child = find_xml_node(local_diff, tag, FALSE); + if (diff_child == NULL) { + diff_child = create_xml_node(local_diff, tag); + } + + tag = XML_TAG_CIB; + cib = find_xml_node(diff_child, tag, FALSE); + if (cib == NULL) { + cib = create_xml_node(diff_child, tag); + } + + for (lpc = 0; next && lpc < PCMK__NELEM(vfields); lpc++) { + const char *value = crm_element_value(next, vfields[lpc]); + + crm_xml_add(diff_child, vfields[lpc], value); + } + + for (xmlAttrPtr a = pcmk__xe_first_attr(next); a != NULL; a = a->next) { + const char *p_value = crm_element_value(next, (const char *) a->name); + + xmlSetProp(cib, a->name, (pcmkXmlStr) p_value); + } + + crm_log_xml_explicit(local_diff, "Repaired-diff"); +} + +static xmlNode * +xml_create_patchset_v1(xmlNode *source, xmlNode *target, bool config, + bool suppress) +{ + xmlNode *patchset = diff_xml_object(source, target, suppress); + + if (patchset) { + CRM_LOG_ASSERT(xml_document_dirty(target)); + xml_repair_v1_diff(source, target, patchset, config); + crm_xml_add(patchset, "format", "1"); + } + return patchset; +} + +static xmlNode * +xml_create_patchset_v2(xmlNode *source, xmlNode *target) +{ + int lpc = 0; + GList *gIter = NULL; + xml_doc_private_t *docpriv; + + xmlNode *v = NULL; + xmlNode *version = NULL; + xmlNode *patchset = NULL; + const char *vfields[] = { + XML_ATTR_GENERATION_ADMIN, + XML_ATTR_GENERATION, + XML_ATTR_NUMUPDATES, + }; + + CRM_ASSERT(target); + if (!xml_document_dirty(target)) { + return NULL; + } + + CRM_ASSERT(target->doc); + docpriv = target->doc->_private; + + patchset = create_xml_node(NULL, XML_TAG_DIFF); + crm_xml_add_int(patchset, "format", 2); + + version = create_xml_node(patchset, XML_DIFF_VERSION); + + v = create_xml_node(version, XML_DIFF_VSOURCE); + for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { + const char *value = crm_element_value(source, vfields[lpc]); + + if (value == NULL) { + value = "1"; + } + crm_xml_add(v, vfields[lpc], value); + } + + v = create_xml_node(version, XML_DIFF_VTARGET); + for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { + const char *value = crm_element_value(target, vfields[lpc]); + + if (value == NULL) { + value = "1"; + } + crm_xml_add(v, vfields[lpc], value); + } + + 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); + + crm_xml_add(change, XML_DIFF_OP, "delete"); + crm_xml_add(change, XML_DIFF_PATH, deleted_obj->path); + if (deleted_obj->position >= 0) { + crm_xml_add_int(change, XML_DIFF_POSITION, deleted_obj->position); + } + } + + add_xml_changes_to_patchset(target, patchset); + return patchset; +} + +xmlNode * +xml_create_patchset(int format, xmlNode *source, xmlNode *target, + bool *config_changed, bool manage_version) +{ + int counter = 0; + bool config = FALSE; + xmlNode *patch = NULL; + const char *version = crm_element_value(source, XML_ATTR_CRM_VERSION); + + xml_acl_disable(target); + if (!xml_document_dirty(target)) { + crm_trace("No change %d", format); + return NULL; /* No change */ + } + + config = is_config_change(target); + if (config_changed) { + *config_changed = config; + } + + if (manage_version && config) { + crm_trace("Config changed %d", format); + crm_xml_add(target, XML_ATTR_NUMUPDATES, "0"); + + crm_element_value_int(target, XML_ATTR_GENERATION, &counter); + crm_xml_add_int(target, XML_ATTR_GENERATION, counter+1); + + } else if (manage_version) { + crm_element_value_int(target, XML_ATTR_NUMUPDATES, &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)); + } + + if (format == 0) { + if (compare_version("3.0.8", version) < 0) { + format = 2; + } else { + format = 1; + } + crm_trace("Using patch format %d for version: %s", format, version); + } + + switch (format) { + case 1: + patch = xml_create_patchset_v1(source, target, config, FALSE); + break; + case 2: + patch = xml_create_patchset_v2(source, target); + break; + default: + crm_err("Unknown patch format: %d", format); + return NULL; + } + return patch; +} + +void +patchset_process_digest(xmlNode *patch, xmlNode *source, xmlNode *target, + bool with_digest) +{ + int format = 1; + const char *version = NULL; + char *digest = NULL; + + if ((patch == NULL) || (source == NULL) || (target == NULL)) { + return; + } + + /* We should always call xml_accept_changes() before calculating a digest. + * Otherwise, with an on-tracking dirty target, we could get a wrong digest. + */ + CRM_LOG_ASSERT(!xml_document_dirty(target)); + + crm_element_value_int(patch, "format", &format); + if ((format > 1) && !with_digest) { + return; + } + + version = crm_element_value(source, XML_ATTR_CRM_VERSION); + digest = calculate_xml_versioned_digest(target, FALSE, TRUE, version); + + crm_xml_add(patch, XML_ATTR_DIGEST, digest); + free(digest); + + return; +} + +// Return true if attribute name is not "id" +static bool +not_id(xmlAttrPtr attr, void *user_data) +{ + return strcmp((const char *) attr->name, XML_ATTR_ID) != 0; +} + +// Apply the removals section of an v1 patchset to an XML node +static void +process_v1_removals(xmlNode *target, xmlNode *patch) +{ + xmlNode *patch_child = NULL; + xmlNode *cIter = NULL; + + char *id = NULL; + const char *name = NULL; + const char *value = NULL; + + if ((target == NULL) || (patch == NULL)) { + return; + } + + if (target->type == XML_COMMENT_NODE) { + gboolean dummy; + + subtract_xml_comment(target->parent, target, patch, &dummy); + } + + name = crm_element_name(target); + CRM_CHECK(name != NULL, return); + CRM_CHECK(pcmk__str_eq(crm_element_name(target), crm_element_name(patch), + pcmk__str_casei), + return); + CRM_CHECK(pcmk__str_eq(ID(target), ID(patch), pcmk__str_casei), 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); + if ((value != NULL) && (strcmp(value, "removed:top") == 0)) { + crm_trace("We are the root of the deletion: %s.id=%s", name, id); + free_xml(target); + free(id); + return; + } + + // Removing then restoring id would change ordering of properties + pcmk__xe_remove_matching_attrs(patch, not_id, NULL); + + // Changes to child objects + cIter = pcmk__xml_first_child(target); + while (cIter) { + xmlNode *target_child = cIter; + + cIter = pcmk__xml_next(cIter); + patch_child = pcmk__xml_match(patch, target_child, false); + process_v1_removals(target_child, patch_child); + } + free(id); +} + +// Apply the additions section of an v1 patchset to an XML node +static void +process_v1_additions(xmlNode *parent, xmlNode *target, xmlNode *patch) +{ + xmlNode *patch_child = NULL; + xmlNode *target_child = NULL; + xmlAttrPtr xIter = NULL; + + const char *id = NULL; + const char *name = NULL; + const char *value = NULL; + + if (patch == NULL) { + return; + } else if ((parent == NULL) && (target == NULL)) { + return; + } + + // Check for XML_DIFF_MARKER in a child + value = crm_element_value(patch, XML_DIFF_MARKER); + if ((target == NULL) && (value != NULL) + && (strcmp(value, "added:top") == 0)) { + id = ID(patch); + name = crm_element_name(patch); + crm_trace("We are the root of the addition: %s.id=%s", name, id); + add_node_copy(parent, patch); + return; + + } else if (target == NULL) { + id = ID(patch); + name = crm_element_name(patch); + crm_err("Could not locate: %s.id=%s", name, id); + return; + } + + if (target->type == XML_COMMENT_NODE) { + pcmk__xc_update(parent, target, patch); + } + + name = crm_element_name(target); + CRM_CHECK(name != NULL, return); + CRM_CHECK(pcmk__str_eq(crm_element_name(target), crm_element_name(patch), + pcmk__str_casei), + return); + CRM_CHECK(pcmk__str_eq(ID(target), ID(patch), pcmk__str_casei), 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 = crm_element_value(patch, p_name); + + xml_remove_prop(target, p_name); // Preserve patch order + crm_xml_add(target, p_name, p_value); + } + + // Changes to child objects + for (patch_child = pcmk__xml_first_child(patch); patch_child != NULL; + patch_child = pcmk__xml_next(patch_child)) { + + target_child = pcmk__xml_match(target, patch_child, false); + process_v1_additions(target, target_child, patch_child); + } +} + +/*! + * \internal + * \brief Find additions or removals in a patch set + * + * \param[in] patchset XML of patch + * \param[in] format Patch version + * \param[in] added TRUE if looking for additions, FALSE if removals + * \param[in,out] patch_node Will be set to node if found + * + * \return TRUE if format is valid, FALSE if invalid + */ +static bool +find_patch_xml_node(const xmlNode *patchset, int format, bool added, + xmlNode **patch_node) +{ + xmlNode *cib_node; + const char *label; + + switch (format) { + case 1: + label = added? "diff-added" : "diff-removed"; + *patch_node = find_xml_node(patchset, label, FALSE); + cib_node = find_xml_node(*patch_node, "cib", FALSE); + 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); + break; + default: + crm_warn("Unknown patch format: %d", format); + *patch_node = NULL; + return FALSE; + } + return TRUE; +} + +// Get CIB versions used for additions and deletions in a patchset +bool +xml_patch_versions(const xmlNode *patchset, int add[3], int del[3]) +{ + int lpc = 0; + int format = 1; + xmlNode *tmp = NULL; + + const char *vfields[] = { + XML_ATTR_GENERATION_ADMIN, + XML_ATTR_GENERATION, + XML_ATTR_NUMUPDATES, + }; + + + crm_element_value_int(patchset, "format", &format); + + /* Process removals */ + if (!find_patch_xml_node(patchset, format, FALSE, &tmp)) { + return -EINVAL; + } + if (tmp != NULL) { + for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { + crm_element_value_int(tmp, vfields[lpc], &(del[lpc])); + crm_trace("Got %d for del[%s]", del[lpc], vfields[lpc]); + } + } + + /* Process additions */ + if (!find_patch_xml_node(patchset, format, TRUE, &tmp)) { + return -EINVAL; + } + if (tmp != NULL) { + for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { + crm_element_value_int(tmp, vfields[lpc], &(add[lpc])); + crm_trace("Got %d for add[%s]", add[lpc], vfields[lpc]); + } + } + return pcmk_ok; +} + +/*! + * \internal + * \brief Check whether patchset can be applied to current CIB + * + * \param[in] xml Root of current CIB + * \param[in] patchset Patchset to check + * \param[in] format Patchset version + * + * \return Standard Pacemaker return code + */ +static int +xml_patch_version_check(const xmlNode *xml, const xmlNode *patchset, int format) +{ + int lpc = 0; + bool changed = FALSE; + + int this[] = { 0, 0, 0 }; + int add[] = { 0, 0, 0 }; + int del[] = { 0, 0, 0 }; + + const char *vfields[] = { + XML_ATTR_GENERATION_ADMIN, + XML_ATTR_GENERATION, + XML_ATTR_NUMUPDATES, + }; + + for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { + crm_element_value_int(xml, vfields[lpc], &(this[lpc])); + crm_trace("Got %d for this[%s]", this[lpc], vfields[lpc]); + if (this[lpc] < 0) { + this[lpc] = 0; + } + } + + /* Set some defaults in case nothing is present */ + add[0] = this[0]; + add[1] = this[1]; + add[2] = this[2] + 1; + for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { + del[lpc] = this[lpc]; + } + + xml_patch_versions(patchset, add, del); + + for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { + if (this[lpc] < del[lpc]) { + crm_debug("Current %s is too low (%d.%d.%d < %d.%d.%d --> %d.%d.%d)", + vfields[lpc], this[0], this[1], this[2], + del[0], del[1], del[2], add[0], add[1], add[2]); + return pcmk_rc_diff_resync; + + } else if (this[lpc] > del[lpc]) { + crm_info("Current %s is too high (%d.%d.%d > %d.%d.%d --> %d.%d.%d) %p", + vfields[lpc], this[0], this[1], this[2], + del[0], del[1], del[2], add[0], add[1], add[2], patchset); + crm_log_xml_info(patchset, "OldPatch"); + return pcmk_rc_old_data; + } + } + + for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { + if (add[lpc] > del[lpc]) { + changed = TRUE; + } + } + + if (!changed) { + crm_notice("Versions did not change in patch %d.%d.%d", + add[0], add[1], add[2]); + return pcmk_rc_old_data; + } + + crm_debug("Can apply patch %d.%d.%d to %d.%d.%d", + add[0], add[1], add[2], this[0], this[1], this[2]); + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Apply a version 1 patchset to an XML node + * + * \param[in,out] xml XML to apply patchset to + * \param[in] patchset Patchset to apply + * + * \return Standard Pacemaker return code + */ +static int +apply_v1_patchset(xmlNode *xml, const xmlNode *patchset) +{ + int rc = pcmk_rc_ok; + int root_nodes_seen = 0; + + xmlNode *child_diff = NULL; + xmlNode *added = find_xml_node(patchset, "diff-added", FALSE); + xmlNode *removed = find_xml_node(patchset, "diff-removed", FALSE); + xmlNode *old = copy_xml(xml); + + crm_trace("Subtraction Phase"); + for (child_diff = pcmk__xml_first_child(removed); child_diff != NULL; + child_diff = pcmk__xml_next(child_diff)) { + CRM_CHECK(root_nodes_seen == 0, rc = FALSE); + if (root_nodes_seen == 0) { + process_v1_removals(xml, child_diff); + } + root_nodes_seen++; + } + + if (root_nodes_seen > 1) { + crm_err("(-) Diffs cannot contain more than one change set... saw %d", + root_nodes_seen); + rc = ENOTUNIQ; + } + + root_nodes_seen = 0; + crm_trace("Addition Phase"); + if (rc == pcmk_rc_ok) { + xmlNode *child_diff = NULL; + + for (child_diff = pcmk__xml_first_child(added); child_diff != NULL; + child_diff = pcmk__xml_next(child_diff)) { + CRM_CHECK(root_nodes_seen == 0, rc = FALSE); + if (root_nodes_seen == 0) { + process_v1_additions(NULL, xml, child_diff); + } + root_nodes_seen++; + } + } + + if (root_nodes_seen > 1) { + crm_err("(+) Diffs cannot contain more than one change set... saw %d", + root_nodes_seen); + rc = ENOTUNIQ; + } + + purge_diff_markers(xml); // Purge prior to checking digest + + free_xml(old); + return rc; +} + +// Return first child matching element name and optionally id or position +static xmlNode * +first_matching_xml_child(const xmlNode *parent, const char *name, + const char *id, int position) +{ + xmlNode *cIter = NULL; + + for (cIter = pcmk__xml_first_child(parent); cIter != NULL; + cIter = pcmk__xml_next(cIter)) { + if (strcmp((const char *) cIter->name, name) != 0) { + continue; + } else if (id) { + const char *cid = ID(cIter); + + if ((cid == NULL) || (strcmp(cid, id) != 0)) { + continue; + } + } + + // "position" makes sense only for XML comments for now + if ((cIter->type == XML_COMMENT_NODE) + && (position >= 0) + && (pcmk__xml_position(cIter, pcmk__xf_skip) != position)) { + continue; + } + + return cIter; + } + return NULL; +} + +/*! + * \internal + * \brief Simplified, more efficient alternative to get_xpath_object() + * + * \param[in] top Root of XML to search + * \param[in] key Search xpath + * \param[in] target_position If deleting, where to delete + * + * \return XML child matching xpath if found, NULL otherwise + * + * \note This only works on simplified xpaths found in v2 patchset diffs, + * i.e. the only allowed search predicate is [@id='XXX']. + */ +static xmlNode * +search_v2_xpath(const xmlNode *top, const char *key, int target_position) +{ + xmlNode *target = (xmlNode *) top->doc; + const char *current = key; + char *section; + char *remainder; + char *id; + char *tag; + char *path = NULL; + int rc; + size_t key_len; + + CRM_CHECK(key != NULL, return NULL); + key_len = strlen(key); + + /* These are scanned from key after a slash, so they can't be bigger + * 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); + + 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 current_position = -1; + + /* The target position is for the final component tag, so only use + * it if there is nothing left to search after this component. + */ + if ((rc == 1) && (target_position >= 0)) { + current_position = target_position; + } + + switch (f) { + case 1: + target = first_matching_xml_child(target, tag, NULL, + current_position); + break; + case 2: + target = first_matching_xml_child(target, tag, id, + current_position); + break; + default: + // This should not be possible + target = NULL; + break; + } + current = remainder; + } + + // Continue if something remains to search, and we've matched so far + } while ((rc == 2) && target); + + if (target) { + crm_trace("Found %s for %s", + (path = (char *) xmlGetNodePath(target)), key); + free(path); + } else { + crm_debug("No match for %s", key); + } + + free(remainder); + free(section); + free(tag); + free(id); + return target; +} + +typedef struct xml_change_obj_s { + const xmlNode *change; + xmlNode *match; +} xml_change_obj_t; + +static gint +sort_change_obj_by_position(gconstpointer a, gconstpointer b) +{ + const xml_change_obj_t *change_obj_a = a; + const xml_change_obj_t *change_obj_b = 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); + + if (position_a < position_b) { + return -1; + + } else if (position_a > position_b) { + return 1; + } + + return 0; +} + +/*! + * \internal + * \brief Apply a version 2 patchset to an XML node + * + * \param[in,out] xml XML to apply patchset to + * \param[in] patchset Patchset to apply + * + * \return Standard Pacemaker return code + */ +static int +apply_v2_patchset(xmlNode *xml, const xmlNode *patchset) +{ + int rc = pcmk_rc_ok; + const xmlNode *change = NULL; + GList *change_objs = NULL; + GList *gIter = NULL; + + 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); + int position = -1; + + if (op == NULL) { + continue; + } + + 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); + } + 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)) { + crm_debug("No %s match for %s in %p", op, xpath, xml->doc); + continue; + + } else if (match == NULL) { + crm_err("No %s match for %s in %p", op, xpath, xml->doc); + 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); + + 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 ((match->parent != NULL) && (match->parent->last != NULL)) { + xmlAddNextSibling(match->parent->last, match); + } + } + + } else if (strcmp(op, "delete") == 0) { + free_xml(match); + + } else if (strcmp(op, "modify") == 0) { + xmlNode *attrs = NULL; + + attrs = pcmk__xml_first_child(first_named_child(change, + XML_DIFF_RESULT)); + if (attrs == NULL) { + rc = ENOMSG; + continue; + } + pcmk__xe_remove_matching_attrs(match, NULL, NULL); // Remove all + + for (xmlAttrPtr pIter = pcmk__xe_first_attr(attrs); pIter != NULL; + pIter = pIter->next) { + const char *name = (const char *) pIter->name; + const char *value = crm_element_value(attrs, name); + + crm_xml_add(match, name, value); + } + + } else { + crm_err("Unknown operation: %s", op); + rc = pcmk_rc_diff_failed; + } + } + + // Changes should be generated in the right order. Double checking. + change_objs = g_list_sort(change_objs, sort_change_obj_by_position); + + for (gIter = change_objs; gIter; gIter = gIter->next) { + xml_change_obj_t *change_obj = gIter->data; + xmlNode *match = change_obj->match; + const char *op = NULL; + const char *xpath = NULL; + + change = change_obj->change; + + op = crm_element_value(change, XML_DIFF_OP); + xpath = crm_element_value(change, XML_DIFF_PATH); + + crm_trace("Continue performing %s on %s with %p", op, xpath, match); + + if (strcmp(op, "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); + + while ((match_child != NULL) + && (position != pcmk__xml_position(match_child, pcmk__xf_skip))) { + match_child = match_child->next; + } + + child = xmlDocCopyNode(change->children, match->doc, 1); + if (match_child) { + crm_trace("Adding %s at position %d", child->name, position); + xmlAddPrevSibling(match_child, child); + + } else if (match->last) { + crm_trace("Adding %s at position %d (end)", + child->name, position); + xmlAddNextSibling(match->last, child); + + } else { + crm_trace("Adding %s at position %d (first)", + child->name, position); + CRM_LOG_ASSERT(position == 0); + xmlAddChild(match, child); + } + pcmk__mark_xml_created(child); + + } else if (strcmp(op, "move") == 0) { + int position = 0; + + crm_element_value_int(change, XML_DIFF_POSITION, &position); + if (position != pcmk__xml_position(match, pcmk__xf_skip)) { + xmlNode *match_child = NULL; + int p = position; + + if (p > pcmk__xml_position(match, pcmk__xf_skip)) { + p++; // Skip ourselves + } + + CRM_ASSERT(match->parent != NULL); + match_child = match->parent->children; + + while ((match_child != NULL) + && (p != pcmk__xml_position(match_child, pcmk__xf_skip))) { + match_child = match_child->next; + } + + crm_trace("Moving %s to position %d (was %d, prev %p, %s %p)", + match->name, position, + pcmk__xml_position(match, pcmk__xf_skip), + match->prev, (match_child? "next":"last"), + (match_child? match_child : match->parent->last)); + + if (match_child) { + xmlAddPrevSibling(match_child, match); + + } else { + CRM_ASSERT(match->parent->last != NULL); + xmlAddNextSibling(match->parent->last, match); + } + + } else { + crm_trace("%s is already in position %d", + match->name, position); + } + + 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), + pcmk__xml_position(match, pcmk__xf_skip), + position, match->prev); + rc = pcmk_rc_diff_failed; + } + } + } + + g_list_free_full(change_objs, free); + return rc; +} + +int +xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version) +{ + int format = 1; + int rc = pcmk_ok; + xmlNode *old = NULL; + const char *digest = crm_element_value(patchset, XML_ATTR_DIGEST); + + if (patchset == NULL) { + return rc; + } + + pcmk__if_tracing( + { + pcmk__output_t *logger_out = NULL; + + rc = pcmk_rc2legacy(pcmk__log_output_new(&logger_out)); + CRM_CHECK(rc == pcmk_ok, return rc); + + pcmk__output_set_log_level(logger_out, LOG_TRACE); + rc = logger_out->message(logger_out, "xml-patchset", patchset); + logger_out->finish(logger_out, pcmk_rc2exitc(rc), true, + NULL); + pcmk__output_free(logger_out); + rc = pcmk_ok; + }, + {} + ); + + crm_element_value_int(patchset, "format", &format); + if (check_version) { + rc = pcmk_rc2legacy(xml_patch_version_check(xml, patchset, format)); + if (rc != pcmk_ok) { + return rc; + } + } + + if (digest) { + // Make it available for logging if result doesn't have expected digest + old = copy_xml(xml); + } + + if (rc == pcmk_ok) { + switch (format) { + case 1: + rc = pcmk_rc2legacy(apply_v1_patchset(xml, patchset)); + break; + case 2: + rc = pcmk_rc2legacy(apply_v2_patchset(xml, patchset)); + break; + default: + crm_err("Unknown patch format: %d", format); + rc = -EINVAL; + } + } + + if ((rc == pcmk_ok) && (digest != NULL)) { + char *new_digest = NULL; + char *version = crm_element_value_copy(xml, XML_ATTR_CRM_VERSION); + + new_digest = calculate_xml_versioned_digest(xml, FALSE, TRUE, version); + if (!pcmk__str_eq(new_digest, digest, pcmk__str_casei)) { + crm_info("v%d digest mis-match: expected %s, calculated %s", + format, digest, new_digest); + rc = -pcmk_err_diff_failed; + pcmk__if_tracing( + { + save_xml_to_file(old, "PatchDigest:input", NULL); + save_xml_to_file(xml, "PatchDigest:result", NULL); + save_xml_to_file(patchset, "PatchDigest:diff", NULL); + }, + {} + ); + + } else { + crm_trace("v%d digest matched: expected %s, calculated %s", + format, digest, new_digest); + } + free(new_digest); + free(version); + } + free_xml(old); + 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) +{ + xmlNode *tmp1 = NULL; + xmlNode *diff = create_xml_node(NULL, "diff"); + xmlNode *removed = create_xml_node(diff, "diff-removed"); + xmlNode *added = create_xml_node(diff, "diff-added"); + + crm_xml_add(diff, XML_ATTR_CRM_VERSION, CRM_FEATURE_SET); + + tmp1 = subtract_xml_object(removed, old, new, FALSE, NULL, "removed:top"); + if (suppress && (tmp1 != NULL) && can_prune_leaf(tmp1)) { + free_xml(tmp1); + } + + tmp1 = subtract_xml_object(added, new, old, TRUE, NULL, "added:top"); + if (suppress && (tmp1 != NULL) && can_prune_leaf(tmp1)) { + free_xml(tmp1); + } + + if ((added->children == NULL) && (removed->children == NULL)) { + free_xml(diff); + diff = NULL; + } + + 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; + + return deleted; + } + + return NULL; +} + +xmlNode * +subtract_xml_object(xmlNode *parent, xmlNode *left, xmlNode *right, + gboolean 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_xml_comment(parent, left, right, changed); + } + + id = ID(left); + if (right == NULL) { + xmlNode *deleted = NULL; + + crm_trace("Processing <%s " XML_ATTR_ID "=%s> (complete copy)", + crm_element_name(left), id); + deleted = add_node_copy(parent, left); + crm_xml_add(deleted, XML_DIFF_MARKER, marker); + + *changed = TRUE; + return deleted; + } + + name = crm_element_name(left); + CRM_CHECK(name != NULL, return NULL); + CRM_CHECK(pcmk__str_eq(crm_element_name(left), crm_element_name(right), + pcmk__str_casei), + 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); + + 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, 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 = crm_element_value(left, prop_name); + + 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 = crm_element_value(left, prop_name); + + 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, crm_element_name(left), 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, + crm_element_name(left), 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, XML_ATTR_ID, id); + } + done: + return diff; +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include <crm/common/xml_compat.h> + +gboolean +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); + + xmlNode *child_diff = NULL; + xmlNode *added = find_xml_node(diff, "diff-added", FALSE); + xmlNode *removed = find_xml_node(diff, "diff-removed", FALSE); + + CRM_CHECK(new_xml != NULL, return FALSE); + + crm_trace("Subtraction Phase"); + for (child_diff = pcmk__xml_first_child(removed); child_diff != NULL; + 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); + } + root_nodes_seen++; + } + + if (root_nodes_seen == 0) { + *new_xml = copy_xml(old_xml); + + } else if (root_nodes_seen > 1) { + crm_err("(-) Diffs cannot contain more than one change set... saw %d", + root_nodes_seen); + result = FALSE; + } + + root_nodes_seen = 0; + crm_trace("Addition Phase"); + if (result) { + xmlNode *child_diff = NULL; + + for (child_diff = pcmk__xml_first_child(added); child_diff != NULL; + 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); + } + root_nodes_seen++; + } + } + + if (root_nodes_seen > 1) { + crm_err("(+) Diffs cannot contain more than one change set... saw %d", + root_nodes_seen); + result = FALSE; + + } else if (result && (digest != NULL)) { + char *new_digest = NULL; + + purge_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)) { + crm_info("Digest mis-match: expected %s, calculated %s", + digest, new_digest); + result = FALSE; + + pcmk__if_tracing( + { + save_xml_to_file(old_xml, "diff:original", NULL); + save_xml_to_file(diff, "diff:input", NULL); + save_xml_to_file(*new_xml, "diff:new", NULL); + }, + {} + ); + + } else { + crm_trace("Digest matched: expected %s, calculated %s", + digest, new_digest); + } + free(new_digest); + + } else if (result) { + purge_diff_markers(*new_xml); // Purge now so diff is ok + } + + return result; +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/patchset_display.c b/lib/common/patchset_display.c new file mode 100644 index 0000000..731d437 --- /dev/null +++ b/lib/common/patchset_display.c @@ -0,0 +1,519 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <crm/msg_xml.h> + +#include "crmcommon_private.h" + +/*! + * \internal + * \brief Output an XML patchset header + * + * This function parses a header from an XML patchset (an \p XML_ATTR_DIFF + * element and its children). + * + * 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 + * + * Lines containing \p "---" describe removals and end with the patch format + * number. Lines containing \p "+++" describe additions and end with the patch + * digest. + * + * \param[in,out] out Output object + * \param[in] patchset XML patchset to output + * + * \return Standard Pacemaker return code + * + * \note This function produces output only for text-like formats. + */ +static int +xml_show_patchset_header(pcmk__output_t *out, const xmlNode *patchset) +{ + int rc = pcmk_rc_no_output; + int add[] = { 0, 0, 0 }; + int del[] = { 0, 0, 0 }; + + xml_patch_versions(patchset, add, del); + + if ((add[0] != del[0]) || (add[1] != del[1]) || (add[2] != del[2])) { + const char *fmt = crm_element_value(patchset, "format"); + const char *digest = crm_element_value(patchset, XML_ATTR_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", + add[0], add[1], add[2], digest); + + } else if ((add[0] != 0) || (add[1] != 0) || (add[2] != 0)) { + rc = out->info(out, "Local-only Change: %d.%d.%d", + add[0], add[1], add[2]); + } + + return rc; +} + +/*! + * \internal + * \brief Output a user-friendly form of XML additions or removals + * + * \param[in,out] out Output object + * \param[in] prefix String to prepend to every line of output + * \param[in] data XML node to output + * \param[in] depth Current indentation level + * \param[in] options Group of \p pcmk__xml_fmt_options flags + * + * \return Standard Pacemaker return code + * + * \note This function produces output only for text-like formats. + */ +static int +xml_show_patchset_v1_recursive(pcmk__output_t *out, const char *prefix, + const xmlNode *data, int depth, uint32_t options) +{ + if (!xml_has_children(data) + || (crm_element_value(data, XML_DIFF_MARKER) != NULL)) { + + // Found a change; clear the pcmk__xml_fmt_diff_short option if set + options &= ~pcmk__xml_fmt_diff_short; + + if (pcmk_is_set(options, pcmk__xml_fmt_diff_plus)) { + prefix = PCMK__XML_PREFIX_CREATED; + } else { // pcmk_is_set(options, pcmk__xml_fmt_diff_minus) + prefix = PCMK__XML_PREFIX_DELETED; + } + } + + if (pcmk_is_set(options, pcmk__xml_fmt_diff_short)) { + int rc = pcmk_rc_no_output; + + // Keep looking for the actual change + for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL; + child = pcmk__xml_next(child)) { + int temp_rc = xml_show_patchset_v1_recursive(out, prefix, child, + depth + 1, options); + rc = pcmk__output_select_rc(rc, temp_rc); + } + return rc; + } + + return pcmk__xml_show(out, prefix, data, depth, + options + |pcmk__xml_fmt_open + |pcmk__xml_fmt_children + |pcmk__xml_fmt_close); +} + +/*! + * \internal + * \brief Output a user-friendly form of an XML patchset (format 1) + * + * This function parses an XML patchset (an \p XML_ATTR_DIFF element and its + * children) into a user-friendly combined diff output. + * + * \param[in,out] out Output object + * \param[in] patchset XML patchset to output + * \param[in] options Group of \p pcmk__xml_fmt_options flags + * + * \return Standard Pacemaker return code + * + * \note This function produces output only for text-like formats. + */ +static int +xml_show_patchset_v1(pcmk__output_t *out, const xmlNode *patchset, + uint32_t options) +{ + const xmlNode *removed = NULL; + const xmlNode *added = NULL; + const xmlNode *child = NULL; + bool is_first = true; + int rc = xml_show_patchset_header(out, patchset); + + /* It's not clear whether "- " or "+ " ever does *not* get overridden by + * PCMK__XML_PREFIX_DELETED or PCMK__XML_PREFIX_CREATED in practice. + * However, v1 patchsets can only exist during rolling upgrades from + * Pacemaker 1.1.11, so not worth worrying about. + */ + removed = find_xml_node(patchset, "diff-removed", FALSE); + 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, + options + |pcmk__xml_fmt_diff_minus); + rc = pcmk__output_select_rc(rc, temp_rc); + + if (is_first) { + is_first = false; + } else { + rc = pcmk__output_select_rc(rc, out->info(out, " --- ")); + } + } + + is_first = true; + added = find_xml_node(patchset, "diff-added", FALSE); + 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, + options + |pcmk__xml_fmt_diff_plus); + rc = pcmk__output_select_rc(rc, temp_rc); + + if (is_first) { + is_first = false; + } else { + rc = pcmk__output_select_rc(rc, out->info(out, " +++ ")); + } + } + + return rc; +} + +/*! + * \internal + * \brief Output a user-friendly form of an XML patchset (format 2) + * + * This function parses an XML patchset (an \p XML_ATTR_DIFF element and its + * children) into a user-friendly combined diff output. + * + * \param[in,out] out Output object + * \param[in] patchset XML patchset to output + * + * \return Standard Pacemaker return code + * + * \note This function produces output only for text-like formats. + */ +static int +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); + + if (op == NULL) { + continue; + } + + if (strcmp(op, "create") == 0) { + char *prefix = crm_strdup_printf(PCMK__XML_PREFIX_CREATED " %s: ", + xpath); + + temp_rc = pcmk__xml_show(out, prefix, change->children, 0, + pcmk__xml_fmt_pretty|pcmk__xml_fmt_open); + rc = pcmk__output_select_rc(rc, temp_rc); + + // Overwrite all except the first two characters with spaces + for (char *ch = prefix + 2; *ch != '\0'; ch++) { + *ch = ' '; + } + + temp_rc = pcmk__xml_show(out, prefix, change->children, 0, + pcmk__xml_fmt_pretty + |pcmk__xml_fmt_children + |pcmk__xml_fmt_close); + 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); + + 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); + 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"); + + op = crm_element_value(child, XML_DIFF_OP); + if (op == NULL) { + continue; + } + + if (strcmp(op, "set") == 0) { + const char *value = crm_element_value(child, "value"); + + pcmk__add_separated_word(&buffer_set, 256, "@", ", "); + pcmk__g_strcat(buffer_set, name, "=", value, NULL); + + } else if (strcmp(op, "unset") == 0) { + pcmk__add_separated_word(&buffer_unset, 256, "@", ", "); + g_string_append(buffer_unset, name); + } + } + + if (buffer_set != NULL) { + temp_rc = out->info(out, "+ %s: %s", xpath, buffer_set->str); + rc = pcmk__output_select_rc(rc, temp_rc); + g_string_free(buffer_set, TRUE); + } + + if (buffer_unset != NULL) { + temp_rc = out->info(out, "-- %s: %s", + xpath, buffer_unset->str); + rc = pcmk__output_select_rc(rc, temp_rc); + g_string_free(buffer_unset, TRUE); + } + + } else if (strcmp(op, "delete") == 0) { + int position = -1; + + crm_element_value_int(change, XML_DIFF_POSITION, &position); + if (position >= 0) { + temp_rc = out->info(out, "-- %s (%d)", xpath, position); + } else { + temp_rc = out->info(out, "-- %s", xpath); + } + rc = pcmk__output_select_rc(rc, temp_rc); + } + } + + return rc; +} + +/*! + * \internal + * \brief Output a user-friendly form of an XML patchset + * + * This function parses an XML patchset (an \p XML_ATTR_DIFF element and its + * children) into a user-friendly combined diff output. + * + * \param[in,out] out Output object + * \param[in] args Message-specific arguments + * + * \return Standard Pacemaker return code + * + * \note \p args should contain only the XML patchset + */ +PCMK__OUTPUT_ARGS("xml-patchset", "xmlNodePtr") +static int +xml_patchset_default(pcmk__output_t *out, va_list args) +{ + xmlNodePtr patchset = va_arg(args, xmlNodePtr); + + int format = 1; + + if (patchset == NULL) { + crm_trace("Empty patch"); + return pcmk_rc_no_output; + } + + crm_element_value_int(patchset, "format", &format); + switch (format) { + case 1: + return xml_show_patchset_v1(out, patchset, pcmk__xml_fmt_pretty); + case 2: + return xml_show_patchset_v2(out, patchset); + default: + crm_err("Unknown patch format: %d", format); + return pcmk_rc_bad_xml_patch; + } +} + +/*! + * \internal + * \brief Output a user-friendly form of an XML patchset + * + * This function parses an XML patchset (an \p XML_ATTR_DIFF element and its + * children) into a user-friendly combined diff output. + * + * \param[in,out] out Output object + * \param[in] args Message-specific arguments + * + * \return Standard Pacemaker return code + * + * \note \p args should contain only the XML patchset + */ +PCMK__OUTPUT_ARGS("xml-patchset", "xmlNodePtr") +static int +xml_patchset_log(pcmk__output_t *out, va_list args) +{ + static struct qb_log_callsite *patchset_cs = NULL; + + xmlNodePtr patchset = va_arg(args, xmlNodePtr); + + uint8_t log_level = pcmk__output_get_log_level(out); + int format = 1; + + if (log_level == LOG_NEVER) { + return pcmk_rc_no_output; + } + + if (patchset == NULL) { + crm_trace("Empty patch"); + return pcmk_rc_no_output; + } + + if (patchset_cs == NULL) { + patchset_cs = qb_log_callsite_get(__func__, __FILE__, "xml-patchset", + log_level, __LINE__, + crm_trace_nonlog); + } + + if (!crm_is_callsite_active(patchset_cs, log_level, crm_trace_nonlog)) { + // Nothing would be logged, so skip all the work + return pcmk_rc_no_output; + } + + crm_element_value_int(patchset, "format", &format); + switch (format) { + case 1: + if (log_level < LOG_DEBUG) { + return xml_show_patchset_v1(out, patchset, + pcmk__xml_fmt_pretty + |pcmk__xml_fmt_diff_short); + } + return xml_show_patchset_v1(out, patchset, pcmk__xml_fmt_pretty); + case 2: + return xml_show_patchset_v2(out, patchset); + default: + crm_err("Unknown patch format: %d", format); + return pcmk_rc_bad_xml_patch; + } +} + +/*! + * \internal + * \brief Output an XML patchset + * + * This function outputs an XML patchset (an \p XML_ATTR_DIFF element and its + * children) without modification, as a CDATA block. + * + * \param[in,out] out Output object + * \param[in] args Message-specific arguments + * + * \return Standard Pacemaker return code + * + * \note \p args should contain only the XML patchset + */ +PCMK__OUTPUT_ARGS("xml-patchset", "xmlNodePtr") +static int +xml_patchset_xml(pcmk__output_t *out, va_list args) +{ + xmlNodePtr patchset = va_arg(args, xmlNodePtr); + + if (patchset != NULL) { + char *buf = dump_xml_formatted_with_text(patchset); + + out->output_xml(out, "xml-patchset", buf); + free(buf); + return pcmk_rc_ok; + } + crm_trace("Empty patch"); + return pcmk_rc_no_output; +} + +static pcmk__message_entry_t fmt_functions[] = { + { "xml-patchset", "default", xml_patchset_default }, + { "xml-patchset", "log", xml_patchset_log }, + { "xml-patchset", "xml", xml_patchset_xml }, + + { NULL, NULL, NULL } +}; + +/*! + * \internal + * \brief Register the formatting functions for XML patchsets + * + * \param[in,out] out Output object + */ +void +pcmk__register_patchset_messages(pcmk__output_t *out) { + pcmk__register_messages(out, fmt_functions); +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include <crm/common/xml_compat.h> + +void +xml_log_patchset(uint8_t log_level, const char *function, + const xmlNode *patchset) +{ + /* This function has some duplication relative to the message functions. + * This way, we can maintain the const xmlNode * in the signature. The + * message functions must be non-const. They have to support XML output + * objects, which must make a copy of a the patchset, requiring a non-const + * function call. + * + * In contrast, this legacy function doesn't need to support XML output. + */ + static struct qb_log_callsite *patchset_cs = NULL; + + pcmk__output_t *out = NULL; + int format = 1; + int rc = pcmk_rc_no_output; + + switch (log_level) { + case LOG_NEVER: + return; + case LOG_STDOUT: + CRM_CHECK(pcmk__text_output_new(&out, NULL) == pcmk_rc_ok, return); + break; + default: + if (patchset_cs == NULL) { + patchset_cs = qb_log_callsite_get(__func__, __FILE__, + "xml-patchset", log_level, + __LINE__, crm_trace_nonlog); + } + if (!crm_is_callsite_active(patchset_cs, log_level, + crm_trace_nonlog)) { + return; + } + CRM_CHECK(pcmk__log_output_new(&out) == pcmk_rc_ok, return); + pcmk__output_set_log_level(out, log_level); + break; + } + + if (patchset == NULL) { + // Should come after the LOG_NEVER check + crm_trace("Empty patch"); + goto done; + } + + crm_element_value_int(patchset, "format", &format); + switch (format) { + case 1: + if (log_level < LOG_DEBUG) { + rc = xml_show_patchset_v1(out, patchset, + pcmk__xml_fmt_pretty + |pcmk__xml_fmt_diff_short); + } else { // Note: LOG_STDOUT > LOG_DEBUG + rc = xml_show_patchset_v1(out, patchset, pcmk__xml_fmt_pretty); + } + break; + case 2: + rc = xml_show_patchset_v2(out, patchset); + break; + default: + crm_err("Unknown patch format: %d", format); + rc = pcmk_rc_bad_xml_patch; + break; + } + +done: + out->finish(out, pcmk_rc2exitc(rc), true, NULL); + pcmk__output_free(out); +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/pid.c b/lib/common/pid.c new file mode 100644 index 0000000..bb53153 --- /dev/null +++ b/lib/common/pid.c @@ -0,0 +1,247 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include <stdio.h> +#include <string.h> +#include <sys/stat.h> + +#include <crm/crm.h> + +int +pcmk__pid_active(pid_t pid, const char *daemon) +{ + static pid_t last_asked_pid = 0; /* log spam prevention */ + int rc = 0; + + if (pid <= 0) { + return EINVAL; + } + + rc = kill(pid, 0); + if ((rc < 0) && (errno == ESRCH)) { + return ESRCH; /* no such PID detected */ + + } else if ((daemon == NULL) || !pcmk__procfs_has_pids()) { + // The kill result is all we have, we can't check the name + + if (rc == 0) { + return pcmk_rc_ok; + } + rc = errno; + if (last_asked_pid != pid) { + crm_info("Cannot examine PID %lld: %s", + (long long) pid, pcmk_rc_str(rc)); + last_asked_pid = pid; + } + return rc; /* errno != ESRCH */ + + } else { + /* make sure PID hasn't been reused by another process + XXX: might still be just a zombie, which could confuse decisions */ + bool checked_through_kill = (rc == 0); + char exe_path[PATH_MAX], myexe_path[PATH_MAX]; + + rc = pcmk__procfs_pid2path(pid, exe_path, sizeof(exe_path)); + if (rc != pcmk_rc_ok) { + if (rc != EACCES) { + // Check again to filter out races + if ((kill(pid, 0) < 0) && (errno == ESRCH)) { + return ESRCH; + } + } + if (last_asked_pid != pid) { + if (rc == EACCES) { + crm_info("Could not get executable for PID %lld: %s " + CRM_XS " rc=%d", + (long long) pid, pcmk_rc_str(rc), rc); + } else { + crm_err("Could not get executable for PID %lld: %s " + CRM_XS " rc=%d", + (long long) pid, pcmk_rc_str(rc), rc); + } + last_asked_pid = pid; + } + if (rc == EACCES) { + // Trust kill if it was OK (we can't double-check via path) + return checked_through_kill? pcmk_rc_ok : EACCES; + } else { + return ESRCH; /* most likely errno == ENOENT */ + } + } + + if (daemon[0] != '/') { + rc = snprintf(myexe_path, sizeof(myexe_path), CRM_DAEMON_DIR"/%s", + daemon); + } else { + rc = snprintf(myexe_path, sizeof(myexe_path), "%s", daemon); + } + + if (rc > 0 && rc < sizeof(myexe_path) && !strcmp(exe_path, myexe_path)) { + return pcmk_rc_ok; + } + } + + return ESRCH; +} + +#define LOCKSTRLEN 11 + +/*! + * \internal + * \brief Read a process ID from a file + * + * \param[in] filename Process ID file to read + * \param[out] pid Where to put PID that was read + * + * \return Standard Pacemaker return code + */ +int +pcmk__read_pidfile(const char *filename, pid_t *pid) +{ + int fd; + struct stat sbuf; + int rc = pcmk_rc_ok; + long long pid_read = 0; + char buf[LOCKSTRLEN + 1]; + + CRM_CHECK((filename != NULL) && (pid != NULL), return EINVAL); + + fd = open(filename, O_RDONLY); + if (fd < 0) { + return errno; + } + + if ((fstat(fd, &sbuf) >= 0) && (sbuf.st_size < LOCKSTRLEN)) { + sleep(2); /* if someone was about to create one, + * give'm a sec to do so + */ + } + + if (read(fd, buf, sizeof(buf)) < 1) { + rc = errno; + goto bail; + } + + errno = 0; + rc = sscanf(buf, "%lld", &pid_read); + + if (rc > 0) { + if (pid_read <= 0) { + rc = ESRCH; + } else { + rc = pcmk_rc_ok; + *pid = (pid_t) pid_read; + crm_trace("Read pid %lld from %s", pid_read, filename); + } + } else if (rc == 0) { + rc = ENODATA; + } else { + rc = errno; + } + + bail: + close(fd); + return rc; +} + +/*! + * \internal + * \brief Check whether a process from a PID file matches expected values + * + * \param[in] filename Path of PID file + * \param[in] expected_pid If positive, compare to this PID + * \param[in] expected_name If not NULL, the PID from the PID file is valid + * only if it is active as a process with this name + * \param[out] pid If not NULL, store PID found in PID file here + * + * \return Standard Pacemaker return code + */ +int +pcmk__pidfile_matches(const char *filename, pid_t expected_pid, + const char *expected_name, pid_t *pid) +{ + pid_t pidfile_pid = 0; + int rc = pcmk__read_pidfile(filename, &pidfile_pid); + + if (pid) { + *pid = pidfile_pid; + } + + if (rc != pcmk_rc_ok) { + // Error reading PID file or invalid contents + unlink(filename); + rc = ENOENT; + + } else if ((expected_pid > 0) && (pidfile_pid == expected_pid)) { + // PID in file matches what was expected + rc = pcmk_rc_ok; + + } else if (pcmk__pid_active(pidfile_pid, expected_name) == ESRCH) { + // Contains a stale value + unlink(filename); + rc = ENOENT; + + } else if ((expected_pid > 0) && (pidfile_pid != expected_pid)) { + // Locked by existing process + rc = EEXIST; + } + + return rc; +} + +/*! + * \internal + * \brief Create a PID file for the current process (if not already existent) + * + * \param[in] filename Name of PID file to create + * \param[in] name Name of current process + * + * \return Standard Pacemaker return code + */ +int +pcmk__lock_pidfile(const char *filename, const char *name) +{ + pid_t mypid = getpid(); + int fd = 0; + int rc = 0; + char buf[LOCKSTRLEN + 2]; + + rc = pcmk__pidfile_matches(filename, 0, name, NULL); + if ((rc != pcmk_rc_ok) && (rc != ENOENT)) { + // Locked by existing process + return rc; + } + + fd = open(filename, O_CREAT | O_WRONLY | O_EXCL, 0644); + if (fd < 0) { + return errno; + } + + snprintf(buf, sizeof(buf), "%*lld\n", LOCKSTRLEN - 1, (long long) mypid); + rc = write(fd, buf, LOCKSTRLEN); + close(fd); + + if (rc != LOCKSTRLEN) { + crm_perror(LOG_ERR, "Incomplete write to %s", filename); + return errno; + } + + rc = pcmk__pidfile_matches(filename, mypid, name, NULL); + if (rc != pcmk_rc_ok) { + // Something is really wrong -- maybe I/O error on read back? + unlink(filename); + } + return rc; +} diff --git a/lib/common/procfs.c b/lib/common/procfs.c new file mode 100644 index 0000000..8179cc9 --- /dev/null +++ b/lib/common/procfs.c @@ -0,0 +1,227 @@ +/* + * Copyright 2015-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <dirent.h> +#include <ctype.h> + +/*! + * \internal + * \brief Get process ID and name associated with a /proc directory entry + * + * \param[in] entry Directory entry (must be result of readdir() on /proc) + * \param[out] name If not NULL, a char[16] to hold the process name + * \param[out] pid If not NULL, will be set to process ID of entry + * + * \return Standard Pacemaker return code + * \note This should be called only on Linux systems, as not all systems that + * support /proc store process names and IDs in the same way. The kernel + * limits the process name to the first 15 characters (plus terminator). + * It would be nice if there were a public kernel API constant for that + * limit, but there isn't. + */ +static int +pcmk__procfs_process_info(const struct dirent *entry, char *name, pid_t *pid) +{ + int fd, local_pid; + FILE *file; + struct stat statbuf; + char procpath[128] = { 0 }; + + /* We're only interested in entries whose name is a PID, + * so skip anything non-numeric or that is too long. + * + * 114 = 128 - strlen("/proc/") - strlen("/status") - 1 + */ + local_pid = atoi(entry->d_name); + if ((local_pid <= 0) || (strlen(entry->d_name) > 114)) { + return -1; + } + if (pid) { + *pid = (pid_t) local_pid; + } + + /* Get this entry's file information */ + strcpy(procpath, "/proc/"); + strcat(procpath, entry->d_name); + fd = open(procpath, O_RDONLY); + if (fd < 0 ) { + return -1; + } + if (fstat(fd, &statbuf) < 0) { + close(fd); + return -1; + } + close(fd); + + /* We're only interested in subdirectories */ + if (!S_ISDIR(statbuf.st_mode)) { + return -1; + } + + /* Read the first entry ("Name:") from the process's status file. + * We could handle the valgrind case if we parsed the cmdline file + * instead, but that's more of a pain than it's worth. + */ + if (name != NULL) { + strcat(procpath, "/status"); + file = fopen(procpath, "r"); + if (!file) { + return -1; + } + if (fscanf(file, "Name:\t%15[^\n]", name) != 1) { + fclose(file); + return -1; + } + name[15] = 0; + fclose(file); + } + + return 0; +} + +/*! + * \internal + * \brief Return process ID of a named process + * + * \param[in] name Process name (as used in /proc/.../status) + * + * \return Process ID of named process if running, 0 otherwise + * + * \note This will return 0 if the process is being run via valgrind. + * This should be called only on Linux systems. + */ +pid_t +pcmk__procfs_pid_of(const char *name) +{ + DIR *dp; + struct dirent *entry; + pid_t pid = 0; + char entry_name[64] = { 0 }; + + dp = opendir("/proc"); + if (dp == NULL) { + crm_notice("Can not read /proc directory to track existing components"); + return 0; + } + + while ((entry = readdir(dp)) != NULL) { + if ((pcmk__procfs_process_info(entry, entry_name, &pid) == pcmk_rc_ok) + && pcmk__str_eq(entry_name, name, pcmk__str_casei) + && (pcmk__pid_active(pid, NULL) == pcmk_rc_ok)) { + + crm_info("Found %s active as process %lld", name, (long long) pid); + break; + } + pid = 0; + } + closedir(dp); + return pid; +} + +/*! + * \internal + * \brief Calculate number of logical CPU cores from procfs + * + * \return Number of cores (or 1 if unable to determine) + */ +unsigned int +pcmk__procfs_num_cores(void) +{ + int cores = 0; + FILE *stream = NULL; + + /* Parse /proc/stat instead of /proc/cpuinfo because it's smaller */ + stream = fopen("/proc/stat", "r"); + if (stream == NULL) { + crm_perror(LOG_INFO, "Could not open /proc/stat"); + } else { + char buffer[2048]; + + while (fgets(buffer, sizeof(buffer), stream)) { + if (pcmk__starts_with(buffer, "cpu") && isdigit(buffer[3])) { + ++cores; + } + } + fclose(stream); + } + return cores? cores : 1; +} + +/*! + * \internal + * \brief Get the executable path corresponding to a process ID + * + * \param[in] pid Process ID to check + * \param[out] path Where to store executable path + * \param[in] path_size Size of \p path in characters (ideally PATH_MAX) + * + * \return Standard Pacemaker error code (as possible errno values from + * readlink()) + */ +int +pcmk__procfs_pid2path(pid_t pid, char path[], size_t path_size) +{ +#if HAVE_LINUX_PROCFS + char procfs_exe_path[PATH_MAX]; + ssize_t link_rc; + + if (snprintf(procfs_exe_path, PATH_MAX, "/proc/%lld/exe", + (long long) pid) >= PATH_MAX) { + return ENAMETOOLONG; // Truncated (shouldn't be possible in practice) + } + + link_rc = readlink(procfs_exe_path, path, path_size - 1); + if (link_rc < 0) { + return errno; + } else if (link_rc >= (path_size - 1)) { + return ENAMETOOLONG; + } + + path[link_rc] = '\0'; + return pcmk_rc_ok; +#else + return EOPNOTSUPP; +#endif // HAVE_LINUX_PROCFS +} + +/*! + * \internal + * \brief Check whether process ID information is available from procfs + * + * \return true if process ID information is available, otherwise false + */ +bool +pcmk__procfs_has_pids(void) +{ +#if HAVE_LINUX_PROCFS + static bool have_pids = false; + static bool checked = false; + + if (!checked) { + char path[PATH_MAX]; + + have_pids = pcmk__procfs_pid2path(getpid(), path, sizeof(path)) == pcmk_rc_ok; + checked = true; + } + return have_pids; +#else + return false; +#endif // HAVE_LINUX_PROCFS +} diff --git a/lib/common/remote.c b/lib/common/remote.c new file mode 100644 index 0000000..8c5969a --- /dev/null +++ b/lib/common/remote.c @@ -0,0 +1,1270 @@ +/* + * Copyright 2008-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> +#include <crm/crm.h> + +#include <sys/param.h> +#include <stdio.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <sys/socket.h> +#include <arpa/inet.h> +#include <netinet/in.h> +#include <netinet/ip.h> +#include <netinet/tcp.h> +#include <netdb.h> +#include <stdlib.h> +#include <errno.h> +#include <inttypes.h> // PRIx32 + +#include <glib.h> +#include <bzlib.h> + +#include <crm/common/ipc_internal.h> +#include <crm/common/xml.h> +#include <crm/common/mainloop.h> +#include <crm/common/remote_internal.h> + +#ifdef HAVE_GNUTLS_GNUTLS_H +# include <gnutls/gnutls.h> +#endif + +/* Swab macros from linux/swab.h */ +#ifdef HAVE_LINUX_SWAB_H +# include <linux/swab.h> +#else +/* + * casts are necessary for constants, because we never know how for sure + * how U/UL/ULL map to __u16, __u32, __u64. At least not in a portable way. + */ +#define __swab16(x) ((uint16_t)( \ + (((uint16_t)(x) & (uint16_t)0x00ffU) << 8) | \ + (((uint16_t)(x) & (uint16_t)0xff00U) >> 8))) + +#define __swab32(x) ((uint32_t)( \ + (((uint32_t)(x) & (uint32_t)0x000000ffUL) << 24) | \ + (((uint32_t)(x) & (uint32_t)0x0000ff00UL) << 8) | \ + (((uint32_t)(x) & (uint32_t)0x00ff0000UL) >> 8) | \ + (((uint32_t)(x) & (uint32_t)0xff000000UL) >> 24))) + +#define __swab64(x) ((uint64_t)( \ + (((uint64_t)(x) & (uint64_t)0x00000000000000ffULL) << 56) | \ + (((uint64_t)(x) & (uint64_t)0x000000000000ff00ULL) << 40) | \ + (((uint64_t)(x) & (uint64_t)0x0000000000ff0000ULL) << 24) | \ + (((uint64_t)(x) & (uint64_t)0x00000000ff000000ULL) << 8) | \ + (((uint64_t)(x) & (uint64_t)0x000000ff00000000ULL) >> 8) | \ + (((uint64_t)(x) & (uint64_t)0x0000ff0000000000ULL) >> 24) | \ + (((uint64_t)(x) & (uint64_t)0x00ff000000000000ULL) >> 40) | \ + (((uint64_t)(x) & (uint64_t)0xff00000000000000ULL) >> 56))) +#endif + +#define REMOTE_MSG_VERSION 1 +#define ENDIAN_LOCAL 0xBADADBBD + +struct remote_header_v0 { + uint32_t endian; /* Detect messages from hosts with different endian-ness */ + uint32_t version; + uint64_t id; + uint64_t flags; + uint32_t size_total; + uint32_t payload_offset; + uint32_t payload_compressed; + uint32_t payload_uncompressed; + + /* New fields get added here */ + +} __attribute__ ((packed)); + +/*! + * \internal + * \brief Retrieve remote message header, in local endianness + * + * Return a pointer to the header portion of a remote connection's message + * buffer, converting the header to local endianness if needed. + * + * \param[in,out] remote Remote connection with new message + * + * \return Pointer to message header, localized if necessary + */ +static struct remote_header_v0 * +localized_remote_header(pcmk__remote_t *remote) +{ + struct remote_header_v0 *header = (struct remote_header_v0 *)remote->buffer; + if(remote->buffer_offset < sizeof(struct remote_header_v0)) { + return NULL; + + } else if(header->endian != ENDIAN_LOCAL) { + uint32_t endian = __swab32(header->endian); + + CRM_LOG_ASSERT(endian == ENDIAN_LOCAL); + if(endian != ENDIAN_LOCAL) { + crm_err("Invalid message detected, endian mismatch: %" PRIx32 + " is neither %" PRIx32 " nor the swab'd %" PRIx32, + ENDIAN_LOCAL, header->endian, endian); + return NULL; + } + + header->id = __swab64(header->id); + header->flags = __swab64(header->flags); + header->endian = __swab32(header->endian); + + header->version = __swab32(header->version); + header->size_total = __swab32(header->size_total); + header->payload_offset = __swab32(header->payload_offset); + header->payload_compressed = __swab32(header->payload_compressed); + header->payload_uncompressed = __swab32(header->payload_uncompressed); + } + + return header; +} + +#ifdef HAVE_GNUTLS_GNUTLS_H + +int +pcmk__tls_client_handshake(pcmk__remote_t *remote, int timeout_ms) +{ + int rc = 0; + int pollrc = 0; + time_t time_limit = time(NULL) + timeout_ms / 1000; + + do { + rc = gnutls_handshake(*remote->tls_session); + if ((rc == GNUTLS_E_INTERRUPTED) || (rc == GNUTLS_E_AGAIN)) { + pollrc = pcmk__remote_ready(remote, 1000); + if ((pollrc != pcmk_rc_ok) && (pollrc != ETIME)) { + /* poll returned error, there is no hope */ + crm_trace("TLS handshake poll failed: %s (%d)", + pcmk_strerror(pollrc), pollrc); + return pcmk_legacy2rc(pollrc); + } + } else if (rc < 0) { + crm_trace("TLS handshake failed: %s (%d)", + gnutls_strerror(rc), rc); + return EPROTO; + } else { + return pcmk_rc_ok; + } + } while (time(NULL) < time_limit); + return ETIME; +} + +/*! + * \internal + * \brief Set minimum prime size required by TLS client + * + * \param[in] session TLS session to affect + */ +static void +set_minimum_dh_bits(const gnutls_session_t *session) +{ + int dh_min_bits; + + pcmk__scan_min_int(getenv("PCMK_dh_min_bits"), &dh_min_bits, 0); + + /* This function is deprecated since GnuTLS 3.1.7, in favor of letting + * the priority string imply the DH requirements, but this is the only + * way to give the user control over compatibility with older servers. + */ + if (dh_min_bits > 0) { + crm_info("Requiring server use a Diffie-Hellman prime of at least %d bits", + dh_min_bits); + gnutls_dh_set_prime_bits(*session, dh_min_bits); + } +} + +static unsigned int +get_bound_dh_bits(unsigned int dh_bits) +{ + int dh_min_bits; + int dh_max_bits; + + pcmk__scan_min_int(getenv("PCMK_dh_min_bits"), &dh_min_bits, 0); + pcmk__scan_min_int(getenv("PCMK_dh_max_bits"), &dh_max_bits, 0); + if ((dh_max_bits > 0) && (dh_max_bits < dh_min_bits)) { + crm_warn("Ignoring PCMK_dh_max_bits less than PCMK_dh_min_bits"); + dh_max_bits = 0; + } + if ((dh_min_bits > 0) && (dh_bits < dh_min_bits)) { + return dh_min_bits; + } + if ((dh_max_bits > 0) && (dh_bits > dh_max_bits)) { + return dh_max_bits; + } + return dh_bits; +} + +/*! + * \internal + * \brief Initialize a new TLS session + * + * \param[in] csock Connected socket for TLS session + * \param[in] conn_type GNUTLS_SERVER or GNUTLS_CLIENT + * \param[in] cred_type GNUTLS_CRD_ANON or GNUTLS_CRD_PSK + * \param[in] credentials TLS session credentials + * + * \return Pointer to newly created session object, or NULL on error + */ +gnutls_session_t * +pcmk__new_tls_session(int csock, unsigned int conn_type, + gnutls_credentials_type_t cred_type, void *credentials) +{ + int rc = GNUTLS_E_SUCCESS; + const char *prio_base = NULL; + char *prio = NULL; + gnutls_session_t *session = NULL; + + /* Determine list of acceptable ciphers, etc. Pacemaker always adds the + * values required for its functionality. + * + * For an example of anonymous authentication, see: + * http://www.manpagez.com/info/gnutls/gnutls-2.10.4/gnutls_81.php#Echo-Server-with-anonymous-authentication + */ + + prio_base = getenv("PCMK_tls_priorities"); + if (prio_base == NULL) { + prio_base = PCMK_GNUTLS_PRIORITIES; + } + prio = crm_strdup_printf("%s:%s", prio_base, + (cred_type == GNUTLS_CRD_ANON)? "+ANON-DH" : "+DHE-PSK:+PSK"); + + session = gnutls_malloc(sizeof(gnutls_session_t)); + if (session == NULL) { + rc = GNUTLS_E_MEMORY_ERROR; + goto error; + } + + rc = gnutls_init(session, conn_type); + if (rc != GNUTLS_E_SUCCESS) { + goto error; + } + + /* @TODO On the server side, it would be more efficient to cache the + * priority with gnutls_priority_init2() and set it with + * gnutls_priority_set() for all sessions. + */ + rc = gnutls_priority_set_direct(*session, prio, NULL); + if (rc != GNUTLS_E_SUCCESS) { + goto error; + } + if (conn_type == GNUTLS_CLIENT) { + set_minimum_dh_bits(session); + } + + gnutls_transport_set_ptr(*session, + (gnutls_transport_ptr_t) GINT_TO_POINTER(csock)); + + rc = gnutls_credentials_set(*session, cred_type, credentials); + if (rc != GNUTLS_E_SUCCESS) { + goto error; + } + free(prio); + return session; + +error: + crm_err("Could not initialize %s TLS %s session: %s " + CRM_XS " rc=%d priority='%s'", + (cred_type == GNUTLS_CRD_ANON)? "anonymous" : "PSK", + (conn_type == GNUTLS_SERVER)? "server" : "client", + gnutls_strerror(rc), rc, prio); + free(prio); + if (session != NULL) { + gnutls_free(session); + } + return NULL; +} + +/*! + * \internal + * \brief Initialize Diffie-Hellman parameters for a TLS server + * + * \param[out] dh_params Parameter object to initialize + * + * \return Standard Pacemaker return code + * \todo The current best practice is to allow the client and server to + * negotiate the Diffie-Hellman parameters via a TLS extension (RFC 7919). + * However, we have to support both older versions of GnuTLS (<3.6) that + * don't support the extension on our side, and older Pacemaker versions + * that don't support the extension on the other side. The next best + * practice would be to use a known good prime (see RFC 5114 section 2.2), + * possibly stored in a file distributed with Pacemaker. + */ +int +pcmk__init_tls_dh(gnutls_dh_params_t *dh_params) +{ + int rc = GNUTLS_E_SUCCESS; + unsigned int dh_bits = 0; + + rc = gnutls_dh_params_init(dh_params); + if (rc != GNUTLS_E_SUCCESS) { + goto error; + } + + dh_bits = gnutls_sec_param_to_pk_bits(GNUTLS_PK_DH, + GNUTLS_SEC_PARAM_NORMAL); + if (dh_bits == 0) { + rc = GNUTLS_E_DH_PRIME_UNACCEPTABLE; + goto error; + } + dh_bits = get_bound_dh_bits(dh_bits); + + crm_info("Generating Diffie-Hellman parameters with %u-bit prime for TLS", + dh_bits); + rc = gnutls_dh_params_generate2(*dh_params, dh_bits); + if (rc != GNUTLS_E_SUCCESS) { + goto error; + } + + return pcmk_rc_ok; + +error: + crm_err("Could not initialize Diffie-Hellman parameters for TLS: %s " + CRM_XS " rc=%d", gnutls_strerror(rc), rc); + return EPROTO; +} + +/*! + * \internal + * \brief Process handshake data from TLS client + * + * Read as much TLS handshake data as is available. + * + * \param[in] client Client connection + * + * \return Standard Pacemaker return code (of particular interest, EAGAIN + * if some data was successfully read but more data is needed) + */ +int +pcmk__read_handshake_data(const pcmk__client_t *client) +{ + int rc = 0; + + CRM_ASSERT(client && client->remote && client->remote->tls_session); + + do { + rc = gnutls_handshake(*client->remote->tls_session); + } while (rc == GNUTLS_E_INTERRUPTED); + + if (rc == GNUTLS_E_AGAIN) { + /* No more data is available at the moment. This function should be + * invoked again once the client sends more. + */ + return EAGAIN; + } else if (rc != GNUTLS_E_SUCCESS) { + crm_err("TLS handshake with remote client failed: %s " + CRM_XS " rc=%d", gnutls_strerror(rc), rc); + return EPROTO; + } + return pcmk_rc_ok; +} + +// \return Standard Pacemaker return code +static int +send_tls(gnutls_session_t *session, struct iovec *iov) +{ + const char *unsent = iov->iov_base; + size_t unsent_len = iov->iov_len; + ssize_t gnutls_rc; + + if (unsent == NULL) { + return EINVAL; + } + + crm_trace("Sending TLS message of %llu bytes", + (unsigned long long) unsent_len); + while (true) { + gnutls_rc = gnutls_record_send(*session, unsent, unsent_len); + + if (gnutls_rc == GNUTLS_E_INTERRUPTED || gnutls_rc == GNUTLS_E_AGAIN) { + crm_trace("Retrying to send %llu bytes remaining", + (unsigned long long) unsent_len); + + } else if (gnutls_rc < 0) { + // Caller can log as error if necessary + crm_info("TLS connection terminated: %s " CRM_XS " rc=%lld", + gnutls_strerror((int) gnutls_rc), + (long long) gnutls_rc); + return ECONNABORTED; + + } else if (gnutls_rc < unsent_len) { + crm_trace("Sent %lld of %llu bytes remaining", + (long long) gnutls_rc, (unsigned long long) unsent_len); + unsent_len -= gnutls_rc; + unsent += gnutls_rc; + } else { + crm_trace("Sent all %lld bytes remaining", (long long) gnutls_rc); + break; + } + } + return pcmk_rc_ok; +} +#endif + +// \return Standard Pacemaker return code +static int +send_plaintext(int sock, struct iovec *iov) +{ + const char *unsent = iov->iov_base; + size_t unsent_len = iov->iov_len; + ssize_t write_rc; + + if (unsent == NULL) { + return EINVAL; + } + + crm_debug("Sending plaintext message of %llu bytes to socket %d", + (unsigned long long) unsent_len, sock); + while (true) { + write_rc = write(sock, unsent, unsent_len); + if (write_rc < 0) { + int rc = errno; + + if ((errno == EINTR) || (errno == EAGAIN)) { + crm_trace("Retrying to send %llu bytes remaining to socket %d", + (unsigned long long) unsent_len, sock); + continue; + } + + // Caller can log as error if necessary + crm_info("Could not send message: %s " CRM_XS " rc=%d socket=%d", + pcmk_rc_str(rc), rc, sock); + return rc; + + } else if (write_rc < unsent_len) { + crm_trace("Sent %lld of %llu bytes remaining", + (long long) write_rc, (unsigned long long) unsent_len); + unsent += write_rc; + unsent_len -= write_rc; + continue; + + } else { + crm_trace("Sent all %lld bytes remaining: %.100s", + (long long) write_rc, (char *) (iov->iov_base)); + break; + } + } + return pcmk_rc_ok; +} + +// \return Standard Pacemaker return code +static int +remote_send_iovs(pcmk__remote_t *remote, struct iovec *iov, int iovs) +{ + int rc = pcmk_rc_ok; + + for (int lpc = 0; (lpc < iovs) && (rc == pcmk_rc_ok); lpc++) { +#ifdef HAVE_GNUTLS_GNUTLS_H + if (remote->tls_session) { + rc = send_tls(remote->tls_session, &(iov[lpc])); + continue; + } +#endif + if (remote->tcp_socket) { + rc = send_plaintext(remote->tcp_socket, &(iov[lpc])); + } else { + rc = ESOCKTNOSUPPORT; + } + } + return rc; +} + +/*! + * \internal + * \brief Send an XML message over a Pacemaker Remote connection + * + * \param[in,out] remote Pacemaker Remote connection to use + * \param[in] msg XML to send + * + * \return Standard Pacemaker return code + */ +int +pcmk__remote_send_xml(pcmk__remote_t *remote, xmlNode *msg) +{ + int rc = pcmk_rc_ok; + static uint64_t id = 0; + char *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); + + header = calloc(1, sizeof(struct remote_header_v0)); + CRM_ASSERT(header != NULL); + + 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); + + id++; + header->id = id; + header->endian = ENDIAN_LOCAL; + header->version = REMOTE_MSG_VERSION; + header->payload_offset = iov[0].iov_len; + header->payload_uncompressed = iov[1].iov_len; + header->size_total = iov[0].iov_len + iov[1].iov_len; + + rc = remote_send_iovs(remote, iov, 2); + if (rc != pcmk_rc_ok) { + crm_err("Could not send remote message: %s " CRM_XS " rc=%d", + pcmk_rc_str(rc), rc); + } + + free(iov[0].iov_base); + free(iov[1].iov_base); + return rc; +} + +/*! + * \internal + * \brief Obtain the XML from the currently buffered remote connection message + * + * \param[in,out] remote Remote connection possibly with message available + * + * \return Newly allocated XML object corresponding to message data, or NULL + * \note This effectively removes the message from the connection buffer. + */ +xmlNode * +pcmk__remote_message_xml(pcmk__remote_t *remote) +{ + xmlNode *xml = NULL; + struct remote_header_v0 *header = localized_remote_header(remote); + + if (header == NULL) { + return NULL; + } + + /* Support compression on the receiving end now, in case we ever want to add it later */ + if (header->payload_compressed) { + int rc = 0; + unsigned int size_u = 1 + header->payload_uncompressed; + char *uncompressed = calloc(1, header->payload_offset + size_u); + + crm_trace("Decompressing message data %d bytes into %d bytes", + header->payload_compressed, size_u); + + rc = BZ2_bzBuffToBuffDecompress(uncompressed + header->payload_offset, &size_u, + remote->buffer + header->payload_offset, + header->payload_compressed, 1, 0); + + if (rc != BZ_OK && header->version > REMOTE_MSG_VERSION) { + crm_warn("Couldn't decompress v%d message, we only understand v%d", + header->version, REMOTE_MSG_VERSION); + free(uncompressed); + return NULL; + + } else if (rc != BZ_OK) { + crm_err("Decompression failed: %s " CRM_XS " bzerror=%d", + bz2_strerror(rc), rc); + free(uncompressed); + return NULL; + } + + CRM_ASSERT(size_u == header->payload_uncompressed); + + memcpy(uncompressed, remote->buffer, header->payload_offset); /* Preserve the header */ + remote->buffer_size = header->payload_offset + size_u; + + free(remote->buffer); + remote->buffer = uncompressed; + header = localized_remote_header(remote); + } + + /* take ownership of the buffer */ + remote->buffer_offset = 0; + + CRM_LOG_ASSERT(remote->buffer[sizeof(struct remote_header_v0) + header->payload_uncompressed - 1] == 0); + + xml = string2xml(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); + + } else if (xml == NULL) { + crm_err("Couldn't parse: '%.120s'", remote->buffer + header->payload_offset); + } + + return xml; +} + +static int +get_remote_socket(const pcmk__remote_t *remote) +{ +#ifdef HAVE_GNUTLS_GNUTLS_H + if (remote->tls_session) { + void *sock_ptr = gnutls_transport_get_ptr(*remote->tls_session); + + return GPOINTER_TO_INT(sock_ptr); + } +#endif + + if (remote->tcp_socket) { + return remote->tcp_socket; + } + + crm_err("Remote connection type undetermined (bug?)"); + return -1; +} + +/*! + * \internal + * \brief Wait for a remote session to have data to read + * + * \param[in] remote Connection to check + * \param[in] timeout_ms Maximum time (in ms) to wait + * + * \return Standard Pacemaker return code (of particular interest, pcmk_rc_ok if + * there is data ready to be read, and ETIME if there is no data within + * the specified timeout) + */ +int +pcmk__remote_ready(const pcmk__remote_t *remote, int timeout_ms) +{ + struct pollfd fds = { 0, }; + int sock = 0; + int rc = 0; + time_t start; + int timeout = timeout_ms; + + sock = get_remote_socket(remote); + if (sock <= 0) { + crm_trace("No longer connected"); + return ENOTCONN; + } + + start = time(NULL); + errno = 0; + do { + fds.fd = sock; + fds.events = POLLIN; + + /* If we got an EINTR while polling, and we have a + * specific timeout we are trying to honor, attempt + * to adjust the timeout to the closest second. */ + if (errno == EINTR && (timeout > 0)) { + timeout = timeout_ms - ((time(NULL) - start) * 1000); + if (timeout < 1000) { + timeout = 1000; + } + } + + rc = poll(&fds, 1, timeout); + } while (rc < 0 && errno == EINTR); + + if (rc < 0) { + return errno; + } + return (rc == 0)? ETIME : pcmk_rc_ok; +} + +/*! + * \internal + * \brief Read bytes from non-blocking remote connection + * + * \param[in,out] remote Remote connection to read + * + * \return Standard Pacemaker return code (of particular interest, pcmk_rc_ok if + * a full message has been received, or EAGAIN for a partial message) + * \note Use only with non-blocking sockets after polling the socket. + * \note This function will return when the socket read buffer is empty or an + * error is encountered. + */ +static int +read_available_remote_data(pcmk__remote_t *remote) +{ + int rc = pcmk_rc_ok; + size_t read_len = sizeof(struct remote_header_v0); + struct remote_header_v0 *header = localized_remote_header(remote); + bool received = false; + ssize_t read_rc; + + if(header) { + /* Stop at the end of the current message */ + read_len = header->size_total; + } + + /* automatically grow the buffer when needed */ + if(remote->buffer_size < read_len) { + remote->buffer_size = 2 * read_len; + crm_trace("Expanding buffer to %llu bytes", + (unsigned long long) remote->buffer_size); + remote->buffer = pcmk__realloc(remote->buffer, remote->buffer_size + 1); + } + +#ifdef HAVE_GNUTLS_GNUTLS_H + if (!received && remote->tls_session) { + read_rc = gnutls_record_recv(*(remote->tls_session), + remote->buffer + remote->buffer_offset, + remote->buffer_size - remote->buffer_offset); + if (read_rc == GNUTLS_E_INTERRUPTED) { + rc = EINTR; + } else if (read_rc == GNUTLS_E_AGAIN) { + rc = EAGAIN; + } else if (read_rc < 0) { + crm_debug("TLS receive failed: %s (%lld)", + gnutls_strerror(read_rc), (long long) read_rc); + rc = EIO; + } + received = true; + } +#endif + + if (!received && remote->tcp_socket) { + read_rc = read(remote->tcp_socket, + remote->buffer + remote->buffer_offset, + remote->buffer_size - remote->buffer_offset); + if (read_rc < 0) { + rc = errno; + } + received = true; + } + + if (!received) { + crm_err("Remote connection type undetermined (bug?)"); + return ESOCKTNOSUPPORT; + } + + /* process any errors. */ + if (read_rc > 0) { + remote->buffer_offset += read_rc; + /* always null terminate buffer, the +1 to alloc always allows for this. */ + remote->buffer[remote->buffer_offset] = '\0'; + crm_trace("Received %lld more bytes (%llu total)", + (long long) read_rc, + (unsigned long long) remote->buffer_offset); + + } else if ((rc == EINTR) || (rc == EAGAIN)) { + crm_trace("No data available for non-blocking remote read: %s (%d)", + pcmk_rc_str(rc), rc); + + } else if (read_rc == 0) { + crm_debug("End of remote data encountered after %llu bytes", + (unsigned long long) remote->buffer_offset); + return ENOTCONN; + + } else { + crm_debug("Error receiving remote data after %llu bytes: %s (%d)", + (unsigned long long) remote->buffer_offset, + pcmk_rc_str(rc), rc); + return ENOTCONN; + } + + header = localized_remote_header(remote); + if(header) { + if(remote->buffer_offset < header->size_total) { + crm_trace("Read partial remote message (%llu of %u bytes)", + (unsigned long long) remote->buffer_offset, + header->size_total); + } else { + crm_trace("Read full remote message of %llu bytes", + (unsigned long long) remote->buffer_offset); + return pcmk_rc_ok; + } + } + + return EAGAIN; +} + +/*! + * \internal + * \brief Read one message from a remote connection + * + * \param[in,out] remote Remote connection to read + * \param[in] timeout_ms Fail if message not read in this many milliseconds + * (10s will be used if 0, and 60s if negative) + * + * \return Standard Pacemaker return code + */ +int +pcmk__read_remote_message(pcmk__remote_t *remote, int timeout_ms) +{ + int rc = pcmk_rc_ok; + time_t start = time(NULL); + int remaining_timeout = 0; + + if (timeout_ms == 0) { + timeout_ms = 10000; + } else if (timeout_ms < 0) { + timeout_ms = 60000; + } + + remaining_timeout = timeout_ms; + while (remaining_timeout > 0) { + + crm_trace("Waiting for remote data (%d ms of %d ms timeout remaining)", + remaining_timeout, timeout_ms); + rc = pcmk__remote_ready(remote, remaining_timeout); + + if (rc == ETIME) { + crm_err("Timed out (%d ms) while waiting for remote data", + remaining_timeout); + return rc; + + } else if (rc != pcmk_rc_ok) { + crm_debug("Wait for remote data aborted (will retry): %s " + CRM_XS " rc=%d", pcmk_rc_str(rc), rc); + + } else { + rc = read_available_remote_data(remote); + if (rc == pcmk_rc_ok) { + return rc; + } else if (rc == EAGAIN) { + crm_trace("Waiting for more remote data"); + } else { + crm_debug("Could not receive remote data: %s " CRM_XS " rc=%d", + pcmk_rc_str(rc), rc); + } + } + + // Don't waste time retrying after fatal errors + if ((rc == ENOTCONN) || (rc == ESOCKTNOSUPPORT)) { + return rc; + } + + remaining_timeout = timeout_ms - ((time(NULL) - start) * 1000); + } + return ETIME; +} + +struct tcp_async_cb_data { + int sock; + int timeout_ms; + time_t start; + void *userdata; + void (*callback) (void *userdata, int rc, int sock); +}; + +// \return TRUE if timer should be rescheduled, FALSE otherwise +static gboolean +check_connect_finished(gpointer userdata) +{ + struct tcp_async_cb_data *cb_data = userdata; + int rc; + + fd_set rset, wset; + struct timeval ts = { 0, }; + + if (cb_data->start == 0) { + // Last connect() returned success immediately + rc = pcmk_rc_ok; + goto dispatch_done; + } + + // If the socket is ready for reading or writing, the connect succeeded + FD_ZERO(&rset); + FD_SET(cb_data->sock, &rset); + wset = rset; + rc = select(cb_data->sock + 1, &rset, &wset, NULL, &ts); + + if (rc < 0) { // select() error + rc = errno; + if ((rc == EINPROGRESS) || (rc == EAGAIN)) { + if ((time(NULL) - cb_data->start) < (cb_data->timeout_ms / 1000)) { + return TRUE; // There is time left, so reschedule timer + } else { + rc = ETIMEDOUT; + } + } + crm_trace("Could not check socket %d for connection success: %s (%d)", + cb_data->sock, pcmk_rc_str(rc), rc); + + } else if (rc == 0) { // select() timeout + if ((time(NULL) - cb_data->start) < (cb_data->timeout_ms / 1000)) { + return TRUE; // There is time left, so reschedule timer + } + crm_debug("Timed out while waiting for socket %d connection success", + cb_data->sock); + rc = ETIMEDOUT; + + // select() returned number of file descriptors that are ready + + } else if (FD_ISSET(cb_data->sock, &rset) + || FD_ISSET(cb_data->sock, &wset)) { + + // The socket is ready; check it for connection errors + int error = 0; + socklen_t len = sizeof(error); + + if (getsockopt(cb_data->sock, SOL_SOCKET, SO_ERROR, &error, &len) < 0) { + rc = errno; + crm_trace("Couldn't check socket %d for connection errors: %s (%d)", + cb_data->sock, pcmk_rc_str(rc), rc); + } else if (error != 0) { + rc = error; + crm_trace("Socket %d connected with error: %s (%d)", + cb_data->sock, pcmk_rc_str(rc), rc); + } else { + rc = pcmk_rc_ok; + } + + } else { // Should not be possible + crm_trace("select() succeeded, but socket %d not in resulting " + "read/write sets", cb_data->sock); + rc = EAGAIN; + } + + dispatch_done: + if (rc == pcmk_rc_ok) { + crm_trace("Socket %d is connected", cb_data->sock); + } else { + close(cb_data->sock); + cb_data->sock = -1; + } + + if (cb_data->callback) { + cb_data->callback(cb_data->userdata, rc, cb_data->sock); + } + free(cb_data); + return FALSE; // Do not reschedule timer +} + +/*! + * \internal + * \brief Attempt to connect socket, calling callback when done + * + * Set a given socket non-blocking, then attempt to connect to it, + * retrying periodically until success or a timeout is reached. + * Call a caller-supplied callback function when completed. + * + * \param[in] sock Newly created socket + * \param[in] addr Socket address information for connect + * \param[in] addrlen Size of socket address information in bytes + * \param[in] timeout_ms Fail if not connected within this much time + * \param[out] timer_id If not NULL, store retry timer ID here + * \param[in] userdata User data to pass to callback + * \param[in] callback Function to call when connection attempt completes + * + * \return Standard Pacemaker return code + */ +static int +connect_socket_retry(int sock, const struct sockaddr *addr, socklen_t addrlen, + int timeout_ms, int *timer_id, void *userdata, + void (*callback) (void *userdata, int rc, int sock)) +{ + int rc = 0; + int interval = 500; + int timer; + struct tcp_async_cb_data *cb_data = NULL; + + rc = pcmk__set_nonblocking(sock); + if (rc != pcmk_rc_ok) { + crm_warn("Could not set socket non-blocking: %s " CRM_XS " rc=%d", + pcmk_rc_str(rc), rc); + return rc; + } + + rc = connect(sock, addr, addrlen); + if (rc < 0 && (errno != EINPROGRESS) && (errno != EAGAIN)) { + rc = errno; + crm_warn("Could not connect socket: %s " CRM_XS " rc=%d", + pcmk_rc_str(rc), rc); + return rc; + } + + cb_data = calloc(1, sizeof(struct tcp_async_cb_data)); + cb_data->userdata = userdata; + cb_data->callback = callback; + cb_data->sock = sock; + cb_data->timeout_ms = timeout_ms; + + if (rc == 0) { + /* The connect was successful immediately, we still return to mainloop + * and let this callback get called later. This avoids the user of this api + * to have to account for the fact the callback could be invoked within this + * function before returning. */ + cb_data->start = 0; + interval = 1; + } else { + cb_data->start = time(NULL); + } + + /* This timer function does a non-blocking poll on the socket to see if we + * can use it. Once we can, the connect has completed. This method allows us + * to connect without blocking the mainloop. + * + * @TODO Use a mainloop fd callback for this instead of polling. Something + * about the way mainloop is currently polling prevents this from + * working at the moment though. (See connect(2) regarding EINPROGRESS + * for possible new handling needed.) + */ + crm_trace("Scheduling check in %dms for whether connect to fd %d finished", + interval, sock); + timer = g_timeout_add(interval, check_connect_finished, cb_data); + if (timer_id) { + *timer_id = timer; + } + + // timer callback should be taking care of cb_data + // cppcheck-suppress memleak + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Attempt once to connect socket and set it non-blocking + * + * \param[in] sock Newly created socket + * \param[in] addr Socket address information for connect + * \param[in] addrlen Size of socket address information in bytes + * + * \return Standard Pacemaker return code + */ +static int +connect_socket_once(int sock, const struct sockaddr *addr, socklen_t addrlen) +{ + int rc = connect(sock, addr, addrlen); + + if (rc < 0) { + rc = errno; + crm_warn("Could not connect socket: %s " CRM_XS " rc=%d", + pcmk_rc_str(rc), rc); + return rc; + } + + rc = pcmk__set_nonblocking(sock); + if (rc != pcmk_rc_ok) { + crm_warn("Could not set socket non-blocking: %s " CRM_XS " rc=%d", + pcmk_rc_str(rc), rc); + return rc; + } + + return pcmk_ok; +} + +/*! + * \internal + * \brief Connect to server at specified TCP port + * + * \param[in] host Name of server to connect to + * \param[in] port Server port to connect to + * \param[in] timeout_ms If asynchronous, fail if not connected in this time + * \param[out] timer_id If asynchronous and this is non-NULL, retry timer ID + * will be put here (for ease of cancelling by caller) + * \param[out] sock_fd Where to store socket file descriptor + * \param[in] userdata If asynchronous, data to pass to callback + * \param[in] callback If NULL, attempt a single synchronous connection, + * otherwise retry asynchronously then call this + * + * \return Standard Pacemaker return code + */ +int +pcmk__connect_remote(const char *host, int port, int timeout, int *timer_id, + int *sock_fd, void *userdata, + void (*callback) (void *userdata, int rc, int sock)) +{ + char buffer[INET6_ADDRSTRLEN]; + struct addrinfo *res = NULL; + struct addrinfo *rp = NULL; + struct addrinfo hints; + const char *server = host; + int rc; + int sock = -1; + + CRM_CHECK((host != NULL) && (sock_fd != NULL), return EINVAL); + + // Get host's IP address(es) + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_family = AF_UNSPEC; /* Allow IPv4 or IPv6 */ + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_CANONNAME; + rc = getaddrinfo(server, NULL, &hints, &res); + if (rc != 0) { + crm_err("Unable to get IP address info for %s: %s", + server, gai_strerror(rc)); + rc = ENOTCONN; + goto async_cleanup; + } + if (!res || !res->ai_addr) { + crm_err("Unable to get IP address info for %s: no result", server); + rc = ENOTCONN; + goto async_cleanup; + } + + // getaddrinfo() returns a list of host's addresses, try them in order + for (rp = res; rp != NULL; rp = rp->ai_next) { + struct sockaddr *addr = rp->ai_addr; + + if (!addr) { + continue; + } + + if (rp->ai_canonname) { + server = res->ai_canonname; + } + crm_debug("Got canonical name %s for %s", server, host); + + sock = socket(rp->ai_family, SOCK_STREAM, IPPROTO_TCP); + if (sock == -1) { + rc = errno; + crm_warn("Could not create socket for remote connection to %s:%d: " + "%s " CRM_XS " rc=%d", server, port, pcmk_rc_str(rc), rc); + continue; + } + + /* Set port appropriately for address family */ + /* (void*) casts avoid false-positive compiler alignment warnings */ + if (addr->sa_family == AF_INET6) { + ((struct sockaddr_in6 *)(void*)addr)->sin6_port = htons(port); + } else { + ((struct sockaddr_in *)(void*)addr)->sin_port = htons(port); + } + + memset(buffer, 0, PCMK__NELEM(buffer)); + pcmk__sockaddr2str(addr, buffer); + crm_info("Attempting remote connection to %s:%d", buffer, port); + + if (callback) { + if (connect_socket_retry(sock, rp->ai_addr, rp->ai_addrlen, timeout, + timer_id, userdata, callback) == pcmk_rc_ok) { + goto async_cleanup; /* Success for now, we'll hear back later in the callback */ + } + + } else if (connect_socket_once(sock, rp->ai_addr, + rp->ai_addrlen) == pcmk_rc_ok) { + break; /* Success */ + } + + // Connect failed + close(sock); + sock = -1; + rc = ENOTCONN; + } + +async_cleanup: + + if (res) { + freeaddrinfo(res); + } + *sock_fd = sock; + return rc; +} + +/*! + * \internal + * \brief Convert an IP address (IPv4 or IPv6) to a string for logging + * + * \param[in] sa Socket address for IP + * \param[out] s Storage for at least INET6_ADDRSTRLEN bytes + * + * \note sa The socket address can be a pointer to struct sockaddr_in (IPv4), + * struct sockaddr_in6 (IPv6) or struct sockaddr_storage (either), + * as long as its sa_family member is set correctly. + */ +void +pcmk__sockaddr2str(const void *sa, char *s) +{ + switch (((const struct sockaddr *) sa)->sa_family) { + case AF_INET: + inet_ntop(AF_INET, &(((const struct sockaddr_in *) sa)->sin_addr), + s, INET6_ADDRSTRLEN); + break; + + case AF_INET6: + inet_ntop(AF_INET6, + &(((const struct sockaddr_in6 *) sa)->sin6_addr), + s, INET6_ADDRSTRLEN); + break; + + default: + strcpy(s, "<invalid>"); + } +} + +/*! + * \internal + * \brief Accept a client connection on a remote server socket + * + * \param[in] ssock Server socket file descriptor being listened on + * \param[out] csock Where to put new client socket's file descriptor + * + * \return Standard Pacemaker return code + */ +int +pcmk__accept_remote_connection(int ssock, int *csock) +{ + int rc; + struct sockaddr_storage addr; + socklen_t laddr = sizeof(addr); + char addr_str[INET6_ADDRSTRLEN]; + + /* accept the connection */ + memset(&addr, 0, sizeof(addr)); + *csock = accept(ssock, (struct sockaddr *)&addr, &laddr); + if (*csock == -1) { + rc = errno; + crm_err("Could not accept remote client connection: %s " + CRM_XS " rc=%d", pcmk_rc_str(rc), rc); + return rc; + } + pcmk__sockaddr2str(&addr, addr_str); + crm_info("Accepted new remote client connection from %s", addr_str); + + rc = pcmk__set_nonblocking(*csock); + if (rc != pcmk_rc_ok) { + crm_err("Could not set socket non-blocking: %s " CRM_XS " rc=%d", + pcmk_rc_str(rc), rc); + close(*csock); + *csock = -1; + return rc; + } + +#ifdef TCP_USER_TIMEOUT + if (pcmk__get_sbd_timeout() > 0) { + // Time to fail and retry before watchdog + unsigned int optval = (unsigned int) pcmk__get_sbd_timeout() / 2; + + rc = setsockopt(*csock, SOL_TCP, TCP_USER_TIMEOUT, + &optval, sizeof(optval)); + if (rc < 0) { + rc = errno; + crm_err("Could not set TCP timeout to %d ms on remote connection: " + "%s " CRM_XS " rc=%d", optval, pcmk_rc_str(rc), rc); + close(*csock); + *csock = -1; + return rc; + } + } +#endif + + return rc; +} + +/*! + * \brief Get the default remote connection TCP port on this host + * + * \return Remote connection TCP port number + */ +int +crm_default_remote_port(void) +{ + static int port = 0; + + if (port == 0) { + const char *env = getenv("PCMK_remote_port"); + + if (env) { + errno = 0; + port = strtol(env, NULL, 10); + if (errno || (port < 1) || (port > 65535)) { + crm_warn("Environment variable PCMK_remote_port has invalid value '%s', using %d instead", + env, DEFAULT_REMOTE_PORT); + port = DEFAULT_REMOTE_PORT; + } + } else { + port = DEFAULT_REMOTE_PORT; + } + } + return port; +} diff --git a/lib/common/results.c b/lib/common/results.c new file mode 100644 index 0000000..93d79eb --- /dev/null +++ b/lib/common/results.c @@ -0,0 +1,1049 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include <bzlib.h> +#include <errno.h> +#include <stdlib.h> +#include <string.h> +#include <qb/qbdefs.h> + +#include <crm/common/mainloop.h> +#include <crm/common/xml.h> + +G_DEFINE_QUARK(pcmk-rc-error-quark, pcmk__rc_error) +G_DEFINE_QUARK(pcmk-exitc-error-quark, pcmk__exitc_error) + +// General (all result code types) + +/*! + * \brief Get the name and description of a given result code + * + * A result code can be interpreted as a member of any one of several families. + * + * \param[in] code The result code to look up + * \param[in] type How \p code should be interpreted + * \param[out] name Where to store the result code's name + * \param[out] desc Where to store the result code's description + * + * \return Standard Pacemaker return code + */ +int +pcmk_result_get_strings(int code, enum pcmk_result_type type, const char **name, + const char **desc) +{ + const char *code_name = NULL; + const char *code_desc = NULL; + + switch (type) { + case pcmk_result_legacy: + code_name = pcmk_errorname(code); + code_desc = pcmk_strerror(code); + break; + case pcmk_result_rc: + code_name = pcmk_rc_name(code); + code_desc = pcmk_rc_str(code); + break; + case pcmk_result_exitcode: + code_name = crm_exit_name(code); + code_desc = crm_exit_str((crm_exit_t) code); + break; + default: + return pcmk_rc_undetermined; + } + + if (name != NULL) { + *name = code_name; + } + + if (desc != NULL) { + *desc = code_desc; + } + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Get the lower and upper bounds of a result code family + * + * \param[in] type Type of result code + * \param[out] lower Where to store the lower bound + * \param[out] upper Where to store the upper bound + * + * \return Standard Pacemaker return code + * + * \note There is no true upper bound on standard Pacemaker return codes or + * legacy return codes. All system \p errno values are valid members of + * these result code families, and there is no global upper limit nor a + * constant by which to refer to the highest \p errno value on a given + * system. + */ +int +pcmk__result_bounds(enum pcmk_result_type type, int *lower, int *upper) +{ + CRM_ASSERT((lower != NULL) && (upper != NULL)); + + switch (type) { + case pcmk_result_legacy: + *lower = pcmk_ok; + *upper = 256; // should be enough for almost any system error code + break; + case pcmk_result_rc: + *lower = pcmk_rc_error - pcmk__n_rc + 1; + *upper = 256; + break; + case pcmk_result_exitcode: + *lower = CRM_EX_OK; + *upper = CRM_EX_MAX; + break; + default: + *lower = 0; + *upper = -1; + return pcmk_rc_undetermined; + } + return pcmk_rc_ok; +} + +// @COMPAT Legacy function return codes + +//! \deprecated Use standard return codes and pcmk_rc_name() instead +const char * +pcmk_errorname(int rc) +{ + rc = abs(rc); + switch (rc) { + case pcmk_err_generic: return "pcmk_err_generic"; + case pcmk_err_no_quorum: return "pcmk_err_no_quorum"; + case pcmk_err_schema_validation: return "pcmk_err_schema_validation"; + case pcmk_err_transform_failed: return "pcmk_err_transform_failed"; + case pcmk_err_old_data: return "pcmk_err_old_data"; + case pcmk_err_diff_failed: return "pcmk_err_diff_failed"; + case pcmk_err_diff_resync: return "pcmk_err_diff_resync"; + case pcmk_err_cib_modified: return "pcmk_err_cib_modified"; + case pcmk_err_cib_backup: return "pcmk_err_cib_backup"; + case pcmk_err_cib_save: return "pcmk_err_cib_save"; + case pcmk_err_cib_corrupt: return "pcmk_err_cib_corrupt"; + case pcmk_err_multiple: return "pcmk_err_multiple"; + case pcmk_err_node_unknown: return "pcmk_err_node_unknown"; + case pcmk_err_already: return "pcmk_err_already"; + case pcmk_err_bad_nvpair: return "pcmk_err_bad_nvpair"; + case pcmk_err_unknown_format: return "pcmk_err_unknown_format"; + default: return pcmk_rc_name(rc); // system errno + } +} + +//! \deprecated Use standard return codes and pcmk_rc_str() instead +const char * +pcmk_strerror(int rc) +{ + return pcmk_rc_str(pcmk_legacy2rc(rc)); +} + +// Standard Pacemaker API return codes + +/* This array is used only for nonzero values of pcmk_rc_e. Its values must be + * kept in the exact reverse order of the enum value numbering (i.e. add new + * values to the end of the array). + */ +static const struct pcmk__rc_info { + const char *name; + const char *desc; + int legacy_rc; +} pcmk__rcs[] = { + { "pcmk_rc_error", + "Error", + -pcmk_err_generic, + }, + { "pcmk_rc_unknown_format", + "Unknown output format", + -pcmk_err_unknown_format, + }, + { "pcmk_rc_bad_nvpair", + "Bad name/value pair given", + -pcmk_err_bad_nvpair, + }, + { "pcmk_rc_already", + "Already in requested state", + -pcmk_err_already, + }, + { "pcmk_rc_node_unknown", + "Node not found", + -pcmk_err_node_unknown, + }, + { "pcmk_rc_multiple", + "Resource active on multiple nodes", + -pcmk_err_multiple, + }, + { "pcmk_rc_cib_corrupt", + "Could not parse on-disk configuration", + -pcmk_err_cib_corrupt, + }, + { "pcmk_rc_cib_save", + "Could not save new configuration to disk", + -pcmk_err_cib_save, + }, + { "pcmk_rc_cib_backup", + "Could not archive previous configuration", + -pcmk_err_cib_backup, + }, + { "pcmk_rc_cib_modified", + "On-disk configuration was manually modified", + -pcmk_err_cib_modified, + }, + { "pcmk_rc_diff_resync", + "Application of update diff failed, requesting full refresh", + -pcmk_err_diff_resync, + }, + { "pcmk_rc_diff_failed", + "Application of update diff failed", + -pcmk_err_diff_failed, + }, + { "pcmk_rc_old_data", + "Update was older than existing configuration", + -pcmk_err_old_data, + }, + { "pcmk_rc_transform_failed", + "Schema transform failed", + -pcmk_err_transform_failed, + }, + { "pcmk_rc_schema_unchanged", + "Schema is already the latest available", + -pcmk_err_schema_unchanged, + }, + { "pcmk_rc_schema_validation", + "Update does not conform to the configured schema", + -pcmk_err_schema_validation, + }, + { "pcmk_rc_no_quorum", + "Operation requires quorum", + -pcmk_err_no_quorum, + }, + { "pcmk_rc_ipc_unauthorized", + "IPC server is blocked by unauthorized process", + -pcmk_err_generic, + }, + { "pcmk_rc_ipc_unresponsive", + "IPC server is unresponsive", + -pcmk_err_generic, + }, + { "pcmk_rc_ipc_pid_only", + "IPC server process is active but not accepting connections", + -pcmk_err_generic, + }, + { "pcmk_rc_op_unsatisfied", + "Not applicable under current conditions", + -pcmk_err_generic, + }, + { "pcmk_rc_undetermined", + "Result undetermined", + -pcmk_err_generic, + }, + { "pcmk_rc_before_range", + "Result occurs before given range", + -pcmk_err_generic, + }, + { "pcmk_rc_within_range", + "Result occurs within given range", + -pcmk_err_generic, + }, + { "pcmk_rc_after_range", + "Result occurs after given range", + -pcmk_err_generic, + }, + { "pcmk_rc_no_output", + "Output message produced no output", + -pcmk_err_generic, + }, + { "pcmk_rc_no_input", + "Input file not available", + -pcmk_err_generic, + }, + { "pcmk_rc_underflow", + "Value too small to be stored in data type", + -pcmk_err_generic, + }, + { "pcmk_rc_dot_error", + "Error writing dot(1) file", + -pcmk_err_generic, + }, + { "pcmk_rc_graph_error", + "Error writing graph file", + -pcmk_err_generic, + }, + { "pcmk_rc_invalid_transition", + "Cluster simulation produced invalid transition", + -pcmk_err_generic, + }, + { "pcmk_rc_unpack_error", + "Unable to parse CIB XML", + -pcmk_err_generic, + }, + { "pcmk_rc_duplicate_id", + "Two or more XML elements have the same ID", + -pcmk_err_generic, + }, + { "pcmk_rc_disabled", + "Disabled", + -pcmk_err_generic, + }, + { "pcmk_rc_bad_input", + "Bad input value provided", + -pcmk_err_generic, + }, + { "pcmk_rc_bad_xml_patch", + "Bad XML patch format", + -pcmk_err_generic, + }, +}; + +/*! + * \internal + * \brief The number of <tt>enum pcmk_rc_e</tt> values, excluding \c pcmk_rc_ok + * + * This constant stores the number of negative standard Pacemaker return codes. + * These represent Pacemaker-custom error codes. The count does not include + * positive system error numbers, nor does it include \c pcmk_rc_ok (success). + */ +const size_t pcmk__n_rc = PCMK__NELEM(pcmk__rcs); + +/*! + * \brief Get a return code constant name as a string + * + * \param[in] rc Integer return code to convert + * + * \return String of constant name corresponding to rc + */ +const char * +pcmk_rc_name(int rc) +{ + if ((rc <= pcmk_rc_error) && ((pcmk_rc_error - rc) < pcmk__n_rc)) { + return pcmk__rcs[pcmk_rc_error - rc].name; + } + switch (rc) { + case pcmk_rc_ok: return "pcmk_rc_ok"; + case E2BIG: return "E2BIG"; + case EACCES: return "EACCES"; + case EADDRINUSE: return "EADDRINUSE"; + case EADDRNOTAVAIL: return "EADDRNOTAVAIL"; + case EAFNOSUPPORT: return "EAFNOSUPPORT"; + case EAGAIN: return "EAGAIN"; + case EALREADY: return "EALREADY"; + case EBADF: return "EBADF"; + case EBADMSG: return "EBADMSG"; + case EBUSY: return "EBUSY"; + case ECANCELED: return "ECANCELED"; + case ECHILD: return "ECHILD"; + case ECOMM: return "ECOMM"; + case ECONNABORTED: return "ECONNABORTED"; + case ECONNREFUSED: return "ECONNREFUSED"; + case ECONNRESET: return "ECONNRESET"; + /* case EDEADLK: return "EDEADLK"; */ + case EDESTADDRREQ: return "EDESTADDRREQ"; + case EDOM: return "EDOM"; + case EDQUOT: return "EDQUOT"; + case EEXIST: return "EEXIST"; + case EFAULT: return "EFAULT"; + case EFBIG: return "EFBIG"; + case EHOSTDOWN: return "EHOSTDOWN"; + case EHOSTUNREACH: return "EHOSTUNREACH"; + case EIDRM: return "EIDRM"; + case EILSEQ: return "EILSEQ"; + case EINPROGRESS: return "EINPROGRESS"; + case EINTR: return "EINTR"; + case EINVAL: return "EINVAL"; + case EIO: return "EIO"; + case EISCONN: return "EISCONN"; + case EISDIR: return "EISDIR"; + case ELIBACC: return "ELIBACC"; + case ELOOP: return "ELOOP"; + case EMFILE: return "EMFILE"; + case EMLINK: return "EMLINK"; + case EMSGSIZE: return "EMSGSIZE"; +#ifdef EMULTIHOP // Not available on OpenBSD + case EMULTIHOP: return "EMULTIHOP"; +#endif + case ENAMETOOLONG: return "ENAMETOOLONG"; + case ENETDOWN: return "ENETDOWN"; + case ENETRESET: return "ENETRESET"; + case ENETUNREACH: return "ENETUNREACH"; + case ENFILE: return "ENFILE"; + case ENOBUFS: return "ENOBUFS"; + case ENODATA: return "ENODATA"; + case ENODEV: return "ENODEV"; + case ENOENT: return "ENOENT"; + case ENOEXEC: return "ENOEXEC"; + case ENOKEY: return "ENOKEY"; + case ENOLCK: return "ENOLCK"; +#ifdef ENOLINK // Not available on OpenBSD + case ENOLINK: return "ENOLINK"; +#endif + case ENOMEM: return "ENOMEM"; + case ENOMSG: return "ENOMSG"; + case ENOPROTOOPT: return "ENOPROTOOPT"; + case ENOSPC: return "ENOSPC"; +#ifdef ENOSR + case ENOSR: return "ENOSR"; +#endif +#ifdef ENOSTR + case ENOSTR: return "ENOSTR"; +#endif + case ENOSYS: return "ENOSYS"; + case ENOTBLK: return "ENOTBLK"; + case ENOTCONN: return "ENOTCONN"; + case ENOTDIR: return "ENOTDIR"; + case ENOTEMPTY: return "ENOTEMPTY"; + case ENOTSOCK: return "ENOTSOCK"; +#if ENOTSUP != EOPNOTSUPP + case ENOTSUP: return "ENOTSUP"; +#endif + case ENOTTY: return "ENOTTY"; + case ENOTUNIQ: return "ENOTUNIQ"; + case ENXIO: return "ENXIO"; + case EOPNOTSUPP: return "EOPNOTSUPP"; + case EOVERFLOW: return "EOVERFLOW"; + case EPERM: return "EPERM"; + case EPFNOSUPPORT: return "EPFNOSUPPORT"; + case EPIPE: return "EPIPE"; + case EPROTO: return "EPROTO"; + case EPROTONOSUPPORT: return "EPROTONOSUPPORT"; + case EPROTOTYPE: return "EPROTOTYPE"; + case ERANGE: return "ERANGE"; + case EREMOTE: return "EREMOTE"; + case EREMOTEIO: return "EREMOTEIO"; + case EROFS: return "EROFS"; + case ESHUTDOWN: return "ESHUTDOWN"; + case ESPIPE: return "ESPIPE"; + case ESOCKTNOSUPPORT: return "ESOCKTNOSUPPORT"; + case ESRCH: return "ESRCH"; + case ESTALE: return "ESTALE"; + case ETIME: return "ETIME"; + case ETIMEDOUT: return "ETIMEDOUT"; + case ETXTBSY: return "ETXTBSY"; +#ifdef EUNATCH + case EUNATCH: return "EUNATCH"; +#endif + case EUSERS: return "EUSERS"; + /* case EWOULDBLOCK: return "EWOULDBLOCK"; */ + case EXDEV: return "EXDEV"; + +#ifdef EBADE // Not available on OS X + case EBADE: return "EBADE"; + case EBADFD: return "EBADFD"; + case EBADSLT: return "EBADSLT"; + case EDEADLOCK: return "EDEADLOCK"; + case EBADR: return "EBADR"; + case EBADRQC: return "EBADRQC"; + case ECHRNG: return "ECHRNG"; +#ifdef EISNAM // Not available on OS X, Illumos, Solaris + case EISNAM: return "EISNAM"; + case EKEYEXPIRED: return "EKEYEXPIRED"; + case EKEYREVOKED: return "EKEYREVOKED"; +#endif + case EKEYREJECTED: return "EKEYREJECTED"; + case EL2HLT: return "EL2HLT"; + case EL2NSYNC: return "EL2NSYNC"; + case EL3HLT: return "EL3HLT"; + case EL3RST: return "EL3RST"; + case ELIBBAD: return "ELIBBAD"; + case ELIBMAX: return "ELIBMAX"; + case ELIBSCN: return "ELIBSCN"; + case ELIBEXEC: return "ELIBEXEC"; +#ifdef ENOMEDIUM // Not available on OS X, Illumos, Solaris + case ENOMEDIUM: return "ENOMEDIUM"; + case EMEDIUMTYPE: return "EMEDIUMTYPE"; +#endif + case ENONET: return "ENONET"; + case ENOPKG: return "ENOPKG"; + case EREMCHG: return "EREMCHG"; + case ERESTART: return "ERESTART"; + case ESTRPIPE: return "ESTRPIPE"; +#ifdef EUCLEAN // Not available on OS X, Illumos, Solaris + case EUCLEAN: return "EUCLEAN"; +#endif + case EXFULL: return "EXFULL"; +#endif // EBADE + default: return "Unknown"; + } +} + +/*! + * \brief Get a user-friendly description of a return code + * + * \param[in] rc Integer return code to convert + * + * \return String description of rc + */ +const char * +pcmk_rc_str(int rc) +{ + if (rc == pcmk_rc_ok) { + return "OK"; + } + if ((rc <= pcmk_rc_error) && ((pcmk_rc_error - rc) < pcmk__n_rc)) { + return pcmk__rcs[pcmk_rc_error - rc].desc; + } + if (rc < 0) { + return "Error"; + } + + // Handle values that could be defined by system or by portability.h + switch (rc) { +#ifdef PCMK__ENOTUNIQ + case ENOTUNIQ: return "Name not unique on network"; +#endif +#ifdef PCMK__ECOMM + case ECOMM: return "Communication error on send"; +#endif +#ifdef PCMK__ELIBACC + case ELIBACC: return "Can not access a needed shared library"; +#endif +#ifdef PCMK__EREMOTEIO + case EREMOTEIO: return "Remote I/O error"; +#endif +#ifdef PCMK__ENOKEY + case ENOKEY: return "Required key not available"; +#endif +#ifdef PCMK__ENODATA + case ENODATA: return "No data available"; +#endif +#ifdef PCMK__ETIME + case ETIME: return "Timer expired"; +#endif +#ifdef PCMK__EKEYREJECTED + case EKEYREJECTED: return "Key was rejected by service"; +#endif + default: return strerror(rc); + } +} + +// This returns negative values for errors +//! \deprecated Use standard return codes instead +int +pcmk_rc2legacy(int rc) +{ + if (rc >= 0) { + return -rc; // OK or system errno + } + if ((rc <= pcmk_rc_error) && ((pcmk_rc_error - rc) < pcmk__n_rc)) { + return pcmk__rcs[pcmk_rc_error - rc].legacy_rc; + } + return -pcmk_err_generic; +} + +//! \deprecated Use standard return codes instead +int +pcmk_legacy2rc(int legacy_rc) +{ + legacy_rc = abs(legacy_rc); + switch (legacy_rc) { + case pcmk_err_no_quorum: return pcmk_rc_no_quorum; + case pcmk_err_schema_validation: return pcmk_rc_schema_validation; + case pcmk_err_schema_unchanged: return pcmk_rc_schema_unchanged; + case pcmk_err_transform_failed: return pcmk_rc_transform_failed; + case pcmk_err_old_data: return pcmk_rc_old_data; + case pcmk_err_diff_failed: return pcmk_rc_diff_failed; + case pcmk_err_diff_resync: return pcmk_rc_diff_resync; + case pcmk_err_cib_modified: return pcmk_rc_cib_modified; + case pcmk_err_cib_backup: return pcmk_rc_cib_backup; + case pcmk_err_cib_save: return pcmk_rc_cib_save; + case pcmk_err_cib_corrupt: return pcmk_rc_cib_corrupt; + case pcmk_err_multiple: return pcmk_rc_multiple; + case pcmk_err_node_unknown: return pcmk_rc_node_unknown; + case pcmk_err_already: return pcmk_rc_already; + case pcmk_err_bad_nvpair: return pcmk_rc_bad_nvpair; + case pcmk_err_unknown_format: return pcmk_rc_unknown_format; + case pcmk_err_generic: return pcmk_rc_error; + case pcmk_ok: return pcmk_rc_ok; + default: return legacy_rc; // system errno + } +} + +// Exit status codes + +const char * +crm_exit_name(crm_exit_t exit_code) +{ + switch (exit_code) { + case CRM_EX_OK: return "CRM_EX_OK"; + case CRM_EX_ERROR: return "CRM_EX_ERROR"; + case CRM_EX_INVALID_PARAM: return "CRM_EX_INVALID_PARAM"; + case CRM_EX_UNIMPLEMENT_FEATURE: return "CRM_EX_UNIMPLEMENT_FEATURE"; + case CRM_EX_INSUFFICIENT_PRIV: return "CRM_EX_INSUFFICIENT_PRIV"; + case CRM_EX_NOT_INSTALLED: return "CRM_EX_NOT_INSTALLED"; + case CRM_EX_NOT_CONFIGURED: return "CRM_EX_NOT_CONFIGURED"; + case CRM_EX_NOT_RUNNING: return "CRM_EX_NOT_RUNNING"; + case CRM_EX_PROMOTED: return "CRM_EX_PROMOTED"; + case CRM_EX_FAILED_PROMOTED: return "CRM_EX_FAILED_PROMOTED"; + case CRM_EX_USAGE: return "CRM_EX_USAGE"; + case CRM_EX_DATAERR: return "CRM_EX_DATAERR"; + case CRM_EX_NOINPUT: return "CRM_EX_NOINPUT"; + case CRM_EX_NOUSER: return "CRM_EX_NOUSER"; + case CRM_EX_NOHOST: return "CRM_EX_NOHOST"; + case CRM_EX_UNAVAILABLE: return "CRM_EX_UNAVAILABLE"; + case CRM_EX_SOFTWARE: return "CRM_EX_SOFTWARE"; + case CRM_EX_OSERR: return "CRM_EX_OSERR"; + case CRM_EX_OSFILE: return "CRM_EX_OSFILE"; + case CRM_EX_CANTCREAT: return "CRM_EX_CANTCREAT"; + case CRM_EX_IOERR: return "CRM_EX_IOERR"; + case CRM_EX_TEMPFAIL: return "CRM_EX_TEMPFAIL"; + case CRM_EX_PROTOCOL: return "CRM_EX_PROTOCOL"; + case CRM_EX_NOPERM: return "CRM_EX_NOPERM"; + case CRM_EX_CONFIG: return "CRM_EX_CONFIG"; + case CRM_EX_FATAL: return "CRM_EX_FATAL"; + case CRM_EX_PANIC: return "CRM_EX_PANIC"; + case CRM_EX_DISCONNECT: return "CRM_EX_DISCONNECT"; + case CRM_EX_DIGEST: return "CRM_EX_DIGEST"; + case CRM_EX_NOSUCH: return "CRM_EX_NOSUCH"; + case CRM_EX_QUORUM: return "CRM_EX_QUORUM"; + case CRM_EX_UNSAFE: return "CRM_EX_UNSAFE"; + case CRM_EX_EXISTS: return "CRM_EX_EXISTS"; + case CRM_EX_MULTIPLE: return "CRM_EX_MULTIPLE"; + case CRM_EX_EXPIRED: return "CRM_EX_EXPIRED"; + case CRM_EX_NOT_YET_IN_EFFECT: return "CRM_EX_NOT_YET_IN_EFFECT"; + case CRM_EX_INDETERMINATE: return "CRM_EX_INDETERMINATE"; + case CRM_EX_UNSATISFIED: return "CRM_EX_UNSATISFIED"; + case CRM_EX_OLD: return "CRM_EX_OLD"; + case CRM_EX_TIMEOUT: return "CRM_EX_TIMEOUT"; + case CRM_EX_DEGRADED: return "CRM_EX_DEGRADED"; + case CRM_EX_DEGRADED_PROMOTED: return "CRM_EX_DEGRADED_PROMOTED"; + case CRM_EX_NONE: return "CRM_EX_NONE"; + case CRM_EX_MAX: return "CRM_EX_UNKNOWN"; + } + return "CRM_EX_UNKNOWN"; +} + +const char * +crm_exit_str(crm_exit_t exit_code) +{ + switch (exit_code) { + case CRM_EX_OK: return "OK"; + case CRM_EX_ERROR: return "Error occurred"; + case CRM_EX_INVALID_PARAM: return "Invalid parameter"; + case CRM_EX_UNIMPLEMENT_FEATURE: return "Unimplemented"; + case CRM_EX_INSUFFICIENT_PRIV: return "Insufficient privileges"; + case CRM_EX_NOT_INSTALLED: return "Not installed"; + case CRM_EX_NOT_CONFIGURED: return "Not configured"; + case CRM_EX_NOT_RUNNING: return "Not running"; + case CRM_EX_PROMOTED: return "Promoted"; + case CRM_EX_FAILED_PROMOTED: return "Failed in promoted role"; + case CRM_EX_USAGE: return "Incorrect usage"; + case CRM_EX_DATAERR: return "Invalid data given"; + case CRM_EX_NOINPUT: return "Input file not available"; + case CRM_EX_NOUSER: return "User does not exist"; + case CRM_EX_NOHOST: return "Host does not exist"; + case CRM_EX_UNAVAILABLE: return "Necessary service unavailable"; + case CRM_EX_SOFTWARE: return "Internal software bug"; + case CRM_EX_OSERR: return "Operating system error occurred"; + case CRM_EX_OSFILE: return "System file not available"; + case CRM_EX_CANTCREAT: return "Cannot create output file"; + case CRM_EX_IOERR: return "I/O error occurred"; + case CRM_EX_TEMPFAIL: return "Temporary failure, try again"; + case CRM_EX_PROTOCOL: return "Protocol violated"; + case CRM_EX_NOPERM: return "Insufficient privileges"; + case CRM_EX_CONFIG: return "Invalid configuration"; + case CRM_EX_FATAL: return "Fatal error occurred, will not respawn"; + case CRM_EX_PANIC: return "System panic required"; + case CRM_EX_DISCONNECT: return "Not connected"; + case CRM_EX_DIGEST: return "Digest mismatch"; + case CRM_EX_NOSUCH: return "No such object"; + case CRM_EX_QUORUM: return "Quorum required"; + case CRM_EX_UNSAFE: return "Operation not safe"; + case CRM_EX_EXISTS: return "Requested item already exists"; + case CRM_EX_MULTIPLE: return "Multiple items match request"; + case CRM_EX_EXPIRED: return "Requested item has expired"; + case CRM_EX_NOT_YET_IN_EFFECT: return "Requested item is not yet in effect"; + case CRM_EX_INDETERMINATE: return "Could not determine status"; + case CRM_EX_UNSATISFIED: return "Not applicable under current conditions"; + case CRM_EX_OLD: return "Update was older than existing configuration"; + case CRM_EX_TIMEOUT: return "Timeout occurred"; + case CRM_EX_DEGRADED: return "Service is active but might fail soon"; + case CRM_EX_DEGRADED_PROMOTED: return "Service is promoted but might fail soon"; + case CRM_EX_NONE: return "No exit status available"; + case CRM_EX_MAX: return "Error occurred"; + } + if ((exit_code > 128) && (exit_code < CRM_EX_MAX)) { + return "Interrupted by signal"; + } + return "Unknown exit status"; +} + +/*! + * \brief Map a function return code to the most similar exit code + * + * \param[in] rc Function return code + * + * \return Most similar exit code + */ +crm_exit_t +pcmk_rc2exitc(int rc) +{ + switch (rc) { + case pcmk_rc_ok: + case pcmk_rc_no_output: // quiet mode, or nothing to output + return CRM_EX_OK; + + case pcmk_rc_no_quorum: + return CRM_EX_QUORUM; + + case pcmk_rc_old_data: + return CRM_EX_OLD; + + case pcmk_rc_schema_validation: + case pcmk_rc_transform_failed: + case pcmk_rc_unpack_error: + return CRM_EX_CONFIG; + + case pcmk_rc_bad_nvpair: + return CRM_EX_INVALID_PARAM; + + case EACCES: + return CRM_EX_INSUFFICIENT_PRIV; + + case EBADF: + case EINVAL: + case EFAULT: + case ENOSYS: + case EOVERFLOW: + case pcmk_rc_underflow: + return CRM_EX_SOFTWARE; + + case EBADMSG: + case EMSGSIZE: + case ENOMSG: + case ENOPROTOOPT: + case EPROTO: + case EPROTONOSUPPORT: + case EPROTOTYPE: + return CRM_EX_PROTOCOL; + + case ECOMM: + case ENOMEM: + return CRM_EX_OSERR; + + case ECONNABORTED: + case ECONNREFUSED: + case ECONNRESET: + case ENOTCONN: + return CRM_EX_DISCONNECT; + + case EEXIST: + case pcmk_rc_already: + return CRM_EX_EXISTS; + + case EIO: + case pcmk_rc_dot_error: + case pcmk_rc_graph_error: + return CRM_EX_IOERR; + + case ENOTSUP: +#if EOPNOTSUPP != ENOTSUP + case EOPNOTSUPP: +#endif + return CRM_EX_UNIMPLEMENT_FEATURE; + + case ENOTUNIQ: + case pcmk_rc_multiple: + return CRM_EX_MULTIPLE; + + case ENODEV: + case ENOENT: + case ENXIO: + case pcmk_rc_unknown_format: + return CRM_EX_NOSUCH; + + case pcmk_rc_node_unknown: + return CRM_EX_NOHOST; + + case ETIME: + case ETIMEDOUT: + return CRM_EX_TIMEOUT; + + case EAGAIN: + case EBUSY: + return CRM_EX_UNSATISFIED; + + case pcmk_rc_before_range: + return CRM_EX_NOT_YET_IN_EFFECT; + + case pcmk_rc_after_range: + return CRM_EX_EXPIRED; + + case pcmk_rc_undetermined: + return CRM_EX_INDETERMINATE; + + case pcmk_rc_op_unsatisfied: + return CRM_EX_UNSATISFIED; + + case pcmk_rc_within_range: + return CRM_EX_OK; + + case pcmk_rc_no_input: + return CRM_EX_NOINPUT; + + case pcmk_rc_duplicate_id: + return CRM_EX_MULTIPLE; + + case pcmk_rc_bad_input: + case pcmk_rc_bad_xml_patch: + return CRM_EX_DATAERR; + + default: + return CRM_EX_ERROR; + } +} + +/*! + * \brief Map a function return code to the most similar OCF exit code + * + * \param[in] rc Function return code + * + * \return Most similar OCF exit code + */ +enum ocf_exitcode +pcmk_rc2ocf(int rc) +{ + switch (rc) { + case pcmk_rc_ok: + return PCMK_OCF_OK; + + case pcmk_rc_bad_nvpair: + return PCMK_OCF_INVALID_PARAM; + + case EACCES: + return PCMK_OCF_INSUFFICIENT_PRIV; + + case ENOTSUP: +#if EOPNOTSUPP != ENOTSUP + case EOPNOTSUPP: +#endif + return PCMK_OCF_UNIMPLEMENT_FEATURE; + + default: + return PCMK_OCF_UNKNOWN_ERROR; + } +} + + +// Other functions + +const char * +bz2_strerror(int rc) +{ + // See ftp://sources.redhat.com/pub/bzip2/docs/manual_3.html#SEC17 + switch (rc) { + case BZ_OK: + case BZ_RUN_OK: + case BZ_FLUSH_OK: + case BZ_FINISH_OK: + case BZ_STREAM_END: + return "Ok"; + case BZ_CONFIG_ERROR: + return "libbz2 has been improperly compiled on your platform"; + case BZ_SEQUENCE_ERROR: + return "library functions called in the wrong order"; + case BZ_PARAM_ERROR: + return "parameter is out of range or otherwise incorrect"; + case BZ_MEM_ERROR: + return "memory allocation failed"; + case BZ_DATA_ERROR: + return "data integrity error is detected during decompression"; + case BZ_DATA_ERROR_MAGIC: + return "the compressed stream does not start with the correct magic bytes"; + case BZ_IO_ERROR: + return "error reading or writing in the compressed file"; + case BZ_UNEXPECTED_EOF: + return "compressed file finishes before the logical end of stream is detected"; + case BZ_OUTBUFF_FULL: + return "output data will not fit into the buffer provided"; + } + return "Data compression error"; +} + +crm_exit_t +crm_exit(crm_exit_t rc) +{ + /* A compiler could theoretically use any type for crm_exit_t, but an int + * should always hold it, so cast to int to keep static analysis happy. + */ + if ((((int) rc) < 0) || (((int) rc) > CRM_EX_MAX)) { + rc = CRM_EX_ERROR; + } + + mainloop_cleanup(); + crm_xml_cleanup(); + + free(pcmk__our_nodename); + + if (crm_system_name) { + crm_info("Exiting %s " CRM_XS " with status %d", crm_system_name, rc); + free(crm_system_name); + } else { + crm_trace("Exiting with status %d", rc); + } + pcmk__free_common_logger(); + qb_log_fini(); // Don't log anything after this point + + exit(rc); +} + +/* + * External action results + */ + +/*! + * \internal + * \brief Set the result of an action + * + * \param[out] result Where to set action result + * \param[in] exit_status OCF exit status to set + * \param[in] exec_status Execution status to set + * \param[in] exit_reason Human-friendly description of event to set + */ +void +pcmk__set_result(pcmk__action_result_t *result, int exit_status, + enum pcmk_exec_status exec_status, const char *exit_reason) +{ + if (result == NULL) { + return; + } + + result->exit_status = exit_status; + result->execution_status = exec_status; + + if (!pcmk__str_eq(result->exit_reason, exit_reason, pcmk__str_none)) { + free(result->exit_reason); + result->exit_reason = (exit_reason == NULL)? NULL : strdup(exit_reason); + } +} + + +/*! + * \internal + * \brief Set the result of an action, with a formatted exit reason + * + * \param[out] result Where to set action result + * \param[in] exit_status OCF exit status to set + * \param[in] exec_status Execution status to set + * \param[in] format printf-style format for a human-friendly + * description of reason for result + * \param[in] ... arguments for \p format + */ +G_GNUC_PRINTF(4, 5) +void +pcmk__format_result(pcmk__action_result_t *result, int exit_status, + enum pcmk_exec_status exec_status, + const char *format, ...) +{ + va_list ap; + int len = 0; + char *reason = NULL; + + if (result == NULL) { + return; + } + + result->exit_status = exit_status; + result->execution_status = exec_status; + + if (format != NULL) { + va_start(ap, format); + len = vasprintf(&reason, format, ap); + CRM_ASSERT(len > 0); + va_end(ap); + } + free(result->exit_reason); + result->exit_reason = reason; +} + +/*! + * \internal + * \brief Set the output of an action + * + * \param[out] result Action result to set output for + * \param[in] out Action output to set (must be dynamically + * allocated) + * \param[in] err Action error output to set (must be dynamically + * allocated) + * + * \note \p result will take ownership of \p out and \p err, so the caller + * should not free them. + */ +void +pcmk__set_result_output(pcmk__action_result_t *result, char *out, char *err) +{ + if (result == NULL) { + return; + } + + free(result->action_stdout); + result->action_stdout = out; + + free(result->action_stderr); + result->action_stderr = err; +} + +/*! + * \internal + * \brief Clear a result's exit reason, output, and error output + * + * \param[in,out] result Result to reset + */ +void +pcmk__reset_result(pcmk__action_result_t *result) +{ + if (result == NULL) { + return; + } + + free(result->exit_reason); + result->exit_reason = NULL; + + free(result->action_stdout); + result->action_stdout = NULL; + + free(result->action_stderr); + result->action_stderr = NULL; +} + +/*! + * \internal + * \brief Copy the result of an action + * + * \param[in] src Result to copy + * \param[out] dst Where to copy \p src to + */ +void +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); +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include <crm/common/results_compat.h> + +crm_exit_t +crm_errno2exit(int rc) +{ + return pcmk_rc2exitc(pcmk_legacy2rc(rc)); +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/schemas.c b/lib/common/schemas.c new file mode 100644 index 0000000..88a3051 --- /dev/null +++ b/lib/common/schemas.c @@ -0,0 +1,1303 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <stdio.h> +#include <string.h> +#include <dirent.h> +#include <errno.h> +#include <sys/stat.h> +#include <stdarg.h> + +#include <libxml/relaxng.h> +#include <libxslt/xslt.h> +#include <libxslt/transform.h> +#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; + +#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]) + +typedef struct { + xmlRelaxNGPtr rng; + xmlRelaxNGValidCtxtPtr valid; + 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 bool silent_logging = FALSE; + +static void +xml_log(int priority, const char *fmt, ...) +G_GNUC_PRINTF(2, 3); + +static void +xml_log(int priority, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + if (silent_logging == FALSE) { + /* XXX should not this enable dechunking as well? */ + PCMK__XML_LOG_BASE(priority, FALSE, 0, NULL, fmt, ap); + } + va_end(ap); +} + +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" +} + +static int +xml_minimum_schema_index(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; +} + +const char * +xml_latest_schema(void) +{ + return get_schema_name(xml_latest_schema_index()); +} + +static inline bool +version_from_filename(const char *filename, schema_version_t *version) +{ + int rc = schema_scanf(filename, "pacemaker-", *version, ".rng"); + + return (rc == 2); +} + +static int +schema_filter(const struct dirent *a) +{ + int rc = 0; + schema_version_t version = SCHEMA_ZERO; + + if (strstr(a->d_name, "pacemaker-") != a->d_name) { + /* crm_trace("%s - wrong prefix", a->d_name); */ + + } else if (!pcmk__ends_with_ext(a->d_name, ".rng")) { + /* crm_trace("%s - wrong suffix", a->d_name); */ + + } else if (!version_from_filename(a->d_name, &version)) { + /* crm_trace("%s - wrong format", a->d_name); */ + + } else { + /* crm_debug("%s - candidate", a->d_name); */ + rc = 1; + } + + return rc; +} + +static int +schema_sort(const struct dirent **a, const struct dirent **b) +{ + 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; + } else if (a_version.v[i] > b_version.v[i]) { + return 1; + } + } + return 0; +} + +/*! + * \internal + * \brief Add given schema + auxiliary data to internal bookkeeping. + * + * \note When providing \p version, should not be called directly but + * through \c add_schema_by_version. + */ +static void +add_schema(enum schema_validator_e validator, const schema_version_t *version, + const char *name, const char *transform, + const char *transform_enter, bool transform_onleave, + int after_transform) +{ + int last = xml_schema_max; + bool have_version = FALSE; + + 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; + + 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, ""); + } else { + CRM_ASSERT(name); + schema_scanf(name, "%*[^-]-", known_schemas[last].version, ""); + known_schemas[last].name = strdup(name); + } + + if (transform) { + known_schemas[last].transform = strdup(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 */ + } + 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); + } +} + +/*! + * \internal + * \brief Add version-specified schema + auxiliary data to internal bookkeeping. + * \return Standard Pacemaker return value (the only possible values are + * \c ENOENT when no upgrade schema is associated, or \c pcmk_rc_ok otherwise. + * + * \note There's no reliance on the particular order of schemas entering here. + * + * \par A bit of theory + * We track 3 XSLT stylesheets that differ per usage: + * - "upgrade": + * . sparsely spread over the sequence of all available schemas, + * as they are only relevant when major version of the schema + * is getting bumped -- in that case, it MUST be set + * . name convention: upgrade-X.Y.xsl + * - "upgrade-enter": + * . may only accompany "upgrade" occurrence, but doesn't need to + * be present anytime such one is, i.e., it MAY not be set when + * "upgrade" is + * . name convention: upgrade-X.Y-enter.xsl, + * when not present: upgrade-enter.xsl + * - "upgrade-leave": + * . like "upgrade-enter", but SHOULD be present whenever + * "upgrade-enter" is (and vice versa, but that's only + * to prevent confusion based on observing the files, + * it would get ignored regardless) + * . name convention: (see "upgrade-enter") + */ +static int +add_schema_by_version(const schema_version_t *version, int next, + bool transform_expected) +{ + bool transform_onleave = FALSE; + int rc = pcmk_rc_ok; + struct stat s; + char *xslt = NULL, + *transform_upgrade = NULL, + *transform_enter = NULL; + + /* prologue for further transform_expected handling */ + if (transform_expected) { + /* check if there's suitable "upgrade" stylesheet */ + transform_upgrade = schema_strdup_printf("upgrade-", *version, ); + xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, + transform_upgrade); + } + + if (!transform_expected) { + /* jump directly to the end */ + + } else if (stat(xslt, &s) == 0) { + /* perhaps there's also a targeted "upgrade-enter" stylesheet */ + transform_enter = schema_strdup_printf("upgrade-", *version, "-enter"); + free(xslt); + xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, + transform_enter); + if (stat(xslt, &s) != 0) { + /* or initially, at least a generic one */ + crm_debug("Upgrade-enter transform %s.xsl not found", xslt); + free(xslt); + free(transform_enter); + transform_enter = strdup("upgrade-enter"); + xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, + transform_enter); + if (stat(xslt, &s) != 0) { + crm_debug("Upgrade-enter transform %s.xsl not found, either", xslt); + free(xslt); + xslt = NULL; + } + } + /* xslt contains full path to "upgrade-enter" stylesheet */ + if (xslt != NULL) { + /* then there should be "upgrade-leave" counterpart (enter->leave) */ + memcpy(strrchr(xslt, '-') + 1, "leave", sizeof("leave") - 1); + transform_onleave = (stat(xslt, &s) == 0); + free(xslt); + } else { + free(transform_enter); + transform_enter = NULL; + } + + } else { + crm_err("Upgrade transform %s not found", xslt); + 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); + + free(transform_upgrade); + free(transform_enter); + + return rc; +} + +static void +wrap_libxslt(bool finalize) +{ + static xsltSecurityPrefsPtr secprefs; + int ret = 0; + + /* security framework preferences */ + if (!finalize) { + CRM_ASSERT(secprefs == NULL); + secprefs = xsltNewSecurityPrefs(); + ret = xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_WRITE_FILE, + xsltSecurityForbid) + | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_CREATE_DIRECTORY, + xsltSecurityForbid) + | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_READ_NETWORK, + xsltSecurityForbid) + | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_WRITE_NETWORK, + xsltSecurityForbid); + if (ret != 0) { + return; + } + } else { + xsltFreeSecurityPrefs(secprefs); + secprefs = NULL; + } + + /* cleanup only */ + if (finalize) { + xsltCleanupGlobals(); + } +} + +/*! + * \internal + * \brief Load pacemaker schemas into cache + * + * \note This currently also serves as an entry point for the + * generic initialization of the libxslt library. + */ +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; + + wrap_libxslt(false); + + max = scandir(base, &namelist, schema_filter, schema_sort); + if (max < 0) { + crm_notice("scandir(%s) failed: %s (%d)", base, strerror(errno), errno); + free(base); + + } else { + 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; + } + + } else { + next = -1; + } + if (add_schema_by_version(&version, next, transform_expected) + == ENOENT) { + break; + } + } + + 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); +} + +#if 0 +static void +relaxng_invalid_stderr(void *userData, xmlErrorPtr error) +{ + /* + Structure xmlError + struct _xmlError { + int domain : What part of the library raised this er + int code : The error code, e.g. an xmlParserError + char * message : human-readable informative error messag + xmlErrorLevel level : how consequent is the error + char * file : the filename + int line : the line number if available + char * str1 : extra string information + char * str2 : extra string information + char * str3 : extra string information + int int1 : extra number information + int int2 : column number of the error or 0 if N/A + void * ctxt : the parser context if available + void * node : the node in the tree + } + */ + crm_err("Structured error: line=%d, level=%d %s", error->line, error->level, error->message); +} +#endif + +static gboolean +validate_with_relaxng(xmlDocPtr doc, gboolean to_logs, const char *relaxng_file, + relaxng_ctx_cache_t **cached_ctx) +{ + int rc = 0; + gboolean valid = TRUE; + relaxng_ctx_cache_t *ctx = NULL; + + 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)); + + xmlLoadExtDtdDefaultValue = 1; + ctx->parser = xmlRelaxNGNewParserCtxt(relaxng_file); + CRM_CHECK(ctx->parser != NULL, goto cleanup); + + if (to_logs) { + xmlRelaxNGSetParserErrors(ctx->parser, + (xmlRelaxNGValidityErrorFunc) xml_log, + (xmlRelaxNGValidityWarningFunc) xml_log, + GUINT_TO_POINTER(LOG_ERR)); + } else { + xmlRelaxNGSetParserErrors(ctx->parser, + (xmlRelaxNGValidityErrorFunc) fprintf, + (xmlRelaxNGValidityWarningFunc) fprintf, + stderr); + } + + ctx->rng = xmlRelaxNGParse(ctx->parser); + CRM_CHECK(ctx->rng != NULL, + crm_err("Could not find/parse %s", relaxng_file); + goto cleanup); + + ctx->valid = xmlRelaxNGNewValidCtxt(ctx->rng); + CRM_CHECK(ctx->valid != NULL, goto cleanup); + + if (to_logs) { + xmlRelaxNGSetValidErrors(ctx->valid, + (xmlRelaxNGValidityErrorFunc) xml_log, + (xmlRelaxNGValidityWarningFunc) xml_log, + GUINT_TO_POINTER(LOG_ERR)); + } else { + xmlRelaxNGSetValidErrors(ctx->valid, + (xmlRelaxNGValidityErrorFunc) fprintf, + (xmlRelaxNGValidityWarningFunc) fprintf, + stderr); + } + } + + /* xmlRelaxNGSetValidStructuredErrors( */ + /* valid, relaxng_invalid_stderr, valid); */ + + xmlLineNumbersDefault(1); + rc = xmlRelaxNGValidateDoc(ctx->valid, doc); + if (rc > 0) { + valid = FALSE; + + } else if (rc < 0) { + crm_err("Internal libxml error during validation"); + } + + cleanup: + + if (cached_ctx) { + *cached_ctx = ctx; + + } else { + if (ctx->parser != NULL) { + xmlRelaxNGFreeParserCtxt(ctx->parser); + } + if (ctx->valid != NULL) { + xmlRelaxNGFreeValidCtxt(ctx->valid); + } + if (ctx->rng != NULL) { + xmlRelaxNGFree(ctx->rng); + } + free(ctx); + } + + return valid; +} + +/*! + * \internal + * \brief Clean up global memory associated with XML schemas + */ +void +crm_schema_cleanup(void) +{ + int lpc; + relaxng_ctx_cache_t *ctx = NULL; + + for (lpc = 0; lpc < xml_schema_max; lpc++) { + + 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; + } + free(known_schemas[lpc].name); + free(known_schemas[lpc].transform); + free(known_schemas[lpc].transform_enter); + } + free(known_schemas); + known_schemas = NULL; + + wrap_libxslt(true); +} + +static gboolean +validate_with(xmlNode *xml, int method, gboolean to_logs) +{ + xmlDocPtr doc = NULL; + gboolean valid = FALSE; + char *file = NULL; + + if (method < 0) { + return FALSE; + } + + if (known_schemas[method].validator == schema_validator_none) { + return TRUE; + } + + CRM_CHECK(xml != NULL, return FALSE); + + if (pcmk__str_eq(known_schemas[method].name, "pacemaker-next", + pcmk__str_none)) { + crm_warn("The pacemaker-next schema is deprecated and will be removed " + "in a future release."); + } + + doc = getDocPtr(xml); + file = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng, + known_schemas[method].name); + + crm_trace("Validating with %s (type=%d)", + pcmk__s(file, "missing schema"), known_schemas[method].validator); + switch (known_schemas[method].validator) { + case schema_validator_rng: + valid = + validate_with_relaxng(doc, to_logs, file, + (relaxng_ctx_cache_t **) & (known_schemas[method].cache)); + break; + default: + crm_err("Unknown validator type: %d", + known_schemas[method].validator); + break; + } + + free(file); + return valid; +} + +static bool +validate_with_silent(xmlNode *xml, int method) +{ + bool rc, sl_backup = silent_logging; + silent_logging = TRUE; + rc = validate_with(xml, method, TRUE); + silent_logging = sl_backup; + return rc; +} + +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(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 = xmlParseFile(filename); + 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) +{ + int version = 0; + + if (validation == NULL) { + validation = crm_element_value(xml_blob, XML_ATTR_VALIDATION); + } + + if (validation == NULL) { + int lpc = 0; + bool valid = FALSE; + + for (lpc = 0; lpc < xml_schema_max; lpc++) { + if (validate_with(xml_blob, lpc, FALSE)) { + 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; + } + } + } + + 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, to_logs); + } + + crm_err("Unknown validator: %s", validation); + return FALSE; +} + +static void +cib_upgrade_err(void *ctx, const char *fmt, ...) +G_GNUC_PRINTF(2, 3); + +/* With this arrangement, an attempt to identify the message severity + as explicitly signalled directly from XSLT is performed in rather + a smart way (no reliance on formatting string + arguments being + always specified as ["%s", purposeful_string], as it can also be + ["%s: %s", some_prefix, purposeful_string] etc. so every argument + pertaining %s specifier is investigated), and if such a mark found, + the respective level is determined and, when the messages are to go + to the native logs, the mark itself gets dropped + (by the means of string shift). + + NOTE: whether the native logging is the right sink is decided per + the ctx parameter -- NULL denotes this case, otherwise it + carries a pointer to the numeric expression of the desired + target logging level (messages with higher level will be + suppressed) + + NOTE: on some architectures, this string shift may not have any + effect, but that's an acceptable tradeoff + + The logging level for not explicitly designated messages + (suspicious, likely internal errors or some runaways) is + LOG_WARNING. + */ +static void +cib_upgrade_err(void *ctx, const char *fmt, ...) +{ + va_list ap, aq; + char *arg_cur; + + bool found = FALSE; + const char *fmt_iter = fmt; + uint8_t msg_log_level = LOG_WARNING; /* default for runaway messages */ + const unsigned * log_level = (const unsigned *) ctx; + enum { + escan_seennothing, + escan_seenpercent, + } scan_state = escan_seennothing; + + va_start(ap, fmt); + va_copy(aq, ap); + + while (!found && *fmt_iter != '\0') { + /* while casing schema borrowed from libqb:qb_vsnprintf_serialize */ + switch (*fmt_iter++) { + case '%': + if (scan_state == escan_seennothing) { + scan_state = escan_seenpercent; + } else if (scan_state == escan_seenpercent) { + scan_state = escan_seennothing; + } + break; + case 's': + if (scan_state == escan_seenpercent) { + scan_state = escan_seennothing; + arg_cur = va_arg(aq, char *); + if (arg_cur != NULL) { + switch (arg_cur[0]) { + case 'W': + if (!strncmp(arg_cur, "WARNING: ", + sizeof("WARNING: ") - 1)) { + msg_log_level = LOG_WARNING; + } + if (ctx == NULL) { + memmove(arg_cur, arg_cur + sizeof("WARNING: ") - 1, + strlen(arg_cur + sizeof("WARNING: ") - 1) + 1); + } + found = TRUE; + break; + case 'I': + if (!strncmp(arg_cur, "INFO: ", + sizeof("INFO: ") - 1)) { + msg_log_level = LOG_INFO; + } + if (ctx == NULL) { + memmove(arg_cur, arg_cur + sizeof("INFO: ") - 1, + strlen(arg_cur + sizeof("INFO: ") - 1) + 1); + } + found = TRUE; + break; + case 'D': + if (!strncmp(arg_cur, "DEBUG: ", + sizeof("DEBUG: ") - 1)) { + msg_log_level = LOG_DEBUG; + } + if (ctx == NULL) { + memmove(arg_cur, arg_cur + sizeof("DEBUG: ") - 1, + strlen(arg_cur + sizeof("DEBUG: ") - 1) + 1); + } + found = TRUE; + break; + } + } + } + break; + case '#': case '-': case ' ': case '+': case '\'': case 'I': case '.': + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + case '*': + break; + case 'l': + case 'z': + case 't': + case 'j': + case 'd': case 'i': + case 'o': + case 'u': + case 'x': case 'X': + case 'e': case 'E': + case 'f': case 'F': + case 'g': case 'G': + case 'a': case 'A': + case 'c': + case 'p': + if (scan_state == escan_seenpercent) { + (void) va_arg(aq, void *); /* skip forward */ + scan_state = escan_seennothing; + } + break; + default: + scan_state = escan_seennothing; + break; + } + } + + if (log_level != NULL) { + /* intention of the following offset is: + cibadmin -V -> start showing INFO labelled messages */ + if (*log_level + 4 >= msg_log_level) { + vfprintf(stderr, fmt, ap); + } + } else { + PCMK__XML_LOG_BASE(msg_log_level, TRUE, 0, "CIB upgrade: ", fmt, ap); + } + + va_end(aq); + va_end(ap); +} + + +/* Denotes temporary emergency fix for "xmldiff'ing not text-node-ready"; + proper fix is most likely to teach __xml_diff_object and friends to + deal with XML_TEXT_NODE (and more?), i.e., those nodes currently + missing "_private" field (implicitly as NULL) which clashes with + unchecked accesses (e.g. in __xml_offset) -- the outcome may be that + those unexpected XML nodes will simply be ignored for the purpose of + diff'ing, or it may be made more robust, or per the user's preference + (which then may be exposed as crm_diff switch). + + Said XML_TEXT_NODE may appear unexpectedly due to how upgrade-2.10.xsl + is arranged. + + The emergency fix is simple: reparse XSLT output with blank-ignoring + parser. */ +#ifndef PCMK_SCHEMAS_EMERGENCY_XSLT +#define PCMK_SCHEMAS_EMERGENCY_XSLT 1 +#endif + +static xmlNode * +apply_transformation(xmlNode *xml, const char *transform, gboolean to_logs) +{ + char *xform = NULL; + xmlNode *out = NULL; + xmlDocPtr res = NULL; + xmlDocPtr doc = NULL; + xsltStylesheet *xslt = NULL; +#if PCMK_SCHEMAS_EMERGENCY_XSLT != 0 + xmlChar *emergency_result; + int emergency_txt_len; + int emergency_res; +#endif + + CRM_CHECK(xml != NULL, return FALSE); + doc = getDocPtr(xml); + xform = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, + transform); + + xmlLoadExtDtdDefaultValue = 1; + xmlSubstituteEntitiesDefault(1); + + /* for capturing, e.g., what's emitted via <xsl:message> */ + if (to_logs) { + xsltSetGenericErrorFunc(NULL, cib_upgrade_err); + } else { + xsltSetGenericErrorFunc(&crm_log_level, cib_upgrade_err); + } + + xslt = xsltParseStylesheetFile((pcmkXmlStr) xform); + CRM_CHECK(xslt != NULL, goto cleanup); + + res = xsltApplyStylesheet(xslt, doc, NULL); + CRM_CHECK(res != NULL, goto cleanup); + + xsltSetGenericErrorFunc(NULL, NULL); /* restore default one */ + + +#if PCMK_SCHEMAS_EMERGENCY_XSLT != 0 + emergency_res = xsltSaveResultToString(&emergency_result, + &emergency_txt_len, res, xslt); + xmlFreeDoc(res); + CRM_CHECK(emergency_res == 0, goto cleanup); + out = string2xml((const char *) emergency_result); + free(emergency_result); +#else + out = xmlDocGetRootElement(res); +#endif + + cleanup: + if (xslt) { + xsltFreeStylesheet(xslt); + } + + free(xform); + + return out; +} + +/*! + * \internal + * \brief Possibly full enter->upgrade->leave trip per internal bookkeeping. + * + * \note Only emits warnings about enter/leave phases in case of issues. + */ +static xmlNode * +apply_upgrade(xmlNode *xml, const struct schema_s *schema, gboolean to_logs) +{ + bool transform_onleave = schema->transform_onleave; + char *transform_leave; + xmlNode *upgrade = NULL, + *final = NULL; + + if (schema->transform_enter) { + crm_debug("Upgrading %s-style configuration, pre-upgrade phase with %s.xsl", + 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", + schema->transform_enter); + transform_onleave = FALSE; + } + } + 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); + if (upgrade != xml) { + free_xml(upgrade); + upgrade = NULL; + } + + 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); + final = apply_transformation(upgrade, transform_leave, to_logs); + if (final == NULL) { + crm_warn("Upgrade-leave transformation %s.xsl failed", transform_leave); + final = upgrade; + } else { + free_xml(upgrade); + } + free(transform_leave); + } + + return final; +} + +const char * +get_schema_name(int version) +{ + if (version < 0 || version >= xml_schema_max) { + return "unknown"; + } + return known_schemas[version].name; +} + +int +get_schema_version(const char *name) +{ + int lpc = 0; + + if (name == NULL) { + name = PCMK__VALUE_NONE; + } + for (; lpc < xml_schema_max; lpc++) { + if (pcmk__str_eq(name, known_schemas[lpc].name, pcmk__str_casei)) { + return lpc; + } + } + return -1; +} + +/* set which validation to use */ +int +update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, + gboolean 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 */ + + CRM_CHECK(best != NULL, return -EINVAL); + *best = 0; + + CRM_CHECK(xml_blob != NULL, return -EINVAL); + CRM_CHECK(*xml_blob != NULL, return -EINVAL); + + xml = *xml_blob; + value = crm_element_value_copy(xml, XML_ATTR_VALIDATION); + + if (value != NULL) { + match = get_schema_version(value); + + lpc = match; + if (lpc >= 0 && transform == FALSE) { + *best = lpc++; + + } else if (lpc < 0) { + crm_debug("Unknown validation schema"); + lpc = 0; + } + } + + if (match >= max_stable_schemas) { + /* nothing to do */ + free(value); + *best = match; + return pcmk_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); + + if (validate_with(xml, lpc, to_logs) == 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) { + /* 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; + } + + if (rc == pcmk_ok) { + *best = lpc; + } + + 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, to_logs)) { + 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 == FALSE || rc != pcmk_ok) { + /* we need some progress! */ + lpc++; + } + } + + 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); + } + + *xml_blob = xml; + free(value); + return rc; +} + +gboolean +cli_config_update(xmlNode **xml, int *best_version, gboolean 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 version = get_schema_version(value); + int orig_version = version; + int min_version = xml_minimum_schema_index(); + + if (version < min_version) { + // Current configuration schema is not acceptable, try to update + xmlNode *converted = NULL; + + converted = copy_xml(*xml); + update_validation(&converted, &version, 0, TRUE, to_logs); + + value = crm_element_value(converted, XML_ATTR_VALIDATION); + if (version < min_version) { + // Updated configuration schema is still not acceptable + + if (version < orig_version || orig_version == -1) { + // 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 " + "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()); + } else { + fprintf(stderr, "Cannot upgrade configuration (claiming " + "schema %s) 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()); + } + } 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 " + "would not upgrade past %s", + orig_value, + get_schema_name(min_version), + pcmk__s(value, "unspecified version")); + } else { + fprintf(stderr, "Cannot upgrade configuration (claiming " + "schema %s) to at least %s because it " + "would not upgrade past %s\n", + orig_value, + get_schema_name(min_version), + pcmk__s(value, "unspecified version")); + } + } + + free_xml(converted); + converted = NULL; + rc = FALSE; + + } else { + // Updated configuration schema is acceptable + free_xml(*xml); + *xml = converted; + + if (version < xml_latest_schema_index()) { + if (to_logs) { + pcmk__config_warn("Configuration with schema %s 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)); + } + } + } + + } 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 { + fprintf(stderr, "Schema validation of configuration is disabled " + "(enabling is encouraged and prevents common " + "misconfigurations)\n"); + } + } + + if (best_version) { + *best_version = version; + } + + free(orig_value); + return rc; +} diff --git a/lib/common/scores.c b/lib/common/scores.c new file mode 100644 index 0000000..63c314e --- /dev/null +++ b/lib/common/scores.c @@ -0,0 +1,166 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include <stdio.h> // snprintf(), NULL +#include <string.h> // strcpy(), strdup() +#include <sys/types.h> // size_t + +int pcmk__score_red = 0; +int pcmk__score_green = 0; +int pcmk__score_yellow = 0; + +/*! + * \brief Get the integer value of a score string + * + * Given a string representation of a score, return the integer equivalent. + * This accepts infinity strings as well as red, yellow, and green, and + * bounds the result to +/-INFINITY. + * + * \param[in] score Score as string + * + * \return Integer value corresponding to \p score + */ +int +char2score(const char *score) +{ + if (score == NULL) { + return 0; + + } else if (pcmk_str_is_minus_infinity(score)) { + return -CRM_SCORE_INFINITY; + + } else if (pcmk_str_is_infinity(score)) { + return CRM_SCORE_INFINITY; + + } 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)) { + return pcmk__score_yellow; + + } 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; + + } else if (score_ll < -CRM_SCORE_INFINITY) { + return -CRM_SCORE_INFINITY; + + } else { + return (int) score_ll; + } + } +} + +/*! + * \brief Return a displayable static string for a score value + * + * Given a score value, return a pointer to a static string representation of + * the score suitable for log messages, output, etc. + * + * \param[in] score Score to display + * + * \return Pointer to static memory containing string representation of \p score + * \note Subsequent calls to this function will overwrite the returned value, so + * it should be used only in a local context such as a printf()-style + * statement. + */ +const char * +pcmk_readable_score(int score) +{ + // The longest possible result is "-INFINITY" + static char score_s[sizeof(CRM_MINUS_INFINITY_S)]; + + if (score >= CRM_SCORE_INFINITY) { + strcpy(score_s, CRM_INFINITY_S); + + } else if (score <= -CRM_SCORE_INFINITY) { + strcpy(score_s, CRM_MINUS_INFINITY_S); + + } else { + // Range is limited to +/-1000000, so no chance of overflow + snprintf(score_s, sizeof(score_s), "%d", score); + } + + return score_s; +} + +/*! + * \internal + * \brief Add two scores, bounding to +/-INFINITY + * + * \param[in] score1 First score to add + * \param[in] score2 Second score to add + * + * \note This function does not have context about what the scores mean, so it + * does not log any messages. + */ +int +pcmk__add_scores(int score1, int score2) +{ + /* As long as CRM_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 >= CRM_SCORE_INFINITY) || (score2 >= CRM_SCORE_INFINITY)) { + return CRM_SCORE_INFINITY; + } + + // Bound result to infinity. + if (result >= CRM_SCORE_INFINITY) { + return CRM_SCORE_INFINITY; + } + if (result <= -CRM_SCORE_INFINITY) { + return -CRM_SCORE_INFINITY; + } + + return result; +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include <crm/common/util_compat.h> + +char * +score2char(int score) +{ + char *result = strdup(pcmk_readable_score(score)); + + CRM_ASSERT(result != NULL); + return result; +} + +char * +score2char_stack(int score, char *buf, size_t len) +{ + CRM_CHECK((buf != NULL) && (len >= sizeof(CRM_MINUS_INFINITY_S)), + return NULL); + strcpy(buf, pcmk_readable_score(score)); + return buf; +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/strings.c b/lib/common/strings.c new file mode 100644 index 0000000..b245102 --- /dev/null +++ b/lib/common/strings.c @@ -0,0 +1,1363 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include "crm/common/results.h" +#include <crm_internal.h> + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include <regex.h> +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <ctype.h> +#include <float.h> // DBL_MIN +#include <limits.h> +#include <bzlib.h> +#include <sys/types.h> + +/*! + * \internal + * \brief Scan a long long integer from a string + * + * \param[in] text String to scan + * \param[out] result If not NULL, where to store scanned value + * \param[in] default_value Value to use if text is NULL or invalid + * \param[out] end_text If not NULL, where to store pointer to first + * non-integer character + * + * \return Standard Pacemaker return code (\c pcmk_rc_ok on success, + * \c EINVAL on failed string conversion due to invalid input, + * or \c EOVERFLOW on arithmetic overflow) + * \note Sets \c errno on error + */ +static int +scan_ll(const char *text, long long *result, long long default_value, + char **end_text) +{ + long long local_result = default_value; + char *local_end_text = NULL; + int rc = pcmk_rc_ok; + + errno = 0; + if (text != NULL) { + local_result = strtoll(text, &local_end_text, 10); + if (errno == ERANGE) { + rc = EOVERFLOW; + crm_warn("Integer parsed from '%s' was clipped to %lld", + text, local_result); + + } else if (errno != 0) { + rc = errno; + local_result = default_value; + crm_warn("Could not parse integer from '%s' (using %lld instead): " + "%s", text, default_value, pcmk_rc_str(rc)); + + } else if (local_end_text == text) { + rc = EINVAL; + local_result = default_value; + crm_warn("Could not parse integer from '%s' (using %lld instead): " + "No digits found", text, default_value); + } + + if ((end_text == NULL) && !pcmk__str_empty(local_end_text)) { + crm_warn("Characters left over after parsing '%s': '%s'", + text, local_end_text); + } + errno = rc; + } + if (end_text != NULL) { + *end_text = local_end_text; + } + if (result != NULL) { + *result = local_result; + } + return rc; +} + +/*! + * \internal + * \brief Scan a long long integer value from a string + * + * \param[in] text The string to scan (may be NULL) + * \param[out] result Where to store result (or NULL to ignore) + * \param[in] default_value Value to use if text is NULL or invalid + * + * \return Standard Pacemaker return code + */ +int +pcmk__scan_ll(const char *text, long long *result, long long default_value) +{ + long long local_result = default_value; + int rc = pcmk_rc_ok; + + if (text != NULL) { + rc = scan_ll(text, &local_result, default_value, NULL); + if (rc != pcmk_rc_ok) { + local_result = default_value; + } + } + if (result != NULL) { + *result = local_result; + } + return rc; +} + +/*! + * \internal + * \brief Scan an integer value from a string, constrained to a minimum + * + * \param[in] text The string to scan (may be NULL) + * \param[out] result Where to store result (or NULL to ignore) + * \param[in] minimum Value to use as default and minimum + * + * \return Standard Pacemaker return code + * \note If the value is larger than the maximum integer, EOVERFLOW will be + * returned and \p result will be set to the maximum integer. + */ +int +pcmk__scan_min_int(const char *text, int *result, int minimum) +{ + int rc; + long long result_ll; + + rc = pcmk__scan_ll(text, &result_ll, (long long) minimum); + + if (result_ll < (long long) minimum) { + crm_warn("Clipped '%s' to minimum acceptable value %d", text, minimum); + result_ll = (long long) minimum; + + } else if (result_ll > INT_MAX) { + crm_warn("Clipped '%s' to maximum integer %d", text, INT_MAX); + result_ll = (long long) INT_MAX; + rc = EOVERFLOW; + } + + if (result != NULL) { + *result = (int) result_ll; + } + return rc; +} + +/*! + * \internal + * \brief Scan a TCP port number from a string + * + * \param[in] text The string to scan + * \param[out] port Where to store result (or NULL to ignore) + * + * \return Standard Pacemaker return code + * \note \p port will be -1 if \p text is NULL or invalid + */ +int +pcmk__scan_port(const char *text, int *port) +{ + long long port_ll; + int rc = pcmk__scan_ll(text, &port_ll, -1LL); + + if ((text != NULL) && (rc == pcmk_rc_ok) // wasn't default or invalid + && ((port_ll < 0LL) || (port_ll > 65535LL))) { + crm_warn("Ignoring port specification '%s' " + "not in valid range (0-65535)", text); + rc = (port_ll < 0LL)? pcmk_rc_before_range : pcmk_rc_after_range; + port_ll = -1LL; + } + if (port != NULL) { + *port = (int) port_ll; + } + return rc; +} + +/*! + * \internal + * \brief Scan a double-precision floating-point value from a string + * + * \param[in] text The string to parse + * \param[out] result Parsed value on success, or + * \c PCMK__PARSE_DBL_DEFAULT on error + * \param[in] default_text Default string to parse if \p text is + * \c NULL + * \param[out] end_text If not \c NULL, where to store a pointer + * to the position immediately after the + * value + * + * \return Standard Pacemaker return code (\c pcmk_rc_ok on success, + * \c EINVAL on failed string conversion due to invalid input, + * \c EOVERFLOW on arithmetic overflow, \c pcmk_rc_underflow + * on arithmetic underflow, or \c errno from \c strtod() on + * other parse errors) + */ +int +pcmk__scan_double(const char *text, double *result, const char *default_text, + char **end_text) +{ + int rc = pcmk_rc_ok; + char *local_end_text = NULL; + + CRM_ASSERT(result != NULL); + *result = PCMK__PARSE_DBL_DEFAULT; + + text = (text != NULL) ? text : default_text; + + if (text == NULL) { + rc = EINVAL; + crm_debug("No text and no default conversion value supplied"); + + } else { + errno = 0; + *result = strtod(text, &local_end_text); + + if (errno == ERANGE) { + /* + * Overflow: strtod() returns +/- HUGE_VAL and sets errno to + * ERANGE + * + * Underflow: strtod() returns "a value whose magnitude is + * no greater than the smallest normalized + * positive" double. Whether ERANGE is set is + * implementation-defined. + */ + const char *over_under; + + if (QB_ABS(*result) > DBL_MIN) { + rc = EOVERFLOW; + over_under = "over"; + } else { + rc = pcmk_rc_underflow; + over_under = "under"; + } + + crm_debug("Floating-point value parsed from '%s' would %sflow " + "(using %g instead)", text, over_under, *result); + + } else if (errno != 0) { + rc = errno; + // strtod() set *result = 0 on parse failure + *result = PCMK__PARSE_DBL_DEFAULT; + + crm_debug("Could not parse floating-point value from '%s' (using " + "%.1f instead): %s", text, PCMK__PARSE_DBL_DEFAULT, + pcmk_rc_str(rc)); + + } else if (local_end_text == text) { + // errno == 0, but nothing was parsed + rc = EINVAL; + *result = PCMK__PARSE_DBL_DEFAULT; + + crm_debug("Could not parse floating-point value from '%s' (using " + "%.1f instead): No digits found", text, + PCMK__PARSE_DBL_DEFAULT); + + } else if (QB_ABS(*result) <= DBL_MIN) { + /* + * errno == 0 and text was parsed, but value might have + * underflowed. + * + * ERANGE might not be set for underflow. Check magnitude + * of *result, but also make sure the input number is not + * actually zero (0 <= DBL_MIN is not underflow). + * + * This check must come last. A parse failure in strtod() + * also sets *result == 0, so a parse failure would match + * this test condition prematurely. + */ + for (const char *p = text; p != local_end_text; p++) { + if (strchr("0.eE", *p) == NULL) { + rc = pcmk_rc_underflow; + crm_debug("Floating-point value parsed from '%s' would " + "underflow (using %g instead)", text, *result); + break; + } + } + + } else { + crm_trace("Floating-point value parsed successfully from " + "'%s': %g", text, *result); + } + + if ((end_text == NULL) && !pcmk__str_empty(local_end_text)) { + crm_debug("Characters left over after parsing '%s': '%s'", + text, local_end_text); + } + } + + if (end_text != NULL) { + *end_text = local_end_text; + } + + return rc; +} + +/*! + * \internal + * \brief Parse a guint from a string stored in a hash table + * + * \param[in] table Hash table to search + * \param[in] key Hash table key to use to retrieve string + * \param[in] default_val What to use if key has no entry in table + * \param[out] result If not NULL, where to store parsed integer + * + * \return Standard Pacemaker return code + */ +int +pcmk__guint_from_hash(GHashTable *table, const char *key, guint default_val, + guint *result) +{ + const char *value; + long long value_ll; + int rc = pcmk_rc_ok; + + CRM_CHECK((table != NULL) && (key != NULL), return EINVAL); + + if (result != NULL) { + *result = default_val; + } + + value = g_hash_table_lookup(table, key); + if (value == NULL) { + return pcmk_rc_ok; + } + + rc = pcmk__scan_ll(value, &value_ll, 0LL); + if (rc != pcmk_rc_ok) { + return rc; + } + + if ((value_ll < 0) || (value_ll > G_MAXUINT)) { + crm_warn("Could not parse non-negative integer from %s", value); + return ERANGE; + } + + if (result != NULL) { + *result = (guint) value_ll; + } + 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. + * + * \return Milliseconds corresponding to string expression, or + * PCMK__PARSE_INT_DEFAULT on error + */ +long long +crm_get_msec(const char *input) +{ + const char *num_start = NULL; + const char *units; + 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) { + return PCMK__PARSE_INT_DEFAULT; + } + units = num_start + num_len; + units += strspn(units, WHITESPACE); + + if (!strncasecmp(units, "ms", 2) || !strncasecmp(units, "msec", 4)) { + multiplier = 1; + divisor = 1; + } else if (!strncasecmp(units, "us", 2) || !strncasecmp(units, "usec", 4)) { + 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)) { + multiplier = 60 * 1000; + divisor = 1; + } else if (!strncasecmp(units, "h", 1) || !strncasecmp(units, "hr", 2)) { + multiplier = 60 * 60 * 1000; + divisor = 1; + } else if ((*units != '\0') && (*units != '\n') && (*units != '\r')) { + return PCMK__PARSE_INT_DEFAULT; + } + + scan_ll(num_start, &msec, PCMK__PARSE_INT_DEFAULT, &end_text); + if (msec > (LLONG_MAX / multiplier)) { + // Arithmetics overflow while multiplier/divisor mutually exclusive + return LLONG_MAX; + } + msec *= multiplier; + msec /= divisor; + return msec; +} + +gboolean +crm_is_true(const char *s) +{ + gboolean ret = FALSE; + + if (s != NULL) { + crm_str_to_boolean(s, &ret); + } + return ret; +} + +int +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; + 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; + return 1; + } + return -1; +} + +/*! + * \internal + * \brief Replace any trailing newlines in a string with \0's + * + * \param[in,out] str String to trim + * + * \return \p str + */ +char * +pcmk__trim(char *str) +{ + int len; + + if (str == NULL) { + return str; + } + + for (len = strlen(str) - 1; len >= 0 && str[len] == '\n'; len--) { + str[len] = '\0'; + } + + return str; +} + +/*! + * \brief Check whether a string starts with a certain sequence + * + * \param[in] str String to check + * \param[in] prefix Sequence to match against beginning of \p str + * + * \return \c true if \p str begins with match, \c false otherwise + * \note This is equivalent to !strncmp(s, prefix, strlen(prefix)) + * but is likely less efficient when prefix is a string literal + * if the compiler optimizes away the strlen() at compile time, + * and more efficient otherwise. + */ +bool +pcmk__starts_with(const char *str, const char *prefix) +{ + const char *s = str; + const char *p = prefix; + + if (!s || !p) { + return false; + } + while (*s && *p) { + if (*s++ != *p++) { + return false; + } + } + return (*p == 0); +} + +static inline bool +ends_with(const char *s, const char *match, bool as_extension) +{ + if (pcmk__str_empty(match)) { + return true; + } else if (s == NULL) { + return false; + } else { + size_t slen, mlen; + + /* Besides as_extension, we could also check + !strchr(&match[1], match[0]) but that would be inefficient. + */ + if (as_extension) { + s = strrchr(s, match[0]); + return (s == NULL)? false : !strcmp(s, match); + } + + mlen = strlen(match); + slen = strlen(s); + return ((slen >= mlen) && !strcmp(s + slen - mlen, match)); + } +} + +/*! + * \internal + * \brief Check whether a string ends with a certain sequence + * + * \param[in] s String to check + * \param[in] match Sequence to match against end of \p s + * + * \return \c true if \p s ends case-sensitively with match, \c false otherwise + * \note pcmk__ends_with_ext() can be used if the first character of match + * does not recur in match. + */ +bool +pcmk__ends_with(const char *s, const char *match) +{ + return ends_with(s, match, false); +} + +/*! + * \internal + * \brief Check whether a string ends with a certain "extension" + * + * \param[in] s String to check + * \param[in] match Extension to match against end of \p s, that is, + * its first character must not occur anywhere + * in the rest of that very sequence (example: file + * extension where the last dot is its delimiter, + * e.g., ".html"); incorrect results may be + * returned otherwise. + * + * \return \c true if \p s ends (verbatim, i.e., case sensitively) + * with "extension" designated as \p match (including empty + * string), \c false otherwise + * + * \note Main incentive to prefer this function over \c pcmk__ends_with() + * where possible is the efficiency (at the cost of added + * restriction on \p match as stated; the complexity class + * remains the same, though: BigO(M+N) vs. BigO(M+2N)). + */ +bool +pcmk__ends_with_ext(const char *s, const char *match) +{ + return ends_with(s, match, true); +} + +/*! + * \internal + * \brief Create a hash of a string suitable for use with GHashTable + * + * \param[in] v String to hash + * + * \return A hash of \p v compatible with g_str_hash() before glib 2.28 + * \note glib changed their hash implementation: + * + * https://gitlab.gnome.org/GNOME/glib/commit/354d655ba8a54b754cb5a3efb42767327775696c + * + * Note that the new g_str_hash is presumably a *better* hash (it's actually + * a correct implementation of DJB's hash), but we need to preserve existing + * behaviour, because the hash key ultimately determines the "sort" order + * when iterating through GHashTables, which affects allocation of scores to + * clone instances when iterating through rsc->allowed_nodes. It (somehow) + * also appears to have some minor impact on the ordering of a few + * pseudo_event IDs in the transition graph. + */ +static guint +pcmk__str_hash(gconstpointer v) +{ + const signed char *p; + guint32 h = 0; + + for (p = v; *p != '\0'; p++) + h = (h << 5) - h + *p; + + return h; +} + +/*! + * \internal + * \brief Create a hash table with case-sensitive strings as keys + * + * \param[in] key_destroy_func Function to free a key + * \param[in] value_destroy_func Function to free a value + * + * \return Newly allocated hash table + * \note It is the caller's responsibility to free the result, using + * g_hash_table_destroy(). + */ +GHashTable * +pcmk__strkey_table(GDestroyNotify key_destroy_func, + GDestroyNotify value_destroy_func) +{ + return g_hash_table_new_full(pcmk__str_hash, g_str_equal, + key_destroy_func, value_destroy_func); +} + +/* used with hash tables where case does not matter */ +static gboolean +pcmk__strcase_equal(gconstpointer a, gconstpointer b) +{ + return pcmk__str_eq((const char *)a, (const char *)b, pcmk__str_casei); +} + +static guint +pcmk__strcase_hash(gconstpointer v) +{ + const signed char *p; + guint32 h = 0; + + for (p = v; *p != '\0'; p++) + h = (h << 5) - h + g_ascii_tolower(*p); + + return h; +} + +/*! + * \internal + * \brief Create a hash table with case-insensitive strings as keys + * + * \param[in] key_destroy_func Function to free a key + * \param[in] value_destroy_func Function to free a value + * + * \return Newly allocated hash table + * \note It is the caller's responsibility to free the result, using + * g_hash_table_destroy(). + */ +GHashTable * +pcmk__strikey_table(GDestroyNotify key_destroy_func, + GDestroyNotify value_destroy_func) +{ + return g_hash_table_new_full(pcmk__strcase_hash, pcmk__strcase_equal, + key_destroy_func, value_destroy_func); +} + +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)); + } +} + +/*! + * \internal + * \brief Copy a hash table that uses dynamically allocated strings + * + * \param[in,out] old_table Hash table to duplicate + * + * \return New hash table with copies of everything in \p old_table + * \note This assumes the hash table uses dynamically allocated strings -- that + * is, both the key and value free functions are free(). + */ +GHashTable * +pcmk__str_table_dup(GHashTable *old_table) +{ + GHashTable *new_table = NULL; + + if (old_table) { + new_table = pcmk__strkey_table(free, free); + g_hash_table_foreach(old_table, copy_str_table_entry, new_table); + } + return new_table; +} + +/*! + * \internal + * \brief Add a word to a string list of words + * + * \param[in,out] list Pointer to current string list (may not be \p NULL) + * \param[in] init_size \p list will be initialized to at least this size, + * if it needs initialization (if 0, use GLib's default + * initial string size) + * \param[in] word String to add to \p list (\p list will be + * unchanged if this is \p NULL or the empty string) + * \param[in] separator String to separate words in \p list + * (a space will be used if this is NULL) + * + * \note \p word may contain \p separator, though that would be a bad idea if + * the string needs to be parsed later. + */ +void +pcmk__add_separated_word(GString **list, size_t init_size, const char *word, + const char *separator) +{ + CRM_ASSERT(list != NULL); + + if (pcmk__str_empty(word)) { + return; + } + + if (*list == NULL) { + if (init_size > 0) { + *list = g_string_sized_new(init_size); + } else { + *list = g_string_new(NULL); + } + } + + if ((*list)->len == 0) { + // Don't add a separator before the first word in the list + separator = ""; + + } else if (separator == NULL) { + // Default to space-separated + separator = " "; + } + + g_string_append(*list, separator); + g_string_append(*list, word); +} + +/*! + * \internal + * \brief Compress data + * + * \param[in] data Data to compress + * \param[in] length Number of characters of data to compress + * \param[in] max Maximum size of compressed data (or 0 to estimate) + * \param[out] result Where to store newly allocated compressed result + * \param[out] result_len Where to store actual compressed length of result + * + * \return Standard Pacemaker return code + */ +int +pcmk__compress(const char *data, unsigned int length, unsigned int max, + char **result, unsigned int *result_len) +{ + int rc; + char *compressed = NULL; + char *uncompressed = strdup(data); +#ifdef CLOCK_MONOTONIC + struct timespec after_t; + struct timespec before_t; +#endif + + if (max == 0) { + max = (length * 1.01) + 601; // Size guaranteed to hold result + } + +#ifdef CLOCK_MONOTONIC + clock_gettime(CLOCK_MONOTONIC, &before_t); +#endif + + compressed = calloc((size_t) max, sizeof(char)); + CRM_ASSERT(compressed); + + *result_len = max; + rc = BZ2_bzBuffToBuffCompress(compressed, result_len, uncompressed, length, + CRM_BZ2_BLOCKS, 0, CRM_BZ2_WORK); + free(uncompressed); + if (rc != BZ_OK) { + crm_err("Compression of %d bytes failed: %s " CRM_XS " bzerror=%d", + length, bz2_strerror(rc), rc); + free(compressed); + return pcmk_rc_error; + } + +#ifdef CLOCK_MONOTONIC + clock_gettime(CLOCK_MONOTONIC, &after_t); + + crm_trace("Compressed %d bytes into %d (ratio %d:1) in %.0fms", + length, *result_len, length / (*result_len), + (after_t.tv_sec - before_t.tv_sec) * 1000 + + (after_t.tv_nsec - before_t.tv_nsec) / 1e6); +#else + crm_trace("Compressed %d bytes into %d (ratio %d:1)", + length, *result_len, length / (*result_len)); +#endif + + *result = compressed; + return pcmk_rc_ok; +} + +char * +crm_strdup_printf(char const *format, ...) +{ + va_list ap; + int len = 0; + char *string = NULL; + + va_start(ap, format); + len = vasprintf (&string, format, ap); + CRM_ASSERT(len > 0); + va_end(ap); + return string; +} + +int +pcmk__parse_ll_range(const char *srcstring, long long *start, long long *end) +{ + char *remainder = NULL; + int rc = pcmk_rc_ok; + + CRM_ASSERT(start != NULL && end != NULL); + + *start = PCMK__PARSE_INT_DEFAULT; + *end = PCMK__PARSE_INT_DEFAULT; + + crm_trace("Attempting to decode: [%s]", srcstring); + if (pcmk__str_eq(srcstring, "", pcmk__str_null_matches)) { + return ENODATA; + } else if (pcmk__str_eq(srcstring, "-", pcmk__str_none)) { + return pcmk_rc_bad_input; + } + + /* String starts with a dash, so this is either a range with + * no beginning or garbage. + * */ + if (*srcstring == '-') { + int rc = scan_ll(srcstring+1, end, PCMK__PARSE_INT_DEFAULT, &remainder); + + if (rc != pcmk_rc_ok || *remainder != '\0') { + return pcmk_rc_bad_input; + } else { + return pcmk_rc_ok; + } + } + + rc = scan_ll(srcstring, start, PCMK__PARSE_INT_DEFAULT, &remainder); + if (rc != pcmk_rc_ok) { + return rc; + } + + if (*remainder && *remainder == '-') { + if (*(remainder+1)) { + char *more_remainder = NULL; + int rc = scan_ll(remainder+1, end, PCMK__PARSE_INT_DEFAULT, + &more_remainder); + + if (rc != pcmk_rc_ok) { + return rc; + } else if (*more_remainder != '\0') { + return pcmk_rc_bad_input; + } + } + } else if (*remainder && *remainder != '-') { + *start = PCMK__PARSE_INT_DEFAULT; + return pcmk_rc_bad_input; + } else { + /* The input string contained only one number. Set start and end + * to the same value and return pcmk_rc_ok. This gives the caller + * a way to tell this condition apart from a range with no end. + */ + *end = *start; + } + + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Find a string in a list of strings + * + * \note This function takes the same flags and has the same behavior as + * pcmk__str_eq(). + * + * \note No matter what input string or flags are provided, an empty + * list will always return FALSE. + * + * \param[in] s String to search for + * \param[in] lst List to search + * \param[in] flags A bitfield of pcmk__str_flags to modify operation + * + * \return \c TRUE if \p s is in \p lst, or \c FALSE otherwise + */ +gboolean +pcmk__str_in_list(const gchar *s, const GList *lst, uint32_t flags) +{ + for (const GList *ele = lst; ele != NULL; ele = ele->next) { + if (pcmk__str_eq(s, ele->data, flags)) { + return TRUE; + } + } + + return FALSE; +} + +static bool +str_any_of(const char *s, va_list args, uint32_t flags) +{ + if (s == NULL) { + return pcmk_is_set(flags, pcmk__str_null_matches); + } + + while (1) { + const char *ele = va_arg(args, const char *); + + if (ele == NULL) { + break; + } else if (pcmk__str_eq(s, ele, flags)) { + return true; + } + } + + return false; +} + +/*! + * \internal + * \brief Is a string a member of a list of strings? + * + * \param[in] s String to search for in \p ... + * \param[in] ... Strings to compare \p s against. The final string + * must be NULL. + * + * \note The comparison is done case-insensitively. The function name is + * meant to be reminiscent of strcasecmp. + * + * \return \c true if \p s is in \p ..., or \c false otherwise + */ +bool +pcmk__strcase_any_of(const char *s, ...) +{ + va_list ap; + bool rc; + + va_start(ap, s); + rc = str_any_of(s, ap, pcmk__str_casei); + va_end(ap); + return rc; +} + +/*! + * \internal + * \brief Is a string a member of a list of strings? + * + * \param[in] s String to search for in \p ... + * \param[in] ... Strings to compare \p s against. The final string + * must be NULL. + * + * \note The comparison is done taking case into account. + * + * \return \c true if \p s is in \p ..., or \c false otherwise + */ +bool +pcmk__str_any_of(const char *s, ...) +{ + va_list ap; + bool rc; + + va_start(ap, s); + rc = str_any_of(s, ap, pcmk__str_none); + va_end(ap); + return rc; +} + +/*! + * \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 + * portions of the string sorted numerically. This is particularly useful for + * node names (for example, "node10" will sort higher than "node9" but lower + * than "remotenode9"). + * + * \param[in] s1 First string to compare (must not be NULL) + * \param[in] s2 Second string to compare (must not be NULL) + * + * \retval -1 \p s1 comes before \p s2 + * \retval 0 \p s1 and \p s2 are equal + * \retval 1 \p s1 comes after \p s2 + */ +int +pcmk__numeric_strcasecmp(const char *s1, const char *s2) +{ + CRM_ASSERT((s1 != NULL) && (s2 != NULL)); + + while (*s1 && *s2) { + if (isdigit(*s1) && isdigit(*s2)) { + // If node names contain a number, sort numerically + + char *end1 = NULL; + char *end2 = NULL; + long num1 = strtol(s1, &end1, 10); + long num2 = strtol(s2, &end2, 10); + + // allow ordering e.g. 007 > 7 + size_t len1 = end1 - s1; + size_t len2 = end2 - s2; + + if (num1 < num2) { + return -1; + } else if (num1 > num2) { + return 1; + } else if (len1 < len2) { + return -1; + } else if (len1 > len2) { + return 1; + } + s1 = end1; + s2 = end2; + } else { + // Compare non-digits case-insensitively + int lower1 = tolower(*s1); + int lower2 = tolower(*s2); + + if (lower1 < lower2) { + return -1; + } else if (lower1 > lower2) { + return 1; + } + ++s1; + ++s2; + } + } + if (!*s1 && *s2) { + return -1; + } else if (*s1 && !*s2) { + return 1; + } + return 0; +} + +/*! + * \internal + * \brief Sort strings. + * + * This is your one-stop function for string comparison. By default, this + * function works like \p g_strcmp0. That is, like \p strcmp but a \p NULL + * string sorts before a non-<tt>NULL</tt> string. + * + * The \p pcmk__str_none flag produces the default behavior. Behavior can be + * changed with various flags: + * + * - \p pcmk__str_regex - The second string is a regular expression that the + * first string will be matched against. + * - \p pcmk__str_casei - By default, comparisons are done taking case into + * account. This flag makes comparisons case- + * insensitive. This can be combined with + * \p pcmk__str_regex. + * - \p pcmk__str_null_matches - If one string is \p NULL and the other is not, + * still return \p 0. + * - \p pcmk__str_star_matches - If one string is \p "*" and the other is not, + * still return \p 0. + * + * \param[in] s1 First string to compare + * \param[in] s2 Second string to compare, or a regular expression to + * match if \p pcmk__str_regex is set + * \param[in] flags A bitfield of \p pcmk__str_flags to modify operation + * + * \retval negative \p s1 is \p NULL or comes before \p s2 + * \retval 0 \p s1 and \p s2 are equal, or \p s1 is found in \p s2 if + * \c pcmk__str_regex is set + * \retval positive \p s2 is \p NULL or \p s1 comes after \p s2, or \p s2 + * is an invalid regular expression, or \p s1 was not found + * in \p s2 if \p pcmk__str_regex is set. + */ +int +pcmk__strcmp(const char *s1, const char *s2, uint32_t flags) +{ + /* If this flag is set, the second string is a regex. */ + if (pcmk_is_set(flags, pcmk__str_regex)) { + regex_t r_patt; + int reg_flags = REG_EXTENDED | REG_NOSUB; + int regcomp_rc = 0; + int rc = 0; + + if (s1 == NULL || s2 == NULL) { + return 1; + } + + if (pcmk_is_set(flags, pcmk__str_casei)) { + reg_flags |= REG_ICASE; + } + regcomp_rc = regcomp(&r_patt, s2, reg_flags); + if (regcomp_rc != 0) { + rc = 1; + crm_err("Bad regex '%s' for update: %s", s2, strerror(regcomp_rc)); + } else { + rc = regexec(&r_patt, s1, 0, NULL, 0); + regfree(&r_patt); + if (rc != 0) { + rc = 1; + } + } + return rc; + } + + /* If the strings are the same pointer, return 0 immediately. */ + if (s1 == s2) { + return 0; + } + + /* If this flag is set, return 0 if either (or both) of the input strings + * are NULL. If neither one is NULL, we need to continue and compare + * them normally. + */ + if (pcmk_is_set(flags, pcmk__str_null_matches)) { + if (s1 == NULL || s2 == NULL) { + return 0; + } + } + + /* Handle the cases where one is NULL and the str_null_matches flag is not set. + * A NULL string always sorts to the beginning. + */ + if (s1 == NULL) { + return -1; + } else if (s2 == NULL) { + return 1; + } + + /* If this flag is set, return 0 if either (or both) of the input strings + * are "*". If neither one is, we need to continue and compare them + * normally. + */ + if (pcmk_is_set(flags, pcmk__str_star_matches)) { + if (strcmp(s1, "*") == 0 || strcmp(s2, "*") == 0) { + return 0; + } + } + + if (pcmk_is_set(flags, pcmk__str_casei)) { + return strcasecmp(s1, s2); + } else { + return strcmp(s1, s2); + } +} + +/*! + * \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 + * is different from the new value, free the string and replace it with either a + * newly allocated duplicate of the value or NULL as appropriate. + * + * \param[in,out] str Pointer to dynamically allocated string + * \param[in] value New value to duplicate (or NULL) + * + * \note The caller remains responsibile for freeing \p *str. + */ +void +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); + } + } +} + +/*! + * \internal + * \brief Append a list of strings to a destination \p GString + * + * \param[in,out] buffer Where to append the strings (must not be \p NULL) + * \param[in] ... A <tt>NULL</tt>-terminated list of strings + * + * \note This tends to be more efficient than a single call to + * \p g_string_append_printf(). + */ +void +pcmk__g_strcat(GString *buffer, ...) +{ + va_list ap; + + CRM_ASSERT(buffer != NULL); + va_start(ap, buffer); + + while (true) { + const char *ele = va_arg(ap, const char *); + + if (ele == NULL) { + break; + } + g_string_append(buffer, ele); + } + va_end(ap); +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include <crm/common/util_compat.h> + +gboolean +safe_str_neq(const char *a, const char *b) +{ + if (a == b) { + return FALSE; + + } else if (a == NULL || b == NULL) { + return TRUE; + + } else if (strcasecmp(a, b) == 0) { + return FALSE; + } + return TRUE; +} + +gboolean +crm_str_eq(const char *a, const char *b, gboolean use_case) +{ + if (use_case) { + return g_strcmp0(a, b) == 0; + + /* TODO - Figure out which calls, if any, really need to be case independent */ + } else if (a == b) { + return TRUE; + + } else if (a == NULL || b == NULL) { + /* shouldn't be comparing NULLs */ + return FALSE; + + } else if (strcasecmp(a, b) == 0) { + return TRUE; + } + return FALSE; +} + +char * +crm_itoa_stack(int an_int, char *buffer, size_t len) +{ + if (buffer != NULL) { + snprintf(buffer, len, "%d", an_int); + } + return buffer; +} + +guint +g_str_hash_traditional(gconstpointer v) +{ + return pcmk__str_hash(v); +} + +gboolean +crm_strcase_equal(gconstpointer a, gconstpointer b) +{ + return pcmk__strcase_equal(a, b); +} + +guint +crm_strcase_hash(gconstpointer v) +{ + return pcmk__strcase_hash(v); +} + +GHashTable * +crm_str_table_dup(GHashTable *old_table) +{ + return pcmk__str_table_dup(old_table); +} + +long long +crm_parse_ll(const char *text, const char *default_text) +{ + long long result; + + if (text == NULL) { + text = default_text; + if (text == NULL) { + crm_err("No default conversion value supplied"); + errno = EINVAL; + return PCMK__PARSE_INT_DEFAULT; + } + } + scan_ll(text, &result, PCMK__PARSE_INT_DEFAULT, NULL); + return result; +} + +int +crm_parse_int(const char *text, const char *default_text) +{ + long long result = crm_parse_ll(text, default_text); + + if (result < INT_MIN) { + // If errno is ERANGE, crm_parse_ll() has already logged a message + if (errno != ERANGE) { + crm_err("Conversion of %s was clipped: %lld", text, result); + errno = ERANGE; + } + return INT_MIN; + + } else if (result > INT_MAX) { + // If errno is ERANGE, crm_parse_ll() has already logged a message + if (errno != ERANGE) { + crm_err("Conversion of %s was clipped: %lld", text, result); + errno = ERANGE; + } + return INT_MAX; + } + + return (int) result; +} + +char * +crm_strip_trailing_newline(char *str) +{ + return pcmk__trim(str); +} + +int +pcmk_numeric_strcasecmp(const char *s1, const char *s2) +{ + return pcmk__numeric_strcasecmp(s1, s2); +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/tests/Makefile.am b/lib/common/tests/Makefile.am new file mode 100644 index 0000000..b147309 --- /dev/null +++ b/lib/common/tests/Makefile.am @@ -0,0 +1,32 @@ +# +# Copyright 2020-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. +# + +SUBDIRS = \ + acl \ + agents \ + cmdline \ + flags \ + health \ + io \ + iso8601 \ + lists \ + nvpair \ + operations \ + options \ + output \ + results \ + scores \ + strings \ + utils \ + xml \ + xpath + +if SUPPORT_PROCFS +SUBDIRS += procfs +endif diff --git a/lib/common/tests/acl/Makefile.am b/lib/common/tests/acl/Makefile.am new file mode 100644 index 0000000..50408f9 --- /dev/null +++ b/lib/common/tests/acl/Makefile.am @@ -0,0 +1,21 @@ +# +# Copyright 2021-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 $(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_user_in_group_test \ + pcmk_acl_required_test \ + xml_acl_denied_test \ + xml_acl_enabled_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/acl/pcmk__is_user_in_group_test.c b/lib/common/tests/acl/pcmk__is_user_in_group_test.c new file mode 100644 index 0000000..917d92e --- /dev/null +++ b/lib/common/tests/acl/pcmk__is_user_in_group_test.c @@ -0,0 +1,38 @@ +/* + * Copyright 2020-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 <crm/common/acl.h> + +#include "../../crmcommon_private.h" +#include "mock_private.h" + +static void +is_pcmk__is_user_in_group(void **state) +{ + pcmk__mock_grent = true; + + // null user + assert_false(pcmk__is_user_in_group(NULL, "grp0")); + // null group + assert_false(pcmk__is_user_in_group("user0", NULL)); + // nonexistent group + assert_false(pcmk__is_user_in_group("user0", "nonexistent_group")); + // user is in group + assert_true(pcmk__is_user_in_group("user0", "grp0")); + // user is not in group + assert_false(pcmk__is_user_in_group("user2", "grp0")); + + pcmk__mock_grent = false; +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(is_pcmk__is_user_in_group)) diff --git a/lib/common/tests/acl/pcmk_acl_required_test.c b/lib/common/tests/acl/pcmk_acl_required_test.c new file mode 100644 index 0000000..bd5b922 --- /dev/null +++ b/lib/common/tests/acl/pcmk_acl_required_test.c @@ -0,0 +1,26 @@ +/* + * 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> +#include <crm/common/acl.h> + +static void +is_pcmk_acl_required(void **state) +{ + assert_false(pcmk_acl_required(NULL)); + assert_false(pcmk_acl_required("")); + assert_true(pcmk_acl_required("123")); + assert_false(pcmk_acl_required(CRM_DAEMON_USER)); + assert_false(pcmk_acl_required("root")); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(is_pcmk_acl_required)) diff --git a/lib/common/tests/acl/xml_acl_denied_test.c b/lib/common/tests/acl/xml_acl_denied_test.c new file mode 100644 index 0000000..faf2a39 --- /dev/null +++ b/lib/common/tests/acl/xml_acl_denied_test.c @@ -0,0 +1,61 @@ +/* + * 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> +#include <crm/common/acl.h> + +#include "../../crmcommon_private.h" + +static void +is_xml_acl_denied_without_node(void **state) +{ + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + assert_false(xml_acl_denied(test_xml)); + + test_xml->doc->_private = NULL; + assert_false(xml_acl_denied(test_xml)); + + test_xml->doc = NULL; + assert_false(xml_acl_denied(test_xml)); + + test_xml = NULL; + assert_false(xml_acl_denied(test_xml)); +} + +static void +is_xml_acl_denied_with_node(void **state) +{ + xml_doc_private_t *docpriv; + + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + + // allocate memory for _private, which is NULL by default + test_xml->doc->_private = calloc(1, sizeof(xml_doc_private_t)); + + assert_false(xml_acl_denied(test_xml)); + + // cast _private from void* to xml_doc_private_t* + docpriv = test_xml->doc->_private; + + // enable an irrelevant flag + docpriv->flags |= pcmk__xf_acl_enabled; + + assert_false(xml_acl_denied(test_xml)); + + // enable pcmk__xf_acl_denied + docpriv->flags |= pcmk__xf_acl_denied; + + assert_true(xml_acl_denied(test_xml)); +} + +PCMK__UNIT_TEST(NULL, 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 new file mode 100644 index 0000000..28665f4 --- /dev/null +++ b/lib/common/tests/acl/xml_acl_enabled_test.c @@ -0,0 +1,61 @@ +/* + * 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> +#include <crm/common/acl.h> + +#include "../../crmcommon_private.h" + +static void +is_xml_acl_enabled_without_node(void **state) +{ + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + assert_false(xml_acl_enabled(test_xml)); + + test_xml->doc->_private = NULL; + assert_false(xml_acl_enabled(test_xml)); + + test_xml->doc = NULL; + assert_false(xml_acl_enabled(test_xml)); + + test_xml = NULL; + assert_false(xml_acl_enabled(test_xml)); +} + +static void +is_xml_acl_enabled_with_node(void **state) +{ + xml_doc_private_t *docpriv; + + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + + // allocate memory for _private, which is NULL by default + test_xml->doc->_private = calloc(1, sizeof(xml_doc_private_t)); + + assert_false(xml_acl_enabled(test_xml)); + + // cast _private from void* to xml_doc_private_t* + docpriv = test_xml->doc->_private; + + // enable an irrelevant flag + docpriv->flags |= pcmk__xf_acl_denied; + + assert_false(xml_acl_enabled(test_xml)); + + // enable pcmk__xf_acl_enabled + docpriv->flags |= pcmk__xf_acl_enabled; + + assert_true(xml_acl_enabled(test_xml)); +} + +PCMK__UNIT_TEST(NULL, 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/agents/Makefile.am b/lib/common/tests/agents/Makefile.am new file mode 100644 index 0000000..7a54b7d --- /dev/null +++ b/lib/common/tests/agents/Makefile.am @@ -0,0 +1,20 @@ +# +# Copyright 2020-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 $(top_srcdir)/mk/tap.mk +include $(top_srcdir)/mk/unittest.mk + +# Add "_test" to the end of all test program names to simplify .gitignore. +check_PROGRAMS = crm_generate_ra_key_test \ + crm_parse_agent_spec_test \ + pcmk__effective_rc_test \ + pcmk_get_ra_caps_test \ + pcmk_stonith_param_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/agents/crm_generate_ra_key_test.c b/lib/common/tests/agents/crm_generate_ra_key_test.c new file mode 100644 index 0000000..f71c1c2 --- /dev/null +++ b/lib/common/tests/agents/crm_generate_ra_key_test.c @@ -0,0 +1,48 @@ +/* + * 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 <crm/common/agents.h> + +static void +all_params_null(void **state) { + assert_null(crm_generate_ra_key(NULL, NULL, NULL)); +} + +static void +some_params_null(void **state) { + char *retval; + + assert_null(crm_generate_ra_key("std", "prov", NULL)); + + retval = crm_generate_ra_key("std", NULL, "ty"); + assert_string_equal(retval, "std:ty"); + free(retval); + + assert_null(crm_generate_ra_key(NULL, "prov", "ty")); + assert_null(crm_generate_ra_key("std", NULL, NULL)); + assert_null(crm_generate_ra_key(NULL, "prov", NULL)); + assert_null(crm_generate_ra_key(NULL, NULL, "ty")); +} + +static void +no_params_null(void **state) { + char *retval; + + retval = crm_generate_ra_key("std", "prov", "ty"); + assert_string_equal(retval, "std:prov:ty"); + free(retval); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(all_params_null), + cmocka_unit_test(some_params_null), + cmocka_unit_test(no_params_null)) diff --git a/lib/common/tests/agents/crm_parse_agent_spec_test.c b/lib/common/tests/agents/crm_parse_agent_spec_test.c new file mode 100644 index 0000000..cfd75f0 --- /dev/null +++ b/lib/common/tests/agents/crm_parse_agent_spec_test.c @@ -0,0 +1,87 @@ +/* + * 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 <crm/common/agents.h> + +static void +all_params_null(void **state) { + assert_int_equal(crm_parse_agent_spec(NULL, NULL, NULL, NULL), -EINVAL); + assert_int_equal(crm_parse_agent_spec("", NULL, NULL, NULL), -EINVAL); + assert_int_equal(crm_parse_agent_spec(":", NULL, NULL, NULL), -EINVAL); + assert_int_equal(crm_parse_agent_spec("::", NULL, NULL, NULL), -EINVAL); +} + +static void +no_prov_or_type(void **state) { + assert_int_equal(crm_parse_agent_spec("ocf", NULL, NULL, NULL), -EINVAL); + assert_int_equal(crm_parse_agent_spec("ocf:", NULL, NULL, NULL), -EINVAL); + assert_int_equal(crm_parse_agent_spec("ocf::", NULL, NULL, NULL), -EINVAL); +} + +static void +no_type(void **state) { + assert_int_equal(crm_parse_agent_spec("ocf:pacemaker:", NULL, NULL, NULL), -EINVAL); +} + +static void +get_std_and_ty(void **state) { + char *std = NULL; + char *prov = NULL; + char *ty = NULL; + + assert_int_equal(crm_parse_agent_spec("stonith:fence_xvm", &std, &prov, &ty), pcmk_ok); + assert_string_equal(std, "stonith"); + assert_null(prov); + assert_string_equal(ty, "fence_xvm"); + + free(std); + free(ty); +} + +static void +get_all_values(void **state) { + char *std = NULL; + char *prov = NULL; + char *ty = NULL; + + assert_int_equal(crm_parse_agent_spec("ocf:pacemaker:ping", &std, &prov, &ty), pcmk_ok); + assert_string_equal(std, "ocf"); + assert_string_equal(prov, "pacemaker"); + assert_string_equal(ty, "ping"); + + free(std); + free(prov); + free(ty); +} + +static void +get_systemd_values(void **state) { + char *std = NULL; + char *prov = NULL; + char *ty = NULL; + + assert_int_equal(crm_parse_agent_spec("systemd:UNIT@A:B", &std, &prov, &ty), pcmk_ok); + assert_string_equal(std, "systemd"); + assert_null(prov); + assert_string_equal(ty, "UNIT@A:B"); + + free(std); + free(ty); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(all_params_null), + cmocka_unit_test(no_prov_or_type), + cmocka_unit_test(no_type), + cmocka_unit_test(get_std_and_ty), + cmocka_unit_test(get_all_values), + cmocka_unit_test(get_systemd_values)) diff --git a/lib/common/tests/agents/pcmk__effective_rc_test.c b/lib/common/tests/agents/pcmk__effective_rc_test.c new file mode 100644 index 0000000..c9bad97 --- /dev/null +++ b/lib/common/tests/agents/pcmk__effective_rc_test.c @@ -0,0 +1,36 @@ +/* + * 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 <crm/common/agents.h> + +static void +pcmk__effective_rc_test(void **state) { + /* All other PCMK_OCF_* values after UNKNOWN are deprecated and no longer used, + * so probably not worth testing them. + */ + assert_int_equal(PCMK_OCF_OK, pcmk__effective_rc(PCMK_OCF_OK)); + assert_int_equal(PCMK_OCF_OK, pcmk__effective_rc(PCMK_OCF_DEGRADED)); + assert_int_equal(PCMK_OCF_RUNNING_PROMOTED, pcmk__effective_rc(PCMK_OCF_DEGRADED_PROMOTED)); + assert_int_equal(PCMK_OCF_UNKNOWN, pcmk__effective_rc(PCMK_OCF_UNKNOWN)); + + /* There's nothing that says pcmk__effective_rc is restricted to PCMK_OCF_* + * values. That's just how it's used. Let's check some values outside + * that range just to be sure. + */ + assert_int_equal(-1, pcmk__effective_rc(-1)); + assert_int_equal(255, pcmk__effective_rc(255)); + assert_int_equal(INT_MAX, pcmk__effective_rc(INT_MAX)); + assert_int_equal(INT_MIN, pcmk__effective_rc(INT_MIN)); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(pcmk__effective_rc_test)) diff --git a/lib/common/tests/agents/pcmk_get_ra_caps_test.c b/lib/common/tests/agents/pcmk_get_ra_caps_test.c new file mode 100644 index 0000000..178dce5 --- /dev/null +++ b/lib/common/tests/agents/pcmk_get_ra_caps_test.c @@ -0,0 +1,63 @@ +/* + * 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 <crm/common/agents.h> + +static void +ocf_standard(void **state) { + uint32_t expected = pcmk_ra_cap_provider | pcmk_ra_cap_params | + pcmk_ra_cap_unique | pcmk_ra_cap_promotable; + + assert_int_equal(pcmk_get_ra_caps("ocf"), expected); + assert_int_equal(pcmk_get_ra_caps("OCF"), expected); +} + +static void +stonith_standard(void **state) { + uint32_t expected = pcmk_ra_cap_params | pcmk_ra_cap_unique | + pcmk_ra_cap_stdin | pcmk_ra_cap_fence_params; + + assert_int_equal(pcmk_get_ra_caps("stonith"), expected); + assert_int_equal(pcmk_get_ra_caps("StOnItH"), expected); +} + +static void +service_standard(void **state) { + assert_int_equal(pcmk_get_ra_caps("systemd"), pcmk_ra_cap_status); + assert_int_equal(pcmk_get_ra_caps("SYSTEMD"), pcmk_ra_cap_status); + assert_int_equal(pcmk_get_ra_caps("service"), pcmk_ra_cap_status); + assert_int_equal(pcmk_get_ra_caps("SeRvIcE"), pcmk_ra_cap_status); + assert_int_equal(pcmk_get_ra_caps("lsb"), pcmk_ra_cap_status); + assert_int_equal(pcmk_get_ra_caps("LSB"), pcmk_ra_cap_status); + assert_int_equal(pcmk_get_ra_caps("upstart"), pcmk_ra_cap_status); + assert_int_equal(pcmk_get_ra_caps("uPsTaRt"), pcmk_ra_cap_status); +} + +static void +nagios_standard(void **state) { + assert_int_equal(pcmk_get_ra_caps("nagios"), pcmk_ra_cap_params); + assert_int_equal(pcmk_get_ra_caps("NAGios"), pcmk_ra_cap_params); +} + +static void +unknown_standard(void **state) { + assert_int_equal(pcmk_get_ra_caps("blahblah"), pcmk_ra_cap_none); + assert_int_equal(pcmk_get_ra_caps(""), pcmk_ra_cap_none); + assert_int_equal(pcmk_get_ra_caps(NULL), pcmk_ra_cap_none); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(ocf_standard), + cmocka_unit_test(stonith_standard), + cmocka_unit_test(service_standard), + cmocka_unit_test(nagios_standard), + cmocka_unit_test(unknown_standard)) diff --git a/lib/common/tests/agents/pcmk_stonith_param_test.c b/lib/common/tests/agents/pcmk_stonith_param_test.c new file mode 100644 index 0000000..fad431e --- /dev/null +++ b/lib/common/tests/agents/pcmk_stonith_param_test.c @@ -0,0 +1,50 @@ +/* + * 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> +#include <crm/common/agents.h> + +static void +is_stonith_param(void **state) +{ + assert_false(pcmk_stonith_param(NULL)); + assert_false(pcmk_stonith_param("")); + assert_false(pcmk_stonith_param("unrecognized")); + assert_false(pcmk_stonith_param("pcmk_unrecognized")); + assert_false(pcmk_stonith_param("x" PCMK_STONITH_ACTION_LIMIT)); + assert_false(pcmk_stonith_param(PCMK_STONITH_ACTION_LIMIT "x")); + + assert_true(pcmk_stonith_param(PCMK_STONITH_ACTION_LIMIT)); + assert_true(pcmk_stonith_param(PCMK_STONITH_DELAY_BASE)); + assert_true(pcmk_stonith_param(PCMK_STONITH_DELAY_MAX)); + assert_true(pcmk_stonith_param(PCMK_STONITH_HOST_ARGUMENT)); + assert_true(pcmk_stonith_param(PCMK_STONITH_HOST_CHECK)); + assert_true(pcmk_stonith_param(PCMK_STONITH_HOST_LIST)); + assert_true(pcmk_stonith_param(PCMK_STONITH_HOST_MAP)); + assert_true(pcmk_stonith_param(PCMK_STONITH_PROVIDES)); + assert_true(pcmk_stonith_param(PCMK_STONITH_STONITH_TIMEOUT)); +} + +static void +is_stonith_action_param(void **state) +{ + /* Currently, the function accepts any string not containing underbars as + * the action name, so we do not need to verify particular action names. + */ + assert_false(pcmk_stonith_param("pcmk_on_unrecognized")); + assert_true(pcmk_stonith_param("pcmk_on_action")); + assert_true(pcmk_stonith_param("pcmk_on_timeout")); + assert_true(pcmk_stonith_param("pcmk_on_retries")); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(is_stonith_param), + cmocka_unit_test(is_stonith_action_param)) diff --git a/lib/common/tests/cmdline/Makefile.am b/lib/common/tests/cmdline/Makefile.am new file mode 100644 index 0000000..d781ed5 --- /dev/null +++ b/lib/common/tests/cmdline/Makefile.am @@ -0,0 +1,17 @@ +# +# Copyright 2020-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 $(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__cmdline_preproc_test \ + pcmk__quote_cmdline_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/cmdline/pcmk__cmdline_preproc_test.c b/lib/common/tests/cmdline/pcmk__cmdline_preproc_test.c new file mode 100644 index 0000000..863fbb9 --- /dev/null +++ b/lib/common/tests/cmdline/pcmk__cmdline_preproc_test.c @@ -0,0 +1,156 @@ +/* + * Copyright 2020-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 <crm/common/cmdline_internal.h> + +#include <glib.h> +#include <stdint.h> + +#define LISTS_EQ(a, b) { \ + assert_int_equal(g_strv_length((gchar **) (a)), g_strv_length((gchar **) (b))); \ + for (int i = 0; i < g_strv_length((a)); i++) { \ + assert_string_equal((a)[i], (b)[i]); \ + } \ +} + +static void +empty_input(void **state) { + assert_null(pcmk__cmdline_preproc(NULL, "")); +} + +static void +no_specials(void **state) { + const char *argv[] = { "crm_mon", "-a", "-b", "-c", "-d", "-1", NULL }; + const gchar *expected[] = { "crm_mon", "-a", "-b", "-c", "-d", "-1", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, NULL); + LISTS_EQ(processed, expected); + g_strfreev(processed); + + processed = pcmk__cmdline_preproc((char **) argv, ""); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + +static void +single_dash(void **state) { + const char *argv[] = { "crm_mon", "-", NULL }; + const gchar *expected[] = { "crm_mon", "-", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, NULL); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + +static void +double_dash(void **state) { + const char *argv[] = { "crm_mon", "-a", "--", "-bc", NULL }; + const gchar *expected[] = { "crm_mon", "-a", "--", "-bc", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, NULL); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + +static void +special_args(void **state) { + const char *argv[] = { "crm_mon", "-aX", "-Fval", NULL }; + const gchar *expected[] = { "crm_mon", "-a", "X", "-F", "val", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, "aF"); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + +static void +special_arg_at_end(void **state) { + const char *argv[] = { "crm_mon", "-a", NULL }; + const gchar *expected[] = { "crm_mon", "-a", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, "a"); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + +static void +long_arg(void **state) { + const char *argv[] = { "crm_mon", "--blah=foo", NULL }; + const gchar *expected[] = { "crm_mon", "--blah=foo", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, NULL); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + +static void +negative_score(void **state) { + const char *argv[] = { "crm_mon", "-v", "-1000", NULL }; + const gchar *expected[] = { "crm_mon", "-v", "-1000", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, "v"); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + +static void +negative_score_2(void **state) { + const char *argv[] = { "crm_mon", "-1i3", NULL }; + const gchar *expected[] = { "crm_mon", "-1", "-i", "-3", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, NULL); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + +static void +string_arg_with_dash(void **state) { + const char *argv[] = { "crm_mon", "-n", "crm_mon_options", "-v", "--opt1 --opt2", NULL }; + const gchar *expected[] = { "crm_mon", "-n", "crm_mon_options", "-v", "--opt1 --opt2", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, "v"); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + +static void +string_arg_with_dash_2(void **state) { + const char *argv[] = { "crm_mon", "-n", "crm_mon_options", "-v", "-1i3", NULL }; + const gchar *expected[] = { "crm_mon", "-n", "crm_mon_options", "-v", "-1i3", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, "v"); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + +static void +string_arg_with_dash_3(void **state) { + const char *argv[] = { "crm_mon", "-abc", "-1i3", NULL }; + const gchar *expected[] = { "crm_mon", "-a", "-b", "-c", "-1i3", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, "c"); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_input), + cmocka_unit_test(no_specials), + cmocka_unit_test(single_dash), + cmocka_unit_test(double_dash), + cmocka_unit_test(special_args), + cmocka_unit_test(special_arg_at_end), + cmocka_unit_test(long_arg), + cmocka_unit_test(negative_score), + cmocka_unit_test(negative_score_2), + cmocka_unit_test(string_arg_with_dash), + cmocka_unit_test(string_arg_with_dash_2), + cmocka_unit_test(string_arg_with_dash_3)) diff --git a/lib/common/tests/cmdline/pcmk__quote_cmdline_test.c b/lib/common/tests/cmdline/pcmk__quote_cmdline_test.c new file mode 100644 index 0000000..42bd8ca --- /dev/null +++ b/lib/common/tests/cmdline/pcmk__quote_cmdline_test.c @@ -0,0 +1,56 @@ +/* + * 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 <crm/common/cmdline_internal.h> + +#include <glib.h> + +static void +empty_input(void **state) { + assert_null(pcmk__quote_cmdline(NULL)); +} + +static void +no_spaces(void **state) { + const char *argv[] = { "crm_resource", "-r", "rsc1", "--meta", "-p", "comment", "-v", "hello", "--output-as=xml", NULL }; + const gchar *expected = "crm_resource -r rsc1 --meta -p comment -v hello --output-as=xml"; + + gchar *processed = pcmk__quote_cmdline((gchar **) argv); + assert_string_equal(processed, expected); + g_free(processed); +} + +static void +spaces_no_quote(void **state) { + const char *argv[] = { "crm_resource", "-r", "rsc1", "--meta", "-p", "comment", "-v", "hello world", "--output-as=xml", NULL }; + const gchar *expected = "crm_resource -r rsc1 --meta -p comment -v 'hello world' --output-as=xml"; + + gchar *processed = pcmk__quote_cmdline((gchar **) argv); + assert_string_equal(processed, expected); + g_free(processed); +} + +static void +spaces_with_quote(void **state) { + const char *argv[] = { "crm_resource", "-r", "rsc1", "--meta", "-p", "comment", "-v", "here's johnny", "--output-as=xml", NULL }; + const gchar *expected = "crm_resource -r rsc1 --meta -p comment -v 'here\\\'s johnny' --output-as=xml"; + + gchar *processed = pcmk__quote_cmdline((gchar **) argv); + assert_string_equal(processed, expected); + g_free(processed); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_input), + cmocka_unit_test(no_spaces), + cmocka_unit_test(spaces_no_quote), + cmocka_unit_test(spaces_with_quote)) diff --git a/lib/common/tests/flags/Makefile.am b/lib/common/tests/flags/Makefile.am new file mode 100644 index 0000000..16d8ffb --- /dev/null +++ b/lib/common/tests/flags/Makefile.am @@ -0,0 +1,20 @@ +# +# Copyright 2020-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 $(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__clear_flags_as_test \ + pcmk__set_flags_as_test \ + pcmk_all_flags_set_test \ + pcmk_any_flags_set_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/flags/pcmk__clear_flags_as_test.c b/lib/common/tests/flags/pcmk__clear_flags_as_test.c new file mode 100644 index 0000000..07dbe28 --- /dev/null +++ b/lib/common/tests/flags/pcmk__clear_flags_as_test.c @@ -0,0 +1,41 @@ +/* + * 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 +clear_none(void **state) { + assert_int_equal(pcmk__clear_flags_as(__func__, __LINE__, LOG_TRACE, "Test", + "test", 0x0f0, 0x00f, NULL), 0x0f0); + assert_int_equal(pcmk__clear_flags_as(__func__, __LINE__, LOG_TRACE, "Test", + "test", 0x0f0, 0xf0f, NULL), 0x0f0); +} + +static void +clear_some(void **state) { + assert_int_equal(pcmk__clear_flags_as(__func__, __LINE__, LOG_TRACE, "Test", + "test", 0x0f0, 0x020, NULL), 0x0d0); + assert_int_equal(pcmk__clear_flags_as(__func__, __LINE__, LOG_TRACE, "Test", + "test", 0x0f0, 0x030, NULL), 0x0c0); +} + +static void +clear_all(void **state) { + assert_int_equal(pcmk__clear_flags_as(__func__, __LINE__, LOG_TRACE, "Test", + "test", 0x0f0, 0x0f0, NULL), 0x000); + assert_int_equal(pcmk__clear_flags_as(__func__, __LINE__, LOG_TRACE, "Test", + "test", 0x0f0, 0xfff, NULL), 0x000); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(clear_none), + cmocka_unit_test(clear_some), + cmocka_unit_test(clear_all)) diff --git a/lib/common/tests/flags/pcmk__set_flags_as_test.c b/lib/common/tests/flags/pcmk__set_flags_as_test.c new file mode 100644 index 0000000..cd14c85 --- /dev/null +++ b/lib/common/tests/flags/pcmk__set_flags_as_test.c @@ -0,0 +1,25 @@ +/* + * 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 +set_flags(void **state) { + assert_int_equal(pcmk__set_flags_as(__func__, __LINE__, LOG_TRACE, "Test", + "test", 0x0f0, 0x00f, NULL), 0x0ff); + assert_int_equal(pcmk__set_flags_as(__func__, __LINE__, LOG_TRACE, "Test", + "test", 0x0f0, 0xf0f, NULL), 0xfff); + assert_int_equal(pcmk__set_flags_as(__func__, __LINE__, LOG_TRACE, "Test", + "test", 0x0f0, 0xfff, NULL), 0xfff); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(set_flags)) diff --git a/lib/common/tests/flags/pcmk_all_flags_set_test.c b/lib/common/tests/flags/pcmk_all_flags_set_test.c new file mode 100644 index 0000000..512ccce --- /dev/null +++ b/lib/common/tests/flags/pcmk_all_flags_set_test.c @@ -0,0 +1,33 @@ +/* + * 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 +all_set(void **state) { + assert_false(pcmk_all_flags_set(0x000, 0x003)); + assert_true(pcmk_all_flags_set(0x00f, 0x003)); + assert_false(pcmk_all_flags_set(0x00f, 0x010)); + assert_false(pcmk_all_flags_set(0x00f, 0x011)); + assert_true(pcmk_all_flags_set(0x000, 0x000)); + assert_true(pcmk_all_flags_set(0x00f, 0x000)); +} + +static void +one_is_set(void **state) { + // pcmk_is_set() is a simple macro alias for pcmk_all_flags_set() + assert_true(pcmk_is_set(0x00f, 0x001)); + assert_false(pcmk_is_set(0x00f, 0x010)); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(all_set), + cmocka_unit_test(one_is_set)) diff --git a/lib/common/tests/flags/pcmk_any_flags_set_test.c b/lib/common/tests/flags/pcmk_any_flags_set_test.c new file mode 100644 index 0000000..dc3aabc --- /dev/null +++ b/lib/common/tests/flags/pcmk_any_flags_set_test.c @@ -0,0 +1,26 @@ +/* + * 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 +any_set(void **state) { + assert_false(pcmk_any_flags_set(0x000, 0x000)); + assert_false(pcmk_any_flags_set(0x000, 0x001)); + assert_true(pcmk_any_flags_set(0x00f, 0x001)); + assert_false(pcmk_any_flags_set(0x00f, 0x010)); + assert_true(pcmk_any_flags_set(0x00f, 0x011)); + assert_false(pcmk_any_flags_set(0x000, 0x000)); + assert_false(pcmk_any_flags_set(0x00f, 0x000)); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(any_set)) diff --git a/lib/common/tests/health/Makefile.am b/lib/common/tests/health/Makefile.am new file mode 100644 index 0000000..ad2a2da --- /dev/null +++ b/lib/common/tests/health/Makefile.am @@ -0,0 +1,17 @@ +# +# 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 $(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__parse_health_strategy_test \ + pcmk__validate_health_strategy_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/health/pcmk__parse_health_strategy_test.c b/lib/common/tests/health/pcmk__parse_health_strategy_test.c new file mode 100644 index 0000000..28cc702 --- /dev/null +++ b/lib/common/tests/health/pcmk__parse_health_strategy_test.c @@ -0,0 +1,56 @@ +/* + * 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> + +static void +valid(void **state) { + assert_int_equal(pcmk__parse_health_strategy(NULL), + pcmk__health_strategy_none); + + assert_int_equal(pcmk__parse_health_strategy("none"), + pcmk__health_strategy_none); + + assert_int_equal(pcmk__parse_health_strategy("NONE"), + pcmk__health_strategy_none); + + assert_int_equal(pcmk__parse_health_strategy("None"), + pcmk__health_strategy_none); + + assert_int_equal(pcmk__parse_health_strategy("nOnE"), + pcmk__health_strategy_none); + + assert_int_equal(pcmk__parse_health_strategy("migrate-on-red"), + pcmk__health_strategy_no_red); + + assert_int_equal(pcmk__parse_health_strategy("only-green"), + pcmk__health_strategy_only_green); + + assert_int_equal(pcmk__parse_health_strategy("progressive"), + pcmk__health_strategy_progressive); + + assert_int_equal(pcmk__parse_health_strategy("custom"), + pcmk__health_strategy_custom); +} + +static void +invalid(void **state) { + assert_int_equal(pcmk__parse_health_strategy("foo"), + pcmk__health_strategy_none); + assert_int_equal(pcmk__parse_health_strategy("custom1"), + pcmk__health_strategy_none); + assert_int_equal(pcmk__parse_health_strategy("not-only-green-here"), + pcmk__health_strategy_none); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(valid), + cmocka_unit_test(invalid)) diff --git a/lib/common/tests/health/pcmk__validate_health_strategy_test.c b/lib/common/tests/health/pcmk__validate_health_strategy_test.c new file mode 100644 index 0000000..c7c60aa --- /dev/null +++ b/lib/common/tests/health/pcmk__validate_health_strategy_test.c @@ -0,0 +1,38 @@ +/* + * 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> + +// Test functions + +static void +valid_strategy(void **state) { + assert_true(pcmk__validate_health_strategy("none")); + assert_true(pcmk__validate_health_strategy("None")); + assert_true(pcmk__validate_health_strategy("NONE")); + assert_true(pcmk__validate_health_strategy("NoNe")); + assert_true(pcmk__validate_health_strategy("migrate-on-red")); + assert_true(pcmk__validate_health_strategy("only-green")); + assert_true(pcmk__validate_health_strategy("progressive")); + assert_true(pcmk__validate_health_strategy("custom")); +} + +static void +invalid_strategy(void **state) { + assert_false(pcmk__validate_health_strategy(NULL)); + assert_false(pcmk__validate_health_strategy("")); + assert_false(pcmk__validate_health_strategy("none to speak of")); + assert_false(pcmk__validate_health_strategy("customized")); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(valid_strategy), + cmocka_unit_test(invalid_strategy)) diff --git a/lib/common/tests/io/Makefile.am b/lib/common/tests/io/Makefile.am new file mode 100644 index 0000000..c26482c --- /dev/null +++ b/lib/common/tests/io/Makefile.am @@ -0,0 +1,18 @@ +# +# Copyright 2020-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 $(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__full_path_test \ + pcmk__get_tmpdir_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/io/pcmk__full_path_test.c b/lib/common/tests/io/pcmk__full_path_test.c new file mode 100644 index 0000000..dbbd71b --- /dev/null +++ b/lib/common/tests/io/pcmk__full_path_test.c @@ -0,0 +1,52 @@ +/* + * Copyright 2020-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 "mock_private.h" + +static void +function_asserts(void **state) +{ + pcmk__assert_asserts(pcmk__full_path(NULL, "/dir")); + pcmk__assert_asserts(pcmk__full_path("file", NULL)); + + pcmk__assert_asserts( + { + pcmk__mock_strdup = true; // strdup() will return NULL + expect_string(__wrap_strdup, s, "/full/path"); + pcmk__full_path("/full/path", "/dir"); + pcmk__mock_strdup = false; // Use real strdup() + } + ); +} + +static void +full_path(void **state) +{ + char *path = NULL; + + path = pcmk__full_path("file", "/dir"); + assert_int_equal(strcmp(path, "/dir/file"), 0); + free(path); + + path = pcmk__full_path("/full/path", "/dir"); + assert_int_equal(strcmp(path, "/full/path"), 0); + free(path); + + path = pcmk__full_path("../relative/path", "/dir"); + assert_int_equal(strcmp(path, "/dir/../relative/path"), 0); + free(path); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(function_asserts), + cmocka_unit_test(full_path)) diff --git a/lib/common/tests/io/pcmk__get_tmpdir_test.c b/lib/common/tests/io/pcmk__get_tmpdir_test.c new file mode 100644 index 0000000..bc17513 --- /dev/null +++ b/lib/common/tests/io/pcmk__get_tmpdir_test.c @@ -0,0 +1,68 @@ +/* + * Copyright 2021-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 "mock_private.h" + +static void +getenv_returns_invalid(void **state) +{ + const char *result; + + pcmk__mock_getenv = true; + + expect_string(__wrap_getenv, name, "TMPDIR"); + will_return(__wrap_getenv, NULL); // getenv("TMPDIR") return value + result = pcmk__get_tmpdir(); + assert_string_equal(result, "/tmp"); + + expect_string(__wrap_getenv, name, "TMPDIR"); + will_return(__wrap_getenv, ""); // getenv("TMPDIR") return value + result = pcmk__get_tmpdir(); + assert_string_equal(result, "/tmp"); + + expect_string(__wrap_getenv, name, "TMPDIR"); + will_return(__wrap_getenv, "subpath"); // getenv("TMPDIR") return value + result = pcmk__get_tmpdir(); + assert_string_equal(result, "/tmp"); + + pcmk__mock_getenv = false; +} + +static void +getenv_returns_valid(void **state) +{ + const char *result; + + pcmk__mock_getenv = true; + + expect_string(__wrap_getenv, name, "TMPDIR"); + will_return(__wrap_getenv, "/var/tmp"); // getenv("TMPDIR") return value + result = pcmk__get_tmpdir(); + assert_string_equal(result, "/var/tmp"); + + expect_string(__wrap_getenv, name, "TMPDIR"); + will_return(__wrap_getenv, "/"); // getenv("TMPDIR") return value + result = pcmk__get_tmpdir(); + assert_string_equal(result, "/"); + + expect_string(__wrap_getenv, name, "TMPDIR"); + will_return(__wrap_getenv, "/tmp/abcd.1234"); // getenv("TMPDIR") return value + result = pcmk__get_tmpdir(); + assert_string_equal(result, "/tmp/abcd.1234"); + + pcmk__mock_getenv = false; +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(getenv_returns_invalid), + cmocka_unit_test(getenv_returns_valid)) diff --git a/lib/common/tests/iso8601/Makefile.am b/lib/common/tests/iso8601/Makefile.am new file mode 100644 index 0000000..5187aec --- /dev/null +++ b/lib/common/tests/iso8601/Makefile.am @@ -0,0 +1,16 @@ +# +# Copyright 2020-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 $(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 + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/iso8601/pcmk__readable_interval_test.c b/lib/common/tests/iso8601/pcmk__readable_interval_test.c new file mode 100644 index 0000000..43b5541 --- /dev/null +++ b/lib/common/tests/iso8601/pcmk__readable_interval_test.c @@ -0,0 +1,27 @@ +/* + * 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 <limits.h> + +static void +readable_interval(void **state) +{ + assert_string_equal(pcmk__readable_interval(0), "0s"); + assert_string_equal(pcmk__readable_interval(30000), "30s"); + 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"); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(readable_interval)) diff --git a/lib/common/tests/lists/Makefile.am b/lib/common/tests/lists/Makefile.am new file mode 100644 index 0000000..ae0c0b6 --- /dev/null +++ b/lib/common/tests/lists/Makefile.am @@ -0,0 +1,20 @@ +# +# 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 $(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__list_of_1_test \ + pcmk__list_of_multiple_test \ + pcmk__subtract_lists_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/lists/pcmk__list_of_1_test.c b/lib/common/tests/lists/pcmk__list_of_1_test.c new file mode 100644 index 0000000..a3e0bbc --- /dev/null +++ b/lib/common/tests/lists/pcmk__list_of_1_test.c @@ -0,0 +1,45 @@ +/* + * 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 +empty_list(void **state) { + assert_false(pcmk__list_of_1(NULL)); +} + +static void +singleton_list(void **state) { + GList *lst = NULL; + + lst = g_list_append(lst, strdup("abc")); + assert_true(pcmk__list_of_1(lst)); + + g_list_free_full(lst, free); +} + +static void +longer_list(void **state) { + GList *lst = NULL; + + lst = g_list_append(lst, strdup("abc")); + lst = g_list_append(lst, strdup("xyz")); + assert_false(pcmk__list_of_1(lst)); + + g_list_free_full(lst, free); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_list), + cmocka_unit_test(singleton_list), + cmocka_unit_test(longer_list)) diff --git a/lib/common/tests/lists/pcmk__list_of_multiple_test.c b/lib/common/tests/lists/pcmk__list_of_multiple_test.c new file mode 100644 index 0000000..0966037 --- /dev/null +++ b/lib/common/tests/lists/pcmk__list_of_multiple_test.c @@ -0,0 +1,45 @@ +/* + * 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 +empty_list(void **state) { + assert_false(pcmk__list_of_multiple(NULL)); +} + +static void +singleton_list(void **state) { + GList *lst = NULL; + + lst = g_list_append(lst, strdup("abc")); + assert_false(pcmk__list_of_multiple(lst)); + + g_list_free_full(lst, free); +} + +static void +longer_list(void **state) { + GList *lst = NULL; + + lst = g_list_append(lst, strdup("abc")); + lst = g_list_append(lst, strdup("xyz")); + assert_true(pcmk__list_of_multiple(lst)); + + g_list_free_full(lst, free); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_list), + cmocka_unit_test(singleton_list), + cmocka_unit_test(longer_list)) diff --git a/lib/common/tests/lists/pcmk__subtract_lists_test.c b/lib/common/tests/lists/pcmk__subtract_lists_test.c new file mode 100644 index 0000000..1198e2b --- /dev/null +++ b/lib/common/tests/lists/pcmk__subtract_lists_test.c @@ -0,0 +1,144 @@ +/* + * 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 <crm/common/lists_internal.h> + +#include <glib.h> + +static void +different_lists(void **state) +{ + GList *from = NULL; + GList *items = NULL; + GList *result = NULL; + + from = g_list_append(from, strdup("abc")); + from = g_list_append(from, strdup("def")); + from = g_list_append(from, strdup("ghi")); + + items = g_list_append(items, strdup("123")); + items = g_list_append(items, strdup("456")); + + result = pcmk__subtract_lists(from, items, (GCompareFunc) strcmp); + + assert_int_equal(g_list_length(result), 3); + assert_string_equal(g_list_nth_data(result, 0), "abc"); + assert_string_equal(g_list_nth_data(result, 1), "def"); + assert_string_equal(g_list_nth_data(result, 2), "ghi"); + + g_list_free(result); + g_list_free_full(from, free); + g_list_free_full(items, free); +} + +static void +remove_first_item(void **state) +{ + GList *from = NULL; + GList *items = NULL; + GList *result = NULL; + + from = g_list_append(from, strdup("abc")); + from = g_list_append(from, strdup("def")); + from = g_list_append(from, strdup("ghi")); + + items = g_list_append(items, strdup("abc")); + + result = pcmk__subtract_lists(from, items, (GCompareFunc) strcmp); + + assert_int_equal(g_list_length(result), 2); + assert_string_equal(g_list_nth_data(result, 0), "def"); + assert_string_equal(g_list_nth_data(result, 1), "ghi"); + + g_list_free(result); + g_list_free_full(from, free); + g_list_free_full(items, free); +} + +static void +remove_middle_item(void **state) +{ + GList *from = NULL; + GList *items = NULL; + GList *result = NULL; + + from = g_list_append(from, strdup("abc")); + from = g_list_append(from, strdup("def")); + from = g_list_append(from, strdup("ghi")); + + items = g_list_append(items, strdup("def")); + + result = pcmk__subtract_lists(from, items, (GCompareFunc) strcmp); + + assert_int_equal(g_list_length(result), 2); + assert_string_equal(g_list_nth_data(result, 0), "abc"); + assert_string_equal(g_list_nth_data(result, 1), "ghi"); + + g_list_free(result); + g_list_free_full(from, free); + g_list_free_full(items, free); +} + +static void +remove_last_item(void **state) +{ + GList *from = NULL; + GList *items = NULL; + GList *result = NULL; + + from = g_list_append(from, strdup("abc")); + from = g_list_append(from, strdup("def")); + from = g_list_append(from, strdup("ghi")); + + items = g_list_append(items, strdup("ghi")); + + result = pcmk__subtract_lists(from, items, (GCompareFunc) strcmp); + + assert_int_equal(g_list_length(result), 2); + assert_string_equal(g_list_nth_data(result, 0), "abc"); + assert_string_equal(g_list_nth_data(result, 1), "def"); + + g_list_free(result); + g_list_free_full(from, free); + g_list_free_full(items, free); +} + +static void +remove_all_items(void **state) +{ + GList *from = NULL; + GList *items = NULL; + GList *result = NULL; + + from = g_list_append(from, strdup("abc")); + from = g_list_append(from, strdup("def")); + from = g_list_append(from, strdup("ghi")); + + items = g_list_append(items, strdup("abc")); + items = g_list_append(items, strdup("def")); + items = g_list_append(items, strdup("ghi")); + + result = pcmk__subtract_lists(from, items, (GCompareFunc) strcmp); + + assert_int_equal(g_list_length(result), 0); + + g_list_free(result); + g_list_free_full(from, free); + g_list_free_full(items, free); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(different_lists), + cmocka_unit_test(remove_first_item), + cmocka_unit_test(remove_middle_item), + cmocka_unit_test(remove_last_item), + cmocka_unit_test(remove_all_items)) diff --git a/lib/common/tests/nvpair/Makefile.am b/lib/common/tests/nvpair/Makefile.am new file mode 100644 index 0000000..7acaba3 --- /dev/null +++ b/lib/common/tests/nvpair/Makefile.am @@ -0,0 +1,18 @@ +# +# Copyright 2021-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 $(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 \ + pcmk__xe_get_bool_attr_test \ + pcmk__xe_set_bool_attr_test + +TESTS = $(check_PROGRAMS) 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 new file mode 100644 index 0000000..3723707 --- /dev/null +++ b/lib/common/tests/nvpair/pcmk__xe_attr_is_true_test.c @@ -0,0 +1,50 @@ +/* + * 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 <crm/common/xml_internal.h> + +static void +empty_input(void **state) +{ + xmlNode *node = string2xml("<node/>"); + + assert_false(pcmk__xe_attr_is_true(NULL, NULL)); + assert_false(pcmk__xe_attr_is_true(NULL, "whatever")); + assert_false(pcmk__xe_attr_is_true(node, NULL)); + + free_xml(node); +} + +static void +attr_missing(void **state) +{ + xmlNode *node = string2xml("<node a=\"true\" b=\"false\"/>"); + + assert_false(pcmk__xe_attr_is_true(node, "c")); + free_xml(node); +} + +static void +attr_present(void **state) +{ + xmlNode *node = string2xml("<node a=\"true\" b=\"false\"/>"); + + assert_true(pcmk__xe_attr_is_true(node, "a")); + assert_false(pcmk__xe_attr_is_true(node, "b")); + + free_xml(node); +} + +PCMK__UNIT_TEST(NULL, 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 new file mode 100644 index 0000000..500d8a6 --- /dev/null +++ b/lib/common/tests/nvpair/pcmk__xe_get_bool_attr_test.c @@ -0,0 +1,59 @@ +/* + * Copyright 2021-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/xml_internal.h> + +static void +empty_input(void **state) +{ + xmlNode *node = string2xml("<node/>"); + bool value; + + assert_int_equal(pcmk__xe_get_bool_attr(NULL, NULL, &value), ENODATA); + assert_int_equal(pcmk__xe_get_bool_attr(NULL, "whatever", &value), ENODATA); + assert_int_equal(pcmk__xe_get_bool_attr(node, NULL, &value), EINVAL); + assert_int_equal(pcmk__xe_get_bool_attr(node, "whatever", NULL), EINVAL); + + free_xml(node); +} + +static void +attr_missing(void **state) +{ + xmlNode *node = string2xml("<node a=\"true\" b=\"false\"/>"); + bool value; + + assert_int_equal(pcmk__xe_get_bool_attr(node, "c", &value), ENODATA); + free_xml(node); +} + +static void +attr_present(void **state) +{ + xmlNode *node = string2xml("<node a=\"true\" b=\"false\" c=\"blah\"/>"); + bool value; + + value = false; + assert_int_equal(pcmk__xe_get_bool_attr(node, "a", &value), pcmk_rc_ok); + assert_true(value); + value = true; + assert_int_equal(pcmk__xe_get_bool_attr(node, "b", &value), pcmk_rc_ok); + assert_false(value); + assert_int_equal(pcmk__xe_get_bool_attr(node, "c", &value), pcmk_rc_bad_input); + + free_xml(node); +} + +PCMK__UNIT_TEST(NULL, 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_set_bool_attr_test.c b/lib/common/tests/nvpair/pcmk__xe_set_bool_attr_test.c new file mode 100644 index 0000000..e1fb9c4 --- /dev/null +++ b/lib/common/tests/nvpair/pcmk__xe_set_bool_attr_test.c @@ -0,0 +1,31 @@ +/* + * 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 <crm/common/xml_internal.h> +#include <crm/msg_xml.h> + +static void +set_attr(void **state) +{ + xmlNode *node = string2xml("<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); + + free_xml(node); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(set_attr)) diff --git a/lib/common/tests/operations/Makefile.am b/lib/common/tests/operations/Makefile.am new file mode 100644 index 0000000..4687e1b --- /dev/null +++ b/lib/common/tests/operations/Makefile.am @@ -0,0 +1,22 @@ +# +# Copyright 2020-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 $(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 + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/operations/copy_in_properties_test.c b/lib/common/tests/operations/copy_in_properties_test.c new file mode 100644 index 0000000..7882551 --- /dev/null +++ b/lib/common/tests/operations/copy_in_properties_test.c @@ -0,0 +1,62 @@ + /* + * 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/operations/expand_plus_plus_test.c b/lib/common/tests/operations/expand_plus_plus_test.c new file mode 100644 index 0000000..41471f9 --- /dev/null +++ b/lib/common/tests/operations/expand_plus_plus_test.c @@ -0,0 +1,256 @@ +/* + * 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/operations/fix_plus_plus_recursive_test.c b/lib/common/tests/operations/fix_plus_plus_recursive_test.c new file mode 100644 index 0000000..b3c7cc2 --- /dev/null +++ b/lib/common/tests/operations/fix_plus_plus_recursive_test.c @@ -0,0 +1,47 @@ +/* + * 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/operations/parse_op_key_test.c b/lib/common/tests/operations/parse_op_key_test.c new file mode 100644 index 0000000..1b1bfff --- /dev/null +++ b/lib/common/tests/operations/parse_op_key_test.c @@ -0,0 +1,275 @@ +/* + * Copyright 2020-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 <glib.h> + +static void +basic(void **state) +{ + char *rsc = NULL; + char *ty = NULL; + guint ms = 0; + + assert_true(parse_op_key("Fencing_monitor_60000", &rsc, &ty, &ms)); + assert_string_equal(rsc, "Fencing"); + assert_string_equal(ty, "monitor"); + assert_int_equal(ms, 60000); + free(rsc); + free(ty); + + // Single-character resource name + assert_true(parse_op_key("R_monitor_100000", &rsc, &ty, &ms)); + assert_string_equal(rsc, "R"); + assert_string_equal(ty, "monitor"); + assert_int_equal(ms, 100000); + free(rsc); + free(ty); + + // Single-character action name + assert_true(parse_op_key("R_A_0", &rsc, &ty, &ms)); + assert_string_equal(rsc, "R"); + assert_string_equal(ty, "A"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); +} + +static void +rsc_just_underbars(void **state) +{ + char *rsc = NULL; + char *ty = NULL; + guint ms = 0; + + assert_true(parse_op_key("__monitor_1000", &rsc, &ty, &ms)); + assert_string_equal(rsc, "_"); + assert_string_equal(ty, "monitor"); + assert_int_equal(ms, 1000); + free(rsc); + free(ty); + + assert_true(parse_op_key("___migrate_from_0", &rsc, &ty, &ms)); + assert_string_equal(rsc, "__"); + assert_string_equal(ty, "migrate_from"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); + + assert_true(parse_op_key("____pre_notify_stop_0", &rsc, &ty, &ms)); + assert_string_equal(rsc, "___"); + assert_string_equal(ty, "pre_notify_stop"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); +} + +static void +colon_in_rsc(void **state) +{ + char *rsc = NULL; + char *ty = NULL; + guint ms = 0; + + assert_true(parse_op_key("ClusterIP:0_start_0", &rsc, &ty, &ms)); + assert_string_equal(rsc, "ClusterIP:0"); + assert_string_equal(ty, "start"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); + + assert_true(parse_op_key("imagestoreclone:1_post_notify_stop_0", &rsc, &ty, &ms)); + assert_string_equal(rsc, "imagestoreclone:1"); + assert_string_equal(ty, "post_notify_stop"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); +} + +static void +dashes_in_rsc(void **state) +{ + char *rsc = NULL; + char *ty = NULL; + guint ms = 0; + + assert_true(parse_op_key("httpd-bundle-0_monitor_30000", &rsc, &ty, &ms)); + assert_string_equal(rsc, "httpd-bundle-0"); + assert_string_equal(ty, "monitor"); + assert_int_equal(ms, 30000); + free(rsc); + free(ty); + + assert_true(parse_op_key("httpd-bundle-ip-192.168.122.132_start_0", &rsc, &ty, &ms)); + assert_string_equal(rsc, "httpd-bundle-ip-192.168.122.132"); + assert_string_equal(ty, "start"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); +} + +static void +migrate_to_from(void **state) +{ + char *rsc = NULL; + char *ty = NULL; + guint ms = 0; + + assert_true(parse_op_key("vm_migrate_from_0", &rsc, &ty, &ms)); + assert_string_equal(rsc, "vm"); + assert_string_equal(ty, "migrate_from"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); + + assert_true(parse_op_key("vm_migrate_to_0", &rsc, &ty, &ms)); + assert_string_equal(rsc, "vm"); + assert_string_equal(ty, "migrate_to"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); + + assert_true(parse_op_key("vm_idcc_devel_migrate_to_0", &rsc, &ty, &ms)); + assert_string_equal(rsc, "vm_idcc_devel"); + assert_string_equal(ty, "migrate_to"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); +} + +static void +pre_post(void **state) +{ + char *rsc = NULL; + char *ty = NULL; + guint ms = 0; + + assert_true(parse_op_key("rsc_drbd_7788:1_post_notify_start_0", &rsc, &ty, &ms)); + assert_string_equal(rsc, "rsc_drbd_7788:1"); + assert_string_equal(ty, "post_notify_start"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); + + assert_true(parse_op_key("rabbitmq-bundle-clone_pre_notify_stop_0", &rsc, &ty, &ms)); + assert_string_equal(rsc, "rabbitmq-bundle-clone"); + assert_string_equal(ty, "pre_notify_stop"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); + + assert_true(parse_op_key("post_notify_start_0", &rsc, &ty, &ms)); + assert_string_equal(rsc, "post_notify"); + assert_string_equal(ty, "start"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); + + assert_true(parse_op_key("r_confirmed-post_notify_start_0", + &rsc, &ty, &ms)); + assert_string_equal(rsc, "r"); + assert_string_equal(ty, "confirmed-post_notify_start"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); +} + +static void +skip_rsc(void **state) +{ + char *ty = NULL; + guint ms = 0; + + assert_true(parse_op_key("Fencing_monitor_60000", NULL, &ty, &ms)); + assert_string_equal(ty, "monitor"); + assert_int_equal(ms, 60000); + free(ty); +} + +static void +skip_ty(void **state) +{ + char *rsc = NULL; + guint ms = 0; + + assert_true(parse_op_key("Fencing_monitor_60000", &rsc, NULL, &ms)); + assert_string_equal(rsc, "Fencing"); + assert_int_equal(ms, 60000); + free(rsc); +} + +static void +skip_ms(void **state) +{ + char *rsc = NULL; + char *ty = NULL; + + assert_true(parse_op_key("Fencing_monitor_60000", &rsc, &ty, NULL)); + assert_string_equal(rsc, "Fencing"); + assert_string_equal(ty, "monitor"); + free(rsc); + free(ty); +} + +static void +empty_input(void **state) +{ + char *rsc = NULL; + char *ty = NULL; + guint ms = 0; + + assert_false(parse_op_key("", &rsc, &ty, &ms)); + assert_null(rsc); + assert_null(ty); + assert_int_equal(ms, 0); + + assert_false(parse_op_key(NULL, &rsc, &ty, &ms)); + assert_null(rsc); + assert_null(ty); + assert_int_equal(ms, 0); +} + +static void +malformed_input(void **state) +{ + char *rsc = NULL; + char *ty = NULL; + guint ms = 0; + + assert_false(parse_op_key("httpd-bundle-0", &rsc, &ty, &ms)); + assert_null(rsc); + assert_null(ty); + assert_int_equal(ms, 0); + + assert_false(parse_op_key("httpd-bundle-0_monitor", &rsc, &ty, &ms)); + assert_null(rsc); + assert_null(ty); + assert_int_equal(ms, 0); + + assert_false(parse_op_key("httpd-bundle-0_30000", &rsc, &ty, &ms)); + assert_null(rsc); + assert_null(ty); + assert_int_equal(ms, 0); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(basic), + cmocka_unit_test(rsc_just_underbars), + cmocka_unit_test(colon_in_rsc), + cmocka_unit_test(dashes_in_rsc), + cmocka_unit_test(migrate_to_from), + cmocka_unit_test(pre_post), + cmocka_unit_test(skip_rsc), + cmocka_unit_test(skip_ty), + cmocka_unit_test(skip_ms), + cmocka_unit_test(empty_input), + cmocka_unit_test(malformed_input)) diff --git a/lib/common/tests/operations/pcmk_is_probe_test.c b/lib/common/tests/operations/pcmk_is_probe_test.c new file mode 100644 index 0000000..4a65e3f --- /dev/null +++ b/lib/common/tests/operations/pcmk_is_probe_test.c @@ -0,0 +1,25 @@ +/* + * 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 +is_probe_test(void **state) +{ + assert_false(pcmk_is_probe(NULL, 0)); + assert_false(pcmk_is_probe("", 0)); + assert_false(pcmk_is_probe("blahblah", 0)); + assert_false(pcmk_is_probe("monitor", 1)); + assert_true(pcmk_is_probe("monitor", 0)); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(is_probe_test)) diff --git a/lib/common/tests/operations/pcmk_xe_is_probe_test.c b/lib/common/tests/operations/pcmk_xe_is_probe_test.c new file mode 100644 index 0000000..62b21d9 --- /dev/null +++ b/lib/common/tests/operations/pcmk_xe_is_probe_test.c @@ -0,0 +1,43 @@ +/* + * 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/operations/pcmk_xe_mask_probe_failure_test.c b/lib/common/tests/operations/pcmk_xe_mask_probe_failure_test.c new file mode 100644 index 0000000..9e38019 --- /dev/null +++ b/lib/common/tests/operations/pcmk_xe_mask_probe_failure_test.c @@ -0,0 +1,150 @@ +/* + * 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/options/Makefile.am b/lib/common/tests/options/Makefile.am new file mode 100644 index 0000000..9a5fa98 --- /dev/null +++ b/lib/common/tests/options/Makefile.am @@ -0,0 +1,19 @@ +# +# 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 $(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__env_option_test \ + pcmk__set_env_option_test \ + pcmk__env_option_enabled_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/options/pcmk__env_option_enabled_test.c b/lib/common/tests/options/pcmk__env_option_enabled_test.c new file mode 100644 index 0000000..b7d5d25 --- /dev/null +++ b/lib/common/tests/options/pcmk__env_option_enabled_test.c @@ -0,0 +1,101 @@ +/* + * 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 "mock_private.h" + +static void +disabled_null_value(void **state) +{ + // Return false if option value not found (NULL accomplishes this) + assert_false(pcmk__env_option_enabled(NULL, NULL)); + assert_false(pcmk__env_option_enabled("pacemaker-execd", NULL)); +} + +static void +enabled_true_value(void **state) +{ + // Return true if option value is true, with or without daemon name + pcmk__mock_getenv = true; + + expect_string(__wrap_getenv, name, "PCMK_env_var"); + will_return(__wrap_getenv, "true"); + assert_true(pcmk__env_option_enabled(NULL, "env_var")); + + expect_string(__wrap_getenv, name, "PCMK_env_var"); + will_return(__wrap_getenv, "true"); + assert_true(pcmk__env_option_enabled("pacemaker-execd", "env_var")); + + pcmk__mock_getenv = false; +} + +static void +disabled_false_value(void **state) +{ + // Return false if option value is false (no daemon list) + pcmk__mock_getenv = true; + + expect_string(__wrap_getenv, name, "PCMK_env_var"); + will_return(__wrap_getenv, "false"); + assert_false(pcmk__env_option_enabled(NULL, "env_var")); + + expect_string(__wrap_getenv, name, "PCMK_env_var"); + will_return(__wrap_getenv, "false"); + assert_false(pcmk__env_option_enabled("pacemaker-execd", "env_var")); + + pcmk__mock_getenv = false; +} + +static void +enabled_daemon_in_list(void **state) +{ + // Return true if daemon is in the option's value + pcmk__mock_getenv = true; + + expect_string(__wrap_getenv, name, "PCMK_env_var"); + will_return(__wrap_getenv, "pacemaker-execd"); + assert_true(pcmk__env_option_enabled("pacemaker-execd", "env_var")); + + expect_string(__wrap_getenv, name, "PCMK_env_var"); + will_return(__wrap_getenv, "pacemaker-execd,pacemaker-fenced"); + assert_true(pcmk__env_option_enabled("pacemaker-execd", "env_var")); + + expect_string(__wrap_getenv, name, "PCMK_env_var"); + will_return(__wrap_getenv, "pacemaker-controld,pacemaker-execd"); + assert_true(pcmk__env_option_enabled("pacemaker-execd", "env_var")); + + expect_string(__wrap_getenv, name, "PCMK_env_var"); + will_return(__wrap_getenv, + "pacemaker-controld,pacemaker-execd,pacemaker-fenced"); + assert_true(pcmk__env_option_enabled("pacemaker-execd", "env_var")); + + pcmk__mock_getenv = false; +} + +static void +disabled_daemon_not_in_list(void **state) +{ + // Return false if value is not true and daemon is not in the option's value + pcmk__mock_getenv = true; + + expect_string(__wrap_getenv, name, "PCMK_env_var"); + will_return(__wrap_getenv, "pacemaker-controld,pacemaker-fenced"); + assert_false(pcmk__env_option_enabled("pacemaker-execd", "env_var")); + + pcmk__mock_getenv = false; +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(disabled_null_value), + cmocka_unit_test(enabled_true_value), + cmocka_unit_test(disabled_false_value), + cmocka_unit_test(enabled_daemon_in_list), + cmocka_unit_test(disabled_daemon_not_in_list)) diff --git a/lib/common/tests/options/pcmk__env_option_test.c b/lib/common/tests/options/pcmk__env_option_test.c new file mode 100644 index 0000000..2b85b68 --- /dev/null +++ b/lib/common/tests/options/pcmk__env_option_test.c @@ -0,0 +1,134 @@ +/* + * 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 "mock_private.h" + +static void +empty_input_string(void **state) +{ + pcmk__mock_getenv = true; + + // getenv() not called + assert_null(pcmk__env_option(NULL)); + assert_null(pcmk__env_option("")); + + pcmk__mock_getenv = false; +} + +static void +input_too_long_for_both(void **state) +{ + /* pcmk__env_option() prepends "PCMK_" before lookup. If the option name is + * too long for the buffer or isn't found in the env, then it prepends "HA_" + * and tries again. A string of length (NAME_MAX - 3) will set us just over + * just over the edge for both tries. + */ + char long_opt[NAME_MAX - 2]; + + for (int i = 0; i < NAME_MAX - 3; i++) { + long_opt[i] = 'a'; + } + long_opt[NAME_MAX - 3] = '\0'; + + pcmk__mock_getenv = true; + + // getenv() not called + assert_null(pcmk__env_option(long_opt)); + + pcmk__mock_getenv = false; +} + +static void +input_too_long_for_pcmk(void **state) +{ + /* If an input is too long for "PCMK_<option>", make sure we fall through + * to try "HA_<option>". + * + * pcmk__env_option() prepends "PCMK_" first. A string of length + * (NAME_MAX - 5) will set us just over the edge, still short enough for + * "HA_<option>" to fit. + */ + char long_opt[NAME_MAX - 4]; + char buf[NAME_MAX]; + + for (int i = 0; i < NAME_MAX - 5; i++) { + long_opt[i] = 'a'; + } + long_opt[NAME_MAX - 5] = '\0'; + + pcmk__mock_getenv = true; + + /* NULL/non-NULL retval doesn't really matter here; just testing that we + * call getenv() for "HA_" prefix after too long for "PCMK_". + */ + snprintf(buf, NAME_MAX, "HA_%s", long_opt); + expect_string(__wrap_getenv, name, buf); + will_return(__wrap_getenv, "value"); + assert_string_equal(pcmk__env_option(long_opt), "value"); + + pcmk__mock_getenv = false; +} + +static void +value_not_found(void **state) +{ + // Value not found using PCMK_ or HA_ prefix. Should return NULL. + pcmk__mock_getenv = true; + + expect_string(__wrap_getenv, name, "PCMK_env_var"); + will_return(__wrap_getenv, NULL); + + expect_string(__wrap_getenv, name, "HA_env_var"); + will_return(__wrap_getenv, NULL); + + assert_null(pcmk__env_option("env_var")); + + pcmk__mock_getenv = false; +} + +static void +value_found_pcmk(void **state) +{ + // Value found using PCMK_. Should return value and skip HA_ lookup. + pcmk__mock_getenv = true; + + expect_string(__wrap_getenv, name, "PCMK_env_var"); + will_return(__wrap_getenv, "value"); + assert_string_equal(pcmk__env_option("env_var"), "value"); + + pcmk__mock_getenv = false; +} + +static void +value_found_ha(void **state) +{ + // Value not found using PCMK_. Move on to HA_ lookup, find, and return. + pcmk__mock_getenv = true; + + expect_string(__wrap_getenv, name, "PCMK_env_var"); + will_return(__wrap_getenv, NULL); + + expect_string(__wrap_getenv, name, "HA_env_var"); + will_return(__wrap_getenv, "value"); + + assert_string_equal(pcmk__env_option("env_var"), "value"); + + pcmk__mock_getenv = false; +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_input_string), + cmocka_unit_test(input_too_long_for_both), + cmocka_unit_test(input_too_long_for_pcmk), + cmocka_unit_test(value_not_found), + cmocka_unit_test(value_found_pcmk), + cmocka_unit_test(value_found_ha)) diff --git a/lib/common/tests/options/pcmk__set_env_option_test.c b/lib/common/tests/options/pcmk__set_env_option_test.c new file mode 100644 index 0000000..753bf74 --- /dev/null +++ b/lib/common/tests/options/pcmk__set_env_option_test.c @@ -0,0 +1,154 @@ +/* + * 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 "mock_private.h" + +static void +bad_input_string(void **state) +{ + // Bad setenv()/unsetenv() input: NULL, empty, or containing '=' + + // Never call setenv() + pcmk__mock_setenv = true; + + pcmk__set_env_option(NULL, "new_value"); + pcmk__set_env_option("", "new_value"); + pcmk__set_env_option("name=val", "new_value"); + + pcmk__mock_setenv = false; + + // Never call unsetenv() + pcmk__mock_unsetenv = true; + + pcmk__set_env_option(NULL, NULL); + pcmk__set_env_option("", NULL); + pcmk__set_env_option("name=val", NULL); + + pcmk__mock_unsetenv = false; +} + +static void +input_too_long_for_both(void **state) +{ + /* pcmk__set_env_option() wants to set "PCMK_<option>" and "HA_<option>". If + * "PCMK_<option>" is too long for the buffer, it simply moves on to + * "HA_<option>". A string of length (NAME_MAX - 3) will set us just over + * the edge for both tries. + */ + char long_opt[NAME_MAX - 2]; + + for (int i = 0; i < NAME_MAX - 3; i++) { + long_opt[i] = 'a'; + } + long_opt[NAME_MAX - 3] = '\0'; + + // Never call setenv() or unsetenv() + pcmk__mock_setenv = true; + pcmk__set_env_option(long_opt, "new_value"); + pcmk__mock_setenv = false; + + pcmk__mock_unsetenv = true; + pcmk__set_env_option(long_opt, NULL); + pcmk__mock_unsetenv = false; +} + +static void +input_too_long_for_pcmk(void **state) +{ + /* If an input is too long to set "PCMK_<option>", make sure we fall through + * to try to set "HA_<option>". + * + * A string of length (NAME_MAX - 5) will set us just over the edge for + * "PCMK_<option>", while still short enough for "HA_<option>" to fit. + */ + char long_opt[NAME_MAX - 4]; + char buf[NAME_MAX]; + + for (int i = 0; i < NAME_MAX - 5; i++) { + long_opt[i] = 'a'; + } + long_opt[NAME_MAX - 5] = '\0'; + + snprintf(buf, NAME_MAX, "HA_%s", long_opt); + + // Call setenv() for "HA_" only + pcmk__mock_setenv = true; + + expect_string(__wrap_setenv, name, buf); + expect_string(__wrap_setenv, value, "new_value"); + expect_value(__wrap_setenv, overwrite, 1); + will_return(__wrap_setenv, 0); + pcmk__set_env_option(long_opt, "new_value"); + + pcmk__mock_setenv = false; + + // Call unsetenv() for "HA_" only + pcmk__mock_unsetenv = true; + + expect_string(__wrap_unsetenv, name, buf); + will_return(__wrap_unsetenv, 0); + pcmk__set_env_option(long_opt, NULL); + + pcmk__mock_unsetenv = false; +} + +static void +valid_inputs_set(void **state) +{ + // Make sure we set "PCMK_<option>" and "HA_<option>" + pcmk__mock_setenv = true; + + expect_string(__wrap_setenv, name, "PCMK_env_var"); + expect_string(__wrap_setenv, value, "new_value"); + expect_value(__wrap_setenv, overwrite, 1); + will_return(__wrap_setenv, 0); + expect_string(__wrap_setenv, name, "HA_env_var"); + expect_string(__wrap_setenv, value, "new_value"); + expect_value(__wrap_setenv, overwrite, 1); + will_return(__wrap_setenv, 0); + pcmk__set_env_option("env_var", "new_value"); + + // Empty string is also a valid value + expect_string(__wrap_setenv, name, "PCMK_env_var"); + expect_string(__wrap_setenv, value, ""); + expect_value(__wrap_setenv, overwrite, 1); + will_return(__wrap_setenv, 0); + expect_string(__wrap_setenv, name, "HA_env_var"); + expect_string(__wrap_setenv, value, ""); + expect_value(__wrap_setenv, overwrite, 1); + will_return(__wrap_setenv, 0); + pcmk__set_env_option("env_var", ""); + + pcmk__mock_setenv = false; +} + +static void +valid_inputs_unset(void **state) +{ + // Make sure we unset "PCMK_<option>" and "HA_<option>" + pcmk__mock_unsetenv = true; + + expect_string(__wrap_unsetenv, name, "PCMK_env_var"); + will_return(__wrap_unsetenv, 0); + expect_string(__wrap_unsetenv, name, "HA_env_var"); + will_return(__wrap_unsetenv, 0); + pcmk__set_env_option("env_var", NULL); + + pcmk__mock_unsetenv = false; +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(bad_input_string), + cmocka_unit_test(input_too_long_for_both), + cmocka_unit_test(input_too_long_for_pcmk), + cmocka_unit_test(valid_inputs_set), + cmocka_unit_test(valid_inputs_unset)) diff --git a/lib/common/tests/output/Makefile.am b/lib/common/tests/output/Makefile.am new file mode 100644 index 0000000..6ac7b5f --- /dev/null +++ b/lib/common/tests/output/Makefile.am @@ -0,0 +1,24 @@ +# +# Copyright 2021-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 $(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__call_message_test \ + pcmk__output_and_clear_error_test \ + pcmk__output_free_test \ + pcmk__output_new_test \ + pcmk__register_format_test \ + pcmk__register_formats_test \ + pcmk__register_message_test \ + pcmk__register_messages_test \ + pcmk__unregister_formats_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/output/pcmk__call_message_test.c b/lib/common/tests/output/pcmk__call_message_test.c new file mode 100644 index 0000000..824eac7 --- /dev/null +++ b/lib/common/tests/output/pcmk__call_message_test.c @@ -0,0 +1,156 @@ +/* + * 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 <crm/common/output_internal.h> + +static int +default_message_fn(pcmk__output_t *out, va_list args) { + function_called(); + return pcmk_rc_ok; +} + +static int +failed_message_fn(pcmk__output_t *out, va_list args) { + function_called(); + return pcmk_rc_no_output; +} + +static int +message_fn_1(pcmk__output_t *out, va_list args) { + function_called(); + return pcmk_rc_ok; +} + +static int +message_fn_2(pcmk__output_t *out, va_list args) { + function_called(); + return pcmk_rc_ok; +} + +static bool +fake_text_init(pcmk__output_t *out) { + return true; +} + +static void +fake_text_free_priv(pcmk__output_t *out) { + /* This function intentionally left blank */ +} + +static pcmk__output_t * +mk_fake_text_output(char **argv) { + pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t)); + + if (retval == NULL) { + return NULL; + } + + retval->fmt_name = "text"; + retval->init = fake_text_init; + retval->free_priv = fake_text_free_priv; + + retval->register_message = pcmk__register_message; + retval->message = pcmk__call_message; + + return retval; +} + +static int +setup(void **state) { + pcmk__register_format(NULL, "text", mk_fake_text_output, NULL); + return 0; +} + +static int +teardown(void **state) { + pcmk__unregister_formats(); + return 0; +} + +static void +no_such_message(void **state) { + pcmk__output_t *out = NULL; + + pcmk__output_new(&out, "text", NULL, NULL); + + assert_int_equal(out->message(out, "fake"), EINVAL); + pcmk__assert_asserts(out->message(out, "")); + pcmk__assert_asserts(out->message(out, NULL)); + + pcmk__output_free(out); +} + +static void +message_return_value(void **state) { + pcmk__output_t *out = NULL; + + pcmk__message_entry_t entries[] = { + { "msg1", "text", message_fn_1 }, + { "msg2", "text", message_fn_2 }, + { "fail", "text", failed_message_fn }, + { NULL }, + }; + + pcmk__output_new(&out, "text", NULL, NULL); + pcmk__register_messages(out, entries); + + expect_function_call(message_fn_1); + assert_int_equal(out->message(out, "msg1"), pcmk_rc_ok); + expect_function_call(message_fn_2); + assert_int_equal(out->message(out, "msg2"), pcmk_rc_ok); + expect_function_call(failed_message_fn); + assert_int_equal(out->message(out, "fail"), pcmk_rc_no_output); + + pcmk__output_free(out); +} + +static void +wrong_format(void **state) { + pcmk__output_t *out = NULL; + + pcmk__message_entry_t entries[] = { + { "msg1", "xml", message_fn_1 }, + { NULL }, + }; + + pcmk__output_new(&out, "text", NULL, NULL); + pcmk__register_messages(out, entries); + + assert_int_equal(out->message(out, "msg1"), EINVAL); + + pcmk__output_free(out); +} + +static void +default_called(void **state) { + pcmk__output_t *out = NULL; + + pcmk__message_entry_t entries[] = { + { "msg1", "default", default_message_fn }, + { "msg1", "xml", message_fn_1 }, + { NULL }, + }; + + pcmk__output_new(&out, "text", NULL, NULL); + pcmk__register_messages(out, entries); + + expect_function_call(default_message_fn); + assert_int_equal(out->message(out, "msg1"), pcmk_rc_ok); + + pcmk__output_free(out); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test_setup_teardown(no_such_message, setup, teardown), + cmocka_unit_test_setup_teardown(message_return_value, setup, teardown), + cmocka_unit_test_setup_teardown(wrong_format, setup, teardown), + cmocka_unit_test_setup_teardown(default_called, setup, teardown)) diff --git a/lib/common/tests/output/pcmk__output_and_clear_error_test.c b/lib/common/tests/output/pcmk__output_and_clear_error_test.c new file mode 100644 index 0000000..f54ed8a --- /dev/null +++ b/lib/common/tests/output/pcmk__output_and_clear_error_test.c @@ -0,0 +1,82 @@ +/* + * Copyright 2022-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/output_internal.h> + +#include <glib.h> + +static bool +fake_text_init(pcmk__output_t *out) { + return true; +} + +static void +fake_text_free_priv(pcmk__output_t *out) { + /* This function intentionally left blank */ +} + +G_GNUC_PRINTF(2, 3) +static void +fake_text_err(pcmk__output_t *out, const char *format, ...) { + function_called(); +} + +static pcmk__output_t * +mk_fake_text_output(char **argv) { + pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t)); + + if (retval == NULL) { + return NULL; + } + + retval->fmt_name = "text"; + retval->init = fake_text_init; + retval->free_priv = fake_text_free_priv; + + retval->register_message = pcmk__register_message; + retval->message = pcmk__call_message; + + retval->err = fake_text_err; + + return retval; +} + +static int +setup(void **state) { + pcmk__register_format(NULL, "text", mk_fake_text_output, NULL); + return 0; +} + +static int +teardown(void **state) { + pcmk__unregister_formats(); + return 0; +} + +static void +standard_usage(void **state) { + GError *error = NULL; + pcmk__output_t *out = NULL; + + pcmk__output_new(&out, "text", NULL, NULL); + g_set_error(&error, PCMK__RC_ERROR, pcmk_rc_bad_nvpair, + "some error message"); + + expect_function_call(fake_text_err); + pcmk__output_and_clear_error(&error, out); + + pcmk__output_free(out); + assert_null(error); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test_setup_teardown(standard_usage, setup, teardown)) diff --git a/lib/common/tests/output/pcmk__output_free_test.c b/lib/common/tests/output/pcmk__output_free_test.c new file mode 100644 index 0000000..ef074d1 --- /dev/null +++ b/lib/common/tests/output/pcmk__output_free_test.c @@ -0,0 +1,84 @@ +/* + * 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 <crm/common/output_internal.h> + +static int +null_message_fn(pcmk__output_t *out, va_list args) { + return pcmk_rc_ok; +} + +static bool +fake_text_init(pcmk__output_t *out) { + return true; +} + +static void +fake_text_free_priv(pcmk__output_t *out) { + function_called(); + /* This function intentionally left blank */ +} + +static pcmk__output_t * +mk_fake_text_output(char **argv) { + pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t)); + + if (retval == NULL) { + return NULL; + } + + retval->fmt_name = "text"; + retval->init = fake_text_init; + retval->free_priv = fake_text_free_priv; + + retval->register_message = pcmk__register_message; + retval->message = pcmk__call_message; + + return retval; +} + +static int +setup(void **state) { + pcmk__register_format(NULL, "text", mk_fake_text_output, NULL); + return 0; +} + +static int +teardown(void **state) { + pcmk__unregister_formats(); + return 0; +} + +static void +no_messages(void **state) { + pcmk__output_t *out = NULL; + + pcmk__output_new(&out, "text", NULL, NULL); + + expect_function_call(fake_text_free_priv); + pcmk__output_free(out); +} + +static void +messages(void **state) { + pcmk__output_t *out = NULL; + + pcmk__output_new(&out, "text", NULL, NULL); + pcmk__register_message(out, "fake", null_message_fn); + + expect_function_call(fake_text_free_priv); + pcmk__output_free(out); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test_setup_teardown(no_messages, setup, teardown), + cmocka_unit_test_setup_teardown(messages, setup, teardown)) diff --git a/lib/common/tests/output/pcmk__output_new_test.c b/lib/common/tests/output/pcmk__output_new_test.c new file mode 100644 index 0000000..de4268c --- /dev/null +++ b/lib/common/tests/output/pcmk__output_new_test.c @@ -0,0 +1,148 @@ +/* + * 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 <crm/common/output_internal.h> + +#include "mock_private.h" + +static bool init_succeeds = true; + +static bool +fake_text_init(pcmk__output_t *out) { + return init_succeeds; +} + +static void +fake_text_free_priv(pcmk__output_t *out) { + /* This function intentionally left blank */ +} + +/* "text" is the default for pcmk__output_new. */ +static pcmk__output_t * +mk_fake_text_output(char **argv) { + pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t)); + + if (retval == NULL) { + return NULL; + } + + retval->fmt_name = "text"; + retval->init = fake_text_init; + retval->free_priv = fake_text_free_priv; + + retval->register_message = pcmk__register_message; + retval->message = pcmk__call_message; + + return retval; +} + +static int +setup(void **state) { + pcmk__register_format(NULL, "text", mk_fake_text_output, NULL); + return 0; +} + +static int +teardown(void **state) { + pcmk__unregister_formats(); + return 0; +} + +static void +empty_formatters(void **state) { + pcmk__output_t *out = NULL; + + pcmk__assert_asserts(pcmk__output_new(&out, "fake", NULL, NULL)); +} + +static void +invalid_params(void **state) { + /* This must be called with the setup/teardown functions so formatters is not NULL. */ + pcmk__assert_asserts(pcmk__output_new(NULL, "fake", NULL, NULL)); +} + +static void +no_such_format(void **state) { + pcmk__output_t *out = NULL; + + assert_int_equal(pcmk__output_new(&out, "fake", NULL, NULL), pcmk_rc_unknown_format); +} + +static void +create_fails(void **state) { + pcmk__output_t *out = NULL; + + pcmk__mock_calloc = true; // calloc() will return NULL + + expect_value(__wrap_calloc, nmemb, 1); + expect_value(__wrap_calloc, size, sizeof(pcmk__output_t)); + assert_int_equal(pcmk__output_new(&out, "text", NULL, NULL), ENOMEM); + + pcmk__mock_calloc = false; // Use real calloc() +} + +static void +fopen_fails(void **state) { + pcmk__output_t *out = NULL; + + pcmk__mock_fopen = true; + expect_string(__wrap_fopen, pathname, "destfile"); + expect_string(__wrap_fopen, mode, "w"); + will_return(__wrap_fopen, EPERM); + + assert_int_equal(pcmk__output_new(&out, "text", "destfile", NULL), EPERM); + + pcmk__mock_fopen = false; +} + +static void +init_fails(void **state) { + pcmk__output_t *out = NULL; + + init_succeeds = false; + assert_int_equal(pcmk__output_new(&out, "text", NULL, NULL), ENOMEM); + init_succeeds = true; +} + +static void +everything_succeeds(void **state) { + pcmk__output_t *out = NULL; + + assert_int_equal(pcmk__output_new(&out, "text", NULL, NULL), pcmk_rc_ok); + assert_string_equal(out->fmt_name, "text"); + assert_ptr_equal(out->dest, stdout); + assert_false(out->quiet); + assert_non_null(out->messages); + assert_string_equal(getenv("OCF_OUTPUT_FORMAT"), "text"); + + pcmk__output_free(out); +} + +static void +no_fmt_name_given(void **state) { + pcmk__output_t *out = NULL; + + assert_int_equal(pcmk__output_new(&out, NULL, NULL, NULL), pcmk_rc_ok); + assert_string_equal(out->fmt_name, "text"); + + pcmk__output_free(out); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_formatters), + cmocka_unit_test_setup_teardown(invalid_params, setup, teardown), + cmocka_unit_test_setup_teardown(no_such_format, setup, teardown), + cmocka_unit_test_setup_teardown(create_fails, setup, teardown), + cmocka_unit_test_setup_teardown(init_fails, setup, teardown), + cmocka_unit_test_setup_teardown(fopen_fails, setup, teardown), + cmocka_unit_test_setup_teardown(everything_succeeds, setup, teardown), + cmocka_unit_test_setup_teardown(no_fmt_name_given, setup, teardown)) diff --git a/lib/common/tests/output/pcmk__register_format_test.c b/lib/common/tests/output/pcmk__register_format_test.c new file mode 100644 index 0000000..bcbde48 --- /dev/null +++ b/lib/common/tests/output/pcmk__register_format_test.c @@ -0,0 +1,63 @@ +/* + * 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 <crm/common/output_internal.h> + +static pcmk__output_t * +null_create_fn(char **argv) { + return NULL; +} + +static pcmk__output_t * +null_create_fn_2(char **argv) { + return NULL; +} + +static void +invalid_params(void **state) { + pcmk__assert_asserts(pcmk__register_format(NULL, "fake", NULL, NULL)); + pcmk__assert_asserts(pcmk__register_format(NULL, "", null_create_fn, NULL)); + pcmk__assert_asserts(pcmk__register_format(NULL, NULL, null_create_fn, NULL)); +} + +static void +add_format(void **state) { + GHashTable *formatters = NULL; + gpointer value; + + /* For starters, there should be no formatters defined. */ + assert_null(pcmk__output_formatters()); + + /* Add a fake formatter and check that it's the only item in the hash table. */ + assert_int_equal(pcmk__register_format(NULL, "fake", null_create_fn, NULL), pcmk_rc_ok); + formatters = pcmk__output_formatters(); + assert_int_equal(g_hash_table_size(formatters), 1); + + value = g_hash_table_lookup(formatters, "fake"); + assert_ptr_equal(value, null_create_fn); + + /* Add a second fake formatter which should overwrite the first one, leaving + * only one item in the hash table but pointing at the new function. + */ + assert_int_equal(pcmk__register_format(NULL, "fake", null_create_fn_2, NULL), pcmk_rc_ok); + formatters = pcmk__output_formatters(); + assert_int_equal(g_hash_table_size(formatters), 1); + + value = g_hash_table_lookup(formatters, "fake"); + assert_ptr_equal(value, null_create_fn_2); + + pcmk__unregister_formats(); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(invalid_params), + cmocka_unit_test(add_format)) diff --git a/lib/common/tests/output/pcmk__register_formats_test.c b/lib/common/tests/output/pcmk__register_formats_test.c new file mode 100644 index 0000000..4be2d78 --- /dev/null +++ b/lib/common/tests/output/pcmk__register_formats_test.c @@ -0,0 +1,108 @@ +/* + * 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 <crm/common/output_internal.h> + +static pcmk__output_t * +null_create_fn(char **argv) { + return NULL; +} + +static pcmk__output_t * +null_create_fn_2(char **argv) { + return NULL; +} + +static void +no_formats(void **state) { + pcmk__register_formats(NULL, NULL); + assert_null(pcmk__output_formatters()); +} + +static void +invalid_entries(void **state) { + /* Here, we can only test that an empty name won't be added. A NULL name is + * the marker for the end of the format table. + */ + pcmk__supported_format_t formats[] = { + { "", null_create_fn, NULL }, + { NULL }, + }; + + pcmk__assert_asserts(pcmk__register_formats(NULL, formats)); +} + +static void +valid_entries(void **state) { + GHashTable *formatters = NULL; + + pcmk__supported_format_t formats[] = { + { "fmt1", null_create_fn, NULL }, + { "fmt2", null_create_fn_2, NULL }, + { NULL }, + }; + + pcmk__register_formats(NULL, formats); + + formatters = pcmk__output_formatters(); + assert_int_equal(g_hash_table_size(formatters), 2); + assert_ptr_equal(g_hash_table_lookup(formatters, "fmt1"), null_create_fn); + assert_ptr_equal(g_hash_table_lookup(formatters, "fmt2"), null_create_fn_2); + + pcmk__unregister_formats(); +} + +static void +duplicate_keys(void **state) { + GHashTable *formatters = NULL; + + pcmk__supported_format_t formats[] = { + { "fmt1", null_create_fn, NULL }, + { "fmt1", null_create_fn_2, NULL }, + { NULL }, + }; + + pcmk__register_formats(NULL, formats); + + formatters = pcmk__output_formatters(); + assert_int_equal(g_hash_table_size(formatters), 1); + assert_ptr_equal(g_hash_table_lookup(formatters, "fmt1"), null_create_fn_2); + + pcmk__unregister_formats(); +} + +static void +duplicate_values(void **state) { + GHashTable *formatters = NULL; + + pcmk__supported_format_t formats[] = { + { "fmt1", null_create_fn, NULL }, + { "fmt2", null_create_fn, NULL }, + { NULL }, + }; + + pcmk__register_formats(NULL, formats); + + formatters = pcmk__output_formatters(); + assert_int_equal(g_hash_table_size(formatters), 2); + assert_ptr_equal(g_hash_table_lookup(formatters, "fmt1"), null_create_fn); + assert_ptr_equal(g_hash_table_lookup(formatters, "fmt2"), null_create_fn); + + pcmk__unregister_formats(); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(no_formats), + cmocka_unit_test(invalid_entries), + cmocka_unit_test(valid_entries), + cmocka_unit_test(duplicate_keys), + cmocka_unit_test(duplicate_values)) diff --git a/lib/common/tests/output/pcmk__register_message_test.c b/lib/common/tests/output/pcmk__register_message_test.c new file mode 100644 index 0000000..4b4a282 --- /dev/null +++ b/lib/common/tests/output/pcmk__register_message_test.c @@ -0,0 +1,107 @@ +/* + * Copyright 2022-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/output_internal.h> + +#include "../../crmcommon_private.h" + +static int +null_message_fn(pcmk__output_t *out, va_list args) { + return pcmk_rc_ok; +} + +static int +null_message_fn_2(pcmk__output_t *out, va_list args) { + return pcmk_rc_ok; +} + +static bool +fake_text_init(pcmk__output_t *out) { + return true; +} + +static void +fake_text_free_priv(pcmk__output_t *out) { + /* This function intentionally left blank */ +} + +static pcmk__output_t * +mk_fake_text_output(char **argv) { + pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t)); + + if (retval == NULL) { + return NULL; + } + + retval->fmt_name = "text"; + retval->init = fake_text_init; + retval->free_priv = fake_text_free_priv; + + retval->register_message = pcmk__register_message; + retval->message = pcmk__call_message; + + return retval; +} + +static int +setup(void **state) { + pcmk__register_format(NULL, "text", mk_fake_text_output, NULL); + return 0; +} + +static int +teardown(void **state) { + pcmk__unregister_formats(); + return 0; +} + +static void +null_params(void **state) { + pcmk__output_t *out = NULL; + + pcmk__output_new(&out, "text", NULL, NULL); + + pcmk__assert_asserts(pcmk__register_message(NULL, "fake", null_message_fn)); + pcmk__assert_asserts(pcmk__register_message(out, NULL, null_message_fn)); + pcmk__assert_asserts(pcmk__register_message(out, "", null_message_fn)); + pcmk__assert_asserts(pcmk__register_message(out, "fake", NULL)); + + pcmk__output_free(out); +} + +static void +add_message(void **state) { + pcmk__output_t *out = NULL; + + pcmk__bare_output_new(&out, "text", NULL, NULL); + + /* For starters, there should be no messages defined. */ + assert_int_equal(g_hash_table_size(out->messages), 0); + + /* Add a fake function and check that it's the only item in the hash table. */ + pcmk__register_message(out, "fake", null_message_fn); + assert_int_equal(g_hash_table_size(out->messages), 1); + assert_ptr_equal(g_hash_table_lookup(out->messages, "fake"), null_message_fn); + + /* Add a second fake function which should overwrite the first one, leaving + * only one item in the hash table but pointing at the new function. + */ + pcmk__register_message(out, "fake", null_message_fn_2); + assert_int_equal(g_hash_table_size(out->messages), 1); + assert_ptr_equal(g_hash_table_lookup(out->messages, "fake"), null_message_fn_2); + + pcmk__output_free(out); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test_setup_teardown(null_params, setup, teardown), + cmocka_unit_test_setup_teardown(add_message, setup, teardown)) diff --git a/lib/common/tests/output/pcmk__register_messages_test.c b/lib/common/tests/output/pcmk__register_messages_test.c new file mode 100644 index 0000000..3fdd759 --- /dev/null +++ b/lib/common/tests/output/pcmk__register_messages_test.c @@ -0,0 +1,191 @@ +/* + * Copyright 2022-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/output_internal.h> + +#include "../../crmcommon_private.h" + +static int +null_message_fn(pcmk__output_t *out, va_list args) { + return pcmk_rc_ok; +} + +static int +null_message_fn_2(pcmk__output_t *out, va_list args) { + return pcmk_rc_ok; +} + +static bool +fake_text_init(pcmk__output_t *out) { + return true; +} + +static void +fake_text_free_priv(pcmk__output_t *out) { + /* This function intentionally left blank */ +} + +static pcmk__output_t * +mk_fake_text_output(char **argv) { + pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t)); + + if (retval == NULL) { + return NULL; + } + + retval->fmt_name = "text"; + retval->init = fake_text_init; + retval->free_priv = fake_text_free_priv; + + retval->register_message = pcmk__register_message; + retval->message = pcmk__call_message; + + return retval; +} + +static int +setup(void **state) { + pcmk__register_format(NULL, "text", mk_fake_text_output, NULL); + return 0; +} + +static int +teardown(void **state) { + pcmk__unregister_formats(); + return 0; +} + +static void +invalid_entries(void **state) { + pcmk__output_t *out = NULL; + + pcmk__message_entry_t entries[] = { + /* We can't test a NULL message_id here because that's the marker for + * the end of the table. + */ + { "", "", null_message_fn }, + { "", NULL, null_message_fn }, + { "", "text", NULL }, + { NULL }, + }; + + pcmk__bare_output_new(&out, "text", NULL, NULL); + + pcmk__assert_asserts(pcmk__register_messages(out, entries)); + assert_int_equal(g_hash_table_size(out->messages), 0); + + pcmk__output_free(out); +} + +static void +valid_entries(void **state) { + pcmk__output_t *out = NULL; + + pcmk__message_entry_t entries[] = { + { "msg1", "text", null_message_fn }, + { "msg2", "text", null_message_fn_2 }, + { NULL }, + }; + + pcmk__bare_output_new(&out, "text", NULL, NULL); + + pcmk__register_messages(out, entries); + assert_int_equal(g_hash_table_size(out->messages), 2); + assert_ptr_equal(g_hash_table_lookup(out->messages, "msg1"), null_message_fn); + assert_ptr_equal(g_hash_table_lookup(out->messages, "msg2"), null_message_fn_2); + + pcmk__output_free(out); +} + +static void +duplicate_message_ids(void **state) { + pcmk__output_t *out = NULL; + + pcmk__message_entry_t entries[] = { + { "msg1", "text", null_message_fn }, + { "msg1", "text", null_message_fn_2 }, + { NULL }, + }; + + pcmk__bare_output_new(&out, "text", NULL, NULL); + + pcmk__register_messages(out, entries); + assert_int_equal(g_hash_table_size(out->messages), 1); + assert_ptr_equal(g_hash_table_lookup(out->messages, "msg1"), null_message_fn_2); + + pcmk__output_free(out); +} + +static void +duplicate_functions(void **state) { + pcmk__output_t *out = NULL; + + pcmk__message_entry_t entries[] = { + { "msg1", "text", null_message_fn }, + { "msg2", "text", null_message_fn }, + { NULL }, + }; + + pcmk__bare_output_new(&out, "text", NULL, NULL); + + pcmk__register_messages(out, entries); + assert_int_equal(g_hash_table_size(out->messages), 2); + assert_ptr_equal(g_hash_table_lookup(out->messages, "msg1"), null_message_fn); + assert_ptr_equal(g_hash_table_lookup(out->messages, "msg2"), null_message_fn); + + pcmk__output_free(out); +} + +static void +default_handler(void **state) { + pcmk__output_t *out = NULL; + + pcmk__message_entry_t entries[] = { + { "msg1", "default", null_message_fn }, + { NULL }, + }; + + pcmk__bare_output_new(&out, "text", NULL, NULL); + + pcmk__register_messages(out, entries); + assert_int_equal(g_hash_table_size(out->messages), 1); + assert_ptr_equal(g_hash_table_lookup(out->messages, "msg1"), null_message_fn); + + pcmk__output_free(out); +} + +static void +override_default_handler(void **state) { + pcmk__output_t *out = NULL; + + pcmk__message_entry_t entries[] = { + { "msg1", "default", null_message_fn }, + { "msg1", "text", null_message_fn_2 }, + { NULL }, + }; + + pcmk__bare_output_new(&out, "text", NULL, NULL); + + pcmk__register_messages(out, entries); + assert_int_equal(g_hash_table_size(out->messages), 1); + assert_ptr_equal(g_hash_table_lookup(out->messages, "msg1"), null_message_fn_2); + + pcmk__output_free(out); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test_setup_teardown(invalid_entries, setup, teardown), + cmocka_unit_test_setup_teardown(valid_entries, setup, teardown), + cmocka_unit_test_setup_teardown(duplicate_message_ids, setup, teardown), + cmocka_unit_test_setup_teardown(duplicate_functions, setup, teardown), + cmocka_unit_test_setup_teardown(default_handler, setup, teardown), + cmocka_unit_test_setup_teardown(override_default_handler, setup, teardown)) diff --git a/lib/common/tests/output/pcmk__unregister_formats_test.c b/lib/common/tests/output/pcmk__unregister_formats_test.c new file mode 100644 index 0000000..0631c95 --- /dev/null +++ b/lib/common/tests/output/pcmk__unregister_formats_test.c @@ -0,0 +1,39 @@ +/* + * 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 <crm/common/output_internal.h> + +static pcmk__output_t * +null_create_fn(char **argv) { + return NULL; +} + +static void +invalid_params(void **state) { + /* This is basically just here to make sure that calling pcmk__unregister_formats + * with formatters=NULL doesn't segfault. + */ + pcmk__unregister_formats(); + assert_null(pcmk__output_formatters()); +} + +static void +non_null_formatters(void **state) { + pcmk__register_format(NULL, "fake", null_create_fn, NULL); + + pcmk__unregister_formats(); + assert_null(pcmk__output_formatters()); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(invalid_params), + cmocka_unit_test(non_null_formatters)) diff --git a/lib/common/tests/procfs/Makefile.am b/lib/common/tests/procfs/Makefile.am new file mode 100644 index 0000000..75511f5 --- /dev/null +++ b/lib/common/tests/procfs/Makefile.am @@ -0,0 +1,18 @@ +# +# 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 $(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__procfs_has_pids_false_test \ + pcmk__procfs_has_pids_true_test \ + pcmk__procfs_pid2path_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/procfs/pcmk__procfs_has_pids_false_test.c b/lib/common/tests/procfs/pcmk__procfs_has_pids_false_test.c new file mode 100644 index 0000000..4601aac --- /dev/null +++ b/lib/common/tests/procfs/pcmk__procfs_has_pids_false_test.c @@ -0,0 +1,42 @@ +/* + * 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 "mock_private.h" + +#include <unistd.h> +#include <string.h> +#include <errno.h> + + +static void +no_pids(void **state) +{ + char path[PATH_MAX]; + + snprintf(path, PATH_MAX, "/proc/%u/exe", getpid()); + + // Set readlink() errno and link contents (for /proc/PID/exe) + pcmk__mock_readlink = true; + + expect_string(__wrap_readlink, path, path); + expect_any(__wrap_readlink, buf); + expect_value(__wrap_readlink, bufsize, PATH_MAX - 1); + will_return(__wrap_readlink, ENOENT); + will_return(__wrap_readlink, NULL); + + assert_false(pcmk__procfs_has_pids()); + + pcmk__mock_readlink = false; +} + +PCMK__UNIT_TEST(NULL, NULL, cmocka_unit_test(no_pids)) diff --git a/lib/common/tests/procfs/pcmk__procfs_has_pids_true_test.c b/lib/common/tests/procfs/pcmk__procfs_has_pids_true_test.c new file mode 100644 index 0000000..758e3b9 --- /dev/null +++ b/lib/common/tests/procfs/pcmk__procfs_has_pids_true_test.c @@ -0,0 +1,41 @@ +/* + * 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 "mock_private.h" + +#include <unistd.h> +#include <string.h> +#include <errno.h> + +static void +has_pids(void **state) +{ + char path[PATH_MAX]; + + snprintf(path, PATH_MAX, "/proc/%u/exe", getpid()); + + // Set readlink() errno and link contents (for /proc/PID/exe) + pcmk__mock_readlink = true; + + expect_string(__wrap_readlink, path, path); + expect_any(__wrap_readlink, buf); + expect_value(__wrap_readlink, bufsize, PATH_MAX - 1); + will_return(__wrap_readlink, 0); + will_return(__wrap_readlink, "/ok"); + + assert_true(pcmk__procfs_has_pids()); + + pcmk__mock_readlink = false; +} + +PCMK__UNIT_TEST(NULL, NULL, cmocka_unit_test(has_pids)) diff --git a/lib/common/tests/procfs/pcmk__procfs_pid2path_test.c b/lib/common/tests/procfs/pcmk__procfs_pid2path_test.c new file mode 100644 index 0000000..2bae541 --- /dev/null +++ b/lib/common/tests/procfs/pcmk__procfs_pid2path_test.c @@ -0,0 +1,92 @@ +/* + * 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 "mock_private.h" + +#include <unistd.h> +#include <string.h> +#include <errno.h> + +static void +no_exe_file(void **state) +{ + size_t len = PATH_MAX; + char *path = calloc(len, sizeof(char)); + + // Set readlink() errno and link contents + pcmk__mock_readlink = true; + + expect_string(__wrap_readlink, path, "/proc/1000/exe"); + expect_value(__wrap_readlink, buf, path); + expect_value(__wrap_readlink, bufsize, len - 1); + will_return(__wrap_readlink, ENOENT); + will_return(__wrap_readlink, NULL); + + assert_int_equal(pcmk__procfs_pid2path(1000, path, len), ENOENT); + + pcmk__mock_readlink = false; + + free(path); +} + +static void +contents_too_long(void **state) +{ + size_t len = 10; + char *path = calloc(len, sizeof(char)); + + // Set readlink() errno and link contents + pcmk__mock_readlink = true; + + expect_string(__wrap_readlink, path, "/proc/1000/exe"); + expect_value(__wrap_readlink, buf, path); + expect_value(__wrap_readlink, bufsize, len - 1); + will_return(__wrap_readlink, 0); + will_return(__wrap_readlink, "/more/than/10/characters"); + + assert_int_equal(pcmk__procfs_pid2path(1000, path, len), + ENAMETOOLONG); + + pcmk__mock_readlink = false; + + free(path); +} + +static void +contents_ok(void **state) +{ + size_t len = PATH_MAX; + char *path = calloc(len, sizeof(char)); + + // Set readlink() errno and link contents + pcmk__mock_readlink = true; + + expect_string(__wrap_readlink, path, "/proc/1000/exe"); + expect_value(__wrap_readlink, buf, path); + expect_value(__wrap_readlink, bufsize, len - 1); + will_return(__wrap_readlink, 0); + will_return(__wrap_readlink, "/ok"); + + assert_int_equal(pcmk__procfs_pid2path((pid_t) 1000, path, len), + pcmk_rc_ok); + assert_string_equal(path, "/ok"); + + pcmk__mock_readlink = false; + + free(path); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(no_exe_file), + cmocka_unit_test(contents_too_long), + cmocka_unit_test(contents_ok)) diff --git a/lib/common/tests/results/Makefile.am b/lib/common/tests/results/Makefile.am new file mode 100644 index 0000000..8d51d12 --- /dev/null +++ b/lib/common/tests/results/Makefile.am @@ -0,0 +1,16 @@ +# +# Copyright 2021-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 $(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__results_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/results/pcmk__results_test.c b/lib/common/tests/results/pcmk__results_test.c new file mode 100644 index 0000000..53665d1 --- /dev/null +++ b/lib/common/tests/results/pcmk__results_test.c @@ -0,0 +1,61 @@ +/* + * 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> + +#include <glib.h> +#include <bzlib.h> + +static void +test_for_pcmk_rc_name(void **state) { + assert_string_equal(pcmk_rc_name(pcmk_rc_error-1), "pcmk_rc_unknown_format"); + assert_string_equal(pcmk_rc_name(pcmk_rc_ok), "pcmk_rc_ok"); + assert_string_equal(pcmk_rc_name(pcmk_rc_ok), "pcmk_rc_ok"); + assert_string_equal(pcmk_rc_name(-7777777), "Unknown"); +} + +static void +test_for_pcmk_rc_str(void **state) { + assert_string_equal(pcmk_rc_str(pcmk_rc_error-1), "Unknown output format"); + assert_string_equal(pcmk_rc_str(pcmk_rc_ok), "OK"); + assert_string_equal(pcmk_rc_str(-1), "Error"); +} + +static void +test_for_crm_exit_name(void **state) { + assert_string_equal(crm_exit_name(CRM_EX_OK), "CRM_EX_OK"); +} + +static void +test_for_crm_exit_str(void **state) { + assert_string_equal(crm_exit_str(CRM_EX_OK), "OK"); + assert_string_equal(crm_exit_str(129), "Interrupted by signal"); + assert_string_equal(crm_exit_str(-7777777), "Unknown exit status"); +} + +static void +test_for_pcmk_rc2exitc(void **state) { + assert_int_equal(pcmk_rc2exitc(pcmk_rc_ok), CRM_EX_OK); + assert_int_equal(pcmk_rc2exitc(-7777777), CRM_EX_ERROR); +} + +static void +test_for_bz2_strerror(void **state) { + assert_string_equal(bz2_strerror(BZ_STREAM_END), "Ok"); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(test_for_pcmk_rc_name), + cmocka_unit_test(test_for_pcmk_rc_str), + cmocka_unit_test(test_for_crm_exit_name), + cmocka_unit_test(test_for_crm_exit_str), + cmocka_unit_test(test_for_pcmk_rc2exitc), + cmocka_unit_test(test_for_bz2_strerror)) diff --git a/lib/common/tests/scores/Makefile.am b/lib/common/tests/scores/Makefile.am new file mode 100644 index 0000000..66ca073 --- /dev/null +++ b/lib/common/tests/scores/Makefile.am @@ -0,0 +1,19 @@ +# +# Copyright 2020-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 $(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 = \ + char2score_test \ + pcmk__add_scores_test \ + pcmk_readable_score_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/scores/char2score_test.c b/lib/common/tests/scores/char2score_test.c new file mode 100644 index 0000000..fbba12a --- /dev/null +++ b/lib/common/tests/scores/char2score_test.c @@ -0,0 +1,75 @@ +/* + * 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> + +extern int pcmk__score_red; +extern int pcmk__score_green; +extern int pcmk__score_yellow; + +static void +empty_input(void **state) +{ + assert_int_equal(char2score(NULL), 0); +} + +static void +bad_input(void **state) +{ + assert_int_equal(char2score("PQRST"), 0); + assert_int_equal(char2score("3.141592"), 3); + assert_int_equal(char2score("0xf00d"), 0); +} + +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); + + pcmk__score_red = 10; + pcmk__score_green = 20; + pcmk__score_yellow = 30; + + assert_int_equal(char2score("red"), pcmk__score_red); + assert_int_equal(char2score("green"), pcmk__score_green); + assert_int_equal(char2score("yellow"), pcmk__score_yellow); + + assert_int_equal(char2score("ReD"), pcmk__score_red); + assert_int_equal(char2score("GrEeN"), pcmk__score_green); + assert_int_equal(char2score("yElLoW"), pcmk__score_yellow); +} + +/* These ridiculous macros turn an integer constant into a string constant. */ +#define A(x) #x +#define B(x) A(x) + +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); +} + +static void +inside_limits(void **state) +{ + assert_int_equal(char2score("1234"), 1234); + assert_int_equal(char2score("-1234"), -1234); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_input), + cmocka_unit_test(bad_input), + cmocka_unit_test(special_values), + cmocka_unit_test(outside_limits), + cmocka_unit_test(inside_limits)) diff --git a/lib/common/tests/scores/pcmk__add_scores_test.c b/lib/common/tests/scores/pcmk__add_scores_test.c new file mode 100644 index 0000000..85ac232 --- /dev/null +++ b/lib/common/tests/scores/pcmk__add_scores_test.c @@ -0,0 +1,74 @@ +/* + * 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> + +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); +} + +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); +} + +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); +} + +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); +} + +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(-4000000, 50), -CRM_SCORE_INFINITY); +} + +static void +result_finite(void **state) +{ + assert_int_equal(pcmk__add_scores(0, 0), 0); + assert_int_equal(pcmk__add_scores(0, 100), 100); + assert_int_equal(pcmk__add_scores(200, 0), 200); + assert_int_equal(pcmk__add_scores(200, -50), 150); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(score1_minus_inf), + cmocka_unit_test(score2_minus_inf), + cmocka_unit_test(score1_pos_inf), + cmocka_unit_test(score2_pos_inf), + cmocka_unit_test(result_infinite), + cmocka_unit_test(result_finite)) diff --git a/lib/common/tests/scores/pcmk_readable_score_test.c b/lib/common/tests/scores/pcmk_readable_score_test.c new file mode 100644 index 0000000..ae24159 --- /dev/null +++ b/lib/common/tests/scores/pcmk_readable_score_test.c @@ -0,0 +1,33 @@ +/* + * 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> + +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); +} + +static void +inside_limits(void **state) +{ + assert_string_equal(pcmk_readable_score(0), "0"); + assert_string_equal(pcmk_readable_score(1024), "1024"); + assert_string_equal(pcmk_readable_score(-1024), "-1024"); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(outside_limits), + cmocka_unit_test(inside_limits)) diff --git a/lib/common/tests/strings/Makefile.am b/lib/common/tests/strings/Makefile.am new file mode 100644 index 0000000..9abb8e9 --- /dev/null +++ b/lib/common/tests/strings/Makefile.am @@ -0,0 +1,41 @@ +# +# Copyright 2020-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 $(top_srcdir)/mk/tap.mk +include $(top_srcdir)/mk/unittest.mk + +# Add "_test" to the end of all test program names to simplify .gitignore. +check_PROGRAMS = \ + crm_get_msec_test \ + crm_is_true_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 \ + pcmk__guint_from_hash_test \ + pcmk__numeric_strcasecmp_test \ + pcmk__parse_ll_range_test \ + pcmk__s_test \ + pcmk__scan_double_test \ + pcmk__scan_min_int_test \ + pcmk__scan_port_test \ + pcmk__starts_with_test \ + pcmk__str_any_of_test \ + pcmk__str_in_list_test \ + pcmk__str_table_dup_test \ + pcmk__str_update_test \ + pcmk__strcmp_test \ + pcmk__strkey_table_test \ + pcmk__strikey_table_test \ + pcmk__trim_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/strings/crm_get_msec_test.c b/lib/common/tests/strings/crm_get_msec_test.c new file mode 100644 index 0000000..5da548b --- /dev/null +++ b/lib/common/tests/strings/crm_get_msec_test.c @@ -0,0 +1,50 @@ +/* + * 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 +bad_input(void **state) { + assert_int_equal(crm_get_msec(NULL), PCMK__PARSE_INT_DEFAULT); + assert_int_equal(crm_get_msec(" "), PCMK__PARSE_INT_DEFAULT); + assert_int_equal(crm_get_msec("abcxyz"), PCMK__PARSE_INT_DEFAULT); + 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); +} + +static void +good_input(void **state) { + assert_int_equal(crm_get_msec("100"), 100000); + assert_int_equal(crm_get_msec(" 100 "), 100000); + assert_int_equal(crm_get_msec("\t100\n"), 100000); + + assert_int_equal(crm_get_msec("100ms"), 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); + assert_int_equal(crm_get_msec("12s"), 12000); + assert_int_equal(crm_get_msec("12 sec"), 12000); + assert_int_equal(crm_get_msec("1m"), 60000); + 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); +} + +static void +overflow(void **state) { + assert_int_equal(crm_get_msec("9223372036854775807s"), LLONG_MAX); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(bad_input), + cmocka_unit_test(good_input), + cmocka_unit_test(overflow)) diff --git a/lib/common/tests/strings/crm_is_true_test.c b/lib/common/tests/strings/crm_is_true_test.c new file mode 100644 index 0000000..2a9e31c --- /dev/null +++ b/lib/common/tests/strings/crm_is_true_test.c @@ -0,0 +1,57 @@ +/* + * 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 +bad_input(void **state) { + assert_false(crm_is_true(NULL)); +} + +static void +is_true(void **state) { + assert_true(crm_is_true("true")); + assert_true(crm_is_true("TrUe")); + assert_true(crm_is_true("on")); + assert_true(crm_is_true("ON")); + assert_true(crm_is_true("yes")); + assert_true(crm_is_true("yES")); + assert_true(crm_is_true("y")); + assert_true(crm_is_true("Y")); + assert_true(crm_is_true("1")); +} + +static void +is_false(void **state) { + assert_false(crm_is_true("false")); + assert_false(crm_is_true("fAlSe")); + assert_false(crm_is_true("off")); + assert_false(crm_is_true("OFF")); + assert_false(crm_is_true("no")); + assert_false(crm_is_true("No")); + assert_false(crm_is_true("n")); + assert_false(crm_is_true("N")); + assert_false(crm_is_true("0")); + + assert_false(crm_is_true("")); + assert_false(crm_is_true("blahblah")); + + assert_false(crm_is_true("truedat")); + assert_false(crm_is_true("onnn")); + assert_false(crm_is_true("yep")); + assert_false(crm_is_true("Y!")); + assert_false(crm_is_true("100")); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(bad_input), + cmocka_unit_test(is_true), + cmocka_unit_test(is_false)) diff --git a/lib/common/tests/strings/crm_str_to_boolean_test.c b/lib/common/tests/strings/crm_str_to_boolean_test.c new file mode 100644 index 0000000..3bd2e5d --- /dev/null +++ b/lib/common/tests/strings/crm_str_to_boolean_test.c @@ -0,0 +1,92 @@ +/* + * 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 +bad_input(void **state) { + assert_int_equal(crm_str_to_boolean(NULL, NULL), -1); + assert_int_equal(crm_str_to_boolean("", NULL), -1); + assert_int_equal(crm_str_to_boolean("blahblah", NULL), -1); +} + +static void +is_true(void **state) { + int ret; + + assert_int_equal(crm_str_to_boolean("true", &ret), 1); + assert_true(ret); + assert_int_equal(crm_str_to_boolean("TrUe", &ret), 1); + assert_true(ret); + assert_int_equal(crm_str_to_boolean("on", &ret), 1); + assert_true(ret); + assert_int_equal(crm_str_to_boolean("ON", &ret), 1); + assert_true(ret); + assert_int_equal(crm_str_to_boolean("yes", &ret), 1); + assert_true(ret); + assert_int_equal(crm_str_to_boolean("yES", &ret), 1); + assert_true(ret); + assert_int_equal(crm_str_to_boolean("y", &ret), 1); + assert_true(ret); + assert_int_equal(crm_str_to_boolean("Y", &ret), 1); + assert_true(ret); + assert_int_equal(crm_str_to_boolean("1", &ret), 1); + assert_true(ret); +} + +static void +is_not_true(void **state) { + assert_int_equal(crm_str_to_boolean("truedat", NULL), -1); + assert_int_equal(crm_str_to_boolean("onnn", NULL), -1); + assert_int_equal(crm_str_to_boolean("yep", NULL), -1); + assert_int_equal(crm_str_to_boolean("Y!", NULL), -1); + assert_int_equal(crm_str_to_boolean("100", NULL), -1); +} + +static void +is_false(void **state) { + int ret; + + assert_int_equal(crm_str_to_boolean("false", &ret), 1); + assert_false(ret); + assert_int_equal(crm_str_to_boolean("fAlSe", &ret), 1); + assert_false(ret); + assert_int_equal(crm_str_to_boolean("off", &ret), 1); + assert_false(ret); + assert_int_equal(crm_str_to_boolean("OFF", &ret), 1); + assert_false(ret); + assert_int_equal(crm_str_to_boolean("no", &ret), 1); + assert_false(ret); + assert_int_equal(crm_str_to_boolean("No", &ret), 1); + assert_false(ret); + assert_int_equal(crm_str_to_boolean("n", &ret), 1); + assert_false(ret); + assert_int_equal(crm_str_to_boolean("N", &ret), 1); + assert_false(ret); + assert_int_equal(crm_str_to_boolean("0", &ret), 1); + assert_false(ret); +} + +static void +is_not_false(void **state) { + assert_int_equal(crm_str_to_boolean("falseee", NULL), -1); + assert_int_equal(crm_str_to_boolean("of", NULL), -1); + assert_int_equal(crm_str_to_boolean("nope", NULL), -1); + assert_int_equal(crm_str_to_boolean("N!", NULL), -1); + assert_int_equal(crm_str_to_boolean("000", NULL), -1); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(bad_input), + cmocka_unit_test(is_true), + cmocka_unit_test(is_not_true), + cmocka_unit_test(is_false), + cmocka_unit_test(is_not_false)) diff --git a/lib/common/tests/strings/pcmk__add_word_test.c b/lib/common/tests/strings/pcmk__add_word_test.c new file mode 100644 index 0000000..16a749e --- /dev/null +++ b/lib/common/tests/strings/pcmk__add_word_test.c @@ -0,0 +1,93 @@ +/* + * Copyright 2020-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> + +static void +add_words(void **state) +{ + GString *list = NULL; + + pcmk__add_word(&list, 16, "hello"); + pcmk__add_word(&list, 16, "world"); + assert_int_equal(strcmp((const char *) list->str, "hello world"), 0); + g_string_free(list, TRUE); +} + +static void +add_with_no_len(void **state) +{ + GString *list = NULL; + + pcmk__add_word(&list, 0, "hello"); + pcmk__add_word(&list, 0, "world"); + assert_int_equal(strcmp((const char *) list->str, "hello world"), 0); + g_string_free(list, TRUE); +} + +static void +add_nothing(void **state) +{ + GString *list = NULL; + + pcmk__add_word(&list, 0, "hello"); + pcmk__add_word(&list, 0, NULL); + pcmk__add_word(&list, 0, ""); + assert_int_equal(strcmp((const char *) list->str, "hello"), 0); + g_string_free(list, TRUE); +} + +static void +add_with_null(void **state) +{ + GString *list = NULL; + + pcmk__add_separated_word(&list, 32, "hello", NULL); + pcmk__add_separated_word(&list, 32, "world", NULL); + pcmk__add_separated_word(&list, 32, "I am a unit test", NULL); + assert_int_equal(strcmp((const char *) list->str, + "hello world I am a unit test"), 0); + g_string_free(list, TRUE); +} + +static void +add_with_comma(void **state) +{ + GString *list = NULL; + + pcmk__add_separated_word(&list, 32, "hello", ","); + pcmk__add_separated_word(&list, 32, "world", ","); + pcmk__add_separated_word(&list, 32, "I am a unit test", ","); + assert_int_equal(strcmp((const char *) list->str, + "hello,world,I am a unit test"), 0); + g_string_free(list, TRUE); +} + +static void +add_with_comma_and_space(void **state) +{ + GString *list = NULL; + + pcmk__add_separated_word(&list, 32, "hello", ", "); + pcmk__add_separated_word(&list, 32, "world", ", "); + pcmk__add_separated_word(&list, 32, "I am a unit test", ", "); + assert_int_equal(strcmp((const char *) list->str, + "hello, world, I am a unit test"), 0); + g_string_free(list, TRUE); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(add_words), + cmocka_unit_test(add_with_no_len), + cmocka_unit_test(add_nothing), + cmocka_unit_test(add_with_null), + cmocka_unit_test(add_with_comma), + cmocka_unit_test(add_with_comma_and_space)) diff --git a/lib/common/tests/strings/pcmk__btoa_test.c b/lib/common/tests/strings/pcmk__btoa_test.c new file mode 100644 index 0000000..f7dee9e --- /dev/null +++ b/lib/common/tests/strings/pcmk__btoa_test.c @@ -0,0 +1,22 @@ +/* + * 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 +btoa(void **state) { + assert_string_equal(pcmk__btoa(false), "false"); + assert_string_equal(pcmk__btoa(true), "true"); + assert_string_equal(pcmk__btoa(1 == 0), "false"); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(btoa)) 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 new file mode 100644 index 0000000..e70dfb4 --- /dev/null +++ b/lib/common/tests/strings/pcmk__char_in_any_str_test.c @@ -0,0 +1,46 @@ +/* + * 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 new file mode 100644 index 0000000..7480937 --- /dev/null +++ b/lib/common/tests/strings/pcmk__compress_test.c @@ -0,0 +1,58 @@ +/* + * 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 "mock_private.h" + +#define SIMPLE_DATA "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + +const char *SIMPLE_COMPRESSED = "BZh41AY&SYO\x1ai"; + +static void +simple_compress(void **state) +{ + char *result = calloc(1024, sizeof(char)); + unsigned int len; + + assert_int_equal(pcmk__compress(SIMPLE_DATA, 40, 0, &result, &len), pcmk_rc_ok); + assert_memory_equal(result, SIMPLE_COMPRESSED, 13); +} + +static void +max_too_small(void **state) +{ + char *result = calloc(1024, sizeof(char)); + unsigned int len; + + assert_int_equal(pcmk__compress(SIMPLE_DATA, 40, 10, &result, &len), pcmk_rc_error); +} + +static void +calloc_fails(void **state) { + char *result = calloc(1024, sizeof(char)); + unsigned int len; + + pcmk__assert_asserts( + { + pcmk__mock_calloc = true; // calloc() will return NULL + expect_value(__wrap_calloc, nmemb, (size_t) ((40 * 1.01) + 601)); + expect_value(__wrap_calloc, size, sizeof(char)); + pcmk__compress(SIMPLE_DATA, 40, 0, &result, &len); + pcmk__mock_calloc = false; // Use the real calloc() + } + ); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(simple_compress), + cmocka_unit_test(max_too_small), + cmocka_unit_test(calloc_fails)) diff --git a/lib/common/tests/strings/pcmk__ends_with_test.c b/lib/common/tests/strings/pcmk__ends_with_test.c new file mode 100644 index 0000000..7503571 --- /dev/null +++ b/lib/common/tests/strings/pcmk__ends_with_test.c @@ -0,0 +1,57 @@ +/* + * 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 +bad_input(void **state) { + assert_false(pcmk__ends_with(NULL, "xyz")); + + assert_true(pcmk__ends_with(NULL, NULL)); + assert_true(pcmk__ends_with(NULL, "")); + assert_true(pcmk__ends_with("", NULL)); + assert_true(pcmk__ends_with("", "")); + assert_true(pcmk__ends_with("abc", NULL)); + assert_true(pcmk__ends_with("abc", "")); +} + +static void +ends_with(void **state) { + assert_true(pcmk__ends_with("abc", "abc")); + assert_true(pcmk__ends_with("abc", "bc")); + assert_true(pcmk__ends_with("abc", "c")); + assert_true(pcmk__ends_with("abcbc", "bc")); + + assert_false(pcmk__ends_with("abc", "def")); + assert_false(pcmk__ends_with("abc", "defg")); + assert_false(pcmk__ends_with("abc", "bcd")); + assert_false(pcmk__ends_with("abc", "ab")); + + assert_false(pcmk__ends_with("abc", "BC")); +} + +static void +ends_with_ext(void **state) { + assert_true(pcmk__ends_with_ext("ab.c", ".c")); + assert_true(pcmk__ends_with_ext("ab.cb.c", ".c")); + + assert_false(pcmk__ends_with_ext("ab.c", ".def")); + assert_false(pcmk__ends_with_ext("ab.c", ".defg")); + assert_false(pcmk__ends_with_ext("ab.c", ".cd")); + assert_false(pcmk__ends_with_ext("ab.c", "ab")); + + assert_false(pcmk__ends_with_ext("ab.c", ".C")); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(bad_input), + cmocka_unit_test(ends_with), + cmocka_unit_test(ends_with_ext)) diff --git a/lib/common/tests/strings/pcmk__g_strcat_test.c b/lib/common/tests/strings/pcmk__g_strcat_test.c new file mode 100644 index 0000000..2116f0e --- /dev/null +++ b/lib/common/tests/strings/pcmk__g_strcat_test.c @@ -0,0 +1,73 @@ +/* + * Copyright 2020-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> + +static void +add_to_null(void **state) +{ + pcmk__assert_asserts(pcmk__g_strcat(NULL, NULL)); + pcmk__assert_asserts(pcmk__g_strcat(NULL, "hello", NULL)); +} + +static void +add_nothing(void **state) +{ + GString *buf = g_string_new(NULL); + + // Start with empty string + pcmk__g_strcat(buf, NULL); + assert_string_equal((const char *) buf->str, ""); + + pcmk__g_strcat(buf, "", NULL); + assert_string_equal((const char *) buf->str, ""); + + // Start with populated string + g_string_append(buf, "hello"); + pcmk__g_strcat(buf, NULL); + assert_string_equal((const char *) buf->str, "hello"); + + pcmk__g_strcat(buf, "", NULL); + assert_string_equal((const char *) buf->str, "hello"); + g_string_free(buf, TRUE); +} + +static void +add_words(void **state) +{ + GString *buf = g_string_new(NULL); + + // Verify a call with multiple words + pcmk__g_strcat(buf, "hello", " ", NULL); + assert_string_equal((const char *) buf->str, "hello "); + + // Verify that a second call doesn't overwrite the first one + pcmk__g_strcat(buf, "world", NULL); + assert_string_equal((const char *) buf->str, "hello world"); + g_string_free(buf, TRUE); +} + +static void +stop_early(void **state) +{ + GString *buf = g_string_new(NULL); + + // NULL anywhere after buf in the arg list should cause a return + pcmk__g_strcat(buf, "hello", NULL, " world", NULL); + assert_string_equal((const char *) buf->str, "hello"); + g_string_free(buf, TRUE); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(add_to_null), + cmocka_unit_test(add_nothing), + cmocka_unit_test(add_words), + cmocka_unit_test(stop_early)) diff --git a/lib/common/tests/strings/pcmk__guint_from_hash_test.c b/lib/common/tests/strings/pcmk__guint_from_hash_test.c new file mode 100644 index 0000000..e2b4762 --- /dev/null +++ b/lib/common/tests/strings/pcmk__guint_from_hash_test.c @@ -0,0 +1,76 @@ +/* + * 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 +null_args(void **state) +{ + GHashTable *tbl = pcmk__strkey_table(free, free); + guint result; + + assert_int_equal(pcmk__guint_from_hash(NULL, "abc", 123, &result), EINVAL); + assert_int_equal(pcmk__guint_from_hash(tbl, NULL, 123, &result), EINVAL); + + g_hash_table_destroy(tbl); +} + +static void +missing_key(void **state) +{ + GHashTable *tbl = pcmk__strkey_table(free, free); + guint result; + + assert_int_equal(pcmk__guint_from_hash(tbl, "abc", 123, &result), pcmk_rc_ok); + assert_int_equal(result, 123); + + g_hash_table_destroy(tbl); +} + +static void +standard_usage(void **state) +{ + GHashTable *tbl = pcmk__strkey_table(free, free); + guint result; + + g_hash_table_insert(tbl, strdup("abc"), strdup("123")); + + assert_int_equal(pcmk__guint_from_hash(tbl, "abc", 456, &result), pcmk_rc_ok); + assert_int_equal(result, 123); + + g_hash_table_destroy(tbl); +} + +static void +conversion_errors(void **state) +{ + GHashTable *tbl = pcmk__strkey_table(free, free); + guint result; + + g_hash_table_insert(tbl, strdup("negative"), strdup("-3")); + g_hash_table_insert(tbl, strdup("toobig"), strdup("20000000000000000")); + + assert_int_equal(pcmk__guint_from_hash(tbl, "negative", 456, &result), ERANGE); + assert_int_equal(result, 456); + + assert_int_equal(pcmk__guint_from_hash(tbl, "toobig", 456, &result), ERANGE); + assert_int_equal(result, 456); + + g_hash_table_destroy(tbl); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(null_args), + cmocka_unit_test(missing_key), + cmocka_unit_test(standard_usage), + cmocka_unit_test(conversion_errors)) diff --git a/lib/common/tests/strings/pcmk__numeric_strcasecmp_test.c b/lib/common/tests/strings/pcmk__numeric_strcasecmp_test.c new file mode 100644 index 0000000..df7b11c --- /dev/null +++ b/lib/common/tests/strings/pcmk__numeric_strcasecmp_test.c @@ -0,0 +1,79 @@ +/* + * 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> + +static void +null_ptr(void **state) +{ + pcmk__assert_asserts(pcmk__numeric_strcasecmp(NULL, NULL)); + pcmk__assert_asserts(pcmk__numeric_strcasecmp("a", NULL)); + pcmk__assert_asserts(pcmk__numeric_strcasecmp(NULL, "a")); +} + +static void +no_numbers(void **state) +{ + /* All comparisons are done case-insensitively. */ + assert_int_equal(pcmk__numeric_strcasecmp("abcd", "efgh"), -1); + assert_int_equal(pcmk__numeric_strcasecmp("abcd", "abcd"), 0); + assert_int_equal(pcmk__numeric_strcasecmp("efgh", "abcd"), 1); + + assert_int_equal(pcmk__numeric_strcasecmp("AbCd", "eFgH"), -1); + assert_int_equal(pcmk__numeric_strcasecmp("ABCD", "abcd"), 0); + assert_int_equal(pcmk__numeric_strcasecmp("EFgh", "ABcd"), 1); +} + +static void +trailing_numbers(void **state) +{ + assert_int_equal(pcmk__numeric_strcasecmp("node1", "node2"), -1); + assert_int_equal(pcmk__numeric_strcasecmp("node1", "node1"), 0); + assert_int_equal(pcmk__numeric_strcasecmp("node2", "node1"), 1); + + assert_int_equal(pcmk__numeric_strcasecmp("node1", "node10"), -1); + assert_int_equal(pcmk__numeric_strcasecmp("node10", "node10"), 0); + assert_int_equal(pcmk__numeric_strcasecmp("node10", "node1"), 1); + + assert_int_equal(pcmk__numeric_strcasecmp("node10", "remotenode9"), -1); + assert_int_equal(pcmk__numeric_strcasecmp("remotenode9", "node10"), 1); + + /* Longer numbers sort higher than shorter numbers. */ + assert_int_equal(pcmk__numeric_strcasecmp("node001", "node1"), 1); + assert_int_equal(pcmk__numeric_strcasecmp("node1", "node001"), -1); +} + +static void +middle_numbers(void **state) +{ + assert_int_equal(pcmk__numeric_strcasecmp("node1abc", "node1def"), -1); + assert_int_equal(pcmk__numeric_strcasecmp("node1def", "node1abc"), 1); + + assert_int_equal(pcmk__numeric_strcasecmp("node1abc", "node2abc"), -1); + assert_int_equal(pcmk__numeric_strcasecmp("node2abc", "node1abc"), 1); +} + +static void +unequal_lengths(void **state) +{ + assert_int_equal(pcmk__numeric_strcasecmp("node-ab", "node-abc"), -1); + assert_int_equal(pcmk__numeric_strcasecmp("node-abc", "node-ab"), 1); + + assert_int_equal(pcmk__numeric_strcasecmp("node1ab", "node1abc"), -1); + assert_int_equal(pcmk__numeric_strcasecmp("node1abc", "node1ab"), 1); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(null_ptr), + cmocka_unit_test(no_numbers), + cmocka_unit_test(trailing_numbers), + cmocka_unit_test(middle_numbers), + cmocka_unit_test(unequal_lengths)) diff --git a/lib/common/tests/strings/pcmk__parse_ll_range_test.c b/lib/common/tests/strings/pcmk__parse_ll_range_test.c new file mode 100644 index 0000000..7656ad7 --- /dev/null +++ b/lib/common/tests/strings/pcmk__parse_ll_range_test.c @@ -0,0 +1,117 @@ +/* + * Copyright 2020-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/common/results.h" +#include <crm_internal.h> + +#include <crm/common/unittest_internal.h> + +static void +empty_input_string(void **state) +{ + long long start, end; + + assert_int_equal(pcmk__parse_ll_range(NULL, &start, &end), ENODATA); + assert_int_equal(pcmk__parse_ll_range("", &start, &end), ENODATA); +} + +static void +null_input_variables(void **state) +{ + long long start, end; + + pcmk__assert_asserts(pcmk__parse_ll_range("1234", NULL, &end)); + pcmk__assert_asserts(pcmk__parse_ll_range("1234", &start, NULL)); +} + +static void +missing_separator(void **state) +{ + long long start, end; + + assert_int_equal(pcmk__parse_ll_range("1234", &start, &end), pcmk_rc_ok); + assert_int_equal(start, 1234); + assert_int_equal(end, 1234); +} + +static void +only_separator(void **state) +{ + long long start, end; + + assert_int_equal(pcmk__parse_ll_range("-", &start, &end), pcmk_rc_bad_input); + assert_int_equal(start, PCMK__PARSE_INT_DEFAULT); + assert_int_equal(end, PCMK__PARSE_INT_DEFAULT); +} + +static void +no_range_end(void **state) +{ + long long start, end; + + assert_int_equal(pcmk__parse_ll_range("2000-", &start, &end), pcmk_rc_ok); + assert_int_equal(start, 2000); + assert_int_equal(end, PCMK__PARSE_INT_DEFAULT); +} + +static void +no_range_start(void **state) +{ + long long start, end; + + assert_int_equal(pcmk__parse_ll_range("-2020", &start, &end), pcmk_rc_ok); + assert_int_equal(start, PCMK__PARSE_INT_DEFAULT); + assert_int_equal(end, 2020); +} + +static void +range_start_and_end(void **state) +{ + long long start, end; + + assert_int_equal(pcmk__parse_ll_range("2000-2020", &start, &end), pcmk_rc_ok); + assert_int_equal(start, 2000); + assert_int_equal(end, 2020); + + assert_int_equal(pcmk__parse_ll_range("2000-2020-2030", &start, &end), pcmk_rc_bad_input); +} + +static void +garbage(void **state) +{ + long long start, end; + + assert_int_equal(pcmk__parse_ll_range("2000x-", &start, &end), pcmk_rc_bad_input); + assert_int_equal(start, PCMK__PARSE_INT_DEFAULT); + assert_int_equal(end, PCMK__PARSE_INT_DEFAULT); + + assert_int_equal(pcmk__parse_ll_range("-x2000", &start, &end), pcmk_rc_bad_input); + assert_int_equal(start, PCMK__PARSE_INT_DEFAULT); + assert_int_equal(end, PCMK__PARSE_INT_DEFAULT); +} + +static void +strtoll_errors(void **state) +{ + long long start, end; + + assert_int_equal(pcmk__parse_ll_range("20000000000000000000-", &start, &end), EOVERFLOW); + assert_int_equal(pcmk__parse_ll_range("100-20000000000000000000", &start, &end), EOVERFLOW); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_input_string), + cmocka_unit_test(null_input_variables), + cmocka_unit_test(missing_separator), + cmocka_unit_test(only_separator), + cmocka_unit_test(no_range_end), + cmocka_unit_test(no_range_start), + cmocka_unit_test(range_start_and_end), + cmocka_unit_test(strtoll_errors), + cmocka_unit_test(garbage)) diff --git a/lib/common/tests/strings/pcmk__s_test.c b/lib/common/tests/strings/pcmk__s_test.c new file mode 100644 index 0000000..cdc2551 --- /dev/null +++ b/lib/common/tests/strings/pcmk__s_test.c @@ -0,0 +1,29 @@ +/* + * 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> + +static void +s_is_null(void **state) { + assert_null(pcmk__s(NULL, NULL)); + assert_string_equal(pcmk__s(NULL, ""), ""); + assert_string_equal(pcmk__s(NULL, "something"), "something"); +} + +static void +s_is_not_null(void **state) { + assert_string_equal(pcmk__s("something", NULL), "something"); + assert_string_equal(pcmk__s("something", "default"), "something"); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(s_is_null), + cmocka_unit_test(s_is_not_null)) diff --git a/lib/common/tests/strings/pcmk__scan_double_test.c b/lib/common/tests/strings/pcmk__scan_double_test.c new file mode 100644 index 0000000..a1a180a --- /dev/null +++ b/lib/common/tests/strings/pcmk__scan_double_test.c @@ -0,0 +1,158 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <crm/common/unittest_internal.h> + +#include <float.h> // DBL_MAX, etc. +#include <math.h> // fabs() + +// Ensure plenty of characters for %f display +#define LOCAL_BUF_SIZE 2 * DBL_MAX_10_EXP + +/* + * assert_float_equal doesn't exist for older versions of cmocka installed on some + * of our builders, so define it in terms of regular assert() here in that case. + */ +#if HAVE_DECL_ASSERT_FLOAT_EQUAL == 0 +#define assert_float_equal(a, b, epsilon) assert_true(fabs((a) - (b)) < (epsilon)) +#endif + +static void +empty_input_string(void **state) +{ + double result; + + // Without default_text + assert_int_equal(pcmk__scan_double(NULL, &result, NULL, NULL), EINVAL); + assert_float_equal(result, PCMK__PARSE_DBL_DEFAULT, DBL_EPSILON); + + assert_int_equal(pcmk__scan_double("", &result, NULL, NULL), EINVAL); + assert_float_equal(result, PCMK__PARSE_DBL_DEFAULT, DBL_EPSILON); + + // With default_text + assert_int_equal(pcmk__scan_double(NULL, &result, "2.0", NULL), pcmk_rc_ok); + assert_float_equal(result, 2.0, DBL_EPSILON); + + assert_int_equal(pcmk__scan_double("", &result, "2.0", NULL), EINVAL); + assert_float_equal(result, PCMK__PARSE_DBL_DEFAULT, DBL_EPSILON); +} + +static void +bad_input_string(void **state) +{ + double result; + + // Without default text + assert_int_equal(pcmk__scan_double("asdf", &result, NULL, NULL), EINVAL); + assert_float_equal(result, PCMK__PARSE_DBL_DEFAULT, DBL_EPSILON); + + assert_int_equal(pcmk__scan_double("as2.0", &result, NULL, NULL), EINVAL); + assert_float_equal(result, PCMK__PARSE_DBL_DEFAULT, DBL_EPSILON); + + // With default text (not used) + assert_int_equal(pcmk__scan_double("asdf", &result, "2.0", NULL), EINVAL); + assert_float_equal(result, PCMK__PARSE_DBL_DEFAULT, DBL_EPSILON); + + assert_int_equal(pcmk__scan_double("as2.0", &result, "2.0", NULL), EINVAL); + assert_float_equal(result, PCMK__PARSE_DBL_DEFAULT, DBL_EPSILON); +} + +static void +trailing_chars(void **state) +{ + double result; + char *end_text; + + assert_int_equal(pcmk__scan_double("2.0asdf", &result, NULL, &end_text), pcmk_rc_ok); + assert_float_equal(result, 2.0, DBL_EPSILON); + assert_string_equal(end_text, "asdf"); +} + +static void +no_result_variable(void **state) +{ + pcmk__assert_asserts(pcmk__scan_double("asdf", NULL, NULL, NULL)); +} + +static void +typical_case(void **state) +{ + char str[LOCAL_BUF_SIZE]; + double result; + + assert_int_equal(pcmk__scan_double("0.0", &result, NULL, NULL), pcmk_rc_ok); + assert_float_equal(result, 0.0, DBL_EPSILON); + + assert_int_equal(pcmk__scan_double("1.0", &result, NULL, NULL), pcmk_rc_ok); + assert_float_equal(result, 1.0, DBL_EPSILON); + + assert_int_equal(pcmk__scan_double("-1.0", &result, NULL, NULL), pcmk_rc_ok); + assert_float_equal(result, -1.0, DBL_EPSILON); + + snprintf(str, LOCAL_BUF_SIZE, "%f", DBL_MAX); + assert_int_equal(pcmk__scan_double(str, &result, NULL, NULL), pcmk_rc_ok); + assert_float_equal(result, DBL_MAX, DBL_EPSILON); + + snprintf(str, LOCAL_BUF_SIZE, "%f", -DBL_MAX); + assert_int_equal(pcmk__scan_double(str, &result, NULL, NULL), pcmk_rc_ok); + assert_float_equal(result, -DBL_MAX, DBL_EPSILON); +} + +static void +double_overflow(void **state) +{ + char str[LOCAL_BUF_SIZE]; + double result; + + /* + * 1e(DBL_MAX_10_EXP + 1) produces an inf value + * Can't use assert_float_equal() because (inf - inf) == NaN + */ + snprintf(str, LOCAL_BUF_SIZE, "1e%d", DBL_MAX_10_EXP + 1); + assert_int_equal(pcmk__scan_double(str, &result, NULL, NULL), EOVERFLOW); + assert_true(result > DBL_MAX); + + snprintf(str, LOCAL_BUF_SIZE, "-1e%d", DBL_MAX_10_EXP + 1); + assert_int_equal(pcmk__scan_double(str, &result, NULL, NULL), EOVERFLOW); + assert_true(result < -DBL_MAX); +} + +static void +double_underflow(void **state) +{ + char str[LOCAL_BUF_SIZE]; + double result; + + /* + * 1e(DBL_MIN_10_EXP - 1) produces a denormalized value (between 0 + * and DBL_MIN) + * + * C99/C11: result will be **no greater than** DBL_MIN + */ + snprintf(str, LOCAL_BUF_SIZE, "1e%d", DBL_MIN_10_EXP - 1); + assert_int_equal(pcmk__scan_double(str, &result, NULL, NULL), pcmk_rc_underflow); + assert_true(result >= 0.0); + assert_true(result <= DBL_MIN); + + snprintf(str, LOCAL_BUF_SIZE, "-1e%d", DBL_MIN_10_EXP - 1); + assert_int_equal(pcmk__scan_double(str, &result, NULL, NULL), pcmk_rc_underflow); + assert_true(result <= 0.0); + assert_true(result >= -DBL_MIN); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_input_string), + cmocka_unit_test(bad_input_string), + cmocka_unit_test(trailing_chars), + cmocka_unit_test(no_result_variable), + cmocka_unit_test(typical_case), + cmocka_unit_test(double_overflow), + cmocka_unit_test(double_underflow)) diff --git a/lib/common/tests/strings/pcmk__scan_min_int_test.c b/lib/common/tests/strings/pcmk__scan_min_int_test.c new file mode 100644 index 0000000..90c3e87 --- /dev/null +++ b/lib/common/tests/strings/pcmk__scan_min_int_test.c @@ -0,0 +1,60 @@ +/* + * 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> + +static void +empty_input_string(void **state) +{ + int result; + + assert_int_equal(pcmk__scan_min_int("", &result, 1), EINVAL); + assert_int_equal(result, 1); + + assert_int_equal(pcmk__scan_min_int(NULL, &result, 1), pcmk_rc_ok); + assert_int_equal(result, 1); +} + +static void +input_below_minimum(void **state) +{ + int result; + + assert_int_equal(pcmk__scan_min_int("100", &result, 1024), pcmk_rc_ok); + assert_int_equal(result, 1024); +} + +static void +input_above_maximum(void **state) +{ + int result; + + assert_int_equal(pcmk__scan_min_int("20000000000000000", &result, 100), EOVERFLOW); + assert_int_equal(result, INT_MAX); +} + +static void +input_just_right(void **state) +{ + int result; + + assert_int_equal(pcmk__scan_min_int("1024", &result, 1024), pcmk_rc_ok); + assert_int_equal(result, 1024); + + assert_int_equal(pcmk__scan_min_int("2048", &result, 1024), pcmk_rc_ok); + assert_int_equal(result, 2048); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_input_string), + cmocka_unit_test(input_below_minimum), + cmocka_unit_test(input_above_maximum), + cmocka_unit_test(input_just_right)) diff --git a/lib/common/tests/strings/pcmk__scan_port_test.c b/lib/common/tests/strings/pcmk__scan_port_test.c new file mode 100644 index 0000000..cf927e4 --- /dev/null +++ b/lib/common/tests/strings/pcmk__scan_port_test.c @@ -0,0 +1,59 @@ +/* + * 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> + +static void +empty_input_string(void **state) +{ + int result; + + assert_int_equal(pcmk__scan_port("", &result), EINVAL); + assert_int_equal(result, -1); +} + +static void +bad_input_string(void **state) +{ + int result; + + assert_int_equal(pcmk__scan_port("abc", &result), EINVAL); + assert_int_equal(result, -1); +} + +static void +out_of_range(void **state) +{ + int result; + + assert_int_equal(pcmk__scan_port("-1", &result), pcmk_rc_before_range); + assert_int_equal(result, -1); + assert_int_equal(pcmk__scan_port("65536", &result), pcmk_rc_after_range); + assert_int_equal(result, -1); +} + +static void +typical_case(void **state) +{ + int result; + + assert_int_equal(pcmk__scan_port("0", &result), pcmk_rc_ok); + assert_int_equal(result, 0); + + assert_int_equal(pcmk__scan_port("80", &result), pcmk_rc_ok); + assert_int_equal(result, 80); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_input_string), + cmocka_unit_test(bad_input_string), + cmocka_unit_test(out_of_range), + cmocka_unit_test(typical_case)) diff --git a/lib/common/tests/strings/pcmk__starts_with_test.c b/lib/common/tests/strings/pcmk__starts_with_test.c new file mode 100644 index 0000000..5429417 --- /dev/null +++ b/lib/common/tests/strings/pcmk__starts_with_test.c @@ -0,0 +1,35 @@ +/* + * 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 +bad_input(void **state) { + assert_false(pcmk__starts_with(NULL, "x")); + assert_false(pcmk__starts_with("abc", NULL)); +} + +static void +starts_with(void **state) { + assert_true(pcmk__starts_with("abc", "a")); + assert_true(pcmk__starts_with("abc", "ab")); + assert_true(pcmk__starts_with("abc", "abc")); + + assert_false(pcmk__starts_with("abc", "A")); + assert_false(pcmk__starts_with("abc", "bc")); + + assert_false(pcmk__starts_with("", "x")); + assert_true(pcmk__starts_with("xyz", "")); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(bad_input), + cmocka_unit_test(starts_with)) diff --git a/lib/common/tests/strings/pcmk__str_any_of_test.c b/lib/common/tests/strings/pcmk__str_any_of_test.c new file mode 100644 index 0000000..bd4ae2c --- /dev/null +++ b/lib/common/tests/strings/pcmk__str_any_of_test.c @@ -0,0 +1,48 @@ +/* + * 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_input_list(void **state) { + assert_false(pcmk__strcase_any_of("xxx", NULL)); + assert_false(pcmk__str_any_of("xxx", NULL)); + assert_false(pcmk__strcase_any_of("", NULL)); + assert_false(pcmk__str_any_of("", NULL)); +} + +static void +empty_string(void **state) { + assert_false(pcmk__strcase_any_of("", "xxx", "yyy", NULL)); + assert_false(pcmk__str_any_of("", "xxx", "yyy", NULL)); + assert_false(pcmk__strcase_any_of(NULL, "xxx", "yyy", NULL)); + assert_false(pcmk__str_any_of(NULL, "xxx", "yyy", NULL)); +} + +static void +in_list(void **state) { + assert_true(pcmk__strcase_any_of("xxx", "aaa", "bbb", "xxx", NULL)); + assert_true(pcmk__str_any_of("xxx", "aaa", "bbb", "xxx", NULL)); + assert_true(pcmk__strcase_any_of("XXX", "aaa", "bbb", "xxx", NULL)); +} + +static void +not_in_list(void **state) { + assert_false(pcmk__strcase_any_of("xxx", "aaa", "bbb", NULL)); + assert_false(pcmk__str_any_of("xxx", "aaa", "bbb", NULL)); + assert_false(pcmk__str_any_of("AAA", "aaa", "bbb", NULL)); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_input_list), + cmocka_unit_test(empty_string), + cmocka_unit_test(in_list), + cmocka_unit_test(not_in_list)) diff --git a/lib/common/tests/strings/pcmk__str_in_list_test.c b/lib/common/tests/strings/pcmk__str_in_list_test.c new file mode 100644 index 0000000..cff536a --- /dev/null +++ b/lib/common/tests/strings/pcmk__str_in_list_test.c @@ -0,0 +1,107 @@ +/* + * 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 <glib.h> + +static void +empty_input_list(void **state) { + assert_false(pcmk__str_in_list(NULL, NULL, pcmk__str_none)); + assert_false(pcmk__str_in_list(NULL, NULL, pcmk__str_null_matches)); + assert_false(pcmk__str_in_list("xxx", NULL, pcmk__str_none)); + assert_false(pcmk__str_in_list("", NULL, pcmk__str_none)); +} + +static void +empty_string(void **state) { + GList *list = NULL; + + list = g_list_prepend(list, (gpointer) "xxx"); + + assert_false(pcmk__str_in_list(NULL, list, pcmk__str_none)); + assert_true(pcmk__str_in_list(NULL, list, pcmk__str_null_matches)); + assert_false(pcmk__str_in_list("", list, pcmk__str_none)); + assert_false(pcmk__str_in_list("", list, pcmk__str_null_matches)); + + g_list_free(list); +} + +static void +star_matches(void **state) { + GList *list = NULL; + + list = g_list_prepend(list, (gpointer) "*"); + list = g_list_append(list, (gpointer) "more"); + + assert_true(pcmk__str_in_list("xxx", list, pcmk__str_star_matches)); + assert_true(pcmk__str_in_list("yyy", list, pcmk__str_star_matches)); + assert_true(pcmk__str_in_list("XXX", list, pcmk__str_star_matches|pcmk__str_casei)); + assert_true(pcmk__str_in_list("", list, pcmk__str_star_matches)); + + g_list_free(list); +} + +static void +star_doesnt_match(void **state) { + GList *list = NULL; + + list = g_list_prepend(list, (gpointer) "*"); + + assert_false(pcmk__str_in_list("xxx", list, pcmk__str_none)); + assert_false(pcmk__str_in_list("yyy", list, pcmk__str_none)); + assert_false(pcmk__str_in_list("XXX", list, pcmk__str_casei)); + assert_false(pcmk__str_in_list("", list, pcmk__str_none)); + assert_false(pcmk__str_in_list(NULL, list, pcmk__str_star_matches)); + + g_list_free(list); +} + +static void +in_list(void **state) { + GList *list = NULL; + + list = g_list_prepend(list, (gpointer) "xxx"); + list = g_list_prepend(list, (gpointer) "yyy"); + list = g_list_prepend(list, (gpointer) "zzz"); + + assert_true(pcmk__str_in_list("xxx", list, pcmk__str_none)); + assert_true(pcmk__str_in_list("XXX", list, pcmk__str_casei)); + assert_true(pcmk__str_in_list("yyy", list, pcmk__str_none)); + assert_true(pcmk__str_in_list("YYY", list, pcmk__str_casei)); + assert_true(pcmk__str_in_list("zzz", list, pcmk__str_none)); + assert_true(pcmk__str_in_list("ZZZ", list, pcmk__str_casei)); + + g_list_free(list); +} + +static void +not_in_list(void **state) { + GList *list = NULL; + + list = g_list_prepend(list, (gpointer) "xxx"); + list = g_list_prepend(list, (gpointer) "yyy"); + + assert_false(pcmk__str_in_list("xx", list, pcmk__str_none)); + assert_false(pcmk__str_in_list("XXX", list, pcmk__str_none)); + assert_false(pcmk__str_in_list("zzz", list, pcmk__str_none)); + assert_false(pcmk__str_in_list("zzz", list, pcmk__str_casei)); + + g_list_free(list); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_input_list), + cmocka_unit_test(empty_string), + cmocka_unit_test(star_matches), + cmocka_unit_test(star_doesnt_match), + cmocka_unit_test(in_list), + cmocka_unit_test(not_in_list)) diff --git a/lib/common/tests/strings/pcmk__str_table_dup_test.c b/lib/common/tests/strings/pcmk__str_table_dup_test.c new file mode 100644 index 0000000..754bde6 --- /dev/null +++ b/lib/common/tests/strings/pcmk__str_table_dup_test.c @@ -0,0 +1,59 @@ +/* + * 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 +null_input_table(void **state) +{ + assert_null(pcmk__str_table_dup(NULL)); +} + +static void +empty_input_table(void **state) +{ + GHashTable *tbl = pcmk__strkey_table(free, free); + GHashTable *copy = NULL; + + copy = pcmk__str_table_dup(tbl); + assert_int_equal(g_hash_table_size(copy), 0); + + g_hash_table_destroy(tbl); + g_hash_table_destroy(copy); +} + +static void +regular_input_table(void **state) +{ + GHashTable *tbl = pcmk__strkey_table(free, free); + GHashTable *copy = NULL; + + g_hash_table_insert(tbl, strdup("abc"), strdup("123")); + g_hash_table_insert(tbl, strdup("def"), strdup("456")); + g_hash_table_insert(tbl, strdup("ghi"), strdup("789")); + + copy = pcmk__str_table_dup(tbl); + assert_int_equal(g_hash_table_size(copy), 3); + + assert_string_equal(g_hash_table_lookup(tbl, "abc"), "123"); + assert_string_equal(g_hash_table_lookup(tbl, "def"), "456"); + assert_string_equal(g_hash_table_lookup(tbl, "ghi"), "789"); + + g_hash_table_destroy(tbl); + g_hash_table_destroy(copy); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(null_input_table), + cmocka_unit_test(empty_input_table), + cmocka_unit_test(regular_input_table)) diff --git a/lib/common/tests/strings/pcmk__str_update_test.c b/lib/common/tests/strings/pcmk__str_update_test.c new file mode 100644 index 0000000..571031d --- /dev/null +++ b/lib/common/tests/strings/pcmk__str_update_test.c @@ -0,0 +1,78 @@ +/* + * 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 "mock_private.h" + +static void +update_null(void **state) { + char *str = NULL; + + // These just make sure they don't crash + pcmk__str_update(NULL, NULL); + pcmk__str_update(NULL, "value"); + + // Update an already NULL string to NULL + pcmk__str_update(&str, NULL); + assert_null(str); + + // Update an already allocated string to NULL + str = strdup("hello"); + pcmk__str_update(&str, NULL); + assert_null(str); +} + +static void +update_same(void **state) { + char *str = NULL; + char *saved = NULL; + + str = strdup("hello"); + saved = str; + pcmk__str_update(&str, "hello"); + assert_ptr_equal(saved, str); // No free and reallocation + free(str); +} + +static void +update_different(void **state) { + char *str = NULL; + + str = strdup("hello"); + pcmk__str_update(&str, "world"); + assert_string_equal(str, "world"); + free(str); +} + +static void +strdup_fails(void **state) { + char *str = NULL; + + str = strdup("hello"); + + pcmk__assert_asserts( + { + pcmk__mock_strdup = true; // strdup() will return NULL + expect_string(__wrap_strdup, s, "world"); + pcmk__str_update(&str, "world"); + pcmk__mock_strdup = false; // Use the real strdup() + } + ); + + free(str); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(update_null), + cmocka_unit_test(update_same), + cmocka_unit_test(update_different), + cmocka_unit_test(strdup_fails)) diff --git a/lib/common/tests/strings/pcmk__strcmp_test.c b/lib/common/tests/strings/pcmk__strcmp_test.c new file mode 100644 index 0000000..a709f18 --- /dev/null +++ b/lib/common/tests/strings/pcmk__strcmp_test.c @@ -0,0 +1,80 @@ +/* + * 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 +same_pointer(void **state) { + const char *s1 = "abcd"; + const char *s2 = "wxyz"; + + assert_int_equal(pcmk__strcmp(s1, s1, pcmk__str_none), 0); + assert_true(pcmk__str_eq(s1, s1, pcmk__str_none)); + assert_int_not_equal(pcmk__strcmp(s1, s2, pcmk__str_none), 0); + assert_false(pcmk__str_eq(s1, s2, pcmk__str_none)); + assert_int_equal(pcmk__strcmp(NULL, NULL, pcmk__str_none), 0); +} + +static void +one_is_null(void **state) { + const char *s1 = "abcd"; + + assert_int_equal(pcmk__strcmp(s1, NULL, pcmk__str_null_matches), 0); + assert_true(pcmk__str_eq(s1, NULL, pcmk__str_null_matches)); + assert_int_equal(pcmk__strcmp(NULL, s1, pcmk__str_null_matches), 0); + assert_true(pcmk__strcmp(s1, NULL, pcmk__str_none) > 0); + assert_false(pcmk__str_eq(s1, NULL, pcmk__str_none)); + assert_true(pcmk__strcmp(NULL, s1, pcmk__str_none) < 0); +} + +static void +case_matters(void **state) { + const char *s1 = "abcd"; + const char *s2 = "ABCD"; + + assert_true(pcmk__strcmp(s1, s2, pcmk__str_none) > 0); + assert_false(pcmk__str_eq(s1, s2, pcmk__str_none)); + assert_true(pcmk__strcmp(s2, s1, pcmk__str_none) < 0); +} + +static void +case_insensitive(void **state) { + const char *s1 = "abcd"; + const char *s2 = "ABCD"; + + assert_int_equal(pcmk__strcmp(s1, s2, pcmk__str_casei), 0); + assert_true(pcmk__str_eq(s1, s2, pcmk__str_casei)); +} + +static void +regex(void **state) { + const char *s1 = "abcd"; + const char *s2 = "ABCD"; + + assert_true(pcmk__strcmp(NULL, "a..d", pcmk__str_regex) > 0); + assert_true(pcmk__strcmp(s1, NULL, pcmk__str_regex) > 0); + assert_int_equal(pcmk__strcmp(s1, "a..d", pcmk__str_regex), 0); + assert_true(pcmk__str_eq(s1, "a..d", pcmk__str_regex)); + assert_int_not_equal(pcmk__strcmp(s1, "xxyy", pcmk__str_regex), 0); + assert_false(pcmk__str_eq(s1, "xxyy", pcmk__str_regex)); + assert_int_equal(pcmk__strcmp(s2, "a..d", pcmk__str_regex|pcmk__str_casei), 0); + assert_true(pcmk__str_eq(s2, "a..d", pcmk__str_regex|pcmk__str_casei)); + assert_int_not_equal(pcmk__strcmp(s2, "a..d", pcmk__str_regex), 0); + assert_false(pcmk__str_eq(s2, "a..d", pcmk__str_regex)); + assert_true(pcmk__strcmp(s2, "*ab", pcmk__str_regex) > 0); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(same_pointer), + cmocka_unit_test(one_is_null), + cmocka_unit_test(case_matters), + cmocka_unit_test(case_insensitive), + cmocka_unit_test(regex)) diff --git a/lib/common/tests/strings/pcmk__strikey_table_test.c b/lib/common/tests/strings/pcmk__strikey_table_test.c new file mode 100644 index 0000000..d5f8635 --- /dev/null +++ b/lib/common/tests/strings/pcmk__strikey_table_test.c @@ -0,0 +1,40 @@ +/* + * 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 +store_strs(void **state) +{ + GHashTable *tbl = NULL; + + tbl = pcmk__strikey_table(free, free); + assert_non_null(tbl); + + assert_true(g_hash_table_insert(tbl, strdup("key-abc"), strdup("val-abc"))); + assert_int_equal(g_hash_table_size(tbl), 1); + assert_string_equal(g_hash_table_lookup(tbl, "key-abc"), "val-abc"); + + assert_false(g_hash_table_insert(tbl, strdup("key-abc"), strdup("val-def"))); + assert_int_equal(g_hash_table_size(tbl), 1); + assert_string_equal(g_hash_table_lookup(tbl, "key-abc"), "val-def"); + + assert_false(g_hash_table_insert(tbl, strdup("key-ABC"), strdup("val-ABC"))); + assert_int_equal(g_hash_table_size(tbl), 1); + assert_string_equal(g_hash_table_lookup(tbl, "key-ABC"), "val-ABC"); + + g_hash_table_destroy(tbl); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(store_strs)) diff --git a/lib/common/tests/strings/pcmk__strkey_table_test.c b/lib/common/tests/strings/pcmk__strkey_table_test.c new file mode 100644 index 0000000..ac6d92e --- /dev/null +++ b/lib/common/tests/strings/pcmk__strkey_table_test.c @@ -0,0 +1,40 @@ +/* + * 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 +store_strs(void **state) +{ + GHashTable *tbl = NULL; + + tbl = pcmk__strkey_table(free, free); + assert_non_null(tbl); + + assert_true(g_hash_table_insert(tbl, strdup("key-abc"), strdup("val-abc"))); + assert_int_equal(g_hash_table_size(tbl), 1); + assert_string_equal(g_hash_table_lookup(tbl, "key-abc"), "val-abc"); + + assert_false(g_hash_table_insert(tbl, strdup("key-abc"), strdup("val-def"))); + assert_int_equal(g_hash_table_size(tbl), 1); + assert_string_equal(g_hash_table_lookup(tbl, "key-abc"), "val-def"); + + assert_true(g_hash_table_insert(tbl, strdup("key-ABC"), strdup("val-abc"))); + assert_int_equal(g_hash_table_size(tbl), 2); + assert_string_equal(g_hash_table_lookup(tbl, "key-ABC"), "val-abc"); + + g_hash_table_destroy(tbl); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(store_strs)) diff --git a/lib/common/tests/strings/pcmk__trim_test.c b/lib/common/tests/strings/pcmk__trim_test.c new file mode 100644 index 0000000..56bdd17 --- /dev/null +++ b/lib/common/tests/strings/pcmk__trim_test.c @@ -0,0 +1,72 @@ +/* + * 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 <string.h> + +static void +empty_input(void **state) +{ + char *s = strdup(""); + + assert_null(pcmk__trim(NULL)); + assert_string_equal(pcmk__trim(s), ""); + + free(s); +} + +static void +leading_newline(void **state) +{ + char *s = strdup("\nabcd"); + + assert_string_equal(pcmk__trim(s), "\nabcd"); + free(s); +} + +static void +middle_newline(void **state) +{ + char *s = strdup("ab\ncd"); + + assert_string_equal(pcmk__trim(s), "ab\ncd"); + free(s); +} + +static void +trailing_newline(void **state) +{ + char *s = strdup("abcd\n\n"); + + assert_string_equal(pcmk__trim(s), "abcd"); + free(s); + + s = strdup("abcd\n "); + assert_string_equal(pcmk__trim(s), "abcd\n "); + free(s); +} + +static void +other_whitespace(void **state) +{ + char *s = strdup(" ab\t\ncd \t"); + + assert_string_equal(pcmk__trim(s), " ab\t\ncd \t"); + free(s); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_input), + cmocka_unit_test(leading_newline), + cmocka_unit_test(middle_newline), + cmocka_unit_test(trailing_newline), + cmocka_unit_test(other_whitespace)) diff --git a/lib/common/tests/utils/Makefile.am b/lib/common/tests/utils/Makefile.am new file mode 100644 index 0000000..edccf09 --- /dev/null +++ b/lib/common/tests/utils/Makefile.am @@ -0,0 +1,28 @@ +# +# Copyright 2020-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 $(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 = \ + compare_version_test \ + crm_meta_name_test \ + crm_meta_value_test \ + crm_user_lookup_test \ + pcmk_daemon_user_test \ + pcmk_str_is_infinity_test \ + pcmk_str_is_minus_infinity_test \ + pcmk__getpid_s_test + +if WRAPPABLE_UNAME +check_PROGRAMS += pcmk_hostname_test +endif + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/utils/compare_version_test.c b/lib/common/tests/utils/compare_version_test.c new file mode 100644 index 0000000..35ebb63 --- /dev/null +++ b/lib/common/tests/utils/compare_version_test.c @@ -0,0 +1,55 @@ +/* + * 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> + +static void +empty_params(void **state) +{ + assert_int_equal(compare_version(NULL, NULL), 0); + assert_int_equal(compare_version(NULL, "abc"), -1); + assert_int_equal(compare_version(NULL, "1.0.1"), -1); + assert_int_equal(compare_version("abc", NULL), 1); + assert_int_equal(compare_version("1.0.1", NULL), 1); +} + +static void +equal_versions(void **state) +{ + assert_int_equal(compare_version("0.4.7", "0.4.7"), 0); + assert_int_equal(compare_version("1.0", "1.0"), 0); +} + +static void +unequal_versions(void **state) +{ + assert_int_equal(compare_version("0.4.7", "0.4.8"), -1); + assert_int_equal(compare_version("0.4.8", "0.4.7"), 1); + + assert_int_equal(compare_version("0.2.3", "0.3"), -1); + assert_int_equal(compare_version("0.3", "0.2.3"), 1); + + assert_int_equal(compare_version("0.99", "1.0"), -1); + assert_int_equal(compare_version("1.0", "0.99"), 1); +} + +static void +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); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_params), + cmocka_unit_test(equal_versions), + cmocka_unit_test(unequal_versions), + cmocka_unit_test(shorter_versions)) diff --git a/lib/common/tests/utils/crm_meta_name_test.c b/lib/common/tests/utils/crm_meta_name_test.c new file mode 100644 index 0000000..06fecc5 --- /dev/null +++ b/lib/common/tests/utils/crm_meta_name_test.c @@ -0,0 +1,41 @@ +/* + * 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 <crm/msg_xml.h> + +static void +empty_params(void **state) +{ + assert_null(crm_meta_name(NULL)); +} + +static void +standard_usage(void **state) +{ + char *s = NULL; + + s = crm_meta_name(XML_RSC_ATTR_NOTIFY); + assert_string_equal(s, "CRM_meta_notify"); + free(s); + + s = crm_meta_name(XML_RSC_ATTR_STICKINESS); + assert_string_equal(s, "CRM_meta_resource_stickiness"); + free(s); + + s = crm_meta_name("blah"); + assert_string_equal(s, "CRM_meta_blah"); + free(s); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_params), + cmocka_unit_test(standard_usage)) diff --git a/lib/common/tests/utils/crm_meta_value_test.c b/lib/common/tests/utils/crm_meta_value_test.c new file mode 100644 index 0000000..0bde5c6 --- /dev/null +++ b/lib/common/tests/utils/crm_meta_value_test.c @@ -0,0 +1,56 @@ +/* + * 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 <crm/msg_xml.h> + +#include <glib.h> + +static void +empty_params(void **state) +{ + GHashTable *tbl = pcmk__strkey_table(free, free); + + assert_null(crm_meta_value(NULL, NULL)); + assert_null(crm_meta_value(tbl, NULL)); + + g_hash_table_destroy(tbl); +} + +static void +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)); + + g_hash_table_destroy(tbl); +} + +static void +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")); + + assert_string_equal(crm_meta_value(tbl, XML_RSC_ATTR_NOTIFY), "1"); + assert_string_equal(crm_meta_value(tbl, XML_RSC_ATTR_STICKINESS), "2"); + + g_hash_table_destroy(tbl); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_params), + cmocka_unit_test(key_not_in_table), + cmocka_unit_test(key_in_table)) diff --git a/lib/common/tests/utils/crm_user_lookup_test.c b/lib/common/tests/utils/crm_user_lookup_test.c new file mode 100644 index 0000000..5842ec5 --- /dev/null +++ b/lib/common/tests/utils/crm_user_lookup_test.c @@ -0,0 +1,127 @@ +/* + * 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 "crmcommon_private.h" +#include "mock_private.h" + +#include <pwd.h> +#include <sys/types.h> + +static void +calloc_fails(void **state) +{ + uid_t uid; + gid_t gid; + + pcmk__mock_calloc = true; // calloc() will return NULL + + expect_value(__wrap_calloc, nmemb, 1); + expect_value(__wrap_calloc, size, PCMK__PW_BUFFER_LEN); + assert_int_equal(crm_user_lookup("hauser", &uid, &gid), -ENOMEM); + + pcmk__mock_calloc = false; // Use real calloc() +} + +static void +getpwnam_r_fails(void **state) +{ + uid_t uid; + gid_t gid; + + // Set getpwnam_r() return value and result parameter + pcmk__mock_getpwnam_r = true; + + expect_string(__wrap_getpwnam_r, name, "hauser"); + expect_any(__wrap_getpwnam_r, pwd); + expect_any(__wrap_getpwnam_r, buf); + expect_value(__wrap_getpwnam_r, buflen, PCMK__PW_BUFFER_LEN); + expect_any(__wrap_getpwnam_r, result); + will_return(__wrap_getpwnam_r, EIO); + will_return(__wrap_getpwnam_r, NULL); + + assert_int_equal(crm_user_lookup("hauser", &uid, &gid), -EIO); + + pcmk__mock_getpwnam_r = false; +} + +static void +no_matching_pwent(void **state) +{ + uid_t uid; + gid_t gid; + + // Set getpwnam_r() return value and result parameter + pcmk__mock_getpwnam_r = true; + + expect_string(__wrap_getpwnam_r, name, "hauser"); + expect_any(__wrap_getpwnam_r, pwd); + expect_any(__wrap_getpwnam_r, buf); + expect_value(__wrap_getpwnam_r, buflen, PCMK__PW_BUFFER_LEN); + expect_any(__wrap_getpwnam_r, result); + will_return(__wrap_getpwnam_r, 0); + will_return(__wrap_getpwnam_r, NULL); + + assert_int_equal(crm_user_lookup("hauser", &uid, &gid), -EINVAL); + + pcmk__mock_getpwnam_r = false; +} + +static void +entry_found(void **state) +{ + uid_t uid; + gid_t gid; + + /* We don't care about any of the other fields of the password entry, so just + * leave them blank. + */ + struct passwd returned_ent = { .pw_uid = 1000, .pw_gid = 1000 }; + + /* Test getpwnam_r returning a valid passwd entry, but we don't pass uid or gid. */ + + // Set getpwnam_r() return value and result parameter + pcmk__mock_getpwnam_r = true; + + expect_string(__wrap_getpwnam_r, name, "hauser"); + expect_any(__wrap_getpwnam_r, pwd); + expect_any(__wrap_getpwnam_r, buf); + expect_value(__wrap_getpwnam_r, buflen, PCMK__PW_BUFFER_LEN); + expect_any(__wrap_getpwnam_r, result); + will_return(__wrap_getpwnam_r, 0); + will_return(__wrap_getpwnam_r, &returned_ent); + + assert_int_equal(crm_user_lookup("hauser", NULL, NULL), 0); + + /* Test getpwnam_r returning a valid passwd entry, and we do pass uid and gid. */ + + // Set getpwnam_r() return value and result parameter + expect_string(__wrap_getpwnam_r, name, "hauser"); + expect_any(__wrap_getpwnam_r, pwd); + expect_any(__wrap_getpwnam_r, buf); + expect_value(__wrap_getpwnam_r, buflen, PCMK__PW_BUFFER_LEN); + expect_any(__wrap_getpwnam_r, result); + will_return(__wrap_getpwnam_r, 0); + will_return(__wrap_getpwnam_r, &returned_ent); + + assert_int_equal(crm_user_lookup("hauser", &uid, &gid), 0); + assert_int_equal(uid, 1000); + assert_int_equal(gid, 1000); + + pcmk__mock_getpwnam_r = false; +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(calloc_fails), + cmocka_unit_test(getpwnam_r_fails), + cmocka_unit_test(no_matching_pwent), + cmocka_unit_test(entry_found)) diff --git a/lib/common/tests/utils/pcmk__getpid_s_test.c b/lib/common/tests/utils/pcmk__getpid_s_test.c new file mode 100644 index 0000000..20ba36a --- /dev/null +++ b/lib/common/tests/utils/pcmk__getpid_s_test.c @@ -0,0 +1,38 @@ +/* + * 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 "mock_private.h" + +#include <sys/types.h> +#include <unistd.h> + +static void +pcmk__getpid_s_test(void **state) +{ + char *retval; + + // Set getpid() return value + pcmk__mock_getpid = true; + will_return(__wrap_getpid, 1234); + + retval = pcmk__getpid_s(); + assert_non_null(retval); + assert_string_equal("1234", retval); + + free(retval); + + pcmk__mock_getpid = false; +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(pcmk__getpid_s_test)) diff --git a/lib/common/tests/utils/pcmk_daemon_user_test.c b/lib/common/tests/utils/pcmk_daemon_user_test.c new file mode 100644 index 0000000..a63ca73 --- /dev/null +++ b/lib/common/tests/utils/pcmk_daemon_user_test.c @@ -0,0 +1,83 @@ +/* + * 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 "crmcommon_private.h" +#include "mock_private.h" + +#include <pwd.h> +#include <sys/types.h> + +static void +no_matching_pwent(void **state) +{ + uid_t uid; + gid_t gid; + + // Set getpwnam_r() return value and result parameter + pcmk__mock_getpwnam_r = true; + + expect_string(__wrap_getpwnam_r, name, "hacluster"); + expect_any(__wrap_getpwnam_r, pwd); + expect_any(__wrap_getpwnam_r, buf); + expect_value(__wrap_getpwnam_r, buflen, PCMK__PW_BUFFER_LEN); + expect_any(__wrap_getpwnam_r, result); + will_return(__wrap_getpwnam_r, ENOENT); + will_return(__wrap_getpwnam_r, NULL); + + assert_int_equal(pcmk_daemon_user(&uid, &gid), -ENOENT); + + pcmk__mock_getpwnam_r = false; +} + +static void +entry_found(void **state) +{ + uid_t uid; + gid_t gid; + + /* We don't care about any of the other fields of the password entry, so just + * leave them blank. + */ + struct passwd returned_ent = { .pw_uid = 1000, .pw_gid = 1000 }; + + /* Test getpwnam_r returning a valid passwd entry, but we don't pass uid or gid. */ + + // Set getpwnam_r() return value and result parameter + pcmk__mock_getpwnam_r = true; + + expect_string(__wrap_getpwnam_r, name, "hacluster"); + expect_any(__wrap_getpwnam_r, pwd); + expect_any(__wrap_getpwnam_r, buf); + expect_value(__wrap_getpwnam_r, buflen, PCMK__PW_BUFFER_LEN); + expect_any(__wrap_getpwnam_r, result); + will_return(__wrap_getpwnam_r, 0); + will_return(__wrap_getpwnam_r, &returned_ent); + + assert_int_equal(pcmk_daemon_user(NULL, NULL), 0); + + /* Test getpwnam_r returning a valid passwd entry, and we do pass uid and gid. */ + + /* We don't need to call will_return() again because pcmk_daemon_user() + * will have cached the uid/gid from the previous call and won't make + * another call to getpwnam_r(). + */ + assert_int_equal(pcmk_daemon_user(&uid, &gid), 0); + assert_int_equal(uid, 1000); + assert_int_equal(gid, 1000); + + pcmk__mock_getpwnam_r = false; +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(no_matching_pwent), + cmocka_unit_test(entry_found)) diff --git a/lib/common/tests/utils/pcmk_hostname_test.c b/lib/common/tests/utils/pcmk_hostname_test.c new file mode 100644 index 0000000..7329486 --- /dev/null +++ b/lib/common/tests/utils/pcmk_hostname_test.c @@ -0,0 +1,56 @@ +/* + * 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/utils/pcmk_str_is_infinity_test.c b/lib/common/tests/utils/pcmk_str_is_infinity_test.c new file mode 100644 index 0000000..fff58ab --- /dev/null +++ b/lib/common/tests/utils/pcmk_str_is_infinity_test.c @@ -0,0 +1,57 @@ +/* + * 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 +uppercase_str_passes(void **state) +{ + assert_true(pcmk_str_is_infinity("INFINITY")); + assert_true(pcmk_str_is_infinity("+INFINITY")); +} + +static void +mixed_case_str_fails(void **state) +{ + assert_false(pcmk_str_is_infinity("infinity")); + assert_false(pcmk_str_is_infinity("+infinity")); + assert_false(pcmk_str_is_infinity("Infinity")); + assert_false(pcmk_str_is_infinity("+Infinity")); +} + +static void +added_whitespace_fails(void **state) +{ + assert_false(pcmk_str_is_infinity(" INFINITY")); + assert_false(pcmk_str_is_infinity("INFINITY ")); + assert_false(pcmk_str_is_infinity(" INFINITY ")); + assert_false(pcmk_str_is_infinity("+ INFINITY")); +} + +static void +empty_str_fails(void **state) +{ + assert_false(pcmk_str_is_infinity(NULL)); + assert_false(pcmk_str_is_infinity("")); +} + +static void +minus_infinity_fails(void **state) +{ + assert_false(pcmk_str_is_infinity("-INFINITY")); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(uppercase_str_passes), + cmocka_unit_test(mixed_case_str_fails), + cmocka_unit_test(added_whitespace_fails), + cmocka_unit_test(empty_str_fails), + cmocka_unit_test(minus_infinity_fails)) diff --git a/lib/common/tests/utils/pcmk_str_is_minus_infinity_test.c b/lib/common/tests/utils/pcmk_str_is_minus_infinity_test.c new file mode 100644 index 0000000..477b055 --- /dev/null +++ b/lib/common/tests/utils/pcmk_str_is_minus_infinity_test.c @@ -0,0 +1,54 @@ +/* + * 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 +uppercase_str_passes(void **state) +{ + assert_true(pcmk_str_is_minus_infinity("-INFINITY")); +} + +static void +mixed_case_str_fails(void **state) +{ + assert_false(pcmk_str_is_minus_infinity("-infinity")); + assert_false(pcmk_str_is_minus_infinity("-Infinity")); +} + +static void +added_whitespace_fails(void **state) +{ + assert_false(pcmk_str_is_minus_infinity(" -INFINITY")); + assert_false(pcmk_str_is_minus_infinity("-INFINITY ")); + assert_false(pcmk_str_is_minus_infinity(" -INFINITY ")); + assert_false(pcmk_str_is_minus_infinity("- INFINITY")); +} + +static void +empty_str_fails(void **state) +{ + assert_false(pcmk_str_is_minus_infinity(NULL)); + assert_false(pcmk_str_is_minus_infinity("")); +} + +static void +infinity_fails(void **state) +{ + assert_false(pcmk_str_is_minus_infinity("INFINITY")); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(uppercase_str_passes), + cmocka_unit_test(mixed_case_str_fails), + cmocka_unit_test(added_whitespace_fails), + cmocka_unit_test(empty_str_fails), + cmocka_unit_test(infinity_fails)) diff --git a/lib/common/tests/xml/Makefile.am b/lib/common/tests/xml/Makefile.am new file mode 100644 index 0000000..0ccdcc3 --- /dev/null +++ b/lib/common/tests/xml/Makefile.am @@ -0,0 +1,17 @@ +# +# 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 $(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 + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/xml/pcmk__xe_foreach_child_test.c b/lib/common/tests/xml/pcmk__xe_foreach_child_test.c new file mode 100644 index 0000000..9bcba87 --- /dev/null +++ b/lib/common/tests/xml/pcmk__xe_foreach_child_test.c @@ -0,0 +1,215 @@ +/* + * 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 <crm/common/xml_internal.h> + +static int compare_name_handler(xmlNode *xml, void *userdata) { + function_called(); + assert_string_equal((char *) userdata, crm_element_name(xml)); + return pcmk_rc_ok; +} + +const char *str1 = + "<xml>\n" + " <!-- This is a level 1 node -->\n" + " <level1>\n" + " content\n" + " </level1>\n" + " <!-- This is a level 1 node -->\n" + " <level1>\n" + " content\n" + " </level1>\n" + " <!-- This is a level 1 node -->\n" + " <level1>\n" + " content\n" + " </level1>\n" + "</xml>"; + +static void +bad_input(void **state) { + xmlNode *xml = string2xml(str1); + + pcmk__assert_asserts(pcmk__xe_foreach_child(xml, NULL, NULL, NULL)); + + free_xml(xml); +} + +static void +name_given_test(void **state) { + xmlNode *xml = string2xml(str1); + + /* The handler should be called once for every <level1> node. */ + expect_function_call(compare_name_handler); + expect_function_call(compare_name_handler); + expect_function_call(compare_name_handler); + + pcmk__xe_foreach_child(xml, "level1", compare_name_handler, (void *) "level1"); + free_xml(xml); +} + +static void +no_name_given_test(void **state) { + xmlNode *xml = string2xml(str1); + + /* The handler should be called once for every <level1> node. */ + expect_function_call(compare_name_handler); + expect_function_call(compare_name_handler); + expect_function_call(compare_name_handler); + + pcmk__xe_foreach_child(xml, NULL, compare_name_handler, (void *) "level1"); + free_xml(xml); +} + +static void +name_doesnt_exist_test(void **state) { + xmlNode *xml = string2xml(str1); + pcmk__xe_foreach_child(xml, "xxx", compare_name_handler, NULL); + free_xml(xml); +} + +const char *str2 = + "<xml>\n" + " <level1>\n" + " <!-- Inside a level 1 node -->\n" + " <level2>\n" + " <!-- Inside a level 2 node -->\n" + " </level2>\n" + " </level1>\n" + " <level1>\n" + " <!-- Inside a level 1 node -->\n" + " <level2>\n" + " <!-- Inside a level 2 node -->\n" + " <level3>\n" + " <!-- Inside a level 3 node -->\n" + " </level3>\n" + " </level2>\n" + " <level2>\n" + " <!-- Inside a level 2 node -->\n" + " </level2>\n" + " </level1>\n" + "</xml>"; + +static void +multiple_levels_test(void **state) { + xmlNode *xml = string2xml(str2); + + /* The handler should be called once for every <level1> node. */ + expect_function_call(compare_name_handler); + expect_function_call(compare_name_handler); + + pcmk__xe_foreach_child(xml, "level1", compare_name_handler, (void *) "level1"); + free_xml(xml); +} + +static void +multiple_levels_no_name_test(void **state) { + xmlNode *xml = string2xml(str2); + + /* The handler should be called once for every <level1> node. */ + expect_function_call(compare_name_handler); + expect_function_call(compare_name_handler); + + pcmk__xe_foreach_child(xml, NULL, compare_name_handler, (void *) "level1"); + free_xml(xml); +} + +const char *str3 = + "<xml>\n" + " <!-- This is node #1 -->\n" + " <node1>\n" + " content\n" + " </node1>\n" + " <!-- This is node #2 -->\n" + " <node2>\n" + " content\n" + " </node2>\n" + " <!-- This is node #3 -->\n" + " <node3>\n" + " content\n" + " </node3>\n" + "</xml>"; + +static int any_of_handler(xmlNode *xml, void *userdata) { + function_called(); + assert_true(pcmk__str_any_of(crm_element_name(xml), "node1", "node2", "node3", NULL)); + return pcmk_rc_ok; +} + +static void +any_of_test(void **state) { + xmlNode *xml = string2xml(str3); + + /* The handler should be called once for every <nodeX> node. */ + expect_function_call(any_of_handler); + expect_function_call(any_of_handler); + expect_function_call(any_of_handler); + + pcmk__xe_foreach_child(xml, NULL, any_of_handler, NULL); + free_xml(xml); +} + +static int stops_on_first_handler(xmlNode *xml, void *userdata) { + function_called(); + + if (pcmk__str_eq(crm_element_name(xml), "node1", pcmk__str_none)) { + return pcmk_rc_error; + } else { + return pcmk_rc_ok; + } +} + +static int stops_on_second_handler(xmlNode *xml, void *userdata) { + function_called(); + + if (pcmk__str_eq(crm_element_name(xml), "node2", pcmk__str_none)) { + return pcmk_rc_error; + } else { + return pcmk_rc_ok; + } +} + +static int stops_on_third_handler(xmlNode *xml, void *userdata) { + function_called(); + + if (pcmk__str_eq(crm_element_name(xml), "node3", pcmk__str_none)) { + return pcmk_rc_error; + } else { + return pcmk_rc_ok; + } +} + +static void +one_of_test(void **state) { + xmlNode *xml = string2xml(str3); + + /* The handler should be called once. */ + expect_function_call(stops_on_first_handler); + assert_int_equal(pcmk__xe_foreach_child(xml, "node1", stops_on_first_handler, NULL), pcmk_rc_error); + + expect_function_call(stops_on_second_handler); + assert_int_equal(pcmk__xe_foreach_child(xml, "node2", stops_on_second_handler, NULL), pcmk_rc_error); + + expect_function_call(stops_on_third_handler); + assert_int_equal(pcmk__xe_foreach_child(xml, "node3", stops_on_third_handler, NULL), pcmk_rc_error); + + free_xml(xml); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(bad_input), + cmocka_unit_test(name_given_test), + cmocka_unit_test(no_name_given_test), + cmocka_unit_test(name_doesnt_exist_test), + cmocka_unit_test(multiple_levels_test), + cmocka_unit_test(multiple_levels_no_name_test), + cmocka_unit_test(any_of_test), + cmocka_unit_test(one_of_test)) diff --git a/lib/common/tests/xml/pcmk__xe_match_test.c b/lib/common/tests/xml/pcmk__xe_match_test.c new file mode 100644 index 0000000..be2c949 --- /dev/null +++ b/lib/common/tests/xml/pcmk__xe_match_test.c @@ -0,0 +1,106 @@ +/* + * 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/msg_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" + " content\n" + " </nodeA>\n" + " <!-- This is an A node -->\n" + " <nodeA attrA=\"456\" " XML_ATTR_ID "=\"2\">\n" + " content\n" + " </nodeA>\n" + " <!-- This is an A node -->\n" + " <nodeA attrB=\"XYZ\" " XML_ATTR_ID "=\"3\">\n" + " content\n" + " </nodeA>\n" + " <!-- This is a B node -->\n" + " <nodeB attrA=\"123\" " XML_ATTR_ID "=\"4\">\n" + " content\n" + " </nodeA>\n" + " <!-- This is a B node -->\n" + " <nodeB attrB=\"ABC\" " XML_ATTR_ID "=\"5\">\n" + " content\n" + " </nodeA>\n" + "</xml>"; + +static void +bad_input(void **state) { + xmlNode *xml = string2xml(str1); + + assert_null(pcmk__xe_match(NULL, NULL, NULL, NULL)); + assert_null(pcmk__xe_match(NULL, NULL, NULL, "attrX")); + + free_xml(xml); +} + +static void +not_found(void **state) { + xmlNode *xml = string2xml(str1); + + /* No node with an attrX attribute */ + assert_null(pcmk__xe_match(xml, NULL, "attrX", NULL)); + /* No nodeX node */ + assert_null(pcmk__xe_match(xml, "nodeX", NULL, NULL)); + /* No nodeA node with attrX */ + assert_null(pcmk__xe_match(xml, "nodeA", "attrX", NULL)); + /* No nodeA node with attrA=XYZ */ + assert_null(pcmk__xe_match(xml, "nodeA", "attrA", "XYZ")); + + free_xml(xml); +} + +static void +find_attrB(void **state) { + xmlNode *xml = string2xml(str1); + xmlNode *result = NULL; + + /* Find the first node with attrB */ + result = pcmk__xe_match(xml, NULL, "attrB", NULL); + assert_non_null(result); + assert_string_equal(crm_element_value(result, "id"), "3"); + + /* Find the first nodeB with attrB */ + result = pcmk__xe_match(xml, "nodeB", "attrB", NULL); + assert_non_null(result); + assert_string_equal(crm_element_value(result, "id"), "5"); + + free_xml(xml); +} + +static void +find_attrA_matching(void **state) { + xmlNode *xml = string2xml(str1); + xmlNode *result = NULL; + + /* Find attrA=456 */ + result = pcmk__xe_match(xml, NULL, "attrA", "456"); + assert_non_null(result); + assert_string_equal(crm_element_value(result, "id"), "2"); + + /* Find a nodeB with attrA=123 */ + result = pcmk__xe_match(xml, "nodeB", "attrA", "123"); + assert_non_null(result); + assert_string_equal(crm_element_value(result, "id"), "4"); + + free_xml(xml); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(bad_input), + cmocka_unit_test(not_found), + cmocka_unit_test(find_attrB), + cmocka_unit_test(find_attrA_matching)); diff --git a/lib/common/tests/xpath/Makefile.am b/lib/common/tests/xpath/Makefile.am new file mode 100644 index 0000000..94abeee --- /dev/null +++ b/lib/common/tests/xpath/Makefile.am @@ -0,0 +1,16 @@ +# +# Copyright 2021-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 $(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__xpath_node_id_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/xpath/pcmk__xpath_node_id_test.c b/lib/common/tests/xpath/pcmk__xpath_node_id_test.c new file mode 100644 index 0000000..3922b34 --- /dev/null +++ b/lib/common/tests/xpath/pcmk__xpath_node_id_test.c @@ -0,0 +1,59 @@ +/* + * Copyright 2021-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/msg_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("/blah/blah", NULL)); + assert_null(pcmk__xpath_node_id("/blah/blah", "")); + assert_null(pcmk__xpath_node_id(NULL, NULL)); +} + +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")); +} + +static void +not_present(void **state) { + const char *xpath = "/some/xpath/string[@" XML_ATTR_ID "='xyz']"; + assert_null(pcmk__xpath_node_id(xpath, "lrm")); + + xpath = "/some/xpath/containing[@" XML_ATTR_ID "='lrm']"; + assert_null(pcmk__xpath_node_id(xpath, "lrm")); +} + +static void +present(void **state) { + char *s = NULL; + const char *xpath = "/some/xpath/containing/lrm[@" XML_ATTR_ID "='xyz']"; + + s = pcmk__xpath_node_id(xpath, "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"); + assert_int_equal(strcmp(s, "xyz"), 0); + free(s); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_input), + cmocka_unit_test(no_quotes), + cmocka_unit_test(not_present), + cmocka_unit_test(present)) diff --git a/lib/common/utils.c b/lib/common/utils.c new file mode 100644 index 0000000..e5b9ef0 --- /dev/null +++ b/lib/common/utils.c @@ -0,0 +1,594 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include <sys/types.h> +#include <sys/wait.h> +#include <sys/stat.h> +#include <sys/utsname.h> + +#include <stdio.h> +#include <unistd.h> +#include <string.h> +#include <stdlib.h> +#include <limits.h> +#include <pwd.h> +#include <time.h> +#include <libgen.h> +#include <signal.h> +#include <grp.h> + +#include <qb/qbdefs.h> + +#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> +#include <crm/common/ipc.h> +#include <crm/common/iso8601.h> +#include <crm/common/mainloop.h> +#include <libxml2/libxml/relaxng.h> + +#include "crmcommon_private.h" + +CRM_TRACE_INIT_DATA(common); + +gboolean crm_config_error = FALSE; +gboolean crm_config_warning = FALSE; +char *crm_system_name = NULL; + +bool +pcmk__is_user_in_group(const char *user, const char *group) +{ + struct group *grent; + char **gr_mem; + + if (user == NULL || group == NULL) { + return false; + } + + setgrent(); + while ((grent = getgrent()) != NULL) { + if (grent->gr_mem == NULL) { + continue; + } + + if(strcmp(group, grent->gr_name) != 0) { + continue; + } + + gr_mem = grent->gr_mem; + while (*gr_mem != NULL) { + if (!strcmp(user, *gr_mem++)) { + endgrent(); + return true; + } + } + } + endgrent(); + return false; +} + +int +crm_user_lookup(const char *name, uid_t * uid, gid_t * gid) +{ + int rc = pcmk_ok; + char *buffer = NULL; + struct passwd pwd; + struct passwd *pwentry = NULL; + + buffer = calloc(1, PCMK__PW_BUFFER_LEN); + if (buffer == NULL) { + return -ENOMEM; + } + + rc = getpwnam_r(name, &pwd, buffer, PCMK__PW_BUFFER_LEN, &pwentry); + if (pwentry) { + if (uid) { + *uid = pwentry->pw_uid; + } + if (gid) { + *gid = pwentry->pw_gid; + } + crm_trace("User %s has uid=%d gid=%d", name, pwentry->pw_uid, pwentry->pw_gid); + + } else { + rc = rc? -rc : -EINVAL; + crm_info("User %s lookup: %s", name, pcmk_strerror(rc)); + } + + free(buffer); + return rc; +} + +/*! + * \brief Get user and group IDs of pacemaker daemon user + * + * \param[out] uid If non-NULL, where to store daemon user ID + * \param[out] gid If non-NULL, where to store daemon group ID + * + * \return pcmk_ok on success, -errno otherwise + */ +int +pcmk_daemon_user(uid_t *uid, gid_t *gid) +{ + static uid_t daemon_uid; + static gid_t daemon_gid; + static bool found = false; + int rc = pcmk_ok; + + if (!found) { + rc = crm_user_lookup(CRM_DAEMON_USER, &daemon_uid, &daemon_gid); + if (rc == pcmk_ok) { + found = true; + } + } + if (found) { + if (uid) { + *uid = daemon_uid; + } + if (gid) { + *gid = daemon_gid; + } + } + return rc; +} + +/*! + * \internal + * \brief Return the integer equivalent of a portion of a string + * + * \param[in] text Pointer to beginning of string portion + * \param[out] end_text This will point to next character after integer + */ +static int +version_helper(const char *text, const char **end_text) +{ + int atoi_result = -1; + + CRM_ASSERT(end_text != NULL); + + errno = 0; + + if (text != NULL && text[0] != 0) { + /* seemingly sacrificing const-correctness -- because while strtol + doesn't modify the input, it doesn't want to artificially taint the + "end_text" pointer-to-pointer-to-first-char-in-string with constness + in case the input wasn't actually constant -- by semantic definition + not a single character will get modified so it shall be perfectly + safe to make compiler happy with dropping "const" qualifier here */ + atoi_result = (int) strtol(text, (char **) end_text, 10); + + if (errno == EINVAL) { + crm_err("Conversion of '%s' %c failed", text, text[0]); + atoi_result = -1; + } + } + return atoi_result; +} + +/* + * version1 < version2 : -1 + * version1 = version2 : 0 + * version1 > version2 : 1 + */ +int +compare_version(const char *version1, const char *version2) +{ + int rc = 0; + int lpc = 0; + const char *ver1_iter, *ver2_iter; + + if (version1 == NULL && version2 == NULL) { + return 0; + } else if (version1 == NULL) { + return -1; + } else if (version2 == NULL) { + return 1; + } + + ver1_iter = version1; + ver2_iter = version2; + + while (1) { + int digit1 = 0; + int digit2 = 0; + + lpc++; + + if (ver1_iter == ver2_iter) { + break; + } + + if (ver1_iter != NULL) { + digit1 = version_helper(ver1_iter, &ver1_iter); + } + + if (ver2_iter != NULL) { + digit2 = version_helper(ver2_iter, &ver2_iter); + } + + if (digit1 < digit2) { + rc = -1; + break; + + } else if (digit1 > digit2) { + rc = 1; + break; + } + + if (ver1_iter != NULL && *ver1_iter == '.') { + ver1_iter++; + } + if (ver1_iter != NULL && *ver1_iter == '\0') { + ver1_iter = NULL; + } + + if (ver2_iter != NULL && *ver2_iter == '.') { + ver2_iter++; + } + if (ver2_iter != NULL && *ver2_iter == 0) { + ver2_iter = NULL; + } + } + + if (rc == 0) { + crm_trace("%s == %s (%d)", version1, version2, lpc); + } else if (rc < 0) { + crm_trace("%s < %s (%d)", version1, version2, lpc); + } else if (rc > 0) { + crm_trace("%s > %s (%d)", version1, version2, lpc); + } + + return rc; +} + +/*! + * \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 + * + * \param[in] file File making the assertion + * \param[in] function Function making the assertion + * \param[in] line Line of file making the assertion + * \param[in] assert_condition String representation of assertion + */ +static void +log_assertion_as(const char *file, const char *function, int line, + const char *assert_condition) +{ + if (!pcmk__is_daemon) { + crm_enable_stderr(TRUE); // Make sure command-line user sees message + } + crm_err("%s: Triggered fatal assertion at %s:%d : %s", + function, file, line, assert_condition); +} + +/* coverity[+kill] */ +/*! + * \internal + * \brief Log a failed assertion and abort + * + * \param[in] file File making the assertion + * \param[in] function Function making the assertion + * \param[in] line Line of file making the assertion + * \param[in] assert_condition String representation of assertion + * + * \note This does not return + */ +static _Noreturn void +abort_as(const char *file, const char *function, int line, + const char *assert_condition) +{ + log_assertion_as(file, function, line, assert_condition); + abort(); +} + +/* coverity[+kill] */ +/*! + * \internal + * \brief Handle a failed assertion + * + * When called by a daemon, fork a child that aborts (to dump core), otherwise + * abort the current process. + * + * \param[in] file File making the assertion + * \param[in] function Function making the assertion + * \param[in] line Line of file making the assertion + * \param[in] assert_condition String representation of assertion + */ +static void +fail_assert_as(const char *file, const char *function, int line, + const char *assert_condition) +{ + int status = 0; + pid_t pid = 0; + + if (!pcmk__is_daemon) { + abort_as(file, function, line, assert_condition); // does not return + } + + pid = fork(); + switch (pid) { + case -1: // Fork failed + crm_warn("%s: Cannot dump core for non-fatal assertion at %s:%d " + ": %s", function, file, line, assert_condition); + break; + + case 0: // Child process: just abort to dump core + abort(); + break; + + default: // Parent process: wait for child + crm_err("%s: Forked child [%d] to record non-fatal assertion at " + "%s:%d : %s", function, pid, file, line, assert_condition); + crm_write_blackbox(SIGTRAP, NULL); + do { + if (waitpid(pid, &status, 0) == pid) { + return; // Child finished dumping core + } + } while (errno == EINTR); + if (errno == ECHILD) { + // crm_mon ignores SIGCHLD + crm_trace("Cannot wait on forked child [%d] " + "(SIGCHLD is probably ignored)", pid); + } else { + crm_err("Cannot wait on forked child [%d]: %s", + pid, pcmk_rc_str(errno)); + } + break; + } +} + +/* coverity[+kill] */ +void +crm_abort(const char *file, const char *function, int line, + const char *assert_condition, gboolean do_core, gboolean do_fork) +{ + if (!do_fork) { + abort_as(file, function, line, assert_condition); + } else if (do_core) { + fail_assert_as(file, function, line, assert_condition); + } else { + log_assertion_as(file, function, line, assert_condition); + } +} + +/*! + * \internal + * \brief Convert the current process to a daemon process + * + * Fork a child process, exit the parent, create a PID file with the current + * process ID, and close the standard input/output/error file descriptors. + * Exit instead if a daemon is already running and using the PID file. + * + * \param[in] name Daemon executable name + * \param[in] pidfile File name to use as PID file + */ +void +pcmk__daemonize(const char *name, const char *pidfile) +{ + int rc; + pid_t pid; + + /* Check before we even try... */ + rc = pcmk__pidfile_matches(pidfile, 1, name, &pid); + if ((rc != pcmk_rc_ok) && (rc != ENOENT)) { + crm_err("%s: already running [pid %lld in %s]", + name, (long long) pid, pidfile); + printf("%s: already running [pid %lld in %s]\n", + name, (long long) pid, pidfile); + crm_exit(CRM_EX_ERROR); + } + + pid = fork(); + if (pid < 0) { + fprintf(stderr, "%s: could not start daemon\n", name); + crm_perror(LOG_ERR, "fork"); + crm_exit(CRM_EX_OSERR); + + } else if (pid > 0) { + crm_exit(CRM_EX_OK); + } + + rc = pcmk__lock_pidfile(pidfile, name); + if (rc != pcmk_rc_ok) { + crm_err("Could not lock '%s' for %s: %s " CRM_XS " rc=%d", + pidfile, name, pcmk_rc_str(rc), rc); + printf("Could not lock '%s' for %s: %s (%d)\n", + pidfile, name, pcmk_rc_str(rc), rc); + crm_exit(CRM_EX_ERROR); + } + + umask(S_IWGRP | S_IWOTH | S_IROTH); + + close(STDIN_FILENO); + pcmk__open_devnull(O_RDONLY); // stdin (fd 0) + + close(STDOUT_FILENO); + pcmk__open_devnull(O_WRONLY); // stdout (fd 1) + + close(STDERR_FILENO); + 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 + +char * +crm_generate_uuid(void) +{ + unsigned char uuid[16]; + char *buffer = malloc(37); /* Including NUL byte */ + + CRM_ASSERT(buffer != NULL); + uuid_generate(uuid); + uuid_unparse(uuid, buffer); + return buffer; +} + +#ifdef HAVE_GNUTLS_GNUTLS_H +void +crm_gnutls_global_init(void) +{ + signal(SIGPIPE, SIG_IGN); + gnutls_global_init(); +} +#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); +} + +bool +pcmk_str_is_minus_infinity(const char *s) { + return pcmk__str_eq(s, CRM_MINUS_INFINITY_S, pcmk__str_none); +} + +/*! + * \internal + * \brief Sleep for given milliseconds + * + * \param[in] ms Time to sleep + * + * \note The full time might not be slept if a signal is received. + */ +void +pcmk__sleep_ms(unsigned int ms) +{ + // @TODO Impose a sane maximum sleep to avoid hanging a process for long + //CRM_CHECK(ms <= MAX_SLEEP, ms = MAX_SLEEP); + + // Use sleep() for any whole seconds + if (ms >= 1000) { + sleep(ms / 1000); + ms -= ms / 1000; + } + + if (ms == 0) { + return; + } + +#if defined(HAVE_NANOSLEEP) + // nanosleep() is POSIX-2008, so prefer that + { + struct timespec req = { .tv_sec = 0, .tv_nsec = (long) (ms * 1000000) }; + + nanosleep(&req, NULL); + } +#elif defined(HAVE_USLEEP) + // usleep() is widely available, though considered obsolete + usleep((useconds_t) ms); +#else + // Otherwise use a trick with select() timeout + { + struct timeval tv = { .tv_sec = 0, .tv_usec = (suseconds_t) ms }; + + select(0, NULL, NULL, NULL, &tv); + } +#endif +} diff --git a/lib/common/watchdog.c b/lib/common/watchdog.c new file mode 100644 index 0000000..ff2d273 --- /dev/null +++ b/lib/common/watchdog.c @@ -0,0 +1,311 @@ +/* + * Copyright 2013-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <sched.h> +#include <sys/ioctl.h> +#include <sys/reboot.h> + +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <ctype.h> +#include <dirent.h> +#include <signal.h> + +#ifdef _POSIX_MEMLOCK +# include <sys/mman.h> +#endif + +static pid_t sbd_pid = 0; + +static void +sysrq_trigger(char t) +{ +#if HAVE_LINUX_PROCFS + FILE *procf; + + // Root can always write here, regardless of kernel.sysrq value + procf = fopen("/proc/sysrq-trigger", "a"); + if (!procf) { + crm_perror(LOG_WARNING, "Opening sysrq-trigger failed"); + return; + } + crm_info("sysrq-trigger: %c", t); + fprintf(procf, "%c\n", t); + fclose(procf); +#endif // HAVE_LINUX_PROCFS + return; +} + + +/*! + * \internal + * \brief Panic the local host (if root) or tell pacemakerd to do so + */ +static void +panic_local(void) +{ + int rc = pcmk_ok; + uid_t uid = geteuid(); + pid_t ppid = getppid(); + + if(uid != 0 && ppid > 1) { + /* We're a non-root pacemaker daemon (pacemaker-based, + * pacemaker-controld, pacemaker-schedulerd, pacemaker-attrd, etc.) with + * the original pacemakerd parent. + * + * Of these, only the controller is likely to be initiating resets. + */ + crm_emerg("Signaling parent %lld to panic", (long long) ppid); + crm_exit(CRM_EX_PANIC); + return; + + } else if (uid != 0) { +#if HAVE_LINUX_PROCFS + /* + * No permissions, and no pacemakerd parent to escalate to. + * Track down the new pacemakerd process and send a signal instead. + */ + union sigval signal_value; + + memset(&signal_value, 0, sizeof(signal_value)); + ppid = pcmk__procfs_pid_of("pacemakerd"); + crm_emerg("Signaling pacemakerd[%lld] to panic", (long long) ppid); + + if(ppid > 1 && sigqueue(ppid, SIGQUIT, signal_value) < 0) { + crm_perror(LOG_EMERG, "Cannot signal pacemakerd[%lld] to panic", + (long long) ppid); + } +#endif // HAVE_LINUX_PROCFS + + /* The best we can do now is die */ + crm_exit(CRM_EX_PANIC); + return; + } + + /* We're either pacemakerd, or a pacemaker daemon running as root */ + + if (pcmk__str_eq("crash", getenv("PCMK_panic_action"), pcmk__str_casei)) { + sysrq_trigger('c'); + } else if (pcmk__str_eq("sync-crash", getenv("PCMK_panic_action"), pcmk__str_casei)) { + sync(); + sysrq_trigger('c'); + } else { + if (pcmk__str_eq("sync-reboot", getenv("PCMK_panic_action"), pcmk__str_casei)) { + sync(); + } + sysrq_trigger('b'); + } + /* reboot(RB_HALT_SYSTEM); rc = errno; */ + reboot(RB_AUTOBOOT); + rc = errno; + + crm_emerg("Reboot failed, escalating to parent %lld: %s " CRM_XS " rc=%d", + (long long) ppid, pcmk_rc_str(rc), rc); + + if(ppid > 1) { + /* child daemon */ + exit(CRM_EX_PANIC); + } else { + /* pacemakerd or orphan child */ + exit(CRM_EX_FATAL); + } +} + +/*! + * \internal + * \brief Tell sbd to kill the local host, then exit + */ +static void +panic_sbd(void) +{ + union sigval signal_value; + pid_t ppid = getppid(); + + crm_emerg("Signaling sbd[%lld] to panic", (long long) sbd_pid); + + memset(&signal_value, 0, sizeof(signal_value)); + /* TODO: Arrange for a slightly less brutal option? */ + if(sigqueue(sbd_pid, SIGKILL, signal_value) < 0) { + crm_perror(LOG_EMERG, "Cannot signal sbd[%lld] to terminate", + (long long) sbd_pid); + panic_local(); + } + + if(ppid > 1) { + /* child daemon */ + exit(CRM_EX_PANIC); + } else { + /* pacemakerd or orphan child */ + exit(CRM_EX_FATAL); + } +} + +/*! + * \internal + * \brief Panic the local host + * + * Panic the local host either by sbd (if running), directly, or by asking + * pacemakerd. If trace logging this function, exit instead. + * + * \param[in] origin Function caller (for logging only) + */ +void +pcmk__panic(const char *origin) +{ + /* Ensure sbd_pid is set */ + (void) pcmk__locate_sbd(); + + pcmk__if_tracing( + { + // getppid() == 1 means our original parent no longer exists + crm_emerg("Shutting down instead of panicking the node " + CRM_XS " origin=%s sbd=%lld parent=%d", + origin, (long long) sbd_pid, getppid()); + crm_exit(CRM_EX_FATAL); + return; + }, + {} + ); + + if(sbd_pid > 1) { + crm_emerg("Signaling sbd[%lld] to panic the system: %s", + (long long) sbd_pid, origin); + panic_sbd(); + + } else { + crm_emerg("Panicking the system directly: %s", origin); + panic_local(); + } +} + +/*! + * \internal + * \brief Return the process ID of sbd (or 0 if it is not running) + */ +pid_t +pcmk__locate_sbd(void) +{ + char *pidfile = NULL; + char *sbd_path = NULL; + int rc; + + if(sbd_pid > 1) { + return sbd_pid; + } + + /* Look for the pid file */ + pidfile = crm_strdup_printf(PCMK_RUN_DIR "/sbd.pid"); + sbd_path = crm_strdup_printf("%s/sbd", SBIN_DIR); + + /* Read the pid file */ + rc = pcmk__pidfile_matches(pidfile, 0, sbd_path, &sbd_pid); + if (rc == pcmk_rc_ok) { + crm_trace("SBD detected at pid %lld (via PID file %s)", + (long long) sbd_pid, pidfile); + +#if HAVE_LINUX_PROCFS + } else { + /* Fall back to /proc for systems that support it */ + sbd_pid = pcmk__procfs_pid_of("sbd"); + crm_trace("SBD detected at pid %lld (via procfs)", + (long long) sbd_pid); +#endif // HAVE_LINUX_PROCFS + } + + if(sbd_pid < 0) { + sbd_pid = 0; + crm_trace("SBD not detected"); + } + + free(pidfile); + free(sbd_path); + + return sbd_pid; +} + +long +pcmk__get_sbd_timeout(void) +{ + static long sbd_timeout = -2; + + if (sbd_timeout == -2) { + sbd_timeout = crm_get_msec(getenv("SBD_WATCHDOG_TIMEOUT")); + } + return sbd_timeout; +} + +bool +pcmk__get_sbd_sync_resource_startup(void) +{ + static int sync_resource_startup = PCMK__SBD_SYNC_DEFAULT; + static bool checked_sync_resource_startup = false; + + if (!checked_sync_resource_startup) { + const char *sync_env = getenv("SBD_SYNC_RESOURCE_STARTUP"); + + if (sync_env == NULL) { + crm_trace("Defaulting to %sstart-up synchronization with sbd", + (PCMK__SBD_SYNC_DEFAULT? "" : "no ")); + + } else if (crm_str_to_boolean(sync_env, &sync_resource_startup) < 0) { + crm_warn("Defaulting to %sstart-up synchronization with sbd " + "because environment value '%s' is invalid", + (PCMK__SBD_SYNC_DEFAULT? "" : "no "), sync_env); + } + checked_sync_resource_startup = true; + } + return sync_resource_startup != 0; +} + +long +pcmk__auto_watchdog_timeout(void) +{ + long sbd_timeout = pcmk__get_sbd_timeout(); + + return (sbd_timeout <= 0)? 0 : (2 * sbd_timeout); +} + +bool +pcmk__valid_sbd_timeout(const char *value) +{ + 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, value); + } + + if (st_timeout == 0) { + crm_debug("Watchdog may be enabled but 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_exit(CRM_EX_FATAL); + return false; + + } else { + long sbd_timeout = pcmk__get_sbd_timeout(); + + if (st_timeout < sbd_timeout) { + crm_emerg("Shutting down: 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", + value, sbd_timeout); + } + return true; +} diff --git a/lib/common/xml.c b/lib/common/xml.c new file mode 100644 index 0000000..22078ce --- /dev/null +++ b/lib/common/xml.c @@ -0,0 +1,2753 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#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 <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 "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). + * + * 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. + */ +#define PCMK__XML_PARSE_OPTS (XML_PARSE_NOBLANKS | XML_PARSE_RECOVER) + +bool +pcmk__tracking_xml_changes(xmlNode *xml, bool lazy) +{ + if(xml == NULL || xml->doc == NULL || xml->doc->_private == NULL) { + return FALSE; + } else if (!pcmk_is_set(((xml_doc_private_t *)xml->doc->_private)->flags, + pcmk__xf_tracking)) { + return FALSE; + } else if (lazy && !pcmk_is_set(((xml_doc_private_t *)xml->doc->_private)->flags, + pcmk__xf_lazy)) { + return FALSE; + } + return TRUE; +} + +static inline void +set_parent_flag(xmlNode *xml, long flag) +{ + for(; xml; xml = xml->parent) { + xml_node_private_t *nodepriv = xml->_private; + + if (nodepriv == NULL) { + /* During calls to xmlDocCopyNode(), _private will be unset for parent nodes */ + } else { + pcmk__set_xml_flags(nodepriv, flag); + } + } +} + +void +pcmk__set_xml_doc_flag(xmlNode *xml, enum xml_private_flags flag) +{ + if(xml && xml->doc && xml->doc->_private){ + /* During calls to xmlDocCopyNode(), xml->doc may be unset */ + xml_doc_private_t *docpriv = xml->doc->_private; + + pcmk__set_xml_flags(docpriv, flag); + } +} + +// Mark document, element, and all element's parents as changed +static inline void +mark_xml_node_dirty(xmlNode *xml) +{ + pcmk__set_xml_doc_flag(xml, pcmk__xf_dirty); + set_parent_flag(xml, pcmk__xf_dirty); +} + +// Clear flags on XML node and its children +static void +reset_xml_node_flags(xmlNode *xml) +{ + xmlNode *cIter = NULL; + xml_node_private_t *nodepriv = xml->_private; + + if (nodepriv) { + nodepriv->flags = 0; + } + + for (cIter = pcmk__xml_first_child(xml); cIter != NULL; + cIter = pcmk__xml_next(cIter)) { + reset_xml_node_flags(cIter); + } +} + +// Set xpf_created flag on XML node and any children +void +pcmk__mark_xml_created(xmlNode *xml) +{ + xmlNode *cIter = NULL; + xml_node_private_t *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); + mark_xml_node_dirty(xml); + } + for (cIter = pcmk__xml_first_child(xml); cIter != NULL; + cIter = pcmk__xml_next(cIter)) { + pcmk__mark_xml_created(cIter); + } + } +} + +void +pcmk__mark_xml_attr_dirty(xmlAttr *a) +{ + xmlNode *parent = a->parent; + xml_node_private_t *nodepriv = a->_private; + + pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_modified); + pcmk__clear_xml_flags(nodepriv, pcmk__xf_deleted); + mark_xml_node_dirty(parent); +} + +#define XML_DOC_PRIVATE_MAGIC 0x81726354UL +#define XML_NODE_PRIVATE_MAGIC 0x54637281UL + +// Free an XML object previously marked as deleted +static void +free_deleted_object(void *data) +{ + if(data) { + pcmk__deleted_xml_t *deleted_obj = data; + + free(deleted_obj->path); + free(deleted_obj); + } +} + +// Free and NULL user, ACLs, and deleted objects in an XML node's private data +static void +reset_xml_private_data(xml_doc_private_t *docpriv) +{ + if (docpriv != NULL) { + CRM_ASSERT(docpriv->check == XML_DOC_PRIVATE_MAGIC); + + free(docpriv->user); + docpriv->user = NULL; + + if (docpriv->acls != NULL) { + pcmk__free_acls(docpriv->acls); + docpriv->acls = NULL; + } + + if(docpriv->deleted_objs) { + g_list_free_full(docpriv->deleted_objs, free_deleted_object); + docpriv->deleted_objs = NULL; + } + } +} + +// Free all private data associated with an XML node +static void +free_private_data(xmlNode *node) +{ + /* Note: + + This function frees private data assosciated with an XML node, + unless the function is being called as a result of internal + XSLT cleanup. + + That could happen through, for example, the following chain of + function calls: + + xsltApplyStylesheetInternal + -> xsltFreeTransformContext + -> xsltFreeRVTs + -> xmlFreeDoc + + And in that case, the node would fulfill three conditions: + + 1. It would be a standalone document (i.e. it wouldn't be + part of a document) + 2. It would have a space-prefixed name (for reference, please + see xsltInternals.h: XSLT_MARK_RES_TREE_FRAG) + 3. It would carry its own payload in the _private field. + + We do not free data in this circumstance to avoid a failed + assertion on the XML_*_PRIVATE_MAGIC later. + + */ + if (node->name == NULL || node->name[0] != ' ') { + if (node->_private) { + if (node->type == XML_DOCUMENT_NODE) { + reset_xml_private_data(node->_private); + } else { + CRM_ASSERT(((xml_node_private_t *) node->_private)->check + == XML_NODE_PRIVATE_MAGIC); + /* nothing dynamically allocated nested */ + } + free(node->_private); + node->_private = NULL; + } + } +} + +// Allocate and initialize private data for an XML node +static void +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); + 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); + node->_private = docpriv; + break; + } + 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); + 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); + node->_private = nodepriv; + if (pcmk__tracking_xml_changes(node, FALSE)) { + /* XML_ELEMENT_NODE doesn't get picked up here, node->doc is + * not hooked up at the point we are called + */ + mark_xml_node_dirty(node); + } + break; + } + case XML_TEXT_NODE: + case XML_DTD_NODE: + case XML_CDATA_SECTION_NODE: + break; + default: + /* Ignore */ + crm_trace("Ignoring %p %d", node, node->type); + CRM_LOG_ASSERT(node->type == XML_ELEMENT_NODE); + break; + } +} + +void +xml_track_changes(xmlNode * xml, const char *user, xmlNode *acl_source, bool enforce_acls) +{ + xml_accept_changes(xml); + crm_trace("Tracking changes%s to %p", enforce_acls?" with ACLs":"", xml); + pcmk__set_xml_doc_flag(xml, pcmk__xf_tracking); + if(enforce_acls) { + if(acl_source == NULL) { + acl_source = xml; + } + pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_enabled); + pcmk__unpack_acl(acl_source, xml, user); + pcmk__apply_acl(xml); + } +} + +bool xml_tracking_changes(xmlNode * xml) +{ + return (xml != NULL) && (xml->doc != NULL) && (xml->doc->_private != NULL) + && pcmk_is_set(((xml_doc_private_t *)(xml->doc->_private))->flags, + pcmk__xf_tracking); +} + +bool xml_document_dirty(xmlNode *xml) +{ + return (xml != NULL) && (xml->doc != NULL) && (xml->doc->_private != NULL) + && pcmk_is_set(((xml_doc_private_t *)(xml->doc->_private))->flags, + pcmk__xf_dirty); +} + +/*! + * \internal + * \brief Return ordinal position of an XML node among its siblings + * + * \param[in] xml XML node to check + * \param[in] ignore_if_set Don't count siblings with this flag set + * + * \return Ordinal position of \p xml (starting with 0) + */ +int +pcmk__xml_position(const xmlNode *xml, enum xml_private_flags ignore_if_set) +{ + int position = 0; + + for (const xmlNode *cIter = xml; cIter->prev; cIter = cIter->prev) { + xml_node_private_t *nodepriv = ((xmlNode*)cIter->prev)->_private; + + if (!pcmk_is_set(nodepriv->flags, ignore_if_set)) { + position++; + } + } + + return position; +} + +// This also clears attribute's flags if not marked as deleted +static bool +marked_as_deleted(xmlAttrPtr a, void *user_data) +{ + xml_node_private_t *nodepriv = a->_private; + + if (pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) { + return true; + } + nodepriv->flags = pcmk__xf_none; + return false; +} + +// Remove all attributes marked as deleted from an XML node +static void +accept_attr_deletions(xmlNode *xml) +{ + // 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 + pcmk__xe_remove_matching_attrs(xml, 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); + } +} + +/*! + * \internal + * \brief Find first child XML node matching another given XML node + * + * \param[in] haystack XML whose children should be checked + * \param[in] needle XML to match (comment content or element name and ID) + * \param[in] exact If true and needle is a comment, position must match + */ +xmlNode * +pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle, bool exact) +{ + CRM_CHECK(needle != NULL, return NULL); + + if (needle->type == XML_COMMENT_NODE) { + return pcmk__xc_match(haystack, needle, exact); + + } else { + const char *id = ID(needle); + const char *attr = (id == NULL)? NULL : XML_ATTR_ID; + + return pcmk__xe_match(haystack, crm_element_name(needle), attr, id); + } +} + +void +xml_accept_changes(xmlNode * xml) +{ + xmlNode *top = NULL; + xml_doc_private_t *docpriv = NULL; + + if(xml == NULL) { + return; + } + + crm_trace("Accepting changes to %p", xml); + docpriv = xml->doc->_private; + top = xmlDocGetRootElement(xml->doc); + + reset_xml_private_data(xml->doc->_private); + + if (!pcmk_is_set(docpriv->flags, pcmk__xf_dirty)) { + docpriv->flags = pcmk__xf_none; + return; + } + + 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 = "NULL"; + + if (root != NULL) { + name = crm_element_name(root); + } + + 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) { +/* crm_trace("returning node (%s).", crm_element_name(a_child)); */ + 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; +} + +#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 + * 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 + */ +xmlNode * +pcmk__xe_match(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); + + 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_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 : ""), + crm_element_name(parent)); + return NULL; +} + +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; + } + } + } + + return; +} + +/*! + * \brief Parse integer assignment statements on this node and all its child + * nodes + * + * \param[in,out] target Root XML node to be processed + * + * \note This function is recursive + */ +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__xml_first_child(target); child != NULL; + child = pcmk__xml_next(child)) { + fix_plus_plus_recursive(child); + } +} + +/*! + * \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. + */ +void +expand_plus_plus(xmlNode * target, const char *name, const char *value) +{ + int offset = 1; + int name_len = 0; + int int_value = 0; + int value_len = 0; + + const char *old_value = NULL; + + if (target == NULL || value == NULL || name == NULL) { + return; + } + + old_value = crm_element_value(target, name); + + if (old_value == NULL) { + /* if no previous value, set unexpanded */ + goto set_unexpanded; + + } else if (strstr(value, name) != value) { + goto set_unexpanded; + } + + 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; + } + + /* 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); + } + + if (value[name_len + 1] != '+') { + const char *offset_s = value + (name_len + 2); + + offset = char2score(offset_s); + } + int_value += offset; + + if (int_value > INFINITY) { + int_value = (int)INFINITY; + } + + crm_xml_add_int(target, name, int_value); + return; + + set_unexpanded: + if (old_value == value) { + /* the old value is already set, nothing to do */ + return; + } + crm_xml_add(target, name, value); + return; +} + +/*! + * \internal + * \brief Remove an XML element's attributes that match some criteria + * + * \param[in,out] element XML element to modify + * \param[in] match If not NULL, only remove attributes for which + * this function returns true + * \param[in,out] user_data Data to pass to \p match + */ +void +pcmk__xe_remove_matching_attrs(xmlNode *element, + bool (*match)(xmlAttrPtr, void *), + void *user_data) +{ + xmlAttrPtr next = NULL; + + 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); + } + } + } +} + +xmlDoc * +getDocPtr(xmlNode * node) +{ + xmlDoc *doc = NULL; + + CRM_CHECK(node != NULL, return NULL); + + doc = node->doc; + if (doc == NULL) { + doc = xmlNewDoc((pcmkXmlStr) "1.0"); + xmlDocSetRootElement(doc, node); + xmlSetTreeDoc(node, doc); + } + return doc; +} + +xmlNode * +add_node_copy(xmlNode * parent, xmlNode * src_node) +{ + xmlNode *child = NULL; + xmlDoc *doc = getDocPtr(parent); + + CRM_CHECK(src_node != NULL, return NULL); + + child = xmlDocCopyNode(src_node, doc, 1); + xmlAddChild(parent, child); + pcmk__mark_xml_created(child); + return child; +} + +xmlNode * +create_xml_node(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; + } + + if (parent == NULL) { + doc = xmlNewDoc((pcmkXmlStr) "1.0"); + node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL); + xmlDocSetRootElement(doc, node); + + } else { + doc = getDocPtr(parent); + node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL); + xmlAddChild(parent, node); + } + pcmk__mark_xml_created(node); + return node; +} + +xmlNode * +pcmk_create_xml_text_node(xmlNode * parent, const char *name, const char *content) +{ + xmlNode *node = create_xml_node(parent, name); + + if (node != NULL) { + xmlNodeSetContent(node, (pcmkXmlStr) 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_create_xml_text_node(parent, element_name, text); + + if (class_name != NULL) { + crm_xml_add(node, "class", class_name); + } + + if (id != NULL) { + crm_xml_add(node, "id", id); + } + + return node; +} + +/*! + * Free an XML element and all of its children, removing it from its parent + * + * \param[in,out] xml XML element to free + */ +void +pcmk_free_xml_subtree(xmlNode *xml) +{ + xmlUnlinkNode(xml); // Detaches from parent and siblings + xmlFreeNode(xml); // Frees +} + +static void +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); + } + + if (doc != NULL && top == child) { + /* Free everything */ + xmlFreeDoc(doc); + + } else if (pcmk__check_acl(child, NULL, pcmk__xf_acl_write) == FALSE) { + GString *xpath = NULL; + + 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; + + } else { + if (doc && pcmk__tracking_xml_changes(child, FALSE) + && !pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { + + GString *xpath = pcmk__element_xpath(child); + + if (xpath != NULL) { + pcmk__deleted_xml_t *deleted_obj = NULL; + + crm_trace("Deleting %s %p from %p", + (const char *) xpath->str, child, doc); + + deleted_obj = calloc(1, sizeof(pcmk__deleted_xml_t)); + deleted_obj->path = strdup((const char *) xpath->str); + + CRM_ASSERT(deleted_obj->path != NULL); + g_string_free(xpath, TRUE); + + 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; + + } else { + deleted_obj->position = pcmk__xml_position(child, + pcmk__xf_skip); + } + } + + docpriv = doc->_private; + 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); + } + } +} + + +void +free_xml(xmlNode * child) +{ + free_xml_with_position(child, -1); +} + +xmlNode * +copy_xml(xmlNode * src) +{ + xmlDoc *doc = xmlNewDoc((pcmkXmlStr) "1.0"); + xmlNode *copy = xmlDocCopyNode(src, doc, 1); + + CRM_ASSERT(copy != NULL); + xmlDocSetRootElement(doc, copy); + xmlSetTreeDoc(copy, doc); + return copy; +} + +xmlNode * +string2xml(const char *input) +{ + xmlNode *xml = NULL; + xmlDocPtr output = NULL; + xmlParserCtxtPtr ctxt = NULL; + xmlErrorPtr 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); + 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) +{ + char *buffer = NULL; + int rc = 0; + size_t length = 0, read_len = 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); + if (rc != BZ_OK) { + crm_err("Could not prepare to read compressed %s: %s " + CRM_XS " bzerror=%d", filename, bz2_strerror(rc), rc); + BZ2_bzReadClose(&rc, bz_file); + fclose(input); + return 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); + + crm_trace("Read %ld bytes from file: %d", (long)read_len, rc); + + if (rc == BZ_OK || rc == BZ_STREAM_END) { + length += read_len; + } + } + + buffer[length] = '\0'; + + if (rc != BZ_STREAM_END) { + crm_err("Could not read compressed %s: %s " + CRM_XS " bzerror=%d", filename, bz2_strerror(rc), rc); + free(buffer); + buffer = NULL; + } + + BZ2_bzReadClose(&rc, bz_file); + fclose(input); + return buffer; +} + +/*! + * \internal + * \brief Remove XML text nodes from specified XML and all its children + * + * \param[in,out] xml XML to strip text from + */ +void +pcmk__strip_xml_text(xmlNode *xml) +{ + xmlNode *iter = xml->children; + + while (iter) { + xmlNode *next = iter->next; + + switch (iter->type) { + case XML_TEXT_NODE: + /* Remove it */ + pcmk_free_xml_subtree(iter); + break; + + case XML_ELEMENT_NODE: + /* Search it */ + pcmk__strip_xml_text(iter); + break; + + default: + /* Leave it */ + break; + } + + iter = next; + } +} + +xmlNode * +filename2xml(const char *filename) +{ + xmlNode *xml = NULL; + xmlDocPtr output = NULL; + bool uncompressed = true; + xmlParserCtxtPtr ctxt = NULL; + xmlErrorPtr 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); + + } else if (uncompressed) { + output = xmlCtxtReadFile(ctxt, filename, NULL, PCMK__XML_PARSE_OPTS); + + } else { + char *input = decompress_file(filename); + + output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL, + PCMK__XML_PARSE_OPTS); + 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 + * + * \param[in,out] xe XML element to add attribute to + * + * \return Value that was set, or NULL on error + */ +const char * +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, + pcmk__s(now_s, "Could not determine current time")); + free(now_s); + return result; +} + +/*! + * \brief Sanitize a string so it is usable as an XML ID + * + * \param[in,out] id String to sanitize + */ +void +crm_xml_sanitize_id(char *id) +{ + char *c; + + for (c = id; *c; ++c) { + /* @TODO Sanitize more comprehensively */ + switch (*c) { + case ':': + case '#': + *c = '.'; + } + } +} + +/*! + * \brief Set the ID of an XML element using a format + * + * \param[in,out] xml XML element + * \param[in] fmt printf-style format + * \param[in] ... any arguments required by format + */ +void +crm_xml_set_id(xmlNode *xml, const char *format, ...) +{ + va_list ap; + int len = 0; + char *id = NULL; + + /* equivalent to crm_strdup_printf() */ + va_start(ap, format); + len = vasprintf(&id, format, ap); + va_end(ap); + CRM_ASSERT(len > 0); + + crm_xml_sanitize_id(id); + crm_xml_add(xml, XML_ATTR_ID, id); + free(id); +} + +/*! + * \internal + * \brief Write XML to a file stream + * + * \param[in] xml_node 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 + * + * \return Standard Pacemaker return code + */ +static int +write_xml_stream(xmlNode *xml_node, 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_node, "writing"); + + buffer = dump_xml_formatted(xml_node); + CRM_CHECK(buffer && strlen(buffer), + crm_log_xml_warn(xml_node, "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); + if (rc != BZ_OK) { + crm_warn("Not compressing %s: could not prepare file stream: %s " + CRM_XS " bzerror=%d", filename, bz2_strerror(rc), rc); + } else { + BZ2_bzWrite(&rc, bz_file, buffer, strlen(buffer)); + if (rc != BZ_OK) { + crm_warn("Not compressing %s: could not compress data: %s " + CRM_XS " bzerror=%d errno=%d", + filename, bz2_strerror(rc), rc, errno); + } + } + + if (rc == BZ_OK) { + BZ2_bzWriteClose(&rc, bz_file, 0, &in, nbytes); + if (rc != BZ_OK) { + crm_warn("Not compressing %s: could not write compressed data: %s " + CRM_XS " bzerror=%d errno=%d", + filename, bz2_strerror(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_node 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 + * + * \return Number of bytes written on success, -errno otherwise + */ +int +write_xml_fd(xmlNode * xml_node, const char *filename, int fd, gboolean compress) +{ + FILE *stream = NULL; + unsigned int nbytes = 0; + int rc = pcmk_rc_ok; + + CRM_CHECK(xml_node && (fd > 0), return -EINVAL); + stream = fdopen(fd, "w"); + if (stream == NULL) { + return -errno; + } + rc = write_xml_stream(xml_node, 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_node 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(xmlNode * xml_node, const char *filename, gboolean compress) +{ + FILE *stream = NULL; + unsigned int nbytes = 0; + int rc = pcmk_rc_ok; + + CRM_CHECK(xml_node && filename, return -EINVAL); + stream = fopen(filename, "w"); + if (stream == NULL) { + return -errno; + } + rc = write_xml_stream(xml_node, filename, stream, compress, &nbytes); + if (rc != pcmk_rc_ok) { + return pcmk_rc2legacy(rc); + } + return (int) nbytes; +} + +// 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 + + *length += offset; + text = pcmk__realloc(text, *length); + + for (size_t lpc = (*length) - 1; lpc > (start + offset); lpc--) { + text[lpc] = text[lpc - offset]; + } + + memcpy(text + start, replace, offset + 1); + return text; +} + +/*! + * \brief Replace special characters with their XML escape sequences + * + * \param[in] text Text to escape + * + * \return Newly allocated string equivalent to \p text but with special + * characters replaced with XML escape sequences (or NULL if \p text + * is NULL) + */ +char * +crm_xml_escape(const char *text) +{ + 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. + */ + + if (text == NULL) { + return NULL; + } + + 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]); + + copy = replace_text(copy, index, &length, replace); + free(replace); + } + } + } + return copy; +} + +/*! + * \internal + * \brief Append an XML attribute to a buffer + * + * \param[in] attr Attribute to append + * \param[in,out] buffer Where to append the content (must not be \p NULL) + */ +static void +dump_xml_attr(const xmlAttr *attr, GString *buffer) +{ + char *p_value = NULL; + const char *p_name = NULL; + xml_node_private_t *nodepriv = NULL; + + if (attr == NULL || attr->children == NULL) { + return; + } + + nodepriv = attr->_private; + if (nodepriv && pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) { + 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); + + free(p_value); +} + +/*! + * \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) +{ + const char *name = crm_element_name(data); + 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; + + CRM_ASSERT(name != NULL); + + for (int lpc = 0; lpc < spaces; lpc++) { + g_string_append_c(buffer, ' '); + } + + pcmk__g_strcat(buffer, "<", 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))) { + 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) { + xmlNode *xChild = NULL; + for(xChild = data->children; xChild != NULL; xChild = xChild->next) { + pcmk__xml2text(xChild, options, buffer, depth + 1); + } + + for (int lpc = 0; lpc < spaces; lpc++) { + g_string_append_c(buffer, ' '); + } + + pcmk__g_strcat(buffer, "</", 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) +{ + /* @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'); + } +} + +/*! + * \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'); + } +} + +#define PCMK__XMLDUMP_STATS 0 + +/*! + * \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(xmlNodePtr 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); + + if (pcmk_is_set(options, pcmk__xml_fmt_full)) { + /* libxml's serialization reuse is a good idea, sadly we cannot + apply it for the filtered cases (preceding filtering pass + would preclude further reuse of such in-situ modified XML + in generic context and is likely not a win performance-wise), + and there's also a historically unstable throughput argument + (likely stemming from memory allocation overhead, eventhough + that shall be minimized with defaults preset in crm_xml_init) */ +#if (PCMK__XMLDUMP_STATS - 0) + time_t next, new = time(NULL); +#endif + xmlDoc *doc; + xmlOutputBuffer *xml_buffer; + + doc = getDocPtr(data); + /* doc will only be NULL if data is */ + CRM_CHECK(doc != NULL, return); + + xml_buffer = xmlAllocOutputBuffer(NULL); + CRM_ASSERT(xml_buffer != NULL); + + /* XXX we could setup custom allocation scheme for the particular + buffer, but it's subsumed with crm_xml_init that needs to + be invoked prior to entering this function as such, since + its other branch vitally depends on it -- what can be done + about this all is to have a facade parsing functions that + would 100% mark entering libxml code for us, since we don't + do anything as crazy as swapping out the binary form of the + parsed tree (but those would need to be strictly used as + opposed to libxml's raw functions) */ + + xmlNodeDumpOutput(xml_buffer, doc, data, 0, + pcmk_is_set(options, pcmk__xml_fmt_pretty), NULL); + /* attempt adding final NL - failing shouldn't be fatal here */ + (void) xmlOutputBufferWrite(xml_buffer, sizeof("\n") - 1, "\n"); + if (xml_buffer->buffer != NULL) { + g_string_append(buffer, + (const gchar *) xmlBufContent(xml_buffer->buffer)); + } + +#if (PCMK__XMLDUMP_STATS - 0) + next = time(NULL); + if ((now + 1) < next) { + crm_log_xml_trace(data, "Long time"); + crm_err("xmlNodeDumpOutput() -> %lld bytes took %ds", + (long long) buffer->len, next - now); + } +#endif + + /* asserted allocation before so there should be something to remove */ + (void) xmlOutputBufferClose(xml_buffer); + return; + } + + 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)) { + /* @COMPAT: Remove when log_data_element() is removed. There are + * no other internal code paths that set pcmk__xml_fmt_text. + * Keep an empty case handler so that we don't log an unhandled + * type warning. + */ + 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("Unhandled type: %d", data->type); + break; + + /* + XML_ATTRIBUTE_NODE = 2 + XML_ENTITY_REF_NODE = 5 + XML_ENTITY_NODE = 6 + XML_PI_NODE = 7 + XML_DOCUMENT_NODE = 9 + XML_DOCUMENT_TYPE_NODE = 10 + XML_DOCUMENT_FRAG_NODE = 11 + XML_NOTATION_NODE = 12 + XML_HTML_DOCUMENT_NODE = 13 + XML_DTD_NODE = 14 + XML_ELEMENT_DECL = 15 + XML_ATTRIBUTE_DECL = 16 + XML_ENTITY_DECL = 17 + XML_NAMESPACE_DECL = 18 + XML_XINCLUDE_START = 19 + XML_XINCLUDE_END = 20 + XML_DOCB_DOCUMENT_NODE = 21 + */ + } +} + +char * +dump_xml_formatted_with_text(xmlNode * an_xml_node) +{ + char *buffer = NULL; + GString *g_buffer = g_string_sized_new(1024); + + pcmk__xml2text(an_xml_node, pcmk__xml_fmt_pretty|pcmk__xml_fmt_full, + g_buffer, 0); + + pcmk__str_update(&buffer, g_buffer->str); + g_string_free(g_buffer, TRUE); + return buffer; +} + +char * +dump_xml_formatted(xmlNode * an_xml_node) +{ + char *buffer = NULL; + GString *g_buffer = g_string_sized_new(1024); + + pcmk__xml2text(an_xml_node, 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(xmlNode * an_xml_node) +{ + char *buffer = NULL; + GString *g_buffer = g_string_sized_new(1024); + + pcmk__xml2text(an_xml_node, 0, g_buffer, 0); + + pcmk__str_update(&buffer, g_buffer->str); + g_string_free(g_buffer, TRUE); + return buffer; +} + +gboolean +xml_has_children(const xmlNode * xml_root) +{ + if (xml_root != NULL && xml_root->children != NULL) { + return TRUE; + } + return FALSE; +} + +void +xml_remove_prop(xmlNode * obj, const char *name) +{ + 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(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); +} + +/*! + * \internal + * \brief Set a flag on all attributes of an XML element + * + * \param[in,out] xml XML node to set flags on + * \param[in] flag XML private flag to set + */ +static void +set_attrs_flag(xmlNode *xml, enum xml_private_flags flag) +{ + for (xmlAttr *attr = pcmk__xe_first_attr(xml); attr; attr = attr->next) { + pcmk__set_xml_flags((xml_node_private_t *) (attr->_private), flag); + } +} + +/*! + * \internal + * \brief Add an XML attribute to a node, marked as deleted + * + * When calculating XML changes, we need to know when an attribute has been + * deleted. Add the attribute back to the new XML, so that we can check the + * removal against ACLs, and mark it as deleted for later removal after + * differences have been calculated. + * + * \param[in,out] new_xml XML to modify + * \param[in] element Name of XML element that changed (for logging) + * \param[in] attr_name Name of attribute that was deleted + * \param[in] old_value Value of attribute that was deleted + */ +static void +mark_attr_deleted(xmlNode *new_xml, const char *element, const char *attr_name, + const char *old_value) +{ + xml_doc_private_t *docpriv = new_xml->doc->_private; + xmlAttr *attr = NULL; + xml_node_private_t *nodepriv; + + // Prevent the dirty flag being set recursively upwards + pcmk__clear_xml_flags(docpriv, pcmk__xf_tracking); + + // Restore the old value (and the tracking flag) + attr = xmlSetProp(new_xml, (pcmkXmlStr) attr_name, (pcmkXmlStr) old_value); + pcmk__set_xml_flags(docpriv, pcmk__xf_tracking); + + // Reset flags (so the attribute doesn't appear as newly created) + nodepriv = attr->_private; + nodepriv->flags = 0; + + // Check ACLs and mark restored value for later removal + xml_remove_prop(new_xml, attr_name); + + crm_trace("XML attribute %s=%s was removed from %s", + attr_name, old_value, element); +} + +/* + * \internal + * \brief Check ACLs for a changed XML attribute + */ +static void +mark_attr_changed(xmlNode *new_xml, const char *element, const char *attr_name, + const char *old_value) +{ + char *vcopy = crm_element_value_copy(new_xml, attr_name); + + crm_trace("XML attribute %s was changed from '%s' to '%s' in %s", + attr_name, old_value, vcopy, element); + + // Restore the original value + xmlSetProp(new_xml, (pcmkXmlStr) attr_name, (pcmkXmlStr) old_value); + + // Change it back to the new value, to check ACLs + crm_xml_add(new_xml, attr_name, vcopy); + free(vcopy); +} + +/*! + * \internal + * \brief Mark an XML attribute as having changed position + * + * \param[in,out] new_xml XML to modify + * \param[in] element Name of XML element that changed (for logging) + * \param[in,out] old_attr Attribute that moved, in original XML + * \param[in,out] new_attr Attribute that moved, in \p new_xml + * \param[in] p_old Ordinal position of \p old_attr in original XML + * \param[in] p_new Ordinal position of \p new_attr in \p new_xml + */ +static void +mark_attr_moved(xmlNode *new_xml, const char *element, xmlAttr *old_attr, + xmlAttr *new_attr, int p_old, int p_new) +{ + xml_node_private_t *nodepriv = new_attr->_private; + + crm_trace("XML attribute %s moved from position %d to %d in %s", + old_attr->name, p_old, p_new, element); + + // Mark document, element, and all element's parents as changed + mark_xml_node_dirty(new_xml); + + // Mark attribute as changed + pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_moved); + + nodepriv = (p_old > p_new)? old_attr->_private : new_attr->_private; + pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); +} + +/*! + * \internal + * \brief Calculate differences in all previously existing XML attributes + * + * \param[in,out] old_xml Original XML to compare + * \param[in,out] new_xml New XML to compare + */ +static void +xml_diff_old_attrs(xmlNode *old_xml, xmlNode *new_xml) +{ + xmlAttr *attr_iter = pcmk__xe_first_attr(old_xml); + + while (attr_iter != NULL) { + xmlAttr *old_attr = attr_iter; + xmlAttr *new_attr = xmlHasProp(new_xml, attr_iter->name); + const char *name = (const char *) attr_iter->name; + const char *old_value = crm_element_value(old_xml, name); + + attr_iter = attr_iter->next; + if (new_attr == NULL) { + mark_attr_deleted(new_xml, (const char *) old_xml->name, name, + old_value); + + } else { + xml_node_private_t *nodepriv = new_attr->_private; + int new_pos = pcmk__xml_position((xmlNode*) new_attr, + pcmk__xf_skip); + int old_pos = pcmk__xml_position((xmlNode*) old_attr, + pcmk__xf_skip); + const char *new_value = crm_element_value(new_xml, name); + + // This attribute isn't new + pcmk__clear_xml_flags(nodepriv, pcmk__xf_created); + + if (strcmp(new_value, old_value) != 0) { + mark_attr_changed(new_xml, (const char *) old_xml->name, name, + old_value); + + } else if ((old_pos != new_pos) + && !pcmk__tracking_xml_changes(new_xml, TRUE)) { + mark_attr_moved(new_xml, (const char *) old_xml->name, + old_attr, new_attr, old_pos, new_pos); + } + } + } +} + +/*! + * \internal + * \brief Check all attributes in new XML for creation + * + * For each of a given XML element's attributes marked as newly created, accept + * (and mark as dirty) or reject the creation according to ACLs. + * + * \param[in,out] new_xml XML to check + */ +static void +mark_created_attrs(xmlNode *new_xml) +{ + xmlAttr *attr_iter = pcmk__xe_first_attr(new_xml); + + while (attr_iter != NULL) { + xmlAttr *new_attr = attr_iter; + xml_node_private_t *nodepriv = attr_iter->_private; + + attr_iter = attr_iter->next; + if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { + const char *attr_name = (const char *) new_attr->name; + + crm_trace("Created new attribute %s=%s in %s", + attr_name, crm_element_value(new_xml, attr_name), + new_xml->name); + + /* Check ACLs (we can't use the remove-then-create trick because it + * would modify the attribute position). + */ + if (pcmk__check_acl(new_xml, attr_name, pcmk__xf_acl_write)) { + pcmk__mark_xml_attr_dirty(new_attr); + } else { + // Creation was not allowed, so remove the attribute + xmlUnsetProp(new_xml, new_attr->name); + } + } + } +} + +/*! + * \internal + * \brief Calculate differences in attributes between two XML nodes + * + * \param[in,out] old_xml Original XML to compare + * \param[in,out] new_xml New XML to compare + */ +static void +xml_diff_attrs(xmlNode *old_xml, xmlNode *new_xml) +{ + set_attrs_flag(new_xml, pcmk__xf_created); // cleared later if not really new + xml_diff_old_attrs(old_xml, new_xml); + mark_created_attrs(new_xml); +} + +/*! + * \internal + * \brief Add an XML child element to a node, marked as deleted + * + * When calculating XML changes, we need to know when a child element has been + * deleted. Add the child back to the new XML, so that we can check the removal + * against ACLs, and mark it as deleted for later removal after differences have + * been calculated. + * + * \param[in,out] old_child Child element from original XML + * \param[in,out] new_parent New XML to add marked copy to + */ +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); + + // Clear flags on new child and its children + reset_xml_node_flags(candidate); + + // Check whether ACLs allow the deletion + pcmk__apply_acl(xmlDocGetRootElement(candidate->doc)); + + // Remove the child again (which will track it in document's deleted_objs) + free_xml_with_position(candidate, + pcmk__xml_position(old_child, pcmk__xf_skip)); + + if (pcmk__xml_match(new_parent, old_child, true) == NULL) { + pcmk__set_xml_flags((xml_node_private_t *) (old_child->_private), + pcmk__xf_skip); + } +} + +static void +mark_child_moved(xmlNode *old_child, xmlNode *new_parent, xmlNode *new_child, + int p_old, int p_new) +{ + 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>"), + p_old, p_new, new_parent->name); + mark_xml_node_dirty(new_parent); + pcmk__set_xml_flags(nodepriv, pcmk__xf_moved); + + if (p_old > p_new) { + nodepriv = old_child->_private; + } else { + nodepriv = new_child->_private; + } + pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); +} + +// Given original and new XML, mark new XML portions that have changed +static void +mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top) +{ + xmlNode *cIter = NULL; + xml_node_private_t *nodepriv = NULL; + + CRM_CHECK(new_xml != NULL, return); + if (old_xml == NULL) { + pcmk__mark_xml_created(new_xml); + pcmk__apply_creation_acl(new_xml, check_top); + return; + } + + nodepriv = new_xml->_private; + CRM_CHECK(nodepriv != NULL, return); + + if(nodepriv->flags & pcmk__xf_processed) { + /* Avoid re-comparing nodes */ + return; + } + pcmk__set_xml_flags(nodepriv, pcmk__xf_processed); + + 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); + + cIter = pcmk__xml_next(cIter); + if(new_child) { + mark_xml_changes(old_child, new_child, TRUE); + + } else { + mark_child_deleted(old_child, new_xml); + } + } + + // 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); + + cIter = pcmk__xml_next(cIter); + 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); + + } else { + /* Check for movement, we already checked for differences */ + int p_new = pcmk__xml_position(new_child, pcmk__xf_skip); + int p_old = pcmk__xml_position(old_child, pcmk__xf_skip); + + if(p_old != p_new) { + mark_child_moved(old_child, new_xml, new_child, p_old, p_new); + } + } + } +} + +void +xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml) +{ + pcmk__set_xml_doc_flag(new_xml, pcmk__xf_lazy); + xml_calculate_changes(old_xml, new_xml); +} + +// Called functions may set the \p pcmk__xf_skip flag on parts of \p old_xml +void +xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml) +{ + CRM_CHECK(pcmk__str_eq(crm_element_name(old_xml), crm_element_name(new_xml), pcmk__str_casei), + return); + CRM_CHECK(pcmk__str_eq(ID(old_xml), ID(new_xml), pcmk__str_casei), return); + + if(xml_tracking_changes(new_xml) == FALSE) { + xml_track_changes(new_xml, NULL, NULL, FALSE); + } + + mark_xml_changes(old_xml, new_xml, FALSE); +} + +gboolean +can_prune_leaf(xmlNode * xml_node) +{ + xmlNode *cIter = NULL; + gboolean can_prune = TRUE; + const char *name = crm_element_name(xml_node); + + if (pcmk__strcase_any_of(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 + * + * \param[in] root XML to search + * \param[in] search_comment Comment whose content should be searched for + * \param[in] exact If true, comment must also be at same position + */ +xmlNode * +pcmk__xc_match(const xmlNode *root, const xmlNode *search_comment, bool exact) +{ + xmlNode *a_child = NULL; + int search_offset = pcmk__xml_position(search_comment, pcmk__xf_skip); + + CRM_CHECK(search_comment->type == XML_COMMENT_NODE, return NULL); + + for (a_child = pcmk__xml_first_child(root); a_child != NULL; + a_child = pcmk__xml_next(a_child)) { + if (exact) { + int offset = pcmk__xml_position(a_child, pcmk__xf_skip); + xml_node_private_t *nodepriv = a_child->_private; + + if (offset < search_offset) { + continue; + + } else if (offset > search_offset) { + return NULL; + } + + if (pcmk_is_set(nodepriv->flags, pcmk__xf_skip)) { + continue; + } + } + + if (a_child->type == XML_COMMENT_NODE + && pcmk__str_eq((const char *)a_child->content, (const char *)search_comment->content, pcmk__str_casei)) { + return a_child; + + } else if (exact) { + return NULL; + } + } + + return NULL; +} + +/*! + * \internal + * \brief Make one XML comment match another (in content) + * + * \param[in,out] parent If \p target is NULL and this is not, add or update + * comment child of this XML node that matches \p update + * \param[in,out] target If not NULL, update this XML comment node + * \param[in] update Make comment content match this (must not be NULL) + * + * \note At least one of \parent and \target must be non-NULL + */ +void +pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update) +{ + CRM_CHECK(update != NULL, return); + CRM_CHECK(update->type == XML_COMMENT_NODE, return); + + if (target == NULL) { + target = pcmk__xc_match(parent, update, false); + } + + if (target == NULL) { + add_node_copy(parent, update); + + } else if (!pcmk__str_eq((const char *)target->content, (const char *)update->content, pcmk__str_casei)) { + xmlFree(target->content); + target->content = xmlStrdup(update->content); + } +} + +/*! + * \internal + * \brief Make one XML tree match another (in children and attributes) + * + * \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 + * + * \note At least one of \p parent and \p target must be non-NULL + */ +void +pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update, + bool as_diff) +{ + xmlNode *a_child = NULL; + const char *object_name = NULL, + *object_href = NULL, + *object_href_val = NULL; + +#if XML_PARSER_DEBUG + crm_log_xml_trace(update, "update:"); + crm_log_xml_trace(target, "target:"); +#endif + + CRM_CHECK(update != NULL, return); + + if (update->type == XML_COMMENT_NODE) { + pcmk__xc_update(parent, target, update); + return; + } + + object_name = crm_element_name(update); + object_href_val = ID(update); + if (object_href_val != NULL) { + object_href = XML_ATTR_ID; + } else { + object_href_val = crm_element_value(update, XML_ATTR_IDREF); + object_href = (object_href_val == NULL) ? NULL : XML_ATTR_IDREF; + } + + CRM_CHECK(object_name != NULL, return); + CRM_CHECK(target != NULL || parent != NULL, return); + + if (target == NULL) { + target = pcmk__xe_match(parent, object_name, + object_href, object_href_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 : ""); + + } 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 + } + + CRM_CHECK(pcmk__str_eq(crm_element_name(target), crm_element_name(update), + pcmk__str_casei), + return); + + if (as_diff == FALSE) { + /* So that expand_plus_plus() gets called */ + copy_in_properties(target, update); + + } else { + /* No need for expand_plus_plus(), just raw speed */ + for (xmlAttrPtr a = pcmk__xe_first_attr(update); a != NULL; + a = a->next) { + const char *p_value = pcmk__xml_attr_value(a); + + /* Remove it first so the ordering of the update is preserved */ + xmlUnsetProp(target, a->name); + xmlSetProp(target, a->name, (pcmkXmlStr) p_value); + } + } + + 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 +} + +gboolean +update_xml_child(xmlNode * child, xmlNode * to_update) +{ + gboolean can_update = TRUE; + xmlNode *child_of_child = NULL; + + CRM_CHECK(child != NULL, return FALSE); + CRM_CHECK(to_update != NULL, return FALSE); + + if (!pcmk__str_eq(crm_element_name(to_update), crm_element_name(child), pcmk__str_none)) { + can_update = FALSE; + + } else if (!pcmk__str_eq(ID(to_update), ID(child), pcmk__str_none)) { + can_update = FALSE; + + } 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); + } + + 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; + } + can_update = update_xml_child(child_of_child, to_update); + } + + return can_update; +} + +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__str_eq(tag, crm_element_name(root), pcmk__str_casei)) { + + } 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__); + } + add_node_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; +} + +gboolean +replace_xml_child(xmlNode * parent, xmlNode * child, xmlNode * update, gboolean delete_only) +{ + gboolean can_delete = FALSE; + xmlNode *child_of_child = NULL; + + const char *up_id = NULL; + const char *child_id = NULL; + const char *right_val = NULL; + + CRM_CHECK(child != NULL, return FALSE); + CRM_CHECK(update != NULL, return FALSE); + + up_id = ID(update); + child_id = ID(child); + + if (up_id == NULL || (child_id && strcmp(child_id, up_id) == 0)) { + can_delete = TRUE; + } + if (!pcmk__str_eq(crm_element_name(update), crm_element_name(child), pcmk__str_casei)) { + 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; + } + } + } + + if (can_delete && parent != NULL) { + crm_log_xml_trace(child, "Delete match found..."); + if (delete_only || update == NULL) { + free_xml(child); + + } else { + xmlNode *tmp = copy_xml(update); + xmlDoc *doc = tmp->doc; + xmlNode *old = NULL; + + xml_accept_changes(tmp); + old = xmlReplaceNode(child, tmp); + + if(xml_tracking_changes(tmp)) { + /* Replaced sections may have included relevant ACLs */ + pcmk__apply_acl(tmp); + } + + xml_calculate_changes(old, tmp); + xmlDocSetRootElement(doc, old); + free_xml(old); + } + child = NULL; + return TRUE; + + } else if (can_delete) { + crm_log_xml_debug(child, "Cannot delete the search root"); + can_delete = FALSE; + } + + child_of_child = pcmk__xml_first_child(child); + while (child_of_child) { + xmlNode *next = pcmk__xml_next(child_of_child); + + can_delete = replace_xml_child(child, child_of_child, update, delete_only); + + /* only delete the first one */ + if (can_delete) { + child_of_child = NULL; + } else { + child_of_child = next; + } + } + + return can_delete; +} + +xmlNode * +sorted_xml(xmlNode *input, xmlNode *parent, gboolean recursive) +{ + xmlNode *child = NULL; + GSList *nvpairs = NULL; + xmlNode *result = NULL; + const char *name = NULL; + + CRM_CHECK(input != NULL, return NULL); + + name = crm_element_name(input); + CRM_CHECK(name != NULL, return NULL); + + result = create_xml_node(parent, 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)) { + + if (recursive) { + sorted_xml(child, result, recursive); + } else { + add_node_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 + * + * \param[in] sibling XML tag to start from + * + * \return Next sibling XML tag with same name + */ +xmlNode * +crm_next_same_xml(const xmlNode *sibling) +{ + xmlNode *match = pcmk__xe_next(sibling); + const char *name = crm_element_name(sibling); + + while (match != NULL) { + if (!strcmp(crm_element_name(match), name)) { + return match; + } + match = pcmk__xe_next(match); + } + return NULL; +} + +void +crm_xml_init(void) +{ + static bool init = true; + + if(init) { + init = false; + /* The default allocator XML_BUFFER_ALLOC_EXACT does far too many + * pcmk__realloc()s and it can take upwards of 18 seconds (yes, seconds) + * to dump a 28kb tree which XML_BUFFER_ALLOC_DOUBLEIT can do in + * less than 1 second. + */ + xmlSetBufferAllocationScheme(XML_BUFFER_ALLOC_DOUBLEIT); + + /* Populate and free the _private field when nodes are created and destroyed */ + xmlDeregisterNodeDefault(free_private_data); + xmlRegisterNodeDefault(new_private_data); + + crm_schema_init(); + } +} + +void +crm_xml_cleanup(void) +{ + crm_schema_cleanup(); + xmlCleanupParser(); +} + +#define XPATH_MAX 512 + +xmlNode * +expand_idref(xmlNode * input, xmlNode * top) +{ + const char *tag = NULL; + const char *ref = NULL; + xmlNode *result = input; + + if (result == NULL) { + return NULL; + + } else if (top == NULL) { + top = input; + } + + tag = crm_element_name(result); + ref = crm_element_value(result, XML_ATTR_IDREF); + + if (ref != NULL) { + char *xpath_string = crm_strdup_printf("//%s[@" XML_ATTR_ID "='%s']", + tag, ref); + + result = get_xpath_object(xpath_string, top, LOG_ERR); + if (result == NULL) { + char *nodePath = (char *)xmlGetNodePath(top); + + crm_err("No match for %s found in %s: Invalid configuration", + xpath_string, pcmk__s(nodePath, "unrecognizable path")); + free(nodePath); + } + free(xpath_string); + } + return result; +} + +char * +pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns) +{ + static const char *base = NULL; + char *ret = NULL; + + if (base == NULL) { + base = getenv("PCMK_schema_directory"); + } + if (pcmk__str_empty(base)) { + base = CRM_SCHEMA_DIRECTORY; + } + + switch (ns) { + case pcmk__xml_artefact_ns_legacy_rng: + case pcmk__xml_artefact_ns_legacy_xslt: + ret = strdup(base); + break; + case pcmk__xml_artefact_ns_base_rng: + case pcmk__xml_artefact_ns_base_xslt: + ret = crm_strdup_printf("%s/base", base); + 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) +{ + char *base = pcmk__xml_artefact_root(ns), *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); + break; + case pcmk__xml_artefact_ns_legacy_xslt: + case pcmk__xml_artefact_ns_base_xslt: + ret = crm_strdup_printf("%s/%s.xsl", base, filespec); + break; + default: + crm_err("XML artefact family specified as %u not recognized", ns); + } + free(base); + + return ret; +} + +void +pcmk__xe_set_propv(xmlNodePtr node, va_list pairs) +{ + while (true) { + const char *name, *value; + + name = va_arg(pairs, const char *); + if (name == NULL) { + return; + } + + value = va_arg(pairs, const char *); + if (value != NULL) { + crm_xml_add(node, name, value); + } + } +} + +void +pcmk__xe_set_props(xmlNodePtr node, ...) +{ + va_list pairs; + va_start(pairs, node); + pcmk__xe_set_propv(node, pairs); + va_end(pairs); +} + +int +pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name, + int (*handler)(xmlNode *xml, void *userdata), + void *userdata) +{ + xmlNode *children = (xml? xml->children : NULL); + + 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)) { + int rc = handler(node, userdata); + + if (rc != pcmk_rc_ok) { + return rc; + } + } + } + + return pcmk_rc_ok; +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include <crm/common/xml_compat.h> + +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); +} + +void +crm_destroy_xml(gpointer data) +{ + free_xml(data); +} + +int +add_node_nocopy(xmlNode *parent, const char *name, xmlNode *child) +{ + add_node_copy(parent, child); + free_xml(child); + return 1; +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/xml_display.c b/lib/common/xml_display.c new file mode 100644 index 0000000..e2d46ce --- /dev/null +++ b/lib/common/xml_display.c @@ -0,0 +1,549 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#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" + +static int show_xml_node(pcmk__output_t *out, GString *buffer, + const char *prefix, const xmlNode *data, int depth, + uint32_t options); + +// Log an XML library error +void +pcmk__log_xmllib_err(void *ctx, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + pcmk__if_tracing( + { + PCMK__XML_LOG_BASE(LOG_ERR, TRUE, + crm_abort(__FILE__, __PRETTY_FUNCTION__, + __LINE__, "xml library error", TRUE, + TRUE), + "XML Error: ", fmt, ap); + }, + { + PCMK__XML_LOG_BASE(LOG_ERR, TRUE, 0, "XML Error: ", fmt, ap); + } + ); + va_end(ap); +} + +/*! + * \internal + * \brief Output an XML comment with depth-based indentation + * + * \param[in,out] out Output object + * \param[in] data XML node to output + * \param[in] depth Current indentation level + * \param[in] options Group of \p pcmk__xml_fmt_options flags + * + * \return Standard Pacemaker return code + * + * \note This currently produces output only for text-like output objects. + */ +static int +show_xml_comment(pcmk__output_t *out, const xmlNode *data, int depth, + uint32_t options) +{ + if (pcmk_is_set(options, pcmk__xml_fmt_open)) { + int width = pcmk_is_set(options, pcmk__xml_fmt_pretty)? (2 * depth) : 0; + + return out->info(out, "%*s<!--%s-->", + width, "", (const char *) data->content); + } + return pcmk_rc_no_output; +} + +/*! + * \internal + * \brief Output an XML element in a formatted way + * + * \param[in,out] out Output object + * \param[in,out] buffer Where to build output strings + * \param[in] prefix String to prepend to every line of output + * \param[in] data XML node to output + * \param[in] depth Current indentation level + * \param[in] options Group of \p pcmk__xml_fmt_options flags + * + * \return Standard Pacemaker return code + * + * \note This is a recursive helper function for \p show_xml_node(). + * \note This currently produces output only for text-like output objects. + * \note \p buffer may be overwritten many times. The caller is responsible for + * freeing it using \p g_string_free() but should not rely on its + * contents. + */ +static int +show_xml_element(pcmk__output_t *out, GString *buffer, const char *prefix, + const xmlNode *data, int depth, uint32_t options) +{ + const char *name = crm_element_name(data); + int spaces = pcmk_is_set(options, pcmk__xml_fmt_pretty)? (2 * depth) : 0; + int rc = pcmk_rc_no_output; + + if (pcmk_is_set(options, pcmk__xml_fmt_open)) { + const char *hidden = crm_element_value(data, "hidden"); + + g_string_truncate(buffer, 0); + + for (int lpc = 0; lpc < spaces; lpc++) { + g_string_append_c(buffer, ' '); + } + pcmk__g_strcat(buffer, "<", name, NULL); + + for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL; + attr = attr->next) { + 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; + + if (pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) { + continue; + } + + // @COMPAT Remove when v1 patchsets are removed + if (pcmk_any_flags_set(options, + pcmk__xml_fmt_diff_plus + |pcmk__xml_fmt_diff_minus) + && (strcmp(XML_DIFF_MARKER, p_name) == 0)) { + continue; + } + + if ((hidden != NULL) && (p_name[0] != '\0') + && (strstr(hidden, p_name) != NULL)) { + pcmk__str_update(&p_copy, "*****"); + + } else { + p_copy = crm_xml_escape(p_value); + } + + pcmk__g_strcat(buffer, " ", p_name, "=\"", + pcmk__s(p_copy, "<null>"), "\"", NULL); + free(p_copy); + } + + if (xml_has_children(data) + && pcmk_is_set(options, pcmk__xml_fmt_children)) { + g_string_append_c(buffer, '>'); + + } else { + g_string_append(buffer, "/>"); + } + + rc = out->info(out, "%s%s%s", + pcmk__s(prefix, ""), pcmk__str_empty(prefix)? "" : " ", + buffer->str); + } + + if (!xml_has_children(data)) { + return rc; + } + + if (pcmk_is_set(options, pcmk__xml_fmt_children)) { + for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL; + child = pcmk__xml_next(child)) { + + int temp_rc = show_xml_node(out, buffer, prefix, child, depth + 1, + options + |pcmk__xml_fmt_open + |pcmk__xml_fmt_close); + rc = pcmk__output_select_rc(rc, temp_rc); + } + } + + if (pcmk_is_set(options, pcmk__xml_fmt_close)) { + int temp_rc = out->info(out, "%s%s%*s</%s>", + pcmk__s(prefix, ""), + pcmk__str_empty(prefix)? "" : " ", + spaces, "", name); + rc = pcmk__output_select_rc(rc, temp_rc); + } + + return rc; +} + +/*! + * \internal + * \brief Output an XML element or comment in a formatted way + * + * \param[in,out] out Output object + * \param[in,out] buffer Where to build output strings + * \param[in] prefix String to prepend to every line of output + * \param[in] data XML node to log + * \param[in] depth Current indentation level + * \param[in] options Group of \p pcmk__xml_fmt_options flags + * + * \return Standard Pacemaker return code + * + * \note This is a recursive helper function for \p pcmk__xml_show(). + * \note This currently produces output only for text-like output objects. + * \note \p buffer may be overwritten many times. The caller is responsible for + * freeing it using \p g_string_free() but should not rely on its + * contents. + */ +static int +show_xml_node(pcmk__output_t *out, GString *buffer, const char *prefix, + const xmlNode *data, int depth, uint32_t options) +{ + switch (data->type) { + case XML_COMMENT_NODE: + return show_xml_comment(out, data, depth, options); + case XML_ELEMENT_NODE: + return show_xml_element(out, buffer, prefix, data, depth, options); + default: + return pcmk_rc_no_output; + } +} + +/*! + * \internal + * \brief Output an XML element or comment in a formatted way + * + * \param[in,out] out Output object + * \param[in] prefix String to prepend to every line of output + * \param[in] data XML node to output + * \param[in] depth Current nesting level + * \param[in] options Group of \p pcmk__xml_fmt_options flags + * + * \return Standard Pacemaker return code + * + * \note This currently produces output only for text-like output objects. + */ +int +pcmk__xml_show(pcmk__output_t *out, const char *prefix, const xmlNode *data, + int depth, uint32_t options) +{ + int rc = pcmk_rc_no_output; + GString *buffer = NULL; + + CRM_ASSERT(out != NULL); + CRM_CHECK(depth >= 0, depth = 0); + + if (data == NULL) { + return rc; + } + + /* Allocate a buffer once, for show_xml_node() to truncate and reuse in + * recursive calls + */ + buffer = g_string_sized_new(1024); + rc = show_xml_node(out, buffer, prefix, data, depth, options); + g_string_free(buffer, TRUE); + + return rc; +} + +/*! + * \internal + * \brief Output XML portions that have been marked as changed + * + * \param[in,out] out Output object + * \param[in] data XML node to output + * \param[in] depth Current indentation level + * \param[in] options Group of \p pcmk__xml_fmt_options flags + * + * \note This is a recursive helper for \p pcmk__xml_show_changes(), showing + * changes to \p data and its children. + * \note This currently produces output only for text-like output objects. + */ +static int +show_xml_changes_recursive(pcmk__output_t *out, const xmlNode *data, int depth, + uint32_t options) +{ + /* @COMPAT: When log_data_element() is removed, we can remove the options + * argument here and instead hard-code pcmk__xml_log_pretty. + */ + xml_node_private_t *nodepriv = (xml_node_private_t *) data->_private; + int rc = pcmk_rc_no_output; + int temp_rc = pcmk_rc_no_output; + + if (pcmk_all_flags_set(nodepriv->flags, pcmk__xf_dirty|pcmk__xf_created)) { + // Newly created + return pcmk__xml_show(out, PCMK__XML_PREFIX_CREATED, data, depth, + options + |pcmk__xml_fmt_open + |pcmk__xml_fmt_children + |pcmk__xml_fmt_close); + } + + if (pcmk_is_set(nodepriv->flags, pcmk__xf_dirty)) { + // Modified or moved + bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); + int spaces = pretty? (2 * depth) : 0; + const char *prefix = PCMK__XML_PREFIX_MODIFIED; + + if (pcmk_is_set(nodepriv->flags, pcmk__xf_moved)) { + prefix = PCMK__XML_PREFIX_MOVED; + } + + // Log opening tag + rc = pcmk__xml_show(out, prefix, data, depth, + options|pcmk__xml_fmt_open); + + // Log changes to attributes + for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL; + attr = attr->next) { + const char *name = (const char *) attr->name; + + nodepriv = attr->_private; + + if (pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) { + const char *value = crm_element_value(data, name); + + temp_rc = out->info(out, "%s %*s @%s=%s", + PCMK__XML_PREFIX_DELETED, spaces, "", name, + value); + + } else if (pcmk_is_set(nodepriv->flags, pcmk__xf_dirty)) { + const char *value = crm_element_value(data, name); + + if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { + prefix = PCMK__XML_PREFIX_CREATED; + + } else if (pcmk_is_set(nodepriv->flags, pcmk__xf_modified)) { + prefix = PCMK__XML_PREFIX_MODIFIED; + + } else if (pcmk_is_set(nodepriv->flags, pcmk__xf_moved)) { + prefix = PCMK__XML_PREFIX_MOVED; + + } else { + prefix = PCMK__XML_PREFIX_MODIFIED; + } + + temp_rc = out->info(out, "%s %*s @%s=%s", + prefix, spaces, "", name, value); + } + rc = pcmk__output_select_rc(rc, temp_rc); + } + + // Log changes to children + for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL; + child = pcmk__xml_next(child)) { + temp_rc = show_xml_changes_recursive(out, child, depth + 1, + options); + rc = pcmk__output_select_rc(rc, temp_rc); + } + + // Log closing tag + temp_rc = pcmk__xml_show(out, PCMK__XML_PREFIX_MODIFIED, data, depth, + options|pcmk__xml_fmt_close); + return pcmk__output_select_rc(rc, temp_rc); + } + + // This node hasn't changed, but check its children + for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL; + child = pcmk__xml_next(child)) { + temp_rc = show_xml_changes_recursive(out, child, depth + 1, options); + rc = pcmk__output_select_rc(rc, temp_rc); + } + return rc; +} + +/*! + * \internal + * \brief Output changes to an XML node and any children + * + * \param[in,out] out Output object + * \param[in] xml XML node to output + * + * \return Standard Pacemaker return code + * + * \note This currently produces output only for text-like output objects. + */ +int +pcmk__xml_show_changes(pcmk__output_t *out, const xmlNode *xml) +{ + xml_doc_private_t *docpriv = NULL; + int rc = pcmk_rc_no_output; + int temp_rc = pcmk_rc_no_output; + + CRM_ASSERT(out != NULL); + CRM_ASSERT(xml != NULL); + CRM_ASSERT(xml->doc != NULL); + + docpriv = xml->doc->_private; + if (!pcmk_is_set(docpriv->flags, pcmk__xf_dirty)) { + return rc; + } + + for (const GList *iter = docpriv->deleted_objs; iter != NULL; + iter = iter->next) { + const pcmk__deleted_xml_t *deleted_obj = iter->data; + + if (deleted_obj->position >= 0) { + temp_rc = out->info(out, PCMK__XML_PREFIX_DELETED " %s (%d)", + deleted_obj->path, deleted_obj->position); + } else { + temp_rc = out->info(out, PCMK__XML_PREFIX_DELETED " %s", + deleted_obj->path); + } + rc = pcmk__output_select_rc(rc, temp_rc); + } + + temp_rc = show_xml_changes_recursive(out, xml, 0, pcmk__xml_fmt_pretty); + return pcmk__output_select_rc(rc, temp_rc); +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include <crm/common/logging_compat.h> +#include <crm/common/xml_compat.h> + +void +log_data_element(int log_level, const char *file, const char *function, + int line, const char *prefix, const xmlNode *data, int depth, + int legacy_options) +{ + uint32_t options = 0; + pcmk__output_t *out = NULL; + + // Confine log_level to uint8_t range + log_level = pcmk__clip_log_level(log_level); + + if (data == NULL) { + do_crm_log(log_level, "%s%sNo data to dump as XML", + pcmk__s(prefix, ""), pcmk__str_empty(prefix)? "" : " "); + return; + } + + switch (log_level) { + case LOG_NEVER: + return; + case LOG_STDOUT: + CRM_CHECK(pcmk__text_output_new(&out, NULL) == pcmk_rc_ok, return); + break; + default: + CRM_CHECK(pcmk__log_output_new(&out) == pcmk_rc_ok, return); + pcmk__output_set_log_level(out, log_level); + break; + } + + /* Map xml_log_options to pcmk__xml_fmt_options so that we can go ahead and + * start using the pcmk__xml_fmt_options in all the internal functions. + * + * xml_log_option_dirty_add and xml_log_option_diff_all are ignored by + * internal code and only used here, so they don't need to be addressed. + */ + if (pcmk_is_set(legacy_options, xml_log_option_filtered)) { + options |= pcmk__xml_fmt_filtered; + } + if (pcmk_is_set(legacy_options, xml_log_option_formatted)) { + options |= pcmk__xml_fmt_pretty; + } + if (pcmk_is_set(legacy_options, xml_log_option_full_fledged)) { + options |= pcmk__xml_fmt_full; + } + if (pcmk_is_set(legacy_options, xml_log_option_open)) { + options |= pcmk__xml_fmt_open; + } + if (pcmk_is_set(legacy_options, xml_log_option_children)) { + options |= pcmk__xml_fmt_children; + } + if (pcmk_is_set(legacy_options, xml_log_option_close)) { + options |= pcmk__xml_fmt_close; + } + if (pcmk_is_set(legacy_options, xml_log_option_text)) { + options |= pcmk__xml_fmt_text; + } + if (pcmk_is_set(legacy_options, xml_log_option_diff_plus)) { + options |= pcmk__xml_fmt_diff_plus; + } + if (pcmk_is_set(legacy_options, xml_log_option_diff_minus)) { + options |= pcmk__xml_fmt_diff_minus; + } + if (pcmk_is_set(legacy_options, xml_log_option_diff_short)) { + options |= pcmk__xml_fmt_diff_short; + } + + // Log element based on options + if (pcmk_is_set(legacy_options, xml_log_option_dirty_add)) { + CRM_CHECK(depth >= 0, depth = 0); + show_xml_changes_recursive(out, data, depth, options); + goto done; + } + + if (pcmk_is_set(options, pcmk__xml_fmt_pretty) + && (!xml_has_children(data) + || (crm_element_value(data, XML_DIFF_MARKER) != NULL))) { + + if (pcmk_is_set(options, pcmk__xml_fmt_diff_plus)) { + legacy_options |= xml_log_option_diff_all; + prefix = PCMK__XML_PREFIX_CREATED; + + } else if (pcmk_is_set(options, pcmk__xml_fmt_diff_minus)) { + legacy_options |= xml_log_option_diff_all; + prefix = PCMK__XML_PREFIX_DELETED; + } + } + + if (pcmk_is_set(options, pcmk__xml_fmt_diff_short) + && !pcmk_is_set(legacy_options, xml_log_option_diff_all)) { + + if (!pcmk_any_flags_set(options, + pcmk__xml_fmt_diff_plus + |pcmk__xml_fmt_diff_minus)) { + // Nothing will ever be logged + goto done; + } + + // Keep looking for the actual change + for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL; + child = pcmk__xml_next(child)) { + log_data_element(log_level, file, function, line, prefix, child, + depth + 1, options); + } + + } else { + pcmk__xml_show(out, prefix, data, depth, + options + |pcmk__xml_fmt_open + |pcmk__xml_fmt_children + |pcmk__xml_fmt_close); + } + +done: + out->finish(out, CRM_EX_OK, true, NULL); + pcmk__output_free(out); +} + +void +xml_log_changes(uint8_t log_level, const char *function, const xmlNode *xml) +{ + pcmk__output_t *out = NULL; + int rc = pcmk_rc_ok; + + switch (log_level) { + case LOG_NEVER: + return; + case LOG_STDOUT: + CRM_CHECK(pcmk__text_output_new(&out, NULL) == pcmk_rc_ok, return); + break; + default: + CRM_CHECK(pcmk__log_output_new(&out) == pcmk_rc_ok, return); + pcmk__output_set_log_level(out, log_level); + break; + } + rc = pcmk__xml_show_changes(out, xml); + out->finish(out, pcmk_rc2exitc(rc), true, NULL); + pcmk__output_free(out); +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/xpath.c b/lib/common/xpath.c new file mode 100644 index 0000000..1f5c0a8 --- /dev/null +++ b/lib/common/xpath.c @@ -0,0 +1,378 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> +#include <stdio.h> +#include <string.h> +#include <crm/msg_xml.h> +#include <crm/common/xml_internal.h> +#include "crmcommon_private.h" + +/* + * From xpath2.c + * + * All the elements returned by an XPath query are pointers to + * elements from the tree *except* namespace nodes where the XPath + * semantic is different from the implementation in libxml2 tree. + * As a result when a returned node set is freed when + * xmlXPathFreeObject() is called, that routine must check the + * element type. But node from the returned set may have been removed + * by xmlNodeSetContent() resulting in access to freed data. + * + * This can be exercised by running + * valgrind xpath2 test3.xml '//discarded' discarded + * + * There is 2 ways around it: + * - make a copy of the pointers to the nodes from the result set + * then call xmlXPathFreeObject() and then modify the nodes + * or + * - remove the references from the node set, if they are not + namespace nodes, before calling xmlXPathFreeObject(). + */ +void +freeXpathObject(xmlXPathObjectPtr xpathObj) +{ + int lpc, max = numXpathResults(xpathObj); + + if (xpathObj == NULL) { + return; + } + + for (lpc = 0; lpc < max; lpc++) { + if (xpathObj->nodesetval->nodeTab[lpc] && xpathObj->nodesetval->nodeTab[lpc]->type != XML_NAMESPACE_DECL) { + xpathObj->nodesetval->nodeTab[lpc] = NULL; + } + } + + /* _Now_ it's safe to free it */ + xmlXPathFreeObject(xpathObj); +} + +xmlNode * +getXpathResult(xmlXPathObjectPtr xpathObj, int index) +{ + xmlNode *match = NULL; + int max = numXpathResults(xpathObj); + + CRM_CHECK(index >= 0, return NULL); + CRM_CHECK(xpathObj != NULL, return NULL); + + if (index >= max) { + crm_err("Requested index %d of only %d items", index, max); + return NULL; + + } else if(xpathObj->nodesetval->nodeTab[index] == NULL) { + /* Previously requested */ + return NULL; + } + + match = xpathObj->nodesetval->nodeTab[index]; + CRM_CHECK(match != NULL, return NULL); + + if (xpathObj->nodesetval->nodeTab[index]->type != XML_NAMESPACE_DECL) { + /* See the comment for freeXpathObject() */ + xpathObj->nodesetval->nodeTab[index] = NULL; + } + + if (match->type == XML_DOCUMENT_NODE) { + /* Will happen if section = '/' */ + match = match->children; + + } else if (match->type != XML_ELEMENT_NODE + && match->parent && match->parent->type == XML_ELEMENT_NODE) { + /* Return the parent instead */ + match = match->parent; + + } else if (match->type != XML_ELEMENT_NODE) { + /* We only support searching nodes */ + crm_err("We only support %d not %d", XML_ELEMENT_NODE, match->type); + match = NULL; + } + return match; +} + +void +dedupXpathResults(xmlXPathObjectPtr xpathObj) +{ + int lpc, max = numXpathResults(xpathObj); + + if (xpathObj == NULL) { + return; + } + + for (lpc = 0; lpc < max; lpc++) { + xmlNode *xml = NULL; + gboolean dedup = FALSE; + + if (xpathObj->nodesetval->nodeTab[lpc] == NULL) { + continue; + } + + xml = xpathObj->nodesetval->nodeTab[lpc]->parent; + + for (; xml; xml = xml->parent) { + int lpc2 = 0; + + for (lpc2 = 0; lpc2 < max; lpc2++) { + if (xpathObj->nodesetval->nodeTab[lpc2] == xml) { + xpathObj->nodesetval->nodeTab[lpc] = NULL; + dedup = TRUE; + break; + } + } + + if (dedup) { + break; + } + } + } +} + +/* the caller needs to check if the result contains a xmlDocPtr or xmlNodePtr */ +xmlXPathObjectPtr +xpath_search(xmlNode * xml_top, const char *path) +{ + xmlDocPtr doc = NULL; + xmlXPathObjectPtr xpathObj = NULL; + xmlXPathContextPtr xpathCtx = NULL; + const xmlChar *xpathExpr = (pcmkXmlStr) path; + + CRM_CHECK(path != NULL, return NULL); + CRM_CHECK(xml_top != NULL, return NULL); + CRM_CHECK(strlen(path) > 0, return NULL); + + doc = getDocPtr(xml_top); + + xpathCtx = xmlXPathNewContext(doc); + CRM_ASSERT(xpathCtx != NULL); + + xpathObj = xmlXPathEvalExpression(xpathExpr, xpathCtx); + xmlXPathFreeContext(xpathCtx); + return xpathObj; +} + +/*! + * \brief Run a supplied function for each result of an xpath search + * + * \param[in,out] xml XML to search + * \param[in] xpath XPath search string + * \param[in] helper Function to call for each result + * \param[in,out] user_data Data to pass to supplied function + * + * \note The helper function will be passed the XML node of the result, + * and the supplied user_data. This function does not otherwise + * use user_data. + */ +void +crm_foreach_xpath_result(xmlNode *xml, const char *xpath, + void (*helper)(xmlNode*, void*), void *user_data) +{ + xmlXPathObjectPtr xpathObj = xpath_search(xml, xpath); + int nresults = numXpathResults(xpathObj); + int i; + + for (i = 0; i < nresults; i++) { + xmlNode *result = getXpathResult(xpathObj, i); + + CRM_LOG_ASSERT(result != NULL); + if (result) { + (*helper)(result, user_data); + } + } + freeXpathObject(xpathObj); +} + +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; + xmlNode *result = NULL; + xmlXPathObjectPtr xpathObj = NULL; + char *nodePath = NULL; + char *matchNodePath = NULL; + + if (xpath == NULL) { + return xml_obj; /* or return NULL? */ + } + + xpathObj = xpath_search(xml_obj, xpath); + nodePath = (char *)xmlGetNodePath(xml_obj); + max = numXpathResults(xpathObj); + + if (max < 1) { + if (error_level < LOG_NEVER) { + do_crm_log(error_level, "No match for %s in %s", + xpath, pcmk__s(nodePath, "unknown path")); + crm_log_xml_explicit(xml_obj, "Unexpected Input"); + } + + } else if (max > 1) { + if (error_level < LOG_NEVER) { + int lpc = 0; + + do_crm_log(error_level, "Too many matches for %s in %s", + xpath, pcmk__s(nodePath, "unknown path")); + + for (lpc = 0; lpc < max; lpc++) { + xmlNode *match = getXpathResult(xpathObj, lpc); + + CRM_LOG_ASSERT(match != NULL); + if (match != NULL) { + matchNodePath = (char *) xmlGetNodePath(match); + do_crm_log(error_level, "%s[%d] = %s", + xpath, lpc, + pcmk__s(matchNodePath, "unrecognizable match")); + free(matchNodePath); + } + } + crm_log_xml_explicit(xml_obj, "Bad Input"); + } + + } else { + result = getXpathResult(xpathObj, 0); + } + + freeXpathObject(xpathObj); + free(nodePath); + + return result; +} + +/*! + * \internal + * \brief Get an XPath string that matches an XML element as closely as possible + * + * \param[in] xml The XML element for which to build an XPath string + * + * \return A \p GString that matches \p xml, or \p NULL if \p xml is \p NULL. + * + * \note The caller is responsible for freeing the string using + * \p g_string_free(). + */ +GString * +pcmk__element_xpath(const xmlNode *xml) +{ + const xmlNode *parent = NULL; + GString *xpath = NULL; + const char *id = NULL; + + if (xml == NULL) { + return NULL; + } + + parent = xml->parent; + xpath = pcmk__element_xpath(parent); + if (xpath == NULL) { + xpath = g_string_sized_new(256); + } + + // Build xpath like "/" -> "/cib" -> "/cib/configuration" + if (parent == NULL) { + g_string_append_c(xpath, '/'); + } else if (parent->parent == NULL) { + g_string_append(xpath, TYPE(xml)); + } else { + pcmk__g_strcat(xpath, "/", TYPE(xml), NULL); + } + + id = ID(xml); + if (id != NULL) { + pcmk__g_strcat(xpath, "[@" XML_ATTR_ID "='", id, "']", NULL); + } + + return xpath; +} + +char * +pcmk__xpath_node_id(const char *xpath, const char *node) +{ + char *retval = NULL; + char *patt = NULL; + char *start = NULL; + char *end = NULL; + + if (node == NULL || xpath == NULL) { + return retval; + } + + patt = crm_strdup_printf("/%s[@" XML_ATTR_ID "=", node); + start = strstr(xpath, patt); + + if (!start) { + free(patt); + return retval; + } + + start += strlen(patt); + start++; + + end = strstr(start, "\'"); + CRM_ASSERT(end); + retval = strndup(start, end-start); + + free(patt); + return retval; +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include <crm/common/xml_compat.h> + +/*! + * \deprecated This function will be removed in a future release + * \brief Get an XPath string that matches an XML element as closely as possible + * + * \param[in] xml The XML element for which to build an XPath string + * + * \return A string that matches \p xml, or \p NULL if \p xml is \p NULL. + * + * \note The caller is responsible for freeing the string using free(). + */ +char * +xml_get_path(const xmlNode *xml) +{ + char *path = NULL; + GString *g_path = pcmk__element_xpath(xml); + + if (g_path == NULL) { + return NULL; + } + + path = strdup((const char *) g_path->str); + CRM_ASSERT(path != NULL); + + g_string_free(g_path, TRUE); + return path; +} + +// LCOV_EXCL_STOP +// End deprecated API |