From 9e0c58dc3b949e4eadfa43364e60677478b7aa0f Mon Sep 17 00:00:00 2001
From: Chris Lumens <clumens@redhat.com>
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 <clumens@redhat.com>
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 <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))
-- 
2.41.0

From 76859d61f4d35e1f8b7c35d8766d3d0c123d4552 Mon Sep 17 00:00:00 2001
From: Chris Lumens <clumens@redhat.com>
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 <clumens@redhat.com>
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 <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 <clumens@redhat.com>
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 <clumens@redhat.com>
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 <crm_internal.h>
 
 #include <stdio.h>
+#include <sys/stat.h>
 #include <sys/types.h>
 #include <unistd.h>
 #include <time.h>
@@ -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 <clumens@redhat.com>
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 <clumens@redhat.com>
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 <file path="..."> 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 <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)
+{
+    /* 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 <crm_internal.h>
+
+#include <crm/msg_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();
+    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:
+     *
+     * <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__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 <clumens@redhat.com>
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 <clumens@redhat.com>
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 <clumens@redhat.com>
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 <crm_internal.h>
+
+#include <ftw.h>
+#include <unistd.h>
+#include <sys/stat.h>
+
+#include <crm/cib.h>
+#include <crm/cib/cib_types.h>
+#include <crm/cib/internal.h>
+#include <crm/crm.h>
+#include <crm/msg_xml.h>
+#include <crm/common/mainloop.h>
+
+#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 <file> 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:
+         * <cib_command>
+         *   <cib_calldata>
+         *     <schemas>
+         *       <schema version="pacemaker-1.1">
+         *         <file path="foo-1.1">
+         *         </file>
+         *         <file path="bar-1.1">
+         *         </file>
+         *         ...
+         *       </schema>
+         *       <schema version="pacemaker-1.2">
+         *       </schema>
+         *       ...
+         *     </schemas>
+         *   </cib_calldata>
+         * </cib_command>
+         *
+         * All the <schemas> and <schema> tags are really just there for organizing
+         * the XML a little better.  What we really care about are the <file> 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 <file>
+         * 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 <schemas> 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 <clumens@redhat.com>
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 <clumens@redhat.com>
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 <glib.h>
 #include <unistd.h>
 
-#include <crm/crm.h>
-#include <crm/msg_xml.h>
 #include <crm/crm.h>
 #include <crm/msg_xml.h>
 #include <crm/common/mainloop.h>
-- 
2.41.0

From c6b5f73a013515d6acd818d348d011e38f3d8e0e Mon Sep 17 00:00:00 2001
From: Chris Lumens <clumens@redhat.com>
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 <clumens@redhat.com>
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

