summaryrefslogtreecommitdiffstats
path: root/lib/common
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 06:53:20 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 06:53:20 +0000
commite5a812082ae033afb1eed82c0f2df3d0f6bdc93f (patch)
treea6716c9275b4b413f6c9194798b34b91affb3cc7 /lib/common
parentInitial commit. (diff)
downloadpacemaker-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 '')
-rw-r--r--lib/common/Makefile.am124
-rw-r--r--lib/common/acl.c860
-rw-r--r--lib/common/agents.c213
-rw-r--r--lib/common/alerts.c253
-rw-r--r--lib/common/attrs.c89
-rw-r--r--lib/common/cib.c156
-rw-r--r--lib/common/cib_secrets.c192
-rw-r--r--lib/common/cmdline.c379
-rw-r--r--lib/common/crmcommon_private.h325
-rw-r--r--lib/common/digest.c278
-rw-r--r--lib/common/health.c70
-rw-r--r--lib/common/io.c663
-rw-r--r--lib/common/ipc_attrd.c590
-rw-r--r--lib/common/ipc_client.c1576
-rw-r--r--lib/common/ipc_common.c110
-rw-r--r--lib/common/ipc_controld.c671
-rw-r--r--lib/common/ipc_pacemakerd.c316
-rw-r--r--lib/common/ipc_schedulerd.c180
-rw-r--r--lib/common/ipc_server.c1008
-rw-r--r--lib/common/iso8601.c1970
-rw-r--r--lib/common/lists.c27
-rw-r--r--lib/common/logging.c1192
-rw-r--r--lib/common/mainloop.c1480
-rw-r--r--lib/common/messages.c291
-rw-r--r--lib/common/mock.c427
-rw-r--r--lib/common/mock_private.h77
-rw-r--r--lib/common/nodes.c24
-rw-r--r--lib/common/nvpair.c992
-rw-r--r--lib/common/operations.c530
-rw-r--r--lib/common/options.c497
-rw-r--r--lib/common/output.c318
-rw-r--r--lib/common/output_html.c477
-rw-r--r--lib/common/output_log.c353
-rw-r--r--lib/common/output_none.c152
-rw-r--r--lib/common/output_text.c446
-rw-r--r--lib/common/output_xml.c541
-rw-r--r--lib/common/patchset.c1516
-rw-r--r--lib/common/patchset_display.c519
-rw-r--r--lib/common/pid.c247
-rw-r--r--lib/common/procfs.c227
-rw-r--r--lib/common/remote.c1270
-rw-r--r--lib/common/results.c1049
-rw-r--r--lib/common/schemas.c1303
-rw-r--r--lib/common/scores.c166
-rw-r--r--lib/common/strings.c1363
-rw-r--r--lib/common/tests/Makefile.am32
-rw-r--r--lib/common/tests/acl/Makefile.am21
-rw-r--r--lib/common/tests/acl/pcmk__is_user_in_group_test.c38
-rw-r--r--lib/common/tests/acl/pcmk_acl_required_test.c26
-rw-r--r--lib/common/tests/acl/xml_acl_denied_test.c61
-rw-r--r--lib/common/tests/acl/xml_acl_enabled_test.c61
-rw-r--r--lib/common/tests/agents/Makefile.am20
-rw-r--r--lib/common/tests/agents/crm_generate_ra_key_test.c48
-rw-r--r--lib/common/tests/agents/crm_parse_agent_spec_test.c87
-rw-r--r--lib/common/tests/agents/pcmk__effective_rc_test.c36
-rw-r--r--lib/common/tests/agents/pcmk_get_ra_caps_test.c63
-rw-r--r--lib/common/tests/agents/pcmk_stonith_param_test.c50
-rw-r--r--lib/common/tests/cmdline/Makefile.am17
-rw-r--r--lib/common/tests/cmdline/pcmk__cmdline_preproc_test.c156
-rw-r--r--lib/common/tests/cmdline/pcmk__quote_cmdline_test.c56
-rw-r--r--lib/common/tests/flags/Makefile.am20
-rw-r--r--lib/common/tests/flags/pcmk__clear_flags_as_test.c41
-rw-r--r--lib/common/tests/flags/pcmk__set_flags_as_test.c25
-rw-r--r--lib/common/tests/flags/pcmk_all_flags_set_test.c33
-rw-r--r--lib/common/tests/flags/pcmk_any_flags_set_test.c26
-rw-r--r--lib/common/tests/health/Makefile.am17
-rw-r--r--lib/common/tests/health/pcmk__parse_health_strategy_test.c56
-rw-r--r--lib/common/tests/health/pcmk__validate_health_strategy_test.c38
-rw-r--r--lib/common/tests/io/Makefile.am18
-rw-r--r--lib/common/tests/io/pcmk__full_path_test.c52
-rw-r--r--lib/common/tests/io/pcmk__get_tmpdir_test.c68
-rw-r--r--lib/common/tests/iso8601/Makefile.am16
-rw-r--r--lib/common/tests/iso8601/pcmk__readable_interval_test.c27
-rw-r--r--lib/common/tests/lists/Makefile.am20
-rw-r--r--lib/common/tests/lists/pcmk__list_of_1_test.c45
-rw-r--r--lib/common/tests/lists/pcmk__list_of_multiple_test.c45
-rw-r--r--lib/common/tests/lists/pcmk__subtract_lists_test.c144
-rw-r--r--lib/common/tests/nvpair/Makefile.am18
-rw-r--r--lib/common/tests/nvpair/pcmk__xe_attr_is_true_test.c50
-rw-r--r--lib/common/tests/nvpair/pcmk__xe_get_bool_attr_test.c59
-rw-r--r--lib/common/tests/nvpair/pcmk__xe_set_bool_attr_test.c31
-rw-r--r--lib/common/tests/operations/Makefile.am22
-rw-r--r--lib/common/tests/operations/copy_in_properties_test.c62
-rw-r--r--lib/common/tests/operations/expand_plus_plus_test.c256
-rw-r--r--lib/common/tests/operations/fix_plus_plus_recursive_test.c47
-rw-r--r--lib/common/tests/operations/parse_op_key_test.c275
-rw-r--r--lib/common/tests/operations/pcmk_is_probe_test.c25
-rw-r--r--lib/common/tests/operations/pcmk_xe_is_probe_test.c43
-rw-r--r--lib/common/tests/operations/pcmk_xe_mask_probe_failure_test.c150
-rw-r--r--lib/common/tests/options/Makefile.am19
-rw-r--r--lib/common/tests/options/pcmk__env_option_enabled_test.c101
-rw-r--r--lib/common/tests/options/pcmk__env_option_test.c134
-rw-r--r--lib/common/tests/options/pcmk__set_env_option_test.c154
-rw-r--r--lib/common/tests/output/Makefile.am24
-rw-r--r--lib/common/tests/output/pcmk__call_message_test.c156
-rw-r--r--lib/common/tests/output/pcmk__output_and_clear_error_test.c82
-rw-r--r--lib/common/tests/output/pcmk__output_free_test.c84
-rw-r--r--lib/common/tests/output/pcmk__output_new_test.c148
-rw-r--r--lib/common/tests/output/pcmk__register_format_test.c63
-rw-r--r--lib/common/tests/output/pcmk__register_formats_test.c108
-rw-r--r--lib/common/tests/output/pcmk__register_message_test.c107
-rw-r--r--lib/common/tests/output/pcmk__register_messages_test.c191
-rw-r--r--lib/common/tests/output/pcmk__unregister_formats_test.c39
-rw-r--r--lib/common/tests/procfs/Makefile.am18
-rw-r--r--lib/common/tests/procfs/pcmk__procfs_has_pids_false_test.c42
-rw-r--r--lib/common/tests/procfs/pcmk__procfs_has_pids_true_test.c41
-rw-r--r--lib/common/tests/procfs/pcmk__procfs_pid2path_test.c92
-rw-r--r--lib/common/tests/results/Makefile.am16
-rw-r--r--lib/common/tests/results/pcmk__results_test.c61
-rw-r--r--lib/common/tests/scores/Makefile.am19
-rw-r--r--lib/common/tests/scores/char2score_test.c75
-rw-r--r--lib/common/tests/scores/pcmk__add_scores_test.c74
-rw-r--r--lib/common/tests/scores/pcmk_readable_score_test.c33
-rw-r--r--lib/common/tests/strings/Makefile.am41
-rw-r--r--lib/common/tests/strings/crm_get_msec_test.c50
-rw-r--r--lib/common/tests/strings/crm_is_true_test.c57
-rw-r--r--lib/common/tests/strings/crm_str_to_boolean_test.c92
-rw-r--r--lib/common/tests/strings/pcmk__add_word_test.c93
-rw-r--r--lib/common/tests/strings/pcmk__btoa_test.c22
-rw-r--r--lib/common/tests/strings/pcmk__char_in_any_str_test.c46
-rw-r--r--lib/common/tests/strings/pcmk__compress_test.c58
-rw-r--r--lib/common/tests/strings/pcmk__ends_with_test.c57
-rw-r--r--lib/common/tests/strings/pcmk__g_strcat_test.c73
-rw-r--r--lib/common/tests/strings/pcmk__guint_from_hash_test.c76
-rw-r--r--lib/common/tests/strings/pcmk__numeric_strcasecmp_test.c79
-rw-r--r--lib/common/tests/strings/pcmk__parse_ll_range_test.c117
-rw-r--r--lib/common/tests/strings/pcmk__s_test.c29
-rw-r--r--lib/common/tests/strings/pcmk__scan_double_test.c158
-rw-r--r--lib/common/tests/strings/pcmk__scan_min_int_test.c60
-rw-r--r--lib/common/tests/strings/pcmk__scan_port_test.c59
-rw-r--r--lib/common/tests/strings/pcmk__starts_with_test.c35
-rw-r--r--lib/common/tests/strings/pcmk__str_any_of_test.c48
-rw-r--r--lib/common/tests/strings/pcmk__str_in_list_test.c107
-rw-r--r--lib/common/tests/strings/pcmk__str_table_dup_test.c59
-rw-r--r--lib/common/tests/strings/pcmk__str_update_test.c78
-rw-r--r--lib/common/tests/strings/pcmk__strcmp_test.c80
-rw-r--r--lib/common/tests/strings/pcmk__strikey_table_test.c40
-rw-r--r--lib/common/tests/strings/pcmk__strkey_table_test.c40
-rw-r--r--lib/common/tests/strings/pcmk__trim_test.c72
-rw-r--r--lib/common/tests/utils/Makefile.am28
-rw-r--r--lib/common/tests/utils/compare_version_test.c55
-rw-r--r--lib/common/tests/utils/crm_meta_name_test.c41
-rw-r--r--lib/common/tests/utils/crm_meta_value_test.c56
-rw-r--r--lib/common/tests/utils/crm_user_lookup_test.c127
-rw-r--r--lib/common/tests/utils/pcmk__getpid_s_test.c38
-rw-r--r--lib/common/tests/utils/pcmk_daemon_user_test.c83
-rw-r--r--lib/common/tests/utils/pcmk_hostname_test.c56
-rw-r--r--lib/common/tests/utils/pcmk_str_is_infinity_test.c57
-rw-r--r--lib/common/tests/utils/pcmk_str_is_minus_infinity_test.c54
-rw-r--r--lib/common/tests/xml/Makefile.am17
-rw-r--r--lib/common/tests/xml/pcmk__xe_foreach_child_test.c215
-rw-r--r--lib/common/tests/xml/pcmk__xe_match_test.c106
-rw-r--r--lib/common/tests/xpath/Makefile.am16
-rw-r--r--lib/common/tests/xpath/pcmk__xpath_node_id_test.c59
-rw-r--r--lib/common/utils.c594
-rw-r--r--lib/common/watchdog.c311
-rw-r--r--lib/common/xml.c2753
-rw-r--r--lib/common/xml_display.c549
-rw-r--r--lib/common/xpath.c378
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 "&apos;", then
+ * we have introduced an ampersand which libxml will escape. This leaves
+ * us with "&amp;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 &lt; 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, "&lt;");
+ break;
+ case '>':
+ copy = replace_text(copy, index, &length, "&gt;");
+ break;
+ case '"':
+ copy = replace_text(copy, index, &length, "&quot;");
+ break;
+ case '\'':
+ copy = replace_text(copy, index, &length, "&apos;");
+ break;
+ case '&':
+ copy = replace_text(copy, index, &length, "&amp;");
+ 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