From efc289cfe769b036a849d3cd0f4171e2a9a2a524 Mon Sep 17 00:00:00 2001 From: zouzhimin Date: Thu, 21 Mar 2024 07:15:03 +0800 Subject: [PATCH] Pacemaker Remote nodes can validate against later schema versions (cherry picked from commit ab5458c1dc00f0e99a85179a5ff5f4eff1c6729d) --- 002-schema-transfer.patch | 1986 +++++++++++++++++++++++++++++++++++++ pacemaker.spec | 7 +- 2 files changed, 1992 insertions(+), 1 deletion(-) create mode 100644 002-schema-transfer.patch diff --git a/002-schema-transfer.patch b/002-schema-transfer.patch new file mode 100644 index 0000000..9c1c05b --- /dev/null +++ b/002-schema-transfer.patch @@ -0,0 +1,1986 @@ +From 9e0c58dc3b949e4eadfa43364e60677478b7aa0f Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Thu, 7 Sep 2023 16:48:52 -0400 +Subject: [PATCH 01/15] Refactor: libcrmcommon: Break schema version comparison + out... + +...into its own function. Then, wrap this function with +schema_sort_directory so existing code doesn't need to be changed. This +allows us to use the version comparison elsewhere. +--- + lib/common/schemas.c | 26 ++++++++++++++++---------- + 1 file changed, 16 insertions(+), 10 deletions(-) + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index 372e87223..b3ff05917 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -195,7 +195,20 @@ 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) ++{ ++ for (int i = 0; i < 2; ++i) { ++ if (a_version.v[i] < b_version.v[i]) { ++ return -1; ++ } else if (a_version.v[i] > b_version.v[i]) { ++ return 1; ++ } ++ } ++ return 0; ++} ++ ++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; +@@ -206,14 +219,7 @@ schema_sort(const struct dirent **a, const struct dirent **b) + return 0; + } + +- for (int i = 0; i < 2; ++i) { +- if (a_version.v[i] < b_version.v[i]) { +- return -1; +- } else if (a_version.v[i] > b_version.v[i]) { +- return 1; +- } +- } +- return 0; ++ return schema_cmp(a_version, b_version); + } + + /*! +@@ -413,7 +419,7 @@ crm_schema_init(void) + + wrap_libxslt(false); + +- max = scandir(base, &namelist, schema_filter, schema_sort); ++ max = scandir(base, &namelist, schema_filter, schema_cmp_directory); + if (max < 0) { + crm_notice("scandir(%s) failed: %s (%d)", base, strerror(errno), errno); + free(base); +-- +2.41.0 + +From e7d7c33eb8329c3c50fe648133cbb7651c1ecb9d Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Wed, 29 Nov 2023 09:37:31 -0500 +Subject: [PATCH 02/15] Feature: libcrmcommon: Add + pcmk__schema_files_later_than. + +This function takes in a schema version and returns a list of all RNG +and XSLT files from all schema versions more recent than that one. + +Also, add unit tests for the new function. +--- + include/crm/common/internal.h | 1 + + lib/common/schemas.c | 62 ++++++++++- + lib/common/tests/schemas/Makefile.am | 42 ++++++- + .../pcmk__schema_files_later_than_test.c | 104 ++++++++++++++++++ + 4 files changed, 207 insertions(+), 2 deletions(-) + create mode 100644 lib/common/tests/schemas/pcmk__schema_files_later_than_test.c + +diff --git a/include/crm/common/internal.h b/include/crm/common/internal.h +index 307860636..a3cd455dc 100644 +--- a/include/crm/common/internal.h ++++ b/include/crm/common/internal.h +@@ -134,6 +134,7 @@ bool pcmk__procfs_has_pids(void); + void crm_schema_init(void); + void crm_schema_cleanup(void); + ++GList *pcmk__schema_files_later_than(const char *name); + + /* internal functions related to process IDs (from pid.c) */ + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index b3ff05917..1c60738ec 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -168,7 +168,11 @@ xml_latest_schema(void) + static inline bool + version_from_filename(const char *filename, pcmk__schema_version_t *version) + { +- return sscanf(filename, "pacemaker-%hhu.%hhu.rng", &(version->v[0]), &(version->v[1])) == 2; ++ 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 +@@ -1291,6 +1295,62 @@ cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs) + 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(known_schemas)); ++ 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); ++ } ++ } ++ } ++ ++ return lst; ++} ++ + void + pcmk__log_known_schemas(void) + { +diff --git a/lib/common/tests/schemas/Makefile.am b/lib/common/tests/schemas/Makefile.am +index 5f485b3e9..b5c5e7f3c 100644 +--- a/lib/common/tests/schemas/Makefile.am ++++ b/lib/common/tests/schemas/Makefile.am +@@ -10,7 +10,47 @@ + 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. +-check_PROGRAMS = pcmk__xml_find_x_0_schema_index_test ++check_PROGRAMS = pcmk__schema_files_later_than_test \ ++ pcmk__xml_find_x_0_schema_index_test + + TESTS = $(check_PROGRAMS) ++ ++$(TESTS): setup-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. ++.PHONY: setup-schema-dir ++setup-schema-dir: ++ $(MKDIR_P) schemas ++ ( cd schemas ; \ ++ ln -sf $(abs_top_builddir)/xml/pacemaker-next.rng . ; \ ++ ln -sf $(abs_top_builddir)/xml/upgrade-*.xsl . ; \ ++ for f in $(shell ls -1v $(abs_top_builddir)/xml/pacemaker-[0-9]*.rng | head -15); do \ ++ ln -sf $$f $$(basename $$f); \ ++ done ; \ ++ for f in $(shell ls -1 $(top_srcdir)/xml/*.rng | grep -v pacemaker); do \ ++ ln -sf ../$$f $$(basename $$f); \ ++ done ) ++ ++.PHONY: clean-local ++clean-local: ++ -rm -rf schemas +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 000000000..50b5f4334 +--- /dev/null ++++ b/lib/common/tests/schemas/pcmk__schema_files_later_than_test.c +@@ -0,0 +1,104 @@ ++/* ++ * 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 ++ ++#include ++#include ++ ++#include ++ ++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)) +-- +2.41.0 + +From 76859d61f4d35e1f8b7c35d8766d3d0c123d4552 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Mon, 11 Sep 2023 08:56:22 -0400 +Subject: [PATCH 03/15] Refactor: libcrmcommon: Allow more specs in + pcmk__xml_artefact_path. + +If the given filespec already has a .rng or .xsl extension, don't add an +additional one. This allows reusing this function to grab files given +in another schema file's externalRef links without modification. +--- + lib/common/xml.c | 12 ++++++++++-- + 1 file changed, 10 insertions(+), 2 deletions(-) + +diff --git a/lib/common/xml.c b/lib/common/xml.c +index 53ebff770..142b501df 100644 +--- a/lib/common/xml.c ++++ b/lib/common/xml.c +@@ -2618,11 +2618,19 @@ pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec) + 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", base, filespec); ++ } else { ++ ret = crm_strdup_printf("%s/%s.rng", base, filespec); ++ } + break; + case pcmk__xml_artefact_ns_legacy_xslt: + case pcmk__xml_artefact_ns_base_xslt: +- ret = crm_strdup_printf("%s/%s.xsl", base, filespec); ++ if (pcmk__ends_with(filespec, ".xsl")) { ++ ret = crm_strdup_printf("%s/%s", base, filespec); ++ } else { ++ ret = crm_strdup_printf("%s/%s.xsl", base, filespec); ++ } + break; + default: + crm_err("XML artefact family specified as %u not recognized", ns); +-- +2.41.0 + +From 0d702bba5b50e1eede201853c7680a2517fade9f Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Mon, 18 Sep 2023 12:37:46 -0400 +Subject: [PATCH 04/15] Feature: build: Add PCMK__REMOTE_SCHEMA_DIR to + configure.ac. + +This is a new subdirectory where schema files downloaded to a remote +executor can be stored. + +Also, add support for an environment variable that can override this +compile-time setting and explain it in the documentation. +--- + configure.ac | 5 +++++ + doc/sphinx/Pacemaker_Explained/local-options.rst | 12 ++++++++++++ + etc/sysconfig/pacemaker.in | 7 +++++++ + include/crm/common/options_internal.h | 1 + + 4 files changed, 25 insertions(+) + +diff --git a/configure.ac b/configure.ac +index 17cee41e9..bd548200c 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -838,6 +838,11 @@ AC_DEFINE_UNQUOTED([CRM_SCHEMA_DIRECTORY], ["$CRM_SCHEMA_DIRECTORY"], + [Location for the Pacemaker Relax-NG Schema]) + AC_SUBST(CRM_SCHEMA_DIRECTORY) + ++PCMK__REMOTE_SCHEMA_DIR="${localstatedir}/lib/pacemaker/schemas" ++AC_DEFINE_UNQUOTED([PCMK__REMOTE_SCHEMA_DIR], ["$PCMK__REMOTE_SCHEMA_DIR"], ++ [Location to store Relax-NG Schema files on remote nodes]) ++AC_SUBST(PCMK__REMOTE_SCHEMA_DIR) ++ + CRM_CORE_DIR="${localstatedir}/lib/pacemaker/cores" + AC_DEFINE_UNQUOTED([CRM_CORE_DIR], ["$CRM_CORE_DIR"], + [Directory Pacemaker daemons should change to (without systemd, core files will go here)]) +diff --git a/doc/sphinx/Pacemaker_Explained/local-options.rst b/doc/sphinx/Pacemaker_Explained/local-options.rst +index 91eda6632..118256247 100644 +--- a/doc/sphinx/Pacemaker_Explained/local-options.rst ++++ b/doc/sphinx/Pacemaker_Explained/local-options.rst +@@ -478,6 +478,18 @@ environment variables when Pacemaker daemons start up. + - *Advanced Use Only:* Specify an alternate location for RNG schemas and + XSL transforms. + ++ * - .. _pcmk_remote_schema_directory: ++ ++ .. index:: ++ pair:: node option; PCMK_remote_schema_directory ++ ++ PCMK_remote_schema_directory ++ - :ref:`text ` ++ - |PCMK__REMOTE_SCHEMA_DIR| ++ - *Advanced Use Only:* Specify an alternate location on Pacemaker Remote ++ nodes for storing newer RNG schemas and XSL transforms fetched from ++ the cluster. ++ + * - .. _pcmk_valgrind_enabled: + + .. index:: +diff --git a/etc/sysconfig/pacemaker.in b/etc/sysconfig/pacemaker.in +index 0c3609d8e..487a104a7 100644 +--- a/etc/sysconfig/pacemaker.in ++++ b/etc/sysconfig/pacemaker.in +@@ -339,6 +339,13 @@ + # + # Default: PCMK_schema_directory="@CRM_SCHEMA_DIRECTORY@" + ++# PCMK_remote_schema_directory (Advanced Use Only) ++# ++# Specify an alternate location on Pacemaker Remote nodes for storing newer ++# RNG schemas and XSL transforms fetched from the cluster. ++# ++# Default: PCMK_remote_schema_directory="@PCMK__REMOTE_SCHEMA_DIR@" ++ + # G_SLICE (Advanced Use Only) + # + # Affect the behavior of glib's memory allocator. Setting to "always-malloc" +diff --git a/include/crm/common/options_internal.h b/include/crm/common/options_internal.h +index 5c561fd1f..a9316ca33 100644 +--- a/include/crm/common/options_internal.h ++++ b/include/crm/common/options_internal.h +@@ -95,6 +95,7 @@ bool pcmk__valid_sbd_timeout(const char *value); + #define PCMK__ENV_PANIC_ACTION "panic_action" + #define PCMK__ENV_PHYSICAL_HOST "physical_host" + #define PCMK__ENV_REMOTE_ADDRESS "remote_address" ++#define PCMK__ENV_REMOTE_SCHEMA_DIR "remote_schema_directory" + #define PCMK__ENV_REMOTE_PID1 "remote_pid1" + #define PCMK__ENV_REMOTE_PORT "remote_port" + #define PCMK__ENV_RESPAWNED "respawned" +-- +2.41.0 + +From 16d46de389f33a8b29cbd74ee2c9077f28029446 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Wed, 6 Dec 2023 12:38:12 -0500 +Subject: [PATCH 05/15] Feature: libcrmcommon: Add pcmk__remote_schema_dir. + +This function checks both the environment (thus, +/etc/sysconfig/pacemaker as well) and ./configure options for the +location where any additional schema files should be stored. +--- + include/crm/common/xml_internal.h | 1 + + lib/common/schemas.c | 17 +++++++++++++++++ + 2 files changed, 18 insertions(+) + +diff --git a/include/crm/common/xml_internal.h b/include/crm/common/xml_internal.h +index f319856c8..cb27ec6b2 100644 +--- a/include/crm/common/xml_internal.h ++++ b/include/crm/common/xml_internal.h +@@ -447,5 +447,6 @@ gboolean pcmk__validate_xml(xmlNode *xml_blob, const char *validation, + void *error_handler_context); + + void pcmk__log_known_schemas(void); ++const char *pcmk__remote_schema_dir(void); + + #endif // PCMK__XML_INTERNAL__H +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index 1c60738ec..c03d80036 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -1351,6 +1351,23 @@ pcmk__schema_files_later_than(const char *name) + return lst; + } + ++/*! ++ * \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_DIR); ++ ++ if (pcmk__str_empty(dir)) { ++ return PCMK__REMOTE_SCHEMA_DIR; ++ } ++ ++ return dir; ++} ++ + void + pcmk__log_known_schemas(void) + { +-- +2.41.0 + +From d2ed95651ea7c44153a34ba009494c70928319d7 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Tue, 19 Sep 2023 11:40:59 -0400 +Subject: [PATCH 06/15] Refactor: libcrmcommon: Look in more dirs in + pcmk__xml_artefact_path. + +If the directory returned by pcmk__xml_artefact_root doesn't contain the +file we were looking for, fall back on PCMK__REMOTE_SCHEMA_DIR. If that +still doesn't contain the file, return NULL. +--- + lib/common/xml.c | 32 +++++++++++++++++++++++++------- + 1 file changed, 25 insertions(+), 7 deletions(-) + +diff --git a/lib/common/xml.c b/lib/common/xml.c +index 142b501df..a1b1291f9 100644 +--- a/lib/common/xml.c ++++ b/lib/common/xml.c +@@ -10,6 +10,7 @@ + #include + + #include ++#include + #include + #include + #include +@@ -2610,33 +2611,50 @@ 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: + if (pcmk__ends_with(filespec, ".rng")) { +- ret = crm_strdup_printf("%s/%s", base, filespec); ++ ret = crm_strdup_printf("%s/%s", path, filespec); + } else { +- ret = crm_strdup_printf("%s/%s.rng", base, filespec); ++ ret = crm_strdup_printf("%s/%s.rng", path, filespec); + } + break; + case pcmk__xml_artefact_ns_legacy_xslt: + case pcmk__xml_artefact_ns_base_xslt: + if (pcmk__ends_with(filespec, ".xsl")) { +- ret = crm_strdup_printf("%s/%s", base, filespec); ++ ret = crm_strdup_printf("%s/%s", path, filespec); + } else { +- ret = crm_strdup_printf("%s/%s.xsl", base, filespec); ++ 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(); ++ ret = find_artefact(ns, remote_schema_dir, filespec); ++ } ++ + return ret; + } + +-- +2.41.0 + +From 9f4da4102a40f7dbfe0d78c7735c2801ba852f4b Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Mon, 11 Sep 2023 09:17:18 -0400 +Subject: [PATCH 07/15] Feature: libcrmcommon: Add XML attrs needed for schema + file transfer. + +Also, move PCMK__XA_CONN_HOST to be alphabetized. +--- + include/crm_internal.h | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + +diff --git a/include/crm_internal.h b/include/crm_internal.h +index 71a0f7efa..3bc8d096a 100644 +--- a/include/crm_internal.h ++++ b/include/crm_internal.h +@@ -81,17 +81,21 @@ + #define PCMK__XA_CONFIG_ERRORS "config-errors" + #define PCMK__XA_CONFIG_WARNINGS "config-warnings" + #define PCMK__XA_CONFIRM "confirm" ++#define PCMK__XA_CONN_HOST "connection_host" + #define PCMK__XA_CRMD "crmd" + #define PCMK__XA_EXPECTED "expected" ++#define PCMK__XA_FILE "file" + #define PCMK__XA_GRAPH_ERRORS "graph-errors" + #define PCMK__XA_GRAPH_WARNINGS "graph-warnings" + #define PCMK__XA_IN_CCM "in_ccm" + #define PCMK__XA_JOIN "join" + #define PCMK__XA_MODE "mode" + #define PCMK__XA_NODE_START_STATE "node_start_state" ++#define PCMK__XA_PATH "path" ++#define PCMK__XA_SCHEMA "schema" ++#define PCMK__XA_SCHEMAS "schemas" + #define PCMK__XA_TASK "task" + #define PCMK__XA_UPTIME "uptime" +-#define PCMK__XA_CONN_HOST "connection_host" + + + /* +-- +2.41.0 + +From 3384643bd4572ad03e183b16d5cc84fe69599380 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Mon, 11 Sep 2023 10:46:26 -0400 +Subject: [PATCH 08/15] Feature: libcrmcommon: Add pcmk__build_schema_xml_node. + +This function adds a given RNG or XSLT file and all of the files they +refer to as children of a given XML node. + +Also, add unit tests for the new function. +--- + include/crm/common/internal.h | 3 + + lib/common/schemas.c | 144 +++++++++++++++++ + lib/common/tests/schemas/Makefile.am | 3 +- + .../pcmk__build_schema_xml_node_test.c | 149 ++++++++++++++++++ + 4 files changed, 298 insertions(+), 1 deletion(-) + create mode 100644 lib/common/tests/schemas/pcmk__build_schema_xml_node_test.c + +diff --git a/include/crm/common/internal.h b/include/crm/common/internal.h +index a3cd455dc..318003efe 100644 +--- a/include/crm/common/internal.h ++++ b/include/crm/common/internal.h +@@ -135,6 +135,9 @@ void crm_schema_init(void); + void crm_schema_cleanup(void); + + GList *pcmk__schema_files_later_than(const char *name); ++void pcmk__build_schema_xml_node(xmlNode *parent, const char *name, ++ GList **already_included); ++ + + /* internal functions related to process IDs (from pid.c) */ + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index c03d80036..1bcdff031 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -1351,6 +1351,150 @@ pcmk__schema_files_later_than(const char *name) + return lst; + } + ++static void ++append_href(xmlNode *xml, void *user_data) ++{ ++ GList **list = user_data; ++ const char *href = crm_element_value(xml, "href"); ++ char *s = NULL; ++ ++ if (href == NULL) { ++ return; ++ } ++ ++ s = strdup(href); ++ CRM_ASSERT(s != NULL); ++ *list = g_list_prepend(*list, s); ++} ++ ++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 = string2xml(contents); ++ ++ 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 = strdup(file); ++ CRM_ASSERT(path != NULL); ++ } ++ ++ 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 node with the contents of the file ++ * as a CDATA block underneath it. ++ */ ++ file_node = create_xml_node(parent, PCMK__XA_FILE); ++ if (file_node == NULL) { ++ free(contents); ++ free(path); ++ return; ++ } ++ ++ 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 or 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) ++{ ++ /* First, create an unattached node to add all the schema files to as children. */ ++ xmlNode *schema_node = create_xml_node(NULL, PCMK__XA_SCHEMA); ++ ++ crm_xml_add(schema_node, XML_ATTR_VERSION, name); ++ add_schema_file_to_xml(schema_node, name, already_included); ++ ++ /* Then, if we actually added any children, attach the node to parent. If ++ * we did not add any children (for instance, name was invalid), this prevents ++ * us from returning a document with additional empty children. ++ */ ++ if (schema_node->children != NULL) { ++ xmlAddChild(parent, schema_node); ++ } else { ++ free_xml(schema_node); ++ } ++} ++ + /*! + * \internal + * \brief Return the directory containing any extra schema files that a +diff --git a/lib/common/tests/schemas/Makefile.am b/lib/common/tests/schemas/Makefile.am +index b5c5e7f3c..8854eb264 100644 +--- a/lib/common/tests/schemas/Makefile.am ++++ b/lib/common/tests/schemas/Makefile.am +@@ -13,7 +13,8 @@ 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. +-check_PROGRAMS = pcmk__schema_files_later_than_test \ ++check_PROGRAMS = pcmk__build_schema_xml_node_test \ ++ pcmk__schema_files_later_than_test \ + pcmk__xml_find_x_0_schema_index_test + + TESTS = $(check_PROGRAMS) +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 000000000..1f5cb6ce1 +--- /dev/null ++++ b/lib/common/tests/schemas/pcmk__build_schema_xml_node_test.c +@@ -0,0 +1,149 @@ ++/* ++ * 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 ++ ++#include ++#include ++#include ++ ++#include ++ ++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(); ++ 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 = create_xml_node(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 = create_xml_node(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: ++ * ++ * ++ * ++ * CDATA ++ * CDATA ++ * ... ++ * ++ * ++ */ ++ schema_node = pcmk__xml_first_child(parent); ++ assert_string_equal("pacemaker-3.0", crm_element_value(schema_node, XML_ATTR_VERSION)); ++ ++ file_node = pcmk__xml_first_child(schema_node); ++ 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__xml_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 = create_xml_node(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__xml_first_child(parent); ++ assert_string_equal("pacemaker-2.0", crm_element_value(schema_node, XML_ATTR_VERSION)); ++ ++ file_node = pcmk__xml_first_child(schema_node); ++ 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__xml_next(file_node); ++ i++; ++ } ++ ++ schema_node = pcmk__xml_next(schema_node); ++ assert_string_equal("pacemaker-2.1", crm_element_value(schema_node, XML_ATTR_VERSION)); ++ ++ file_node = pcmk__xml_first_child(schema_node); ++ 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__xml_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)) +-- +2.41.0 + +From 036eb9f59326962ed2d1f2f4af88b20755a046d5 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Thu, 14 Sep 2023 10:02:16 -0400 +Subject: [PATCH 09/15] Feature: daemons: Add a new fetch_schemas CIB command. + +This command wraps pcmk__schema_files_later_than and +pcmk__build_schema_xml_node to produce a new CIB command that takes in a +minimum schema version and returns a big XML message containing all the +schema files after that version. +--- + daemons/based/based_messages.c | 43 +++++++++++++++++++++++++++++++++ + daemons/based/based_operation.c | 1 + + daemons/based/pacemaker-based.h | 4 +++ + include/crm/cib/cib_types.h | 3 +++ + include/crm/cib/internal.h | 2 ++ + lib/cib/cib_client.c | 15 ++++++++++++ + lib/cib/cib_ops.c | 3 +++ + 7 files changed, 71 insertions(+) + +diff --git a/daemons/based/based_messages.c b/daemons/based/based_messages.c +index 35d639a89..a87d9ac2e 100644 +--- a/daemons/based/based_messages.c ++++ b/daemons/based/based_messages.c +@@ -478,3 +478,46 @@ cib_process_commit_transaction(const char *op, int options, const char *section, + } + return pcmk_rc2legacy(rc); + } ++ ++int ++cib_process_schemas(const char *op, int options, const char *section, xmlNode *req, ++ xmlNode *input, xmlNode *existing_cib, xmlNode **result_cib, ++ xmlNode **answer) ++{ ++ xmlNode *data = NULL; ++ const char *after_ver = NULL; ++ GList *schemas = NULL; ++ GList *already_included = NULL; ++ ++ *answer = create_xml_node(NULL, PCMK__XA_SCHEMAS); ++ ++ data = get_message_xml(req, F_CIB_CALLDATA); ++ if (data == NULL) { ++ crm_warn("No data specified in request"); ++ return -EPROTO; ++ } ++ ++ after_ver = crm_element_value(data, XML_ATTR_VERSION); ++ if (after_ver == NULL) { ++ crm_warn("No version specified in request"); ++ return -EPROTO; ++ } ++ ++ /* The client requested all schemas after the latest one we know about, which ++ * means the client is fully up-to-date. Return a properly formatted reply ++ * with no schemas. ++ */ ++ if (pcmk__str_eq(after_ver, xml_latest_schema(), pcmk__str_none)) { ++ return pcmk_ok; ++ } ++ ++ schemas = pcmk__schema_files_later_than(after_ver); ++ ++ for (GList *iter = schemas; iter != NULL; iter = iter->next) { ++ pcmk__build_schema_xml_node(*answer, iter->data, &already_included); ++ } ++ ++ g_list_free_full(schemas, free); ++ g_list_free_full(already_included, free); ++ return pcmk_ok; ++} +diff --git a/daemons/based/based_operation.c b/daemons/based/based_operation.c +index 736d425e3..8dd07af93 100644 +--- a/daemons/based/based_operation.c ++++ b/daemons/based/based_operation.c +@@ -35,6 +35,7 @@ static const cib__op_fn_t cib_op_functions[] = { + [cib__op_sync_all] = cib_process_sync, + [cib__op_sync_one] = cib_process_sync_one, + [cib__op_upgrade] = cib_process_upgrade_server, ++ [cib__op_schemas] = cib_process_schemas, + }; + + /*! +diff --git a/daemons/based/pacemaker-based.h b/daemons/based/pacemaker-based.h +index 33c7642c5..de24779ac 100644 +--- a/daemons/based/pacemaker-based.h ++++ b/daemons/based/pacemaker-based.h +@@ -122,6 +122,10 @@ int cib_process_commit_transaction(const char *op, int options, + const char *section, xmlNode *req, + xmlNode *input, xmlNode *existing_cib, + xmlNode **result_cib, xmlNode **answer); ++int cib_process_schemas(const char *op, int options, const char *section, ++ xmlNode *req, xmlNode *input, xmlNode *existing_cib, ++ xmlNode **result_cib, xmlNode **answer); ++ + void send_sync_request(const char *host); + int sync_our_cib(xmlNode *request, gboolean all); + +diff --git a/include/crm/cib/cib_types.h b/include/crm/cib/cib_types.h +index a803311c2..bebe770ed 100644 +--- a/include/crm/cib/cib_types.h ++++ b/include/crm/cib/cib_types.h +@@ -324,6 +324,9 @@ typedef struct cib_api_operations_s { + * processing requests + */ + void (*set_user)(cib_t *cib, const char *user); ++ ++ int (*fetch_schemas)(cib_t *cib, xmlNode **output_data, const char *after_ver, ++ int call_options); + } cib_api_operations_t; + + struct cib_s { +diff --git a/include/crm/cib/internal.h b/include/crm/cib/internal.h +index 20059ec7e..9d54d52b7 100644 +--- a/include/crm/cib/internal.h ++++ b/include/crm/cib/internal.h +@@ -32,6 +32,7 @@ + #define PCMK__CIB_REQUEST_NOOP "noop" + #define PCMK__CIB_REQUEST_SHUTDOWN "cib_shutdown_req" + #define PCMK__CIB_REQUEST_COMMIT_TRANSACT "cib_commit_transact" ++#define PCMK__CIB_REQUEST_SCHEMAS "cib_schemas" + + # define F_CIB_CLIENTID "cib_clientid" + # define F_CIB_CALLOPTS "cib_callopt" +@@ -110,6 +111,7 @@ enum cib__op_type { + cib__op_sync_all, + cib__op_sync_one, + cib__op_upgrade, ++ cib__op_schemas, + }; + + gboolean cib_diff_version_details(xmlNode * diff, int *admin_epoch, int *epoch, int *updates, +diff --git a/lib/cib/cib_client.c b/lib/cib/cib_client.c +index 32e1f83c5..a2fcabbca 100644 +--- a/lib/cib/cib_client.c ++++ b/lib/cib/cib_client.c +@@ -451,6 +451,19 @@ cib_client_end_transaction(cib_t *cib, bool commit, int call_options) + return rc; + } + ++static int ++cib_client_fetch_schemas(cib_t *cib, xmlNode **output_data, const char *after_ver, ++ int call_options) ++{ ++ xmlNode *data = create_xml_node(NULL, PCMK__XA_SCHEMA); ++ ++ crm_xml_add(data, XML_ATTR_VERSION, after_ver); ++ ++ return cib_internal_op(cib, PCMK__CIB_REQUEST_SCHEMAS, NULL, NULL, data, ++ output_data, call_options, NULL); ++ ++} ++ + static void + cib_client_set_user(cib_t *cib, const char *user) + { +@@ -736,6 +749,8 @@ cib_new_variant(void) + + new_cib->cmds->set_user = cib_client_set_user; + ++ new_cib->cmds->fetch_schemas = cib_client_fetch_schemas; ++ + return new_cib; + } + +diff --git a/lib/cib/cib_ops.c b/lib/cib/cib_ops.c +index c324304b9..2165d8af3 100644 +--- a/lib/cib/cib_ops.c ++++ b/lib/cib/cib_ops.c +@@ -127,6 +127,9 @@ static const cib__operation_t cib_ops[] = { + |cib__op_attr_writes_through + |cib__op_attr_transaction + }, ++ { ++ PCMK__CIB_REQUEST_SCHEMAS, cib__op_schemas, cib__op_attr_local ++ } + }; + + /*! +-- +2.41.0 + +From e8076b4a387ee758508f0683739b3e880f79db47 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Thu, 14 Sep 2023 15:48:31 -0400 +Subject: [PATCH 10/15] Refactor: libcrmcommon: Add + pcmk__load_schemas_from_dir. + +This just moves the bulk of crm_schema_init out into its own function, +allowing us to call it multiple times as other schema directories +appear. + +There is no similar function for unloading schemas from a directory. If +you want to do that, you'll have to use crm_schema_cleanup to unload +everything and start over. + +This function has not been tested for the possibility that the same +schema files exist in multiple directories. It is assumed that it won't +be used like that. +--- + include/crm/common/internal.h | 1 + + lib/common/schemas.c | 83 +++++++++++++++++++---------------- + 2 files changed, 45 insertions(+), 39 deletions(-) + +diff --git a/include/crm/common/internal.h b/include/crm/common/internal.h +index 318003efe..542d0a67c 100644 +--- a/include/crm/common/internal.h ++++ b/include/crm/common/internal.h +@@ -134,6 +134,7 @@ bool pcmk__procfs_has_pids(void); + void crm_schema_init(void); + void crm_schema_cleanup(void); + ++void pcmk__load_schemas_from_dir(const char *dir); + GList *pcmk__schema_files_later_than(const char *name); + void pcmk__build_schema_xml_node(xmlNode *parent, const char *name, + GList **already_included); +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index 1bcdff031..a0c844131 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -406,6 +406,49 @@ 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); ++} ++ + /*! + * \internal + * \brief Load pacemaker schemas into cache +@@ -416,50 +459,12 @@ 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 pcmk__schema_version_t zero = SCHEMA_ZERO; + + wrap_libxslt(false); + +- max = scandir(base, &namelist, schema_filter, schema_cmp_directory); +- if (max < 0) { +- crm_notice("scandir(%s) failed: %s (%d)", base, strerror(errno), errno); +- free(base); +- +- } else { +- free(base); +- for (lpc = 0; lpc < max; lpc++) { +- bool transform_expected = FALSE; +- 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_err("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) +- == ENOENT) { +- break; +- } +- } +- +- for (lpc = 0; lpc < max; lpc++) { +- free(namelist[lpc]); +- } +- free(namelist); +- } ++ pcmk__load_schemas_from_dir(base); + + // @COMPAT: Deprecated since 2.1.5 + add_schema(pcmk__schema_validator_rng, &zero, "pacemaker-next", +-- +2.41.0 + +From 5b40f0227b33edd6be65515dcc1af4eb656b78e7 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Thu, 14 Sep 2023 11:24:36 -0400 +Subject: [PATCH 11/15] Feature: daemons: Download newer schema files to a + remote executor. + +If the remote executor supports an older version of the CIB schema than +the rest of the cluster, various operations could fail due to the schema +not validating on the remote node. + +Instead, ask the CIB manager for the updated schema files, store them in +a separate directory on the remote node, and add them to the list of +known schema files. These can then be used just like the packaged +schema files. + +Note that while this is a fairly large patch, it's really just a lot of +boring IO and file management code, which requires a lot of error +handling. + +Fixes T182 +--- + daemons/execd/Makefile.am | 4 +- + daemons/execd/execd_commands.c | 6 + + daemons/execd/pacemaker-execd.h | 1 + + daemons/execd/remoted_schemas.c | 282 ++++++++++++++++++++++++++++++++ + 4 files changed, 292 insertions(+), 1 deletion(-) + create mode 100644 daemons/execd/remoted_schemas.c + +diff --git a/daemons/execd/Makefile.am b/daemons/execd/Makefile.am +index ab8544f9d..ce0e16126 100644 +--- a/daemons/execd/Makefile.am ++++ b/daemons/execd/Makefile.am +@@ -44,12 +44,14 @@ pacemaker_remoted_LDFLAGS = $(LDFLAGS_HARDENED_EXE) + + pacemaker_remoted_LDADD = $(top_builddir)/lib/fencing/libstonithd.la + pacemaker_remoted_LDADD += $(top_builddir)/lib/services/libcrmservice.la ++pacemaker_remoted_LDADD += $(top_builddir)/lib/cib/libcib.la + pacemaker_remoted_LDADD += $(top_builddir)/lib/lrmd/liblrmd.la + pacemaker_remoted_LDADD += $(top_builddir)/lib/common/libcrmcommon.la + pacemaker_remoted_SOURCES = $(pacemaker_execd_SOURCES) \ + remoted_tls.c \ + remoted_pidone.c \ +- remoted_proxy.c ++ remoted_proxy.c \ ++ remoted_schemas.c + endif + + cts_exec_helper_LDADD = $(top_builddir)/lib/pengine/libpe_status.la +diff --git a/daemons/execd/execd_commands.c b/daemons/execd/execd_commands.c +index cf4503a25..1601efb0b 100644 +--- a/daemons/execd/execd_commands.c ++++ b/daemons/execd/execd_commands.c +@@ -1493,9 +1493,15 @@ process_lrmd_signon(pcmk__client_t *client, xmlNode *request, int call_id, + if ((client->remote != NULL) + && pcmk_is_set(client->flags, + pcmk__client_tls_handshake_complete)) { ++ const char *op = crm_element_value(request, F_LRMD_OPERATION); + + // This is a remote connection from a cluster node's controller + ipc_proxy_add_provider(client); ++ ++ /* If this was a register operation, also ask for new schema files. */ ++ if (pcmk__str_eq(op, CRM_OP_REGISTER, pcmk__str_none)) { ++ remoted_request_cib_schema_files(); ++ } + } else { + rc = -EACCES; + } +diff --git a/daemons/execd/pacemaker-execd.h b/daemons/execd/pacemaker-execd.h +index 9c1d173f5..6fb8ef440 100644 +--- a/daemons/execd/pacemaker-execd.h ++++ b/daemons/execd/pacemaker-execd.h +@@ -101,6 +101,7 @@ void ipc_proxy_forward_client(pcmk__client_t *client, xmlNode *xml); + pcmk__client_t *ipc_proxy_get_provider(void); + int ipc_proxy_shutdown_req(pcmk__client_t *ipc_proxy); + void remoted_spawn_pidone(int argc, char **argv, char **envp); ++void remoted_request_cib_schema_files(void); + #endif + + int process_lrmd_alert_exec(pcmk__client_t *client, uint32_t id, +diff --git a/daemons/execd/remoted_schemas.c b/daemons/execd/remoted_schemas.c +new file mode 100644 +index 000000000..d501fa495 +--- /dev/null ++++ b/daemons/execd/remoted_schemas.c +@@ -0,0 +1,282 @@ ++/* ++ * 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 Lesser General Public License ++ * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. ++ */ ++ ++#include ++ ++#include ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "pacemaker-execd.h" ++ ++static pid_t schema_fetch_pid = 0; ++ ++static int ++rm_files(const char *pathname, const struct stat *sbuf, int type, struct FTW *ftwb) ++{ ++ /* Don't delete PCMK__REMOTE_SCHEMA_DIR . */ ++ if (ftwb->level == 0) { ++ return 0; ++ } ++ ++ if (remove(pathname) != 0) { ++ int rc = errno; ++ crm_err("Could not remove %s: %s", pathname, pcmk_rc_str(rc)); ++ return -1; ++ } ++ ++ return 0; ++} ++ ++static void ++clean_up_extra_schema_files(void) ++{ ++ const char *remote_schema_dir = pcmk__remote_schema_dir(); ++ struct stat sb; ++ int rc; ++ ++ rc = stat(remote_schema_dir, &sb); ++ ++ if (rc == -1) { ++ if (errno == ENOENT) { ++ /* If the directory doesn't exist, try to make it first. */ ++ if (mkdir(remote_schema_dir, 0755) != 0) { ++ rc = errno; ++ crm_err("Could not create directory for schemas: %s", ++ pcmk_rc_str(rc)); ++ } ++ ++ } else { ++ rc = errno; ++ crm_err("Could not create directory for schemas: %s", ++ pcmk_rc_str(rc)); ++ } ++ ++ } else if (!S_ISDIR(sb.st_mode)) { ++ /* If something exists with the same name that's not a directory, that's ++ * an error. ++ */ ++ crm_err("%s already exists but is not a directory", remote_schema_dir); ++ ++ } else { ++ /* It's a directory - clear it out so we can download potentially new ++ * schema files. ++ */ ++ rc = nftw(remote_schema_dir, rm_files, 10, FTW_DEPTH|FTW_MOUNT|FTW_PHYS); ++ ++ if (rc != 0) { ++ crm_err("Could not remove %s: %s", remote_schema_dir, pcmk_rc_str(rc)); ++ } ++ } ++} ++ ++static void ++write_extra_schema_file(xmlNode *xml, void *user_data) ++{ ++ const char *remote_schema_dir = pcmk__remote_schema_dir(); ++ const char *file = NULL; ++ char *path = NULL; ++ int rc; ++ ++ file = crm_element_value(xml, PCMK__XA_PATH); ++ if (file == NULL) { ++ crm_warn("No destination path given in schema request"); ++ return; ++ } ++ ++ path = crm_strdup_printf("%s/%s", remote_schema_dir, file); ++ ++ /* The schema is a CDATA node, which is a child of the node. Traverse ++ * all children and look for the first CDATA child. There can't be more than ++ * one because we only have one file attribute on the parent. ++ */ ++ for (xmlNode *child = xml->children; child != NULL; child = child->next) { ++ FILE *stream = NULL; ++ ++ if (child->type != XML_CDATA_SECTION_NODE) { ++ continue; ++ } ++ ++ stream = fopen(path, "w+"); ++ if (stream == NULL) { ++ crm_warn("Could not write schema file %s: %s", path, strerror(errno)); ++ } else { ++ rc = fprintf(stream, "%s", child->content); ++ ++ if (rc < 0) { ++ crm_warn("Could not write schema file %s: %s", path, strerror(errno)); ++ } ++ ++ fclose(stream); ++ } ++ ++ break; ++ } ++ ++ free(path); ++} ++ ++static void ++get_schema_files(void) ++{ ++ int rc = pcmk_rc_ok; ++ cib_t *cib = NULL; ++ xmlNode *reply; ++ ++ cib = cib_new(); ++ if (cib == NULL) { ++ _exit(ENOTCONN); ++ } ++ ++ rc = cib->cmds->signon(cib, crm_system_name, cib_query); ++ if (rc != pcmk_ok) { ++ crm_err("Could not connect to the CIB manager: %s", pcmk_strerror(rc)); ++ _exit(pcmk_rc2exitc(rc)); ++ } ++ ++ rc = cib->cmds->fetch_schemas(cib, &reply, xml_latest_schema(), cib_sync_call); ++ if (rc != pcmk_ok) { ++ crm_err("Could not get schema files: %s", pcmk_strerror(rc)); ++ rc = pcmk_legacy2rc(rc); ++ ++ } else if (reply->children != NULL) { ++ /* The returned document looks something like this: ++ * ++ * ++ * ++ * ++ * ++ * ++ * ++ * ++ * ... ++ * ++ * ++ * ++ * ... ++ * ++ * ++ * ++ * ++ * All the and tags are really just there for organizing ++ * the XML a little better. What we really care about are the nodes, ++ * and specifically the path attributes and the CDATA children (not shown) ++ * of each. We can use an xpath query to reach down and get all the ++ * nodes at once. ++ * ++ * If we already have the latest schema version, or we asked for one later ++ * than what the cluster supports, we'll get back an empty node, ++ * so all this will continue to work. It just won't do anything. ++ */ ++ crm_foreach_xpath_result(reply, "//" PCMK__XA_FILE, write_extra_schema_file, NULL); ++ } ++ ++ cib__clean_up_connection(&cib); ++ _exit(pcmk_rc2exitc(rc)); ++} ++ ++/* Load any additional schema files when the child is finished fetching and ++ * saving them to disk. ++ */ ++static void ++get_schema_files_complete(mainloop_child_t *p, pid_t pid, int core, int signo, int exitcode) ++{ ++ const char *errmsg = "Could not load additional schema files"; ++ ++ if ((signo == 0) && (exitcode == 0)) { ++ const char *remote_schema_dir = pcmk__remote_schema_dir(); ++ ++ /* Don't just crm_schema_init here because that will load the base ++ * schemas again too. Instead just load the things we fetched. ++ */ ++ pcmk__load_schemas_from_dir(remote_schema_dir); ++ crm_info("Fetching extra schema files completed successfully"); ++ ++ } else { ++ if (signo == 0) { ++ crm_err("%s: process %d exited %d", errmsg, (int) pid, exitcode); ++ ++ } else { ++ crm_err("%s: process %d terminated with signal %d (%s)%s", ++ errmsg, (int) pid, signo, strsignal(signo), ++ (core? " and dumped core" : "")); ++ } ++ ++ /* Clean up any incomplete schema data we might have been downloading when ++ * the process timed out or crashed. We don't need to do any extra cleanup ++ * because we never loaded the extra schemas, and we don't need to call ++ * crm_schema_init because that was called in remoted_request_cib_schema_files ++ * before this function. ++ */ ++ clean_up_extra_schema_files(); ++ } ++} ++ ++void ++remoted_request_cib_schema_files(void) ++{ ++ pid_t pid; ++ int rc; ++ ++ /* If a previous schema-fetch process is still running when we're called ++ * again, it's hung. Attempt to kill it before cleaning up the extra ++ * directory. ++ */ ++ if (schema_fetch_pid != 0) { ++ if (mainloop_child_kill(schema_fetch_pid) == FALSE) { ++ crm_warn("Unable to kill pre-existing schema-fetch process"); ++ return; ++ } ++ ++ schema_fetch_pid = 0; ++ } ++ ++ /* Clean up any extra schema files we downloaded from a previous cluster ++ * connection. After the files are gone, we need to wipe them from ++ * known_schemas, but there's no opposite operation for add_schema(). ++ * ++ * Instead, unload all the schemas. This means we'll also forget about all ++ * the installed schemas as well, which means that xml_latest_schema() will ++ * fail. So we need to load the base schemas right now. ++ */ ++ clean_up_extra_schema_files(); ++ crm_schema_cleanup(); ++ crm_schema_init(); ++ ++ crm_info("Fetching extra schema files from cluster"); ++ pid = fork(); ++ ++ switch (pid) { ++ case -1: { ++ rc = errno; ++ crm_warn("Could not spawn process to get schema files: %s", pcmk_rc_str(rc)); ++ break; ++ } ++ ++ case 0: ++ /* child */ ++ get_schema_files(); ++ break; ++ ++ default: ++ /* parent */ ++ schema_fetch_pid = pid; ++ mainloop_child_add_with_flags(pid, 5 * 60 * 1000, "schema-fetch", NULL, ++ mainloop_leave_pid_group, ++ get_schema_files_complete); ++ break; ++ } ++} +-- +2.41.0 + +From b05fef32cf1f9063e01db5108f95386be329d778 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Wed, 29 Nov 2023 10:04:57 -0500 +Subject: [PATCH 12/15] Feature: libcrmcommon: Load additional schema files in + crm_schema_init. + +If the /var/lib/pacemaker/schemas directory exists, load any extra +schemas from it when we init. This makes them available for command +line programs to use. +--- + lib/common/schemas.c | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index a0c844131..68d79cfc7 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -459,12 +459,14 @@ pcmk__load_schemas_from_dir(const char *dir) + void + crm_schema_init(void) + { ++ 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; + + wrap_libxslt(false); + + pcmk__load_schemas_from_dir(base); ++ pcmk__load_schemas_from_dir(remote_schema_dir); + + // @COMPAT: Deprecated since 2.1.5 + add_schema(pcmk__schema_validator_rng, &zero, "pacemaker-next", +-- +2.41.0 + +From 7454a8400238301848cc6694f5413fdb19d5834e Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Fri, 29 Sep 2023 10:41:51 -0400 +Subject: [PATCH 13/15] Refactor: daemons: Remove redundant includes from + remoted_tls.c. + +--- + daemons/execd/remoted_tls.c | 2 -- + 1 file changed, 2 deletions(-) + +diff --git a/daemons/execd/remoted_tls.c b/daemons/execd/remoted_tls.c +index 23a2dcf45..978edfdd3 100644 +--- a/daemons/execd/remoted_tls.c ++++ b/daemons/execd/remoted_tls.c +@@ -12,8 +12,6 @@ + #include + #include + +-#include +-#include + #include + #include + #include +-- +2.41.0 + +From c6b5f73a013515d6acd818d348d011e38f3d8e0e Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Fri, 27 Oct 2023 15:23:41 -0400 +Subject: [PATCH 14/15] Refactor: daemons: Keep XML schemas sorted. + +Now that we can add schemas multiple times (including after next/none +have been added in the initial set), we've broken the assumption that +schemas are always sorted by scandir. + +This introduces a function that can be called wherever to keep them +sorted. It should be called pretty much whenever +pcmk__load_schemas_from_dir is called. +--- + daemons/execd/remoted_schemas.c | 1 + + include/crm/common/xml_internal.h | 1 + + lib/common/schemas.c | 40 +++++++++++++++++++++++++++++++ + 3 files changed, 42 insertions(+) + +diff --git a/daemons/execd/remoted_schemas.c b/daemons/execd/remoted_schemas.c +index d501fa495..eed43dfa9 100644 +--- a/daemons/execd/remoted_schemas.c ++++ b/daemons/execd/remoted_schemas.c +@@ -203,6 +203,7 @@ get_schema_files_complete(mainloop_child_t *p, pid_t pid, int core, int signo, i + * schemas again too. Instead just load the things we fetched. + */ + pcmk__load_schemas_from_dir(remote_schema_dir); ++ pcmk__sort_schemas(); + crm_info("Fetching extra schema files completed successfully"); + + } else { +diff --git a/include/crm/common/xml_internal.h b/include/crm/common/xml_internal.h +index cb27ec6b2..9b50f4e72 100644 +--- a/include/crm/common/xml_internal.h ++++ b/include/crm/common/xml_internal.h +@@ -448,5 +448,6 @@ gboolean pcmk__validate_xml(xmlNode *xml_blob, const char *validation, + + void pcmk__log_known_schemas(void); + const char *pcmk__remote_schema_dir(void); ++void pcmk__sort_schemas(void); + + #endif // PCMK__XML_INTERNAL__H +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index 68d79cfc7..fd6202f62 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -449,6 +449,41 @@ pcmk__load_schemas_from_dir(const char *dir) + 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; ++ ++ if (pcmk__str_eq(schema_a->name, "pacemaker-next", pcmk__str_none)) { ++ if (pcmk__str_eq(schema_b->name, "none", pcmk__str_none)) { ++ return -1; ++ } else { ++ return 1; ++ } ++ } else if (pcmk__str_eq(schema_a->name, "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 +@@ -474,6 +509,11 @@ crm_schema_init(void) + + add_schema(pcmk__schema_validator_none, &zero, PCMK__VALUE_NONE, + NULL, NULL, FALSE); ++ ++ /* This shouldn't be strictly necessary, but we'll do it here just in case ++ * there's anything in PCMK__REMOTE_SCHEMA_DIR that messes up the order. ++ */ ++ pcmk__sort_schemas(); + } + + static gboolean +-- +2.41.0 + +From d6a535ca43db202bd01a2e085b58722ed1abcdb0 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Mon, 30 Oct 2023 12:59:13 -0400 +Subject: [PATCH 15/15] Feature: daemons: Only ask for schemas if supported by + the server + +We can use LRMD_PROTOCOL_VERSION in the handshake to determine what the +server supports, similar to what is being done in attrd. Add a macro to +compare the version we were given with the known minimum version that +supports the schema transfer command. + +Additionally, introduce LRMD_COMPATIBLE_PROTOCOL which is just the major +version number required for the connection to succeed. This gets rid of +the need for LRMD_MIN_PROTOCOL_VERSION, which can now be deprecated. + +And then since I wasn't sure compare_version would work if you give it a +full version number and just a major version, add a unit test for that. +--- + daemons/execd/execd_commands.c | 11 +++++---- + include/crm/lrmd.h | 23 +++++++++++++++---- + lib/common/tests/utils/compare_version_test.c | 5 +++- + 3 files changed, 30 insertions(+), 9 deletions(-) + +diff --git a/daemons/execd/execd_commands.c b/daemons/execd/execd_commands.c +index 1601efb0b..4ec4d03d6 100644 +--- a/daemons/execd/execd_commands.c ++++ b/daemons/execd/execd_commands.c +@@ -1482,9 +1482,9 @@ process_lrmd_signon(pcmk__client_t *client, xmlNode *request, int call_id, + const char *protocol_version = crm_element_value(request, F_LRMD_PROTOCOL_VERSION); + const char *start_state = pcmk__env_option(PCMK__ENV_NODE_START_STATE); + +- if (compare_version(protocol_version, LRMD_MIN_PROTOCOL_VERSION) < 0) { ++ if (compare_version(protocol_version, LRMD_COMPATIBLE_PROTOCOL) < 0) { + crm_err("Cluster API version must be greater than or equal to %s, not %s", +- LRMD_MIN_PROTOCOL_VERSION, protocol_version); ++ LRMD_COMPATIBLE_PROTOCOL, protocol_version); + rc = -EPROTO; + } + +@@ -1498,8 +1498,11 @@ process_lrmd_signon(pcmk__client_t *client, xmlNode *request, int call_id, + // This is a remote connection from a cluster node's controller + ipc_proxy_add_provider(client); + +- /* If this was a register operation, also ask for new schema files. */ +- if (pcmk__str_eq(op, CRM_OP_REGISTER, pcmk__str_none)) { ++ /* If this was a register operation, also ask for new schema files but ++ * only if it's supported by the protocol version. ++ */ ++ if (pcmk__str_eq(op, CRM_OP_REGISTER, pcmk__str_none) && ++ LRMD_SUPPORTS_SCHEMA_XFER(protocol_version)) { + remoted_request_cib_schema_files(); + } + } else { +diff --git a/include/crm/lrmd.h b/include/crm/lrmd.h +index 0c5a40b62..948a3b1fc 100644 +--- a/include/crm/lrmd.h ++++ b/include/crm/lrmd.h +@@ -33,12 +33,27 @@ typedef struct lrmd_key_value_s { + struct lrmd_key_value_s *next; + } lrmd_key_value_t; + +-/* This should be bumped every time there is an incompatible change that +- * prevents older clients from connecting to this version of the server. ++/* The major version should be bumped every time there is an incompatible ++ * change that prevents older clients from connecting to this version of ++ * the server. The minor version indicates feature support. ++ * ++ * Protocol Pacemaker Significant changes ++ * -------- --------- ------------------- ++ * 1.2 2.1.7 PCMK__CIB_REQUEST_SCHEMAS + */ +-#define LRMD_PROTOCOL_VERSION "1.1" ++#define LRMD_PROTOCOL_VERSION "1.2" ++ ++#define LRMD_SUPPORTS_SCHEMA_XFER(x) (compare_version((x), "1.2") >= 0) + +-/* This is the version that the client version will actually be compared ++/* The major protocol version the client and server both need to support for ++ * the connection to be successful. This should only ever be the major ++ * version - not a complete version number. ++ */ ++#define LRMD_COMPATIBLE_PROTOCOL "1" ++ ++/* \deprecated Do not use (will be removed in a future release) ++ * ++ * This is the version that the client version will actually be compared + * against. This should be identical to LRMD_PROTOCOL_VERSION. However, we + * accidentally bumped LRMD_PROTOCOL_VERSION in 6424a647 (1.1.15) when we didn't + * need to, so for now it's different. If we ever have a truly incompatible +diff --git a/lib/common/tests/utils/compare_version_test.c b/lib/common/tests/utils/compare_version_test.c +index 35ebb63c6..d191f4abb 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, +-- +2.41.0 + diff --git a/pacemaker.spec b/pacemaker.spec index 3063121..8e5dc50 100644 --- a/pacemaker.spec +++ b/pacemaker.spec @@ -17,7 +17,7 @@ ## can be incremented to build packages reliably considered "newer" ## than previously built packages with the same pcmkversion) %global pcmkversion 2.1.7 -%global specversion 5 +%global specversion 6 ## Upstream commit (full commit ID, abbreviated commit ID, or tag) to build %global commit 0f7f88312f7a1ccedee60bf768aba79ee13d41e0 @@ -152,6 +152,8 @@ Source2: pacemaker.sysusers Patch0: Add_replace_for_PCMK__REMOTE_SCHEMA_DIR.patch Patch1: 001-schema-glib.patch Patch2: Doc-HealthSMART-fix-the-description-of-temp_lower.patch +Patch3: 002-schema-transfer.patch + Requires: resource-agents Requires: %{pkgname_pcmk_libs} = %{version}-%{release} Requires: %{name}-cluster-libs = %{version}-%{release} @@ -758,6 +760,9 @@ exit 0 %license %{nagios_name}-%{nagios_hash}/COPYING %changelog +* Mon Mar 25 2024 zouzhimin - 2.1.7-6 +- Pacemaker Remote nodes can validate against later schema versions + * Thu Mar 21 2024 bixiaoyan - 2.1.7-5 - Doc: HealthSMART:fix the description of temp_lower_limit -- Gitee