summaryrefslogtreecommitdiffstats
path: root/lib/common
diff options
context:
space:
mode:
Diffstat (limited to 'lib/common')
-rw-r--r--lib/common/Makefile.am22
-rw-r--r--lib/common/acl.c136
-rw-r--r--lib/common/actions.c281
-rw-r--r--lib/common/agents.c3
-rw-r--r--lib/common/alerts.c20
-rw-r--r--lib/common/attrs.c116
-rw-r--r--lib/common/cib.c100
-rw-r--r--lib/common/crmcommon_private.h160
-rw-r--r--lib/common/digest.c90
-rw-r--r--lib/common/health.c39
-rw-r--r--lib/common/io.c15
-rw-r--r--lib/common/ipc_attrd.c258
-rw-r--r--lib/common/ipc_client.c39
-rw-r--r--lib/common/ipc_common.c4
-rw-r--r--lib/common/ipc_controld.c113
-rw-r--r--lib/common/ipc_pacemakerd.c69
-rw-r--r--lib/common/ipc_schedulerd.c30
-rw-r--r--lib/common/ipc_server.c88
-rw-r--r--lib/common/iso8601.c233
-rw-r--r--lib/common/logging.c20
-rw-r--r--lib/common/mainloop.c33
-rw-r--r--lib/common/messages.c122
-rw-r--r--lib/common/mock.c85
-rw-r--r--lib/common/mock_private.h13
-rw-r--r--lib/common/nodes.c145
-rw-r--r--lib/common/nvpair.c161
-rw-r--r--lib/common/options.c1489
-rw-r--r--lib/common/options_display.c493
-rw-r--r--lib/common/output.c41
-rw-r--r--lib/common/output_html.c120
-rw-r--r--lib/common/output_log.c80
-rw-r--r--lib/common/output_none.c8
-rw-r--r--lib/common/output_text.c108
-rw-r--r--lib/common/output_xml.c320
-rw-r--r--lib/common/patchset.c821
-rw-r--r--lib/common/patchset_display.c73
-rw-r--r--lib/common/probes.c84
-rw-r--r--lib/common/remote.c35
-rw-r--r--lib/common/resources.c67
-rw-r--r--lib/common/results.c8
-rw-r--r--lib/common/roles.c88
-rw-r--r--lib/common/rules.c1512
-rw-r--r--lib/common/scheduler.c97
-rw-r--r--lib/common/schemas.c1561
-rw-r--r--lib/common/scores.c57
-rw-r--r--lib/common/strings.c259
-rw-r--r--lib/common/tests/Makefile.am8
-rw-r--r--lib/common/tests/acl/xml_acl_denied_test.c10
-rw-r--r--lib/common/tests/acl/xml_acl_enabled_test.c10
-rw-r--r--lib/common/tests/actions/Makefile.am10
-rw-r--r--lib/common/tests/actions/copy_in_properties_test.c62
-rw-r--r--lib/common/tests/actions/expand_plus_plus_test.c256
-rw-r--r--lib/common/tests/actions/fix_plus_plus_recursive_test.c47
-rw-r--r--lib/common/tests/actions/pcmk_xe_is_probe_test.c43
-rw-r--r--lib/common/tests/actions/pcmk_xe_mask_probe_failure_test.c150
-rw-r--r--lib/common/tests/health/pcmk__parse_health_strategy_test.c2
-rw-r--r--lib/common/tests/health/pcmk__validate_health_strategy_test.c2
-rw-r--r--lib/common/tests/io/pcmk__full_path_test.c10
-rw-r--r--lib/common/tests/iso8601/Makefile.am6
-rw-r--r--lib/common/tests/iso8601/pcmk__add_time_from_xml_test.c243
-rw-r--r--lib/common/tests/iso8601/pcmk__readable_interval_test.c4
-rw-r--r--lib/common/tests/iso8601/pcmk__set_time_if_earlier_test.c80
-rw-r--r--lib/common/tests/nodes/Makefile.am23
-rw-r--r--lib/common/tests/nodes/pcmk__find_node_in_list_test.c53
-rw-r--r--lib/common/tests/nodes/pcmk__xe_add_node_test.c71
-rw-r--r--lib/common/tests/nodes/pcmk_foreach_active_resource_test.c149
-rw-r--r--lib/common/tests/nodes/pcmk_node_is_clean_test.c54
-rw-r--r--lib/common/tests/nodes/pcmk_node_is_in_maintenance_test.c54
-rw-r--r--lib/common/tests/nodes/pcmk_node_is_online_test.c54
-rw-r--r--lib/common/tests/nodes/pcmk_node_is_pending_test.c54
-rw-r--r--lib/common/tests/nodes/pcmk_node_is_shutting_down_test.c54
-rw-r--r--lib/common/tests/nvpair/Makefile.am7
-rw-r--r--lib/common/tests/nvpair/crm_meta_name_test.c (renamed from lib/common/tests/utils/crm_meta_name_test.c)10
-rw-r--r--lib/common/tests/nvpair/crm_meta_value_test.c (renamed from lib/common/tests/utils/crm_meta_value_test.c)18
-rw-r--r--lib/common/tests/nvpair/pcmk__xe_attr_is_true_test.c10
-rw-r--r--lib/common/tests/nvpair/pcmk__xe_get_bool_attr_test.c11
-rw-r--r--lib/common/tests/nvpair/pcmk__xe_get_datetime_test.c108
-rw-r--r--lib/common/tests/nvpair/pcmk__xe_set_bool_attr_test.c12
-rw-r--r--lib/common/tests/probes/Makefile.am18
-rw-r--r--lib/common/tests/probes/pcmk_is_probe_test.c (renamed from lib/common/tests/actions/pcmk_is_probe_test.c)2
-rw-r--r--lib/common/tests/probes/pcmk_xe_is_probe_test.c54
-rw-r--r--lib/common/tests/probes/pcmk_xe_mask_probe_failure_test.c333
-rw-r--r--lib/common/tests/procfs/pcmk__procfs_pid2path_test.c8
-rw-r--r--lib/common/tests/resources/Makefile.am17
-rw-r--r--lib/common/tests/resources/pcmk_resource_id_test.c36
-rw-r--r--lib/common/tests/resources/pcmk_resource_is_managed_test.c46
-rw-r--r--lib/common/tests/rules/Makefile.am29
-rw-r--r--lib/common/tests/rules/pcmk__cmp_by_type_test.c102
-rw-r--r--lib/common/tests/rules/pcmk__evaluate_attr_expression_test.c831
-rw-r--r--lib/common/tests/rules/pcmk__evaluate_condition_test.c197
-rw-r--r--lib/common/tests/rules/pcmk__evaluate_date_expression_test.c684
-rw-r--r--lib/common/tests/rules/pcmk__evaluate_date_spec_test.c231
-rw-r--r--lib/common/tests/rules/pcmk__evaluate_op_expression_test.c207
-rw-r--r--lib/common/tests/rules/pcmk__evaluate_rsc_expression_test.c227
-rw-r--r--lib/common/tests/rules/pcmk__parse_combine_test.c52
-rw-r--r--lib/common/tests/rules/pcmk__parse_comparison_test.c72
-rw-r--r--lib/common/tests/rules/pcmk__parse_source_test.c62
-rw-r--r--lib/common/tests/rules/pcmk__parse_type_test.c127
-rw-r--r--lib/common/tests/rules/pcmk__replace_submatches_test.c81
-rw-r--r--lib/common/tests/rules/pcmk__unpack_duration_test.c120
-rw-r--r--lib/common/tests/rules/pcmk_evaluate_rule_test.c379
-rw-r--r--lib/common/tests/scheduler/Makefile.am19
-rw-r--r--lib/common/tests/scheduler/pcmk_get_dc_test.c47
-rw-r--r--lib/common/tests/scheduler/pcmk_get_no_quorum_policy_test.c34
-rw-r--r--lib/common/tests/scheduler/pcmk_has_quorum_test.c36
-rw-r--r--lib/common/tests/scheduler/pcmk_set_scheduler_cib_test.c71
-rw-r--r--lib/common/tests/schemas/Makefile.am88
-rw-r--r--lib/common/tests/schemas/crm_schema_init_test.c152
-rw-r--r--lib/common/tests/schemas/pcmk__build_schema_xml_node_test.c158
-rw-r--r--lib/common/tests/schemas/pcmk__cmp_schemas_by_name_test.c121
-rw-r--r--lib/common/tests/schemas/pcmk__find_x_0_schema_test.c100
-rw-r--r--lib/common/tests/schemas/pcmk__get_schema_test.c81
-rw-r--r--lib/common/tests/schemas/pcmk__schema_files_later_than_test.c106
-rw-r--r--lib/common/tests/scores/char2score_test.c14
-rw-r--r--lib/common/tests/scores/pcmk__add_scores_test.c69
-rw-r--r--lib/common/tests/scores/pcmk_readable_score_test.c10
-rw-r--r--lib/common/tests/strings/Makefile.am3
-rw-r--r--lib/common/tests/strings/crm_get_msec_test.c30
-rw-r--r--lib/common/tests/strings/crm_str_to_boolean_test.c16
-rw-r--r--lib/common/tests/strings/pcmk__char_in_any_str_test.c46
-rw-r--r--lib/common/tests/strings/pcmk__compress_test.c11
-rw-r--r--lib/common/tests/strings/pcmk__str_update_test.c3
-rw-r--r--lib/common/tests/utils/Makefile.am11
-rw-r--r--lib/common/tests/utils/compare_version_test.c5
-rw-r--r--lib/common/tests/utils/pcmk__realloc_test.c69
-rw-r--r--lib/common/tests/utils/pcmk_hostname_test.c56
-rw-r--r--lib/common/tests/xml/Makefile.am11
-rw-r--r--lib/common/tests/xml/crm_xml_init_test.c230
-rw-r--r--lib/common/tests/xml/pcmk__xe_copy_attrs_test.c188
-rw-r--r--lib/common/tests/xml/pcmk__xe_first_child_test.c (renamed from lib/common/tests/xml/pcmk__xe_match_test.c)52
-rw-r--r--lib/common/tests/xml/pcmk__xe_foreach_child_test.c20
-rw-r--r--lib/common/tests/xml/pcmk__xe_set_score_test.c188
-rw-r--r--lib/common/tests/xml/pcmk__xml_escape_test.c213
-rw-r--r--lib/common/tests/xml/pcmk__xml_needs_escape_test.c337
-rw-r--r--lib/common/tests/xpath/pcmk__xpath_node_id_test.c29
-rw-r--r--lib/common/unittest.c128
-rw-r--r--lib/common/utils.c145
-rw-r--r--lib/common/watchdog.c45
-rw-r--r--lib/common/xml.c2825
-rw-r--r--lib/common/xml_attr.c31
-rw-r--r--lib/common/xml_display.c21
-rw-r--r--lib/common/xml_io.c840
-rw-r--r--lib/common/xpath.c85
143 files changed, 17299 insertions, 4761 deletions
diff --git a/lib/common/Makefile.am b/lib/common/Makefile.am
index f9c43b9..bfa5c1d 100644
--- a/lib/common/Makefile.am
+++ b/lib/common/Makefile.am
@@ -1,5 +1,5 @@
#
-# Copyright 2004-2023 the Pacemaker project contributors
+# Copyright 2004-2024 the Pacemaker project contributors
#
# The version control history for this file may have further details.
#
@@ -33,7 +33,7 @@ SUBDIRS = . tests
noinst_HEADERS = crmcommon_private.h \
mock_private.h
-libcrmcommon_la_LDFLAGS = -version-info 46:0:12
+libcrmcommon_la_LDFLAGS = -version-info 47:0:13
libcrmcommon_la_CFLAGS = $(CFLAGS_HARDENED_LIB)
libcrmcommon_la_LDFLAGS += $(LDFLAGS_HARDENED_LIB)
@@ -70,7 +70,7 @@ libcrmcommon_la_SOURCES += ipc_client.c
libcrmcommon_la_SOURCES += ipc_common.c
libcrmcommon_la_SOURCES += ipc_controld.c
libcrmcommon_la_SOURCES += ipc_pacemakerd.c
-libcrmcommon_la_SOURCES += ipc_schedulerd.c
+libcrmcommon_la_SOURCES += ipc_schedulerd.c
libcrmcommon_la_SOURCES += ipc_server.c
libcrmcommon_la_SOURCES += iso8601.c
libcrmcommon_la_SOURCES += lists.c
@@ -80,6 +80,7 @@ libcrmcommon_la_SOURCES += messages.c
libcrmcommon_la_SOURCES += nodes.c
libcrmcommon_la_SOURCES += nvpair.c
libcrmcommon_la_SOURCES += options.c
+libcrmcommon_la_SOURCES += options_display.c
libcrmcommon_la_SOURCES += output.c
libcrmcommon_la_SOURCES += output_html.c
libcrmcommon_la_SOURCES += output_log.c
@@ -89,9 +90,13 @@ libcrmcommon_la_SOURCES += output_xml.c
libcrmcommon_la_SOURCES += patchset.c
libcrmcommon_la_SOURCES += patchset_display.c
libcrmcommon_la_SOURCES += pid.c
+libcrmcommon_la_SOURCES += probes.c
libcrmcommon_la_SOURCES += procfs.c
libcrmcommon_la_SOURCES += remote.c
+libcrmcommon_la_SOURCES += resources.c
libcrmcommon_la_SOURCES += results.c
+libcrmcommon_la_SOURCES += roles.c
+libcrmcommon_la_SOURCES += rules.c
libcrmcommon_la_SOURCES += scheduler.c
libcrmcommon_la_SOURCES += schemas.c
libcrmcommon_la_SOURCES += scores.c
@@ -101,6 +106,7 @@ libcrmcommon_la_SOURCES += watchdog.c
libcrmcommon_la_SOURCES += xml.c
libcrmcommon_la_SOURCES += xml_attr.c
libcrmcommon_la_SOURCES += xml_display.c
+libcrmcommon_la_SOURCES += xml_io.c
libcrmcommon_la_SOURCES += xpath.c
#
@@ -112,6 +118,7 @@ include $(top_srcdir)/mk/tap.mk
libcrmcommon_test_la_SOURCES = $(libcrmcommon_la_SOURCES)
libcrmcommon_test_la_SOURCES += mock.c
+libcrmcommon_test_la_SOURCES += unittest.c
libcrmcommon_test_la_LDFLAGS = $(libcrmcommon_la_LDFLAGS) \
-rpath $(libdir) \
$(LDFLAGS_WRAP)
@@ -126,8 +133,11 @@ libcrmcommon_test_la_CFLAGS = $(libcrmcommon_la_CFLAGS) \
-fno-inline
# If -fno-builtin is used, -lm also needs to be added. See the comment at
# BUILD_PROFILING above.
-libcrmcommon_test_la_LIBADD = $(libcrmcommon_la_LIBADD) \
- -lcmocka \
- -lm
+libcrmcommon_test_la_LIBADD = $(libcrmcommon_la_LIBADD)
+if BUILD_COVERAGE
+libcrmcommon_test_la_LIBADD += -lgcov
+endif
+libcrmcommon_test_la_LIBADD += -lcmocka
+libcrmcommon_test_la_LIBADD += -lm
nodist_libcrmcommon_test_la_SOURCES = $(nodist_libcrmcommon_la_SOURCES)
diff --git a/lib/common/acl.c b/lib/common/acl.c
index 1ebd765..b8914be 100644
--- a/lib/common/acl.c
+++ b/lib/common/acl.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2004-2023 the Pacemaker project contributors
+ * Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -19,7 +19,6 @@
#include <libxml/tree.h>
#include <crm/crm.h>
-#include <crm/msg_xml.h>
#include <crm/common/xml.h>
#include <crm/common/xml_internal.h>
#include "crmcommon_private.h"
@@ -51,18 +50,18 @@ create_acl(const xmlNode *xml, GList *acls, enum xml_private_flags mode)
{
xml_acl_t *acl = NULL;
- const char *tag = crm_element_value(xml, XML_ACL_ATTR_TAG);
- const char *ref = crm_element_value(xml, XML_ACL_ATTR_REF);
- const char *xpath = crm_element_value(xml, XML_ACL_ATTR_XPATH);
- const char *attr = crm_element_value(xml, XML_ACL_ATTR_ATTRIBUTE);
+ const char *tag = crm_element_value(xml, PCMK_XA_OBJECT_TYPE);
+ const char *ref = crm_element_value(xml, PCMK_XA_REFERENCE);
+ const char *xpath = crm_element_value(xml, PCMK_XA_XPATH);
+ const char *attr = crm_element_value(xml, PCMK_XA_ATTRIBUTE);
if (tag == NULL) {
- // @COMPAT rolling upgrades <=1.1.11
- tag = crm_element_value(xml, XML_ACL_ATTR_TAGv1);
+ // @COMPAT Deprecated since 1.1.12 (needed for rolling upgrades)
+ tag = crm_element_value(xml, PCMK_XA_TAG);
}
if (ref == NULL) {
- // @COMPAT rolling upgrades <=1.1.11
- ref = crm_element_value(xml, XML_ACL_ATTR_REFv1);
+ // @COMPAT Deprecated since 1.1.12 (needed for rolling upgrades)
+ ref = crm_element_value(xml, PCMK__XA_REF);
}
if ((tag == NULL) && (ref == NULL) && (xpath == NULL)) {
@@ -72,8 +71,7 @@ create_acl(const xmlNode *xml, GList *acls, enum xml_private_flags mode)
return NULL;
}
- acl = calloc(1, sizeof (xml_acl_t));
- CRM_ASSERT(acl != NULL);
+ acl = pcmk__assert_alloc(1, sizeof (xml_acl_t));
acl->mode = mode;
if (xpath) {
@@ -86,11 +84,11 @@ create_acl(const xmlNode *xml, GList *acls, enum xml_private_flags mode)
if ((ref != NULL) && (attr != NULL)) {
// NOTE: schema currently does not allow this
- pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@" XML_ATTR_ID "='",
+ pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@" PCMK_XA_ID "='",
ref, "' and @", attr, "]", NULL);
} else if (ref != NULL) {
- pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@" XML_ATTR_ID "='",
+ pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@" PCMK_XA_ID "='",
ref, "']", NULL);
} else if (attr != NULL) {
@@ -127,12 +125,13 @@ parse_acl_entry(const xmlNode *acl_top, const xmlNode *acl_entry, GList *acls)
{
xmlNode *child = NULL;
- for (child = pcmk__xe_first_child(acl_entry); child;
- child = pcmk__xe_next(child)) {
+ for (child = pcmk__xe_first_child(acl_entry, NULL, NULL, NULL);
+ child != NULL; child = pcmk__xe_next(child)) {
+
const char *tag = (const char *) child->name;
- const char *kind = crm_element_value(child, XML_ACL_ATTR_KIND);
+ const char *kind = crm_element_value(child, PCMK_XA_KIND);
- if (pcmk__xe_is(child, XML_ACL_TAG_PERMISSION)) {
+ if (pcmk__xe_is(child, PCMK_XE_ACL_PERMISSION)) {
CRM_ASSERT(kind != NULL);
crm_trace("Unpacking ACL <%s> element of kind '%s'", tag, kind);
tag = kind;
@@ -140,18 +139,21 @@ parse_acl_entry(const xmlNode *acl_top, const xmlNode *acl_entry, GList *acls)
crm_trace("Unpacking ACL <%s> element", tag);
}
- if (strcmp(XML_ACL_TAG_ROLE_REF, tag) == 0
- || strcmp(XML_ACL_TAG_ROLE_REFv1, tag) == 0) {
- const char *ref_role = crm_element_value(child, XML_ATTR_ID);
+ /* @COMPAT PCMK__XE_ROLE_REF was deprecated in Pacemaker 1.1.12 (needed
+ * for rolling upgrades)
+ */
+ if (pcmk__str_any_of(tag, PCMK_XE_ROLE, PCMK__XE_ROLE_REF, NULL)) {
+ const char *ref_role = crm_element_value(child, PCMK_XA_ID);
if (ref_role) {
xmlNode *role = NULL;
- for (role = pcmk__xe_first_child(acl_top); role;
- role = pcmk__xe_next(role)) {
- if (!strcmp(XML_ACL_TAG_ROLE, (const char *) role->name)) {
+ for (role = pcmk__xe_first_child(acl_top, NULL, NULL, NULL);
+ role != NULL; role = pcmk__xe_next(role)) {
+
+ if (!strcmp(PCMK_XE_ACL_ROLE, (const char *) role->name)) {
const char *role_id = crm_element_value(role,
- XML_ATTR_ID);
+ PCMK_XA_ID);
if (role_id && strcmp(ref_role, role_id) == 0) {
crm_trace("Unpacking referenced role '%s' in ACL <%s> element",
@@ -163,13 +165,19 @@ parse_acl_entry(const xmlNode *acl_top, const xmlNode *acl_entry, GList *acls)
}
}
- } else if (strcmp(XML_ACL_TAG_READ, tag) == 0) {
+ /* @COMPAT Use of a tag instead of a PCMK_XA_KIND attribute was
+ * deprecated in 1.1.12. We still need to look for tags named
+ * PCMK_VALUE_READ, etc., to support rolling upgrades. However,
+ * eventually we can clean this up and make the variables more intuitive
+ * (for example, don't assign a PCMK_XA_KIND value to the tag variable).
+ */
+ } else if (strcmp(tag, PCMK_VALUE_READ) == 0) {
acls = create_acl(child, acls, pcmk__xf_acl_read);
- } else if (strcmp(XML_ACL_TAG_WRITE, tag) == 0) {
+ } else if (strcmp(tag, PCMK_VALUE_WRITE) == 0) {
acls = create_acl(child, acls, pcmk__xf_acl_write);
- } else if (strcmp(XML_ACL_TAG_DENY, tag) == 0) {
+ } else if (strcmp(tag, PCMK_VALUE_DENY) == 0) {
acls = create_acl(child, acls, pcmk__xf_acl_deny);
} else {
@@ -292,34 +300,36 @@ pcmk__unpack_acl(xmlNode *source, xmlNode *target, const char *user)
user);
} else if (docpriv->acls == NULL) {
- xmlNode *acls = get_xpath_object("//" XML_CIB_TAG_ACLS,
- source, LOG_NEVER);
+ xmlNode *acls = get_xpath_object("//" PCMK_XE_ACLS, source, LOG_NEVER);
pcmk__str_update(&docpriv->user, user);
if (acls) {
xmlNode *child = NULL;
- for (child = pcmk__xe_first_child(acls); child;
- child = pcmk__xe_next(child)) {
+ for (child = pcmk__xe_first_child(acls, NULL, NULL, NULL);
+ child != NULL; child = pcmk__xe_next(child)) {
- if (pcmk__xe_is(child, XML_ACL_TAG_USER)
- || pcmk__xe_is(child, XML_ACL_TAG_USERv1)) {
- const char *id = crm_element_value(child, XML_ATTR_NAME);
+ /* @COMPAT PCMK__XE_ACL_USER was deprecated in Pacemaker 1.1.12
+ * (needed for rolling upgrades)
+ */
+ if (pcmk__xe_is(child, PCMK_XE_ACL_TARGET)
+ || pcmk__xe_is(child, PCMK__XE_ACL_USER)) {
+ const char *id = crm_element_value(child, PCMK_XA_NAME);
if (id == NULL) {
- id = crm_element_value(child, XML_ATTR_ID);
+ id = crm_element_value(child, PCMK_XA_ID);
}
if (id && strcmp(id, user) == 0) {
crm_debug("Unpacking ACLs for user '%s'", id);
docpriv->acls = parse_acl_entry(acls, child, docpriv->acls);
}
- } else if (pcmk__xe_is(child, XML_ACL_TAG_GROUP)) {
- const char *id = crm_element_value(child, XML_ATTR_NAME);
+ } else if (pcmk__xe_is(child, PCMK_XE_ACL_GROUP)) {
+ const char *id = crm_element_value(child, PCMK_XA_NAME);
if (id == NULL) {
- id = crm_element_value(child, XML_ATTR_ID);
+ id = crm_element_value(child, PCMK_XA_ID);
}
if (id && pcmk__is_user_in_group(user,id)) {
@@ -388,8 +398,8 @@ purge_xml_attributes(xmlNode *xml)
xml_node_private_t *nodepriv = xml->_private;
if (test_acl_mode(nodepriv->flags, pcmk__xf_acl_read)) {
- crm_trace("%s[@" XML_ATTR_ID "=%s] is readable",
- xml->name, ID(xml));
+ crm_trace("%s[@" PCMK_XA_ID "=%s] is readable",
+ xml->name, pcmk__xe_id(xml));
return true;
}
@@ -399,7 +409,7 @@ purge_xml_attributes(xmlNode *xml)
const char *prop_name = (const char *)xIter->name;
xIter = xIter->next;
- if (strcmp(prop_name, XML_ATTR_ID) == 0) {
+ if (strcmp(prop_name, PCMK_XA_ID) == 0) {
continue;
}
@@ -447,7 +457,7 @@ xml_acl_filtered_copy(const char *user, xmlNode *acl_source, xmlNode *xml,
}
crm_trace("Filtering XML copy using user '%s' ACLs", user);
- target = copy_xml(xml);
+ target = pcmk__xml_copy(NULL, xml);
if (target == NULL) {
return true;
}
@@ -513,7 +523,7 @@ xml_acl_filtered_copy(const char *user, xmlNode *acl_source, xmlNode *xml,
*
* Check whether XML is a "scaffolding" element whose creation is implicitly
* allowed regardless of ACLs (that is, it is not in the ACL section and has
- * no attributes other than "id").
+ * no attributes other than \c PCMK_XA_ID).
*
* \param[in] xml XML element to check
*
@@ -525,7 +535,7 @@ implicitly_allowed(const xmlNode *xml)
GString *path = NULL;
for (xmlAttr *prop = xml->properties; prop != NULL; prop = prop->next) {
- if (strcmp((const char *) prop->name, XML_ATTR_ID) != 0) {
+ if (strcmp((const char *) prop->name, PCMK_XA_ID) != 0) {
return false;
}
}
@@ -533,7 +543,7 @@ implicitly_allowed(const xmlNode *xml)
path = pcmk__element_xpath(xml);
CRM_ASSERT(path != NULL);
- if (strstr((const char *) path->str, "/" XML_CIB_TAG_ACLS "/") != NULL) {
+ if (strstr((const char *) path->str, "/" PCMK_XE_ACLS "/") != NULL) {
g_string_free(path, TRUE);
return false;
}
@@ -542,7 +552,7 @@ implicitly_allowed(const xmlNode *xml)
return true;
}
-#define display_id(xml) (ID(xml)? ID(xml) : "<unset>")
+#define display_id(xml) pcmk__s(pcmk__xe_id(xml), "<unset>")
/*!
* \internal
@@ -551,7 +561,7 @@ implicitly_allowed(const xmlNode *xml)
* Given an XML element, free all of its descendant nodes created in violation
* of ACLs, with the exception of allowing "scaffolding" elements (i.e. those
* that aren't in the ACL section and don't have any attributes other than
- * "id").
+ * \c PCMK_XA_ID).
*
* \param[in,out] xml XML to check
* \param[in] check_top Whether to apply checks to argument itself
@@ -566,22 +576,23 @@ pcmk__apply_creation_acl(xmlNode *xml, bool check_top)
if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) {
if (implicitly_allowed(xml)) {
- crm_trace("Creation of <%s> scaffolding with id=\"%s\""
+ crm_trace("Creation of <%s> scaffolding with " PCMK_XA_ID "=\"%s\""
" is implicitly allowed",
xml->name, display_id(xml));
} else if (pcmk__check_acl(xml, NULL, pcmk__xf_acl_write)) {
- crm_trace("ACLs allow creation of <%s> with id=\"%s\"",
+ crm_trace("ACLs allow creation of <%s> with " PCMK_XA_ID "=\"%s\"",
xml->name, display_id(xml));
} else if (check_top) {
- crm_trace("ACLs disallow creation of <%s> with id=\"%s\"",
- xml->name, display_id(xml));
+ crm_trace("ACLs disallow creation of <%s> with "
+ PCMK_XA_ID "=\"%s\"", xml->name, display_id(xml));
pcmk_free_xml_subtree(xml);
return;
} else {
- crm_notice("ACLs would disallow creation of %s<%s> with id=\"%s\"",
+ crm_notice("ACLs would disallow creation of %s<%s> with "
+ PCMK_XA_ID "=\"%s\"",
((xml == xmlDocGetRootElement(xml->doc))? "root element " : ""),
xml->name, display_id(xml));
}
@@ -757,15 +768,13 @@ pcmk_acl_required(const char *user)
char *
pcmk__uid2username(uid_t uid)
{
- char *result = NULL;
struct passwd *pwent = getpwuid(uid);
if (pwent == NULL) {
crm_perror(LOG_INFO, "Cannot get user details for user ID %d", uid);
return NULL;
}
- pcmk__str_update(&result, pwent->pw_name);
- return result;
+ return pcmk__str_copy(pwent->pw_name);
}
/*!
@@ -797,25 +806,24 @@ pcmk__update_acl_user(xmlNode *request, const char *field,
if (effective_user == NULL) {
effective_user = pcmk__uid2username(geteuid());
if (effective_user == NULL) {
- effective_user = strdup("#unprivileged");
- CRM_CHECK(effective_user != NULL, return NULL);
+ effective_user = pcmk__str_copy("#unprivileged");
crm_err("Unable to determine effective user, assuming unprivileged for ACLs");
}
}
- requested_user = crm_element_value(request, XML_ACL_TAG_USER);
+ requested_user = crm_element_value(request, PCMK_XE_ACL_TARGET);
if (requested_user == NULL) {
/* @COMPAT rolling upgrades <=1.1.11
*
* field is checked for backward compatibility with older versions that
- * did not use XML_ACL_TAG_USER.
+ * did not use PCMK_XE_ACL_TARGET.
*/
requested_user = crm_element_value(request, field);
}
if (!pcmk__is_privileged(effective_user)) {
/* We're not running as a privileged user, set or overwrite any existing
- * value for $XML_ACL_TAG_USER
+ * value for PCMK_XE_ACL_TARGET
*/
user = effective_user;
@@ -831,7 +839,7 @@ pcmk__update_acl_user(xmlNode *request, const char *field,
} else if (!pcmk__is_privileged(peer_user)) {
/* The peer is not a privileged user, set or overwrite any existing
- * value for $XML_ACL_TAG_USER
+ * value for PCMK_XE_ACL_TARGET
*/
user = peer_user;
@@ -845,8 +853,8 @@ pcmk__update_acl_user(xmlNode *request, const char *field,
}
// This requires pointer comparison, not string comparison
- if (user != crm_element_value(request, XML_ACL_TAG_USER)) {
- crm_xml_add(request, XML_ACL_TAG_USER, user);
+ if (user != crm_element_value(request, PCMK_XE_ACL_TARGET)) {
+ crm_xml_add(request, PCMK_XE_ACL_TARGET, user);
}
if (field != NULL && user != crm_element_value(request, field)) {
diff --git a/lib/common/actions.c b/lib/common/actions.c
index e710615..ed1056f 100644
--- a/lib/common/actions.c
+++ b/lib/common/actions.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2004-2023 the Pacemaker project contributors
+ * Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -21,10 +21,164 @@
#include <crm/crm.h>
#include <crm/lrmd.h>
-#include <crm/msg_xml.h>
#include <crm/common/xml.h>
#include <crm/common/xml_internal.h>
#include <crm/common/util.h>
+#include <crm/common/scheduler.h>
+
+/*!
+ * \brief Get string equivalent of an action type
+ *
+ * \param[in] action Action type
+ *
+ * \return Static string describing \p action
+ */
+const char *
+pcmk_action_text(enum action_tasks action)
+{
+ switch (action) {
+ case pcmk_action_stop:
+ return PCMK_ACTION_STOP;
+
+ case pcmk_action_stopped:
+ return PCMK_ACTION_STOPPED;
+
+ case pcmk_action_start:
+ return PCMK_ACTION_START;
+
+ case pcmk_action_started:
+ return PCMK_ACTION_RUNNING;
+
+ case pcmk_action_shutdown:
+ return PCMK_ACTION_DO_SHUTDOWN;
+
+ case pcmk_action_fence:
+ return PCMK_ACTION_STONITH;
+
+ case pcmk_action_monitor:
+ return PCMK_ACTION_MONITOR;
+
+ case pcmk_action_notify:
+ return PCMK_ACTION_NOTIFY;
+
+ case pcmk_action_notified:
+ return PCMK_ACTION_NOTIFIED;
+
+ case pcmk_action_promote:
+ return PCMK_ACTION_PROMOTE;
+
+ case pcmk_action_promoted:
+ return PCMK_ACTION_PROMOTED;
+
+ case pcmk_action_demote:
+ return PCMK_ACTION_DEMOTE;
+
+ case pcmk_action_demoted:
+ return PCMK_ACTION_DEMOTED;
+
+ default: // pcmk_action_unspecified or invalid
+ return "no_action";
+ }
+}
+
+/*!
+ * \brief Parse an action type from an action name
+ *
+ * \param[in] action_name Action name
+ *
+ * \return Action type corresponding to \p action_name
+ */
+enum action_tasks
+pcmk_parse_action(const char *action_name)
+{
+ if (pcmk__str_eq(action_name, PCMK_ACTION_STOP, pcmk__str_none)) {
+ return pcmk_action_stop;
+
+ } else if (pcmk__str_eq(action_name, PCMK_ACTION_STOPPED, pcmk__str_none)) {
+ return pcmk_action_stopped;
+
+ } else if (pcmk__str_eq(action_name, PCMK_ACTION_START, pcmk__str_none)) {
+ return pcmk_action_start;
+
+ } else if (pcmk__str_eq(action_name, PCMK_ACTION_RUNNING, pcmk__str_none)) {
+ return pcmk_action_started;
+
+ } else if (pcmk__str_eq(action_name, PCMK_ACTION_DO_SHUTDOWN,
+ pcmk__str_none)) {
+ return pcmk_action_shutdown;
+
+ } else if (pcmk__str_eq(action_name, PCMK_ACTION_STONITH, pcmk__str_none)) {
+ return pcmk_action_fence;
+
+ } else if (pcmk__str_eq(action_name, PCMK_ACTION_MONITOR, pcmk__str_none)) {
+ return pcmk_action_monitor;
+
+ } else if (pcmk__str_eq(action_name, PCMK_ACTION_NOTIFY, pcmk__str_none)) {
+ return pcmk_action_notify;
+
+ } else if (pcmk__str_eq(action_name, PCMK_ACTION_NOTIFIED,
+ pcmk__str_none)) {
+ return pcmk_action_notified;
+
+ } else if (pcmk__str_eq(action_name, PCMK_ACTION_PROMOTE, pcmk__str_none)) {
+ return pcmk_action_promote;
+
+ } else if (pcmk__str_eq(action_name, PCMK_ACTION_DEMOTE, pcmk__str_none)) {
+ return pcmk_action_demote;
+
+ } else if (pcmk__str_eq(action_name, PCMK_ACTION_PROMOTED,
+ pcmk__str_none)) {
+ return pcmk_action_promoted;
+
+ } else if (pcmk__str_eq(action_name, PCMK_ACTION_DEMOTED, pcmk__str_none)) {
+ return pcmk_action_demoted;
+ }
+ return pcmk_action_unspecified;
+}
+
+/*!
+ * \brief Get string equivalent of a failure handling type
+ *
+ * \param[in] on_fail Failure handling type
+ *
+ * \return Static string describing \p on_fail
+ */
+const char *
+pcmk_on_fail_text(enum action_fail_response on_fail)
+{
+ switch (on_fail) {
+ case pcmk_on_fail_ignore:
+ return "ignore";
+
+ case pcmk_on_fail_demote:
+ return "demote";
+
+ case pcmk_on_fail_block:
+ return "block";
+
+ case pcmk_on_fail_restart:
+ return "recover";
+
+ case pcmk_on_fail_ban:
+ return "migrate";
+
+ case pcmk_on_fail_stop:
+ return "stop";
+
+ case pcmk_on_fail_fence_node:
+ return "fence";
+
+ case pcmk_on_fail_standby_node:
+ return "standby";
+
+ case pcmk_on_fail_restart_container:
+ return "restart-container";
+
+ case pcmk_on_fail_reset_remote:
+ return "reset-remote";
+ }
+ return "<unknown>";
+}
/*!
* \brief Generate an operation key (RESOURCE_ACTION_INTERVAL)
@@ -166,12 +320,12 @@ parse_op_key(const char *key, char **rsc_id, char **op_type, guint *interval_ms)
// Set output variables
if (rsc_id != NULL) {
*rsc_id = strndup(key, action_underbar);
- CRM_ASSERT(*rsc_id != NULL);
+ pcmk__mem_assert(*rsc_id);
}
if (op_type != NULL) {
*op_type = strndup(key + action_underbar + 1,
interval_underbar - action_underbar - 1);
- CRM_ASSERT(*op_type != NULL);
+ pcmk__mem_assert(*op_type);
}
if (interval_ms != NULL) {
*interval_ms = local_interval_ms;
@@ -220,8 +374,8 @@ decode_transition_magic(const char *magic, char **uuid, int *transition_id, int
#ifdef HAVE_SSCANF_M
res = sscanf(magic, "%d:%d;%ms", &local_op_status, &local_op_rc, &key);
#else
- key = calloc(1, strlen(magic) - 3); // magic must have >=4 other characters
- CRM_ASSERT(key);
+ // magic must have >=4 other characters
+ key = pcmk__assert_alloc(1, strlen(magic) - 3);
res = sscanf(magic, "%d:%d;%s", &local_op_status, &local_op_rc, key);
#endif
if (res == EOF) {
@@ -301,8 +455,7 @@ decode_transition_key(const char *key, char **uuid, int *transition_id, int *act
crm_warn("Invalid UUID '%s' in transition key '%s'", local_uuid, key);
}
if (uuid) {
- *uuid = strdup(local_uuid);
- CRM_ASSERT(*uuid);
+ *uuid = pcmk__str_copy(local_uuid);
}
if (transition_id) {
*transition_id = local_transition_id;
@@ -316,66 +469,6 @@ decode_transition_key(const char *key, char **uuid, int *transition_id, int *act
return TRUE;
}
-// Return true if a is an attribute that should be filtered
-static bool
-should_filter_for_digest(xmlAttrPtr a, void *user_data)
-{
- if (strncmp((const char *) a->name, CRM_META "_",
- sizeof(CRM_META " ") - 1) == 0) {
- return true;
- }
- return pcmk__str_any_of((const char *) a->name,
- XML_ATTR_ID,
- XML_ATTR_CRM_VERSION,
- XML_LRM_ATTR_OP_DIGEST,
- XML_LRM_ATTR_TARGET,
- XML_LRM_ATTR_TARGET_UUID,
- "pcmk_external_ip",
- NULL);
-}
-
-/*!
- * \internal
- * \brief Remove XML attributes not needed for operation digest
- *
- * \param[in,out] param_set XML with operation parameters
- */
-void
-pcmk__filter_op_for_digest(xmlNode *param_set)
-{
- char *key = NULL;
- char *timeout = NULL;
- guint interval_ms = 0;
-
- if (param_set == NULL) {
- return;
- }
-
- /* Timeout is useful for recurring operation digests, so grab it before
- * removing meta-attributes
- */
- key = crm_meta_name(XML_LRM_ATTR_INTERVAL_MS);
- if (crm_element_value_ms(param_set, key, &interval_ms) != pcmk_ok) {
- interval_ms = 0;
- }
- free(key);
- key = NULL;
- if (interval_ms != 0) {
- key = crm_meta_name(XML_ATTR_TIMEOUT);
- timeout = crm_element_value_copy(param_set, key);
- }
-
- // Remove all CRM_meta_* attributes and certain other attributes
- pcmk__xe_remove_matching_attrs(param_set, should_filter_for_digest, NULL);
-
- // Add timeout back for recurring operation digests
- if (timeout != NULL) {
- crm_xml_add(param_set, key, timeout);
- }
- free(timeout);
- free(key);
-}
-
int
rsc_op_expected_rc(const lrmd_event_data_t *op)
{
@@ -432,12 +525,12 @@ crm_create_op_xml(xmlNode *parent, const char *prefix, const char *task,
CRM_CHECK(prefix && task && interval_spec, return NULL);
- xml_op = create_xml_node(parent, XML_ATTR_OP);
+ xml_op = pcmk__xe_create(parent, PCMK_XE_OP);
crm_xml_set_id(xml_op, "%s-%s-%s", prefix, task, interval_spec);
- crm_xml_add(xml_op, XML_LRM_ATTR_INTERVAL, interval_spec);
- crm_xml_add(xml_op, "name", task);
+ crm_xml_add(xml_op, PCMK_META_INTERVAL, interval_spec);
+ crm_xml_add(xml_op, PCMK_XA_NAME, task);
if (timeout) {
- crm_xml_add(xml_op, XML_ATTR_TIMEOUT, timeout);
+ crm_xml_add(xml_op, PCMK_META_TIMEOUT, timeout);
}
return xml_op;
}
@@ -483,50 +576,12 @@ crm_op_needs_metadata(const char *rsc_class, const char *op)
*
* \param[in] action Action name to check
*
- * \return true if \p action is "off", "reboot", or "poweroff", otherwise false
+ * \return \c true if \p action is \c PCMK_ACTION_OFF, \c PCMK_ACTION_REBOOT,
+ * or \c PCMK__ACTION_POWEROFF, otherwise \c false
*/
bool
pcmk__is_fencing_action(const char *action)
{
return pcmk__str_any_of(action, PCMK_ACTION_OFF, PCMK_ACTION_REBOOT,
- "poweroff", NULL);
-}
-
-bool
-pcmk_is_probe(const char *task, guint interval)
-{
- if (task == NULL) {
- return false;
- }
-
- return (interval == 0)
- && pcmk__str_eq(task, PCMK_ACTION_MONITOR, pcmk__str_none);
-}
-
-bool
-pcmk_xe_is_probe(const xmlNode *xml_op)
-{
- const char *task = crm_element_value(xml_op, XML_LRM_ATTR_TASK);
- const char *interval_ms_s = crm_element_value(xml_op, XML_LRM_ATTR_INTERVAL_MS);
- int interval_ms;
-
- pcmk__scan_min_int(interval_ms_s, &interval_ms, 0);
- return pcmk_is_probe(task, interval_ms);
-}
-
-bool
-pcmk_xe_mask_probe_failure(const xmlNode *xml_op)
-{
- int status = PCMK_EXEC_UNKNOWN;
- int rc = PCMK_OCF_OK;
-
- if (!pcmk_xe_is_probe(xml_op)) {
- return false;
- }
-
- crm_element_value_int(xml_op, XML_LRM_ATTR_OPSTATUS, &status);
- crm_element_value_int(xml_op, XML_LRM_ATTR_RC, &rc);
-
- return rc == PCMK_OCF_NOT_INSTALLED || rc == PCMK_OCF_INVALID_PARAM ||
- status == PCMK_EXEC_NOT_INSTALLED;
+ PCMK__ACTION_POWEROFF, NULL);
}
diff --git a/lib/common/agents.c b/lib/common/agents.c
index d2066c0..dd4ba2e 100644
--- a/lib/common/agents.c
+++ b/lib/common/agents.c
@@ -46,7 +46,8 @@ pcmk_get_ra_caps(const char *standard)
* (which were likely never used as real configurations).
*
* @TODO Remove pcmk_ra_cap_unique at the next major schema version
- * bump, with a transform to remove globally-unique from the config.
+ * bump, with a transform to remove PCMK_META_GLOBALLY_UNIQUE from the
+ * config.
*/
return pcmk_ra_cap_params | pcmk_ra_cap_unique | pcmk_ra_cap_stdin
| pcmk_ra_cap_fence_params;
diff --git a/lib/common/alerts.c b/lib/common/alerts.c
index 98b1e3f..eac3e2e 100644
--- a/lib/common/alerts.c
+++ b/lib/common/alerts.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2015-2023 the Pacemaker project contributors
+ * Copyright 2015-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -10,7 +10,7 @@
#include <crm_internal.h>
#include <crm/crm.h>
#include <crm/lrmd.h>
-#include <crm/msg_xml.h>
+#include <crm/common/xml.h>
#include <crm/common/alerts_internal.h>
#include <crm/common/cib_internal.h>
#include <crm/common/xml_internal.h>
@@ -94,11 +94,11 @@ const char *pcmk__alert_keys[PCMK__ALERT_INTERNAL_KEY_MAX][3] =
pcmk__alert_t *
pcmk__alert_new(const char *id, const char *path)
{
- pcmk__alert_t *entry = calloc(1, sizeof(pcmk__alert_t));
+ pcmk__alert_t *entry = pcmk__assert_alloc(1, sizeof(pcmk__alert_t));
- CRM_ASSERT(entry && id && path);
- entry->id = strdup(id);
- entry->path = strdup(path);
+ CRM_ASSERT((id != NULL) && (path != NULL));
+ entry->id = pcmk__str_copy(id);
+ entry->path = pcmk__str_copy(path);
entry->timeout = PCMK__ALERT_DEFAULT_TIMEOUT_MS;
entry->flags = pcmk__alert_default;
return entry;
@@ -137,8 +137,8 @@ pcmk__dup_alert(const pcmk__alert_t *entry)
new_entry->timeout = entry->timeout;
new_entry->flags = entry->flags;
new_entry->envvars = pcmk__str_table_dup(entry->envvars);
- pcmk__str_update(&new_entry->tstamp_format, entry->tstamp_format);
- pcmk__str_update(&new_entry->recipient, entry->recipient);
+ new_entry->tstamp_format = pcmk__str_copy(entry->tstamp_format);
+ new_entry->recipient = pcmk__str_copy(entry->recipient);
if (entry->select_attribute_name) {
new_entry->select_attribute_name = g_strdupv(entry->select_attribute_name);
}
@@ -152,7 +152,7 @@ pcmk__add_alert_key(GHashTable *table, enum pcmk__alert_keys_e name,
for (const char **key = pcmk__alert_keys[name]; *key; key++) {
crm_trace("Inserting alert key %s = '%s'", *key, value);
if (value) {
- g_hash_table_insert(table, strdup(*key), strdup(value));
+ pcmk__insert_dup(table, *key, value);
} else {
g_hash_table_remove(table, *key);
}
@@ -165,6 +165,6 @@ pcmk__add_alert_key_int(GHashTable *table, enum pcmk__alert_keys_e name,
{
for (const char **key = pcmk__alert_keys[name]; *key; key++) {
crm_trace("Inserting alert key %s = %d", *key, value);
- g_hash_table_insert(table, strdup(*key), pcmk__itoa(value));
+ g_hash_table_insert(table, pcmk__str_copy(*key), pcmk__itoa(value));
}
}
diff --git a/lib/common/attrs.c b/lib/common/attrs.c
index 2be03b4..35715df 100644
--- a/lib/common/attrs.c
+++ b/lib/common/attrs.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2011-2022 the Pacemaker project contributors
+ * Copyright 2011-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -15,10 +15,12 @@
#include <stdio.h>
-#include <crm/msg_xml.h>
-#include <crm/common/attrd_internal.h>
+#include <crm/common/xml.h>
+#include <crm/common/scheduler.h>
+#include <crm/common/scheduler_internal.h>
-#define LRM_TARGET_ENV "OCF_RESKEY_" CRM_META "_" XML_LRM_ATTR_TARGET
+#define OCF_RESKEY_PREFIX "OCF_RESKEY_"
+#define LRM_TARGET_ENV OCF_RESKEY_PREFIX CRM_META "_" PCMK__META_ON_NODE
/*!
* \internal
@@ -27,7 +29,8 @@
* If given NULL, "auto", or "localhost" as an argument, check the environment
* to detect the node name that should be used to set node attributes. (The
* caller might not know the correct name, for example if the target is part of
- * a bundle with container-attribute-target set to "host".)
+ * a bundle with \c PCMK_META_CONTAINER_ATTRIBUTE_TARGET set to
+ * \c PCMK_VALUE_HOST.)
*
* \param[in] name NULL, "auto" or "localhost" to check environment variables,
* or anything else to return NULL
@@ -39,13 +42,22 @@ const char *
pcmk__node_attr_target(const char *name)
{
if (name == NULL || pcmk__strcase_any_of(name, "auto", "localhost", NULL)) {
- char *target_var = crm_meta_name(XML_RSC_ATTR_TARGET);
- char *phys_var = crm_meta_name(PCMK__ENV_PHYSICAL_HOST);
- const char *target = getenv(target_var);
- const char *host_physical = getenv(phys_var);
+ char buf[128] = OCF_RESKEY_PREFIX;
+ size_t offset = sizeof(OCF_RESKEY_PREFIX) - 1;
+ char *target_var = crm_meta_name(PCMK_META_CONTAINER_ATTRIBUTE_TARGET);
+ char *phys_var = crm_meta_name(PCMK__META_PHYSICAL_HOST);
+ const char *target = NULL;
+ const char *host_physical = NULL;
+
+ snprintf(buf + offset, sizeof(buf) - offset, "%s", target_var);
+ target = getenv(buf);
+
+ snprintf(buf + offset, sizeof(buf) - offset, "%s", phys_var);
+ host_physical = getenv(buf);
// It is important to use the name by which the scheduler knows us
- if (host_physical && pcmk__str_eq(target, "host", pcmk__str_casei)) {
+ if (host_physical
+ && pcmk__str_eq(target, PCMK_VALUE_HOST, pcmk__str_casei)) {
name = host_physical;
} else {
@@ -58,7 +70,7 @@ pcmk__node_attr_target(const char *name)
free(target_var);
free(phys_var);
- // TODO? Call get_local_node_name() if name == NULL
+ // TODO? Call pcmk__cluster_local_node_name() if name == NULL
// (currently would require linkage against libcrmcluster)
return name;
} else {
@@ -87,3 +99,85 @@ pcmk_promotion_score_name(const char *rsc_id)
}
return crm_strdup_printf("master-%s", rsc_id);
}
+
+/*!
+ * \internal
+ * \brief Get the value of a node attribute
+ *
+ * \param[in] node Node to get attribute for
+ * \param[in] name Name of node attribute to get
+ * \param[in] target If this is \c PCMK_VALUE_HOST and \p node is a guest
+ * (bundle) node, get the value from the guest's host,
+ * otherwise get the value from \p node itself
+ * \param[in] node_type If getting the value from \p node's host, this
+ * indicates whether to check the current or assigned host
+ *
+ * \return Value of \p name attribute for \p node
+ */
+const char *
+pcmk__node_attr(const pcmk_node_t *node, const char *name, const char *target,
+ enum pcmk__rsc_node node_type)
+{
+ const char *value = NULL; // Attribute value to return
+ const char *node_type_s = NULL; // Readable equivalent of node_type
+ const pcmk_node_t *host = NULL;
+ const pcmk_resource_t *container = NULL;
+
+ if ((node == NULL) || (name == NULL)) {
+ return NULL;
+ }
+
+ /* Check the node's own attributes unless this is a guest (bundle) node with
+ * the container host as the attribute target.
+ */
+ if (!pcmk__is_guest_or_bundle_node(node)
+ || !pcmk__str_eq(target, PCMK_VALUE_HOST, pcmk__str_casei)) {
+ value = g_hash_table_lookup(node->details->attrs, name);
+ crm_trace("%s='%s' on %s",
+ name, pcmk__s(value, ""), pcmk__node_name(node));
+ return value;
+ }
+
+ /* This resource needs attributes set for the container's host instead of
+ * for the container itself (useful when the container uses the host's
+ * storage).
+ */
+ container = node->details->remote_rsc->container;
+
+ switch (node_type) {
+ case pcmk__rsc_node_assigned:
+ host = container->allocated_to;
+ if (host == NULL) {
+ crm_trace("Skipping %s lookup for %s because "
+ "its container %s is unassigned",
+ name, pcmk__node_name(node), container->id);
+ return NULL;
+ }
+ node_type_s = "assigned";
+ break;
+
+ case pcmk__rsc_node_current:
+ if (container->running_on != NULL) {
+ host = container->running_on->data;
+ }
+ if (host == NULL) {
+ crm_trace("Skipping %s lookup for %s because "
+ "its container %s is inactive",
+ name, pcmk__node_name(node), container->id);
+ return NULL;
+ }
+ node_type_s = "current";
+ break;
+
+ default:
+ // Add support for other enum pcmk__rsc_node values if needed
+ CRM_ASSERT(false);
+ break;
+ }
+
+ value = g_hash_table_lookup(host->details->attrs, name);
+ crm_trace("%s='%s' for %s on %s container host %s",
+ name, pcmk__s(value, ""), pcmk__node_name(node), node_type_s,
+ pcmk__node_name(host));
+ return value;
+}
diff --git a/lib/common/cib.c b/lib/common/cib.c
index fee7881..fee962b 100644
--- a/lib/common/cib.c
+++ b/lib/common/cib.c
@@ -1,6 +1,6 @@
/*
* Original copyright 2004 International Business Machines
- * Later changes copyright 2008-2023 the Pacemaker project contributors
+ * Later changes copyright 2008-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -13,7 +13,7 @@
#include <stdio.h>
#include <libxml/tree.h> // xmlNode
-#include <crm/msg_xml.h>
+#include <crm/common/xml.h>
#include <crm/common/cib.h>
#include <crm/common/cib_internal.h>
@@ -29,74 +29,74 @@ static struct {
} cib_sections[] = {
{
// This first entry is also the default if a NULL is compared
- XML_TAG_CIB,
+ PCMK_XE_CIB,
NULL,
- "//" XML_TAG_CIB
+ "//" PCMK_XE_CIB
},
{
- XML_CIB_TAG_STATUS,
- "/" XML_TAG_CIB,
- "//" XML_TAG_CIB "/" XML_CIB_TAG_STATUS
+ PCMK_XE_STATUS,
+ "/" PCMK_XE_CIB,
+ "//" PCMK_XE_CIB "/" PCMK_XE_STATUS
},
{
- XML_CIB_TAG_CONFIGURATION,
- "/" XML_TAG_CIB,
- "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION
+ PCMK_XE_CONFIGURATION,
+ "/" PCMK_XE_CIB,
+ "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION
},
{
- XML_CIB_TAG_CRMCONFIG,
- "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION,
- "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_CRMCONFIG
+ PCMK_XE_CRM_CONFIG,
+ "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION,
+ "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_CRM_CONFIG
},
{
- XML_CIB_TAG_NODES,
- "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION,
- "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_NODES
+ PCMK_XE_NODES,
+ "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION,
+ "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_NODES
},
{
- XML_CIB_TAG_RESOURCES,
- "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION,
- "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_RESOURCES
+ PCMK_XE_RESOURCES,
+ "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION,
+ "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_RESOURCES
},
{
- XML_CIB_TAG_CONSTRAINTS,
- "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION,
- "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_CONSTRAINTS
+ PCMK_XE_CONSTRAINTS,
+ "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION,
+ "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_CONSTRAINTS
},
{
- XML_CIB_TAG_OPCONFIG,
- "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION,
- "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_OPCONFIG
+ PCMK_XE_OP_DEFAULTS,
+ "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION,
+ "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_OP_DEFAULTS
},
{
- XML_CIB_TAG_RSCCONFIG,
- "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION,
- "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_RSCCONFIG
+ PCMK_XE_RSC_DEFAULTS,
+ "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION,
+ "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_RSC_DEFAULTS
},
{
- XML_CIB_TAG_ACLS,
- "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION,
- "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_ACLS
+ PCMK_XE_ACLS,
+ "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION,
+ "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_ACLS
},
{
- XML_TAG_FENCING_TOPOLOGY,
- "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION,
- "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_TAG_FENCING_TOPOLOGY
+ PCMK_XE_FENCING_TOPOLOGY,
+ "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION,
+ "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_FENCING_TOPOLOGY
},
{
- XML_CIB_TAG_TAGS,
- "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION,
- "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_TAGS
+ PCMK_XE_TAGS,
+ "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION,
+ "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_TAGS
},
{
- XML_CIB_TAG_ALERTS,
- "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION,
- "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_ALERTS
+ PCMK_XE_ALERTS,
+ "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION,
+ "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_ALERTS
},
{
- XML_CIB_TAG_SECTION_ALL,
+ PCMK__XE_ALL,
NULL,
- "//" XML_TAG_CIB
+ "//" PCMK_XE_CIB
},
};
@@ -173,3 +173,19 @@ pcmk_find_cib_element(xmlNode *cib, const char *element_name)
{
return get_xpath_object(pcmk_cib_xpath_for(element_name), cib, LOG_TRACE);
}
+
+/*!
+ * \internal
+ * \brief Check that the feature set in the CIB is supported on this node
+ *
+ * \param[in] new_version PCMK_XA_CRM_FEATURE_SET attribute from the CIB
+ */
+int
+pcmk__check_feature_set(const char *cib_version)
+{
+ if (cib_version && compare_version(cib_version, CRM_FEATURE_SET) > 0) {
+ return EPROTONOSUPPORT;
+ }
+
+ return pcmk_rc_ok;
+}
diff --git a/lib/common/crmcommon_private.h b/lib/common/crmcommon_private.h
index 121d663..d90a64e 100644
--- a/lib/common/crmcommon_private.h
+++ b/lib/common/crmcommon_private.h
@@ -1,5 +1,5 @@
/*
- * Copyright 2018-2023 the Pacemaker project contributors
+ * Copyright 2018-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -17,7 +17,7 @@
#include <stdint.h> // uint8_t, uint32_t
#include <stdbool.h> // bool
#include <sys/types.h> // size_t
-#include <glib.h> // GList
+#include <glib.h> // gchar, GList
#include <libxml/tree.h> // xmlNode, xmlAttr
#include <qb/qbipcc.h> // struct qb_ipc_response_header
@@ -33,23 +33,33 @@
* (e.g. when checking differences) that something was deleted.
*/
typedef struct pcmk__deleted_xml_s {
- char *path;
- int position;
+ gchar *path;
+ int position;
} pcmk__deleted_xml_t;
typedef struct xml_node_private_s {
- long check;
+ uint32_t check;
uint32_t flags;
} xml_node_private_t;
typedef struct xml_doc_private_s {
- long check;
+ uint32_t check;
uint32_t flags;
char *user;
GList *acls;
GList *deleted_objs; // List of pcmk__deleted_xml_t
} xml_doc_private_t;
+// XML entity references
+
+#define PCMK__XML_ENTITY_AMP "&amp;"
+#define PCMK__XML_ENTITY_GT "&gt;"
+#define PCMK__XML_ENTITY_LT "&lt;"
+#define PCMK__XML_ENTITY_QUOT "&quot;"
+
+//! libxml2 supports only XML version 1.0, at least as of libxml2-2.12.5
+#define PCMK__XML_VERSION ((pcmkXmlStr) "1.0")
+
#define pcmk__set_xml_flags(xml_priv, flags_to_set) do { \
(xml_priv)->flags = pcmk__set_flags_as(__func__, __LINE__, \
LOG_NEVER, "XML", "XML node", (xml_priv)->flags, \
@@ -63,14 +73,10 @@ typedef struct xml_doc_private_s {
} while (0)
G_GNUC_INTERNAL
-void pcmk__xml2text(const xmlNode *data, uint32_t options, GString *buffer,
- int depth);
-
-G_GNUC_INTERNAL
bool pcmk__tracking_xml_changes(xmlNode *xml, bool lazy);
G_GNUC_INTERNAL
-void pcmk__mark_xml_created(xmlNode *xml);
+void pcmk__xml_mark_created(xmlNode *xml);
G_GNUC_INTERNAL
int pcmk__xml_position(const xmlNode *xml,
@@ -82,7 +88,7 @@ xmlNode *pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle,
G_GNUC_INTERNAL
void pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update,
- bool as_diff);
+ uint32_t flags, bool as_diff);
G_GNUC_INTERNAL
xmlNode *pcmk__xc_match(const xmlNode *root, const xmlNode *search_comment,
@@ -125,6 +131,36 @@ bool pcmk__marked_as_deleted(xmlAttrPtr a, void *user_data);
G_GNUC_INTERNAL
void pcmk__dump_xml_attr(const xmlAttr *attr, GString *buffer);
+G_GNUC_INTERNAL
+int pcmk__xe_set_score(xmlNode *target, const char *name, const char *value);
+
+/*
+ * Date/times
+ */
+
+// For use with pcmk__add_time_from_xml()
+enum pcmk__time_component {
+ pcmk__time_unknown,
+ pcmk__time_years,
+ pcmk__time_months,
+ pcmk__time_weeks,
+ pcmk__time_days,
+ pcmk__time_hours,
+ pcmk__time_minutes,
+ pcmk__time_seconds,
+};
+
+G_GNUC_INTERNAL
+const char *pcmk__time_component_attr(enum pcmk__time_component component);
+
+G_GNUC_INTERNAL
+int pcmk__add_time_from_xml(crm_time_t *t, enum pcmk__time_component component,
+ const xmlNode *xml);
+
+G_GNUC_INTERNAL
+void pcmk__set_time_if_earlier(crm_time_t *target, const crm_time_t *source);
+
+
/*
* IPC
*/
@@ -274,8 +310,81 @@ int pcmk__bare_output_new(pcmk__output_t **out, const char *fmt_name,
const char *filename, char **argv);
G_GNUC_INTERNAL
+void pcmk__register_option_messages(pcmk__output_t *out);
+
+G_GNUC_INTERNAL
void pcmk__register_patchset_messages(pcmk__output_t *out);
+G_GNUC_INTERNAL
+bool pcmk__output_text_get_fancy(pcmk__output_t *out);
+
+/*
+ * Rules
+ */
+
+// How node attribute values may be compared in rules
+enum pcmk__comparison {
+ pcmk__comparison_unknown,
+ pcmk__comparison_defined,
+ pcmk__comparison_undefined,
+ pcmk__comparison_eq,
+ pcmk__comparison_ne,
+ pcmk__comparison_lt,
+ pcmk__comparison_lte,
+ pcmk__comparison_gt,
+ pcmk__comparison_gte,
+};
+
+// How node attribute values may be parsed in rules
+enum pcmk__type {
+ pcmk__type_unknown,
+ pcmk__type_string,
+ pcmk__type_integer,
+ pcmk__type_number,
+ pcmk__type_version,
+};
+
+// Where to obtain reference value for a node attribute comparison
+enum pcmk__reference_source {
+ pcmk__source_unknown,
+ pcmk__source_literal,
+ pcmk__source_instance_attrs,
+ pcmk__source_meta_attrs,
+};
+
+G_GNUC_INTERNAL
+enum pcmk__comparison pcmk__parse_comparison(const char *op);
+
+G_GNUC_INTERNAL
+enum pcmk__type pcmk__parse_type(const char *type, enum pcmk__comparison op,
+ const char *value1, const char *value2);
+
+G_GNUC_INTERNAL
+enum pcmk__reference_source pcmk__parse_source(const char *source);
+
+G_GNUC_INTERNAL
+int pcmk__cmp_by_type(const char *value1, const char *value2,
+ enum pcmk__type type);
+
+G_GNUC_INTERNAL
+int pcmk__unpack_duration(const xmlNode *duration, const crm_time_t *start,
+ crm_time_t **end);
+
+G_GNUC_INTERNAL
+int pcmk__evaluate_date_spec(const xmlNode *date_spec, const crm_time_t *now);
+
+G_GNUC_INTERNAL
+int pcmk__evaluate_attr_expression(const xmlNode *expression,
+ const pcmk_rule_input_t *rule_input);
+
+G_GNUC_INTERNAL
+int pcmk__evaluate_rsc_expression(const xmlNode *expr,
+ const pcmk_rule_input_t *rule_input);
+
+G_GNUC_INTERNAL
+int pcmk__evaluate_op_expression(const xmlNode *expr,
+ const pcmk_rule_input_t *rule_input);
+
/*
* Utils
@@ -283,4 +392,31 @@ void pcmk__register_patchset_messages(pcmk__output_t *out);
#define PCMK__PW_BUFFER_LEN 500
+/*
+ * Schemas
+ */
+typedef struct {
+ unsigned char v[2];
+} pcmk__schema_version_t;
+
+enum pcmk__schema_validator {
+ pcmk__schema_validator_none,
+ pcmk__schema_validator_rng
+};
+
+typedef struct {
+ int schema_index;
+ char *name;
+ char *transform;
+ void *cache;
+ enum pcmk__schema_validator validator;
+ pcmk__schema_version_t version;
+ char *transform_enter;
+ bool transform_onleave;
+} pcmk__schema_t;
+
+G_GNUC_INTERNAL
+GList *pcmk__find_x_0_schema(void);
+
+
#endif // CRMCOMMON_PRIVATE__H
diff --git a/lib/common/digest.c b/lib/common/digest.c
index 4de6f97..9a06ff4 100644
--- a/lib/common/digest.c
+++ b/lib/common/digest.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2015-2023 the Pacemaker project contributors
+ * Copyright 2015-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -16,7 +16,6 @@
#include <md5.h>
#include <crm/crm.h>
-#include <crm/msg_xml.h>
#include <crm/common/xml.h>
#include "crmcommon_private.h"
@@ -37,7 +36,7 @@ dump_xml_for_digest(xmlNodePtr xml)
/* for compatibility with the old result which is used for v1 digests */
g_string_append_c(buffer, ' ');
- pcmk__xml2text(xml, 0, buffer, 0);
+ pcmk__xml_string(xml, 0, buffer, 0);
g_string_append_c(buffer, '\n');
return buffer;
@@ -92,13 +91,12 @@ static char *
calculate_xml_digest_v2(const xmlNode *source, gboolean do_filter)
{
char *digest = NULL;
- GString *buffer = g_string_sized_new(1024);
+ GString *buf = g_string_sized_new(1024);
crm_trace("Begin digest %s", do_filter?"filtered":"");
- pcmk__xml2text(source, (do_filter? pcmk__xml_fmt_filtered : 0), buffer, 0);
- CRM_ASSERT(buffer != NULL);
- digest = crm_md5sum((const char *) buffer->str);
+ pcmk__xml_string(source, (do_filter? pcmk__xml_fmt_filtered : 0), buf, 0);
+ digest = crm_md5sum(buf->str);
pcmk__if_tracing(
{
@@ -106,17 +104,17 @@ calculate_xml_digest_v2(const xmlNode *source, gboolean do_filter)
pcmk__get_tmpdir(), digest);
crm_trace("Saving %s.%s.%s to %s",
- crm_element_value(source, XML_ATTR_GENERATION_ADMIN),
- crm_element_value(source, XML_ATTR_GENERATION),
- crm_element_value(source, XML_ATTR_NUMUPDATES),
+ crm_element_value(source, PCMK_XA_ADMIN_EPOCH),
+ crm_element_value(source, PCMK_XA_EPOCH),
+ crm_element_value(source, PCMK_XA_NUM_UPDATES),
trace_file);
save_xml_to_file(source, "digest input", trace_file);
free(trace_file);
},
{}
);
- g_string_free(buffer, TRUE);
crm_trace("End digest");
+ g_string_free(buf, TRUE);
return digest;
}
@@ -234,11 +232,11 @@ bool
pcmk__xa_filterable(const char *name)
{
static const char *filter[] = {
- XML_ATTR_ORIGIN,
- XML_CIB_ATTR_WRITTEN,
- XML_ATTR_UPDATE_ORIG,
- XML_ATTR_UPDATE_CLIENT,
- XML_ATTR_UPDATE_USER,
+ PCMK_XA_CRM_DEBUG_ORIGIN,
+ PCMK_XA_CIB_LAST_WRITTEN,
+ PCMK_XA_UPDATE_ORIGIN,
+ PCMK_XA_UPDATE_CLIENT,
+ PCMK_XA_UPDATE_USER,
};
for (int i = 0; i < PCMK__NELEM(filter); i++) {
@@ -276,3 +274,63 @@ crm_md5sum(const char *buffer)
}
return digest;
}
+
+// Return true if a is an attribute that should be filtered
+static bool
+should_filter_for_digest(xmlAttrPtr a, void *user_data)
+{
+ if (strncmp((const char *) a->name, CRM_META "_",
+ sizeof(CRM_META " ") - 1) == 0) {
+ return true;
+ }
+ return pcmk__str_any_of((const char *) a->name,
+ PCMK_XA_ID,
+ PCMK_XA_CRM_FEATURE_SET,
+ PCMK__XA_OP_DIGEST,
+ PCMK__META_ON_NODE,
+ PCMK__META_ON_NODE_UUID,
+ "pcmk_external_ip",
+ NULL);
+}
+
+/*!
+ * \internal
+ * \brief Remove XML attributes not needed for operation digest
+ *
+ * \param[in,out] param_set XML with operation parameters
+ */
+void
+pcmk__filter_op_for_digest(xmlNode *param_set)
+{
+ char *key = NULL;
+ char *timeout = NULL;
+ guint interval_ms = 0;
+
+ if (param_set == NULL) {
+ return;
+ }
+
+ /* Timeout is useful for recurring operation digests, so grab it before
+ * removing meta-attributes
+ */
+ key = crm_meta_name(PCMK_META_INTERVAL);
+ if (crm_element_value_ms(param_set, key, &interval_ms) != pcmk_ok) {
+ interval_ms = 0;
+ }
+ free(key);
+ key = NULL;
+ if (interval_ms != 0) {
+ key = crm_meta_name(PCMK_META_TIMEOUT);
+ timeout = crm_element_value_copy(param_set, key);
+ }
+
+ // Remove all CRM_meta_* attributes and certain other attributes
+ pcmk__xe_remove_matching_attrs(param_set, should_filter_for_digest, NULL);
+
+ // Add timeout back for recurring operation digests
+ if (timeout != NULL) {
+ crm_xml_add(param_set, key, timeout);
+ }
+ free(timeout);
+ free(key);
+}
diff --git a/lib/common/health.c b/lib/common/health.c
index ee412be..20cf36e 100644
--- a/lib/common/health.c
+++ b/lib/common/health.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 the Pacemaker project contributors
+ * Copyright 2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -21,11 +21,11 @@ bool
pcmk__validate_health_strategy(const char *value)
{
return pcmk__strcase_any_of(value,
- PCMK__VALUE_NONE,
- PCMK__VALUE_CUSTOM,
- PCMK__VALUE_ONLY_GREEN,
- PCMK__VALUE_PROGRESSIVE,
- PCMK__VALUE_MIGRATE_ON_RED,
+ PCMK_VALUE_NONE,
+ PCMK_VALUE_CUSTOM,
+ PCMK_VALUE_ONLY_GREEN,
+ PCMK_VALUE_PROGRESSIVE,
+ PCMK_VALUE_MIGRATE_ON_RED,
NULL);
}
@@ -40,29 +40,24 @@ pcmk__validate_health_strategy(const char *value)
enum pcmk__health_strategy
pcmk__parse_health_strategy(const char *value)
{
- if (pcmk__str_eq(value, PCMK__VALUE_NONE,
+ if (pcmk__str_eq(value, PCMK_VALUE_NONE,
pcmk__str_null_matches|pcmk__str_casei)) {
return pcmk__health_strategy_none;
-
- } else if (pcmk__str_eq(value, PCMK__VALUE_MIGRATE_ON_RED,
- pcmk__str_casei)) {
+ }
+ if (pcmk__str_eq(value, PCMK_VALUE_MIGRATE_ON_RED, pcmk__str_casei)) {
return pcmk__health_strategy_no_red;
-
- } else if (pcmk__str_eq(value, PCMK__VALUE_ONLY_GREEN,
- pcmk__str_casei)) {
+ }
+ if (pcmk__str_eq(value, PCMK_VALUE_ONLY_GREEN, pcmk__str_casei)) {
return pcmk__health_strategy_only_green;
-
- } else if (pcmk__str_eq(value, PCMK__VALUE_PROGRESSIVE,
- pcmk__str_casei)) {
+ }
+ if (pcmk__str_eq(value, PCMK_VALUE_PROGRESSIVE, pcmk__str_casei)) {
return pcmk__health_strategy_progressive;
-
- } else if (pcmk__str_eq(value, PCMK__VALUE_CUSTOM,
- pcmk__str_casei)) {
+ }
+ if (pcmk__str_eq(value, PCMK_VALUE_CUSTOM, pcmk__str_casei)) {
return pcmk__health_strategy_custom;
-
} else {
- pcmk__config_err("Using default of \"" PCMK__VALUE_NONE "\" for "
- PCMK__OPT_NODE_HEALTH_STRATEGY
+ pcmk__config_err("Using default of \"" PCMK_VALUE_NONE "\" for "
+ PCMK_OPT_NODE_HEALTH_STRATEGY
" because '%s' is not a valid value",
value);
return pcmk__health_strategy_none;
diff --git a/lib/common/io.c b/lib/common/io.c
index 35efbe9..ee71af7 100644
--- a/lib/common/io.c
+++ b/lib/common/io.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2004-2022 the Pacemaker project contributors
+ * Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -633,20 +633,13 @@ pcmk__close_fds_in_child(bool all)
char *
pcmk__full_path(const char *filename, const char *dirname)
{
- char *path = NULL;
-
CRM_ASSERT(filename != NULL);
if (filename[0] == '/') {
- path = strdup(filename);
- CRM_ASSERT(path != NULL);
-
- } else {
- CRM_ASSERT(dirname != NULL);
- path = crm_strdup_printf("%s/%s", dirname, filename);
+ return pcmk__str_copy(filename);
}
-
- return path;
+ CRM_ASSERT(dirname != NULL);
+ return crm_strdup_printf("%s/%s", dirname, filename);
}
// Deprecated functions kept only for backward API compatibility
diff --git a/lib/common/ipc_attrd.c b/lib/common/ipc_attrd.c
index 9caaabe..5ab0f2d 100644
--- a/lib/common/ipc_attrd.c
+++ b/lib/common/ipc_attrd.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2011-2023 the Pacemaker project contributors
+ * Copyright 2011-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -16,10 +16,10 @@
#include <stdio.h>
#include <crm/crm.h>
+#include <crm/common/attrs_internal.h>
#include <crm/common/ipc.h>
#include <crm/common/ipc_attrd_internal.h>
-#include <crm/common/attrd_internal.h>
-#include <crm/msg_xml.h>
+#include <crm/common/xml.h>
#include "crmcommon_private.h"
static void
@@ -30,13 +30,13 @@ set_pairs_data(pcmk__attrd_api_reply_t *data, xmlNode *msg_data)
name = crm_element_value(msg_data, PCMK__XA_ATTR_NAME);
- for (xmlNode *node = first_named_child(msg_data, XML_CIB_TAG_NODE);
- node != NULL; node = crm_next_same_xml(node)) {
- pair = calloc(1, sizeof(pcmk__attrd_query_pair_t));
+ for (xmlNode *node = pcmk__xe_first_child(msg_data, PCMK_XE_NODE, NULL,
+ NULL);
+ node != NULL; node = pcmk__xe_next_same(node)) {
- CRM_ASSERT(pair != NULL);
+ pair = pcmk__assert_alloc(1, sizeof(pcmk__attrd_query_pair_t));
- pair->node = crm_element_value(node, PCMK__XA_ATTR_NODE_NAME);
+ pair->node = crm_element_value(node, PCMK__XA_ATTR_HOST);
pair->name = name;
pair->value = crm_element_value(node, PCMK__XA_ATTR_VALUE);
data->data.pairs = g_list_prepend(data->data.pairs, pair);
@@ -46,7 +46,7 @@ set_pairs_data(pcmk__attrd_api_reply_t *data, xmlNode *msg_data)
static bool
reply_expected(pcmk_ipc_api_t *api, const xmlNode *request)
{
- const char *command = crm_element_value(request, PCMK__XA_TASK);
+ const char *command = crm_element_value(request, PCMK_XA_TASK);
return pcmk__str_any_of(command,
PCMK__ATTRD_CMD_CLEAR_FAILURE,
@@ -68,21 +68,22 @@ dispatch(pcmk_ipc_api_t *api, xmlNode *reply)
pcmk__attrd_reply_unknown
};
- if (pcmk__str_eq((const char *) reply->name, "ack", pcmk__str_none)) {
+ if (pcmk__xe_is(reply, PCMK__XE_ACK)) {
return false;
}
/* Do some basic validation of the reply */
- value = crm_element_value(reply, F_TYPE);
+ value = crm_element_value(reply, PCMK__XA_T);
if (pcmk__str_empty(value)
- || !pcmk__str_eq(value, T_ATTRD, pcmk__str_none)) {
+ || !pcmk__str_eq(value, PCMK__VALUE_ATTRD, pcmk__str_none)) {
crm_info("Unrecognizable message from attribute manager: "
- "message type '%s' not '" T_ATTRD "'", pcmk__s(value, ""));
+ "message type '%s' not '" PCMK__VALUE_ATTRD "'",
+ pcmk__s(value, ""));
status = CRM_EX_PROTOCOL;
goto done;
}
- value = crm_element_value(reply, F_SUBTYPE);
+ value = crm_element_value(reply, PCMK__XA_SUBT);
/* Only the query command gets a reply for now. NULL counts as query for
* backward compatibility with attribute managers <2.1.3 that didn't set it.
@@ -139,61 +140,38 @@ pcmk__attrd_api_methods(void)
static xmlNode *
create_attrd_op(const char *user_name)
{
- xmlNode *attrd_op = create_xml_node(NULL, __func__);
+ xmlNode *attrd_op = pcmk__xe_create(NULL, __func__);
- crm_xml_add(attrd_op, F_TYPE, T_ATTRD);
- crm_xml_add(attrd_op, F_ORIG, (crm_system_name? crm_system_name: "unknown"));
+ crm_xml_add(attrd_op, PCMK__XA_T, PCMK__VALUE_ATTRD);
+ crm_xml_add(attrd_op, PCMK__XA_SRC, pcmk__s(crm_system_name, "unknown"));
crm_xml_add(attrd_op, PCMK__XA_ATTR_USER, user_name);
return attrd_op;
}
static int
-create_api(pcmk_ipc_api_t **api)
-{
- int rc = pcmk_new_ipc_api(api, pcmk_ipc_attrd);
-
- if (rc != pcmk_rc_ok) {
- crm_err("Could not connect to attrd: %s", pcmk_rc_str(rc));
- }
-
- return rc;
-}
-
-static void
-destroy_api(pcmk_ipc_api_t *api)
-{
- pcmk_disconnect_ipc(api);
- pcmk_free_ipc_api(api);
- api = NULL;
-}
-
-static int
connect_and_send_attrd_request(pcmk_ipc_api_t *api, const xmlNode *request)
{
int rc = pcmk_rc_ok;
+ bool created_api = false;
- rc = pcmk__connect_ipc(api, pcmk_ipc_dispatch_sync, 5);
- if (rc != pcmk_rc_ok) {
- crm_err("Could not connect to %s: %s",
- pcmk_ipc_name(api, true), pcmk_rc_str(rc));
- return rc;
+ if (api == NULL) {
+ rc = pcmk_new_ipc_api(&api, pcmk_ipc_attrd);
+ if (rc != pcmk_rc_ok) {
+ return rc;
+ }
+ created_api = true;
}
- rc = pcmk__send_ipc_request(api, request);
- if (rc != pcmk_rc_ok) {
- crm_err("Could not send request to %s: %s",
- pcmk_ipc_name(api, true), pcmk_rc_str(rc));
- return rc;
+ rc = pcmk__connect_ipc(api, pcmk_ipc_dispatch_sync, 5);
+ if (rc == pcmk_rc_ok) {
+ rc = pcmk__send_ipc_request(api, request);
}
- return pcmk_rc_ok;
-}
-
-static int
-send_attrd_request(pcmk_ipc_api_t *api, const xmlNode *request)
-{
- return pcmk__send_ipc_request(api, request);
+ if (created_api) {
+ pcmk_free_ipc_api(api);
+ }
+ return rc;
}
int
@@ -212,44 +190,28 @@ pcmk__attrd_api_clear_failures(pcmk_ipc_api_t *api, const char *node,
node = target;
}
- crm_xml_add(request, PCMK__XA_TASK, PCMK__ATTRD_CMD_CLEAR_FAILURE);
- pcmk__xe_add_node(request, node, 0);
- crm_xml_add(request, PCMK__XA_ATTR_RESOURCE, resource);
- crm_xml_add(request, PCMK__XA_ATTR_OPERATION, operation);
- crm_xml_add(request, PCMK__XA_ATTR_INTERVAL, interval_spec);
- crm_xml_add_int(request, PCMK__XA_ATTR_IS_REMOTE,
- pcmk_is_set(options, pcmk__node_attr_remote));
-
- if (api == NULL) {
- rc = create_api(&api);
- if (rc != pcmk_rc_ok) {
- return rc;
- }
-
- rc = connect_and_send_attrd_request(api, request);
- destroy_api(api);
-
- } else if (!pcmk_ipc_is_connected(api)) {
- rc = connect_and_send_attrd_request(api, request);
-
- } else {
- rc = send_attrd_request(api, request);
- }
-
- free_xml(request);
-
if (operation) {
- interval_desc = interval_spec? interval_spec : "nonrecurring";
+ interval_desc = pcmk__s(interval_spec, "nonrecurring");
op_desc = operation;
} else {
interval_desc = "all";
op_desc = "operations";
}
+ crm_debug("Asking %s to clear failure of %s %s for %s on %s",
+ pcmk_ipc_name(api, true), interval_desc, op_desc,
+ pcmk__s(resource, "all resources"), pcmk__s(node, "all nodes"));
- crm_debug("Asked pacemaker-attrd to clear failure of %s %s for %s on %s: %s (%d)",
- interval_desc, op_desc, (resource? resource : "all resources"),
- (node? node : "all nodes"), pcmk_rc_str(rc), rc);
+ crm_xml_add(request, PCMK_XA_TASK, PCMK__ATTRD_CMD_CLEAR_FAILURE);
+ pcmk__xe_add_node(request, node, 0);
+ crm_xml_add(request, PCMK__XA_ATTR_RESOURCE, resource);
+ crm_xml_add(request, PCMK__XA_ATTR_CLEAR_OPERATION, operation);
+ crm_xml_add(request, PCMK__XA_ATTR_CLEAR_INTERVAL, interval_spec);
+ crm_xml_add_int(request, PCMK__XA_ATTR_IS_REMOTE,
+ pcmk_is_set(options, pcmk__node_attr_remote));
+
+ rc = connect_and_send_attrd_request(api, request);
+ free_xml(request);
return rc;
}
@@ -277,43 +239,30 @@ pcmk__attrd_api_delete(pcmk_ipc_api_t *api, const char *node, const char *name,
}
int
-pcmk__attrd_api_purge(pcmk_ipc_api_t *api, const char *node)
+pcmk__attrd_api_purge(pcmk_ipc_api_t *api, const char *node, bool reap)
{
int rc = pcmk_rc_ok;
xmlNode *request = NULL;
- const char *display_host = (node ? node : "localhost");
const char *target = pcmk__node_attr_target(node);
if (target != NULL) {
node = target;
}
+ crm_debug("Asking %s to purge transient attributes%s for %s",
+ pcmk_ipc_name(api, true),
+ (reap? " and node cache entries" : ""),
+ pcmk__s(node, "local node"));
+
request = create_attrd_op(NULL);
- crm_xml_add(request, PCMK__XA_TASK, PCMK__ATTRD_CMD_PEER_REMOVE);
+ crm_xml_add(request, PCMK_XA_TASK, PCMK__ATTRD_CMD_PEER_REMOVE);
+ pcmk__xe_set_bool_attr(request, PCMK__XA_REAP, reap);
pcmk__xe_add_node(request, node, 0);
- if (api == NULL) {
- rc = create_api(&api);
- if (rc != pcmk_rc_ok) {
- return rc;
- }
-
- rc = connect_and_send_attrd_request(api, request);
- destroy_api(api);
-
- } else if (!pcmk_ipc_is_connected(api)) {
- rc = connect_and_send_attrd_request(api, request);
-
- } else {
- rc = send_attrd_request(api, request);
- }
+ rc = connect_and_send_attrd_request(api, request);
free_xml(request);
-
- crm_debug("Asked pacemaker-attrd to purge %s: %s (%d)",
- display_host, pcmk_rc_str(rc), rc);
-
return rc;
}
@@ -339,23 +288,18 @@ pcmk__attrd_api_query(pcmk_ipc_api_t *api, const char *node, const char *name,
}
}
+ crm_debug("Querying %s for value of '%s'%s%s",
+ pcmk_ipc_name(api, true), name,
+ ((node == NULL)? "" : " on "), pcmk__s(node, ""));
+
request = create_attrd_op(NULL);
crm_xml_add(request, PCMK__XA_ATTR_NAME, name);
- crm_xml_add(request, PCMK__XA_TASK, PCMK__ATTRD_CMD_QUERY);
+ crm_xml_add(request, PCMK_XA_TASK, PCMK__ATTRD_CMD_QUERY);
pcmk__xe_add_node(request, node, 0);
- rc = send_attrd_request(api, request);
+ rc = connect_and_send_attrd_request(api, request);
free_xml(request);
-
- if (node) {
- crm_debug("Queried pacemaker-attrd for %s on %s: %s (%d)",
- name, node, pcmk_rc_str(rc), rc);
- } else {
- crm_debug("Queried pacemaker-attrd for %s: %s (%d)",
- name, pcmk_rc_str(rc), rc);
- }
-
return rc;
}
@@ -364,39 +308,23 @@ pcmk__attrd_api_refresh(pcmk_ipc_api_t *api, const char *node)
{
int rc = pcmk_rc_ok;
xmlNode *request = NULL;
- const char *display_host = (node ? node : "localhost");
const char *target = pcmk__node_attr_target(node);
if (target != NULL) {
node = target;
}
+ crm_debug("Asking %s to write all transient attributes for %s to CIB",
+ pcmk_ipc_name(api, true), pcmk__s(node, "local node"));
+
request = create_attrd_op(NULL);
- crm_xml_add(request, PCMK__XA_TASK, PCMK__ATTRD_CMD_REFRESH);
+ crm_xml_add(request, PCMK_XA_TASK, PCMK__ATTRD_CMD_REFRESH);
pcmk__xe_add_node(request, node, 0);
- if (api == NULL) {
- rc = create_api(&api);
- if (rc != pcmk_rc_ok) {
- return rc;
- }
-
- rc = connect_and_send_attrd_request(api, request);
- destroy_api(api);
-
- } else if (!pcmk_ipc_is_connected(api)) {
- rc = connect_and_send_attrd_request(api, request);
-
- } else {
- rc = send_attrd_request(api, request);
- }
+ rc = connect_and_send_attrd_request(api, request);
free_xml(request);
-
- crm_debug("Asked pacemaker-attrd to refresh %s: %s (%d)",
- display_host, pcmk_rc_str(rc), rc);
-
return rc;
}
@@ -404,11 +332,11 @@ static void
add_op_attr(xmlNode *op, uint32_t options)
{
if (pcmk_all_flags_set(options, pcmk__node_attr_value | pcmk__node_attr_delay)) {
- crm_xml_add(op, PCMK__XA_TASK, PCMK__ATTRD_CMD_UPDATE_BOTH);
+ crm_xml_add(op, PCMK_XA_TASK, PCMK__ATTRD_CMD_UPDATE_BOTH);
} else if (pcmk_is_set(options, pcmk__node_attr_value)) {
- crm_xml_add(op, PCMK__XA_TASK, PCMK__ATTRD_CMD_UPDATE);
+ crm_xml_add(op, PCMK_XA_TASK, PCMK__ATTRD_CMD_UPDATE);
} else if (pcmk_is_set(options, pcmk__node_attr_delay)) {
- crm_xml_add(op, PCMK__XA_TASK, PCMK__ATTRD_CMD_UPDATE_DELAY);
+ crm_xml_add(op, PCMK_XA_TASK, PCMK__ATTRD_CMD_UPDATE_DELAY);
}
}
@@ -417,15 +345,15 @@ populate_update_op(xmlNode *op, const char *node, const char *name, const char *
const char *dampen, const char *set, uint32_t options)
{
if (pcmk_is_set(options, pcmk__node_attr_pattern)) {
- crm_xml_add(op, PCMK__XA_ATTR_PATTERN, name);
+ crm_xml_add(op, PCMK__XA_ATTR_REGEX, name);
} else {
crm_xml_add(op, PCMK__XA_ATTR_NAME, name);
}
if (pcmk_is_set(options, pcmk__node_attr_utilization)) {
- crm_xml_add(op, PCMK__XA_ATTR_SET_TYPE, XML_TAG_UTILIZATION);
+ crm_xml_add(op, PCMK__XA_ATTR_SET_TYPE, PCMK_XE_UTILIZATION);
} else {
- crm_xml_add(op, PCMK__XA_ATTR_SET_TYPE, XML_TAG_ATTR_SETS);
+ crm_xml_add(op, PCMK__XA_ATTR_SET_TYPE, PCMK_XE_INSTANCE_ATTRIBUTES);
}
add_op_attr(op, options);
@@ -453,7 +381,6 @@ pcmk__attrd_api_update(pcmk_ipc_api_t *api, const char *node, const char *name,
{
int rc = pcmk_rc_ok;
xmlNode *request = NULL;
- const char *display_host = (node ? node : "localhost");
const char *target = NULL;
if (name == NULL) {
@@ -466,30 +393,16 @@ pcmk__attrd_api_update(pcmk_ipc_api_t *api, const char *node, const char *name,
node = target;
}
+ crm_debug("Asking %s to update '%s' to '%s' for %s",
+ pcmk_ipc_name(api, true), name, pcmk__s(value, "(null)"),
+ pcmk__s(node, "local node"));
+
request = create_attrd_op(user_name);
populate_update_op(request, node, name, value, dampen, set, options);
- if (api == NULL) {
- rc = create_api(&api);
- if (rc != pcmk_rc_ok) {
- return rc;
- }
-
- rc = connect_and_send_attrd_request(api, request);
- destroy_api(api);
-
- } else if (!pcmk_ipc_is_connected(api)) {
- rc = connect_and_send_attrd_request(api, request);
-
- } else {
- rc = send_attrd_request(api, request);
- }
+ rc = connect_and_send_attrd_request(api, request);
free_xml(request);
-
- crm_debug("Asked pacemaker-attrd to update %s on %s: %s (%d)",
- name, display_host, pcmk_rc_str(rc), rc);
-
return rc;
}
@@ -545,7 +458,7 @@ pcmk__attrd_api_update_list(pcmk_ipc_api_t *api, GList *attrs, const char *dampe
* then we also add the task to each child node in populate_update_op
* so attrd_client_update knows what form of update is taking place.
*/
- child = create_xml_node(request, XML_ATTR_OP);
+ child = pcmk__xe_create(request, PCMK_XE_OP);
target = pcmk__node_attr_target(pair->node);
if (target != NULL) {
@@ -564,23 +477,8 @@ pcmk__attrd_api_update_list(pcmk_ipc_api_t *api, GList *attrs, const char *dampe
* request. Do that now, creating and destroying the API object if needed.
*/
if (pcmk__is_daemon) {
- bool created_api = false;
-
- if (api == NULL) {
- rc = create_api(&api);
- if (rc != pcmk_rc_ok) {
- return rc;
- }
-
- created_api = true;
- }
-
rc = connect_and_send_attrd_request(api, request);
free_xml(request);
-
- if (created_api) {
- destroy_api(api);
- }
}
return rc;
diff --git a/lib/common/ipc_client.c b/lib/common/ipc_client.c
index 0d38650..37a0982 100644
--- a/lib/common/ipc_client.c
+++ b/lib/common/ipc_client.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2004-2023 the Pacemaker project contributors
+ * Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -26,7 +26,7 @@
#include <bzlib.h>
#include <crm/crm.h> /* indirectly: pcmk_err_generic */
-#include <crm/msg_xml.h>
+#include <crm/common/xml.h>
#include <crm/common/ipc.h>
#include <crm/common/ipc_internal.h>
#include "crmcommon_private.h"
@@ -251,7 +251,7 @@ pcmk_ipc_name(const pcmk_ipc_api_t *api, bool for_log)
}
switch (api->server) {
case pcmk_ipc_attrd:
- return for_log? "attribute manager" : T_ATTRD;
+ return for_log? "attribute manager" : PCMK__VALUE_ATTRD;
case pcmk_ipc_based:
return for_log? "CIB manager" : NULL /* PCMK__SERVER_BASED_RW */;
@@ -338,7 +338,7 @@ dispatch_ipc_data(const char *buffer, pcmk_ipc_api_t *api)
return ENOMSG;
}
- msg = string2xml(buffer);
+ msg = pcmk__xml_parse(buffer);
if (msg == NULL) {
crm_warn("Malformed message received from %s IPC",
pcmk_ipc_name(api, true));
@@ -755,10 +755,11 @@ create_purge_node_request(const pcmk_ipc_api_t *api, const char *node_name,
switch (api->server) {
case pcmk_ipc_attrd:
- request = create_xml_node(NULL, __func__);
- crm_xml_add(request, F_TYPE, T_ATTRD);
- crm_xml_add(request, F_ORIG, crm_system_name);
- crm_xml_add(request, PCMK__XA_TASK, PCMK__ATTRD_CMD_PEER_REMOVE);
+ request = pcmk__xe_create(NULL, __func__);
+ crm_xml_add(request, PCMK__XA_T, PCMK__VALUE_ATTRD);
+ crm_xml_add(request, PCMK__XA_SRC, crm_system_name);
+ crm_xml_add(request, PCMK_XA_TASK, PCMK__ATTRD_CMD_PEER_REMOVE);
+ pcmk__xe_set_bool_attr(request, PCMK__XA_REAP, true);
pcmk__xe_add_node(request, node_name, nodeid);
break;
@@ -770,7 +771,7 @@ create_purge_node_request(const pcmk_ipc_api_t *api, const char *node_name,
if (nodeid > 0) {
crm_xml_set_id(request, "%lu", (unsigned long) nodeid);
}
- crm_xml_add(request, XML_ATTR_UNAME, node_name);
+ crm_xml_add(request, PCMK_XA_UNAME, node_name);
break;
case pcmk_ipc_based:
@@ -1122,7 +1123,7 @@ crm_ipc_decompress(crm_ipc_t * client)
unsigned int size_u = 1 + header->size_uncompressed;
/* never let buf size fall below our max size required for ipc reads. */
unsigned int new_buf_size = QB_MAX((sizeof(pcmk__ipc_header_t) + size_u), client->max_buf_size);
- char *uncompressed = calloc(1, new_buf_size);
+ char *uncompressed = pcmk__assert_alloc(1, new_buf_size);
crm_trace("Decompressing message data %u bytes into %u bytes",
header->size_compressed, size_u);
@@ -1264,13 +1265,13 @@ internal_ipc_get_reply(crm_ipc_t *client, int request_id, int ms_timeout,
/* Got it */
break;
} else if (hdr->qb.id < request_id) {
- xmlNode *bad = string2xml(crm_ipc_buffer(client));
+ xmlNode *bad = pcmk__xml_parse(crm_ipc_buffer(client));
crm_err("Discarding old reply %d (need %d)", hdr->qb.id, request_id);
crm_log_xml_notice(bad, "OldIpcReply");
} else {
- xmlNode *bad = string2xml(crm_ipc_buffer(client));
+ xmlNode *bad = pcmk__xml_parse(crm_ipc_buffer(client));
crm_err("Discarding newer reply %d (need %d)", hdr->qb.id, request_id);
crm_log_xml_notice(bad, "ImpossibleReply");
@@ -1429,7 +1430,7 @@ crm_ipc_send(crm_ipc_t *client, const xmlNode *message,
crm_ipc_buffer(client));
if (reply) {
- *reply = string2xml(crm_ipc_buffer(client));
+ *reply = pcmk__xml_parse(crm_ipc_buffer(client));
}
} else {
@@ -1622,13 +1623,17 @@ pcmk__ipc_is_authentic_process_active(const char *name, uid_t refuid,
do {
poll_rc = poll(&pollfd, 1, 2000);
} while ((poll_rc == -1) && (errno == EINTR));
- if ((poll_rc <= 0) || (qb_ipcc_connect_continue(c) != 0)) {
+
+ /* If poll() failed, given that disconnect function is not registered yet,
+ * qb_ipcc_disconnect() won't clean up the socket. In any case, call
+ * qb_ipcc_connect_continue() here so that it may fail and do the cleanup
+ * for us.
+ */
+ if (qb_ipcc_connect_continue(c) != 0) {
crm_info("Could not connect to %s IPC: %s", name,
(poll_rc == 0)?"timeout":strerror(errno));
rc = pcmk_rc_ipc_unresponsive;
- if (poll_rc > 0) {
- c = NULL; // qb_ipcc_connect_continue cleaned up for us
- }
+ c = NULL; // qb_ipcc_connect_continue cleaned up for us
goto bail;
}
#endif
diff --git a/lib/common/ipc_common.c b/lib/common/ipc_common.c
index a48b0e9..5bea139 100644
--- a/lib/common/ipc_common.c
+++ b/lib/common/ipc_common.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2004-2021 the Pacemaker project contributors
+ * Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -13,7 +13,7 @@
#include <stdint.h> // uint64_t
#include <sys/types.h>
-#include <crm/msg_xml.h>
+#include <crm/common/xml.h>
#include "crmcommon_private.h"
#define MIN_MSG_SIZE 12336 // sizeof(struct qb_ipc_connection_response)
diff --git a/lib/common/ipc_controld.c b/lib/common/ipc_controld.c
index 8e2016e..755c034 100644
--- a/lib/common/ipc_controld.c
+++ b/lib/common/ipc_controld.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2020-2023 the Pacemaker project contributors
+ * Copyright 2020-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -15,7 +15,6 @@
#include <libxml/tree.h>
#include <crm/crm.h>
-#include <crm/msg_xml.h>
#include <crm/common/xml.h>
#include <crm/common/ipc.h>
#include <crm/common/ipc_internal.h>
@@ -70,8 +69,8 @@ new_data(pcmk_ipc_api_t *api)
/* This is set to the PID because that's how it was always done, but PIDs
* are not unique because clients can be remote. The value appears to be
- * unused other than as part of F_CRM_SYS_FROM in IPC requests, which is
- * only compared against the internal system names (CRM_SYSTEM_TENGINE,
+ * unused other than as part of PCMK__XA_CRM_SYS_FROM in IPC requests, which
+ * is only compared against the internal system names (CRM_SYSTEM_TENGINE,
* etc.), so it shouldn't be a problem.
*/
private->client_uuid = pcmk__getpid_s();
@@ -123,19 +122,21 @@ set_node_info_data(pcmk_controld_api_reply_t *data, xmlNode *msg_data)
if (msg_data == NULL) {
return;
}
- data->data.node_info.have_quorum = pcmk__xe_attr_is_true(msg_data, XML_ATTR_HAVE_QUORUM);
- data->data.node_info.is_remote = pcmk__xe_attr_is_true(msg_data, XML_NODE_IS_REMOTE);
+ data->data.node_info.have_quorum =
+ pcmk__xe_attr_is_true(msg_data, PCMK_XA_HAVE_QUORUM);
+ data->data.node_info.is_remote =
+ pcmk__xe_attr_is_true(msg_data, PCMK_XA_REMOTE_NODE);
/* Integer node_info.id is currently valid only for Corosync nodes.
*
* @TODO: Improve handling after crm_node_t is refactored to handle layer-
* specific data better.
*/
- crm_element_value_int(msg_data, XML_ATTR_ID, &(data->data.node_info.id));
+ crm_element_value_int(msg_data, PCMK_XA_ID, &(data->data.node_info.id));
- data->data.node_info.uuid = crm_element_value(msg_data, XML_ATTR_ID);
- data->data.node_info.uname = crm_element_value(msg_data, XML_ATTR_UNAME);
- data->data.node_info.state = crm_element_value(msg_data, PCMK__XA_CRMD);
+ data->data.node_info.uuid = crm_element_value(msg_data, PCMK_XA_ID);
+ data->data.node_info.uname = crm_element_value(msg_data, PCMK_XA_UNAME);
+ data->data.node_info.state = crm_element_value(msg_data, PCMK_XA_CRMD);
}
static void
@@ -146,10 +147,10 @@ set_ping_data(pcmk_controld_api_reply_t *data, xmlNode *msg_data)
return;
}
data->data.ping.sys_from = crm_element_value(msg_data,
- XML_PING_ATTR_SYSFROM);
+ PCMK__XA_CRM_SUBSYSTEM);
data->data.ping.fsa_state = crm_element_value(msg_data,
- XML_PING_ATTR_CRMDSTATE);
- data->data.ping.result = crm_element_value(msg_data, XML_PING_ATTR_STATUS);
+ PCMK__XA_CRMD_STATE);
+ data->data.ping.result = crm_element_value(msg_data, PCMK_XA_RESULT);
}
static void
@@ -158,17 +159,18 @@ set_nodes_data(pcmk_controld_api_reply_t *data, xmlNode *msg_data)
pcmk_controld_api_node_t *node_info;
data->reply_type = pcmk_controld_reply_nodes;
- for (xmlNode *node = first_named_child(msg_data, XML_CIB_TAG_NODE);
- node != NULL; node = crm_next_same_xml(node)) {
+ for (xmlNode *node = pcmk__xe_first_child(msg_data, PCMK_XE_NODE, NULL,
+ NULL);
+ node != NULL; node = pcmk__xe_next_same(node)) {
long long id_ll = 0;
- node_info = calloc(1, sizeof(pcmk_controld_api_node_t));
- crm_element_value_ll(node, XML_ATTR_ID, &id_ll);
+ node_info = pcmk__assert_alloc(1, sizeof(pcmk_controld_api_node_t));
+ crm_element_value_ll(node, PCMK_XA_ID, &id_ll);
if (id_ll > 0) {
node_info->id = id_ll;
}
- node_info->uname = crm_element_value(node, XML_ATTR_UNAME);
+ node_info->uname = crm_element_value(node, PCMK_XA_UNAME);
node_info->state = crm_element_value(node, PCMK__XA_IN_CCM);
data->data.nodes = g_list_prepend(data->data.nodes, node_info);
}
@@ -178,7 +180,7 @@ static bool
reply_expected(pcmk_ipc_api_t *api, const xmlNode *request)
{
// We only need to handle commands that API functions can send
- return pcmk__str_any_of(crm_element_value(request, F_CRM_TASK),
+ return pcmk__str_any_of(crm_element_value(request, PCMK__XA_CRM_TASK),
PCMK__CONTROLD_CMD_NODES,
CRM_OP_LRM_DELETE,
CRM_OP_LRM_FAIL,
@@ -194,13 +196,14 @@ dispatch(pcmk_ipc_api_t *api, xmlNode *reply)
{
struct controld_api_private_s *private = api->api_data;
crm_exit_t status = CRM_EX_OK;
+ xmlNode *wrapper = NULL;
xmlNode *msg_data = NULL;
const char *value = NULL;
pcmk_controld_api_reply_t reply_data = {
pcmk_controld_reply_unknown, NULL, NULL,
};
- if (pcmk__xe_is(reply, "ack")) {
+ if (pcmk__xe_is(reply, PCMK__XE_ACK)) {
/* ACKs are trivial responses that do not count toward expected replies,
* and do not have all the fields that validation requires, so skip that
* processing.
@@ -219,22 +222,22 @@ dispatch(pcmk_ipc_api_t *api, xmlNode *reply)
* if we fix the controller, we'll still need to handle replies from
* old versions (feature set could be used to differentiate).
*/
- value = crm_element_value(reply, F_CRM_MSG_TYPE);
- if (pcmk__str_empty(value)
- || !pcmk__str_any_of(value, XML_ATTR_REQUEST, XML_ATTR_RESPONSE, NULL)) {
+ value = crm_element_value(reply, PCMK__XA_SUBT);
+ if (!pcmk__str_any_of(value, PCMK__VALUE_REQUEST, PCMK__VALUE_RESPONSE,
+ NULL)) {
crm_info("Unrecognizable message from controller: "
"invalid message type '%s'", pcmk__s(value, ""));
status = CRM_EX_PROTOCOL;
goto done;
}
- if (pcmk__str_empty(crm_element_value(reply, XML_ATTR_REFERENCE))) {
+ if (pcmk__str_empty(crm_element_value(reply, PCMK_XA_REFERENCE))) {
crm_info("Unrecognizable message from controller: no reference");
status = CRM_EX_PROTOCOL;
goto done;
}
- value = crm_element_value(reply, F_CRM_TASK);
+ value = crm_element_value(reply, PCMK__XA_CRM_TASK);
if (pcmk__str_empty(value)) {
crm_info("Unrecognizable message from controller: no command name");
status = CRM_EX_PROTOCOL;
@@ -243,9 +246,11 @@ dispatch(pcmk_ipc_api_t *api, xmlNode *reply)
// Parse useful info from reply
- reply_data.feature_set = crm_element_value(reply, XML_ATTR_VERSION);
- reply_data.host_from = crm_element_value(reply, F_CRM_HOST_FROM);
- msg_data = get_message_xml(reply, F_CRM_DATA);
+ reply_data.feature_set = crm_element_value(reply, PCMK_XA_VERSION);
+ reply_data.host_from = crm_element_value(reply, PCMK__XA_SRC);
+
+ wrapper = pcmk__xe_first_child(reply, PCMK__XE_CRM_XML, NULL, NULL);
+ msg_data = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
if (!strcmp(value, CRM_OP_REPROBE)) {
reply_data.reply_type = pcmk_controld_reply_reprobe;
@@ -332,7 +337,7 @@ static int
send_controller_request(pcmk_ipc_api_t *api, const xmlNode *request,
bool reply_is_expected)
{
- if (crm_element_value(request, XML_ATTR_REFERENCE) == NULL) {
+ if (crm_element_value(request, PCMK_XA_REFERENCE) == NULL) {
return EINVAL;
}
if (reply_is_expected) {
@@ -348,10 +353,10 @@ create_reprobe_message_data(const char *target_node, const char *router_node)
{
xmlNode *msg_data;
- msg_data = create_xml_node(NULL, "data_for_" CRM_OP_REPROBE);
- crm_xml_add(msg_data, XML_LRM_ATTR_TARGET, target_node);
+ msg_data = pcmk__xe_create(NULL, "data_for_" CRM_OP_REPROBE);
+ crm_xml_add(msg_data, PCMK__META_ON_NODE, target_node);
if ((router_node != NULL) && !pcmk__str_eq(router_node, target_node, pcmk__str_casei)) {
- crm_xml_add(msg_data, XML_LRM_ATTR_ROUTER_NODE, router_node);
+ crm_xml_add(msg_data, PCMK__XA_ROUTER_NODE, router_node);
}
return msg_data;
}
@@ -486,7 +491,7 @@ controller_resource_op(pcmk_ipc_api_t *api, const char *op,
router_node = target_node;
}
- msg_data = create_xml_node(NULL, XML_GRAPH_TAG_RSC_OP);
+ msg_data = pcmk__xe_create(NULL, PCMK__XE_RSC_OP);
/* The controller logs the transition key from resource op requests, so we
* need to have *something* for it.
@@ -494,31 +499,31 @@ controller_resource_op(pcmk_ipc_api_t *api, const char *op,
*/
key = pcmk__transition_key(0, getpid(), 0,
"xxxxxxxx-xrsc-opxx-xcrm-resourcexxxx");
- crm_xml_add(msg_data, XML_ATTR_TRANSITION_KEY, key);
+ crm_xml_add(msg_data, PCMK__XA_TRANSITION_KEY, key);
free(key);
- crm_xml_add(msg_data, XML_LRM_ATTR_TARGET, target_node);
+ crm_xml_add(msg_data, PCMK__META_ON_NODE, target_node);
if (!pcmk__str_eq(router_node, target_node, pcmk__str_casei)) {
- crm_xml_add(msg_data, XML_LRM_ATTR_ROUTER_NODE, router_node);
+ crm_xml_add(msg_data, PCMK__XA_ROUTER_NODE, router_node);
}
if (cib_only) {
// Indicate that only the CIB needs to be cleaned
- crm_xml_add(msg_data, PCMK__XA_MODE, XML_TAG_CIB);
+ crm_xml_add(msg_data, PCMK__XA_MODE, PCMK__VALUE_CIB);
}
- xml_rsc = create_xml_node(msg_data, XML_CIB_TAG_RESOURCE);
- crm_xml_add(xml_rsc, XML_ATTR_ID, rsc_id);
- crm_xml_add(xml_rsc, XML_ATTR_ID_LONG, rsc_long_id);
- crm_xml_add(xml_rsc, XML_AGENT_ATTR_CLASS, standard);
- crm_xml_add(xml_rsc, XML_AGENT_ATTR_PROVIDER, provider);
- crm_xml_add(xml_rsc, XML_ATTR_TYPE, type);
+ xml_rsc = pcmk__xe_create(msg_data, PCMK_XE_PRIMITIVE);
+ crm_xml_add(xml_rsc, PCMK_XA_ID, rsc_id);
+ crm_xml_add(xml_rsc, PCMK__XA_LONG_ID, rsc_long_id);
+ crm_xml_add(xml_rsc, PCMK_XA_CLASS, standard);
+ crm_xml_add(xml_rsc, PCMK_XA_PROVIDER, provider);
+ crm_xml_add(xml_rsc, PCMK_XA_TYPE, type);
- params = create_xml_node(msg_data, XML_TAG_ATTRS);
- crm_xml_add(params, XML_ATTR_CRM_VERSION, CRM_FEATURE_SET);
+ params = pcmk__xe_create(msg_data, PCMK__XE_ATTRIBUTES);
+ crm_xml_add(params, PCMK_XA_CRM_FEATURE_SET, CRM_FEATURE_SET);
// The controller parses the timeout from the request
- key = crm_meta_name(XML_ATTR_TIMEOUT);
+ key = crm_meta_name(PCMK_META_TIMEOUT);
crm_xml_add(params, key, "60000"); /* 1 minute */ //@TODO pass as arg
free(key);
@@ -631,17 +636,13 @@ create_hello_message(const char *uuid, const char *client_name,
return NULL;
}
- hello_node = create_xml_node(NULL, XML_TAG_OPTIONS);
- if (hello_node == NULL) {
- crm_err("Could not create IPC hello message from %s (UUID %s): "
- "Message data creation failed", client_name, uuid);
- return NULL;
- }
+ hello_node = pcmk__xe_create(NULL, PCMK__XE_OPTIONS);
+ crm_xml_add(hello_node, PCMK__XA_MAJOR_VERSION, major_version);
+ crm_xml_add(hello_node, PCMK__XA_MINOR_VERSION, minor_version);
+ crm_xml_add(hello_node, PCMK__XA_CLIENT_NAME, client_name);
- crm_xml_add(hello_node, "major_version", major_version);
- crm_xml_add(hello_node, "minor_version", minor_version);
- crm_xml_add(hello_node, "client_name", client_name);
- crm_xml_add(hello_node, "client_uuid", uuid);
+ // @TODO Nothing uses this. Drop, or keep for debugging?
+ crm_xml_add(hello_node, PCMK__XA_CLIENT_UUID, uuid);
hello = create_request(CRM_OP_HELLO, hello_node, NULL, NULL, client_name, uuid);
if (hello == NULL) {
diff --git a/lib/common/ipc_pacemakerd.c b/lib/common/ipc_pacemakerd.c
index 2f03709..7557491 100644
--- a/lib/common/ipc_pacemakerd.c
+++ b/lib/common/ipc_pacemakerd.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2020-2023 the Pacemaker project contributors
+ * Copyright 2020-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -13,7 +13,6 @@
#include <time.h>
#include <crm/crm.h>
-#include <crm/msg_xml.h>
#include <crm/common/xml.h>
#include <crm/common/ipc.h>
#include <crm/common/ipc_internal.h>
@@ -26,13 +25,13 @@ typedef struct pacemakerd_api_private_s {
} pacemakerd_api_private_t;
static const char *pacemakerd_state_str[] = {
- XML_PING_ATTR_PACEMAKERDSTATE_INIT,
- XML_PING_ATTR_PACEMAKERDSTATE_STARTINGDAEMONS,
- XML_PING_ATTR_PACEMAKERDSTATE_WAITPING,
- XML_PING_ATTR_PACEMAKERDSTATE_RUNNING,
- XML_PING_ATTR_PACEMAKERDSTATE_SHUTTINGDOWN,
- XML_PING_ATTR_PACEMAKERDSTATE_SHUTDOWNCOMPLETE,
- XML_PING_ATTR_PACEMAKERDSTATE_REMOTE,
+ PCMK__VALUE_INIT,
+ PCMK__VALUE_STARTING_DAEMONS,
+ PCMK__VALUE_WAIT_FOR_PING,
+ PCMK__VALUE_RUNNING,
+ PCMK__VALUE_SHUTTING_DOWN,
+ PCMK__VALUE_SHUTDOWN_COMPLETE,
+ PCMK_VALUE_REMOTE,
};
enum pcmk_pacemakerd_state
@@ -180,7 +179,7 @@ post_disconnect(pcmk_ipc_api_t *api)
static bool
reply_expected(pcmk_ipc_api_t *api, const xmlNode *request)
{
- const char *command = crm_element_value(request, F_CRM_TASK);
+ const char *command = crm_element_value(request, PCMK__XA_CRM_TASK);
if (command == NULL) {
return false;
@@ -194,6 +193,7 @@ static bool
dispatch(pcmk_ipc_api_t *api, xmlNode *reply)
{
crm_exit_t status = CRM_EX_OK;
+ xmlNode *wrapper = NULL;
xmlNode *msg_data = NULL;
pcmk_pacemakerd_api_reply_t reply_data = {
pcmk_pacemakerd_reply_unknown
@@ -201,51 +201,56 @@ dispatch(pcmk_ipc_api_t *api, xmlNode *reply)
const char *value = NULL;
long long value_ll = 0;
- if (pcmk__str_eq((const char *) reply->name, "ack", pcmk__str_none)) {
+ if (pcmk__xe_is(reply, PCMK__XE_ACK)) {
long long int ack_status = 0;
- pcmk__scan_ll(crm_element_value(reply, "status"), &ack_status, CRM_EX_OK);
+ pcmk__scan_ll(crm_element_value(reply, PCMK_XA_STATUS), &ack_status,
+ CRM_EX_OK);
return ack_status == CRM_EX_INDETERMINATE;
}
- value = crm_element_value(reply, F_CRM_MSG_TYPE);
- if (pcmk__str_empty(value)
- || !pcmk__str_eq(value, XML_ATTR_RESPONSE, pcmk__str_none)) {
- crm_info("Unrecognizable message from pacemakerd: "
- "message type '%s' not '" XML_ATTR_RESPONSE "'",
- pcmk__s(value, ""));
+ value = crm_element_value(reply, PCMK__XA_SUBT);
+ if (!pcmk__str_eq(value, PCMK__VALUE_RESPONSE, pcmk__str_none)) {
+ crm_info("Unrecognizable message from %s: "
+ "message type '%s' not '" PCMK__VALUE_RESPONSE "'",
+ pcmk_ipc_name(api, true), pcmk__s(value, ""));
status = CRM_EX_PROTOCOL;
goto done;
}
- if (pcmk__str_empty(crm_element_value(reply, XML_ATTR_REFERENCE))) {
- crm_info("Unrecognizable message from pacemakerd: no reference");
+ if (pcmk__str_empty(crm_element_value(reply, PCMK_XA_REFERENCE))) {
+ crm_info("Unrecognizable message from %s: no reference",
+ pcmk_ipc_name(api, true));
status = CRM_EX_PROTOCOL;
goto done;
}
- value = crm_element_value(reply, F_CRM_TASK);
+ value = crm_element_value(reply, PCMK__XA_CRM_TASK);
// Parse useful info from reply
- msg_data = get_message_xml(reply, F_CRM_DATA);
- crm_element_value_ll(msg_data, XML_ATTR_TSTAMP, &value_ll);
+ wrapper = pcmk__xe_first_child(reply, PCMK__XE_CRM_XML, NULL, NULL);
+ msg_data = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
+
+ crm_element_value_ll(msg_data, PCMK_XA_CRM_TIMESTAMP, &value_ll);
if (pcmk__str_eq(value, CRM_OP_PING, pcmk__str_none)) {
reply_data.reply_type = pcmk_pacemakerd_reply_ping;
reply_data.data.ping.state =
pcmk_pacemakerd_api_daemon_state_text2enum(
- crm_element_value(msg_data, XML_PING_ATTR_PACEMAKERDSTATE));
+ crm_element_value(msg_data, PCMK__XA_PACEMAKERD_STATE));
reply_data.data.ping.status =
- pcmk__str_eq(crm_element_value(msg_data, XML_PING_ATTR_STATUS), "ok",
+ pcmk__str_eq(crm_element_value(msg_data, PCMK_XA_RESULT), "ok",
pcmk__str_casei)?pcmk_rc_ok:pcmk_rc_error;
reply_data.data.ping.last_good = (value_ll < 0)? 0 : (time_t) value_ll;
- reply_data.data.ping.sys_from = crm_element_value(msg_data,
- XML_PING_ATTR_SYSFROM);
+ reply_data.data.ping.sys_from =
+ crm_element_value(msg_data, PCMK__XA_CRM_SUBSYSTEM);
} else if (pcmk__str_eq(value, CRM_OP_QUIT, pcmk__str_none)) {
+ const char *op_status = crm_element_value(msg_data, PCMK__XA_OP_STATUS);
+
reply_data.reply_type = pcmk_pacemakerd_reply_shutdown;
- reply_data.data.shutdown.status = atoi(crm_element_value(msg_data, XML_LRM_ATTR_OPSTATUS));
+ reply_data.data.shutdown.status = atoi(op_status);
} else {
- crm_info("Unrecognizable message from pacemakerd: "
- "unknown command '%s'", pcmk__s(value, ""));
+ crm_info("Unrecognizable message from %s: unknown command '%s'",
+ pcmk_ipc_name(api, true), pcmk__s(value, ""));
status = CRM_EX_PROTOCOL;
goto done;
}
@@ -292,8 +297,8 @@ do_pacemakerd_api_call(pcmk_ipc_api_t *api, const char *ipc_name, const char *ta
if (cmd) {
rc = pcmk__send_ipc_request(api, cmd);
if (rc != pcmk_rc_ok) {
- crm_debug("Couldn't send request to pacemakerd: %s rc=%d",
- pcmk_rc_str(rc), rc);
+ crm_debug("Couldn't send request to %s: %s rc=%d",
+ pcmk_ipc_name(api, true), pcmk_rc_str(rc), rc);
}
free_xml(cmd);
} else {
diff --git a/lib/common/ipc_schedulerd.c b/lib/common/ipc_schedulerd.c
index cf788e5..45c5803 100644
--- a/lib/common/ipc_schedulerd.c
+++ b/lib/common/ipc_schedulerd.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2021-2023 the Pacemaker project contributors
+ * Copyright 2021-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -13,7 +13,6 @@
#include <time.h>
#include <crm/crm.h>
-#include <crm/msg_xml.h>
#include <crm/common/xml.h>
#include <crm/common/ipc.h>
#include <crm/common/ipc_internal.h>
@@ -64,7 +63,7 @@ post_connect(pcmk_ipc_api_t *api)
static bool
reply_expected(pcmk_ipc_api_t *api, const xmlNode *request)
{
- const char *command = crm_element_value(request, F_CRM_TASK);
+ const char *command = crm_element_value(request, PCMK__XA_CRM_TASK);
if (command == NULL) {
return false;
@@ -78,39 +77,44 @@ static bool
dispatch(pcmk_ipc_api_t *api, xmlNode *reply)
{
crm_exit_t status = CRM_EX_OK;
+ xmlNode *wrapper = NULL;
xmlNode *msg_data = NULL;
pcmk_schedulerd_api_reply_t reply_data = {
pcmk_schedulerd_reply_unknown
};
const char *value = NULL;
- if (pcmk__str_eq((const char *) reply->name, "ack", pcmk__str_casei)) {
+ if (pcmk__xe_is(reply, PCMK__XE_ACK)) {
return false;
}
- value = crm_element_value(reply, F_CRM_MSG_TYPE);
- if (!pcmk__str_eq(value, XML_ATTR_RESPONSE, pcmk__str_none)) {
+ value = crm_element_value(reply, PCMK__XA_SUBT);
+ if (!pcmk__str_eq(value, PCMK__VALUE_RESPONSE, pcmk__str_none)) {
crm_info("Unrecognizable message from schedulerd: "
- "message type '%s' not '" XML_ATTR_RESPONSE "'",
+ "message type '%s' not '" PCMK__VALUE_RESPONSE "'",
pcmk__s(value, ""));
status = CRM_EX_PROTOCOL;
goto done;
}
- if (pcmk__str_empty(crm_element_value(reply, XML_ATTR_REFERENCE))) {
+ if (pcmk__str_empty(crm_element_value(reply, PCMK_XA_REFERENCE))) {
crm_info("Unrecognizable message from schedulerd: no reference");
status = CRM_EX_PROTOCOL;
goto done;
}
// Parse useful info from reply
- msg_data = get_message_xml(reply, F_CRM_DATA);
- value = crm_element_value(reply, F_CRM_TASK);
+ wrapper = pcmk__xe_first_child(reply, PCMK__XE_CRM_XML, NULL, NULL);
+ msg_data = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
+
+ value = crm_element_value(reply, PCMK__XA_CRM_TASK);
if (pcmk__str_eq(value, CRM_OP_PECALC, pcmk__str_none)) {
reply_data.reply_type = pcmk_schedulerd_reply_graph;
- reply_data.data.graph.reference = crm_element_value(reply, XML_ATTR_REFERENCE);
- reply_data.data.graph.input = crm_element_value(reply, F_CRM_TGRAPH_INPUT);
+ reply_data.data.graph.reference = crm_element_value(reply,
+ PCMK_XA_REFERENCE);
+ reply_data.data.graph.input = crm_element_value(reply,
+ PCMK__XA_CRM_TGRAPH_IN);
reply_data.data.graph.tgraph = msg_data;
} else {
crm_info("Unrecognizable message from schedulerd: "
@@ -164,7 +168,7 @@ do_schedulerd_api_call(pcmk_ipc_api_t *api, const char *task, xmlNode *cib, char
pcmk_rc_str(rc), rc);
}
- *ref = strdup(crm_element_value(cmd, F_CRM_REFERENCE));
+ *ref = strdup(crm_element_value(cmd, PCMK_XA_REFERENCE));
free_xml(cmd);
} else {
rc = ENOMSG;
diff --git a/lib/common/ipc_server.c b/lib/common/ipc_server.c
index 5cd7e70..a9201b9 100644
--- a/lib/common/ipc_server.c
+++ b/lib/common/ipc_server.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2004-2023 the Pacemaker project contributors
+ * Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -16,7 +16,7 @@
#include <sys/types.h>
#include <crm/crm.h>
-#include <crm/msg_xml.h>
+#include <crm/common/xml.h>
#include <crm/common/ipc.h>
#include <crm/common/ipc_internal.h>
#include "crmcommon_private.h"
@@ -158,23 +158,17 @@ pcmk__drop_all_clients(qb_ipcs_service_t *service)
* \param[in] key Connection table key (NULL to use sane default)
* \param[in] uid_client UID corresponding to c (ignored if c is NULL)
*
- * \return Pointer to new pcmk__client_t (or NULL on error)
+ * \return Pointer to new pcmk__client_t (guaranteed not to be \c NULL)
*/
static pcmk__client_t *
client_from_connection(qb_ipcs_connection_t *c, void *key, uid_t uid_client)
{
- pcmk__client_t *client = calloc(1, sizeof(pcmk__client_t));
-
- if (client == NULL) {
- crm_perror(LOG_ERR, "Allocating client");
- return NULL;
- }
+ pcmk__client_t *client = pcmk__assert_alloc(1, sizeof(pcmk__client_t));
if (c) {
client->user = pcmk__uid2username(uid_client);
if (client->user == NULL) {
- client->user = strdup("#unprivileged");
- CRM_CHECK(client->user != NULL, free(client); return NULL);
+ client->user = pcmk__str_copy("#unprivileged");
crm_err("Unable to enforce ACLs for user ID %d, assuming unprivileged",
uid_client);
}
@@ -208,10 +202,7 @@ client_from_connection(qb_ipcs_connection_t *c, void *key, uid_t uid_client)
pcmk__client_t *
pcmk__new_unauth_client(void *key)
{
- pcmk__client_t *client = client_from_connection(NULL, key, 0);
-
- CRM_ASSERT(client != NULL);
- return client;
+ return client_from_connection(NULL, key, 0);
}
pcmk__client_t *
@@ -242,9 +233,6 @@ pcmk__new_client(qb_ipcs_connection_t *c, uid_t uid_client, gid_t gid_client)
/* TODO: Do our own auth checking, return NULL if unauthorized */
client = client_from_connection(c, NULL, uid_client);
- if (client == NULL) {
- return NULL;
- }
if ((uid_client == 0) || (uid_client == uid_cluster)) {
/* Remember when a connection came from root or hacluster */
@@ -259,10 +247,7 @@ pcmk__new_client(qb_ipcs_connection_t *c, uid_t uid_client, gid_t gid_client)
static struct iovec *
pcmk__new_ipc_event(void)
{
- struct iovec *iov = calloc(2, sizeof(struct iovec));
-
- CRM_ASSERT(iov != NULL);
- return iov;
+ return (struct iovec *) pcmk__assert_alloc(2, sizeof(struct iovec));
}
/*!
@@ -331,6 +316,14 @@ pcmk__free_client(pcmk__client_t *c)
if (c->remote->auth_timeout) {
g_source_remove(c->remote->auth_timeout);
}
+#ifdef HAVE_GNUTLS_GNUTLS_H
+ if (c->remote->tls_session != NULL) {
+ /* @TODO Reduce duplication at callers. Put here everything
+ * necessary to tear down and free tls_session.
+ */
+ gnutls_free(c->remote->tls_session);
+ }
+#endif // HAVE_GNUTLS_GNUTLS_H
free(c->remote->buffer);
free(c->remote);
}
@@ -413,7 +406,7 @@ pcmk__client_data2xml(pcmk__client_t *c, void *data, uint32_t *id,
if (header->size_compressed) {
int rc = 0;
unsigned int size_u = 1 + header->size_uncompressed;
- uncompressed = calloc(1, size_u);
+ uncompressed = pcmk__assert_alloc(1, size_u);
crm_trace("Decompressing message data %u bytes into %u bytes",
header->size_compressed, size_u);
@@ -433,7 +426,7 @@ pcmk__client_data2xml(pcmk__client_t *c, void *data, uint32_t *id,
CRM_ASSERT(text[header->size_uncompressed - 1] == 0);
- xml = string2xml(text);
+ xml = pcmk__xml_parse(text);
crm_log_xml_trace(xml, "[IPC received]");
free(uncompressed);
@@ -583,23 +576,25 @@ pcmk__ipc_prepare_iov(uint32_t request, const xmlNode *message,
uint32_t max_send_size, struct iovec **result,
ssize_t *bytes)
{
- static unsigned int biggest = 0;
struct iovec *iov;
unsigned int total = 0;
- char *compressed = NULL;
- char *buffer = NULL;
+ GString *buffer = NULL;
pcmk__ipc_header_t *header = NULL;
+ int rc = pcmk_rc_ok;
if ((message == NULL) || (result == NULL)) {
- return EINVAL;
+ rc = EINVAL;
+ goto done;
}
header = calloc(1, sizeof(pcmk__ipc_header_t));
if (header == NULL) {
- return ENOMEM; /* errno mightn't be set by allocator */
+ rc = ENOMEM;
+ goto done;
}
- buffer = dump_xml_unformatted(message);
+ buffer = g_string_sized_new(1024);
+ pcmk__xml_string(message, 0, buffer, 0);
if (max_send_size == 0) {
max_send_size = crm_ipc_default_buffer_size();
@@ -612,17 +607,21 @@ pcmk__ipc_prepare_iov(uint32_t request, const xmlNode *message,
iov[0].iov_base = header;
header->version = PCMK__IPC_VERSION;
- header->size_uncompressed = 1 + strlen(buffer);
+ header->size_uncompressed = 1 + buffer->len;
total = iov[0].iov_len + header->size_uncompressed;
if (total < max_send_size) {
- iov[1].iov_base = buffer;
+ iov[1].iov_base = pcmk__str_copy(buffer->str);
iov[1].iov_len = header->size_uncompressed;
} else {
+ static unsigned int biggest = 0;
+
+ char *compressed = NULL;
unsigned int new_size = 0;
- if (pcmk__compress(buffer, (unsigned int) header->size_uncompressed,
+ if (pcmk__compress(buffer->str,
+ (unsigned int) header->size_uncompressed,
(unsigned int) max_send_size, &compressed,
&new_size) == pcmk_rc_ok) {
@@ -632,8 +631,6 @@ pcmk__ipc_prepare_iov(uint32_t request, const xmlNode *message,
iov[1].iov_len = header->size_compressed;
iov[1].iov_base = compressed;
- free(buffer);
-
biggest = QB_MAX(header->size_compressed, biggest);
} else {
@@ -646,9 +643,9 @@ pcmk__ipc_prepare_iov(uint32_t request, const xmlNode *message,
header->size_uncompressed, max_send_size, 4 * biggest);
free(compressed);
- free(buffer);
pcmk_free_ipc_event(iov);
- return EMSGSIZE;
+ rc = EMSGSIZE;
+ goto done;
}
}
@@ -660,7 +657,12 @@ pcmk__ipc_prepare_iov(uint32_t request, const xmlNode *message,
if (bytes != NULL) {
*bytes = header->qb.size;
}
- return pcmk_rc_ok;
+
+done:
+ if (buffer != NULL) {
+ g_string_free(buffer, TRUE);
+ }
+ return rc;
}
int
@@ -786,10 +788,10 @@ pcmk__ipc_create_ack_as(const char *function, int line, uint32_t flags,
xmlNode *ack = NULL;
if (pcmk_is_set(flags, crm_ipc_client_response)) {
- ack = create_xml_node(NULL, tag);
- crm_xml_add(ack, "function", function);
- crm_xml_add_int(ack, "line", line);
- crm_xml_add_int(ack, "status", (int) status);
+ ack = pcmk__xe_create(NULL, tag);
+ crm_xml_add(ack, PCMK_XA_FUNCTION, function);
+ crm_xml_add_int(ack, PCMK__XA_LINE, line);
+ crm_xml_add_int(ack, PCMK_XA_STATUS, (int) status);
crm_xml_add(ack, PCMK__XA_IPC_PROTO_VERSION, ver);
}
return ack;
@@ -911,7 +913,7 @@ void
pcmk__serve_attrd_ipc(qb_ipcs_service_t **ipcs,
struct qb_ipcs_service_handlers *cb)
{
- *ipcs = mainloop_add_ipc_server(T_ATTRD, QB_IPC_NATIVE, cb);
+ *ipcs = mainloop_add_ipc_server(PCMK__VALUE_ATTRD, QB_IPC_NATIVE, cb);
if (*ipcs == NULL) {
crm_err("Failed to create pacemaker-attrd server: exiting and inhibiting respawn");
diff --git a/lib/common/iso8601.c b/lib/common/iso8601.c
index 9de018f..d24f268 100644
--- a/lib/common/iso8601.c
+++ b/lib/common/iso8601.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2005-2022 the Pacemaker project contributors
+ * Copyright 2005-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -18,9 +18,12 @@
#include <time.h>
#include <ctype.h>
#include <inttypes.h>
+#include <limits.h> // INT_MIN, INT_MAX
#include <string.h>
#include <stdbool.h>
#include <crm/common/iso8601.h>
+#include <crm/common/iso8601_internal.h>
+#include "crmcommon_private.h"
/*
* Andrew's code was originally written for OSes whose "struct tm" contains:
@@ -125,10 +128,7 @@ crm_time_new(const char *date_time)
crm_time_t *
crm_time_new_undefined(void)
{
- crm_time_t *result = calloc(1, sizeof(crm_time_t));
-
- CRM_ASSERT(result != NULL);
- return result;
+ return (crm_time_t *) pcmk__assert_alloc(1, sizeof(crm_time_t));
}
/*!
@@ -623,7 +623,9 @@ time_as_string_common(const crm_time_t *dt, int usec, uint32_t flags,
if (pcmk_is_set(flags, crm_time_log_date)) {
if (pcmk_is_set(flags, crm_time_weeks)) { // YYYY-WW-D
- uint32_t y, w, d;
+ uint32_t y = 0;
+ uint32_t w = 0;
+ uint32_t d = 0;
if (crm_time_get_isoweek(dt, &y, &w, &d)) {
offset += snprintf(result + offset, DATE_MAX - offset,
@@ -632,7 +634,8 @@ time_as_string_common(const crm_time_t *dt, int usec, uint32_t flags,
}
} else if (pcmk_is_set(flags, crm_time_ordinal)) { // YYYY-DDD
- uint32_t y, d;
+ uint32_t y = 0;
+ uint32_t d = 0;
if (crm_time_get_ordinal(dt, &y, &d)) {
offset += snprintf(result + offset, DATE_MAX - offset,
@@ -640,7 +643,9 @@ time_as_string_common(const crm_time_t *dt, int usec, uint32_t flags,
}
} else { // YYYY-MM-DD
- uint32_t y, m, d;
+ uint32_t y = 0;
+ uint32_t m = 0;
+ uint32_t d = 0;
if (crm_time_get_gregorian(dt, &y, &m, &d)) {
offset += snprintf(result + offset, DATE_MAX - offset,
@@ -694,12 +699,9 @@ char *
crm_time_as_string(const crm_time_t *dt, int flags)
{
char result[DATE_MAX] = { '\0', };
- char *result_copy = NULL;
time_as_string_common(dt, 0, flags, result);
-
- pcmk__str_update(&result_copy, result);
- return result_copy;
+ return pcmk__str_copy(result);
}
/*!
@@ -864,7 +866,8 @@ crm_time_parse(const char *time_str, crm_time_t *a_time)
* \internal
* \brief Parse a time object from an ISO 8601 date/time specification
*
- * \param[in] date_str ISO 8601 date/time specification (or "epoch")
+ * \param[in] date_str ISO 8601 date/time specification (or
+ * \c PCMK__VALUE_EPOCH)
*
* \return New time object on success, NULL (and set errno) otherwise
*/
@@ -898,8 +901,10 @@ parse_date(const char *date_str)
dt = crm_time_new_undefined();
- if (!strncasecmp("epoch", date_str, 5)
- && ((date_str[5] == '\0') || (date_str[5] == '/') || isspace(date_str[5]))) {
+ if ((strncasecmp(PCMK__VALUE_EPOCH, date_str, 5) == 0)
+ && ((date_str[5] == '\0')
+ || (date_str[5] == '/')
+ || isspace(date_str[5]))) {
dt->days = 1;
dt->years = 1970;
crm_time_log(LOG_TRACE, "Unpacked", dt, crm_time_log_date | crm_time_log_timeofday);
@@ -1211,8 +1216,7 @@ crm_time_parse_period(const char *period_str)
}
tzset();
- period = calloc(1, sizeof(crm_time_period_t));
- CRM_ASSERT(period != NULL);
+ period = pcmk__assert_alloc(1, sizeof(crm_time_period_t));
if (period_str[0] == 'P') {
period->diff = crm_time_parse_duration(period_str);
@@ -1370,6 +1374,23 @@ crm_time_set_timet(crm_time_t *target, const time_t *source)
ha_set_tm_time(target, localtime(source));
}
+/*!
+ * \internal
+ * \brief Set one time object to another if the other is earlier
+ *
+ * \param[in,out] target Time object to set
+ * \param[in] source Time object to use if earlier
+ */
+void
+pcmk__set_time_if_earlier(crm_time_t *target, const crm_time_t *source)
+{
+ if ((target != NULL) && (source != NULL)
+ && (!crm_time_is_defined(target)
+ || (crm_time_compare(source, target) < 0))) {
+ crm_time_set(target, source);
+ }
+}
+
crm_time_t *
pcmk_copy_time(const crm_time_t *source)
{
@@ -1424,6 +1445,127 @@ crm_time_add(const crm_time_t *dt, const crm_time_t *value)
return answer;
}
+/*!
+ * \internal
+ * \brief Return the XML attribute name corresponding to a time component
+ *
+ * \param[in] component Component to check
+ *
+ * \return XML attribute name corresponding to \p component, or NULL if
+ * \p component is invalid
+ */
+const char *
+pcmk__time_component_attr(enum pcmk__time_component component)
+{
+ switch (component) {
+ case pcmk__time_years:
+ return PCMK_XA_YEARS;
+
+ case pcmk__time_months:
+ return PCMK_XA_MONTHS;
+
+ case pcmk__time_weeks:
+ return PCMK_XA_WEEKS;
+
+ case pcmk__time_days:
+ return PCMK_XA_DAYS;
+
+ case pcmk__time_hours:
+ return PCMK_XA_HOURS;
+
+ case pcmk__time_minutes:
+ return PCMK_XA_MINUTES;
+
+ case pcmk__time_seconds:
+ return PCMK_XA_SECONDS;
+
+ default:
+ return NULL;
+ }
+}
+
+typedef void (*component_fn_t)(crm_time_t *, int);
+
+/*!
+ * \internal
+ * \brief Get the addition function corresponding to a time component
+ * \param[in] component Component to check
+ *
+ * \return Addition function corresponding to \p component, or NULL if
+ * \p component is invalid
+ */
+static component_fn_t
+component_fn(enum pcmk__time_component component)
+{
+ switch (component) {
+ case pcmk__time_years:
+ return crm_time_add_years;
+
+ case pcmk__time_months:
+ return crm_time_add_months;
+
+ case pcmk__time_weeks:
+ return crm_time_add_weeks;
+
+ case pcmk__time_days:
+ return crm_time_add_days;
+
+ case pcmk__time_hours:
+ return crm_time_add_hours;
+
+ case pcmk__time_minutes:
+ return crm_time_add_minutes;
+
+ case pcmk__time_seconds:
+ return crm_time_add_seconds;
+
+ default:
+ return NULL;
+ }
+
+}
+
+/*!
+ * \internal
+ * \brief Add the value of an XML attribute to a time object
+ *
+ * \param[in,out] t Time object to add to
+ * \param[in] component Component of \p t to add to
+ * \param[in] xml XML with value to add
+ *
+ * \return Standard Pacemaker return code
+ */
+int
+pcmk__add_time_from_xml(crm_time_t *t, enum pcmk__time_component component,
+ const xmlNode *xml)
+{
+ long long value;
+ const char *attr = pcmk__time_component_attr(component);
+ component_fn_t add = component_fn(component);
+
+ if ((t == NULL) || (attr == NULL) || (add == NULL)) {
+ return EINVAL;
+ }
+
+ if (xml == NULL) {
+ return pcmk_rc_ok;
+ }
+
+ if (pcmk__scan_ll(crm_element_value(xml, attr), &value,
+ 0LL) != pcmk_rc_ok) {
+ return pcmk_rc_unpack_error;
+ }
+
+ if ((value < INT_MIN) || (value > INT_MAX)) {
+ return ERANGE;
+ }
+
+ if (value != 0LL) {
+ add(t, (int) value);
+ }
+ return pcmk_rc_ok;
+}
+
crm_time_t *
crm_time_calculate_duration(const crm_time_t *dt, const crm_time_t *value)
{
@@ -1690,8 +1832,11 @@ pcmk__time_hr_convert(pcmk__time_hr_t *target, const crm_time_t *dt)
pcmk__time_hr_t *hr_dt = NULL;
if (dt) {
- hr_dt = target?target:calloc(1, sizeof(pcmk__time_hr_t));
- CRM_ASSERT(hr_dt != NULL);
+ hr_dt = target;
+ if (hr_dt == NULL) {
+ hr_dt = pcmk__assert_alloc(1, sizeof(pcmk__time_hr_t));
+ }
+
*hr_dt = (pcmk__time_hr_t) {
.years = dt->years,
.months = dt->months,
@@ -1772,12 +1917,21 @@ pcmk__time_hr_free(pcmk__time_hr_t * hr_dt)
char *
pcmk__time_format_hr(const char *format, const pcmk__time_hr_t *hr_dt)
{
- const char *mark_s;
- int max = 128, scanned_pos = 0, printed_pos = 0, fmt_pos = 0,
- date_len = 0, nano_digits = 0;
- char nano_s[10], date_s[max+1], nanofmt_s[5] = "%", *tmp_fmt_s;
- struct tm tm;
- crm_time_t dt;
+#define DATE_LEN_MAX 128
+ const char *mark_s = NULL;
+ int scanned_pos = 0;
+ int printed_pos = 0;
+ int fmt_pos = 0;
+ size_t date_len = 0;
+ int nano_digits = 0;
+
+ char nano_s[10] = { '\0', };
+ char date_s[DATE_LEN_MAX] = { '\0', };
+ char nanofmt_s[5] = "%";
+ char *tmp_fmt_s = NULL;
+
+ struct tm tm = { 0, };
+ crm_time_t dt = { 0, };
if (!format) {
return NULL;
@@ -1818,7 +1972,8 @@ pcmk__time_format_hr(const char *format, const pcmk__time_hr_t *hr_dt)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
#endif
- date_len += strftime(&date_s[date_len], max-date_len, tmp_fmt_s, &tm);
+ date_len += strftime(&date_s[date_len], DATE_LEN_MAX - date_len,
+ tmp_fmt_s, &tm);
#ifdef HAVE_FORMAT_NONLITERAL
#pragma GCC diagnostic pop
#endif
@@ -1829,7 +1984,7 @@ pcmk__time_format_hr(const char *format, const pcmk__time_hr_t *hr_dt)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
#endif
- date_len += snprintf(&date_s[date_len], max-date_len,
+ date_len += snprintf(&date_s[date_len], DATE_LEN_MAX - date_len,
nanofmt_s, nano_s);
#ifdef HAVE_FORMAT_NONLITERAL
#pragma GCC diagnostic pop
@@ -1839,6 +1994,7 @@ pcmk__time_format_hr(const char *format, const pcmk__time_hr_t *hr_dt)
}
return (date_len == 0)?NULL:strdup(date_s);
+#undef DATE_LEN_MAX
}
/*!
@@ -1858,22 +2014,15 @@ char *
pcmk__epoch2str(const time_t *source, uint32_t flags)
{
time_t epoch_time = (source == NULL)? time(NULL) : *source;
- char *result = NULL;
if (flags == 0) {
- const char *buf = pcmk__trim(ctime(&epoch_time));
-
- if (buf != NULL) {
- result = strdup(buf);
- CRM_ASSERT(result != NULL);
- }
+ return pcmk__str_copy(pcmk__trim(ctime(&epoch_time)));
} else {
crm_time_t dt;
crm_time_set_timet(&dt, &epoch_time);
- result = crm_time_as_string(&dt, flags);
+ return crm_time_as_string(&dt, flags);
}
- return result;
}
/*!
@@ -1899,7 +2048,6 @@ pcmk__timespec2str(const struct timespec *ts, uint32_t flags)
struct timespec tmp_ts;
crm_time_t dt;
char result[DATE_MAX] = { 0 };
- char *result_copy = NULL;
if (ts == NULL) {
qb_util_timespec_from_epoch_get(&tmp_ts);
@@ -1907,8 +2055,7 @@ pcmk__timespec2str(const struct timespec *ts, uint32_t flags)
}
crm_time_set_timet(&dt, &ts->tv_sec);
time_as_string_common(&dt, ts->tv_nsec / QB_TIME_NS_IN_USEC, flags, result);
- pcmk__str_update(&result_copy, result);
- return result_copy;
+ return pcmk__str_copy(result);
}
/*!
@@ -1934,24 +2081,24 @@ pcmk__readable_interval(guint interval_ms)
int offset = 0;
str[0] = '\0';
- if (interval_ms > MS_IN_D) {
+ if (interval_ms >= MS_IN_D) {
offset += snprintf(str + offset, MAXSTR - offset, "%ud",
interval_ms / MS_IN_D);
interval_ms -= (interval_ms / MS_IN_D) * MS_IN_D;
}
- if (interval_ms > MS_IN_H) {
+ if (interval_ms >= MS_IN_H) {
offset += snprintf(str + offset, MAXSTR - offset, "%uh",
interval_ms / MS_IN_H);
interval_ms -= (interval_ms / MS_IN_H) * MS_IN_H;
}
- if (interval_ms > MS_IN_M) {
+ if (interval_ms >= MS_IN_M) {
offset += snprintf(str + offset, MAXSTR - offset, "%um",
interval_ms / MS_IN_M);
interval_ms -= (interval_ms / MS_IN_M) * MS_IN_M;
}
// Ns, N.NNNs, or NNNms
- if (interval_ms > MS_IN_S) {
+ if (interval_ms >= MS_IN_S) {
offset += snprintf(str + offset, MAXSTR - offset, "%u",
interval_ms / MS_IN_S);
interval_ms -= (interval_ms / MS_IN_S) * MS_IN_S;
diff --git a/lib/common/logging.c b/lib/common/logging.c
index 7768c35..efdbac3 100644
--- a/lib/common/logging.c
+++ b/lib/common/logging.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2004-2023 the Pacemaker project contributors
+ * Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -192,7 +192,7 @@ set_format_string(int method, const char *daemon, pid_t use_pid,
static bool
logfile_disabled(const char *filename)
{
- return pcmk__str_eq(filename, PCMK__VALUE_NONE, pcmk__str_casei)
+ return pcmk__str_eq(filename, PCMK_VALUE_NONE, pcmk__str_casei)
|| pcmk__str_eq(filename, "/dev/null", pcmk__str_none);
}
@@ -785,7 +785,7 @@ set_identity(const char *entity, int argc, char *const *argv)
}
if (entity != NULL) {
- crm_system_name = strdup(entity);
+ crm_system_name = pcmk__str_copy(entity);
} else if ((argc > 0) && (argv != NULL)) {
char *mutable = strdup(argv[0]);
@@ -794,15 +794,13 @@ set_identity(const char *entity, int argc, char *const *argv)
if (strstr(modified, "lt-") == modified) {
modified += 3;
}
- crm_system_name = strdup(modified);
+ crm_system_name = pcmk__str_copy(modified);
free(mutable);
} else {
- crm_system_name = strdup("Unknown");
+ crm_system_name = pcmk__str_copy("Unknown");
}
- CRM_ASSERT(crm_system_name != NULL);
-
// Used by fencing.py.py (in fence-agents)
pcmk__set_env_option(PCMK__ENV_SERVICE, crm_system_name, false);
}
@@ -915,12 +913,12 @@ crm_log_init(const char *entity, uint8_t level, gboolean daemon, gboolean to_std
if (pcmk__is_daemon) {
facility = "daemon";
} else {
- facility = PCMK__VALUE_NONE;
+ facility = PCMK_VALUE_NONE;
}
pcmk__set_env_option(PCMK__ENV_LOGFACILITY, facility, true);
}
- if (pcmk__str_eq(facility, PCMK__VALUE_NONE, pcmk__str_casei)) {
+ if (pcmk__str_eq(facility, PCMK_VALUE_NONE, pcmk__str_casei)) {
quiet = TRUE;
@@ -957,7 +955,7 @@ crm_log_init(const char *entity, uint8_t level, gboolean daemon, gboolean to_std
{
const char *logfile = pcmk__env_option(PCMK__ENV_LOGFILE);
- if (!pcmk__str_eq(PCMK__VALUE_NONE, logfile, pcmk__str_casei)
+ if (!pcmk__str_eq(PCMK_VALUE_NONE, logfile, pcmk__str_casei)
&& (pcmk__is_daemon || (logfile != NULL))) {
// Daemons always get a log file, unless explicitly set to "none"
pcmk__add_logfile(logfile);
@@ -1296,4 +1294,4 @@ void pcmk__set_config_warning_handler(pcmk__config_warning_func warning_handler,
{
pcmk__config_warning_handler = warning_handler;
pcmk__config_warning_context = warning_context;
-} \ No newline at end of file
+}
diff --git a/lib/common/mainloop.c b/lib/common/mainloop.c
index f971713..7626134 100644
--- a/lib/common/mainloop.c
+++ b/lib/common/mainloop.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2004-2023 the Pacemaker project contributors
+ * Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -191,7 +191,6 @@ mainloop_add_trigger(int priority, int (*dispatch) (gpointer user_data),
CRM_ASSERT(sizeof(crm_trigger_t) > sizeof(GSource));
source = g_source_new(&crm_trigger_funcs, sizeof(crm_trigger_t));
- CRM_ASSERT(source != NULL);
return mainloop_setup_trigger(source, priority, dispatch, userdata);
}
@@ -1256,7 +1255,7 @@ mainloop_child_add_with_flags(pid_t pid, int timeout, const char *desc, void *pr
void (*callback) (mainloop_child_t * p, pid_t pid, int core, int signo, int exitcode))
{
static bool need_init = TRUE;
- mainloop_child_t *child = calloc(1, sizeof(mainloop_child_t));
+ mainloop_child_t *child = pcmk__assert_alloc(1, sizeof(mainloop_child_t));
child->pid = pid;
child->timerid = 0;
@@ -1264,7 +1263,7 @@ mainloop_child_add_with_flags(pid_t pid, int timeout, const char *desc, void *pr
child->privatedata = privatedata;
child->callback = callback;
child->flags = flags;
- pcmk__str_update(&child->desc, desc);
+ child->desc = pcmk__str_copy(desc);
if (timeout) {
child->timerid = g_timeout_add(timeout, child_timeout_callback, child);
@@ -1368,21 +1367,19 @@ mainloop_timer_set_period(mainloop_timer_t *t, guint period_ms)
mainloop_timer_t *
mainloop_timer_add(const char *name, guint period_ms, bool repeat, GSourceFunc cb, void *userdata)
{
- mainloop_timer_t *t = calloc(1, sizeof(mainloop_timer_t));
+ mainloop_timer_t *t = pcmk__assert_alloc(1, sizeof(mainloop_timer_t));
- if(t) {
- if(name) {
- t->name = crm_strdup_printf("%s-%u-%d", name, period_ms, repeat);
- } else {
- t->name = crm_strdup_printf("%p-%u-%d", t, period_ms, repeat);
- }
- t->id = 0;
- t->period_ms = period_ms;
- t->repeat = repeat;
- t->cb = cb;
- t->userdata = userdata;
- crm_trace("Created timer %s with %p %p", t->name, userdata, t->userdata);
- }
+ if (name != NULL) {
+ t->name = crm_strdup_printf("%s-%u-%d", name, period_ms, repeat);
+ } else {
+ t->name = crm_strdup_printf("%p-%u-%d", t, period_ms, repeat);
+ }
+ t->id = 0;
+ t->period_ms = period_ms;
+ t->repeat = repeat;
+ t->cb = cb;
+ t->userdata = userdata;
+ crm_trace("Created timer %s with %p %p", t->name, userdata, t->userdata);
return t;
}
diff --git a/lib/common/messages.c b/lib/common/messages.c
index 2c01eed..fa21169 100644
--- a/lib/common/messages.c
+++ b/lib/common/messages.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2004-2022 the Pacemaker project contributors
+ * Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -15,7 +15,7 @@
#include <glib.h>
#include <libxml/tree.h>
-#include <crm/msg_xml.h>
+#include <crm/common/xml.h>
#include <crm/common/xml_internal.h>
/*!
@@ -61,23 +61,25 @@ create_request_adv(const char *task, xmlNode *msg_data,
}
// host_from will get set for us if necessary by the controller when routed
- request = create_xml_node(NULL, __func__);
- crm_xml_add(request, F_CRM_ORIGIN, origin);
- crm_xml_add(request, F_TYPE, T_CRM);
- crm_xml_add(request, F_CRM_VERSION, CRM_FEATURE_SET);
- crm_xml_add(request, F_CRM_MSG_TYPE, XML_ATTR_REQUEST);
- crm_xml_add(request, F_CRM_REFERENCE, reference);
- crm_xml_add(request, F_CRM_TASK, task);
- crm_xml_add(request, F_CRM_SYS_TO, sys_to);
- crm_xml_add(request, F_CRM_SYS_FROM, true_from);
+ request = pcmk__xe_create(NULL, __func__);
+ crm_xml_add(request, PCMK_XA_ORIGIN, origin);
+ crm_xml_add(request, PCMK__XA_T, PCMK__VALUE_CRMD);
+ crm_xml_add(request, PCMK_XA_VERSION, CRM_FEATURE_SET);
+ crm_xml_add(request, PCMK__XA_SUBT, PCMK__VALUE_REQUEST);
+ crm_xml_add(request, PCMK_XA_REFERENCE, reference);
+ crm_xml_add(request, PCMK__XA_CRM_TASK, task);
+ crm_xml_add(request, PCMK__XA_CRM_SYS_TO, sys_to);
+ crm_xml_add(request, PCMK__XA_CRM_SYS_FROM, true_from);
/* HOSTTO will be ignored if it is to the DC anyway. */
if (host_to != NULL && strlen(host_to) > 0) {
- crm_xml_add(request, F_CRM_HOST_TO, host_to);
+ crm_xml_add(request, PCMK__XA_CRM_HOST_TO, host_to);
}
if (msg_data != NULL) {
- add_message_xml(request, F_CRM_DATA, msg_data);
+ xmlNode *wrapper = pcmk__xe_create(request, PCMK__XE_CRM_XML);
+
+ pcmk__xml_copy(wrapper, msg_data);
}
free(reference);
free(true_from);
@@ -104,67 +106,56 @@ create_reply_adv(const xmlNode *original_request, xmlNode *xml_response_data,
{
xmlNode *reply = NULL;
- const char *host_from = crm_element_value(original_request, F_CRM_HOST_FROM);
- const char *sys_from = crm_element_value(original_request, F_CRM_SYS_FROM);
- const char *sys_to = crm_element_value(original_request, F_CRM_SYS_TO);
- const char *type = crm_element_value(original_request, F_CRM_MSG_TYPE);
- const char *operation = crm_element_value(original_request, F_CRM_TASK);
- const char *crm_msg_reference = crm_element_value(original_request, F_CRM_REFERENCE);
+ const char *host_from = crm_element_value(original_request, PCMK__XA_SRC);
+ const char *sys_from = crm_element_value(original_request,
+ PCMK__XA_CRM_SYS_FROM);
+ const char *sys_to = crm_element_value(original_request,
+ PCMK__XA_CRM_SYS_TO);
+ const char *type = crm_element_value(original_request, PCMK__XA_SUBT);
+ const char *operation = crm_element_value(original_request,
+ PCMK__XA_CRM_TASK);
+ const char *crm_msg_reference = crm_element_value(original_request,
+ PCMK_XA_REFERENCE);
if (type == NULL) {
crm_err("Cannot create new_message, no message type in original message");
CRM_ASSERT(type != NULL);
return NULL;
-#if 0
- } else if (strcasecmp(XML_ATTR_REQUEST, type) != 0) {
- crm_err("Cannot create new_message, original message was not a request");
- return NULL;
-#endif
}
- reply = create_xml_node(NULL, __func__);
- if (reply == NULL) {
- crm_err("Cannot create new_message, malloc failed");
- return NULL;
+
+ if (strcmp(type, PCMK__VALUE_REQUEST) != 0) {
+ /* Replies should only be generated for request messages, but it's possible
+ * we expect replies to other messages right now so this can't be enforced.
+ */
+ crm_trace("Creating a reply for a non-request original message");
}
- crm_xml_add(reply, F_CRM_ORIGIN, origin);
- crm_xml_add(reply, F_TYPE, T_CRM);
- crm_xml_add(reply, F_CRM_VERSION, CRM_FEATURE_SET);
- crm_xml_add(reply, F_CRM_MSG_TYPE, XML_ATTR_RESPONSE);
- crm_xml_add(reply, F_CRM_REFERENCE, crm_msg_reference);
- crm_xml_add(reply, F_CRM_TASK, operation);
+ reply = pcmk__xe_create(NULL, __func__);
+ crm_xml_add(reply, PCMK_XA_ORIGIN, origin);
+ crm_xml_add(reply, PCMK__XA_T, PCMK__VALUE_CRMD);
+ crm_xml_add(reply, PCMK_XA_VERSION, CRM_FEATURE_SET);
+ crm_xml_add(reply, PCMK__XA_SUBT, PCMK__VALUE_RESPONSE);
+ crm_xml_add(reply, PCMK_XA_REFERENCE, crm_msg_reference);
+ crm_xml_add(reply, PCMK__XA_CRM_TASK, operation);
/* since this is a reply, we reverse the from and to */
- crm_xml_add(reply, F_CRM_SYS_TO, sys_from);
- crm_xml_add(reply, F_CRM_SYS_FROM, sys_to);
+ crm_xml_add(reply, PCMK__XA_CRM_SYS_TO, sys_from);
+ crm_xml_add(reply, PCMK__XA_CRM_SYS_FROM, sys_to);
/* HOSTTO will be ignored if it is to the DC anyway. */
if (host_from != NULL && strlen(host_from) > 0) {
- crm_xml_add(reply, F_CRM_HOST_TO, host_from);
+ crm_xml_add(reply, PCMK__XA_CRM_HOST_TO, host_from);
}
if (xml_response_data != NULL) {
- add_message_xml(reply, F_CRM_DATA, xml_response_data);
+ xmlNode *wrapper = pcmk__xe_create(reply, PCMK__XE_CRM_XML);
+
+ pcmk__xml_copy(wrapper, xml_response_data);
}
return reply;
}
-xmlNode *
-get_message_xml(const xmlNode *msg, const char *field)
-{
- return pcmk__xml_first_child(first_named_child(msg, field));
-}
-
-gboolean
-add_message_xml(xmlNode *msg, const char *field, xmlNode *xml)
-{
- xmlNode *holder = create_xml_node(msg, field);
-
- add_node_copy(holder, xml);
- return TRUE;
-}
-
/*!
* \brief Get name to be used as identifier for cluster messages
*
@@ -289,3 +280,28 @@ pcmk__reset_request(pcmk__request_t *request)
pcmk__reset_result(&(request->result));
}
+
+// Deprecated functions kept only for backward API compatibility
+// LCOV_EXCL_START
+
+#include <crm/common/xml_compat.h>
+
+gboolean
+add_message_xml(xmlNode *msg, const char *field, xmlNode *xml)
+{
+ xmlNode *holder = pcmk__xe_create(msg, field);
+
+ pcmk__xml_copy(holder, xml);
+ return TRUE;
+}
+
+xmlNode *
+get_message_xml(const xmlNode *msg, const char *field)
+{
+ xmlNode *child = pcmk__xe_first_child(msg, field, NULL, NULL);
+
+ return pcmk__xe_first_child(child, NULL, NULL, NULL);
+}
+
+// LCOV_EXCL_STOP
+// End deprecated API
diff --git a/lib/common/mock.c b/lib/common/mock.c
index 6f837ad..43c6e8f 100644
--- a/lib/common/mock.c
+++ b/lib/common/mock.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2021-2023 the Pacemaker project contributors
+ * Copyright 2021-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -25,6 +25,7 @@
#include <grp.h>
#include <cmocka.h>
+#include <crm/common/unittest_internal.h>
#include "mock_private.h"
/* This file is only used when running "make check". It is built into
@@ -54,6 +55,27 @@
*/
// LCOV_EXCL_START
+
+/* abort()
+ *
+ * Always mock abort - there's no pcmk__mock_abort tuneable to control this.
+ * Because abort calls _exit(), which doesn't run any of the things registered
+ * with atexit(), coverage numbers do not get written out. This most noticably
+ * affects places where we are testing that things abort when they should.
+ *
+ * The solution is this wrapper that is always enabled when we are running
+ * unit tests (mock.c does not get included for the regular libcrmcommon.so).
+ * All it does is dump coverage data and call the real abort().
+ */
+_Noreturn void
+__wrap_abort(void)
+{
+#if (PCMK__WITH_COVERAGE == 1)
+ __gcov_dump();
+#endif
+ __real_abort();
+}
+
/* calloc()
*
* If pcmk__mock_calloc is set to true, later calls to calloc() will return
@@ -103,6 +125,31 @@ __wrap_getenv(const char *name)
}
+/* realloc()
+ *
+ * If pcmk__mock_realloc is set to true, later calls to realloc() will return
+ * NULL and must be preceded by:
+ *
+ * expect_*(__wrap_realloc, ptr[, ...]);
+ * expect_*(__wrap_realloc, size[, ...]);
+ *
+ * expect_* functions: https://api.cmocka.org/group__cmocka__param.html
+ */
+
+bool pcmk__mock_realloc = false;
+
+void *
+__wrap_realloc(void *ptr, size_t size)
+{
+ if (!pcmk__mock_realloc) {
+ return __real_realloc(ptr, size);
+ }
+ check_expected_ptr(ptr);
+ check_expected(size);
+ return NULL;
+}
+
+
/* setenv()
*
* If pcmk__mock_setenv is set to true, later calls to setenv() must be preceded
@@ -412,40 +459,4 @@ __wrap_strdup(const char *s)
return NULL;
}
-
-/* uname()
- *
- * If pcmk__mock_uname is set to true, later calls to uname() must be preceded
- * by:
- *
- * expect_*(__wrap_uname, buf[, ...]);
- * will_return(__wrap_uname, return_value);
- * will_return(__wrap_uname, node_name_for_buf_parameter_to_uname);
- *
- * expect_* functions: https://api.cmocka.org/group__cmocka__param.html
- */
-
-bool pcmk__mock_uname = false;
-
-int
-__wrap_uname(struct utsname *buf)
-{
- if (pcmk__mock_uname) {
- int retval = 0;
- char *result = NULL;
-
- check_expected_ptr(buf);
- retval = mock_type(int);
- result = mock_ptr_type(char *);
-
- if (result != NULL) {
- strcpy(buf->nodename, result);
- }
- return retval;
-
- } else {
- return __real_uname(buf);
- }
-}
-
// LCOV_EXCL_STOP
diff --git a/lib/common/mock_private.h b/lib/common/mock_private.h
index b0e0ed2..3beeda4 100644
--- a/lib/common/mock_private.h
+++ b/lib/common/mock_private.h
@@ -1,5 +1,5 @@
/*
- * Copyright 2021-2023 the Pacemaker project contributors
+ * Copyright 2021-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -22,6 +22,9 @@
/* This header is for the sole use of libcrmcommon_test and unit tests */
+_Noreturn void __real_abort(void);
+_Noreturn void __wrap_abort(void);
+
extern bool pcmk__mock_calloc;
void *__real_calloc(size_t nmemb, size_t size);
void *__wrap_calloc(size_t nmemb, size_t size);
@@ -38,6 +41,10 @@ extern bool pcmk__mock_getenv;
char *__real_getenv(const char *name);
char *__wrap_getenv(const char *name);
+extern bool pcmk__mock_realloc;
+void *__real_realloc(void *ptr, size_t size);
+void *__wrap_realloc(void *ptr, size_t size);
+
extern bool pcmk__mock_setenv;
int __real_setenv(const char *name, const char *value, int overwrite);
int __wrap_setenv(const char *name, const char *value, int overwrite);
@@ -74,8 +81,4 @@ extern bool pcmk__mock_strdup;
char *__real_strdup(const char *s);
char *__wrap_strdup(const char *s);
-extern bool pcmk__mock_uname;
-int __real_uname(struct utsname *buf);
-int __wrap_uname(struct utsname *buf);
-
#endif // MOCK_PRIVATE__H
diff --git a/lib/common/nodes.c b/lib/common/nodes.c
index a17d587..4edeafb 100644
--- a/lib/common/nodes.c
+++ b/lib/common/nodes.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 the Pacemaker project contributors
+ * Copyright 2022-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -9,16 +9,155 @@
#include <crm_internal.h>
+#include <libxml/tree.h> // xmlNode
#include <crm/common/nvpair.h>
+/*!
+ * \internal
+ * \brief Check whether a node is online
+ *
+ * \param[in] node Node to check
+ *
+ * \return true if \p node is online, otherwise false
+ */
+bool
+pcmk_node_is_online(const pcmk_node_t *node)
+{
+ return (node != NULL) && node->details->online;
+}
+
+/*!
+ * \internal
+ * \brief Check whether a node is pending
+ *
+ * Check whether a node is pending. A node is pending if it is a member of the
+ * cluster but not the controller group, which means it is in the process of
+ * either joining or leaving the cluster.
+ *
+ * \param[in] node Node to check
+ *
+ * \return true if \p node is pending, otherwise false
+ */
+bool
+pcmk_node_is_pending(const pcmk_node_t *node)
+{
+ return (node != NULL) && node->details->pending;
+}
+
+/*!
+ * \internal
+ * \brief Check whether a node is clean
+ *
+ * Check whether a node is clean. A node is clean if it is a cluster node or
+ * remote node that has been seen by the cluster at least once, or the
+ * startup-fencing cluster option is false; and the node, and its host if a
+ * guest or bundle node, are not scheduled to be fenced.
+ *
+ * \param[in] node Node to check
+ *
+ * \return true if \p node is clean, otherwise false
+ */
+bool
+pcmk_node_is_clean(const pcmk_node_t *node)
+{
+ return (node != NULL) && !(node->details->unclean);
+}
+
+/*!
+ * \internal
+ * \brief Check whether a node is shutting down
+ *
+ * \param[in] node Node to check
+ *
+ * \return true if \p node is shutting down, otherwise false
+ */
+bool
+pcmk_node_is_shutting_down(const pcmk_node_t *node)
+{
+ return (node != NULL) && node->details->shutdown;
+}
+
+/*!
+ * \internal
+ * \brief Check whether a node is in maintenance mode
+ *
+ * \param[in] node Node to check
+ *
+ * \return true if \p node is in maintenance mode, otherwise false
+ */
+bool
+pcmk_node_is_in_maintenance(const pcmk_node_t *node)
+{
+ return (node != NULL) && node->details->maintenance;
+}
+
+/*!
+ * \internal
+ * \brief Call a function for each resource active on a node
+ *
+ * Call a caller-supplied function with a caller-supplied argument for each
+ * resource that is active on a given node. If the function returns false, this
+ * function will return immediately without processing any remaining resources.
+ *
+ * \param[in] node Node to check
+ *
+ * \return Result of last call of \p fn (or false if none)
+ */
+bool
+pcmk_foreach_active_resource(pcmk_node_t *node,
+ bool (*fn)(pcmk_resource_t *, void *),
+ void *user_data)
+{
+ bool result = false;
+
+ if ((node != NULL) && (fn != NULL)) {
+ for (GList *item = node->details->running_rsc; item != NULL;
+ item = item->next) {
+
+ result = fn((pcmk_resource_t *) item->data, user_data);
+ if (!result) {
+ break;
+ }
+ }
+ }
+ return result;
+}
+
void
pcmk__xe_add_node(xmlNode *xml, const char *node, int nodeid)
{
+ CRM_ASSERT(xml != NULL);
+
if (node != NULL) {
- crm_xml_add(xml, PCMK__XA_ATTR_NODE_NAME, node);
+ crm_xml_add(xml, PCMK__XA_ATTR_HOST, node);
}
if (nodeid > 0) {
- crm_xml_add_int(xml, PCMK__XA_ATTR_NODE_ID, nodeid);
+ crm_xml_add_int(xml, PCMK__XA_ATTR_HOST_ID, nodeid);
+ }
+}
+
+/*!
+ * \internal
+ * \brief Find a node by name in a list of nodes
+ *
+ * \param[in] nodes List of nodes (as pcmk_node_t*)
+ * \param[in] node_name Name of node to find
+ *
+ * \return Node from \p nodes that matches \p node_name if any, otherwise NULL
+ */
+pcmk_node_t *
+pcmk__find_node_in_list(const GList *nodes, const char *node_name)
+{
+ if (node_name != NULL) {
+ for (const GList *iter = nodes; iter != NULL; iter = iter->next) {
+ pcmk_node_t *node = (pcmk_node_t *) iter->data;
+
+ if (pcmk__str_eq(node->details->uname, node_name,
+ pcmk__str_casei)) {
+ return node;
+ }
+ }
}
+ return NULL;
}
diff --git a/lib/common/nvpair.c b/lib/common/nvpair.c
index dbb9c99..295e923 100644
--- a/lib/common/nvpair.c
+++ b/lib/common/nvpair.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2004-2023 the Pacemaker project contributors
+ * Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -17,17 +17,17 @@
#include <libxml/tree.h>
#include <crm/crm.h>
-#include <crm/msg_xml.h>
#include <crm/common/xml.h>
#include <crm/common/xml_internal.h>
#include "crmcommon_private.h"
/*
- * This file isolates handling of three types of name/value pairs:
+ * This file isolates handling of various kinds of name/value pairs:
*
* - pcmk_nvpair_t data type
* - XML attributes (<TAG ... NAME=VALUE ...>)
* - XML nvpair elements (<nvpair id=ID name=NAME value=VALUE>)
+ * - Meta-attributes (for resources and actions)
*/
// pcmk_nvpair_t handling
@@ -50,11 +50,10 @@ pcmk__new_nvpair(const char *name, const char *value)
CRM_ASSERT(name);
- nvpair = calloc(1, sizeof(pcmk_nvpair_t));
- CRM_ASSERT(nvpair);
+ nvpair = pcmk__assert_alloc(1, sizeof(pcmk_nvpair_t));
- pcmk__str_update(&nvpair->name, name);
- pcmk__str_update(&nvpair->value, value);
+ nvpair->name = pcmk__str_copy(name);
+ nvpair->value = pcmk__str_copy(value);
return nvpair;
}
@@ -630,6 +629,37 @@ crm_element_value_timeval(const xmlNode *xml, const char *name_sec,
}
/*!
+ * \internal
+ * \brief Get a date/time object from an XML attribute value
+ *
+ * \param[in] xml XML with attribute to parse (from CIB)
+ * \param[in] attr Name of attribute to parse
+ * \param[out] t Where to create date/time object
+ * (\p *t must be NULL initially)
+ *
+ * \return Standard Pacemaker return code
+ * \note The caller is responsible for freeing \p *t using crm_time_free().
+ */
+int
+pcmk__xe_get_datetime(const xmlNode *xml, const char *attr, crm_time_t **t)
+{
+ const char *value = NULL;
+
+ if ((t == NULL) || (*t != NULL) || (xml == NULL) || (attr == NULL)) {
+ return EINVAL;
+ }
+
+ value = crm_element_value(xml, attr);
+ if (value != NULL) {
+ *t = crm_time_new(value);
+ if (*t == NULL) {
+ return pcmk_rc_unpack_error;
+ }
+ }
+ return pcmk_rc_ok;
+}
+
+/*!
* \brief Retrieve a copy of the value of an XML attribute
*
* This is like \c crm_element_value() but allocating new memory for the result.
@@ -643,20 +673,18 @@ crm_element_value_timeval(const xmlNode *xml, const char *name_sec,
char *
crm_element_value_copy(const xmlNode *data, const char *name)
{
- char *value_copy = NULL;
-
- pcmk__str_update(&value_copy, crm_element_value(data, name));
- return value_copy;
+ return pcmk__str_copy(crm_element_value(data, name));
}
/*!
- * \brief Add hash table entry to XML as (possibly legacy) name/value
+ * \brief Safely add hash table entry to XML as attribute or name-value pair
*
* Suitable for \c g_hash_table_foreach(), this function takes a hash table key
* and value, with an XML node passed as user data, and adds an XML attribute
* with the specified name and value if it does not already exist. If the key
- * name starts with a digit, this will instead add a \<param name=NAME
- * value=VALUE/> child to the XML (for legacy compatibility with heartbeat).
+ * name starts with a digit, then it's not a valid XML attribute name. In that
+ * case, this will instead add a <tt><param name=NAME value=VALUE/></tt> child
+ * to the XML.
*
* \param[in] key Key of hash table entry
* \param[in] value Value of hash table entry
@@ -665,16 +693,24 @@ crm_element_value_copy(const xmlNode *data, const char *name)
void
hash2smartfield(gpointer key, gpointer value, gpointer user_data)
{
+ /* @TODO Generate PCMK__XE_PARAM nodes for all keys that aren't valid XML
+ * attribute names (not just those that start with digits), or possibly for
+ * all keys to simplify parsing.
+ *
+ * Consider either deprecating as public API or exposing PCMK__XE_PARAM.
+ * PCMK__XE_PARAM is currently private because it doesn't appear in any
+ * output that Pacemaker generates.
+ */
const char *name = key;
const char *s_value = value;
xmlNode *xml_node = user_data;
if (isdigit(name[0])) {
- xmlNode *tmp = create_xml_node(xml_node, XML_TAG_PARAM);
+ xmlNode *tmp = pcmk__xe_create(xml_node, PCMK__XE_PARAM);
- crm_xml_add(tmp, XML_NVPAIR_ATTR_NAME, name);
- crm_xml_add(tmp, XML_NVPAIR_ATTR_VALUE, s_value);
+ crm_xml_add(tmp, PCMK_XA_NAME, name);
+ crm_xml_add(tmp, PCMK_XA_VALUE, s_value);
} else if (crm_element_value(xml_node, name) == NULL) {
crm_xml_add(xml_node, name, s_value);
@@ -770,19 +806,16 @@ crm_create_nvpair_xml(xmlNode *parent, const char *id, const char *name,
*/
CRM_CHECK(id || name, return NULL);
- nvp = create_xml_node(parent, XML_CIB_TAG_NVPAIR);
- CRM_CHECK(nvp, return NULL);
+ nvp = pcmk__xe_create(parent, PCMK_XE_NVPAIR);
if (id) {
- crm_xml_add(nvp, XML_ATTR_ID, id);
+ crm_xml_add(nvp, PCMK_XA_ID, id);
} else {
- const char *parent_id = ID(parent);
-
crm_xml_set_id(nvp, "%s-%s",
- (parent_id? parent_id : XML_CIB_TAG_NVPAIR), name);
+ pcmk__s(pcmk__xe_id(parent), PCMK_XE_NVPAIR), name);
}
- crm_xml_add(nvp, XML_NVPAIR_ATTR_NAME, name);
- crm_xml_add(nvp, XML_NVPAIR_ATTR_VALUE, value);
+ crm_xml_add(nvp, PCMK_XA_NAME, name);
+ crm_xml_add(nvp, PCMK_XA_VALUE, value);
return nvp;
}
@@ -832,7 +865,7 @@ xml2list(const xmlNode *parent)
CRM_CHECK(parent != NULL, return nvpair_hash);
- nvpair_list = find_xml_node(parent, XML_TAG_ATTRS, FALSE);
+ nvpair_list = pcmk__xe_first_child(parent, PCMK__XE_ATTRIBUTES, NULL, NULL);
if (nvpair_list == NULL) {
crm_trace("No attributes in %s", parent->name);
crm_log_xml_trace(parent, "No attributes for resource op");
@@ -848,20 +881,18 @@ xml2list(const xmlNode *parent)
crm_trace("Added %s=%s", p_name, p_value);
- g_hash_table_insert(nvpair_hash, strdup(p_name), strdup(p_value));
+ pcmk__insert_dup(nvpair_hash, p_name, p_value);
}
- for (child = pcmk__xml_first_child(nvpair_list); child != NULL;
- child = pcmk__xml_next(child)) {
+ for (child = pcmk__xe_first_child(nvpair_list, PCMK__XE_PARAM, NULL, NULL);
+ child != NULL; child = pcmk__xe_next_same(child)) {
- if (strcmp((const char *)child->name, XML_TAG_PARAM) == 0) {
- const char *key = crm_element_value(child, XML_NVPAIR_ATTR_NAME);
- const char *value = crm_element_value(child, XML_NVPAIR_ATTR_VALUE);
+ const char *key = crm_element_value(child, PCMK_XA_NAME);
+ const char *value = crm_element_value(child, PCMK_XA_VALUE);
- crm_trace("Added %s=%s", key, value);
- if (key != NULL && value != NULL) {
- g_hash_table_insert(nvpair_hash, strdup(key), strdup(value));
- }
+ crm_trace("Added %s=%s", key, value);
+ if (key != NULL && value != NULL) {
+ pcmk__insert_dup(nvpair_hash, key, value);
}
}
@@ -871,7 +902,7 @@ xml2list(const xmlNode *parent)
void
pcmk__xe_set_bool_attr(xmlNodePtr node, const char *name, bool value)
{
- crm_xml_add(node, name, value ? XML_BOOLEAN_TRUE : XML_BOOLEAN_FALSE);
+ crm_xml_add(node, name, pcmk__btoa(value));
}
int
@@ -911,6 +942,60 @@ pcmk__xe_attr_is_true(const xmlNode *node, const char *name)
return rc == pcmk_rc_ok && value == true;
}
+// Meta-attribute handling
+
+/*!
+ * \brief Get the environment variable equivalent of a meta-attribute name
+ *
+ * \param[in] attr_name Name of meta-attribute
+ *
+ * \return Newly allocated string for \p attr_name with "CRM_meta_" prefix and
+ * underbars instead of dashes
+ * \note This asserts on an invalid argument or memory allocation error, so
+ * callers can assume the result is non-NULL. The caller is responsible
+ * for freeing the result using free().
+ */
+char *
+crm_meta_name(const char *attr_name)
+{
+ char *env_name = NULL;
+
+ CRM_ASSERT(!pcmk__str_empty(attr_name));
+
+ env_name = crm_strdup_printf(CRM_META "_%s", attr_name);
+ for (char *c = env_name; *c != '\0'; ++c) {
+ if (*c == '-') {
+ *c = '_';
+ }
+ }
+ return env_name;
+}
+
+/*!
+ * \brief Get the value of a meta-attribute
+ *
+ * Get the value of a meta-attribute from a hash table whose keys are
+ * meta-attribute environment variable names (as crm_meta_name() would
+ * create, like pcmk__graph_action_t:params, not pcmk_resource_t:meta).
+ *
+ * \param[in] meta Hash table of meta-attributes
+ * \param[in] attr_name Name of meta-attribute to get
+ *
+ * \return Value of given meta-attribute
+ */
+const char *
+crm_meta_value(GHashTable *meta, const char *attr_name)
+{
+ if ((meta != NULL) && (attr_name != NULL)) {
+ char *key = crm_meta_name(attr_name);
+ const char *value = g_hash_table_lookup(meta, key);
+
+ free(key);
+ return value;
+ }
+ return NULL;
+}
+
// Deprecated functions kept only for backward API compatibility
// LCOV_EXCL_START
@@ -960,7 +1045,7 @@ crm_xml_replace(xmlNode *node, const char *name, const char *value)
return NULL;
} else if (old_value && !value) {
- xml_remove_prop(node, name);
+ pcmk__xe_remove_attr(node, name);
return NULL;
}
diff --git a/lib/common/options.c b/lib/common/options.c
index 2d86ebc..ba64959 100644
--- a/lib/common/options.c
+++ b/lib/common/options.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2004-2022 the Pacemaker project contributors
+ * Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -20,6 +20,7 @@
#include <sys/stat.h>
#include <crm/crm.h>
+#include <crm/common/xml.h>
void
pcmk__cli_help(char cmd)
@@ -39,6 +40,1035 @@ pcmk__cli_help(char cmd)
/*
+ * Option metadata
+ */
+
+static const pcmk__cluster_option_t cluster_options[] = {
+ /* name, old name, type, allowed values,
+ * default value, validator,
+ * flags,
+ * short description,
+ * long description
+ */
+ {
+ PCMK_OPT_DC_VERSION, NULL, PCMK_VALUE_VERSION, NULL,
+ NULL, NULL,
+ pcmk__opt_controld|pcmk__opt_generated,
+ N_("Pacemaker version on cluster node elected Designated Controller "
+ "(DC)"),
+ N_("Includes a hash which identifies the exact revision the code was "
+ "built from. Used for diagnostic purposes."),
+ },
+ {
+ PCMK_OPT_CLUSTER_INFRASTRUCTURE, NULL, PCMK_VALUE_STRING, NULL,
+ NULL, NULL,
+ pcmk__opt_controld|pcmk__opt_generated,
+ N_("The messaging layer on which Pacemaker is currently running"),
+ N_("Used for informational and diagnostic purposes."),
+ },
+ {
+ PCMK_OPT_CLUSTER_NAME, NULL, PCMK_VALUE_STRING, NULL,
+ NULL, NULL,
+ pcmk__opt_controld,
+ N_("An arbitrary name for the cluster"),
+ N_("This optional value is mostly for users' convenience as desired "
+ "in administration, but may also be used in Pacemaker "
+ "configuration rules via the #cluster-name node attribute, and "
+ "by higher-level tools and resource agents."),
+ },
+ {
+ PCMK_OPT_DC_DEADTIME, NULL, PCMK_VALUE_DURATION, NULL,
+ "20s", pcmk__valid_interval_spec,
+ pcmk__opt_controld,
+ N_("How long to wait for a response from other nodes during start-up"),
+ N_("The optimal value will depend on the speed and load of your "
+ "network and the type of switches used."),
+ },
+ {
+ PCMK_OPT_CLUSTER_RECHECK_INTERVAL, NULL, PCMK_VALUE_DURATION, NULL,
+ "15min", pcmk__valid_interval_spec,
+ pcmk__opt_controld,
+ N_("Polling interval to recheck cluster state and evaluate rules "
+ "with date specifications"),
+ N_("Pacemaker is primarily event-driven, and looks ahead to know when "
+ "to recheck cluster state for failure-timeout settings and most "
+ "time-based rules. However, it will also recheck the cluster after "
+ "this amount of inactivity, to evaluate rules with date "
+ "specifications and serve as a fail-safe for certain types of "
+ "scheduler bugs. A value of 0 disables polling. A positive value "
+ "sets an interval in seconds, unless other units are specified "
+ "(for example, \"5min\")."),
+ },
+ {
+ PCMK_OPT_FENCE_REACTION, NULL, PCMK_VALUE_SELECT,
+ PCMK_VALUE_STOP ", " PCMK_VALUE_PANIC,
+ PCMK_VALUE_STOP, NULL,
+ pcmk__opt_controld,
+ N_("How a cluster node should react if notified of its own fencing"),
+ N_("A cluster node may receive notification of a \"succeeded\" "
+ "fencing that targeted it if fencing is misconfigured, or if "
+ "fabric fencing is in use that doesn't cut cluster communication. "
+ "Use \"stop\" to attempt to immediately stop Pacemaker and stay "
+ "stopped, or \"panic\" to attempt to immediately reboot the local "
+ "node, falling back to stop on failure."),
+ },
+ {
+ PCMK_OPT_ELECTION_TIMEOUT, NULL, PCMK_VALUE_DURATION, NULL,
+ "2min", pcmk__valid_interval_spec,
+ pcmk__opt_controld|pcmk__opt_advanced,
+ N_("Declare an election failed if it is not decided within this much "
+ "time. If you need to adjust this value, it probably indicates "
+ "the presence of a bug."),
+ NULL,
+ },
+ {
+ PCMK_OPT_SHUTDOWN_ESCALATION, NULL, PCMK_VALUE_DURATION, NULL,
+ "20min", pcmk__valid_interval_spec,
+ pcmk__opt_controld|pcmk__opt_advanced,
+ N_("Exit immediately if shutdown does not complete within this much "
+ "time. If you need to adjust this value, it probably indicates "
+ "the presence of a bug."),
+ NULL,
+ },
+ {
+ PCMK_OPT_JOIN_INTEGRATION_TIMEOUT, "crmd-integration-timeout",
+ PCMK_VALUE_DURATION, NULL,
+ "3min", pcmk__valid_interval_spec,
+ pcmk__opt_controld|pcmk__opt_advanced,
+ N_("If you need to adjust this value, it probably indicates "
+ "the presence of a bug."),
+ NULL,
+ },
+ {
+ PCMK_OPT_JOIN_FINALIZATION_TIMEOUT, "crmd-finalization-timeout",
+ PCMK_VALUE_DURATION, NULL,
+ "30min", pcmk__valid_interval_spec,
+ pcmk__opt_controld|pcmk__opt_advanced,
+ N_("If you need to adjust this value, it probably indicates "
+ "the presence of a bug."),
+ NULL,
+ },
+ {
+ PCMK_OPT_TRANSITION_DELAY, "crmd-transition-delay", PCMK_VALUE_DURATION,
+ NULL,
+ "0s", pcmk__valid_interval_spec,
+ pcmk__opt_controld|pcmk__opt_advanced,
+ N_("Enabling this option will slow down cluster recovery under all "
+ "conditions"),
+ N_("Delay cluster recovery for this much time to allow for additional "
+ "events to occur. Useful if your configuration is sensitive to "
+ "the order in which ping updates arrive."),
+ },
+ {
+ PCMK_OPT_NO_QUORUM_POLICY, NULL, PCMK_VALUE_SELECT,
+ PCMK_VALUE_STOP ", " PCMK_VALUE_FREEZE ", " PCMK_VALUE_IGNORE
+ ", " PCMK_VALUE_DEMOTE ", " PCMK_VALUE_FENCE_LEGACY,
+ PCMK_VALUE_STOP, pcmk__valid_no_quorum_policy,
+ pcmk__opt_schedulerd,
+ N_("What to do when the cluster does not have quorum"),
+ NULL,
+ },
+ {
+ PCMK_OPT_SHUTDOWN_LOCK, NULL, PCMK_VALUE_BOOLEAN, NULL,
+ PCMK_VALUE_FALSE, pcmk__valid_boolean,
+ pcmk__opt_schedulerd,
+ N_("Whether to lock resources to a cleanly shut down node"),
+ N_("When true, resources active on a node when it is cleanly shut down "
+ "are kept \"locked\" to that node (not allowed to run elsewhere) "
+ "until they start again on that node after it rejoins (or for at "
+ "most shutdown-lock-limit, if set). Stonith resources and "
+ "Pacemaker Remote connections are never locked. Clone and bundle "
+ "instances and the promoted role of promotable clones are "
+ "currently never locked, though support could be added in a future "
+ "release."),
+ },
+ {
+ PCMK_OPT_SHUTDOWN_LOCK_LIMIT, NULL, PCMK_VALUE_DURATION, NULL,
+ "0", pcmk__valid_interval_spec,
+ pcmk__opt_schedulerd,
+ N_("Do not lock resources to a cleanly shut down node longer than "
+ "this"),
+ N_("If shutdown-lock is true and this is set to a nonzero time "
+ "duration, shutdown locks will expire after this much time has "
+ "passed since the shutdown was initiated, even if the node has not "
+ "rejoined."),
+ },
+ {
+ PCMK_OPT_ENABLE_ACL, NULL, PCMK_VALUE_BOOLEAN, NULL,
+ PCMK_VALUE_FALSE, pcmk__valid_boolean,
+ pcmk__opt_based,
+ N_("Enable Access Control Lists (ACLs) for the CIB"),
+ NULL,
+ },
+ {
+ PCMK_OPT_SYMMETRIC_CLUSTER, NULL, PCMK_VALUE_BOOLEAN, NULL,
+ PCMK_VALUE_TRUE, pcmk__valid_boolean,
+ pcmk__opt_schedulerd,
+ N_("Whether resources can run on any node by default"),
+ NULL,
+ },
+ {
+ PCMK_OPT_MAINTENANCE_MODE, NULL, PCMK_VALUE_BOOLEAN, NULL,
+ PCMK_VALUE_FALSE, pcmk__valid_boolean,
+ pcmk__opt_schedulerd,
+ N_("Whether the cluster should refrain from monitoring, starting, and "
+ "stopping resources"),
+ NULL,
+ },
+ {
+ PCMK_OPT_START_FAILURE_IS_FATAL, NULL, PCMK_VALUE_BOOLEAN, NULL,
+ PCMK_VALUE_TRUE, pcmk__valid_boolean,
+ pcmk__opt_schedulerd,
+ N_("Whether a start failure should prevent a resource from being "
+ "recovered on the same node"),
+ N_("When true, the cluster will immediately ban a resource from a node "
+ "if it fails to start there. When false, the cluster will instead "
+ "check the resource's fail count against its migration-threshold.")
+ },
+ {
+ PCMK_OPT_ENABLE_STARTUP_PROBES, NULL, PCMK_VALUE_BOOLEAN, NULL,
+ PCMK_VALUE_TRUE, pcmk__valid_boolean,
+ pcmk__opt_schedulerd,
+ N_("Whether the cluster should check for active resources during "
+ "start-up"),
+ NULL,
+ },
+
+ // Fencing-related options
+ {
+ PCMK_OPT_STONITH_ENABLED, NULL, PCMK_VALUE_BOOLEAN, NULL,
+ PCMK_VALUE_TRUE, pcmk__valid_boolean,
+ pcmk__opt_schedulerd|pcmk__opt_advanced,
+ N_("Whether nodes may be fenced as part of recovery"),
+ N_("If false, unresponsive nodes are immediately assumed to be "
+ "harmless, and resources that were active on them may be recovered "
+ "elsewhere. This can result in a \"split-brain\" situation, "
+ "potentially leading to data loss and/or service unavailability."),
+ },
+ {
+ PCMK_OPT_STONITH_ACTION, NULL, PCMK_VALUE_SELECT,
+ PCMK_ACTION_REBOOT ", " PCMK_ACTION_OFF ", " PCMK__ACTION_POWEROFF,
+ PCMK_ACTION_REBOOT, pcmk__is_fencing_action,
+ pcmk__opt_schedulerd,
+ N_("Action to send to fence device when a node needs to be fenced "
+ "(\"poweroff\" is a deprecated alias for \"off\")"),
+ NULL,
+ },
+ {
+ PCMK_OPT_STONITH_TIMEOUT, NULL, PCMK_VALUE_DURATION, NULL,
+ "60s", pcmk__valid_interval_spec,
+ pcmk__opt_schedulerd,
+ N_("How long to wait for on, off, and reboot fence actions to complete "
+ "by default"),
+ NULL,
+ },
+ {
+ PCMK_OPT_HAVE_WATCHDOG, NULL, PCMK_VALUE_BOOLEAN, NULL,
+ PCMK_VALUE_FALSE, pcmk__valid_boolean,
+ pcmk__opt_schedulerd|pcmk__opt_generated,
+ N_("Whether watchdog integration is enabled"),
+ N_("This is set automatically by the cluster according to whether SBD "
+ "is detected to be in use. User-configured values are ignored. "
+ "The value `true` is meaningful if diskless SBD is used and "
+ "`stonith-watchdog-timeout` is nonzero. In that case, if fencing "
+ "is required, watchdog-based self-fencing will be performed via "
+ "SBD without requiring a fencing resource explicitly configured."),
+ },
+ {
+ /* @COMPAT Currently, unparsable values default to -1 (auto-calculate),
+ * while missing values default to 0 (disable). All values are accepted
+ * (unless the controller finds that the value conflicts with the
+ * SBD_WATCHDOG_TIMEOUT).
+ *
+ * At a compatibility break: properly validate as a timeout, let
+ * either negative values or a particular string like "auto" mean auto-
+ * calculate, and use 0 as the single default for when the option either
+ * is unset or fails to validate.
+ */
+ PCMK_OPT_STONITH_WATCHDOG_TIMEOUT, NULL, PCMK_VALUE_TIMEOUT, NULL,
+ "0", NULL,
+ pcmk__opt_controld,
+ N_("How long before nodes can be assumed to be safely down when "
+ "watchdog-based self-fencing via SBD is in use"),
+ N_("If this is set to a positive value, lost nodes are assumed to "
+ "achieve self-fencing using watchdog-based SBD within this much "
+ "time. This does not require a fencing resource to be explicitly "
+ "configured, though a fence_watchdog resource can be configured, to "
+ "limit use to specific nodes. If this is set to 0 (the default), "
+ "the cluster will never assume watchdog-based self-fencing. If this "
+ "is set to a negative value, the cluster will use twice the local "
+ "value of the `SBD_WATCHDOG_TIMEOUT` environment variable if that "
+ "is positive, or otherwise treat this as 0. WARNING: When used, "
+ "this timeout must be larger than `SBD_WATCHDOG_TIMEOUT` on all "
+ "nodes that use watchdog-based SBD, and Pacemaker will refuse to "
+ "start on any of those nodes where this is not true for the local "
+ "value or SBD is not active. When this is set to a negative value, "
+ "`SBD_WATCHDOG_TIMEOUT` must be set to the same value on all nodes "
+ "that use SBD, otherwise data corruption or loss could occur."),
+ },
+ {
+ PCMK_OPT_STONITH_MAX_ATTEMPTS, NULL, PCMK_VALUE_SCORE, NULL,
+ "10", pcmk__valid_positive_int,
+ pcmk__opt_controld,
+ N_("How many times fencing can fail before it will no longer be "
+ "immediately re-attempted on a target"),
+ NULL,
+ },
+ {
+ PCMK_OPT_CONCURRENT_FENCING, NULL, PCMK_VALUE_BOOLEAN, NULL,
+ PCMK__CONCURRENT_FENCING_DEFAULT, pcmk__valid_boolean,
+ pcmk__opt_schedulerd,
+ N_("Allow performing fencing operations in parallel"),
+ NULL,
+ },
+ {
+ PCMK_OPT_STARTUP_FENCING, NULL, PCMK_VALUE_BOOLEAN, NULL,
+ PCMK_VALUE_TRUE, pcmk__valid_boolean,
+ pcmk__opt_schedulerd|pcmk__opt_advanced,
+ N_("Whether to fence unseen nodes at start-up"),
+ N_("Setting this to false may lead to a \"split-brain\" situation, "
+ "potentially leading to data loss and/or service unavailability."),
+ },
+ {
+ PCMK_OPT_PRIORITY_FENCING_DELAY, NULL, PCMK_VALUE_DURATION, NULL,
+ "0", pcmk__valid_interval_spec,
+ pcmk__opt_schedulerd,
+ N_("Apply fencing delay targeting the lost nodes with the highest "
+ "total resource priority"),
+ N_("Apply specified delay for the fencings that are targeting the lost "
+ "nodes with the highest total resource priority in case we don't "
+ "have the majority of the nodes in our cluster partition, so that "
+ "the more significant nodes potentially win any fencing match, "
+ "which is especially meaningful under split-brain of 2-node "
+ "cluster. A promoted resource instance takes the base priority + 1 "
+ "on calculation if the base priority is not 0. Any static/random "
+ "delays that are introduced by `pcmk_delay_base/max` configured "
+ "for the corresponding fencing resources will be added to this "
+ "delay. This delay should be significantly greater than, safely "
+ "twice, the maximum `pcmk_delay_base/max`. By default, priority "
+ "fencing delay is disabled."),
+ },
+ {
+ PCMK_OPT_NODE_PENDING_TIMEOUT, NULL, PCMK_VALUE_DURATION, NULL,
+ "0", pcmk__valid_interval_spec,
+ pcmk__opt_schedulerd,
+ N_("How long to wait for a node that has joined the cluster to join "
+ "the controller process group"),
+ N_("Fence nodes that do not join the controller process group within "
+ "this much time after joining the cluster, to allow the cluster "
+ "to continue managing resources. A value of 0 means never fence "
+ "pending nodes. Setting the value to 2h means fence nodes after "
+ "2 hours."),
+ },
+ {
+ PCMK_OPT_CLUSTER_DELAY, NULL, PCMK_VALUE_DURATION, NULL,
+ "60s", pcmk__valid_interval_spec,
+ pcmk__opt_schedulerd,
+ N_("Maximum time for node-to-node communication"),
+ N_("The node elected Designated Controller (DC) will consider an action "
+ "failed if it does not get a response from the node executing the "
+ "action within this time (after considering the action's own "
+ "timeout). The \"correct\" value will depend on the speed and "
+ "load of your network and cluster nodes.")
+ },
+
+ // Limits
+ {
+ PCMK_OPT_LOAD_THRESHOLD, NULL, PCMK_VALUE_PERCENTAGE, NULL,
+ "80%", pcmk__valid_percentage,
+ pcmk__opt_controld,
+ N_("Maximum amount of system load that should be used by cluster "
+ "nodes"),
+ N_("The cluster will slow down its recovery process when the amount of "
+ "system resources used (currently CPU) approaches this limit"),
+ },
+ {
+ PCMK_OPT_NODE_ACTION_LIMIT, NULL, PCMK_VALUE_INTEGER, NULL,
+ "0", pcmk__valid_int,
+ pcmk__opt_controld,
+ N_("Maximum number of jobs that can be scheduled per node (defaults to "
+ "2x cores)"),
+ NULL,
+ },
+ {
+ PCMK_OPT_BATCH_LIMIT, NULL, PCMK_VALUE_INTEGER, NULL,
+ "0", pcmk__valid_int,
+ pcmk__opt_schedulerd,
+ N_("Maximum number of jobs that the cluster may execute in parallel "
+ "across all nodes"),
+ N_("The \"correct\" value will depend on the speed and load of your "
+ "network and cluster nodes. If set to 0, the cluster will "
+ "impose a dynamically calculated limit when any node has a "
+ "high load."),
+ },
+ {
+ PCMK_OPT_MIGRATION_LIMIT, NULL, PCMK_VALUE_INTEGER, NULL,
+ "-1", pcmk__valid_int,
+ pcmk__opt_schedulerd,
+ N_("The number of live migration actions that the cluster is allowed "
+ "to execute in parallel on a node (-1 means no limit)"),
+ NULL,
+ },
+ {
+ /* @TODO This is actually ignored if not strictly positive. We should
+ * overhaul value types in Pacemaker Explained. There are lots of
+ * inaccurate ranges (assumptions of 32-bit width, "nonnegative" when
+ * positive is required, etc.).
+ *
+ * Maybe a single integer type with the allowed range specified would be
+ * better.
+ *
+ * Drop the PCMK_VALUE_NONNEGATIVE_INTEGER constant if we do this before
+ * a release.
+ */
+ PCMK_OPT_CLUSTER_IPC_LIMIT, NULL, PCMK_VALUE_NONNEGATIVE_INTEGER, NULL,
+ "500", pcmk__valid_positive_int,
+ pcmk__opt_based,
+ N_("Maximum IPC message backlog before disconnecting a cluster daemon"),
+ N_("Raise this if log has \"Evicting client\" messages for cluster "
+ "daemon PIDs (a good value is the number of resources in the "
+ "cluster multiplied by the number of nodes)."),
+ },
+
+ // Orphans and stopping
+ {
+ PCMK_OPT_STOP_ALL_RESOURCES, NULL, PCMK_VALUE_BOOLEAN, NULL,
+ PCMK_VALUE_FALSE, pcmk__valid_boolean,
+ pcmk__opt_schedulerd,
+ N_("Whether the cluster should stop all active resources"),
+ NULL,
+ },
+ {
+ PCMK_OPT_STOP_ORPHAN_RESOURCES, NULL, PCMK_VALUE_BOOLEAN, NULL,
+ PCMK_VALUE_TRUE, pcmk__valid_boolean,
+ pcmk__opt_schedulerd,
+ N_("Whether to stop resources that were removed from the "
+ "configuration"),
+ NULL,
+ },
+ {
+ PCMK_OPT_STOP_ORPHAN_ACTIONS, NULL, PCMK_VALUE_BOOLEAN, NULL,
+ PCMK_VALUE_TRUE, pcmk__valid_boolean,
+ pcmk__opt_schedulerd,
+ N_("Whether to cancel recurring actions removed from the "
+ "configuration"),
+ NULL,
+ },
+ {
+ PCMK__OPT_REMOVE_AFTER_STOP, NULL, PCMK_VALUE_BOOLEAN, NULL,
+ PCMK_VALUE_FALSE, pcmk__valid_boolean,
+ pcmk__opt_schedulerd|pcmk__opt_deprecated,
+ N_("Whether to remove stopped resources from the executor"),
+ N_("Values other than default are poorly tested and potentially "
+ "dangerous."),
+ },
+
+ // Storing inputs
+ {
+ PCMK_OPT_PE_ERROR_SERIES_MAX, NULL, PCMK_VALUE_INTEGER, NULL,
+ "-1", pcmk__valid_int,
+ pcmk__opt_schedulerd,
+ N_("The number of scheduler inputs resulting in errors to save"),
+ N_("Zero to disable, -1 to store unlimited."),
+ },
+ {
+ PCMK_OPT_PE_WARN_SERIES_MAX, NULL, PCMK_VALUE_INTEGER, NULL,
+ "5000", pcmk__valid_int,
+ pcmk__opt_schedulerd,
+ N_("The number of scheduler inputs resulting in warnings to save"),
+ N_("Zero to disable, -1 to store unlimited."),
+ },
+ {
+ PCMK_OPT_PE_INPUT_SERIES_MAX, NULL, PCMK_VALUE_INTEGER, NULL,
+ "4000", pcmk__valid_int,
+ pcmk__opt_schedulerd,
+ N_("The number of scheduler inputs without errors or warnings to save"),
+ N_("Zero to disable, -1 to store unlimited."),
+ },
+
+ // Node health
+ {
+ PCMK_OPT_NODE_HEALTH_STRATEGY, NULL, PCMK_VALUE_SELECT,
+ PCMK_VALUE_NONE ", " PCMK_VALUE_MIGRATE_ON_RED ", "
+ PCMK_VALUE_ONLY_GREEN ", " PCMK_VALUE_PROGRESSIVE ", "
+ PCMK_VALUE_CUSTOM,
+ PCMK_VALUE_NONE, pcmk__validate_health_strategy,
+ pcmk__opt_schedulerd,
+ N_("How cluster should react to node health attributes"),
+ N_("Requires external entities to create node attributes (named with "
+ "the prefix \"#health\") with values \"red\", \"yellow\", or "
+ "\"green\".")
+ },
+ {
+ PCMK_OPT_NODE_HEALTH_BASE, NULL, PCMK_VALUE_SCORE, NULL,
+ "0", pcmk__valid_int,
+ pcmk__opt_schedulerd,
+ N_("Base health score assigned to a node"),
+ N_("Only used when \"node-health-strategy\" is set to "
+ "\"progressive\"."),
+ },
+ {
+ PCMK_OPT_NODE_HEALTH_GREEN, NULL, PCMK_VALUE_SCORE, NULL,
+ "0", pcmk__valid_int,
+ pcmk__opt_schedulerd,
+ N_("The score to use for a node health attribute whose value is "
+ "\"green\""),
+ N_("Only used when \"node-health-strategy\" is set to \"custom\" or "
+ "\"progressive\"."),
+ },
+ {
+ PCMK_OPT_NODE_HEALTH_YELLOW, NULL, PCMK_VALUE_SCORE, NULL,
+ "0", pcmk__valid_int,
+ pcmk__opt_schedulerd,
+ N_("The score to use for a node health attribute whose value is "
+ "\"yellow\""),
+ N_("Only used when \"node-health-strategy\" is set to \"custom\" or "
+ "\"progressive\"."),
+ },
+ {
+ PCMK_OPT_NODE_HEALTH_RED, NULL, PCMK_VALUE_SCORE, NULL,
+ "-INFINITY", pcmk__valid_int,
+ pcmk__opt_schedulerd,
+ N_("The score to use for a node health attribute whose value is "
+ "\"red\""),
+ N_("Only used when \"node-health-strategy\" is set to \"custom\" or "
+ "\"progressive\".")
+ },
+
+ // Placement strategy
+ {
+ PCMK_OPT_PLACEMENT_STRATEGY, NULL, PCMK_VALUE_SELECT,
+ PCMK_VALUE_DEFAULT ", " PCMK_VALUE_UTILIZATION ", "
+ PCMK_VALUE_MINIMAL ", " PCMK_VALUE_BALANCED,
+ PCMK_VALUE_DEFAULT, pcmk__valid_placement_strategy,
+ pcmk__opt_schedulerd,
+ N_("How the cluster should allocate resources to nodes"),
+ NULL,
+ },
+
+ { NULL, },
+};
+
+static const pcmk__cluster_option_t fencing_params[] = {
+ /* name, old name, type, allowed values,
+ * default value, validator,
+ * flags,
+ * short description,
+ * long description
+ */
+ {
+ PCMK_STONITH_HOST_ARGUMENT, NULL, PCMK_VALUE_STRING, NULL,
+ "port", NULL,
+ pcmk__opt_advanced,
+ N_("An alternate parameter to supply instead of 'port'"),
+ N_("Some devices do not support the standard 'port' parameter or may "
+ "provide additional ones. Use this to specify an alternate, device-"
+ "specific, parameter that should indicate the machine to be "
+ "fenced. A value of \"none\" can be used to tell the cluster not "
+ "to supply any additional parameters."),
+ },
+ {
+ PCMK_STONITH_HOST_MAP, NULL, PCMK_VALUE_STRING, NULL,
+ NULL, NULL,
+ pcmk__opt_none,
+ N_("A mapping of node names to port numbers for devices that do not "
+ "support node names."),
+ N_("For example, \"node1:1;node2:2,3\" would tell the cluster to use "
+ "port 1 for node1 and ports 2 and 3 for node2."),
+ },
+ {
+ PCMK_STONITH_HOST_LIST, NULL, PCMK_VALUE_STRING, NULL,
+ NULL, NULL,
+ pcmk__opt_none,
+ N_("Nodes targeted by this device"),
+ N_("Comma-separated list of nodes that can be targeted by this device "
+ "(for example, \"node1,node2,node3\"). If pcmk_host_check is "
+ "\"static-list\", either this or pcmk_host_map must be set."),
+ },
+ {
+ PCMK_STONITH_HOST_CHECK, NULL, PCMK_VALUE_SELECT,
+ PCMK_VALUE_DYNAMIC_LIST ", " PCMK_VALUE_STATIC_LIST ", "
+ PCMK_VALUE_STATUS ", " PCMK_VALUE_NONE,
+ NULL, NULL,
+ pcmk__opt_none,
+ N_("How to determine which nodes can be targeted by the device"),
+ N_("Use \"dynamic-list\" to query the device via the 'list' command; "
+ "\"static-list\" to check the pcmk_host_list attribute; "
+ "\"status\" to query the device via the 'status' command; or "
+ "\"none\" to assume every device can fence every node. "
+ "The default value is \"static-list\" if pcmk_host_map or "
+ "pcmk_host_list is set; otherwise \"dynamic-list\" if the device "
+ "supports the list operation; otherwise \"status\" if the device "
+ "supports the status operation; otherwise \"none\""),
+ },
+ {
+ PCMK_STONITH_DELAY_MAX, NULL, PCMK_VALUE_DURATION, NULL,
+ "0s", NULL,
+ pcmk__opt_none,
+ N_("Enable a delay of no more than the time specified before executing "
+ "fencing actions."),
+ N_("Enable a delay of no more than the time specified before executing "
+ "fencing actions. Pacemaker derives the overall delay by taking "
+ "the value of pcmk_delay_base and adding a random delay value such "
+ "that the sum is kept below this maximum."),
+ },
+ {
+ PCMK_STONITH_DELAY_BASE, NULL, PCMK_VALUE_STRING, NULL,
+ "0s", NULL,
+ pcmk__opt_none,
+ N_("Enable a base delay for fencing actions and specify base delay "
+ "value."),
+ N_("This enables a static delay for fencing actions, which can help "
+ "avoid \"death matches\" where two nodes try to fence each other "
+ "at the same time. If pcmk_delay_max is also used, a random delay "
+ "will be added such that the total delay is kept below that value. "
+ "This can be set to a single time value to apply to any node "
+ "targeted by this device (useful if a separate device is "
+ "configured for each target), or to a node map (for example, "
+ "\"node1:1s;node2:5\") to set a different value for each target."),
+ },
+ {
+ PCMK_STONITH_ACTION_LIMIT, NULL, PCMK_VALUE_INTEGER, NULL,
+ "1", NULL,
+ pcmk__opt_none,
+ N_("The maximum number of actions can be performed in parallel on this "
+ "device"),
+ N_("Cluster property concurrent-fencing=\"true\" needs to be "
+ "configured first. Then use this to specify the maximum number of "
+ "actions can be performed in parallel on this device. A value of "
+ "-1 means an unlimited number of actions can be performed in "
+ "parallel."),
+ },
+ {
+ "pcmk_reboot_action", NULL, PCMK_VALUE_STRING, NULL,
+ PCMK_ACTION_REBOOT, NULL,
+ pcmk__opt_advanced,
+ N_("An alternate command to run instead of 'reboot'"),
+ N_("Some devices do not support the standard commands or may provide "
+ "additional ones. Use this to specify an alternate, device-"
+ "specific, command that implements the 'reboot' action."),
+ },
+ {
+ "pcmk_reboot_timeout", NULL, PCMK_VALUE_TIMEOUT, NULL,
+ "60s", NULL,
+ pcmk__opt_advanced,
+ N_("Specify an alternate timeout to use for 'reboot' actions instead "
+ "of stonith-timeout"),
+ N_("Some devices need much more/less time to complete than normal. "
+ "Use this to specify an alternate, device-specific, timeout for "
+ "'reboot' actions."),
+ },
+ {
+ "pcmk_reboot_retries", NULL, PCMK_VALUE_INTEGER, NULL,
+ "2", NULL,
+ pcmk__opt_advanced,
+ N_("The maximum number of times to try the 'reboot' command within the "
+ "timeout period"),
+ N_("Some devices do not support multiple connections. Operations may "
+ "\"fail\" if the device is busy with another task. In that case, "
+ "Pacemaker will automatically retry the operation if there is time "
+ "remaining. Use this option to alter the number of times Pacemaker "
+ "tries a 'reboot' action before giving up."),
+ },
+ {
+ "pcmk_off_action", NULL, PCMK_VALUE_STRING, NULL,
+ PCMK_ACTION_OFF, NULL,
+ pcmk__opt_advanced,
+ N_("An alternate command to run instead of 'off'"),
+ N_("Some devices do not support the standard commands or may provide "
+ "additional ones. Use this to specify an alternate, device-"
+ "specific, command that implements the 'off' action."),
+ },
+ {
+ "pcmk_off_timeout", NULL, PCMK_VALUE_TIMEOUT, NULL,
+ "60s", NULL,
+ pcmk__opt_advanced,
+ N_("Specify an alternate timeout to use for 'off' actions instead of "
+ "stonith-timeout"),
+ N_("Some devices need much more/less time to complete than normal. "
+ "Use this to specify an alternate, device-specific, timeout for "
+ "'off' actions."),
+ },
+ {
+ "pcmk_off_retries", NULL, PCMK_VALUE_INTEGER, NULL,
+ "2", NULL,
+ pcmk__opt_advanced,
+ N_("The maximum number of times to try the 'off' command within the "
+ "timeout period"),
+ N_("Some devices do not support multiple connections. Operations may "
+ "\"fail\" if the device is busy with another task. In that case, "
+ "Pacemaker will automatically retry the operation if there is time "
+ "remaining. Use this option to alter the number of times Pacemaker "
+ "tries a 'off' action before giving up."),
+ },
+ {
+ "pcmk_on_action", NULL, PCMK_VALUE_STRING, NULL,
+ PCMK_ACTION_ON, NULL,
+ pcmk__opt_advanced,
+ N_("An alternate command to run instead of 'on'"),
+ N_("Some devices do not support the standard commands or may provide "
+ "additional ones. Use this to specify an alternate, device-"
+ "specific, command that implements the 'on' action."),
+ },
+ {
+ "pcmk_on_timeout", NULL, PCMK_VALUE_TIMEOUT, NULL,
+ "60s", NULL,
+ pcmk__opt_advanced,
+ N_("Specify an alternate timeout to use for 'on' actions instead of "
+ "stonith-timeout"),
+ N_("Some devices need much more/less time to complete than normal. "
+ "Use this to specify an alternate, device-specific, timeout for "
+ "'on' actions."),
+ },
+ {
+ "pcmk_on_retries", NULL, PCMK_VALUE_INTEGER, NULL,
+ "2", NULL,
+ pcmk__opt_advanced,
+ N_("The maximum number of times to try the 'on' command within the "
+ "timeout period"),
+ N_("Some devices do not support multiple connections. Operations may "
+ "\"fail\" if the device is busy with another task. In that case, "
+ "Pacemaker will automatically retry the operation if there is time "
+ "remaining. Use this option to alter the number of times Pacemaker "
+ "tries a 'on' action before giving up."),
+ },
+ {
+ "pcmk_list_action", NULL, PCMK_VALUE_STRING, NULL,
+ PCMK_ACTION_LIST, NULL,
+ pcmk__opt_advanced,
+ N_("An alternate command to run instead of 'list'"),
+ N_("Some devices do not support the standard commands or may provide "
+ "additional ones. Use this to specify an alternate, device-"
+ "specific, command that implements the 'list' action."),
+ },
+ {
+ "pcmk_list_timeout", NULL, PCMK_VALUE_TIMEOUT, NULL,
+ "60s", NULL,
+ pcmk__opt_advanced,
+ N_("Specify an alternate timeout to use for 'list' actions instead of "
+ "stonith-timeout"),
+ N_("Some devices need much more/less time to complete than normal. "
+ "Use this to specify an alternate, device-specific, timeout for "
+ "'list' actions."),
+ },
+ {
+ "pcmk_list_retries", NULL, PCMK_VALUE_INTEGER, NULL,
+ "2", NULL,
+ pcmk__opt_advanced,
+ N_("The maximum number of times to try the 'list' command within the "
+ "timeout period"),
+ N_("Some devices do not support multiple connections. Operations may "
+ "\"fail\" if the device is busy with another task. In that case, "
+ "Pacemaker will automatically retry the operation if there is time "
+ "remaining. Use this option to alter the number of times Pacemaker "
+ "tries a 'list' action before giving up."),
+ },
+ {
+ "pcmk_monitor_action", NULL, PCMK_VALUE_STRING, NULL,
+ PCMK_ACTION_MONITOR, NULL,
+ pcmk__opt_advanced,
+ N_("An alternate command to run instead of 'monitor'"),
+ N_("Some devices do not support the standard commands or may provide "
+ "additional ones. Use this to specify an alternate, device-"
+ "specific, command that implements the 'monitor' action."),
+ },
+ {
+ "pcmk_monitor_timeout", NULL, PCMK_VALUE_TIMEOUT, NULL,
+ "60s", NULL,
+ pcmk__opt_advanced,
+ N_("Specify an alternate timeout to use for 'monitor' actions instead "
+ "of stonith-timeout"),
+ N_("Some devices need much more/less time to complete than normal. "
+ "Use this to specify an alternate, device-specific, timeout for "
+ "'monitor' actions."),
+ },
+ {
+ "pcmk_monitor_retries", NULL, PCMK_VALUE_INTEGER, NULL,
+ "2", NULL,
+ pcmk__opt_advanced,
+ N_("The maximum number of times to try the 'monitor' command within "
+ "the timeout period"),
+ N_("Some devices do not support multiple connections. Operations may "
+ "\"fail\" if the device is busy with another task. In that case, "
+ "Pacemaker will automatically retry the operation if there is time "
+ "remaining. Use this option to alter the number of times Pacemaker "
+ "tries a 'monitor' action before giving up."),
+ },
+ {
+ "pcmk_status_action", NULL, PCMK_VALUE_STRING, NULL,
+ PCMK_ACTION_STATUS, NULL,
+ pcmk__opt_advanced,
+ N_("An alternate command to run instead of 'status'"),
+ N_("Some devices do not support the standard commands or may provide "
+ "additional ones. Use this to specify an alternate, device-"
+ "specific, command that implements the 'status' action."),
+ },
+ {
+ "pcmk_status_timeout", NULL, PCMK_VALUE_TIMEOUT, NULL,
+ "60s", NULL,
+ pcmk__opt_advanced,
+ N_("Specify an alternate timeout to use for 'status' actions instead "
+ "of stonith-timeout"),
+ N_("Some devices need much more/less time to complete than normal. "
+ "Use this to specify an alternate, device-specific, timeout for "
+ "'status' actions."),
+ },
+ {
+ "pcmk_status_retries", NULL, PCMK_VALUE_INTEGER, NULL,
+ "2", NULL,
+ pcmk__opt_advanced,
+ N_("The maximum number of times to try the 'status' command within "
+ "the timeout period"),
+ N_("Some devices do not support multiple connections. Operations may "
+ "\"fail\" if the device is busy with another task. In that case, "
+ "Pacemaker will automatically retry the operation if there is time "
+ "remaining. Use this option to alter the number of times Pacemaker "
+ "tries a 'status' action before giving up."),
+ },
+
+ { NULL, },
+};
+
+static const pcmk__cluster_option_t primitive_meta[] = {
+ /* name, old name, type, allowed values,
+ * default value, validator,
+ * flags,
+ * short description,
+ * long description
+ */
+ {
+ PCMK_META_PRIORITY, NULL, PCMK_VALUE_SCORE, NULL,
+ "0", NULL,
+ pcmk__opt_none,
+ N_("Resource assignment priority"),
+ N_("If not all resources can be active, the cluster will stop "
+ "lower-priority resources in order to keep higher-priority ones "
+ "active."),
+ },
+ {
+ PCMK_META_CRITICAL, NULL, PCMK_VALUE_BOOLEAN, NULL,
+ PCMK_VALUE_TRUE, NULL,
+ pcmk__opt_none,
+ N_("Default value for influence in colocation constraints"),
+ N_("Use this value as the default for influence in all colocation "
+ "constraints involving this resource, as well as in the implicit "
+ "colocation constraints created if this resource is in a group."),
+ },
+ {
+ PCMK_META_TARGET_ROLE, NULL, PCMK_VALUE_SELECT,
+ PCMK_ROLE_STOPPED ", " PCMK_ROLE_STARTED ", "
+ PCMK_ROLE_UNPROMOTED ", " PCMK_ROLE_PROMOTED,
+ PCMK_ROLE_STARTED, NULL,
+ pcmk__opt_none,
+ N_("State the cluster should attempt to keep this resource in"),
+ N_("\"Stopped\" forces the resource to be stopped. "
+ "\"Started\" allows the resource to be started (and in the case of "
+ "promotable clone resources, promoted if appropriate). "
+ "\"Unpromoted\" allows the resource to be started, but only in the "
+ "unpromoted role if the resource is promotable. "
+ "\"Promoted\" is equivalent to \"Started\"."),
+ },
+ {
+ PCMK_META_IS_MANAGED, NULL, PCMK_VALUE_BOOLEAN, NULL,
+ PCMK_VALUE_TRUE, NULL,
+ pcmk__opt_none,
+ N_("Whether the cluster is allowed to actively change the resource's "
+ "state"),
+ N_("If false, the cluster will not start, stop, promote, or demote the "
+ "resource on any node. Recurring actions for the resource are "
+ "unaffected. If true, a true value for the maintenance-mode "
+ "cluster option, the maintenance node attribute, or the "
+ "maintenance resource meta-attribute overrides this."),
+ },
+ {
+ PCMK_META_MAINTENANCE, NULL, PCMK_VALUE_BOOLEAN, NULL,
+ PCMK_VALUE_FALSE, NULL,
+ pcmk__opt_none,
+ N_("If true, the cluster will not schedule any actions involving the "
+ "resource"),
+ N_("If true, the cluster will not start, stop, promote, or demote the "
+ "resource on any node, and will pause any recurring monitors "
+ "(except those specifying role as \"Stopped\"). If false, a true "
+ "value for the maintenance-mode cluster option or maintenance node "
+ "attribute overrides this."),
+ },
+ {
+ PCMK_META_RESOURCE_STICKINESS, NULL, PCMK_VALUE_SCORE, NULL,
+ NULL, NULL,
+ pcmk__opt_none,
+ N_("Score to add to the current node when a resource is already "
+ "active"),
+ N_("Score to add to the current node when a resource is already "
+ "active. This allows running resources to stay where they are, "
+ "even if they would be placed elsewhere if they were being started "
+ "from a stopped state. "
+ "The default is 1 for individual clone instances, and 0 for all "
+ "other resources."),
+ },
+ {
+ PCMK_META_REQUIRES, NULL, PCMK_VALUE_SELECT,
+ PCMK_VALUE_NOTHING ", " PCMK_VALUE_QUORUM ", "
+ PCMK_VALUE_FENCING ", " PCMK_VALUE_UNFENCING,
+ NULL, NULL,
+ pcmk__opt_none,
+ N_("Conditions under which the resource can be started"),
+ N_("Conditions under which the resource can be started. "
+ "\"nothing\" means the cluster can always start this resource. "
+ "\"quorum\" means the cluster can start this resource only if a "
+ "majority of the configured nodes are active. "
+ "\"fencing\" means the cluster can start this resource only if a "
+ "majority of the configured nodes are active and any failed or "
+ "unknown nodes have been fenced. "
+ "\"unfencing\" means the cluster can start this resource only if "
+ "a majority of the configured nodes are active and any failed or "
+ "unknown nodes have been fenced, and only on nodes that have been "
+ "unfenced. "
+ "The default is \"quorum\" for resources with a class of stonith; "
+ "otherwise, \"unfencing\" if unfencing is active in the cluster; "
+ "otherwise, \"fencing\" if the stonith-enabled cluster option is "
+ "true; "
+ "otherwise, \"quorum\"."),
+ },
+ {
+ PCMK_META_MIGRATION_THRESHOLD, NULL, PCMK_VALUE_SCORE, NULL,
+ PCMK_VALUE_INFINITY, NULL,
+ pcmk__opt_none,
+ N_("Number of failures on a node before the resource becomes "
+ "ineligible to run there."),
+ N_("Number of failures that may occur for this resource on a node, "
+ "before that node is marked ineligible to host this resource. A "
+ "value of 0 indicates that this feature is disabled (the node will "
+ "never be marked ineligible). By contrast, the cluster treats "
+ "\"INFINITY\" (the default) as a very large but finite number. "
+ "This option has an effect only if the failed operation specifies "
+ "its on-fail attribute as \"restart\" (the default), and "
+ "additionally for failed start operations, if the "
+ "start-failure-is-fatal cluster property is set to false."),
+ },
+ {
+ PCMK_META_FAILURE_TIMEOUT, NULL, PCMK_VALUE_DURATION, NULL,
+ "0", NULL,
+ pcmk__opt_none,
+ N_("Number of seconds before acting as if a failure had not occurred"),
+ N_("Number of seconds after a failed action for this resource before "
+ "acting as if the failure had not occurred, and potentially "
+ "allowing the resource back to the node on which it failed. "
+ "A value of 0 indicates that this feature is disabled."),
+ },
+ {
+ PCMK_META_MULTIPLE_ACTIVE, NULL, PCMK_VALUE_SELECT,
+ PCMK_VALUE_BLOCK ", " PCMK_VALUE_STOP_ONLY ", "
+ PCMK_VALUE_STOP_START ", " PCMK_VALUE_STOP_UNEXPECTED,
+ PCMK_VALUE_STOP_START, NULL,
+ pcmk__opt_none,
+ N_("What to do if the cluster finds the resource active on more than "
+ "one node"),
+ N_("What to do if the cluster finds the resource active on more than "
+ "one node. "
+ "\"block\" means to mark the resource as unmanaged. "
+ "\"stop_only\" means to stop all active instances of this resource "
+ "and leave them stopped. "
+ "\"stop_start\" means to stop all active instances of this "
+ "resource and start the resource in one location only. "
+ "\"stop_unexpected\" means to stop all active instances of this "
+ "resource except where the resource should be active. (This should "
+ "be used only when extra instances are not expected to disrupt "
+ "existing instances, and the resource agent's monitor of an "
+ "existing instance is capable of detecting any problems that could "
+ "be caused. Note that any resources ordered after this one will "
+ "still need to be restarted.)"),
+ },
+ {
+ PCMK_META_ALLOW_MIGRATE, NULL, PCMK_VALUE_BOOLEAN, NULL,
+ NULL, NULL,
+ pcmk__opt_none,
+ N_("Whether the cluster should try to \"live migrate\" this resource "
+ "when it needs to be moved"),
+ N_("Whether the cluster should try to \"live migrate\" this resource "
+ "when it needs to be moved. "
+ "The default is true for ocf:pacemaker:remote resources, and false "
+ "otherwise."),
+ },
+ {
+ PCMK_META_ALLOW_UNHEALTHY_NODES, NULL, PCMK_VALUE_BOOLEAN, NULL,
+ PCMK_VALUE_FALSE, NULL,
+ pcmk__opt_none,
+ N_("Whether the resource should be allowed to run on a node even if "
+ "the node's health score would otherwise prevent it"),
+ NULL,
+ },
+ {
+ PCMK_META_CONTAINER_ATTRIBUTE_TARGET, NULL, PCMK_VALUE_STRING, NULL,
+ NULL, NULL,
+ pcmk__opt_none,
+ N_("Where to check user-defined node attributes"),
+ N_("Whether to check user-defined node attributes on the physical host "
+ "where a container is running or on the local node. This is "
+ "usually set for a bundle resource and inherited by the bundle's "
+ "primitive resource. "
+ "A value of \"host\" means to check user-defined node attributes "
+ "on the underlying physical host. Any other value means to check "
+ "user-defined node attributes on the local node (for a bundled "
+ "primitive resource, this is the bundle node)."),
+ },
+ {
+ PCMK_META_REMOTE_NODE, NULL, PCMK_VALUE_STRING, NULL,
+ NULL, NULL,
+ pcmk__opt_none,
+ N_("Name of the Pacemaker Remote guest node this resource is "
+ "associated with, if any"),
+ N_("Name of the Pacemaker Remote guest node this resource is "
+ "associated with, if any. If specified, this both enables the "
+ "resource as a guest node and defines the unique name used to "
+ "identify the guest node. The guest must be configured to run the "
+ "Pacemaker Remote daemon when it is started. "
+ "WARNING: This value cannot overlap with any resource or node "
+ "IDs."),
+ },
+ {
+ PCMK_META_REMOTE_ADDR, NULL, PCMK_VALUE_STRING, NULL,
+ NULL, NULL,
+ pcmk__opt_none,
+ N_("If remote-node is specified, the IP address or hostname used to "
+ "connect to the guest via Pacemaker Remote"),
+ N_("If remote-node is specified, the IP address or hostname used to "
+ "connect to the guest via Pacemaker Remote. The Pacemaker Remote "
+ "daemon on the guest must be configured to accept connections on "
+ "this address. "
+ "The default is the value of the remote-node meta-attribute."),
+ },
+ {
+ PCMK_META_REMOTE_PORT, NULL, PCMK_VALUE_PORT, NULL,
+ "3121", NULL,
+ pcmk__opt_none,
+ N_("If remote-node is specified, port on the guest used for its "
+ "Pacemaker Remote connection"),
+ N_("If remote-node is specified, the port on the guest used for its "
+ "Pacemaker Remote connection. The Pacemaker Remote daemon on the "
+ "guest must be configured to listen on this port."),
+ },
+ {
+ PCMK_META_REMOTE_CONNECT_TIMEOUT, NULL, PCMK_VALUE_TIMEOUT, NULL,
+ "60s", NULL,
+ pcmk__opt_none,
+ N_("If remote-node is specified, how long before a pending Pacemaker "
+ "Remote guest connection times out."),
+ NULL,
+ },
+ {
+ PCMK_META_REMOTE_ALLOW_MIGRATE, NULL, PCMK_VALUE_BOOLEAN, NULL,
+ PCMK_VALUE_TRUE, NULL,
+ pcmk__opt_none,
+ N_("If remote-node is specified, this acts as the allow-migrate "
+ "meta-attribute for the implicit remote connection resource "
+ "(ocf:pacemaker:remote)."),
+ NULL,
+ },
+
+ { NULL, },
+};
+
+/*
* Environment variable option handling
*/
@@ -176,161 +1206,195 @@ pcmk__env_option_enabled(const char *daemon, const char *option)
* Cluster option handling
*/
+/*!
+ * \internal
+ * \brief Check whether a string represents a valid interval specification
+ *
+ * \param[in] value String to validate
+ *
+ * \return \c true if \p value is a valid interval specification, or \c false
+ * otherwise
+ */
bool
pcmk__valid_interval_spec(const char *value)
{
- (void) crm_parse_interval_spec(value);
- return errno == 0;
+ return pcmk_parse_interval_spec(value, NULL) == pcmk_rc_ok;
}
+/*!
+ * \internal
+ * \brief Check whether a string represents a valid boolean value
+ *
+ * \param[in] value String to validate
+ *
+ * \return \c true if \p value is a valid boolean value, or \c false otherwise
+ */
bool
pcmk__valid_boolean(const char *value)
{
- int tmp;
-
- return crm_str_to_boolean(value, &tmp) == 1;
+ return crm_str_to_boolean(value, NULL) == 1;
}
+/*!
+ * \internal
+ * \brief Check whether a string represents a valid integer
+ *
+ * Valid values include \c INFINITY, \c -INFINITY, and all 64-bit integers.
+ *
+ * \param[in] value String to validate
+ *
+ * \return \c true if \p value is a valid integer, or \c false otherwise
+ */
bool
-pcmk__valid_number(const char *value)
+pcmk__valid_int(const char *value)
{
- if (value == NULL) {
- return false;
-
- } else if (pcmk_str_is_minus_infinity(value) ||
- pcmk_str_is_infinity(value)) {
- return true;
- }
-
- return pcmk__scan_ll(value, NULL, 0LL) == pcmk_rc_ok;
+ return (value != NULL)
+ && (pcmk_str_is_infinity(value)
+ || pcmk_str_is_minus_infinity(value)
+ || (pcmk__scan_ll(value, NULL, 0LL) == pcmk_rc_ok));
}
+/*!
+ * \internal
+ * \brief Check whether a string represents a valid positive integer
+ *
+ * Valid values include \c INFINITY and all 64-bit positive integers.
+ *
+ * \param[in] value String to validate
+ *
+ * \return \c true if \p value is a valid positive integer, or \c false
+ * otherwise
+ */
bool
-pcmk__valid_positive_number(const char *value)
+pcmk__valid_positive_int(const char *value)
{
long long num = 0LL;
return pcmk_str_is_infinity(value)
- || ((pcmk__scan_ll(value, &num, 0LL) == pcmk_rc_ok) && (num > 0));
+ || ((pcmk__scan_ll(value, &num, 0LL) == pcmk_rc_ok)
+ && (num > 0));
}
+/*!
+ * \internal
+ * \brief Check whether a string represents a valid
+ * \c PCMK__OPT_NO_QUORUM_POLICY value
+ *
+ * \param[in] value String to validate
+ *
+ * \return \c true if \p value is a valid \c PCMK__OPT_NO_QUORUM_POLICY value,
+ * or \c false otherwise
+ */
bool
-pcmk__valid_quorum(const char *value)
+pcmk__valid_no_quorum_policy(const char *value)
{
- return pcmk__strcase_any_of(value, "stop", "freeze", "ignore", "demote", "suicide", NULL);
+ return pcmk__strcase_any_of(value,
+ PCMK_VALUE_STOP, PCMK_VALUE_FREEZE,
+ PCMK_VALUE_IGNORE, PCMK_VALUE_DEMOTE,
+ PCMK_VALUE_FENCE_LEGACY, NULL);
}
+/*!
+ * \internal
+ * \brief Check whether a string represents a valid percentage
+ *
+ * Valid values include long integers, with an optional trailing string
+ * beginning with '%'.
+ *
+ * \param[in] value String to validate
+ *
+ * \return \c true if \p value is a valid percentage value, or \c false
+ * otherwise
+ */
bool
-pcmk__valid_script(const char *value)
+pcmk__valid_percentage(const char *value)
{
- struct stat st;
-
- if (pcmk__str_eq(value, "/dev/null", pcmk__str_casei)) {
- return true;
- }
-
- if (stat(value, &st) != 0) {
- crm_err("Script %s does not exist", value);
- return false;
- }
-
- if (S_ISREG(st.st_mode) == 0) {
- crm_err("Script %s is not a regular file", value);
- return false;
- }
-
- if ((st.st_mode & (S_IXUSR | S_IXGRP)) == 0) {
- crm_err("Script %s is not executable", value);
- return false;
- }
+ char *end = NULL;
+ float number = strtof(value, &end);
- return true;
+ return ((end == NULL) || (end[0] == '%')) && (number >= 0);
}
+/*!
+ * \internal
+ * \brief Check whether a string represents a valid placement strategy
+ *
+ * \param[in] value String to validate
+ *
+ * \return \c true if \p value is a valid placement strategy, or \c false
+ * otherwise
+ */
bool
-pcmk__valid_percentage(const char *value)
+pcmk__valid_placement_strategy(const char *value)
{
- char *end = NULL;
- long number = strtol(value, &end, 10);
-
- if (end && (end[0] != '%')) {
- return false;
- }
- return number >= 0;
+ return pcmk__strcase_any_of(value,
+ PCMK_VALUE_DEFAULT, PCMK_VALUE_UTILIZATION,
+ PCMK_VALUE_MINIMAL, PCMK_VALUE_BALANCED, NULL);
}
/*!
* \internal
* \brief Check a table of configured options for a particular option
*
- * \param[in,out] options Name/value pairs for configured options
- * \param[in] validate If not NULL, validator function for option value
- * \param[in] name Option name to look for
- * \param[in] old_name Alternative option name to look for
- * \param[in] def_value Default to use if option not configured
+ * \param[in,out] table Name/value pairs for configured options
+ * \param[in] option Option to look up
*
* \return Option value (from supplied options table or default value)
*/
static const char *
-cluster_option_value(GHashTable *options, bool (*validate)(const char *),
- const char *name, const char *old_name,
- const char *def_value)
+cluster_option_value(GHashTable *table, const pcmk__cluster_option_t *option)
{
const char *value = NULL;
- char *new_value = NULL;
- CRM_ASSERT(name != NULL);
+ CRM_ASSERT((option != NULL) && (option->name != NULL));
- if (options) {
- value = g_hash_table_lookup(options, name);
+ if (table != NULL) {
+ value = g_hash_table_lookup(table, option->name);
- if ((value == NULL) && old_name) {
- value = g_hash_table_lookup(options, old_name);
+ if ((value == NULL) && (option->alt_name != NULL)) {
+ value = g_hash_table_lookup(table, option->alt_name);
if (value != NULL) {
pcmk__config_warn("Support for legacy name '%s' for cluster "
"option '%s' is deprecated and will be "
"removed in a future release",
- old_name, name);
+ option->alt_name, option->name);
// Inserting copy with current name ensures we only warn once
- new_value = strdup(value);
- g_hash_table_insert(options, strdup(name), new_value);
- value = new_value;
+ pcmk__insert_dup(table, option->name, value);
}
}
- if (value && validate && (validate(value) == FALSE)) {
+ if ((value != NULL) && (option->is_valid != NULL)
+ && !option->is_valid(value)) {
+
pcmk__config_err("Using default value for cluster option '%s' "
- "because '%s' is invalid", name, value);
+ "because '%s' is invalid", option->name, value);
value = NULL;
}
- if (value) {
+ if (value != NULL) {
return value;
}
}
// No value found, use default
- value = def_value;
+ value = option->default_value;
if (value == NULL) {
crm_trace("No value or default provided for cluster option '%s'",
- name);
+ option->name);
return NULL;
}
- if (validate) {
- CRM_CHECK(validate(value) != FALSE,
- crm_err("Bug: default value for cluster option '%s' is invalid", name);
- return NULL);
- }
+ CRM_CHECK((option->is_valid == NULL) || option->is_valid(value),
+ crm_err("Bug: default value for cluster option '%s' is invalid",
+ option->name);
+ return NULL);
crm_trace("Using default value '%s' for cluster option '%s'",
- value, name);
- if (options) {
- new_value = strdup(value);
- g_hash_table_insert(options, strdup(name), new_value);
- value = new_value;
+ value, option->name);
+ if (table != NULL) {
+ pcmk__insert_dup(table, option->name, value);
}
return value;
}
@@ -339,27 +1403,19 @@ cluster_option_value(GHashTable *options, bool (*validate)(const char *),
* \internal
* \brief Get the value of a cluster option
*
- * \param[in,out] options Name/value pairs for configured options
- * \param[in] option_list Possible cluster options
- * \param[in] len Length of \p option_list
- * \param[in] name (Primary) option name to look for
+ * \param[in,out] options Name/value pairs for configured options
+ * \param[in] name (Primary) option name to look for
*
* \return Option value
*/
const char *
-pcmk__cluster_option(GHashTable *options,
- const pcmk__cluster_option_t *option_list,
- int len, const char *name)
+pcmk__cluster_option(GHashTable *options, const char *name)
{
- const char *value = NULL;
+ for (const pcmk__cluster_option_t *option = cluster_options;
+ option->name != NULL; option++) {
- for (int lpc = 0; lpc < len; lpc++) {
- if (pcmk__str_eq(name, option_list[lpc].name, pcmk__str_casei)) {
- value = cluster_option_value(options, option_list[lpc].is_valid,
- option_list[lpc].name,
- option_list[lpc].alt_name,
- option_list[lpc].default_value);
- return value;
+ if (pcmk__str_eq(name, option->name, pcmk__str_casei)) {
+ return cluster_option_value(options, option);
}
}
CRM_CHECK(FALSE, crm_err("Bug: looking for unknown option '%s'", name));
@@ -368,143 +1424,142 @@ pcmk__cluster_option(GHashTable *options,
/*!
* \internal
- * \brief Add a description element to a meta-data string
- *
- * \param[in,out] s Meta-data string to add to
- * \param[in] tag Name of element to add ("longdesc" or "shortdesc")
- * \param[in] desc Textual description to add
- * \param[in] values If not \p NULL, the allowed values for the parameter
- * \param[in] spaces If not \p NULL, spaces to insert at the beginning of
- * each line
+ * \brief Output cluster option metadata as OCF-like XML
+ *
+ * \param[in,out] out Output object
+ * \param[in] name Fake resource agent name for the option list
+ * \param[in] desc_short Short description of the option list
+ * \param[in] desc_long Long description of the option list
+ * \param[in] filter Group of <tt>enum pcmk__opt_flags</tt>; output an
+ * option only if its \c flags member has all these
+ * flags set
+ * \param[in] all If \c true, output all options; otherwise, exclude
+ * advanced and deprecated options unless
+ * \c pcmk__opt_advanced and \c pcmk__opt_deprecated
+ * flags (respectively) are set in \p filter. This is
+ * always treated as true for XML output objects.
+ *
+ * \return Standard Pacemaker return code
*/
-static void
-add_desc(GString *s, const char *tag, const char *desc, const char *values,
- const char *spaces)
+int
+pcmk__output_cluster_options(pcmk__output_t *out, const char *name,
+ const char *desc_short, const char *desc_long,
+ uint32_t filter, bool all)
{
- char *escaped_en = crm_xml_escape(desc);
-
- if (spaces != NULL) {
- g_string_append(s, spaces);
- }
- pcmk__g_strcat(s, "<", tag, " lang=\"en\">", escaped_en, NULL);
-
- if (values != NULL) {
- pcmk__g_strcat(s, " Allowed values: ", values, NULL);
- }
- pcmk__g_strcat(s, "</", tag, ">\n", NULL);
-
-#ifdef ENABLE_NLS
- {
- static const char *locale = NULL;
-
- char *localized = crm_xml_escape(_(desc));
-
- if (strcmp(escaped_en, localized) != 0) {
- if (locale == NULL) {
- locale = strtok(setlocale(LC_ALL, NULL), "_");
- }
-
- if (spaces != NULL) {
- g_string_append(s, spaces);
- }
- pcmk__g_strcat(s, "<", tag, " lang=\"", locale, "\">", localized,
- NULL);
-
- if (values != NULL) {
- pcmk__g_strcat(s, _(" Allowed values: "), _(values), NULL);
- }
- pcmk__g_strcat(s, "</", tag, ">\n", NULL);
- }
- free(localized);
- }
-#endif
-
- free(escaped_en);
+ return out->message(out, "option-list", name, desc_short, desc_long, filter,
+ cluster_options, all);
}
-gchar *
-pcmk__format_option_metadata(const char *name, const char *desc_short,
- const char *desc_long,
- pcmk__cluster_option_t *option_list, int len)
+/*!
+ * \internal
+ * \brief Output primitive resource meta-attributes as OCF-like XML
+ *
+ * \param[in,out] out Output object
+ * \param[in] name Fake resource agent name for the option list
+ * \param[in] desc_short Short description of the option list
+ * \param[in] desc_long Long description of the option list
+ * \param[in] all If \c true, output all options; otherwise, exclude
+ * advanced and deprecated options. This is always
+ * treated as true for XML output objects.
+ *
+ * \return Standard Pacemaker return code
+ */
+int
+pcmk__output_primitive_meta(pcmk__output_t *out, const char *name,
+ const char *desc_short, const char *desc_long,
+ bool all)
{
- /* big enough to hold "pacemaker-schedulerd metadata" output */
- GString *s = g_string_sized_new(13000);
-
- pcmk__g_strcat(s,
- "<?xml version=\"1.0\"?>\n"
- "<resource-agent name=\"", name, "\" "
- "version=\"" PACEMAKER_VERSION "\">\n"
- " <version>" PCMK_OCF_VERSION "</version>\n", NULL);
-
- add_desc(s, "longdesc", desc_long, NULL, " ");
- add_desc(s, "shortdesc", desc_short, NULL, " ");
-
- g_string_append(s, " <parameters>\n");
-
- for (int lpc = 0; lpc < len; lpc++) {
- const char *opt_name = option_list[lpc].name;
- const char *opt_type = option_list[lpc].type;
- const char *opt_values = option_list[lpc].values;
- const char *opt_default = option_list[lpc].default_value;
- const char *opt_desc_short = option_list[lpc].description_short;
- const char *opt_desc_long = option_list[lpc].description_long;
-
- // The standard requires long and short parameter descriptions
- CRM_ASSERT((opt_desc_short != NULL) || (opt_desc_long != NULL));
-
- if (opt_desc_short == NULL) {
- opt_desc_short = opt_desc_long;
- } else if (opt_desc_long == NULL) {
- opt_desc_long = opt_desc_short;
- }
+ return out->message(out, "option-list", name, desc_short, desc_long,
+ pcmk__opt_none, primitive_meta, all);
+}
- // The standard requires a parameter type
- CRM_ASSERT(opt_type != NULL);
+/*!
+ * \internal
+ * \brief Output fence device common parameter metadata as OCF-like XML
+ *
+ * These are parameters that are available for all fencing resources, regardless
+ * of type. They are processed by Pacemaker, rather than by the fence agent or
+ * the fencing library.
+ *
+ * \param[in,out] out Output object
+ * \param[in] name Fake resource agent name for the option list
+ * \param[in] desc_short Short description of the option list
+ * \param[in] desc_long Long description of the option list
+ * \param[in] all If \c true, output all options; otherwise, exclude
+ * advanced and deprecated options. This is always
+ * treated as true for XML output objects.
+ *
+ * \return Standard Pacemaker return code
+ */
+int
+pcmk__output_fencing_params(pcmk__output_t *out, const char *name,
+ const char *desc_short, const char *desc_long,
+ bool all)
+{
+ return out->message(out, "option-list", name, desc_short, desc_long,
+ pcmk__opt_none, fencing_params, all);
+}
- pcmk__g_strcat(s, " <parameter name=\"", opt_name, "\">\n", NULL);
+/*!
+ * \internal
+ * \brief Output a list of cluster options for a daemon
+ *
+ * \brief[in,out] out Output object
+ * \brief[in] name Daemon name
+ * \brief[in] desc_short Short description of the option list
+ * \brief[in] desc_long Long description of the option list
+ * \brief[in] filter <tt>enum pcmk__opt_flags</tt> flag corresponding
+ * to daemon
+ *
+ * \return Standard Pacemaker return code
+ */
+int
+pcmk__daemon_metadata(pcmk__output_t *out, const char *name,
+ const char *desc_short, const char *desc_long,
+ enum pcmk__opt_flags filter)
+{
+ // @COMPAT Drop this function when we drop daemon metadata
+ pcmk__output_t *tmp_out = NULL;
+ xmlNode *top = NULL;
+ const xmlNode *metadata = NULL;
+ GString *metadata_s = NULL;
- add_desc(s, "longdesc", opt_desc_long, opt_values, " ");
- add_desc(s, "shortdesc", opt_desc_short, NULL, " ");
+ int rc = pcmk__output_new(&tmp_out, "xml", "/dev/null", NULL);
- pcmk__g_strcat(s, " <content type=\"", opt_type, "\"", NULL);
- if (opt_default != NULL) {
- pcmk__g_strcat(s, " default=\"", opt_default, "\"", NULL);
- }
+ if (rc != pcmk_rc_ok) {
+ return rc;
+ }
- if ((opt_values != NULL) && (strcmp(opt_type, "select") == 0)) {
- char *str = strdup(opt_values);
- const char *delim = ", ";
- char *ptr = strtok(str, delim);
+ pcmk__output_set_legacy_xml(tmp_out);
- g_string_append(s, ">\n");
+ if (filter == pcmk__opt_fencing) {
+ pcmk__output_fencing_params(tmp_out, name, desc_short, desc_long, true);
+ } else {
+ pcmk__output_cluster_options(tmp_out, name, desc_short, desc_long,
+ (uint32_t) filter, true);
+ }
- while (ptr != NULL) {
- pcmk__g_strcat(s, " <option value=\"", ptr, "\" />\n",
- NULL);
- ptr = strtok(NULL, delim);
- }
- g_string_append_printf(s, " </content>\n");
- free(str);
+ tmp_out->finish(tmp_out, CRM_EX_OK, false, (void **) &top);
+ metadata = pcmk__xe_first_child(top, PCMK_XE_RESOURCE_AGENT, NULL, NULL);
- } else {
- g_string_append(s, "/>\n");
- }
+ metadata_s = g_string_sized_new(16384);
+ pcmk__xml_string(metadata, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text,
+ metadata_s, 0);
- g_string_append(s, " </parameter>\n");
- }
- g_string_append(s, " </parameters>\n</resource-agent>\n");
+ out->output_xml(out, PCMK_XE_METADATA, metadata_s->str);
- return g_string_free(s, FALSE);
+ pcmk__output_free(tmp_out);
+ free_xml(top);
+ g_string_free(metadata_s, TRUE);
+ return pcmk_rc_ok;
}
void
-pcmk__validate_cluster_options(GHashTable *options,
- pcmk__cluster_option_t *option_list, int len)
+pcmk__validate_cluster_options(GHashTable *options)
{
- for (int lpc = 0; lpc < len; lpc++) {
- cluster_option_value(options, option_list[lpc].is_valid,
- option_list[lpc].name,
- option_list[lpc].alt_name,
- option_list[lpc].default_value);
+ for (const pcmk__cluster_option_t *option = cluster_options;
+ option->name != NULL; option++) {
+
+ cluster_option_value(options, option);
}
}
diff --git a/lib/common/options_display.c b/lib/common/options_display.c
new file mode 100644
index 0000000..9798b31
--- /dev/null
+++ b/lib/common/options_display.c
@@ -0,0 +1,493 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU Lesser General Public License
+ * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <glib.h> // GSList, GString
+
+#include "crmcommon_private.h"
+
+/*!
+ * \internal
+ * \brief Output an option's possible values
+ *
+ * \param[in,out] out Output object
+ * \param[in] option Option whose possible values to add
+ */
+static void
+add_possible_values_default(pcmk__output_t *out,
+ const pcmk__cluster_option_t *option)
+{
+ const char *id = _("Possible values");
+ GString *buf = g_string_sized_new(256);
+
+ CRM_ASSERT(option->type != NULL);
+
+ if (pcmk_is_set(option->flags, pcmk__opt_generated)) {
+ id = _("Possible values (generated by Pacemaker)");
+ }
+
+ if ((option->values != NULL) && (strcmp(option->type, "select") == 0)) {
+ const char *delim = ", ";
+ bool found_default = (option->default_value == NULL);
+ char *str = pcmk__str_copy(option->values);
+
+ for (const char *value = strtok(str, delim); value != NULL;
+ value = strtok(NULL, delim)) {
+
+ if (buf->len > 0) {
+ g_string_append(buf, delim);
+ }
+ g_string_append_c(buf, '"');
+ g_string_append(buf, value);
+ g_string_append_c(buf, '"');
+
+ if (!found_default && (strcmp(value, option->default_value) == 0)) {
+ found_default = true;
+ g_string_append(buf, _(" (default)"));
+ }
+ }
+ free(str);
+
+ } else if (option->default_value != NULL) {
+ pcmk__g_strcat(buf,
+ option->type, _(" (default: \""), option->default_value,
+ "\")", NULL);
+
+ } else {
+ pcmk__g_strcat(buf, option->type, _(" (no default)"), NULL);
+ }
+
+ out->list_item(out, id, "%s", buf->str);
+ g_string_free(buf, TRUE);
+}
+
+/*!
+ * \internal
+ * \brief Output a single option's metadata
+ *
+ * \param[in,out] out Output object
+ * \param[in] option Option to add
+ */
+static void
+add_option_metadata_default(pcmk__output_t *out,
+ const pcmk__cluster_option_t *option)
+{
+ const char *desc_short = option->description_short;
+ const char *desc_long = option->description_long;
+
+ CRM_ASSERT((desc_short != NULL) || (desc_long != NULL));
+
+ if (desc_short == NULL) {
+ desc_short = desc_long;
+ desc_long = NULL;
+ }
+
+ out->list_item(out, option->name, "%s", _(desc_short));
+
+ out->begin_list(out, NULL, NULL, NULL);
+
+ if (desc_long != NULL) {
+ out->list_item(out, NULL, "%s", _(desc_long));
+ }
+ add_possible_values_default(out, option);
+ out->end_list(out);
+}
+
+/*!
+ * \internal
+ * \brief Output the metadata for a list of options
+ *
+ * \param[in,out] out Output object
+ * \param[in] args Message-specific arguments
+ *
+ * \return Standard Pacemaker return code
+ *
+ * \note \p args should contain the following:
+ * -# Fake resource agent name for the option list (ignored)
+ * -# Short description of option list
+ * -# Long description of option list
+ * -# Filter: Group of <tt>enum pcmk__opt_flags</tt>; output an option
+ * only if its \c flags member has all these flags set
+ * -# <tt>NULL</tt>-terminated list of options whose metadata to format
+ * -# All: If \c true, output all options; otherwise, exclude advanced and
+ * deprecated options unless \c pcmk__opt_advanced and
+ * \c pcmk__opt_deprecated flags (respectively) are set in the filter.
+ */
+PCMK__OUTPUT_ARGS("option-list", "const char *", "const char *", "const char *",
+ "uint32_t", "const pcmk__cluster_option_t *", "bool")
+static int
+option_list_default(pcmk__output_t *out, va_list args)
+{
+ const char *name G_GNUC_UNUSED = va_arg(args, const char *);
+ const char *desc_short = va_arg(args, const char *);
+ const char *desc_long = va_arg(args, const char *);
+ const uint32_t filter = va_arg(args, uint32_t);
+ const pcmk__cluster_option_t *option_list =
+ va_arg(args, pcmk__cluster_option_t *);
+ const bool all = (bool) va_arg(args, int);
+
+ const bool show_deprecated = all
+ || pcmk_is_set(filter, pcmk__opt_deprecated);
+ const bool show_advanced = all || pcmk_is_set(filter, pcmk__opt_advanced);
+ bool old_fancy = false;
+
+ GSList *deprecated = NULL;
+ GSList *advanced = NULL;
+
+ CRM_ASSERT((out != NULL) && (desc_short != NULL) && (desc_long != NULL)
+ && (option_list != NULL));
+
+ old_fancy = pcmk__output_text_get_fancy(out);
+ pcmk__output_text_set_fancy(out, true);
+
+ out->info(out, "%s", _(desc_short));
+ out->spacer(out);
+ out->info(out, "%s", _(desc_long));
+ out->begin_list(out, NULL, NULL, NULL);
+
+ for (const pcmk__cluster_option_t *option = option_list;
+ option->name != NULL; option++) {
+
+ // Store deprecated and advanced options to display later if appropriate
+ if (pcmk_all_flags_set(option->flags, filter)) {
+ if (pcmk_is_set(option->flags, pcmk__opt_deprecated)) {
+ if (show_deprecated) {
+ deprecated = g_slist_prepend(deprecated, (gpointer) option);
+ }
+
+ } else if (pcmk_is_set(option->flags, pcmk__opt_advanced)) {
+ if (show_advanced) {
+ advanced = g_slist_prepend(advanced, (gpointer) option);
+ }
+
+ } else {
+ out->spacer(out);
+ add_option_metadata_default(out, option);
+ }
+ }
+ }
+
+ if (advanced != NULL) {
+ advanced = g_slist_reverse(advanced);
+
+ out->spacer(out);
+ out->begin_list(out, NULL, NULL, _("ADVANCED OPTIONS"));
+ for (const GSList *iter = advanced; iter != NULL; iter = iter->next) {
+ const pcmk__cluster_option_t *option = iter->data;
+
+ out->spacer(out);
+ add_option_metadata_default(out, option);
+ }
+ out->end_list(out);
+ g_slist_free(advanced);
+ }
+
+ if (deprecated != NULL) {
+ deprecated = g_slist_reverse(deprecated);
+
+ out->spacer(out);
+ out->begin_list(out, NULL, NULL,
+ _("DEPRECATED OPTIONS (will be removed in a future "
+ "release)"));
+ for (const GSList *iter = deprecated; iter != NULL; iter = iter->next) {
+ const pcmk__cluster_option_t *option = iter->data;
+
+ out->spacer(out);
+ add_option_metadata_default(out, option);
+ }
+ out->end_list(out);
+ g_slist_free(deprecated);
+ }
+
+ out->end_list(out);
+ pcmk__output_text_set_fancy(out, old_fancy);
+ return pcmk_rc_ok;
+}
+
+/*!
+ * \internal
+ * \brief Add a description element to an OCF-like metadata XML node
+ *
+ * Include a translation based on the current locale if \c ENABLE_NLS is
+ * defined.
+ *
+ * \param[in,out] out Output object
+ * \param[in] for_long If \c true, add long description; otherwise, add
+ * short description
+ * \param[in] desc Textual description to add
+ */
+static void
+add_desc_xml(pcmk__output_t *out, bool for_long, const char *desc)
+{
+ const char *tag = (for_long? PCMK_XE_LONGDESC : PCMK_XE_SHORTDESC);
+ xmlNode *node = pcmk__output_create_xml_text_node(out, tag, desc);
+
+ crm_xml_add(node, PCMK_XA_LANG, PCMK__VALUE_EN);
+
+#ifdef ENABLE_NLS
+ {
+ static const char *locale = NULL;
+
+ if (strcmp(desc, _(desc)) == 0) {
+ return;
+ }
+
+ if (locale == NULL) {
+ locale = strtok(setlocale(LC_ALL, NULL), "_");
+ }
+ node = pcmk__output_create_xml_text_node(out, tag, _(desc));
+ crm_xml_add(node, PCMK_XA_LANG, locale);
+ }
+#endif
+}
+
+/*!
+ * \internal
+ * \brief Output an option's possible values
+ *
+ * Add a \c PCMK_XE_OPTION element for each of the option's possible values.
+ *
+ * \param[in,out] out Output object
+ * \param[in] option Option whose possible values to add
+ */
+static void
+add_possible_values_xml(pcmk__output_t *out,
+ const pcmk__cluster_option_t *option)
+{
+ if ((option->values != NULL) && (strcmp(option->type, "select") == 0)) {
+ const char *delim = ", ";
+ char *str = pcmk__str_copy(option->values);
+ const char *ptr = strtok(str, delim);
+
+ while (ptr != NULL) {
+ pcmk__output_create_xml_node(out, PCMK_XE_OPTION,
+ PCMK_XA_VALUE, ptr,
+ NULL);
+ ptr = strtok(NULL, delim);
+ }
+ free(str);
+ }
+}
+
+/*!
+ * \internal
+ * \brief Map an option type to one suitable for daemon metadata
+ *
+ * \param[in] type Option type to map
+ *
+ * \return String suitable for daemon metadata to display as an option type
+ */
+static const char *
+map_legacy_option_type(const char *type)
+{
+ // @COMPAT Drop this function when we drop daemon metadata commands
+ if (pcmk__str_any_of(type, PCMK_VALUE_DURATION, PCMK_VALUE_TIMEOUT, NULL)) {
+ return PCMK__VALUE_TIME;
+
+ } else if (pcmk__str_any_of(type,
+ PCMK_VALUE_NONNEGATIVE_INTEGER,
+ PCMK_VALUE_SCORE, NULL)) {
+ return PCMK_VALUE_INTEGER;
+
+ } else if (pcmk__str_eq(type, PCMK_VALUE_VERSION, pcmk__str_none)) {
+ return PCMK_VALUE_STRING;
+
+ } else {
+ return type;
+ }
+}
+
+/*!
+ * \internal
+ * \brief Add a \c PCMK_XE_PARAMETER element to an OCF-like metadata XML node
+ *
+ * \param[in,out] out Output object
+ * \param[in] option Option to add as a \c PCMK_XE_PARAMETER element
+ */
+static void
+add_option_metadata_xml(pcmk__output_t *out,
+ const pcmk__cluster_option_t *option)
+{
+ const char *type = option->type;
+ const char *desc_long = option->description_long;
+ const char *desc_short = option->description_short;
+ const bool advanced = pcmk_is_set(option->flags, pcmk__opt_advanced);
+ const bool deprecated = pcmk_is_set(option->flags, pcmk__opt_deprecated);
+ const bool generated = pcmk_is_set(option->flags, pcmk__opt_generated);
+
+ // OCF requires "1"/"0" and does not allow "true"/"false
+ // @COMPAT Variables no longer needed after we drop legacy mode
+ const char *advanced_s = advanced? "1" : "0";
+ const char *generated_s = generated? "1" : "0";
+
+ // @COMPAT For daemon metadata only; drop when daemon metadata is dropped
+ const bool legacy = pcmk__output_get_legacy_xml(out);
+ char *desc_long_legacy = NULL;
+ GString *desc_short_legacy = NULL;
+
+ // The standard requires a parameter type
+ CRM_ASSERT(type != NULL);
+
+ // The standard requires long and short parameter descriptions
+ CRM_ASSERT((desc_long != NULL) || (desc_short != NULL));
+
+ if (desc_long == NULL) {
+ desc_long = desc_short;
+ } else if (desc_short == NULL) {
+ desc_short = desc_long;
+ }
+
+ if (legacy) {
+ // This is ugly but it will go away at a major release bump
+ type = map_legacy_option_type(type);
+
+ if (option->values != NULL) {
+ desc_long_legacy = crm_strdup_printf("%s Allowed values: %s",
+ desc_long, option->values);
+ desc_long = desc_long_legacy;
+ }
+
+ if (deprecated || advanced) {
+ const size_t init_sz = 1023;
+
+ if (desc_long != option->description_long) {
+ /* desc_long was NULL and got assigned desc_short, which was
+ * non-empty. Let desc_long have the "real" description, and put
+ * the flag in desc_short.
+ */
+ desc_short = "";
+ } else {
+ desc_short = pcmk__s(option->description_short, "");
+ }
+
+ if (deprecated) {
+ pcmk__add_separated_word(&desc_short_legacy, init_sz,
+ "*** Deprecated ***", NULL);
+ }
+ if (advanced) {
+ pcmk__add_separated_word(&desc_short_legacy, init_sz,
+ "*** Advanced Use Only ***", NULL);
+ }
+ pcmk__add_separated_word(&desc_short_legacy, 0, desc_short, NULL);
+
+ desc_short = desc_short_legacy->str;
+ }
+
+ /* These must be NULL when used as attribute values later.
+ * PCMK_XA_ADVANCED and PCMK_XA_GENERATED break validation for some
+ * legacy tools.
+ */
+ advanced_s = NULL;
+ generated_s = NULL;
+ }
+
+ pcmk__output_xml_create_parent(out, PCMK_XE_PARAMETER,
+ PCMK_XA_NAME, option->name,
+ PCMK_XA_ADVANCED, advanced_s,
+ PCMK_XA_GENERATED, generated_s,
+ NULL);
+
+ if (deprecated && !legacy) {
+ // No need yet to support "replaced-with" or "desc"; add if needed
+ pcmk__output_create_xml_node(out, PCMK_XE_DEPRECATED, NULL);
+ }
+ add_desc_xml(out, true, desc_long);
+ add_desc_xml(out, false, desc_short);
+
+ pcmk__output_xml_create_parent(out, PCMK_XE_CONTENT,
+ PCMK_XA_TYPE, type,
+ PCMK_XA_DEFAULT, option->default_value,
+ NULL);
+
+ add_possible_values_xml(out, option);
+
+ pcmk__output_xml_pop_parent(out);
+ pcmk__output_xml_pop_parent(out);
+
+ free(desc_long_legacy);
+ if (desc_short_legacy != NULL) {
+ g_string_free(desc_short_legacy, TRUE);
+ }
+}
+
+/*!
+ * \internal
+ * \brief Output the metadata for a list of options as OCF-like XML
+ *
+ * \param[in,out] out Output object
+ * \param[in] args Message-specific arguments
+ *
+ * \return Standard Pacemaker return code
+ *
+ * \note \p args should contain the following:
+ * -# Fake resource agent name for the option list
+ * -# Short description of option list
+ * -# Long description of option list
+ * -# Filter: Group of <tt>enum pcmk__opt_flags</tt>; output an option
+ * only if its \c flags member has all these flags set
+ * -# <tt>NULL</tt>-terminated list of options whose metadata to format
+ * -# Whether to output all options (ignored, treated as \c true)
+ */
+PCMK__OUTPUT_ARGS("option-list", "const char *", "const char *", "const char *",
+ "uint32_t", "const pcmk__cluster_option_t *", "bool")
+static int
+option_list_xml(pcmk__output_t *out, va_list args)
+{
+ const char *name = va_arg(args, const char *);
+ const char *desc_short = va_arg(args, const char *);
+ const char *desc_long = va_arg(args, const char *);
+ const uint32_t filter = va_arg(args, uint32_t);
+ const pcmk__cluster_option_t *option_list =
+ va_arg(args, pcmk__cluster_option_t *);
+
+ CRM_ASSERT((out != NULL) && (name != NULL) && (desc_short != NULL)
+ && (desc_long != NULL) && (option_list != NULL));
+
+ pcmk__output_xml_create_parent(out, PCMK_XE_RESOURCE_AGENT,
+ PCMK_XA_NAME, name,
+ PCMK_XA_VERSION, PACEMAKER_VERSION,
+ NULL);
+
+ pcmk__output_create_xml_text_node(out, PCMK_XE_VERSION, PCMK_OCF_VERSION);
+ add_desc_xml(out, true, desc_long);
+ add_desc_xml(out, false, desc_short);
+
+ pcmk__output_xml_create_parent(out, PCMK_XE_PARAMETERS, NULL);
+
+ for (const pcmk__cluster_option_t *option = option_list;
+ option->name != NULL; option++) {
+
+ if (pcmk_all_flags_set(option->flags, filter)) {
+ add_option_metadata_xml(out, option);
+ }
+ }
+
+ pcmk__output_xml_pop_parent(out);
+ pcmk__output_xml_pop_parent(out);
+ return pcmk_rc_ok;
+}
+
+static pcmk__message_entry_t fmt_functions[] = {
+ { "option-list", "default", option_list_default },
+ { "option-list", "xml", option_list_xml },
+
+ { NULL, NULL, NULL }
+};
+
+/*!
+ * \internal
+ * \brief Register the formatting functions for option lists
+ *
+ * \param[in,out] out Output object
+ */
+void
+pcmk__register_option_messages(pcmk__output_t *out) {
+ pcmk__register_messages(out, fmt_functions);
+}
diff --git a/lib/common/output.c b/lib/common/output.c
index 2ea9b0b..92fbfda 100644
--- a/lib/common/output.c
+++ b/lib/common/output.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2019-2023 the Pacemaker project contributors
+ * Copyright 2019-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -18,10 +18,12 @@
static GHashTable *formatters = NULL;
#if defined(PCMK__UNIT_TESTING)
+// LCOV_EXCL_START
GHashTable *
pcmk__output_formatters(void) {
return formatters;
}
+// LCOV_EXCL_STOP
#endif
void
@@ -114,9 +116,8 @@ pcmk__output_new(pcmk__output_t **out, const char *fmt_name,
int rc = pcmk__bare_output_new(out, fmt_name, filename, argv);
if (rc == pcmk_rc_ok) {
- /* Register libcrmcommon messages (currently they exist only for
- * patchset)
- */
+ // Register libcrmcommon messages
+ pcmk__register_option_messages(*out);
pcmk__register_patchset_messages(*out);
}
return rc;
@@ -127,8 +128,15 @@ pcmk__register_format(GOptionGroup *group, const char *name,
pcmk__output_factory_t create,
const GOptionEntry *options)
{
+ char *name_copy = NULL;
+
CRM_ASSERT(create != NULL && !pcmk__str_empty(name));
+ name_copy = strdup(name);
+ if (name_copy == NULL) {
+ return ENOMEM;
+ }
+
if (formatters == NULL) {
formatters = pcmk__strkey_table(free, NULL);
}
@@ -137,7 +145,7 @@ pcmk__register_format(GOptionGroup *group, const char *name,
g_option_group_add_entries(group, options);
}
- g_hash_table_insert(formatters, strdup(name), create);
+ g_hash_table_insert(formatters, name_copy, create);
return pcmk_rc_ok;
}
@@ -189,7 +197,7 @@ pcmk__register_message(pcmk__output_t *out, const char *message_id,
pcmk__message_fn_t fn) {
CRM_ASSERT(out != NULL && !pcmk__str_empty(message_id) && fn != NULL);
- g_hash_table_replace(out->messages, strdup(message_id), fn);
+ g_hash_table_replace(out->messages, pcmk__str_copy(message_id), fn);
}
void
@@ -228,7 +236,7 @@ pcmk__output_and_clear_error(GError **error, pcmk__output_t *out)
* functions that want to free any previous result supplied by the caller).
*
* \param[out] out Where to put newly created output object
- * \param[in,out] xml If non-NULL, this will be freed
+ * \param[in,out] xml If \c *xml is non-NULL, this will be freed
*
* \return Standard Pacemaker return code
*/
@@ -239,6 +247,10 @@ pcmk__xml_output_new(pcmk__output_t **out, xmlNodePtr *xml) {
{ NULL, NULL, NULL }
};
+ if (xml == NULL) {
+ return EINVAL;
+ }
+
if (*xml != NULL) {
xmlFreeNode(*xml);
*xml = NULL;
@@ -251,12 +263,19 @@ pcmk__xml_output_new(pcmk__output_t **out, xmlNodePtr *xml) {
* \internal
* \brief Finish and free an XML-only output object
*
- * \param[in,out] out Output object to free
- * \param[out] xml If not NULL, where to store XML output
+ * \param[in,out] out Output object to free
+ * \param[in] exit_status The exit value of the whole program
+ * \param[out] xml If not NULL, where to store XML output
*/
void
-pcmk__xml_output_finish(pcmk__output_t *out, xmlNodePtr *xml) {
- out->finish(out, 0, FALSE, (void **) xml);
+pcmk__xml_output_finish(pcmk__output_t *out, crm_exit_t exit_status,
+ xmlNodePtr *xml)
+{
+ if (out == NULL) {
+ return;
+ }
+
+ out->finish(out, exit_status, FALSE, (void **) xml);
pcmk__output_free(out);
}
diff --git a/lib/common/output_html.c b/lib/common/output_html.c
index 92e9010..afb2609 100644
--- a/lib/common/output_html.c
+++ b/lib/common/output_html.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2019-2022 the Pacemaker project contributors
+ * Copyright 2019-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -19,22 +19,22 @@
#include <crm/common/xml.h>
static const char *stylesheet_default =
- ".bold { font-weight: bold }\n"
+ "." PCMK__VALUE_BOLD " { font-weight: bold }\n"
- ".online { color: green }\n"
- ".offline { color: red }\n"
- ".maint { color: blue }\n"
- ".standby { color: blue }\n"
- ".health_red { color: red }\n"
- ".health_yellow { color: GoldenRod }\n"
+ "." PCMK_VALUE_ONLINE " { color: green }\n"
+ "." PCMK_VALUE_OFFLINE " { color: red }\n"
+ "." PCMK__VALUE_MAINT " { color: blue }\n"
+ "." PCMK_VALUE_STANDBY " { color: blue }\n"
+ "." PCMK__VALUE_HEALTH_RED " { color: red }\n"
+ "." PCMK__VALUE_HEALTH_YELLOW " { color: GoldenRod }\n"
- ".rsc-failed { color: red }\n"
- ".rsc-failure-ignored { color: DarkGreen }\n"
- ".rsc-managed { color: blue }\n"
- ".rsc-multiple { color: orange }\n"
- ".rsc-ok { color: green }\n"
+ "." PCMK__VALUE_RSC_FAILED " { color: red }\n"
+ "." PCMK__VALUE_RSC_FAILURE_IGNORED " { color: DarkGreen }\n"
+ "." PCMK__VALUE_RSC_MANAGED " { color: blue }\n"
+ "." PCMK__VALUE_RSC_MULTIPLE " { color: orange }\n"
+ "." PCMK__VALUE_RSC_OK " { color: green }\n"
- ".warning { color: red; font-weight: bold }";
+ "." PCMK__VALUE_WARNING " { color: red; font-weight: bold }";
static gboolean cgi_output = FALSE;
static char *stylesheet_link = NULL;
@@ -81,9 +81,13 @@ html_free_priv(pcmk__output_t *out) {
priv = out->priv;
- xmlFreeNode(priv->root);
+ free_xml(priv->root);
+ /* The elements of parent_q are xmlNodes that are a part of the
+ * priv->root document, so the above line already frees them. Don't
+ * call g_queue_free_full here.
+ */
g_queue_free(priv->parent_q);
- g_slist_free(priv->errors);
+ g_slist_free_full(priv->errors, free);
free(priv);
out->priv = NULL;
}
@@ -108,10 +112,10 @@ html_init(pcmk__output_t *out) {
priv->parent_q = g_queue_new();
- priv->root = create_xml_node(NULL, "html");
+ priv->root = pcmk__xe_create(NULL, "html");
xmlCreateIntSubset(priv->root->doc, (pcmkXmlStr) "html", NULL, NULL);
- crm_xml_add(priv->root, "lang", "en");
+ crm_xml_add(priv->root, PCMK_XA_LANG, PCMK__VALUE_EN);
g_queue_push_tail(priv->parent_q, priv->root);
priv->errors = NULL;
@@ -132,6 +136,7 @@ html_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy
private_data_t *priv = NULL;
htmlNodePtr head_node = NULL;
htmlNodePtr charset_node = NULL;
+ xmlNode *child_node = NULL;
CRM_ASSERT(out != NULL);
@@ -155,12 +160,14 @@ html_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy
head_node = xmlNewDocRawNode(NULL, NULL, (pcmkXmlStr) "head", NULL);
if (title != NULL ) {
- pcmk_create_xml_text_node(head_node, "title", title);
+ child_node = pcmk__xe_create(head_node, "title");
+ pcmk__xe_set_content(child_node, "%s", title);
} else if (out->request != NULL) {
- pcmk_create_xml_text_node(head_node, "title", out->request);
+ child_node = pcmk__xe_create(head_node, "title");
+ pcmk__xe_set_content(child_node, "%s", out->request);
}
- charset_node = create_xml_node(head_node, "meta");
+ charset_node = pcmk__xe_create(head_node, PCMK__XE_META);
crm_xml_add(charset_node, "charset", "utf-8");
/* Add any extra header nodes the caller might have created. */
@@ -174,10 +181,11 @@ html_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy
* stylesheet. The second can override the first. At least one should be
* given.
*/
- pcmk_create_xml_text_node(head_node, "style", stylesheet_default);
+ child_node = pcmk__xe_create(head_node, "style");
+ pcmk__xe_set_content(child_node, "%s", stylesheet_default);
if (stylesheet_link != NULL) {
- htmlNodePtr link_node = create_xml_node(head_node, "link");
+ htmlNodePtr link_node = pcmk__xe_create(head_node, "link");
pcmk__xe_set_props(link_node, "rel", "stylesheet",
"href", stylesheet_link,
NULL);
@@ -196,7 +204,7 @@ html_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy
}
if (copy_dest != NULL) {
- *copy_dest = copy_xml(priv->root);
+ *copy_dest = pcmk__xml_copy(NULL, priv->root);
}
g_slist_free_full(extra_headers, (GDestroyNotify) xmlFreeNode);
@@ -224,15 +232,17 @@ html_subprocess_output(pcmk__output_t *out, int exit_status,
rc_buf = crm_strdup_printf("Return code: %d", exit_status);
pcmk__output_create_xml_text_node(out, "h2", "Command Output");
- pcmk__output_create_html_node(out, "div", NULL, NULL, rc_buf);
+ pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, rc_buf);
if (proc_stdout != NULL) {
- pcmk__output_create_html_node(out, "div", NULL, NULL, "Stdout");
- pcmk__output_create_html_node(out, "div", NULL, "output", proc_stdout);
+ pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, "Stdout");
+ pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL,
+ PCMK__VALUE_OUTPUT, proc_stdout);
}
if (proc_stderr != NULL) {
- pcmk__output_create_html_node(out, "div", NULL, NULL, "Stderr");
- pcmk__output_create_html_node(out, "div", NULL, "output", proc_stderr);
+ pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, "Stderr");
+ pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL,
+ PCMK__VALUE_OUTPUT, proc_stderr);
}
free(rc_buf);
@@ -243,13 +253,17 @@ html_version(pcmk__output_t *out, bool extended) {
CRM_ASSERT(out != NULL);
pcmk__output_create_xml_text_node(out, "h2", "Version Information");
- pcmk__output_create_html_node(out, "div", NULL, NULL, "Program: Pacemaker");
- pcmk__output_create_html_node(out, "div", NULL, NULL, crm_strdup_printf("Version: %s", PACEMAKER_VERSION));
- pcmk__output_create_html_node(out, "div", NULL, NULL,
+ pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL,
+ "Program: Pacemaker");
+ pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL,
+ "Version: " PACEMAKER_VERSION);
+ pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL,
"Author: Andrew Beekhof and "
"the Pacemaker project contributors");
- pcmk__output_create_html_node(out, "div", NULL, NULL, crm_strdup_printf("Build: %s", BUILD_VERSION));
- pcmk__output_create_html_node(out, "div", NULL, NULL, crm_strdup_printf("Features: %s", CRM_FEATURES));
+ pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL,
+ "Build: " BUILD_VERSION);
+ pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL,
+ "Features: " CRM_FEATURES);
}
G_GNUC_PRINTF(2, 3)
@@ -284,7 +298,7 @@ html_output_xml(pcmk__output_t *out, const char *name, const char *buf) {
CRM_ASSERT(out != NULL);
node = pcmk__output_create_html_node(out, "pre", NULL, NULL, buf);
- crm_xml_add(node, "lang", "xml");
+ crm_xml_add(node, PCMK_XA_LANG, "xml");
}
G_GNUC_PRINTF(4, 5)
@@ -349,7 +363,7 @@ html_list_item(pcmk__output_t *out, const char *name, const char *format, ...) {
free(buf);
if (name != NULL) {
- crm_xml_add(item_node, "class", name);
+ crm_xml_add(item_node, PCMK_XA_CLASS, name);
}
}
@@ -365,7 +379,9 @@ html_end_list(pcmk__output_t *out) {
CRM_ASSERT(out != NULL && out->priv != NULL);
priv = out->priv;
- /* Remove the <ul> tag. */
+ /* Remove the <ul> tag, but do not free this result - it's still
+ * part of the document.
+ */
g_queue_pop_tail(priv->parent_q);
pcmk__output_xml_pop_parent(out);
@@ -441,16 +457,42 @@ pcmk__output_create_html_node(pcmk__output_t *out, const char *element_name, con
node = pcmk__output_create_xml_text_node(out, element_name, text);
if (class_name != NULL) {
- crm_xml_add(node, "class", class_name);
+ crm_xml_add(node, PCMK_XA_CLASS, class_name);
}
if (id != NULL) {
- crm_xml_add(node, "id", id);
+ crm_xml_add(node, PCMK_XA_ID, id);
}
return node;
}
+/*!
+ * \internal
+ * \brief Create a new HTML element under a given parent with ID and class
+ *
+ * \param[in,out] parent XML element that will be the new element's parent
+ * (\c NULL to create a new XML document with the new
+ * node as root)
+ * \param[in] name Name of new element
+ * \param[in] id CSS ID of new element (can be \c NULL)
+ * \param[in] class CSS class of new element (can be \c NULL)
+ *
+ * \return Newly created XML element (guaranteed not to be \c NULL)
+ */
+xmlNode *
+pcmk__html_create(xmlNode *parent, const char *name, const char *id,
+ const char *class)
+{
+ xmlNode *node = pcmk__xe_create(parent, name);
+
+ pcmk__xe_set_props(node,
+ PCMK_XA_CLASS, class,
+ PCMK_XA_ID, id,
+ NULL);
+ return node;
+}
+
void
pcmk__html_add_header(const char *name, ...) {
htmlNodePtr header_node;
diff --git a/lib/common/output_log.c b/lib/common/output_log.c
index 54fa37e..81a39c8 100644
--- a/lib/common/output_log.c
+++ b/lib/common/output_log.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2019-2023 the Pacemaker project contributors
+ * Copyright 2019-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -16,10 +16,6 @@
#include <stdlib.h>
#include <stdio.h>
-GOptionEntry pcmk__log_output_entries[] = {
- { NULL }
-};
-
typedef struct private_data_s {
/* gathered in log_begin_list */
GQueue/*<char*>*/ *prefixes;
@@ -165,8 +161,8 @@ log_output_xml(pcmk__output_t *out, const char *name, const char *buf) {
CRM_ASSERT(out != NULL && out->priv != NULL);
priv = out->priv;
- node = create_xml_node(NULL, name);
- xmlNodeSetContent(node, (pcmkXmlStr) buf);
+ node = pcmk__xe_create(NULL, name);
+ pcmk__xe_set_content(node, "%s", buf);
do_crm_log_xml(priv->log_level, name, node);
free(node);
}
@@ -353,34 +349,63 @@ pcmk__mk_log_output(char **argv) {
return retval;
}
+/*!
+ * \internal
+ * \brief Get the log level for a log output object
+ *
+ * This returns 0 if the output object is not of log format.
+ *
+ * \param[in] out Output object
+ *
+ * \return Current log level for \p out
+ */
uint8_t
pcmk__output_get_log_level(const pcmk__output_t *out)
{
- private_data_t *priv = NULL;
+ CRM_ASSERT(out != NULL);
- CRM_ASSERT((out != NULL) && (out->priv != NULL));
- CRM_CHECK(pcmk__str_eq(out->fmt_name, "log", pcmk__str_none), return 0);
+ if (pcmk__str_eq(out->fmt_name, "log", pcmk__str_none)) {
+ private_data_t *priv = out->priv;
- priv = out->priv;
- return priv->log_level;
+ CRM_ASSERT(priv != NULL);
+ return priv->log_level;
+ }
+ return 0;
}
+/*!
+ * \internal
+ * \brief Set the log level for a log output object
+ *
+ * This does nothing if the output object is not of log format.
+ *
+ * \param[in,out] out Output object
+ * \param[in] log_level Log level constant (\c LOG_ERR, etc.) to use
+ *
+ * \note \c LOG_INFO is used by default for new \c pcmk__output_t objects.
+ * \note Almost all formatted output messages respect this setting. However,
+ * <tt>out->err</tt> always logs at \c LOG_ERR.
+ */
void
-pcmk__output_set_log_level(pcmk__output_t *out, uint8_t log_level) {
- private_data_t *priv = NULL;
+pcmk__output_set_log_level(pcmk__output_t *out, uint8_t log_level)
+{
+ CRM_ASSERT(out != NULL);
- CRM_ASSERT(out != NULL && out->priv != NULL);
- CRM_CHECK(pcmk__str_eq(out->fmt_name, "log", pcmk__str_none), return);
+ if (pcmk__str_eq(out->fmt_name, "log", pcmk__str_none)) {
+ private_data_t *priv = out->priv;
- priv = out->priv;
- priv->log_level = log_level;
+ CRM_ASSERT(priv != NULL);
+ priv->log_level = log_level;
+ }
}
/*!
* \internal
* \brief Set the file, function, line, and tags used to filter log output
*
- * \param[in,out] out Logger output object
+ * This does nothing if the output object is not of log format.
+ *
+ * \param[in,out] out Output object
* \param[in] file File name to filter with (or NULL for default)
* \param[in] function Function name to filter with (or NULL for default)
* \param[in] line Line number to filter with (or 0 for default)
@@ -394,14 +419,15 @@ void
pcmk__output_set_log_filter(pcmk__output_t *out, const char *file,
const char *function, uint32_t line, uint32_t tags)
{
- private_data_t *priv = NULL;
+ CRM_ASSERT(out != NULL);
- CRM_ASSERT((out != NULL) && (out->priv != NULL));
- CRM_CHECK(pcmk__str_eq(out->fmt_name, "log", pcmk__str_none), return);
+ if (pcmk__str_eq(out->fmt_name, "log", pcmk__str_none)) {
+ private_data_t *priv = out->priv;
- priv = out->priv;
- priv->file = file;
- priv->function = function;
- priv->line = line;
- priv->tags = tags;
+ CRM_ASSERT(priv != NULL);
+ priv->file = file;
+ priv->function = function;
+ priv->line = line;
+ priv->tags = tags;
+ }
}
diff --git a/lib/common/output_none.c b/lib/common/output_none.c
index 581a8b4..d1cdacc 100644
--- a/lib/common/output_none.c
+++ b/lib/common/output_none.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2019-2022 the Pacemaker project contributors
+ * Copyright 2019-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -15,10 +15,6 @@
#include <crm/crm.h>
#include <crm/common/cmdline_internal.h>
-GOptionEntry pcmk__none_output_entries[] = {
- { NULL }
-};
-
static void
none_free_priv(pcmk__output_t *out) {
/* This function intentionally left blank */
@@ -120,7 +116,7 @@ pcmk__mk_none_output(char **argv) {
return NULL;
}
- retval->fmt_name = PCMK__VALUE_NONE;
+ retval->fmt_name = PCMK_VALUE_NONE;
retval->request = pcmk__quote_cmdline(argv);
retval->init = none_init;
diff --git a/lib/common/output_text.c b/lib/common/output_text.c
index 6bd362d..d43e29f 100644
--- a/lib/common/output_text.c
+++ b/lib/common/output_text.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2019-2022 the Pacemaker project contributors
+ * Copyright 2019-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -15,10 +15,14 @@
#include <glib.h>
#include <termios.h>
+#include "crmcommon_private.h"
+
+// @COMPAT Drop at 3.0.0
static gboolean fancy = FALSE;
+// @COMPAT Drop at 3.0.0
GOptionEntry pcmk__text_output_entries[] = {
- { "text-fancy", 0, 0, G_OPTION_ARG_NONE, &fancy,
+ { "text-fancy", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &fancy,
"Use more highly formatted output (requires --output-as=text)",
NULL },
@@ -33,9 +37,18 @@ typedef struct text_list_data_s {
typedef struct private_data_s {
GQueue *parent_q;
+ bool fancy;
} private_data_t;
static void
+free_list_data(gpointer data) {
+ text_list_data_t *list_data = data;
+
+ free(list_data->singular_noun);
+ free(list_data->plural_noun);
+}
+
+static void
text_free_priv(pcmk__output_t *out) {
private_data_t *priv = NULL;
@@ -45,7 +58,7 @@ text_free_priv(pcmk__output_t *out) {
priv = out->priv;
- g_queue_free(priv->parent_q);
+ g_queue_free_full(priv->parent_q, free_list_data);
free(priv);
out->priv = NULL;
}
@@ -59,15 +72,14 @@ text_init(pcmk__output_t *out) {
/* If text_init was previously called on this output struct, just return. */
if (out->priv != NULL) {
return true;
- } else {
- out->priv = calloc(1, sizeof(private_data_t));
- if (out->priv == NULL) {
- return false;
- }
+ }
- priv = out->priv;
+ out->priv = calloc(1, sizeof(private_data_t));
+ if (out->priv == NULL) {
+ return false;
}
+ priv = out->priv;
priv->parent_q = g_queue_new();
return true;
}
@@ -80,6 +92,9 @@ text_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy
static void
text_reset(pcmk__output_t *out) {
+ private_data_t *priv = NULL;
+ bool old_fancy = false;
+
CRM_ASSERT(out != NULL);
if (out->dest != stdout) {
@@ -88,8 +103,15 @@ text_reset(pcmk__output_t *out) {
CRM_ASSERT(out->dest != NULL);
+ // Save priv->fancy before free/init sequence overwrites it
+ priv = out->priv;
+ old_fancy = priv->fancy;
+
text_free_priv(out);
text_init(out);
+
+ priv = out->priv;
+ priv->fancy = old_fancy;
}
static void
@@ -192,17 +214,17 @@ text_begin_list(pcmk__output_t *out, const char *singular_noun, const char *plur
va_start(ap, format);
- if (fancy && format) {
+ if ((fancy || priv->fancy) && (format != NULL)) {
pcmk__indented_vprintf(out, format, ap);
fprintf(out->dest, ":\n");
}
va_end(ap);
- new_list = calloc(1, sizeof(text_list_data_t));
+ new_list = pcmk__assert_alloc(1, sizeof(text_list_data_t));
new_list->len = 0;
- pcmk__str_update(&new_list->singular_noun, singular_noun);
- pcmk__str_update(&new_list->plural_noun, plural_noun);
+ new_list->singular_noun = pcmk__str_copy(singular_noun);
+ new_list->plural_noun = pcmk__str_copy(plural_noun);
g_queue_push_tail(priv->parent_q, new_list);
}
@@ -210,13 +232,15 @@ text_begin_list(pcmk__output_t *out, const char *singular_noun, const char *plur
G_GNUC_PRINTF(3, 4)
static void
text_list_item(pcmk__output_t *out, const char *id, const char *format, ...) {
+ private_data_t *priv = NULL;
va_list ap;
CRM_ASSERT(out != NULL);
+ priv = out->priv;
va_start(ap, format);
- if (fancy) {
+ if (fancy || priv->fancy) {
if (id != NULL) {
/* Not really a good way to do this all in one call, so make it two.
* The first handles the indentation and list styling. The second
@@ -269,7 +293,7 @@ text_end_list(pcmk__output_t *out) {
}
}
- free(node);
+ free_list_data(node);
}
static bool
@@ -336,6 +360,52 @@ pcmk__mk_text_output(char **argv) {
return retval;
}
+/*!
+ * \internal
+ * \brief Check whether fancy output is enabled for a text output object
+ *
+ * This returns \c false if the output object is not of text format.
+ *
+ * \param[in] out Output object
+ *
+ * \return \c true if \p out has fancy output enabled, or \c false otherwise
+ */
+bool
+pcmk__output_text_get_fancy(pcmk__output_t *out)
+{
+ CRM_ASSERT(out != NULL);
+
+ if (pcmk__str_eq(out->fmt_name, "text", pcmk__str_none)) {
+ private_data_t *priv = out->priv;
+
+ CRM_ASSERT(priv != NULL);
+ return priv->fancy;
+ }
+ return false;
+}
+
+/*!
+ * \internal
+ * \brief Enable or disable fancy output for a text output object
+ *
+ * This does nothing if the output object is not of text format.
+ *
+ * \param[in,out] out Output object
+ * \param[in] enabled Whether fancy output should be enabled for \p out
+ */
+void
+pcmk__output_text_set_fancy(pcmk__output_t *out, bool enabled)
+{
+ CRM_ASSERT(out != NULL);
+
+ if (pcmk__str_eq(out->fmt_name, "text", pcmk__str_none)) {
+ private_data_t *priv = out->priv;
+
+ CRM_ASSERT(priv != NULL);
+ priv->fancy = enabled;
+ }
+}
+
G_GNUC_PRINTF(2, 0)
void
pcmk__formatted_vprintf(pcmk__output_t *out, const char *format, va_list args) {
@@ -363,10 +433,14 @@ pcmk__formatted_printf(pcmk__output_t *out, const char *format, ...) {
G_GNUC_PRINTF(2, 0)
void
pcmk__indented_vprintf(pcmk__output_t *out, const char *format, va_list args) {
+ private_data_t *priv = NULL;
+
CRM_ASSERT(out != NULL);
CRM_CHECK(pcmk__str_eq(out->fmt_name, "text", pcmk__str_none), return);
- if (fancy) {
+ priv = out->priv;
+
+ if (fancy || priv->fancy) {
int level = 0;
private_data_t *priv = out->priv;
@@ -428,7 +502,7 @@ pcmk__text_prompt(const char *prompt, bool echo, char **dest)
#if HAVE_SSCANF_M
rc = scanf("%ms", dest);
#else
- *dest = calloc(1, 1024);
+ *dest = pcmk__assert_alloc(1, 1024);
rc = scanf("%1023s", *dest);
#endif
fprintf(stderr, "\n");
diff --git a/lib/common/output_xml.c b/lib/common/output_xml.c
index ba61145..9b0d417 100644
--- a/lib/common/output_xml.c
+++ b/lib/common/output_xml.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2019-2023 the Pacemaker project contributors
+ * Copyright 2019-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -14,57 +14,59 @@
#include <stdlib.h>
#include <stdio.h>
#include <crm/crm.h>
-#include <crm/common/output.h>
-#include <crm/common/xml.h>
-#include <crm/common/xml_internal.h> /* pcmk__xml2fd */
#include <glib.h>
#include <crm/common/cmdline_internal.h>
+#include <crm/common/output.h>
#include <crm/common/xml.h>
-
-static gboolean legacy_xml = FALSE;
-static gboolean simple_list = FALSE;
-static gboolean substitute = FALSE;
-
-GOptionEntry pcmk__xml_output_entries[] = {
- { "xml-legacy", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &legacy_xml,
- NULL,
- NULL },
- { "xml-simple-list", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &simple_list,
- NULL,
- NULL },
- { "xml-substitute", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &substitute,
- NULL,
- NULL },
-
- { NULL }
-};
+#include <crm/common/xml_internal.h> // pcmk__xml2fd
typedef struct subst_s {
const char *from;
const char *to;
} subst_t;
-static subst_t substitutions[] = {
- { "Active Resources", "resources" },
- { "Assignment Scores", "allocations" },
- { "Assignment Scores and Utilization Information", "allocations_utilizations" },
- { "Cluster Summary", "summary" },
- { "Current cluster status", "cluster_status" },
- { "Executing Cluster Transition", "transition" },
- { "Failed Resource Actions", "failures" },
- { "Fencing History", "fence_history" },
- { "Full List of Resources", "resources" },
- { "Inactive Resources", "resources" },
- { "Migration Summary", "node_history" },
- { "Negative Location Constraints", "bans" },
- { "Node Attributes", "node_attributes" },
- { "Operations", "node_history" },
- { "Resource Config", "resource_config" },
- { "Resource Operations", "operations" },
- { "Revised Cluster Status", "revised_cluster_status" },
- { "Transition Summary", "actions" },
- { "Utilization Information", "utilizations" },
+static const subst_t substitutions[] = {
+ { "Active Resources",
+ PCMK_XE_RESOURCES, },
+ { "Assignment Scores",
+ PCMK_XE_ALLOCATIONS, },
+ { "Assignment Scores and Utilization Information",
+ PCMK_XE_ALLOCATIONS_UTILIZATIONS, },
+ { "Cluster Summary",
+ PCMK_XE_SUMMARY, },
+ { "Current cluster status",
+ PCMK_XE_CLUSTER_STATUS, },
+ { "Executing Cluster Transition",
+ PCMK_XE_TRANSITION, },
+ { "Failed Resource Actions",
+ PCMK_XE_FAILURES, },
+ { "Fencing History",
+ PCMK_XE_FENCE_HISTORY, },
+ { "Full List of Resources",
+ PCMK_XE_RESOURCES, },
+ { "Inactive Resources",
+ PCMK_XE_RESOURCES, },
+ { "Migration Summary",
+ PCMK_XE_NODE_HISTORY, },
+ { "Negative Location Constraints",
+ PCMK_XE_BANS, },
+ { "Node Attributes",
+ PCMK_XE_NODE_ATTRIBUTES, },
+ { "Operations",
+ PCMK_XE_NODE_HISTORY, },
+ { "Resource Config",
+ PCMK_XE_RESOURCE_CONFIG, },
+ { "Resource Operations",
+ PCMK_XE_OPERATIONS, },
+ { "Revised Cluster Status",
+ PCMK_XE_REVISED_CLUSTER_STATUS, },
+ { "Timings",
+ PCMK_XE_TIMINGS, },
+ { "Transition Summary",
+ PCMK_XE_ACTIONS, },
+ { "Utilization Information",
+ PCMK_XE_UTILIZATIONS, },
{ NULL, NULL }
};
@@ -82,8 +84,46 @@ typedef struct private_data_s {
GSList *errors;
/* End members that must match the HTML version */
bool legacy_xml;
+ bool list_element;
} private_data_t;
+static bool
+has_root_node(pcmk__output_t *out)
+{
+ private_data_t *priv = NULL;
+
+ CRM_ASSERT(out != NULL);
+
+ priv = out->priv;
+ return priv != NULL && priv->root != NULL;
+}
+
+static void
+add_root_node(pcmk__output_t *out)
+{
+ private_data_t *priv = NULL;
+
+ /* has_root_node will assert if out is NULL, so no need to do it here */
+ if (has_root_node(out)) {
+ return;
+ }
+
+ priv = out->priv;
+
+ if (priv->legacy_xml) {
+ priv->root = pcmk__xe_create(NULL, PCMK_XE_CRM_MON);
+ crm_xml_add(priv->root, PCMK_XA_VERSION, PACEMAKER_VERSION);
+ } else {
+ priv->root = pcmk__xe_create(NULL, PCMK_XE_PACEMAKER_RESULT);
+ crm_xml_add(priv->root, PCMK_XA_API_VERSION, PCMK__API_VERSION);
+ crm_xml_add(priv->root, PCMK_XA_REQUEST,
+ pcmk__s(out->request, "libpacemaker"));
+ }
+
+ priv->parent_q = g_queue_new();
+ g_queue_push_tail(priv->parent_q, priv->root);
+}
+
static void
xml_free_priv(pcmk__output_t *out) {
private_data_t *priv = NULL;
@@ -94,9 +134,16 @@ xml_free_priv(pcmk__output_t *out) {
priv = out->priv;
- free_xml(priv->root);
- g_queue_free(priv->parent_q);
- g_slist_free(priv->errors);
+ if (has_root_node(out)) {
+ free_xml(priv->root);
+ /* The elements of parent_q are xmlNodes that are a part of the
+ * priv->root document, so the above line already frees them. Don't
+ * call g_queue_free_full here.
+ */
+ g_queue_free(priv->parent_q);
+ }
+
+ g_slist_free_full(priv->errors, free);
free(priv);
out->priv = NULL;
}
@@ -119,36 +166,18 @@ xml_init(pcmk__output_t *out) {
priv = out->priv;
}
- if (legacy_xml) {
- priv->root = create_xml_node(NULL, "crm_mon");
- crm_xml_add(priv->root, "version", PACEMAKER_VERSION);
- } else {
- priv->root = create_xml_node(NULL, "pacemaker-result");
- crm_xml_add(priv->root, "api-version", PCMK__API_VERSION);
-
- if (out->request != NULL) {
- crm_xml_add(priv->root, "request", out->request);
- }
- }
-
- priv->parent_q = g_queue_new();
priv->errors = NULL;
- g_queue_push_tail(priv->parent_q, priv->root);
-
- /* Copy this from the file-level variable. This means that it is only settable
- * as a command line option, and that pcmk__output_new must be called after all
- * command line processing is completed.
- */
- priv->legacy_xml = legacy_xml;
return true;
}
static void
add_error_node(gpointer data, gpointer user_data) {
- char *str = (char *) data;
+ const char *str = (const char *) data;
xmlNodePtr node = (xmlNodePtr) user_data;
- pcmk_create_xml_text_node(node, "error", str);
+
+ node = pcmk__xe_create(node, PCMK_XE_ERROR);
+ pcmk__xe_set_content(node, "%s", str);
}
static void
@@ -159,14 +188,13 @@ xml_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_
CRM_ASSERT(out != NULL);
priv = out->priv;
- /* If root is NULL, xml_init failed and we are being called from pcmk__output_free
- * in the pcmk__output_new path.
- */
- if (priv == NULL || priv->root == NULL) {
+ if (priv == NULL) {
return;
}
- if (legacy_xml) {
+ add_root_node(out);
+
+ if (priv->legacy_xml) {
GSList *node = priv->errors;
if (exit_status != CRM_EX_OK) {
@@ -180,13 +208,14 @@ xml_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_
} else {
char *rc_as_str = pcmk__itoa(exit_status);
- node = create_xml_node(priv->root, "status");
- pcmk__xe_set_props(node, "code", rc_as_str,
- "message", crm_exit_str(exit_status),
+ node = pcmk__xe_create(priv->root, PCMK_XE_STATUS);
+ pcmk__xe_set_props(node,
+ PCMK_XA_CODE, rc_as_str,
+ PCMK_XA_MESSAGE, crm_exit_str(exit_status),
NULL);
if (g_slist_length(priv->errors) > 0) {
- xmlNodePtr errors_node = create_xml_node(node, "errors");
+ xmlNodePtr errors_node = pcmk__xe_create(node, PCMK_XE_ERRORS);
g_slist_foreach(priv->errors, add_error_node, (gpointer) errors_node);
}
@@ -198,7 +227,7 @@ xml_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_
}
if (copy_dest != NULL) {
- *copy_dest = copy_xml(priv->root);
+ *copy_dest = pcmk__xml_copy(NULL, priv->root);
}
}
@@ -223,18 +252,20 @@ xml_subprocess_output(pcmk__output_t *out, int exit_status,
rc_as_str = pcmk__itoa(exit_status);
- node = pcmk__output_xml_create_parent(out, "command",
- "code", rc_as_str,
+ node = pcmk__output_xml_create_parent(out, PCMK_XE_COMMAND,
+ PCMK_XA_CODE, rc_as_str,
NULL);
if (proc_stdout != NULL) {
- child_node = pcmk_create_xml_text_node(node, "output", proc_stdout);
- crm_xml_add(child_node, "source", "stdout");
+ child_node = pcmk__xe_create(node, PCMK_XE_OUTPUT);
+ pcmk__xe_set_content(child_node, "%s", proc_stdout);
+ crm_xml_add(child_node, PCMK_XA_SOURCE, "stdout");
}
if (proc_stderr != NULL) {
- child_node = pcmk_create_xml_text_node(node, "output", proc_stderr);
- crm_xml_add(child_node, "source", "stderr");
+ child_node = pcmk__xe_create(node, PCMK_XE_OUTPUT);
+ pcmk__xe_set_content(child_node, "%s", proc_stderr);
+ crm_xml_add(child_node, PCMK_XA_SOURCE, "stderr");
}
free(rc_as_str);
@@ -242,15 +273,16 @@ xml_subprocess_output(pcmk__output_t *out, int exit_status,
static void
xml_version(pcmk__output_t *out, bool extended) {
+ const char *author = "Andrew Beekhof and the Pacemaker project "
+ "contributors";
CRM_ASSERT(out != NULL);
- pcmk__output_create_xml_node(out, "version",
- "program", "Pacemaker",
- "version", PACEMAKER_VERSION,
- "author", "Andrew Beekhof and the "
- "Pacemaker project contributors",
- "build", BUILD_VERSION,
- "features", CRM_FEATURES,
+ pcmk__output_create_xml_node(out, PCMK_XE_VERSION,
+ PCMK_XA_PROGRAM, "Pacemaker",
+ PCMK_XA_VERSION, PACEMAKER_VERSION,
+ PCMK_XA_AUTHOR, author,
+ PCMK_XA_BUILD, BUILD_VERSION,
+ PCMK_XA_FEATURES, CRM_FEATURES,
NULL);
}
@@ -265,6 +297,8 @@ xml_err(pcmk__output_t *out, const char *format, ...) {
CRM_ASSERT(out != NULL && out->priv != NULL);
priv = out->priv;
+ add_root_node(out);
+
va_start(ap, format);
len = vasprintf(&buf, format, ap);
CRM_ASSERT(len > 0);
@@ -302,20 +336,20 @@ xml_begin_list(pcmk__output_t *out, const char *singular_noun, const char *plura
char *name = NULL;
char *buf = NULL;
int len;
+ private_data_t *priv = NULL;
- CRM_ASSERT(out != NULL);
+ CRM_ASSERT(out != NULL && out->priv != NULL);
+ priv = out->priv;
va_start(ap, format);
len = vasprintf(&buf, format, ap);
CRM_ASSERT(len >= 0);
va_end(ap);
- if (substitute) {
- for (subst_t *s = substitutions; s->from != NULL; s++) {
- if (!strcmp(s->from, buf)) {
- name = g_strdup(s->to);
- break;
- }
+ for (const subst_t *s = substitutions; s->from != NULL; s++) {
+ if (strcmp(s->from, buf) == 0) {
+ name = g_strdup(s->to);
+ break;
}
}
@@ -323,12 +357,12 @@ xml_begin_list(pcmk__output_t *out, const char *singular_noun, const char *plura
name = g_ascii_strdown(buf, -1);
}
- if (legacy_xml || simple_list) {
- pcmk__output_xml_create_parent(out, name, NULL);
- } else {
- pcmk__output_xml_create_parent(out, "list",
- "name", name,
+ if (priv->list_element) {
+ pcmk__output_xml_create_parent(out, PCMK_XE_LIST,
+ PCMK_XA_NAME, name,
NULL);
+ } else {
+ pcmk__output_xml_create_parent(out, name, NULL);
}
g_free(name);
@@ -350,10 +384,10 @@ xml_list_item(pcmk__output_t *out, const char *name, const char *format, ...) {
CRM_ASSERT(len >= 0);
va_end(ap);
- item_node = pcmk__output_create_xml_text_node(out, "item", buf);
+ item_node = pcmk__output_create_xml_text_node(out, PCMK_XE_ITEM, buf);
if (name != NULL) {
- crm_xml_add(item_node, "name", name);
+ crm_xml_add(item_node, PCMK_XA_NAME, name);
}
free(buf);
@@ -371,16 +405,18 @@ xml_end_list(pcmk__output_t *out) {
CRM_ASSERT(out != NULL && out->priv != NULL);
priv = out->priv;
- if (priv->legacy_xml || simple_list) {
- g_queue_pop_tail(priv->parent_q);
- } else {
+ if (priv->list_element) {
char *buf = NULL;
xmlNodePtr node;
+ /* Do not free node here - it's still part of the document */
node = g_queue_pop_tail(priv->parent_q);
buf = crm_strdup_printf("%lu", xmlChildElementCount(node));
- crm_xml_add(node, "count", buf);
+ crm_xml_add(node, PCMK_XA_COUNT, buf);
free(buf);
+ } else {
+ /* Do not free this result - it's still part of the document */
+ g_queue_pop_tail(priv->parent_q);
}
}
@@ -465,13 +501,15 @@ pcmk__output_xml_add_node_copy(pcmk__output_t *out, xmlNodePtr node) {
CRM_ASSERT(node != NULL);
CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return);
+ add_root_node(out);
+
priv = out->priv;
parent = g_queue_peek_tail(priv->parent_q);
// Shouldn't happen unless the caller popped priv->root
CRM_CHECK(parent != NULL, return);
- add_node_copy(parent, node);
+ pcmk__xml_copy(parent, node);
}
xmlNodePtr
@@ -483,9 +521,11 @@ pcmk__output_create_xml_node(pcmk__output_t *out, const char *name, ...) {
CRM_ASSERT(out != NULL && out->priv != NULL);
CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL);
+ add_root_node(out);
+
priv = out->priv;
- node = create_xml_node(g_queue_peek_tail(priv->parent_q), name);
+ node = pcmk__xe_create(g_queue_peek_tail(priv->parent_q), name);
va_start(args, name);
pcmk__xe_set_propv(node, args);
va_end(args);
@@ -501,7 +541,7 @@ pcmk__output_create_xml_text_node(pcmk__output_t *out, const char *name, const c
CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL);
node = pcmk__output_create_xml_node(out, name, NULL);
- xmlNodeSetContent(node, (pcmkXmlStr) content);
+ pcmk__xe_set_content(node, "%s", content);
return node;
}
@@ -513,6 +553,8 @@ pcmk__output_xml_push_parent(pcmk__output_t *out, xmlNodePtr parent) {
CRM_ASSERT(parent != NULL);
CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return);
+ add_root_node(out);
+
priv = out->priv;
g_queue_push_tail(priv->parent_q, parent);
@@ -525,9 +567,12 @@ pcmk__output_xml_pop_parent(pcmk__output_t *out) {
CRM_ASSERT(out != NULL && out->priv != NULL);
CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return);
+ add_root_node(out);
+
priv = out->priv;
CRM_ASSERT(g_queue_get_length(priv->parent_q) > 0);
+ /* Do not free this result - it's still part of the document */
g_queue_pop_tail(priv->parent_q);
}
@@ -538,8 +583,61 @@ pcmk__output_xml_peek_parent(pcmk__output_t *out) {
CRM_ASSERT(out != NULL && out->priv != NULL);
CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL);
+ add_root_node(out);
+
priv = out->priv;
/* If queue is empty NULL will be returned */
return g_queue_peek_tail(priv->parent_q);
}
+
+bool
+pcmk__output_get_legacy_xml(pcmk__output_t *out)
+{
+ private_data_t *priv = NULL;
+
+ CRM_ASSERT(out != NULL);
+
+ if (!pcmk__str_eq(out->fmt_name, "xml", pcmk__str_none)) {
+ return false;
+ }
+
+ CRM_ASSERT(out->priv != NULL);
+
+ priv = out->priv;
+ return priv->legacy_xml;
+}
+
+void
+pcmk__output_set_legacy_xml(pcmk__output_t *out)
+{
+ private_data_t *priv = NULL;
+
+ CRM_ASSERT(out != NULL);
+
+ if (!pcmk__str_eq(out->fmt_name, "xml", pcmk__str_none)) {
+ return;
+ }
+
+ CRM_ASSERT(out->priv != NULL);
+
+ priv = out->priv;
+ priv->legacy_xml = true;
+}
+
+void
+pcmk__output_enable_list_element(pcmk__output_t *out)
+{
+ private_data_t *priv = NULL;
+
+ CRM_ASSERT(out != NULL);
+
+ if (!pcmk__str_eq(out->fmt_name, "xml", pcmk__str_none)) {
+ return;
+ }
+
+ CRM_ASSERT(out->priv != NULL);
+
+ priv = out->priv;
+ priv->list_element = true;
+}
diff --git a/lib/common/patchset.c b/lib/common/patchset.c
index 34e27fb..c12a8ef 100644
--- a/lib/common/patchset.c
+++ b/lib/common/patchset.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2004-2023 the Pacemaker project contributors
+ * Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -21,14 +21,10 @@
#include <libxml/tree.h>
#include <crm/crm.h>
-#include <crm/msg_xml.h>
#include <crm/common/xml.h>
#include <crm/common/xml_internal.h> // CRM_XML_LOG_BASE, etc.
#include "crmcommon_private.h"
-static xmlNode *subtract_xml_comment(xmlNode *parent, xmlNode *left,
- xmlNode *right, gboolean *changed);
-
/* Add changes for specified XML to patchset.
* For patchset format, refer to diff schema.
*/
@@ -56,12 +52,12 @@ add_xml_changes_to_patchset(xmlNode *xml, xmlNode *patchset)
if (xpath != NULL) {
int position = pcmk__xml_position(xml, pcmk__xf_deleted);
- change = create_xml_node(patchset, XML_DIFF_CHANGE);
+ change = pcmk__xe_create(patchset, PCMK_XE_CHANGE);
- crm_xml_add(change, XML_DIFF_OP, "create");
- crm_xml_add(change, XML_DIFF_PATH, (const char *) xpath->str);
- crm_xml_add_int(change, XML_DIFF_POSITION, position);
- add_node_copy(change, xml);
+ crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_CREATE);
+ crm_xml_add(change, PCMK_XA_PATH, (const char *) xpath->str);
+ crm_xml_add_int(change, PCMK_XE_POSITION, position);
+ pcmk__xml_copy(change, xml);
g_string_free(xpath, TRUE);
}
@@ -82,35 +78,35 @@ add_xml_changes_to_patchset(xmlNode *xml, xmlNode *patchset)
GString *xpath = pcmk__element_xpath(xml);
if (xpath != NULL) {
- change = create_xml_node(patchset, XML_DIFF_CHANGE);
+ change = pcmk__xe_create(patchset, PCMK_XE_CHANGE);
- crm_xml_add(change, XML_DIFF_OP, "modify");
- crm_xml_add(change, XML_DIFF_PATH, (const char *) xpath->str);
+ crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_MODIFY);
+ crm_xml_add(change, PCMK_XA_PATH, (const char *) xpath->str);
- change = create_xml_node(change, XML_DIFF_LIST);
+ change = pcmk__xe_create(change, PCMK_XE_CHANGE_LIST);
g_string_free(xpath, TRUE);
}
}
- attr = create_xml_node(change, XML_DIFF_ATTR);
+ attr = pcmk__xe_create(change, PCMK_XE_CHANGE_ATTR);
- crm_xml_add(attr, XML_NVPAIR_ATTR_NAME, (const char *)pIter->name);
+ crm_xml_add(attr, PCMK_XA_NAME, (const char *) pIter->name);
if (nodepriv->flags & pcmk__xf_deleted) {
- crm_xml_add(attr, XML_DIFF_OP, "unset");
+ crm_xml_add(attr, PCMK_XA_OPERATION, "unset");
} else {
- crm_xml_add(attr, XML_DIFF_OP, "set");
+ crm_xml_add(attr, PCMK_XA_OPERATION, "set");
value = pcmk__xml_attr_value(pIter);
- crm_xml_add(attr, XML_NVPAIR_ATTR_VALUE, value);
+ crm_xml_add(attr, PCMK_XA_VALUE, value);
}
}
if (change) {
xmlNode *result = NULL;
- change = create_xml_node(change->parent, XML_DIFF_RESULT);
- result = create_xml_node(change, (const char *)xml->name);
+ change = pcmk__xe_create(change->parent, PCMK_XE_CHANGE_RESULT);
+ result = pcmk__xe_create(change, (const char *)xml->name);
for (pIter = pcmk__xe_first_attr(xml); pIter != NULL;
pIter = pIter->next) {
@@ -133,14 +129,15 @@ add_xml_changes_to_patchset(xmlNode *xml, xmlNode *patchset)
GString *xpath = pcmk__element_xpath(xml);
crm_trace("%s.%s moved to position %d",
- xml->name, ID(xml), pcmk__xml_position(xml, pcmk__xf_skip));
+ xml->name, pcmk__xe_id(xml),
+ pcmk__xml_position(xml, pcmk__xf_skip));
if (xpath != NULL) {
- change = create_xml_node(patchset, XML_DIFF_CHANGE);
+ change = pcmk__xe_create(patchset, PCMK_XE_CHANGE);
- crm_xml_add(change, XML_DIFF_OP, "move");
- crm_xml_add(change, XML_DIFF_PATH, (const char *) xpath->str);
- crm_xml_add_int(change, XML_DIFF_POSITION,
+ crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_MOVE);
+ crm_xml_add(change, PCMK_XA_PATH, (const char *) xpath->str);
+ crm_xml_add_int(change, PCMK_XE_POSITION,
pcmk__xml_position(xml, pcmk__xf_deleted));
g_string_free(xpath, TRUE);
}
@@ -153,7 +150,8 @@ is_config_change(xmlNode *xml)
GList *gIter = NULL;
xml_node_private_t *nodepriv = NULL;
xml_doc_private_t *docpriv;
- xmlNode *config = first_named_child(xml, XML_CIB_TAG_CONFIGURATION);
+ xmlNode *config = pcmk__xe_first_child(xml, PCMK_XE_CONFIGURATION, NULL,
+ NULL);
if (config) {
nodepriv = config->_private;
@@ -168,7 +166,7 @@ is_config_change(xmlNode *xml)
pcmk__deleted_xml_t *deleted_obj = gIter->data;
if (strstr(deleted_obj->path,
- "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION) != NULL) {
+ "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION) != NULL) {
return TRUE;
}
}
@@ -176,6 +174,7 @@ is_config_change(xmlNode *xml)
return FALSE;
}
+// @COMPAT Remove when v1 patchsets are removed
static void
xml_repair_v1_diff(xmlNode *last, xmlNode *next, xmlNode *local_diff,
gboolean changed)
@@ -187,9 +186,9 @@ xml_repair_v1_diff(xmlNode *last, xmlNode *next, xmlNode *local_diff,
const char *tag = NULL;
const char *vfields[] = {
- XML_ATTR_GENERATION_ADMIN,
- XML_ATTR_GENERATION,
- XML_ATTR_NUMUPDATES,
+ PCMK_XA_ADMIN_EPOCH,
+ PCMK_XA_EPOCH,
+ PCMK_XA_NUM_UPDATES,
};
if (local_diff == NULL) {
@@ -197,16 +196,16 @@ xml_repair_v1_diff(xmlNode *last, xmlNode *next, xmlNode *local_diff,
return;
}
- tag = XML_TAG_DIFF_REMOVED;
- diff_child = find_xml_node(local_diff, tag, FALSE);
+ tag = PCMK__XE_DIFF_REMOVED;
+ diff_child = pcmk__xe_first_child(local_diff, tag, NULL, NULL);
if (diff_child == NULL) {
- diff_child = create_xml_node(local_diff, tag);
+ diff_child = pcmk__xe_create(local_diff, tag);
}
- tag = XML_TAG_CIB;
- cib = find_xml_node(diff_child, tag, FALSE);
+ tag = PCMK_XE_CIB;
+ cib = pcmk__xe_first_child(diff_child, tag, NULL, NULL);
if (cib == NULL) {
- cib = create_xml_node(diff_child, tag);
+ cib = pcmk__xe_create(diff_child, tag);
}
for (lpc = 0; (last != NULL) && (lpc < PCMK__NELEM(vfields)); lpc++) {
@@ -218,16 +217,16 @@ xml_repair_v1_diff(xmlNode *last, xmlNode *next, xmlNode *local_diff,
}
}
- tag = XML_TAG_DIFF_ADDED;
- diff_child = find_xml_node(local_diff, tag, FALSE);
+ tag = PCMK__XE_DIFF_ADDED;
+ diff_child = pcmk__xe_first_child(local_diff, tag, NULL, NULL);
if (diff_child == NULL) {
- diff_child = create_xml_node(local_diff, tag);
+ diff_child = pcmk__xe_create(local_diff, tag);
}
- tag = XML_TAG_CIB;
- cib = find_xml_node(diff_child, tag, FALSE);
+ tag = PCMK_XE_CIB;
+ cib = pcmk__xe_first_child(diff_child, tag, NULL, NULL);
if (cib == NULL) {
- cib = create_xml_node(diff_child, tag);
+ cib = pcmk__xe_create(diff_child, tag);
}
for (lpc = 0; next && lpc < PCMK__NELEM(vfields); lpc++) {
@@ -246,11 +245,12 @@ xml_repair_v1_diff(xmlNode *last, xmlNode *next, xmlNode *local_diff,
crm_log_xml_explicit(local_diff, "Repaired-diff");
}
+// @COMPAT Remove when v1 patchsets are removed
static xmlNode *
xml_create_patchset_v1(xmlNode *source, xmlNode *target, bool config,
bool suppress)
{
- xmlNode *patchset = diff_xml_object(source, target, suppress);
+ xmlNode *patchset = pcmk__diff_v1_xml_object(source, target, suppress);
if (patchset) {
CRM_LOG_ASSERT(xml_document_dirty(target));
@@ -271,9 +271,9 @@ xml_create_patchset_v2(xmlNode *source, xmlNode *target)
xmlNode *version = NULL;
xmlNode *patchset = NULL;
const char *vfields[] = {
- XML_ATTR_GENERATION_ADMIN,
- XML_ATTR_GENERATION,
- XML_ATTR_NUMUPDATES,
+ PCMK_XA_ADMIN_EPOCH,
+ PCMK_XA_EPOCH,
+ PCMK_XA_NUM_UPDATES,
};
CRM_ASSERT(target);
@@ -284,12 +284,12 @@ xml_create_patchset_v2(xmlNode *source, xmlNode *target)
CRM_ASSERT(target->doc);
docpriv = target->doc->_private;
- patchset = create_xml_node(NULL, XML_TAG_DIFF);
+ patchset = pcmk__xe_create(NULL, PCMK_XE_DIFF);
crm_xml_add_int(patchset, PCMK_XA_FORMAT, 2);
- version = create_xml_node(patchset, XML_DIFF_VERSION);
+ version = pcmk__xe_create(patchset, PCMK_XE_VERSION);
- v = create_xml_node(version, XML_DIFF_VSOURCE);
+ v = pcmk__xe_create(version, PCMK_XE_SOURCE);
for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
const char *value = crm_element_value(source, vfields[lpc]);
@@ -299,7 +299,7 @@ xml_create_patchset_v2(xmlNode *source, xmlNode *target)
crm_xml_add(v, vfields[lpc], value);
}
- v = create_xml_node(version, XML_DIFF_VTARGET);
+ v = pcmk__xe_create(version, PCMK_XE_TARGET);
for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
const char *value = crm_element_value(target, vfields[lpc]);
@@ -311,12 +311,12 @@ xml_create_patchset_v2(xmlNode *source, xmlNode *target)
for (gIter = docpriv->deleted_objs; gIter; gIter = gIter->next) {
pcmk__deleted_xml_t *deleted_obj = gIter->data;
- xmlNode *change = create_xml_node(patchset, XML_DIFF_CHANGE);
+ xmlNode *change = pcmk__xe_create(patchset, PCMK_XE_CHANGE);
- crm_xml_add(change, XML_DIFF_OP, "delete");
- crm_xml_add(change, XML_DIFF_PATH, deleted_obj->path);
+ crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_DELETE);
+ crm_xml_add(change, PCMK_XA_PATH, deleted_obj->path);
if (deleted_obj->position >= 0) {
- crm_xml_add_int(change, XML_DIFF_POSITION, deleted_obj->position);
+ crm_xml_add_int(change, PCMK_XE_POSITION, deleted_obj->position);
}
}
@@ -331,7 +331,7 @@ xml_create_patchset(int format, xmlNode *source, xmlNode *target,
int counter = 0;
bool config = FALSE;
xmlNode *patch = NULL;
- const char *version = crm_element_value(source, XML_ATTR_CRM_VERSION);
+ const char *version = crm_element_value(source, PCMK_XA_CRM_FEATURE_SET);
xml_acl_disable(target);
if (!xml_document_dirty(target)) {
@@ -346,16 +346,16 @@ xml_create_patchset(int format, xmlNode *source, xmlNode *target,
if (manage_version && config) {
crm_trace("Config changed %d", format);
- crm_xml_add(target, XML_ATTR_NUMUPDATES, "0");
+ crm_xml_add(target, PCMK_XA_NUM_UPDATES, "0");
- crm_element_value_int(target, XML_ATTR_GENERATION, &counter);
- crm_xml_add_int(target, XML_ATTR_GENERATION, counter+1);
+ crm_element_value_int(target, PCMK_XA_EPOCH, &counter);
+ crm_xml_add_int(target, PCMK_XA_EPOCH, counter+1);
} else if (manage_version) {
- crm_element_value_int(target, XML_ATTR_NUMUPDATES, &counter);
+ crm_element_value_int(target, PCMK_XA_NUM_UPDATES, &counter);
crm_trace("Status changed %d - %d %s", format, counter,
- crm_element_value(source, XML_ATTR_NUMUPDATES));
- crm_xml_add_int(target, XML_ATTR_NUMUPDATES, (counter + 1));
+ crm_element_value(source, PCMK_XA_NUM_UPDATES));
+ crm_xml_add_int(target, PCMK_XA_NUM_UPDATES, (counter + 1));
}
if (format == 0) {
@@ -369,6 +369,7 @@ xml_create_patchset(int format, xmlNode *source, xmlNode *target,
switch (format) {
case 1:
+ // @COMPAT Remove when v1 patchsets are removed
patch = xml_create_patchset_v1(source, target, config, FALSE);
break;
case 2:
@@ -403,23 +404,228 @@ patchset_process_digest(xmlNode *patch, xmlNode *source, xmlNode *target,
return;
}
- version = crm_element_value(source, XML_ATTR_CRM_VERSION);
+ version = crm_element_value(source, PCMK_XA_CRM_FEATURE_SET);
digest = calculate_xml_versioned_digest(target, FALSE, TRUE, version);
- crm_xml_add(patch, XML_ATTR_DIGEST, digest);
+ crm_xml_add(patch, PCMK__XA_DIGEST, digest);
free(digest);
return;
}
-// Return true if attribute name is not "id"
+// @COMPAT Remove when v1 patchsets are removed
+static xmlNode *
+subtract_v1_xml_comment(xmlNode *parent, xmlNode *left, xmlNode *right,
+ gboolean *changed)
+{
+ CRM_CHECK(left != NULL, return NULL);
+ CRM_CHECK(left->type == XML_COMMENT_NODE, return NULL);
+
+ if ((right == NULL) || !pcmk__str_eq((const char *)left->content,
+ (const char *)right->content,
+ pcmk__str_casei)) {
+ xmlNode *deleted = NULL;
+
+ deleted = pcmk__xml_copy(parent, left);
+ *changed = TRUE;
+
+ return deleted;
+ }
+
+ return NULL;
+}
+
+// @COMPAT Remove when v1 patchsets are removed
+static xmlNode *
+subtract_v1_xml_object(xmlNode *parent, xmlNode *left, xmlNode *right,
+ bool full, gboolean *changed, const char *marker)
+{
+ gboolean dummy = FALSE;
+ xmlNode *diff = NULL;
+ xmlNode *right_child = NULL;
+ xmlNode *left_child = NULL;
+ xmlAttrPtr xIter = NULL;
+
+ const char *id = NULL;
+ const char *name = NULL;
+ const char *value = NULL;
+ const char *right_val = NULL;
+
+ if (changed == NULL) {
+ changed = &dummy;
+ }
+
+ if (left == NULL) {
+ return NULL;
+ }
+
+ if (left->type == XML_COMMENT_NODE) {
+ return subtract_v1_xml_comment(parent, left, right, changed);
+ }
+
+ id = pcmk__xe_id(left);
+ name = (const char *) left->name;
+ if (right == NULL) {
+ xmlNode *deleted = NULL;
+
+ crm_trace("Processing <%s " PCMK_XA_ID "=%s> (complete copy)",
+ name, id);
+ deleted = pcmk__xml_copy(parent, left);
+ crm_xml_add(deleted, PCMK__XA_CRM_DIFF_MARKER, marker);
+
+ *changed = TRUE;
+ return deleted;
+ }
+
+ CRM_CHECK(name != NULL, return NULL);
+ CRM_CHECK(pcmk__xe_is(left, (const char *) right->name), return NULL);
+
+ // Check for PCMK__XA_CRM_DIFF_MARKER in a child
+ value = crm_element_value(right, PCMK__XA_CRM_DIFF_MARKER);
+ if ((value != NULL) && (strcmp(value, "removed:top") == 0)) {
+ crm_trace("We are the root of the deletion: %s.id=%s", name, id);
+ *changed = TRUE;
+ return NULL;
+ }
+
+ // @TODO Avoiding creating the full hierarchy would save work here
+ diff = pcmk__xe_create(parent, name);
+
+ // Changes to child objects
+ for (left_child = pcmk__xml_first_child(left); left_child != NULL;
+ left_child = pcmk__xml_next(left_child)) {
+ gboolean child_changed = FALSE;
+
+ right_child = pcmk__xml_match(right, left_child, false);
+ subtract_v1_xml_object(diff, left_child, right_child, full,
+ &child_changed, marker);
+ if (child_changed) {
+ *changed = TRUE;
+ }
+ }
+
+ if (!*changed) {
+ /* Nothing to do */
+
+ } else if (full) {
+ xmlAttrPtr pIter = NULL;
+
+ for (pIter = pcmk__xe_first_attr(left); pIter != NULL;
+ pIter = pIter->next) {
+ const char *p_name = (const char *)pIter->name;
+ const char *p_value = pcmk__xml_attr_value(pIter);
+
+ xmlSetProp(diff, (pcmkXmlStr) p_name, (pcmkXmlStr) p_value);
+ }
+
+ // We have everything we need
+ goto done;
+ }
+
+ // Changes to name/value pairs
+ for (xIter = pcmk__xe_first_attr(left); xIter != NULL;
+ xIter = xIter->next) {
+ const char *prop_name = (const char *) xIter->name;
+ xmlAttrPtr right_attr = NULL;
+ xml_node_private_t *nodepriv = NULL;
+
+ if (strcmp(prop_name, PCMK_XA_ID) == 0) {
+ // id already obtained when present ~ this case, so just reuse
+ xmlSetProp(diff, (pcmkXmlStr) PCMK_XA_ID, (pcmkXmlStr) id);
+ continue;
+ }
+
+ if (pcmk__xa_filterable(prop_name)) {
+ continue;
+ }
+
+ right_attr = xmlHasProp(right, (pcmkXmlStr) prop_name);
+ if (right_attr) {
+ nodepriv = right_attr->_private;
+ }
+
+ right_val = crm_element_value(right, prop_name);
+ if ((right_val == NULL) || (nodepriv && pcmk_is_set(nodepriv->flags, pcmk__xf_deleted))) {
+ /* new */
+ *changed = TRUE;
+ if (full) {
+ xmlAttrPtr pIter = NULL;
+
+ for (pIter = pcmk__xe_first_attr(left); pIter != NULL;
+ pIter = pIter->next) {
+ const char *p_name = (const char *) pIter->name;
+ const char *p_value = pcmk__xml_attr_value(pIter);
+
+ xmlSetProp(diff, (pcmkXmlStr) p_name, (pcmkXmlStr) p_value);
+ }
+ break;
+
+ } else {
+ const char *left_value = pcmk__xml_attr_value(xIter);
+
+ xmlSetProp(diff, (pcmkXmlStr) prop_name, (pcmkXmlStr) value);
+ crm_xml_add(diff, prop_name, left_value);
+ }
+
+ } else {
+ /* Only now do we need the left value */
+ const char *left_value = pcmk__xml_attr_value(xIter);
+
+ if (strcmp(left_value, right_val) == 0) {
+ /* unchanged */
+
+ } else {
+ *changed = TRUE;
+ if (full) {
+ xmlAttrPtr pIter = NULL;
+
+ crm_trace("Changes detected to %s in "
+ "<%s " PCMK_XA_ID "=%s>", prop_name, name, id);
+ for (pIter = pcmk__xe_first_attr(left); pIter != NULL;
+ pIter = pIter->next) {
+ const char *p_name = (const char *) pIter->name;
+ const char *p_value = pcmk__xml_attr_value(pIter);
+
+ xmlSetProp(diff, (pcmkXmlStr) p_name,
+ (pcmkXmlStr) p_value);
+ }
+ break;
+
+ } else {
+ crm_trace("Changes detected to %s (%s -> %s) in "
+ "<%s " PCMK_XA_ID "=%s>",
+ prop_name, left_value, right_val, name, id);
+ crm_xml_add(diff, prop_name, left_value);
+ }
+ }
+ }
+ }
+
+ if (!*changed) {
+ free_xml(diff);
+ return NULL;
+
+ } else if (!full && (id != NULL)) {
+ crm_xml_add(diff, PCMK_XA_ID, id);
+ }
+ done:
+ return diff;
+}
+
+/* @COMPAT Remove when v1 patchsets are removed.
+ *
+ * Return true if attribute name is not \c PCMK_XML_ID.
+ */
static bool
not_id(xmlAttrPtr attr, void *user_data)
{
- return strcmp((const char *) attr->name, XML_ATTR_ID) != 0;
+ return strcmp((const char *) attr->name, PCMK_XA_ID) != 0;
}
-// Apply the removals section of an v1 patchset to an XML node
+/* @COMPAT Remove when v1 patchsets are removed.
+ *
+ * Apply the removals section of a v1 patchset to an XML node.
+ */
static void
process_v1_removals(xmlNode *target, xmlNode *patch)
{
@@ -436,15 +642,17 @@ process_v1_removals(xmlNode *target, xmlNode *patch)
if (target->type == XML_COMMENT_NODE) {
gboolean dummy;
- subtract_xml_comment(target->parent, target, patch, &dummy);
+ subtract_v1_xml_comment(target->parent, target, patch, &dummy);
}
CRM_CHECK(pcmk__xe_is(target, (const char *) patch->name), return);
- CRM_CHECK(pcmk__str_eq(ID(target), ID(patch), pcmk__str_casei), return);
+ CRM_CHECK(pcmk__str_eq(pcmk__xe_id(target), pcmk__xe_id(patch),
+ pcmk__str_none),
+ return);
- // Check for XML_DIFF_MARKER in a child
- id = crm_element_value_copy(target, XML_ATTR_ID);
- value = crm_element_value(patch, XML_DIFF_MARKER);
+ // Check for PCMK__XA_CRM_DIFF_MARKER in a child
+ id = crm_element_value_copy(target, PCMK_XA_ID);
+ value = crm_element_value(patch, PCMK__XA_CRM_DIFF_MARKER);
if ((value != NULL) && (strcmp(value, "removed:top") == 0)) {
crm_trace("We are the root of the deletion: %s.id=%s",
target->name, id);
@@ -468,7 +676,10 @@ process_v1_removals(xmlNode *target, xmlNode *patch)
free(id);
}
-// Apply the additions section of an v1 patchset to an XML node
+/* @COMPAT Remove when v1 patchsets are removed.
+ *
+ * Apply the additions section of a v1 patchset to an XML node.
+ */
static void
process_v1_additions(xmlNode *parent, xmlNode *target, xmlNode *patch)
{
@@ -486,18 +697,18 @@ process_v1_additions(xmlNode *parent, xmlNode *target, xmlNode *patch)
return;
}
- // Check for XML_DIFF_MARKER in a child
+ // Check for PCMK__XA_CRM_DIFF_MARKER in a child
name = (const char *) patch->name;
- value = crm_element_value(patch, XML_DIFF_MARKER);
+ value = crm_element_value(patch, PCMK__XA_CRM_DIFF_MARKER);
if ((target == NULL) && (value != NULL)
&& (strcmp(value, "added:top") == 0)) {
- id = ID(patch);
+ id = pcmk__xe_id(patch);
crm_trace("We are the root of the addition: %s.id=%s", name, id);
- add_node_copy(parent, patch);
+ pcmk__xml_copy(parent, patch);
return;
} else if (target == NULL) {
- id = ID(patch);
+ id = pcmk__xe_id(patch);
crm_err("Could not locate: %s.id=%s", name, id);
return;
}
@@ -507,14 +718,16 @@ process_v1_additions(xmlNode *parent, xmlNode *target, xmlNode *patch)
}
CRM_CHECK(pcmk__xe_is(target, name), return);
- CRM_CHECK(pcmk__str_eq(ID(target), ID(patch), pcmk__str_casei), return);
+ CRM_CHECK(pcmk__str_eq(pcmk__xe_id(target), pcmk__xe_id(patch),
+ pcmk__str_none),
+ return);
for (xIter = pcmk__xe_first_attr(patch); xIter != NULL;
xIter = xIter->next) {
const char *p_name = (const char *) xIter->name;
const char *p_value = pcmk__xml_attr_value(xIter);
- xml_remove_prop(target, p_name); // Preserve patch order
+ pcmk__xe_remove_attr(target, p_name); // Preserve patch order
crm_xml_add(target, p_name, p_value);
}
@@ -547,17 +760,20 @@ find_patch_xml_node(const xmlNode *patchset, int format, bool added,
switch (format) {
case 1:
- label = added? XML_TAG_DIFF_ADDED : XML_TAG_DIFF_REMOVED;
- *patch_node = find_xml_node(patchset, label, FALSE);
- cib_node = find_xml_node(*patch_node, "cib", FALSE);
+ // @COMPAT Remove when v1 patchsets are removed
+ label = added? PCMK__XE_DIFF_ADDED : PCMK__XE_DIFF_REMOVED;
+ *patch_node = pcmk__xe_first_child(patchset, label, NULL, NULL);
+ cib_node = pcmk__xe_first_child(*patch_node, PCMK_XE_CIB, NULL,
+ NULL);
if (cib_node != NULL) {
*patch_node = cib_node;
}
break;
case 2:
- label = added? "target" : "source";
- *patch_node = find_xml_node(patchset, "version", FALSE);
- *patch_node = find_xml_node(*patch_node, label, FALSE);
+ label = added? PCMK_XE_TARGET : PCMK_XE_SOURCE;
+ *patch_node = pcmk__xe_first_child(patchset, PCMK_XE_VERSION, NULL,
+ NULL);
+ *patch_node = pcmk__xe_first_child(*patch_node, label, NULL, NULL);
break;
default:
crm_warn("Unknown patch format: %d", format);
@@ -576,9 +792,9 @@ xml_patch_versions(const xmlNode *patchset, int add[3], int del[3])
xmlNode *tmp = NULL;
const char *vfields[] = {
- XML_ATTR_GENERATION_ADMIN,
- XML_ATTR_GENERATION,
- XML_ATTR_NUMUPDATES,
+ PCMK_XA_ADMIN_EPOCH,
+ PCMK_XA_EPOCH,
+ PCMK_XA_NUM_UPDATES,
};
@@ -628,9 +844,9 @@ xml_patch_version_check(const xmlNode *xml, const xmlNode *patchset)
int del[] = { 0, 0, 0 };
const char *vfields[] = {
- XML_ATTR_GENERATION_ADMIN,
- XML_ATTR_GENERATION,
- XML_ATTR_NUMUPDATES,
+ PCMK_XA_ADMIN_EPOCH,
+ PCMK_XA_EPOCH,
+ PCMK_XA_NUM_UPDATES,
};
for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
@@ -684,6 +900,22 @@ xml_patch_version_check(const xmlNode *xml, const xmlNode *patchset)
return pcmk_rc_ok;
}
+// @COMPAT Remove when v1 patchsets are removed
+static void
+purge_v1_diff_markers(xmlNode *node)
+{
+ xmlNode *child = NULL;
+
+ CRM_CHECK(node != NULL, return);
+
+ pcmk__xe_remove_attr(node, PCMK__XA_CRM_DIFF_MARKER);
+ for (child = pcmk__xml_first_child(node); child != NULL;
+ child = pcmk__xml_next(child)) {
+ purge_v1_diff_markers(child);
+ }
+}
+
+// @COMPAT Remove when v1 patchsets are removed
/*!
* \internal
* \brief Apply a version 1 patchset to an XML node
@@ -700,9 +932,11 @@ apply_v1_patchset(xmlNode *xml, const xmlNode *patchset)
int root_nodes_seen = 0;
xmlNode *child_diff = NULL;
- xmlNode *added = find_xml_node(patchset, XML_TAG_DIFF_ADDED, FALSE);
- xmlNode *removed = find_xml_node(patchset, XML_TAG_DIFF_REMOVED, FALSE);
- xmlNode *old = copy_xml(xml);
+ xmlNode *added = pcmk__xe_first_child(patchset, PCMK__XE_DIFF_ADDED, NULL,
+ NULL);
+ xmlNode *removed = pcmk__xe_first_child(patchset, PCMK__XE_DIFF_REMOVED,
+ NULL, NULL);
+ xmlNode *old = pcmk__xml_copy(NULL, xml);
crm_trace("Subtraction Phase");
for (child_diff = pcmk__xml_first_child(removed); child_diff != NULL;
@@ -741,7 +975,7 @@ apply_v1_patchset(xmlNode *xml, const xmlNode *patchset)
rc = ENOTUNIQ;
}
- purge_diff_markers(xml); // Purge prior to checking digest
+ purge_v1_diff_markers(xml); // Purge prior to checking digest
free_xml(old);
return rc;
@@ -759,7 +993,7 @@ first_matching_xml_child(const xmlNode *parent, const char *name,
if (strcmp((const char *) cIter->name, name) != 0) {
continue;
} else if (id) {
- const char *cid = ID(cIter);
+ const char *cid = pcmk__xe_id(cIter);
if ((cid == NULL) || (strcmp(cid, id) != 0)) {
continue;
@@ -811,24 +1045,17 @@ search_v2_xpath(const xmlNode *top, const char *key, int target_position)
* than key_len - 1 characters plus a null terminator.
*/
- remainder = calloc(key_len, sizeof(char));
- CRM_ASSERT(remainder != NULL);
-
- section = calloc(key_len, sizeof(char));
- CRM_ASSERT(section != NULL);
-
- id = calloc(key_len, sizeof(char));
- CRM_ASSERT(id != NULL);
-
- tag = calloc(key_len, sizeof(char));
- CRM_ASSERT(tag != NULL);
+ remainder = pcmk__assert_alloc(key_len, sizeof(char));
+ section = pcmk__assert_alloc(key_len, sizeof(char));
+ id = pcmk__assert_alloc(key_len, sizeof(char));
+ tag = pcmk__assert_alloc(key_len, sizeof(char));
do {
// Look for /NEXT_COMPONENT/REMAINING_COMPONENTS
rc = sscanf(current, "/%[^/]%s", section, remainder);
if (rc > 0) {
// Separate FIRST_COMPONENT into TAG[@id='ID']
- int f = sscanf(section, "%[^[][@" XML_ATTR_ID "='%[^']", tag, id);
+ int f = sscanf(section, "%[^[][@" PCMK_XA_ID "='%[^']", tag, id);
int current_position = -1;
/* The target position is for the final component tag, so only use
@@ -840,6 +1067,7 @@ search_v2_xpath(const xmlNode *top, const char *key, int target_position)
switch (f) {
case 1:
+ // @COMPAT Remove when v1 patchsets are removed
target = first_matching_xml_child(target, tag, NULL,
current_position);
break;
@@ -886,8 +1114,8 @@ sort_change_obj_by_position(gconstpointer a, gconstpointer b)
int position_a = -1;
int position_b = -1;
- crm_element_value_int(change_obj_a->change, XML_DIFF_POSITION, &position_a);
- crm_element_value_int(change_obj_b->change, XML_DIFF_POSITION, &position_b);
+ crm_element_value_int(change_obj_a->change, PCMK_XE_POSITION, &position_a);
+ crm_element_value_int(change_obj_b->change, PCMK_XE_POSITION, &position_b);
if (position_a < position_b) {
return -1;
@@ -919,8 +1147,8 @@ apply_v2_patchset(xmlNode *xml, const xmlNode *patchset)
for (change = pcmk__xml_first_child(patchset); change != NULL;
change = pcmk__xml_next(change)) {
xmlNode *match = NULL;
- const char *op = crm_element_value(change, XML_DIFF_OP);
- const char *xpath = crm_element_value(change, XML_DIFF_PATH);
+ const char *op = crm_element_value(change, PCMK_XA_OPERATION);
+ const char *xpath = crm_element_value(change, PCMK_XA_PATH);
int position = -1;
if (op == NULL) {
@@ -929,14 +1157,16 @@ apply_v2_patchset(xmlNode *xml, const xmlNode *patchset)
crm_trace("Processing %s %s", change->name, op);
- // "delete" changes for XML comments are generated with "position"
- if (strcmp(op, "delete") == 0) {
- crm_element_value_int(change, XML_DIFF_POSITION, &position);
+ /* PCMK_VALUE_DELETE changes for XML comments are generated with
+ * PCMK_XE_POSITION
+ */
+ if (strcmp(op, PCMK_VALUE_DELETE) == 0) {
+ crm_element_value_int(change, PCMK_XE_POSITION, &position);
}
match = search_v2_xpath(xml, xpath, position);
crm_trace("Performing %s on %s with %p", op, xpath, match);
- if ((match == NULL) && (strcmp(op, "delete") == 0)) {
+ if ((match == NULL) && (strcmp(op, PCMK_VALUE_DELETE) == 0)) {
crm_debug("No %s match for %s in %p", op, xpath, xml->doc);
continue;
@@ -945,32 +1175,33 @@ apply_v2_patchset(xmlNode *xml, const xmlNode *patchset)
rc = pcmk_rc_diff_failed;
continue;
- } else if ((strcmp(op, "create") == 0) || (strcmp(op, "move") == 0)) {
- // Delay the adding of a "create" object
- xml_change_obj_t *change_obj = calloc(1, sizeof(xml_change_obj_t));
-
- CRM_ASSERT(change_obj != NULL);
+ } else if (pcmk__str_any_of(op,
+ PCMK_VALUE_CREATE, PCMK_VALUE_MOVE, NULL)) {
+ // Delay the adding of a PCMK_VALUE_CREATE object
+ xml_change_obj_t *change_obj =
+ pcmk__assert_alloc(1, sizeof(xml_change_obj_t));
change_obj->change = change;
change_obj->match = match;
change_objs = g_list_append(change_objs, change_obj);
- if (strcmp(op, "move") == 0) {
- // Temporarily put the "move" object after the last sibling
+ if (strcmp(op, PCMK_VALUE_MOVE) == 0) {
+ // Temporarily put the PCMK_VALUE_MOVE object after the last sibling
if ((match->parent != NULL) && (match->parent->last != NULL)) {
xmlAddNextSibling(match->parent->last, match);
}
}
- } else if (strcmp(op, "delete") == 0) {
+ } else if (strcmp(op, PCMK_VALUE_DELETE) == 0) {
free_xml(match);
- } else if (strcmp(op, "modify") == 0) {
- xmlNode *attrs = NULL;
+ } else if (strcmp(op, PCMK_VALUE_MODIFY) == 0) {
+ const xmlNode *child = pcmk__xe_first_child(change,
+ PCMK_XE_CHANGE_RESULT,
+ NULL, NULL);
+ const xmlNode *attrs = pcmk__xml_first_child(child);
- attrs = pcmk__xml_first_child(first_named_child(change,
- XML_DIFF_RESULT));
if (attrs == NULL) {
rc = ENOMSG;
continue;
@@ -1002,18 +1233,18 @@ apply_v2_patchset(xmlNode *xml, const xmlNode *patchset)
change = change_obj->change;
- op = crm_element_value(change, XML_DIFF_OP);
- xpath = crm_element_value(change, XML_DIFF_PATH);
+ op = crm_element_value(change, PCMK_XA_OPERATION);
+ xpath = crm_element_value(change, PCMK_XA_PATH);
crm_trace("Continue performing %s on %s with %p", op, xpath, match);
- if (strcmp(op, "create") == 0) {
+ if (strcmp(op, PCMK_VALUE_CREATE) == 0) {
int position = 0;
xmlNode *child = NULL;
xmlNode *match_child = NULL;
match_child = match->children;
- crm_element_value_int(change, XML_DIFF_POSITION, &position);
+ crm_element_value_int(change, PCMK_XE_POSITION, &position);
while ((match_child != NULL)
&& (position != pcmk__xml_position(match_child, pcmk__xf_skip))) {
@@ -1040,12 +1271,12 @@ apply_v2_patchset(xmlNode *xml, const xmlNode *patchset)
CRM_LOG_ASSERT(position == 0);
xmlAddChild(match, child);
}
- pcmk__mark_xml_created(child);
+ pcmk__xml_mark_created(child);
- } else if (strcmp(op, "move") == 0) {
+ } else if (strcmp(op, PCMK_VALUE_MOVE) == 0) {
int position = 0;
- crm_element_value_int(change, XML_DIFF_POSITION, &position);
+ crm_element_value_int(change, PCMK_XE_POSITION, &position);
if (position != pcmk__xml_position(match, pcmk__xf_skip)) {
xmlNode *match_child = NULL;
int p = position;
@@ -1083,7 +1314,7 @@ apply_v2_patchset(xmlNode *xml, const xmlNode *patchset)
if (position != pcmk__xml_position(match, pcmk__xf_skip)) {
crm_err("Moved %s.%s to position %d instead of %d (%p)",
- match->name, ID(match),
+ match->name, pcmk__xe_id(match),
pcmk__xml_position(match, pcmk__xf_skip),
position, match->prev);
rc = pcmk_rc_diff_failed;
@@ -1116,18 +1347,19 @@ xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version)
}
}
- digest = crm_element_value(patchset, XML_ATTR_DIGEST);
+ digest = crm_element_value(patchset, PCMK__XA_DIGEST);
if (digest != NULL) {
/* Make original XML available for logging in case result doesn't have
* expected digest
*/
- pcmk__if_tracing(old = copy_xml(xml), {});
+ pcmk__if_tracing(old = pcmk__xml_copy(NULL, xml), {});
}
if (rc == pcmk_ok) {
crm_element_value_int(patchset, PCMK_XA_FORMAT, &format);
switch (format) {
case 1:
+ // @COMPAT Remove when v1 patchsets are removed
rc = pcmk_rc2legacy(apply_v1_patchset(xml, patchset));
break;
case 2:
@@ -1141,7 +1373,7 @@ xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version)
if ((rc == pcmk_ok) && (digest != NULL)) {
char *new_digest = NULL;
- char *version = crm_element_value_copy(xml, XML_ATTR_CRM_VERSION);
+ char *version = crm_element_value_copy(xml, PCMK_XA_CRM_FEATURE_SET);
new_digest = calculate_xml_versioned_digest(xml, FALSE, TRUE, version);
if (!pcmk__str_eq(new_digest, digest, pcmk__str_casei)) {
@@ -1168,242 +1400,75 @@ xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version)
return rc;
}
-void
-purge_diff_markers(xmlNode *a_node)
-{
- xmlNode *child = NULL;
-
- CRM_CHECK(a_node != NULL, return);
-
- xml_remove_prop(a_node, XML_DIFF_MARKER);
- for (child = pcmk__xml_first_child(a_node); child != NULL;
- child = pcmk__xml_next(child)) {
- purge_diff_markers(child);
- }
-}
-
-xmlNode *
-diff_xml_object(xmlNode *old, xmlNode *new, gboolean suppress)
+// @COMPAT Remove when v1 patchsets are removed
+static bool
+can_prune_leaf_v1(xmlNode *node)
{
- xmlNode *tmp1 = NULL;
- xmlNode *diff = create_xml_node(NULL, XML_TAG_DIFF);
- xmlNode *removed = create_xml_node(diff, XML_TAG_DIFF_REMOVED);
- xmlNode *added = create_xml_node(diff, XML_TAG_DIFF_ADDED);
+ xmlNode *cIter = NULL;
+ bool can_prune = true;
- crm_xml_add(diff, XML_ATTR_CRM_VERSION, CRM_FEATURE_SET);
+ CRM_CHECK(node != NULL, return false);
- tmp1 = subtract_xml_object(removed, old, new, FALSE, NULL, "removed:top");
- if (suppress && (tmp1 != NULL) && can_prune_leaf(tmp1)) {
- free_xml(tmp1);
+ /* @COMPAT PCMK__XE_ROLE_REF was deprecated in Pacemaker 1.1.12 (needed for
+ * rolling upgrades)
+ */
+ if (pcmk__strcase_any_of((const char *) node->name,
+ PCMK_XE_RESOURCE_REF, PCMK_XE_OBJ_REF,
+ PCMK_XE_ROLE, PCMK__XE_ROLE_REF,
+ NULL)) {
+ return false;
}
- tmp1 = subtract_xml_object(added, new, old, TRUE, NULL, "added:top");
- if (suppress && (tmp1 != NULL) && can_prune_leaf(tmp1)) {
- free_xml(tmp1);
- }
+ for (xmlAttrPtr a = pcmk__xe_first_attr(node); a != NULL; a = a->next) {
+ const char *p_name = (const char *) a->name;
- if ((added->children == NULL) && (removed->children == NULL)) {
- free_xml(diff);
- diff = NULL;
+ if (strcmp(p_name, PCMK_XA_ID) == 0) {
+ continue;
+ }
+ can_prune = false;
}
- return diff;
-}
-
-static xmlNode *
-subtract_xml_comment(xmlNode *parent, xmlNode *left, xmlNode *right,
- gboolean *changed)
-{
- CRM_CHECK(left != NULL, return NULL);
- CRM_CHECK(left->type == XML_COMMENT_NODE, return NULL);
-
- if ((right == NULL) || !pcmk__str_eq((const char *)left->content,
- (const char *)right->content,
- pcmk__str_casei)) {
- xmlNode *deleted = NULL;
-
- deleted = add_node_copy(parent, left);
- *changed = TRUE;
+ cIter = pcmk__xml_first_child(node);
+ while (cIter) {
+ xmlNode *child = cIter;
- return deleted;
+ cIter = pcmk__xml_next(cIter);
+ if (can_prune_leaf_v1(child)) {
+ free_xml(child);
+ } else {
+ can_prune = false;
+ }
}
-
- return NULL;
+ return can_prune;
}
+// @COMPAT Remove when v1 patchsets are removed
xmlNode *
-subtract_xml_object(xmlNode *parent, xmlNode *left, xmlNode *right,
- gboolean full, gboolean *changed, const char *marker)
+pcmk__diff_v1_xml_object(xmlNode *old, xmlNode *new, bool suppress)
{
- gboolean dummy = FALSE;
- xmlNode *diff = NULL;
- xmlNode *right_child = NULL;
- xmlNode *left_child = NULL;
- xmlAttrPtr xIter = NULL;
-
- const char *id = NULL;
- const char *name = NULL;
- const char *value = NULL;
- const char *right_val = NULL;
-
- if (changed == NULL) {
- changed = &dummy;
- }
-
- if (left == NULL) {
- return NULL;
- }
-
- if (left->type == XML_COMMENT_NODE) {
- return subtract_xml_comment(parent, left, right, changed);
- }
-
- id = ID(left);
- name = (const char *) left->name;
- if (right == NULL) {
- xmlNode *deleted = NULL;
-
- crm_trace("Processing <%s " XML_ATTR_ID "=%s> (complete copy)",
- name, id);
- deleted = add_node_copy(parent, left);
- crm_xml_add(deleted, XML_DIFF_MARKER, marker);
-
- *changed = TRUE;
- return deleted;
- }
-
- CRM_CHECK(name != NULL, return NULL);
- CRM_CHECK(pcmk__xe_is(left, (const char *) right->name), return NULL);
-
- // Check for XML_DIFF_MARKER in a child
- value = crm_element_value(right, XML_DIFF_MARKER);
- if ((value != NULL) && (strcmp(value, "removed:top") == 0)) {
- crm_trace("We are the root of the deletion: %s.id=%s", name, id);
- *changed = TRUE;
- return NULL;
- }
-
- // @TODO Avoiding creating the full hierarchy would save work here
- diff = create_xml_node(parent, name);
-
- // Changes to child objects
- for (left_child = pcmk__xml_first_child(left); left_child != NULL;
- left_child = pcmk__xml_next(left_child)) {
- gboolean child_changed = FALSE;
-
- right_child = pcmk__xml_match(right, left_child, false);
- subtract_xml_object(diff, left_child, right_child, full, &child_changed,
- marker);
- if (child_changed) {
- *changed = TRUE;
- }
- }
-
- if (!*changed) {
- /* Nothing to do */
-
- } else if (full) {
- xmlAttrPtr pIter = NULL;
-
- for (pIter = pcmk__xe_first_attr(left); pIter != NULL;
- pIter = pIter->next) {
- const char *p_name = (const char *)pIter->name;
- const char *p_value = pcmk__xml_attr_value(pIter);
+ xmlNode *tmp1 = NULL;
+ xmlNode *diff = pcmk__xe_create(NULL, PCMK_XE_DIFF);
+ xmlNode *removed = pcmk__xe_create(diff, PCMK__XE_DIFF_REMOVED);
+ xmlNode *added = pcmk__xe_create(diff, PCMK__XE_DIFF_ADDED);
- xmlSetProp(diff, (pcmkXmlStr) p_name, (pcmkXmlStr) p_value);
- }
+ crm_xml_add(diff, PCMK_XA_CRM_FEATURE_SET, CRM_FEATURE_SET);
- // We have everything we need
- goto done;
+ tmp1 = subtract_v1_xml_object(removed, old, new, false, NULL,
+ "removed:top");
+ if (suppress && (tmp1 != NULL) && can_prune_leaf_v1(tmp1)) {
+ free_xml(tmp1);
}
- // Changes to name/value pairs
- for (xIter = pcmk__xe_first_attr(left); xIter != NULL;
- xIter = xIter->next) {
- const char *prop_name = (const char *) xIter->name;
- xmlAttrPtr right_attr = NULL;
- xml_node_private_t *nodepriv = NULL;
-
- if (strcmp(prop_name, XML_ATTR_ID) == 0) {
- // id already obtained when present ~ this case, so just reuse
- xmlSetProp(diff, (pcmkXmlStr) XML_ATTR_ID, (pcmkXmlStr) id);
- continue;
- }
-
- if (pcmk__xa_filterable(prop_name)) {
- continue;
- }
-
- right_attr = xmlHasProp(right, (pcmkXmlStr) prop_name);
- if (right_attr) {
- nodepriv = right_attr->_private;
- }
-
- right_val = crm_element_value(right, prop_name);
- if ((right_val == NULL) || (nodepriv && pcmk_is_set(nodepriv->flags, pcmk__xf_deleted))) {
- /* new */
- *changed = TRUE;
- if (full) {
- xmlAttrPtr pIter = NULL;
-
- for (pIter = pcmk__xe_first_attr(left); pIter != NULL;
- pIter = pIter->next) {
- const char *p_name = (const char *) pIter->name;
- const char *p_value = pcmk__xml_attr_value(pIter);
-
- xmlSetProp(diff, (pcmkXmlStr) p_name, (pcmkXmlStr) p_value);
- }
- break;
-
- } else {
- const char *left_value = pcmk__xml_attr_value(xIter);
-
- xmlSetProp(diff, (pcmkXmlStr) prop_name, (pcmkXmlStr) value);
- crm_xml_add(diff, prop_name, left_value);
- }
-
- } else {
- /* Only now do we need the left value */
- const char *left_value = pcmk__xml_attr_value(xIter);
-
- if (strcmp(left_value, right_val) == 0) {
- /* unchanged */
-
- } else {
- *changed = TRUE;
- if (full) {
- xmlAttrPtr pIter = NULL;
-
- crm_trace("Changes detected to %s in "
- "<%s " XML_ATTR_ID "=%s>", prop_name, name, id);
- for (pIter = pcmk__xe_first_attr(left); pIter != NULL;
- pIter = pIter->next) {
- const char *p_name = (const char *) pIter->name;
- const char *p_value = pcmk__xml_attr_value(pIter);
-
- xmlSetProp(diff, (pcmkXmlStr) p_name,
- (pcmkXmlStr) p_value);
- }
- break;
-
- } else {
- crm_trace("Changes detected to %s (%s -> %s) in "
- "<%s " XML_ATTR_ID "=%s>",
- prop_name, left_value, right_val, name, id);
- crm_xml_add(diff, prop_name, left_value);
- }
- }
- }
+ tmp1 = subtract_v1_xml_object(added, new, old, true, NULL, "added:top");
+ if (suppress && (tmp1 != NULL) && can_prune_leaf_v1(tmp1)) {
+ free_xml(tmp1);
}
- if (!*changed) {
+ if ((added->children == NULL) && (removed->children == NULL)) {
free_xml(diff);
- return NULL;
-
- } else if (!full && (id != NULL)) {
- crm_xml_add(diff, XML_ATTR_ID, id);
+ diff = NULL;
}
- done:
+
return diff;
}
@@ -1417,12 +1482,14 @@ apply_xml_diff(xmlNode *old_xml, xmlNode *diff, xmlNode **new_xml)
{
gboolean result = TRUE;
int root_nodes_seen = 0;
- const char *digest = crm_element_value(diff, XML_ATTR_DIGEST);
- const char *version = crm_element_value(diff, XML_ATTR_CRM_VERSION);
+ const char *digest = crm_element_value(diff, PCMK__XA_DIGEST);
+ const char *version = crm_element_value(diff, PCMK_XA_CRM_FEATURE_SET);
xmlNode *child_diff = NULL;
- xmlNode *added = find_xml_node(diff, XML_TAG_DIFF_ADDED, FALSE);
- xmlNode *removed = find_xml_node(diff, XML_TAG_DIFF_REMOVED, FALSE);
+ xmlNode *added = pcmk__xe_first_child(diff, PCMK__XE_DIFF_ADDED, NULL,
+ NULL);
+ xmlNode *removed = pcmk__xe_first_child(diff, PCMK__XE_DIFF_REMOVED, NULL,
+ NULL);
CRM_CHECK(new_xml != NULL, return FALSE);
@@ -1431,14 +1498,14 @@ apply_xml_diff(xmlNode *old_xml, xmlNode *diff, xmlNode **new_xml)
child_diff = pcmk__xml_next(child_diff)) {
CRM_CHECK(root_nodes_seen == 0, result = FALSE);
if (root_nodes_seen == 0) {
- *new_xml = subtract_xml_object(NULL, old_xml, child_diff, FALSE,
- NULL, NULL);
+ *new_xml = subtract_v1_xml_object(NULL, old_xml, child_diff, false,
+ NULL, NULL);
}
root_nodes_seen++;
}
if (root_nodes_seen == 0) {
- *new_xml = copy_xml(old_xml);
+ *new_xml = pcmk__xml_copy(NULL, old_xml);
} else if (root_nodes_seen > 1) {
crm_err("(-) Diffs cannot contain more than one change set... saw %d",
@@ -1455,7 +1522,8 @@ apply_xml_diff(xmlNode *old_xml, xmlNode *diff, xmlNode **new_xml)
child_diff = pcmk__xml_next(child_diff)) {
CRM_CHECK(root_nodes_seen == 0, result = FALSE);
if (root_nodes_seen == 0) {
- pcmk__xml_update(NULL, *new_xml, child_diff, true);
+ pcmk__xml_update(NULL, *new_xml, child_diff, pcmk__xaf_none,
+ true);
}
root_nodes_seen++;
}
@@ -1469,7 +1537,7 @@ apply_xml_diff(xmlNode *old_xml, xmlNode *diff, xmlNode **new_xml)
} else if (result && (digest != NULL)) {
char *new_digest = NULL;
- purge_diff_markers(*new_xml); // Purge now so diff is ok
+ purge_v1_diff_markers(*new_xml); // Purge now so diff is ok
new_digest = calculate_xml_versioned_digest(*new_xml, FALSE, TRUE,
version);
if (!pcmk__str_eq(new_digest, digest, pcmk__str_casei)) {
@@ -1493,11 +1561,36 @@ apply_xml_diff(xmlNode *old_xml, xmlNode *diff, xmlNode **new_xml)
free(new_digest);
} else if (result) {
- purge_diff_markers(*new_xml); // Purge now so diff is ok
+ purge_v1_diff_markers(*new_xml); // Purge now so diff is ok
}
return result;
}
+void
+purge_diff_markers(xmlNode *a_node)
+{
+ purge_v1_diff_markers(a_node);
+}
+
+xmlNode *
+diff_xml_object(xmlNode *old, xmlNode *new, gboolean suppress)
+{
+ return pcmk__diff_v1_xml_object(old, new, suppress);
+}
+
+xmlNode *
+subtract_xml_object(xmlNode *parent, xmlNode *left, xmlNode *right,
+ gboolean full, gboolean *changed, const char *marker)
+{
+ return subtract_v1_xml_object(parent, left, right, full, changed, marker);
+}
+
+gboolean
+can_prune_leaf(xmlNode *xml_node)
+{
+ return can_prune_leaf_v1(xml_node);
+}
+
// LCOV_EXCL_STOP
// End deprecated API
diff --git a/lib/common/patchset_display.c b/lib/common/patchset_display.c
index 5cc0b52..1351c86 100644
--- a/lib/common/patchset_display.c
+++ b/lib/common/patchset_display.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2004-2023 the Pacemaker project contributors
+ * Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -9,7 +9,7 @@
#include <crm_internal.h>
-#include <crm/msg_xml.h>
+#include <crm/common/xml.h>
#include "crmcommon_private.h"
@@ -22,9 +22,9 @@
*
* All header lines contain three integers separated by dots, of the form
* <tt>{0}.{1}.{2}</tt>:
- * * \p {0}: \p XML_ATTR_GENERATION_ADMIN
- * * \p {1}: \p XML_ATTR_GENERATION
- * * \p {2}: \p XML_ATTR_NUMUPDATES
+ * * \p {0}: \c PCMK_XA_ADMIN_EPOCH
+ * * \p {1}: \c PCMK_XA_EPOCH
+ * * \p {2}: \c PCMK_XA_NUM_UPDATES
*
* Lines containing \p "---" describe removals and end with the patch format
* number. Lines containing \p "+++" describe additions and end with the patch
@@ -48,7 +48,7 @@ xml_show_patchset_header(pcmk__output_t *out, const xmlNode *patchset)
if ((add[0] != del[0]) || (add[1] != del[1]) || (add[2] != del[2])) {
const char *fmt = crm_element_value(patchset, PCMK_XA_FORMAT);
- const char *digest = crm_element_value(patchset, XML_ATTR_DIGEST);
+ const char *digest = crm_element_value(patchset, PCMK__XA_DIGEST);
out->info(out, "Diff: --- %d.%d.%d %s", del[0], del[1], del[2], fmt);
rc = out->info(out, "Diff: +++ %d.%d.%d %s",
@@ -81,7 +81,7 @@ xml_show_patchset_v1_recursive(pcmk__output_t *out, const char *prefix,
const xmlNode *data, int depth, uint32_t options)
{
if ((data->children == NULL)
- || (crm_element_value(data, XML_DIFF_MARKER) != NULL)) {
+ || (crm_element_value(data, PCMK__XA_CRM_DIFF_MARKER) != NULL)) {
// Found a change; clear the pcmk__xml_fmt_diff_short option if set
options &= ~pcmk__xml_fmt_diff_short;
@@ -143,7 +143,7 @@ xml_show_patchset_v1(pcmk__output_t *out, const xmlNode *patchset,
* However, v1 patchsets can only exist during rolling upgrades from
* Pacemaker 1.1.11, so not worth worrying about.
*/
- removed = find_xml_node(patchset, XML_TAG_DIFF_REMOVED, FALSE);
+ removed = pcmk__xe_first_child(patchset, PCMK__XE_DIFF_REMOVED, NULL, NULL);
for (child = pcmk__xml_first_child(removed); child != NULL;
child = pcmk__xml_next(child)) {
int temp_rc = xml_show_patchset_v1_recursive(out, "- ", child, 0,
@@ -159,7 +159,7 @@ xml_show_patchset_v1(pcmk__output_t *out, const xmlNode *patchset,
}
is_first = true;
- added = find_xml_node(patchset, XML_TAG_DIFF_ADDED, FALSE);
+ added = pcmk__xe_first_child(patchset, PCMK__XE_DIFF_ADDED, NULL, NULL);
for (child = pcmk__xml_first_child(added); child != NULL;
child = pcmk__xml_next(child)) {
int temp_rc = xml_show_patchset_v1_recursive(out, "+ ", child, 0,
@@ -197,16 +197,18 @@ xml_show_patchset_v2(pcmk__output_t *out, const xmlNode *patchset)
int rc = xml_show_patchset_header(out, patchset);
int temp_rc = pcmk_rc_no_output;
- for (const xmlNode *change = pcmk__xml_first_child(patchset);
- change != NULL; change = pcmk__xml_next(change)) {
- const char *op = crm_element_value(change, XML_DIFF_OP);
- const char *xpath = crm_element_value(change, XML_DIFF_PATH);
+ for (const xmlNode *change = pcmk__xe_first_child(patchset, NULL, NULL,
+ NULL);
+ change != NULL; change = pcmk__xe_next(change)) {
+
+ const char *op = crm_element_value(change, PCMK_XA_OPERATION);
+ const char *xpath = crm_element_value(change, PCMK_XA_PATH);
if (op == NULL) {
continue;
}
- if (strcmp(op, "create") == 0) {
+ if (strcmp(op, PCMK_VALUE_CREATE) == 0) {
char *prefix = crm_strdup_printf(PCMK__XML_PREFIX_CREATED " %s: ",
xpath);
@@ -226,30 +228,33 @@ xml_show_patchset_v2(pcmk__output_t *out, const xmlNode *patchset)
rc = pcmk__output_select_rc(rc, temp_rc);
free(prefix);
- } else if (strcmp(op, "move") == 0) {
- const char *position = crm_element_value(change, XML_DIFF_POSITION);
+ } else if (strcmp(op, PCMK_VALUE_MOVE) == 0) {
+ const char *position = crm_element_value(change, PCMK_XE_POSITION);
temp_rc = out->info(out,
PCMK__XML_PREFIX_MOVED " %s moved to offset %s",
xpath, position);
rc = pcmk__output_select_rc(rc, temp_rc);
- } else if (strcmp(op, "modify") == 0) {
- xmlNode *clist = first_named_child(change, XML_DIFF_LIST);
+ } else if (strcmp(op, PCMK_VALUE_MODIFY) == 0) {
+ xmlNode *clist = pcmk__xe_first_child(change, PCMK_XE_CHANGE_LIST,
+ NULL, NULL);
GString *buffer_set = NULL;
GString *buffer_unset = NULL;
- for (const xmlNode *child = pcmk__xml_first_child(clist);
- child != NULL; child = pcmk__xml_next(child)) {
- const char *name = crm_element_value(child, "name");
+ for (const xmlNode *child = pcmk__xe_first_child(clist, NULL, NULL,
+ NULL);
+ child != NULL; child = pcmk__xe_next(child)) {
+
+ const char *name = crm_element_value(child, PCMK_XA_NAME);
- op = crm_element_value(child, XML_DIFF_OP);
+ op = crm_element_value(child, PCMK_XA_OPERATION);
if (op == NULL) {
continue;
}
if (strcmp(op, "set") == 0) {
- const char *value = crm_element_value(child, "value");
+ const char *value = crm_element_value(child, PCMK_XA_VALUE);
pcmk__add_separated_word(&buffer_set, 256, "@", ", ");
pcmk__g_strcat(buffer_set, name, "=", value, NULL);
@@ -273,10 +278,10 @@ xml_show_patchset_v2(pcmk__output_t *out, const xmlNode *patchset)
g_string_free(buffer_unset, TRUE);
}
- } else if (strcmp(op, "delete") == 0) {
+ } else if (strcmp(op, PCMK_VALUE_DELETE) == 0) {
int position = -1;
- crm_element_value_int(change, XML_DIFF_POSITION, &position);
+ crm_element_value_int(change, PCMK_XE_POSITION, &position);
if (position >= 0) {
temp_rc = out->info(out, "-- %s (%d)", xpath, position);
} else {
@@ -301,7 +306,8 @@ xml_show_patchset_v2(pcmk__output_t *out, const xmlNode *patchset)
*
* \return Standard Pacemaker return code
*
- * \note \p args should contain only the XML patchset
+ * \note \p args should contain the following:
+ * -# XML patchset
*/
PCMK__OUTPUT_ARGS("xml-patchset", "const xmlNode *")
static int
@@ -340,7 +346,8 @@ xml_patchset_default(pcmk__output_t *out, va_list args)
*
* \return Standard Pacemaker return code
*
- * \note \p args should contain only the XML patchset
+ * \note \p args should contain the following:
+ * -# XML patchset
*/
PCMK__OUTPUT_ARGS("xml-patchset", "const xmlNode *")
static int
@@ -402,7 +409,8 @@ xml_patchset_log(pcmk__output_t *out, va_list args)
*
* \return Standard Pacemaker return code
*
- * \note \p args should contain only the XML patchset
+ * \note \p args should contain the following:
+ * -# XML patchset
*/
PCMK__OUTPUT_ARGS("xml-patchset", "const xmlNode *")
static int
@@ -411,10 +419,13 @@ xml_patchset_xml(pcmk__output_t *out, va_list args)
const xmlNode *patchset = va_arg(args, const xmlNode *);
if (patchset != NULL) {
- char *buf = dump_xml_formatted_with_text(patchset);
+ GString *buf = g_string_sized_new(1024);
+
+ pcmk__xml_string(patchset, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, buf,
+ 0);
- out->output_xml(out, "xml-patchset", buf);
- free(buf);
+ out->output_xml(out, PCMK_XE_XML_PATCHSET, buf->str);
+ g_string_free(buf, TRUE);
return pcmk_rc_ok;
}
crm_trace("Empty patch");
diff --git a/lib/common/probes.c b/lib/common/probes.c
new file mode 100644
index 0000000..39a3905
--- /dev/null
+++ b/lib/common/probes.c
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2004-2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU Lesser General Public License
+ * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h> // pcmk__str_eq(), etc.
+
+#include <stdio.h> // NULL
+#include <stdbool.h> // bool, true, false
+#include <glib.h> // guint
+#include <libxml/tree.h> // xmlNode
+
+#include <crm/common/options.h> // PCMK_META_INTERVAL
+#include <crm/common/xml.h> // PCMK_XA_OPERATION
+
+/*!
+ * \brief Check whether an action name and interval represent a probe
+ *
+ * \param[in] task Action name
+ * \param[in] interval_ms Action interval in milliseconds
+ *
+ * \return true if \p task is \c PCMK_ACTION_MONITOR and \p interval_ms is 0,
+ * otherwise false
+ */
+bool
+pcmk_is_probe(const char *task, guint interval_ms)
+{
+ // @COMPAT This should be made inline at an API compatibility break
+ return (interval_ms == 0)
+ && pcmk__str_eq(task, PCMK_ACTION_MONITOR, pcmk__str_none);
+}
+
+/*!
+ * \brief Check whether an action history entry represents a probe
+ *
+ * \param[in] xml XML of action history entry
+ *
+ * \return true if \p xml is for a probe action, otherwise false
+ */
+bool
+pcmk_xe_is_probe(const xmlNode *xml)
+{
+ int interval_ms = 0;
+
+ if (xml == NULL) {
+ return false;
+ }
+
+ pcmk__scan_min_int(crm_element_value(xml, PCMK_META_INTERVAL),
+ &interval_ms, 0);
+
+ return pcmk_is_probe(crm_element_value(xml, PCMK_XA_OPERATION),
+ interval_ms);
+}
+
+/*!
+ * \brief Check whether an action history entry represents a maskable probe
+ *
+ * \param[in] xml XML of action history entry
+ *
+ * \return true if \p xml is for a failed probe action that should be treated as
+ * successful, otherwise false
+ */
+bool
+pcmk_xe_mask_probe_failure(const xmlNode *xml)
+{
+ int exec_status = PCMK_EXEC_UNKNOWN;
+ int exit_status = PCMK_OCF_OK;
+
+ if (!pcmk_xe_is_probe(xml)) {
+ return false;
+ }
+
+ crm_element_value_int(xml, PCMK__XA_OP_STATUS, &exec_status);
+ crm_element_value_int(xml, PCMK__XA_RC_CODE, &exit_status);
+
+ return (exit_status == PCMK_OCF_NOT_INSTALLED)
+ || (exit_status == PCMK_OCF_INVALID_PARAM)
+ || (exec_status == PCMK_EXEC_NOT_INSTALLED);
+}
diff --git a/lib/common/remote.c b/lib/common/remote.c
index fe19296..9f8419b 100644
--- a/lib/common/remote.c
+++ b/lib/common/remote.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2008-2023 the Pacemaker project contributors
+ * Copyright 2008-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -493,24 +493,25 @@ pcmk__remote_send_xml(pcmk__remote_t *remote, const xmlNode *msg)
{
int rc = pcmk_rc_ok;
static uint64_t id = 0;
- char *xml_text = NULL;
+ GString *xml_text = NULL;
struct iovec iov[2];
struct remote_header_v0 *header;
CRM_CHECK((remote != NULL) && (msg != NULL), return EINVAL);
- xml_text = dump_xml_unformatted(msg);
- CRM_CHECK(xml_text != NULL, return EINVAL);
+ xml_text = g_string_sized_new(1024);
+ pcmk__xml_string(msg, 0, xml_text, 0);
+ CRM_CHECK(xml_text->len > 0,
+ g_string_free(xml_text, TRUE); return EINVAL);
- header = calloc(1, sizeof(struct remote_header_v0));
- CRM_ASSERT(header != NULL);
+ header = pcmk__assert_alloc(1, sizeof(struct remote_header_v0));
iov[0].iov_base = header;
iov[0].iov_len = sizeof(struct remote_header_v0);
- iov[1].iov_base = xml_text;
- iov[1].iov_len = 1 + strlen(xml_text);
+ iov[1].iov_len = 1 + xml_text->len;
+ iov[1].iov_base = g_string_free(xml_text, FALSE);
id++;
header->id = id;
@@ -527,7 +528,7 @@ pcmk__remote_send_xml(pcmk__remote_t *remote, const xmlNode *msg)
}
free(iov[0].iov_base);
- free(iov[1].iov_base);
+ g_free((gchar *) iov[1].iov_base);
return rc;
}
@@ -554,7 +555,8 @@ pcmk__remote_message_xml(pcmk__remote_t *remote)
if (header->payload_compressed) {
int rc = 0;
unsigned int size_u = 1 + header->payload_uncompressed;
- char *uncompressed = calloc(1, header->payload_offset + size_u);
+ char *uncompressed =
+ pcmk__assert_alloc(1, header->payload_offset + size_u);
crm_trace("Decompressing message data %d bytes into %d bytes",
header->payload_compressed, size_u);
@@ -592,7 +594,7 @@ pcmk__remote_message_xml(pcmk__remote_t *remote)
CRM_LOG_ASSERT(remote->buffer[sizeof(struct remote_header_v0) + header->payload_uncompressed - 1] == 0);
- xml = string2xml(remote->buffer + header->payload_offset);
+ xml = pcmk__xml_parse(remote->buffer + header->payload_offset);
if (xml == NULL && header->version > REMOTE_MSG_VERSION) {
crm_warn("Couldn't parse v%d message, we only understand v%d",
header->version, REMOTE_MSG_VERSION);
@@ -978,7 +980,7 @@ connect_socket_retry(int sock, const struct sockaddr *addr, socklen_t addrlen,
return rc;
}
- cb_data = calloc(1, sizeof(struct tcp_async_cb_data));
+ cb_data = pcmk__assert_alloc(1, sizeof(struct tcp_async_cb_data));
cb_data->userdata = userdata;
cb_data->callback = callback;
cb_data->sock = sock;
@@ -1206,6 +1208,9 @@ pcmk__accept_remote_connection(int ssock, int *csock)
struct sockaddr_storage addr;
socklen_t laddr = sizeof(addr);
char addr_str[INET6_ADDRSTRLEN];
+#ifdef TCP_USER_TIMEOUT
+ long sbd_timeout = 0;
+#endif
/* accept the connection */
memset(&addr, 0, sizeof(addr));
@@ -1229,9 +1234,11 @@ pcmk__accept_remote_connection(int ssock, int *csock)
}
#ifdef TCP_USER_TIMEOUT
- if (pcmk__get_sbd_timeout() > 0) {
+ sbd_timeout = pcmk__get_sbd_watchdog_timeout();
+ if (sbd_timeout > 0) {
// Time to fail and retry before watchdog
- unsigned int optval = (unsigned int) pcmk__get_sbd_timeout() / 2;
+ long half = sbd_timeout / 2;
+ unsigned int optval = (half <= UINT_MAX)? half : UINT_MAX;
rc = setsockopt(*csock, SOL_TCP, TCP_USER_TIMEOUT,
&optval, sizeof(optval));
diff --git a/lib/common/resources.c b/lib/common/resources.c
new file mode 100644
index 0000000..46c038b
--- /dev/null
+++ b/lib/common/resources.c
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU Lesser General Public License
+ * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h> // NULL
+#include <stdbool.h> // bool, false
+
+#include <crm/common/scheduler.h>
+#include <crm/common/scheduler_internal.h>
+
+/*!
+ * \internal
+ * \brief Get a resource's ID
+ *
+ * \param[in] rsc Resource to check
+ *
+ * \return ID of \p rsc (or NULL if \p rsc is NULL)
+ */
+const char *
+pcmk_resource_id(const pcmk_resource_t *rsc)
+{
+ return (rsc == NULL)? NULL : rsc->id;
+}
+
+/*!
+ * \internal
+ * \brief Check whether a resource is managed by the cluster
+ *
+ * \param[in] rsc Resource to check
+ *
+ * \return true if \p rsc is managed, otherwise false
+ */
+bool
+pcmk_resource_is_managed(const pcmk_resource_t *rsc)
+{
+ return (rsc == NULL)? false : pcmk_is_set(rsc->flags, pcmk_rsc_managed);
+}
+
+/*!
+ * \brief Get readable description of a multiply-active recovery type
+ *
+ * \param[in] recovery Recovery type
+ *
+ * \return Static string describing \p recovery
+ */
+const char *
+pcmk__multiply_active_text(enum rsc_recovery_type recovery)
+{
+ switch (recovery) {
+ case pcmk_multiply_active_stop:
+ return "shutting it down";
+ case pcmk_multiply_active_restart:
+ return "attempting recovery";
+ case pcmk_multiply_active_block:
+ return "waiting for an administrator";
+ case pcmk_multiply_active_unexpected:
+ return "stopping unexpected instances";
+ }
+ return "Unknown";
+}
diff --git a/lib/common/results.c b/lib/common/results.c
index dde8b27..396ac0d 100644
--- a/lib/common/results.c
+++ b/lib/common/results.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2004-2023 the Pacemaker project contributors
+ * Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -1091,9 +1091,9 @@ pcmk__copy_result(const pcmk__action_result_t *src, pcmk__action_result_t *dst)
CRM_CHECK((src != NULL) && (dst != NULL), return);
dst->exit_status = src->exit_status;
dst->execution_status = src->execution_status;
- pcmk__str_update(&dst->exit_reason, src->exit_reason);
- pcmk__str_update(&dst->action_stdout, src->action_stdout);
- pcmk__str_update(&dst->action_stderr, src->action_stderr);
+ dst->exit_reason = pcmk__str_copy(src->exit_reason);
+ dst->action_stdout = pcmk__str_copy(src->action_stdout);
+ dst->action_stderr = pcmk__str_copy(src->action_stderr);
}
// Deprecated functions kept only for backward API compatibility
diff --git a/lib/common/roles.c b/lib/common/roles.c
new file mode 100644
index 0000000..96aa72a
--- /dev/null
+++ b/lib/common/roles.c
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2004-2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU Lesser General Public License
+ * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/scheduler.h>
+#include <crm/common/scheduler_internal.h>
+
+/*!
+ * \brief Get readable description of a resource role
+ *
+ * \param[in] role Resource role
+ *
+ * \return Static string describing \p role, suitable for logging or display
+ */
+const char *
+pcmk_role_text(enum rsc_role_e role)
+{
+ switch (role) {
+ case pcmk_role_stopped:
+ return PCMK_ROLE_STOPPED;
+
+ case pcmk_role_started:
+ return PCMK_ROLE_STARTED;
+
+ case pcmk_role_unpromoted:
+#ifdef PCMK__COMPAT_2_0
+ return PCMK__ROLE_UNPROMOTED_LEGACY;
+#else
+ return PCMK_ROLE_UNPROMOTED;
+#endif
+
+ case pcmk_role_promoted:
+#ifdef PCMK__COMPAT_2_0
+ return PCMK__ROLE_PROMOTED_LEGACY;
+#else
+ return PCMK_ROLE_PROMOTED;
+#endif
+
+ default: // pcmk_role_unknown
+ return PCMK__ROLE_UNKNOWN;
+ }
+}
+
+/*!
+ * \brief Parse a resource role from a string role specification
+ *
+ * \param[in] role Role specification
+ *
+ * \return Resource role corresponding to \p role
+ */
+enum rsc_role_e
+pcmk_parse_role(const char *role)
+{
+ if (pcmk__str_eq(role, PCMK__ROLE_UNKNOWN,
+ pcmk__str_casei|pcmk__str_null_matches)) {
+ return pcmk_role_unknown;
+ } else if (pcmk__str_eq(role, PCMK_ROLE_STOPPED, pcmk__str_casei)) {
+ return pcmk_role_stopped;
+ } else if (pcmk__str_eq(role, PCMK_ROLE_STARTED, pcmk__str_casei)) {
+ return pcmk_role_started;
+ } else if (pcmk__str_eq(role, PCMK__ROLE_UNPROMOTED_LEGACY, pcmk__str_casei)) {
+ pcmk__warn_once(pcmk__wo_slave_role,
+ "Support for the " PCMK__ROLE_UNPROMOTED_LEGACY
+ " role is deprecated and will be removed in a "
+ "future release. Use " PCMK_ROLE_UNPROMOTED
+ " instead.");
+ return pcmk_role_unpromoted;
+ } else if (pcmk__str_eq(role, PCMK_ROLE_UNPROMOTED, pcmk__str_casei)) {
+ return pcmk_role_unpromoted;
+ } else if (pcmk__str_eq(role, PCMK__ROLE_PROMOTED_LEGACY, pcmk__str_casei)) {
+ pcmk__warn_once(pcmk__wo_master_role,
+ "Support for the " PCMK__ROLE_PROMOTED_LEGACY
+ " role is deprecated and will be removed in a "
+ "future release. Use " PCMK_ROLE_PROMOTED
+ " instead.");
+ return pcmk_role_promoted;
+ } else if (pcmk__str_eq(role, PCMK_ROLE_PROMOTED, pcmk__str_casei)) {
+ return pcmk_role_promoted;
+ }
+ return pcmk_role_unknown; // Invalid role given
+}
diff --git a/lib/common/rules.c b/lib/common/rules.c
new file mode 100644
index 0000000..32af835
--- /dev/null
+++ b/lib/common/rules.c
@@ -0,0 +1,1512 @@
+/*
+ * Copyright 2004-2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU Lesser General Public License
+ * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h> // NULL, size_t
+#include <stdbool.h> // bool
+#include <ctype.h> // isdigit()
+#include <regex.h> // regmatch_t
+#include <stdint.h> // uint32_t
+#include <inttypes.h> // PRIu32
+#include <glib.h> // gboolean, FALSE
+#include <libxml/tree.h> // xmlNode
+
+#include <crm/common/scheduler.h>
+
+#include <crm/common/iso8601_internal.h>
+#include <crm/common/nvpair_internal.h>
+#include <crm/common/scheduler_internal.h>
+#include "crmcommon_private.h"
+
+/*!
+ * \internal
+ * \brief Get the condition type corresponding to given condition XML
+ *
+ * \param[in] condition Rule condition XML
+ *
+ * \return Condition type corresponding to \p condition
+ */
+enum expression_type
+pcmk__condition_type(const xmlNode *condition)
+{
+ const char *name = NULL;
+
+ // Expression types based on element name
+
+ if (pcmk__xe_is(condition, PCMK_XE_DATE_EXPRESSION)) {
+ return pcmk__condition_datetime;
+
+ } else if (pcmk__xe_is(condition, PCMK_XE_RSC_EXPRESSION)) {
+ return pcmk__condition_resource;
+
+ } else if (pcmk__xe_is(condition, PCMK_XE_OP_EXPRESSION)) {
+ return pcmk__condition_operation;
+
+ } else if (pcmk__xe_is(condition, PCMK_XE_RULE)) {
+ return pcmk__condition_rule;
+
+ } else if (!pcmk__xe_is(condition, PCMK_XE_EXPRESSION)) {
+ return pcmk__condition_unknown;
+ }
+
+ // Expression types based on node attribute name
+
+ name = crm_element_value(condition, PCMK_XA_ATTRIBUTE);
+
+ if (pcmk__str_any_of(name, CRM_ATTR_UNAME, CRM_ATTR_KIND, CRM_ATTR_ID,
+ NULL)) {
+ return pcmk__condition_location;
+ }
+
+ return pcmk__condition_attribute;
+}
+
+/*!
+ * \internal
+ * \brief Get parent XML element's ID for logging purposes
+ *
+ * \param[in] xml XML of a subelement
+ *
+ * \return ID of \p xml's parent for logging purposes (guaranteed non-NULL)
+ */
+static const char *
+loggable_parent_id(const xmlNode *xml)
+{
+ // Default if called without parent (likely for unit testing)
+ const char *parent_id = "implied";
+
+ if ((xml != NULL) && (xml->parent != NULL)) {
+ parent_id = pcmk__xe_id(xml->parent);
+ if (parent_id == NULL) { // Not possible with schema validation enabled
+ parent_id = "without ID";
+ }
+ }
+ return parent_id;
+}
+
+/*!
+ * \internal
+ * \brief Get the moon phase corresponding to a given date/time
+ *
+ * \param[in] now Date/time to get moon phase for
+ *
+ * \return Phase of the moon corresponding to \p now, where 0 is the new moon
+ * and 7 is the full moon
+ * \deprecated This feature has been deprecated since 2.1.6.
+ */
+static int
+phase_of_the_moon(const crm_time_t *now)
+{
+ /* As per the nethack rules:
+ * - A moon period is 29.53058 days ~= 30
+ * - A year is 365.2422 days
+ * - Number of days moon phase advances on first day of year compared to
+ * preceding year is (365.2422 - 12 * 29.53058) ~= 11
+ * - Number of years until same phases fall on the same days of the month
+ * is 18.6 ~= 19
+ * - Moon phase on first day of year (epact) ~= (11 * (year%19) + 29) % 30
+ * (29 as initial condition)
+ * - Current phase in days = first day phase + days elapsed in year
+ * - 6 moons ~= 177 days ~= 8 reported phases * 22 (+ 11/22 for rounding)
+ */
+ uint32_t epact, diy, goldn;
+ uint32_t y;
+
+ crm_time_get_ordinal(now, &y, &diy);
+ goldn = (y % 19) + 1;
+ epact = (11 * goldn + 18) % 30;
+ if (((epact == 25) && (goldn > 11)) || (epact == 24)) {
+ epact++;
+ }
+ return (((((diy + epact) * 6) + 11) % 177) / 22) & 7;
+}
+
+/*!
+ * \internal
+ * \brief Check an integer value against a range from a date specification
+ *
+ * \param[in] date_spec XML of PCMK_XE_DATE_SPEC element to check
+ * \param[in] id XML ID for logging purposes
+ * \param[in] attr Name of XML attribute with range to check against
+ * \param[in] value Value to compare against range
+ *
+ * \return Standard Pacemaker return code (specifically, pcmk_rc_before_range,
+ * pcmk_rc_after_range, or pcmk_rc_ok to indicate that result is either
+ * within range or undetermined)
+ * \note We return pcmk_rc_ok for an undetermined result so we can continue
+ * checking the next range attribute.
+ */
+static int
+check_range(const xmlNode *date_spec, const char *id, const char *attr,
+ uint32_t value)
+{
+ int rc = pcmk_rc_ok;
+ const char *range = crm_element_value(date_spec, attr);
+ long long low, high;
+
+ if (range == NULL) { // Attribute not present
+ goto bail;
+ }
+
+ if (pcmk__parse_ll_range(range, &low, &high) != pcmk_rc_ok) {
+ // Invalid range
+ /* @COMPAT When we can break behavioral backward compatibility, treat
+ * the entire rule as not passing.
+ */
+ pcmk__config_err("Ignoring " PCMK_XE_DATE_SPEC
+ " %s attribute %s because '%s' is not a valid range",
+ id, attr, range);
+
+ } else if ((low != -1) && (value < low)) {
+ rc = pcmk_rc_before_range;
+
+ } else if ((high != -1) && (value > high)) {
+ rc = pcmk_rc_after_range;
+ }
+
+bail:
+ crm_trace("Checked " PCMK_XE_DATE_SPEC " %s %s='%s' for %" PRIu32 ": %s",
+ id, attr, pcmk__s(range, ""), value, pcmk_rc_str(rc));
+ return rc;
+}
+
+/*!
+ * \internal
+ * \brief Evaluate a date specification for a given date/time
+ *
+ * \param[in] date_spec XML of PCMK_XE_DATE_SPEC element to evaluate
+ * \param[in] now Time to check
+ *
+ * \return Standard Pacemaker return code (specifically, EINVAL for NULL
+ * arguments, pcmk_rc_ok if time matches specification, or
+ * pcmk_rc_before_range, pcmk_rc_after_range, or pcmk_rc_op_unsatisfied
+ * as appropriate to how time relates to specification)
+ */
+int
+pcmk__evaluate_date_spec(const xmlNode *date_spec, const crm_time_t *now)
+{
+ const char *id = NULL;
+ const char *parent_id = loggable_parent_id(date_spec);
+
+ // Range attributes that can be specified for a PCMK_XE_DATE_SPEC element
+ struct range {
+ const char *attr;
+ uint32_t value;
+ } ranges[] = {
+ { PCMK_XA_YEARS, 0U },
+ { PCMK_XA_MONTHS, 0U },
+ { PCMK_XA_MONTHDAYS, 0U },
+ { PCMK_XA_HOURS, 0U },
+ { PCMK_XA_MINUTES, 0U },
+ { PCMK_XA_SECONDS, 0U },
+ { PCMK_XA_YEARDAYS, 0U },
+ { PCMK_XA_WEEKYEARS, 0U },
+ { PCMK_XA_WEEKS, 0U },
+ { PCMK_XA_WEEKDAYS, 0U },
+ { PCMK__XA_MOON, 0U },
+ };
+
+ if ((date_spec == NULL) || (now == NULL)) {
+ return EINVAL;
+ }
+
+ // Get specification ID (for logging)
+ id = pcmk__xe_id(date_spec);
+ if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
+ /* @COMPAT When we can break behavioral backward compatibility,
+ * fail the specification
+ */
+ pcmk__config_warn(PCMK_XE_DATE_SPEC " subelement of "
+ PCMK_XE_DATE_EXPRESSION " %s has no " PCMK_XA_ID,
+ parent_id);
+ id = "without ID"; // for logging
+ }
+
+ // Year, month, day
+ crm_time_get_gregorian(now, &(ranges[0].value), &(ranges[1].value),
+ &(ranges[2].value));
+
+ // Hour, minute, second
+ crm_time_get_timeofday(now, &(ranges[3].value), &(ranges[4].value),
+ &(ranges[5].value));
+
+ // Year (redundant) and day of year
+ crm_time_get_ordinal(now, &(ranges[0].value), &(ranges[6].value));
+
+ // Week year, week of week year, day of week
+ crm_time_get_isoweek(now, &(ranges[7].value), &(ranges[8].value),
+ &(ranges[9].value));
+
+ // Moon phase (deprecated)
+ ranges[10].value = phase_of_the_moon(now);
+ if (crm_element_value(date_spec, PCMK__XA_MOON) != NULL) {
+ pcmk__config_warn("Support for '" PCMK__XA_MOON "' in "
+ PCMK_XE_DATE_SPEC " elements (such as %s) is "
+ "deprecated and will be removed in a future release "
+ "of Pacemaker", id);
+ }
+
+ for (int i = 0; i < PCMK__NELEM(ranges); ++i) {
+ int rc = check_range(date_spec, id, ranges[i].attr, ranges[i].value);
+
+ if (rc != pcmk_rc_ok) {
+ return rc;
+ }
+ }
+
+ // All specified ranges passed, or none were given (also considered a pass)
+ return pcmk_rc_ok;
+}
+
+#define ADD_COMPONENT(component) do { \
+ int sub_rc = pcmk__add_time_from_xml(*end, component, duration); \
+ if (sub_rc != pcmk_rc_ok) { \
+ /* @COMPAT return sub_rc when we can break compatibility */ \
+ pcmk__config_warn("Ignoring %s in " PCMK_XE_DURATION " %s " \
+ "because it is invalid", \
+ pcmk__time_component_attr(component), id); \
+ rc = sub_rc; \
+ } \
+ } while (0)
+
+/*!
+ * \internal
+ * \brief Given a duration and a start time, calculate the end time
+ *
+ * \param[in] duration XML of PCMK_XE_DURATION element
+ * \param[in] start Start time
+ * \param[out] end Where to store end time (\p *end must be NULL
+ * initially)
+ *
+ * \return Standard Pacemaker return code
+ * \note The caller is responsible for freeing \p *end using crm_time_free().
+ */
+int
+pcmk__unpack_duration(const xmlNode *duration, const crm_time_t *start,
+ crm_time_t **end)
+{
+ int rc = pcmk_rc_ok;
+ const char *id = NULL;
+ const char *parent_id = loggable_parent_id(duration);
+
+ if ((start == NULL) || (duration == NULL)
+ || (end == NULL) || (*end != NULL)) {
+ return EINVAL;
+ }
+
+ // Get duration ID (for logging)
+ id = pcmk__xe_id(duration);
+ if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
+ /* @COMPAT When we can break behavioral backward compatibility,
+ * return pcmk_rc_unpack_error instead
+ */
+ pcmk__config_warn(PCMK_XE_DURATION " subelement of "
+ PCMK_XE_DATE_EXPRESSION " %s has no " PCMK_XA_ID,
+ parent_id);
+ id = "without ID";
+ }
+
+ *end = pcmk_copy_time(start);
+
+ ADD_COMPONENT(pcmk__time_years);
+ ADD_COMPONENT(pcmk__time_months);
+ ADD_COMPONENT(pcmk__time_weeks);
+ ADD_COMPONENT(pcmk__time_days);
+ ADD_COMPONENT(pcmk__time_hours);
+ ADD_COMPONENT(pcmk__time_minutes);
+ ADD_COMPONENT(pcmk__time_seconds);
+
+ return rc;
+}
+
+/*!
+ * \internal
+ * \brief Evaluate a range check for a given date/time
+ *
+ * \param[in] date_expression XML of PCMK_XE_DATE_EXPRESSION element
+ * \param[in] id Expression ID for logging purposes
+ * \param[in] now Date/time to compare
+ * \param[in,out] next_change If not NULL, set this to when the evaluation
+ * will change, if known and earlier than the
+ * original value
+ *
+ * \return Standard Pacemaker return code
+ */
+static int
+evaluate_in_range(const xmlNode *date_expression, const char *id,
+ const crm_time_t *now, crm_time_t *next_change)
+{
+ crm_time_t *start = NULL;
+ crm_time_t *end = NULL;
+
+ if (pcmk__xe_get_datetime(date_expression, PCMK_XA_START,
+ &start) != pcmk_rc_ok) {
+ /* @COMPAT When we can break behavioral backward compatibility,
+ * return pcmk_rc_unpack_error
+ */
+ pcmk__config_warn("Ignoring " PCMK_XA_START " in "
+ PCMK_XE_DATE_EXPRESSION " %s because it is invalid",
+ id);
+ }
+
+ if (pcmk__xe_get_datetime(date_expression, PCMK_XA_END,
+ &end) != pcmk_rc_ok) {
+ /* @COMPAT When we can break behavioral backward compatibility,
+ * return pcmk_rc_unpack_error
+ */
+ pcmk__config_warn("Ignoring " PCMK_XA_END " in "
+ PCMK_XE_DATE_EXPRESSION " %s because it is invalid",
+ id);
+ }
+
+ if ((start == NULL) && (end == NULL)) {
+ // Not possible with schema validation enabled
+ /* @COMPAT When we can break behavioral backward compatibility,
+ * return pcmk_rc_unpack_error
+ */
+ pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
+ "passing because in_range requires at least one of "
+ PCMK_XA_START " or " PCMK_XA_END, id);
+ return pcmk_rc_undetermined;
+ }
+
+ if (end == NULL) {
+ xmlNode *duration = pcmk__xe_first_child(date_expression,
+ PCMK_XE_DURATION, NULL, NULL);
+
+ if (duration != NULL) {
+ /* @COMPAT When we can break behavioral backward compatibility,
+ * return the result of this if not OK
+ */
+ pcmk__unpack_duration(duration, start, &end);
+ }
+ }
+
+ if ((start != NULL) && (crm_time_compare(now, start) < 0)) {
+ pcmk__set_time_if_earlier(next_change, start);
+ crm_time_free(start);
+ crm_time_free(end);
+ return pcmk_rc_before_range;
+ }
+
+ if (end != NULL) {
+ if (crm_time_compare(now, end) > 0) {
+ crm_time_free(start);
+ crm_time_free(end);
+ return pcmk_rc_after_range;
+ }
+
+ // Evaluation doesn't change until second after end
+ if (next_change != NULL) {
+ crm_time_add_seconds(end, 1);
+ pcmk__set_time_if_earlier(next_change, end);
+ }
+ }
+
+ crm_time_free(start);
+ crm_time_free(end);
+ return pcmk_rc_within_range;
+}
+
+/*!
+ * \internal
+ * \brief Evaluate a greater-than check for a given date/time
+ *
+ * \param[in] date_expression XML of PCMK_XE_DATE_EXPRESSION element
+ * \param[in] id Expression ID for logging purposes
+ * \param[in] now Date/time to compare
+ * \param[in,out] next_change If not NULL, set this to when the evaluation
+ * will change, if known and earlier than the
+ * original value
+ *
+ * \return Standard Pacemaker return code
+ */
+static int
+evaluate_gt(const xmlNode *date_expression, const char *id,
+ const crm_time_t *now, crm_time_t *next_change)
+{
+ crm_time_t *start = NULL;
+
+ if (pcmk__xe_get_datetime(date_expression, PCMK_XA_START,
+ &start) != pcmk_rc_ok) {
+ /* @COMPAT When we can break behavioral backward compatibility,
+ * return pcmk_rc_unpack_error
+ */
+ pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
+ "passing because " PCMK_XA_START " is invalid",
+ id);
+ return pcmk_rc_undetermined;
+ }
+
+ if (start == NULL) { // Not possible with schema validation enabled
+ /* @COMPAT When we can break behavioral backward compatibility,
+ * return pcmk_rc_unpack_error
+ */
+ pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
+ "passing because " PCMK_VALUE_GT " requires "
+ PCMK_XA_START, id);
+ return pcmk_rc_undetermined;
+ }
+
+ if (crm_time_compare(now, start) > 0) {
+ crm_time_free(start);
+ return pcmk_rc_within_range;
+ }
+
+ // Evaluation doesn't change until second after start time
+ crm_time_add_seconds(start, 1);
+ pcmk__set_time_if_earlier(next_change, start);
+ crm_time_free(start);
+ return pcmk_rc_before_range;
+}
+
+/*!
+ * \internal
+ * \brief Evaluate a less-than check for a given date/time
+ *
+ * \param[in] date_expression XML of PCMK_XE_DATE_EXPRESSION element
+ * \param[in] id Expression ID for logging purposes
+ * \param[in] now Date/time to compare
+ * \param[in,out] next_change If not NULL, set this to when the evaluation
+ * will change, if known and earlier than the
+ * original value
+ *
+ * \return Standard Pacemaker return code
+ */
+static int
+evaluate_lt(const xmlNode *date_expression, const char *id,
+ const crm_time_t *now, crm_time_t *next_change)
+{
+ crm_time_t *end = NULL;
+
+ if (pcmk__xe_get_datetime(date_expression, PCMK_XA_END,
+ &end) != pcmk_rc_ok) {
+ /* @COMPAT When we can break behavioral backward compatibility,
+ * return pcmk_rc_unpack_error
+ */
+ pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
+ "passing because " PCMK_XA_END " is invalid", id);
+ return pcmk_rc_undetermined;
+ }
+
+ if (end == NULL) { // Not possible with schema validation enabled
+ /* @COMPAT When we can break behavioral backward compatibility,
+ * return pcmk_rc_unpack_error
+ */
+ pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not "
+ "passing because " PCMK_VALUE_GT " requires "
+ PCMK_XA_END, id);
+ return pcmk_rc_undetermined;
+ }
+
+ if (crm_time_compare(now, end) < 0) {
+ pcmk__set_time_if_earlier(next_change, end);
+ crm_time_free(end);
+ return pcmk_rc_within_range;
+ }
+
+ crm_time_free(end);
+ return pcmk_rc_after_range;
+}
+
+/*!
+ * \internal
+ * \brief Evaluate a rule's date expression for a given date/time
+ *
+ * \param[in] date_expression XML of a PCMK_XE_DATE_EXPRESSION element
+ * \param[in] now Time to use for evaluation
+ * \param[in,out] next_change If not NULL, set this to when the evaluation
+ * will change, if known and earlier than the
+ * original value
+ *
+ * \return Standard Pacemaker return code (unlike most other evaluation
+ * functions, this can return either pcmk_rc_ok or pcmk_rc_within_range
+ * on success)
+ */
+int
+pcmk__evaluate_date_expression(const xmlNode *date_expression,
+ const crm_time_t *now, crm_time_t *next_change)
+{
+ const char *id = NULL;
+ const char *op = NULL;
+ int rc = pcmk_rc_undetermined;
+
+ if ((date_expression == NULL) || (now == NULL)) {
+ return EINVAL;
+ }
+
+ // Get expression ID (for logging)
+ id = pcmk__xe_id(date_expression);
+ if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
+ /* @COMPAT When we can break behavioral backward compatibility,
+ * return pcmk_rc_unpack_error
+ */
+ pcmk__config_warn(PCMK_XE_DATE_EXPRESSION " element has no "
+ PCMK_XA_ID);
+ id = "without ID"; // for logging
+ }
+
+ op = crm_element_value(date_expression, PCMK_XA_OPERATION);
+ if (pcmk__str_eq(op, PCMK_VALUE_IN_RANGE,
+ pcmk__str_null_matches|pcmk__str_casei)) {
+ rc = evaluate_in_range(date_expression, id, now, next_change);
+
+ } else if (pcmk__str_eq(op, PCMK_VALUE_DATE_SPEC, pcmk__str_casei)) {
+ xmlNode *date_spec = pcmk__xe_first_child(date_expression,
+ PCMK_XE_DATE_SPEC, NULL,
+ NULL);
+
+ if (date_spec == NULL) { // Not possible with schema validation enabled
+ /* @COMPAT When we can break behavioral backward compatibility,
+ * return pcmk_rc_unpack_error
+ */
+ pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s "
+ "as not passing because " PCMK_VALUE_DATE_SPEC
+ " operations require a " PCMK_XE_DATE_SPEC
+ " subelement", id);
+ } else {
+ // @TODO set next_change appropriately
+ rc = pcmk__evaluate_date_spec(date_spec, now);
+ }
+
+ } else if (pcmk__str_eq(op, PCMK_VALUE_GT, pcmk__str_casei)) {
+ rc = evaluate_gt(date_expression, id, now, next_change);
+
+ } else if (pcmk__str_eq(op, PCMK_VALUE_LT, pcmk__str_casei)) {
+ rc = evaluate_lt(date_expression, id, now, next_change);
+
+ } else { // Not possible with schema validation enabled
+ /* @COMPAT When we can break behavioral backward compatibility,
+ * return pcmk_rc_unpack_error
+ */
+ pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION
+ " %s as not passing because '%s' is not a valid "
+ PCMK_XE_OPERATION, op);
+ }
+
+ crm_trace(PCMK_XE_DATE_EXPRESSION " %s (%s): %s (%d)",
+ id, op, pcmk_rc_str(rc), rc);
+ return rc;
+}
+
+/*!
+ * \internal
+ * \brief Go through submatches in a string, either counting how many bytes
+ * would be needed for the expansion, or performing the expansion,
+ * as requested
+ *
+ * \param[in] string String possibly containing submatch variables
+ * \param[in] match String that matched the regular expression
+ * \param[in] submatches Regular expression submatches (as set by regexec())
+ * \param[in] nmatches Number of entries in \p submatches[]
+ * \param[out] expansion If not NULL, expand string here (must be
+ * pre-allocated to appropriate size)
+ * \param[out] nbytes If not NULL, set to size needed for expansion
+ *
+ * \return true if any expansion is needed, otherwise false
+ */
+static bool
+process_submatches(const char *string, const char *match,
+ const regmatch_t submatches[], int nmatches,
+ char *expansion, size_t *nbytes)
+{
+ bool expanded = false;
+ const char *src = string;
+
+ if (nbytes != NULL) {
+ *nbytes = 1; // Include space for terminator
+ }
+
+ while (*src != '\0') {
+ int submatch = 0;
+ size_t match_len = 0;
+
+ if ((src[0] != '%') || !isdigit(src[1])) {
+ /* src does not point to the first character of a %N sequence,
+ * so expand this character as-is
+ */
+ if (expansion != NULL) {
+ *expansion++ = *src;
+ }
+ if (nbytes != NULL) {
+ ++(*nbytes);
+ }
+ ++src;
+ continue;
+ }
+
+ submatch = src[1] - '0';
+ src += 2; // Skip over %N sequence in source string
+ expanded = true; // Expansion will be different from source
+
+ // Omit sequence from expansion unless it has a non-empty match
+ if ((nmatches <= submatch) // Not enough submatches
+ || (submatches[submatch].rm_so < 0) // Pattern did not match
+ || (submatches[submatch].rm_eo
+ <= submatches[submatch].rm_so)) { // Match was empty
+ continue;
+ }
+
+ match_len = submatches[submatch].rm_eo - submatches[submatch].rm_so;
+ if (nbytes != NULL) {
+ *nbytes += match_len;
+ }
+ if (expansion != NULL) {
+ memcpy(expansion, match + submatches[submatch].rm_so,
+ match_len);
+ expansion += match_len;
+ }
+ }
+
+ return expanded;
+}
+
+/*!
+ * \internal
+ * \brief Expand any regular expression submatches (%0-%9) in a string
+ *
+ * \param[in] string String possibly containing submatch variables
+ * \param[in] match String that matched the regular expression
+ * \param[in] submatches Regular expression submatches (as set by regexec())
+ * \param[in] nmatches Number of entries in \p submatches[]
+ *
+ * \return Newly allocated string identical to \p string with submatches
+ * expanded on success, or NULL if no expansions were needed
+ * \note The caller is responsible for freeing the result with free()
+ */
+char *
+pcmk__replace_submatches(const char *string, const char *match,
+ const regmatch_t submatches[], int nmatches)
+{
+ size_t nbytes = 0;
+ char *result = NULL;
+
+ if (pcmk__str_empty(string) || pcmk__str_empty(match)) {
+ return NULL; // Nothing to expand
+ }
+
+ // Calculate how much space will be needed for expanded string
+ if (!process_submatches(string, match, submatches, nmatches, NULL,
+ &nbytes)) {
+ return NULL; // No expansions needed
+ }
+
+ // Allocate enough space for expanded string
+ result = pcmk__assert_alloc(nbytes, sizeof(char));
+
+ // Expand submatches
+ (void) process_submatches(string, match, submatches, nmatches, result,
+ NULL);
+ return result;
+}
+
+/*!
+ * \internal
+ * \brief Parse a comparison type from a string
+ *
+ * \param[in] op String with comparison type (valid values are
+ * \c PCMK_VALUE_DEFINED, \c PCMK_VALUE_NOT_DEFINED,
+ * \c PCMK_VALUE_EQ, \c PCMK_VALUE_NE,
+ * \c PCMK_VALUE_LT, \c PCMK_VALUE_LTE,
+ * \c PCMK_VALUE_GT, or \c PCMK_VALUE_GTE)
+ *
+ * \return Comparison type corresponding to \p op
+ */
+enum pcmk__comparison
+pcmk__parse_comparison(const char *op)
+{
+ if (pcmk__str_eq(op, PCMK_VALUE_DEFINED, pcmk__str_casei)) {
+ return pcmk__comparison_defined;
+
+ } else if (pcmk__str_eq(op, PCMK_VALUE_NOT_DEFINED, pcmk__str_casei)) {
+ return pcmk__comparison_undefined;
+
+ } else if (pcmk__str_eq(op, PCMK_VALUE_EQ, pcmk__str_casei)) {
+ return pcmk__comparison_eq;
+
+ } else if (pcmk__str_eq(op, PCMK_VALUE_NE, pcmk__str_casei)) {
+ return pcmk__comparison_ne;
+
+ } else if (pcmk__str_eq(op, PCMK_VALUE_LT, pcmk__str_casei)) {
+ return pcmk__comparison_lt;
+
+ } else if (pcmk__str_eq(op, PCMK_VALUE_LTE, pcmk__str_casei)) {
+ return pcmk__comparison_lte;
+
+ } else if (pcmk__str_eq(op, PCMK_VALUE_GT, pcmk__str_casei)) {
+ return pcmk__comparison_gt;
+
+ } else if (pcmk__str_eq(op, PCMK_VALUE_GTE, pcmk__str_casei)) {
+ return pcmk__comparison_gte;
+ }
+
+ return pcmk__comparison_unknown;
+}
+
+/*!
+ * \internal
+ * \brief Parse a value type from a string
+ *
+ * \param[in] type String with value type (valid values are NULL,
+ * \c PCMK_VALUE_STRING, \c PCMK_VALUE_INTEGER,
+ * \c PCMK_VALUE_NUMBER, and \c PCMK_VALUE_VERSION)
+ * \param[in] op Operation type (used only to select default)
+ * \param[in] value1 First value being compared (used only to select default)
+ * \param[in] value2 Second value being compared (used only to select default)
+ */
+enum pcmk__type
+pcmk__parse_type(const char *type, enum pcmk__comparison op,
+ const char *value1, const char *value2)
+{
+ if (type == NULL) {
+ switch (op) {
+ case pcmk__comparison_lt:
+ case pcmk__comparison_lte:
+ case pcmk__comparison_gt:
+ case pcmk__comparison_gte:
+ if (((value1 != NULL) && (strchr(value1, '.') != NULL))
+ || ((value2 != NULL) && (strchr(value2, '.') != NULL))) {
+ return pcmk__type_number;
+ }
+ return pcmk__type_integer;
+
+ default:
+ return pcmk__type_string;
+ }
+ }
+
+ if (pcmk__str_eq(type, PCMK_VALUE_STRING, pcmk__str_casei)) {
+ return pcmk__type_string;
+
+ } else if (pcmk__str_eq(type, PCMK_VALUE_INTEGER, pcmk__str_casei)) {
+ return pcmk__type_integer;
+
+ } else if (pcmk__str_eq(type, PCMK_VALUE_NUMBER, pcmk__str_casei)) {
+ return pcmk__type_number;
+
+ } else if (pcmk__str_eq(type, PCMK_VALUE_VERSION, pcmk__str_casei)) {
+ return pcmk__type_version;
+ }
+
+ return pcmk__type_unknown;
+}
+
+/*!
+ * \internal
+ * \brief Compare two strings according to a given type
+ *
+ * \param[in] value1 String with first value to compare
+ * \param[in] value2 String with second value to compare
+ * \param[in] type How to interpret the values
+ *
+ * \return Standard comparison result (a negative integer if \p value1 is
+ * lesser, 0 if the values are equal, and a positive integer if
+ * \p value1 is greater)
+ */
+int
+pcmk__cmp_by_type(const char *value1, const char *value2, enum pcmk__type type)
+{
+ // NULL compares as less than non-NULL
+ if (value2 == NULL) {
+ return (value1 == NULL)? 0 : 1;
+ }
+ if (value1 == NULL) {
+ return -1;
+ }
+
+ switch (type) {
+ case pcmk__type_string:
+ return strcasecmp(value1, value2);
+
+ case pcmk__type_integer:
+ {
+ long long integer1;
+ long long integer2;
+
+ if ((pcmk__scan_ll(value1, &integer1, 0LL) != pcmk_rc_ok)
+ || (pcmk__scan_ll(value2, &integer2, 0LL) != pcmk_rc_ok)) {
+ crm_warn("Comparing '%s' and '%s' as strings because "
+ "invalid as integers", value1, value2);
+ return strcasecmp(value1, value2);
+ }
+ return (integer1 < integer2)? -1 : (integer1 > integer2)? 1 : 0;
+ }
+ break;
+
+ case pcmk__type_number:
+ {
+ double num1;
+ double num2;
+
+ if ((pcmk__scan_double(value1, &num1, NULL, NULL) != pcmk_rc_ok)
+ || (pcmk__scan_double(value2, &num2, NULL,
+ NULL) != pcmk_rc_ok)) {
+ crm_warn("Comparing '%s' and '%s' as strings because invalid as "
+ "numbers", value1, value2);
+ return strcasecmp(value1, value2);
+ }
+ return (num1 < num2)? -1 : (num1 > num2)? 1 : 0;
+ }
+ break;
+
+ case pcmk__type_version:
+ return compare_version(value1, value2);
+
+ default: // Invalid type
+ return 0;
+ }
+}
+
+/*!
+ * \internal
+ * \brief Parse a reference value source from a string
+ *
+ * \param[in] source String indicating reference value source
+ *
+ * \return Reference value source corresponding to \p source
+ */
+enum pcmk__reference_source
+pcmk__parse_source(const char *source)
+{
+ if (pcmk__str_eq(source, PCMK_VALUE_LITERAL,
+ pcmk__str_casei|pcmk__str_null_matches)) {
+ return pcmk__source_literal;
+
+ } else if (pcmk__str_eq(source, PCMK_VALUE_PARAM, pcmk__str_casei)) {
+ return pcmk__source_instance_attrs;
+
+ } else if (pcmk__str_eq(source, PCMK_VALUE_META, pcmk__str_casei)) {
+ return pcmk__source_meta_attrs;
+
+ } else {
+ return pcmk__source_unknown;
+ }
+}
+
+/*!
+ * \internal
+ * \brief Parse a boolean operator from a string
+ *
+ * \param[in] combine String indicating boolean operator
+ *
+ * \return Enumeration value corresponding to \p combine
+ */
+enum pcmk__combine
+pcmk__parse_combine(const char *combine)
+{
+ if (pcmk__str_eq(combine, PCMK_VALUE_AND,
+ pcmk__str_null_matches|pcmk__str_casei)) {
+ return pcmk__combine_and;
+
+ } else if (pcmk__str_eq(combine, PCMK_VALUE_OR, pcmk__str_casei)) {
+ return pcmk__combine_or;
+
+ } else {
+ return pcmk__combine_unknown;
+ }
+}
+
+/*!
+ * \internal
+ * \brief Get the result of a node attribute comparison for rule evaluation
+ *
+ * \param[in] actual Actual node attribute value
+ * \param[in] reference Node attribute value from rule (ignored for
+ * \p comparison of \c pcmk__comparison_defined or
+ * \c pcmk__comparison_undefined)
+ * \param[in] type How to interpret the values
+ * \param[in] comparison How to compare the values
+ *
+ * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok if the
+ * comparison passes, and some other value if it does not)
+ */
+static int
+evaluate_attr_comparison(const char *actual, const char *reference,
+ enum pcmk__type type, enum pcmk__comparison comparison)
+{
+ int cmp = 0;
+
+ switch (comparison) {
+ case pcmk__comparison_defined:
+ return (actual != NULL)? pcmk_rc_ok : pcmk_rc_op_unsatisfied;
+
+ case pcmk__comparison_undefined:
+ return (actual == NULL)? pcmk_rc_ok : pcmk_rc_op_unsatisfied;
+
+ default:
+ break;
+ }
+
+ cmp = pcmk__cmp_by_type(actual, reference, type);
+
+ switch (comparison) {
+ case pcmk__comparison_eq:
+ return (cmp == 0)? pcmk_rc_ok : pcmk_rc_op_unsatisfied;
+
+ case pcmk__comparison_ne:
+ return (cmp != 0)? pcmk_rc_ok : pcmk_rc_op_unsatisfied;
+
+ default:
+ break;
+ }
+
+ if ((actual == NULL) || (reference == NULL)) {
+ return pcmk_rc_op_unsatisfied; // Comparison would be meaningless
+ }
+
+ switch (comparison) {
+ case pcmk__comparison_lt:
+ return (cmp < 0)? pcmk_rc_ok : pcmk_rc_after_range;
+
+ case pcmk__comparison_lte:
+ return (cmp <= 0)? pcmk_rc_ok : pcmk_rc_after_range;
+
+ case pcmk__comparison_gt:
+ return (cmp > 0)? pcmk_rc_ok : pcmk_rc_before_range;
+
+ case pcmk__comparison_gte:
+ return (cmp >= 0)? pcmk_rc_ok : pcmk_rc_before_range;
+
+ default: // Not possible with schema validation enabled
+ return pcmk_rc_op_unsatisfied;
+ }
+}
+
+/*!
+ * \internal
+ * \brief Get a reference value from a configured source
+ *
+ * \param[in] value Value given in rule expression
+ * \param[in] source Reference value source
+ * \param[in] rule_input Values used to evaluate rule criteria
+ */
+static const char *
+value_from_source(const char *value, enum pcmk__reference_source source,
+ const pcmk_rule_input_t *rule_input)
+{
+ GHashTable *table = NULL;
+
+ if (pcmk__str_empty(value)) {
+ /* @COMPAT When we can break backward compatibility, drop this block so
+ * empty strings are treated as such (there should never be an empty
+ * string as an instance attribute or meta-attribute name, so those will
+ * get NULL anyway, but it could matter for literal comparisons)
+ */
+ return NULL;
+ }
+
+ switch (source) {
+ case pcmk__source_literal:
+ return value;
+
+ case pcmk__source_instance_attrs:
+ table = rule_input->rsc_params;
+ break;
+
+ case pcmk__source_meta_attrs:
+ table = rule_input->rsc_meta;
+ break;
+
+ default:
+ return NULL; // Not possible
+ }
+
+ if (table == NULL) {
+ return NULL;
+ }
+ return (const char *) g_hash_table_lookup(table, value);
+}
+
+/*!
+ * \internal
+ * \brief Evaluate a node attribute rule expression
+ *
+ * \param[in] expression XML of a rule's PCMK_XE_EXPRESSION subelement
+ * \param[in] rule_input Values used to evaluate rule criteria
+ *
+ * \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression
+ * passes, some other value if it does not)
+ */
+int
+pcmk__evaluate_attr_expression(const xmlNode *expression,
+ const pcmk_rule_input_t *rule_input)
+{
+ const char *id = NULL;
+ const char *op = NULL;
+ const char *attr = NULL;
+ const char *type_s = NULL;
+ const char *value = NULL;
+ const char *actual = NULL;
+ const char *source_s = NULL;
+ const char *reference = NULL;
+ char *expanded_attr = NULL;
+ int rc = pcmk_rc_ok;
+
+ enum pcmk__type type = pcmk__type_unknown;
+ enum pcmk__reference_source source = pcmk__source_unknown;
+ enum pcmk__comparison comparison = pcmk__comparison_unknown;
+
+ if ((expression == NULL) || (rule_input == NULL)) {
+ return EINVAL;
+ }
+
+ // Get expression ID (for logging)
+ id = pcmk__xe_id(expression);
+ if (pcmk__str_empty(id)) {
+ /* @COMPAT When we can break behavioral backward compatibility,
+ * fail the expression
+ */
+ pcmk__config_warn(PCMK_XE_EXPRESSION " element has no " PCMK_XA_ID);
+ id = "without ID"; // for logging
+ }
+
+ /* Get name of node attribute to compare (expanding any %0-%9 to
+ * regular expression submatches)
+ */
+ attr = crm_element_value(expression, PCMK_XA_ATTRIBUTE);
+ if (attr == NULL) {
+ pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not passing "
+ "because " PCMK_XA_ATTRIBUTE " was not specified", id);
+ return pcmk_rc_unpack_error;
+ }
+ expanded_attr = pcmk__replace_submatches(attr, rule_input->rsc_id,
+ rule_input->rsc_id_submatches,
+ rule_input->rsc_id_nmatches);
+ if (expanded_attr != NULL) {
+ attr = expanded_attr;
+ }
+
+ // Get and validate operation
+ op = crm_element_value(expression, PCMK_XA_OPERATION);
+ comparison = pcmk__parse_comparison(op);
+ if (comparison == pcmk__comparison_unknown) {
+ // Not possible with schema validation enabled
+ if (op == NULL) {
+ pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not "
+ "passing because it has no " PCMK_XA_OPERATION,
+ id);
+ } else {
+ pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not "
+ "passing because '%s' is not a valid "
+ PCMK_XA_OPERATION, id, op);
+ }
+ rc = pcmk_rc_unpack_error;
+ goto done;
+ }
+
+ // How reference value is obtained (literal, resource meta-attribute, etc.)
+ source_s = crm_element_value(expression, PCMK_XA_VALUE_SOURCE);
+ source = pcmk__parse_source(source_s);
+ if (source == pcmk__source_unknown) {
+ // Not possible with schema validation enabled
+ // @COMPAT Fail expression once we can break backward compatibility
+ pcmk__config_warn("Expression %s has invalid " PCMK_XA_VALUE_SOURCE
+ " value '%s', using default "
+ "('" PCMK_VALUE_LITERAL "')", id, source_s);
+ source = pcmk__source_literal;
+ }
+
+ // Get and validate reference value
+ value = crm_element_value(expression, PCMK_XA_VALUE);
+ switch (comparison) {
+ case pcmk__comparison_defined:
+ case pcmk__comparison_undefined:
+ if (value != NULL) {
+ pcmk__config_warn("Ignoring " PCMK_XA_VALUE " in "
+ PCMK_XE_EXPRESSION " %s because it is unused "
+ "when " PCMK_XA_BOOLEAN_OP " is %s", id, op);
+ }
+ break;
+
+ default:
+ if (value == NULL) {
+ pcmk__config_warn(PCMK_XE_EXPRESSION " %s has no "
+ PCMK_XA_VALUE, id);
+ }
+ break;
+ }
+ reference = value_from_source(value, source, rule_input);
+
+ // Get actual value of node attribute
+ if (rule_input->node_attrs != NULL) {
+ actual = g_hash_table_lookup(rule_input->node_attrs, attr);
+ }
+
+ // Get and validate value type (after expanding reference value)
+ type_s = crm_element_value(expression, PCMK_XA_TYPE);
+ type = pcmk__parse_type(type_s, comparison, actual, reference);
+ if (type == pcmk__type_unknown) {
+ /* Not possible with schema validation enabled
+ *
+ * @COMPAT When we can break behavioral backward compatibility, treat
+ * the expression as not passing.
+ */
+ pcmk__config_warn("Non-empty node attribute values will be treated as "
+ "equal for " PCMK_XE_EXPRESSION " %s because '%s' "
+ "is not a valid type", id, type);
+ }
+
+ rc = evaluate_attr_comparison(actual, reference, type, comparison);
+ switch (comparison) {
+ case pcmk__comparison_defined:
+ case pcmk__comparison_undefined:
+ crm_trace(PCMK_XE_EXPRESSION " %s result: %s (for attribute %s %s)",
+ id, pcmk_rc_str(rc), attr, op);
+ break;
+
+ default:
+ crm_trace(PCMK_XE_EXPRESSION " %s result: "
+ "%s (attribute %s %s '%s' via %s source as %s type)",
+ id, pcmk_rc_str(rc), attr, op, pcmk__s(reference, ""),
+ pcmk__s(source_s, "default"), pcmk__s(type_s, "default"));
+ break;
+ }
+
+done:
+ free(expanded_attr);
+ return rc;
+}
+
+/*!
+ * \internal
+ * \brief Evaluate a resource rule expression
+ *
+ * \param[in] rsc_expression XML of rule's \c PCMK_XE_RSC_EXPRESSION subelement
+ * \param[in] rule_input Values used to evaluate rule criteria
+ *
+ * \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression
+ * passes, some other value if it does not)
+ */
+int
+pcmk__evaluate_rsc_expression(const xmlNode *rsc_expression,
+ const pcmk_rule_input_t *rule_input)
+{
+ const char *id = NULL;
+ const char *standard = NULL;
+ const char *provider = NULL;
+ const char *type = NULL;
+
+ if ((rsc_expression == NULL) || (rule_input == NULL)) {
+ return EINVAL;
+ }
+
+ // Validate XML ID
+ id = pcmk__xe_id(rsc_expression);
+ if (pcmk__str_empty(id)) {
+ // Not possible with schema validation enabled
+ /* @COMPAT When we can break behavioral backward compatibility,
+ * fail the expression
+ */
+ pcmk__config_warn(PCMK_XE_RSC_EXPRESSION " has no " PCMK_XA_ID);
+ id = "without ID"; // for logging
+ }
+
+ // Compare resource standard
+ standard = crm_element_value(rsc_expression, PCMK_XA_CLASS);
+ if ((standard != NULL)
+ && !pcmk__str_eq(standard, rule_input->rsc_standard, pcmk__str_none)) {
+ crm_trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because "
+ "actual standard '%s' doesn't match '%s'",
+ id, pcmk__s(rule_input->rsc_standard, ""), standard);
+ return pcmk_rc_op_unsatisfied;
+ }
+
+ // Compare resource provider
+ provider = crm_element_value(rsc_expression, PCMK_XA_PROVIDER);
+ if ((provider != NULL)
+ && !pcmk__str_eq(provider, rule_input->rsc_provider, pcmk__str_none)) {
+ crm_trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because "
+ "actual provider '%s' doesn't match '%s'",
+ id, pcmk__s(rule_input->rsc_provider, ""), provider);
+ return pcmk_rc_op_unsatisfied;
+ }
+
+ // Compare resource agent type
+ type = crm_element_value(rsc_expression, PCMK_XA_TYPE);
+ if ((type != NULL)
+ && !pcmk__str_eq(type, rule_input->rsc_agent, pcmk__str_none)) {
+ crm_trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because "
+ "actual agent '%s' doesn't match '%s'",
+ id, pcmk__s(rule_input->rsc_agent, ""), type);
+ return pcmk_rc_op_unsatisfied;
+ }
+
+ crm_trace(PCMK_XE_RSC_EXPRESSION " %s is satisfied by %s%s%s:%s",
+ id, pcmk__s(standard, ""),
+ ((provider == NULL)? "" : ":"), pcmk__s(provider, ""),
+ pcmk__s(type, ""));
+ return pcmk_rc_ok;
+}
+
+/*!
+ * \internal
+ * \brief Evaluate an operation rule expression
+ *
+ * \param[in] op_expression XML of a rule's \c PCMK_XE_OP_EXPRESSION subelement
+ * \param[in] rule_input Values used to evaluate rule criteria
+ *
+ * \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression
+ * is satisfied, some other value if it is not)
+ */
+int
+pcmk__evaluate_op_expression(const xmlNode *op_expression,
+ const pcmk_rule_input_t *rule_input)
+{
+ const char *id = NULL;
+ const char *name = NULL;
+ const char *interval_s = NULL;
+ guint interval_ms = 0U;
+
+ if ((op_expression == NULL) || (rule_input == NULL)) {
+ return EINVAL;
+ }
+
+ // Get operation expression ID (for logging)
+ id = pcmk__xe_id(op_expression);
+ if (pcmk__str_empty(id)) { // Not possible with schema validation enabled
+ /* @COMPAT When we can break behavioral backward compatibility,
+ * return pcmk_rc_op_unsatisfied
+ */
+ pcmk__config_warn(PCMK_XE_OP_EXPRESSION " element has no " PCMK_XA_ID);
+ id = "without ID"; // for logging
+ }
+
+ // Validate operation name
+ name = crm_element_value(op_expression, PCMK_XA_NAME);
+ if (name == NULL) { // Not possible with schema validation enabled
+ pcmk__config_warn("Treating " PCMK_XE_OP_EXPRESSION " %s as not "
+ "passing because it has no " PCMK_XA_NAME, id);
+ return pcmk_rc_unpack_error;
+ }
+
+ // Validate operation interval
+ interval_s = crm_element_value(op_expression, PCMK_META_INTERVAL);
+ if (pcmk_parse_interval_spec(interval_s, &interval_ms) != pcmk_rc_ok) {
+ pcmk__config_warn("Treating " PCMK_XE_OP_EXPRESSION " %s as not "
+ "passing because '%s' is not a valid interval",
+ id, interval_s);
+ return pcmk_rc_unpack_error;
+ }
+
+ // Compare operation name
+ if (!pcmk__str_eq(name, rule_input->op_name, pcmk__str_none)) {
+ crm_trace(PCMK_XE_OP_EXPRESSION " %s is unsatisfied because "
+ "actual name '%s' doesn't match '%s'",
+ id, pcmk__s(rule_input->op_name, ""), name);
+ return pcmk_rc_op_unsatisfied;
+ }
+
+ // Compare operation interval (unspecified interval matches all)
+ if ((interval_s != NULL) && (interval_ms != rule_input->op_interval_ms)) {
+ crm_trace(PCMK_XE_OP_EXPRESSION " %s is unsatisfied because "
+ "actual interval %s doesn't match %s",
+ id, pcmk__readable_interval(rule_input->op_interval_ms),
+ pcmk__readable_interval(interval_ms));
+ return pcmk_rc_op_unsatisfied;
+ }
+
+ crm_trace(PCMK_XE_OP_EXPRESSION " %s is satisfied (name %s, interval %s)",
+ id, name, pcmk__readable_interval(rule_input->op_interval_ms));
+ return pcmk_rc_ok;
+}
+
+/*!
+ * \internal
+ * \brief Evaluate a rule condition
+ *
+ * \param[in,out] condition XML containing a rule condition (a subrule, or an
+ * expression of any type)
+ * \param[in] rule_input Values used to evaluate rule criteria
+ * \param[out] next_change If not NULL, set to when evaluation will change
+ *
+ * \return Standard Pacemaker return code (\c pcmk_rc_ok if the condition
+ * passes, some other value if it does not)
+ */
+int
+pcmk__evaluate_condition(xmlNode *condition,
+ const pcmk_rule_input_t *rule_input,
+ crm_time_t *next_change)
+{
+
+ if ((condition == NULL) || (rule_input == NULL)) {
+ return EINVAL;
+ }
+
+ switch (pcmk__condition_type(condition)) {
+ case pcmk__condition_rule:
+ return pcmk_evaluate_rule(condition, rule_input, next_change);
+
+ case pcmk__condition_attribute:
+ case pcmk__condition_location:
+ return pcmk__evaluate_attr_expression(condition, rule_input);
+
+ case pcmk__condition_datetime:
+ {
+ int rc = pcmk__evaluate_date_expression(condition,
+ rule_input->now,
+ next_change);
+
+ return (rc == pcmk_rc_within_range)? pcmk_rc_ok : rc;
+ }
+
+ case pcmk__condition_resource:
+ return pcmk__evaluate_rsc_expression(condition, rule_input);
+
+ case pcmk__condition_operation:
+ return pcmk__evaluate_op_expression(condition, rule_input);
+
+ default: // Not possible with schema validation enabled
+ pcmk__config_err("Treating rule condition %s as not passing "
+ "because %s is not a valid condition type",
+ pcmk__s(pcmk__xe_id(condition), "without ID"),
+ (const char *) condition->name);
+ return pcmk_rc_unpack_error;
+ }
+}
+
+/*!
+ * \brief Evaluate a single rule, including all its conditions
+ *
+ * \param[in,out] rule XML containing a rule definition or its id-ref
+ * \param[in] rule_input Values used to evaluate rule criteria
+ * \param[out] next_change If not NULL, set to when evaluation will change
+ *
+ * \return Standard Pacemaker return code (\c pcmk_rc_ok if the rule is
+ * satisfied, some other value if it is not)
+ */
+int
+pcmk_evaluate_rule(xmlNode *rule, const pcmk_rule_input_t *rule_input,
+ crm_time_t *next_change)
+{
+ bool empty = true;
+ int rc = pcmk_rc_ok;
+ const char *id = NULL;
+ const char *value = NULL;
+ enum pcmk__combine combine = pcmk__combine_unknown;
+
+ if ((rule == NULL) || (rule_input == NULL)) {
+ return EINVAL;
+ }
+
+ rule = expand_idref(rule, NULL);
+ if (rule == NULL) {
+ // Not possible with schema validation enabled; message already logged
+ return pcmk_rc_unpack_error;
+ }
+
+ // Validate XML ID
+ id = pcmk__xe_id(rule);
+ if (pcmk__str_empty(id)) {
+ /* @COMPAT When we can break behavioral backward compatibility,
+ * fail the rule
+ */
+ pcmk__config_warn(PCMK_XE_RULE " has no " PCMK_XA_ID);
+ id = "without ID"; // for logging
+ }
+
+ value = crm_element_value(rule, PCMK_XA_BOOLEAN_OP);
+ combine = pcmk__parse_combine(value);
+ switch (combine) {
+ case pcmk__combine_and:
+ // For "and", rc defaults to success (reset on failure below)
+ break;
+
+ case pcmk__combine_or:
+ // For "or", rc defaults to failure (reset on success below)
+ rc = pcmk_rc_op_unsatisfied;
+ break;
+
+ default:
+ /* @COMPAT When we can break behavioral backward compatibility,
+ * return pcmk_rc_unpack_error
+ */
+ pcmk__config_warn("Rule %s has invalid " PCMK_XA_BOOLEAN_OP
+ " value '%s', using default '" PCMK_VALUE_AND "'",
+ pcmk__xe_id(rule), value);
+ combine = pcmk__combine_and;
+ break;
+ }
+
+ // Evaluate each condition
+ for (xmlNode *condition = pcmk__xe_first_child(rule, NULL, NULL, NULL);
+ condition != NULL; condition = pcmk__xe_next(condition)) {
+
+ empty = false;
+ if (pcmk__evaluate_condition(condition, rule_input,
+ next_change) == pcmk_rc_ok) {
+ if (combine == pcmk__combine_or) {
+ rc = pcmk_rc_ok; // Any pass is final for "or"
+ break;
+ }
+ } else if (combine == pcmk__combine_and) {
+ rc = pcmk_rc_op_unsatisfied; // Any failure is final for "and"
+ break;
+ }
+ }
+
+ if (empty) { // Not possible with schema validation enabled
+ /* @COMPAT Currently, we don't actually ignore "or" rules because
+ * rc is initialized to failure above in that case. When we can break
+ * backward compatibility, reset rc to pcmk_rc_ok here.
+ */
+ pcmk__config_warn("Ignoring rule %s because it contains no conditions",
+ id);
+ }
+
+ crm_trace("Rule %s is %ssatisfied", id, ((rc == pcmk_rc_ok)? "" : "not "));
+ return rc;
+}
+
+/*!
+ * \internal
+ * \brief Evaluate all rules contained within an element
+ *
+ * \param[in,out] xml XML element possibly containing rule subelements
+ * \param[in] rule_input Values used to evaluate rule criteria
+ * \param[out] next_change If not NULL, set to when evaluation will change
+ *
+ * \return Standard Pacemaker return code (pcmk_rc_ok if there are no contained
+ * rules or any contained rule passes, otherwise the result of the last
+ * rule)
+ * \deprecated On code paths leading to this function, the schema allows
+ * multiple top-level rules only in the deprecated lifetime element
+ * of location constraints. The code also allows multiple top-level
+ * rules when unpacking attribute sets, but this is deprecated and
+ * already prevented by schema validation. This function can be
+ * dropped when support for those is dropped.
+ */
+int
+pcmk__evaluate_rules(xmlNode *xml, const pcmk_rule_input_t *rule_input,
+ crm_time_t *next_change)
+{
+ // If there are no rules, pass by default
+ int rc = pcmk_rc_ok;
+ bool have_rule = false;
+
+ for (xmlNode *rule = pcmk__xe_first_child(xml, PCMK_XE_RULE, NULL, NULL);
+ rule != NULL; rule = pcmk__xe_next_same(rule)) {
+
+ if (have_rule) {
+ pcmk__warn_once(pcmk__wo_multiple_rules,
+ "Support for multiple top-level rules is "
+ "deprecated (replace with a single rule containing "
+ "the existing rules with " PCMK_XA_BOOLEAN_OP
+ "set to " PCMK_VALUE_OR " instead)");
+ } else {
+ have_rule = true;
+ }
+
+ rc = pcmk_evaluate_rule(rule, rule_input, next_change);
+ if (rc == pcmk_rc_ok) {
+ break;
+ }
+ }
+ return rc;
+}
diff --git a/lib/common/scheduler.c b/lib/common/scheduler.c
index 20e6fdf..dad3de9 100644
--- a/lib/common/scheduler.c
+++ b/lib/common/scheduler.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2004-2023 the Pacemaker project contributors
+ * Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -10,5 +10,100 @@
#include <crm_internal.h>
#include <stdint.h> // uint32_t
+#include <errno.h> // EINVAL
+#include <glib.h> // gboolean, FALSE
+#include <libxml/tree.h> // xmlNode
+
+#include <crm/common/scheduler.h>
uint32_t pcmk__warnings = 0;
+
+gboolean was_processing_error = FALSE;
+gboolean was_processing_warning = FALSE;
+
+/*!
+ * \internal
+ * \brief Get the Designated Controller node from scheduler data
+ *
+ * \param[in] scheduler Scheduler data
+ *
+ * \return Designated Controller node from scheduler data, or NULL if none
+ */
+pcmk_node_t *
+pcmk_get_dc(const pcmk_scheduler_t *scheduler)
+{
+ return (scheduler == NULL)? NULL : scheduler->dc_node;
+}
+
+/*!
+ * \internal
+ * \brief Get the no quorum policy from scheduler data
+ *
+ * \param[in] scheduler Scheduler data
+ *
+ * \return No quorum policy from scheduler data
+ */
+enum pe_quorum_policy
+pcmk_get_no_quorum_policy(const pcmk_scheduler_t *scheduler)
+{
+ if (scheduler == NULL) {
+ return pcmk_no_quorum_stop; // The default
+ }
+ return scheduler->no_quorum_policy;
+}
+
+/*!
+ * \internal
+ * \brief Set CIB XML as scheduler input in scheduler data
+ *
+ * \param[out] scheduler Scheduler data
+ * \param[in] cib CIB XML to set as scheduler input
+ *
+ * \return Standard Pacemaker return code (EINVAL if \p scheduler is NULL,
+ * otherwise pcmk_rc_ok)
+ * \note This will not free any previously set scheduler CIB.
+ */
+int
+pcmk_set_scheduler_cib(pcmk_scheduler_t *scheduler, xmlNode *cib)
+{
+ if (scheduler == NULL) {
+ return EINVAL;
+ }
+ scheduler->input = cib;
+ return pcmk_rc_ok;
+}
+
+/*!
+ * \internal
+ * \brief Check whether cluster has quorum
+ *
+ * \param[in] scheduler Scheduler data
+ *
+ * \return true if cluster has quorum, otherwise false
+ */
+bool
+pcmk_has_quorum(const pcmk_scheduler_t *scheduler)
+{
+ if (scheduler == NULL) {
+ return false;
+ }
+ return pcmk_is_set(scheduler->flags, pcmk_sched_quorate);
+}
+
+/*!
+ * \brief Find a node by name in scheduler data
+ *
+ * \param[in] scheduler Scheduler data
+ * \param[in] node_name Name of node to find
+ *
+ * \return Node from scheduler data that matches \p node_name if any,
+ * otherwise NULL
+ */
+pcmk_node_t *
+pcmk_find_node(const pcmk_scheduler_t *scheduler, const char *node_name)
+{
+ if ((scheduler == NULL) || (node_name == NULL)) {
+ return NULL;
+ }
+ return pcmk__find_node_in_list(scheduler->nodes, node_name);
+}
diff --git a/lib/common/schemas.c b/lib/common/schemas.c
index b3c09eb..16f2ccb 100644
--- a/lib/common/schemas.c
+++ b/lib/common/schemas.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2004-2023 the Pacemaker project contributors
+ * Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -22,19 +22,13 @@
#include <libxslt/security.h>
#include <libxslt/xsltutils.h>
-#include <crm/msg_xml.h>
#include <crm/common/xml.h>
#include <crm/common/xml_internal.h> /* PCMK__XML_LOG_BASE */
-typedef struct {
- unsigned char v[2];
-} schema_version_t;
+#include "crmcommon_private.h"
#define SCHEMA_ZERO { .v = { 0, 0 } }
-#define schema_scanf(s, prefix, version, suffix) \
- sscanf((s), prefix "%hhu.%hhu" suffix, &((version).v[0]), &((version).v[1]))
-
#define schema_strdup_printf(prefix, version, suffix) \
crm_strdup_printf(prefix "%u.%u" suffix, (version).v[0], (version).v[1])
@@ -44,31 +38,11 @@ typedef struct {
xmlRelaxNGParserCtxtPtr parser;
} relaxng_ctx_cache_t;
-enum schema_validator_e {
- schema_validator_none,
- schema_validator_rng
-};
-
-struct schema_s {
- char *name;
- char *transform;
- void *cache;
- enum schema_validator_e validator;
- int after_transform;
- schema_version_t version;
- char *transform_enter;
- bool transform_onleave;
-};
-
-static struct schema_s *known_schemas = NULL;
-static int xml_schema_max = 0;
+static GList *known_schemas = NULL;
+static bool initialized = false;
static bool silent_logging = FALSE;
-static void
-xml_log(int priority, const char *fmt, ...)
-G_GNUC_PRINTF(2, 3);
-
-static void
+static void G_GNUC_PRINTF(2, 3)
xml_log(int priority, const char *fmt, ...)
{
va_list ap;
@@ -84,50 +58,114 @@ xml_log(int priority, const char *fmt, ...)
static int
xml_latest_schema_index(void)
{
- // @COMPAT: pacemaker-next is deprecated since 2.1.5
- return xml_schema_max - 3; // index from 0, ignore "pacemaker-next"/"none"
+ /* This function assumes that crm_schema_init() has been called beforehand,
+ * so we have at least three schemas (one real schema, the "pacemaker-next"
+ * schema, and the "none" schema).
+ *
+ * @COMPAT: pacemaker-next is deprecated since 2.1.5 and none since 2.1.8.
+ * Update this when we drop those.
+ */
+ return g_list_length(known_schemas) - 3;
}
-static int
-xml_minimum_schema_index(void)
+/*!
+ * \internal
+ * \brief Return the schema entry of the highest-versioned schema
+ *
+ * \return Schema entry of highest-versioned schema (or NULL on error)
+ */
+static GList *
+get_highest_schema(void)
{
- static int best = 0;
- if (best == 0) {
- int lpc = 0;
-
- best = xml_latest_schema_index();
- for (lpc = best; lpc > 0; lpc--) {
- if (known_schemas[lpc].version.v[0]
- < known_schemas[best].version.v[0]) {
- return best;
- } else {
- best = lpc;
- }
- }
- best = xml_latest_schema_index();
- }
- return best;
+ /* The highest numerically versioned schema is the one before pacemaker-next
+ *
+ * @COMPAT pacemaker-next is deprecated since 2.1.5
+ */
+ GList *entry = pcmk__get_schema("pacemaker-next");
+
+ CRM_ASSERT((entry != NULL) && (entry->prev != NULL));
+ return entry->prev;
}
+/*!
+ * \internal
+ * \brief Return the name of the highest-versioned schema
+ *
+ * \return Name of highest-versioned schema (or NULL on error)
+ */
const char *
-xml_latest_schema(void)
+pcmk__highest_schema_name(void)
{
- return get_schema_name(xml_latest_schema_index());
+ GList *entry = get_highest_schema();
+
+ return ((pcmk__schema_t *)(entry->data))->name;
}
-static inline bool
-version_from_filename(const char *filename, schema_version_t *version)
+/*!
+ * \internal
+ * \brief Find first entry of highest major schema version series
+ *
+ * \return Schema entry of first schema with highest major version
+ */
+GList *
+pcmk__find_x_0_schema(void)
{
- int rc = schema_scanf(filename, "pacemaker-", *version, ".rng");
+#if defined(PCMK__UNIT_TESTING)
+ /* If we're unit testing, this can't be static because it'll stick
+ * around from one test run to the next. It needs to be cleared out
+ * every time.
+ */
+ GList *x_0_entry = NULL;
+#else
+ static GList *x_0_entry = NULL;
+#endif
+
+ pcmk__schema_t *highest_schema = NULL;
+
+ if (x_0_entry != NULL) {
+ return x_0_entry;
+ }
+ x_0_entry = get_highest_schema();
+ highest_schema = x_0_entry->data;
+
+ for (GList *iter = x_0_entry->prev; iter != NULL; iter = iter->prev) {
+ pcmk__schema_t *schema = iter->data;
+
+ /* We've found a schema in an older major version series. Return
+ * the index of the first one in the same major version series as
+ * the highest schema.
+ */
+ if (schema->version.v[0] < highest_schema->version.v[0]) {
+ x_0_entry = iter->next;
+ break;
+ }
+
+ /* We're out of list to examine. This probably means there was only
+ * one major version series, so return the first schema entry.
+ */
+ if (iter->prev == NULL) {
+ x_0_entry = known_schemas->data;
+ break;
+ }
+ }
+ return x_0_entry;
+}
- return (rc == 2);
+static inline bool
+version_from_filename(const char *filename, pcmk__schema_version_t *version)
+{
+ if (pcmk__ends_with(filename, ".rng")) {
+ return sscanf(filename, "pacemaker-%hhu.%hhu.rng", &(version->v[0]), &(version->v[1])) == 2;
+ } else {
+ return sscanf(filename, "pacemaker-%hhu.%hhu", &(version->v[0]), &(version->v[1])) == 2;
+ }
}
static int
schema_filter(const struct dirent *a)
{
int rc = 0;
- schema_version_t version = SCHEMA_ZERO;
+ pcmk__schema_version_t version = SCHEMA_ZERO;
if (strstr(a->d_name, "pacemaker-") != a->d_name) {
/* crm_trace("%s - wrong prefix", a->d_name); */
@@ -147,17 +185,8 @@ schema_filter(const struct dirent *a)
}
static int
-schema_sort(const struct dirent **a, const struct dirent **b)
+schema_cmp(pcmk__schema_version_t a_version, pcmk__schema_version_t b_version)
{
- schema_version_t a_version = SCHEMA_ZERO;
- schema_version_t b_version = SCHEMA_ZERO;
-
- if (!version_from_filename(a[0]->d_name, &a_version)
- || !version_from_filename(b[0]->d_name, &b_version)) {
- // Shouldn't be possible, but makes static analysis happy
- return 0;
- }
-
for (int i = 0; i < 2; ++i) {
if (a_version.v[i] < b_version.v[i]) {
return -1;
@@ -168,6 +197,21 @@ schema_sort(const struct dirent **a, const struct dirent **b)
return 0;
}
+static int
+schema_cmp_directory(const struct dirent **a, const struct dirent **b)
+{
+ pcmk__schema_version_t a_version = SCHEMA_ZERO;
+ pcmk__schema_version_t b_version = SCHEMA_ZERO;
+
+ if (!version_from_filename(a[0]->d_name, &a_version)
+ || !version_from_filename(b[0]->d_name, &b_version)) {
+ // Shouldn't be possible, but makes static analysis happy
+ return 0;
+ }
+
+ return schema_cmp(a_version, b_version);
+}
+
/*!
* \internal
* \brief Add given schema + auxiliary data to internal bookkeeping.
@@ -176,63 +220,35 @@ schema_sort(const struct dirent **a, const struct dirent **b)
* through \c add_schema_by_version.
*/
static void
-add_schema(enum schema_validator_e validator, const schema_version_t *version,
+add_schema(enum pcmk__schema_validator validator, const pcmk__schema_version_t *version,
const char *name, const char *transform,
- const char *transform_enter, bool transform_onleave,
- int after_transform)
+ const char *transform_enter, bool transform_onleave)
{
- int last = xml_schema_max;
- bool have_version = FALSE;
+ pcmk__schema_t *schema = NULL;
- xml_schema_max++;
- known_schemas = pcmk__realloc(known_schemas,
- xml_schema_max * sizeof(struct schema_s));
- CRM_ASSERT(known_schemas != NULL);
- memset(known_schemas+last, 0, sizeof(struct schema_s));
- known_schemas[last].validator = validator;
- known_schemas[last].after_transform = after_transform;
+ schema = pcmk__assert_alloc(1, sizeof(pcmk__schema_t));
- for (int i = 0; i < 2; ++i) {
- known_schemas[last].version.v[i] = version->v[i];
- if (version->v[i]) {
- have_version = TRUE;
- }
- }
- if (have_version) {
- known_schemas[last].name = schema_strdup_printf("pacemaker-", *version, "");
+ schema->validator = validator;
+ schema->version.v[0] = version->v[0];
+ schema->version.v[1] = version->v[1];
+ schema->transform_onleave = transform_onleave;
+ // schema->schema_index is set after all schemas are loaded and sorted
+
+ if (version->v[0] || version->v[1]) {
+ schema->name = schema_strdup_printf("pacemaker-", *version, "");
} else {
- CRM_ASSERT(name);
- schema_scanf(name, "%*[^-]-", known_schemas[last].version, "");
- known_schemas[last].name = strdup(name);
+ schema->name = pcmk__str_copy(name);
}
if (transform) {
- known_schemas[last].transform = strdup(transform);
+ schema->transform = pcmk__str_copy(transform);
}
+
if (transform_enter) {
- known_schemas[last].transform_enter = strdup(transform_enter);
- }
- known_schemas[last].transform_onleave = transform_onleave;
- if (after_transform == 0) {
- after_transform = xml_schema_max; /* upgrade is a one-way */
+ schema->transform_enter = pcmk__str_copy(transform_enter);
}
- known_schemas[last].after_transform = after_transform;
- if (known_schemas[last].after_transform < 0) {
- crm_debug("Added supported schema %d: %s",
- last, known_schemas[last].name);
-
- } else if (known_schemas[last].transform) {
- crm_debug("Added supported schema %d: %s (upgrades to %d with %s.xsl)",
- last, known_schemas[last].name,
- known_schemas[last].after_transform,
- known_schemas[last].transform);
-
- } else {
- crm_debug("Added supported schema %d: %s (upgrades to %d)",
- last, known_schemas[last].name,
- known_schemas[last].after_transform);
- }
+ known_schemas = g_list_prepend(known_schemas, schema);
}
/*!
@@ -264,8 +280,7 @@ add_schema(enum schema_validator_e validator, const schema_version_t *version,
* . name convention: (see "upgrade-enter")
*/
static int
-add_schema_by_version(const schema_version_t *version, int next,
- bool transform_expected)
+add_schema_by_version(const pcmk__schema_version_t *version, bool transform_expected)
{
bool transform_onleave = FALSE;
int rc = pcmk_rc_ok;
@@ -321,12 +336,11 @@ add_schema_by_version(const schema_version_t *version, int next,
free(xslt);
free(transform_upgrade);
transform_upgrade = NULL;
- next = -1;
rc = ENOENT;
}
- add_schema(schema_validator_rng, version, NULL,
- transform_upgrade, transform_enter, transform_onleave, next);
+ add_schema(pcmk__schema_validator_rng, version, NULL,
+ transform_upgrade, transform_enter, transform_onleave);
free(transform_upgrade);
free(transform_enter);
@@ -366,6 +380,85 @@ wrap_libxslt(bool finalize)
}
}
+void
+pcmk__load_schemas_from_dir(const char *dir)
+{
+ int lpc, max;
+ struct dirent **namelist = NULL;
+
+ max = scandir(dir, &namelist, schema_filter, schema_cmp_directory);
+ if (max < 0) {
+ crm_warn("Could not load schemas from %s: %s", dir, strerror(errno));
+ return;
+ }
+
+ for (lpc = 0; lpc < max; lpc++) {
+ bool transform_expected = false;
+ pcmk__schema_version_t version = SCHEMA_ZERO;
+
+ if (!version_from_filename(namelist[lpc]->d_name, &version)) {
+ // Shouldn't be possible, but makes static analysis happy
+ crm_warn("Skipping schema '%s': could not parse version",
+ namelist[lpc]->d_name);
+ continue;
+ }
+ if ((lpc + 1) < max) {
+ pcmk__schema_version_t next_version = SCHEMA_ZERO;
+
+ if (version_from_filename(namelist[lpc+1]->d_name, &next_version)
+ && (version.v[0] < next_version.v[0])) {
+ transform_expected = true;
+ }
+ }
+
+ if (add_schema_by_version(&version, transform_expected) != pcmk_rc_ok) {
+ break;
+ }
+ }
+
+ for (lpc = 0; lpc < max; lpc++) {
+ free(namelist[lpc]);
+ }
+
+ free(namelist);
+}
+
+static gint
+schema_sort_GCompareFunc(gconstpointer a, gconstpointer b)
+{
+ const pcmk__schema_t *schema_a = a;
+ const pcmk__schema_t *schema_b = b;
+
+ // @COMPAT pacemaker-next is deprecated since 2.1.5 and none since 2.1.8
+ if (pcmk__str_eq(schema_a->name, "pacemaker-next", pcmk__str_none)) {
+ if (pcmk__str_eq(schema_b->name, PCMK_VALUE_NONE, pcmk__str_none)) {
+ return -1;
+ } else {
+ return 1;
+ }
+ } else if (pcmk__str_eq(schema_a->name, PCMK_VALUE_NONE, pcmk__str_none)) {
+ return 1;
+ } else if (pcmk__str_eq(schema_b->name, "pacemaker-next", pcmk__str_none)) {
+ return -1;
+ } else {
+ return schema_cmp(schema_a->version, schema_b->version);
+ }
+}
+
+/*!
+ * \internal
+ * \brief Sort the list of known schemas such that all pacemaker-X.Y are in
+ * version order, then pacemaker-next, then none
+ *
+ * This function should be called whenever additional schemas are loaded using
+ * pcmk__load_schemas_from_dir(), after the initial sets in crm_schema_init().
+ */
+void
+pcmk__sort_schemas(void)
+{
+ known_schemas = g_list_sort(known_schemas, schema_sort_GCompareFunc);
+}
+
/*!
* \internal
* \brief Load pacemaker schemas into cache
@@ -376,79 +469,67 @@ wrap_libxslt(bool finalize)
void
crm_schema_init(void)
{
- int lpc, max;
- char *base = pcmk__xml_artefact_root(pcmk__xml_artefact_ns_legacy_rng);
- struct dirent **namelist = NULL;
- const schema_version_t zero = SCHEMA_ZERO;
+ if (!initialized) {
+ const char *remote_schema_dir = pcmk__remote_schema_dir();
+ char *base = pcmk__xml_artefact_root(pcmk__xml_artefact_ns_legacy_rng);
+ const pcmk__schema_version_t zero = SCHEMA_ZERO;
+ int schema_index = 0;
- wrap_libxslt(false);
+ initialized = true;
- max = scandir(base, &namelist, schema_filter, schema_sort);
- if (max < 0) {
- crm_notice("scandir(%s) failed: %s (%d)", base, strerror(errno), errno);
- free(base);
+ wrap_libxslt(false);
- } else {
+ pcmk__load_schemas_from_dir(base);
+ pcmk__load_schemas_from_dir(remote_schema_dir);
free(base);
- for (lpc = 0; lpc < max; lpc++) {
- bool transform_expected = FALSE;
- int next = 0;
- schema_version_t version = SCHEMA_ZERO;
-
- if (!version_from_filename(namelist[lpc]->d_name, &version)) {
- // Shouldn't be possible, but makes static analysis happy
- crm_err("Skipping schema '%s': could not parse version",
- namelist[lpc]->d_name);
- continue;
- }
- if ((lpc + 1) < max) {
- schema_version_t next_version = SCHEMA_ZERO;
- if (version_from_filename(namelist[lpc+1]->d_name, &next_version)
- && (version.v[0] < next_version.v[0])) {
- transform_expected = TRUE;
- }
+ // @COMPAT: Deprecated since 2.1.5
+ add_schema(pcmk__schema_validator_rng, &zero, "pacemaker-next", NULL,
+ NULL, FALSE);
+ // @COMPAT Deprecated since 2.1.8
+ add_schema(pcmk__schema_validator_none, &zero, PCMK_VALUE_NONE, NULL,
+ NULL, FALSE);
+
+ /* add_schema() prepends items to the list, so in the simple case, this
+ * just reverses the list. However if there were any remote schemas,
+ * sorting is necessary.
+ */
+ pcmk__sort_schemas();
+
+ // Now set the schema indexes and log the final result
+ for (GList *iter = known_schemas; iter != NULL; iter = iter->next) {
+ pcmk__schema_t *schema = iter->data;
+
+ if (schema->transform == NULL) {
+ crm_debug("Loaded schema %d: %s", schema_index, schema->name);
} else {
- next = -1;
- }
- if (add_schema_by_version(&version, next, transform_expected)
- == ENOENT) {
- break;
+ crm_debug("Loaded schema %d: %s (upgrades with %s.xsl)",
+ schema_index, schema->name, schema->transform);
}
+ schema->schema_index = schema_index++;
}
-
- for (lpc = 0; lpc < max; lpc++) {
- free(namelist[lpc]);
- }
- free(namelist);
}
-
- // @COMPAT: Deprecated since 2.1.5
- add_schema(schema_validator_rng, &zero, "pacemaker-next",
- NULL, NULL, FALSE, -1);
-
- add_schema(schema_validator_none, &zero, PCMK__VALUE_NONE,
- NULL, NULL, FALSE, -1);
}
-static gboolean
-validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler, void *error_handler_context, const char *relaxng_file,
+static bool
+validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler,
+ void *error_handler_context, const char *relaxng_file,
relaxng_ctx_cache_t **cached_ctx)
{
int rc = 0;
- gboolean valid = TRUE;
+ bool valid = true;
relaxng_ctx_cache_t *ctx = NULL;
- CRM_CHECK(doc != NULL, return FALSE);
- CRM_CHECK(relaxng_file != NULL, return FALSE);
+ CRM_CHECK(doc != NULL, return false);
+ CRM_CHECK(relaxng_file != NULL, return false);
if (cached_ctx && *cached_ctx) {
ctx = *cached_ctx;
} else {
crm_debug("Creating RNG parser context");
- ctx = calloc(1, sizeof(relaxng_ctx_cache_t));
+ ctx = pcmk__assert_alloc(1, sizeof(relaxng_ctx_cache_t));
ctx->parser = xmlRelaxNGNewParserCtxt(relaxng_file);
CRM_CHECK(ctx->parser != NULL, goto cleanup);
@@ -488,7 +569,7 @@ validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler,
rc = xmlRelaxNGValidateDoc(ctx->valid, doc);
if (rc > 0) {
- valid = FALSE;
+ valid = false;
} else if (rc < 0) {
crm_err("Internal libxml error during validation");
@@ -515,6 +596,45 @@ validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler,
return valid;
}
+static void
+free_schema(gpointer data)
+{
+ pcmk__schema_t *schema = data;
+ relaxng_ctx_cache_t *ctx = NULL;
+
+ switch (schema->validator) {
+ case pcmk__schema_validator_none: // not cached
+ break;
+
+ case pcmk__schema_validator_rng: // cached
+ ctx = (relaxng_ctx_cache_t *) schema->cache;
+ if (ctx == NULL) {
+ break;
+ }
+
+ if (ctx->parser != NULL) {
+ xmlRelaxNGFreeParserCtxt(ctx->parser);
+ }
+
+ if (ctx->valid != NULL) {
+ xmlRelaxNGFreeValidCtxt(ctx->valid);
+ }
+
+ if (ctx->rng != NULL) {
+ xmlRelaxNGFree(ctx->rng);
+ }
+
+ free(ctx);
+ schema->cache = NULL;
+ break;
+ }
+
+ free(schema->name);
+ free(schema->transform);
+ free(schema->transform_enter);
+ free(schema);
+}
+
/*!
* \internal
* \brief Clean up global memory associated with XML schemas
@@ -522,62 +642,86 @@ validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler,
void
crm_schema_cleanup(void)
{
- int lpc;
- relaxng_ctx_cache_t *ctx = NULL;
+ if (known_schemas != NULL) {
+ g_list_free_full(known_schemas, free_schema);
+ known_schemas = NULL;
+ }
+ initialized = false;
- for (lpc = 0; lpc < xml_schema_max; lpc++) {
+ wrap_libxslt(true);
+}
- switch (known_schemas[lpc].validator) {
- case schema_validator_none: // not cached
- break;
- case schema_validator_rng: // cached
- ctx = (relaxng_ctx_cache_t *) known_schemas[lpc].cache;
- if (ctx == NULL) {
- break;
- }
- if (ctx->parser != NULL) {
- xmlRelaxNGFreeParserCtxt(ctx->parser);
- }
- if (ctx->valid != NULL) {
- xmlRelaxNGFreeValidCtxt(ctx->valid);
- }
- if (ctx->rng != NULL) {
- xmlRelaxNGFree(ctx->rng);
- }
- free(ctx);
- known_schemas[lpc].cache = NULL;
- break;
+/*!
+ * \internal
+ * \brief Get schema list entry corresponding to a schema name
+ *
+ * \param[in] name Name of schema to get
+ *
+ * \return Schema list entry corresponding to \p name, or NULL if unknown
+ */
+GList *
+pcmk__get_schema(const char *name)
+{
+ // @COMPAT Not specifying a schema name is deprecated since 2.1.8
+ if (name == NULL) {
+ name = PCMK_VALUE_NONE;
+ }
+ for (GList *iter = known_schemas; iter != NULL; iter = iter->next) {
+ pcmk__schema_t *schema = iter->data;
+
+ if (pcmk__str_eq(name, schema->name, pcmk__str_casei)) {
+ return iter;
}
- free(known_schemas[lpc].name);
- free(known_schemas[lpc].transform);
- free(known_schemas[lpc].transform_enter);
}
- free(known_schemas);
- known_schemas = NULL;
+ return NULL;
+}
- wrap_libxslt(true);
+/*!
+ * \internal
+ * \brief Compare two schema version numbers given the schema names
+ *
+ * \param[in] schema1 Name of first schema to compare
+ * \param[in] schema2 Name of second schema to compare
+ *
+ * \return Standard comparison result (negative integer if \p schema1 has the
+ * lower version number, positive integer if \p schema1 has the higher
+ * version number, of 0 if the version numbers are equal)
+ */
+int
+pcmk__cmp_schemas_by_name(const char *schema1_name, const char *schema2_name)
+{
+ GList *entry1 = pcmk__get_schema(schema1_name);
+ GList *entry2 = pcmk__get_schema(schema2_name);
+
+ if (entry1 == NULL) {
+ return (entry2 == NULL)? 0 : -1;
+
+ } else if (entry2 == NULL) {
+ return 1;
+
+ } else {
+ pcmk__schema_t *schema1 = entry1->data;
+ pcmk__schema_t *schema2 = entry2->data;
+
+ return schema1->schema_index - schema2->schema_index;
+ }
}
-static gboolean
-validate_with(xmlNode *xml, int method, xmlRelaxNGValidityErrorFunc error_handler, void* error_handler_context)
+static bool
+validate_with(xmlNode *xml, pcmk__schema_t *schema,
+ xmlRelaxNGValidityErrorFunc error_handler,
+ void *error_handler_context)
{
- gboolean valid = FALSE;
+ bool valid = false;
char *file = NULL;
- struct schema_s *schema = NULL;
relaxng_ctx_cache_t **cache = NULL;
- if (method < 0) {
- return FALSE;
- }
-
- schema = &(known_schemas[method]);
- if (schema->validator == schema_validator_none) {
- return TRUE;
+ if (schema == NULL) {
+ return false;
}
- if (pcmk__str_eq(schema->name, "pacemaker-next", pcmk__str_none)) {
- crm_warn("The pacemaker-next schema is deprecated and will be removed "
- "in a future release.");
+ if (schema->validator == pcmk__schema_validator_none) {
+ return true;
}
file = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng,
@@ -586,13 +730,12 @@ validate_with(xmlNode *xml, int method, xmlRelaxNGValidityErrorFunc error_handle
crm_trace("Validating with %s (type=%d)",
pcmk__s(file, "missing schema"), schema->validator);
switch (schema->validator) {
- case schema_validator_rng:
+ case pcmk__schema_validator_rng:
cache = (relaxng_ctx_cache_t **) &(schema->cache);
valid = validate_with_relaxng(xml->doc, error_handler, error_handler_context, file, cache);
break;
default:
- crm_err("Unknown validator type: %d",
- known_schemas[method].validator);
+ crm_err("Unknown validator type: %d", schema->validator);
break;
}
@@ -601,124 +744,74 @@ validate_with(xmlNode *xml, int method, xmlRelaxNGValidityErrorFunc error_handle
}
static bool
-validate_with_silent(xmlNode *xml, int method)
+validate_with_silent(xmlNode *xml, pcmk__schema_t *schema)
{
bool rc, sl_backup = silent_logging;
silent_logging = TRUE;
- rc = validate_with(xml, method, (xmlRelaxNGValidityErrorFunc) xml_log, GUINT_TO_POINTER(LOG_ERR));
+ rc = validate_with(xml, schema, (xmlRelaxNGValidityErrorFunc) xml_log, GUINT_TO_POINTER(LOG_ERR));
silent_logging = sl_backup;
return rc;
}
-static void
-dump_file(const char *filename)
+bool
+pcmk__validate_xml(xmlNode *xml_blob, const char *validation,
+ xmlRelaxNGValidityErrorFunc error_handler,
+ void *error_handler_context)
{
+ GList *entry = NULL;
+ pcmk__schema_t *schema = NULL;
- FILE *fp = NULL;
- int ch, line = 0;
-
- CRM_CHECK(filename != NULL, return);
-
- fp = fopen(filename, "r");
- if (fp == NULL) {
- crm_perror(LOG_ERR, "Could not open %s for reading", filename);
- return;
- }
-
- fprintf(stderr, "%4d ", ++line);
- do {
- ch = getc(fp);
- if (ch == EOF) {
- putc('\n', stderr);
- break;
- } else if (ch == '\n') {
- fprintf(stderr, "\n%4d ", ++line);
- } else {
- putc(ch, stderr);
- }
- } while (1);
-
- fclose(fp);
-}
-
-gboolean
-validate_xml_verbose(const xmlNode *xml_blob)
-{
- int fd = 0;
- xmlDoc *doc = NULL;
- xmlNode *xml = NULL;
- gboolean rc = FALSE;
- char *filename = NULL;
-
- filename = crm_strdup_printf("%s/cib-invalid.XXXXXX", pcmk__get_tmpdir());
-
- umask(S_IWGRP | S_IWOTH | S_IROTH);
- fd = mkstemp(filename);
- write_xml_fd(xml_blob, filename, fd, FALSE);
-
- dump_file(filename);
-
- doc = xmlReadFile(filename, NULL, 0);
- xml = xmlDocGetRootElement(doc);
- rc = validate_xml(xml, NULL, FALSE);
- free_xml(xml);
-
- unlink(filename);
- free(filename);
-
- return rc;
-}
-
-gboolean
-validate_xml(xmlNode *xml_blob, const char *validation, gboolean to_logs)
-{
- return pcmk__validate_xml(xml_blob, validation, to_logs ? (xmlRelaxNGValidityErrorFunc) xml_log : NULL, GUINT_TO_POINTER(LOG_ERR));
-}
-
-gboolean
-pcmk__validate_xml(xmlNode *xml_blob, const char *validation, xmlRelaxNGValidityErrorFunc error_handler, void* error_handler_context)
-{
- int version = 0;
-
- CRM_CHECK((xml_blob != NULL) && (xml_blob->doc != NULL), return FALSE);
+ CRM_CHECK((xml_blob != NULL) && (xml_blob->doc != NULL), return false);
if (validation == NULL) {
- validation = crm_element_value(xml_blob, XML_ATTR_VALIDATION);
+ validation = crm_element_value(xml_blob, PCMK_XA_VALIDATE_WITH);
}
+ pcmk__warn_if_schema_deprecated(validation);
+ // @COMPAT Not specifying a schema name is deprecated since 2.1.8
if (validation == NULL) {
- int lpc = 0;
- bool valid = FALSE;
-
- for (lpc = 0; lpc < xml_schema_max; lpc++) {
- if (validate_with(xml_blob, lpc, NULL, NULL)) {
- valid = TRUE;
- crm_xml_add(xml_blob, XML_ATTR_VALIDATION,
- known_schemas[lpc].name);
- crm_info("XML validated against %s", known_schemas[lpc].name);
- if(known_schemas[lpc].after_transform == 0) {
- break;
- }
+ bool valid = false;
+
+ for (entry = known_schemas; entry != NULL; entry = entry->next) {
+ schema = entry->data;
+ if (validate_with(xml_blob, schema, NULL, NULL)) {
+ valid = true;
+ crm_xml_add(xml_blob, PCMK_XA_VALIDATE_WITH, schema->name);
+ crm_info("XML validated against %s", schema->name);
}
}
-
return valid;
}
- version = get_schema_version(validation);
- if (strcmp(validation, PCMK__VALUE_NONE) == 0) {
- return TRUE;
- } else if (version < xml_schema_max) {
- return validate_with(xml_blob, version, error_handler, error_handler_context);
+ entry = pcmk__get_schema(validation);
+ if (entry == NULL) {
+ pcmk__config_err("Cannot validate CIB with " PCMK_XA_VALIDATE_WITH
+ " set to an unknown schema such as '%s' (manually"
+ " edit to use a known schema)",
+ validation);
+ return false;
}
- crm_err("Unknown validator: %s", validation);
- return FALSE;
+ schema = entry->data;
+ return validate_with(xml_blob, schema, error_handler,
+ error_handler_context);
}
-static void
-cib_upgrade_err(void *ctx, const char *fmt, ...)
-G_GNUC_PRINTF(2, 3);
+/*!
+ * \internal
+ * \brief Validate XML using its configured schema (and send errors to logs)
+ *
+ * \param[in] xml XML to validate
+ *
+ * \return true if XML validates, otherwise false
+ */
+bool
+pcmk__configured_schema_validates(xmlNode *xml)
+{
+ return pcmk__validate_xml(xml, NULL,
+ (xmlRelaxNGValidityErrorFunc) xml_log,
+ GUINT_TO_POINTER(LOG_ERR));
+}
/* With this arrangement, an attempt to identify the message severity
as explicitly signalled directly from XSLT is performed in rather
@@ -743,7 +836,7 @@ G_GNUC_PRINTF(2, 3);
(suspicious, likely internal errors or some runaways) is
LOG_WARNING.
*/
-static void
+static void G_GNUC_PRINTF(2, 3)
cib_upgrade_err(void *ctx, const char *fmt, ...)
{
va_list ap, aq;
@@ -858,8 +951,20 @@ cib_upgrade_err(void *ctx, const char *fmt, ...)
va_end(ap);
}
+/*!
+ * \internal
+ * \brief Apply a single XSL transformation to given XML
+ *
+ * \param[in] xml XML to transform
+ * \param[in] transform XSL name
+ * \param[in] to_logs If false, certain validation errors will be sent to
+ * stderr rather than logged
+ *
+ * \return Transformed XML on success, otherwise NULL
+ */
static xmlNode *
-apply_transformation(xmlNode *xml, const char *transform, gboolean to_logs)
+apply_transformation(const xmlNode *xml, const char *transform,
+ gboolean to_logs)
{
char *xform = NULL;
xmlNode *out = NULL;
@@ -898,52 +1003,79 @@ apply_transformation(xmlNode *xml, const char *transform, gboolean to_logs)
/*!
* \internal
- * \brief Possibly full enter->upgrade->leave trip per internal bookkeeping.
+ * \brief Perform all transformations needed to upgrade XML to next schema
+ *
+ * A schema upgrade can require up to three XSL transformations: an "enter"
+ * transform, the main upgrade transform, and a "leave" transform. Perform
+ * all needed transforms to upgrade given XML to the next schema.
+ *
+ * \param[in] original_xml XML to transform
+ * \param[in] schema_index Index of schema that successfully validates
+ * \p original_xml
+ * \param[in] to_logs If false, certain validation errors will be sent to
+ * stderr rather than logged
*
- * \note Only emits warnings about enter/leave phases in case of issues.
+ * \return XML result of schema transforms if successful, otherwise NULL
*/
static xmlNode *
-apply_upgrade(xmlNode *xml, const struct schema_s *schema, gboolean to_logs)
+apply_upgrade(const xmlNode *original_xml, int schema_index, gboolean to_logs)
{
- bool transform_onleave = schema->transform_onleave;
+ pcmk__schema_t *schema = g_list_nth_data(known_schemas, schema_index);
+ pcmk__schema_t *upgraded_schema = g_list_nth_data(known_schemas,
+ schema_index + 1);
+ bool transform_onleave = false;
char *transform_leave;
- xmlNode *upgrade = NULL,
- *final = NULL;
+ const xmlNode *xml = original_xml;
+ xmlNode *upgrade = NULL;
+ xmlNode *final = NULL;
+ xmlRelaxNGValidityErrorFunc error_handler = NULL;
- if (schema->transform_enter) {
- crm_debug("Upgrading %s-style configuration, pre-upgrade phase with %s.xsl",
- schema->name, schema->transform_enter);
+ CRM_ASSERT((schema != NULL) && (upgraded_schema != NULL));
+
+ if (to_logs) {
+ error_handler = (xmlRelaxNGValidityErrorFunc) xml_log;
+ }
+
+ transform_onleave = schema->transform_onleave;
+ if (schema->transform_enter != NULL) {
+ crm_debug("Upgrading schema from %s to %s: "
+ "applying pre-upgrade XSL transform %s",
+ schema->name, upgraded_schema->name, schema->transform_enter);
upgrade = apply_transformation(xml, schema->transform_enter, to_logs);
if (upgrade == NULL) {
- crm_warn("Upgrade-enter transformation %s.xsl failed",
+ crm_warn("Pre-upgrade XSL transform %s failed, "
+ "will skip post-upgrade transform",
schema->transform_enter);
transform_onleave = FALSE;
+ } else {
+ xml = upgrade;
}
}
- if (upgrade == NULL) {
- upgrade = xml;
- }
- crm_debug("Upgrading %s-style configuration, main phase with %s.xsl",
- schema->name, schema->transform);
- final = apply_transformation(upgrade, schema->transform, to_logs);
+
+ crm_debug("Upgrading schema from %s to %s: "
+ "applying upgrade XSL transform %s",
+ schema->name, upgraded_schema->name, schema->transform);
+ final = apply_transformation(xml, schema->transform, to_logs);
if (upgrade != xml) {
free_xml(upgrade);
upgrade = NULL;
}
- if (final != NULL && transform_onleave) {
+ if ((final != NULL) && transform_onleave) {
upgrade = final;
/* following condition ensured in add_schema_by_version */
CRM_ASSERT(schema->transform_enter != NULL);
transform_leave = strdup(schema->transform_enter);
/* enter -> leave */
memcpy(strrchr(transform_leave, '-') + 1, "leave", sizeof("leave") - 1);
- crm_debug("Upgrading %s-style configuration, post-upgrade phase with %s.xsl",
- schema->name, transform_leave);
+ crm_debug("Upgrading schema from %s to %s: "
+ "applying post-upgrade XSL transform %s",
+ schema->name, upgraded_schema->name, transform_leave);
final = apply_transformation(upgrade, transform_leave, to_logs);
if (final == NULL) {
- crm_warn("Upgrade-leave transformation %s.xsl failed", transform_leave);
+ crm_warn("Ignoring failure of post-upgrade XSL transform %s",
+ transform_leave);
final = upgrade;
} else {
free_xml(upgrade);
@@ -951,290 +1083,661 @@ apply_upgrade(xmlNode *xml, const struct schema_s *schema, gboolean to_logs)
free(transform_leave);
}
- return final;
-}
+ if (final == NULL) {
+ return NULL;
+ }
-const char *
-get_schema_name(int version)
-{
- if (version < 0 || version >= xml_schema_max) {
- return "unknown";
+ // Ensure result validates with its new schema
+ if (!validate_with(final, upgraded_schema, error_handler,
+ GUINT_TO_POINTER(LOG_ERR))) {
+ crm_err("Schema upgrade from %s to %s failed: "
+ "XSL transform %s produced an invalid configuration",
+ schema->name, upgraded_schema->name, schema->transform);
+ crm_log_xml_debug(final, "bad-transform-result");
+ free_xml(final);
+ return NULL;
}
- return known_schemas[version].name;
+
+ crm_info("Schema upgrade from %s to %s succeeded",
+ schema->name, upgraded_schema->name);
+ return final;
}
-int
-get_schema_version(const char *name)
+/*!
+ * \internal
+ * \brief Get the schema list entry corresponding to XML configuration
+ *
+ * \param[in] xml CIB XML to check
+ *
+ * \return List entry of schema configured in \p xml
+ */
+static GList *
+get_configured_schema(const xmlNode *xml)
{
- int lpc = 0;
+ const char *schema_name = crm_element_value(xml, PCMK_XA_VALIDATE_WITH);
- if (name == NULL) {
- name = PCMK__VALUE_NONE;
+ pcmk__warn_if_schema_deprecated(schema_name);
+ if (schema_name == NULL) {
+ return NULL;
}
- for (; lpc < xml_schema_max; lpc++) {
- if (pcmk__str_eq(name, known_schemas[lpc].name, pcmk__str_casei)) {
- return lpc;
- }
- }
- return -1;
+ return pcmk__get_schema(schema_name);
}
-/* set which validation to use */
+/*!
+ * \brief Update CIB XML to latest schema that validates it
+ *
+ * \param[in,out] xml XML to update (may be freed and replaced
+ * after being transformed)
+ * \param[in] max_schema_name If not NULL, do not update \p xml to any
+ * schema later than this one
+ * \param[in] transform If false, do not update \p xml to any schema
+ * that requires an XSL transform
+ * \param[in] to_logs If false, certain validation errors will be
+ * sent to stderr rather than logged
+ *
+ * \return Standard Pacemaker return code
+ */
int
-update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform,
- gboolean to_logs)
+pcmk__update_schema(xmlNode **xml, const char *max_schema_name, bool transform,
+ bool to_logs)
{
- xmlNode *xml = NULL;
- char *value = NULL;
int max_stable_schemas = xml_latest_schema_index();
- int lpc = 0, match = -1, rc = pcmk_ok;
- int next = -1; /* -1 denotes "inactive" value */
+ int max_schema_index = 0;
+ int rc = pcmk_rc_ok;
+ GList *entry = NULL;
+ pcmk__schema_t *best_schema = NULL;
+ pcmk__schema_t *original_schema = NULL;
xmlRelaxNGValidityErrorFunc error_handler =
to_logs ? (xmlRelaxNGValidityErrorFunc) xml_log : NULL;
- CRM_CHECK(best != NULL, return -EINVAL);
- *best = 0;
-
- CRM_CHECK((xml_blob != NULL) && (*xml_blob != NULL)
- && ((*xml_blob)->doc != NULL),
- return -EINVAL);
-
- xml = *xml_blob;
- value = crm_element_value_copy(xml, XML_ATTR_VALIDATION);
+ CRM_CHECK((xml != NULL) && (*xml != NULL) && ((*xml)->doc != NULL),
+ return EINVAL);
- if (value != NULL) {
- match = get_schema_version(value);
+ if (max_schema_name != NULL) {
+ GList *max_entry = pcmk__get_schema(max_schema_name);
- lpc = match;
- if (lpc >= 0 && transform == FALSE) {
- *best = lpc++;
+ if (max_entry != NULL) {
+ pcmk__schema_t *max_schema = max_entry->data;
- } else if (lpc < 0) {
- crm_debug("Unknown validation schema");
- lpc = 0;
+ max_schema_index = max_schema->schema_index;
}
}
+ if ((max_schema_index < 1) || (max_schema_index > max_stable_schemas)) {
+ max_schema_index = max_stable_schemas;
+ }
- if (match >= max_stable_schemas) {
- /* nothing to do */
- free(value);
- *best = match;
- return pcmk_ok;
+ entry = get_configured_schema(*xml);
+ if (entry == NULL) {
+ // @COMPAT Not specifying a schema name is deprecated since 2.1.8
+ entry = known_schemas;
+ } else {
+ original_schema = entry->data;
+ if (original_schema->schema_index >= max_schema_index) {
+ return pcmk_rc_ok;
+ }
}
- while (lpc <= max_stable_schemas) {
- crm_debug("Testing '%s' validation (%d of %d)",
- known_schemas[lpc].name ? known_schemas[lpc].name : "<unset>",
- lpc, max_stable_schemas);
+ for (; entry != NULL; entry = entry->next) {
+ pcmk__schema_t *current_schema = entry->data;
+ xmlNode *upgrade = NULL;
- if (validate_with(xml, lpc, error_handler, GUINT_TO_POINTER(LOG_ERR)) == FALSE) {
- if (next != -1) {
- crm_info("Configuration not valid for schema: %s",
- known_schemas[lpc].name);
- next = -1;
- } else {
- crm_trace("%s validation failed",
- known_schemas[lpc].name ? known_schemas[lpc].name : "<unset>");
- }
- if (*best) {
+ if (current_schema->schema_index > max_schema_index) {
+ break;
+ }
+
+ if (!validate_with(*xml, current_schema, error_handler,
+ GUINT_TO_POINTER(LOG_ERR))) {
+ crm_debug("Schema %s does not validate", current_schema->name);
+ if (best_schema != NULL) {
/* we've satisfied the validation, no need to check further */
break;
}
- rc = -pcmk_err_schema_validation;
-
- } else {
- if (next != -1) {
- crm_debug("Configuration valid for schema: %s",
- known_schemas[next].name);
- next = -1;
- }
- rc = pcmk_ok;
+ rc = pcmk_rc_schema_validation;
+ continue; // Try again with the next higher schema
}
- if (rc == pcmk_ok) {
- *best = lpc;
+ crm_debug("Schema %s validates", current_schema->name);
+ rc = pcmk_rc_ok;
+ best_schema = current_schema;
+ if (current_schema->schema_index == max_schema_index) {
+ break; // No further transformations possible
}
- if (rc == pcmk_ok && transform) {
- xmlNode *upgrade = NULL;
- next = known_schemas[lpc].after_transform;
-
- if (next <= lpc) {
- /* There is no next version, or next would regress */
- crm_trace("Stopping at %s", known_schemas[lpc].name);
- break;
-
- } else if (max > 0 && (lpc == max || next > max)) {
- crm_trace("Upgrade limit reached at %s (lpc=%d, next=%d, max=%d)",
- known_schemas[lpc].name, lpc, next, max);
- break;
-
- } else if (known_schemas[lpc].transform == NULL
- /* possibly avoid transforming when readily valid
- (in general more restricted when crossing the major
- version boundary, as X.0 "transitional" version is
- expected to be more strict than it's successors that
- may re-allow constructs from previous major line) */
- || validate_with_silent(xml, next)) {
- crm_debug("%s-style configuration is also valid for %s",
- known_schemas[lpc].name, known_schemas[next].name);
-
- lpc = next;
-
- } else {
- crm_debug("Upgrading %s-style configuration to %s with %s.xsl",
- known_schemas[lpc].name, known_schemas[next].name,
- known_schemas[lpc].transform);
-
- upgrade = apply_upgrade(xml, &known_schemas[lpc], to_logs);
- if (upgrade == NULL) {
- crm_err("Transformation %s.xsl failed",
- known_schemas[lpc].transform);
- rc = -pcmk_err_transform_failed;
-
- } else if (validate_with(upgrade, next, error_handler, GUINT_TO_POINTER(LOG_ERR))) {
- crm_info("Transformation %s.xsl successful",
- known_schemas[lpc].transform);
- lpc = next;
- *best = next;
- free_xml(xml);
- xml = upgrade;
- rc = pcmk_ok;
-
- } else {
- crm_err("Transformation %s.xsl did not produce a valid configuration",
- known_schemas[lpc].transform);
- crm_log_xml_info(upgrade, "transform:bad");
- free_xml(upgrade);
- rc = -pcmk_err_schema_validation;
- }
- next = -1;
- }
+ if (!transform || (current_schema->transform == NULL)
+ || validate_with_silent(*xml, entry->next->data)) {
+ /* The next schema either doesn't require a transform or validates
+ * successfully even without the transform. Skip the transform and
+ * try the next schema with the same XML.
+ */
+ continue;
}
- if (transform == FALSE || rc != pcmk_ok) {
- /* we need some progress! */
- lpc++;
+ upgrade = apply_upgrade(*xml, current_schema->schema_index, to_logs);
+ if (upgrade == NULL) {
+ /* The transform failed, so this schema can't be used. Later
+ * schemas are unlikely to validate, but try anyway until we
+ * run out of options.
+ */
+ rc = pcmk_rc_transform_failed;
+ } else {
+ best_schema = current_schema;
+ free_xml(*xml);
+ *xml = upgrade;
}
}
- if (*best > match && *best) {
- crm_info("%s the configuration from %s to %s",
- transform?"Transformed":"Upgraded",
- value ? value : "<none>", known_schemas[*best].name);
- crm_xml_add(xml, XML_ATTR_VALIDATION, known_schemas[*best].name);
+ if (best_schema != NULL) {
+ if ((original_schema == NULL)
+ || (best_schema->schema_index > original_schema->schema_index)) {
+ crm_info("%s the configuration schema to %s",
+ (transform? "Transformed" : "Upgraded"),
+ best_schema->name);
+ crm_xml_add(*xml, PCMK_XA_VALIDATE_WITH, best_schema->name);
+ }
}
-
- *xml_blob = xml;
- free(value);
return rc;
}
-gboolean
-cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs)
+/*!
+ * \brief Update XML from its configured schema to the latest major series
+ *
+ * \param[in,out] xml XML to update
+ * \param[in] to_logs If false, certain validation errors will be
+ * sent to stderr rather than logged
+ *
+ * \return Standard Pacemaker return code
+ */
+int
+pcmk_update_configured_schema(xmlNode **xml, bool to_logs)
{
- gboolean rc = TRUE;
- const char *value = crm_element_value(*xml, XML_ATTR_VALIDATION);
- char *const orig_value = strdup(value == NULL ? "(none)" : value);
+ int rc = pcmk_rc_ok;
+ char *original_schema_name = NULL;
+
+ // @COMPAT Not specifying a schema name is deprecated since 2.1.8
+ const char *effective_original_name = "the first";
+
+ int orig_version = -1;
+ pcmk__schema_t *x_0_schema = pcmk__find_x_0_schema()->data;
+ GList *entry = NULL;
+
+ CRM_CHECK(xml != NULL, return EINVAL);
- int version = get_schema_version(value);
- int orig_version = version;
- int min_version = xml_minimum_schema_index();
+ original_schema_name = crm_element_value_copy(*xml, PCMK_XA_VALIDATE_WITH);
+ pcmk__warn_if_schema_deprecated(original_schema_name);
+ entry = pcmk__get_schema(original_schema_name);
+ if (entry != NULL) {
+ pcmk__schema_t *original_schema = entry->data;
- if (version < min_version) {
+ effective_original_name = original_schema->name;
+ orig_version = original_schema->schema_index;
+ }
+
+ if (orig_version < x_0_schema->schema_index) {
// Current configuration schema is not acceptable, try to update
xmlNode *converted = NULL;
+ const char *new_schema_name = NULL;
+ pcmk__schema_t *schema = NULL;
+
+ entry = NULL;
+ converted = pcmk__xml_copy(NULL, *xml);
+ if (pcmk__update_schema(&converted, NULL, true, to_logs) == pcmk_rc_ok) {
+ new_schema_name = crm_element_value(converted,
+ PCMK_XA_VALIDATE_WITH);
+ entry = pcmk__get_schema(new_schema_name);
+ }
+ schema = (entry == NULL)? NULL : entry->data;
- converted = copy_xml(*xml);
- update_validation(&converted, &version, 0, TRUE, to_logs);
-
- value = crm_element_value(converted, XML_ATTR_VALIDATION);
- if (version < min_version) {
+ if ((schema == NULL)
+ || (schema->schema_index < x_0_schema->schema_index)) {
// Updated configuration schema is still not acceptable
- if (version < orig_version || orig_version == -1) {
+ if ((orig_version == -1) || (schema == NULL)
+ || (schema->schema_index < orig_version)) {
// We couldn't validate any schema at all
if (to_logs) {
pcmk__config_err("Cannot upgrade configuration (claiming "
- "schema %s) to at least %s because it "
+ "%s schema) to at least %s because it "
"does not validate with any schema from "
- "%s to %s",
- orig_value,
- get_schema_name(min_version),
- get_schema_name(orig_version),
- xml_latest_schema());
+ "%s to the latest",
+ pcmk__s(original_schema_name, "no"),
+ x_0_schema->name, effective_original_name);
} else {
fprintf(stderr, "Cannot upgrade configuration (claiming "
- "schema %s) to at least %s because it "
+ "%s schema) to at least %s because it "
"does not validate with any schema from "
- "%s to %s\n",
- orig_value,
- get_schema_name(min_version),
- get_schema_name(orig_version),
- xml_latest_schema());
+ "%s to the latest\n",
+ pcmk__s(original_schema_name, "no"),
+ x_0_schema->name, effective_original_name);
}
} else {
// We updated configuration successfully, but still too low
if (to_logs) {
pcmk__config_err("Cannot upgrade configuration (claiming "
- "schema %s) to at least %s because it "
+ "%s schema) to at least %s because it "
"would not upgrade past %s",
- orig_value,
- get_schema_name(min_version),
- pcmk__s(value, "unspecified version"));
+ pcmk__s(original_schema_name, "no"),
+ x_0_schema->name,
+ pcmk__s(new_schema_name, "unspecified version"));
} else {
fprintf(stderr, "Cannot upgrade configuration (claiming "
- "schema %s) to at least %s because it "
+ "%s schema) to at least %s because it "
"would not upgrade past %s\n",
- orig_value,
- get_schema_name(min_version),
- pcmk__s(value, "unspecified version"));
+ pcmk__s(original_schema_name, "no"),
+ x_0_schema->name,
+ pcmk__s(new_schema_name, "unspecified version"));
}
}
free_xml(converted);
converted = NULL;
- rc = FALSE;
+ rc = pcmk_rc_transform_failed;
} else {
// Updated configuration schema is acceptable
free_xml(*xml);
*xml = converted;
- if (version < xml_latest_schema_index()) {
+ if (schema->schema_index < xml_latest_schema_index()) {
if (to_logs) {
- pcmk__config_warn("Configuration with schema %s was "
+ pcmk__config_warn("Configuration with %s schema was "
"internally upgraded to acceptable (but "
"not most recent) %s",
- orig_value, get_schema_name(version));
- }
- } else {
- if (to_logs) {
- crm_info("Configuration with schema %s was internally "
- "upgraded to latest version %s",
- orig_value, get_schema_name(version));
+ pcmk__s(original_schema_name, "no"),
+ schema->name);
}
+ } else if (to_logs) {
+ crm_info("Configuration with %s schema was internally "
+ "upgraded to latest version %s",
+ pcmk__s(original_schema_name, "no"),
+ schema->name);
}
}
- } else if (version >= get_schema_version(PCMK__VALUE_NONE)) {
- // Schema validation is disabled
- if (to_logs) {
- pcmk__config_warn("Schema validation of configuration is disabled "
- "(enabling is encouraged and prevents common "
- "misconfigurations)");
+ } else {
+ // @COMPAT the none schema is deprecated since 2.1.8
+ pcmk__schema_t *none_schema = NULL;
+
+ entry = pcmk__get_schema(PCMK_VALUE_NONE);
+ CRM_ASSERT((entry != NULL) && (entry->data != NULL));
+
+ none_schema = entry->data;
+ if (!to_logs && (orig_version >= none_schema->schema_index)) {
+ fprintf(stderr, "Schema validation of configuration is "
+ "disabled (support for " PCMK_XA_VALIDATE_WITH
+ " set to \"" PCMK_VALUE_NONE "\" is deprecated"
+ " and will be removed in a future release)\n");
+ }
+ }
- } else {
- fprintf(stderr, "Schema validation of configuration is disabled "
- "(enabling is encouraged and prevents common "
- "misconfigurations)\n");
+ free(original_schema_name);
+ return rc;
+}
+
+/*!
+ * \internal
+ * \brief Return a list of all schema files and any associated XSLT files
+ * later than the given one
+ * \brief Return a list of all schema versions later than the given one
+ *
+ * \param[in] schema The schema to compare against (for example,
+ * "pacemaker-3.1.rng" or "pacemaker-3.1")
+ *
+ * \note The caller is responsible for freeing both the returned list and
+ * the elements of the list
+ */
+GList *
+pcmk__schema_files_later_than(const char *name)
+{
+ GList *lst = NULL;
+ pcmk__schema_version_t ver;
+
+ if (!version_from_filename(name, &ver)) {
+ return lst;
+ }
+
+ for (GList *iter = g_list_nth(known_schemas, xml_latest_schema_index());
+ iter != NULL; iter = iter->prev) {
+ pcmk__schema_t *schema = iter->data;
+ char *s = NULL;
+
+ if (schema_cmp(ver, schema->version) != -1) {
+ continue;
+ }
+
+ s = crm_strdup_printf("%s.rng", schema->name);
+ lst = g_list_prepend(lst, s);
+
+ if (schema->transform != NULL) {
+ char *xform = crm_strdup_printf("%s.xsl", schema->transform);
+ lst = g_list_prepend(lst, xform);
+ }
+
+ if (schema->transform_enter != NULL) {
+ char *enter = crm_strdup_printf("%s.xsl", schema->transform_enter);
+
+ lst = g_list_prepend(lst, enter);
+
+ if (schema->transform_onleave) {
+ int last_dash = strrchr(enter, '-') - enter;
+ char *leave = crm_strdup_printf("%.*s-leave.xsl", last_dash, enter);
+
+ lst = g_list_prepend(lst, leave);
+ }
}
}
- if (best_version) {
- *best_version = version;
+ return lst;
+}
+
+static void
+append_href(xmlNode *xml, void *user_data)
+{
+ GList **list = user_data;
+ char *href = crm_element_value_copy(xml, "href");
+
+ if (href == NULL) {
+ return;
}
+ *list = g_list_prepend(*list, href);
+}
+
+static void
+external_refs_in_schema(GList **list, const char *contents)
+{
+ /* local-name()= is needed to ignore the xmlns= setting at the top of
+ * the XML file. Otherwise, the xpath query will always return nothing.
+ */
+ const char *search = "//*[local-name()='externalRef'] | //*[local-name()='include']";
+ xmlNode *xml = pcmk__xml_parse(contents);
- free(orig_value);
+ crm_foreach_xpath_result(xml, search, append_href, list);
+ free_xml(xml);
+}
+
+static int
+read_file_contents(const char *file, char **contents)
+{
+ int rc = pcmk_rc_ok;
+ char *path = NULL;
+
+ if (pcmk__ends_with(file, ".rng")) {
+ path = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng, file);
+ } else {
+ path = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, file);
+ }
+
+ rc = pcmk__file_contents(path, contents);
+
+ free(path);
return rc;
}
+
+static void
+add_schema_file_to_xml(xmlNode *parent, const char *file, GList **already_included)
+{
+ char *contents = NULL;
+ char *path = NULL;
+ xmlNode *file_node = NULL;
+ GList *includes = NULL;
+ int rc = pcmk_rc_ok;
+
+ /* If we already included this file, don't do so again. */
+ if (g_list_find_custom(*already_included, file, (GCompareFunc) strcmp) != NULL) {
+ return;
+ }
+
+ /* Ensure whatever file we were given has a suffix we know about. If not,
+ * just assume it's an RNG file.
+ */
+ if (!pcmk__ends_with(file, ".rng") && !pcmk__ends_with(file, ".xsl")) {
+ path = crm_strdup_printf("%s.rng", file);
+ } else {
+ path = pcmk__str_copy(file);
+ }
+
+ rc = read_file_contents(path, &contents);
+ if (rc != pcmk_rc_ok || contents == NULL) {
+ crm_warn("Could not read schema file %s: %s", file, pcmk_rc_str(rc));
+ free(path);
+ return;
+ }
+
+ /* Create a new <file path="..."> node with the contents of the file
+ * as a CDATA block underneath it.
+ */
+ file_node = pcmk__xe_create(parent, PCMK_XA_FILE);
+ crm_xml_add(file_node, PCMK_XA_PATH, path);
+ *already_included = g_list_prepend(*already_included, path);
+
+ xmlAddChild(file_node, xmlNewCDataBlock(parent->doc, (pcmkXmlStr) contents,
+ strlen(contents)));
+
+ /* Scan the file for any <externalRef> or <include> nodes and build up
+ * a list of the files they reference.
+ */
+ external_refs_in_schema(&includes, contents);
+
+ /* For each referenced file, recurse to add it (and potentially anything it
+ * references, ...) to the XML.
+ */
+ for (GList *iter = includes; iter != NULL; iter = iter->next) {
+ add_schema_file_to_xml(parent, iter->data, already_included);
+ }
+
+ free(contents);
+ g_list_free_full(includes, free);
+}
+
+/*!
+ * \internal
+ * \brief Add an XML schema file and all the files it references as children
+ * of a given XML node
+ *
+ * \param[in,out] parent The parent XML node
+ * \param[in] name The schema version to compare against
+ * (for example, "pacemaker-3.1" or "pacemaker-3.1.rng")
+ * \param[in,out] already_included A list of names that have already been added
+ * to the parent node.
+ *
+ * \note The caller is responsible for freeing both the returned list and
+ * the elements of the list
+ */
+void
+pcmk__build_schema_xml_node(xmlNode *parent, const char *name, GList **already_included)
+{
+ xmlNode *schema_node = pcmk__xe_create(parent, PCMK__XA_SCHEMA);
+
+ crm_xml_add(schema_node, PCMK_XA_VERSION, name);
+ add_schema_file_to_xml(schema_node, name, already_included);
+
+ if (schema_node->children == NULL) {
+ // Not needed if empty. May happen if name was invalid, for example.
+ free_xml(schema_node);
+ }
+}
+
+/*!
+ * \internal
+ * \brief Return the directory containing any extra schema files that a
+ * Pacemaker Remote node fetched from the cluster
+ */
+const char *
+pcmk__remote_schema_dir(void)
+{
+ const char *dir = pcmk__env_option(PCMK__ENV_REMOTE_SCHEMA_DIRECTORY);
+
+ if (pcmk__str_empty(dir)) {
+ return PCMK__REMOTE_SCHEMA_DIR;
+ }
+
+ return dir;
+}
+
+/*!
+ * \internal
+ * \brief Warn if a given validation schema is deprecated
+ *
+ * \param[in] Schema name to check
+ */
+void
+pcmk__warn_if_schema_deprecated(const char *schema)
+{
+ if ((schema == NULL) ||
+ pcmk__strcase_any_of(schema, "pacemaker-next", PCMK_VALUE_NONE, NULL)) {
+ pcmk__config_warn("Support for " PCMK_XA_VALIDATE_WITH "='%s' is "
+ "deprecated and will be removed in a future release "
+ "without the possibility of upgrades (manually edit "
+ "to use a supported schema)", pcmk__s(schema, ""));
+ }
+}
+
+// Deprecated functions kept only for backward API compatibility
+// LCOV_EXCL_START
+
+#include <crm/common/xml_compat.h>
+
+const char *
+xml_latest_schema(void)
+{
+ return pcmk__highest_schema_name();
+}
+
+const char *
+get_schema_name(int version)
+{
+ pcmk__schema_t *schema = g_list_nth_data(known_schemas, version);
+
+ return (schema != NULL)? schema->name : "unknown";
+}
+
+int
+get_schema_version(const char *name)
+{
+ int lpc = 0;
+
+ if (name == NULL) {
+ name = PCMK_VALUE_NONE;
+ }
+
+ for (GList *iter = known_schemas; iter != NULL; iter = iter->next) {
+ pcmk__schema_t *schema = iter->data;
+
+ if (pcmk__str_eq(name, schema->name, pcmk__str_casei)) {
+ return lpc;
+ }
+
+ lpc++;
+ }
+
+ return -1;
+}
+
+int
+update_validation(xmlNode **xml, int *best, int max, gboolean transform,
+ gboolean to_logs)
+{
+ int rc = pcmk__update_schema(xml, get_schema_name(max), transform, to_logs);
+
+ if ((best != NULL) && (xml != NULL) && (rc == pcmk_rc_ok)) {
+ const char *schema_name = crm_element_value(*xml,
+ PCMK_XA_VALIDATE_WITH);
+ GList *schema_entry = pcmk__get_schema(schema_name);
+
+ if (schema_entry != NULL) {
+ *best = ((pcmk__schema_t *)(schema_entry->data))->schema_index;
+ }
+ }
+
+ return pcmk_rc2legacy(rc);
+}
+
+gboolean
+validate_xml(xmlNode *xml_blob, const char *validation, gboolean to_logs)
+{
+ bool rc = pcmk__validate_xml(xml_blob, validation,
+ to_logs? (xmlRelaxNGValidityErrorFunc) xml_log : NULL,
+ GUINT_TO_POINTER(LOG_ERR));
+ return rc? TRUE : FALSE;
+}
+
+static void
+dump_file(const char *filename)
+{
+
+ FILE *fp = NULL;
+ int ch, line = 0;
+
+ CRM_CHECK(filename != NULL, return);
+
+ fp = fopen(filename, "r");
+ if (fp == NULL) {
+ crm_perror(LOG_ERR, "Could not open %s for reading", filename);
+ return;
+ }
+
+ fprintf(stderr, "%4d ", ++line);
+ do {
+ ch = getc(fp);
+ if (ch == EOF) {
+ putc('\n', stderr);
+ break;
+ } else if (ch == '\n') {
+ fprintf(stderr, "\n%4d ", ++line);
+ } else {
+ putc(ch, stderr);
+ }
+ } while (1);
+
+ fclose(fp);
+}
+
+gboolean
+validate_xml_verbose(const xmlNode *xml_blob)
+{
+ int fd = 0;
+ xmlDoc *doc = NULL;
+ xmlNode *xml = NULL;
+ gboolean rc = FALSE;
+ char *filename = NULL;
+
+ filename = crm_strdup_printf("%s/cib-invalid.XXXXXX", pcmk__get_tmpdir());
+
+ umask(S_IWGRP | S_IWOTH | S_IROTH);
+ fd = mkstemp(filename);
+ pcmk__xml_write_fd(xml_blob, filename, fd, false, NULL);
+
+ dump_file(filename);
+
+ doc = xmlReadFile(filename, NULL, 0);
+ xml = xmlDocGetRootElement(doc);
+ rc = pcmk__validate_xml(xml, NULL, NULL, NULL);
+ free_xml(xml);
+
+ unlink(filename);
+ free(filename);
+
+ return rc? TRUE : FALSE;
+}
+
+gboolean
+cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs)
+{
+ int rc = pcmk_update_configured_schema(xml, to_logs);
+
+ if (best_version != NULL) {
+ const char *name = crm_element_value(*xml, PCMK_XA_VALIDATE_WITH);
+
+ if (name == NULL) {
+ *best_version = -1;
+ } else {
+ GList *entry = pcmk__get_schema(name);
+ pcmk__schema_t *schema = (entry == NULL)? NULL : entry->data;
+
+ *best_version = (schema == NULL)? -1 : schema->schema_index;
+ }
+ }
+ return (rc == pcmk_rc_ok)? TRUE: FALSE;
+}
+
+// LCOV_EXCL_STOP
+// End deprecated API
diff --git a/lib/common/scores.c b/lib/common/scores.c
index 63c314e..39e224e 100644
--- a/lib/common/scores.c
+++ b/lib/common/scores.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2004-2023 the Pacemaker project contributors
+ * Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -39,29 +39,29 @@ char2score(const char *score)
return 0;
} else if (pcmk_str_is_minus_infinity(score)) {
- return -CRM_SCORE_INFINITY;
+ return -PCMK_SCORE_INFINITY;
} else if (pcmk_str_is_infinity(score)) {
- return CRM_SCORE_INFINITY;
+ return PCMK_SCORE_INFINITY;
- } else if (pcmk__str_eq(score, PCMK__VALUE_RED, pcmk__str_casei)) {
+ } else if (pcmk__str_eq(score, PCMK_VALUE_RED, pcmk__str_casei)) {
return pcmk__score_red;
- } else if (pcmk__str_eq(score, PCMK__VALUE_YELLOW, pcmk__str_casei)) {
+ } else if (pcmk__str_eq(score, PCMK_VALUE_YELLOW, pcmk__str_casei)) {
return pcmk__score_yellow;
- } else if (pcmk__str_eq(score, PCMK__VALUE_GREEN, pcmk__str_casei)) {
+ } else if (pcmk__str_eq(score, PCMK_VALUE_GREEN, pcmk__str_casei)) {
return pcmk__score_green;
} else {
long long score_ll;
pcmk__scan_ll(score, &score_ll, 0LL);
- if (score_ll > CRM_SCORE_INFINITY) {
- return CRM_SCORE_INFINITY;
+ if (score_ll > PCMK_SCORE_INFINITY) {
+ return PCMK_SCORE_INFINITY;
- } else if (score_ll < -CRM_SCORE_INFINITY) {
- return -CRM_SCORE_INFINITY;
+ } else if (score_ll < -PCMK_SCORE_INFINITY) {
+ return -PCMK_SCORE_INFINITY;
} else {
return (int) score_ll;
@@ -86,13 +86,13 @@ const char *
pcmk_readable_score(int score)
{
// The longest possible result is "-INFINITY"
- static char score_s[sizeof(CRM_MINUS_INFINITY_S)];
+ static char score_s[sizeof(PCMK_VALUE_MINUS_INFINITY)];
- if (score >= CRM_SCORE_INFINITY) {
- strcpy(score_s, CRM_INFINITY_S);
+ if (score >= PCMK_SCORE_INFINITY) {
+ strcpy(score_s, PCMK_VALUE_INFINITY);
- } else if (score <= -CRM_SCORE_INFINITY) {
- strcpy(score_s, CRM_MINUS_INFINITY_S);
+ } else if (score <= -PCMK_SCORE_INFINITY) {
+ strcpy(score_s, PCMK_VALUE_MINUS_INFINITY);
} else {
// Range is limited to +/-1000000, so no chance of overflow
@@ -115,25 +115,25 @@ pcmk_readable_score(int score)
int
pcmk__add_scores(int score1, int score2)
{
- /* As long as CRM_SCORE_INFINITY is less than half of the maximum integer,
+ /* As long as PCMK_SCORE_INFINITY is less than half of the maximum integer,
* we can ignore the possibility of integer overflow.
*/
int result = score1 + score2;
// First handle the cases where one or both is infinite
- if ((score1 <= -CRM_SCORE_INFINITY) || (score2 <= -CRM_SCORE_INFINITY)) {
- return -CRM_SCORE_INFINITY;
+ if ((score1 <= -PCMK_SCORE_INFINITY) || (score2 <= -PCMK_SCORE_INFINITY)) {
+ return -PCMK_SCORE_INFINITY;
}
- if ((score1 >= CRM_SCORE_INFINITY) || (score2 >= CRM_SCORE_INFINITY)) {
- return CRM_SCORE_INFINITY;
+ if ((score1 >= PCMK_SCORE_INFINITY) || (score2 >= PCMK_SCORE_INFINITY)) {
+ return PCMK_SCORE_INFINITY;
}
// Bound result to infinity.
- if (result >= CRM_SCORE_INFINITY) {
- return CRM_SCORE_INFINITY;
+ if (result >= PCMK_SCORE_INFINITY) {
+ return PCMK_SCORE_INFINITY;
}
- if (result <= -CRM_SCORE_INFINITY) {
- return -CRM_SCORE_INFINITY;
+ if (result <= -PCMK_SCORE_INFINITY) {
+ return -PCMK_SCORE_INFINITY;
}
return result;
@@ -142,21 +142,18 @@ pcmk__add_scores(int score1, int score2)
// Deprecated functions kept only for backward API compatibility
// LCOV_EXCL_START
-#include <crm/common/util_compat.h>
+#include <crm/common/scores_compat.h>
char *
score2char(int score)
{
- char *result = strdup(pcmk_readable_score(score));
-
- CRM_ASSERT(result != NULL);
- return result;
+ return pcmk__str_copy(pcmk_readable_score(score));
}
char *
score2char_stack(int score, char *buf, size_t len)
{
- CRM_CHECK((buf != NULL) && (len >= sizeof(CRM_MINUS_INFINITY_S)),
+ CRM_CHECK((buf != NULL) && (len >= sizeof(PCMK_VALUE_MINUS_INFINITY)),
return NULL);
strcpy(buf, pcmk_readable_score(score));
return buf;
diff --git a/lib/common/strings.c b/lib/common/strings.c
index d9d2fda..acf174d 100644
--- a/lib/common/strings.c
+++ b/lib/common/strings.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2004-2023 the Pacemaker project contributors
+ * Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -342,74 +342,146 @@ pcmk__guint_from_hash(GHashTable *table, const char *key, guint default_val,
return pcmk_rc_ok;
}
-#ifndef NUMCHARS
-# define NUMCHARS "0123456789."
-#endif
-
-#ifndef WHITESPACE
-# define WHITESPACE " \t\n\r\f"
-#endif
-
/*!
* \brief Parse a time+units string and return milliseconds equivalent
*
- * \param[in] input String with a number and optional unit (optionally
- * with whitespace before and/or after the number). If
- * missing, the unit defaults to seconds.
+ * \param[in] input String with a nonnegative number and optional unit
+ * (optionally with whitespace before and/or after the
+ * number). If missing, the unit defaults to seconds.
*
* \return Milliseconds corresponding to string expression, or
- * PCMK__PARSE_INT_DEFAULT on error
+ * \c PCMK__PARSE_INT_DEFAULT on error
*/
long long
crm_get_msec(const char *input)
{
- const char *num_start = NULL;
- const char *units;
+ char *units = NULL; // Do not free; will point to part of input
long long multiplier = 1000;
long long divisor = 1;
long long msec = PCMK__PARSE_INT_DEFAULT;
- size_t num_len = 0;
- char *end_text = NULL;
if (input == NULL) {
return PCMK__PARSE_INT_DEFAULT;
}
- num_start = input + strspn(input, WHITESPACE);
- num_len = strspn(num_start, NUMCHARS);
- if (num_len < 1) {
+ // Skip initial whitespace
+ while (isspace(*input)) {
+ input++;
+ }
+
+ // Reject negative and unparsable inputs
+ scan_ll(input, &msec, -1, &units);
+ if (msec < 0) {
return PCMK__PARSE_INT_DEFAULT;
}
- units = num_start + num_len;
- units += strspn(units, WHITESPACE);
- if (!strncasecmp(units, "ms", 2) || !strncasecmp(units, "msec", 4)) {
+ /* If the number is a decimal, scan_ll() reads only the integer part. Skip
+ * any remaining digits or decimal characters.
+ *
+ * @COMPAT Well-formed and malformed decimals are both accepted inputs. For
+ * example, "3.14 ms" and "3.1.4 ms" are treated the same as "3ms" and
+ * parsed successfully. At a compatibility break, decide if this is still
+ * desired.
+ */
+ while (isdigit(*units) || (*units == '.')) {
+ units++;
+ }
+
+ // Skip any additional whitespace after the number
+ while (isspace(*units)) {
+ units++;
+ }
+
+ /* @COMPAT Use exact comparisons. Currently, we match too liberally, and the
+ * second strncasecmp() in each case is redundant.
+ */
+ if ((*units == '\0')
+ || (strncasecmp(units, "s", 1) == 0)
+ || (strncasecmp(units, "sec", 3) == 0)) {
+ multiplier = 1000;
+ divisor = 1;
+
+ } else if ((strncasecmp(units, "ms", 2) == 0)
+ || (strncasecmp(units, "msec", 4) == 0)) {
multiplier = 1;
divisor = 1;
- } else if (!strncasecmp(units, "us", 2) || !strncasecmp(units, "usec", 4)) {
+
+ } else if ((strncasecmp(units, "us", 2) == 0)
+ || (strncasecmp(units, "usec", 4) == 0)) {
multiplier = 1;
divisor = 1000;
- } else if (!strncasecmp(units, "s", 1) || !strncasecmp(units, "sec", 3)) {
- multiplier = 1000;
- divisor = 1;
- } else if (!strncasecmp(units, "m", 1) || !strncasecmp(units, "min", 3)) {
+
+ } else if ((strncasecmp(units, "m", 1) == 0)
+ || (strncasecmp(units, "min", 3) == 0)) {
multiplier = 60 * 1000;
divisor = 1;
- } else if (!strncasecmp(units, "h", 1) || !strncasecmp(units, "hr", 2)) {
+
+ } else if ((strncasecmp(units, "h", 1) == 0)
+ || (strncasecmp(units, "hr", 2) == 0)) {
multiplier = 60 * 60 * 1000;
divisor = 1;
- } else if ((*units != '\0') && (*units != '\n') && (*units != '\r')) {
+
+ } else {
+ // Invalid units
return PCMK__PARSE_INT_DEFAULT;
}
- scan_ll(num_start, &msec, PCMK__PARSE_INT_DEFAULT, &end_text);
+ // Apply units, capping at LLONG_MAX
if (msec > (LLONG_MAX / multiplier)) {
- // Arithmetics overflow while multiplier/divisor mutually exclusive
return LLONG_MAX;
}
- msec *= multiplier;
- msec /= divisor;
- return msec;
+ return (msec * multiplier) / divisor;
+}
+
+/*!
+ * \brief Parse milliseconds from a Pacemaker interval specification
+ *
+ * \param[in] input Pacemaker time interval specification (a bare number
+ * of seconds; a number with a unit, optionally with
+ * whitespace before and/or after the number; or an ISO
+ * 8601 duration)
+ * \param[out] result_ms Where to store milliseconds equivalent of \p input on
+ * success (limited to the range of an unsigned integer),
+ * or 0 if \p input is \c NULL or invalid
+ *
+ * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok if
+ * \p input is valid or \c NULL, and \c EINVAL otherwise)
+ */
+int
+pcmk_parse_interval_spec(const char *input, guint *result_ms)
+{
+ long long msec = PCMK__PARSE_INT_DEFAULT;
+ int rc = pcmk_rc_ok;
+
+ if (input == NULL) {
+ msec = 0;
+ goto done;
+ }
+
+ if (input[0] == 'P') {
+ crm_time_t *period_s = crm_time_parse_duration(input);
+
+ if (period_s != NULL) {
+ msec = 1000 * crm_time_get_seconds(period_s);
+ crm_time_free(period_s);
+ }
+
+ } else {
+ msec = crm_get_msec(input);
+ }
+
+ if (msec == PCMK__PARSE_INT_DEFAULT) {
+ crm_warn("Using 0 instead of invalid interval specification '%s'",
+ input);
+ msec = 0;
+ rc = EINVAL;
+ }
+
+done:
+ if (result_ms != NULL) {
+ *result_ms = (msec >= G_MAXUINT)? G_MAXUINT : (guint) msec;
+ }
+ return rc;
}
gboolean
@@ -425,17 +497,20 @@ crm_str_to_boolean(const char *s, int *ret)
{
if (s == NULL) {
return -1;
+ }
- } else if (strcasecmp(s, "true") == 0
- || strcasecmp(s, "on") == 0
- || strcasecmp(s, "yes") == 0 || strcasecmp(s, "y") == 0 || strcasecmp(s, "1") == 0) {
- *ret = TRUE;
+ if (pcmk__strcase_any_of(s, PCMK_VALUE_TRUE, "on", "yes", "y", "1", NULL)) {
+ if (ret != NULL) {
+ *ret = TRUE;
+ }
return 1;
+ }
- } else if (strcasecmp(s, "false") == 0
- || strcasecmp(s, "off") == 0
- || strcasecmp(s, "no") == 0 || strcasecmp(s, "n") == 0 || strcasecmp(s, "0") == 0) {
- *ret = FALSE;
+ if (pcmk__strcase_any_of(s, PCMK_VALUE_FALSE, "off", "no", "n", "0",
+ NULL)) {
+ if (ret != NULL) {
+ *ret = FALSE;
+ }
return 1;
}
return -1;
@@ -612,6 +687,24 @@ pcmk__strkey_table(GDestroyNotify key_destroy_func,
key_destroy_func, value_destroy_func);
}
+/*!
+ * \internal
+ * \brief Insert string copies into a hash table as key and value
+ *
+ * \param[in,out] table Hash table to add to
+ * \param[in] name String to add a copy of as key
+ * \param[in] value String to add a copy of as value
+ *
+ * \note This asserts on invalid arguments or memory allocation failure.
+ */
+void
+pcmk__insert_dup(GHashTable *table, const char *name, const char *value)
+{
+ CRM_ASSERT((table != NULL) && (name != NULL));
+
+ g_hash_table_insert(table, pcmk__str_copy(name), pcmk__str_copy(value));
+}
+
/* used with hash tables where case does not matter */
static gboolean
pcmk__strcase_equal(gconstpointer a, gconstpointer b)
@@ -654,7 +747,8 @@ static void
copy_str_table_entry(gpointer key, gpointer value, gpointer user_data)
{
if (key && value && user_data) {
- g_hash_table_insert((GHashTable*)user_data, strdup(key), strdup(value));
+ pcmk__insert_dup((GHashTable *) user_data,
+ (const char *) key, (const char *) value);
}
}
@@ -759,8 +853,7 @@ pcmk__compress(const char *data, unsigned int length, unsigned int max,
clock_gettime(CLOCK_MONOTONIC, &before_t);
#endif
- compressed = calloc((size_t) max, sizeof(char));
- CRM_ASSERT(compressed);
+ compressed = pcmk__assert_alloc((size_t) max, sizeof(char));
*result_len = max;
rc = BZ2_bzBuffToBuffCompress(compressed, result_len, uncompressed, length,
@@ -967,44 +1060,6 @@ pcmk__str_any_of(const char *s, ...)
/*!
* \internal
- * \brief Check whether a character is in any of a list of strings
- *
- * \param[in] ch Character (ASCII) to search for
- * \param[in] ... Strings to search. Final argument must be
- * \c NULL.
- *
- * \return \c true if any of \p ... contain \p ch, \c false otherwise
- * \note \p ... must contain at least one argument (\c NULL).
- */
-bool
-pcmk__char_in_any_str(int ch, ...)
-{
- bool rc = false;
- va_list ap;
-
- /*
- * Passing a char to va_start() can generate compiler warnings,
- * so ch is declared as an int.
- */
- va_start(ap, ch);
-
- while (1) {
- const char *ele = va_arg(ap, const char *);
-
- if (ele == NULL) {
- break;
- } else if (strchr(ele, ch) != NULL) {
- rc = true;
- break;
- }
- }
-
- va_end(ap);
- return rc;
-}
-
-/*!
- * \internal
* \brief Sort strings, with numeric portions sorted numerically
*
* Sort two strings case-insensitively like strcasecmp(), but with any numeric
@@ -1178,6 +1233,35 @@ pcmk__strcmp(const char *s1, const char *s2, uint32_t flags)
/*!
* \internal
+ * \brief Copy a string, asserting on failure
+ *
+ * \param[in] file File where \p function is located
+ * \param[in] function Calling function
+ * \param[in] line Line within \p file
+ * \param[in] str String to copy (can be \c NULL)
+ *
+ * \return Newly allocated copy of \p str, or \c NULL if \p str is \c NULL
+ *
+ * \note The caller is responsible for freeing the return value using \c free().
+ */
+char *
+pcmk__str_copy_as(const char *file, const char *function, uint32_t line,
+ const char *str)
+{
+ if (str != NULL) {
+ char *result = strdup(str);
+
+ if (result == NULL) {
+ crm_abort(file, function, line, "Out of memory", FALSE, TRUE);
+ crm_exit(CRM_EX_OSERR);
+ }
+ return result;
+ }
+ return NULL;
+}
+
+/*!
+ * \internal
* \brief Update a dynamically allocated string with a new value
*
* Given a dynamically allocated string and a new value for it, if the string
@@ -1194,12 +1278,7 @@ pcmk__str_update(char **str, const char *value)
{
if ((str != NULL) && !pcmk__str_eq(*str, value, pcmk__str_none)) {
free(*str);
- if (value == NULL) {
- *str = NULL;
- } else {
- *str = strdup(value);
- CRM_ASSERT(*str != NULL);
- }
+ *str = pcmk__str_copy(value);
}
}
diff --git a/lib/common/tests/Makefile.am b/lib/common/tests/Makefile.am
index c0407e5..bde6605 100644
--- a/lib/common/tests/Makefile.am
+++ b/lib/common/tests/Makefile.am
@@ -1,5 +1,5 @@
#
-# Copyright 2020-2023 the Pacemaker project contributors
+# Copyright 2020-2024 the Pacemaker project contributors
#
# The version control history for this file may have further details.
#
@@ -17,10 +17,16 @@ SUBDIRS = \
io \
iso8601 \
lists \
+ nodes \
nvpair \
options \
output \
+ probes \
+ resources \
results \
+ rules \
+ scheduler \
+ schemas \
scores \
strings \
utils \
diff --git a/lib/common/tests/acl/xml_acl_denied_test.c b/lib/common/tests/acl/xml_acl_denied_test.c
index faf2a39..7c5457e 100644
--- a/lib/common/tests/acl/xml_acl_denied_test.c
+++ b/lib/common/tests/acl/xml_acl_denied_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2020-2021 the Pacemaker project contributors
+ * Copyright 2020-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -17,7 +17,7 @@
static void
is_xml_acl_denied_without_node(void **state)
{
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ xmlNode *test_xml = pcmk__xe_create(NULL, "test_xml");
assert_false(xml_acl_denied(test_xml));
test_xml->doc->_private = NULL;
@@ -35,10 +35,10 @@ is_xml_acl_denied_with_node(void **state)
{
xml_doc_private_t *docpriv;
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ xmlNode *test_xml = pcmk__xe_create(NULL, "test_xml");
// allocate memory for _private, which is NULL by default
- test_xml->doc->_private = calloc(1, sizeof(xml_doc_private_t));
+ test_xml->doc->_private = pcmk__assert_alloc(1, sizeof(xml_doc_private_t));
assert_false(xml_acl_denied(test_xml));
@@ -56,6 +56,6 @@ is_xml_acl_denied_with_node(void **state)
assert_true(xml_acl_denied(test_xml));
}
-PCMK__UNIT_TEST(NULL, NULL,
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
cmocka_unit_test(is_xml_acl_denied_without_node),
cmocka_unit_test(is_xml_acl_denied_with_node))
diff --git a/lib/common/tests/acl/xml_acl_enabled_test.c b/lib/common/tests/acl/xml_acl_enabled_test.c
index 28665f4..97e361e 100644
--- a/lib/common/tests/acl/xml_acl_enabled_test.c
+++ b/lib/common/tests/acl/xml_acl_enabled_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2020-2021 the Pacemaker project contributors
+ * Copyright 2020-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -17,7 +17,7 @@
static void
is_xml_acl_enabled_without_node(void **state)
{
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ xmlNode *test_xml = pcmk__xe_create(NULL, "test_xml");
assert_false(xml_acl_enabled(test_xml));
test_xml->doc->_private = NULL;
@@ -35,10 +35,10 @@ is_xml_acl_enabled_with_node(void **state)
{
xml_doc_private_t *docpriv;
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ xmlNode *test_xml = pcmk__xe_create(NULL, "test_xml");
// allocate memory for _private, which is NULL by default
- test_xml->doc->_private = calloc(1, sizeof(xml_doc_private_t));
+ test_xml->doc->_private = pcmk__assert_alloc(1, sizeof(xml_doc_private_t));
assert_false(xml_acl_enabled(test_xml));
@@ -56,6 +56,6 @@ is_xml_acl_enabled_with_node(void **state)
assert_true(xml_acl_enabled(test_xml));
}
-PCMK__UNIT_TEST(NULL, NULL,
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
cmocka_unit_test(is_xml_acl_enabled_without_node),
cmocka_unit_test(is_xml_acl_enabled_with_node))
diff --git a/lib/common/tests/actions/Makefile.am b/lib/common/tests/actions/Makefile.am
index 6890b84..0dfe521 100644
--- a/lib/common/tests/actions/Makefile.am
+++ b/lib/common/tests/actions/Makefile.am
@@ -1,5 +1,5 @@
#
-# Copyright 2020-2023 the Pacemaker project contributors
+# Copyright 2020-2024 the Pacemaker project contributors
#
# The version control history for this file may have further details.
#
@@ -11,12 +11,6 @@ include $(top_srcdir)/mk/tap.mk
include $(top_srcdir)/mk/unittest.mk
# Add "_test" to the end of all test program names to simplify .gitignore.
-check_PROGRAMS = copy_in_properties_test \
- expand_plus_plus_test \
- fix_plus_plus_recursive_test \
- parse_op_key_test \
- pcmk_is_probe_test \
- pcmk_xe_is_probe_test \
- pcmk_xe_mask_probe_failure_test
+check_PROGRAMS = parse_op_key_test
TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/actions/copy_in_properties_test.c b/lib/common/tests/actions/copy_in_properties_test.c
deleted file mode 100644
index 7882551..0000000
--- a/lib/common/tests/actions/copy_in_properties_test.c
+++ /dev/null
@@ -1,62 +0,0 @@
- /*
- * Copyright 2022 the Pacemaker project contributors
- *
- * The version control history for this file may have further details.
- *
- * This source code is licensed under the GNU General Public License version 2
- * or later (GPLv2+) WITHOUT ANY WARRANTY.
- */
-
-#include <crm_internal.h>
-
-#include <crm/common/unittest_internal.h>
-
-#include <glib.h>
-
-static void
-target_is_NULL(void **state)
-{
- xmlNode *test_xml_1 = create_xml_node(NULL, "test_xml_1");
- xmlNode *test_xml_2 = NULL;
-
- pcmk__xe_set_props(test_xml_1, "test_prop", "test_value", NULL);
-
- copy_in_properties(test_xml_2, test_xml_1);
-
- assert_ptr_equal(test_xml_2, NULL);
-}
-
-static void
-src_is_NULL(void **state)
-{
- xmlNode *test_xml_1 = NULL;
- xmlNode *test_xml_2 = create_xml_node(NULL, "test_xml_2");
-
- copy_in_properties(test_xml_2, test_xml_1);
-
- assert_ptr_equal(test_xml_2->properties, NULL);
-}
-
-static void
-copying_is_successful(void **state)
-{
- const char *xml_1_value;
- const char *xml_2_value;
-
- xmlNode *test_xml_1 = create_xml_node(NULL, "test_xml_1");
- xmlNode *test_xml_2 = create_xml_node(NULL, "test_xml_2");
-
- pcmk__xe_set_props(test_xml_1, "test_prop", "test_value", NULL);
-
- copy_in_properties(test_xml_2, test_xml_1);
-
- xml_1_value = crm_element_value(test_xml_1, "test_prop");
- xml_2_value = crm_element_value(test_xml_2, "test_prop");
-
- assert_string_equal(xml_1_value, xml_2_value);
-}
-
-PCMK__UNIT_TEST(NULL, NULL,
- cmocka_unit_test(target_is_NULL),
- cmocka_unit_test(src_is_NULL),
- cmocka_unit_test(copying_is_successful))
diff --git a/lib/common/tests/actions/expand_plus_plus_test.c b/lib/common/tests/actions/expand_plus_plus_test.c
deleted file mode 100644
index 41471f9..0000000
--- a/lib/common/tests/actions/expand_plus_plus_test.c
+++ /dev/null
@@ -1,256 +0,0 @@
-/*
- * Copyright 2022 the Pacemaker project contributors
- *
- * The version control history for this file may have further details.
- *
- * This source code is licensed under the GNU General Public License version 2
- * or later (GPLv2+) WITHOUT ANY WARRANTY.
- */
-
-#include <crm_internal.h>
-
-#include <crm/common/unittest_internal.h>
-
-#include <glib.h>
-
-static void
-value_is_name_plus_plus(void **state)
-{
- const char *new_value;
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
- crm_xml_add(test_xml, "X", "5");
- expand_plus_plus(test_xml, "X", "X++");
- new_value = crm_element_value(test_xml, "X");
- assert_string_equal(new_value, "6");
-}
-
-static void
-value_is_name_plus_equals_integer(void **state)
-{
- const char *new_value;
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
- crm_xml_add(test_xml, "X", "5");
- expand_plus_plus(test_xml, "X", "X+=2");
- new_value = crm_element_value(test_xml, "X");
- assert_string_equal(new_value, "7");
-}
-
-// NULL input
-
-static void
-target_is_NULL(void **state)
-{
-
- const char *new_value;
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
- crm_xml_add(test_xml, "X", "5");
- expand_plus_plus(NULL, "X", "X++");
- new_value = crm_element_value(test_xml, "X");
- assert_string_equal(new_value, "5");
-}
-
-static void
-name_is_NULL(void **state)
-{
- const char *new_value;
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
- crm_xml_add(test_xml, "X", "5");
- expand_plus_plus(test_xml, NULL, "X++");
- new_value = crm_element_value(test_xml, "X");
- assert_string_equal(new_value, "5");
-}
-
-static void
-value_is_NULL(void **state)
-{
- const char *new_value;
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
- crm_xml_add(test_xml, "X", "5");
- expand_plus_plus(test_xml, "X", NULL);
- new_value = crm_element_value(test_xml, "X");
- assert_string_equal(new_value, "5");
-}
-
-// the value input doesn't start with the name input
-
-static void
-value_is_wrong_name(void **state)
-{
- const char *new_value;
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
- crm_xml_add(test_xml, "X", "5");
- expand_plus_plus(test_xml, "X", "Y++");
- new_value = crm_element_value(test_xml, "X");
- assert_string_equal(new_value, "Y++");
-}
-
-static void
-value_is_only_an_integer(void **state)
-{
- const char *new_value;
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
- crm_xml_add(test_xml, "X", "5");
- expand_plus_plus(test_xml, "X", "2");
- new_value = crm_element_value(test_xml, "X");
- assert_string_equal(new_value, "2");
-}
-
-// non-integers
-
-static void
-variable_is_initialized_to_be_NULL(void **state)
-{
- const char *new_value;
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
- crm_xml_add(test_xml, "X", NULL);
- expand_plus_plus(test_xml, "X", "X++");
- new_value = crm_element_value(test_xml, "X");
- assert_string_equal(new_value, "X++");
-}
-
-static void
-variable_is_initialized_to_be_non_numeric(void **state)
-{
- const char *new_value;
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
- crm_xml_add(test_xml, "X", "hello");
- expand_plus_plus(test_xml, "X", "X++");
- new_value = crm_element_value(test_xml, "X");
- assert_string_equal(new_value, "1");
-}
-
-static void
-variable_is_initialized_to_be_non_numeric_2(void **state)
-{
- const char *new_value;
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
- crm_xml_add(test_xml, "X", "hello");
- expand_plus_plus(test_xml, "X", "X+=2");
- new_value = crm_element_value(test_xml, "X");
- assert_string_equal(new_value, "2");
-}
-
-static void
-variable_is_initialized_to_be_numeric_and_decimal_point_containing(void **state)
-{
- const char *new_value;
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
- crm_xml_add(test_xml, "X", "5.01");
- expand_plus_plus(test_xml, "X", "X++");
- new_value = crm_element_value(test_xml, "X");
- assert_string_equal(new_value, "6");
-}
-
-static void
-variable_is_initialized_to_be_numeric_and_decimal_point_containing_2(void **state)
-{
- const char *new_value;
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
- crm_xml_add(test_xml, "X", "5.50");
- expand_plus_plus(test_xml, "X", "X++");
- new_value = crm_element_value(test_xml, "X");
- assert_string_equal(new_value, "6");
-}
-
-static void
-variable_is_initialized_to_be_numeric_and_decimal_point_containing_3(void **state)
-{
- const char *new_value;
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
- crm_xml_add(test_xml, "X", "5.99");
- expand_plus_plus(test_xml, "X", "X++");
- new_value = crm_element_value(test_xml, "X");
- assert_string_equal(new_value, "6");
-}
-
-static void
-value_is_non_numeric(void **state)
-{
- const char *new_value;
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
- crm_xml_add(test_xml, "X", "5");
- expand_plus_plus(test_xml, "X", "X+=hello");
- new_value = crm_element_value(test_xml, "X");
- assert_string_equal(new_value, "5");
-}
-
-static void
-value_is_numeric_and_decimal_point_containing(void **state)
-{
- const char *new_value;
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
- crm_xml_add(test_xml, "X", "5");
- expand_plus_plus(test_xml, "X", "X+=2.01");
- new_value = crm_element_value(test_xml, "X");
- assert_string_equal(new_value, "7");
-}
-
-static void
-value_is_numeric_and_decimal_point_containing_2(void **state)
-{
- const char *new_value;
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
- crm_xml_add(test_xml, "X", "5");
- expand_plus_plus(test_xml, "X", "X+=1.50");
- new_value = crm_element_value(test_xml, "X");
- assert_string_equal(new_value, "6");
-}
-
-static void
-value_is_numeric_and_decimal_point_containing_3(void **state)
-{
- const char *new_value;
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
- crm_xml_add(test_xml, "X", "5");
- expand_plus_plus(test_xml, "X", "X+=1.99");
- new_value = crm_element_value(test_xml, "X");
- assert_string_equal(new_value, "6");
-}
-
-// undefined input
-
-static void
-name_is_undefined(void **state)
-{
- const char *new_value;
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
- crm_xml_add(test_xml, "Y", "5");
- expand_plus_plus(test_xml, "X", "X++");
- new_value = crm_element_value(test_xml, "X");
- assert_string_equal(new_value, "X++");
-}
-
-// large input
-
-static void
-assignment_result_is_too_large(void **state)
-{
- const char *new_value;
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
- crm_xml_add(test_xml, "X", "5");
- expand_plus_plus(test_xml, "X", "X+=100000000000");
- new_value = crm_element_value(test_xml, "X");
- printf("assignment result is too large %s\n", new_value);
- assert_string_equal(new_value, "1000000");
-}
-
-PCMK__UNIT_TEST(NULL, NULL,
- cmocka_unit_test(value_is_name_plus_plus),
- cmocka_unit_test(value_is_name_plus_equals_integer),
- cmocka_unit_test(target_is_NULL),
- cmocka_unit_test(name_is_NULL),
- cmocka_unit_test(value_is_NULL),
- cmocka_unit_test(value_is_wrong_name),
- cmocka_unit_test(value_is_only_an_integer),
- cmocka_unit_test(variable_is_initialized_to_be_NULL),
- cmocka_unit_test(variable_is_initialized_to_be_non_numeric),
- cmocka_unit_test(variable_is_initialized_to_be_non_numeric_2),
- cmocka_unit_test(variable_is_initialized_to_be_numeric_and_decimal_point_containing),
- cmocka_unit_test(variable_is_initialized_to_be_numeric_and_decimal_point_containing_2),
- cmocka_unit_test(variable_is_initialized_to_be_numeric_and_decimal_point_containing_3),
- cmocka_unit_test(value_is_non_numeric),
- cmocka_unit_test(value_is_numeric_and_decimal_point_containing),
- cmocka_unit_test(value_is_numeric_and_decimal_point_containing_2),
- cmocka_unit_test(value_is_numeric_and_decimal_point_containing_3),
- cmocka_unit_test(name_is_undefined),
- cmocka_unit_test(assignment_result_is_too_large))
diff --git a/lib/common/tests/actions/fix_plus_plus_recursive_test.c b/lib/common/tests/actions/fix_plus_plus_recursive_test.c
deleted file mode 100644
index b3c7cc2..0000000
--- a/lib/common/tests/actions/fix_plus_plus_recursive_test.c
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright 2022 the Pacemaker project contributors
- *
- * The version control history for this file may have further details.
- *
- * This source code is licensed under the GNU General Public License version 2
- * or later (GPLv2+) WITHOUT ANY WARRANTY.
- */
-
-#include <crm_internal.h>
-
-#include <crm/common/unittest_internal.h>
-
-#include <glib.h>
-
-static void
-element_nodes(void **state)
-{
- const char *new_value_root;
- const char *new_value_child;
- const char *new_value_grandchild;
-
- xmlNode *test_xml_root = create_xml_node(NULL, "test_xml_root");
- xmlNode *test_xml_child = create_xml_node(test_xml_root, "test_xml_child");
- xmlNode *test_xml_grandchild = create_xml_node(test_xml_child, "test_xml_grandchild");
- xmlNode *test_xml_text = pcmk_create_xml_text_node(test_xml_root, "text_xml_text", "content");
- xmlNode *test_xml_comment = string2xml("<!-- a comment -->");
-
- crm_xml_add(test_xml_root, "X", "5");
- crm_xml_add(test_xml_child, "X", "X++");
- crm_xml_add(test_xml_grandchild, "X", "X+=2");
- crm_xml_add(test_xml_text, "X", "X++");
-
- fix_plus_plus_recursive(test_xml_root);
- fix_plus_plus_recursive(test_xml_comment);
-
- new_value_root = crm_element_value(test_xml_root, "X");
- new_value_child = crm_element_value(test_xml_child, "X");
- new_value_grandchild = crm_element_value(test_xml_grandchild, "X");
-
- assert_string_equal(new_value_root, "5");
- assert_string_equal(new_value_child, "1");
- assert_string_equal(new_value_grandchild, "2");
-}
-
-PCMK__UNIT_TEST(NULL, NULL,
- cmocka_unit_test(element_nodes))
diff --git a/lib/common/tests/actions/pcmk_xe_is_probe_test.c b/lib/common/tests/actions/pcmk_xe_is_probe_test.c
deleted file mode 100644
index 62b21d9..0000000
--- a/lib/common/tests/actions/pcmk_xe_is_probe_test.c
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright 2021 the Pacemaker project contributors
- *
- * The version control history for this file may have further details.
- *
- * This source code is licensed under the GNU General Public License version 2
- * or later (GPLv2+) WITHOUT ANY WARRANTY.
- */
-
-#include <crm_internal.h>
-
-#include <crm/common/unittest_internal.h>
-
-static void
-op_is_probe_test(void **state)
-{
- xmlNode *node = NULL;
-
- assert_false(pcmk_xe_is_probe(NULL));
-
- node = string2xml("<lrm_rsc_op/>");
- assert_false(pcmk_xe_is_probe(node));
- free_xml(node);
-
- node = string2xml("<lrm_rsc_op operation_key=\"blah\" interval=\"30s\"/>");
- assert_false(pcmk_xe_is_probe(node));
- free_xml(node);
-
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"30s\"/>");
- assert_false(pcmk_xe_is_probe(node));
- free_xml(node);
-
- node = string2xml("<lrm_rsc_op operation=\"start\" interval=\"0\"/>");
- assert_false(pcmk_xe_is_probe(node));
- free_xml(node);
-
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\"/>");
- assert_true(pcmk_xe_is_probe(node));
- free_xml(node);
-}
-
-PCMK__UNIT_TEST(NULL, NULL,
- cmocka_unit_test(op_is_probe_test))
diff --git a/lib/common/tests/actions/pcmk_xe_mask_probe_failure_test.c b/lib/common/tests/actions/pcmk_xe_mask_probe_failure_test.c
deleted file mode 100644
index 9e38019..0000000
--- a/lib/common/tests/actions/pcmk_xe_mask_probe_failure_test.c
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * Copyright 2021 the Pacemaker project contributors
- *
- * The version control history for this file may have further details.
- *
- * This source code is licensed under the GNU General Public License version 2
- * or later (GPLv2+) WITHOUT ANY WARRANTY.
- */
-
-#include <crm_internal.h>
-
-#include <crm/common/unittest_internal.h>
-
-static void
-op_is_not_probe_test(void **state) {
- xmlNode *node = NULL;
-
- /* Not worth testing this thoroughly since it's just a duplicate of whether
- * pcmk_op_is_probe works or not.
- */
-
- node = string2xml("<lrm_rsc_op operation=\"start\" interval=\"0\"/>");
- assert_false(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-}
-
-static void
-op_does_not_have_right_values_test(void **state) {
- xmlNode *node = NULL;
-
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\"/>");
- assert_false(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"0\" op-status=\"\"/>");
- assert_false(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-}
-
-static void
-check_values_test(void **state) {
- xmlNode *node = NULL;
-
- /* PCMK_EXEC_NOT_SUPPORTED */
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"0\" op-status=\"3\"/>");
- assert_false(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"5\" op-status=\"3\"/>");
- assert_true(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-
- /* PCMK_EXEC_DONE */
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"0\" op-status=\"0\"/>");
- assert_false(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"2\" op-status=\"0\"/>");
- assert_true(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"5\" op-status=\"0\"/>");
- assert_true(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"6\" op-status=\"0\"/>");
- assert_false(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"7\" op-status=\"0\"/>");
- assert_false(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-
- /* PCMK_EXEC_NOT_INSTALLED */
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"0\" op-status=\"7\"/>");
- assert_true(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"5\" op-status=\"7\"/>");
- assert_true(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-
- /* PCMK_EXEC_ERROR */
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"0\" op-status=\"4\"/>");
- assert_false(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"2\" op-status=\"4\"/>");
- assert_true(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"5\" op-status=\"4\"/>");
- assert_true(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"6\" op-status=\"4\"/>");
- assert_false(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"7\" op-status=\"4\"/>");
- assert_false(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-
- /* PCMK_EXEC_ERROR_HARD */
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"0\" op-status=\"5\"/>");
- assert_false(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"2\" op-status=\"5\"/>");
- assert_true(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"5\" op-status=\"5\"/>");
- assert_true(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"6\" op-status=\"5\"/>");
- assert_false(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"7\" op-status=\"5\"/>");
- assert_false(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-
- /* PCMK_EXEC_ERROR_FATAL */
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"0\" op-status=\"6\"/>");
- assert_false(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"2\" op-status=\"6\"/>");
- assert_true(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"5\" op-status=\"6\"/>");
- assert_true(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"6\" op-status=\"6\"/>");
- assert_false(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-
- node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"7\" op-status=\"6\"/>");
- assert_false(pcmk_xe_mask_probe_failure(node));
- free_xml(node);
-}
-
-PCMK__UNIT_TEST(NULL, NULL,
- cmocka_unit_test(op_is_not_probe_test),
- cmocka_unit_test(op_does_not_have_right_values_test),
- cmocka_unit_test(check_values_test))
diff --git a/lib/common/tests/health/pcmk__parse_health_strategy_test.c b/lib/common/tests/health/pcmk__parse_health_strategy_test.c
index 28cc702..197ad1f 100644
--- a/lib/common/tests/health/pcmk__parse_health_strategy_test.c
+++ b/lib/common/tests/health/pcmk__parse_health_strategy_test.c
@@ -16,7 +16,7 @@ valid(void **state) {
assert_int_equal(pcmk__parse_health_strategy(NULL),
pcmk__health_strategy_none);
- assert_int_equal(pcmk__parse_health_strategy("none"),
+ assert_int_equal(pcmk__parse_health_strategy(PCMK_VALUE_NONE),
pcmk__health_strategy_none);
assert_int_equal(pcmk__parse_health_strategy("NONE"),
diff --git a/lib/common/tests/health/pcmk__validate_health_strategy_test.c b/lib/common/tests/health/pcmk__validate_health_strategy_test.c
index c7c60aa..e706185 100644
--- a/lib/common/tests/health/pcmk__validate_health_strategy_test.c
+++ b/lib/common/tests/health/pcmk__validate_health_strategy_test.c
@@ -15,7 +15,7 @@
static void
valid_strategy(void **state) {
- assert_true(pcmk__validate_health_strategy("none"));
+ assert_true(pcmk__validate_health_strategy(PCMK_VALUE_NONE));
assert_true(pcmk__validate_health_strategy("None"));
assert_true(pcmk__validate_health_strategy("NONE"));
assert_true(pcmk__validate_health_strategy("NoNe"));
diff --git a/lib/common/tests/io/pcmk__full_path_test.c b/lib/common/tests/io/pcmk__full_path_test.c
index dbbd71b..2f514aa 100644
--- a/lib/common/tests/io/pcmk__full_path_test.c
+++ b/lib/common/tests/io/pcmk__full_path_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2020-2022 the Pacemaker project contributors
+ * Copyright 2020-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -18,8 +18,13 @@ function_asserts(void **state)
{
pcmk__assert_asserts(pcmk__full_path(NULL, "/dir"));
pcmk__assert_asserts(pcmk__full_path("file", NULL));
+}
- pcmk__assert_asserts(
+static void
+function_exits(void **state)
+{
+ pcmk__assert_exits(
+ CRM_EX_OSERR,
{
pcmk__mock_strdup = true; // strdup() will return NULL
expect_string(__wrap_strdup, s, "/full/path");
@@ -49,4 +54,5 @@ full_path(void **state)
PCMK__UNIT_TEST(NULL, NULL,
cmocka_unit_test(function_asserts),
+ cmocka_unit_test(function_exits),
cmocka_unit_test(full_path))
diff --git a/lib/common/tests/iso8601/Makefile.am b/lib/common/tests/iso8601/Makefile.am
index 5187aec..581115a 100644
--- a/lib/common/tests/iso8601/Makefile.am
+++ b/lib/common/tests/iso8601/Makefile.am
@@ -1,5 +1,5 @@
#
-# Copyright 2020-2022 the Pacemaker project contributors
+# Copyright 2020-2024 the Pacemaker project contributors
#
# The version control history for this file may have further details.
#
@@ -11,6 +11,8 @@ include $(top_srcdir)/mk/tap.mk
include $(top_srcdir)/mk/unittest.mk
# Add "_test" to the end of all test program names to simplify .gitignore.
-check_PROGRAMS = pcmk__readable_interval_test
+check_PROGRAMS = pcmk__add_time_from_xml_test \
+ pcmk__readable_interval_test \
+ pcmk__set_time_if_earlier_test
TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/iso8601/pcmk__add_time_from_xml_test.c b/lib/common/tests/iso8601/pcmk__add_time_from_xml_test.c
new file mode 100644
index 0000000..60a71c0
--- /dev/null
+++ b/lib/common/tests/iso8601/pcmk__add_time_from_xml_test.c
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <libxml/tree.h> // xmlNode
+
+#include <crm/common/unittest_internal.h>
+
+#include <crm/common/iso8601.h>
+#include <crm/common/iso8601_internal.h>
+#include <crm/common/xml.h>
+#include "../../crmcommon_private.h"
+
+#define ALL_VALID "<duration id=\"duration1\" years=\"1\" months=\"2\" " \
+ "weeks=\"3\" days=\"-1\" hours=\"1\" minutes=\"1\" " \
+ "seconds=\"1\" />"
+
+#define YEARS_INVALID "<duration id=\"duration1\" years=\"not-a-number\" />"
+
+#define YEARS_TOO_BIG "<duration id=\"duration1\" years=\"2222222222\" />"
+
+#define YEARS_TOO_SMALL "<duration id=\"duration1\" years=\"-2222222222\" />"
+
+static void
+null_time_invalid(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(ALL_VALID);
+
+ assert_int_equal(pcmk__add_time_from_xml(NULL, pcmk__time_years, xml),
+ EINVAL);
+ free_xml(xml);
+}
+
+static void
+null_xml_ok(void **state)
+{
+ crm_time_t *t = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *reference = pcmk_copy_time(t);
+
+ assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_years, NULL),
+ pcmk_rc_ok);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+
+ crm_time_free(t);
+ crm_time_free(reference);
+}
+
+static void
+invalid_component(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(ALL_VALID);
+
+ assert_int_equal(pcmk__add_time_from_xml(NULL, pcmk__time_unknown, xml),
+ EINVAL);
+ free_xml(xml);
+}
+
+static void
+missing_attr(void **state)
+{
+ crm_time_t *t = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *reference = pcmk_copy_time(t);
+ xmlNode *xml = pcmk__xml_parse(YEARS_INVALID);
+
+ assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_months, xml),
+ pcmk_rc_ok);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+
+ crm_time_free(t);
+ crm_time_free(reference);
+ free_xml(xml);
+}
+
+static void
+invalid_attr(void **state)
+{
+ crm_time_t *t = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *reference = pcmk_copy_time(t);
+ xmlNode *xml = pcmk__xml_parse(YEARS_INVALID);
+
+ assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_years, xml),
+ pcmk_rc_unpack_error);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+
+ crm_time_free(t);
+ crm_time_free(reference);
+ free_xml(xml);
+}
+
+static void
+out_of_range_attr(void **state)
+{
+ crm_time_t *t = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *reference = pcmk_copy_time(t);
+ xmlNode *xml = NULL;
+
+ xml = pcmk__xml_parse(YEARS_TOO_BIG);
+ assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_years, xml), ERANGE);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+ free_xml(xml);
+
+ xml = pcmk__xml_parse(YEARS_TOO_SMALL);
+ assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_years, xml), ERANGE);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+ free_xml(xml);
+
+ crm_time_free(t);
+ crm_time_free(reference);
+}
+
+static void
+add_years(void **state)
+{
+ crm_time_t *t = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *reference = crm_time_new("2025-01-01 15:00:00");
+ xmlNode *xml = pcmk__xml_parse(ALL_VALID);
+
+ assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_years, xml),
+ pcmk_rc_ok);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+
+ crm_time_free(t);
+ crm_time_free(reference);
+ free_xml(xml);
+}
+
+static void
+add_months(void **state)
+{
+ crm_time_t *t = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *reference = crm_time_new("2024-03-01 15:00:00");
+ xmlNode *xml = pcmk__xml_parse(ALL_VALID);
+
+ assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_months, xml),
+ pcmk_rc_ok);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+
+ crm_time_free(t);
+ crm_time_free(reference);
+ free_xml(xml);
+}
+
+static void
+add_weeks(void **state)
+{
+ crm_time_t *t = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *reference = crm_time_new("2024-01-22 15:00:00");
+ xmlNode *xml = pcmk__xml_parse(ALL_VALID);
+
+ assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_weeks, xml),
+ pcmk_rc_ok);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+
+ crm_time_free(t);
+ crm_time_free(reference);
+ free_xml(xml);
+}
+
+static void
+add_days(void **state)
+{
+ crm_time_t *t = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *reference = crm_time_new("2023-12-31 15:00:00");
+ xmlNode *xml = pcmk__xml_parse(ALL_VALID);
+
+ assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_days, xml),
+ pcmk_rc_ok);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+
+ crm_time_free(t);
+ crm_time_free(reference);
+ free_xml(xml);
+}
+
+static void
+add_hours(void **state)
+{
+ crm_time_t *t = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *reference = crm_time_new("2024-01-01 16:00:00");
+ xmlNode *xml = pcmk__xml_parse(ALL_VALID);
+
+ assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_hours, xml),
+ pcmk_rc_ok);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+
+ crm_time_free(t);
+ crm_time_free(reference);
+ free_xml(xml);
+}
+
+static void
+add_minutes(void **state)
+{
+ crm_time_t *t = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *reference = crm_time_new("2024-01-01 15:01:00");
+ xmlNode *xml = pcmk__xml_parse(ALL_VALID);
+
+ assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_minutes, xml),
+ pcmk_rc_ok);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+
+ crm_time_free(t);
+ crm_time_free(reference);
+ free_xml(xml);
+}
+
+static void
+add_seconds(void **state)
+{
+ crm_time_t *t = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *reference = crm_time_new("2024-01-01 15:00:01");
+ xmlNode *xml = pcmk__xml_parse(ALL_VALID);
+
+ assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_seconds, xml),
+ pcmk_rc_ok);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+
+ crm_time_free(t);
+ crm_time_free(reference);
+ free_xml(xml);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_time_invalid),
+ cmocka_unit_test(null_xml_ok),
+ cmocka_unit_test(invalid_component),
+ cmocka_unit_test(missing_attr),
+ cmocka_unit_test(invalid_attr),
+ cmocka_unit_test(out_of_range_attr),
+ cmocka_unit_test(add_years),
+ cmocka_unit_test(add_months),
+ cmocka_unit_test(add_weeks),
+ cmocka_unit_test(add_days),
+ cmocka_unit_test(add_hours),
+ cmocka_unit_test(add_minutes),
+ cmocka_unit_test(add_seconds));
diff --git a/lib/common/tests/iso8601/pcmk__readable_interval_test.c b/lib/common/tests/iso8601/pcmk__readable_interval_test.c
index 43b5541..d354975 100644
--- a/lib/common/tests/iso8601/pcmk__readable_interval_test.c
+++ b/lib/common/tests/iso8601/pcmk__readable_interval_test.c
@@ -17,9 +17,11 @@ static void
readable_interval(void **state)
{
assert_string_equal(pcmk__readable_interval(0), "0s");
+ assert_string_equal(pcmk__readable_interval(503), "503ms");
+ assert_string_equal(pcmk__readable_interval(3333), "3.333s");
assert_string_equal(pcmk__readable_interval(30000), "30s");
+ assert_string_equal(pcmk__readable_interval(61000), "1m1s");
assert_string_equal(pcmk__readable_interval(150000), "2m30s");
- assert_string_equal(pcmk__readable_interval(3333), "3.333s");
assert_string_equal(pcmk__readable_interval(UINT_MAX), "49d17h2m47.295s");
}
diff --git a/lib/common/tests/iso8601/pcmk__set_time_if_earlier_test.c b/lib/common/tests/iso8601/pcmk__set_time_if_earlier_test.c
new file mode 100644
index 0000000..c4bf014
--- /dev/null
+++ b/lib/common/tests/iso8601/pcmk__set_time_if_earlier_test.c
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/unittest_internal.h>
+
+#include <crm/common/iso8601.h>
+#include "../../crmcommon_private.h"
+
+static void
+null_ok(void **state)
+{
+ crm_time_t *target = crm_time_new("2024-01-01 00:30:00 +01:00");
+ crm_time_t *target_copy = pcmk_copy_time(target);
+
+ // Should do nothing (just checking it doesn't assert or crash)
+ pcmk__set_time_if_earlier(NULL, NULL);
+ pcmk__set_time_if_earlier(NULL, target);
+
+ // Shouldn't assert, crash, or change target
+ pcmk__set_time_if_earlier(target, NULL);
+ assert_int_equal(crm_time_compare(target, target_copy), 0);
+
+ crm_time_free(target);
+ crm_time_free(target_copy);
+}
+
+static void
+target_undefined(void **state)
+{
+ crm_time_t *source = crm_time_new("2024-01-01 00:29:59 +01:00");
+ crm_time_t *target = crm_time_new_undefined();
+
+ pcmk__set_time_if_earlier(target, source);
+ assert_int_equal(crm_time_compare(target, source), 0);
+
+ crm_time_free(source);
+ crm_time_free(target);
+}
+
+static void
+source_earlier(void **state)
+{
+ crm_time_t *source = crm_time_new("2024-01-01 00:29:59 +01:00");
+ crm_time_t *target = crm_time_new("2024-01-01 00:30:00 +01:00");
+
+ pcmk__set_time_if_earlier(target, source);
+ assert_int_equal(crm_time_compare(target, source), 0);
+
+ crm_time_free(source);
+ crm_time_free(target);
+}
+
+static void
+source_later(void **state)
+{
+ crm_time_t *source = crm_time_new("2024-01-01 00:31:00 +01:00");
+ crm_time_t *target = crm_time_new("2024-01-01 00:30:00 +01:00");
+ crm_time_t *target_copy = pcmk_copy_time(target);
+
+ pcmk__set_time_if_earlier(target, source);
+ assert_int_equal(crm_time_compare(target, target_copy), 0);
+
+ crm_time_free(source);
+ crm_time_free(target);
+ crm_time_free(target_copy);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_ok),
+ cmocka_unit_test(target_undefined),
+ cmocka_unit_test(source_earlier),
+ cmocka_unit_test(source_later))
diff --git a/lib/common/tests/nodes/Makefile.am b/lib/common/tests/nodes/Makefile.am
new file mode 100644
index 0000000..f52c615
--- /dev/null
+++ b/lib/common/tests/nodes/Makefile.am
@@ -0,0 +1,23 @@
+#
+# Copyright 2024 the Pacemaker project contributors
+#
+# The version control history for this file may have further details.
+#
+# This source code is licensed under the GNU General Public License version 2
+# or later (GPLv2+) WITHOUT ANY WARRANTY.
+#
+
+include $(top_srcdir)/mk/tap.mk
+include $(top_srcdir)/mk/unittest.mk
+
+# Add "_test" to the end of all test program names to simplify .gitignore.
+check_PROGRAMS = pcmk__find_node_in_list_test \
+ pcmk_foreach_active_resource_test \
+ pcmk_node_is_clean_test \
+ pcmk_node_is_in_maintenance_test \
+ pcmk_node_is_online_test \
+ pcmk_node_is_pending_test \
+ pcmk_node_is_shutting_down_test \
+ pcmk__xe_add_node_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/nodes/pcmk__find_node_in_list_test.c b/lib/common/tests/nodes/pcmk__find_node_in_list_test.c
new file mode 100644
index 0000000..7726bf5
--- /dev/null
+++ b/lib/common/tests/nodes/pcmk__find_node_in_list_test.c
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2022-2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/unittest_internal.h>
+#include <crm/pengine/internal.h>
+
+static void
+empty_list(void **state)
+{
+ assert_null(pcmk__find_node_in_list(NULL, NULL));
+ assert_null(pcmk__find_node_in_list(NULL, "cluster1"));
+}
+
+static void
+non_null_list(void **state)
+{
+ GList *nodes = NULL;
+
+ pcmk_node_t *a = pcmk__assert_alloc(1, sizeof(pcmk_node_t));
+ pcmk_node_t *b = pcmk__assert_alloc(1, sizeof(pcmk_node_t));
+
+ a->details = pcmk__assert_alloc(1, sizeof(struct pe_node_shared_s));
+ a->details->uname = "cluster1";
+ b->details = pcmk__assert_alloc(1, sizeof(struct pe_node_shared_s));
+ b->details->uname = "cluster2";
+
+ nodes = g_list_append(nodes, a);
+ nodes = g_list_append(nodes, b);
+
+ assert_ptr_equal(a, pcmk__find_node_in_list(nodes, "cluster1"));
+ assert_null(pcmk__find_node_in_list(nodes, "cluster10"));
+ assert_null(pcmk__find_node_in_list(nodes, "nodecluster1"));
+ assert_ptr_equal(b, pcmk__find_node_in_list(nodes, "CLUSTER2"));
+ assert_null(pcmk__find_node_in_list(nodes, "xyz"));
+
+ free(a->details);
+ free(a);
+ free(b->details);
+ free(b);
+ g_list_free(nodes);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(empty_list),
+ cmocka_unit_test(non_null_list))
diff --git a/lib/common/tests/nodes/pcmk__xe_add_node_test.c b/lib/common/tests/nodes/pcmk__xe_add_node_test.c
new file mode 100644
index 0000000..dd77527
--- /dev/null
+++ b/lib/common/tests/nodes/pcmk__xe_add_node_test.c
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/xml.h>
+#include <crm/common/unittest_internal.h>
+#include <crm/common/xml_internal.h>
+
+static void
+bad_input(void **state) {
+ xmlNode *node = NULL;
+
+ pcmk__assert_asserts(pcmk__xe_add_node(NULL, NULL, 0));
+
+ node = pcmk__xe_create(NULL, "test");
+
+ pcmk__xe_add_node(node, NULL, 0);
+ assert_null(xmlHasProp(node, (pcmkXmlStr) PCMK__XA_ATTR_HOST));
+ assert_null(xmlHasProp(node, (pcmkXmlStr) PCMK__XA_ATTR_HOST_ID));
+
+ pcmk__xe_add_node(node, NULL, -100);
+ assert_null(xmlHasProp(node, (pcmkXmlStr) PCMK__XA_ATTR_HOST));
+ assert_null(xmlHasProp(node, (pcmkXmlStr) PCMK__XA_ATTR_HOST_ID));
+
+ free_xml(node);
+}
+
+static void
+expected_input(void **state) {
+ xmlNode *node = pcmk__xe_create(NULL, "test");
+ int i;
+
+ pcmk__xe_add_node(node, "somenode", 47);
+ assert_string_equal("somenode",
+ crm_element_value(node, PCMK__XA_ATTR_HOST));
+ assert_int_equal(pcmk_rc_ok,
+ crm_element_value_int(node, PCMK__XA_ATTR_HOST_ID, &i));
+ assert_int_equal(i, 47);
+
+ free_xml(node);
+}
+
+static void
+repeated_use(void **state) {
+ xmlNode *node = pcmk__xe_create(NULL, "test");
+ int i;
+
+ /* Later calls override settings from earlier calls. */
+ pcmk__xe_add_node(node, "nodeA", 1);
+ pcmk__xe_add_node(node, "nodeB", 2);
+ pcmk__xe_add_node(node, "nodeC", 3);
+
+ assert_string_equal("nodeC", crm_element_value(node, PCMK__XA_ATTR_HOST));
+ assert_int_equal(pcmk_rc_ok,
+ crm_element_value_int(node, PCMK__XA_ATTR_HOST_ID, &i));
+ assert_int_equal(i, 3);
+
+ free_xml(node);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(bad_input),
+ cmocka_unit_test(expected_input),
+ cmocka_unit_test(repeated_use))
diff --git a/lib/common/tests/nodes/pcmk_foreach_active_resource_test.c b/lib/common/tests/nodes/pcmk_foreach_active_resource_test.c
new file mode 100644
index 0000000..2402789
--- /dev/null
+++ b/lib/common/tests/nodes/pcmk_foreach_active_resource_test.c
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h> // NULL
+#include <glib.h> // GList, TRUE, FALSE
+
+#include <crm/common/nodes.h>
+#include <crm/common/resources.h>
+#include <crm/common/unittest_internal.h>
+
+static int counter = 1;
+static int return_false = -1;
+
+static char rsc1_id[] = "rsc1";
+static char rsc2_id[] = "rsc2";
+static char rsc3_id[] = "rsc3";
+
+static pcmk_resource_t rsc1 = {
+ .id = rsc1_id,
+};
+static pcmk_resource_t rsc2 = {
+ .id = rsc2_id,
+};
+static pcmk_resource_t rsc3 = {
+ .id = rsc3_id,
+};
+
+static bool
+fn(pcmk_resource_t *rsc, void *user_data)
+{
+ char *expected_id = crm_strdup_printf("rsc%d", counter);
+
+ assert_string_equal(rsc->id, expected_id);
+ free(expected_id);
+
+ return counter++ != return_false;
+}
+
+static void
+null_args(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .running_rsc = NULL,
+ };
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ counter = 1;
+
+ // These just test that it doesn't crash
+ pcmk_foreach_active_resource(NULL, NULL, NULL);
+ pcmk_foreach_active_resource(&node, NULL, NULL);
+
+ pcmk_foreach_active_resource(NULL, fn, NULL);
+ assert_int_equal(counter, 1);
+}
+
+static void
+list_of_0(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .running_rsc = NULL,
+ };
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ counter = 1;
+ pcmk_foreach_active_resource(&node, fn, NULL);
+ assert_int_equal(counter, 1);
+}
+
+static void
+list_of_1(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .running_rsc = NULL,
+ };
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ shared.running_rsc = g_list_append(shared.running_rsc, &rsc1);
+
+ counter = 1;
+ pcmk_foreach_active_resource(&node, fn, NULL);
+ assert_int_equal(counter, 2);
+
+ g_list_free(shared.running_rsc);
+}
+
+static void
+list_of_3(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .running_rsc = NULL,
+ };
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ shared.running_rsc = g_list_append(shared.running_rsc, &rsc1);
+ shared.running_rsc = g_list_append(shared.running_rsc, &rsc2);
+ shared.running_rsc = g_list_append(shared.running_rsc, &rsc3);
+
+ counter = 1;
+ pcmk_foreach_active_resource(&node, fn, NULL);
+ assert_int_equal(counter, 4);
+
+ g_list_free(shared.running_rsc);
+}
+
+static void
+list_of_3_return_false(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .running_rsc = NULL,
+ };
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ shared.running_rsc = g_list_append(shared.running_rsc, &rsc1);
+ shared.running_rsc = g_list_append(shared.running_rsc, &rsc2);
+ shared.running_rsc = g_list_append(shared.running_rsc, &rsc3);
+
+ counter = 1;
+ return_false = 2;
+ pcmk_foreach_active_resource(&node, fn, NULL);
+ assert_int_equal(counter, 3);
+
+ g_list_free(shared.running_rsc);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_args),
+ cmocka_unit_test(list_of_0),
+ cmocka_unit_test(list_of_1),
+ cmocka_unit_test(list_of_3),
+ cmocka_unit_test(list_of_3_return_false))
diff --git a/lib/common/tests/nodes/pcmk_node_is_clean_test.c b/lib/common/tests/nodes/pcmk_node_is_clean_test.c
new file mode 100644
index 0000000..0534633
--- /dev/null
+++ b/lib/common/tests/nodes/pcmk_node_is_clean_test.c
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h> // NULL
+#include <glib.h> // TRUE, FALSE
+
+#include <crm/common/nodes.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+null_is_unclean(void **state)
+{
+ assert_false(pcmk_node_is_clean(NULL));
+}
+
+static void
+node_is_clean(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .unclean = FALSE,
+ };
+
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ assert_true(pcmk_node_is_clean(&node));
+}
+
+static void
+node_is_unclean(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .unclean = TRUE,
+ };
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ assert_false(pcmk_node_is_clean(&node));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_is_unclean),
+ cmocka_unit_test(node_is_clean),
+ cmocka_unit_test(node_is_unclean))
diff --git a/lib/common/tests/nodes/pcmk_node_is_in_maintenance_test.c b/lib/common/tests/nodes/pcmk_node_is_in_maintenance_test.c
new file mode 100644
index 0000000..45a3b6f
--- /dev/null
+++ b/lib/common/tests/nodes/pcmk_node_is_in_maintenance_test.c
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h> // NULL
+#include <glib.h> // TRUE, FALSE
+
+#include <crm/common/nodes.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+null_is_not_in_maintenance(void **state)
+{
+ assert_false(pcmk_node_is_in_maintenance(NULL));
+}
+
+static void
+node_is_in_maintenance(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .maintenance = TRUE,
+ };
+
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ assert_true(pcmk_node_is_in_maintenance(&node));
+}
+
+static void
+node_is_not_in_maintenance(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .maintenance = FALSE,
+ };
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ assert_false(pcmk_node_is_in_maintenance(&node));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_is_not_in_maintenance),
+ cmocka_unit_test(node_is_in_maintenance),
+ cmocka_unit_test(node_is_not_in_maintenance))
diff --git a/lib/common/tests/nodes/pcmk_node_is_online_test.c b/lib/common/tests/nodes/pcmk_node_is_online_test.c
new file mode 100644
index 0000000..d22e3b4
--- /dev/null
+++ b/lib/common/tests/nodes/pcmk_node_is_online_test.c
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h> // NULL
+#include <glib.h> // TRUE, FALSE
+
+#include <crm/common/nodes.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+null_is_offline(void **state)
+{
+ assert_false(pcmk_node_is_online(NULL));
+}
+
+static void
+node_is_online(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .online = TRUE,
+ };
+
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ assert_true(pcmk_node_is_online(&node));
+}
+
+static void
+node_is_offline(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .online = FALSE,
+ };
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ assert_false(pcmk_node_is_online(&node));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_is_offline),
+ cmocka_unit_test(node_is_online),
+ cmocka_unit_test(node_is_offline))
diff --git a/lib/common/tests/nodes/pcmk_node_is_pending_test.c b/lib/common/tests/nodes/pcmk_node_is_pending_test.c
new file mode 100644
index 0000000..9f2abca
--- /dev/null
+++ b/lib/common/tests/nodes/pcmk_node_is_pending_test.c
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h> // NULL
+#include <glib.h> // TRUE, FALSE
+
+#include <crm/common/nodes.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+null_is_not_pending(void **state)
+{
+ assert_false(pcmk_node_is_pending(NULL));
+}
+
+static void
+node_is_pending(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .pending = TRUE,
+ };
+
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ assert_true(pcmk_node_is_pending(&node));
+}
+
+static void
+node_is_not_pending(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .pending = FALSE,
+ };
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ assert_false(pcmk_node_is_pending(&node));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_is_not_pending),
+ cmocka_unit_test(node_is_pending),
+ cmocka_unit_test(node_is_not_pending))
diff --git a/lib/common/tests/nodes/pcmk_node_is_shutting_down_test.c b/lib/common/tests/nodes/pcmk_node_is_shutting_down_test.c
new file mode 100644
index 0000000..b6054b0
--- /dev/null
+++ b/lib/common/tests/nodes/pcmk_node_is_shutting_down_test.c
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h> // NULL
+#include <glib.h> // TRUE, FALSE
+
+#include <crm/common/nodes.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+null_is_not_shutting_down(void **state)
+{
+ assert_false(pcmk_node_is_shutting_down(NULL));
+}
+
+static void
+node_is_shutting_down(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .shutdown = TRUE,
+ };
+
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ assert_true(pcmk_node_is_shutting_down(&node));
+}
+
+static void
+node_is_not_shutting_down(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .shutdown = FALSE,
+ };
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ assert_false(pcmk_node_is_shutting_down(&node));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_is_not_shutting_down),
+ cmocka_unit_test(node_is_shutting_down),
+ cmocka_unit_test(node_is_not_shutting_down))
diff --git a/lib/common/tests/nvpair/Makefile.am b/lib/common/tests/nvpair/Makefile.am
index 7f406bd..9f762d4 100644
--- a/lib/common/tests/nvpair/Makefile.am
+++ b/lib/common/tests/nvpair/Makefile.am
@@ -1,5 +1,5 @@
#
-# Copyright 2021-2023 the Pacemaker project contributors
+# Copyright 2021-2024 the Pacemaker project contributors
#
# The version control history for this file may have further details.
#
@@ -11,7 +11,10 @@ include $(top_srcdir)/mk/tap.mk
include $(top_srcdir)/mk/unittest.mk
# Add "_test" to the end of all test program names to simplify .gitignore.
-check_PROGRAMS = pcmk__xe_attr_is_true_test \
+check_PROGRAMS = crm_meta_name_test \
+ crm_meta_value_test \
+ pcmk__xe_attr_is_true_test \
+ pcmk__xe_get_datetime_test \
pcmk__xe_get_bool_attr_test \
pcmk__xe_set_bool_attr_test
diff --git a/lib/common/tests/utils/crm_meta_name_test.c b/lib/common/tests/nvpair/crm_meta_name_test.c
index 06fecc5..7c6c32b 100644
--- a/lib/common/tests/utils/crm_meta_name_test.c
+++ b/lib/common/tests/nvpair/crm_meta_name_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 the Pacemaker project contributors
+ * Copyright 2022-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -10,12 +10,12 @@
#include <crm_internal.h>
#include <crm/common/unittest_internal.h>
-#include <crm/msg_xml.h>
+#include <crm/common/xml.h>
static void
empty_params(void **state)
{
- assert_null(crm_meta_name(NULL));
+ pcmk__assert_asserts(crm_meta_name(NULL));
}
static void
@@ -23,11 +23,11 @@ standard_usage(void **state)
{
char *s = NULL;
- s = crm_meta_name(XML_RSC_ATTR_NOTIFY);
+ s = crm_meta_name(PCMK_META_NOTIFY);
assert_string_equal(s, "CRM_meta_notify");
free(s);
- s = crm_meta_name(XML_RSC_ATTR_STICKINESS);
+ s = crm_meta_name(PCMK_META_RESOURCE_STICKINESS);
assert_string_equal(s, "CRM_meta_resource_stickiness");
free(s);
diff --git a/lib/common/tests/utils/crm_meta_value_test.c b/lib/common/tests/nvpair/crm_meta_value_test.c
index 0bde5c6..ffe9619 100644
--- a/lib/common/tests/utils/crm_meta_value_test.c
+++ b/lib/common/tests/nvpair/crm_meta_value_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 the Pacemaker project contributors
+ * Copyright 2022-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -10,7 +10,7 @@
#include <crm_internal.h>
#include <crm/common/unittest_internal.h>
-#include <crm/msg_xml.h>
+#include <crm/common/xml.h>
#include <glib.h>
@@ -30,8 +30,8 @@ key_not_in_table(void **state)
{
GHashTable *tbl = pcmk__strkey_table(free, free);
- assert_null(crm_meta_value(tbl, XML_RSC_ATTR_NOTIFY));
- assert_null(crm_meta_value(tbl, XML_RSC_ATTR_STICKINESS));
+ assert_null(crm_meta_value(tbl, PCMK_META_NOTIFY));
+ assert_null(crm_meta_value(tbl, PCMK_META_RESOURCE_STICKINESS));
g_hash_table_destroy(tbl);
}
@@ -41,11 +41,13 @@ key_in_table(void **state)
{
GHashTable *tbl = pcmk__strkey_table(free, free);
- g_hash_table_insert(tbl, crm_meta_name(XML_RSC_ATTR_NOTIFY), strdup("1"));
- g_hash_table_insert(tbl, crm_meta_name(XML_RSC_ATTR_STICKINESS), strdup("2"));
+ g_hash_table_insert(tbl, crm_meta_name(PCMK_META_NOTIFY), strdup("1"));
+ g_hash_table_insert(tbl, crm_meta_name(PCMK_META_RESOURCE_STICKINESS),
+ strdup("2"));
- assert_string_equal(crm_meta_value(tbl, XML_RSC_ATTR_NOTIFY), "1");
- assert_string_equal(crm_meta_value(tbl, XML_RSC_ATTR_STICKINESS), "2");
+ assert_string_equal(crm_meta_value(tbl, PCMK_META_NOTIFY), "1");
+ assert_string_equal(crm_meta_value(tbl, PCMK_META_RESOURCE_STICKINESS),
+ "2");
g_hash_table_destroy(tbl);
}
diff --git a/lib/common/tests/nvpair/pcmk__xe_attr_is_true_test.c b/lib/common/tests/nvpair/pcmk__xe_attr_is_true_test.c
index 3723707..84187da 100644
--- a/lib/common/tests/nvpair/pcmk__xe_attr_is_true_test.c
+++ b/lib/common/tests/nvpair/pcmk__xe_attr_is_true_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 the Pacemaker project contributors
+ * Copyright 2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -15,7 +15,7 @@
static void
empty_input(void **state)
{
- xmlNode *node = string2xml("<node/>");
+ xmlNode *node = pcmk__xml_parse("<node/>");
assert_false(pcmk__xe_attr_is_true(NULL, NULL));
assert_false(pcmk__xe_attr_is_true(NULL, "whatever"));
@@ -27,7 +27,7 @@ empty_input(void **state)
static void
attr_missing(void **state)
{
- xmlNode *node = string2xml("<node a=\"true\" b=\"false\"/>");
+ xmlNode *node = pcmk__xml_parse("<node a=\"true\" b=\"false\"/>");
assert_false(pcmk__xe_attr_is_true(node, "c"));
free_xml(node);
@@ -36,7 +36,7 @@ attr_missing(void **state)
static void
attr_present(void **state)
{
- xmlNode *node = string2xml("<node a=\"true\" b=\"false\"/>");
+ xmlNode *node = pcmk__xml_parse("<node a=\"true\" b=\"false\"/>");
assert_true(pcmk__xe_attr_is_true(node, "a"));
assert_false(pcmk__xe_attr_is_true(node, "b"));
@@ -44,7 +44,7 @@ attr_present(void **state)
free_xml(node);
}
-PCMK__UNIT_TEST(NULL, NULL,
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
cmocka_unit_test(empty_input),
cmocka_unit_test(attr_missing),
cmocka_unit_test(attr_present))
diff --git a/lib/common/tests/nvpair/pcmk__xe_get_bool_attr_test.c b/lib/common/tests/nvpair/pcmk__xe_get_bool_attr_test.c
index 500d8a6..4823f6a 100644
--- a/lib/common/tests/nvpair/pcmk__xe_get_bool_attr_test.c
+++ b/lib/common/tests/nvpair/pcmk__xe_get_bool_attr_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2021-2023 the Pacemaker project contributors
+ * Copyright 2021-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -15,7 +15,7 @@
static void
empty_input(void **state)
{
- xmlNode *node = string2xml("<node/>");
+ xmlNode *node = pcmk__xml_parse("<node/>");
bool value;
assert_int_equal(pcmk__xe_get_bool_attr(NULL, NULL, &value), ENODATA);
@@ -29,7 +29,7 @@ empty_input(void **state)
static void
attr_missing(void **state)
{
- xmlNode *node = string2xml("<node a=\"true\" b=\"false\"/>");
+ xmlNode *node = pcmk__xml_parse("<node a=\"true\" b=\"false\"/>");
bool value;
assert_int_equal(pcmk__xe_get_bool_attr(node, "c", &value), ENODATA);
@@ -39,7 +39,8 @@ attr_missing(void **state)
static void
attr_present(void **state)
{
- xmlNode *node = string2xml("<node a=\"true\" b=\"false\" c=\"blah\"/>");
+ xmlNode *node = pcmk__xml_parse("<node a=\"true\" b=\"false\" "
+ "c=\"blah\"/>");
bool value;
value = false;
@@ -53,7 +54,7 @@ attr_present(void **state)
free_xml(node);
}
-PCMK__UNIT_TEST(NULL, NULL,
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
cmocka_unit_test(empty_input),
cmocka_unit_test(attr_missing),
cmocka_unit_test(attr_present))
diff --git a/lib/common/tests/nvpair/pcmk__xe_get_datetime_test.c b/lib/common/tests/nvpair/pcmk__xe_get_datetime_test.c
new file mode 100644
index 0000000..6da1e23
--- /dev/null
+++ b/lib/common/tests/nvpair/pcmk__xe_get_datetime_test.c
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <errno.h>
+#include <libxml/tree.h>
+
+#include <crm/common/unittest_internal.h>
+
+#include <crm/common/iso8601.h>
+#include <crm/common/xml.h>
+#include <crm/common/nvpair_internal.h>
+
+#define REFERENCE_ISO8601 "2024-001"
+#define ATTR_PRESENT "start"
+#define ATTR_MISSING "end"
+#define REFERENCE_XML "<date_expression id=\"id1\" " \
+ ATTR_PRESENT "=\"" REFERENCE_ISO8601 "\"" \
+ " operation=\"gt\">"
+#define BAD_XML "<date_expression id=\"id1\" " \
+ ATTR_PRESENT "=\"not_a_time\"" \
+ " operation=\"gt\">"
+
+static void
+null_invalid(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(REFERENCE_XML);
+ crm_time_t *t = NULL;
+
+ assert_int_equal(pcmk__xe_get_datetime(NULL, NULL, NULL), EINVAL);
+ assert_int_equal(pcmk__xe_get_datetime(xml, NULL, NULL), EINVAL);
+ assert_int_equal(pcmk__xe_get_datetime(xml, ATTR_PRESENT, NULL), EINVAL);
+ assert_int_equal(pcmk__xe_get_datetime(xml, NULL, &t), EINVAL);
+ assert_null(t);
+ assert_int_equal(pcmk__xe_get_datetime(NULL, ATTR_PRESENT, NULL), EINVAL);
+ assert_int_equal(pcmk__xe_get_datetime(NULL, ATTR_PRESENT, &t), EINVAL);
+ assert_null(t);
+ assert_int_equal(pcmk__xe_get_datetime(NULL, NULL, &t), EINVAL);
+ assert_null(t);
+
+ free_xml(xml);
+}
+
+static void
+nonnull_time_invalid(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(REFERENCE_XML);
+ crm_time_t *t = crm_time_new_undefined();
+
+ assert_int_equal(pcmk__xe_get_datetime(xml, ATTR_PRESENT, &t), EINVAL);
+
+ crm_time_free(t);
+ free_xml(xml);
+}
+
+static void
+attr_missing(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(REFERENCE_XML);
+ crm_time_t *t = NULL;
+
+ assert_int_equal(pcmk__xe_get_datetime(xml, ATTR_MISSING, &t), pcmk_rc_ok);
+ assert_null(t);
+
+ free_xml(xml);
+}
+
+static void
+attr_valid(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(REFERENCE_XML);
+ crm_time_t *t = NULL;
+ crm_time_t *reference = crm_time_new(REFERENCE_ISO8601);
+
+ assert_int_equal(pcmk__xe_get_datetime(xml, ATTR_PRESENT, &t), pcmk_rc_ok);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+
+ crm_time_free(t);
+ crm_time_free(reference);
+ free_xml(xml);
+}
+
+static void
+attr_invalid(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(BAD_XML);
+ crm_time_t *t = NULL;
+
+ assert_int_equal(pcmk__xe_get_datetime(xml, ATTR_PRESENT, &t),
+ pcmk_rc_unpack_error);
+ assert_null(t);
+
+ free_xml(xml);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_invalid),
+ cmocka_unit_test(nonnull_time_invalid),
+ cmocka_unit_test(attr_missing),
+ cmocka_unit_test(attr_valid),
+ cmocka_unit_test(attr_invalid))
diff --git a/lib/common/tests/nvpair/pcmk__xe_set_bool_attr_test.c b/lib/common/tests/nvpair/pcmk__xe_set_bool_attr_test.c
index e1fb9c4..dda2878 100644
--- a/lib/common/tests/nvpair/pcmk__xe_set_bool_attr_test.c
+++ b/lib/common/tests/nvpair/pcmk__xe_set_bool_attr_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 the Pacemaker project contributors
+ * Copyright 2021-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -11,21 +11,21 @@
#include <crm/common/unittest_internal.h>
#include <crm/common/xml_internal.h>
-#include <crm/msg_xml.h>
+#include <crm/common/xml.h>
static void
set_attr(void **state)
{
- xmlNode *node = string2xml("<node/>");
+ xmlNode *node = pcmk__xml_parse("<node/>");
pcmk__xe_set_bool_attr(node, "a", true);
pcmk__xe_set_bool_attr(node, "b", false);
- assert_string_equal(crm_element_value(node, "a"), XML_BOOLEAN_TRUE);
- assert_string_equal(crm_element_value(node, "b"), XML_BOOLEAN_FALSE);
+ assert_string_equal(crm_element_value(node, "a"), PCMK_VALUE_TRUE);
+ assert_string_equal(crm_element_value(node, "b"), PCMK_VALUE_FALSE);
free_xml(node);
}
-PCMK__UNIT_TEST(NULL, NULL,
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
cmocka_unit_test(set_attr))
diff --git a/lib/common/tests/probes/Makefile.am b/lib/common/tests/probes/Makefile.am
new file mode 100644
index 0000000..f5a3fb4
--- /dev/null
+++ b/lib/common/tests/probes/Makefile.am
@@ -0,0 +1,18 @@
+#
+# Copyright 2024 the Pacemaker project contributors
+#
+# The version control history for this file may have further details.
+#
+# This source code is licensed under the GNU General Public License version 2
+# or later (GPLv2+) WITHOUT ANY WARRANTY.
+#
+
+include $(top_srcdir)/mk/tap.mk
+include $(top_srcdir)/mk/unittest.mk
+
+# Add "_test" to the end of all test program names to simplify .gitignore.
+check_PROGRAMS = pcmk_is_probe_test \
+ pcmk_xe_is_probe_test \
+ pcmk_xe_mask_probe_failure_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/actions/pcmk_is_probe_test.c b/lib/common/tests/probes/pcmk_is_probe_test.c
index 4a65e3f..835aac1 100644
--- a/lib/common/tests/actions/pcmk_is_probe_test.c
+++ b/lib/common/tests/probes/pcmk_is_probe_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 the Pacemaker project contributors
+ * Copyright 2021-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
diff --git a/lib/common/tests/probes/pcmk_xe_is_probe_test.c b/lib/common/tests/probes/pcmk_xe_is_probe_test.c
new file mode 100644
index 0000000..e42476a
--- /dev/null
+++ b/lib/common/tests/probes/pcmk_xe_is_probe_test.c
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2021-2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/unittest_internal.h>
+
+static void
+op_is_probe_test(void **state)
+{
+ xmlNode *node = NULL;
+
+ assert_false(pcmk_xe_is_probe(NULL));
+
+ node = pcmk__xml_parse("<" PCMK__XE_LRM_RSC_OP "/>");
+ assert_false(pcmk_xe_is_probe(node));
+ free_xml(node);
+
+ node = pcmk__xml_parse("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK__XA_OPERATION_KEY "=\"blah\" "
+ PCMK_META_INTERVAL "=\"30s\"/>");
+ assert_false(pcmk_xe_is_probe(node));
+ free_xml(node);
+
+ node = pcmk__xml_parse("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION
+ "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"30s\"/>");
+ assert_false(pcmk_xe_is_probe(node));
+ free_xml(node);
+
+ node = pcmk__xml_parse("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION
+ "=\"" PCMK_ACTION_START "\" "
+ PCMK_META_INTERVAL "=\"0\"/>");
+ assert_false(pcmk_xe_is_probe(node));
+ free_xml(node);
+
+ node = pcmk__xml_parse("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION
+ "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\"/>");
+ assert_true(pcmk_xe_is_probe(node));
+ free_xml(node);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(op_is_probe_test))
diff --git a/lib/common/tests/probes/pcmk_xe_mask_probe_failure_test.c b/lib/common/tests/probes/pcmk_xe_mask_probe_failure_test.c
new file mode 100644
index 0000000..ad378bf
--- /dev/null
+++ b/lib/common/tests/probes/pcmk_xe_mask_probe_failure_test.c
@@ -0,0 +1,333 @@
+/*
+ * Copyright 2021-2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/unittest_internal.h>
+
+static void
+op_is_not_probe_test(void **state) {
+ xmlNode *node = NULL;
+
+ /* Not worth testing this thoroughly since it's just a duplicate of whether
+ * pcmk_op_is_probe works or not.
+ */
+
+ node = pcmk__xml_parse("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION
+ "=\"" PCMK_ACTION_START "\" "
+ PCMK_META_INTERVAL "=\"0\"/>");
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+}
+
+static void
+op_does_not_have_right_values_test(void **state) {
+ xmlNode *node = NULL;
+ char *s = NULL;
+
+ node = pcmk__xml_parse("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION
+ "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\"/>");
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"\"/>",
+ PCMK_OCF_OK);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+}
+
+static void
+check_values_test(void **state) {
+ xmlNode *node = NULL;
+ char *s = NULL;
+
+ /* PCMK_EXEC_NOT_SUPPORTED */
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_OK, PCMK_EXEC_NOT_SUPPORTED);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_INSTALLED, PCMK_EXEC_NOT_SUPPORTED);
+ node = pcmk__xml_parse(s);
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ /* PCMK_EXEC_DONE */
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_OK, PCMK_EXEC_DONE);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_INVALID_PARAM, PCMK_EXEC_DONE);
+ node = pcmk__xml_parse(s);
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_INSTALLED, PCMK_EXEC_DONE);
+ node = pcmk__xml_parse(s);
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_CONFIGURED, PCMK_EXEC_DONE);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_RUNNING, PCMK_EXEC_DONE);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ /* PCMK_EXEC_NOT_INSTALLED */
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_OK, PCMK_EXEC_NOT_INSTALLED);
+ node = pcmk__xml_parse(s);
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_INSTALLED, PCMK_EXEC_NOT_INSTALLED);
+ node = pcmk__xml_parse(s);
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ /* PCMK_EXEC_ERROR */
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_OK, PCMK_EXEC_ERROR);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_INVALID_PARAM, PCMK_EXEC_ERROR);
+ node = pcmk__xml_parse(s);
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_INSTALLED, PCMK_EXEC_ERROR);
+ node = pcmk__xml_parse(s);
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_CONFIGURED, PCMK_EXEC_ERROR);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_RUNNING, PCMK_EXEC_ERROR);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ /* PCMK_EXEC_ERROR_HARD */
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_OK, PCMK_EXEC_ERROR_HARD);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_INVALID_PARAM, PCMK_EXEC_ERROR_HARD);
+ node = pcmk__xml_parse(s);
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_INSTALLED, PCMK_EXEC_ERROR_HARD);
+ node = pcmk__xml_parse(s);
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_CONFIGURED, PCMK_EXEC_ERROR_HARD);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_RUNNING, PCMK_EXEC_ERROR_HARD);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ /* PCMK_EXEC_ERROR_FATAL */
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_OK, PCMK_EXEC_ERROR_FATAL);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_INVALID_PARAM, PCMK_EXEC_ERROR_FATAL);
+ node = pcmk__xml_parse(s);
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_INSTALLED, PCMK_EXEC_ERROR_FATAL);
+ node = pcmk__xml_parse(s);
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_CONFIGURED, PCMK_EXEC_ERROR_FATAL);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_RUNNING, PCMK_EXEC_ERROR_FATAL);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(op_is_not_probe_test),
+ cmocka_unit_test(op_does_not_have_right_values_test),
+ cmocka_unit_test(check_values_test))
diff --git a/lib/common/tests/procfs/pcmk__procfs_pid2path_test.c b/lib/common/tests/procfs/pcmk__procfs_pid2path_test.c
index 2bae541..52fe006 100644
--- a/lib/common/tests/procfs/pcmk__procfs_pid2path_test.c
+++ b/lib/common/tests/procfs/pcmk__procfs_pid2path_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 the Pacemaker project contributors
+ * Copyright 2022-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -21,7 +21,7 @@ static void
no_exe_file(void **state)
{
size_t len = PATH_MAX;
- char *path = calloc(len, sizeof(char));
+ char *path = pcmk__assert_alloc(len, sizeof(char));
// Set readlink() errno and link contents
pcmk__mock_readlink = true;
@@ -43,7 +43,7 @@ static void
contents_too_long(void **state)
{
size_t len = 10;
- char *path = calloc(len, sizeof(char));
+ char *path = pcmk__assert_alloc(len, sizeof(char));
// Set readlink() errno and link contents
pcmk__mock_readlink = true;
@@ -66,7 +66,7 @@ static void
contents_ok(void **state)
{
size_t len = PATH_MAX;
- char *path = calloc(len, sizeof(char));
+ char *path = pcmk__assert_alloc(len, sizeof(char));
// Set readlink() errno and link contents
pcmk__mock_readlink = true;
diff --git a/lib/common/tests/resources/Makefile.am b/lib/common/tests/resources/Makefile.am
new file mode 100644
index 0000000..91e29a6
--- /dev/null
+++ b/lib/common/tests/resources/Makefile.am
@@ -0,0 +1,17 @@
+#
+# Copyright 2024 the Pacemaker project contributors
+#
+# The version control history for this file may have further details.
+#
+# This source code is licensed under the GNU General Public License version 2
+# or later (GPLv2+) WITHOUT ANY WARRANTY.
+#
+
+include $(top_srcdir)/mk/tap.mk
+include $(top_srcdir)/mk/unittest.mk
+
+# Add "_test" to the end of all test program names to simplify .gitignore.
+check_PROGRAMS = pcmk_resource_id_test \
+ pcmk_resource_is_managed_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/resources/pcmk_resource_id_test.c b/lib/common/tests/resources/pcmk_resource_id_test.c
new file mode 100644
index 0000000..5676d2a
--- /dev/null
+++ b/lib/common/tests/resources/pcmk_resource_id_test.c
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h> // NULL
+
+#include <crm/common/resources.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+null_resource(void **state)
+{
+ assert_null(pcmk_resource_id(NULL));
+}
+
+static void
+resource_with_id(void **state)
+{
+ char rsc1_id[] = "rsc1";
+ pcmk_resource_t rsc1 = {
+ .id = rsc1_id,
+ };
+
+ assert_string_equal(pcmk_resource_id(&rsc1), "rsc1");
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_resource),
+ cmocka_unit_test(resource_with_id))
diff --git a/lib/common/tests/resources/pcmk_resource_is_managed_test.c b/lib/common/tests/resources/pcmk_resource_is_managed_test.c
new file mode 100644
index 0000000..958bdc2
--- /dev/null
+++ b/lib/common/tests/resources/pcmk_resource_is_managed_test.c
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h> // NULL
+
+#include <crm/common/resources.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+null_resource(void **state)
+{
+ assert_false(pcmk_resource_is_managed(NULL));
+}
+
+static void
+resource_is_managed(void **state)
+{
+ pcmk_resource_t rsc1 = {
+ .flags = pcmk_rsc_managed,
+ };
+
+ assert_true(pcmk_resource_is_managed(&rsc1));
+}
+
+static void
+resource_is_not_managed(void **state)
+{
+ pcmk_resource_t rsc1 = {
+ .flags = 0,
+ };
+
+ assert_false(pcmk_resource_is_managed(&rsc1));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_resource),
+ cmocka_unit_test(resource_is_managed),
+ cmocka_unit_test(resource_is_not_managed))
diff --git a/lib/common/tests/rules/Makefile.am b/lib/common/tests/rules/Makefile.am
new file mode 100644
index 0000000..4163037
--- /dev/null
+++ b/lib/common/tests/rules/Makefile.am
@@ -0,0 +1,29 @@
+#
+# Copyright 2020-2024 the Pacemaker project contributors
+#
+# The version control history for this file may have further details.
+#
+# This source code is licensed under the GNU General Public License version 2
+# or later (GPLv2+) WITHOUT ANY WARRANTY.
+#
+
+include $(top_srcdir)/mk/tap.mk
+include $(top_srcdir)/mk/unittest.mk
+
+# Add "_test" to the end of all test program names to simplify .gitignore.
+check_PROGRAMS = pcmk__cmp_by_type_test \
+ pcmk__evaluate_attr_expression_test \
+ pcmk__evaluate_date_expression_test \
+ pcmk__evaluate_date_spec_test \
+ pcmk__evaluate_condition_test \
+ pcmk__evaluate_op_expression_test \
+ pcmk__evaluate_rsc_expression_test \
+ pcmk__parse_combine_test \
+ pcmk__parse_comparison_test \
+ pcmk__parse_source_test \
+ pcmk__parse_type_test \
+ pcmk__replace_submatches_test \
+ pcmk__unpack_duration_test \
+ pcmk_evaluate_rule_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/rules/pcmk__cmp_by_type_test.c b/lib/common/tests/rules/pcmk__cmp_by_type_test.c
new file mode 100644
index 0000000..cf468f1
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__cmp_by_type_test.c
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <limits.h> // INT_MIN, INT_MAX
+
+#include <crm/common/util.h> // crm_strdup_printf()
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+#include "crmcommon_private.h"
+
+static void
+null_compares_lesser(void **state)
+{
+ assert_int_equal(pcmk__cmp_by_type(NULL, NULL, pcmk__type_string), 0);
+ assert_true(pcmk__cmp_by_type("0", NULL, pcmk__type_integer) > 0);
+ assert_true(pcmk__cmp_by_type(NULL, "0", pcmk__type_number) < 0);
+}
+
+static void
+invalid_compares_equal(void **state)
+{
+ assert_int_equal(pcmk__cmp_by_type("0", "1", pcmk__type_unknown), 0);
+ assert_int_equal(pcmk__cmp_by_type("hi", "bye", pcmk__type_unknown), 0);
+ assert_int_equal(pcmk__cmp_by_type("-1.0", "2.0", pcmk__type_unknown), 0);
+}
+
+static void
+compare_string_type(void **state)
+{
+ assert_int_equal(pcmk__cmp_by_type("bye", "bye", pcmk__type_string), 0);
+ assert_int_equal(pcmk__cmp_by_type("bye", "BYE", pcmk__type_string), 0);
+ assert_true(pcmk__cmp_by_type("bye", "hello", pcmk__type_string) < 0);
+ assert_true(pcmk__cmp_by_type("bye", "HELLO", pcmk__type_string) < 0);
+ assert_true(pcmk__cmp_by_type("bye", "boo", pcmk__type_string) > 0);
+ assert_true(pcmk__cmp_by_type("bye", "Boo", pcmk__type_string) > 0);
+}
+
+static void
+compare_integer_type(void **state)
+{
+ char *int_min = crm_strdup_printf("%d", INT_MIN);
+ char *int_max = crm_strdup_printf("%d", INT_MAX);
+
+ assert_int_equal(pcmk__cmp_by_type("0", "0", pcmk__type_integer), 0);
+ assert_true(pcmk__cmp_by_type("0", "1", pcmk__type_integer) < 0);
+ assert_true(pcmk__cmp_by_type("1", "0", pcmk__type_integer) > 0);
+ assert_true(pcmk__cmp_by_type("3999", "399", pcmk__type_integer) > 0);
+ assert_true(pcmk__cmp_by_type(int_min, int_max, pcmk__type_integer) < 0);
+ assert_true(pcmk__cmp_by_type(int_max, int_min, pcmk__type_integer) > 0);
+ free(int_min);
+ free(int_max);
+
+ // Non-integers compare as strings
+ assert_int_equal(pcmk__cmp_by_type("0", "x", pcmk__type_integer),
+ pcmk__cmp_by_type("0", "x", pcmk__type_string));
+ assert_int_equal(pcmk__cmp_by_type("x", "0", pcmk__type_integer),
+ pcmk__cmp_by_type("x", "0", pcmk__type_string));
+ assert_int_equal(pcmk__cmp_by_type("x", "X", pcmk__type_integer),
+ pcmk__cmp_by_type("x", "X", pcmk__type_string));
+}
+
+static void
+compare_number_type(void **state)
+{
+ assert_int_equal(pcmk__cmp_by_type("0", "0.0", pcmk__type_number), 0);
+ assert_true(pcmk__cmp_by_type("0.345", "0.5", pcmk__type_number) < 0);
+ assert_true(pcmk__cmp_by_type("5", "3.1", pcmk__type_number) > 0);
+ assert_true(pcmk__cmp_by_type("3999", "399", pcmk__type_number) > 0);
+
+ // Non-numbers compare as strings
+ assert_int_equal(pcmk__cmp_by_type("0.0", "x", pcmk__type_number),
+ pcmk__cmp_by_type("0.0", "x", pcmk__type_string));
+ assert_int_equal(pcmk__cmp_by_type("x", "0.0", pcmk__type_number),
+ pcmk__cmp_by_type("x", "0.0", pcmk__type_string));
+ assert_int_equal(pcmk__cmp_by_type("x", "X", pcmk__type_number),
+ pcmk__cmp_by_type("x", "X", pcmk__type_string));
+}
+
+static void
+compare_version_type(void **state)
+{
+ assert_int_equal(pcmk__cmp_by_type("1.0", "1.0", pcmk__type_version), 0);
+ assert_true(pcmk__cmp_by_type("1.0.0", "1.0.1", pcmk__type_version) < 0);
+ assert_true(pcmk__cmp_by_type("5.0", "3.1.15", pcmk__type_version) > 0);
+ assert_true(pcmk__cmp_by_type("3999", "399", pcmk__type_version) > 0);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_compares_lesser),
+ cmocka_unit_test(invalid_compares_equal),
+ cmocka_unit_test(compare_string_type),
+ cmocka_unit_test(compare_integer_type),
+ cmocka_unit_test(compare_number_type),
+ cmocka_unit_test(compare_version_type))
diff --git a/lib/common/tests/rules/pcmk__evaluate_attr_expression_test.c b/lib/common/tests/rules/pcmk__evaluate_attr_expression_test.c
new file mode 100644
index 0000000..d28cb11
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__evaluate_attr_expression_test.c
@@ -0,0 +1,831 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h>
+#include <glib.h>
+
+#include <crm/common/xml.h>
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+#include "crmcommon_private.h"
+
+/*
+ * Shared data
+ */
+
+#define MATCHED_STRING "server-north"
+
+static const regmatch_t submatches[] = {
+ { .rm_so = 0, .rm_eo = 12 }, // %0 = Entire string
+ { .rm_so = 7, .rm_eo = 12 }, // %1 = "north"
+};
+
+static pcmk_rule_input_t rule_input = {
+ // These are the only members used to evaluate attribute expressions
+
+ // Used to replace submatches in attribute name
+ .rsc_id = MATCHED_STRING,
+ .rsc_id_submatches = submatches,
+ .rsc_id_nmatches = 2,
+
+ // Used when source is instance attributes
+ .rsc_params = NULL,
+
+ // Used when source is meta-attributes
+ .rsc_meta = NULL,
+
+ // Used to get actual value of node attribute
+ .node_attrs = NULL,
+};
+
+static int
+setup(void **state)
+{
+ rule_input.rsc_params = pcmk__strkey_table(free, free);
+ pcmk__insert_dup(rule_input.rsc_params, "foo-param", "bar");
+ pcmk__insert_dup(rule_input.rsc_params, "myparam", "different");
+
+ rule_input.rsc_meta = pcmk__strkey_table(free, free);
+ pcmk__insert_dup(rule_input.rsc_meta, "foo-meta", "bar");
+ pcmk__insert_dup(rule_input.rsc_params, "mymeta", "different");
+
+ rule_input.node_attrs = pcmk__strkey_table(free, free);
+ pcmk__insert_dup(rule_input.node_attrs, "foo", "bar");
+ pcmk__insert_dup(rule_input.node_attrs, "num", "10");
+ pcmk__insert_dup(rule_input.node_attrs, "ver", "3.5.0");
+ pcmk__insert_dup(rule_input.node_attrs, "prefer-north", "100");
+
+ return 0;
+}
+
+static int
+teardown(void **state)
+{
+ g_hash_table_destroy(rule_input.rsc_params);
+ g_hash_table_destroy(rule_input.rsc_meta);
+ g_hash_table_destroy(rule_input.node_attrs);
+ return 0;
+}
+
+/*!
+ * \internal
+ * \brief Run one test, comparing return value
+ *
+ * \param[in] xml_string Node attribute expression XML as string
+ * \param[in] reference_rc Assert that evaluation result equals this
+ */
+static void
+assert_attr_expression(const char *xml_string, int reference_rc)
+{
+ xmlNode *xml = pcmk__xml_parse(xml_string);
+
+ assert_int_equal(pcmk__evaluate_attr_expression(xml, &rule_input),
+ reference_rc);
+ free_xml(xml);
+}
+
+
+/*
+ * Invalid arguments
+ */
+
+#define EXPR_SOURCE_LITERAL_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='bar' " \
+ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_LITERAL "' />"
+
+static void
+null_invalid(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(EXPR_SOURCE_LITERAL_PASSES);
+
+ assert_int_equal(pcmk__evaluate_attr_expression(NULL, NULL), EINVAL);
+ assert_int_equal(pcmk__evaluate_attr_expression(xml, NULL), EINVAL);
+ assert_int_equal(pcmk__evaluate_attr_expression(NULL, &rule_input), EINVAL);
+
+ free_xml(xml);
+}
+
+
+/*
+ * Test PCMK_XA_ID
+ */
+
+#define EXPR_ID_MISSING \
+ "<" PCMK_XE_EXPRESSION " " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='bar' />"
+
+static void
+id_missing(void **state)
+{
+ // Currently acceptable
+ assert_attr_expression(EXPR_ID_MISSING, pcmk_rc_ok);
+}
+
+
+/*
+ * Test PCMK_XA_ATTRIBUTE
+ */
+
+#define EXPR_ATTR_MISSING \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='bar' />"
+
+static void
+attr_missing(void **state)
+{
+ assert_attr_expression(EXPR_ATTR_MISSING, pcmk_rc_unpack_error);
+}
+
+#define EXPR_ATTR_SUBMATCH_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='prefer-%1' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_DEFINED "' />"
+
+static void
+attr_with_submatch_passes(void **state)
+{
+ assert_attr_expression(EXPR_ATTR_SUBMATCH_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_ATTR_SUBMATCH_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='undefined-%1' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_DEFINED "' />"
+
+static void
+attr_with_submatch_fails(void **state)
+{
+ assert_attr_expression(EXPR_ATTR_SUBMATCH_FAILS, pcmk_rc_op_unsatisfied);
+}
+
+
+/*
+ * Test PCMK_XA_VALUE_SOURCE
+ */
+
+#define EXPR_SOURCE_MISSING \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_VALUE "='bar' />"
+
+static void
+source_missing(void **state)
+{
+ // Defaults to literal
+ assert_attr_expression(EXPR_SOURCE_MISSING, pcmk_rc_ok);
+}
+
+#define EXPR_SOURCE_INVALID \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='bar' " \
+ PCMK_XA_VALUE_SOURCE "='not-a-source' />"
+
+static void
+source_invalid(void **state)
+{
+ // Currently treated as literal
+ assert_attr_expression(EXPR_SOURCE_INVALID, pcmk_rc_ok);
+}
+
+static void
+source_literal_passes(void **state)
+{
+ assert_attr_expression(EXPR_SOURCE_LITERAL_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_SOURCE_LITERAL_VALUE_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='wrong-value' " \
+ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_LITERAL "' />"
+
+static void
+source_literal_value_fails(void **state)
+{
+ assert_attr_expression(EXPR_SOURCE_LITERAL_VALUE_FAILS,
+ pcmk_rc_op_unsatisfied);
+}
+
+#define EXPR_SOURCE_LITERAL_ATTR_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='not-an-attribute' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='bar' " \
+ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_LITERAL "' />"
+
+static void
+source_literal_attr_fails(void **state)
+{
+ assert_attr_expression(EXPR_SOURCE_LITERAL_ATTR_FAILS,
+ pcmk_rc_op_unsatisfied);
+}
+
+#define EXPR_SOURCE_PARAM_MISSING \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='not-a-param' " \
+ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_PARAM "' />"
+
+static void
+source_params_missing(void **state)
+{
+ assert_attr_expression(EXPR_SOURCE_PARAM_MISSING, pcmk_rc_op_unsatisfied);
+}
+
+#define EXPR_SOURCE_PARAM_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='foo-param' " \
+ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_PARAM "' />"
+
+static void
+source_params_passes(void **state)
+{
+ assert_attr_expression(EXPR_SOURCE_PARAM_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_SOURCE_PARAM_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='myparam' " \
+ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_PARAM "' />"
+
+static void
+source_params_fails(void **state)
+{
+ assert_attr_expression(EXPR_SOURCE_PARAM_FAILS, pcmk_rc_op_unsatisfied);
+}
+
+#define EXPR_SOURCE_META_MISSING \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='not-a-meta' " \
+ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_META "' />"
+
+static void
+source_meta_missing(void **state)
+{
+ assert_attr_expression(EXPR_SOURCE_META_MISSING, pcmk_rc_op_unsatisfied);
+}
+
+#define EXPR_SOURCE_META_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='foo-meta' " \
+ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_META "' />"
+
+static void
+source_meta_passes(void **state)
+{
+ assert_attr_expression(EXPR_SOURCE_META_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_SOURCE_META_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='mymeta' " \
+ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_META "' />"
+
+static void
+source_meta_fails(void **state)
+{
+ assert_attr_expression(EXPR_SOURCE_META_FAILS, pcmk_rc_op_unsatisfied);
+}
+
+
+/*
+ * Test PCMK_XA_TYPE
+ */
+
+#define EXPR_TYPE_DEFAULT_NUMBER \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='2.5' />"
+
+static void
+type_default_number(void **state)
+{
+ // Defaults to number for "gt" if either value contains a decimal point
+ assert_attr_expression(EXPR_TYPE_DEFAULT_NUMBER, pcmk_rc_ok);
+}
+
+#define EXPR_TYPE_DEFAULT_INT \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='2' />"
+
+static void
+type_default_int(void **state)
+{
+ // Defaults to integer for "gt" if neither value contains a decimal point
+ assert_attr_expression(EXPR_TYPE_DEFAULT_INT, pcmk_rc_ok);
+}
+
+#define EXPR_TYPE_STRING_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_STRING "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_VALUE "='bar' />"
+
+static void
+type_string_passes(void **state)
+{
+ assert_attr_expression(EXPR_TYPE_STRING_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_TYPE_STRING_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_STRING "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_VALUE "='bat' />"
+
+static void
+type_string_fails(void **state)
+{
+ assert_attr_expression(EXPR_TYPE_STRING_FAILS, pcmk_rc_op_unsatisfied);
+}
+
+#define EXPR_TYPE_INTEGER_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='10' />"
+
+static void
+type_integer_passes(void **state)
+{
+ assert_attr_expression(EXPR_TYPE_INTEGER_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_TYPE_INTEGER_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='11' />"
+
+static void
+type_integer_fails(void **state)
+{
+ assert_attr_expression(EXPR_TYPE_INTEGER_FAILS, pcmk_rc_op_unsatisfied);
+}
+
+#define EXPR_TYPE_INTEGER_TRUNCATION \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='10.5' />"
+
+static void
+type_integer_truncation(void **state)
+{
+ assert_attr_expression(EXPR_TYPE_INTEGER_TRUNCATION, pcmk_rc_ok);
+}
+
+#define EXPR_TYPE_NUMBER_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_NUMBER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='10.0' />"
+
+static void
+type_number_passes(void **state)
+{
+ assert_attr_expression(EXPR_TYPE_NUMBER_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_TYPE_NUMBER_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_NUMBER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='10.1' />"
+
+static void
+type_number_fails(void **state)
+{
+ assert_attr_expression(EXPR_TYPE_NUMBER_FAILS, pcmk_rc_op_unsatisfied);
+}
+
+#define EXPR_TYPE_VERSION_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_VERSION "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \
+ PCMK_XA_ATTRIBUTE "='ver' " \
+ PCMK_XA_VALUE "='3.4.9' />"
+
+static void
+type_version_passes(void **state)
+{
+ assert_attr_expression(EXPR_TYPE_VERSION_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_TYPE_VERSION_EQUALITY \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_VERSION "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_ATTRIBUTE "='ver' " \
+ PCMK_XA_VALUE "='3.5' />"
+
+static void
+type_version_equality(void **state)
+{
+ assert_attr_expression(EXPR_TYPE_VERSION_EQUALITY, pcmk_rc_ok);
+}
+
+#define EXPR_TYPE_VERSION_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_VERSION "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_GTE "' " \
+ PCMK_XA_ATTRIBUTE "='ver' " \
+ PCMK_XA_VALUE "='4.0' />"
+
+static void
+type_version_fails(void **state)
+{
+ assert_attr_expression(EXPR_TYPE_VERSION_FAILS, pcmk_rc_before_range);
+}
+
+/*
+ * Test PCMK_XA_OPERATION
+ */
+
+#define EXPR_OP_MISSING \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_VALUE "='bar' />"
+
+static void
+op_missing(void **state)
+{
+ assert_attr_expression(EXPR_OP_MISSING, pcmk_rc_unpack_error);
+}
+
+#define EXPR_OP_INVALID \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='not-an-operation' " \
+ PCMK_XA_VALUE "='bar' />"
+
+static void
+op_invalid(void **state)
+{
+ assert_attr_expression(EXPR_OP_INVALID, pcmk_rc_unpack_error);
+}
+
+#define EXPR_OP_LT_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_LT "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='20' />"
+
+static void
+op_lt_passes(void **state)
+{
+ assert_attr_expression(EXPR_OP_LT_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_OP_LT_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_LT "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='2' />"
+
+static void
+op_lt_fails(void **state)
+{
+ assert_attr_expression(EXPR_OP_LT_FAILS, pcmk_rc_after_range);
+}
+
+#define EXPR_OP_GT_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='2' />"
+
+static void
+op_gt_passes(void **state)
+{
+ assert_attr_expression(EXPR_OP_GT_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_OP_GT_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='20' />"
+
+static void
+op_gt_fails(void **state)
+{
+ assert_attr_expression(EXPR_OP_GT_FAILS, pcmk_rc_before_range);
+}
+
+#define EXPR_OP_LTE_LT_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_LTE "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='20' />"
+
+static void
+op_lte_lt_passes(void **state)
+{
+ assert_attr_expression(EXPR_OP_LTE_LT_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_OP_LTE_EQ_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_LTE "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='10' />"
+
+static void
+op_lte_eq_passes(void **state)
+{
+ assert_attr_expression(EXPR_OP_LTE_EQ_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_OP_LTE_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_LTE "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='9' />"
+
+static void
+op_lte_fails(void **state)
+{
+ assert_attr_expression(EXPR_OP_LTE_FAILS, pcmk_rc_after_range);
+}
+
+#define EXPR_OP_GTE_GT_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_GTE "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='1' />"
+
+static void
+op_gte_gt_passes(void **state)
+{
+ assert_attr_expression(EXPR_OP_GTE_GT_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_OP_GTE_EQ_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_GTE "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='10' />"
+
+static void
+op_gte_eq_passes(void **state)
+{
+ assert_attr_expression(EXPR_OP_GTE_EQ_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_OP_GTE_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_GTE "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='11' />"
+
+static void
+op_gte_fails(void **state)
+{
+ assert_attr_expression(EXPR_OP_GTE_FAILS, pcmk_rc_before_range);
+}
+
+// This also tests that string is used if values aren't parseable as numbers
+#define EXPR_OP_EQ_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_NUMBER "' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='bar' " \
+ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_LITERAL "' />"
+
+static void
+op_eq_passes(void **state)
+{
+ assert_attr_expression(EXPR_OP_EQ_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_OP_EQ_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='bar' />"
+
+static void
+op_eq_fails(void **state)
+{
+ assert_attr_expression(EXPR_OP_EQ_FAILS, pcmk_rc_op_unsatisfied);
+}
+
+#define EXPR_OP_NE_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_STRING "' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_NE "' " \
+ PCMK_XA_VALUE "='bat' " \
+ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_LITERAL "' />"
+
+static void
+op_ne_passes(void **state)
+{
+ assert_attr_expression(EXPR_OP_NE_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_OP_NE_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_NE "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='10' />"
+
+static void
+op_ne_fails(void **state)
+{
+ assert_attr_expression(EXPR_OP_NE_FAILS, pcmk_rc_op_unsatisfied);
+}
+
+#define EXPR_OP_DEFINED_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_DEFINED "' />"
+
+static void
+op_defined_passes(void **state)
+{
+ assert_attr_expression(EXPR_OP_DEFINED_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_OP_DEFINED_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='boo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_DEFINED "' />"
+
+static void
+op_defined_fails(void **state)
+{
+ assert_attr_expression(EXPR_OP_DEFINED_FAILS, pcmk_rc_op_unsatisfied);
+}
+
+#define EXPR_OP_DEFINED_WITH_VALUE \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_VALUE "='bar' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_DEFINED "' />"
+
+static void
+op_defined_with_value(void **state)
+{
+ // Ill-formed but currently accepted
+ assert_attr_expression(EXPR_OP_DEFINED_WITH_VALUE, pcmk_rc_ok);
+}
+
+#define EXPR_OP_UNDEFINED_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='boo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_NOT_DEFINED "' />"
+
+static void
+op_undefined_passes(void **state)
+{
+ assert_attr_expression(EXPR_OP_UNDEFINED_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_OP_UNDEFINED_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_NOT_DEFINED "' />"
+
+static void
+op_undefined_fails(void **state)
+{
+ assert_attr_expression(EXPR_OP_DEFINED_FAILS, pcmk_rc_op_unsatisfied);
+}
+
+
+/*
+ * Test PCMK_XA_VALUE
+ */
+
+#define EXPR_VALUE_MISSING_DEFINED_OK \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_DEFINED "' />"
+
+static void
+value_missing_defined_ok(void **state)
+{
+ assert_attr_expression(EXPR_VALUE_MISSING_DEFINED_OK, pcmk_rc_ok);
+}
+
+#define EXPR_VALUE_MISSING_EQ_OK \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='not-an-attr' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' />"
+
+static void
+value_missing_eq_ok(void **state)
+{
+ // Currently treated as NULL reference value
+ assert_attr_expression(EXPR_VALUE_MISSING_EQ_OK, pcmk_rc_ok);
+}
+
+
+#define expr_test(f) cmocka_unit_test_setup_teardown(f, setup, teardown)
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_invalid),
+ expr_test(id_missing),
+ expr_test(attr_missing),
+ expr_test(attr_with_submatch_passes),
+ expr_test(attr_with_submatch_fails),
+ expr_test(source_missing),
+ expr_test(source_invalid),
+ expr_test(source_literal_passes),
+ expr_test(source_literal_value_fails),
+ expr_test(source_literal_attr_fails),
+ expr_test(source_params_missing),
+ expr_test(source_params_passes),
+ expr_test(source_params_fails),
+ expr_test(source_meta_missing),
+ expr_test(source_meta_passes),
+ expr_test(source_meta_fails),
+ expr_test(type_default_number),
+ expr_test(type_default_int),
+ expr_test(type_string_passes),
+ expr_test(type_string_fails),
+ expr_test(type_integer_passes),
+ expr_test(type_integer_fails),
+ expr_test(type_integer_truncation),
+ expr_test(type_number_passes),
+ expr_test(type_number_fails),
+ expr_test(type_version_passes),
+ expr_test(type_version_equality),
+ expr_test(type_version_fails),
+ expr_test(op_missing),
+ expr_test(op_invalid),
+ expr_test(op_lt_passes),
+ expr_test(op_lt_fails),
+ expr_test(op_gt_passes),
+ expr_test(op_gt_fails),
+ expr_test(op_lte_lt_passes),
+ expr_test(op_lte_eq_passes),
+ expr_test(op_lte_fails),
+ expr_test(op_gte_gt_passes),
+ expr_test(op_gte_eq_passes),
+ expr_test(op_gte_fails),
+ expr_test(op_eq_passes),
+ expr_test(op_eq_fails),
+ expr_test(op_ne_passes),
+ expr_test(op_ne_fails),
+ expr_test(op_defined_passes),
+ expr_test(op_defined_fails),
+ expr_test(op_defined_with_value),
+ expr_test(op_undefined_passes),
+ expr_test(op_undefined_fails),
+ expr_test(value_missing_defined_ok),
+ expr_test(value_missing_eq_ok))
diff --git a/lib/common/tests/rules/pcmk__evaluate_condition_test.c b/lib/common/tests/rules/pcmk__evaluate_condition_test.c
new file mode 100644
index 0000000..bcb13a0
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__evaluate_condition_test.c
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h>
+#include <glib.h>
+
+#include <crm/common/xml.h>
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+
+/*
+ * Shared data
+ */
+
+static pcmk_rule_input_t rule_input = {
+ .rsc_standard = PCMK_RESOURCE_CLASS_OCF,
+ .rsc_provider = "heartbeat",
+ .rsc_agent = "IPaddr2",
+ .op_name = PCMK_ACTION_MONITOR,
+ .op_interval_ms = 10000,
+};
+
+
+/*
+ * Test invalid arguments
+ */
+
+#define EXPR_ATTRIBUTE \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='bar' />"
+
+static void
+null_invalid(void **state)
+{
+ xmlNode *xml = NULL;
+ crm_time_t *next_change = crm_time_new_undefined();
+
+ assert_int_equal(pcmk__evaluate_condition(NULL, NULL, next_change), EINVAL);
+
+ xml = pcmk__xml_parse(EXPR_ATTRIBUTE);
+ assert_int_equal(pcmk__evaluate_condition(xml, NULL, next_change), EINVAL);
+ free_xml(xml);
+
+ assert_int_equal(pcmk__evaluate_condition(NULL, &rule_input, next_change),
+ EINVAL);
+
+ crm_time_free(next_change);
+}
+
+
+#define EXPR_INVALID "<not_an_expression " PCMK_XA_ID "='e' />"
+
+static void
+invalid_expression(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(EXPR_INVALID);
+ crm_time_t *next_change = crm_time_new_undefined();
+
+ assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, next_change),
+ pcmk_rc_unpack_error);
+
+ crm_time_free(next_change);
+ free_xml(xml);
+}
+
+
+/* Each expression type function already has unit tests, so we just need to test
+ * that they are called correctly (essentially, one of each one's own tests).
+ */
+
+static void
+attribute_expression(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(EXPR_ATTRIBUTE);
+
+ rule_input.node_attrs = pcmk__strkey_table(free, free);
+ pcmk__insert_dup(rule_input.node_attrs, "foo", "bar");
+
+ assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, NULL),
+ pcmk_rc_ok);
+
+ g_hash_table_destroy(rule_input.node_attrs);
+ rule_input.node_attrs = NULL;
+ free_xml(xml);
+}
+
+#define EXPR_LOCATION \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='" CRM_ATTR_UNAME "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='node1' />"
+
+static void
+location_expression(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(EXPR_LOCATION);
+
+ rule_input.node_attrs = pcmk__strkey_table(free, free);
+ pcmk__insert_dup(rule_input.node_attrs, CRM_ATTR_UNAME, "node1");
+
+ assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, NULL),
+ pcmk_rc_ok);
+
+ g_hash_table_destroy(rule_input.node_attrs);
+ rule_input.node_attrs = NULL;
+ free_xml(xml);
+}
+
+#define EXPR_DATE \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_START "='2024-02-01 12:00:00' " \
+ PCMK_XA_END "='2024-02-01 15:00:00' />"
+
+static void
+date_expression(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(EXPR_DATE);
+ crm_time_t *now = crm_time_new("2024-02-01 11:59:59");
+ crm_time_t *next_change = crm_time_new("2024-02-01 14:00:00");
+ crm_time_t *reference = crm_time_new("2024-02-01 12:00:00");
+
+ rule_input.now = now;
+ assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, next_change),
+ pcmk_rc_before_range);
+ assert_int_equal(crm_time_compare(next_change, reference), 0);
+ rule_input.now = NULL;
+
+ crm_time_free(reference);
+ crm_time_free(next_change);
+ crm_time_free(now);
+}
+
+#define EXPR_RESOURCE \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
+ PCMK_XA_TYPE "='IPaddr2' />"
+
+static void
+resource_expression(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(EXPR_RESOURCE);
+
+ assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, NULL),
+ pcmk_rc_ok);
+ free_xml(xml);
+}
+
+#define EXPR_OP \
+ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />"
+
+static void
+op_expression(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(EXPR_OP);
+
+ assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, NULL),
+ pcmk_rc_ok);
+ free_xml(xml);
+}
+
+#define EXPR_SUBRULE \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' /> />"
+
+static void
+subrule(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(EXPR_SUBRULE);
+ assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, NULL),
+ pcmk_rc_ok);
+ free_xml(xml);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_invalid),
+ cmocka_unit_test(invalid_expression),
+ cmocka_unit_test(attribute_expression),
+ cmocka_unit_test(location_expression),
+ cmocka_unit_test(date_expression),
+ cmocka_unit_test(resource_expression),
+ cmocka_unit_test(op_expression),
+ cmocka_unit_test(subrule))
diff --git a/lib/common/tests/rules/pcmk__evaluate_date_expression_test.c b/lib/common/tests/rules/pcmk__evaluate_date_expression_test.c
new file mode 100644
index 0000000..df8dcbf
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__evaluate_date_expression_test.c
@@ -0,0 +1,684 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h>
+#include <glib.h>
+
+#include <crm/common/xml.h>
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+#include "crmcommon_private.h"
+
+/*!
+ * \internal
+ * \brief Run one test, comparing return value and output argument
+ *
+ * \param[in] xml Date expression XML
+ * \param[in] now_s Time to evaluate expression with (as string)
+ * \param[in] next_change_s If this and \p reference_s are not NULL, initialize
+ * next change time with this time (as string),
+ * and assert that its value after evaluation is the
+ * reference
+ * \param[in] reference_s If not NULL, time (as string) that next change
+ * should be after expression evaluation
+ * \param[in] reference_rc Assert that evaluation result equals this
+ */
+static void
+assert_date_expression(const xmlNode *xml, const char *now_s,
+ const char *next_change_s, const char *reference_s,
+ int reference_rc)
+{
+ crm_time_t *now = NULL;
+ crm_time_t *next_change = NULL;
+ bool check_next_change = (next_change_s != NULL) && (reference_s != NULL);
+
+ if (check_next_change) {
+ next_change = crm_time_new(next_change_s);
+ }
+
+ now = crm_time_new(now_s);
+ assert_int_equal(pcmk__evaluate_date_expression(xml, now, next_change),
+ reference_rc);
+ crm_time_free(now);
+
+ if (check_next_change) {
+ crm_time_t *reference = crm_time_new(reference_s);
+
+ assert_int_equal(crm_time_compare(next_change, reference), 0);
+ crm_time_free(reference);
+ crm_time_free(next_change);
+ }
+}
+
+#define EXPR_LT_VALID \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_LT "' " \
+ PCMK_XA_END "='2024-02-01 15:00:00' />"
+
+static void
+null_invalid(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_LT_VALID);
+ crm_time_t *t = crm_time_new("2024-02-01");
+
+ assert_int_equal(pcmk__evaluate_date_expression(NULL, NULL, NULL), EINVAL);
+ assert_int_equal(pcmk__evaluate_date_expression(xml, NULL, NULL), EINVAL);
+ assert_int_equal(pcmk__evaluate_date_expression(NULL, t, NULL), EINVAL);
+
+ crm_time_free(t);
+ free_xml(xml);
+}
+
+static void
+null_next_change_ok(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_LT_VALID);
+
+ assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_within_range);
+ free_xml(xml);
+}
+
+#define EXPR_ID_MISSING \
+ "<" PCMK_XE_DATE_EXPRESSION " " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_LT "' " \
+ PCMK_XA_END "='2024-02-01 15:00:00' />"
+
+static void
+id_missing(void **state)
+{
+ // Currently acceptable
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_ID_MISSING);
+
+ assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_within_range);
+ free_xml(xml);
+}
+
+#define EXPR_OP_INVALID \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='not-a-choice' />"
+
+static void
+op_invalid(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_OP_INVALID);
+
+ assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined);
+ free_xml(xml);
+}
+
+#define EXPR_LT_MISSING_END \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_LT "' />"
+
+static void
+lt_missing_end(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_LT_MISSING_END);
+
+ assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined);
+ free_xml(xml);
+}
+
+#define EXPR_LT_INVALID_END \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_LT "' " \
+ PCMK_XA_END "='not-a-datetime' />"
+
+static void
+lt_invalid_end(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_LT_INVALID_END);
+
+ assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined);
+ free_xml(xml);
+}
+
+static void
+lt_valid(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_LT_VALID);
+
+ // Now and next change are both before end
+ assert_date_expression(xml, "2023-01-01 05:00:00", "2024-02-01 10:00:00",
+ "2024-02-01 10:00:00", pcmk_rc_within_range);
+
+ // Now is before end, next change is after end
+ assert_date_expression(xml, "2024-02-01 14:59:59", "2024-02-01 18:00:00",
+ "2024-02-01 15:00:00", pcmk_rc_within_range);
+
+ // Now is equal to end, next change is after end
+ assert_date_expression(xml, "2024-02-01 15:00:00", "2024-02-01 20:00:00",
+ "2024-02-01 20:00:00", pcmk_rc_after_range);
+
+ // Now and next change are both after end
+ assert_date_expression(xml, "2024-03-01 12:00:00", "2024-02-01 20:00:00",
+ "2024-02-01 20:00:00", pcmk_rc_after_range);
+
+ free_xml(xml);
+}
+
+#define EXPR_GT_MISSING_START \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' />"
+
+static void
+gt_missing_start(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_GT_MISSING_START);
+
+ assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined);
+ free_xml(xml);
+}
+
+#define EXPR_GT_INVALID_START \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \
+ PCMK_XA_START "='not-a-datetime' />"
+
+static void
+gt_invalid_start(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_GT_INVALID_START);
+
+ assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined);
+ free_xml(xml);
+}
+
+#define EXPR_GT_VALID \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \
+ PCMK_XA_START "='2024-02-01 12:00:00' />"
+
+static void
+gt_valid(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_GT_VALID);
+
+ // Now and next change are both before start
+ assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00",
+ "2024-01-01 11:00:00", pcmk_rc_before_range);
+
+ // Now is before start, next change is after start
+ assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 18:00:00",
+ "2024-02-01 12:00:01", pcmk_rc_before_range);
+
+ // Now is equal to start, next change is after start
+ assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 18:00:00",
+ "2024-02-01 12:00:01", pcmk_rc_before_range);
+
+ // Now is one second after start, next change is after start
+ assert_date_expression(xml, "2024-02-01 12:00:01", "2024-02-01 18:00:00",
+ "2024-02-01 18:00:00", pcmk_rc_within_range);
+
+ // t is after start, next change is after start
+ assert_date_expression(xml, "2024-03-01 05:03:11", "2024-04-04 04:04:04",
+ "2024-04-04 04:04:04", pcmk_rc_within_range);
+
+ free_xml(xml);
+}
+
+#define EXPR_RANGE_MISSING \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' />"
+
+static void
+range_missing(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_MISSING);
+ crm_time_t *t = crm_time_new("2024-01-01");
+
+ assert_int_equal(pcmk__evaluate_date_expression(xml, t, NULL),
+ pcmk_rc_undetermined);
+
+ crm_time_free(t);
+ free_xml(xml);
+}
+
+#define EXPR_RANGE_INVALID_START_INVALID_END \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_START "='not-a-date' " \
+ PCMK_XA_END "='not-a-date' />"
+
+static void
+range_invalid_start_invalid_end(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_INVALID_START_INVALID_END);
+
+ assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined);
+ free_xml(xml);
+}
+
+#define EXPR_RANGE_INVALID_START_ONLY \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_START "='not-a-date' />"
+
+static void
+range_invalid_start_only(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_INVALID_START_ONLY);
+
+ assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined);
+ free_xml(xml);
+}
+
+#define EXPR_RANGE_VALID_START_ONLY \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_START "='2024-02-01 12:00:00' />"
+
+static void
+range_valid_start_only(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_ONLY);
+
+ // Now and next change are before start
+ assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00",
+ "2024-01-01 11:00:00", pcmk_rc_before_range);
+
+ // Now is before start, next change is after start
+ assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 18:00:00",
+ "2024-02-01 12:00:00", pcmk_rc_before_range);
+
+ // Now is equal to start, next change is after start
+ assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 18:00:00",
+ "2024-02-01 18:00:00", pcmk_rc_within_range);
+
+ // Now and next change are after start
+ assert_date_expression(xml, "2024-03-01 05:03:11", "2024-04-04 04:04:04",
+ "2024-04-04 04:04:04", pcmk_rc_within_range);
+
+ free_xml(xml);
+}
+
+#define EXPR_RANGE_INVALID_END_ONLY \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_END "='not-a-date' />"
+
+static void
+range_invalid_end_only(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_INVALID_END_ONLY);
+
+ assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined);
+ free_xml(xml);
+}
+
+#define EXPR_RANGE_VALID_END_ONLY \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_END "='2024-02-01 15:00:00' />"
+
+static void
+range_valid_end_only(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_END_ONLY);
+
+ // Now and next change are before end
+ assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00",
+ "2024-01-01 11:00:00", pcmk_rc_within_range);
+
+ // Now is before end, next change is after end
+ assert_date_expression(xml, "2024-02-01 14:59:59", "2024-02-01 18:00:00",
+ "2024-02-01 15:00:01", pcmk_rc_within_range);
+
+ // Now is equal to end, next change is after end
+ assert_date_expression(xml, "2024-02-01 15:00:00", "2024-02-01 18:00:00",
+ "2024-02-01 15:00:01", pcmk_rc_within_range);
+
+ // Now and next change are after end
+ assert_date_expression(xml, "2024-02-01 15:00:01", "2024-04-04 04:04:04",
+ "2024-04-04 04:04:04", pcmk_rc_after_range);
+
+ free_xml(xml);
+}
+
+#define EXPR_RANGE_VALID_START_INVALID_END \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_START "='2024-02-01 12:00:00' " \
+ PCMK_XA_END "='not-a-date' />"
+
+static void
+range_valid_start_invalid_end(void **state)
+{
+ // Currently treated same as start without end
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_INVALID_END);
+
+ // Now and next change are before start
+ assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00",
+ "2024-01-01 11:00:00", pcmk_rc_before_range);
+
+ // Now is before start, next change is after start
+ assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 18:00:00",
+ "2024-02-01 12:00:00", pcmk_rc_before_range);
+
+ // Now is equal to start, next change is after start
+ assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 18:00:00",
+ "2024-02-01 18:00:00", pcmk_rc_within_range);
+
+ // Now and next change are after start
+ assert_date_expression(xml, "2024-03-01 05:03:11", "2024-04-04 04:04:04",
+ "2024-04-04 04:04:04", pcmk_rc_within_range);
+
+ free_xml(xml);
+}
+
+#define EXPR_RANGE_INVALID_START_VALID_END \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_START "='not-a-date' " \
+ PCMK_XA_END "='2024-02-01 15:00:00' />"
+
+static void
+range_invalid_start_valid_end(void **state)
+{
+ // Currently treated same as end without start
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_INVALID_START_VALID_END);
+
+ // Now and next change are before end
+ assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00",
+ "2024-01-01 11:00:00", pcmk_rc_within_range);
+
+ // Now is before end, next change is after end
+ assert_date_expression(xml, "2024-02-01 14:59:59", "2024-02-01 18:00:00",
+ "2024-02-01 15:00:01", pcmk_rc_within_range);
+
+ // Now is equal to end, next change is after end
+ assert_date_expression(xml, "2024-02-01 15:00:00", "2024-02-01 18:00:00",
+ "2024-02-01 15:00:01", pcmk_rc_within_range);
+
+ // Now and next change are after end
+ assert_date_expression(xml, "2024-02-01 15:00:01", "2024-04-04 04:04:04",
+ "2024-04-04 04:04:04", pcmk_rc_after_range);
+
+ free_xml(xml);
+}
+
+#define EXPR_RANGE_VALID_START_VALID_END \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_START "='2024-02-01 12:00:00' " \
+ PCMK_XA_END "='2024-02-01 15:00:00' />"
+
+static void
+range_valid_start_valid_end(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_VALID_END);
+
+ // Now and next change are before start
+ assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00",
+ "2024-01-01 11:00:00", pcmk_rc_before_range);
+
+ // Now is before start, next change is between start and end
+ assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 14:00:00",
+ "2024-02-01 12:00:00", pcmk_rc_before_range);
+
+ // Now is equal to start, next change is between start and end
+ assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 14:30:00",
+ "2024-02-01 14:30:00", pcmk_rc_within_range);
+
+ // Now is between start and end, next change is after end
+ assert_date_expression(xml, "2024-02-01 14:03:11", "2024-04-04 04:04:04",
+ "2024-02-01 15:00:01", pcmk_rc_within_range);
+
+ // Now is equal to end, next change is after end
+ assert_date_expression(xml, "2024-02-01 15:00:00", "2028-04-04 04:04:04",
+ "2024-02-01 15:00:01", pcmk_rc_within_range);
+
+ // Now and next change are after end
+ assert_date_expression(xml, "2024-02-01 15:00:01", "2028-04-04 04:04:04",
+ "2028-04-04 04:04:04", pcmk_rc_after_range);
+
+ free_xml(xml);
+}
+
+#define EXPR_RANGE_VALID_START_INVALID_DURATION \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_START "='2024-02-01 12:00:00'>" \
+ "<" PCMK_XE_DURATION " " PCMK_XA_ID "='d' " \
+ PCMK_XA_HOURS "='not-a-number' />" \
+ "</" PCMK_XE_DATE_EXPRESSION ">"
+
+static void
+range_valid_start_invalid_duration(void **state)
+{
+ // Currently treated same as end equals start
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_INVALID_DURATION);
+
+ // Now and next change are before start
+ assert_date_expression(xml, "2024-02-01 04:30:05", "2024-01-01 11:00:00",
+ "2024-01-01 11:00:00", pcmk_rc_before_range);
+
+ // Now is before start, next change is after start
+ assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 18:00:00",
+ "2024-02-01 12:00:00", pcmk_rc_before_range);
+
+ // Now is equal to start, next change is after start
+ assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 14:30:00",
+ "2024-02-01 12:00:01", pcmk_rc_within_range);
+
+ // Now and next change are after start
+ assert_date_expression(xml, "2024-02-01 12:00:01", "2024-02-01 14:30:00",
+ "2024-02-01 14:30:00", pcmk_rc_after_range);
+
+ free_xml(xml);
+}
+
+#define EXPR_RANGE_VALID_START_VALID_DURATION \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_START "='2024-02-01 12:00:00'>" \
+ "<" PCMK_XE_DURATION " " PCMK_XA_ID "='d' " \
+ PCMK_XA_HOURS "='3' />" \
+ "</" PCMK_XE_DATE_EXPRESSION ">"
+
+static void
+range_valid_start_valid_duration(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_VALID_DURATION);
+
+ // Now and next change are before start
+ assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00",
+ "2024-01-01 11:00:00", pcmk_rc_before_range);
+
+ // Now is before start, next change is between start and end
+ assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 14:00:00",
+ "2024-02-01 12:00:00", pcmk_rc_before_range);
+
+ // Now is equal to start, next change is between start and end
+ assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 14:30:00",
+ "2024-02-01 14:30:00", pcmk_rc_within_range);
+
+ // Now is between start and end, next change is after end
+ assert_date_expression(xml, "2024-02-01 14:03:11", "2024-04-04 04:04:04",
+ "2024-02-01 15:00:01", pcmk_rc_within_range);
+
+ // Now is equal to end, next change is after end
+ assert_date_expression(xml, "2024-02-01 15:00:00", "2028-04-04 04:04:04",
+ "2024-02-01 15:00:01", pcmk_rc_within_range);
+
+ // Now and next change are after end
+ assert_date_expression(xml, "2024-02-01 15:00:01", "2028-04-04 04:04:04",
+ "2028-04-04 04:04:04", pcmk_rc_after_range);
+
+ free_xml(xml);
+}
+
+#define EXPR_RANGE_VALID_START_DURATION_MISSING_ID \
+ "<" PCMK_XE_DATE_EXPRESSION " " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_START "='2024-02-01 12:00:00'>" \
+ "<" PCMK_XE_DURATION " " PCMK_XA_ID "='d' " \
+ PCMK_XA_HOURS "='3' />" \
+ "</" PCMK_XE_DATE_EXPRESSION ">"
+
+static void
+range_valid_start_duration_missing_id(void **state)
+{
+ // Currently acceptable
+ xmlNodePtr xml = NULL;
+
+ xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_DURATION_MISSING_ID);
+
+ // Now and next change are before start
+ assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00",
+ "2024-01-01 11:00:00", pcmk_rc_before_range);
+
+ // Now is before start, next change is between start and end
+ assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 14:00:00",
+ "2024-02-01 12:00:00", pcmk_rc_before_range);
+
+ // Now is equal to start, next change is between start and end
+ assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 14:30:00",
+ "2024-02-01 14:30:00", pcmk_rc_within_range);
+
+ // Now is between start and end, next change is after end
+ assert_date_expression(xml, "2024-02-01 14:03:11", "2024-04-04 04:04:04",
+ "2024-02-01 15:00:01", pcmk_rc_within_range);
+
+ // Now is equal to end, next change is after end
+ assert_date_expression(xml, "2024-02-01 15:00:00", "2028-04-04 04:04:04",
+ "2024-02-01 15:00:01", pcmk_rc_within_range);
+
+ // Now and next change are after end
+ assert_date_expression(xml, "2024-02-01 15:00:01", "2028-04-04 04:04:04",
+ "2028-04-04 04:04:04", pcmk_rc_after_range);
+
+ free_xml(xml);
+}
+
+#define EXPR_SPEC_MISSING \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_DATE_SPEC "' />"
+
+static void
+spec_missing(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_SPEC_MISSING);
+
+ assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined);
+ free_xml(xml);
+}
+
+#define EXPR_SPEC_INVALID \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_DATE_SPEC "'>" \
+ "<" PCMK_XE_DATE_SPEC " " PCMK_XA_ID "='s' " \
+ PCMK_XA_MONTHS "='not-a-number'/>" \
+ "</" PCMK_XE_DATE_EXPRESSION ">"
+
+static void
+spec_invalid(void **state)
+{
+ // Currently treated as date_spec with no ranges (which passes)
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_SPEC_INVALID);
+
+ assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_ok);
+ free_xml(xml);
+}
+
+#define EXPR_SPEC_VALID \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_DATE_SPEC "'>" \
+ "<" PCMK_XE_DATE_SPEC " " PCMK_XA_ID "='s' " \
+ PCMK_XA_MONTHS "='2'/>" \
+ "</" PCMK_XE_DATE_EXPRESSION ">"
+
+static void
+spec_valid(void **state)
+{
+ // date_spec does not currently support next_change
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_SPEC_VALID);
+
+ // Now is just before spec start
+ assert_date_expression(xml, "2024-01-01 23:59:59", NULL, NULL,
+ pcmk_rc_before_range);
+
+ // Now matches spec start
+ assert_date_expression(xml, "2024-02-01 00:00:00", NULL, NULL, pcmk_rc_ok);
+
+ // Now is within spec range
+ assert_date_expression(xml, "2024-02-22 22:22:22", NULL, NULL, pcmk_rc_ok);
+
+ // Now matches spec end
+ assert_date_expression(xml, "2024-02-29 23:59:59", NULL, NULL, pcmk_rc_ok);
+
+ // Now is just past spec end
+ assert_date_expression(xml, "2024-03-01 00:00:00", NULL, NULL,
+ pcmk_rc_after_range);
+
+ free_xml(xml);
+}
+
+#define EXPR_SPEC_MISSING_ID \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_DATE_SPEC "'>" \
+ "<" PCMK_XE_DATE_SPEC " " \
+ PCMK_XA_MONTHS "='2'/>" \
+ "</" PCMK_XE_DATE_EXPRESSION ">"
+
+static void
+spec_missing_id(void **state)
+{
+ // Currently acceptable; date_spec does not currently support next_change
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_SPEC_MISSING_ID);
+
+ // Now is just before spec start
+ assert_date_expression(xml, "2024-01-01 23:59:59", NULL, NULL,
+ pcmk_rc_before_range);
+
+ // Now matches spec start
+ assert_date_expression(xml, "2024-02-01 00:00:00", NULL, NULL, pcmk_rc_ok);
+
+ // Now is within spec range
+ assert_date_expression(xml, "2024-02-22 22:22:22", NULL, NULL, pcmk_rc_ok);
+
+ // Now matches spec end
+ assert_date_expression(xml, "2024-02-29 23:59:59", NULL, NULL, pcmk_rc_ok);
+
+ // Now is just past spec end
+ assert_date_expression(xml, "2024-03-01 00:00:00", NULL, NULL,
+ pcmk_rc_after_range);
+
+ free_xml(xml);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_invalid),
+ cmocka_unit_test(null_next_change_ok),
+ cmocka_unit_test(id_missing),
+ cmocka_unit_test(op_invalid),
+ cmocka_unit_test(lt_missing_end),
+ cmocka_unit_test(lt_invalid_end),
+ cmocka_unit_test(lt_valid),
+ cmocka_unit_test(gt_missing_start),
+ cmocka_unit_test(gt_invalid_start),
+ cmocka_unit_test(gt_valid),
+ cmocka_unit_test(range_missing),
+ cmocka_unit_test(range_invalid_start_invalid_end),
+ cmocka_unit_test(range_invalid_start_only),
+ cmocka_unit_test(range_valid_start_only),
+ cmocka_unit_test(range_invalid_end_only),
+ cmocka_unit_test(range_valid_end_only),
+ cmocka_unit_test(range_valid_start_invalid_end),
+ cmocka_unit_test(range_invalid_start_valid_end),
+ cmocka_unit_test(range_valid_start_valid_end),
+ cmocka_unit_test(range_valid_start_invalid_duration),
+ cmocka_unit_test(range_valid_start_valid_duration),
+ cmocka_unit_test(range_valid_start_duration_missing_id),
+ cmocka_unit_test(spec_missing),
+ cmocka_unit_test(spec_invalid),
+ cmocka_unit_test(spec_valid),
+ cmocka_unit_test(spec_missing_id))
diff --git a/lib/common/tests/rules/pcmk__evaluate_date_spec_test.c b/lib/common/tests/rules/pcmk__evaluate_date_spec_test.c
new file mode 100644
index 0000000..6048adf
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__evaluate_date_spec_test.c
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2020-2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <errno.h>
+#include <glib.h>
+
+#include <crm/common/xml.h>
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+#include "crmcommon_private.h"
+
+static void
+run_one_test(const char *t, const char *x, int expected)
+{
+ crm_time_t *tm = crm_time_new(t);
+ xmlNodePtr xml = pcmk__xml_parse(x);
+
+ assert_int_equal(pcmk__evaluate_date_spec(xml, tm), expected);
+
+ crm_time_free(tm);
+ free_xml(xml);
+}
+
+static void
+null_invalid(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse("<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARS "='2019'/>");
+ crm_time_t *tm = crm_time_new(NULL);
+
+ assert_int_equal(pcmk__evaluate_date_spec(NULL, NULL), EINVAL);
+ assert_int_equal(pcmk__evaluate_date_spec(xml, NULL), EINVAL);
+ assert_int_equal(pcmk__evaluate_date_spec(NULL, tm), EINVAL);
+
+ crm_time_free(tm);
+ free_xml(xml);
+}
+
+static void
+spec_id_missing(void **state)
+{
+ // Currently acceptable
+ run_one_test("2020-01-01", "<date_spec years='2020'/>", pcmk_rc_ok);
+}
+
+static void
+invalid_range(void **state)
+{
+ // Currently acceptable
+ run_one_test("2020-01-01", "<date_spec years='not-a-year' months='1'/>",
+ pcmk_rc_ok);
+}
+
+static void
+time_satisfies_year_spec(void **state)
+{
+ run_one_test("2020-01-01",
+ "<date_spec " PCMK_XA_ID "='spec' years='2020'/>",
+ pcmk_rc_ok);
+}
+
+static void
+time_after_year_spec(void **state)
+{
+ run_one_test("2020-01-01",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARS "='2019'/>",
+ pcmk_rc_after_range);
+}
+
+static void
+time_satisfies_year_range(void **state)
+{
+ run_one_test("2020-01-01",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARS "='2010-2030'/>",
+ pcmk_rc_ok);
+}
+
+static void
+time_before_year_range(void **state)
+{
+ run_one_test("2000-01-01",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARS "='2010-2030'/>",
+ pcmk_rc_before_range);
+}
+
+static void
+time_after_year_range(void **state)
+{
+ run_one_test("2020-01-01",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARS "='2010-2015'/>",
+ pcmk_rc_after_range);
+}
+
+static void
+range_without_start_year_passes(void **state)
+{
+ run_one_test("2010-01-01",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARS "='-2020'/>",
+ pcmk_rc_ok);
+}
+
+static void
+range_without_end_year_passes(void **state)
+{
+ run_one_test("2010-01-01",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARS "='2000-'/>",
+ pcmk_rc_ok);
+ run_one_test("2000-10-01",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARS "='2000-'/>",
+ pcmk_rc_ok);
+}
+
+static void
+yeardays_satisfies(void **state)
+{
+ run_one_test("2020-01-30",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARDAYS "='30'/>",
+ pcmk_rc_ok);
+}
+
+static void
+time_after_yeardays_spec(void **state)
+{
+ run_one_test("2020-02-15",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARDAYS "='40'/>",
+ pcmk_rc_after_range);
+}
+
+static void
+yeardays_feb_29_satisfies(void **state)
+{
+ run_one_test("2016-02-29",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARDAYS "='60'/>",
+ pcmk_rc_ok);
+}
+
+static void
+exact_ymd_satisfies(void **state)
+{
+ run_one_test("2001-12-31",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARS "='2001' "
+ PCMK_XA_MONTHS "='12' "
+ PCMK_XA_MONTHDAYS "='31'/>",
+ pcmk_rc_ok);
+}
+
+static void
+range_in_month_satisfies(void **state)
+{
+ run_one_test("2001-06-10",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARS "='2001' "
+ PCMK_XA_MONTHS "='6' "
+ PCMK_XA_MONTHDAYS "='1-10'/>",
+ pcmk_rc_ok);
+}
+
+static void
+exact_ymd_after_range(void **state)
+{
+ run_one_test("2001-12-31",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARS "='2001' "
+ PCMK_XA_MONTHS "='12' "
+ PCMK_XA_MONTHDAYS "='30'/>",
+ pcmk_rc_after_range);
+}
+
+static void
+time_after_monthdays_range(void **state)
+{
+ run_one_test("2001-06-10",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARS "='2001' "
+ PCMK_XA_MONTHS "='6' "
+ PCMK_XA_MONTHDAYS "='11-15'/>",
+ pcmk_rc_before_range);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_invalid),
+ cmocka_unit_test(spec_id_missing),
+ cmocka_unit_test(invalid_range),
+ cmocka_unit_test(time_satisfies_year_spec),
+ cmocka_unit_test(time_after_year_spec),
+ cmocka_unit_test(time_satisfies_year_range),
+ cmocka_unit_test(time_before_year_range),
+ cmocka_unit_test(time_after_year_range),
+ cmocka_unit_test(range_without_start_year_passes),
+ cmocka_unit_test(range_without_end_year_passes),
+ cmocka_unit_test(yeardays_satisfies),
+ cmocka_unit_test(time_after_yeardays_spec),
+ cmocka_unit_test(yeardays_feb_29_satisfies),
+ cmocka_unit_test(exact_ymd_satisfies),
+ cmocka_unit_test(range_in_month_satisfies),
+ cmocka_unit_test(exact_ymd_after_range),
+ cmocka_unit_test(time_after_monthdays_range))
diff --git a/lib/common/tests/rules/pcmk__evaluate_op_expression_test.c b/lib/common/tests/rules/pcmk__evaluate_op_expression_test.c
new file mode 100644
index 0000000..d1cb35f
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__evaluate_op_expression_test.c
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h>
+#include <glib.h>
+
+#include <crm/common/xml.h>
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+#include "crmcommon_private.h"
+
+/*
+ * Shared data
+ */
+
+static pcmk_rule_input_t rule_input = {
+ // These are the only members used to evaluate operation expressions
+ .op_name = PCMK_ACTION_MONITOR,
+ .op_interval_ms = 10000,
+};
+
+/*!
+ * \internal
+ * \brief Run one test, comparing return value
+ *
+ * \param[in] xml_string Operation expression XML as string
+ * \param[in] reference_rc Assert that evaluation result equals this
+ */
+static void
+assert_op_expression(const char *xml_string, int reference_rc)
+{
+ xmlNode *xml = pcmk__xml_parse(xml_string);
+
+ assert_int_equal(pcmk__evaluate_op_expression(xml, &rule_input),
+ reference_rc);
+ free_xml(xml);
+}
+
+
+/*
+ * Invalid arguments
+ */
+
+#define EXPR_FAIL_BOTH \
+ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_START "' " \
+ PCMK_XA_INTERVAL "='0' />"
+
+static void
+null_invalid(void **state)
+{
+ xmlNode *xml = NULL;
+
+ assert_int_equal(pcmk__evaluate_op_expression(NULL, NULL), EINVAL);
+
+ xml = pcmk__xml_parse(EXPR_FAIL_BOTH);
+ assert_int_equal(pcmk__evaluate_op_expression(xml, NULL), EINVAL);
+ free_xml(xml);
+
+ assert_op_expression(NULL, EINVAL);
+}
+
+
+/*
+ * Test PCMK_XA_ID
+ */
+
+#define EXPR_ID_MISSING \
+ "<" PCMK_XE_OP_EXPRESSION " " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />"
+
+#define EXPR_ID_EMPTY \
+ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />"
+
+static void
+id_missing(void **state)
+{
+ // Currently acceptable
+ assert_op_expression(EXPR_ID_MISSING, pcmk_rc_ok);
+ assert_op_expression(EXPR_ID_EMPTY, pcmk_rc_ok);
+}
+
+
+/*
+ * Test PCMK_XA_NAME
+ */
+
+#define EXPR_NAME_MISSING \
+ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_INTERVAL "='10s' />"
+
+static void
+name_missing(void **state)
+{
+ assert_op_expression(EXPR_NAME_MISSING, pcmk_rc_unpack_error);
+}
+
+#define EXPR_MATCH_BOTH \
+ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />"
+
+#define EXPR_EMPTY_NAME \
+ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='' " PCMK_XA_INTERVAL "='10s' />"
+
+static void
+input_name_missing(void **state)
+{
+ rule_input.op_name = NULL;
+ assert_op_expression(EXPR_MATCH_BOTH, pcmk_rc_op_unsatisfied);
+ assert_op_expression(EXPR_EMPTY_NAME, pcmk_rc_op_unsatisfied);
+ rule_input.op_name = PCMK_ACTION_MONITOR;
+}
+
+#define EXPR_FAIL_NAME \
+ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_START "' " \
+ PCMK_XA_INTERVAL "='10s' />"
+
+static void
+fail_name(void **state)
+{
+ assert_op_expression(EXPR_FAIL_NAME, pcmk_rc_op_unsatisfied);
+
+ // An empty name is meaningless but accepted, so not an unpack error
+ assert_op_expression(EXPR_EMPTY_NAME, pcmk_rc_op_unsatisfied);
+}
+
+
+/*
+ * Test PCMK_XA_INTERVAL
+ */
+
+#define EXPR_EMPTY_INTERVAL \
+ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='' />"
+
+#define EXPR_INVALID_INTERVAL \
+ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='not-an-interval' />"
+
+static void
+invalid_interval(void **state)
+{
+ assert_op_expression(EXPR_EMPTY_INTERVAL, pcmk_rc_unpack_error);
+ assert_op_expression(EXPR_INVALID_INTERVAL, pcmk_rc_unpack_error);
+}
+
+#define EXPR_DEFAULT_INTERVAL \
+ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' />"
+
+static void
+default_interval(void **state)
+{
+ assert_op_expression(EXPR_DEFAULT_INTERVAL, pcmk_rc_ok);
+}
+
+#define EXPR_FAIL_INTERVAL \
+ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='9s' />"
+
+static void
+fail_interval(void **state)
+{
+ assert_op_expression(EXPR_FAIL_INTERVAL, pcmk_rc_op_unsatisfied);
+}
+
+
+static void
+match_both(void **state)
+{
+ assert_op_expression(EXPR_MATCH_BOTH, pcmk_rc_ok);
+}
+
+static void
+fail_both(void **state)
+{
+ assert_op_expression(EXPR_FAIL_BOTH, pcmk_rc_op_unsatisfied);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_invalid),
+ cmocka_unit_test(id_missing),
+ cmocka_unit_test(name_missing),
+ cmocka_unit_test(input_name_missing),
+ cmocka_unit_test(fail_name),
+ cmocka_unit_test(invalid_interval),
+ cmocka_unit_test(default_interval),
+ cmocka_unit_test(fail_interval),
+ cmocka_unit_test(match_both),
+ cmocka_unit_test(fail_both))
diff --git a/lib/common/tests/rules/pcmk__evaluate_rsc_expression_test.c b/lib/common/tests/rules/pcmk__evaluate_rsc_expression_test.c
new file mode 100644
index 0000000..c3a164e
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__evaluate_rsc_expression_test.c
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h>
+#include <glib.h>
+
+#include <crm/common/xml.h>
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+#include "crmcommon_private.h"
+
+/*
+ * Shared data
+ */
+
+static pcmk_rule_input_t rule_input = {
+ // These are the only members used to evaluate resource expressions
+ .rsc_standard = PCMK_RESOURCE_CLASS_OCF,
+ .rsc_provider = "heartbeat",
+ .rsc_agent = "IPaddr2",
+};
+
+/*!
+ * \internal
+ * \brief Run one test, comparing return value
+ *
+ * \param[in] xml_string Resource expression XML as string
+ * \param[in] reference_rc Assert that evaluation result equals this
+ */
+static void
+assert_rsc_expression(const char *xml_string, int reference_rc)
+{
+ xmlNode *xml = pcmk__xml_parse(xml_string);
+
+ assert_int_equal(pcmk__evaluate_rsc_expression(xml, &rule_input),
+ reference_rc);
+ free_xml(xml);
+}
+
+
+/*
+ * Invalid arguments
+ */
+
+#define EXPR_ALL_MATCH \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
+ PCMK_XA_PROVIDER "='heartbeat' " \
+ PCMK_XA_TYPE "='IPaddr2' />"
+
+static void
+null_invalid(void **state)
+{
+ xmlNode *xml = NULL;
+
+ assert_int_equal(pcmk__evaluate_rsc_expression(NULL, NULL), EINVAL);
+
+ xml = pcmk__xml_parse(EXPR_ALL_MATCH);
+ assert_int_equal(pcmk__evaluate_rsc_expression(xml, NULL), EINVAL);
+ free_xml(xml);
+
+ assert_rsc_expression(NULL, EINVAL);
+}
+
+
+/*
+ * Test PCMK_XA_ID
+ */
+
+#define EXPR_ID_MISSING \
+ "<" PCMK_XE_RSC_EXPRESSION " " \
+ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
+ PCMK_XA_PROVIDER "='heartbeat' " \
+ PCMK_XA_TYPE "='IPaddr2' />"
+
+#define EXPR_ID_EMPTY \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='' " \
+ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
+ PCMK_XA_PROVIDER "='heartbeat' " \
+ PCMK_XA_TYPE "='IPaddr2' />"
+
+static void
+id_missing(void **state)
+{
+ // Currently acceptable
+ assert_rsc_expression(EXPR_ID_MISSING, pcmk_rc_ok);
+ assert_rsc_expression(EXPR_ID_EMPTY, pcmk_rc_ok);
+}
+
+
+/*
+ * Test standard, provider, and agent
+ */
+
+#define EXPR_FAIL_STANDARD \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_LSB "' />"
+
+#define EXPR_EMPTY_STANDARD \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_CLASS "='' />"
+
+static void
+fail_standard(void **state)
+{
+ assert_rsc_expression(EXPR_FAIL_STANDARD, pcmk_rc_op_unsatisfied);
+ assert_rsc_expression(EXPR_EMPTY_STANDARD, pcmk_rc_op_unsatisfied);
+
+ rule_input.rsc_standard = NULL;
+ assert_rsc_expression(EXPR_FAIL_STANDARD, pcmk_rc_op_unsatisfied);
+ assert_rsc_expression(EXPR_EMPTY_STANDARD, pcmk_rc_op_unsatisfied);
+ rule_input.rsc_standard = PCMK_RESOURCE_CLASS_OCF;
+}
+
+#define EXPR_FAIL_PROVIDER \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
+ PCMK_XA_PROVIDER "='pacemaker' " \
+ PCMK_XA_TYPE "='IPaddr2' />"
+
+#define EXPR_EMPTY_PROVIDER \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
+ PCMK_XA_PROVIDER "='' " PCMK_XA_TYPE "='IPaddr2' />"
+
+static void
+fail_provider(void **state)
+{
+ assert_rsc_expression(EXPR_FAIL_PROVIDER, pcmk_rc_op_unsatisfied);
+ assert_rsc_expression(EXPR_EMPTY_PROVIDER, pcmk_rc_op_unsatisfied);
+
+ rule_input.rsc_provider = NULL;
+ assert_rsc_expression(EXPR_FAIL_PROVIDER, pcmk_rc_op_unsatisfied);
+ assert_rsc_expression(EXPR_EMPTY_PROVIDER, pcmk_rc_op_unsatisfied);
+ rule_input.rsc_provider = "heartbeat";
+}
+
+#define EXPR_FAIL_AGENT \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
+ PCMK_XA_PROVIDER "='heartbeat' " \
+ PCMK_XA_TYPE "='IPaddr3' />"
+
+#define EXPR_EMPTY_AGENT \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
+ PCMK_XA_PROVIDER "='heartbeat' " PCMK_XA_TYPE "='' />"
+
+static void
+fail_agent(void **state)
+{
+ assert_rsc_expression(EXPR_FAIL_AGENT, pcmk_rc_op_unsatisfied);
+ assert_rsc_expression(EXPR_EMPTY_AGENT, pcmk_rc_op_unsatisfied);
+
+ rule_input.rsc_agent = NULL;
+ assert_rsc_expression(EXPR_FAIL_AGENT, pcmk_rc_op_unsatisfied);
+ assert_rsc_expression(EXPR_EMPTY_AGENT, pcmk_rc_op_unsatisfied);
+ rule_input.rsc_agent = "IPaddr2";
+}
+
+#define EXPR_NO_STANDARD_MATCHES \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_PROVIDER "='heartbeat' " \
+ PCMK_XA_TYPE "='IPaddr2' />"
+
+static void
+no_standard_matches(void **state)
+{
+ assert_rsc_expression(EXPR_NO_STANDARD_MATCHES, pcmk_rc_ok);
+}
+
+#define EXPR_NO_PROVIDER_MATCHES \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
+ PCMK_XA_TYPE "='IPaddr2' />"
+
+static void
+no_provider_matches(void **state)
+{
+ assert_rsc_expression(EXPR_NO_PROVIDER_MATCHES, pcmk_rc_ok);
+}
+
+#define EXPR_NO_AGENT_MATCHES \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
+ PCMK_XA_PROVIDER "='heartbeat' />"
+
+static void
+no_agent_matches(void **state)
+{
+ assert_rsc_expression(EXPR_NO_AGENT_MATCHES, pcmk_rc_ok);
+}
+
+#define EXPR_NO_CRITERIA_MATCHES \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' />"
+
+static void
+no_criteria_matches(void **state)
+{
+ assert_rsc_expression(EXPR_NO_CRITERIA_MATCHES, pcmk_rc_ok);
+}
+
+static void
+all_match(void **state)
+{
+ assert_rsc_expression(EXPR_ALL_MATCH, pcmk_rc_ok);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_invalid),
+ cmocka_unit_test(id_missing),
+ cmocka_unit_test(fail_standard),
+ cmocka_unit_test(fail_provider),
+ cmocka_unit_test(fail_agent),
+ cmocka_unit_test(no_standard_matches),
+ cmocka_unit_test(no_provider_matches),
+ cmocka_unit_test(no_agent_matches),
+ cmocka_unit_test(no_criteria_matches),
+ cmocka_unit_test(all_match))
diff --git a/lib/common/tests/rules/pcmk__parse_combine_test.c b/lib/common/tests/rules/pcmk__parse_combine_test.c
new file mode 100644
index 0000000..afebcf8
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__parse_combine_test.c
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h>
+
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+default_and(void **state)
+{
+ assert_int_equal(pcmk__parse_combine(NULL), pcmk__combine_and);
+}
+
+static void
+invalid(void **state)
+{
+ assert_int_equal(pcmk__parse_combine(""), pcmk__combine_unknown);
+ assert_int_equal(pcmk__parse_combine(" "), pcmk__combine_unknown);
+ assert_int_equal(pcmk__parse_combine("but"), pcmk__combine_unknown);
+}
+
+static void
+valid(void **state)
+{
+ assert_int_equal(pcmk__parse_combine(PCMK_VALUE_AND), pcmk__combine_and);
+ assert_int_equal(pcmk__parse_combine(PCMK_VALUE_OR), pcmk__combine_or);
+}
+
+static void
+case_insensitive(void **state)
+{
+ assert_int_equal(pcmk__parse_combine("And"),
+ pcmk__combine_and);
+
+ assert_int_equal(pcmk__parse_combine("OR"),
+ pcmk__combine_or);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(default_and),
+ cmocka_unit_test(invalid),
+ cmocka_unit_test(valid),
+ cmocka_unit_test(case_insensitive))
diff --git a/lib/common/tests/rules/pcmk__parse_comparison_test.c b/lib/common/tests/rules/pcmk__parse_comparison_test.c
new file mode 100644
index 0000000..a995596
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__parse_comparison_test.c
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h>
+
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+#include "crmcommon_private.h"
+
+static void
+null_unknown(void **state)
+{
+ assert_int_equal(pcmk__parse_comparison(NULL), pcmk__comparison_unknown);
+}
+
+static void
+invalid(void **state)
+{
+ assert_int_equal(pcmk__parse_comparison("nope"), pcmk__comparison_unknown);
+}
+
+static void
+valid(void **state)
+{
+ assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_DEFINED),
+ pcmk__comparison_defined);
+
+ assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_NOT_DEFINED),
+ pcmk__comparison_undefined);
+
+ assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_EQ),
+ pcmk__comparison_eq);
+
+ assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_NE),
+ pcmk__comparison_ne);
+
+ assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_LT),
+ pcmk__comparison_lt);
+
+ assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_LTE),
+ pcmk__comparison_lte);
+
+ assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_GT),
+ pcmk__comparison_gt);
+
+ assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_GTE),
+ pcmk__comparison_gte);
+}
+
+static void
+case_insensitive(void **state)
+{
+ assert_int_equal(pcmk__parse_comparison("DEFINED"),
+ pcmk__comparison_defined);
+
+ assert_int_equal(pcmk__parse_comparison("Not_Defined"),
+ pcmk__comparison_undefined);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_unknown),
+ cmocka_unit_test(invalid),
+ cmocka_unit_test(valid),
+ cmocka_unit_test(case_insensitive))
diff --git a/lib/common/tests/rules/pcmk__parse_source_test.c b/lib/common/tests/rules/pcmk__parse_source_test.c
new file mode 100644
index 0000000..9cf9b32
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__parse_source_test.c
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h>
+
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+#include "crmcommon_private.h"
+
+static void
+default_literal(void **state)
+{
+ assert_int_equal(pcmk__parse_source(NULL), pcmk__source_literal);
+}
+
+static void
+invalid(void **state)
+{
+ assert_int_equal(pcmk__parse_source(""), pcmk__source_unknown);
+ assert_int_equal(pcmk__parse_source(" "), pcmk__source_unknown);
+ assert_int_equal(pcmk__parse_source("params"), pcmk__source_unknown);
+}
+
+static void
+valid(void **state)
+{
+ assert_int_equal(pcmk__parse_source(PCMK_VALUE_LITERAL),
+ pcmk__source_literal);
+
+ assert_int_equal(pcmk__parse_source(PCMK_VALUE_PARAM),
+ pcmk__source_instance_attrs);
+
+ assert_int_equal(pcmk__parse_source(PCMK_VALUE_META),
+ pcmk__source_meta_attrs);
+}
+
+static void
+case_insensitive(void **state)
+{
+ assert_int_equal(pcmk__parse_source("LITERAL"),
+ pcmk__source_literal);
+
+ assert_int_equal(pcmk__parse_source("Param"),
+ pcmk__source_instance_attrs);
+
+ assert_int_equal(pcmk__parse_source("MeTa"),
+ pcmk__source_meta_attrs);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(default_literal),
+ cmocka_unit_test(invalid),
+ cmocka_unit_test(valid),
+ cmocka_unit_test(case_insensitive))
diff --git a/lib/common/tests/rules/pcmk__parse_type_test.c b/lib/common/tests/rules/pcmk__parse_type_test.c
new file mode 100644
index 0000000..96f02c8
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__parse_type_test.c
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h>
+
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+#include "crmcommon_private.h"
+
+static void
+invalid(void **state)
+{
+ assert_int_equal(pcmk__parse_type("nope", pcmk__comparison_unknown,
+ NULL, NULL),
+ pcmk__type_unknown);
+}
+
+static void
+valid(void **state)
+{
+ assert_int_equal(pcmk__parse_type(PCMK_VALUE_STRING,
+ pcmk__comparison_unknown, NULL, NULL),
+ pcmk__type_string);
+
+ assert_int_equal(pcmk__parse_type(PCMK_VALUE_INTEGER,
+ pcmk__comparison_unknown, NULL, NULL),
+ pcmk__type_integer);
+
+ assert_int_equal(pcmk__parse_type(PCMK_VALUE_NUMBER,
+ pcmk__comparison_unknown, NULL, NULL),
+ pcmk__type_number);
+
+ assert_int_equal(pcmk__parse_type(PCMK_VALUE_VERSION,
+ pcmk__comparison_unknown, NULL, NULL),
+ pcmk__type_version);
+}
+
+static void
+case_insensitive(void **state)
+{
+ assert_int_equal(pcmk__parse_type("STRING", pcmk__comparison_unknown,
+ NULL, NULL),
+ pcmk__type_string);
+
+ assert_int_equal(pcmk__parse_type("Integer", pcmk__comparison_unknown,
+ NULL, NULL),
+ pcmk__type_integer);
+}
+
+static void
+default_number(void **state)
+{
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_lt, "1.0", "2.5"),
+ pcmk__type_number);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_lte, "1.", "2"),
+ pcmk__type_number);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gt, "1", ".5"),
+ pcmk__type_number);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gte, "1.0", "2"),
+ pcmk__type_number);
+}
+
+static void
+default_integer(void **state)
+{
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_lt, "1", "2"),
+ pcmk__type_integer);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_lte, "1", "2"),
+ pcmk__type_integer);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gt, "1", "2"),
+ pcmk__type_integer);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gte, "1", "2"),
+ pcmk__type_integer);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gte, NULL, NULL),
+ pcmk__type_integer);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gte, "1", NULL),
+ pcmk__type_integer);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gte, NULL, "2.5"),
+ pcmk__type_number);
+}
+
+static void
+default_string(void **state)
+{
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_unknown,
+ NULL, NULL),
+ pcmk__type_string);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_defined,
+ NULL, NULL),
+ pcmk__type_string);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_undefined,
+ NULL, NULL),
+ pcmk__type_string);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_eq, NULL, NULL),
+ pcmk__type_string);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_ne, NULL, NULL),
+ pcmk__type_string);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(invalid),
+ cmocka_unit_test(valid),
+ cmocka_unit_test(case_insensitive),
+ cmocka_unit_test(default_number),
+ cmocka_unit_test(default_integer),
+ cmocka_unit_test(default_string))
diff --git a/lib/common/tests/rules/pcmk__replace_submatches_test.c b/lib/common/tests/rules/pcmk__replace_submatches_test.c
new file mode 100644
index 0000000..d404fcc
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__replace_submatches_test.c
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <regex.h> // regmatch_t
+
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+
+// An example matched string with submatches
+static const char *match = "this is a string";
+static const regmatch_t submatches[] = {
+ { .rm_so = 0, .rm_eo = 16 }, // %0 = entire string
+ { .rm_so = 5, .rm_eo = 7 }, // %1 = "is"
+ { .rm_so = 9, .rm_eo = 9 }, // %2 = empty match
+};
+static const int nmatches = 3;
+
+static void
+assert_submatch(const char *string, const char *reference)
+{
+ char *expanded = NULL;
+
+ expanded = pcmk__replace_submatches(string, match, submatches, nmatches);
+ if ((expanded == NULL) || (reference == NULL)) {
+ assert_null(expanded);
+ assert_null(reference);
+ } else {
+ assert_int_equal(strcmp(expanded, reference), 0);
+ }
+ free(expanded);
+}
+
+static void
+no_source(void **state)
+{
+ assert_null(pcmk__replace_submatches(NULL, NULL, NULL, 0));
+ assert_submatch(NULL, NULL);
+ assert_submatch("", NULL);
+}
+
+static void
+source_has_no_variables(void **state)
+{
+ assert_null(pcmk__replace_submatches("this has no submatch variables",
+ match, submatches, nmatches));
+ assert_null(pcmk__replace_submatches("this ends in a %",
+ match, submatches, nmatches));
+ assert_null(pcmk__replace_submatches("%this starts with one",
+ match, submatches, nmatches));
+}
+
+static void
+without_matches(void **state)
+{
+ assert_submatch("this has an empty submatch %2",
+ "this has an empty submatch ");
+ assert_submatch("this has a nonexistent submatch %3",
+ "this has a nonexistent submatch ");
+}
+
+static void
+with_matches(void **state)
+{
+ assert_submatch("%0", match); // %0 matches entire string
+ assert_submatch("this %1", "this is");
+ assert_submatch("%1 this %ok", "is this %ok");
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(no_source),
+ cmocka_unit_test(source_has_no_variables),
+ cmocka_unit_test(without_matches),
+ cmocka_unit_test(with_matches))
diff --git a/lib/common/tests/rules/pcmk__unpack_duration_test.c b/lib/common/tests/rules/pcmk__unpack_duration_test.c
new file mode 100644
index 0000000..e82546c
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__unpack_duration_test.c
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <glib.h>
+
+#include <crm/common/unittest_internal.h>
+
+#include <crm/common/iso8601.h>
+#include <crm/common/xml.h>
+#include "../../crmcommon_private.h"
+
+#define MONTHS_TO_SECONDS "months=\"2\" weeks=\"3\" days=\"-1\" " \
+ "hours=\"1\" minutes=\"1\" seconds=\"1\" />"
+
+#define ALL_VALID "<duration id=\"duration1\" years=\"1\" " MONTHS_TO_SECONDS
+
+#define NO_ID "<duration years=\"1\" " MONTHS_TO_SECONDS
+
+#define YEARS_INVALID "<duration id=\"duration1\" years=\"not-a-number\" " \
+ MONTHS_TO_SECONDS
+
+static void
+null_invalid(void **state)
+{
+ xmlNode *duration = pcmk__xml_parse(ALL_VALID);
+ crm_time_t *start = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *end = NULL;
+
+ assert_int_equal(pcmk__unpack_duration(NULL, NULL, NULL), EINVAL);
+ assert_int_equal(pcmk__unpack_duration(duration, NULL, NULL), EINVAL);
+ assert_int_equal(pcmk__unpack_duration(duration, start, NULL), EINVAL);
+ assert_int_equal(pcmk__unpack_duration(duration, NULL, &end), EINVAL);
+ assert_int_equal(pcmk__unpack_duration(NULL, start, NULL), EINVAL);
+ assert_int_equal(pcmk__unpack_duration(NULL, start, &end), EINVAL);
+ assert_int_equal(pcmk__unpack_duration(NULL, NULL, &end), EINVAL);
+
+ crm_time_free(start);
+ free_xml(duration);
+}
+
+static void
+nonnull_end_invalid(void **state)
+{
+ xmlNode *duration = pcmk__xml_parse(ALL_VALID);
+ crm_time_t *start = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *end = crm_time_new("2024-01-01 15:00:01");
+
+ assert_int_equal(pcmk__unpack_duration(duration, start, &end), EINVAL);
+
+ crm_time_free(start);
+ crm_time_free(end);
+ free_xml(duration);
+}
+
+static void
+no_id(void **state)
+{
+ xmlNode *duration = pcmk__xml_parse(NO_ID);
+ crm_time_t *start = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *end = NULL;
+ crm_time_t *reference = crm_time_new("2025-03-21 16:01:01");
+
+ assert_int_equal(pcmk__unpack_duration(duration, start, &end), pcmk_rc_ok);
+ assert_int_equal(crm_time_compare(end, reference), 0);
+
+ crm_time_free(start);
+ crm_time_free(end);
+ crm_time_free(reference);
+ free_xml(duration);
+}
+
+static void
+years_invalid(void **state)
+{
+ xmlNode *duration = pcmk__xml_parse(YEARS_INVALID);
+ crm_time_t *start = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *end = NULL;
+ crm_time_t *reference = crm_time_new("2024-03-21 16:01:01");
+
+ assert_int_equal(pcmk__unpack_duration(duration, start, &end),
+ pcmk_rc_unpack_error);
+ assert_int_equal(crm_time_compare(end, reference), 0);
+
+ crm_time_free(start);
+ crm_time_free(end);
+ crm_time_free(reference);
+ free_xml(duration);
+}
+
+static void
+all_valid(void **state)
+{
+ xmlNode *duration = pcmk__xml_parse(ALL_VALID);
+ crm_time_t *start = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *end = NULL;
+ crm_time_t *reference = crm_time_new("2025-03-21 16:01:01");
+
+ assert_int_equal(pcmk__unpack_duration(duration, start, &end), pcmk_rc_ok);
+ assert_int_equal(crm_time_compare(end, reference), 0);
+
+ crm_time_free(start);
+ crm_time_free(end);
+ crm_time_free(reference);
+ free_xml(duration);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_invalid),
+ cmocka_unit_test(nonnull_end_invalid),
+ cmocka_unit_test(no_id),
+ cmocka_unit_test(years_invalid),
+ cmocka_unit_test(all_valid))
diff --git a/lib/common/tests/rules/pcmk_evaluate_rule_test.c b/lib/common/tests/rules/pcmk_evaluate_rule_test.c
new file mode 100644
index 0000000..6b6f9eb
--- /dev/null
+++ b/lib/common/tests/rules/pcmk_evaluate_rule_test.c
@@ -0,0 +1,379 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h>
+#include <glib.h>
+
+#include <crm/common/xml.h>
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+
+/*
+ * Shared data
+ */
+
+static pcmk_rule_input_t rule_input = {
+ .rsc_standard = PCMK_RESOURCE_CLASS_OCF,
+ .rsc_provider = "heartbeat",
+ .rsc_agent = "IPaddr2",
+ .op_name = PCMK_ACTION_MONITOR,
+ .op_interval_ms = 10000,
+};
+
+
+/*
+ * Test invalid arguments
+ */
+
+#define RULE_OP \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' > " \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+null_invalid(void **state)
+{
+ xmlNode *xml = NULL;
+ crm_time_t *next_change = crm_time_new_undefined();
+
+ assert_int_equal(pcmk_evaluate_rule(NULL, NULL, next_change),
+ EINVAL);
+
+ xml = pcmk__xml_parse(RULE_OP);
+ assert_int_equal(pcmk_evaluate_rule(xml, NULL, next_change), EINVAL);
+ free_xml(xml);
+
+ assert_int_equal(pcmk_evaluate_rule(NULL, &rule_input, next_change),
+ EINVAL);
+
+ crm_time_free(next_change);
+}
+
+#define RULE_OP_MISSING_ID \
+ "<" PCMK_XE_RULE "> " \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+id_missing(void **state)
+{
+ // Currently acceptable
+ xmlNode *xml = pcmk__xml_parse(RULE_OP_MISSING_ID);
+ crm_time_t *next_change = crm_time_new_undefined();
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, next_change),
+ pcmk_rc_ok);
+
+ crm_time_free(next_change);
+ free_xml(xml);
+}
+
+#define RULE_IDREF_PARENT "<" PCMK_XE_CIB ">" RULE_OP "</" PCMK_XE_CIB ">"
+
+static void
+good_idref(void **state)
+{
+ xmlNode *parent_xml = pcmk__xml_parse(RULE_IDREF_PARENT);
+ xmlNode *rule_xml = pcmk__xe_create(parent_xml, PCMK_XE_RULE);
+ crm_time_t *next_change = crm_time_new_undefined();
+
+ crm_xml_add(rule_xml, PCMK_XA_ID_REF, "r");
+ assert_int_equal(pcmk_evaluate_rule(rule_xml, &rule_input, next_change),
+ pcmk_rc_ok);
+
+ crm_time_free(next_change);
+ free_xml(parent_xml);
+}
+
+static void
+bad_idref(void **state)
+{
+ xmlNode *parent_xml = pcmk__xml_parse(RULE_IDREF_PARENT);
+ xmlNode *rule_xml = pcmk__xe_create(parent_xml, PCMK_XE_RULE);
+ crm_time_t *next_change = crm_time_new_undefined();
+
+ crm_xml_add(rule_xml, PCMK_XA_ID_REF, "x");
+ assert_int_equal(pcmk_evaluate_rule(rule_xml, &rule_input, next_change),
+ pcmk_rc_unpack_error);
+
+ crm_time_free(next_change);
+ free_xml(parent_xml);
+}
+
+#define RULE_EMPTY "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' />"
+
+static void
+empty_default(void **state)
+{
+ // Currently acceptable
+ xmlNode *xml = pcmk__xml_parse(RULE_EMPTY);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL),
+ pcmk_rc_ok);
+
+ free_xml(xml);
+}
+
+#define RULE_EMPTY_AND \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' />"
+
+static void
+empty_and(void **state)
+{
+ // Currently acceptable
+ xmlNode *xml = pcmk__xml_parse(RULE_EMPTY_AND);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL),
+ pcmk_rc_ok);
+
+ free_xml(xml);
+}
+
+#define RULE_EMPTY_OR \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' />"
+
+static void
+empty_or(void **state)
+{
+ // Currently treated as unsatisfied
+ xmlNode *xml = pcmk__xml_parse(RULE_EMPTY_OR);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL),
+ pcmk_rc_op_unsatisfied);
+
+ free_xml(xml);
+}
+
+#define RULE_DEFAULT_BOOLEAN_OP \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='Dummy' />" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+default_boolean_op(void **state)
+{
+ // Defaults to PCMK_VALUE_AND
+ xmlNode *xml = pcmk__xml_parse(RULE_DEFAULT_BOOLEAN_OP);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL),
+ pcmk_rc_op_unsatisfied);
+
+ free_xml(xml);
+}
+
+#define RULE_INVALID_BOOLEAN_OP \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='not-an-op' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='Dummy' />" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+invalid_boolean_op(void **state)
+{
+ // Currently defaults to PCMK_VALUE_AND
+ xmlNode *xml = pcmk__xml_parse(RULE_INVALID_BOOLEAN_OP);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL),
+ pcmk_rc_op_unsatisfied);
+
+ free_xml(xml);
+}
+
+#define RULE_AND_PASSES \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='IPaddr2' />" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+and_passes(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(RULE_AND_PASSES);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok);
+
+ free_xml(xml);
+}
+
+#define RULE_LONELY_AND \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='IPaddr2' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+lonely_and_passes(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(RULE_LONELY_AND);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok);
+
+ free_xml(xml);
+}
+
+#define RULE_AND_ONE_FAILS \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='Dummy' />" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+and_one_fails(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(RULE_AND_ONE_FAILS);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL),
+ pcmk_rc_op_unsatisfied);
+
+ free_xml(xml);
+}
+
+#define RULE_AND_TWO_FAIL \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='Dummy' />" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='9s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+and_two_fail(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(RULE_AND_TWO_FAIL);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL),
+ pcmk_rc_op_unsatisfied);
+
+ free_xml(xml);
+}
+
+#define RULE_OR_ONE_PASSES \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='Dummy' />" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+or_one_passes(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(RULE_OR_ONE_PASSES);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok);
+
+ free_xml(xml);
+}
+
+#define RULE_OR_TWO_PASS \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='IPAddr2' />" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+or_two_pass(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(RULE_OR_TWO_PASS);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok);
+
+ free_xml(xml);
+}
+
+#define RULE_LONELY_OR \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' >" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+lonely_or_passes(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(RULE_LONELY_OR);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok);
+
+ free_xml(xml);
+}
+
+#define RULE_OR_FAILS \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='Dummy' />" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='20s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+or_fails(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(RULE_OR_FAILS);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL),
+ pcmk_rc_op_unsatisfied);
+
+ free_xml(xml);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_invalid),
+ cmocka_unit_test(id_missing),
+ cmocka_unit_test(good_idref),
+ cmocka_unit_test(bad_idref),
+ cmocka_unit_test(empty_default),
+ cmocka_unit_test(empty_and),
+ cmocka_unit_test(empty_or),
+ cmocka_unit_test(default_boolean_op),
+ cmocka_unit_test(invalid_boolean_op),
+ cmocka_unit_test(and_passes),
+ cmocka_unit_test(lonely_and_passes),
+ cmocka_unit_test(and_one_fails),
+ cmocka_unit_test(and_two_fail),
+ cmocka_unit_test(or_one_passes),
+ cmocka_unit_test(or_two_pass),
+ cmocka_unit_test(lonely_or_passes),
+ cmocka_unit_test(or_fails))
diff --git a/lib/common/tests/scheduler/Makefile.am b/lib/common/tests/scheduler/Makefile.am
new file mode 100644
index 0000000..6d5f4f8
--- /dev/null
+++ b/lib/common/tests/scheduler/Makefile.am
@@ -0,0 +1,19 @@
+#
+# Copyright 2024 the Pacemaker project contributors
+#
+# The version control history for this file may have further details.
+#
+# This source code is licensed under the GNU General Public License version 2
+# or later (GPLv2+) WITHOUT ANY WARRANTY.
+#
+
+include $(top_srcdir)/mk/tap.mk
+include $(top_srcdir)/mk/unittest.mk
+
+# Add "_test" to the end of all test program names to simplify .gitignore.
+check_PROGRAMS = pcmk_get_dc_test \
+ pcmk_get_no_quorum_policy_test \
+ pcmk_has_quorum_test \
+ pcmk_set_scheduler_cib_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/scheduler/pcmk_get_dc_test.c b/lib/common/tests/scheduler/pcmk_get_dc_test.c
new file mode 100644
index 0000000..5d9d459
--- /dev/null
+++ b/lib/common/tests/scheduler/pcmk_get_dc_test.c
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/scheduler.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+null_scheduler(void **state)
+{
+ assert_null(pcmk_get_dc(NULL));
+}
+
+static void
+null_dc(void **state)
+{
+ pcmk_scheduler_t scheduler = {
+ .dc_node = NULL,
+ };
+
+ assert_null(pcmk_get_dc(&scheduler));
+}
+
+static void
+valid_dc(void **state)
+{
+ pcmk_node_t dc = {
+ .weight = 1,
+ };
+ pcmk_scheduler_t scheduler = {
+ .dc_node = &dc,
+ };
+
+ assert_ptr_equal(&dc, pcmk_get_dc(&scheduler));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_scheduler),
+ cmocka_unit_test(null_dc),
+ cmocka_unit_test(valid_dc))
diff --git a/lib/common/tests/scheduler/pcmk_get_no_quorum_policy_test.c b/lib/common/tests/scheduler/pcmk_get_no_quorum_policy_test.c
new file mode 100644
index 0000000..61c97e6
--- /dev/null
+++ b/lib/common/tests/scheduler/pcmk_get_no_quorum_policy_test.c
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/scheduler.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+null_scheduler(void **state)
+{
+ assert_int_equal(pcmk_get_no_quorum_policy(NULL), pcmk_no_quorum_stop);
+}
+
+static void
+valid_no_quorum_policy(void **state)
+{
+ pcmk_scheduler_t scheduler = {
+ .no_quorum_policy = pcmk_no_quorum_fence,
+ };
+
+ assert_int_equal(pcmk_get_no_quorum_policy(&scheduler),
+ pcmk_no_quorum_fence);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_scheduler),
+ cmocka_unit_test(valid_no_quorum_policy))
diff --git a/lib/common/tests/scheduler/pcmk_has_quorum_test.c b/lib/common/tests/scheduler/pcmk_has_quorum_test.c
new file mode 100644
index 0000000..51903df
--- /dev/null
+++ b/lib/common/tests/scheduler/pcmk_has_quorum_test.c
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/scheduler.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+null_scheduler(void **state)
+{
+ assert_false(pcmk_has_quorum(NULL));
+}
+
+static void
+valid_scheduler(void **state)
+{
+ pcmk_scheduler_t scheduler = {
+ .flags = pcmk_sched_quorate,
+ };
+
+ assert_true(pcmk_has_quorum(&scheduler));
+
+ scheduler.flags = pcmk_sched_none;
+ assert_false(pcmk_has_quorum(&scheduler));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_scheduler),
+ cmocka_unit_test(valid_scheduler))
diff --git a/lib/common/tests/scheduler/pcmk_set_scheduler_cib_test.c b/lib/common/tests/scheduler/pcmk_set_scheduler_cib_test.c
new file mode 100644
index 0000000..71e690b
--- /dev/null
+++ b/lib/common/tests/scheduler/pcmk_set_scheduler_cib_test.c
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/scheduler.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+null_scheduler(void **state)
+{
+ xmlNode *cib = pcmk__xe_create(NULL, "test");
+
+ assert_int_equal(pcmk_set_scheduler_cib(NULL, NULL), EINVAL);
+ assert_int_equal(pcmk_set_scheduler_cib(NULL, cib), EINVAL);
+
+ free_xml(cib);
+}
+
+static void
+null_cib(void **state)
+{
+ pcmk_scheduler_t scheduler = {
+ .input = NULL,
+ };
+
+ assert_int_equal(pcmk_set_scheduler_cib(&scheduler, NULL), pcmk_rc_ok);
+ assert_null(scheduler.input);
+}
+
+static void
+previous_cib_null(void **state)
+{
+ pcmk_scheduler_t scheduler = {
+ .input = NULL,
+ };
+ xmlNode *cib = pcmk__xe_create(NULL, "test");
+
+ assert_int_equal(pcmk_set_scheduler_cib(&scheduler, cib), pcmk_rc_ok);
+ assert_ptr_equal(scheduler.input, cib);
+
+ free_xml(cib);
+}
+
+static void
+previous_cib_nonnull(void **state)
+{
+ xmlNode *old_cib = pcmk__xe_create(NULL, "old");
+ xmlNode *new_cib = pcmk__xe_create(NULL, "new");
+ pcmk_scheduler_t scheduler = {
+ .input = old_cib,
+ };
+
+ assert_int_equal(pcmk_set_scheduler_cib(&scheduler, new_cib), pcmk_rc_ok);
+ assert_ptr_equal(scheduler.input, new_cib);
+
+ free_xml(old_cib);
+ free_xml(new_cib);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_scheduler),
+ cmocka_unit_test(null_cib),
+ cmocka_unit_test(previous_cib_null),
+ cmocka_unit_test(previous_cib_nonnull))
diff --git a/lib/common/tests/schemas/Makefile.am b/lib/common/tests/schemas/Makefile.am
new file mode 100644
index 0000000..ba0f805
--- /dev/null
+++ b/lib/common/tests/schemas/Makefile.am
@@ -0,0 +1,88 @@
+#
+# Copyright 2023-2024 the Pacemaker project contributors
+#
+# The version control history for this file may have further details.
+#
+# This source code is licensed under the GNU General Public License version 2
+# or later (GPLv2+) WITHOUT ANY WARRANTY.
+#
+
+include $(top_srcdir)/mk/tap.mk
+include $(top_srcdir)/mk/unittest.mk
+
+CFLAGS += -DPCMK__TEST_SCHEMA_DIR='"$(abs_builddir)/schemas"'
+
+# Add "_test" to the end of all test program names to simplify .gitignore.
+
+# These tests share a schema subdirectory
+SHARED_SCHEMA_TESTS = pcmk__cmp_schemas_by_name_test \
+ crm_schema_init_test \
+ pcmk__build_schema_xml_node_test \
+ pcmk__get_schema_test \
+ pcmk__schema_files_later_than_test
+
+# This test has its own schema directory
+FIND_X_0_SCHEMA_TEST = pcmk__find_x_0_schema_test
+
+check_PROGRAMS = $(SHARED_SCHEMA_TESTS) $(FIND_X_0_SCHEMA_TEST)
+
+TESTS = $(check_PROGRAMS)
+
+$(SHARED_SCHEMA_TESTS): setup-schema-dir
+
+$(FIND_X_0_SCHEMA_TEST): setup-find_x_0-schema-dir
+
+# Set up a temporary schemas/ directory containing only some of the full set of
+# pacemaker schema files. This lets us know exactly how many schemas are present,
+# allowing us to write tests without having to make changes when new schemas are
+# added.
+#
+# This directory contains the following:
+#
+# * pacemaker-next.rng - Used to verify that this sorts before all versions
+# * upgrade-*.xsl - Required by various schema versions
+# * pacemaker-[0-9]*.rng - We're only pulling in 15 schemas, which is enough
+# to get everything through pacemaker-3.0.rng. This
+# includes 2.10, needed so we can check that versions
+# are compared as numbers instead of strings.
+# * other RNG files - This catches everything except the pacemaker-*rng
+# files. These files are included by the top-level
+# pacemaker-*rng files, so we need them for tests.
+# This will glob more than we need, but the extra ones
+# won't get in the way.
+
+LINK_FILES = $(abs_top_builddir)/xml/pacemaker-next.rng \
+ $(abs_top_builddir)/xml/upgrade-*.xsl
+ROOT_RNGS = $(shell ls -1v $(abs_top_builddir)/xml/pacemaker-[0-9]*.rng | head -15)
+INCLUDED_RNGS = $(shell ls -1 $(top_srcdir)/xml/*.rng | grep -v pacemaker-[0-9])
+
+# Most tests share a common, read-only schema directory
+.PHONY: setup-schema-dir
+setup-schema-dir:
+ $(MKDIR_P) schemas
+ ( cd schemas ; \
+ ln -sf $(LINK_FILES) . ; \
+ for f in $(ROOT_RNGS); do \
+ ln -sf $$f $$(basename $$f); \
+ done ; \
+ for f in $(INCLUDED_RNGS); do \
+ ln -sf ../$$f $$(basename $$f); \
+ done )
+
+# pcmk__find_x_0_schema_test moves schema files around, so it needs its
+# own directory, otherwise other tests run in parallel could fail.
+.PHONY: setup-find_x_0-schema-dir
+setup-find_x_0-schema-dir:
+ $(MKDIR_P) schemas/find_x_0
+ ( cd schemas/find_x_0 ; \
+ ln -sf $(LINK_FILES) . ; \
+ for f in $(ROOT_RNGS); do \
+ ln -sf $$f $$(basename $$f); \
+ done ; \
+ for f in $(INCLUDED_RNGS); do \
+ ln -sf ../$$f $$(basename $$f); \
+ done )
+
+.PHONY: clean-local
+clean-local:
+ -rm -rf schemas
diff --git a/lib/common/tests/schemas/crm_schema_init_test.c b/lib/common/tests/schemas/crm_schema_init_test.c
new file mode 100644
index 0000000..8da79cb
--- /dev/null
+++ b/lib/common/tests/schemas/crm_schema_init_test.c
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2023-2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <ftw.h>
+#include <unistd.h>
+
+#include <crm/common/xml.h>
+#include <crm/common/unittest_internal.h>
+#include <crm/common/xml_internal.h>
+#include "crmcommon_private.h"
+
+static char *remote_schema_dir = NULL;
+
+static int
+symlink_schema(const char *tmpdir, const char *target_file, const char *link_file)
+{
+ int rc = 0;
+ char *oldpath = NULL;
+ char *newpath = NULL;
+
+ oldpath = crm_strdup_printf("%s/%s", PCMK__TEST_SCHEMA_DIR, target_file);
+ newpath = crm_strdup_printf("%s/%s", tmpdir, link_file);
+
+ rc = symlink(oldpath, newpath);
+
+ free(oldpath);
+ free(newpath);
+ return rc;
+}
+
+static int
+rm_files(const char *pathname, const struct stat *sbuf, int type, struct FTW *ftwb)
+{
+ return remove(pathname);
+}
+
+static int
+rmtree(const char *dir)
+{
+ return nftw(dir, rm_files, 10, FTW_DEPTH|FTW_MOUNT|FTW_PHYS);
+}
+
+static int
+setup(void **state)
+{
+ char *dir = NULL;
+
+ /* Create a directory to hold additional schema files. These don't need
+ * to be anything special - we can just copy existing schemas but give
+ * them new names.
+ */
+ dir = crm_strdup_printf("%s/test-schemas.XXXXXX", pcmk__get_tmpdir());
+ remote_schema_dir = mkdtemp(dir);
+
+ if (remote_schema_dir == NULL) {
+ free(dir);
+ return -1;
+ }
+
+ /* Add new files to simulate a remote node not being up-to-date. We can't
+ * add a new major version here without also creating an XSL transform, and
+ * we can't add an older version (like 1.1 or 2.11 or something) because
+ * remotes will only ever ask for stuff newer than their newest.
+ */
+ if (symlink_schema(dir, "pacemaker-3.0.rng", "pacemaker-3.1.rng") != 0) {
+ rmdir(dir);
+ free(dir);
+ return -1;
+ }
+
+ if (symlink_schema(dir, "pacemaker-3.0.rng", "pacemaker-3.2.rng") != 0) {
+ rmdir(dir);
+ free(dir);
+ return -1;
+ }
+
+ setenv("PCMK_remote_schema_directory", remote_schema_dir, 1);
+ setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR, 1);
+
+ /* Do not call crm_schema_init here because that is the function we're
+ * testing. It needs to be called in each unit test. However, we can
+ * call crm_schema_cleanup in teardown().
+ */
+
+ return 0;
+}
+
+static int
+teardown(void **state)
+{
+ int rc = 0;
+ char *f = NULL;
+
+ crm_schema_cleanup();
+ unsetenv("PCMK_remote_schema_directory");
+ unsetenv("PCMK_schema_directory");
+
+ rc = rmtree(remote_schema_dir);
+
+ free(remote_schema_dir);
+ free(f);
+ return rc;
+}
+
+static void
+assert_schema(const char *schema_name, int schema_index)
+{
+ GList *entry = NULL;
+ pcmk__schema_t *schema = NULL;
+
+ entry = pcmk__get_schema(schema_name);
+ assert_non_null(entry);
+
+ schema = entry->data;
+ assert_non_null(schema);
+
+ assert_int_equal(schema_index, schema->schema_index);
+}
+
+static void
+extra_schema_files(void **state)
+{
+ crm_schema_init();
+
+ /* Just iterate through the list of schemas and make sure everything
+ * (including the new schemas we loaded from a second directory) is in
+ * the right order.
+ */
+ assert_schema("pacemaker-1.0", 0);
+ assert_schema("pacemaker-1.2", 1);
+ assert_schema("pacemaker-2.0", 3);
+ assert_schema("pacemaker-3.0", 14);
+ assert_schema("pacemaker-3.1", 15);
+ assert_schema("pacemaker-3.2", 16);
+
+ // @COMPAT pacemaker-next is deprecated since 2.1.5
+ assert_schema("pacemaker-next", 17);
+
+ // @COMPAT none is deprecated since 2.1.8
+ assert_schema(PCMK_VALUE_NONE, 18);
+}
+
+PCMK__UNIT_TEST(setup, teardown,
+ cmocka_unit_test(extra_schema_files));
diff --git a/lib/common/tests/schemas/pcmk__build_schema_xml_node_test.c b/lib/common/tests/schemas/pcmk__build_schema_xml_node_test.c
new file mode 100644
index 0000000..e4454e2
--- /dev/null
+++ b/lib/common/tests/schemas/pcmk__build_schema_xml_node_test.c
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2023-2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/xml.h>
+#include <crm/common/unittest_internal.h>
+#include <crm/common/lists_internal.h>
+
+#include <glib.h>
+
+const char *rngs1[] = { "pacemaker-3.0.rng", "status-1.0.rng", "alerts-2.10.rng",
+ "nvset-2.9.rng", "score.rng", "rule-2.9.rng",
+ "tags-1.3.rng", "acls-2.0.rng", "fencing-2.4.rng",
+ "constraints-3.0.rng", "resources-3.0.rng", "nvset-3.0.rng",
+ "nodes-3.0.rng", "options-3.0.rng", NULL };
+
+const char *rngs2[] = { "pacemaker-2.0.rng", "status-1.0.rng", "tags-1.3.rng",
+ "acls-2.0.rng", "fencing-1.2.rng", "constraints-1.2.rng",
+ "rule.rng", "score.rng", "resources-1.3.rng",
+ "nvset-1.3.rng", "nodes-1.3.rng", "options-1.0.rng",
+ "nvset.rng", "cib-1.2.rng", NULL };
+
+const char *rngs3[] = { "pacemaker-2.1.rng", "constraints-2.1.rng", NULL };
+
+static int
+setup(void **state)
+{
+ setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR, 1);
+ crm_schema_init();
+ pcmk__xml_test_setup_group(state);
+ return 0;
+}
+
+static int
+teardown(void **state)
+{
+ crm_schema_cleanup();
+ unsetenv("PCMK_schema_directory");
+ return 0;
+}
+
+static void
+invalid_name(void **state)
+{
+ GList *already_included = NULL;
+ xmlNode *parent = pcmk__xe_create(NULL, PCMK__XA_SCHEMAS);
+
+ pcmk__build_schema_xml_node(parent, "pacemaker-9.0", &already_included);
+ assert_null(parent->children);
+ assert_null(already_included);
+ free_xml(parent);
+}
+
+static void
+single_schema(void **state)
+{
+ GList *already_included = NULL;
+ xmlNode *parent = pcmk__xe_create(NULL, PCMK__XA_SCHEMAS);
+ xmlNode *schema_node = NULL;
+ xmlNode *file_node = NULL;
+ int i = 0;
+
+ pcmk__build_schema_xml_node(parent, "pacemaker-3.0", &already_included);
+
+ assert_non_null(already_included);
+ assert_non_null(parent->children);
+
+ /* Test that the result looks like this:
+ *
+ * <schemas>
+ * <schema version="pacemaker-3.0">
+ * <file path="pacemaker-3.0.rng">CDATA</file>
+ * <file path="status-1.0.rng">CDATA</file>
+ * ...
+ * </schema>
+ * </schemas>
+ */
+ schema_node = pcmk__xe_first_child(parent, NULL, NULL, NULL);
+ assert_string_equal("pacemaker-3.0",
+ crm_element_value(schema_node, PCMK_XA_VERSION));
+
+ file_node = pcmk__xe_first_child(schema_node, NULL, NULL, NULL);
+ while (file_node != NULL && rngs1[i] != NULL) {
+ assert_string_equal(rngs1[i],
+ crm_element_value(file_node, PCMK_XA_PATH));
+ assert_int_equal(pcmk__xml_first_child(file_node)->type, XML_CDATA_SECTION_NODE);
+
+ file_node = pcmk__xe_next(file_node);
+ i++;
+ }
+
+ g_list_free_full(already_included, free);
+ free_xml(parent);
+}
+
+static void
+multiple_schemas(void **state)
+{
+ GList *already_included = NULL;
+ xmlNode *parent = pcmk__xe_create(NULL, PCMK__XA_SCHEMAS);
+ xmlNode *schema_node = NULL;
+ xmlNode *file_node = NULL;
+ int i = 0;
+
+ pcmk__build_schema_xml_node(parent, "pacemaker-2.0", &already_included);
+ pcmk__build_schema_xml_node(parent, "pacemaker-2.1", &already_included);
+
+ assert_non_null(already_included);
+ assert_non_null(parent->children);
+
+ /* Like single_schema, but make sure files aren't included multiple times
+ * when the function is called repeatedly.
+ */
+ schema_node = pcmk__xe_first_child(parent, NULL, NULL, NULL);
+ assert_string_equal("pacemaker-2.0",
+ crm_element_value(schema_node, PCMK_XA_VERSION));
+
+ file_node = pcmk__xe_first_child(schema_node, NULL, NULL, NULL);
+ while (file_node != NULL && rngs2[i] != NULL) {
+ assert_string_equal(rngs2[i],
+ crm_element_value(file_node, PCMK_XA_PATH));
+ assert_int_equal(pcmk__xml_first_child(file_node)->type, XML_CDATA_SECTION_NODE);
+
+ file_node = pcmk__xe_next(file_node);
+ i++;
+ }
+
+ schema_node = pcmk__xe_next(schema_node);
+ assert_string_equal("pacemaker-2.1",
+ crm_element_value(schema_node, PCMK_XA_VERSION));
+
+ file_node = pcmk__xe_first_child(schema_node, NULL, NULL, NULL);
+ i = 0;
+
+ while (file_node != NULL && rngs3[i] != NULL) {
+ assert_string_equal(rngs3[i],
+ crm_element_value(file_node, PCMK_XA_PATH));
+ assert_int_equal(pcmk__xml_first_child(file_node)->type, XML_CDATA_SECTION_NODE);
+
+ file_node = pcmk__xe_next(file_node);
+ i++;
+ }
+
+ g_list_free_full(already_included, free);
+ free_xml(parent);
+}
+
+PCMK__UNIT_TEST(setup, teardown,
+ cmocka_unit_test(invalid_name),
+ cmocka_unit_test(single_schema),
+ cmocka_unit_test(multiple_schemas))
diff --git a/lib/common/tests/schemas/pcmk__cmp_schemas_by_name_test.c b/lib/common/tests/schemas/pcmk__cmp_schemas_by_name_test.c
new file mode 100644
index 0000000..19ec743
--- /dev/null
+++ b/lib/common/tests/schemas/pcmk__cmp_schemas_by_name_test.c
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/xml.h>
+#include <crm/common/unittest_internal.h>
+#include <crm/common/xml_internal.h>
+#include "crmcommon_private.h"
+
+static int
+setup(void **state)
+{
+ setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR, 1);
+ crm_schema_init();
+ return 0;
+}
+
+static int
+teardown(void **state)
+{
+ crm_schema_cleanup();
+ unsetenv("PCMK_schema_directory");
+ return 0;
+}
+
+// NULL schema name defaults to the "none" schema
+// @COMPAT none is deprecated since 2.1.8
+
+static void
+unknown_is_lesser(void **state)
+{
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-0.1",
+ "pacemaker-0.2") == 0);
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-0.1",
+ "pacemaker-1.0") < 0);
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-1.0",
+ "pacemaker-0.1") > 0);
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-1.1", NULL) < 0);
+ assert_true(pcmk__cmp_schemas_by_name(NULL, "pacemaker-0.0") > 0);
+
+ /* @COMPAT pacemaker-next is deprecated since 2.1.5,
+ * and pacemaker-0.6 and pacemaker-0.7 since 2.1.8
+ */
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-0.6",
+ "pacemaker-next") < 0);
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-next",
+ "pacemaker-0.7") > 0);
+}
+
+// @COMPAT none is deprecated since 2.1.8
+static void
+none_is_greater(void **state)
+{
+ assert_true(pcmk__cmp_schemas_by_name(NULL, NULL) == 0);
+ assert_true(pcmk__cmp_schemas_by_name(NULL, PCMK_VALUE_NONE) == 0);
+ assert_true(pcmk__cmp_schemas_by_name(PCMK_VALUE_NONE, NULL) == 0);
+ assert_true(pcmk__cmp_schemas_by_name(PCMK_VALUE_NONE,
+ PCMK_VALUE_NONE) == 0);
+
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-3.0",
+ PCMK_VALUE_NONE) < 0);
+ assert_true(pcmk__cmp_schemas_by_name(PCMK_VALUE_NONE,
+ "pacemaker-1.0") > 0);
+
+ // @COMPAT pacemaker-next is deprecated since 2.1.5
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-next",
+ PCMK_VALUE_NONE) < 0);
+ assert_true(pcmk__cmp_schemas_by_name(PCMK_VALUE_NONE,
+ "pacemaker-next") > 0);
+}
+
+// @COMPAT pacemaker-next is deprecated since 2.1.5
+// @COMPAT none is deprecated since 2.1.8
+static void
+next_is_before_none(void **state)
+{
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-next",
+ "pacemaker-next") == 0);
+ assert_true(pcmk__cmp_schemas_by_name(NULL, "pacemaker-next") > 0);
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-next", NULL) < 0);
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-3.0",
+ "pacemaker-next") < 0);
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-next",
+ "pacemaker-1.0") > 0);
+}
+
+static void
+known_numeric(void **state)
+{
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-1.0",
+ "pacemaker-1.0") == 0);
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-1.2",
+ "pacemaker-1.0") > 0);
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-1.2",
+ "pacemaker-2.0") < 0);
+}
+
+static void
+case_insensitive(void **state)
+{
+ assert_true(pcmk__cmp_schemas_by_name("Pacemaker-1.0",
+ "pacemaker-1.0") == 0);
+ assert_true(pcmk__cmp_schemas_by_name("PACEMAKER-1.2",
+ "pacemaker-1.0") > 0);
+ assert_true(pcmk__cmp_schemas_by_name("PaceMaker-1.2",
+ "pacemaker-2.0") < 0);
+}
+
+PCMK__UNIT_TEST(setup, teardown,
+ cmocka_unit_test(unknown_is_lesser),
+ cmocka_unit_test(none_is_greater),
+ cmocka_unit_test(next_is_before_none),
+ cmocka_unit_test(known_numeric),
+ cmocka_unit_test(case_insensitive));
diff --git a/lib/common/tests/schemas/pcmk__find_x_0_schema_test.c b/lib/common/tests/schemas/pcmk__find_x_0_schema_test.c
new file mode 100644
index 0000000..25ba0f3
--- /dev/null
+++ b/lib/common/tests/schemas/pcmk__find_x_0_schema_test.c
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2023-2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h> // NULL, rename()
+#include <stdlib.h> // setenv(), unsetenv()
+#include <glib.h>
+
+#include <crm/common/unittest_internal.h>
+#include "crmcommon_private.h"
+
+#define SCHEMA_PREFIX PCMK__TEST_SCHEMA_DIR "/find_x_0/pacemaker-"
+
+static int
+setup(void **state)
+{
+ // Use a unique schema directory so we can move files around
+ setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR "/find_x_0", 1);
+ return 0;
+}
+
+static int
+teardown(void **state)
+{
+ unsetenv("PCMK_schema_directory");
+ return 0;
+}
+
+static void
+assert_schema_0(int schema_index, const char *schema_name)
+{
+ GList *entry = NULL;
+ pcmk__schema_t *schema = NULL;
+
+ entry = pcmk__find_x_0_schema();
+ assert_non_null(entry);
+
+ schema = entry->data;
+ assert_non_null(schema);
+
+ assert_int_equal(schema->schema_index, schema_index);
+ assert_string_equal(schema->name, schema_name);
+}
+
+static void
+last_is_0(void **state)
+{
+ /* This loads all the schemas normally linked for unit testing, so we have
+ * many 1.x and 2.x schemas and a single pacemaker-3.0 schema at index 14.
+ */
+ crm_schema_init();
+ assert_schema_0(14, "pacemaker-3.0");
+ crm_schema_cleanup();
+}
+
+static void
+last_is_not_0(void **state)
+{
+ /* Disable the pacemaker-3.0 schema, so we now should get pacemaker-2.0 at
+ * index 3.
+ */
+ assert_int_equal(0, rename(SCHEMA_PREFIX "3.0.rng",
+ SCHEMA_PREFIX "3.0.bak"));
+ crm_schema_init();
+ assert_schema_0(3, "pacemaker-2.0");
+ assert_int_equal(0, rename(SCHEMA_PREFIX "3.0.bak",
+ SCHEMA_PREFIX "3.0.rng"));
+ crm_schema_cleanup();
+}
+
+static void
+schema_0_missing(void **state)
+{
+ /* Disable the pacemaker-3.0 and pacemaker-2.0 schemas, so we now should get
+ * pacemaker-2.1 at index 3.
+ */
+ assert_int_equal(0, rename(SCHEMA_PREFIX "3.0.rng",
+ SCHEMA_PREFIX "3.0.bak"));
+ assert_int_equal(0, rename(SCHEMA_PREFIX "2.0.rng",
+ SCHEMA_PREFIX "2.0.bak"));
+ crm_schema_init();
+ assert_schema_0(3, "pacemaker-2.1");
+ assert_int_equal(0, rename(SCHEMA_PREFIX "2.0.bak",
+ SCHEMA_PREFIX "2.0.rng"));
+ assert_int_equal(0, rename(SCHEMA_PREFIX "3.0.bak",
+ SCHEMA_PREFIX "3.0.rng"));
+ crm_schema_cleanup();
+}
+
+PCMK__UNIT_TEST(setup, teardown,
+ cmocka_unit_test(last_is_0),
+ cmocka_unit_test(last_is_not_0),
+ cmocka_unit_test(schema_0_missing))
diff --git a/lib/common/tests/schemas/pcmk__get_schema_test.c b/lib/common/tests/schemas/pcmk__get_schema_test.c
new file mode 100644
index 0000000..6513dfc
--- /dev/null
+++ b/lib/common/tests/schemas/pcmk__get_schema_test.c
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2023-2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/xml.h>
+#include <crm/common/unittest_internal.h>
+#include <crm/common/xml_internal.h>
+#include "crmcommon_private.h"
+
+static int
+setup(void **state)
+{
+ setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR, 1);
+ crm_schema_init();
+ return 0;
+}
+
+static int
+teardown(void **state)
+{
+ crm_schema_cleanup();
+ unsetenv("PCMK_schema_directory");
+ return 0;
+}
+
+static void
+assert_schema(const char *name, int expected_index)
+{
+ GList *schema_entry = NULL;
+ pcmk__schema_t *schema = NULL;
+
+ schema_entry = pcmk__get_schema(name);
+ assert_non_null(schema_entry);
+
+ schema = schema_entry->data;
+ assert_non_null(schema);
+
+ assert_int_equal(schema->schema_index, expected_index);
+}
+
+static void
+unknown_schema(void **state)
+{
+ assert_null(pcmk__get_schema(""));
+ assert_null(pcmk__get_schema("blahblah"));
+ assert_null(pcmk__get_schema("pacemaker-2.47"));
+ assert_null(pcmk__get_schema("pacemaker-47.0"));
+}
+
+static void
+known_schema(void **state)
+{
+ // @COMPAT none is deprecated since 2.1.8
+ assert_schema(NULL, 16); // defaults to "none"
+
+ assert_schema("pacemaker-1.0", 0);
+ assert_schema("pacemaker-1.2", 1);
+ assert_schema("pacemaker-2.0", 3);
+ assert_schema("pacemaker-2.5", 8);
+ assert_schema("pacemaker-3.0", 14);
+}
+
+static void
+case_insensitive(void **state)
+{
+ assert_schema("PACEMAKER-1.0", 0);
+ assert_schema("pAcEmAkEr-2.0", 3);
+ assert_schema("paceMAKER-3.0", 14);
+}
+
+PCMK__UNIT_TEST(setup, teardown,
+ cmocka_unit_test(unknown_schema),
+ cmocka_unit_test(known_schema),
+ cmocka_unit_test(case_insensitive));
diff --git a/lib/common/tests/schemas/pcmk__schema_files_later_than_test.c b/lib/common/tests/schemas/pcmk__schema_files_later_than_test.c
new file mode 100644
index 0000000..68744ef
--- /dev/null
+++ b/lib/common/tests/schemas/pcmk__schema_files_later_than_test.c
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2023 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/unittest_internal.h>
+#include <crm/common/lists_internal.h>
+
+#include <glib.h>
+
+static int
+setup(void **state)
+{
+ setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR, 1);
+ crm_schema_init();
+ return 0;
+}
+
+static int
+teardown(void **state)
+{
+ crm_schema_cleanup();
+ unsetenv("PCMK_schema_directory");
+ return 0;
+}
+
+static void
+invalid_name(void **state)
+{
+ assert_null(pcmk__schema_files_later_than("xyz"));
+ assert_null(pcmk__schema_files_later_than("pacemaker-"));
+}
+
+static void
+valid_name(void **state)
+{
+ GList *schemas = NULL;
+
+ schemas = pcmk__schema_files_later_than("pacemaker-1.0");
+ assert_int_equal(g_list_length(schemas), 18);
+ /* There is no "pacemaker-1.1". */
+ assert_string_equal("pacemaker-1.2.rng", g_list_nth_data(schemas, 0));
+ assert_string_equal("upgrade-1.3.xsl", g_list_nth_data(schemas, 1));
+ assert_string_equal("pacemaker-1.3.rng", g_list_nth_data(schemas, 2));
+ assert_string_equal("pacemaker-2.0.rng", g_list_nth_data(schemas, 3));
+ assert_string_equal("pacemaker-2.1.rng", g_list_nth_data(schemas, 4));
+ assert_string_equal("pacemaker-2.2.rng", g_list_nth_data(schemas, 5));
+ assert_string_equal("pacemaker-2.3.rng", g_list_nth_data(schemas, 6));
+ assert_string_equal("pacemaker-2.4.rng", g_list_nth_data(schemas, 7));
+ assert_string_equal("pacemaker-2.5.rng", g_list_nth_data(schemas, 8));
+ assert_string_equal("pacemaker-2.6.rng", g_list_nth_data(schemas, 9));
+ assert_string_equal("pacemaker-2.7.rng", g_list_nth_data(schemas, 10));
+ assert_string_equal("pacemaker-2.8.rng", g_list_nth_data(schemas, 11));
+ assert_string_equal("pacemaker-2.9.rng", g_list_nth_data(schemas, 12));
+ assert_string_equal("upgrade-2.10-leave.xsl", g_list_nth_data(schemas, 13));
+ assert_string_equal("upgrade-2.10-enter.xsl", g_list_nth_data(schemas, 14));
+ assert_string_equal("upgrade-2.10.xsl", g_list_nth_data(schemas, 15));
+ assert_string_equal("pacemaker-2.10.rng", g_list_nth_data(schemas, 16));
+ assert_string_equal("pacemaker-3.0.rng", g_list_nth_data(schemas, 17));
+ g_list_free_full(schemas, free);
+
+ /* Adding .rng to the end of the schema we're requesting is also valid. */
+ schemas = pcmk__schema_files_later_than("pacemaker-2.0.rng");
+ assert_int_equal(g_list_length(schemas), 14);
+ assert_string_equal("pacemaker-2.1.rng", g_list_nth_data(schemas, 0));
+ assert_string_equal("pacemaker-2.2.rng", g_list_nth_data(schemas, 1));
+ assert_string_equal("pacemaker-2.3.rng", g_list_nth_data(schemas, 2));
+ assert_string_equal("pacemaker-2.4.rng", g_list_nth_data(schemas, 3));
+ assert_string_equal("pacemaker-2.5.rng", g_list_nth_data(schemas, 4));
+ assert_string_equal("pacemaker-2.6.rng", g_list_nth_data(schemas, 5));
+ assert_string_equal("pacemaker-2.7.rng", g_list_nth_data(schemas, 6));
+ assert_string_equal("pacemaker-2.8.rng", g_list_nth_data(schemas, 7));
+ assert_string_equal("pacemaker-2.9.rng", g_list_nth_data(schemas, 8));
+ assert_string_equal("upgrade-2.10-leave.xsl", g_list_nth_data(schemas, 9));
+ assert_string_equal("upgrade-2.10-enter.xsl", g_list_nth_data(schemas, 10));
+ assert_string_equal("upgrade-2.10.xsl", g_list_nth_data(schemas, 11));
+ assert_string_equal("pacemaker-2.10.rng", g_list_nth_data(schemas, 12));
+ assert_string_equal("pacemaker-3.0.rng", g_list_nth_data(schemas, 13));
+ g_list_free_full(schemas, free);
+
+ /* Check that "pacemaker-2.10" counts as later than "pacemaker-2.9". */
+ schemas = pcmk__schema_files_later_than("pacemaker-2.9");
+ assert_int_equal(g_list_length(schemas), 5);
+ assert_string_equal("upgrade-2.10-leave.xsl", g_list_nth_data(schemas, 0));
+ assert_string_equal("upgrade-2.10-enter.xsl", g_list_nth_data(schemas, 1));
+ assert_string_equal("upgrade-2.10.xsl", g_list_nth_data(schemas, 2));
+ assert_string_equal("pacemaker-2.10.rng", g_list_nth_data(schemas, 3));
+ assert_string_equal("pacemaker-3.0.rng", g_list_nth_data(schemas, 4));
+ g_list_free_full(schemas, free);
+
+ /* And then something way in the future that will never apply due to our
+ * special schema directory.
+ */
+ schemas = pcmk__schema_files_later_than("pacemaker-9.0");
+ assert_null(schemas);
+}
+
+PCMK__UNIT_TEST(setup, teardown,
+ cmocka_unit_test(invalid_name),
+ cmocka_unit_test(valid_name))
diff --git a/lib/common/tests/scores/char2score_test.c b/lib/common/tests/scores/char2score_test.c
index fbba12a..5d7252f 100644
--- a/lib/common/tests/scores/char2score_test.c
+++ b/lib/common/tests/scores/char2score_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 the Pacemaker project contributors
+ * Copyright 2022-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -32,9 +32,9 @@ bad_input(void **state)
static void
special_values(void **state)
{
- assert_int_equal(char2score("-INFINITY"), -CRM_SCORE_INFINITY);
- assert_int_equal(char2score("INFINITY"), CRM_SCORE_INFINITY);
- assert_int_equal(char2score("+INFINITY"), CRM_SCORE_INFINITY);
+ assert_int_equal(char2score("-INFINITY"), -PCMK_SCORE_INFINITY);
+ assert_int_equal(char2score("INFINITY"), PCMK_SCORE_INFINITY);
+ assert_int_equal(char2score("+INFINITY"), PCMK_SCORE_INFINITY);
pcmk__score_red = 10;
pcmk__score_green = 20;
@@ -56,8 +56,10 @@ special_values(void **state)
static void
outside_limits(void **state)
{
- assert_int_equal(char2score(B(CRM_SCORE_INFINITY) "00"), CRM_SCORE_INFINITY);
- assert_int_equal(char2score("-" B(CRM_SCORE_INFINITY) "00"), -CRM_SCORE_INFINITY);
+ assert_int_equal(char2score(B(PCMK_SCORE_INFINITY) "00"),
+ PCMK_SCORE_INFINITY);
+ assert_int_equal(char2score("-" B(PCMK_SCORE_INFINITY) "00"),
+ -PCMK_SCORE_INFINITY);
}
static void
diff --git a/lib/common/tests/scores/pcmk__add_scores_test.c b/lib/common/tests/scores/pcmk__add_scores_test.c
index 1309659..952cf97 100644
--- a/lib/common/tests/scores/pcmk__add_scores_test.c
+++ b/lib/common/tests/scores/pcmk__add_scores_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2023 the Pacemaker project contributors
+ * Copyright 2022-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -14,48 +14,71 @@
static void
score1_minus_inf(void **state)
{
- assert_int_equal(pcmk__add_scores(-CRM_SCORE_INFINITY, -CRM_SCORE_INFINITY), -CRM_SCORE_INFINITY);
- assert_int_equal(pcmk__add_scores(-CRM_SCORE_INFINITY, -1), -CRM_SCORE_INFINITY);
- assert_int_equal(pcmk__add_scores(-CRM_SCORE_INFINITY, 0), -CRM_SCORE_INFINITY);
- assert_int_equal(pcmk__add_scores(-CRM_SCORE_INFINITY, 1), -CRM_SCORE_INFINITY);
- assert_int_equal(pcmk__add_scores(-CRM_SCORE_INFINITY, CRM_SCORE_INFINITY), -CRM_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(-PCMK_SCORE_INFINITY,
+ -PCMK_SCORE_INFINITY),
+ -PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(-PCMK_SCORE_INFINITY, -1),
+ -PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(-PCMK_SCORE_INFINITY, 0),
+ -PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(-PCMK_SCORE_INFINITY, 1),
+ -PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(-PCMK_SCORE_INFINITY,
+ PCMK_SCORE_INFINITY),
+ -PCMK_SCORE_INFINITY);
}
static void
score2_minus_inf(void **state)
{
- assert_int_equal(pcmk__add_scores(-1, -CRM_SCORE_INFINITY), -CRM_SCORE_INFINITY);
- assert_int_equal(pcmk__add_scores(0, -CRM_SCORE_INFINITY), -CRM_SCORE_INFINITY);
- assert_int_equal(pcmk__add_scores(1, -CRM_SCORE_INFINITY), -CRM_SCORE_INFINITY);
- assert_int_equal(pcmk__add_scores(CRM_SCORE_INFINITY, -CRM_SCORE_INFINITY), -CRM_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(-1, -PCMK_SCORE_INFINITY),
+ -PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(0, -PCMK_SCORE_INFINITY),
+ -PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(1, -PCMK_SCORE_INFINITY),
+ -PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(PCMK_SCORE_INFINITY,
+ -PCMK_SCORE_INFINITY),
+ -PCMK_SCORE_INFINITY);
}
static void
score1_pos_inf(void **state)
{
- assert_int_equal(pcmk__add_scores(CRM_SCORE_INFINITY, CRM_SCORE_INFINITY), CRM_SCORE_INFINITY);
- assert_int_equal(pcmk__add_scores(CRM_SCORE_INFINITY, -1), CRM_SCORE_INFINITY);
- assert_int_equal(pcmk__add_scores(CRM_SCORE_INFINITY, 0), CRM_SCORE_INFINITY);
- assert_int_equal(pcmk__add_scores(CRM_SCORE_INFINITY, 1), CRM_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(PCMK_SCORE_INFINITY, PCMK_SCORE_INFINITY),
+ PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(PCMK_SCORE_INFINITY, -1),
+ PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(PCMK_SCORE_INFINITY, 0),
+ PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(PCMK_SCORE_INFINITY, 1),
+ PCMK_SCORE_INFINITY);
}
static void
score2_pos_inf(void **state)
{
- assert_int_equal(pcmk__add_scores(-1, CRM_SCORE_INFINITY), CRM_SCORE_INFINITY);
- assert_int_equal(pcmk__add_scores(0, CRM_SCORE_INFINITY), CRM_SCORE_INFINITY);
- assert_int_equal(pcmk__add_scores(1, CRM_SCORE_INFINITY), CRM_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(-1, PCMK_SCORE_INFINITY),
+ PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(0, PCMK_SCORE_INFINITY),
+ PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(1, PCMK_SCORE_INFINITY),
+ PCMK_SCORE_INFINITY);
}
static void
result_infinite(void **state)
{
- assert_int_equal(pcmk__add_scores(INT_MAX, INT_MAX), CRM_SCORE_INFINITY);
- assert_int_equal(pcmk__add_scores(INT_MIN, INT_MIN), -CRM_SCORE_INFINITY);
- assert_int_equal(pcmk__add_scores(2000000, 50), CRM_SCORE_INFINITY);
- assert_int_equal(pcmk__add_scores(CRM_SCORE_INFINITY/2, CRM_SCORE_INFINITY/2), CRM_SCORE_INFINITY);
- assert_int_equal(pcmk__add_scores(-CRM_SCORE_INFINITY/2, -CRM_SCORE_INFINITY/2), -CRM_SCORE_INFINITY);
- assert_int_equal(pcmk__add_scores(-4000000, 50), -CRM_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(INT_MAX, INT_MAX), PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(INT_MIN, INT_MIN), -PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(2000000, 50), PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(PCMK_SCORE_INFINITY/2,
+ PCMK_SCORE_INFINITY/2),
+ PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(-PCMK_SCORE_INFINITY/2,
+ -PCMK_SCORE_INFINITY/2),
+ -PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(-4000000, 50), -PCMK_SCORE_INFINITY);
}
static void
diff --git a/lib/common/tests/scores/pcmk_readable_score_test.c b/lib/common/tests/scores/pcmk_readable_score_test.c
index ae24159..c3d66f6 100644
--- a/lib/common/tests/scores/pcmk_readable_score_test.c
+++ b/lib/common/tests/scores/pcmk_readable_score_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 the Pacemaker project contributors
+ * Copyright 2022-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -14,10 +14,10 @@
static void
outside_limits(void **state)
{
- assert_string_equal(pcmk_readable_score(CRM_SCORE_INFINITY * 2),
- CRM_INFINITY_S);
- assert_string_equal(pcmk_readable_score(-CRM_SCORE_INFINITY * 2),
- CRM_MINUS_INFINITY_S);
+ assert_string_equal(pcmk_readable_score(PCMK_SCORE_INFINITY * 2),
+ PCMK_VALUE_INFINITY);
+ assert_string_equal(pcmk_readable_score(-PCMK_SCORE_INFINITY * 2),
+ PCMK_VALUE_MINUS_INFINITY);
}
static void
diff --git a/lib/common/tests/strings/Makefile.am b/lib/common/tests/strings/Makefile.am
index e66af0d..439b6bf 100644
--- a/lib/common/tests/strings/Makefile.am
+++ b/lib/common/tests/strings/Makefile.am
@@ -1,5 +1,5 @@
#
-# Copyright 2020-2023 the Pacemaker project contributors
+# Copyright 2020-2024 the Pacemaker project contributors
#
# The version control history for this file may have further details.
#
@@ -16,7 +16,6 @@ check_PROGRAMS = crm_get_msec_test \
crm_str_to_boolean_test \
pcmk__add_word_test \
pcmk__btoa_test \
- pcmk__char_in_any_str_test \
pcmk__compress_test \
pcmk__ends_with_test \
pcmk__g_strcat_test \
diff --git a/lib/common/tests/strings/crm_get_msec_test.c b/lib/common/tests/strings/crm_get_msec_test.c
index 5da548b..14b87cf 100644
--- a/lib/common/tests/strings/crm_get_msec_test.c
+++ b/lib/common/tests/strings/crm_get_msec_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 the Pacemaker project contributors
+ * Copyright 2021-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -19,6 +19,11 @@ bad_input(void **state) {
assert_int_equal(crm_get_msec("100xs"), PCMK__PARSE_INT_DEFAULT);
assert_int_equal(crm_get_msec(" 100 xs "), PCMK__PARSE_INT_DEFAULT);
assert_int_equal(crm_get_msec("-100ms"), PCMK__PARSE_INT_DEFAULT);
+
+ assert_int_equal(crm_get_msec("3.xs"), PCMK__PARSE_INT_DEFAULT);
+ assert_int_equal(crm_get_msec(" 3. xs "), PCMK__PARSE_INT_DEFAULT);
+ assert_int_equal(crm_get_msec("3.14xs"), PCMK__PARSE_INT_DEFAULT);
+ assert_int_equal(crm_get_msec(" 3.14 xs "), PCMK__PARSE_INT_DEFAULT);
}
static void
@@ -28,6 +33,7 @@ good_input(void **state) {
assert_int_equal(crm_get_msec("\t100\n"), 100000);
assert_int_equal(crm_get_msec("100ms"), 100);
+ assert_int_equal(crm_get_msec(" 100 ms "), 100);
assert_int_equal(crm_get_msec("100 MSEC"), 100);
assert_int_equal(crm_get_msec("1000US"), 1);
assert_int_equal(crm_get_msec("1000usec"), 1);
@@ -37,6 +43,28 @@ good_input(void **state) {
assert_int_equal(crm_get_msec("13 min"), 780000);
assert_int_equal(crm_get_msec("2\th"), 7200000);
assert_int_equal(crm_get_msec("1 hr"), 3600000);
+
+ assert_int_equal(crm_get_msec("3."), 3000);
+ assert_int_equal(crm_get_msec(" 3. ms "), 3);
+ assert_int_equal(crm_get_msec("3.14"), 3000);
+ assert_int_equal(crm_get_msec(" 3.14 ms "), 3);
+
+ // Questionable
+ assert_int_equal(crm_get_msec("3.14."), 3000);
+ assert_int_equal(crm_get_msec(" 3.14. ms "), 3);
+ assert_int_equal(crm_get_msec("3.14.159"), 3000);
+ assert_int_equal(crm_get_msec(" 3.14.159 "), 3000);
+ assert_int_equal(crm_get_msec("3.14.159ms"), 3);
+ assert_int_equal(crm_get_msec(" 3.14.159 ms "), 3);
+
+ // Questionable
+ assert_int_equal(crm_get_msec(" 100 mshr "), 100);
+ assert_int_equal(crm_get_msec(" 100 ms hr "), 100);
+ assert_int_equal(crm_get_msec(" 100 sasdf "), 100000);
+ assert_int_equal(crm_get_msec(" 100 s asdf "), 100000);
+ assert_int_equal(crm_get_msec(" 3.14 shour "), 3000);
+ assert_int_equal(crm_get_msec(" 3.14 s hour "), 3000);
+ assert_int_equal(crm_get_msec(" 3.14 ms!@#$ "), 3);
}
static void
diff --git a/lib/common/tests/strings/crm_str_to_boolean_test.c b/lib/common/tests/strings/crm_str_to_boolean_test.c
index 3bd2e5d..1b0ba45 100644
--- a/lib/common/tests/strings/crm_str_to_boolean_test.c
+++ b/lib/common/tests/strings/crm_str_to_boolean_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 the Pacemaker project contributors
+ * Copyright 2021-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -40,6 +40,13 @@ is_true(void **state) {
assert_true(ret);
assert_int_equal(crm_str_to_boolean("1", &ret), 1);
assert_true(ret);
+
+ // Ensure it still validates the string with a NULL result argument
+ assert_int_equal(crm_str_to_boolean("true", NULL), 1);
+ assert_int_equal(crm_str_to_boolean("on", NULL), 1);
+ assert_int_equal(crm_str_to_boolean("yes", NULL), 1);
+ assert_int_equal(crm_str_to_boolean("y", NULL), 1);
+ assert_int_equal(crm_str_to_boolean("1", NULL), 1);
}
static void
@@ -73,6 +80,13 @@ is_false(void **state) {
assert_false(ret);
assert_int_equal(crm_str_to_boolean("0", &ret), 1);
assert_false(ret);
+
+ // Ensure it still validates the string with a NULL result argument
+ assert_int_equal(crm_str_to_boolean("false", NULL), 1);
+ assert_int_equal(crm_str_to_boolean("off", NULL), 1);
+ assert_int_equal(crm_str_to_boolean("no", NULL), 1);
+ assert_int_equal(crm_str_to_boolean("n", NULL), 1);
+ assert_int_equal(crm_str_to_boolean("0", NULL), 1);
}
static void
diff --git a/lib/common/tests/strings/pcmk__char_in_any_str_test.c b/lib/common/tests/strings/pcmk__char_in_any_str_test.c
deleted file mode 100644
index e70dfb4..0000000
--- a/lib/common/tests/strings/pcmk__char_in_any_str_test.c
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 2020-2021 the Pacemaker project contributors
- *
- * The version control history for this file may have further details.
- *
- * This source code is licensed under the GNU General Public License version 2
- * or later (GPLv2+) WITHOUT ANY WARRANTY.
- */
-
-#include <crm_internal.h>
-
-#include <crm/common/unittest_internal.h>
-
-static void
-empty_list(void **state)
-{
- assert_false(pcmk__char_in_any_str('x', NULL));
- assert_false(pcmk__char_in_any_str('\0', NULL));
-}
-
-static void
-null_char(void **state)
-{
- assert_true(pcmk__char_in_any_str('\0', "xxx", "yyy", NULL));
- assert_true(pcmk__char_in_any_str('\0', "", NULL));
-}
-
-static void
-in_list(void **state)
-{
- assert_true(pcmk__char_in_any_str('x', "aaa", "bbb", "xxx", NULL));
-}
-
-static void
-not_in_list(void **state)
-{
- assert_false(pcmk__char_in_any_str('x', "aaa", "bbb", NULL));
- assert_false(pcmk__char_in_any_str('A', "aaa", "bbb", NULL));
- assert_false(pcmk__char_in_any_str('x', "", NULL));
-}
-
-PCMK__UNIT_TEST(NULL, NULL,
- cmocka_unit_test(empty_list),
- cmocka_unit_test(null_char),
- cmocka_unit_test(in_list),
- cmocka_unit_test(not_in_list))
diff --git a/lib/common/tests/strings/pcmk__compress_test.c b/lib/common/tests/strings/pcmk__compress_test.c
index 7b59d9d..813bcdb 100644
--- a/lib/common/tests/strings/pcmk__compress_test.c
+++ b/lib/common/tests/strings/pcmk__compress_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 the Pacemaker project contributors
+ * Copyright 2022-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -20,7 +20,7 @@ const char *SIMPLE_COMPRESSED = "BZh41AY&SYO\x1ai";
static void
simple_compress(void **state)
{
- char *result = calloc(1024, sizeof(char));
+ char *result = pcmk__assert_alloc(1024, sizeof(char));
unsigned int len;
assert_int_equal(pcmk__compress(SIMPLE_DATA, 40, 0, &result, &len), pcmk_rc_ok);
@@ -30,7 +30,7 @@ simple_compress(void **state)
static void
max_too_small(void **state)
{
- char *result = calloc(1024, sizeof(char));
+ char *result = pcmk__assert_alloc(1024, sizeof(char));
unsigned int len;
assert_int_equal(pcmk__compress(SIMPLE_DATA, 40, 10, &result, &len), EFBIG);
@@ -38,10 +38,11 @@ max_too_small(void **state)
static void
calloc_fails(void **state) {
- char *result = calloc(1024, sizeof(char));
+ char *result = pcmk__assert_alloc(1024, sizeof(char));
unsigned int len;
- pcmk__assert_asserts(
+ pcmk__assert_exits(
+ CRM_EX_OSERR,
{
pcmk__mock_calloc = true; // calloc() will return NULL
expect_value(__wrap_calloc, nmemb, (size_t) ((40 * 1.01) + 601));
diff --git a/lib/common/tests/strings/pcmk__str_update_test.c b/lib/common/tests/strings/pcmk__str_update_test.c
index 571031d..4a44fba 100644
--- a/lib/common/tests/strings/pcmk__str_update_test.c
+++ b/lib/common/tests/strings/pcmk__str_update_test.c
@@ -59,7 +59,8 @@ strdup_fails(void **state) {
str = strdup("hello");
- pcmk__assert_asserts(
+ pcmk__assert_exits(
+ CRM_EX_OSERR,
{
pcmk__mock_strdup = true; // strdup() will return NULL
expect_string(__wrap_strdup, s, "world");
diff --git a/lib/common/tests/utils/Makefile.am b/lib/common/tests/utils/Makefile.am
index f028ce4..fb9d5c3 100644
--- a/lib/common/tests/utils/Makefile.am
+++ b/lib/common/tests/utils/Makefile.am
@@ -1,5 +1,5 @@
#
-# Copyright 2020-2023 the Pacemaker project contributors
+# Copyright 2020-2024 the Pacemaker project contributors
#
# The version control history for this file may have further details.
#
@@ -12,8 +12,6 @@ include $(top_srcdir)/mk/unittest.mk
# Add "_test" to the end of all test program names to simplify .gitignore.
check_PROGRAMS = compare_version_test \
- crm_meta_name_test \
- crm_meta_value_test \
crm_user_lookup_test \
pcmk_daemon_user_test \
pcmk_str_is_infinity_test \
@@ -21,10 +19,7 @@ check_PROGRAMS = compare_version_test \
pcmk__fail_attr_name_test \
pcmk__failcount_name_test \
pcmk__getpid_s_test \
- pcmk__lastfailure_name_test
-
-if WRAPPABLE_UNAME
-check_PROGRAMS += pcmk_hostname_test
-endif
+ pcmk__lastfailure_name_test \
+ pcmk__realloc_test
TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/utils/compare_version_test.c b/lib/common/tests/utils/compare_version_test.c
index 35ebb63..d191f4a 100644
--- a/lib/common/tests/utils/compare_version_test.c
+++ b/lib/common/tests/utils/compare_version_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 the Pacemaker project contributors
+ * Copyright 2022-2023 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -46,6 +46,9 @@ shorter_versions(void **state)
{
assert_int_equal(compare_version("1.0", "1.0.1"), -1);
assert_int_equal(compare_version("1.0.1", "1.0"), 1);
+ assert_int_equal(compare_version("1.0", "1"), 0);
+ assert_int_equal(compare_version("1", "1.2"), -1);
+ assert_int_equal(compare_version("1.2", "1"), 1);
}
PCMK__UNIT_TEST(NULL, NULL,
diff --git a/lib/common/tests/utils/pcmk__realloc_test.c b/lib/common/tests/utils/pcmk__realloc_test.c
new file mode 100644
index 0000000..62d51df
--- /dev/null
+++ b/lib/common/tests/utils/pcmk__realloc_test.c
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/unittest_internal.h>
+
+#include "mock_private.h"
+
+static void
+bad_size(void **state)
+{
+ char *ptr = NULL;
+
+ pcmk__assert_asserts(pcmk__realloc(ptr, 0));
+}
+
+static void
+realloc_fails(void **state)
+{
+ char *ptr = NULL;
+
+ pcmk__assert_aborts(
+ {
+ pcmk__mock_realloc = true; // realloc() will return NULL
+ expect_any(__wrap_realloc, ptr);
+ expect_value(__wrap_realloc, size, 1000);
+ pcmk__realloc(ptr, 1000);
+ pcmk__mock_realloc = false; // Use real realloc()
+ }
+ );
+}
+
+static void
+realloc_succeeds(void **state)
+{
+ char *ptr = NULL;
+
+ /* We can't really test that the resulting pointer is the size we asked
+ * for - it might be larger if that's what the memory allocator decides
+ * to do. And anyway, testing realloc isn't really the point. All we
+ * want to do here is make sure the function works when given good input.
+ */
+
+ /* Allocate new memory */
+ ptr = pcmk__realloc(ptr, 1000);
+ assert_non_null(ptr);
+
+ /* Grow previously allocated memory */
+ ptr = pcmk__realloc(ptr, 2000);
+ assert_non_null(ptr);
+
+ /* Shrink previously allocated memory */
+ ptr = pcmk__realloc(ptr, 500);
+ assert_non_null(ptr);
+
+ free(ptr);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(bad_size),
+ cmocka_unit_test(realloc_fails),
+ cmocka_unit_test(realloc_succeeds))
diff --git a/lib/common/tests/utils/pcmk_hostname_test.c b/lib/common/tests/utils/pcmk_hostname_test.c
deleted file mode 100644
index 7329486..0000000
--- a/lib/common/tests/utils/pcmk_hostname_test.c
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright 2021 the Pacemaker project contributors
- *
- * The version control history for this file may have further details.
- *
- * This source code is licensed under the GNU General Public License version 2
- * or later (GPLv2+) WITHOUT ANY WARRANTY.
- */
-
-#include <crm_internal.h>
-
-#include <crm/common/unittest_internal.h>
-
-#include "mock_private.h"
-
-#include <sys/utsname.h>
-
-static void
-uname_succeeded_test(void **state)
-{
- char *retval;
-
- // Set uname() return value and buf parameter node name
- pcmk__mock_uname = true;
-
- expect_any(__wrap_uname, buf);
- will_return(__wrap_uname, 0);
- will_return(__wrap_uname, "somename");
-
- retval = pcmk_hostname();
- assert_non_null(retval);
- assert_string_equal("somename", retval);
-
- free(retval);
-
- pcmk__mock_uname = false;
-}
-
-static void
-uname_failed_test(void **state)
-{
- // Set uname() return value and buf parameter node name
- pcmk__mock_uname = true;
-
- expect_any(__wrap_uname, buf);
- will_return(__wrap_uname, -1);
- will_return(__wrap_uname, NULL);
-
- assert_null(pcmk_hostname());
-
- pcmk__mock_uname = false;
-}
-
-PCMK__UNIT_TEST(NULL, NULL,
- cmocka_unit_test(uname_succeeded_test),
- cmocka_unit_test(uname_failed_test))
diff --git a/lib/common/tests/xml/Makefile.am b/lib/common/tests/xml/Makefile.am
index 465c950..9ed1620 100644
--- a/lib/common/tests/xml/Makefile.am
+++ b/lib/common/tests/xml/Makefile.am
@@ -1,5 +1,5 @@
#
-# Copyright 2022-2023 the Pacemaker project contributors
+# Copyright 2022-2024 the Pacemaker project contributors
#
# The version control history for this file may have further details.
#
@@ -11,7 +11,12 @@ include $(top_srcdir)/mk/tap.mk
include $(top_srcdir)/mk/unittest.mk
# Add "_test" to the end of all test program names to simplify .gitignore.
-check_PROGRAMS = pcmk__xe_foreach_child_test \
- pcmk__xe_match_test
+check_PROGRAMS = crm_xml_init_test \
+ pcmk__xe_copy_attrs_test \
+ pcmk__xe_first_child_test \
+ pcmk__xe_foreach_child_test \
+ pcmk__xe_set_score_test \
+ pcmk__xml_escape_test \
+ pcmk__xml_needs_escape_test
TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/xml/crm_xml_init_test.c b/lib/common/tests/xml/crm_xml_init_test.c
new file mode 100644
index 0000000..1f78728
--- /dev/null
+++ b/lib/common/tests/xml/crm_xml_init_test.c
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2023-2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/xml.h>
+#include <crm/common/unittest_internal.h>
+#include <crm/common/xml_internal.h>
+
+#include "crmcommon_private.h"
+
+/* Copied from lib/common/xml.c */
+#define XML_DOC_PRIVATE_MAGIC 0x81726354UL
+#define XML_NODE_PRIVATE_MAGIC 0x54637281UL
+
+static int
+setup(void **state) {
+ crm_xml_init();
+ return 0;
+}
+
+static int
+teardown(void **state) {
+ crm_xml_cleanup();
+ return 0;
+}
+
+static void
+buffer_scheme_test(void **state) {
+ assert_int_equal(XML_BUFFER_ALLOC_DOUBLEIT, xmlGetBufferAllocationScheme());
+}
+
+/* These functions also serve as unit tests of the static new_private_data
+ * function. We can't test free_private_data because libxml will call that as
+ * part of freeing everything else. By the time we'd get back into a unit test
+ * where we could check that private members are NULL, the structure containing
+ * the private data would have been freed.
+ *
+ * This could probably be tested with a lot of function mocking, but that
+ * doesn't seem worth it.
+ */
+
+static void
+create_document_node(void **state) {
+ xml_doc_private_t *docpriv = NULL;
+ xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
+
+ /* Double check things */
+ assert_non_null(doc);
+ assert_int_equal(doc->type, XML_DOCUMENT_NODE);
+
+ /* Check that the private data is initialized correctly */
+ docpriv = doc->_private;
+ assert_non_null(docpriv);
+ assert_int_equal(docpriv->check, XML_DOC_PRIVATE_MAGIC);
+ assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty|pcmk__xf_created));
+
+ /* Clean up */
+ xmlFreeDoc(doc);
+}
+
+static void
+create_element_node(void **state) {
+ xml_doc_private_t *docpriv = NULL;
+ xml_node_private_t *priv = NULL;
+ xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
+ xmlNodePtr node = xmlNewDocNode(doc, NULL, (pcmkXmlStr) "test", NULL);
+
+ /* Adding a node to the document marks it as dirty */
+ docpriv = doc->_private;
+ assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
+
+ /* Double check things */
+ assert_non_null(node);
+ assert_int_equal(node->type, XML_ELEMENT_NODE);
+
+ /* Check that the private data is initialized correctly */
+ priv = node->_private;
+ assert_non_null(priv);
+ assert_int_equal(priv->check, XML_NODE_PRIVATE_MAGIC);
+ assert_true(pcmk_all_flags_set(priv->flags, pcmk__xf_dirty|pcmk__xf_created));
+
+ /* Clean up */
+ xmlFreeNode(node);
+ xmlFreeDoc(doc);
+}
+
+static void
+create_attr_node(void **state) {
+ xml_doc_private_t *docpriv = NULL;
+ xml_node_private_t *priv = NULL;
+ xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
+ xmlNodePtr node = xmlNewDocNode(doc, NULL, (pcmkXmlStr) "test", NULL);
+ xmlAttrPtr attr = xmlNewProp(node, (pcmkXmlStr) PCMK_XA_NAME,
+ (pcmkXmlStr) "dummy-value");
+
+ /* Adding a node to the document marks it as dirty */
+ docpriv = doc->_private;
+ assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
+
+ /* Double check things */
+ assert_non_null(attr);
+ assert_int_equal(attr->type, XML_ATTRIBUTE_NODE);
+
+ /* Check that the private data is initialized correctly */
+ priv = attr->_private;
+ assert_non_null(priv);
+ assert_int_equal(priv->check, XML_NODE_PRIVATE_MAGIC);
+ assert_true(pcmk_all_flags_set(priv->flags, pcmk__xf_dirty|pcmk__xf_created));
+
+ /* Clean up */
+ xmlFreeNode(node);
+ xmlFreeDoc(doc);
+}
+
+static void
+create_comment_node(void **state) {
+ xml_doc_private_t *docpriv = NULL;
+ xml_node_private_t *priv = NULL;
+ xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
+ xmlNodePtr node = xmlNewDocComment(doc, (pcmkXmlStr) "blahblah");
+
+ /* Adding a node to the document marks it as dirty */
+ docpriv = doc->_private;
+ assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
+
+ /* Double check things */
+ assert_non_null(node);
+ assert_int_equal(node->type, XML_COMMENT_NODE);
+
+ /* Check that the private data is initialized correctly */
+ priv = node->_private;
+ assert_non_null(priv);
+ assert_int_equal(priv->check, XML_NODE_PRIVATE_MAGIC);
+ assert_true(pcmk_all_flags_set(priv->flags, pcmk__xf_dirty|pcmk__xf_created));
+
+ /* Clean up */
+ xmlFreeNode(node);
+ xmlFreeDoc(doc);
+}
+
+static void
+create_text_node(void **state) {
+ xml_doc_private_t *docpriv = NULL;
+ xml_node_private_t *priv = NULL;
+ xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
+ xmlNodePtr node = xmlNewDocText(doc, (pcmkXmlStr) "blahblah");
+
+ /* Adding a node to the document marks it as dirty */
+ docpriv = doc->_private;
+ assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
+
+ /* Double check things */
+ assert_non_null(node);
+ assert_int_equal(node->type, XML_TEXT_NODE);
+
+ /* Check that no private data was created */
+ priv = node->_private;
+ assert_null(priv);
+
+ /* Clean up */
+ xmlFreeNode(node);
+ xmlFreeDoc(doc);
+}
+
+static void
+create_dtd_node(void **state) {
+ xml_doc_private_t *docpriv = NULL;
+ xml_node_private_t *priv = NULL;
+ xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
+ xmlDtdPtr dtd = xmlNewDtd(doc, (pcmkXmlStr) PCMK_XA_NAME,
+ (pcmkXmlStr) "externalId",
+ (pcmkXmlStr) "systemId");
+
+ /* Adding a node to the document marks it as dirty */
+ docpriv = doc->_private;
+ assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
+
+ /* Double check things */
+ assert_non_null(dtd);
+ assert_int_equal(dtd->type, XML_DTD_NODE);
+
+ /* Check that no private data was created */
+ priv = dtd->_private;
+ assert_null(priv);
+
+ /* Clean up */
+ /* If you call xmlFreeDtd before xmlFreeDoc, you get a segfault */
+ xmlFreeDoc(doc);
+}
+
+static void
+create_cdata_node(void **state) {
+ xml_doc_private_t *docpriv = NULL;
+ xml_node_private_t *priv = NULL;
+ xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
+ xmlNodePtr node = xmlNewCDataBlock(doc, (pcmkXmlStr) "blahblah", 8);
+
+ /* Adding a node to the document marks it as dirty */
+ docpriv = doc->_private;
+ assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
+
+ /* Double check things */
+ assert_non_null(node);
+ assert_int_equal(node->type, XML_CDATA_SECTION_NODE);
+
+ /* Check that no private data was created */
+ priv = node->_private;
+ assert_null(priv);
+
+ /* Clean up */
+ xmlFreeNode(node);
+ xmlFreeDoc(doc);
+}
+
+PCMK__UNIT_TEST(setup, teardown,
+ cmocka_unit_test(buffer_scheme_test),
+ cmocka_unit_test(create_document_node),
+ cmocka_unit_test(create_element_node),
+ cmocka_unit_test(create_attr_node),
+ cmocka_unit_test(create_comment_node),
+ cmocka_unit_test(create_text_node),
+ cmocka_unit_test(create_dtd_node),
+ cmocka_unit_test(create_cdata_node));
diff --git a/lib/common/tests/xml/pcmk__xe_copy_attrs_test.c b/lib/common/tests/xml/pcmk__xe_copy_attrs_test.c
new file mode 100644
index 0000000..146317c
--- /dev/null
+++ b/lib/common/tests/xml/pcmk__xe_copy_attrs_test.c
@@ -0,0 +1,188 @@
+ /*
+ * Copyright 2022-2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/unittest_internal.h>
+
+#include <glib.h>
+
+static void
+null_args(void **state)
+{
+ // This test dumps core via CRM_CHECK()
+ xmlNode *xml = pcmk__xe_create(NULL, "test");
+
+ assert_int_equal(pcmk__xe_copy_attrs(NULL, NULL, pcmk__xaf_none), EINVAL);
+ assert_int_equal(pcmk__xe_copy_attrs(NULL, xml, pcmk__xaf_none), EINVAL);
+ assert_int_equal(pcmk__xe_copy_attrs(xml, NULL, pcmk__xaf_none), EINVAL);
+ assert_ptr_equal(xml->properties, NULL);
+
+ free_xml(xml);
+}
+
+static void
+no_source_attrs(void **state)
+{
+ xmlNode *src = pcmk__xe_create(NULL, "test");
+ xmlNode *target = pcmk__xe_create(NULL, "test");
+
+ // Ensure copying from empty source doesn't create target properties
+ assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_none),
+ pcmk_rc_ok);
+ assert_ptr_equal(target->properties, NULL);
+
+ // Ensure copying from empty source doesn't delete target attributes
+ crm_xml_add(target, "attr", "value");
+ assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_none),
+ pcmk_rc_ok);
+ assert_string_equal(crm_element_value(target, "attr"), "value");
+
+ free_xml(src);
+ free_xml(target);
+}
+
+static void
+copy_one(void **state)
+{
+ xmlNode *src = pcmk__xe_create(NULL, "test");
+ xmlNode *target = pcmk__xe_create(NULL, "test");
+
+ crm_xml_add(src, "attr", "value");
+
+ assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_none),
+ pcmk_rc_ok);
+ assert_string_equal(crm_element_value(src, "attr"),
+ crm_element_value(target, "attr"));
+
+ free_xml(src);
+ free_xml(target);
+}
+
+static void
+copy_multiple(void **state)
+{
+ xmlNode *src = pcmk__xe_create(NULL, "test");
+ xmlNode *target = pcmk__xe_create(NULL, "test");
+
+ pcmk__xe_set_props(src,
+ "attr1", "value1",
+ "attr2", "value2",
+ "attr3", "value3",
+ NULL);
+
+ assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_none),
+ pcmk_rc_ok);
+ assert_string_equal(crm_element_value(src, "attr1"),
+ crm_element_value(target, "attr1"));
+ assert_string_equal(crm_element_value(src, "attr2"),
+ crm_element_value(target, "attr2"));
+ assert_string_equal(crm_element_value(src, "attr3"),
+ crm_element_value(target, "attr3"));
+
+ free_xml(src);
+ free_xml(target);
+}
+
+static void
+overwrite(void **state)
+{
+ xmlNode *src = pcmk__xe_create(NULL, "test");
+ xmlNode *target = pcmk__xe_create(NULL, "test");
+
+ crm_xml_add(src, "attr", "src_value");
+ crm_xml_add(target, "attr", "target_value");
+
+ // Overwrite enabled by default
+ assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_none),
+ pcmk_rc_ok);
+ assert_string_equal(crm_element_value(src, "attr"),
+ crm_element_value(target, "attr"));
+ free_xml(src);
+ free_xml(target);
+}
+
+static void
+no_overwrite(void **state)
+{
+ xmlNode *src = pcmk__xe_create(NULL, "test");
+ xmlNode *target = pcmk__xe_create(NULL, "test");
+
+ crm_xml_add(src, "attr", "src_value");
+ crm_xml_add(target, "attr", "target_value");
+
+ assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_no_overwrite),
+ pcmk_rc_ok);
+ assert_string_not_equal(crm_element_value(src, "attr"),
+ crm_element_value(target, "attr"));
+
+ // no_overwrite doesn't prevent copy if there's no conflict
+ pcmk__xe_remove_attr(target, "attr");
+
+ assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_no_overwrite),
+ pcmk_rc_ok);
+ assert_string_equal(crm_element_value(src, "attr"),
+ crm_element_value(target, "attr"));
+
+ free_xml(src);
+ free_xml(target);
+}
+
+static void
+score_update(void **state)
+{
+ xmlNode *src = pcmk__xe_create(NULL, "test");
+ xmlNode *target = pcmk__xe_create(NULL, "test");
+
+ crm_xml_add(src, "plus_plus_attr", "plus_plus_attr++");
+ crm_xml_add(src, "plus_two_attr", "plus_two_attr+=2");
+ crm_xml_add(target, "plus_plus_attr", "1");
+ crm_xml_add(target, "plus_two_attr", "1");
+
+ assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_score_update),
+ pcmk_rc_ok);
+ assert_string_equal(crm_element_value(target, "plus_plus_attr"), "2");
+ assert_string_equal(crm_element_value(target, "plus_two_attr"), "3");
+
+ free_xml(src);
+ free_xml(target);
+}
+
+static void
+no_score_update(void **state)
+{
+ xmlNode *src = pcmk__xe_create(NULL, "test");
+ xmlNode *target = pcmk__xe_create(NULL, "test");
+
+ crm_xml_add(src, "plus_plus_attr", "plus_plus_attr++");
+ crm_xml_add(src, "plus_two_attr", "plus_two_attr+=2");
+ crm_xml_add(target, "plus_plus_attr", "1");
+ crm_xml_add(target, "plus_two_attr", "1");
+
+ // Score update disabled by default
+ assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_none),
+ pcmk_rc_ok);
+ assert_string_equal(crm_element_value(target, "plus_plus_attr"),
+ "plus_plus_attr++");
+ assert_string_equal(crm_element_value(target, "plus_two_attr"),
+ "plus_two_attr+=2");
+
+ free_xml(src);
+ free_xml(target);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_args),
+ cmocka_unit_test(no_source_attrs),
+ cmocka_unit_test(copy_one),
+ cmocka_unit_test(copy_multiple),
+ cmocka_unit_test(overwrite),
+ cmocka_unit_test(no_overwrite),
+ cmocka_unit_test(score_update),
+ cmocka_unit_test(no_score_update));
diff --git a/lib/common/tests/xml/pcmk__xe_match_test.c b/lib/common/tests/xml/pcmk__xe_first_child_test.c
index be2c949..64b90b0 100644
--- a/lib/common/tests/xml/pcmk__xe_match_test.c
+++ b/lib/common/tests/xml/pcmk__xe_first_child_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 the Pacemaker project contributors
+ * Copyright 2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -9,97 +9,97 @@
#include <crm_internal.h>
-#include <crm/msg_xml.h>
+#include <crm/common/xml.h>
#include <crm/common/unittest_internal.h>
#include <crm/common/xml_internal.h>
const char *str1 =
"<xml>\n"
" <!-- This is an A node -->\n"
- " <nodeA attrA=\"123\" " XML_ATTR_ID "=\"1\">\n"
+ " <nodeA attrA=\"123\" " PCMK_XA_ID "=\"1\">\n"
" content\n"
" </nodeA>\n"
" <!-- This is an A node -->\n"
- " <nodeA attrA=\"456\" " XML_ATTR_ID "=\"2\">\n"
+ " <nodeA attrA=\"456\" " PCMK_XA_ID "=\"2\">\n"
" content\n"
" </nodeA>\n"
" <!-- This is an A node -->\n"
- " <nodeA attrB=\"XYZ\" " XML_ATTR_ID "=\"3\">\n"
+ " <nodeA attrB=\"XYZ\" " PCMK_XA_ID "=\"3\">\n"
" content\n"
" </nodeA>\n"
" <!-- This is a B node -->\n"
- " <nodeB attrA=\"123\" " XML_ATTR_ID "=\"4\">\n"
+ " <nodeB attrA=\"123\" " PCMK_XA_ID "=\"4\">\n"
" content\n"
" </nodeA>\n"
" <!-- This is a B node -->\n"
- " <nodeB attrB=\"ABC\" " XML_ATTR_ID "=\"5\">\n"
+ " <nodeB attrB=\"ABC\" " PCMK_XA_ID "=\"5\">\n"
" content\n"
" </nodeA>\n"
"</xml>";
static void
bad_input(void **state) {
- xmlNode *xml = string2xml(str1);
+ xmlNode *xml = pcmk__xml_parse(str1);
- assert_null(pcmk__xe_match(NULL, NULL, NULL, NULL));
- assert_null(pcmk__xe_match(NULL, NULL, NULL, "attrX"));
+ assert_null(pcmk__xe_first_child(NULL, NULL, NULL, NULL));
+ assert_null(pcmk__xe_first_child(NULL, NULL, NULL, "attrX"));
free_xml(xml);
}
static void
not_found(void **state) {
- xmlNode *xml = string2xml(str1);
+ xmlNode *xml = pcmk__xml_parse(str1);
/* No node with an attrX attribute */
- assert_null(pcmk__xe_match(xml, NULL, "attrX", NULL));
+ assert_null(pcmk__xe_first_child(xml, NULL, "attrX", NULL));
/* No nodeX node */
- assert_null(pcmk__xe_match(xml, "nodeX", NULL, NULL));
+ assert_null(pcmk__xe_first_child(xml, "nodeX", NULL, NULL));
/* No nodeA node with attrX */
- assert_null(pcmk__xe_match(xml, "nodeA", "attrX", NULL));
+ assert_null(pcmk__xe_first_child(xml, "nodeA", "attrX", NULL));
/* No nodeA node with attrA=XYZ */
- assert_null(pcmk__xe_match(xml, "nodeA", "attrA", "XYZ"));
+ assert_null(pcmk__xe_first_child(xml, "nodeA", "attrA", "XYZ"));
free_xml(xml);
}
static void
find_attrB(void **state) {
- xmlNode *xml = string2xml(str1);
+ xmlNode *xml = pcmk__xml_parse(str1);
xmlNode *result = NULL;
/* Find the first node with attrB */
- result = pcmk__xe_match(xml, NULL, "attrB", NULL);
+ result = pcmk__xe_first_child(xml, NULL, "attrB", NULL);
assert_non_null(result);
- assert_string_equal(crm_element_value(result, "id"), "3");
+ assert_string_equal(crm_element_value(result, PCMK_XA_ID), "3");
/* Find the first nodeB with attrB */
- result = pcmk__xe_match(xml, "nodeB", "attrB", NULL);
+ result = pcmk__xe_first_child(xml, "nodeB", "attrB", NULL);
assert_non_null(result);
- assert_string_equal(crm_element_value(result, "id"), "5");
+ assert_string_equal(crm_element_value(result, PCMK_XA_ID), "5");
free_xml(xml);
}
static void
find_attrA_matching(void **state) {
- xmlNode *xml = string2xml(str1);
+ xmlNode *xml = pcmk__xml_parse(str1);
xmlNode *result = NULL;
/* Find attrA=456 */
- result = pcmk__xe_match(xml, NULL, "attrA", "456");
+ result = pcmk__xe_first_child(xml, NULL, "attrA", "456");
assert_non_null(result);
- assert_string_equal(crm_element_value(result, "id"), "2");
+ assert_string_equal(crm_element_value(result, PCMK_XA_ID), "2");
/* Find a nodeB with attrA=123 */
- result = pcmk__xe_match(xml, "nodeB", "attrA", "123");
+ result = pcmk__xe_first_child(xml, "nodeB", "attrA", "123");
assert_non_null(result);
- assert_string_equal(crm_element_value(result, "id"), "4");
+ assert_string_equal(crm_element_value(result, PCMK_XA_ID), "4");
free_xml(xml);
}
-PCMK__UNIT_TEST(NULL, NULL,
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
cmocka_unit_test(bad_input),
cmocka_unit_test(not_found),
cmocka_unit_test(find_attrB),
diff --git a/lib/common/tests/xml/pcmk__xe_foreach_child_test.c b/lib/common/tests/xml/pcmk__xe_foreach_child_test.c
index ffb9171..a833dde 100644
--- a/lib/common/tests/xml/pcmk__xe_foreach_child_test.c
+++ b/lib/common/tests/xml/pcmk__xe_foreach_child_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2023 the Pacemaker project contributors
+ * Copyright 2022-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -36,7 +36,7 @@ const char *str1 =
static void
bad_input(void **state) {
- xmlNode *xml = string2xml(str1);
+ xmlNode *xml = pcmk__xml_parse(str1);
pcmk__assert_asserts(pcmk__xe_foreach_child(xml, NULL, NULL, NULL));
@@ -45,7 +45,7 @@ bad_input(void **state) {
static void
name_given_test(void **state) {
- xmlNode *xml = string2xml(str1);
+ xmlNode *xml = pcmk__xml_parse(str1);
/* The handler should be called once for every <level1> node. */
expect_function_call(compare_name_handler);
@@ -58,7 +58,7 @@ name_given_test(void **state) {
static void
no_name_given_test(void **state) {
- xmlNode *xml = string2xml(str1);
+ xmlNode *xml = pcmk__xml_parse(str1);
/* The handler should be called once for every <level1> node. */
expect_function_call(compare_name_handler);
@@ -71,7 +71,7 @@ no_name_given_test(void **state) {
static void
name_doesnt_exist_test(void **state) {
- xmlNode *xml = string2xml(str1);
+ xmlNode *xml = pcmk__xml_parse(str1);
pcmk__xe_foreach_child(xml, "xxx", compare_name_handler, NULL);
free_xml(xml);
}
@@ -100,7 +100,7 @@ const char *str2 =
static void
multiple_levels_test(void **state) {
- xmlNode *xml = string2xml(str2);
+ xmlNode *xml = pcmk__xml_parse(str2);
/* The handler should be called once for every <level1> node. */
expect_function_call(compare_name_handler);
@@ -112,7 +112,7 @@ multiple_levels_test(void **state) {
static void
multiple_levels_no_name_test(void **state) {
- xmlNode *xml = string2xml(str2);
+ xmlNode *xml = pcmk__xml_parse(str2);
/* The handler should be called once for every <level1> node. */
expect_function_call(compare_name_handler);
@@ -147,7 +147,7 @@ static int any_of_handler(xmlNode *xml, void *userdata) {
static void
any_of_test(void **state) {
- xmlNode *xml = string2xml(str3);
+ xmlNode *xml = pcmk__xml_parse(str3);
/* The handler should be called once for every <nodeX> node. */
expect_function_call(any_of_handler);
@@ -190,7 +190,7 @@ static int stops_on_third_handler(xmlNode *xml, void *userdata) {
static void
one_of_test(void **state) {
- xmlNode *xml = string2xml(str3);
+ xmlNode *xml = pcmk__xml_parse(str3);
/* The handler should be called once. */
expect_function_call(stops_on_first_handler);
@@ -205,7 +205,7 @@ one_of_test(void **state) {
free_xml(xml);
}
-PCMK__UNIT_TEST(NULL, NULL,
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
cmocka_unit_test(bad_input),
cmocka_unit_test(name_given_test),
cmocka_unit_test(no_name_given_test),
diff --git a/lib/common/tests/xml/pcmk__xe_set_score_test.c b/lib/common/tests/xml/pcmk__xe_set_score_test.c
new file mode 100644
index 0000000..deb85b0
--- /dev/null
+++ b/lib/common/tests/xml/pcmk__xe_set_score_test.c
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2022-2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/unittest_internal.h>
+
+#include <glib.h>
+
+#include "crmcommon_private.h" // pcmk__xe_set_score()
+
+/*!
+ * \internal
+ * \brief Update an XML attribute value and check it against a reference value
+ *
+ * The attribute name is hard-coded as \c "X".
+ *
+ * \param[in] initial Initial value
+ * \param[in] new Value to set
+ * \param[in] reference_val Expected attribute value after update
+ * \param[in] reference_rc Expected return code from \c pcmk__xe_set_score()
+ */
+static void
+assert_set_score(const char *initial, const char *new,
+ const char *reference_val, int reference_rc)
+{
+ const char *name = "X";
+ xmlNode *test_xml = pcmk__xe_create(NULL, "test_xml");
+
+ crm_xml_add(test_xml, name, initial);
+ assert_int_equal(pcmk__xe_set_score(test_xml, name, new), reference_rc);
+ assert_string_equal(crm_element_value(test_xml, name), reference_val);
+
+ free_xml(test_xml);
+}
+
+static void
+value_is_name_plus_plus(void **state)
+{
+ assert_set_score("5", "X++", "6", pcmk_rc_ok);
+}
+
+static void
+value_is_name_plus_equals_integer(void **state)
+{
+ assert_set_score("5", "X+=2", "7", pcmk_rc_ok);
+}
+
+// NULL input
+
+static void
+target_is_NULL(void **state)
+{
+ // Dumps core via CRM_CHECK()
+ assert_int_equal(pcmk__xe_set_score(NULL, "X", "X++"), EINVAL);
+}
+
+static void
+name_is_NULL(void **state)
+{
+ xmlNode *test_xml = pcmk__xe_create(NULL, "test_xml");
+
+ crm_xml_add(test_xml, "X", "5");
+
+ // Dumps core via CRM_CHECK()
+ assert_int_equal(pcmk__xe_set_score(test_xml, NULL, "X++"), EINVAL);
+ assert_string_equal(crm_element_value(test_xml, "X"), "5");
+
+ free_xml(test_xml);
+}
+
+static void
+value_is_NULL(void **state)
+{
+ assert_set_score("5", NULL, "5", pcmk_rc_ok);
+}
+
+// the value input doesn't start with the name input
+
+static void
+value_is_wrong_name(void **state)
+{
+ assert_set_score("5", "Y++", "Y++", pcmk_rc_ok);
+}
+
+static void
+value_is_only_an_integer(void **state)
+{
+ assert_set_score("5", "2", "2", pcmk_rc_ok);
+}
+
+// non-integers
+
+static void
+variable_is_initialized_to_be_non_numeric(void **state)
+{
+ assert_set_score("hello", "X++", "1", pcmk_rc_ok);
+}
+
+static void
+variable_is_initialized_to_be_non_numeric_2(void **state)
+{
+ assert_set_score("hello", "X+=2", "2", pcmk_rc_ok);
+}
+
+static void
+variable_is_initialized_to_be_numeric_and_decimal_point_containing(void **state)
+{
+ assert_set_score("5.01", "X++", "6", pcmk_rc_ok);
+}
+
+static void
+variable_is_initialized_to_be_numeric_and_decimal_point_containing_2(void **state)
+{
+ assert_set_score("5.50", "X++", "6", pcmk_rc_ok);
+}
+
+static void
+variable_is_initialized_to_be_numeric_and_decimal_point_containing_3(void **state)
+{
+ assert_set_score("5.99", "X++", "6", pcmk_rc_ok);
+}
+
+static void
+value_is_non_numeric(void **state)
+{
+ assert_set_score("5", "X+=hello", "5", pcmk_rc_ok);
+}
+
+static void
+value_is_numeric_and_decimal_point_containing(void **state)
+{
+ assert_set_score("5", "X+=2.01", "7", pcmk_rc_ok);
+}
+
+static void
+value_is_numeric_and_decimal_point_containing_2(void **state)
+{
+ assert_set_score("5", "X+=1.50", "6", pcmk_rc_ok);
+}
+
+static void
+value_is_numeric_and_decimal_point_containing_3(void **state)
+{
+ assert_set_score("5", "X+=1.99", "6", pcmk_rc_ok);
+}
+
+// undefined input
+
+static void
+name_is_undefined(void **state)
+{
+ assert_set_score(NULL, "X++", "X++", pcmk_rc_ok);
+}
+
+// large input
+
+static void
+assignment_result_is_too_large(void **state)
+{
+ assert_set_score("5", "X+=100000000000", "1000000", pcmk_rc_ok);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(value_is_name_plus_plus),
+ cmocka_unit_test(value_is_name_plus_equals_integer),
+ cmocka_unit_test(target_is_NULL),
+ cmocka_unit_test(name_is_NULL),
+ cmocka_unit_test(value_is_NULL),
+ cmocka_unit_test(value_is_wrong_name),
+ cmocka_unit_test(value_is_only_an_integer),
+ cmocka_unit_test(variable_is_initialized_to_be_non_numeric),
+ cmocka_unit_test(variable_is_initialized_to_be_non_numeric_2),
+ cmocka_unit_test(variable_is_initialized_to_be_numeric_and_decimal_point_containing),
+ cmocka_unit_test(variable_is_initialized_to_be_numeric_and_decimal_point_containing_2),
+ cmocka_unit_test(variable_is_initialized_to_be_numeric_and_decimal_point_containing_3),
+ cmocka_unit_test(value_is_non_numeric),
+ cmocka_unit_test(value_is_numeric_and_decimal_point_containing),
+ cmocka_unit_test(value_is_numeric_and_decimal_point_containing_2),
+ cmocka_unit_test(value_is_numeric_and_decimal_point_containing_3),
+ cmocka_unit_test(name_is_undefined),
+ cmocka_unit_test(assignment_result_is_too_large))
diff --git a/lib/common/tests/xml/pcmk__xml_escape_test.c b/lib/common/tests/xml/pcmk__xml_escape_test.c
new file mode 100644
index 0000000..8c6fd21
--- /dev/null
+++ b/lib/common/tests/xml/pcmk__xml_escape_test.c
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/unittest_internal.h>
+#include <crm/common/xml_internal.h>
+
+#include "crmcommon_private.h"
+
+static void
+assert_escape(const char *str, const char *reference,
+ enum pcmk__xml_escape_type type)
+{
+ gchar *buf = pcmk__xml_escape(str, type);
+
+ assert_string_equal(buf, reference);
+ g_free(buf);
+}
+
+static void
+null_empty(void **state)
+{
+ assert_null(pcmk__xml_escape(NULL, pcmk__xml_escape_text));
+ assert_null(pcmk__xml_escape(NULL, pcmk__xml_escape_attr));
+ assert_null(pcmk__xml_escape(NULL, pcmk__xml_escape_attr_pretty));
+
+ assert_escape("", "", pcmk__xml_escape_text);
+ assert_escape("", "", pcmk__xml_escape_attr);
+ assert_escape("", "", pcmk__xml_escape_attr_pretty);
+}
+
+static void
+invalid_type(void **state)
+{
+ const enum pcmk__xml_escape_type type = (enum pcmk__xml_escape_type) -1;
+
+ // Easier to ignore invalid type for NULL or empty string
+ assert_null(pcmk__xml_escape(NULL, type));
+ assert_escape("", "", type);
+
+ // Otherwise, assert if we somehow passed an invalid type
+ pcmk__assert_asserts(pcmk__xml_escape("he<>llo", type));
+}
+
+static void
+escape_unchanged(void **state)
+{
+ // No escaped characters (note: this string includes single quote at end)
+ const char *unchanged = "abcdefghijklmnopqrstuvwxyz"
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "0123456789"
+ "`~!@#$%^*()-_=+/|\\[]{}?.,'";
+
+ assert_escape(unchanged, unchanged, pcmk__xml_escape_text);
+ assert_escape(unchanged, unchanged, pcmk__xml_escape_attr);
+ assert_escape(unchanged, unchanged, pcmk__xml_escape_attr_pretty);
+}
+
+// Ensure special characters get escaped at start, middle, and end
+
+static void
+escape_left_angle(void **state)
+{
+ const char *l_angle = "<abc<def<";
+ const char *l_angle_esc = PCMK__XML_ENTITY_LT "abc"
+ PCMK__XML_ENTITY_LT "def" PCMK__XML_ENTITY_LT;
+
+ assert_escape(l_angle, l_angle_esc, pcmk__xml_escape_text);
+ assert_escape(l_angle, l_angle_esc, pcmk__xml_escape_attr);
+ assert_escape(l_angle, l_angle, pcmk__xml_escape_attr_pretty);
+}
+
+static void
+escape_right_angle(void **state)
+{
+ const char *r_angle = ">abc>def>";
+ const char *r_angle_esc = PCMK__XML_ENTITY_GT "abc"
+ PCMK__XML_ENTITY_GT "def" PCMK__XML_ENTITY_GT;
+
+ assert_escape(r_angle, r_angle_esc, pcmk__xml_escape_text);
+ assert_escape(r_angle, r_angle_esc, pcmk__xml_escape_attr);
+ assert_escape(r_angle, r_angle, pcmk__xml_escape_attr_pretty);
+}
+
+static void
+escape_ampersand(void **state)
+{
+ const char *ampersand = "&abc&def&";
+ const char *ampersand_esc = PCMK__XML_ENTITY_AMP "abc"
+ PCMK__XML_ENTITY_AMP "def" PCMK__XML_ENTITY_AMP;
+
+ assert_escape(ampersand, ampersand_esc, pcmk__xml_escape_text);
+ assert_escape(ampersand, ampersand_esc, pcmk__xml_escape_attr);
+ assert_escape(ampersand, ampersand, pcmk__xml_escape_attr_pretty);
+}
+
+static void
+escape_double_quote(void **state)
+{
+ const char *double_quote = "\"abc\"def\"";
+ const char *double_quote_esc_ref = PCMK__XML_ENTITY_QUOT "abc"
+ PCMK__XML_ENTITY_QUOT "def"
+ PCMK__XML_ENTITY_QUOT;
+ const char *double_quote_esc_backslash = "\\\"abc\\\"def\\\"";
+
+ assert_escape(double_quote, double_quote, pcmk__xml_escape_text);
+ assert_escape(double_quote, double_quote_esc_ref, pcmk__xml_escape_attr);
+ assert_escape(double_quote, double_quote_esc_backslash,
+ pcmk__xml_escape_attr_pretty);
+}
+
+static void
+escape_newline(void **state)
+{
+ const char *newline = "\nabc\ndef\n";
+ const char *newline_esc_ref = "&#x0A;abc&#x0A;def&#x0A;";
+ const char *newline_esc_backslash = "\\nabc\\ndef\\n";
+
+ assert_escape(newline, newline, pcmk__xml_escape_text);
+ assert_escape(newline, newline_esc_ref, pcmk__xml_escape_attr);
+ assert_escape(newline, newline_esc_backslash, pcmk__xml_escape_attr_pretty);
+}
+
+static void
+escape_tab(void **state)
+{
+ const char *tab = "\tabc\tdef\t";
+ const char *tab_esc_ref = "&#x09;abc&#x09;def&#x09;";
+ const char *tab_esc_backslash = "\\tabc\\tdef\\t";
+
+ assert_escape(tab, tab, pcmk__xml_escape_text);
+ assert_escape(tab, tab_esc_ref, pcmk__xml_escape_attr);
+ assert_escape(tab, tab_esc_backslash, pcmk__xml_escape_attr_pretty);
+}
+
+static void
+escape_carriage_return(void **state)
+{
+ const char *cr = "\rabc\rdef\r";
+ const char *cr_esc_ref = "&#x0D;abc&#x0D;def&#x0D;";
+ const char *cr_esc_backslash = "\\rabc\\rdef\\r";
+
+ assert_escape(cr, cr_esc_ref, pcmk__xml_escape_text);
+ assert_escape(cr, cr_esc_ref, pcmk__xml_escape_attr);
+ assert_escape(cr, cr_esc_backslash, pcmk__xml_escape_attr_pretty);
+}
+
+static void
+escape_nonprinting(void **state)
+{
+ const char *nonprinting = "\a\x7F\x1B";
+ const char *nonprinting_esc = "&#x07;&#x7F;&#x1B;";
+
+ assert_escape(nonprinting, nonprinting_esc, pcmk__xml_escape_text);
+ assert_escape(nonprinting, nonprinting_esc, pcmk__xml_escape_attr);
+ assert_escape(nonprinting, nonprinting, pcmk__xml_escape_attr_pretty);
+}
+
+static void
+escape_utf8(void **state)
+{
+ /* Non-ASCII UTF-8 characters may be two, three, or four 8-bit bytes wide
+ * and should not be escaped.
+ */
+ const char *chinese = "仅高级使用";
+ const char *two_byte = "abc""\xCF\xA6""d<ef";
+ const char *two_byte_esc = "abc""\xCF\xA6""d" PCMK__XML_ENTITY_LT "ef";
+
+ const char *three_byte = "abc""\xEF\x98\x98""d<ef";
+ const char *three_byte_esc = "abc""\xEF\x98\x98""d"
+ PCMK__XML_ENTITY_LT "ef";
+
+ const char *four_byte = "abc""\xF0\x94\x81\x90""d<ef";
+ const char *four_byte_esc = "abc""\xF0\x94\x81\x90""d"
+ PCMK__XML_ENTITY_LT "ef";
+
+ assert_escape(chinese, chinese, pcmk__xml_escape_text);
+ assert_escape(chinese, chinese, pcmk__xml_escape_attr);
+ assert_escape(chinese, chinese, pcmk__xml_escape_attr_pretty);
+
+ assert_escape(two_byte, two_byte_esc, pcmk__xml_escape_text);
+ assert_escape(two_byte, two_byte_esc, pcmk__xml_escape_attr);
+ assert_escape(two_byte, two_byte, pcmk__xml_escape_attr_pretty);
+
+ assert_escape(three_byte, three_byte_esc, pcmk__xml_escape_text);
+ assert_escape(three_byte, three_byte_esc, pcmk__xml_escape_attr);
+ assert_escape(three_byte, three_byte, pcmk__xml_escape_attr_pretty);
+
+ assert_escape(four_byte, four_byte_esc, pcmk__xml_escape_text);
+ assert_escape(four_byte, four_byte_esc, pcmk__xml_escape_attr);
+ assert_escape(four_byte, four_byte, pcmk__xml_escape_attr_pretty);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_empty),
+ cmocka_unit_test(invalid_type),
+ cmocka_unit_test(escape_unchanged),
+ cmocka_unit_test(escape_left_angle),
+ cmocka_unit_test(escape_right_angle),
+ cmocka_unit_test(escape_ampersand),
+ cmocka_unit_test(escape_double_quote),
+ cmocka_unit_test(escape_newline),
+ cmocka_unit_test(escape_tab),
+ cmocka_unit_test(escape_carriage_return),
+ cmocka_unit_test(escape_nonprinting),
+ cmocka_unit_test(escape_utf8));
diff --git a/lib/common/tests/xml/pcmk__xml_needs_escape_test.c b/lib/common/tests/xml/pcmk__xml_needs_escape_test.c
new file mode 100644
index 0000000..612f61b
--- /dev/null
+++ b/lib/common/tests/xml/pcmk__xml_needs_escape_test.c
@@ -0,0 +1,337 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/unittest_internal.h>
+#include <crm/common/xml_internal.h>
+
+#include "crmcommon_private.h"
+
+static void
+null_empty(void **state)
+{
+ assert_false(pcmk__xml_needs_escape(NULL, pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape(NULL, pcmk__xml_escape_attr));
+ assert_false(pcmk__xml_needs_escape(NULL, pcmk__xml_escape_attr_pretty));
+
+ assert_false(pcmk__xml_needs_escape("", pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape("", pcmk__xml_escape_attr));
+ assert_false(pcmk__xml_needs_escape("", pcmk__xml_escape_attr_pretty));
+}
+
+static void
+invalid_type(void **state)
+{
+ const enum pcmk__xml_escape_type type = (enum pcmk__xml_escape_type) -1;
+
+ // Easier to ignore invalid type for NULL or empty string
+ assert_false(pcmk__xml_needs_escape(NULL, type));
+ assert_false(pcmk__xml_needs_escape("", type));
+
+ // Otherwise, assert if we somehow passed an invalid type
+ pcmk__assert_asserts(pcmk__xml_needs_escape("he<>llo", type));
+}
+
+static void
+escape_unchanged(void **state)
+{
+ // No escaped characters (note: this string includes single quote at end)
+ const char *unchanged = "abcdefghijklmnopqrstuvwxyz"
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "0123456789"
+ "`~!@#$%^*()-_=+/|\\[]{}?.,'";
+
+ assert_false(pcmk__xml_needs_escape(unchanged, pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape(unchanged, pcmk__xml_escape_attr));
+ assert_false(pcmk__xml_needs_escape(unchanged,
+ pcmk__xml_escape_attr_pretty));
+}
+
+// Ensure special characters get escaped at start, middle, and end
+
+static void
+escape_left_angle(void **state)
+{
+ const char *l_angle_left = "<abcdef";
+ const char *l_angle_mid = "abc<def";
+ const char *l_angle_right = "abcdef<";
+
+ assert_true(pcmk__xml_needs_escape(l_angle_left, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(l_angle_mid, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(l_angle_right, pcmk__xml_escape_text));
+
+ assert_true(pcmk__xml_needs_escape(l_angle_left, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(l_angle_mid, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(l_angle_right, pcmk__xml_escape_attr));
+
+ assert_false(pcmk__xml_needs_escape(l_angle_left,
+ pcmk__xml_escape_attr_pretty));
+ assert_false(pcmk__xml_needs_escape(l_angle_mid,
+ pcmk__xml_escape_attr_pretty));
+ assert_false(pcmk__xml_needs_escape(l_angle_right,
+ pcmk__xml_escape_attr_pretty));
+}
+
+static void
+escape_right_angle(void **state)
+{
+ const char *r_angle_left = ">abcdef";
+ const char *r_angle_mid = "abc>def";
+ const char *r_angle_right = "abcdef>";
+
+ assert_true(pcmk__xml_needs_escape(r_angle_left, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(r_angle_mid, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(r_angle_right, pcmk__xml_escape_text));
+
+ assert_true(pcmk__xml_needs_escape(r_angle_left, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(r_angle_mid, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(r_angle_right, pcmk__xml_escape_attr));
+
+ assert_false(pcmk__xml_needs_escape(r_angle_left,
+ pcmk__xml_escape_attr_pretty));
+ assert_false(pcmk__xml_needs_escape(r_angle_mid,
+ pcmk__xml_escape_attr_pretty));
+ assert_false(pcmk__xml_needs_escape(r_angle_right,
+ pcmk__xml_escape_attr_pretty));
+}
+
+static void
+escape_ampersand(void **state)
+{
+ const char *ampersand_left = "&abcdef";
+ const char *ampersand_mid = "abc&def";
+ const char *ampersand_right = "abcdef&";
+
+ assert_true(pcmk__xml_needs_escape(ampersand_left, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(ampersand_mid, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(ampersand_right, pcmk__xml_escape_text));
+
+ assert_true(pcmk__xml_needs_escape(ampersand_left, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(ampersand_mid, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(ampersand_right, pcmk__xml_escape_attr));
+
+ assert_false(pcmk__xml_needs_escape(ampersand_left,
+ pcmk__xml_escape_attr_pretty));
+ assert_false(pcmk__xml_needs_escape(ampersand_mid,
+ pcmk__xml_escape_attr_pretty));
+ assert_false(pcmk__xml_needs_escape(ampersand_right,
+ pcmk__xml_escape_attr_pretty));
+}
+
+static void
+escape_double_quote(void **state)
+{
+ const char *double_quote_left = "\"abcdef";
+ const char *double_quote_mid = "abc\"def";
+ const char *double_quote_right = "abcdef\"";
+
+ assert_false(pcmk__xml_needs_escape(double_quote_left,
+ pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape(double_quote_mid,
+ pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape(double_quote_right,
+ pcmk__xml_escape_text));
+
+ assert_true(pcmk__xml_needs_escape(double_quote_left,
+ pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(double_quote_mid,
+ pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(double_quote_right,
+ pcmk__xml_escape_attr));
+
+ assert_true(pcmk__xml_needs_escape(double_quote_left,
+ pcmk__xml_escape_attr_pretty));
+ assert_true(pcmk__xml_needs_escape(double_quote_mid,
+ pcmk__xml_escape_attr_pretty));
+ assert_true(pcmk__xml_needs_escape(double_quote_right,
+ pcmk__xml_escape_attr_pretty));
+}
+
+static void
+escape_newline(void **state)
+{
+ const char *newline_left = "\nabcdef";
+ const char *newline_mid = "abc\ndef";
+ const char *newline_right = "abcdef\n";
+
+ assert_false(pcmk__xml_needs_escape(newline_left, pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape(newline_mid, pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape(newline_right, pcmk__xml_escape_text));
+
+ assert_true(pcmk__xml_needs_escape(newline_left, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(newline_mid, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(newline_right, pcmk__xml_escape_attr));
+
+ assert_true(pcmk__xml_needs_escape(newline_left,
+ pcmk__xml_escape_attr_pretty));
+ assert_true(pcmk__xml_needs_escape(newline_mid,
+ pcmk__xml_escape_attr_pretty));
+ assert_true(pcmk__xml_needs_escape(newline_right,
+ pcmk__xml_escape_attr_pretty));
+}
+
+static void
+escape_tab(void **state)
+{
+ const char *tab_left = "\tabcdef";
+ const char *tab_mid = "abc\tdef";
+ const char *tab_right = "abcdef\t";
+
+ assert_false(pcmk__xml_needs_escape(tab_left, pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape(tab_mid, pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape(tab_right, pcmk__xml_escape_text));
+
+ assert_true(pcmk__xml_needs_escape(tab_left, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(tab_mid, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(tab_right, pcmk__xml_escape_attr));
+
+ assert_true(pcmk__xml_needs_escape(tab_left, pcmk__xml_escape_attr_pretty));
+ assert_true(pcmk__xml_needs_escape(tab_mid, pcmk__xml_escape_attr_pretty));
+ assert_true(pcmk__xml_needs_escape(tab_right,
+ pcmk__xml_escape_attr_pretty));
+}
+
+static void
+escape_carriage_return(void **state)
+{
+ const char *cr_left = "\rabcdef";
+ const char *cr_mid = "abc\rdef";
+ const char *cr_right = "abcdef\r";
+
+ assert_true(pcmk__xml_needs_escape(cr_left, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(cr_mid, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(cr_right, pcmk__xml_escape_text));
+
+ assert_true(pcmk__xml_needs_escape(cr_left, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(cr_mid, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(cr_right, pcmk__xml_escape_attr));
+
+ assert_true(pcmk__xml_needs_escape(cr_left, pcmk__xml_escape_attr_pretty));
+ assert_true(pcmk__xml_needs_escape(cr_mid, pcmk__xml_escape_attr_pretty));
+ assert_true(pcmk__xml_needs_escape(cr_right, pcmk__xml_escape_attr_pretty));
+}
+
+static void
+escape_nonprinting(void **state)
+{
+ const char *alert_left = "\aabcdef";
+ const char *alert_mid = "abc\adef";
+ const char *alert_right = "abcdef\a";
+
+ const char *delete_left = "\x7F""abcdef";
+ const char *delete_mid = "abc\x7F""def";
+ const char *delete_right = "abcdef\x7F";
+
+ const char *nonprinting_all = "\a\x7F\x1B";
+
+ assert_true(pcmk__xml_needs_escape(alert_left, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(alert_mid, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(alert_right, pcmk__xml_escape_text));
+
+ assert_true(pcmk__xml_needs_escape(alert_left, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(alert_mid, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(alert_right, pcmk__xml_escape_attr));
+
+ assert_false(pcmk__xml_needs_escape(alert_left,
+ pcmk__xml_escape_attr_pretty));
+ assert_false(pcmk__xml_needs_escape(alert_mid,
+ pcmk__xml_escape_attr_pretty));
+ assert_false(pcmk__xml_needs_escape(alert_right,
+ pcmk__xml_escape_attr_pretty));
+
+ assert_true(pcmk__xml_needs_escape(delete_left, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(delete_mid, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(delete_right, pcmk__xml_escape_text));
+
+ assert_true(pcmk__xml_needs_escape(delete_left, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(delete_mid, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(delete_right, pcmk__xml_escape_attr));
+
+ assert_false(pcmk__xml_needs_escape(delete_left,
+ pcmk__xml_escape_attr_pretty));
+ assert_false(pcmk__xml_needs_escape(delete_mid,
+ pcmk__xml_escape_attr_pretty));
+ assert_false(pcmk__xml_needs_escape(delete_right,
+ pcmk__xml_escape_attr_pretty));
+
+ assert_true(pcmk__xml_needs_escape(nonprinting_all, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(nonprinting_all, pcmk__xml_escape_attr));
+ assert_false(pcmk__xml_needs_escape(nonprinting_all,
+ pcmk__xml_escape_attr_pretty));
+}
+
+static void
+escape_utf8(void **state)
+{
+ /* Non-ASCII UTF-8 characters may be two, three, or four 8-bit bytes wide
+ * and should not be escaped.
+ */
+ const char *chinese = "仅高级使用";
+ const char *two_byte = "abc""\xCF\xA6""def";
+ const char *two_byte_special = "abc""\xCF\xA6""d<ef";
+ const char *three_byte = "abc""\xEF\x98\x98""def";
+ const char *three_byte_special = "abc""\xEF\x98\x98""d<ef";
+ const char *four_byte = "abc""\xF0\x94\x81\x90""def";
+ const char *four_byte_special = "abc""\xF0\x94\x81\x90""d<ef";
+
+ assert_false(pcmk__xml_needs_escape(chinese, pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape(chinese, pcmk__xml_escape_attr));
+ assert_false(pcmk__xml_needs_escape(chinese, pcmk__xml_escape_attr_pretty));
+
+ assert_false(pcmk__xml_needs_escape(two_byte, pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape(two_byte, pcmk__xml_escape_attr));
+ assert_false(pcmk__xml_needs_escape(two_byte,
+ pcmk__xml_escape_attr_pretty));
+
+ assert_true(pcmk__xml_needs_escape(two_byte_special,
+ pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(two_byte_special,
+ pcmk__xml_escape_attr));
+ assert_false(pcmk__xml_needs_escape(two_byte_special,
+ pcmk__xml_escape_attr_pretty));
+
+ assert_false(pcmk__xml_needs_escape(three_byte, pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape(three_byte, pcmk__xml_escape_attr));
+ assert_false(pcmk__xml_needs_escape(three_byte,
+ pcmk__xml_escape_attr_pretty));
+
+ assert_true(pcmk__xml_needs_escape(three_byte_special,
+ pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(three_byte_special,
+ pcmk__xml_escape_attr));
+ assert_false(pcmk__xml_needs_escape(three_byte_special,
+ pcmk__xml_escape_attr_pretty));
+
+ assert_false(pcmk__xml_needs_escape(four_byte, pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape(four_byte, pcmk__xml_escape_attr));
+ assert_false(pcmk__xml_needs_escape(four_byte,
+ pcmk__xml_escape_attr_pretty));
+
+ assert_true(pcmk__xml_needs_escape(four_byte_special,
+ pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(four_byte_special,
+ pcmk__xml_escape_attr));
+ assert_false(pcmk__xml_needs_escape(four_byte_special,
+ pcmk__xml_escape_attr_pretty));
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_empty),
+ cmocka_unit_test(invalid_type),
+ cmocka_unit_test(escape_unchanged),
+ cmocka_unit_test(escape_left_angle),
+ cmocka_unit_test(escape_right_angle),
+ cmocka_unit_test(escape_ampersand),
+ cmocka_unit_test(escape_double_quote),
+ cmocka_unit_test(escape_newline),
+ cmocka_unit_test(escape_tab),
+ cmocka_unit_test(escape_carriage_return),
+ cmocka_unit_test(escape_nonprinting),
+ cmocka_unit_test(escape_utf8));
diff --git a/lib/common/tests/xpath/pcmk__xpath_node_id_test.c b/lib/common/tests/xpath/pcmk__xpath_node_id_test.c
index 3922b34..86500c2 100644
--- a/lib/common/tests/xpath/pcmk__xpath_node_id_test.c
+++ b/lib/common/tests/xpath/pcmk__xpath_node_id_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2021-2022 the Pacemaker project contributors
+ * Copyright 2021-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -9,14 +9,14 @@
#include <crm_internal.h>
-#include <crm/msg_xml.h>
+#include <crm/common/xml.h>
#include <crm/common/unittest_internal.h>
#include <crm/common/xml_internal.h>
static void
empty_input(void **state) {
- assert_null(pcmk__xpath_node_id(NULL, "lrm"));
- assert_null(pcmk__xpath_node_id("", "lrm"));
+ assert_null(pcmk__xpath_node_id(NULL, PCMK__XE_LRM));
+ assert_null(pcmk__xpath_node_id("", PCMK__XE_LRM));
assert_null(pcmk__xpath_node_id("/blah/blah", NULL));
assert_null(pcmk__xpath_node_id("/blah/blah", ""));
assert_null(pcmk__xpath_node_id(NULL, NULL));
@@ -24,30 +24,31 @@ empty_input(void **state) {
static void
no_quotes(void **state) {
- const char *xpath = "/some/xpath/lrm[@" XML_ATTR_ID "=xyz]";
- pcmk__assert_asserts(pcmk__xpath_node_id(xpath, "lrm"));
+ const char *xpath = "/some/xpath/" PCMK__XE_LRM "[@" PCMK_XA_ID "=xyz]";
+ pcmk__assert_asserts(pcmk__xpath_node_id(xpath, PCMK__XE_LRM));
}
static void
not_present(void **state) {
- const char *xpath = "/some/xpath/string[@" XML_ATTR_ID "='xyz']";
- assert_null(pcmk__xpath_node_id(xpath, "lrm"));
+ const char *xpath = "/some/xpath/string[@" PCMK_XA_ID "='xyz']";
+ assert_null(pcmk__xpath_node_id(xpath, PCMK__XE_LRM));
- xpath = "/some/xpath/containing[@" XML_ATTR_ID "='lrm']";
- assert_null(pcmk__xpath_node_id(xpath, "lrm"));
+ xpath = "/some/xpath/containing[@" PCMK_XA_ID "='" PCMK__XE_LRM "']";
+ assert_null(pcmk__xpath_node_id(xpath, PCMK__XE_LRM));
}
static void
present(void **state) {
char *s = NULL;
- const char *xpath = "/some/xpath/containing/lrm[@" XML_ATTR_ID "='xyz']";
+ const char *xpath = "/some/xpath/containing"
+ "/" PCMK__XE_LRM "[@" PCMK_XA_ID "='xyz']";
- s = pcmk__xpath_node_id(xpath, "lrm");
+ s = pcmk__xpath_node_id(xpath, PCMK__XE_LRM);
assert_int_equal(strcmp(s, "xyz"), 0);
free(s);
- xpath = "/some/other/lrm[@" XML_ATTR_ID "='xyz']/xpath";
- s = pcmk__xpath_node_id(xpath, "lrm");
+ xpath = "/some/other/" PCMK__XE_LRM "[@" PCMK_XA_ID "='xyz']/xpath";
+ s = pcmk__xpath_node_id(xpath, PCMK__XE_LRM);
assert_int_equal(strcmp(s, "xyz"), 0);
free(s);
}
diff --git a/lib/common/unittest.c b/lib/common/unittest.c
new file mode 100644
index 0000000..0e63915
--- /dev/null
+++ b/lib/common/unittest.c
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU Lesser General Public License
+ * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/unittest_internal.h>
+
+#include <stdlib.h>
+#include <unistd.h>
+
+// LCOV_EXCL_START
+
+void
+pcmk__assert_validates(xmlNode *xml)
+{
+ const char *schema_dir = NULL;
+ char *cmd = NULL;
+ gchar *out = NULL;
+ gchar *err = NULL;
+ gint status;
+ GError *gerr = NULL;
+ char *xmllint_input = crm_strdup_printf("%s/test-xmllint.XXXXXX",
+ pcmk__get_tmpdir());
+ int fd;
+ int rc;
+
+ fd = mkstemp(xmllint_input);
+ if (fd < 0) {
+ fail_msg("Could not create temp file: %s", strerror(errno));
+ }
+
+ rc = pcmk__xml2fd(fd, xml);
+ if (rc != pcmk_rc_ok) {
+ unlink(xmllint_input);
+ fail_msg("Could not write temp file: %s", pcmk_rc_str(rc));
+ }
+
+ close(fd);
+
+ /* This should be set as part of AM_TESTS_ENVIRONMENT in Makefile.am. */
+ schema_dir = getenv("PCMK_schema_directory");
+ if (schema_dir == NULL) {
+ unlink(xmllint_input);
+ fail_msg("PCMK_schema_directory is not set in test environment");
+ }
+
+ cmd = crm_strdup_printf("xmllint --relaxng %s/api/api-result.rng %s",
+ schema_dir, xmllint_input);
+
+ if (!g_spawn_command_line_sync(cmd, &out, &err, &status, &gerr)) {
+ unlink(xmllint_input);
+ fail_msg("Error occurred when performing validation: %s", gerr->message);
+ }
+
+ if (WIFEXITED(status) && WEXITSTATUS(status) != 0) {
+ unlink(xmllint_input);
+ fail_msg("XML validation failed: %s\n%s\n", out, err);
+ }
+
+ free(cmd);
+ g_free(out);
+ g_free(err);
+ unlink(xmllint_input);
+ free(xmllint_input);
+}
+
+int
+pcmk__xml_test_setup_group(void **state)
+{
+ /* This needs to be run before we run unit tests that manipulate XML.
+ * Without it, document private data won't get created, which can cause
+ * segmentation faults or assertions in functions related to change
+ * tracking and ACLs. There's no harm in doing this before all tests.
+ */
+ crm_xml_init();
+ return 0;
+}
+
+char *
+pcmk__cib_test_copy_cib(const char *in_file)
+{
+ char *in_path = crm_strdup_printf("%s/%s", getenv("PCMK_CTS_CLI_DIR"), in_file);
+ char *out_path = NULL;
+ char *contents = NULL;
+ int fd;
+
+ /* Copy the CIB over to a temp location so we can modify it. */
+ out_path = crm_strdup_printf("%s/test-cib.XXXXXX", pcmk__get_tmpdir());
+
+ fd = mkstemp(out_path);
+ if (fd < 0) {
+ free(out_path);
+ return NULL;
+ }
+
+ if (pcmk__file_contents(in_path, &contents) != pcmk_rc_ok) {
+ free(out_path);
+ close(fd);
+ return NULL;
+ }
+
+ if (pcmk__write_sync(fd, contents) != pcmk_rc_ok) {
+ free(out_path);
+ free(in_path);
+ free(contents);
+ close(fd);
+ return NULL;
+ }
+
+ setenv("CIB_file", out_path, 1);
+ return out_path;
+}
+
+void
+pcmk__cib_test_cleanup(char *out_path)
+{
+ unlink(out_path);
+ free(out_path);
+ unsetenv("CIB_file");
+}
+
+// LCOV_EXCL_STOP
diff --git a/lib/common/utils.c b/lib/common/utils.c
index e5b9ef0..e8d343e 100644
--- a/lib/common/utils.c
+++ b/lib/common/utils.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2004-2022 the Pacemaker project contributors
+ * Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -33,7 +33,6 @@
#include <crm/crm.h>
#include <crm/services.h>
-#include <crm/msg_xml.h>
#include <crm/cib/internal.h>
#include <crm/common/xml.h>
#include <crm/common/util.h>
@@ -257,46 +256,6 @@ compare_version(const char *version1, const char *version2)
}
/*!
- * \brief Parse milliseconds from a Pacemaker interval specification
- *
- * \param[in] input Pacemaker time interval specification (a bare number of
- * seconds, a number with a unit optionally with whitespace
- * before and/or after the number, or an ISO 8601 duration)
- *
- * \return Milliseconds equivalent of given specification on success (limited
- * to the range of an unsigned integer), 0 if input is NULL,
- * or 0 (and set errno to EINVAL) on error
- */
-guint
-crm_parse_interval_spec(const char *input)
-{
- long long msec = -1;
-
- errno = 0;
- if (input == NULL) {
- return 0;
-
- } else if (input[0] == 'P') {
- crm_time_t *period_s = crm_time_parse_duration(input);
-
- if (period_s) {
- msec = 1000 * crm_time_get_seconds(period_s);
- crm_time_free(period_s);
- }
-
- } else {
- msec = crm_get_msec(input);
- }
-
- if (msec < 0) {
- crm_warn("Using 0 instead of '%s'", input);
- errno = EINVAL;
- return 0;
- }
- return (msec >= G_MAXUINT)? G_MAXUINT : (guint) msec;
-}
-
-/*!
* \internal
* \brief Log a failed assertion
*
@@ -464,43 +423,6 @@ pcmk__daemonize(const char *name, const char *pidfile)
pcmk__open_devnull(O_WRONLY); // stderr (fd 2)
}
-char *
-crm_meta_name(const char *field)
-{
- int lpc = 0;
- int max = 0;
- char *crm_name = NULL;
-
- CRM_CHECK(field != NULL, return NULL);
- crm_name = crm_strdup_printf(CRM_META "_%s", field);
-
- /* Massage the names so they can be used as shell variables */
- max = strlen(crm_name);
- for (; lpc < max; lpc++) {
- switch (crm_name[lpc]) {
- case '-':
- crm_name[lpc] = '_';
- break;
- }
- }
- return crm_name;
-}
-
-const char *
-crm_meta_value(GHashTable * hash, const char *field)
-{
- char *key = NULL;
- const char *value = NULL;
-
- key = crm_meta_name(field);
- if (key) {
- value = g_hash_table_lookup(hash, key);
- free(key);
- }
-
- return value;
-}
-
#ifdef HAVE_UUID_UUID_H
# include <uuid/uuid.h>
#endif
@@ -511,7 +433,7 @@ crm_generate_uuid(void)
unsigned char uuid[16];
char *buffer = malloc(37); /* Including NUL byte */
- CRM_ASSERT(buffer != NULL);
+ pcmk__mem_assert(buffer);
uuid_generate(uuid);
uuid_unparse(uuid, buffer);
return buffer;
@@ -526,27 +448,15 @@ crm_gnutls_global_init(void)
}
#endif
-/*!
- * \brief Get the local hostname
- *
- * \return Newly allocated string with name, or NULL (and set errno) on error
- */
-char *
-pcmk_hostname(void)
-{
- struct utsname hostinfo;
-
- return (uname(&hostinfo) < 0)? NULL : strdup(hostinfo.nodename);
-}
-
bool
pcmk_str_is_infinity(const char *s) {
- return pcmk__str_any_of(s, CRM_INFINITY_S, CRM_PLUS_INFINITY_S, NULL);
+ return pcmk__str_any_of(s, PCMK_VALUE_INFINITY, PCMK_VALUE_PLUS_INFINITY,
+ NULL);
}
bool
pcmk_str_is_minus_infinity(const char *s) {
- return pcmk__str_eq(s, CRM_MINUS_INFINITY_S, pcmk__str_none);
+ return pcmk__str_eq(s, PCMK_VALUE_MINUS_INFINITY, pcmk__str_none);
}
/*!
@@ -592,3 +502,48 @@ pcmk__sleep_ms(unsigned int ms)
}
#endif
}
+
+// Deprecated functions kept only for backward API compatibility
+// LCOV_EXCL_START
+
+#include <crm/common/util_compat.h>
+
+guint
+crm_parse_interval_spec(const char *input)
+{
+ long long msec = -1;
+
+ errno = 0;
+ if (input == NULL) {
+ return 0;
+
+ } else if (input[0] == 'P') {
+ crm_time_t *period_s = crm_time_parse_duration(input);
+
+ if (period_s) {
+ msec = 1000 * crm_time_get_seconds(period_s);
+ crm_time_free(period_s);
+ }
+
+ } else {
+ msec = crm_get_msec(input);
+ }
+
+ if (msec < 0) {
+ crm_warn("Using 0 instead of '%s'", input);
+ errno = EINVAL;
+ return 0;
+ }
+ return (msec >= G_MAXUINT)? G_MAXUINT : (guint) msec;
+}
+
+char *
+pcmk_hostname(void)
+{
+ struct utsname hostinfo;
+
+ return (uname(&hostinfo) < 0)? NULL : strdup(hostinfo.nodename);
+}
+
+// LCOV_EXCL_STOP
+// End deprecated API
diff --git a/lib/common/watchdog.c b/lib/common/watchdog.c
index e569214..b7921fe 100644
--- a/lib/common/watchdog.c
+++ b/lib/common/watchdog.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2013-2023 the Pacemaker project contributors
+ * Copyright 2013-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -112,10 +112,10 @@ panic_local(void)
if(ppid > 1) {
/* child daemon */
- exit(CRM_EX_PANIC);
+ crm_exit(CRM_EX_PANIC);
} else {
/* pacemakerd or orphan child */
- exit(CRM_EX_FATAL);
+ crm_exit(CRM_EX_FATAL);
}
}
@@ -141,10 +141,10 @@ panic_sbd(void)
if(ppid > 1) {
/* child daemon */
- exit(CRM_EX_PANIC);
+ crm_exit(CRM_EX_PANIC);
} else {
/* pacemakerd or orphan child */
- exit(CRM_EX_FATAL);
+ crm_exit(CRM_EX_FATAL);
}
}
@@ -232,7 +232,7 @@ pcmk__locate_sbd(void)
}
long
-pcmk__get_sbd_timeout(void)
+pcmk__get_sbd_watchdog_timeout(void)
{
static long sbd_timeout = -2;
@@ -266,44 +266,53 @@ pcmk__get_sbd_sync_resource_startup(void)
}
long
-pcmk__auto_watchdog_timeout(void)
+pcmk__auto_stonith_watchdog_timeout(void)
{
- long sbd_timeout = pcmk__get_sbd_timeout();
+ long sbd_timeout = pcmk__get_sbd_watchdog_timeout();
return (sbd_timeout <= 0)? 0 : (2 * sbd_timeout);
}
bool
-pcmk__valid_sbd_timeout(const char *value)
+pcmk__valid_stonith_watchdog_timeout(const char *value)
{
+ /* @COMPAT At a compatibility break, accept either negative values or a
+ * specific string like "auto" (but not both) to mean "auto-calculate the
+ * timeout." Reject other values that aren't parsable as timeouts.
+ */
long st_timeout = value? crm_get_msec(value) : 0;
if (st_timeout < 0) {
- st_timeout = pcmk__auto_watchdog_timeout();
- crm_debug("Using calculated value %ld for stonith-watchdog-timeout (%s)",
+ st_timeout = pcmk__auto_stonith_watchdog_timeout();
+ crm_debug("Using calculated value %ld for "
+ PCMK_OPT_STONITH_WATCHDOG_TIMEOUT " (%s)",
st_timeout, value);
}
if (st_timeout == 0) {
- crm_debug("Watchdog may be enabled but stonith-watchdog-timeout is disabled (%s)",
+ crm_debug("Watchdog may be enabled but "
+ PCMK_OPT_STONITH_WATCHDOG_TIMEOUT " is disabled (%s)",
value? value : "default");
} else if (pcmk__locate_sbd() == 0) {
- crm_emerg("Shutting down: stonith-watchdog-timeout configured (%s) "
- "but SBD not active", (value? value : "auto"));
+ crm_emerg("Shutting down: " PCMK_OPT_STONITH_WATCHDOG_TIMEOUT
+ " configured (%s) but SBD not active",
+ pcmk__s(value, "auto"));
crm_exit(CRM_EX_FATAL);
return false;
} else {
- long sbd_timeout = pcmk__get_sbd_timeout();
+ long sbd_timeout = pcmk__get_sbd_watchdog_timeout();
if (st_timeout < sbd_timeout) {
- crm_emerg("Shutting down: stonith-watchdog-timeout (%s) too short "
- "(must be >%ldms)", value, sbd_timeout);
+ crm_emerg("Shutting down: " PCMK_OPT_STONITH_WATCHDOG_TIMEOUT
+ " (%s) too short (must be >%ldms)",
+ value, sbd_timeout);
crm_exit(CRM_EX_FATAL);
return false;
}
- crm_info("Watchdog configured with stonith-watchdog-timeout %s and SBD timeout %ldms",
+ crm_info("Watchdog configured with " PCMK_OPT_STONITH_WATCHDOG_TIMEOUT
+ " %s and SBD timeout %ldms",
value, sbd_timeout);
}
return true;
diff --git a/lib/common/xml.c b/lib/common/xml.c
index 53ebff7..73d6025 100644
--- a/lib/common/xml.c
+++ b/lib/common/xml.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2004-2023 the Pacemaker project contributors
+ * Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -9,41 +9,52 @@
#include <crm_internal.h>
+#include <stdarg.h>
+#include <stdint.h> // uint32_t
#include <stdio.h>
-#include <sys/types.h>
-#include <unistd.h>
-#include <time.h>
-#include <string.h>
#include <stdlib.h>
-#include <stdarg.h>
-#include <bzlib.h>
+#include <string.h>
+#include <sys/stat.h> // stat(), S_ISREG, etc.
+#include <sys/types.h>
#include <libxml/parser.h>
#include <libxml/tree.h>
-#include <libxml/xmlIO.h> /* xmlAllocOutputBuffer */
#include <crm/crm.h>
-#include <crm/msg_xml.h>
#include <crm/common/xml.h>
-#include <crm/common/xml_internal.h> // PCMK__XML_LOG_BASE, etc.
+#include <crm/common/xml_internal.h> // PCMK__XML_LOG_BASE, etc.
#include "crmcommon_private.h"
-// Define this as 1 in development to get insanely verbose trace messages
-#ifndef XML_PARSER_DEBUG
-#define XML_PARSER_DEBUG 0
-#endif
-
-/* @TODO XML_PARSE_RECOVER allows some XML errors to be silently worked around
- * by libxml2, which is potentially ambiguous and dangerous. We should drop it
- * when we can break backward compatibility with configurations that might be
- * relying on it (i.e. pacemaker 3.0.0).
+/*!
+ * \internal
+ * \brief Apply a function to each XML node in a tree (pre-order, depth-first)
*
- * It might be a good idea to have a transitional period where we first try
- * parsing without XML_PARSE_RECOVER, and if that fails, try parsing again with
- * it, logging a warning if it succeeds.
+ * \param[in,out] xml XML tree to traverse
+ * \param[in,out] fn Function to call for each node (returns \c true to
+ * continue traversing the tree or \c false to stop)
+ * \param[in,out] user_data Argument to \p fn
+ *
+ * \return \c false if any \p fn call returned \c false, or \c true otherwise
+ *
+ * \note This function is recursive.
*/
-#define PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER (XML_PARSE_NOBLANKS)
-#define PCMK__XML_PARSE_OPTS_WITH_RECOVER (XML_PARSE_NOBLANKS | XML_PARSE_RECOVER)
+bool
+pcmk__xml_tree_foreach(xmlNode *xml, bool (*fn)(xmlNode *, void *),
+ void *user_data)
+{
+ if (!fn(xml, user_data)) {
+ return false;
+ }
+
+ for (xml = pcmk__xml_first_child(xml); xml != NULL;
+ xml = pcmk__xml_next(xml)) {
+
+ if (!pcmk__xml_tree_foreach(xml, fn, user_data)) {
+ return false;
+ }
+ }
+ return true;
+}
bool
pcmk__tracking_xml_changes(xmlNode *xml, bool lazy)
@@ -93,43 +104,72 @@ pcmk__mark_xml_node_dirty(xmlNode *xml)
set_parent_flag(xml, pcmk__xf_dirty);
}
-// Clear flags on XML node and its children
-static void
-reset_xml_node_flags(xmlNode *xml)
+/*!
+ * \internal
+ * \brief Clear flags on an XML node
+ *
+ * \param[in,out] xml XML node whose flags to reset
+ * \param[in,out] user_data Ignored
+ *
+ * \return \c true (to continue traversing the tree)
+ *
+ * \note This is compatible with \c pcmk__xml_tree_foreach().
+ */
+static bool
+reset_xml_node_flags(xmlNode *xml, void *user_data)
{
- xmlNode *cIter = NULL;
xml_node_private_t *nodepriv = xml->_private;
- if (nodepriv) {
- nodepriv->flags = 0;
+ if (nodepriv != NULL) {
+ nodepriv->flags = pcmk__xf_none;
}
+ return true;
+}
- for (cIter = pcmk__xml_first_child(xml); cIter != NULL;
- cIter = pcmk__xml_next(cIter)) {
- reset_xml_node_flags(cIter);
+/*!
+ * \internal
+ * \brief Set the \c pcmk__xf_dirty and \c pcmk__xf_created flags on an XML node
+ *
+ * \param[in,out] xml Node whose flags to set
+ * \param[in] user_data Ignored
+ *
+ * \return \c true (to continue traversing the tree)
+ *
+ * \note This is compatible with \c pcmk__xml_tree_foreach().
+ */
+static bool
+mark_xml_dirty_created(xmlNode *xml, void *user_data)
+{
+ xml_node_private_t *nodepriv = xml->_private;
+
+ if (nodepriv != NULL) {
+ pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created);
}
+ return true;
}
-// Set xpf_created flag on XML node and any children
+/*!
+ * \internal
+ * \brief Mark an XML tree as dirty and created, and mark its parents dirty
+ *
+ * Also mark the document dirty.
+ *
+ * \param[in,out] xml Tree to mark as dirty and created
+ */
void
-pcmk__mark_xml_created(xmlNode *xml)
+pcmk__xml_mark_created(xmlNode *xml)
{
- xmlNode *cIter = NULL;
- xml_node_private_t *nodepriv = NULL;
-
CRM_ASSERT(xml != NULL);
- nodepriv = xml->_private;
- if (nodepriv && pcmk__tracking_xml_changes(xml, FALSE)) {
- if (!pcmk_is_set(nodepriv->flags, pcmk__xf_created)) {
- pcmk__set_xml_flags(nodepriv, pcmk__xf_created);
- pcmk__mark_xml_node_dirty(xml);
- }
- for (cIter = pcmk__xml_first_child(xml); cIter != NULL;
- cIter = pcmk__xml_next(cIter)) {
- pcmk__mark_xml_created(cIter);
- }
+ if (!pcmk__tracking_xml_changes(xml, false)) {
+ // Tracking is disabled for entire document
+ return;
}
+
+ // Mark all parents and document dirty
+ pcmk__mark_xml_node_dirty(xml);
+
+ pcmk__xml_tree_foreach(xml, mark_xml_dirty_created, NULL);
}
#define XML_DOC_PRIVATE_MAGIC 0x81726354UL
@@ -142,7 +182,7 @@ free_deleted_object(void *data)
if(data) {
pcmk__deleted_xml_t *deleted_obj = data;
- free(deleted_obj->path);
+ g_free(deleted_obj->path);
free(deleted_obj);
}
}
@@ -220,9 +260,9 @@ new_private_data(xmlNode *node)
{
switch (node->type) {
case XML_DOCUMENT_NODE: {
- xml_doc_private_t *docpriv = NULL;
- docpriv = calloc(1, sizeof(xml_doc_private_t));
- CRM_ASSERT(docpriv != NULL);
+ xml_doc_private_t *docpriv =
+ pcmk__assert_alloc(1, sizeof(xml_doc_private_t));
+
docpriv->check = XML_DOC_PRIVATE_MAGIC;
/* Flags will be reset if necessary when tracking is enabled */
pcmk__set_xml_flags(docpriv, pcmk__xf_dirty|pcmk__xf_created);
@@ -232,9 +272,9 @@ new_private_data(xmlNode *node)
case XML_ELEMENT_NODE:
case XML_ATTRIBUTE_NODE:
case XML_COMMENT_NODE: {
- xml_node_private_t *nodepriv = NULL;
- nodepriv = calloc(1, sizeof(xml_node_private_t));
- CRM_ASSERT(nodepriv != NULL);
+ xml_node_private_t *nodepriv =
+ pcmk__assert_alloc(1, sizeof(xml_node_private_t));
+
nodepriv->check = XML_NODE_PRIVATE_MAGIC;
/* Flags will be reset if necessary when tracking is enabled */
pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created);
@@ -314,21 +354,23 @@ pcmk__xml_position(const xmlNode *xml, enum xml_private_flags ignore_if_set)
return position;
}
-// Remove all attributes marked as deleted from an XML node
-static void
-accept_attr_deletions(xmlNode *xml)
+/*!
+ * \internal
+ * \brief Remove all attributes marked as deleted from an XML node
+ *
+ * \param[in,out] xml XML node whose deleted attributes to remove
+ * \param[in,out] user_data Ignored
+ *
+ * \return \c true (to continue traversing the tree)
+ *
+ * \note This is compatible with \c pcmk__xml_tree_foreach().
+ */
+static bool
+accept_attr_deletions(xmlNode *xml, void *user_data)
{
- // Clear XML node's flags
- ((xml_node_private_t *) xml->_private)->flags = pcmk__xf_none;
-
- // Remove this XML node's attributes that were marked as deleted
+ reset_xml_node_flags(xml, NULL);
pcmk__xe_remove_matching_attrs(xml, pcmk__marked_as_deleted, NULL);
-
- // Recursively do the same for this XML node's children
- for (xmlNodePtr cIter = pcmk__xml_first_child(xml); cIter != NULL;
- cIter = pcmk__xml_next(cIter)) {
- accept_attr_deletions(cIter);
- }
+ return true;
}
/*!
@@ -348,10 +390,11 @@ pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle, bool exact)
return pcmk__xc_match(haystack, needle, exact);
} else {
- const char *id = ID(needle);
- const char *attr = (id == NULL)? NULL : XML_ATTR_ID;
+ const char *id = pcmk__xe_id(needle);
+ const char *attr = (id == NULL)? NULL : PCMK_XA_ID;
- return pcmk__xe_match(haystack, (const char *) needle->name, attr, id);
+ return pcmk__xe_first_child(haystack, (const char *) needle->name, attr,
+ id);
}
}
@@ -377,207 +420,263 @@ xml_accept_changes(xmlNode * xml)
}
docpriv->flags = pcmk__xf_none;
- accept_attr_deletions(top);
-}
-
-xmlNode *
-find_xml_node(const xmlNode *root, const char *search_path, gboolean must_find)
-{
- xmlNode *a_child = NULL;
- const char *name = (root == NULL)? "<NULL>" : (const char *) root->name;
-
- if (search_path == NULL) {
- crm_warn("Will never find <NULL>");
- return NULL;
- }
-
- for (a_child = pcmk__xml_first_child(root); a_child != NULL;
- a_child = pcmk__xml_next(a_child)) {
- if (strcmp((const char *)a_child->name, search_path) == 0) {
- return a_child;
- }
- }
-
- if (must_find) {
- crm_warn("Could not find %s in %s.", search_path, name);
- } else if (root != NULL) {
- crm_trace("Could not find %s in %s.", search_path, name);
- } else {
- crm_trace("Could not find %s in <NULL>.", search_path);
- }
-
- return NULL;
+ pcmk__xml_tree_foreach(top, accept_attr_deletions, NULL);
}
-#define attr_matches(c, n, v) pcmk__str_eq(crm_element_value((c), (n)), \
- (v), pcmk__str_none)
-
/*!
* \internal
* \brief Find first XML child element matching given criteria
*
- * \param[in] parent XML element to search
- * \param[in] node_name If not NULL, only match children of this type
- * \param[in] attr_n If not NULL, only match children with an attribute
+ * \param[in] parent XML element to search (can be \c NULL)
+ * \param[in] node_name If not \c NULL, only match children of this type
+ * \param[in] attr_n If not \c NULL, only match children with an attribute
* of this name.
* \param[in] attr_v If \p attr_n and this are not NULL, only match children
* with an attribute named \p attr_n and this value
*
- * \return Matching XML child element, or NULL if none found
+ * \return Matching XML child element, or \c NULL if none found
*/
xmlNode *
-pcmk__xe_match(const xmlNode *parent, const char *node_name,
- const char *attr_n, const char *attr_v)
+pcmk__xe_first_child(const xmlNode *parent, const char *node_name,
+ const char *attr_n, const char *attr_v)
{
- CRM_CHECK(parent != NULL, return NULL);
- CRM_CHECK(attr_v == NULL || attr_n != NULL, return NULL);
+ xmlNode *child = NULL;
+ const char *parent_name = "<null>";
- for (xmlNode *child = pcmk__xml_first_child(parent); child != NULL;
- child = pcmk__xml_next(child)) {
- if (pcmk__str_eq(node_name, (const char *) (child->name),
- pcmk__str_null_matches)
- && ((attr_n == NULL) ||
- (attr_v == NULL && xmlHasProp(child, (pcmkXmlStr) attr_n)) ||
- (attr_v != NULL && attr_matches(child, attr_n, attr_v)))) {
- return child;
+ CRM_CHECK((attr_v == NULL) || (attr_n != NULL), return NULL);
+
+ if (parent != NULL) {
+ child = parent->children;
+ while ((child != NULL) && (child->type != XML_ELEMENT_NODE)) {
+ child = child->next;
}
+
+ parent_name = (const char *) parent->name;
}
- crm_trace("XML child node <%s%s%s%s%s> not found in %s",
- (node_name? node_name : "(any)"),
- (attr_n? " " : ""),
- (attr_n? attr_n : ""),
- (attr_n? "=" : ""),
- (attr_n? attr_v : ""),
- (const char *) parent->name);
- return NULL;
-}
-void
-copy_in_properties(xmlNode *target, const xmlNode *src)
-{
- if (src == NULL) {
- crm_warn("No node to copy properties from");
+ for (; child != NULL; child = pcmk__xe_next(child)) {
+ const char *value = NULL;
- } else if (target == NULL) {
- crm_err("No node to copy properties into");
+ if ((node_name != NULL) && !pcmk__xe_is(child, node_name)) {
+ // Node name mismatch
+ continue;
+ }
+ if (attr_n == NULL) {
+ // No attribute match needed
+ return child;
+ }
- } else {
- for (xmlAttrPtr a = pcmk__xe_first_attr(src); a != NULL; a = a->next) {
- const char *p_name = (const char *) a->name;
- const char *p_value = pcmk__xml_attr_value(a);
+ value = crm_element_value(child, attr_n);
- expand_plus_plus(target, p_name, p_value);
- if (xml_acl_denied(target)) {
- crm_trace("Cannot copy %s=%s to %s", p_name, p_value, target->name);
- return;
- }
+ if ((attr_v == NULL) && (value != NULL)) {
+ // attr_v == NULL: Attribute attr_n must be set (to any value)
+ return child;
+ }
+ if ((attr_v != NULL) && (pcmk__str_eq(value, attr_v, pcmk__str_none))) {
+ // attr_v != NULL: Attribute attr_n must be set to value attr_v
+ return child;
}
}
- return;
+ if (node_name == NULL) {
+ node_name = "(any)"; // For logging
+ }
+ if (attr_n != NULL) {
+ crm_trace("XML child node <%s %s=%s> not found in %s",
+ node_name, attr_n, attr_v, parent_name);
+ } else {
+ crm_trace("XML child node <%s> not found in %s",
+ node_name, parent_name);
+ }
+ return NULL;
}
/*!
- * \brief Parse integer assignment statements on this node and all its child
- * nodes
+ * \internal
+ * \brief Set an XML attribute, expanding \c ++ and \c += where appropriate
+ *
+ * If \p target already has an attribute named \p name set to an integer value
+ * and \p value is an addition assignment expression on \p name, then expand
+ * \p value to an integer and set attribute \p name to the expanded value in
+ * \p target.
+ *
+ * Otherwise, set attribute \p name on \p target using the literal \p value.
+ *
+ * The original attribute value in \p target and the number in an assignment
+ * expression in \p value are parsed and added as scores (that is, their values
+ * are capped at \c INFINITY and \c -INFINITY). For more details, refer to
+ * \c char2score().
*
- * \param[in,out] target Root XML node to be processed
+ * For example, suppose \p target has an attribute named \c "X" with value
+ * \c "5", and that \p name is \c "X".
+ * * If \p value is \c "X++", the new value of \c "X" in \p target is \c "6".
+ * * If \p value is \c "X+=3", the new value of \c "X" in \p target is \c "8".
+ * * If \p value is \c "val", the new value of \c "X" in \p target is \c "val".
+ * * If \p value is \c "Y++", the new value of \c "X" in \p target is \c "Y++".
*
- * \note This function is recursive
+ * \param[in,out] target XML node whose attribute to set
+ * \param[in] name Name of the attribute to set
+ * \param[in] value New value of attribute to set
+ *
+ * \return Standard Pacemaker return code (specifically, \c EINVAL on invalid
+ * argument, or \c pcmk_rc_ok otherwise)
*/
-void
-fix_plus_plus_recursive(xmlNode *target)
+int
+pcmk__xe_set_score(xmlNode *target, const char *name, const char *value)
{
- /* TODO: Remove recursion and use xpath searches for value++ */
- xmlNode *child = NULL;
+ const char *old_value = NULL;
- for (xmlAttrPtr a = pcmk__xe_first_attr(target); a != NULL; a = a->next) {
- const char *p_name = (const char *) a->name;
- const char *p_value = pcmk__xml_attr_value(a);
+ CRM_CHECK((target != NULL) && (name != NULL), return EINVAL);
- expand_plus_plus(target, p_name, p_value);
+ if (value == NULL) {
+ return pcmk_rc_ok;
}
- for (child = pcmk__xml_first_child(target); child != NULL;
- child = pcmk__xml_next(child)) {
- fix_plus_plus_recursive(child);
+
+ old_value = crm_element_value(target, name);
+
+ // If no previous value, skip to default case and set the value unexpanded.
+ if (old_value != NULL) {
+ const char *n = name;
+ const char *v = value;
+
+ // Stop at first character that differs between name and value
+ for (; (*n == *v) && (*n != '\0'); n++, v++);
+
+ // If value begins with name followed by a "++" or "+="
+ if ((*n == '\0')
+ && (*v++ == '+')
+ && ((*v == '+') || (*v == '='))) {
+
+ // If we're expanding ourselves, no previous value was set; use 0
+ int old_value_i = (old_value != value)? char2score(old_value) : 0;
+
+ /* value="X++": new value of X is old_value + 1
+ * value="X+=Y": new value of X is old_value + Y (for some number Y)
+ */
+ int add = (*v == '+')? 1 : char2score(++v);
+
+ crm_xml_add_int(target, name, pcmk__add_scores(old_value_i, add));
+ return pcmk_rc_ok;
+ }
+ }
+
+ // Default case: set the attribute unexpanded (with value treated literally)
+ if (old_value != value) {
+ crm_xml_add(target, name, value);
}
+ return pcmk_rc_ok;
}
/*!
- * \brief Update current XML attribute value per parsed integer assignment
- statement
- *
- * \param[in,out] target an XML node, containing a XML attribute that is
- * initialized to some numeric value, to be processed
- * \param[in] name name of the XML attribute, e.g. X, whose value
- * should be updated
- * \param[in] value assignment statement, e.g. "X++" or
- * "X+=5", to be applied to the initialized value.
- *
- * \note The original XML attribute value is treated as 0 if non-numeric and
- * truncated to be an integer if decimal-point-containing.
- * \note The final XML attribute value is truncated to not exceed 1000000.
- * \note Undefined behavior if unexpected input.
+ * \internal
+ * \brief Copy XML attributes from a source element to a target element
+ *
+ * This is similar to \c xmlCopyPropList() except that attributes are marked
+ * as dirty for change tracking purposes.
+ *
+ * \param[in,out] target XML element to receive copied attributes from \p src
+ * \param[in] src XML element whose attributes to copy to \p target
+ * \param[in] flags Group of <tt>enum pcmk__xa_flags</tt>
+ *
+ * \return Standard Pacemaker return code
*/
-void
-expand_plus_plus(xmlNode * target, const char *name, const char *value)
+int
+pcmk__xe_copy_attrs(xmlNode *target, const xmlNode *src, uint32_t flags)
{
- int offset = 1;
- int name_len = 0;
- int int_value = 0;
- int value_len = 0;
+ CRM_CHECK((src != NULL) && (target != NULL), return EINVAL);
- const char *old_value = NULL;
+ for (xmlAttr *attr = pcmk__xe_first_attr(src); attr != NULL;
+ attr = attr->next) {
- if (target == NULL || value == NULL || name == NULL) {
- return;
- }
+ const char *name = (const char *) attr->name;
+ const char *value = pcmk__xml_attr_value(attr);
- old_value = crm_element_value(target, name);
-
- if (old_value == NULL) {
- /* if no previous value, set unexpanded */
- goto set_unexpanded;
+ if (pcmk_is_set(flags, pcmk__xaf_no_overwrite)
+ && (crm_element_value(target, name) != NULL)) {
+ continue;
+ }
- } else if (strstr(value, name) != value) {
- goto set_unexpanded;
+ if (pcmk_is_set(flags, pcmk__xaf_score_update)) {
+ pcmk__xe_set_score(target, name, value);
+ } else {
+ crm_xml_add(target, name, value);
+ }
}
- name_len = strlen(name);
- value_len = strlen(value);
- if (value_len < (name_len + 2)
- || value[name_len] != '+' || (value[name_len + 1] != '+' && value[name_len + 1] != '=')) {
- goto set_unexpanded;
- }
+ return pcmk_rc_ok;
+}
- /* if we are expanding ourselves,
- * then no previous value was set and leave int_value as 0
- */
- if (old_value != value) {
- int_value = char2score(old_value);
+/*!
+ * \internal
+ * \brief Remove an XML attribute from an element
+ *
+ * \param[in,out] element XML element that owns \p attr
+ * \param[in,out] attr XML attribute to remove from \p element
+ *
+ * \return Standard Pacemaker return code (\c EPERM if ACLs prevent removal of
+ * attributes from \p element, or \c pcmk_rc_ok otherwise)
+ */
+static int
+remove_xe_attr(xmlNode *element, xmlAttr *attr)
+{
+ if (attr == NULL) {
+ return pcmk_rc_ok;
}
- if (value[name_len + 1] != '+') {
- const char *offset_s = value + (name_len + 2);
+ if (!pcmk__check_acl(element, NULL, pcmk__xf_acl_write)) {
+ // ACLs apply to element, not to particular attributes
+ crm_trace("ACLs prevent removal of attributes from %s element",
+ (const char *) element->name);
+ return EPERM;
+ }
- offset = char2score(offset_s);
+ if (pcmk__tracking_xml_changes(element, false)) {
+ // Leave in place (marked for removal) until after diff is calculated
+ set_parent_flag(element, pcmk__xf_dirty);
+ pcmk__set_xml_flags((xml_node_private_t *) attr->_private,
+ pcmk__xf_deleted);
+ } else {
+ xmlRemoveProp(attr);
}
- int_value += offset;
+ return pcmk_rc_ok;
+}
- if (int_value > INFINITY) {
- int_value = (int)INFINITY;
+/*!
+ * \internal
+ * \brief Remove a named attribute from an XML element
+ *
+ * \param[in,out] element XML element to remove an attribute from
+ * \param[in] name Name of attribute to remove
+ */
+void
+pcmk__xe_remove_attr(xmlNode *element, const char *name)
+{
+ if (name != NULL) {
+ remove_xe_attr(element, xmlHasProp(element, (pcmkXmlStr) name));
}
+}
- crm_xml_add_int(target, name, int_value);
- return;
+/*!
+ * \internal
+ * \brief Remove a named attribute from an XML element
+ *
+ * This is a wrapper for \c pcmk__xe_remove_attr() for use with
+ * \c pcmk__xml_tree_foreach().
+ *
+ * \param[in,out] xml XML element to remove an attribute from
+ * \param[in] user_data Name of attribute to remove
+ *
+ * \return \c true (to continue traversing the tree)
+ *
+ * \note This is compatible with \c pcmk__xml_tree_foreach().
+ */
+bool
+pcmk__xe_remove_attr_cb(xmlNode *xml, void *user_data)
+{
+ const char *name = user_data;
- set_unexpanded:
- if (old_value == value) {
- /* the old value is already set, nothing to do */
- return;
- }
- crm_xml_add(target, name, value);
- return;
+ pcmk__xe_remove_attr(xml, name);
+ return true;
}
/*!
@@ -599,102 +698,92 @@ pcmk__xe_remove_matching_attrs(xmlNode *element,
for (xmlAttrPtr a = pcmk__xe_first_attr(element); a != NULL; a = next) {
next = a->next; // Grab now because attribute might get removed
if ((match == NULL) || match(a, user_data)) {
- if (!pcmk__check_acl(element, NULL, pcmk__xf_acl_write)) {
- crm_trace("ACLs prevent removal of attributes (%s and "
- "possibly others) from %s element",
- (const char *) a->name, (const char *) element->name);
- return; // ACLs apply to element, not particular attributes
- }
-
- if (pcmk__tracking_xml_changes(element, false)) {
- // Leave (marked for removal) until after diff is calculated
- set_parent_flag(element, pcmk__xf_dirty);
- pcmk__set_xml_flags((xml_node_private_t *) a->_private,
- pcmk__xf_deleted);
- } else {
- xmlRemoveProp(a);
+ if (remove_xe_attr(element, a) != pcmk_rc_ok) {
+ return;
}
}
}
}
+/*!
+ * \internal
+ * \brief Create a new XML element under a given parent
+ *
+ * \param[in,out] parent XML element that will be the new element's parent
+ * (\c NULL to create a new XML document with the new
+ * node as root)
+ * \param[in] name Name of new element
+ *
+ * \return Newly created XML element (guaranteed not to be \c NULL)
+ */
xmlNode *
-add_node_copy(xmlNode * parent, xmlNode * src_node)
-{
- xmlNode *child = NULL;
-
- CRM_CHECK((parent != NULL) && (src_node != NULL), return NULL);
-
- child = xmlDocCopyNode(src_node, parent->doc, 1);
- if (child == NULL) {
- return NULL;
- }
- xmlAddChild(parent, child);
- pcmk__mark_xml_created(child);
- return child;
-}
-
-xmlNode *
-create_xml_node(xmlNode * parent, const char *name)
+pcmk__xe_create(xmlNode *parent, const char *name)
{
- xmlDoc *doc = NULL;
xmlNode *node = NULL;
- if (pcmk__str_empty(name)) {
- CRM_CHECK(name != NULL && name[0] == 0, return NULL);
- return NULL;
- }
+ CRM_ASSERT(!pcmk__str_empty(name));
if (parent == NULL) {
- doc = xmlNewDoc((pcmkXmlStr) "1.0");
- if (doc == NULL) {
- return NULL;
- }
+ xmlDoc *doc = xmlNewDoc(PCMK__XML_VERSION);
+
+ pcmk__mem_assert(doc);
node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL);
- if (node == NULL) {
- xmlFreeDoc(doc);
- return NULL;
- }
+ pcmk__mem_assert(node);
+
xmlDocSetRootElement(doc, node);
} else {
node = xmlNewChild(parent, NULL, (pcmkXmlStr) name, NULL);
- if (node == NULL) {
- return NULL;
- }
+ pcmk__mem_assert(node);
}
- pcmk__mark_xml_created(node);
+
+ pcmk__xml_mark_created(node);
return node;
}
-xmlNode *
-pcmk_create_xml_text_node(xmlNode * parent, const char *name, const char *content)
+/*!
+ * \internal
+ * \brief Set a formatted string as an XML node's content
+ *
+ * \param[in,out] node Node whose content to set
+ * \param[in] format <tt>printf(3)</tt>-style format string
+ * \param[in] ... Arguments for \p format
+ *
+ * \note This function escapes special characters. \c xmlNodeSetContent() does
+ * not.
+ */
+G_GNUC_PRINTF(2, 3)
+void
+pcmk__xe_set_content(xmlNode *node, const char *format, ...)
{
- xmlNode *node = create_xml_node(parent, name);
-
if (node != NULL) {
- xmlNodeSetContent(node, (pcmkXmlStr) content);
- }
+ const char *content = NULL;
+ char *buf = NULL;
- return node;
-}
+ if (strchr(format, '%') == NULL) {
+ // Nothing to format
+ content = format;
-xmlNode *
-pcmk_create_html_node(xmlNode * parent, const char *element_name, const char *id,
- const char *class_name, const char *text)
-{
- xmlNode *node = pcmk_create_xml_text_node(parent, element_name, text);
+ } else {
+ va_list ap;
- if (class_name != NULL) {
- crm_xml_add(node, "class", class_name);
- }
+ va_start(ap, format);
- if (id != NULL) {
- crm_xml_add(node, "id", id);
- }
+ if (pcmk__str_eq(format, "%s", pcmk__str_none)) {
+ // No need to make a copy
+ content = va_arg(ap, const char *);
- return node;
+ } else {
+ CRM_ASSERT(vasprintf(&buf, format, ap) >= 0);
+ content = buf;
+ }
+ va_end(ap);
+ }
+
+ xmlNodeSetContent(node, (pcmkXmlStr) content);
+ free(buf);
+ }
}
/*!
@@ -710,72 +799,67 @@ pcmk_free_xml_subtree(xmlNode *xml)
}
static void
-free_xml_with_position(xmlNode * child, int position)
+free_xml_with_position(xmlNode *child, int position)
{
- if (child != NULL) {
- xmlNode *top = NULL;
- xmlDoc *doc = child->doc;
- xml_node_private_t *nodepriv = child->_private;
- xml_doc_private_t *docpriv = NULL;
-
- if (doc != NULL) {
- top = xmlDocGetRootElement(doc);
- }
+ xmlDoc *doc = NULL;
+ xml_node_private_t *nodepriv = NULL;
- if (doc != NULL && top == child) {
- /* Free everything */
- xmlFreeDoc(doc);
+ if (child == NULL) {
+ return;
+ }
+ doc = child->doc;
+ nodepriv = child->_private;
- } else if (pcmk__check_acl(child, NULL, pcmk__xf_acl_write) == FALSE) {
- GString *xpath = NULL;
+ if ((doc != NULL) && (xmlDocGetRootElement(doc) == child)) {
+ // Free everything
+ xmlFreeDoc(doc);
+ return;
+ }
- pcmk__if_tracing({}, return);
- xpath = pcmk__element_xpath(child);
- qb_log_from_external_source(__func__, __FILE__,
- "Cannot remove %s %x", LOG_TRACE,
- __LINE__, 0, (const char *) xpath->str,
- nodepriv->flags);
- g_string_free(xpath, TRUE);
- return;
+ if (!pcmk__check_acl(child, NULL, pcmk__xf_acl_write)) {
+ GString *xpath = NULL;
- } else {
- if (doc && pcmk__tracking_xml_changes(child, FALSE)
- && !pcmk_is_set(nodepriv->flags, pcmk__xf_created)) {
-
- GString *xpath = pcmk__element_xpath(child);
+ pcmk__if_tracing({}, return);
+ xpath = pcmk__element_xpath(child);
+ qb_log_from_external_source(__func__, __FILE__,
+ "Cannot remove %s %x", LOG_TRACE,
+ __LINE__, 0, xpath->str, nodepriv->flags);
+ g_string_free(xpath, TRUE);
+ return;
+ }
- if (xpath != NULL) {
- pcmk__deleted_xml_t *deleted_obj = NULL;
+ if ((doc != NULL) && pcmk__tracking_xml_changes(child, false)
+ && !pcmk_is_set(nodepriv->flags, pcmk__xf_created)) {
- crm_trace("Deleting %s %p from %p",
- (const char *) xpath->str, child, doc);
+ xml_doc_private_t *docpriv = doc->_private;
+ GString *xpath = pcmk__element_xpath(child);
- deleted_obj = calloc(1, sizeof(pcmk__deleted_xml_t));
- deleted_obj->path = strdup((const char *) xpath->str);
+ if (xpath != NULL) {
+ pcmk__deleted_xml_t *deleted_obj = NULL;
- CRM_ASSERT(deleted_obj->path != NULL);
- g_string_free(xpath, TRUE);
+ crm_trace("Deleting %s %p from %p", xpath->str, child, doc);
- deleted_obj->position = -1;
- /* Record the "position" only for XML comments for now */
- if (child->type == XML_COMMENT_NODE) {
- if (position >= 0) {
- deleted_obj->position = position;
+ deleted_obj = pcmk__assert_alloc(1, sizeof(pcmk__deleted_xml_t));
+ deleted_obj->path = g_string_free(xpath, FALSE);
+ deleted_obj->position = -1;
- } else {
- deleted_obj->position = pcmk__xml_position(child,
- pcmk__xf_skip);
- }
- }
+ // Record the position only for XML comments for now
+ if (child->type == XML_COMMENT_NODE) {
+ if (position >= 0) {
+ deleted_obj->position = position;
- docpriv = doc->_private;
- docpriv->deleted_objs = g_list_append(docpriv->deleted_objs, deleted_obj);
- pcmk__set_xml_doc_flag(child, pcmk__xf_dirty);
+ } else {
+ deleted_obj->position = pcmk__xml_position(child,
+ pcmk__xf_skip);
}
}
- pcmk_free_xml_subtree(child);
+
+ docpriv->deleted_objs = g_list_append(docpriv->deleted_objs,
+ deleted_obj);
+ pcmk__set_xml_doc_flag(child, pcmk__xf_dirty);
}
}
+ pcmk_free_xml_subtree(child);
}
@@ -785,171 +869,48 @@ free_xml(xmlNode * child)
free_xml_with_position(child, -1);
}
+/*!
+ * \internal
+ * \brief Make a deep copy of an XML node under a given parent
+ *
+ * \param[in,out] parent XML element that will be the copy's parent (\c NULL
+ * to create a new XML document with the copy as root)
+ * \param[in] src XML node to copy
+ *
+ * \return Deep copy of \p src, or \c NULL if \p src is \c NULL
+ */
xmlNode *
-copy_xml(xmlNode * src)
-{
- xmlDoc *doc = xmlNewDoc((pcmkXmlStr) "1.0");
- xmlNode *copy = xmlDocCopyNode(src, doc, 1);
-
- CRM_ASSERT(copy != NULL);
- xmlDocSetRootElement(doc, copy);
- return copy;
-}
-
-xmlNode *
-string2xml(const char *input)
-{
- xmlNode *xml = NULL;
- xmlDocPtr output = NULL;
- xmlParserCtxtPtr ctxt = NULL;
- const xmlError *last_error = NULL;
-
- if (input == NULL) {
- crm_err("Can't parse NULL input");
- return NULL;
- }
-
- /* create a parser context */
- ctxt = xmlNewParserCtxt();
- CRM_CHECK(ctxt != NULL, return NULL);
-
- xmlCtxtResetLastError(ctxt);
- xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err);
- output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL,
- PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER);
-
- if (output == NULL) {
- output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL,
- PCMK__XML_PARSE_OPTS_WITH_RECOVER);
- if (output) {
- crm_warn("Successfully recovered from XML errors "
- "(note: a future release will treat this as a fatal failure)");
- }
- }
-
- if (output) {
- xml = xmlDocGetRootElement(output);
- }
- last_error = xmlCtxtGetLastError(ctxt);
- if (last_error && last_error->code != XML_ERR_OK) {
- /* crm_abort(__FILE__,__func__,__LINE__, "last_error->code != XML_ERR_OK", TRUE, TRUE); */
- /*
- * http://xmlsoft.org/html/libxml-xmlerror.html#xmlErrorLevel
- * http://xmlsoft.org/html/libxml-xmlerror.html#xmlParserErrors
- */
- crm_warn("Parsing failed (domain=%d, level=%d, code=%d): %s",
- last_error->domain, last_error->level, last_error->code, last_error->message);
-
- if (last_error->code == XML_ERR_DOCUMENT_EMPTY) {
- CRM_LOG_ASSERT("Cannot parse an empty string");
-
- } else if (last_error->code != XML_ERR_DOCUMENT_END) {
- crm_err("Couldn't%s parse %d chars: %s", xml ? " fully" : "", (int)strlen(input),
- input);
- if (xml != NULL) {
- crm_log_xml_err(xml, "Partial");
- }
-
- } else {
- int len = strlen(input);
- int lpc = 0;
-
- while(lpc < len) {
- crm_warn("Parse error[+%.3d]: %.80s", lpc, input+lpc);
- lpc += 80;
- }
-
- CRM_LOG_ASSERT("String parsing error");
- }
- }
-
- xmlFreeParserCtxt(ctxt);
- return xml;
-}
-
-xmlNode *
-stdin2xml(void)
-{
- size_t data_length = 0;
- size_t read_chars = 0;
-
- char *xml_buffer = NULL;
- xmlNode *xml_obj = NULL;
-
- do {
- xml_buffer = pcmk__realloc(xml_buffer, data_length + PCMK__BUFFER_SIZE);
- read_chars = fread(xml_buffer + data_length, 1, PCMK__BUFFER_SIZE,
- stdin);
- data_length += read_chars;
- } while (read_chars == PCMK__BUFFER_SIZE);
-
- if (data_length == 0) {
- crm_warn("No XML supplied on stdin");
- free(xml_buffer);
- return NULL;
- }
-
- xml_buffer[data_length] = '\0';
- xml_obj = string2xml(xml_buffer);
- free(xml_buffer);
-
- crm_log_xml_trace(xml_obj, "Created fragment");
- return xml_obj;
-}
-
-static char *
-decompress_file(const char *filename)
+pcmk__xml_copy(xmlNode *parent, xmlNode *src)
{
- char *buffer = NULL;
- int rc = 0;
- size_t length = 0, read_len = 0;
- BZFILE *bz_file = NULL;
- FILE *input = fopen(filename, "r");
+ xmlNode *copy = NULL;
- if (input == NULL) {
- crm_perror(LOG_ERR, "Could not open %s for reading", filename);
+ if (src == NULL) {
return NULL;
}
- bz_file = BZ2_bzReadOpen(&rc, input, 0, 0, NULL, 0);
- rc = pcmk__bzlib2rc(rc);
-
- if (rc != pcmk_rc_ok) {
- crm_err("Could not prepare to read compressed %s: %s "
- CRM_XS " rc=%d", filename, pcmk_rc_str(rc), rc);
- BZ2_bzReadClose(&rc, bz_file);
- fclose(input);
- return NULL;
- }
+ if (parent == NULL) {
+ xmlDoc *doc = NULL;
- rc = BZ_OK;
- // cppcheck seems not to understand the abort-logic in pcmk__realloc
- // cppcheck-suppress memleak
- while (rc == BZ_OK) {
- buffer = pcmk__realloc(buffer, PCMK__BUFFER_SIZE + length + 1);
- read_len = BZ2_bzRead(&rc, bz_file, buffer + length, PCMK__BUFFER_SIZE);
+ // The copy will be the root element of a new document
+ CRM_ASSERT(src->type == XML_ELEMENT_NODE);
- crm_trace("Read %ld bytes from file: %d", (long)read_len, rc);
+ doc = xmlNewDoc(PCMK__XML_VERSION);
+ pcmk__mem_assert(doc);
- if (rc == BZ_OK || rc == BZ_STREAM_END) {
- length += read_len;
- }
- }
+ copy = xmlDocCopyNode(src, doc, 1);
+ pcmk__mem_assert(copy);
- buffer[length] = '\0';
+ xmlDocSetRootElement(doc, copy);
- rc = pcmk__bzlib2rc(rc);
+ } else {
+ copy = xmlDocCopyNode(src, parent->doc, 1);
+ pcmk__mem_assert(copy);
- if (rc != pcmk_rc_ok) {
- crm_err("Could not read compressed %s: %s " CRM_XS " rc=%d",
- filename, pcmk_rc_str(rc), rc);
- free(buffer);
- buffer = NULL;
+ xmlAddChild(parent, copy);
}
- BZ2_bzReadClose(&rc, bz_file);
- fclose(input);
- return buffer;
+ pcmk__xml_mark_created(copy);
+ return copy;
}
/*!
@@ -986,97 +947,6 @@ pcmk__strip_xml_text(xmlNode *xml)
}
}
-xmlNode *
-filename2xml(const char *filename)
-{
- xmlNode *xml = NULL;
- xmlDocPtr output = NULL;
- bool uncompressed = true;
- xmlParserCtxtPtr ctxt = NULL;
- const xmlError *last_error = NULL;
-
- /* create a parser context */
- ctxt = xmlNewParserCtxt();
- CRM_CHECK(ctxt != NULL, return NULL);
-
- xmlCtxtResetLastError(ctxt);
- xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err);
-
- if (filename) {
- uncompressed = !pcmk__ends_with_ext(filename, ".bz2");
- }
-
- if (pcmk__str_eq(filename, "-", pcmk__str_null_matches)) {
- /* STDIN_FILENO == fileno(stdin) */
- output = xmlCtxtReadFd(ctxt, STDIN_FILENO, "unknown.xml", NULL,
- PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER);
-
- if (output == NULL) {
- output = xmlCtxtReadFd(ctxt, STDIN_FILENO, "unknown.xml", NULL,
- PCMK__XML_PARSE_OPTS_WITH_RECOVER);
- if (output) {
- crm_warn("Successfully recovered from XML errors "
- "(note: a future release will treat this as a fatal failure)");
- }
- }
-
- } else if (uncompressed) {
- output = xmlCtxtReadFile(ctxt, filename, NULL,
- PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER);
-
- if (output == NULL) {
- output = xmlCtxtReadFile(ctxt, filename, NULL,
- PCMK__XML_PARSE_OPTS_WITH_RECOVER);
- if (output) {
- crm_warn("Successfully recovered from XML errors "
- "(note: a future release will treat this as a fatal failure)");
- }
- }
-
- } else {
- char *input = decompress_file(filename);
-
- output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL,
- PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER);
-
- if (output == NULL) {
- output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL,
- PCMK__XML_PARSE_OPTS_WITH_RECOVER);
- if (output) {
- crm_warn("Successfully recovered from XML errors "
- "(note: a future release will treat this as a fatal failure)");
- }
- }
-
- free(input);
- }
-
- if (output && (xml = xmlDocGetRootElement(output))) {
- pcmk__strip_xml_text(xml);
- }
-
- last_error = xmlCtxtGetLastError(ctxt);
- if (last_error && last_error->code != XML_ERR_OK) {
- /* crm_abort(__FILE__,__func__,__LINE__, "last_error->code != XML_ERR_OK", TRUE, TRUE); */
- /*
- * http://xmlsoft.org/html/libxml-xmlerror.html#xmlErrorLevel
- * http://xmlsoft.org/html/libxml-xmlerror.html#xmlParserErrors
- */
- crm_err("Parsing failed (domain=%d, level=%d, code=%d): %s",
- last_error->domain, last_error->level, last_error->code, last_error->message);
-
- if (last_error && last_error->code != XML_ERR_OK) {
- crm_err("Couldn't%s parse %s", xml ? " fully" : "", filename);
- if (xml != NULL) {
- crm_log_xml_err(xml, "Partial");
- }
- }
- }
-
- xmlFreeParserCtxt(ctxt);
- return xml;
-}
-
/*!
* \internal
* \brief Add a "last written" attribute to an XML element, set to current time
@@ -1091,7 +961,7 @@ pcmk__xe_add_last_written(xmlNode *xe)
char *now_s = pcmk__epoch2str(NULL, 0);
const char *result = NULL;
- result = crm_xml_add(xe, XML_CIB_ATTR_WRITTEN,
+ result = crm_xml_add(xe, PCMK_XA_CIB_LAST_WRITTEN,
pcmk__s(now_s, "Could not determine current time"));
free(now_s);
return result;
@@ -1138,598 +1008,202 @@ crm_xml_set_id(xmlNode *xml, const char *format, ...)
CRM_ASSERT(len > 0);
crm_xml_sanitize_id(id);
- crm_xml_add(xml, XML_ATTR_ID, id);
+ crm_xml_add(xml, PCMK_XA_ID, id);
free(id);
}
/*!
* \internal
- * \brief Write XML to a file stream
+ * \brief Check whether a string has XML special characters that must be escaped
*
- * \param[in] xml XML to write
- * \param[in] filename Name of file being written (for logging only)
- * \param[in,out] stream Open file stream corresponding to filename
- * \param[in] compress Whether to compress XML before writing
- * \param[out] nbytes Number of bytes written
+ * See \c pcmk__xml_escape() and \c pcmk__xml_escape_type for more details.
*
- * \return Standard Pacemaker return code
- */
-static int
-write_xml_stream(const xmlNode *xml, const char *filename, FILE *stream,
- bool compress, unsigned int *nbytes)
-{
- int rc = pcmk_rc_ok;
- char *buffer = NULL;
-
- *nbytes = 0;
- crm_log_xml_trace(xml, "writing");
-
- buffer = dump_xml_formatted(xml);
- CRM_CHECK(buffer && strlen(buffer),
- crm_log_xml_warn(xml, "formatting failed");
- rc = pcmk_rc_error;
- goto bail);
-
- if (compress) {
- unsigned int in = 0;
- BZFILE *bz_file = NULL;
-
- rc = BZ_OK;
- bz_file = BZ2_bzWriteOpen(&rc, stream, 5, 0, 30);
- rc = pcmk__bzlib2rc(rc);
-
- if (rc != pcmk_rc_ok) {
- crm_warn("Not compressing %s: could not prepare file stream: %s "
- CRM_XS " rc=%d", filename, pcmk_rc_str(rc), rc);
- } else {
- BZ2_bzWrite(&rc, bz_file, buffer, strlen(buffer));
- rc = pcmk__bzlib2rc(rc);
-
- if (rc != pcmk_rc_ok) {
- crm_warn("Not compressing %s: could not compress data: %s "
- CRM_XS " rc=%d errno=%d",
- filename, pcmk_rc_str(rc), rc, errno);
- }
- }
-
- if (rc == pcmk_rc_ok) {
- BZ2_bzWriteClose(&rc, bz_file, 0, &in, nbytes);
- rc = pcmk__bzlib2rc(rc);
-
- if (rc != pcmk_rc_ok) {
- crm_warn("Not compressing %s: could not write compressed data: %s "
- CRM_XS " rc=%d errno=%d",
- filename, pcmk_rc_str(rc), rc, errno);
- *nbytes = 0; // retry without compression
- } else {
- crm_trace("Compressed XML for %s from %u bytes to %u",
- filename, in, *nbytes);
- }
- }
- rc = pcmk_rc_ok; // Either true, or we'll retry without compression
- }
-
- if (*nbytes == 0) {
- rc = fprintf(stream, "%s", buffer);
- if (rc < 0) {
- rc = errno;
- crm_perror(LOG_ERR, "writing %s", filename);
- } else {
- *nbytes = (unsigned int) rc;
- rc = pcmk_rc_ok;
- }
- }
-
- bail:
-
- if (fflush(stream) != 0) {
- rc = errno;
- crm_perror(LOG_ERR, "flushing %s", filename);
- }
-
- /* Don't report error if the file does not support synchronization */
- if (fsync(fileno(stream)) < 0 && errno != EROFS && errno != EINVAL) {
- rc = errno;
- crm_perror(LOG_ERR, "synchronizing %s", filename);
- }
-
- fclose(stream);
-
- crm_trace("Saved %d bytes to %s as XML", *nbytes, filename);
- free(buffer);
-
- return rc;
-}
-
-/*!
- * \brief Write XML to a file descriptor
- *
- * \param[in] xml XML to write
- * \param[in] filename Name of file being written (for logging only)
- * \param[in] fd Open file descriptor corresponding to filename
- * \param[in] compress Whether to compress XML before writing
+ * \param[in] text String to check
+ * \param[in] type Type of escaping
*
- * \return Number of bytes written on success, -errno otherwise
+ * \return \c true if \p text has special characters that need to be escaped, or
+ * \c false otherwise
*/
-int
-write_xml_fd(const xmlNode *xml, const char *filename, int fd,
- gboolean compress)
-{
- FILE *stream = NULL;
- unsigned int nbytes = 0;
- int rc = pcmk_rc_ok;
-
- CRM_CHECK((xml != NULL) && (fd > 0), return -EINVAL);
- stream = fdopen(fd, "w");
- if (stream == NULL) {
- return -errno;
- }
- rc = write_xml_stream(xml, filename, stream, compress, &nbytes);
- if (rc != pcmk_rc_ok) {
- return pcmk_rc2legacy(rc);
- }
- return (int) nbytes;
-}
-
-/*!
- * \brief Write XML to a file
- *
- * \param[in] xml XML to write
- * \param[in] filename Name of file to write
- * \param[in] compress Whether to compress XML before writing
- *
- * \return Number of bytes written on success, -errno otherwise
- */
-int
-write_xml_file(const xmlNode *xml, const char *filename, gboolean compress)
+bool
+pcmk__xml_needs_escape(const char *text, enum pcmk__xml_escape_type type)
{
- FILE *stream = NULL;
- unsigned int nbytes = 0;
- int rc = pcmk_rc_ok;
+ if (text == NULL) {
+ return false;
+ }
+
+ while (*text != '\0') {
+ switch (type) {
+ case pcmk__xml_escape_text:
+ switch (*text) {
+ case '<':
+ case '>':
+ case '&':
+ return true;
+ case '\n':
+ case '\t':
+ break;
+ default:
+ if (g_ascii_iscntrl(*text)) {
+ return true;
+ }
+ break;
+ }
+ break;
- CRM_CHECK((xml != NULL) && (filename != NULL), return -EINVAL);
- stream = fopen(filename, "w");
- if (stream == NULL) {
- return -errno;
- }
- rc = write_xml_stream(xml, filename, stream, compress, &nbytes);
- if (rc != pcmk_rc_ok) {
- return pcmk_rc2legacy(rc);
- }
- return (int) nbytes;
-}
+ case pcmk__xml_escape_attr:
+ switch (*text) {
+ case '<':
+ case '>':
+ case '&':
+ case '"':
+ return true;
+ default:
+ if (g_ascii_iscntrl(*text)) {
+ return true;
+ }
+ break;
+ }
+ break;
-// Replace a portion of a dynamically allocated string (reallocating memory)
-static char *
-replace_text(char *text, int start, size_t *length, const char *replace)
-{
- size_t offset = strlen(replace) - 1; // We have space for 1 char already
+ case pcmk__xml_escape_attr_pretty:
+ switch (*text) {
+ case '\n':
+ case '\r':
+ case '\t':
+ case '"':
+ return true;
+ default:
+ break;
+ }
+ break;
- *length += offset;
- text = pcmk__realloc(text, *length);
+ default: // Invalid enum value
+ CRM_ASSERT(false);
+ break;
+ }
- for (size_t lpc = (*length) - 1; lpc > (start + offset); lpc--) {
- text[lpc] = text[lpc - offset];
+ text = g_utf8_next_char(text);
}
-
- memcpy(text + start, replace, offset + 1);
- return text;
+ return false;
}
/*!
+ * \internal
* \brief Replace special characters with their XML escape sequences
*
* \param[in] text Text to escape
+ * \param[in] type Type of escaping
*
* \return Newly allocated string equivalent to \p text but with special
- * characters replaced with XML escape sequences (or NULL if \p text
- * is NULL)
+ * characters replaced with XML escape sequences (or \c NULL if \p text
+ * is \c NULL). If \p text is not \c NULL, the return value is
+ * guaranteed not to be \c NULL.
+ *
+ * \note There are libxml functions that purport to do this:
+ * \c xmlEncodeEntitiesReentrant() and \c xmlEncodeSpecialChars().
+ * However, their escaping is incomplete. See:
+ * https://discourse.gnome.org/t/intended-use-of-xmlencodeentitiesreentrant-vs-xmlencodespecialchars/19252
+ * \note The caller is responsible for freeing the return value using
+ * \c g_free().
*/
-char *
-crm_xml_escape(const char *text)
+gchar *
+pcmk__xml_escape(const char *text, enum pcmk__xml_escape_type type)
{
- size_t length;
- char *copy;
-
- /*
- * When xmlCtxtReadDoc() parses &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.
- */
+ GString *copy = NULL;
if (text == NULL) {
return NULL;
}
+ copy = g_string_sized_new(strlen(text));
- length = 1 + strlen(text);
- copy = strdup(text);
- CRM_ASSERT(copy != NULL);
- for (size_t index = 0; index < length; index++) {
- if(copy[index] & 0x80 && copy[index+1] & 0x80){
- index++;
- break;
- }
- switch (copy[index]) {
- case 0:
- break;
- case '<':
- copy = replace_text(copy, index, &length, "&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]);
+ while (*text != '\0') {
+ // Don't escape any non-ASCII characters
+ if ((*text & 0x80) != 0) {
+ size_t bytes = g_utf8_next_char(text) - text;
- copy = replace_text(copy, index, &length, replace);
- free(replace);
- }
- }
- }
- return copy;
-}
-
-/*!
- * \internal
- * \brief Append a string representation of an XML element to a buffer
- *
- * \param[in] data XML whose representation to append
- * \param[in] options Group of \p pcmk__xml_fmt_options flags
- * \param[in,out] buffer Where to append the content (must not be \p NULL)
- * \param[in] depth Current indentation level
- */
-static void
-dump_xml_element(const xmlNode *data, uint32_t options, GString *buffer,
- int depth)
-{
- bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty);
- bool filtered = pcmk_is_set(options, pcmk__xml_fmt_filtered);
- int spaces = pretty? (2 * depth) : 0;
-
- for (int lpc = 0; lpc < spaces; lpc++) {
- g_string_append_c(buffer, ' ');
- }
-
- pcmk__g_strcat(buffer, "<", data->name, NULL);
-
- for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL;
- attr = attr->next) {
-
- if (!filtered || !pcmk__xa_filterable((const char *) (attr->name))) {
- pcmk__dump_xml_attr(attr, buffer);
+ g_string_append_len(copy, text, bytes);
+ text += bytes;
+ continue;
}
- }
-
- if (data->children == NULL) {
- g_string_append(buffer, "/>");
-
- } else {
- g_string_append_c(buffer, '>');
- }
- if (pretty) {
- g_string_append_c(buffer, '\n');
- }
-
- if (data->children) {
- for (const xmlNode *child = data->children; child != NULL;
- child = child->next) {
- pcmk__xml2text(child, options, buffer, depth + 1);
- }
+ switch (type) {
+ case pcmk__xml_escape_text:
+ switch (*text) {
+ case '<':
+ g_string_append(copy, PCMK__XML_ENTITY_LT);
+ break;
+ case '>':
+ g_string_append(copy, PCMK__XML_ENTITY_GT);
+ break;
+ case '&':
+ g_string_append(copy, PCMK__XML_ENTITY_AMP);
+ break;
+ case '\n':
+ case '\t':
+ g_string_append_c(copy, *text);
+ break;
+ default:
+ if (g_ascii_iscntrl(*text)) {
+ g_string_append_printf(copy, "&#x%.2X;", *text);
+ } else {
+ g_string_append_c(copy, *text);
+ }
+ break;
+ }
+ break;
- for (int lpc = 0; lpc < spaces; lpc++) {
- g_string_append_c(buffer, ' ');
- }
+ case pcmk__xml_escape_attr:
+ switch (*text) {
+ case '<':
+ g_string_append(copy, PCMK__XML_ENTITY_LT);
+ break;
+ case '>':
+ g_string_append(copy, PCMK__XML_ENTITY_GT);
+ break;
+ case '&':
+ g_string_append(copy, PCMK__XML_ENTITY_AMP);
+ break;
+ case '"':
+ g_string_append(copy, PCMK__XML_ENTITY_QUOT);
+ break;
+ default:
+ if (g_ascii_iscntrl(*text)) {
+ g_string_append_printf(copy, "&#x%.2X;", *text);
+ } else {
+ g_string_append_c(copy, *text);
+ }
+ break;
+ }
+ break;
- pcmk__g_strcat(buffer, "</", data->name, ">", NULL);
+ case pcmk__xml_escape_attr_pretty:
+ switch (*text) {
+ case '"':
+ g_string_append(copy, "\\\"");
+ break;
+ case '\n':
+ g_string_append(copy, "\\n");
+ break;
+ case '\r':
+ g_string_append(copy, "\\r");
+ break;
+ case '\t':
+ g_string_append(copy, "\\t");
+ break;
+ default:
+ g_string_append_c(copy, *text);
+ break;
+ }
+ break;
- if (pretty) {
- g_string_append_c(buffer, '\n');
+ default: // Invalid enum value
+ CRM_ASSERT(false);
+ break;
}
- }
-}
-
-/*!
- * \internal
- * \brief Append XML text content to a buffer
- *
- * \param[in] data XML whose content to append
- * \param[in] options Group of \p xml_log_options flags
- * \param[in,out] buffer Where to append the content (must not be \p NULL)
- * \param[in] depth Current indentation level
- */
-static void
-dump_xml_text(const xmlNode *data, uint32_t options, GString *buffer,
- int depth)
-{
- /* @COMPAT: Remove when log_data_element() is removed. There are no internal
- * code paths to this, except through the deprecated log_data_element().
- */
- bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty);
- int spaces = pretty? (2 * depth) : 0;
-
- for (int lpc = 0; lpc < spaces; lpc++) {
- g_string_append_c(buffer, ' ');
- }
-
- g_string_append(buffer, (const gchar *) data->content);
-
- if (pretty) {
- g_string_append_c(buffer, '\n');
- }
-}
-
-/*!
- * \internal
- * \brief Append XML CDATA content to a buffer
- *
- * \param[in] data XML whose content to append
- * \param[in] options Group of \p pcmk__xml_fmt_options flags
- * \param[in,out] buffer Where to append the content (must not be \p NULL)
- * \param[in] depth Current indentation level
- */
-static void
-dump_xml_cdata(const xmlNode *data, uint32_t options, GString *buffer,
- int depth)
-{
- bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty);
- int spaces = pretty? (2 * depth) : 0;
-
- for (int lpc = 0; lpc < spaces; lpc++) {
- g_string_append_c(buffer, ' ');
- }
-
- pcmk__g_strcat(buffer, "<![CDATA[", (const char *) data->content, "]]>",
- NULL);
- if (pretty) {
- g_string_append_c(buffer, '\n');
+ text = g_utf8_next_char(text);
}
-}
-
-/*!
- * \internal
- * \brief Append an XML comment to a buffer
- *
- * \param[in] data XML whose content to append
- * \param[in] options Group of \p pcmk__xml_fmt_options flags
- * \param[in,out] buffer Where to append the content (must not be \p NULL)
- * \param[in] depth Current indentation level
- */
-static void
-dump_xml_comment(const xmlNode *data, uint32_t options, GString *buffer,
- int depth)
-{
- bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty);
- int spaces = pretty? (2 * depth) : 0;
-
- for (int lpc = 0; lpc < spaces; lpc++) {
- g_string_append_c(buffer, ' ');
- }
-
- pcmk__g_strcat(buffer, "<!--", (const char *) data->content, "-->", NULL);
-
- if (pretty) {
- g_string_append_c(buffer, '\n');
- }
-}
-
-/*!
- * \internal
- * \brief Get a string representation of an XML element type
- *
- * \param[in] type XML element type
- *
- * \return String representation of \p type
- */
-static const char *
-xml_element_type2str(xmlElementType type)
-{
- static const char *const element_type_names[] = {
- [XML_ELEMENT_NODE] = "element",
- [XML_ATTRIBUTE_NODE] = "attribute",
- [XML_TEXT_NODE] = "text",
- [XML_CDATA_SECTION_NODE] = "CDATA section",
- [XML_ENTITY_REF_NODE] = "entity reference",
- [XML_ENTITY_NODE] = "entity",
- [XML_PI_NODE] = "PI",
- [XML_COMMENT_NODE] = "comment",
- [XML_DOCUMENT_NODE] = "document",
- [XML_DOCUMENT_TYPE_NODE] = "document type",
- [XML_DOCUMENT_FRAG_NODE] = "document fragment",
- [XML_NOTATION_NODE] = "notation",
- [XML_HTML_DOCUMENT_NODE] = "HTML document",
- [XML_DTD_NODE] = "DTD",
- [XML_ELEMENT_DECL] = "element declaration",
- [XML_ATTRIBUTE_DECL] = "attribute declaration",
- [XML_ENTITY_DECL] = "entity declaration",
- [XML_NAMESPACE_DECL] = "namespace declaration",
- [XML_XINCLUDE_START] = "XInclude start",
- [XML_XINCLUDE_END] = "XInclude end",
- };
-
- if ((type < 0) || (type >= PCMK__NELEM(element_type_names))) {
- return "unrecognized type";
- }
- return element_type_names[type];
-}
-
-/*!
- * \internal
- * \brief Create a text representation of an XML object
- *
- * \param[in] data XML to convert
- * \param[in] options Group of \p pcmk__xml_fmt_options flags
- * \param[in,out] buffer Where to store the text (must not be \p NULL)
- * \param[in] depth Current indentation level
- */
-void
-pcmk__xml2text(const xmlNode *data, uint32_t options, GString *buffer,
- int depth)
-{
- if (data == NULL) {
- crm_trace("Nothing to dump");
- return;
- }
-
- CRM_ASSERT(buffer != NULL);
- CRM_CHECK(depth >= 0, depth = 0);
-
- switch(data->type) {
- case XML_ELEMENT_NODE:
- /* Handle below */
- dump_xml_element(data, options, buffer, depth);
- break;
- case XML_TEXT_NODE:
- if (pcmk_is_set(options, pcmk__xml_fmt_text)) {
- dump_xml_text(data, options, buffer, depth);
- }
- break;
- case XML_COMMENT_NODE:
- dump_xml_comment(data, options, buffer, depth);
- break;
- case XML_CDATA_SECTION_NODE:
- dump_xml_cdata(data, options, buffer, depth);
- break;
- default:
- crm_warn("Cannot convert XML %s node to text " CRM_XS " type=%d",
- xml_element_type2str(data->type), data->type);
- break;
- }
-}
-
-char *
-dump_xml_formatted_with_text(const xmlNode *xml)
-{
- /* libxml's xmlNodeDumpOutput() would work here since we're not specifically
- * filtering out any nodes. However, use pcmk__xml2text() for consistency,
- * to escape attribute values, and to allow a const argument.
- */
- char *buffer = NULL;
- GString *g_buffer = g_string_sized_new(1024);
-
- pcmk__xml2text(xml, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, g_buffer, 0);
-
- pcmk__str_update(&buffer, g_buffer->str);
- g_string_free(g_buffer, TRUE);
- return buffer;
-}
-
-char *
-dump_xml_formatted(const xmlNode *xml)
-{
- char *buffer = NULL;
- GString *g_buffer = g_string_sized_new(1024);
-
- pcmk__xml2text(xml, pcmk__xml_fmt_pretty, g_buffer, 0);
-
- pcmk__str_update(&buffer, g_buffer->str);
- g_string_free(g_buffer, TRUE);
- return buffer;
-}
-
-char *
-dump_xml_unformatted(const xmlNode *xml)
-{
- char *buffer = NULL;
- GString *g_buffer = g_string_sized_new(1024);
-
- pcmk__xml2text(xml, 0, g_buffer, 0);
-
- pcmk__str_update(&buffer, g_buffer->str);
- g_string_free(g_buffer, TRUE);
- return buffer;
-}
-
-int
-pcmk__xml2fd(int fd, xmlNode *cur)
-{
- bool success;
-
- xmlOutputBuffer *fd_out = xmlOutputBufferCreateFd(fd, NULL);
- CRM_ASSERT(fd_out != NULL);
- xmlNodeDumpOutput(fd_out, cur->doc, cur, 0, pcmk__xml_fmt_pretty, NULL);
-
- success = xmlOutputBufferWrite(fd_out, sizeof("\n") - 1, "\n") != -1;
-
- success = xmlOutputBufferClose(fd_out) != -1 && success;
-
- if (!success) {
- return EIO;
- }
-
- fsync(fd);
- return pcmk_rc_ok;
-}
-
-void
-xml_remove_prop(xmlNode * obj, const char *name)
-{
- if (crm_element_value(obj, name) == NULL) {
- return;
- }
-
- if (pcmk__check_acl(obj, NULL, pcmk__xf_acl_write) == FALSE) {
- crm_trace("Cannot remove %s from %s", name, obj->name);
-
- } else if (pcmk__tracking_xml_changes(obj, FALSE)) {
- /* Leave in place (marked for removal) until after the diff is calculated */
- xmlAttr *attr = xmlHasProp(obj, (pcmkXmlStr) name);
- xml_node_private_t *nodepriv = attr->_private;
-
- set_parent_flag(obj, pcmk__xf_dirty);
- pcmk__set_xml_flags(nodepriv, pcmk__xf_deleted);
- } else {
- xmlUnsetProp(obj, (pcmkXmlStr) name);
- }
-}
-
-void
-save_xml_to_file(const xmlNode *xml, const char *desc, const char *filename)
-{
- char *f = NULL;
-
- if (filename == NULL) {
- char *uuid = crm_generate_uuid();
-
- f = crm_strdup_printf("%s/%s", pcmk__get_tmpdir(), uuid);
- filename = f;
- free(uuid);
- }
-
- crm_info("Saving %s to %s", desc, filename);
- write_xml_file(xml, filename, FALSE);
- free(f);
+ return g_string_free(copy, FALSE);
}
/*!
@@ -1781,7 +1255,7 @@ mark_attr_deleted(xmlNode *new_xml, const char *element, const char *attr_name,
nodepriv->flags = 0;
// Check ACLs and mark restored value for later removal
- xml_remove_prop(new_xml, attr_name);
+ remove_xe_attr(new_xml, attr);
crm_trace("XML attribute %s=%s was removed from %s",
attr_name, old_value, element);
@@ -1955,10 +1429,10 @@ static void
mark_child_deleted(xmlNode *old_child, xmlNode *new_parent)
{
// Re-create the child element so we can check ACLs
- xmlNode *candidate = add_node_copy(new_parent, old_child);
+ xmlNode *candidate = pcmk__xml_copy(new_parent, old_child);
// Clear flags on new child and its children
- reset_xml_node_flags(candidate);
+ pcmk__xml_tree_foreach(candidate, reset_xml_node_flags, NULL);
// Check whether ACLs allow the deletion
pcmk__apply_acl(xmlDocGetRootElement(candidate->doc));
@@ -1979,8 +1453,9 @@ mark_child_moved(xmlNode *old_child, xmlNode *new_parent, xmlNode *new_child,
{
xml_node_private_t *nodepriv = new_child->_private;
- crm_trace("Child element %s with id='%s' moved from position %d to %d under %s",
- new_child->name, (ID(new_child)? ID(new_child) : "<no id>"),
+ crm_trace("Child element %s with "
+ PCMK_XA_ID "='%s' moved from position %d to %d under %s",
+ new_child->name, pcmk__s(pcmk__xe_id(new_child), "<no id>"),
p_old, p_new, new_parent->name);
pcmk__mark_xml_node_dirty(new_parent);
pcmk__set_xml_flags(nodepriv, pcmk__xf_moved);
@@ -1997,12 +1472,13 @@ mark_child_moved(xmlNode *old_child, xmlNode *new_parent, xmlNode *new_child,
static void
mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top)
{
- xmlNode *cIter = NULL;
+ xmlNode *old_child = NULL;
+ xmlNode *new_child = NULL;
xml_node_private_t *nodepriv = NULL;
CRM_CHECK(new_xml != NULL, return);
if (old_xml == NULL) {
- pcmk__mark_xml_created(new_xml);
+ pcmk__xml_mark_created(new_xml);
pcmk__apply_creation_acl(new_xml, check_top);
return;
}
@@ -2019,13 +1495,13 @@ mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top)
xml_diff_attrs(old_xml, new_xml);
// Check for differences in the original children
- for (cIter = pcmk__xml_first_child(old_xml); cIter != NULL; ) {
- xmlNode *old_child = cIter;
- xmlNode *new_child = pcmk__xml_match(new_xml, cIter, true);
+ for (old_child = pcmk__xml_first_child(old_xml); old_child != NULL;
+ old_child = pcmk__xml_next(old_child)) {
+
+ new_child = pcmk__xml_match(new_xml, old_child, true);
- cIter = pcmk__xml_next(cIter);
- if(new_child) {
- mark_xml_changes(old_child, new_child, TRUE);
+ if (new_child != NULL) {
+ mark_xml_changes(old_child, new_child, true);
} else {
mark_child_deleted(old_child, new_xml);
@@ -2033,16 +1509,19 @@ mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top)
}
// Check for moved or created children
- for (cIter = pcmk__xml_first_child(new_xml); cIter != NULL; ) {
- xmlNode *new_child = cIter;
- xmlNode *old_child = pcmk__xml_match(old_xml, cIter, true);
+ new_child = pcmk__xml_first_child(new_xml);
+ while (new_child != NULL) {
+ xmlNode *next = pcmk__xml_next(new_child);
+
+ old_child = pcmk__xml_match(old_xml, new_child, true);
- cIter = pcmk__xml_next(cIter);
- if(old_child == NULL) {
+ if (old_child == NULL) {
// This is a newly created child
nodepriv = new_child->_private;
pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
- mark_xml_changes(old_child, new_child, TRUE);
+
+ // May free new_child
+ mark_xml_changes(old_child, new_child, true);
} else {
/* Check for movement, we already checked for differences */
@@ -2053,6 +1532,8 @@ mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top)
mark_child_moved(old_child, new_xml, new_child, p_old, p_new);
}
}
+
+ new_child = next;
}
}
@@ -2069,7 +1550,8 @@ xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml)
{
CRM_CHECK((old_xml != NULL) && (new_xml != NULL)
&& pcmk__xe_is(old_xml, (const char *) new_xml->name)
- && pcmk__str_eq(ID(old_xml), ID(new_xml), pcmk__str_none),
+ && pcmk__str_eq(pcmk__xe_id(old_xml), pcmk__xe_id(new_xml),
+ pcmk__str_none),
return);
if(xml_tracking_changes(new_xml) == FALSE) {
@@ -2079,44 +1561,6 @@ xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml)
mark_xml_changes(old_xml, new_xml, FALSE);
}
-gboolean
-can_prune_leaf(xmlNode * xml_node)
-{
- xmlNode *cIter = NULL;
- gboolean can_prune = TRUE;
-
- CRM_CHECK(xml_node != NULL, return FALSE);
-
- if (pcmk__strcase_any_of((const char *) xml_node->name,
- XML_TAG_RESOURCE_REF, XML_CIB_TAG_OBJ_REF,
- XML_ACL_TAG_ROLE_REF, XML_ACL_TAG_ROLE_REFv1,
- NULL)) {
- return FALSE;
- }
-
- for (xmlAttrPtr a = pcmk__xe_first_attr(xml_node); a != NULL; a = a->next) {
- const char *p_name = (const char *) a->name;
-
- if (strcmp(p_name, XML_ATTR_ID) == 0) {
- continue;
- }
- can_prune = FALSE;
- }
-
- cIter = pcmk__xml_first_child(xml_node);
- while (cIter) {
- xmlNode *child = cIter;
-
- cIter = pcmk__xml_next(cIter);
- if (can_prune_leaf(child)) {
- free_xml(child);
- } else {
- can_prune = FALSE;
- }
- }
- return can_prune;
-}
-
/*!
* \internal
* \brief Find a comment with matching content in specified XML
@@ -2185,7 +1629,7 @@ pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update)
}
if (target == NULL) {
- add_node_copy(parent, update);
+ pcmk__xml_copy(parent, update);
} else if (!pcmk__str_eq((const char *)target->content, (const char *)update->content, pcmk__str_casei)) {
xmlFree(target->content);
@@ -2195,82 +1639,113 @@ pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update)
/*!
* \internal
- * \brief Make one XML tree match another (in children and attributes)
+ * \brief Merge one XML tree into another
+ *
+ * Here, "merge" means:
+ * 1. Copy attribute values from \p update to the target, overwriting in case of
+ * conflict.
+ * 2. Descend through \p update and the target in parallel. At each level, for
+ * each child of \p update, look for a matching child of the target.
+ * a. For each child, if a match is found, go to step 1, recursively merging
+ * the child of \p update into the child of the target.
+ * b. Otherwise, copy the child of \p update as a child of the target.
+ *
+ * A match is defined as the first child of the same type within the target,
+ * with:
+ * * the \c PCMK_XA_ID attribute matching, if set in \p update; otherwise,
+ * * the \c PCMK_XA_ID_REF attribute matching, if set in \p update
+ *
+ * This function does not delete any elements or attributes from the target. It
+ * may add elements or overwrite attributes, as described above.
*
* \param[in,out] parent If \p target is NULL and this is not, add or update
* child of this XML node that matches \p update
* \param[in,out] target If not NULL, update this XML
- * \param[in] update Make the desired XML match this (must not be NULL)
- * \param[in] as_diff If false, expand "++" when making attributes match
+ * \param[in] update Make the desired XML match this (must not be \c NULL)
+ * \param[in] flags Group of <tt>enum pcmk__xa_flags</tt>
+ * \param[in] as_diff If \c true, preserve order of attributes (deprecated
+ * since 2.0.5)
*
- * \note At least one of \p parent and \p target must be non-NULL
+ * \note At least one of \p parent and \p target must be non-<tt>NULL</tt>.
+ * \note This function is recursive. For the top-level call, \p parent is
+ * \c NULL and \p target is not \c NULL. For recursive calls, \p target is
+ * \c NULL and \p parent is not \c NULL.
*/
void
pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update,
- bool as_diff)
+ uint32_t flags, bool as_diff)
{
- xmlNode *a_child = NULL;
- const char *object_name = NULL,
- *object_href = NULL,
- *object_href_val = NULL;
+ /* @COMPAT Refactor further and staticize after v1 patchset deprecation.
+ *
+ * @COMPAT Drop as_diff argument when apply_xml_diff() is dropped.
+ */
+ const char *update_name = NULL;
+ const char *update_id_attr = NULL;
+ const char *update_id_val = NULL;
+ char *trace_s = NULL;
-#if XML_PARSER_DEBUG
- crm_log_xml_trace(update, "update:");
- crm_log_xml_trace(target, "target:");
-#endif
+ crm_log_xml_trace(update, "update");
+ crm_log_xml_trace(target, "target");
- CRM_CHECK(update != NULL, return);
+ CRM_CHECK(update != NULL, goto done);
if (update->type == XML_COMMENT_NODE) {
pcmk__xc_update(parent, target, update);
- return;
+ goto done;
}
- object_name = (const char *) update->name;
- object_href_val = ID(update);
- if (object_href_val != NULL) {
- object_href = XML_ATTR_ID;
+ update_name = (const char *) update->name;
+
+ CRM_CHECK(update_name != NULL, goto done);
+ CRM_CHECK((target != NULL) || (parent != NULL), goto done);
+
+ update_id_val = pcmk__xe_id(update);
+ if (update_id_val != NULL) {
+ update_id_attr = PCMK_XA_ID;
+
} else {
- object_href_val = crm_element_value(update, XML_ATTR_IDREF);
- object_href = (object_href_val == NULL) ? NULL : XML_ATTR_IDREF;
+ update_id_val = crm_element_value(update, PCMK_XA_ID_REF);
+ if (update_id_val != NULL) {
+ update_id_attr = PCMK_XA_ID_REF;
+ }
}
- CRM_CHECK(object_name != NULL, return);
- CRM_CHECK(target != NULL || parent != NULL, return);
+ pcmk__if_tracing(
+ {
+ if (update_id_attr != NULL) {
+ trace_s = crm_strdup_printf("<%s %s=%s/>",
+ update_name, update_id_attr,
+ update_id_val);
+ } else {
+ trace_s = crm_strdup_printf("<%s/>", update_name);
+ }
+ },
+ {}
+ );
if (target == NULL) {
- target = pcmk__xe_match(parent, object_name,
- object_href, object_href_val);
+ // Recursive call
+ target = pcmk__xe_first_child(parent, update_name, update_id_attr,
+ update_id_val);
}
if (target == NULL) {
- target = create_xml_node(parent, object_name);
- CRM_CHECK(target != NULL, return);
-#if XML_PARSER_DEBUG
- crm_trace("Added <%s%s%s%s%s/>", pcmk__s(object_name, "<null>"),
- object_href ? " " : "",
- object_href ? object_href : "",
- object_href ? "=" : "",
- object_href ? object_href_val : "");
+ // Recursive call with no existing matching child
+ target = pcmk__xe_create(parent, update_name);
+ crm_trace("Added %s", pcmk__s(trace_s, update_name));
} else {
- crm_trace("Found node <%s%s%s%s%s/> to update",
- pcmk__s(object_name, "<null>"),
- object_href ? " " : "",
- object_href ? object_href : "",
- object_href ? "=" : "",
- object_href ? object_href_val : "");
-#endif
+ // Either recursive call with match, or top-level call
+ crm_trace("Found node %s to update", pcmk__s(trace_s, update_name));
}
CRM_CHECK(pcmk__xe_is(target, (const char *) update->name), return);
- if (as_diff == FALSE) {
- /* So that expand_plus_plus() gets called */
- copy_in_properties(target, update);
+ if (!as_diff) {
+ pcmk__xe_copy_attrs(target, update, flags);
} else {
- /* No need for expand_plus_plus(), just raw speed */
+ // Preserve order of attributes. Don't use pcmk__xe_copy_attrs().
for (xmlAttrPtr a = pcmk__xe_first_attr(update); a != NULL;
a = a->next) {
const char *p_value = pcmk__xml_attr_value(a);
@@ -2281,175 +1756,316 @@ pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update,
}
}
- for (a_child = pcmk__xml_first_child(update); a_child != NULL;
- a_child = pcmk__xml_next(a_child)) {
-#if XML_PARSER_DEBUG
- crm_trace("Updating child <%s%s%s%s%s/>",
- pcmk__s(object_name, "<null>"),
- object_href ? " " : "",
- object_href ? object_href : "",
- object_href ? "=" : "",
- object_href ? object_href_val : "");
-#endif
- pcmk__xml_update(target, NULL, a_child, as_diff);
- }
-
-#if XML_PARSER_DEBUG
- crm_trace("Finished with <%s%s%s%s%s/>", pcmk__s(object_name, "<null>"),
- object_href ? " " : "",
- object_href ? object_href : "",
- object_href ? "=" : "",
- object_href ? object_href_val : "");
-#endif
-}
+ for (xmlNode *child = pcmk__xml_first_child(update); child != NULL;
+ child = pcmk__xml_next(child)) {
-gboolean
-update_xml_child(xmlNode * child, xmlNode * to_update)
-{
- gboolean can_update = TRUE;
- xmlNode *child_of_child = NULL;
+ crm_trace("Updating child of %s", pcmk__s(trace_s, update_name));
+ pcmk__xml_update(target, NULL, child, flags, as_diff);
+ }
- CRM_CHECK(child != NULL, return FALSE);
- CRM_CHECK(to_update != NULL, return FALSE);
+ crm_trace("Finished with %s", pcmk__s(trace_s, update_name));
- if (!pcmk__xe_is(to_update, (const char *) child->name)) {
- can_update = FALSE;
+done:
+ free(trace_s);
+}
- } else if (!pcmk__str_eq(ID(to_update), ID(child), pcmk__str_none)) {
- can_update = FALSE;
+/*!
+ * \internal
+ * \brief Delete an XML subtree if it matches a search element
+ *
+ * A match is defined as follows:
+ * * \p xml and \p user_data are both element nodes of the same type.
+ * * If \p user_data has attributes set, \p xml has those attributes set to the
+ * same values. (\p xml may have additional attributes set to arbitrary
+ * values.)
+ *
+ * \param[in,out] xml XML subtree to delete upon match
+ * \param[in] user_data Search element
+ *
+ * \return \c true to continue traversing the tree, or \c false to stop (because
+ * \p xml was deleted)
+ *
+ * \note This is compatible with \c pcmk__xml_tree_foreach().
+ */
+static bool
+delete_xe_if_matching(xmlNode *xml, void *user_data)
+{
+ xmlNode *search = user_data;
- } else if (can_update) {
-#if XML_PARSER_DEBUG
- crm_log_xml_trace(child, "Update match found...");
-#endif
- pcmk__xml_update(NULL, child, to_update, false);
+ if (!pcmk__xe_is(search, (const char *) xml->name)) {
+ // No match: either not both elements, or different element types
+ return true;
}
- for (child_of_child = pcmk__xml_first_child(child); child_of_child != NULL;
- child_of_child = pcmk__xml_next(child_of_child)) {
- /* only update the first one */
- if (can_update) {
- break;
+ for (const xmlAttr *attr = pcmk__xe_first_attr(search); attr != NULL;
+ attr = attr->next) {
+
+ const char *search_val = pcmk__xml_attr_value(attr);
+ const char *xml_val = crm_element_value(xml, (const char *) attr->name);
+
+ if (!pcmk__str_eq(search_val, xml_val, pcmk__str_casei)) {
+ // No match: an attr in xml doesn't match the attr in search
+ return true;
}
- can_update = update_xml_child(child_of_child, to_update);
}
- return can_update;
+ crm_log_xml_trace(xml, "delete-match");
+ crm_log_xml_trace(search, "delete-search");
+ free_xml(xml);
+
+ // Found a match and deleted it; stop traversing tree
+ return false;
}
+/*!
+ * \internal
+ * \brief Search an XML tree depth-first and delete the first matching element
+ *
+ * This function does not attempt to match the tree root (\p xml).
+ *
+ * A match with a node \c node is defined as follows:
+ * * \c node and \p search are both element nodes of the same type.
+ * * If \p search has attributes set, \c node has those attributes set to the
+ * same values. (\c node may have additional attributes set to arbitrary
+ * values.)
+ *
+ * \param[in,out] xml XML subtree to search
+ * \param[in] search Element to match against
+ *
+ * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on
+ * successful deletion and an error code otherwise)
+ */
int
-find_xml_children(xmlNode ** children, xmlNode * root,
- const char *tag, const char *field, const char *value, gboolean search_matches)
+pcmk__xe_delete_match(xmlNode *xml, xmlNode *search)
{
- int match_found = 0;
+ // See @COMPAT comment in pcmk__xe_replace_match()
+ CRM_CHECK((xml != NULL) && (search != NULL), return EINVAL);
- CRM_CHECK(root != NULL, return FALSE);
- CRM_CHECK(children != NULL, return FALSE);
-
- if ((tag != NULL) && !pcmk__xe_is(root, tag)) {
+ for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL;
+ xml = pcmk__xe_next(xml)) {
- } else if (value != NULL && !pcmk__str_eq(value, crm_element_value(root, field), pcmk__str_casei)) {
-
- } else {
- if (*children == NULL) {
- *children = create_xml_node(NULL, __func__);
+ if (!pcmk__xml_tree_foreach(xml, delete_xe_if_matching, search)) {
+ // Found and deleted an element
+ return pcmk_rc_ok;
}
- add_node_copy(*children, root);
- match_found = 1;
}
- if (search_matches || match_found == 0) {
- xmlNode *child = NULL;
+ // No match found in this subtree
+ return ENXIO;
+}
- for (child = pcmk__xml_first_child(root); child != NULL;
- child = pcmk__xml_next(child)) {
- match_found += find_xml_children(children, child, tag, field, value, search_matches);
- }
- }
+/*!
+ * \internal
+ * \brief Replace one XML node with a copy of another XML node
+ *
+ * This function handles change tracking and applies ACLs.
+ *
+ * \param[in,out] old XML node to replace
+ * \param[in] new XML node to copy as replacement for \p old
+ *
+ * \note This frees \p old.
+ */
+static void
+replace_node(xmlNode *old, xmlNode *new)
+{
+ new = xmlCopyNode(new, 1);
+ pcmk__mem_assert(new);
- return match_found;
+ // May be unnecessary but avoids slight changes to some test outputs
+ pcmk__xml_tree_foreach(new, reset_xml_node_flags, NULL);
+
+ old = xmlReplaceNode(old, new);
+
+ if (xml_tracking_changes(new)) {
+ // Replaced sections may have included relevant ACLs
+ pcmk__apply_acl(new);
+ }
+ xml_calculate_changes(old, new);
+ xmlFreeNode(old);
}
-gboolean
-replace_xml_child(xmlNode * parent, xmlNode * child, xmlNode * update, gboolean delete_only)
+/*!
+ * \internal
+ * \brief Replace one XML subtree with a copy of another if the two match
+ *
+ * A match is defined as follows:
+ * * \p xml and \p user_data are both element nodes of the same type.
+ * * If \p user_data has the \c PCMK_XA_ID attribute set, then \p xml has
+ * \c PCMK_XA_ID set to the same value.
+ *
+ * \param[in,out] xml XML subtree to replace with \p user_data upon match
+ * \param[in] user_data XML to replace \p xml with a copy of upon match
+ *
+ * \return \c true to continue traversing the tree, or \c false to stop (because
+ * \p xml was replaced by \p user_data)
+ *
+ * \note This is compatible with \c pcmk__xml_tree_foreach().
+ */
+static bool
+replace_xe_if_matching(xmlNode *xml, void *user_data)
{
- gboolean can_delete = FALSE;
- xmlNode *child_of_child = NULL;
+ xmlNode *replace = user_data;
+ const char *xml_id = NULL;
+ const char *replace_id = NULL;
- const char *up_id = NULL;
- const char *child_id = NULL;
- const char *right_val = NULL;
+ xml_id = pcmk__xe_id(xml);
+ replace_id = pcmk__xe_id(replace);
- CRM_CHECK(child != NULL, return FALSE);
- CRM_CHECK(update != NULL, return FALSE);
+ if (!pcmk__xe_is(replace, (const char *) xml->name)) {
+ // No match: either not both elements, or different element types
+ return true;
+ }
- up_id = ID(update);
- child_id = ID(child);
+ if ((replace_id != NULL)
+ && !pcmk__str_eq(replace_id, xml_id, pcmk__str_none)) {
- if (up_id == NULL || (child_id && strcmp(child_id, up_id) == 0)) {
- can_delete = TRUE;
+ // No match: ID was provided in replace and doesn't match xml's ID
+ return true;
}
- if (!pcmk__xe_is(update, (const char *) child->name)) {
- can_delete = FALSE;
- }
- if (can_delete && delete_only) {
- for (xmlAttrPtr a = pcmk__xe_first_attr(update); a != NULL;
- a = a->next) {
- const char *p_name = (const char *) a->name;
- const char *p_value = pcmk__xml_attr_value(a);
- right_val = crm_element_value(child, p_name);
- if (!pcmk__str_eq(p_value, right_val, pcmk__str_casei)) {
- can_delete = FALSE;
- }
+ crm_log_xml_trace(xml, "replace-match");
+ crm_log_xml_trace(replace, "replace-with");
+ replace_node(xml, replace);
+
+ // Found a match and replaced it; stop traversing tree
+ return false;
+}
+
+/*!
+ * \internal
+ * \brief Search an XML tree depth-first and replace the first matching element
+ *
+ * This function does not attempt to match the tree root (\p xml).
+ *
+ * A match with a node \c node is defined as follows:
+ * * \c node and \p replace are both element nodes of the same type.
+ * * If \p replace has the \c PCMK_XA_ID attribute set, then \c node has
+ * \c PCMK_XA_ID set to the same value.
+ *
+ * \param[in,out] xml XML tree to search
+ * \param[in] replace XML to replace a matching element with a copy of
+ *
+ * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on
+ * successful replacement and an error code otherwise)
+ */
+int
+pcmk__xe_replace_match(xmlNode *xml, xmlNode *replace)
+{
+ /* @COMPAT Some of this behavior (like not matching the tree root, which is
+ * allowed by pcmk__xe_update_match()) is questionable for general use but
+ * required for backward compatibility by cib_process_replace() and
+ * cib_process_delete(). Behavior can change at a major version release if
+ * desired.
+ */
+ CRM_CHECK((xml != NULL) && (replace != NULL), return EINVAL);
+
+ for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL;
+ xml = pcmk__xe_next(xml)) {
+
+ if (!pcmk__xml_tree_foreach(xml, replace_xe_if_matching, replace)) {
+ // Found and replaced an element
+ return pcmk_rc_ok;
}
}
- if (can_delete && parent != NULL) {
- crm_log_xml_trace(child, "Delete match found...");
- if (delete_only || update == NULL) {
- free_xml(child);
+ // No match found in this subtree
+ return ENXIO;
+}
- } else {
- xmlNode *old = child;
- xmlNode *new = xmlCopyNode(update, 1);
+//! User data for \c update_xe_if_matching()
+struct update_data {
+ xmlNode *update; //!< Update source
+ uint32_t flags; //!< Group of <tt>enum pcmk__xa_flags</tt>
+};
- CRM_ASSERT(new != NULL);
+/*!
+ * \internal
+ * \brief Update one XML subtree with another if the two match
+ *
+ * "Update" means to merge a source subtree into a target subtree (see
+ * \c pcmk__xml_update()).
+ *
+ * A match is defined as follows:
+ * * \p xml and \p user_data->update are both element nodes of the same type.
+ * * \p xml and \p user_data->update have the same \c PCMK_XA_ID attribute
+ * value, or \c PCMK_XA_ID is unset in both
+ *
+ * \param[in,out] xml XML subtree to update with \p user_data->update
+ * upon match
+ * \param[in] user_data <tt>struct update_data</tt> object
+ *
+ * \return \c true to continue traversing the tree, or \c false to stop (because
+ * \p xml was updated by \p user_data->update)
+ *
+ * \note This is compatible with \c pcmk__xml_tree_foreach().
+ */
+static bool
+update_xe_if_matching(xmlNode *xml, void *user_data)
+{
+ struct update_data *data = user_data;
+ xmlNode *update = data->update;
- // May be unnecessary but avoids slight changes to some test outputs
- reset_xml_node_flags(new);
+ if (!pcmk__xe_is(update, (const char *) xml->name)) {
+ // No match: either not both elements, or different element types
+ return true;
+ }
- old = xmlReplaceNode(old, new);
+ if (!pcmk__str_eq(pcmk__xe_id(xml), pcmk__xe_id(update), pcmk__str_none)) {
+ // No match: ID mismatch
+ return true;
+ }
- if (xml_tracking_changes(new)) {
- // Replaced sections may have included relevant ACLs
- pcmk__apply_acl(new);
- }
- xml_calculate_changes(old, new);
- xmlFreeNode(old);
- }
- return TRUE;
+ crm_log_xml_trace(xml, "update-match");
+ crm_log_xml_trace(update, "update-with");
+ pcmk__xml_update(NULL, xml, update, data->flags, false);
- } else if (can_delete) {
- crm_log_xml_debug(child, "Cannot delete the search root");
- can_delete = FALSE;
- }
+ // Found a match and replaced it; stop traversing tree
+ return false;
+}
- child_of_child = pcmk__xml_first_child(child);
- while (child_of_child) {
- xmlNode *next = pcmk__xml_next(child_of_child);
+/*!
+ * \internal
+ * \brief Search an XML tree depth-first and update the first matching element
+ *
+ * "Update" means to merge a source subtree into a target subtree (see
+ * \c pcmk__xml_update()).
+ *
+ * A match with a node \c node is defined as follows:
+ * * \c node and \p update are both element nodes of the same type.
+ * * \c node and \p update have the same \c PCMK_XA_ID attribute value, or
+ * \c PCMK_XA_ID is unset in both
+ *
+ * \param[in,out] xml XML tree to search
+ * \param[in] update XML to update a matching element with
+ * \param[in] flags Group of <tt>enum pcmk__xa_flags</tt>
+ *
+ * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on
+ * successful update and an error code otherwise)
+ */
+int
+pcmk__xe_update_match(xmlNode *xml, xmlNode *update, uint32_t flags)
+{
+ /* @COMPAT In pcmk__xe_delete_match() and pcmk__xe_replace_match(), we
+ * compare IDs only if the equivalent of the update argument has an ID.
+ * Here, we're stricter: we consider it a mismatch if only one element has
+ * an ID attribute, or if both elements have IDs but they don't match.
+ *
+ * Perhaps we should align the behavior at a major version release.
+ */
+ struct update_data data = {
+ .update = update,
+ .flags = flags,
+ };
- can_delete = replace_xml_child(child, child_of_child, update, delete_only);
+ CRM_CHECK((xml != NULL) && (update != NULL), return EINVAL);
- /* only delete the first one */
- if (can_delete) {
- child_of_child = NULL;
- } else {
- child_of_child = next;
- }
+ if (!pcmk__xml_tree_foreach(xml, update_xe_if_matching, &data)) {
+ // Found and updated an element
+ return pcmk_rc_ok;
}
- return can_delete;
+ // No match found in this subtree
+ return ENXIO;
}
xmlNode *
@@ -2461,61 +2077,42 @@ sorted_xml(xmlNode *input, xmlNode *parent, gboolean recursive)
CRM_CHECK(input != NULL, return NULL);
- result = create_xml_node(parent, (const char *) input->name);
+ result = pcmk__xe_create(parent, (const char *) input->name);
nvpairs = pcmk_xml_attrs2nvpairs(input);
nvpairs = pcmk_sort_nvpairs(nvpairs);
pcmk_nvpairs2xml_attrs(nvpairs, result);
pcmk_free_nvpairs(nvpairs);
- for (child = pcmk__xml_first_child(input); child != NULL;
- child = pcmk__xml_next(child)) {
+ for (child = pcmk__xe_first_child(input, NULL, NULL, NULL); child != NULL;
+ child = pcmk__xe_next(child)) {
if (recursive) {
sorted_xml(child, result, recursive);
} else {
- add_node_copy(result, child);
+ pcmk__xml_copy(result, child);
}
}
return result;
}
-xmlNode *
-first_named_child(const xmlNode *parent, const char *name)
-{
- xmlNode *match = NULL;
-
- for (match = pcmk__xe_first_child(parent); match != NULL;
- match = pcmk__xe_next(match)) {
- /*
- * name == NULL gives first child regardless of name; this is
- * semantically incorrect in this function, but may be necessary
- * due to prior use of xml_child_iter_filter
- */
- if (pcmk__str_eq(name, (const char *)match->name, pcmk__str_null_matches)) {
- return match;
- }
- }
- return NULL;
-}
-
/*!
- * \brief Get next instance of same XML tag
+ * \internal
+ * \brief Get next sibling XML element with the same name as a given element
*
- * \param[in] sibling XML tag to start from
+ * \param[in] node XML element to start from
*
- * \return Next sibling XML tag with same name
+ * \return Next sibling XML element with same name
*/
xmlNode *
-crm_next_same_xml(const xmlNode *sibling)
+pcmk__xe_next_same(const xmlNode *node)
{
- xmlNode *match = pcmk__xe_next(sibling);
+ for (xmlNode *match = pcmk__xe_next(node); match != NULL;
+ match = pcmk__xe_next(match)) {
- while (match != NULL) {
- if (pcmk__xe_is(match, (const char *) sibling->name)) {
+ if (pcmk__xe_is(match, (const char *) node->name)) {
return match;
}
- match = pcmk__xe_next(match);
}
return NULL;
}
@@ -2554,31 +2151,32 @@ crm_xml_cleanup(void)
xmlNode *
expand_idref(xmlNode * input, xmlNode * top)
{
+ char *xpath = NULL;
const char *ref = NULL;
- xmlNode *result = input;
+ xmlNode *result = NULL;
- if (result == NULL) {
+ if (input == NULL) {
return NULL;
-
- } else if (top == NULL) {
- top = input;
}
- ref = crm_element_value(result, XML_ATTR_IDREF);
- if (ref != NULL) {
- char *xpath_string = crm_strdup_printf("//%s[@" XML_ATTR_ID "='%s']",
- result->name, ref);
+ ref = crm_element_value(input, PCMK_XA_ID_REF);
+ if (ref == NULL) {
+ return input;
+ }
- result = get_xpath_object(xpath_string, top, LOG_ERR);
- if (result == NULL) {
- char *nodePath = (char *)xmlGetNodePath(top);
+ if (top == NULL) {
+ top = input;
+ }
- crm_err("No match for %s found in %s: Invalid configuration",
- xpath_string, pcmk__s(nodePath, "unrecognizable path"));
- free(nodePath);
- }
- free(xpath_string);
+ xpath = crm_strdup_printf("//%s[@" PCMK_XA_ID "='%s']", input->name, ref);
+ result = get_xpath_object(xpath, top, LOG_DEBUG);
+ if (result == NULL) { // Not possible with schema validation enabled
+ pcmk__config_err("Ignoring invalid %s configuration: "
+ PCMK_XA_ID_REF " '%s' does not reference "
+ "a valid object " CRM_XS " xpath=%s",
+ input->name, ref, xpath);
}
+ free(xpath);
return result;
}
@@ -2610,25 +2208,52 @@ pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns)
return ret;
}
-char *
-pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec)
+static char *
+find_artefact(enum pcmk__xml_artefact_ns ns, const char *path, const char *filespec)
{
- char *base = pcmk__xml_artefact_root(ns), *ret = NULL;
+ char *ret = NULL;
switch (ns) {
case pcmk__xml_artefact_ns_legacy_rng:
case pcmk__xml_artefact_ns_base_rng:
- ret = crm_strdup_printf("%s/%s.rng", base, filespec);
+ if (pcmk__ends_with(filespec, ".rng")) {
+ ret = crm_strdup_printf("%s/%s", path, filespec);
+ } else {
+ ret = crm_strdup_printf("%s/%s.rng", path, filespec);
+ }
break;
case pcmk__xml_artefact_ns_legacy_xslt:
case pcmk__xml_artefact_ns_base_xslt:
- ret = crm_strdup_printf("%s/%s.xsl", base, filespec);
+ if (pcmk__ends_with(filespec, ".xsl")) {
+ ret = crm_strdup_printf("%s/%s", path, filespec);
+ } else {
+ ret = crm_strdup_printf("%s/%s.xsl", path, filespec);
+ }
break;
default:
crm_err("XML artefact family specified as %u not recognized", ns);
}
+
+ return ret;
+}
+
+char *
+pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec)
+{
+ struct stat sb;
+ char *base = pcmk__xml_artefact_root(ns);
+ char *ret = NULL;
+
+ ret = find_artefact(ns, base, filespec);
free(base);
+ if (stat(ret, &sb) != 0 || !S_ISREG(sb.st_mode)) {
+ const char *remote_schema_dir = pcmk__remote_schema_dir();
+
+ free(ret);
+ ret = find_artefact(ns, remote_schema_dir, filespec);
+ }
+
return ret;
}
@@ -2669,8 +2294,9 @@ pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name,
CRM_ASSERT(handler != NULL);
for (xmlNode *node = children; node != NULL; node = node->next) {
- if (node->type == XML_ELEMENT_NODE &&
- pcmk__str_eq(child_element_name, (const char *) node->name, pcmk__str_null_matches)) {
+ if ((node->type == XML_ELEMENT_NODE)
+ && ((child_element_name == NULL)
+ || pcmk__xe_is(node, child_element_name))) {
int rc = handler(node, userdata);
if (rc != pcmk_rc_ok) {
@@ -2690,8 +2316,8 @@ pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name,
xmlNode *
find_entity(xmlNode *parent, const char *node_name, const char *id)
{
- return pcmk__xe_match(parent, node_name,
- ((id == NULL)? id : XML_ATTR_ID), id);
+ return pcmk__xe_first_child(parent, node_name,
+ ((id == NULL)? id : PCMK_XA_ID), id);
}
void
@@ -2709,12 +2335,28 @@ getDocPtr(xmlNode *node)
doc = node->doc;
if (doc == NULL) {
- doc = xmlNewDoc((pcmkXmlStr) "1.0");
+ doc = xmlNewDoc(PCMK__XML_VERSION);
xmlDocSetRootElement(doc, node);
}
return doc;
}
+xmlNode *
+add_node_copy(xmlNode *parent, xmlNode *src_node)
+{
+ xmlNode *child = NULL;
+
+ CRM_CHECK((parent != NULL) && (src_node != NULL), return NULL);
+
+ child = xmlDocCopyNode(src_node, parent->doc, 1);
+ if (child == NULL) {
+ return NULL;
+ }
+ xmlAddChild(parent, child);
+ pcmk__xml_mark_created(child);
+ return child;
+}
+
int
add_node_nocopy(xmlNode *parent, const char *name, xmlNode *child)
{
@@ -2732,5 +2374,352 @@ xml_has_children(const xmlNode * xml_root)
return FALSE;
}
+static char *
+replace_text(char *text, size_t *index, size_t *length, const char *replace)
+{
+ // We have space for 1 char already
+ size_t offset = strlen(replace) - 1;
+
+ if (offset > 0) {
+ *length += offset;
+ text = pcmk__realloc(text, *length + 1);
+
+ // Shift characters to the right to make room for the replacement string
+ for (size_t i = *length; i > (*index + offset); i--) {
+ text[i] = text[i - offset];
+ }
+ }
+
+ // Replace the character at index by the replacement string
+ memcpy(text + *index, replace, offset + 1);
+
+ // Reset index to the end of replacement string
+ *index += offset;
+ return text;
+}
+
+char *
+crm_xml_escape(const char *text)
+{
+ size_t length = 0;
+ char *copy = NULL;
+
+ if (text == NULL) {
+ return NULL;
+ }
+
+ length = strlen(text);
+ copy = pcmk__str_copy(text);
+ for (size_t index = 0; index <= length; index++) {
+ if(copy[index] & 0x80 && copy[index+1] & 0x80){
+ index++;
+ continue;
+ }
+ switch (copy[index]) {
+ case 0:
+ // Sanity only; loop should stop at the last non-null byte
+ break;
+ case '<':
+ copy = replace_text(copy, &index, &length, "&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;
+}
+
+xmlNode *
+copy_xml(xmlNode *src)
+{
+ xmlDoc *doc = xmlNewDoc(PCMK__XML_VERSION);
+ xmlNode *copy = NULL;
+
+ pcmk__mem_assert(doc);
+
+ copy = xmlDocCopyNode(src, doc, 1);
+ pcmk__mem_assert(copy);
+
+ xmlDocSetRootElement(doc, copy);
+ return copy;
+}
+
+xmlNode *
+create_xml_node(xmlNode *parent, const char *name)
+{
+ // Like pcmk__xe_create(), but returns NULL on failure
+ xmlNode *node = NULL;
+
+ CRM_CHECK(!pcmk__str_empty(name), return NULL);
+
+ if (parent == NULL) {
+ xmlDoc *doc = xmlNewDoc(PCMK__XML_VERSION);
+
+ if (doc == NULL) {
+ return NULL;
+ }
+
+ node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL);
+ if (node == NULL) {
+ xmlFreeDoc(doc);
+ return NULL;
+ }
+ xmlDocSetRootElement(doc, node);
+
+ } else {
+ node = xmlNewChild(parent, NULL, (pcmkXmlStr) name, NULL);
+ if (node == NULL) {
+ return NULL;
+ }
+ }
+ pcmk__xml_mark_created(node);
+ return node;
+}
+
+xmlNode *
+pcmk_create_xml_text_node(xmlNode *parent, const char *name,
+ const char *content)
+{
+ xmlNode *node = pcmk__xe_create(parent, name);
+
+ pcmk__xe_set_content(node, "%s", content);
+ return node;
+}
+
+xmlNode *
+pcmk_create_html_node(xmlNode *parent, const char *element_name, const char *id,
+ const char *class_name, const char *text)
+{
+ xmlNode *node = pcmk__html_create(parent, element_name, id, class_name);
+
+ pcmk__xe_set_content(node, "%s", text);
+ return node;
+}
+
+xmlNode *
+first_named_child(const xmlNode *parent, const char *name)
+{
+ return pcmk__xe_first_child(parent, name, NULL, NULL);
+}
+
+xmlNode *
+find_xml_node(const xmlNode *root, const char *search_path, gboolean must_find)
+{
+ xmlNode *result = NULL;
+
+ if (search_path == NULL) {
+ crm_warn("Will never find <NULL>");
+ return NULL;
+ }
+
+ result = pcmk__xe_first_child(root, search_path, NULL, NULL);
+
+ if (must_find && (result == NULL)) {
+ crm_warn("Could not find %s in %s",
+ search_path,
+ ((root != NULL)? (const char *) root->name : "<NULL>"));
+ }
+
+ return result;
+}
+
+xmlNode *
+crm_next_same_xml(const xmlNode *sibling)
+{
+ return pcmk__xe_next_same(sibling);
+}
+
+void
+xml_remove_prop(xmlNode * obj, const char *name)
+{
+ pcmk__xe_remove_attr(obj, name);
+}
+
+gboolean
+replace_xml_child(xmlNode * parent, xmlNode * child, xmlNode * update, gboolean delete_only)
+{
+ bool is_match = false;
+ const char *child_id = NULL;
+ const char *update_id = NULL;
+
+ CRM_CHECK(child != NULL, return FALSE);
+ CRM_CHECK(update != NULL, return FALSE);
+
+ child_id = pcmk__xe_id(child);
+ update_id = pcmk__xe_id(update);
+
+ /* Match element name and (if provided in update XML) element ID. Don't
+ * match search root (child is search root if parent == NULL).
+ */
+ is_match = (parent != NULL)
+ && pcmk__xe_is(update, (const char *) child->name)
+ && ((update_id == NULL)
+ || pcmk__str_eq(update_id, child_id, pcmk__str_none));
+
+ /* For deletion, match all attributes provided in update. A matching node
+ * can have additional attributes, but values must match for provided ones.
+ */
+ if (is_match && delete_only) {
+ for (xmlAttr *attr = pcmk__xe_first_attr(update); attr != NULL;
+ attr = attr->next) {
+ const char *name = (const char *) attr->name;
+ const char *update_val = pcmk__xml_attr_value(attr);
+ const char *child_val = crm_element_value(child, name);
+
+ if (!pcmk__str_eq(update_val, child_val, pcmk__str_casei)) {
+ is_match = false;
+ break;
+ }
+ }
+ }
+
+ if (is_match) {
+ if (delete_only) {
+ crm_log_xml_trace(child, "delete-match");
+ crm_log_xml_trace(update, "delete-search");
+ free_xml(child);
+
+ } else {
+ crm_log_xml_trace(child, "replace-match");
+ crm_log_xml_trace(update, "replace-with");
+ replace_node(child, update);
+ }
+ return TRUE;
+ }
+
+ // Current node not a match; search the rest of the subtree depth-first
+ parent = child;
+ for (child = pcmk__xml_first_child(parent); child != NULL;
+ child = pcmk__xml_next(child)) {
+
+ // Only delete/replace the first match
+ if (replace_xml_child(parent, child, update, delete_only)) {
+ return TRUE;
+ }
+ }
+
+ // No match found in this subtree
+ return FALSE;
+}
+
+gboolean
+update_xml_child(xmlNode *child, xmlNode *to_update)
+{
+ return pcmk__xe_update_match(child, to_update,
+ pcmk__xaf_score_update) == pcmk_rc_ok;
+}
+
+int
+find_xml_children(xmlNode **children, xmlNode *root, const char *tag,
+ const char *field, const char *value, gboolean search_matches)
+{
+ int match_found = 0;
+
+ CRM_CHECK(root != NULL, return FALSE);
+ CRM_CHECK(children != NULL, return FALSE);
+
+ if ((tag != NULL) && !pcmk__xe_is(root, tag)) {
+
+ } else if ((value != NULL)
+ && !pcmk__str_eq(value, crm_element_value(root, field),
+ pcmk__str_casei)) {
+
+ } else {
+ if (*children == NULL) {
+ *children = pcmk__xe_create(NULL, __func__);
+ }
+ pcmk__xml_copy(*children, root);
+ match_found = 1;
+ }
+
+ if (search_matches || match_found == 0) {
+ xmlNode *child = NULL;
+
+ for (child = pcmk__xml_first_child(root); child != NULL;
+ child = pcmk__xml_next(child)) {
+ match_found += find_xml_children(children, child, tag, field, value,
+ search_matches);
+ }
+ }
+
+ return match_found;
+}
+
+void
+fix_plus_plus_recursive(xmlNode *target)
+{
+ /* TODO: Remove recursion and use xpath searches for value++ */
+ xmlNode *child = NULL;
+
+ for (xmlAttrPtr a = pcmk__xe_first_attr(target); a != NULL; a = a->next) {
+ const char *p_name = (const char *) a->name;
+ const char *p_value = pcmk__xml_attr_value(a);
+
+ expand_plus_plus(target, p_name, p_value);
+ }
+ for (child = pcmk__xe_first_child(target, NULL, NULL, NULL); child != NULL;
+ child = pcmk__xe_next(child)) {
+
+ fix_plus_plus_recursive(child);
+ }
+}
+
+void
+copy_in_properties(xmlNode *target, const xmlNode *src)
+{
+ if (src == NULL) {
+ crm_warn("No node to copy properties from");
+
+ } else if (target == NULL) {
+ crm_err("No node to copy properties into");
+
+ } else {
+ for (xmlAttrPtr a = pcmk__xe_first_attr(src); a != NULL; a = a->next) {
+ const char *p_name = (const char *) a->name;
+ const char *p_value = pcmk__xml_attr_value(a);
+
+ expand_plus_plus(target, p_name, p_value);
+ if (xml_acl_denied(target)) {
+ crm_trace("Cannot copy %s=%s to %s", p_name, p_value, target->name);
+ return;
+ }
+ }
+ }
+}
+
+void
+expand_plus_plus(xmlNode * target, const char *name, const char *value)
+{
+ pcmk__xe_set_score(target, name, value);
+}
+
// LCOV_EXCL_STOP
// End deprecated API
diff --git a/lib/common/xml_attr.c b/lib/common/xml_attr.c
index 427d267..faed37f 100644
--- a/lib/common/xml_attr.c
+++ b/lib/common/xml_attr.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2004-2023 the Pacemaker project contributors
+ * Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -23,7 +23,6 @@
#include <libxml/xmlIO.h> /* xmlAllocOutputBuffer */
#include <crm/crm.h>
-#include <crm/msg_xml.h>
#include <crm/common/xml.h>
#include <crm/common/xml_internal.h> // PCMK__XML_LOG_BASE, etc.
#include "crmcommon_private.h"
@@ -62,8 +61,9 @@ pcmk__marked_as_deleted(xmlAttrPtr a, void *user_data)
void
pcmk__dump_xml_attr(const xmlAttr *attr, GString *buffer)
{
- char *p_value = NULL;
- const char *p_name = NULL;
+ const char *name = NULL;
+ const char *value = NULL;
+ gchar *value_esc = NULL;
xml_node_private_t *nodepriv = NULL;
if (attr == NULL || attr->children == NULL) {
@@ -75,10 +75,21 @@ pcmk__dump_xml_attr(const xmlAttr *attr, GString *buffer)
return;
}
- p_name = (const char *) attr->name;
- p_value = crm_xml_escape((const char *)attr->children->content);
- pcmk__g_strcat(buffer, " ", p_name, "=\"", pcmk__s(p_value, "<null>"), "\"",
- NULL);
+ name = (const char *) attr->name;
+ value = (const char *) attr->children->content;
+ if (value == NULL) {
+ /* Don't print anything for unset attribute. Any null-indicator value,
+ * including the empty string, could also be a real value that needs to
+ * be treated differently from "unset".
+ */
+ return;
+ }
- free(p_value);
-} \ No newline at end of file
+ if (pcmk__xml_needs_escape(value, pcmk__xml_escape_attr)) {
+ value_esc = pcmk__xml_escape(value, pcmk__xml_escape_attr);
+ value = value_esc;
+ }
+
+ pcmk__g_strcat(buffer, " ", name, "=\"", value, "\"", NULL);
+ g_free(value_esc);
+}
diff --git a/lib/common/xml_display.c b/lib/common/xml_display.c
index 18cd3b9..b563d3a 100644
--- a/lib/common/xml_display.c
+++ b/lib/common/xml_display.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2004-2023 the Pacemaker project contributors
+ * Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -12,7 +12,6 @@
#include <libxml/tree.h>
#include <crm/crm.h>
-#include <crm/msg_xml.h>
#include <crm/common/xml.h>
#include <crm/common/xml_internal.h> // PCMK__XML_LOG_BASE, etc.
#include "crmcommon_private.h"
@@ -96,7 +95,7 @@ show_xml_element(pcmk__output_t *out, GString *buffer, const char *prefix,
int rc = pcmk_rc_no_output;
if (pcmk_is_set(options, pcmk__xml_fmt_open)) {
- const char *hidden = crm_element_value(data, "hidden");
+ const char *hidden = crm_element_value(data, PCMK__XA_HIDDEN);
g_string_truncate(buffer, 0);
@@ -110,7 +109,7 @@ show_xml_element(pcmk__output_t *out, GString *buffer, const char *prefix,
xml_node_private_t *nodepriv = attr->_private;
const char *p_name = (const char *) attr->name;
const char *p_value = pcmk__xml_attr_value(attr);
- char *p_copy = NULL;
+ gchar *p_copy = NULL;
if (pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) {
continue;
@@ -120,21 +119,23 @@ show_xml_element(pcmk__output_t *out, GString *buffer, const char *prefix,
if (pcmk_any_flags_set(options,
pcmk__xml_fmt_diff_plus
|pcmk__xml_fmt_diff_minus)
- && (strcmp(XML_DIFF_MARKER, p_name) == 0)) {
+ && (strcmp(PCMK__XA_CRM_DIFF_MARKER, p_name) == 0)) {
continue;
}
if ((hidden != NULL) && (p_name[0] != '\0')
&& (strstr(hidden, p_name) != NULL)) {
- pcmk__str_update(&p_copy, "*****");
+
+ p_value = "*****";
} else {
- p_copy = crm_xml_escape(p_value);
+ p_copy = pcmk__xml_escape(p_value, true);
+ p_value = p_copy;
}
pcmk__g_strcat(buffer, " ", p_name, "=\"",
- pcmk__s(p_copy, "<null>"), "\"", NULL);
- free(p_copy);
+ pcmk__s(p_value, "<null>"), "\"", NULL);
+ g_free(p_copy);
}
if ((data->children != NULL)
@@ -477,7 +478,7 @@ log_data_element(int log_level, const char *file, const char *function,
if (pcmk_is_set(options, pcmk__xml_fmt_pretty)
&& ((data->children == NULL)
- || (crm_element_value(data, XML_DIFF_MARKER) != NULL))) {
+ || (crm_element_value(data, PCMK__XA_CRM_DIFF_MARKER) != NULL))) {
if (pcmk_is_set(options, pcmk__xml_fmt_diff_plus)) {
legacy_options |= xml_log_option_diff_all;
diff --git a/lib/common/xml_io.c b/lib/common/xml_io.c
new file mode 100644
index 0000000..f88e0b5
--- /dev/null
+++ b/lib/common/xml_io.c
@@ -0,0 +1,840 @@
+/*
+ * Copyright 2004-2024 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU Lesser General Public License
+ * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+
+#include <bzlib.h>
+#include <libxml/parser.h>
+#include <libxml/tree.h>
+#include <libxml/xmlIO.h> // xmlOutputBuffer*
+
+#include <crm/crm.h>
+#include <crm/common/xml.h>
+#include <crm/common/xml_io.h>
+#include "crmcommon_private.h"
+
+/* @COMPAT XML_PARSE_RECOVER allows some XML errors to be silently worked around
+ * by libxml2, which is potentially ambiguous and dangerous. We should drop it
+ * when we can break backward compatibility with configurations that might be
+ * relying on it (i.e. pacemaker 3.0.0).
+ */
+#define PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER (XML_PARSE_NOBLANKS)
+#define PCMK__XML_PARSE_OPTS_WITH_RECOVER (XML_PARSE_NOBLANKS \
+ |XML_PARSE_RECOVER)
+
+/*!
+ * \internal
+ * \brief Read from \c stdin until EOF or error
+ *
+ * \return Newly allocated string containing the bytes read from \c stdin, or
+ * \c NULL on error
+ *
+ * \note The caller is responsible for freeing the return value using \c free().
+ */
+static char *
+read_stdin(void)
+{
+ char *buf = NULL;
+ size_t length = 0;
+
+ do {
+ buf = pcmk__realloc(buf, length + PCMK__BUFFER_SIZE + 1);
+ length += fread(buf + length, 1, PCMK__BUFFER_SIZE, stdin);
+ } while ((feof(stdin) == 0) && (ferror(stdin) == 0));
+
+ if (ferror(stdin) != 0) {
+ crm_err("Error reading input from stdin");
+ free(buf);
+ buf = NULL;
+ } else {
+ buf[length] = '\0';
+ }
+ clearerr(stdin);
+ return buf;
+}
+
+/*!
+ * \internal
+ * \brief Decompress a <tt>bzip2</tt>-compressed file into a string buffer
+ *
+ * \param[in] filename Name of file to decompress
+ *
+ * \return Newly allocated string with the decompressed contents of \p filename,
+ * or \c NULL on error.
+ *
+ * \note The caller is responsible for freeing the return value using \c free().
+ */
+static char *
+decompress_file(const char *filename)
+{
+ char *buffer = NULL;
+ int rc = pcmk_rc_ok;
+ size_t length = 0;
+ BZFILE *bz_file = NULL;
+ FILE *input = fopen(filename, "r");
+
+ if (input == NULL) {
+ crm_perror(LOG_ERR, "Could not open %s for reading", filename);
+ return NULL;
+ }
+
+ bz_file = BZ2_bzReadOpen(&rc, input, 0, 0, NULL, 0);
+ rc = pcmk__bzlib2rc(rc);
+ if (rc != pcmk_rc_ok) {
+ crm_err("Could not prepare to read compressed %s: %s "
+ CRM_XS " rc=%d", filename, pcmk_rc_str(rc), rc);
+ goto done;
+ }
+
+ // cppcheck seems not to understand the abort-logic in pcmk__realloc
+ // cppcheck-suppress memleak
+ do {
+ int read_len = 0;
+
+ buffer = pcmk__realloc(buffer, length + PCMK__BUFFER_SIZE + 1);
+ read_len = BZ2_bzRead(&rc, bz_file, buffer + length, PCMK__BUFFER_SIZE);
+
+ if ((rc == BZ_OK) || (rc == BZ_STREAM_END)) {
+ crm_trace("Read %ld bytes from file: %d", (long) read_len, rc);
+ length += read_len;
+ }
+ } while (rc == BZ_OK);
+
+ rc = pcmk__bzlib2rc(rc);
+ if (rc != pcmk_rc_ok) {
+ rc = pcmk__bzlib2rc(rc);
+ crm_err("Could not read compressed %s: %s " CRM_XS " rc=%d",
+ filename, pcmk_rc_str(rc), rc);
+ free(buffer);
+ buffer = NULL;
+ } else {
+ buffer[length] = '\0';
+ }
+
+done:
+ BZ2_bzReadClose(&rc, bz_file);
+ fclose(input);
+ return buffer;
+}
+
+// @COMPAT Remove macro at 3.0.0 when we drop XML_PARSE_RECOVER
+/*!
+ * \internal
+ * \brief Try to parse XML first without and then with recovery enabled
+ *
+ * \param[out] result Where to store the resulting XML doc (<tt>xmlDoc **</tt>)
+ * \param[in] fn XML parser function
+ * \param[in] ... All arguments for \p fn except the final one (an
+ * \c xmlParserOption group)
+ */
+#define parse_xml_recover(result, fn, ...) do { \
+ *result = fn(__VA_ARGS__, PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER); \
+ if (*result == NULL) { \
+ *result = fn(__VA_ARGS__, PCMK__XML_PARSE_OPTS_WITH_RECOVER); \
+ \
+ if (*result != NULL) { \
+ crm_warn("Successfully recovered from XML errors " \
+ "(note: a future release will treat this as a " \
+ "fatal failure)"); \
+ } \
+ } \
+ } while (0);
+
+/*!
+ * \internal
+ * \brief Parse XML from a file
+ *
+ * \param[in] filename Name of file containing XML (\c NULL or \c "-" for
+ * \c stdin); if \p filename ends in \c ".bz2", the file
+ * will be decompressed using \c bzip2
+ *
+ * \return XML tree parsed from the given file; may be \c NULL or only partial
+ * on error
+ */
+xmlNode *
+pcmk__xml_read(const char *filename)
+{
+ bool use_stdin = pcmk__str_eq(filename, "-", pcmk__str_null_matches);
+ xmlNode *xml = NULL;
+ xmlDoc *output = NULL;
+ xmlParserCtxt *ctxt = NULL;
+ const xmlError *last_error = NULL;
+
+ // Create a parser context
+ ctxt = xmlNewParserCtxt();
+ CRM_CHECK(ctxt != NULL, return NULL);
+
+ xmlCtxtResetLastError(ctxt);
+ xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err);
+
+ if (use_stdin) {
+ /* @COMPAT After dropping XML_PARSE_RECOVER, we can avoid capturing
+ * stdin into a buffer and instead call
+ * xmlCtxtReadFd(ctxt, STDIN_FILENO, NULL, NULL, XML_PARSE_NOBLANKS);
+ *
+ * For now we have to save the input so that we can use it twice.
+ */
+ char *input = read_stdin();
+
+ if (input != NULL) {
+ parse_xml_recover(&output, xmlCtxtReadDoc, ctxt, (pcmkXmlStr) input,
+ NULL, NULL);
+ free(input);
+ }
+
+ } else if (pcmk__ends_with_ext(filename, ".bz2")) {
+ char *input = decompress_file(filename);
+
+ if (input != NULL) {
+ parse_xml_recover(&output, xmlCtxtReadDoc, ctxt, (pcmkXmlStr) input,
+ NULL, NULL);
+ free(input);
+ }
+
+ } else {
+ parse_xml_recover(&output, xmlCtxtReadFile, ctxt, filename, NULL);
+ }
+
+ if (output != NULL) {
+ xml = xmlDocGetRootElement(output);
+ if (xml != NULL) {
+ /* @TODO Should we really be stripping out text? This seems like an
+ * overly broad way to get rid of whitespace, if that's the goal.
+ * Text nodes may be invalid in most or all Pacemaker inputs, but
+ * stripping them in a generic "parse XML from file" function may
+ * not be the best way to ignore them.
+ */
+ pcmk__strip_xml_text(xml);
+ }
+ }
+
+ // @COMPAT At 3.0.0, free xml and return NULL if xml != NULL on error
+ last_error = xmlCtxtGetLastError(ctxt);
+ if (last_error != NULL) {
+ if (xml != NULL) {
+ crm_log_xml_info(xml, "Partial");
+ }
+ }
+
+ xmlFreeParserCtxt(ctxt);
+ return xml;
+}
+
+/*!
+ * \internal
+ * \brief Parse XML from a string
+ *
+ * \param[in] input String to parse
+ *
+ * \return XML tree parsed from the given string; may be \c NULL or only partial
+ * on error
+ */
+xmlNode *
+pcmk__xml_parse(const char *input)
+{
+ xmlNode *xml = NULL;
+ xmlDoc *output = NULL;
+ xmlParserCtxt *ctxt = NULL;
+ const xmlError *last_error = NULL;
+
+ if (input == NULL) {
+ return NULL;
+ }
+
+ ctxt = xmlNewParserCtxt();
+ if (ctxt == NULL) {
+ return NULL;
+ }
+
+ xmlCtxtResetLastError(ctxt);
+ xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err);
+
+ parse_xml_recover(&output, xmlCtxtReadDoc, ctxt, (pcmkXmlStr) input, NULL,
+ NULL);
+
+ if (output != NULL) {
+ xml = xmlDocGetRootElement(output);
+ }
+
+ // @COMPAT At 3.0.0, free xml and return NULL if xml != NULL; update doxygen
+ last_error = xmlCtxtGetLastError(ctxt);
+ if (last_error != NULL) {
+ if (xml != NULL) {
+ crm_log_xml_info(xml, "Partial");
+ }
+ }
+
+ xmlFreeParserCtxt(ctxt);
+ return xml;
+}
+
+/*!
+ * \internal
+ * \brief Append a string representation of an XML element to a buffer
+ *
+ * \param[in] data XML whose representation to append
+ * \param[in] options Group of \p pcmk__xml_fmt_options flags
+ * \param[in,out] buffer Where to append the content (must not be \p NULL)
+ * \param[in] depth Current indentation level
+ */
+static void
+dump_xml_element(const xmlNode *data, uint32_t options, GString *buffer,
+ int depth)
+{
+ bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty);
+ bool filtered = pcmk_is_set(options, pcmk__xml_fmt_filtered);
+ int spaces = pretty? (2 * depth) : 0;
+
+ for (int lpc = 0; lpc < spaces; lpc++) {
+ g_string_append_c(buffer, ' ');
+ }
+
+ pcmk__g_strcat(buffer, "<", data->name, NULL);
+
+ for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL;
+ attr = attr->next) {
+
+ if (!filtered || !pcmk__xa_filterable((const char *) (attr->name))) {
+ pcmk__dump_xml_attr(attr, buffer);
+ }
+ }
+
+ if (data->children == NULL) {
+ g_string_append(buffer, "/>");
+
+ } else {
+ g_string_append_c(buffer, '>');
+ }
+
+ if (pretty) {
+ g_string_append_c(buffer, '\n');
+ }
+
+ if (data->children) {
+ for (const xmlNode *child = data->children; child != NULL;
+ child = child->next) {
+ pcmk__xml_string(child, options, buffer, depth + 1);
+ }
+
+ for (int lpc = 0; lpc < spaces; lpc++) {
+ g_string_append_c(buffer, ' ');
+ }
+
+ pcmk__g_strcat(buffer, "</", data->name, ">", NULL);
+
+ if (pretty) {
+ g_string_append_c(buffer, '\n');
+ }
+ }
+}
+
+/*!
+ * \internal
+ * \brief Append XML text content to a buffer
+ *
+ * \param[in] data XML whose content to append
+ * \param[in] options Group of \p xml_log_options flags
+ * \param[in,out] buffer Where to append the content (must not be \p NULL)
+ * \param[in] depth Current indentation level
+ */
+static void
+dump_xml_text(const xmlNode *data, uint32_t options, GString *buffer,
+ int depth)
+{
+ bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty);
+ int spaces = pretty? (2 * depth) : 0;
+ const char *content = (const char *) data->content;
+ gchar *content_esc = NULL;
+
+ if (pcmk__xml_needs_escape(content, pcmk__xml_escape_text)) {
+ content_esc = pcmk__xml_escape(content, pcmk__xml_escape_text);
+ content = content_esc;
+ }
+
+ for (int lpc = 0; lpc < spaces; lpc++) {
+ g_string_append_c(buffer, ' ');
+ }
+
+ g_string_append(buffer, content);
+
+ if (pretty) {
+ g_string_append_c(buffer, '\n');
+ }
+ g_free(content_esc);
+}
+
+/*!
+ * \internal
+ * \brief Append XML CDATA content to a buffer
+ *
+ * \param[in] data XML whose content to append
+ * \param[in] options Group of \p pcmk__xml_fmt_options flags
+ * \param[in,out] buffer Where to append the content (must not be \p NULL)
+ * \param[in] depth Current indentation level
+ */
+static void
+dump_xml_cdata(const xmlNode *data, uint32_t options, GString *buffer,
+ int depth)
+{
+ bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty);
+ int spaces = pretty? (2 * depth) : 0;
+
+ for (int lpc = 0; lpc < spaces; lpc++) {
+ g_string_append_c(buffer, ' ');
+ }
+
+ pcmk__g_strcat(buffer, "<![CDATA[", (const char *) data->content, "]]>",
+ NULL);
+
+ if (pretty) {
+ g_string_append_c(buffer, '\n');
+ }
+}
+
+/*!
+ * \internal
+ * \brief Append an XML comment to a buffer
+ *
+ * \param[in] data XML whose content to append
+ * \param[in] options Group of \p pcmk__xml_fmt_options flags
+ * \param[in,out] buffer Where to append the content (must not be \p NULL)
+ * \param[in] depth Current indentation level
+ */
+static void
+dump_xml_comment(const xmlNode *data, uint32_t options, GString *buffer,
+ int depth)
+{
+ bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty);
+ int spaces = pretty? (2 * depth) : 0;
+
+ for (int lpc = 0; lpc < spaces; lpc++) {
+ g_string_append_c(buffer, ' ');
+ }
+
+ pcmk__g_strcat(buffer, "<!--", (const char *) data->content, "-->", NULL);
+
+ if (pretty) {
+ g_string_append_c(buffer, '\n');
+ }
+}
+
+/*!
+ * \internal
+ * \brief Get a string representation of an XML element type
+ *
+ * \param[in] type XML element type
+ *
+ * \return String representation of \p type
+ */
+static const char *
+xml_element_type_text(xmlElementType type)
+{
+ static const char *const element_type_names[] = {
+ [XML_ELEMENT_NODE] = "element",
+ [XML_ATTRIBUTE_NODE] = "attribute",
+ [XML_TEXT_NODE] = "text",
+ [XML_CDATA_SECTION_NODE] = "CDATA section",
+ [XML_ENTITY_REF_NODE] = "entity reference",
+ [XML_ENTITY_NODE] = "entity",
+ [XML_PI_NODE] = "PI",
+ [XML_COMMENT_NODE] = "comment",
+ [XML_DOCUMENT_NODE] = "document",
+ [XML_DOCUMENT_TYPE_NODE] = "document type",
+ [XML_DOCUMENT_FRAG_NODE] = "document fragment",
+ [XML_NOTATION_NODE] = "notation",
+ [XML_HTML_DOCUMENT_NODE] = "HTML document",
+ [XML_DTD_NODE] = "DTD",
+ [XML_ELEMENT_DECL] = "element declaration",
+ [XML_ATTRIBUTE_DECL] = "attribute declaration",
+ [XML_ENTITY_DECL] = "entity declaration",
+ [XML_NAMESPACE_DECL] = "namespace declaration",
+ [XML_XINCLUDE_START] = "XInclude start",
+ [XML_XINCLUDE_END] = "XInclude end",
+ };
+
+ if ((type < 0) || (type >= PCMK__NELEM(element_type_names))) {
+ return "unrecognized type";
+ }
+ return element_type_names[type];
+}
+
+/*!
+ * \internal
+ * \brief Create a string representation of an XML object
+ *
+ * libxml2's \c xmlNodeDumpOutput() doesn't allow filtering, doesn't escape
+ * special characters thoroughly, and doesn't allow a const argument.
+ *
+ * \param[in] data XML to convert
+ * \param[in] options Group of \p pcmk__xml_fmt_options flags
+ * \param[in,out] buffer Where to store the text (must not be \p NULL)
+ * \param[in] depth Current indentation level
+ *
+ * \todo Create a wrapper that doesn't require \p depth. Only used with
+ * recursive calls currently.
+ */
+void
+pcmk__xml_string(const xmlNode *data, uint32_t options, GString *buffer,
+ int depth)
+{
+ if (data == NULL) {
+ crm_trace("Nothing to dump");
+ return;
+ }
+
+ CRM_ASSERT(buffer != NULL);
+ CRM_CHECK(depth >= 0, depth = 0);
+
+ switch(data->type) {
+ case XML_ELEMENT_NODE:
+ /* Handle below */
+ dump_xml_element(data, options, buffer, depth);
+ break;
+ case XML_TEXT_NODE:
+ if (pcmk_is_set(options, pcmk__xml_fmt_text)) {
+ dump_xml_text(data, options, buffer, depth);
+ }
+ break;
+ case XML_COMMENT_NODE:
+ dump_xml_comment(data, options, buffer, depth);
+ break;
+ case XML_CDATA_SECTION_NODE:
+ dump_xml_cdata(data, options, buffer, depth);
+ break;
+ default:
+ crm_warn("Cannot convert XML %s node to text " CRM_XS " type=%d",
+ xml_element_type_text(data->type), data->type);
+ break;
+ }
+}
+
+/*!
+ * \internal
+ * \brief Write a string to a file stream, compressed using \c bzip2
+ *
+ * \param[in] text String to write
+ * \param[in] filename Name of file being written (for logging only)
+ * \param[in,out] stream Open file stream to write to
+ * \param[out] bytes_out Number of bytes written (valid only on success)
+ *
+ * \return Standard Pacemaker return code
+ */
+static int
+write_compressed_stream(char *text, const char *filename, FILE *stream,
+ unsigned int *bytes_out)
+{
+ unsigned int bytes_in = 0;
+ int rc = pcmk_rc_ok;
+
+ // (5, 0, 0): (intermediate block size, silent, default workFactor)
+ BZFILE *bz_file = BZ2_bzWriteOpen(&rc, stream, 5, 0, 0);
+
+ rc = pcmk__bzlib2rc(rc);
+ if (rc != pcmk_rc_ok) {
+ crm_warn("Not compressing %s: could not prepare file stream: %s "
+ CRM_XS " rc=%d",
+ filename, pcmk_rc_str(rc), rc);
+ goto done;
+ }
+
+ BZ2_bzWrite(&rc, bz_file, text, strlen(text));
+ rc = pcmk__bzlib2rc(rc);
+ if (rc != pcmk_rc_ok) {
+ crm_warn("Not compressing %s: could not compress data: %s "
+ CRM_XS " rc=%d errno=%d",
+ filename, pcmk_rc_str(rc), rc, errno);
+ goto done;
+ }
+
+ BZ2_bzWriteClose(&rc, bz_file, 0, &bytes_in, bytes_out);
+ bz_file = NULL;
+ rc = pcmk__bzlib2rc(rc);
+ if (rc != pcmk_rc_ok) {
+ crm_warn("Not compressing %s: could not write compressed data: %s "
+ CRM_XS " rc=%d errno=%d",
+ filename, pcmk_rc_str(rc), rc, errno);
+ goto done;
+ }
+
+ crm_trace("Compressed XML for %s from %u bytes to %u",
+ filename, bytes_in, *bytes_out);
+
+done:
+ if (bz_file != NULL) {
+ BZ2_bzWriteClose(&rc, bz_file, 0, NULL, NULL);
+ }
+ return rc;
+}
+
+/*!
+ * \internal
+ * \brief Write XML to a file stream
+ *
+ * \param[in] xml XML to write
+ * \param[in] filename Name of file being written (for logging only)
+ * \param[in,out] stream Open file stream corresponding to filename (closed
+ * when this function returns)
+ * \param[in] compress Whether to compress XML before writing
+ * \param[out] nbytes Number of bytes written
+ *
+ * \return Standard Pacemaker return code
+ */
+static int
+write_xml_stream(const xmlNode *xml, const char *filename, FILE *stream,
+ bool compress, unsigned int *nbytes)
+{
+ // @COMPAT Drop nbytes as arg when we drop write_xml_fd()/write_xml_file()
+ GString *buffer = g_string_sized_new(1024);
+ unsigned int bytes_out = 0;
+ int rc = pcmk_rc_ok;
+
+ pcmk__xml_string(xml, pcmk__xml_fmt_pretty, buffer, 0);
+ CRM_CHECK(!pcmk__str_empty(buffer->str),
+ crm_log_xml_info(xml, "dump-failed");
+ rc = pcmk_rc_error;
+ goto done);
+
+ crm_log_xml_trace(xml, "writing");
+
+ if (compress
+ && (write_compressed_stream(buffer->str, filename, stream,
+ &bytes_out) == pcmk_rc_ok)) {
+ goto done;
+ }
+
+ rc = fprintf(stream, "%s", buffer->str);
+ if (rc < 0) {
+ rc = EIO;
+ crm_perror(LOG_ERR, "writing %s", filename);
+ goto done;
+ }
+ bytes_out = (unsigned int) rc;
+ rc = pcmk_rc_ok;
+
+done:
+ if (fflush(stream) != 0) {
+ rc = errno;
+ crm_perror(LOG_ERR, "flushing %s", filename);
+ }
+
+ // Don't report error if the file does not support synchronization
+ if ((fsync(fileno(stream)) < 0) && (errno != EROFS) && (errno != EINVAL)) {
+ rc = errno;
+ crm_perror(LOG_ERR, "synchronizing %s", filename);
+ }
+
+ fclose(stream);
+ crm_trace("Saved %u bytes to %s as XML", bytes_out, filename);
+
+ if (nbytes != NULL) {
+ *nbytes = bytes_out;
+ }
+ g_string_free(buffer, TRUE);
+ return rc;
+}
+
+/*!
+ * \internal
+ * \brief Write XML to a file descriptor
+ *
+ * \param[in] xml XML to write
+ * \param[in] filename Name of file being written (for logging only)
+ * \param[in] fd Open file descriptor corresponding to \p filename
+ * \param[in] compress If \c true, compress XML before writing
+ * \param[out] nbytes Number of bytes written (can be \c NULL)
+ *
+ * \return Standard Pacemaker return code
+ */
+int
+pcmk__xml_write_fd(const xmlNode *xml, const char *filename, int fd,
+ bool compress, unsigned int *nbytes)
+{
+ // @COMPAT Drop compress and nbytes arguments when we drop write_xml_fd()
+ FILE *stream = NULL;
+
+ CRM_CHECK((xml != NULL) && (fd > 0), return EINVAL);
+ stream = fdopen(fd, "w");
+ if (stream == NULL) {
+ return errno;
+ }
+
+ return write_xml_stream(xml, pcmk__s(filename, "unnamed file"), stream,
+ compress, nbytes);
+}
+
+/*!
+ * \internal
+ * \brief Write XML to a file
+ *
+ * \param[in] xml XML to write
+ * \param[in] filename Name of file to write
+ * \param[in] compress If \c true, compress XML before writing
+ * \param[out] nbytes Number of bytes written (can be \c NULL)
+ *
+ * \return Standard Pacemaker return code
+ */
+int
+pcmk__xml_write_file(const xmlNode *xml, const char *filename, bool compress,
+ unsigned int *nbytes)
+{
+ // @COMPAT Drop nbytes argument when we drop write_xml_fd()
+ FILE *stream = NULL;
+
+ CRM_CHECK((xml != NULL) && (filename != NULL), return EINVAL);
+ stream = fopen(filename, "w");
+ if (stream == NULL) {
+ return errno;
+ }
+
+ return write_xml_stream(xml, filename, stream, compress, nbytes);
+}
+
+/*!
+ * \internal
+ * \brief Serialize XML (using libxml) into provided descriptor
+ *
+ * \param[in] fd File descriptor to (piece-wise) write to
+ * \param[in] cur XML subtree to proceed
+ *
+ * \return a standard Pacemaker return code
+ */
+int
+pcmk__xml2fd(int fd, xmlNode *cur)
+{
+ bool success;
+
+ xmlOutputBuffer *fd_out = xmlOutputBufferCreateFd(fd, NULL);
+ pcmk__mem_assert(fd_out);
+ xmlNodeDumpOutput(fd_out, cur->doc, cur, 0, pcmk__xml_fmt_pretty, NULL);
+
+ success = xmlOutputBufferWrite(fd_out, sizeof("\n") - 1, "\n") != -1;
+
+ success = xmlOutputBufferClose(fd_out) != -1 && success;
+
+ if (!success) {
+ return EIO;
+ }
+
+ fsync(fd);
+ return pcmk_rc_ok;
+}
+
+void
+save_xml_to_file(const xmlNode *xml, const char *desc, const char *filename)
+{
+ char *f = NULL;
+
+ if (filename == NULL) {
+ char *uuid = crm_generate_uuid();
+
+ f = crm_strdup_printf("%s/%s", pcmk__get_tmpdir(), uuid);
+ filename = f;
+ free(uuid);
+ }
+
+ crm_info("Saving %s to %s", desc, filename);
+ pcmk__xml_write_file(xml, filename, false, NULL);
+ free(f);
+}
+
+
+// Deprecated functions kept only for backward API compatibility
+// LCOV_EXCL_START
+
+#include <crm/common/xml_io_compat.h>
+
+xmlNode *
+filename2xml(const char *filename)
+{
+ return pcmk__xml_read(filename);
+}
+
+xmlNode *
+stdin2xml(void)
+{
+ return pcmk__xml_read(NULL);
+}
+
+xmlNode *
+string2xml(const char *input)
+{
+ return pcmk__xml_parse(input);
+}
+
+char *
+dump_xml_formatted(const xmlNode *xml)
+{
+ char *str = NULL;
+ GString *buffer = g_string_sized_new(1024);
+
+ pcmk__xml_string(xml, pcmk__xml_fmt_pretty, buffer, 0);
+
+ str = pcmk__str_copy(buffer->str);
+ g_string_free(buffer, TRUE);
+ return str;
+}
+
+char *
+dump_xml_formatted_with_text(const xmlNode *xml)
+{
+ char *str = NULL;
+ GString *buffer = g_string_sized_new(1024);
+
+ pcmk__xml_string(xml, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, buffer, 0);
+
+ str = pcmk__str_copy(buffer->str);
+ g_string_free(buffer, TRUE);
+ return str;
+}
+
+char *
+dump_xml_unformatted(const xmlNode *xml)
+{
+ char *str = NULL;
+ GString *buffer = g_string_sized_new(1024);
+
+ pcmk__xml_string(xml, 0, buffer, 0);
+
+ str = pcmk__str_copy(buffer->str);
+ g_string_free(buffer, TRUE);
+ return str;
+}
+
+int
+write_xml_fd(const xmlNode *xml, const char *filename, int fd,
+ gboolean compress)
+{
+ unsigned int nbytes = 0;
+ int rc = pcmk__xml_write_fd(xml, filename, fd, compress, &nbytes);
+
+ if (rc != pcmk_rc_ok) {
+ return pcmk_rc2legacy(rc);
+ }
+ return (int) nbytes;
+}
+
+int
+write_xml_file(const xmlNode *xml, const char *filename, gboolean compress)
+{
+ unsigned int nbytes = 0;
+ int rc = pcmk__xml_write_file(xml, filename, compress, &nbytes);
+
+ if (rc != pcmk_rc_ok) {
+ return pcmk_rc2legacy(rc);
+ }
+ return (int) nbytes;
+}
+
+// LCOV_EXCL_STOP
+// End deprecated API
diff --git a/lib/common/xpath.c b/lib/common/xpath.c
index d90f1c5..9fc95c5 100644
--- a/lib/common/xpath.c
+++ b/lib/common/xpath.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2004-2023 the Pacemaker project contributors
+ * Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -10,7 +10,7 @@
#include <crm_internal.h>
#include <stdio.h>
#include <string.h>
-#include <crm/msg_xml.h>
+#include <crm/common/xml.h>
#include <crm/common/xml_internal.h>
#include "crmcommon_private.h"
@@ -147,7 +147,7 @@ xpath_search(const xmlNode *xml_top, const char *path)
CRM_CHECK(strlen(path) > 0, return NULL);
xpathCtx = xmlXPathNewContext(xml_top->doc);
- CRM_ASSERT(xpathCtx != NULL);
+ pcmk__mem_assert(xpathCtx);
xpathObj = xmlXPathEvalExpression(xpathExpr, xpathCtx);
xmlXPathFreeContext(xpathCtx);
@@ -186,28 +186,6 @@ crm_foreach_xpath_result(xmlNode *xml, const char *xpath,
}
xmlNode *
-get_xpath_object_relative(const char *xpath, xmlNode * xml_obj, int error_level)
-{
- xmlNode *result = NULL;
- char *xpath_full = NULL;
- char *xpath_prefix = NULL;
-
- if (xml_obj == NULL || xpath == NULL) {
- return NULL;
- }
-
- xpath_prefix = (char *)xmlGetNodePath(xml_obj);
-
- xpath_full = crm_strdup_printf("%s%s", xpath_prefix, xpath);
-
- result = get_xpath_object(xpath_full, xml_obj, error_level);
-
- free(xpath_prefix);
- free(xpath_full);
- return result;
-}
-
-xmlNode *
get_xpath_object(const char *xpath, xmlNode * xml_obj, int error_level)
{
int max;
@@ -300,9 +278,9 @@ pcmk__element_xpath(const xmlNode *xml)
pcmk__g_strcat(xpath, "/", (const char *) xml->name, NULL);
}
- id = ID(xml);
+ id = pcmk__xe_id(xml);
if (id != NULL) {
- pcmk__g_strcat(xpath, "[@" XML_ATTR_ID "='", id, "']", NULL);
+ pcmk__g_strcat(xpath, "[@" PCMK_XA_ID "='", id, "']", NULL);
}
return xpath;
@@ -320,7 +298,7 @@ pcmk__xpath_node_id(const char *xpath, const char *node)
return retval;
}
- patt = crm_strdup_printf("/%s[@" XML_ATTR_ID "=", node);
+ patt = crm_strdup_printf("/%s[@" PCMK_XA_ID "=", node);
start = strstr(xpath, patt);
if (!start) {
@@ -339,6 +317,30 @@ pcmk__xpath_node_id(const char *xpath, const char *node)
return retval;
}
+static int
+output_attr_child(xmlNode *child, void *userdata)
+{
+ pcmk__output_t *out = userdata;
+
+ out->info(out, " Value: %s \t(id=%s)",
+ crm_element_value(child, PCMK_XA_VALUE),
+ pcmk__s(pcmk__xe_id(child), "<none>"));
+ return pcmk_rc_ok;
+}
+
+void
+pcmk__warn_multiple_name_matches(pcmk__output_t *out, xmlNode *search,
+ const char *name)
+{
+ if (out == NULL || name == NULL || search == NULL ||
+ search->children == NULL) {
+ return;
+ }
+
+ out->info(out, "Multiple attributes match " PCMK_XA_NAME "=%s", name);
+ pcmk__xe_foreach_child(search, NULL, output_attr_child, out);
+}
+
// Deprecated functions kept only for backward API compatibility
// LCOV_EXCL_START
@@ -363,13 +365,32 @@ xml_get_path(const xmlNode *xml)
if (g_path == NULL) {
return NULL;
}
-
- path = strdup((const char *) g_path->str);
- CRM_ASSERT(path != NULL);
-
+ path = pcmk__str_copy(g_path->str);
g_string_free(g_path, TRUE);
return path;
}
+xmlNode *
+get_xpath_object_relative(const char *xpath, xmlNode *xml_obj, int error_level)
+{
+ xmlNode *result = NULL;
+ char *xpath_full = NULL;
+ char *xpath_prefix = NULL;
+
+ if (xml_obj == NULL || xpath == NULL) {
+ return NULL;
+ }
+
+ xpath_prefix = (char *)xmlGetNodePath(xml_obj);
+
+ xpath_full = crm_strdup_printf("%s%s", xpath_prefix, xpath);
+
+ result = get_xpath_object(xpath_full, xml_obj, error_level);
+
+ free(xpath_prefix);
+ free(xpath_full);
+ return result;
+}
+
// LCOV_EXCL_STOP
// End deprecated API