diff --git a/daemons/based/based_io.c b/daemons/based/based_io.c
index 0f8f03fd33..f22582de40 100644
--- a/daemons/based/based_io.c
+++ b/daemons/based/based_io.c
@@ -1,477 +1,477 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <stdio.h>
 #include <unistd.h>
 #include <string.h>
 #include <stdlib.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <dirent.h>
 
 #include <sys/param.h>
 #include <sys/types.h>
 #include <sys/wait.h>
 #include <sys/stat.h>
 
 #include <glib.h>
 #include <libxml/tree.h>
 
 #include <crm/crm.h>
 
 #include <crm/cib.h>
 #include <crm/common/util.h>
 #include <crm/common/xml.h>
 #include <crm/cib/internal.h>
 #include <crm/cluster.h>
 
 #include <pacemaker-based.h>
 
 crm_trigger_t *cib_writer = NULL;
 
 int write_cib_contents(gpointer p);
 
 static void
 cib_rename(const char *old)
 {
     int new_fd;
     char *new = crm_strdup_printf("%s/cib.auto.XXXXXX", cib_root);
 
     umask(S_IWGRP | S_IWOTH | S_IROTH);
     new_fd = mkstemp(new);
 
     if ((new_fd < 0) || (rename(old, new) < 0)) {
         crm_err("Couldn't archive unusable file %s (disabling disk writes and continuing)",
                 old);
         cib_writes_enabled = FALSE;
     } else {
         crm_err("Archived unusable file %s as %s", old, new);
     }
 
     if (new_fd > 0) {
         close(new_fd);
     }
     free(new);
 }
 
 /*
  * It is the callers responsibility to free the output of this function
  */
 
 static xmlNode *
 retrieveCib(const char *filename, const char *sigfile)
 {
     xmlNode *root = NULL;
 
     crm_info("Reading cluster configuration file %s (digest: %s)",
              filename, sigfile);
     switch (cib_file_read_and_verify(filename, sigfile, &root)) {
         case -pcmk_err_cib_corrupt:
             crm_warn("Continuing but %s will NOT be used.", filename);
             break;
 
         case -pcmk_err_cib_modified:
             /* Archive the original files so the contents are not lost */
             crm_warn("Continuing but %s will NOT be used.", filename);
             cib_rename(filename);
             cib_rename(sigfile);
             break;
     }
     return root;
 }
 
 /*
  * for OSs without support for direntry->d_type, like Solaris
  */
 #ifndef DT_UNKNOWN
 # define DT_UNKNOWN     0
 # define DT_FIFO        1
 # define DT_CHR         2
 # define DT_DIR         4
 # define DT_BLK         6
 # define DT_REG         8
 # define DT_LNK         10
 # define DT_SOCK        12
 # define DT_WHT         14
 #endif /*DT_UNKNOWN*/
 
 static int cib_archive_filter(const struct dirent * a)
 {
     int rc = 0;
     /* Looking for regular files (d_type = 8) starting with 'cib-' and not ending in .sig */
     struct stat s;
     char *a_path = crm_strdup_printf("%s/%s", cib_root, a->d_name);
 
     if(stat(a_path, &s) != 0) {
         rc = errno;
         crm_trace("%s - stat failed: %s (%d)", a->d_name, pcmk_rc_str(rc), rc);
         rc = 0;
 
     } else if ((s.st_mode & S_IFREG) != S_IFREG) {
         unsigned char dtype;
 #ifdef HAVE_STRUCT_DIRENT_D_TYPE
         dtype = a->d_type;
 #else
         switch (s.st_mode & S_IFMT) {
             case S_IFREG:  dtype = DT_REG;      break;
             case S_IFDIR:  dtype = DT_DIR;      break;
             case S_IFCHR:  dtype = DT_CHR;      break;
             case S_IFBLK:  dtype = DT_BLK;      break;
             case S_IFLNK:  dtype = DT_LNK;      break;
             case S_IFIFO:  dtype = DT_FIFO;     break;
             case S_IFSOCK: dtype = DT_SOCK;     break;
             default:       dtype = DT_UNKNOWN;  break;
         }
 #endif
          crm_trace("%s - wrong type (%d)", a->d_name, dtype);
 
     } else if(strstr(a->d_name, "cib-") != a->d_name) {
         crm_trace("%s - wrong prefix", a->d_name);
 
     } else if (pcmk__ends_with_ext(a->d_name, ".sig")) {
         crm_trace("%s - wrong suffix", a->d_name);
 
     } else {
         crm_debug("%s - candidate", a->d_name);
         rc = 1;
     }
 
     free(a_path);
     return rc;
 }
 
 static int cib_archive_sort(const struct dirent ** a, const struct dirent **b)
 {
     /* Order by creation date - most recently created file first */
     int rc = 0;
     struct stat buf;
 
     time_t a_age = 0;
     time_t b_age = 0;
 
     char *a_path = crm_strdup_printf("%s/%s", cib_root, a[0]->d_name);
     char *b_path = crm_strdup_printf("%s/%s", cib_root, b[0]->d_name);
 
     if(stat(a_path, &buf) == 0) {
         a_age = buf.st_ctime;
     }
     if(stat(b_path, &buf) == 0) {
         b_age = buf.st_ctime;
     }
 
     free(a_path);
     free(b_path);
 
     if(a_age > b_age) {
         rc = 1;
     } else if(a_age < b_age) {
         rc = -1;
     }
 
     crm_trace("%s (%lu) vs. %s (%lu) : %d",
 	a[0]->d_name, (unsigned long)a_age,
 	b[0]->d_name, (unsigned long)b_age, rc);
     return rc;
 }
 
 xmlNode *
 readCibXmlFile(const char *dir, const char *file, gboolean discard_status)
 {
     struct dirent **namelist = NULL;
 
     int lpc = 0;
     char *sigfile = NULL;
     char *sigfilepath = NULL;
     char *filename = NULL;
     const char *name = NULL;
     const char *value = NULL;
     const char *validation = NULL;
     const char *use_valgrind = pcmk__env_option(PCMK__ENV_VALGRIND_ENABLED);
 
     xmlNode *root = NULL;
     xmlNode *status = NULL;
 
     sigfile = crm_strdup_printf("%s.sig", file);
     if (pcmk__daemon_can_write(dir, file) == FALSE
             || pcmk__daemon_can_write(dir, sigfile) == FALSE) {
         cib_status = -EACCES;
         return NULL;
     }
 
     filename = crm_strdup_printf("%s/%s", dir, file);
     sigfilepath = crm_strdup_printf("%s/%s", dir, sigfile);
     free(sigfile);
 
     cib_status = pcmk_ok;
     root = retrieveCib(filename, sigfilepath);
     free(filename);
     free(sigfilepath);
 
     if (root == NULL) {
         crm_warn("Primary configuration corrupt or unusable, trying backups in %s", cib_root);
         lpc = scandir(cib_root, &namelist, cib_archive_filter, cib_archive_sort);
         if (lpc < 0) {
             crm_err("scandir(%s) failed: %s", cib_root, pcmk_rc_str(errno));
         }
     }
 
     while (root == NULL && lpc > 1) {
         crm_debug("Testing %d candidates", lpc);
 
         lpc--;
 
         filename = crm_strdup_printf("%s/%s", cib_root, namelist[lpc]->d_name);
         sigfile = crm_strdup_printf("%s.sig", filename);
 
         crm_info("Reading cluster configuration file %s (digest: %s)",
                  filename, sigfile);
         if (cib_file_read_and_verify(filename, sigfile, &root) < 0) {
             crm_warn("Continuing but %s will NOT be used.", filename);
         } else {
             crm_notice("Continuing with last valid configuration archive: %s", filename);
         }
 
         free(namelist[lpc]);
         free(filename);
         free(sigfile);
     }
     free(namelist);
 
     if (root == NULL) {
         root = createEmptyCib(0);
         crm_warn("Continuing with an empty configuration.");
     }
 
     if (cib_writes_enabled && use_valgrind &&
         (crm_is_true(use_valgrind) || strstr(use_valgrind, "pacemaker-based"))) {
 
         cib_writes_enabled = FALSE;
         crm_err("*** Disabling disk writes to avoid confusing Valgrind ***");
     }
 
     status = pcmk__xe_first_child(root, PCMK_XE_STATUS, NULL, NULL);
     if (discard_status && status != NULL) {
         // Strip out the PCMK_XE_STATUS section if there is one
         free_xml(status);
         status = NULL;
     }
     if (status == NULL) {
         pcmk__xe_create(root, PCMK_XE_STATUS);
     }
 
     /* Do this before schema validation happens */
 
     /* fill in some defaults */
     name = PCMK_XA_ADMIN_EPOCH;
     value = crm_element_value(root, name);
     if (value == NULL) {
         crm_warn("No value for %s was specified in the configuration.", name);
         crm_warn("The recommended course of action is to shutdown,"
                  " run crm_verify and fix any errors it reports.");
         crm_warn("We will default to zero and continue but may get"
                  " confused about which configuration to use if"
                  " multiple nodes are powered up at the same time.");
         crm_xml_add_int(root, name, 0);
     }
 
     name = PCMK_XA_EPOCH;
     value = crm_element_value(root, name);
     if (value == NULL) {
         crm_xml_add_int(root, name, 0);
     }
 
     name = PCMK_XA_NUM_UPDATES;
     value = crm_element_value(root, name);
     if (value == NULL) {
         crm_xml_add_int(root, name, 0);
     }
 
     // Unset (DC should set appropriate value)
     pcmk__xe_remove_attr(root, PCMK_XA_DC_UUID);
 
     if (discard_status) {
         crm_log_xml_trace(root, "[on-disk]");
     }
 
     validation = crm_element_value(root, PCMK_XA_VALIDATE_WITH);
-    if (validate_xml(root, NULL, TRUE) == FALSE) {
+    if (!pcmk__configured_schema_validates(root)) {
         crm_err("CIB does not validate with %s",
                 pcmk__s(validation, "no schema specified"));
         cib_status = -pcmk_err_schema_validation;
 
     } else if (validation == NULL) {
         pcmk__update_schema(&root, NULL, false, false);
         validation = crm_element_value(root, PCMK_XA_VALIDATE_WITH);
         if (validation != NULL) {
             crm_notice("Enabling %s validation on"
                        " the existing (sane) configuration", validation);
         } else {
             crm_err("CIB does not validate with any known schema");
             cib_status = -pcmk_err_schema_validation;
         }
     }
 
     return root;
 }
 
 gboolean
 uninitializeCib(void)
 {
     xmlNode *tmp_cib = the_cib;
 
     if (tmp_cib == NULL) {
         crm_debug("The CIB has already been deallocated.");
         return FALSE;
     }
 
     the_cib = NULL;
 
     crm_debug("Deallocating the CIB.");
 
     free_xml(tmp_cib);
 
     crm_debug("The CIB has been deallocated.");
 
     return TRUE;
 }
 
 /*
  * This method will free the old CIB pointer on success and the new one
  * on failure.
  */
 int
 activateCibXml(xmlNode * new_cib, gboolean to_disk, const char *op)
 {
     if (new_cib) {
         xmlNode *saved_cib = the_cib;
 
         CRM_ASSERT(new_cib != saved_cib);
         the_cib = new_cib;
         free_xml(saved_cib);
         if (cib_writes_enabled && cib_status == pcmk_ok && to_disk) {
             crm_debug("Triggering CIB write for %s op", op);
             mainloop_set_trigger(cib_writer);
         }
         return pcmk_ok;
     }
 
     crm_err("Ignoring invalid CIB");
     if (the_cib) {
         crm_warn("Reverting to last known CIB");
     } else {
         crm_crit("Could not write out new CIB and no saved version to revert to");
     }
     return -ENODATA;
 }
 
 static void
 cib_diskwrite_complete(mainloop_child_t * p, pid_t pid, int core, int signo, int exitcode)
 {
     const char *errmsg = "Could not write CIB to disk";
 
     if ((exitcode != 0) && cib_writes_enabled) {
         cib_writes_enabled = FALSE;
         errmsg = "Disabling CIB disk writes after failure";
     }
 
     if ((signo == 0) && (exitcode == 0)) {
         crm_trace("Disk write [%d] succeeded", (int) pid);
 
     } 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" : ""));
     }
 
     mainloop_trigger_complete(cib_writer);
 }
 
 int
 write_cib_contents(gpointer p)
 {
     int exit_rc = pcmk_ok;
     xmlNode *cib_local = NULL;
 
     /* Make a copy of the CIB to write (possibly in a forked child) */
     if (p) {
         /* Synchronous write out */
         cib_local = pcmk__xml_copy(NULL, p);
 
     } else {
         int pid = 0;
         int bb_state = qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_STATE_GET, 0);
 
         /* Turn it off before the fork() to avoid:
          * - 2 processes writing to the same shared mem
          * - the child needing to disable it
          *   (which would close it from underneath the parent)
          * This way, the shared mem files are already closed
          */
         qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE);
 
         pid = fork();
         if (pid < 0) {
             crm_err("Disabling disk writes after fork failure: %s", pcmk_rc_str(errno));
             cib_writes_enabled = FALSE;
             return FALSE;
         }
 
         if (pid) {
             /* Parent */
             mainloop_child_add(pid, 0, "disk-writer", NULL, cib_diskwrite_complete);
             if (bb_state == QB_LOG_STATE_ENABLED) {
                 /* Re-enable now that it it safe */
                 qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_TRUE);
             }
 
             return -1;          /* -1 means 'still work to do' */
         }
 
         /* Asynchronous write-out after a fork() */
 
         /* In theory, we can scribble on the_cib here and not affect the parent,
          * but let's be safe anyway.
          */
         cib_local = pcmk__xml_copy(NULL, the_cib);
     }
 
     /* Write the CIB */
     exit_rc = cib_file_write_with_digest(cib_local, cib_root, "cib.xml");
 
     /* A nonzero exit code will cause further writes to be disabled */
     free_xml(cib_local);
     if (p == NULL) {
         crm_exit_t exit_code = CRM_EX_OK;
 
         switch (exit_rc) {
             case pcmk_ok:
                 exit_code = CRM_EX_OK;
                 break;
             case pcmk_err_cib_modified:
                 exit_code = CRM_EX_DIGEST; // Existing CIB doesn't match digest
                 break;
             case pcmk_err_cib_backup: // Existing CIB couldn't be backed up
             case pcmk_err_cib_save:   // New CIB couldn't be saved
                 exit_code = CRM_EX_CANTCREAT;
                 break;
             default:
                 exit_code = CRM_EX_ERROR;
                 break;
         }
 
         /* Use _exit() because exit() could affect the parent adversely */
         _exit(exit_code);
     }
     return exit_rc;
 }
diff --git a/include/crm/common/schemas.h b/include/crm/common/schemas.h
index 40b4841dfb..31799463a1 100644
--- a/include/crm/common/schemas.h
+++ b/include/crm/common/schemas.h
@@ -1,32 +1,30 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #ifndef PCMK__CRM_COMMON_SCHEMAS__H
 #  define PCMK__CRM_COMMON_SCHEMAS__H
 
 #include <glib.h>           // gboolean
 #include <libxml/tree.h>    // xmlNode
 
-gboolean validate_xml(xmlNode *xml_blob, const char *validation,
-                      gboolean to_logs);
 gboolean validate_xml_verbose(const xmlNode *xml_blob);
 
 int get_schema_version(const char *name);
 const char *get_schema_name(int version);
 gboolean cli_config_update(xmlNode ** xml, int *best_version, gboolean to_logs);
 
 #if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1)
 #include <crm/common/schemas_compat.h>
 #endif
 
 #ifdef __cplusplus
 }
 #endif
 
 #endif // PCMK__CRM_COMMON_SCHEMAS__H
diff --git a/include/crm/common/schemas_compat.h b/include/crm/common/schemas_compat.h
index 08d245adee..1054b1faf4 100644
--- a/include/crm/common/schemas_compat.h
+++ b/include/crm/common/schemas_compat.h
@@ -1,39 +1,43 @@
 /*
  * Copyright 2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #ifndef PCMK__CRM_COMMON_SCHEMAS_COMPAT__H
 #define PCMK__CRM_COMMON_SCHEMAS_COMPAT__H
 
 #include <libxml/tree.h>    // xmlNode
 #include <glib.h>           // gboolean
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 /**
  * \file
  * \brief Deprecated Pacemaker XML schemas API
  * \ingroup core
  * \deprecated Do not include this header directly. The APIs in this header, and
  *             the header itself, will be removed in a future release.
  */
 
 //! \deprecated Do not use
 const char *xml_latest_schema(void);
 
 //! \deprecated Do not use
 int update_validation(xmlNode **xml_blob, int *best, int max,
                       gboolean transform, gboolean to_logs);
 
+//! \deprecated Do not use
+gboolean validate_xml(xmlNode *xml_blob, const char *validation,
+                      gboolean to_logs);
+
 #ifdef __cplusplus
 }
 #endif
 
 #endif // PCMK__CRM_COMMON_SCHEMAS_COMPAT__H
diff --git a/include/crm/common/schemas_internal.h b/include/crm/common/schemas_internal.h
index b28db5cc98..eaee532733 100644
--- a/include/crm/common/schemas_internal.h
+++ b/include/crm/common/schemas_internal.h
@@ -1,35 +1,36 @@
 /*
  * Copyright 2006-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #ifndef PCMK__SCHEMAS_INTERNAL__H
 #  define PCMK__SCHEMAS_INTERNAL__H
 
 #include <glib.h>           // GList, gboolean
 #include <libxml/tree.h>    // xmlNode, xmlRelaxNGValidityErrorFunc
 
 void crm_schema_init(void);
 void crm_schema_cleanup(void);
 
 void pcmk__load_schemas_from_dir(const char *dir);
 void pcmk__sort_schemas(void);
 GList *pcmk__schema_files_later_than(const char *name);
 void pcmk__build_schema_xml_node(xmlNode *parent, const char *name,
                                  GList **already_included);
 const char *pcmk__remote_schema_dir(void);
 GList *pcmk__get_schema(const char *name);
 const char *pcmk__highest_schema_name(void);
 int pcmk__cmp_schemas_by_name(const char *schema1_name,
                               const char *schema2_name);
 bool pcmk__validate_xml(xmlNode *xml_blob, const char *validation,
                         xmlRelaxNGValidityErrorFunc error_handler,
                         void *error_handler_context);
+bool pcmk__configured_schema_validates(xmlNode *xml);
 int pcmk__update_schema(xmlNode **xml, const char *max_schema_name,
                         bool transform, bool to_logs);
 
 #endif // PCMK__SCHEMAS_INTERNAL__H
diff --git a/lib/cib/cib_file.c b/lib/cib/cib_file.c
index 4f6cdc2f79..76c29b78ca 100644
--- a/lib/cib/cib_file.c
+++ b/lib/cib/cib_file.c
@@ -1,1175 +1,1175 @@
 /*
  * Original copyright 2004 International Business Machines
  * Later changes copyright 2008-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 #include <unistd.h>
 #include <limits.h>
 #include <stdlib.h>
 #include <stdint.h>
 #include <stdio.h>
 #include <stdarg.h>
 #include <string.h>
 #include <pwd.h>
 
 #include <sys/stat.h>
 #include <sys/types.h>
 #include <glib.h>
 
 #include <crm/crm.h>
 #include <crm/cib/internal.h>
 #include <crm/common/ipc.h>
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>
 
 #define CIB_SERIES "cib"
 #define CIB_SERIES_MAX 100
 #define CIB_SERIES_BZIP FALSE /* Must be false because archived copies are
                                  created with hard links
                                */
 
 #define CIB_LIVE_NAME CIB_SERIES ".xml"
 
 // key: client ID (const char *) -> value: client (cib_t *)
 static GHashTable *client_table = NULL;
 
 enum cib_file_flags {
     cib_file_flag_dirty = (1 << 0),
     cib_file_flag_live  = (1 << 1),
 };
 
 typedef struct cib_file_opaque_s {
     char *id;
     char *filename;
     uint32_t flags; // Group of enum cib_file_flags
     xmlNode *cib_xml;
 } cib_file_opaque_t;
 
 static int cib_file_process_commit_transaction(const char *op, int options,
                                                const char *section,
                                                xmlNode *req, xmlNode *input,
                                                xmlNode *existing_cib,
                                                xmlNode **result_cib,
                                                xmlNode **answer);
 
 /*!
  * \internal
  * \brief Add a CIB file client to client table
  *
  * \param[in] cib  CIB client
  */
 static void
 register_client(const cib_t *cib)
 {
     cib_file_opaque_t *private = cib->variant_opaque;
 
     if (client_table == NULL) {
         client_table = pcmk__strkey_table(NULL, NULL);
     }
     g_hash_table_insert(client_table, private->id, (gpointer) cib);
 }
 
 /*!
  * \internal
  * \brief Remove a CIB file client from client table
  *
  * \param[in] cib  CIB client
  */
 static void
 unregister_client(const cib_t *cib)
 {
     cib_file_opaque_t *private = cib->variant_opaque;
 
     if (client_table == NULL) {
         return;
     }
 
     g_hash_table_remove(client_table, private->id);
 
     /* @COMPAT: Add to crm_exit() when libcib and libcrmcommon are merged,
      * instead of destroying the client table when there are no more clients.
      */
     if (g_hash_table_size(client_table) == 0) {
         g_hash_table_destroy(client_table);
         client_table = NULL;
     }
 }
 
 /*!
  * \internal
  * \brief Look up a CIB file client by its ID
  *
  * \param[in] client_id  CIB client ID
  *
  * \return CIB client with matching ID if found, or \p NULL otherwise
  */
 static cib_t *
 get_client(const char *client_id)
 {
     if (client_table == NULL) {
         return NULL;
     }
     return g_hash_table_lookup(client_table, (gpointer) client_id);
 }
 
 static const cib__op_fn_t cib_op_functions[] = {
     [cib__op_apply_patch]      = cib_process_diff,
     [cib__op_bump]             = cib_process_bump,
     [cib__op_commit_transact]  = cib_file_process_commit_transaction,
     [cib__op_create]           = cib_process_create,
     [cib__op_delete]           = cib_process_delete,
     [cib__op_erase]            = cib_process_erase,
     [cib__op_modify]           = cib_process_modify,
     [cib__op_query]            = cib_process_query,
     [cib__op_replace]          = cib_process_replace,
     [cib__op_upgrade]          = cib_process_upgrade,
 };
 
 /* cib_file_backup() and cib_file_write_with_digest() need to chown the
  * written files only in limited circumstances, so these variables allow
  * that to be indicated without affecting external callers
  */
 static uid_t cib_file_owner = 0;
 static uid_t cib_file_group = 0;
 static gboolean cib_do_chown = FALSE;
 
 #define cib_set_file_flags(cibfile, flags_to_set) do {                  \
         (cibfile)->flags = pcmk__set_flags_as(__func__, __LINE__,       \
                                               LOG_TRACE, "CIB file",    \
                                               cibfile->filename,        \
                                               (cibfile)->flags,         \
                                               (flags_to_set),           \
                                               #flags_to_set);           \
     } while (0)
 
 #define cib_clear_file_flags(cibfile, flags_to_clear) do {              \
         (cibfile)->flags = pcmk__clear_flags_as(__func__, __LINE__,     \
                                                 LOG_TRACE, "CIB file",  \
                                                 cibfile->filename,      \
                                                 (cibfile)->flags,       \
                                                 (flags_to_clear),       \
                                                 #flags_to_clear);       \
     } while (0)
 
 /*!
  * \internal
  * \brief Get the function that performs a given CIB file operation
  *
  * \param[in] operation  Operation whose function to look up
  *
  * \return Function that performs \p operation for a CIB file client
  */
 static cib__op_fn_t
 file_get_op_function(const cib__operation_t *operation)
 {
     enum cib__op_type type = operation->type;
 
     CRM_ASSERT(type >= 0);
 
     if (type >= PCMK__NELEM(cib_op_functions)) {
         return NULL;
     }
     return cib_op_functions[type];
 }
 
 /*!
  * \internal
  * \brief Check whether a file is the live CIB
  *
  * \param[in] filename Name of file to check
  *
  * \return TRUE if file exists and its real path is same as live CIB's
  */
 static gboolean
 cib_file_is_live(const char *filename)
 {
     gboolean same = FALSE;
 
     if (filename != NULL) {
         // Canonicalize file names for true comparison
         char *real_filename = NULL;
 
         if (pcmk__real_path(filename, &real_filename) == pcmk_rc_ok) {
             char *real_livename = NULL;
 
             if (pcmk__real_path(CRM_CONFIG_DIR "/" CIB_LIVE_NAME,
                                 &real_livename) == pcmk_rc_ok) {
                 same = !strcmp(real_filename, real_livename);
                 free(real_livename);
             }
             free(real_filename);
         }
     }
     return same;
 }
 
 static int
 cib_file_process_request(cib_t *cib, xmlNode *request, xmlNode **output)
 {
     int rc = pcmk_ok;
     const cib__operation_t *operation = NULL;
     cib__op_fn_t op_function = NULL;
 
     int call_id = 0;
     int call_options = cib_none;
     const char *op = crm_element_value(request, PCMK__XA_CIB_OP);
     const char *section = crm_element_value(request, PCMK__XA_CIB_SECTION);
     xmlNode *wrapper = pcmk__xe_first_child(request, PCMK__XE_CIB_CALLDATA,
                                             NULL, NULL);
     xmlNode *data = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
 
     bool changed = false;
     bool read_only = false;
     xmlNode *result_cib = NULL;
     xmlNode *cib_diff = NULL;
 
     cib_file_opaque_t *private = cib->variant_opaque;
 
     // We error checked these in callers
     cib__get_operation(op, &operation);
     op_function = file_get_op_function(operation);
 
     crm_element_value_int(request, PCMK__XA_CIB_CALLID, &call_id);
     crm_element_value_int(request, PCMK__XA_CIB_CALLOPT, &call_options);
 
     read_only = !pcmk_is_set(operation->flags, cib__op_attr_modifies);
 
     // Mirror the logic in prepare_input() in pacemaker-based
     if ((section != NULL) && pcmk__xe_is(data, PCMK_XE_CIB)) {
 
         data = pcmk_find_cib_element(data, section);
     }
 
     rc = cib_perform_op(cib, op, call_options, op_function, read_only, section,
                         request, data, true, &changed, &private->cib_xml,
                         &result_cib, &cib_diff, output);
 
     if (pcmk_is_set(call_options, cib_transaction)) {
         /* The rest of the logic applies only to the transaction as a whole, not
          * to individual requests.
          */
         goto done;
     }
 
     if (rc == -pcmk_err_schema_validation) {
         validate_xml_verbose(result_cib);
 
     } else if ((rc == pcmk_ok) && !read_only) {
         pcmk__log_xml_patchset(LOG_DEBUG, cib_diff);
 
         if (result_cib != private->cib_xml) {
             free_xml(private->cib_xml);
             private->cib_xml = result_cib;
         }
         cib_set_file_flags(private, cib_file_flag_dirty);
     }
 
     // Global operation callback (deprecated)
     if (cib->op_callback != NULL) {
         cib->op_callback(NULL, call_id, rc, *output);
     }
 
 done:
     if ((result_cib != private->cib_xml) && (result_cib != *output)) {
         free_xml(result_cib);
     }
     free_xml(cib_diff);
     return rc;
 }
 
 static int
 cib_file_perform_op_delegate(cib_t *cib, const char *op, const char *host,
                              const char *section, xmlNode *data,
                              xmlNode **output_data, int call_options,
                              const char *user_name)
 {
     int rc = pcmk_ok;
     xmlNode *request = NULL;
     xmlNode *output = NULL;
     cib_file_opaque_t *private = cib->variant_opaque;
 
     const cib__operation_t *operation = NULL;
 
     crm_info("Handling %s operation for %s as %s",
              pcmk__s(op, "invalid"), pcmk__s(section, "entire CIB"),
              pcmk__s(user_name, "default user"));
 
     if (output_data != NULL) {
         *output_data = NULL;
     }
 
     if (cib->state == cib_disconnected) {
         return -ENOTCONN;
     }
 
     rc = cib__get_operation(op, &operation);
     rc = pcmk_rc2legacy(rc);
     if (rc != pcmk_ok) {
         // @COMPAT: At compatibility break, use rc directly
         return -EPROTONOSUPPORT;
     }
 
     if (file_get_op_function(operation) == NULL) {
         // @COMPAT: At compatibility break, use EOPNOTSUPP
         crm_err("Operation %s is not supported by CIB file clients", op);
         return -EPROTONOSUPPORT;
     }
 
     cib__set_call_options(call_options, "file operation", cib_no_mtime);
 
     rc = cib__create_op(cib, op, host, section, data, call_options, user_name,
                         NULL, &request);
     if (rc != pcmk_ok) {
         return rc;
     }
     crm_xml_add(request, PCMK_XE_ACL_TARGET, user_name);
     crm_xml_add(request, PCMK__XA_CIB_CLIENTID, private->id);
 
     if (pcmk_is_set(call_options, cib_transaction)) {
         rc = cib__extend_transaction(cib, request);
         goto done;
     }
 
     rc = cib_file_process_request(cib, request, &output);
 
     if ((output_data != NULL) && (output != NULL)) {
         if (output->doc == private->cib_xml->doc) {
             *output_data = pcmk__xml_copy(NULL, output);
         } else {
             *output_data = output;
         }
     }
 
 done:
     if ((output != NULL)
         && (output->doc != private->cib_xml->doc)
         && ((output_data == NULL) || (output != *output_data))) {
 
         free_xml(output);
     }
     free_xml(request);
     return rc;
 }
 
 /*!
  * \internal
  * \brief Read CIB from disk and validate it against XML schema
  *
  * \param[in]   filename  Name of file to read CIB from
  * \param[out]  output    Where to store the read CIB XML
  *
  * \return pcmk_ok on success,
  *         -ENXIO if file does not exist (or stat() otherwise fails), or
  *         -pcmk_err_schema_validation if XML doesn't parse or validate
  * \note If filename is the live CIB, this will *not* verify its digest,
  *       though that functionality would be trivial to add here.
  *       Also, this will *not* verify that the file is writable,
  *       because some callers might not need to write.
  */
 static int
 load_file_cib(const char *filename, xmlNode **output)
 {
     struct stat buf;
     xmlNode *root = NULL;
 
     /* Ensure file is readable */
     if (strcmp(filename, "-") && (stat(filename, &buf) < 0)) {
         return -ENXIO;
     }
 
     /* Parse XML from file */
     root = pcmk__xml_read(filename);
     if (root == NULL) {
         return -pcmk_err_schema_validation;
     }
 
     /* Add a status section if not already present */
     if (pcmk__xe_first_child(root, PCMK_XE_STATUS, NULL, NULL) == NULL) {
         pcmk__xe_create(root, PCMK_XE_STATUS);
     }
 
     /* Validate XML against its specified schema */
-    if (validate_xml(root, NULL, TRUE) == FALSE) {
+    if (!pcmk__configured_schema_validates(root)) {
         const char *schema = crm_element_value(root, PCMK_XA_VALIDATE_WITH);
 
         crm_err("CIB does not validate against %s, or that schema is unknown", schema);
         free_xml(root);
         return -pcmk_err_schema_validation;
     }
 
     /* Remember the parsed XML for later use */
     *output = root;
     return pcmk_ok;
 }
 
 static int
 cib_file_signon(cib_t *cib, const char *name, enum cib_conn_type type)
 {
     int rc = pcmk_ok;
     cib_file_opaque_t *private = cib->variant_opaque;
 
     if (private->filename == NULL) {
         rc = -EINVAL;
     } else {
         rc = load_file_cib(private->filename, &private->cib_xml);
     }
 
     if (rc == pcmk_ok) {
         crm_debug("Opened connection to local file '%s' for %s",
                   private->filename, name);
         cib->state = cib_connected_command;
         cib->type = cib_command;
         register_client(cib);
 
     } else {
         crm_info("Connection to local file '%s' for %s (client %s) failed: %s",
                  private->filename, name, private->id, pcmk_strerror(rc));
     }
     return rc;
 }
 
 /*!
  * \internal
  * \brief Write out the in-memory CIB to a live CIB file
  *
  * \param[in]     cib_root  Root of XML tree to write
  * \param[in,out] path      Full path to file to write
  *
  * \return 0 on success, -1 on failure
  */
 static int
 cib_file_write_live(xmlNode *cib_root, char *path)
 {
     uid_t uid = geteuid();
     struct passwd *daemon_pwent;
     char *sep = strrchr(path, '/');
     const char *cib_dirname, *cib_filename;
     int rc = 0;
 
     /* Get the desired uid/gid */
     errno = 0;
     daemon_pwent = getpwnam(CRM_DAEMON_USER);
     if (daemon_pwent == NULL) {
         crm_perror(LOG_ERR, "Could not find %s user", CRM_DAEMON_USER);
         return -1;
     }
 
     /* If we're root, we can change the ownership;
      * if we're daemon, anything we create will be OK;
      * otherwise, block access so we don't create wrong owner
      */
     if ((uid != 0) && (uid != daemon_pwent->pw_uid)) {
         crm_perror(LOG_ERR, "Must be root or %s to modify live CIB",
                    CRM_DAEMON_USER);
         return 0;
     }
 
     /* fancy footwork to separate dirname from filename
      * (we know the canonical name maps to the live CIB,
      * but the given name might be relative, or symlinked)
      */
     if (sep == NULL) { /* no directory component specified */
         cib_dirname = "./";
         cib_filename = path;
     } else if (sep == path) { /* given name is in / */
         cib_dirname = "/";
         cib_filename = path + 1;
     } else { /* typical case; split given name into parts */
         *sep = '\0';
         cib_dirname = path;
         cib_filename = sep + 1;
     }
 
     /* if we're root, we want to update the file ownership */
     if (uid == 0) {
         cib_file_owner = daemon_pwent->pw_uid;
         cib_file_group = daemon_pwent->pw_gid;
         cib_do_chown = TRUE;
     }
 
     /* write the file */
     if (cib_file_write_with_digest(cib_root, cib_dirname,
                                    cib_filename) != pcmk_ok) {
         rc = -1;
     }
 
     /* turn off file ownership changes, for other callers */
     if (uid == 0) {
         cib_do_chown = FALSE;
     }
 
     /* undo fancy stuff */
     if ((sep != NULL) && (*sep == '\0')) {
         *sep = '/';
     }
 
     return rc;
 }
 
 /*!
  * \internal
  * \brief Sign-off method for CIB file variants
  *
  * This will write the file to disk if needed, and free the in-memory CIB. If
  * the file is the live CIB, it will compute and write a signature as well.
  *
  * \param[in,out] cib  CIB object to sign off
  *
  * \return pcmk_ok on success, pcmk_err_generic on failure
  * \todo This method should refuse to write the live CIB if the CIB manager is
  *       running.
  */
 static int
 cib_file_signoff(cib_t *cib)
 {
     int rc = pcmk_ok;
     cib_file_opaque_t *private = cib->variant_opaque;
 
     crm_debug("Disconnecting from the CIB manager");
     cib->state = cib_disconnected;
     cib->type = cib_no_connection;
     unregister_client(cib);
     cib->cmds->end_transaction(cib, false, cib_none);
 
     /* If the in-memory CIB has been changed, write it to disk */
     if (pcmk_is_set(private->flags, cib_file_flag_dirty)) {
 
         /* If this is the live CIB, write it out with a digest */
         if (pcmk_is_set(private->flags, cib_file_flag_live)) {
             if (cib_file_write_live(private->cib_xml, private->filename) < 0) {
                 rc = pcmk_err_generic;
             }
 
         /* Otherwise, it's a simple write */
         } else {
             bool compress = pcmk__ends_with_ext(private->filename, ".bz2");
 
             if (pcmk__xml_write_file(private->cib_xml, private->filename,
                                      compress, NULL) != pcmk_rc_ok) {
                 rc = pcmk_err_generic;
             }
         }
 
         if (rc == pcmk_ok) {
             crm_info("Wrote CIB to %s", private->filename);
             cib_clear_file_flags(private, cib_file_flag_dirty);
         } else {
             crm_err("Could not write CIB to %s", private->filename);
         }
     }
 
     /* Free the in-memory CIB */
     free_xml(private->cib_xml);
     private->cib_xml = NULL;
     return rc;
 }
 
 static int
 cib_file_free(cib_t *cib)
 {
     int rc = pcmk_ok;
 
     if (cib->state != cib_disconnected) {
         rc = cib_file_signoff(cib);
     }
 
     if (rc == pcmk_ok) {
         cib_file_opaque_t *private = cib->variant_opaque;
 
         free(private->id);
         free(private->filename);
         free(private);
         free(cib->cmds);
         free(cib->user);
         free(cib);
 
     } else {
         fprintf(stderr, "Couldn't sign off: %d\n", rc);
     }
 
     return rc;
 }
 
 static int
 cib_file_inputfd(cib_t *cib)
 {
     return -EPROTONOSUPPORT;
 }
 
 static int
 cib_file_register_notification(cib_t *cib, const char *callback, int enabled)
 {
     return -EPROTONOSUPPORT;
 }
 
 static int
 cib_file_set_connection_dnotify(cib_t *cib,
                                 void (*dnotify) (gpointer user_data))
 {
     return -EPROTONOSUPPORT;
 }
 
 /*!
  * \internal
  * \brief Get the given CIB connection's unique client identifier
  *
  * \param[in]  cib       CIB connection
  * \param[out] async_id  If not \p NULL, where to store asynchronous client ID
  * \param[out] sync_id   If not \p NULL, where to store synchronous client ID
  *
  * \return Legacy Pacemaker return code
  *
  * \note This is the \p cib_file variant implementation of
  *       \p cib_api_operations_t:client_id().
  */
 static int
 cib_file_client_id(const cib_t *cib, const char **async_id,
                    const char **sync_id)
 {
     cib_file_opaque_t *private = cib->variant_opaque;
 
     if (async_id != NULL) {
         *async_id = private->id;
     }
     if (sync_id != NULL) {
         *sync_id = private->id;
     }
     return pcmk_ok;
 }
 
 cib_t *
 cib_file_new(const char *cib_location)
 {
     cib_file_opaque_t *private = NULL;
     cib_t *cib = cib_new_variant();
 
     if (cib == NULL) {
         return NULL;
     }
 
     private = calloc(1, sizeof(cib_file_opaque_t));
 
     if (private == NULL) {
         free(cib);
         return NULL;
     }
     private->id = crm_generate_uuid();
 
     cib->variant = cib_file;
     cib->variant_opaque = private;
 
     if (cib_location == NULL) {
         cib_location = getenv("CIB_file");
         CRM_CHECK(cib_location != NULL, return NULL); // Shouldn't be possible
     }
     private->flags = 0;
     if (cib_file_is_live(cib_location)) {
         cib_set_file_flags(private, cib_file_flag_live);
         crm_trace("File %s detected as live CIB", cib_location);
     }
     private->filename = strdup(cib_location);
 
     /* assign variant specific ops */
     cib->delegate_fn = cib_file_perform_op_delegate;
     cib->cmds->signon = cib_file_signon;
     cib->cmds->signoff = cib_file_signoff;
     cib->cmds->free = cib_file_free;
     cib->cmds->inputfd = cib_file_inputfd; // Deprecated method
 
     cib->cmds->register_notification = cib_file_register_notification;
     cib->cmds->set_connection_dnotify = cib_file_set_connection_dnotify;
 
     cib->cmds->client_id = cib_file_client_id;
 
     return cib;
 }
 
 /*!
  * \internal
  * \brief Compare the calculated digest of an XML tree against a signature file
  *
  * \param[in] root     Root of XML tree to compare
  * \param[in] sigfile  Name of signature file containing digest to compare
  *
  * \return TRUE if digests match or signature file does not exist, else FALSE
  */
 static gboolean
 cib_file_verify_digest(xmlNode *root, const char *sigfile)
 {
     gboolean passed = FALSE;
     char *expected;
     int rc = pcmk__file_contents(sigfile, &expected);
 
     switch (rc) {
         case pcmk_rc_ok:
             if (expected == NULL) {
                 crm_err("On-disk digest at %s is empty", sigfile);
                 return FALSE;
             }
             break;
         case ENOENT:
             crm_warn("No on-disk digest present at %s", sigfile);
             return TRUE;
         default:
             crm_err("Could not read on-disk digest from %s: %s",
                     sigfile, pcmk_rc_str(rc));
             return FALSE;
     }
     passed = pcmk__verify_digest(root, expected);
     free(expected);
     return passed;
 }
 
 /*!
  * \internal
  * \brief Read an XML tree from a file and verify its digest
  *
  * \param[in]  filename  Name of XML file to read
  * \param[in]  sigfile   Name of signature file containing digest to compare
  * \param[out] root      If non-NULL, will be set to pointer to parsed XML tree
  *
  * \return 0 if file was successfully read, parsed and verified, otherwise:
  *         -errno on stat() failure,
  *         -pcmk_err_cib_corrupt if file size is 0 or XML is not parseable, or
  *         -pcmk_err_cib_modified if digests do not match
  * \note If root is non-NULL, it is the caller's responsibility to free *root on
  *       successful return.
  */
 int
 cib_file_read_and_verify(const char *filename, const char *sigfile, xmlNode **root)
 {
     int s_res;
     struct stat buf;
     char *local_sigfile = NULL;
     xmlNode *local_root = NULL;
 
     CRM_ASSERT(filename != NULL);
     if (root) {
         *root = NULL;
     }
 
     /* Verify that file exists and its size is nonzero */
     s_res = stat(filename, &buf);
     if (s_res < 0) {
         crm_perror(LOG_WARNING, "Could not verify cluster configuration file %s", filename);
         return -errno;
     } else if (buf.st_size == 0) {
         crm_warn("Cluster configuration file %s is corrupt (size is zero)", filename);
         return -pcmk_err_cib_corrupt;
     }
 
     /* Parse XML */
     local_root = pcmk__xml_read(filename);
     if (local_root == NULL) {
         crm_warn("Cluster configuration file %s is corrupt (unparseable as XML)", filename);
         return -pcmk_err_cib_corrupt;
     }
 
     /* If sigfile is not specified, use original file name plus .sig */
     if (sigfile == NULL) {
         sigfile = local_sigfile = crm_strdup_printf("%s.sig", filename);
     }
 
     /* Verify that digests match */
     if (cib_file_verify_digest(local_root, sigfile) == FALSE) {
         free(local_sigfile);
         free_xml(local_root);
         return -pcmk_err_cib_modified;
     }
 
     free(local_sigfile);
     if (root) {
         *root = local_root;
     } else {
         free_xml(local_root);
     }
     return pcmk_ok;
 }
 
 /*!
  * \internal
  * \brief Back up a CIB
  *
  * \param[in] cib_dirname Directory containing CIB file and backups
  * \param[in] cib_filename Name (relative to cib_dirname) of CIB file to back up
  *
  * \return 0 on success, -1 on error
  */
 static int
 cib_file_backup(const char *cib_dirname, const char *cib_filename)
 {
     int rc = 0;
     unsigned int seq;
     char *cib_path = crm_strdup_printf("%s/%s", cib_dirname, cib_filename);
     char *cib_digest = crm_strdup_printf("%s.sig", cib_path);
     char *backup_path;
     char *backup_digest;
 
     // Determine backup and digest file names
     if (pcmk__read_series_sequence(cib_dirname, CIB_SERIES,
                                    &seq) != pcmk_rc_ok) {
         // @TODO maybe handle errors better ...
         seq = 0;
     }
     backup_path = pcmk__series_filename(cib_dirname, CIB_SERIES, seq,
                                         CIB_SERIES_BZIP);
     backup_digest = crm_strdup_printf("%s.sig", backup_path);
 
     /* Remove the old backups if they exist */
     unlink(backup_path);
     unlink(backup_digest);
 
     /* Back up the CIB, by hard-linking it to the backup name */
     if ((link(cib_path, backup_path) < 0) && (errno != ENOENT)) {
         crm_perror(LOG_ERR, "Could not archive %s by linking to %s",
                    cib_path, backup_path);
         rc = -1;
 
     /* Back up the CIB signature similarly */
     } else if ((link(cib_digest, backup_digest) < 0) && (errno != ENOENT)) {
         crm_perror(LOG_ERR, "Could not archive %s by linking to %s",
                    cib_digest, backup_digest);
         rc = -1;
 
     /* Update the last counter and ensure everything is sync'd to media */
     } else {
         pcmk__write_series_sequence(cib_dirname, CIB_SERIES, ++seq,
                                     CIB_SERIES_MAX);
         if (cib_do_chown) {
             int rc2;
 
             if ((chown(backup_path, cib_file_owner, cib_file_group) < 0)
                     && (errno != ENOENT)) {
                 crm_perror(LOG_ERR, "Could not set owner of %s", backup_path);
                 rc = -1;
             }
             if ((chown(backup_digest, cib_file_owner, cib_file_group) < 0)
                     && (errno != ENOENT)) {
                 crm_perror(LOG_ERR, "Could not set owner of %s", backup_digest);
                 rc = -1;
             }
             rc2 = pcmk__chown_series_sequence(cib_dirname, CIB_SERIES,
                                               cib_file_owner, cib_file_group);
             if (rc2 != pcmk_rc_ok) {
                 crm_err("Could not set owner of sequence file in %s: %s",
                         cib_dirname, pcmk_rc_str(rc2));
                 rc = -1;
             }
         }
         pcmk__sync_directory(cib_dirname);
         crm_info("Archived previous version as %s", backup_path);
     }
 
     free(cib_path);
     free(cib_digest);
     free(backup_path);
     free(backup_digest);
     return rc;
 }
 
 /*!
  * \internal
  * \brief Prepare CIB XML to be written to disk
  *
  * Set \c PCMK_XA_NUM_UPDATES to 0, set \c PCMK_XA_CIB_LAST_WRITTEN to the
  * current timestamp, and strip out the status section.
  *
  * \param[in,out] root  Root of CIB XML tree
  *
  * \return void
  */
 static void
 cib_file_prepare_xml(xmlNode *root)
 {
     xmlNode *cib_status_root = NULL;
 
     /* Always write out with num_updates=0 and current last-written timestamp */
     crm_xml_add(root, PCMK_XA_NUM_UPDATES, "0");
     pcmk__xe_add_last_written(root);
 
     /* Delete status section before writing to file, because
      * we discard it on startup anyway, and users get confused by it */
     cib_status_root = pcmk__xe_first_child(root, PCMK_XE_STATUS, NULL, NULL);
     CRM_CHECK(cib_status_root != NULL, return);
     free_xml(cib_status_root);
 }
 
 /*!
  * \internal
  * \brief Write CIB to disk, along with a signature file containing its digest
  *
  * \param[in,out] cib_root      Root of XML tree to write
  * \param[in]     cib_dirname   Directory containing CIB and signature files
  * \param[in]     cib_filename  Name (relative to cib_dirname) of file to write
  *
  * \return pcmk_ok on success,
  *         pcmk_err_cib_modified if existing cib_filename doesn't match digest,
  *         pcmk_err_cib_backup if existing cib_filename couldn't be backed up,
  *         or pcmk_err_cib_save if new cib_filename couldn't be saved
  */
 int
 cib_file_write_with_digest(xmlNode *cib_root, const char *cib_dirname,
                            const char *cib_filename)
 {
     int exit_rc = pcmk_ok;
     int rc, fd;
     char *digest = NULL;
 
     /* Detect CIB version for diagnostic purposes */
     const char *epoch = crm_element_value(cib_root, PCMK_XA_EPOCH);
     const char *admin_epoch = crm_element_value(cib_root, PCMK_XA_ADMIN_EPOCH);
 
     /* Determine full CIB and signature pathnames */
     char *cib_path = crm_strdup_printf("%s/%s", cib_dirname, cib_filename);
     char *digest_path = crm_strdup_printf("%s.sig", cib_path);
 
     /* Create temporary file name patterns for writing out CIB and signature */
     char *tmp_cib = crm_strdup_printf("%s/cib.XXXXXX", cib_dirname);
     char *tmp_digest = crm_strdup_printf("%s/cib.XXXXXX", cib_dirname);
 
     /* Ensure the admin didn't modify the existing CIB underneath us */
     crm_trace("Reading cluster configuration file %s", cib_path);
     rc = cib_file_read_and_verify(cib_path, NULL, NULL);
     if ((rc != pcmk_ok) && (rc != -ENOENT)) {
         crm_err("%s was manually modified while the cluster was active!",
                 cib_path);
         exit_rc = pcmk_err_cib_modified;
         goto cleanup;
     }
 
     /* Back up the existing CIB */
     if (cib_file_backup(cib_dirname, cib_filename) < 0) {
         exit_rc = pcmk_err_cib_backup;
         goto cleanup;
     }
 
     crm_debug("Writing CIB to disk");
     umask(S_IWGRP | S_IWOTH | S_IROTH);
     cib_file_prepare_xml(cib_root);
 
     /* Write the CIB to a temporary file, so we can deploy (near) atomically */
     fd = mkstemp(tmp_cib);
     if (fd < 0) {
         crm_perror(LOG_ERR, "Couldn't open temporary file %s for writing CIB",
                    tmp_cib);
         exit_rc = pcmk_err_cib_save;
         goto cleanup;
     }
 
     /* Protect the temporary file */
     if (fchmod(fd, S_IRUSR | S_IWUSR) < 0) {
         crm_perror(LOG_ERR, "Couldn't protect temporary file %s for writing CIB",
                    tmp_cib);
         exit_rc = pcmk_err_cib_save;
         goto cleanup;
     }
     if (cib_do_chown && (fchown(fd, cib_file_owner, cib_file_group) < 0)) {
         crm_perror(LOG_ERR, "Couldn't protect temporary file %s for writing CIB",
                    tmp_cib);
         exit_rc = pcmk_err_cib_save;
         goto cleanup;
     }
 
     /* Write out the CIB */
     if (pcmk__xml_write_fd(cib_root, tmp_cib, fd, false, NULL) != pcmk_rc_ok) {
         crm_err("Changes couldn't be written to %s", tmp_cib);
         exit_rc = pcmk_err_cib_save;
         goto cleanup;
     }
 
     /* Calculate CIB digest */
     digest = calculate_on_disk_digest(cib_root);
     CRM_ASSERT(digest != NULL);
     crm_info("Wrote version %s.%s.0 of the CIB to disk (digest: %s)",
              (admin_epoch ? admin_epoch : "0"), (epoch ? epoch : "0"), digest);
 
     /* Write the CIB digest to a temporary file */
     fd = mkstemp(tmp_digest);
     if (fd < 0) {
         crm_perror(LOG_ERR, "Could not create temporary file for CIB digest");
         exit_rc = pcmk_err_cib_save;
         goto cleanup;
     }
     if (cib_do_chown && (fchown(fd, cib_file_owner, cib_file_group) < 0)) {
         crm_perror(LOG_ERR, "Couldn't protect temporary file %s for writing CIB",
                    tmp_cib);
         exit_rc = pcmk_err_cib_save;
         close(fd);
         goto cleanup;
     }
     rc = pcmk__write_sync(fd, digest);
     if (rc != pcmk_rc_ok) {
         crm_err("Could not write digest to %s: %s",
                 tmp_digest, pcmk_rc_str(rc));
         exit_rc = pcmk_err_cib_save;
         close(fd);
         goto cleanup;
     }
     close(fd);
     crm_debug("Wrote digest %s to disk", digest);
 
     /* Verify that what we wrote is sane */
     crm_info("Reading cluster configuration file %s (digest: %s)",
              tmp_cib, tmp_digest);
     rc = cib_file_read_and_verify(tmp_cib, tmp_digest, NULL);
     CRM_ASSERT(rc == 0);
 
     /* Rename temporary files to live, and sync directory changes to media */
     crm_debug("Activating %s", tmp_cib);
     if (rename(tmp_cib, cib_path) < 0) {
         crm_perror(LOG_ERR, "Couldn't rename %s as %s", tmp_cib, cib_path);
         exit_rc = pcmk_err_cib_save;
     }
     if (rename(tmp_digest, digest_path) < 0) {
         crm_perror(LOG_ERR, "Couldn't rename %s as %s", tmp_digest,
                    digest_path);
         exit_rc = pcmk_err_cib_save;
     }
     pcmk__sync_directory(cib_dirname);
 
   cleanup:
     free(cib_path);
     free(digest_path);
     free(digest);
     free(tmp_digest);
     free(tmp_cib);
     return exit_rc;
 }
 
 /*!
  * \internal
  * \brief Process requests in a CIB transaction
  *
  * Stop when a request fails or when all requests have been processed.
  *
  * \param[in,out] cib          CIB client
  * \param[in,out] transaction  CIB transaction
  *
  * \return Standard Pacemaker return code
  */
 static int
 cib_file_process_transaction_requests(cib_t *cib, xmlNode *transaction)
 {
     cib_file_opaque_t *private = cib->variant_opaque;
 
     for (xmlNode *request = pcmk__xe_first_child(transaction,
                                                  PCMK__XE_CIB_COMMAND, NULL,
                                                  NULL);
          request != NULL; request = pcmk__xe_next_same(request)) {
 
         xmlNode *output = NULL;
         const char *op = crm_element_value(request, PCMK__XA_CIB_OP);
 
         int rc = cib_file_process_request(cib, request, &output);
 
         rc = pcmk_legacy2rc(rc);
         if (rc != pcmk_rc_ok) {
             crm_err("Aborting transaction for CIB file client (%s) on file "
                     "'%s' due to failed %s request: %s",
                     private->id, private->filename, op, pcmk_rc_str(rc));
             crm_log_xml_info(request, "Failed request");
             return rc;
         }
 
         crm_trace("Applied %s request to transaction working CIB for CIB file "
                   "client (%s) on file '%s'",
                   op, private->id, private->filename);
         crm_log_xml_trace(request, "Successful request");
     }
 
     return pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Commit a given CIB file client's transaction to a working CIB copy
  *
  * \param[in,out] cib          CIB file client
  * \param[in]     transaction  CIB transaction
  * \param[in,out] result_cib   Where to store result CIB
  *
  * \return Standard Pacemaker return code
  *
  * \note The caller is responsible for replacing the \p cib argument's
  *       \p private->cib_xml with \p result_cib on success, and for freeing
  *       \p result_cib using \p free_xml() on failure.
  */
 static int
 cib_file_commit_transaction(cib_t *cib, xmlNode *transaction,
                             xmlNode **result_cib)
 {
     int rc = pcmk_rc_ok;
     cib_file_opaque_t *private = cib->variant_opaque;
     xmlNode *saved_cib = private->cib_xml;
 
     CRM_CHECK(pcmk__xe_is(transaction, PCMK__XE_CIB_TRANSACTION),
               return pcmk_rc_no_transaction);
 
     /* *result_cib should be a copy of private->cib_xml (created by
      * cib_perform_op()). If not, make a copy now. Change tracking isn't
      * strictly required here because:
      * * Each request in the transaction will have changes tracked and ACLs
      *   checked if appropriate.
      * * cib_perform_op() will infer changes for the commit request at the end.
      */
     CRM_CHECK((*result_cib != NULL) && (*result_cib != private->cib_xml),
               *result_cib = pcmk__xml_copy(NULL, private->cib_xml));
 
     crm_trace("Committing transaction for CIB file client (%s) on file '%s' to "
               "working CIB",
               private->id, private->filename);
 
     // Apply all changes to a working copy of the CIB
     private->cib_xml = *result_cib;
 
     rc = cib_file_process_transaction_requests(cib, transaction);
 
     crm_trace("Transaction commit %s for CIB file client (%s) on file '%s'",
               ((rc == pcmk_rc_ok)? "succeeded" : "failed"),
               private->id, private->filename);
 
     /* Some request types (for example, erase) may have freed private->cib_xml
      * (the working copy) and pointed it at a new XML object. In that case, it
      * follows that *result_cib (the working copy) was freed.
      *
      * Point *result_cib at the updated working copy stored in private->cib_xml.
      */
     *result_cib = private->cib_xml;
 
     // Point private->cib_xml back to the unchanged original copy
     private->cib_xml = saved_cib;
 
     return rc;
 }
 
 static int
 cib_file_process_commit_transaction(const char *op, int options,
                                     const char *section, xmlNode *req,
                                     xmlNode *input, xmlNode *existing_cib,
                                     xmlNode **result_cib, xmlNode **answer)
 {
     int rc = pcmk_rc_ok;
     const char *client_id = crm_element_value(req, PCMK__XA_CIB_CLIENTID);
     cib_t *cib = NULL;
 
     CRM_CHECK(client_id != NULL, return -EINVAL);
 
     cib = get_client(client_id);
     CRM_CHECK(cib != NULL, return -EINVAL);
 
     rc = cib_file_commit_transaction(cib, input, result_cib);
     if (rc != pcmk_rc_ok) {
         cib_file_opaque_t *private = cib->variant_opaque;
 
         crm_err("Could not commit transaction for CIB file client (%s) on "
                 "file '%s': %s",
                 private->id, private->filename, pcmk_rc_str(rc));
     }
     return pcmk_rc2legacy(rc);
 }
diff --git a/lib/cib/cib_utils.c b/lib/cib/cib_utils.c
index 76c45fe13c..81aa12e6f2 100644
--- a/lib/cib/cib_utils.c
+++ b/lib/cib/cib_utils.c
@@ -1,1097 +1,1098 @@
 /*
  * Original copyright 2004 International Business Machines
  * Later changes copyright 2008-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 #include <crm_internal.h>
 #include <unistd.h>
 #include <stdlib.h>
 #include <stdio.h>
 #include <stdarg.h>
 #include <string.h>
 #include <sys/utsname.h>
 
 #include <glib.h>
 
 #include <crm/crm.h>
 #include <crm/cib/internal.h>
 #include <crm/common/cib_internal.h>
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>
 #include <crm/pengine/rules.h>
 
 gboolean
 cib_version_details(xmlNode * cib, int *admin_epoch, int *epoch, int *updates)
 {
     *epoch = -1;
     *updates = -1;
     *admin_epoch = -1;
 
     if (cib == NULL) {
         return FALSE;
 
     } else {
         crm_element_value_int(cib, PCMK_XA_EPOCH, epoch);
         crm_element_value_int(cib, PCMK_XA_NUM_UPDATES, updates);
         crm_element_value_int(cib, PCMK_XA_ADMIN_EPOCH, admin_epoch);
     }
     return TRUE;
 }
 
 gboolean
 cib_diff_version_details(xmlNode * diff, int *admin_epoch, int *epoch, int *updates,
                          int *_admin_epoch, int *_epoch, int *_updates)
 {
     int add[] = { 0, 0, 0 };
     int del[] = { 0, 0, 0 };
 
     xml_patch_versions(diff, add, del);
 
     *admin_epoch = add[0];
     *epoch = add[1];
     *updates = add[2];
 
     *_admin_epoch = del[0];
     *_epoch = del[1];
     *_updates = del[2];
 
     return TRUE;
 }
 
 /*!
  * \internal
  * \brief Get the XML patchset from a CIB diff notification
  *
  * \param[in]  msg       CIB diff notification
  * \param[out] patchset  Where to store XML patchset
  *
  * \return Standard Pacemaker return code
  */
 int
 cib__get_notify_patchset(const xmlNode *msg, const xmlNode **patchset)
 {
     int rc = pcmk_err_generic;
     xmlNode *wrapper = NULL;
 
     CRM_ASSERT(patchset != NULL);
     *patchset = NULL;
 
     if (msg == NULL) {
         crm_err("CIB diff notification received with no XML");
         return ENOMSG;
     }
 
     if ((crm_element_value_int(msg, PCMK__XA_CIB_RC, &rc) != 0)
         || (rc != pcmk_ok)) {
 
         crm_warn("Ignore failed CIB update: %s " CRM_XS " rc=%d",
                  pcmk_strerror(rc), rc);
         crm_log_xml_debug(msg, "failed");
         return pcmk_legacy2rc(rc);
     }
 
     wrapper = pcmk__xe_first_child(msg, PCMK__XE_CIB_UPDATE_RESULT, NULL, NULL);
     *patchset = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
 
     if (*patchset == NULL) {
         crm_err("CIB diff notification received with no patchset");
         return ENOMSG;
     }
     return pcmk_rc_ok;
 }
 
 #define XPATH_DIFF_V1 "//" PCMK__XE_CIB_UPDATE_RESULT "//" PCMK__XE_DIFF_ADDED
 
 /*!
  * \internal
  * \brief Check whether a given CIB element was modified in a CIB patchset (v1)
  *
  * \param[in] patchset  CIB XML patchset
  * \param[in] element   XML tag of CIB element to check (\c NULL is equivalent
  *                      to \c PCMK_XE_CIB)
  *
  * \return \c true if \p element was modified, or \c false otherwise
  */
 static bool
 element_in_patchset_v1(const xmlNode *patchset, const char *element)
 {
     char *xpath = crm_strdup_printf(XPATH_DIFF_V1 "//%s",
                                     pcmk__s(element, PCMK_XE_CIB));
     xmlXPathObject *xpath_obj = xpath_search(patchset, xpath);
 
     free(xpath);
 
     if (xpath_obj == NULL) {
         return false;
     }
     freeXpathObject(xpath_obj);
     return true;
 }
 
 /*!
  * \internal
  * \brief Check whether a given CIB element was modified in a CIB patchset (v2)
  *
  * \param[in] patchset  CIB XML patchset
  * \param[in] element   XML tag of CIB element to check (\c NULL is equivalent
  *                      to \c PCMK_XE_CIB). Supported values include any CIB
  *                      element supported by \c pcmk__cib_abs_xpath_for().
  *
  * \return \c true if \p element was modified, or \c false otherwise
  */
 static bool
 element_in_patchset_v2(const xmlNode *patchset, const char *element)
 {
     const char *element_xpath = pcmk__cib_abs_xpath_for(element);
     const char *parent_xpath = pcmk_cib_parent_name_for(element);
     char *element_regex = NULL;
     bool rc = false;
 
     CRM_CHECK(element_xpath != NULL, return false); // Unsupported element
 
     // Matches if and only if element_xpath is part of a changed path
     element_regex = crm_strdup_printf("^%s(/|$)", element_xpath);
 
     for (const xmlNode *change = pcmk__xe_first_child(patchset, PCMK_XE_CHANGE,
                                                       NULL, NULL);
          change != NULL; change = pcmk__xe_next_same(change)) {
 
         const char *op = crm_element_value(change, PCMK__XA_CIB_OP);
         const char *diff_xpath = crm_element_value(change, PCMK_XA_PATH);
 
         if (pcmk__str_eq(diff_xpath, element_regex, pcmk__str_regex)) {
             // Change to an existing element
             rc = true;
             break;
         }
 
         if (pcmk__str_eq(op, PCMK_VALUE_CREATE, pcmk__str_none)
             && pcmk__str_eq(diff_xpath, parent_xpath, pcmk__str_none)
             && pcmk__xe_is(pcmk__xe_first_child(change, NULL, NULL, NULL),
                                                 element)) {
 
             // Newly added element
             rc = true;
             break;
         }
     }
 
     free(element_regex);
     return rc;
 }
 
 /*!
  * \internal
  * \brief Check whether a given CIB element was modified in a CIB patchset
  *
  * \param[in] patchset  CIB XML patchset
  * \param[in] element   XML tag of CIB element to check (\c NULL is equivalent
  *                      to \c PCMK_XE_CIB). Supported values include any CIB
  *                      element supported by \c pcmk__cib_abs_xpath_for().
  *
  * \return \c true if \p element was modified, or \c false otherwise
  */
 bool
 cib__element_in_patchset(const xmlNode *patchset, const char *element)
 {
     int format = 1;
 
     CRM_ASSERT(patchset != NULL);
 
     crm_element_value_int(patchset, PCMK_XA_FORMAT, &format);
     switch (format) {
         case 1:
             return element_in_patchset_v1(patchset, element);
 
         case 2:
             return element_in_patchset_v2(patchset, element);
 
         default:
             crm_warn("Unknown patch format: %d", format);
             return false;
     }
 }
 
 /*!
  * \brief Create XML for a new (empty) CIB
  *
  * \param[in] cib_epoch  What to use as \c PCMK_XA_EPOCH CIB attribute
  *
  * \return Newly created XML for empty CIB
  * \note It is the caller's responsibility to free the result with free_xml().
  */
 xmlNode *
 createEmptyCib(int cib_epoch)
 {
     xmlNode *cib_root = NULL, *config = NULL;
 
     cib_root = pcmk__xe_create(NULL, PCMK_XE_CIB);
     crm_xml_add(cib_root, PCMK_XA_CRM_FEATURE_SET, CRM_FEATURE_SET);
     crm_xml_add(cib_root, PCMK_XA_VALIDATE_WITH, pcmk__highest_schema_name());
 
     crm_xml_add_int(cib_root, PCMK_XA_EPOCH, cib_epoch);
     crm_xml_add_int(cib_root, PCMK_XA_NUM_UPDATES, 0);
     crm_xml_add_int(cib_root, PCMK_XA_ADMIN_EPOCH, 0);
 
     config = pcmk__xe_create(cib_root, PCMK_XE_CONFIGURATION);
     pcmk__xe_create(cib_root, PCMK_XE_STATUS);
 
     pcmk__xe_create(config, PCMK_XE_CRM_CONFIG);
     pcmk__xe_create(config, PCMK_XE_NODES);
     pcmk__xe_create(config, PCMK_XE_RESOURCES);
     pcmk__xe_create(config, PCMK_XE_CONSTRAINTS);
 
 #if PCMK__RESOURCE_STICKINESS_DEFAULT != 0
     {
         xmlNode *rsc_defaults = pcmk__xe_create(config, PCMK_XE_RSC_DEFAULTS);
         xmlNode *meta = pcmk__xe_create(rsc_defaults, PCMK_XE_META_ATTRIBUTES);
         xmlNode *nvpair = pcmk__xe_create(meta, PCMK_XE_NVPAIR);
 
         crm_xml_add(meta, PCMK_XA_ID, "build-resource-defaults");
         crm_xml_add(nvpair, PCMK_XA_ID, "build-" PCMK_META_RESOURCE_STICKINESS);
         crm_xml_add(nvpair, PCMK_XA_NAME, PCMK_META_RESOURCE_STICKINESS);
         crm_xml_add_int(nvpair, PCMK_XA_VALUE,
                         PCMK__RESOURCE_STICKINESS_DEFAULT);
     }
 #endif
     return cib_root;
 }
 
 static bool
 cib_acl_enabled(xmlNode *xml, const char *user)
 {
     bool rc = FALSE;
 
     if(pcmk_acl_required(user)) {
         const char *value = NULL;
         GHashTable *options = pcmk__strkey_table(free, free);
 
         cib_read_config(options, xml);
         value = pcmk__cluster_option(options, PCMK_OPT_ENABLE_ACL);
         rc = crm_is_true(value);
         g_hash_table_destroy(options);
     }
 
     crm_trace("CIB ACL is %s", rc ? "enabled" : "disabled");
     return rc;
 }
 
 /*!
  * \internal
  * \brief Determine whether to perform operations on a scratch copy of the CIB
  *
  * \param[in] op            CIB operation
  * \param[in] section       CIB section
  * \param[in] call_options  CIB call options
  *
  * \return \p true if we should make a copy of the CIB, or \p false otherwise
  */
 static bool
 should_copy_cib(const char *op, const char *section, int call_options)
 {
     if (pcmk_is_set(call_options, cib_dryrun)) {
         // cib_dryrun implies a scratch copy by definition; no side effects
         return true;
     }
 
     if (pcmk__str_eq(op, PCMK__CIB_REQUEST_COMMIT_TRANSACT, pcmk__str_none)) {
         /* Commit-transaction must make a copy for atomicity. We must revert to
          * the original CIB if the entire transaction cannot be applied
          * successfully.
          */
         return true;
     }
 
     if (pcmk_is_set(call_options, cib_transaction)) {
         /* If cib_transaction is set, then we're in the process of committing a
          * transaction. The commit-transaction request already made a scratch
          * copy, and we're accumulating changes in that copy.
          */
         return false;
     }
 
     if (pcmk__str_eq(section, PCMK_XE_STATUS, pcmk__str_none)) {
         /* Copying large CIBs accounts for a huge percentage of our CIB usage,
          * and this avoids some of it.
          *
          * @TODO: Is this safe? See discussion at
          * https://github.com/ClusterLabs/pacemaker/pull/3094#discussion_r1211400690.
          */
         return false;
     }
 
     // Default behavior is to operate on a scratch copy
     return true;
 }
 
 int
 cib_perform_op(cib_t *cib, const char *op, int call_options, cib__op_fn_t fn,
                bool is_query, const char *section, xmlNode *req, xmlNode *input,
                bool manage_counters, bool *config_changed, xmlNode **current_cib,
                xmlNode **result_cib, xmlNode **diff, xmlNode **output)
 {
     int rc = pcmk_ok;
     bool check_schema = true;
     bool make_copy = true;
     xmlNode *top = NULL;
     xmlNode *scratch = NULL;
     xmlNode *patchset_cib = NULL;
     xmlNode *local_diff = NULL;
 
     const char *user = crm_element_value(req, PCMK__XA_CIB_USER);
     bool with_digest = false;
 
     crm_trace("Begin %s%s%s op",
               (pcmk_is_set(call_options, cib_dryrun)? "dry run of " : ""),
               (is_query? "read-only " : ""), op);
 
     CRM_CHECK(output != NULL, return -ENOMSG);
     CRM_CHECK(current_cib != NULL, return -ENOMSG);
     CRM_CHECK(result_cib != NULL, return -ENOMSG);
     CRM_CHECK(config_changed != NULL, return -ENOMSG);
 
     if(output) {
         *output = NULL;
     }
 
     *result_cib = NULL;
     *config_changed = false;
 
     if (fn == NULL) {
         return -EINVAL;
     }
 
     if (is_query) {
         xmlNode *cib_ro = *current_cib;
         xmlNode *cib_filtered = NULL;
 
         if (cib_acl_enabled(cib_ro, user)
             && xml_acl_filtered_copy(user, *current_cib, *current_cib,
                                      &cib_filtered)) {
 
             if (cib_filtered == NULL) {
                 crm_debug("Pre-filtered the entire cib");
                 return -EACCES;
             }
             cib_ro = cib_filtered;
             crm_log_xml_trace(cib_ro, "filtered");
         }
 
         rc = (*fn) (op, call_options, section, req, input, cib_ro, result_cib, output);
 
         if(output == NULL || *output == NULL) {
             /* nothing */
 
         } else if(cib_filtered == *output) {
             cib_filtered = NULL; /* Let them have this copy */
 
         } else if (*output == *current_cib) {
             /* They already know not to free it */
 
         } else if(cib_filtered && (*output)->doc == cib_filtered->doc) {
             /* We're about to free the document of which *output is a part */
             *output = pcmk__xml_copy(NULL, *output);
 
         } else if ((*output)->doc == (*current_cib)->doc) {
             /* Give them a copy they can free */
             *output = pcmk__xml_copy(NULL, *output);
         }
 
         free_xml(cib_filtered);
         return rc;
     }
 
     make_copy = should_copy_cib(op, section, call_options);
 
     if (!make_copy) {
         /* Conditional on v2 patch style */
 
         scratch = *current_cib;
 
         // Make a copy of the top-level element to store version details
         top = pcmk__xe_create(NULL, (const char *) scratch->name);
         copy_in_properties(top, scratch);
         patchset_cib = top;
 
         xml_track_changes(scratch, user, NULL, cib_acl_enabled(scratch, user));
         rc = (*fn) (op, call_options, section, req, input, scratch, &scratch, output);
 
         /* If scratch points to a new object now (for example, after an erase
          * operation), then *current_cib should point to the same object.
          */
         *current_cib = scratch;
 
     } else {
         scratch = pcmk__xml_copy(NULL, *current_cib);
         patchset_cib = *current_cib;
 
         xml_track_changes(scratch, user, NULL, cib_acl_enabled(scratch, user));
         rc = (*fn) (op, call_options, section, req, input, *current_cib,
                     &scratch, output);
 
         if ((scratch != NULL) && !xml_tracking_changes(scratch)) {
             crm_trace("Inferring changes after %s op", op);
             xml_track_changes(scratch, user, *current_cib,
                               cib_acl_enabled(*current_cib, user));
             xml_calculate_changes(*current_cib, scratch);
         }
         CRM_CHECK(*current_cib != scratch, return -EINVAL);
     }
 
     xml_acl_disable(scratch); /* Allow the system to make any additional changes */
 
     if (rc == pcmk_ok && scratch == NULL) {
         rc = -EINVAL;
         goto done;
 
     } else if(rc == pcmk_ok && xml_acl_denied(scratch)) {
         crm_trace("ACL rejected part or all of the proposed changes");
         rc = -EACCES;
         goto done;
 
     } else if (rc != pcmk_ok) {
         goto done;
     }
 
     /* If the CIB is from a file, we don't need to check that the feature set is
      * supported.  All we care about in that case is the schema version, which
      * is checked elsewhere.
      */
     if (scratch && (cib == NULL || cib->variant != cib_file)) {
         const char *new_version = crm_element_value(scratch, PCMK_XA_CRM_FEATURE_SET);
 
         rc = pcmk__check_feature_set(new_version);
         if (rc != pcmk_rc_ok) {
             pcmk__config_err("Discarding update with feature set '%s' greater than our own '%s'",
                              new_version, CRM_FEATURE_SET);
             rc = pcmk_rc2legacy(rc);
             goto done;
         }
     }
 
     if (patchset_cib != NULL) {
         int old = 0;
         int new = 0;
 
         crm_element_value_int(scratch, PCMK_XA_ADMIN_EPOCH, &new);
         crm_element_value_int(patchset_cib, PCMK_XA_ADMIN_EPOCH, &old);
 
         if (old > new) {
             crm_err("%s went backwards: %d -> %d (Opts: %#x)",
                     PCMK_XA_ADMIN_EPOCH, old, new, call_options);
             crm_log_xml_warn(req, "Bad Op");
             crm_log_xml_warn(input, "Bad Data");
             rc = -pcmk_err_old_data;
 
         } else if (old == new) {
             crm_element_value_int(scratch, PCMK_XA_EPOCH, &new);
             crm_element_value_int(patchset_cib, PCMK_XA_EPOCH, &old);
             if (old > new) {
                 crm_err("%s went backwards: %d -> %d (Opts: %#x)",
                         PCMK_XA_EPOCH, old, new, call_options);
                 crm_log_xml_warn(req, "Bad Op");
                 crm_log_xml_warn(input, "Bad Data");
                 rc = -pcmk_err_old_data;
             }
         }
     }
 
     crm_trace("Massaging CIB contents");
     pcmk__strip_xml_text(scratch);
 
     if (!make_copy) {
         /* At this point, patchset_cib is just the PCMK_XE_CIB tag and its
          * properties.
          *
          * The v1 format would barf on this, but we know the v2 patch
          * format only needs it for the top-level version fields
          */
         local_diff = xml_create_patchset(2, patchset_cib, scratch,
                                          config_changed, manage_counters);
 
     } else {
         static time_t expires = 0;
         time_t tm_now = time(NULL);
 
         if (expires < tm_now) {
             expires = tm_now + 60;  /* Validate clients are correctly applying v2-style diffs at most once a minute */
             with_digest = true;
         }
 
         local_diff = xml_create_patchset(0, patchset_cib, scratch,
                                          config_changed, manage_counters);
     }
 
     pcmk__log_xml_changes(LOG_TRACE, scratch);
     xml_accept_changes(scratch);
 
     if(local_diff) {
         patchset_process_digest(local_diff, patchset_cib, scratch, with_digest);
         pcmk__log_xml_patchset(LOG_INFO, local_diff);
         crm_log_xml_trace(local_diff, "raw patch");
     }
 
     if (make_copy && (local_diff != NULL)) {
         // Original to compare against doesn't exist
         pcmk__if_tracing(
             {
                 // Validate the calculated patch set
                 int test_rc = pcmk_ok;
                 int format = 1;
                 xmlNode *cib_copy = pcmk__xml_copy(NULL, patchset_cib);
 
                 crm_element_value_int(local_diff, PCMK_XA_FORMAT, &format);
                 test_rc = xml_apply_patchset(cib_copy, local_diff,
                                              manage_counters);
 
                 if (test_rc != pcmk_ok) {
                     save_xml_to_file(cib_copy, "PatchApply:calculated", NULL);
                     save_xml_to_file(patchset_cib, "PatchApply:input", NULL);
                     save_xml_to_file(scratch, "PatchApply:actual", NULL);
                     save_xml_to_file(local_diff, "PatchApply:diff", NULL);
                     crm_err("v%d patchset error, patch failed to apply: %s "
                             "(%d)",
                             format, pcmk_rc_str(pcmk_legacy2rc(test_rc)),
                             test_rc);
                 }
                 free_xml(cib_copy);
             },
             {}
         );
     }
 
     if (pcmk__str_eq(section, PCMK_XE_STATUS, pcmk__str_casei)) {
         /* Throttle the amount of costly validation we perform due to status updates
          * a) we don't really care whats in the status section
          * b) we don't validate any of its contents at the moment anyway
          */
         check_schema = false;
     }
 
     /* === scratch must not be modified after this point ===
      * Exceptions, anything in:
 
      static filter_t filter[] = {
      { 0, PCMK_XA_CRM_DEBUG_ORIGIN },
      { 0, PCMK_XA_CIB_LAST_WRITTEN },
      { 0, PCMK_XA_UPDATE_ORIGIN },
      { 0, PCMK_XA_UPDATE_CLIENT },
      { 0, PCMK_XA_UPDATE_USER },
      };
      */
 
     if (*config_changed && !pcmk_is_set(call_options, cib_no_mtime)) {
         const char *schema = crm_element_value(scratch, PCMK_XA_VALIDATE_WITH);
 
         pcmk__xe_add_last_written(scratch);
 
         /* Make values of origin, client, and user in scratch match
          * the ones in req (if the schema allows the attributes)
          */
         if (pcmk__cmp_schemas_by_name(schema, "pacemaker-1.2") >= 0) {
             const char *origin = crm_element_value(req, PCMK__XA_SRC);
             const char *client = crm_element_value(req,
                                                    PCMK__XA_CIB_CLIENTNAME);
 
             if (origin != NULL) {
                 crm_xml_add(scratch, PCMK_XA_UPDATE_ORIGIN, origin);
             } else {
                 pcmk__xe_remove_attr(scratch, PCMK_XA_UPDATE_ORIGIN);
             }
 
             if (client != NULL) {
                 crm_xml_add(scratch, PCMK_XA_UPDATE_CLIENT, user);
             } else {
                 pcmk__xe_remove_attr(scratch, PCMK_XA_UPDATE_CLIENT);
             }
 
             if (user != NULL) {
                 crm_xml_add(scratch, PCMK_XA_UPDATE_USER, user);
             } else {
                 pcmk__xe_remove_attr(scratch, PCMK_XA_UPDATE_USER);
             }
         }
     }
 
     crm_trace("Perform validation: %s", pcmk__btoa(check_schema));
-    if ((rc == pcmk_ok) && check_schema && !validate_xml(scratch, NULL, true)) {
+    if ((rc == pcmk_ok) && check_schema
+        && !pcmk__configured_schema_validates(scratch)) {
         const char *current_schema = crm_element_value(scratch,
                                                        PCMK_XA_VALIDATE_WITH);
 
         crm_warn("Updated CIB does not validate against %s schema",
                  pcmk__s(current_schema, "unspecified"));
         rc = -pcmk_err_schema_validation;
     }
 
   done:
 
     *result_cib = scratch;
 
     /* @TODO: This may not work correctly with !make_copy, since we don't
      * keep the original CIB.
      */
     if ((rc != pcmk_ok) && cib_acl_enabled(patchset_cib, user)
         && xml_acl_filtered_copy(user, patchset_cib, scratch, result_cib)) {
 
         if (*result_cib == NULL) {
             crm_debug("Pre-filtered the entire cib result");
         }
         free_xml(scratch);
     }
 
     if(diff) {
         *diff = local_diff;
     } else {
         free_xml(local_diff);
     }
 
     free_xml(top);
     crm_trace("Done");
     return rc;
 }
 
 int
 cib__create_op(cib_t *cib, const char *op, const char *host,
                const char *section, xmlNode *data, int call_options,
                const char *user_name, const char *client_name,
                xmlNode **op_msg)
 {
     CRM_CHECK((cib != NULL) && (op_msg != NULL), return -EPROTO);
 
     *op_msg = pcmk__xe_create(NULL, PCMK__XE_CIB_COMMAND);
 
     cib->call_id++;
     if (cib->call_id < 1) {
         cib->call_id = 1;
     }
 
     crm_xml_add(*op_msg, PCMK__XA_T, PCMK__VALUE_CIB);
     crm_xml_add(*op_msg, PCMK__XA_CIB_OP, op);
     crm_xml_add(*op_msg, PCMK__XA_CIB_HOST, host);
     crm_xml_add(*op_msg, PCMK__XA_CIB_SECTION, section);
     crm_xml_add(*op_msg, PCMK__XA_CIB_USER, user_name);
     crm_xml_add(*op_msg, PCMK__XA_CIB_CLIENTNAME, client_name);
     crm_xml_add_int(*op_msg, PCMK__XA_CIB_CALLID, cib->call_id);
 
     crm_trace("Sending call options: %.8lx, %d", (long)call_options, call_options);
     crm_xml_add_int(*op_msg, PCMK__XA_CIB_CALLOPT, call_options);
 
     if (data != NULL) {
         xmlNode *wrapper = pcmk__xe_create(*op_msg, PCMK__XE_CIB_CALLDATA);
 
         pcmk__xml_copy(wrapper, data);
     }
 
     if (pcmk_is_set(call_options, cib_inhibit_bcast)) {
         CRM_CHECK(pcmk_is_set(call_options, cib_scope_local),
                   free_xml(*op_msg); return -EPROTO);
     }
     return pcmk_ok;
 }
 
 /*!
  * \internal
  * \brief Check whether a CIB request is supported in a transaction
  *
  * \param[in] request  CIB request
  *
  * \return Standard Pacemaker return code
  */
 static int
 validate_transaction_request(const xmlNode *request)
 {
     const char *op = crm_element_value(request, PCMK__XA_CIB_OP);
     const char *host = crm_element_value(request, PCMK__XA_CIB_HOST);
     const cib__operation_t *operation = NULL;
     int rc = cib__get_operation(op, &operation);
 
     if (rc != pcmk_rc_ok) {
         // cib__get_operation() logs error
         return rc;
     }
 
     if (!pcmk_is_set(operation->flags, cib__op_attr_transaction)) {
         crm_err("Operation %s is not supported in CIB transactions", op);
         return EOPNOTSUPP;
     }
 
     if (host != NULL) {
         crm_err("Operation targeting a specific node (%s) is not supported in "
                 "a CIB transaction",
                 host);
         return EOPNOTSUPP;
     }
     return pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Append a CIB request to a CIB transaction
  *
  * \param[in,out] cib      CIB client whose transaction to extend
  * \param[in,out] request  Request to add to transaction
  *
  * \return Legacy Pacemaker return code
  */
 int
 cib__extend_transaction(cib_t *cib, xmlNode *request)
 {
     int rc = pcmk_rc_ok;
 
     CRM_ASSERT((cib != NULL) && (request != NULL));
 
     rc = validate_transaction_request(request);
 
     if ((rc == pcmk_rc_ok) && (cib->transaction == NULL)) {
         rc = pcmk_rc_no_transaction;
     }
 
     if (rc == pcmk_rc_ok) {
         pcmk__xml_copy(cib->transaction, request);
 
     } else {
         const char *op = crm_element_value(request, PCMK__XA_CIB_OP);
         const char *client_id = NULL;
 
         cib->cmds->client_id(cib, NULL, &client_id);
         crm_err("Failed to add '%s' operation to transaction for client %s: %s",
                 op, pcmk__s(client_id, "(unidentified)"), pcmk_rc_str(rc));
         crm_log_xml_info(request, "failed");
     }
     return pcmk_rc2legacy(rc);
 }
 
 void
 cib_native_callback(cib_t * cib, xmlNode * msg, int call_id, int rc)
 {
     xmlNode *output = NULL;
     cib_callback_client_t *blob = NULL;
 
     if (msg != NULL) {
         xmlNode *wrapper = NULL;
 
         crm_element_value_int(msg, PCMK__XA_CIB_RC, &rc);
         crm_element_value_int(msg, PCMK__XA_CIB_CALLID, &call_id);
         wrapper = pcmk__xe_first_child(msg, PCMK__XE_CIB_CALLDATA, NULL, NULL);
         output = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
     }
 
     blob = cib__lookup_id(call_id);
 
     if (blob == NULL) {
         crm_trace("No callback found for call %d", call_id);
     }
 
     if (cib == NULL) {
         crm_debug("No cib object supplied");
     }
 
     if (rc == -pcmk_err_diff_resync) {
         /* This is an internal value that clients do not and should not care about */
         rc = pcmk_ok;
     }
 
     if (blob && blob->callback && (rc == pcmk_ok || blob->only_success == FALSE)) {
         crm_trace("Invoking callback %s for call %d",
                   pcmk__s(blob->id, "without ID"), call_id);
         blob->callback(msg, call_id, rc, output, blob->user_data);
 
     } else if (cib && cib->op_callback == NULL && rc != pcmk_ok) {
         crm_warn("CIB command failed: %s", pcmk_strerror(rc));
         crm_log_xml_debug(msg, "Failed CIB Update");
     }
 
     /* This may free user_data, so do it after the callback */
     if (blob) {
         remove_cib_op_callback(call_id, FALSE);
     }
 
     if (cib && cib->op_callback != NULL) {
         crm_trace("Invoking global callback for call %d", call_id);
         cib->op_callback(msg, call_id, rc, output);
     }
     crm_trace("OP callback activated for %d", call_id);
 }
 
 void
 cib_native_notify(gpointer data, gpointer user_data)
 {
     xmlNode *msg = user_data;
     cib_notify_client_t *entry = data;
     const char *event = NULL;
 
     if (msg == NULL) {
         crm_warn("Skipping callback - NULL message");
         return;
     }
 
     event = crm_element_value(msg, PCMK__XA_SUBT);
 
     if (entry == NULL) {
         crm_warn("Skipping callback - NULL callback client");
         return;
 
     } else if (entry->callback == NULL) {
         crm_warn("Skipping callback - NULL callback");
         return;
 
     } else if (!pcmk__str_eq(entry->event, event, pcmk__str_casei)) {
         crm_trace("Skipping callback - event mismatch %p/%s vs. %s", entry, entry->event, event);
         return;
     }
 
     crm_trace("Invoking callback for %p/%s event...", entry, event);
     entry->callback(event, msg);
     crm_trace("Callback invoked...");
 }
 
 gboolean
 cib_read_config(GHashTable * options, xmlNode * current_cib)
 {
     xmlNode *config = NULL;
     crm_time_t *now = NULL;
 
     if (options == NULL || current_cib == NULL) {
         return FALSE;
     }
 
     now = crm_time_new(NULL);
 
     g_hash_table_remove_all(options);
 
     config = pcmk_find_cib_element(current_cib, PCMK_XE_CRM_CONFIG);
     if (config) {
         pe_unpack_nvpairs(current_cib, config, PCMK_XE_CLUSTER_PROPERTY_SET,
                           NULL, options, PCMK_VALUE_CIB_BOOTSTRAP_OPTIONS, TRUE,
                           now, NULL);
     }
 
     pcmk__validate_cluster_options(options);
 
     crm_time_free(now);
 
     return TRUE;
 }
 
 int
 cib_internal_op(cib_t * cib, const char *op, const char *host,
                 const char *section, xmlNode * data,
                 xmlNode ** output_data, int call_options, const char *user_name)
 {
     int (*delegate) (cib_t * cib, const char *op, const char *host,
                      const char *section, xmlNode * data,
                      xmlNode ** output_data, int call_options, const char *user_name) =
         cib->delegate_fn;
 
     if(user_name == NULL) {
         user_name = getenv("CIB_user");
     }
 
     return delegate(cib, op, host, section, data, output_data, call_options, user_name);
 }
 
 /*!
  * \brief Apply a CIB update patch to a given CIB
  *
  * \param[in]  event   CIB update patch
  * \param[in]  input   CIB to patch
  * \param[out] output  Resulting CIB after patch
  * \param[in]  level   Log the patch at this log level (unless LOG_CRIT)
  *
  * \return Legacy Pacemaker return code
  * \note sbd calls this function
  */
 int
 cib_apply_patch_event(xmlNode *event, xmlNode *input, xmlNode **output,
                       int level)
 {
     int rc = pcmk_err_generic;
 
     xmlNode *wrapper = NULL;
     xmlNode *diff = NULL;
 
     CRM_ASSERT(event);
     CRM_ASSERT(input);
     CRM_ASSERT(output);
 
     crm_element_value_int(event, PCMK__XA_CIB_RC, &rc);
     wrapper = pcmk__xe_first_child(event, PCMK__XE_CIB_UPDATE_RESULT, NULL,
                                    NULL);
     diff = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
 
     if (rc < pcmk_ok || diff == NULL) {
         return rc;
     }
 
     if (level > LOG_CRIT) {
         pcmk__log_xml_patchset(level, diff);
     }
 
     if (input != NULL) {
         rc = cib_process_diff(NULL, cib_none, NULL, event, diff, input, output,
                               NULL);
 
         if (rc != pcmk_ok) {
             crm_debug("Update didn't apply: %s (%d) %p",
                       pcmk_strerror(rc), rc, *output);
 
             if (rc == -pcmk_err_old_data) {
                 crm_trace("Masking error, we already have the supplied update");
                 return pcmk_ok;
             }
             free_xml(*output);
             *output = NULL;
             return rc;
         }
     }
     return rc;
 }
 
 #define log_signon_query_err(out, fmt, args...) do {    \
         if (out != NULL) {                              \
             out->err(out, fmt, ##args);                 \
         } else {                                        \
             crm_err(fmt, ##args);                       \
         }                                               \
     } while (0)
 
 int
 cib__signon_query(pcmk__output_t *out, cib_t **cib, xmlNode **cib_object)
 {
     int rc = pcmk_rc_ok;
     cib_t *cib_conn = NULL;
 
     CRM_ASSERT(cib_object != NULL);
 
     if (cib == NULL) {
         cib_conn = cib_new();
     } else {
         if (*cib == NULL) {
             *cib = cib_new();
         }
         cib_conn = *cib;
     }
 
     if (cib_conn == NULL) {
         return ENOMEM;
     }
 
     if (cib_conn->state == cib_disconnected) {
         rc = cib_conn->cmds->signon(cib_conn, crm_system_name, cib_command);
         rc = pcmk_legacy2rc(rc);
     }
 
     if (rc != pcmk_rc_ok) {
         log_signon_query_err(out, "Could not connect to the CIB: %s",
                              pcmk_rc_str(rc));
         goto done;
     }
 
     if (out != NULL) {
         out->transient(out, "Querying CIB...");
     }
     rc = cib_conn->cmds->query(cib_conn, NULL, cib_object,
                                cib_scope_local|cib_sync_call);
     rc = pcmk_legacy2rc(rc);
 
     if (rc != pcmk_rc_ok) {
         log_signon_query_err(out, "CIB query failed: %s", pcmk_rc_str(rc));
     }
 
 done:
     if (cib == NULL) {
         cib__clean_up_connection(&cib_conn);
     }
 
     if ((rc == pcmk_rc_ok) && (*cib_object == NULL)) {
         return pcmk_rc_no_input;
     }
     return rc;
 }
 
 int
 cib__clean_up_connection(cib_t **cib)
 {
     int rc;
 
     if (*cib == NULL) {
         return pcmk_rc_ok;
     }
 
     rc = (*cib)->cmds->signoff(*cib);
     cib_delete(*cib);
     *cib = NULL;
     return pcmk_legacy2rc(rc);
 }
 
 // Deprecated functions kept only for backward API compatibility
 // LCOV_EXCL_START
 
 #include <crm/cib/util_compat.h>
 
 xmlNode *
 cib_get_generation(cib_t * cib)
 {
     xmlNode *the_cib = NULL;
     xmlNode *generation = pcmk__xe_create(NULL, PCMK__XE_GENERATION_TUPLE);
 
     cib->cmds->query(cib, NULL, &the_cib, cib_scope_local | cib_sync_call);
     if (the_cib != NULL) {
         copy_in_properties(generation, the_cib);
         free_xml(the_cib);
     }
 
     return generation;
 }
 
 const char *
 get_object_path(const char *object_type)
 {
     return pcmk_cib_xpath_for(object_type);
 }
 
 const char *
 get_object_parent(const char *object_type)
 {
     return pcmk_cib_parent_name_for(object_type);
 }
 
 xmlNode *
 get_object_root(const char *object_type, xmlNode *the_root)
 {
     return pcmk_find_cib_element(the_root, object_type);
 }
 
 const char *
 cib_pref(GHashTable * options, const char *name)
 {
     return pcmk__cluster_option(options, name);
 }
 
 void
 cib_metadata(void)
 {
     pcmk__output_t *out = NULL;
     int rc = pcmk__output_new(&out, "text", NULL, NULL);
 
     if (rc != pcmk_rc_ok) {
         crm_err("Unable to output metadata: %s", pcmk_rc_str(rc));
         return;
     }
 
     pcmk__daemon_metadata(out, "pacemaker-based",
                           "Cluster Information Base manager options",
                           "Cluster options used by Pacemaker's Cluster "
                           "Information Base manager",
                           pcmk__opt_based);
 
     out->finish(out, CRM_EX_OK, true, NULL);
     pcmk__output_free(out);
 }
 
 // LCOV_EXCL_STOP
 // End deprecated API
diff --git a/lib/common/schemas.c b/lib/common/schemas.c
index 27d584ed33..795acaf8e1 100644
--- a/lib/common/schemas.c
+++ b/lib/common/schemas.c
@@ -1,1661 +1,1677 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <stdio.h>
 #include <string.h>
 #include <dirent.h>
 #include <errno.h>
 #include <sys/stat.h>
 #include <stdarg.h>
 
 #include <libxml/relaxng.h>
 #include <libxslt/xslt.h>
 #include <libxslt/transform.h>
 #include <libxslt/security.h>
 #include <libxslt/xsltutils.h>
 
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>  /* PCMK__XML_LOG_BASE */
 
 #include "crmcommon_private.h"
 
 #define SCHEMA_ZERO { .v = { 0, 0 } }
 
 #define schema_strdup_printf(prefix, version, suffix) \
     crm_strdup_printf(prefix "%u.%u" suffix, (version).v[0], (version).v[1])
 
 typedef struct {
     xmlRelaxNGPtr rng;
     xmlRelaxNGValidCtxtPtr valid;
     xmlRelaxNGParserCtxtPtr parser;
 } relaxng_ctx_cache_t;
 
 static GList *known_schemas = NULL;
 static bool silent_logging = FALSE;
 
 static void G_GNUC_PRINTF(2, 3)
 xml_log(int priority, const char *fmt, ...)
 {
     va_list ap;
 
     va_start(ap, fmt);
     if (silent_logging == FALSE) {
         /* XXX should not this enable dechunking as well? */
         PCMK__XML_LOG_BASE(priority, FALSE, 0, NULL, fmt, ap);
     }
     va_end(ap);
 }
 
 static int
 xml_latest_schema_index(void)
 {
     /* This function assumes that crm_schema_init() has been called beforehand,
      * so we have at least three schemas (one real schema, the "pacemaker-next"
      * schema, and the "none" schema).
      *
      * @COMPAT: pacemaker-next is deprecated since 2.1.5.
      * Update this when we drop that schema.
      */
     return g_list_length(known_schemas) - 3;
 }
 
 /*!
  * \internal
  * \brief Return the schema entry of the highest-versioned schema
  *
  * \return Schema entry of highest-versioned schema (or NULL on error)
  */
 static GList *
 get_highest_schema(void)
 {
     /* The highest numerically versioned schema is the one before pacemaker-next
      *
      * @COMPAT pacemaker-next is deprecated since 2.1.5
      */
     GList *entry = pcmk__get_schema("pacemaker-next");
 
     CRM_ASSERT((entry != NULL) && (entry->prev != NULL));
     return entry->prev;
 }
 
 /*!
  * \internal
  * \brief Return the name of the highest-versioned schema
  *
  * \return Name of highest-versioned schema (or NULL on error)
  */
 const char *
 pcmk__highest_schema_name(void)
 {
     GList *entry = get_highest_schema();
 
     return ((pcmk__schema_t *)(entry->data))->name;
 }
 
 /*!
  * \internal
  * \brief Find first entry of highest major schema version series
  *
  * \return Schema entry of first schema with highest major version
  */
 GList *
 pcmk__find_x_0_schema(void)
 {
 #if defined(PCMK__UNIT_TESTING)
     /* If we're unit testing, this can't be static because it'll stick
      * around from one test run to the next. It needs to be cleared out
      * every time.
      */
     GList *x_0_entry = NULL;
 #else
     static GList *x_0_entry = NULL;
 #endif
 
     pcmk__schema_t *highest_schema = NULL;
 
     if (x_0_entry != NULL) {
         return x_0_entry;
     }
     x_0_entry = get_highest_schema();
     highest_schema = x_0_entry->data;
 
     for (GList *iter = x_0_entry->prev; iter != NULL; iter = iter->prev) {
         pcmk__schema_t *schema = iter->data;
 
         /* We've found a schema in an older major version series.  Return
          * the index of the first one in the same major version series as
          * the highest schema.
          */
         if (schema->version.v[0] < highest_schema->version.v[0]) {
             x_0_entry = iter->next;
             break;
         }
 
         /* We're out of list to examine.  This probably means there was only
          * one major version series, so return the first schema entry.
          */
         if (iter->prev == NULL) {
             x_0_entry = known_schemas->data;
             break;
         }
     }
     return x_0_entry;
 }
 
 static inline bool
 version_from_filename(const char *filename, pcmk__schema_version_t *version)
 {
     if (pcmk__ends_with(filename, ".rng")) {
         return sscanf(filename, "pacemaker-%hhu.%hhu.rng", &(version->v[0]), &(version->v[1])) == 2;
     } else {
         return sscanf(filename, "pacemaker-%hhu.%hhu", &(version->v[0]), &(version->v[1])) == 2;
     }
 }
 
 static int
 schema_filter(const struct dirent *a)
 {
     int rc = 0;
     pcmk__schema_version_t version = SCHEMA_ZERO;
 
     if (strstr(a->d_name, "pacemaker-") != a->d_name) {
         /* crm_trace("%s - wrong prefix", a->d_name); */
 
     } else if (!pcmk__ends_with_ext(a->d_name, ".rng")) {
         /* crm_trace("%s - wrong suffix", a->d_name); */
 
     } else if (!version_from_filename(a->d_name, &version)) {
         /* crm_trace("%s - wrong format", a->d_name); */
 
     } else {
         /* crm_debug("%s - candidate", a->d_name); */
         rc = 1;
     }
 
     return rc;
 }
 
 static int
 schema_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;
 
     if (!version_from_filename(a[0]->d_name, &a_version)
         || !version_from_filename(b[0]->d_name, &b_version)) {
         // Shouldn't be possible, but makes static analysis happy
         return 0;
     }
 
     return schema_cmp(a_version, b_version);
 }
 
 /*!
  * \internal
  * \brief Add given schema + auxiliary data to internal bookkeeping.
  *
  * \note When providing \p version, should not be called directly but
  *       through \c add_schema_by_version.
  */
 static void
 add_schema(enum pcmk__schema_validator validator, const pcmk__schema_version_t *version,
            const char *name, const char *transform,
            const char *transform_enter, bool transform_onleave)
 {
     pcmk__schema_t *schema = NULL;
 
     schema = pcmk__assert_alloc(1, sizeof(pcmk__schema_t));
 
     schema->validator = validator;
     schema->version.v[0] = version->v[0];
     schema->version.v[1] = version->v[1];
     schema->transform_onleave = transform_onleave;
     // schema->schema_index is set after all schemas are loaded and sorted
 
     if (version->v[0] || version->v[1]) {
         schema->name = schema_strdup_printf("pacemaker-", *version, "");
     } else {
         schema->name = pcmk__str_copy(name);
     }
 
     if (transform) {
         schema->transform = pcmk__str_copy(transform);
     }
 
     if (transform_enter) {
         schema->transform_enter = pcmk__str_copy(transform_enter);
     }
 
     known_schemas = g_list_prepend(known_schemas, schema);
 }
 
 /*!
  * \internal
  * \brief Add version-specified schema + auxiliary data to internal bookkeeping.
  * \return Standard Pacemaker return value (the only possible values are
  * \c ENOENT when no upgrade schema is associated, or \c pcmk_rc_ok otherwise.
  *
  * \note There's no reliance on the particular order of schemas entering here.
  *
  * \par A bit of theory
  * We track 3 XSLT stylesheets that differ per usage:
  * - "upgrade":
  *   . sparsely spread over the sequence of all available schemas,
  *     as they are only relevant when major version of the schema
  *     is getting bumped -- in that case, it MUST be set
  *   . name convention:  upgrade-X.Y.xsl
  * - "upgrade-enter":
  *   . may only accompany "upgrade" occurrence, but doesn't need to
  *     be present anytime such one is, i.e., it MAY not be set when
  *     "upgrade" is
  *   . name convention:  upgrade-X.Y-enter.xsl,
  *     when not present: upgrade-enter.xsl
  * - "upgrade-leave":
  *   . like "upgrade-enter", but SHOULD be present whenever
  *     "upgrade-enter" is (and vice versa, but that's only
  *     to prevent confusion based on observing the files,
  *     it would get ignored regardless)
  *   . name convention:  (see "upgrade-enter")
  */
 static int
 add_schema_by_version(const pcmk__schema_version_t *version, bool transform_expected)
 {
     bool transform_onleave = FALSE;
     int rc = pcmk_rc_ok;
     struct stat s;
     char *xslt = NULL,
          *transform_upgrade = NULL,
          *transform_enter = NULL;
 
     /* prologue for further transform_expected handling */
     if (transform_expected) {
         /* check if there's suitable "upgrade" stylesheet */
         transform_upgrade = schema_strdup_printf("upgrade-", *version, );
         xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
                                        transform_upgrade);
     }
 
     if (!transform_expected) {
         /* jump directly to the end */
 
     } else if (stat(xslt, &s) == 0) {
         /* perhaps there's also a targeted "upgrade-enter" stylesheet */
         transform_enter = schema_strdup_printf("upgrade-", *version, "-enter");
         free(xslt);
         xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
                                        transform_enter);
         if (stat(xslt, &s) != 0) {
             /* or initially, at least a generic one */
             crm_debug("Upgrade-enter transform %s.xsl not found", xslt);
             free(xslt);
             free(transform_enter);
             transform_enter = strdup("upgrade-enter");
             xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
                                            transform_enter);
             if (stat(xslt, &s) != 0) {
                 crm_debug("Upgrade-enter transform %s.xsl not found, either", xslt);
                 free(xslt);
                 xslt = NULL;
             }
         }
         /* xslt contains full path to "upgrade-enter" stylesheet */
         if (xslt != NULL) {
             /* then there should be "upgrade-leave" counterpart (enter->leave) */
             memcpy(strrchr(xslt, '-') + 1, "leave", sizeof("leave") - 1);
             transform_onleave = (stat(xslt, &s) == 0);
             free(xslt);
         } else {
             free(transform_enter);
             transform_enter = NULL;
         }
 
     } else {
         crm_err("Upgrade transform %s not found", xslt);
         free(xslt);
         free(transform_upgrade);
         transform_upgrade = NULL;
         rc = ENOENT;
     }
 
     add_schema(pcmk__schema_validator_rng, version, NULL,
                transform_upgrade, transform_enter, transform_onleave);
 
     free(transform_upgrade);
     free(transform_enter);
 
     return rc;
 }
 
 static void
 wrap_libxslt(bool finalize)
 {
     static xsltSecurityPrefsPtr secprefs;
     int ret = 0;
 
     /* security framework preferences */
     if (!finalize) {
         CRM_ASSERT(secprefs == NULL);
         secprefs = xsltNewSecurityPrefs();
         ret = xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_WRITE_FILE,
                                    xsltSecurityForbid)
               | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_CREATE_DIRECTORY,
                                      xsltSecurityForbid)
               | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_READ_NETWORK,
                                      xsltSecurityForbid)
               | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_WRITE_NETWORK,
                                      xsltSecurityForbid);
         if (ret != 0) {
             return;
         }
     } else {
         xsltFreeSecurityPrefs(secprefs);
         secprefs = NULL;
     }
 
     /* cleanup only */
     if (finalize) {
         xsltCleanupGlobals();
     }
 }
 
 void
 pcmk__load_schemas_from_dir(const char *dir)
 {
     int lpc, max;
     struct dirent **namelist = NULL;
 
     max = scandir(dir, &namelist, schema_filter, schema_cmp_directory);
     if (max < 0) {
         crm_warn("Could not load schemas from %s: %s", dir, strerror(errno));
         return;
     }
 
     for (lpc = 0; lpc < max; lpc++) {
         bool transform_expected = false;
         pcmk__schema_version_t version = SCHEMA_ZERO;
 
         if (!version_from_filename(namelist[lpc]->d_name, &version)) {
             // Shouldn't be possible, but makes static analysis happy
             crm_warn("Skipping schema '%s': could not parse version",
                      namelist[lpc]->d_name);
             continue;
         }
         if ((lpc + 1) < max) {
             pcmk__schema_version_t next_version = SCHEMA_ZERO;
 
             if (version_from_filename(namelist[lpc+1]->d_name, &next_version)
                     && (version.v[0] < next_version.v[0])) {
                 transform_expected = true;
             }
         }
 
         if (add_schema_by_version(&version, transform_expected) != pcmk_rc_ok) {
             break;
         }
     }
 
     for (lpc = 0; lpc < max; lpc++) {
         free(namelist[lpc]);
     }
 
     free(namelist);
 }
 
 static gint
 schema_sort_GCompareFunc(gconstpointer a, gconstpointer b)
 {
     const pcmk__schema_t *schema_a = a;
     const pcmk__schema_t *schema_b = b;
 
     if (pcmk__str_eq(schema_a->name, "pacemaker-next", pcmk__str_none)) {
         if (pcmk__str_eq(schema_b->name, PCMK_VALUE_NONE, pcmk__str_none)) {
             return -1;
         } else {
             return 1;
         }
     } else if (pcmk__str_eq(schema_a->name, PCMK_VALUE_NONE, pcmk__str_none)) {
         return 1;
     } else if (pcmk__str_eq(schema_b->name, "pacemaker-next", pcmk__str_none)) {
         return -1;
     } else {
         return schema_cmp(schema_a->version, schema_b->version);
     }
 }
 
 /*!
  * \internal
  * \brief Sort the list of known schemas such that all pacemaker-X.Y are in
  *        version order, then pacemaker-next, then none
  *
  * This function should be called whenever additional schemas are loaded using
  * pcmk__load_schemas_from_dir(), after the initial sets in crm_schema_init().
  */
 void
 pcmk__sort_schemas(void)
 {
     known_schemas = g_list_sort(known_schemas, schema_sort_GCompareFunc);
 }
 
 /*!
  * \internal
  * \brief Load pacemaker schemas into cache
  *
  * \note This currently also serves as an entry point for the
  *       generic initialization of the libxslt library.
  */
 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;
     int schema_index = 0;
 
     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",
                NULL, NULL, FALSE);
 
     add_schema(pcmk__schema_validator_none, &zero, PCMK_VALUE_NONE,
                NULL, NULL, FALSE);
 
     /* add_schema() prepends items to the list, so in the simple case, this just
      * reverses the list. However if there were any remote schemas, sorting is
      * necessary.
      */
     pcmk__sort_schemas();
 
     // Now set the schema indexes and log the final result
     for (GList *iter = known_schemas; iter != NULL; iter = iter->next) {
         pcmk__schema_t *schema = iter->data;
 
         if (schema->transform == NULL) {
             crm_debug("Loaded schema %d: %s", schema_index, schema->name);
         } else {
             crm_debug("Loaded schema %d: %s (upgrades with %s.xsl)",
                       schema_index, schema->name, schema->transform);
         }
         schema->schema_index = schema_index++;
     }
 }
 
 static bool
 validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler,
                       void *error_handler_context, const char *relaxng_file,
                       relaxng_ctx_cache_t **cached_ctx)
 {
     int rc = 0;
     bool valid = true;
     relaxng_ctx_cache_t *ctx = NULL;
 
     CRM_CHECK(doc != NULL, return false);
     CRM_CHECK(relaxng_file != NULL, return false);
 
     if (cached_ctx && *cached_ctx) {
         ctx = *cached_ctx;
 
     } else {
         crm_debug("Creating RNG parser context");
         ctx = pcmk__assert_alloc(1, sizeof(relaxng_ctx_cache_t));
 
         ctx->parser = xmlRelaxNGNewParserCtxt(relaxng_file);
         CRM_CHECK(ctx->parser != NULL, goto cleanup);
 
         if (error_handler) {
             xmlRelaxNGSetParserErrors(ctx->parser,
                                       (xmlRelaxNGValidityErrorFunc) error_handler,
                                       (xmlRelaxNGValidityWarningFunc) error_handler,
                                       error_handler_context);
         } else {
             xmlRelaxNGSetParserErrors(ctx->parser,
                                       (xmlRelaxNGValidityErrorFunc) fprintf,
                                       (xmlRelaxNGValidityWarningFunc) fprintf,
                                       stderr);
         }
 
         ctx->rng = xmlRelaxNGParse(ctx->parser);
         CRM_CHECK(ctx->rng != NULL,
                   crm_err("Could not find/parse %s", relaxng_file);
                   goto cleanup);
 
         ctx->valid = xmlRelaxNGNewValidCtxt(ctx->rng);
         CRM_CHECK(ctx->valid != NULL, goto cleanup);
 
         if (error_handler) {
             xmlRelaxNGSetValidErrors(ctx->valid,
                                      (xmlRelaxNGValidityErrorFunc) error_handler,
                                      (xmlRelaxNGValidityWarningFunc) error_handler,
                                      error_handler_context);
         } else {
             xmlRelaxNGSetValidErrors(ctx->valid,
                                      (xmlRelaxNGValidityErrorFunc) fprintf,
                                      (xmlRelaxNGValidityWarningFunc) fprintf,
                                      stderr);
         }
     }
 
     rc = xmlRelaxNGValidateDoc(ctx->valid, doc);
     if (rc > 0) {
         valid = false;
 
     } else if (rc < 0) {
         crm_err("Internal libxml error during validation");
     }
 
   cleanup:
 
     if (cached_ctx) {
         *cached_ctx = ctx;
 
     } else {
         if (ctx->parser != NULL) {
             xmlRelaxNGFreeParserCtxt(ctx->parser);
         }
         if (ctx->valid != NULL) {
             xmlRelaxNGFreeValidCtxt(ctx->valid);
         }
         if (ctx->rng != NULL) {
             xmlRelaxNGFree(ctx->rng);
         }
         free(ctx);
     }
 
     return valid;
 }
 
 static void
 free_schema(gpointer data)
 {
     pcmk__schema_t *schema = data;
     relaxng_ctx_cache_t *ctx = NULL;
 
     switch (schema->validator) {
         case pcmk__schema_validator_none: // not cached
             break;
 
         case pcmk__schema_validator_rng: // cached
             ctx = (relaxng_ctx_cache_t *) schema->cache;
             if (ctx == NULL) {
                 break;
             }
 
             if (ctx->parser != NULL) {
                 xmlRelaxNGFreeParserCtxt(ctx->parser);
             }
 
             if (ctx->valid != NULL) {
                 xmlRelaxNGFreeValidCtxt(ctx->valid);
             }
 
             if (ctx->rng != NULL) {
                 xmlRelaxNGFree(ctx->rng);
             }
 
             free(ctx);
             schema->cache = NULL;
             break;
     }
 
     free(schema->name);
     free(schema->transform);
     free(schema->transform_enter);
 }
 
 /*!
  * \internal
  * \brief Clean up global memory associated with XML schemas
  */
 void
 crm_schema_cleanup(void)
 {
     g_list_free_full(known_schemas, free_schema);
     known_schemas = NULL;
 
     wrap_libxslt(true);
 }
 
 /*!
  * \internal
  * \brief Get schema list entry corresponding to a schema name
  *
  * \param[in] name  Name of schema to get
  *
  * \return Schema list entry corresponding to \p name, or NULL if unknown
  */
 GList *
 pcmk__get_schema(const char *name)
 {
     if (name == NULL) {
         name = PCMK_VALUE_NONE;
     }
     for (GList *iter = known_schemas; iter != NULL; iter = iter->next) {
         pcmk__schema_t *schema = iter->data;
 
         if (pcmk__str_eq(name, schema->name, pcmk__str_casei)) {
             return iter;
         }
     }
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Compare two schema version numbers given the schema names
  *
  * \param[in] schema1  Name of first schema to compare
  * \param[in] schema2  Name of second schema to compare
  *
  * \return Standard comparison result (negative integer if \p schema1 has the
  *         lower version number, positive integer if \p schema1 has the higher
  *         version number, of 0 if the version numbers are equal)
  */
 int
 pcmk__cmp_schemas_by_name(const char *schema1_name, const char *schema2_name)
 {
     GList *entry1 = pcmk__get_schema(schema1_name);
     GList *entry2 = pcmk__get_schema(schema2_name);
 
     if (entry1 == NULL) {
         return (entry2 == NULL)? 0 : -1;
 
     } else if (entry2 == NULL) {
         return 1;
 
     } else {
         pcmk__schema_t *schema1 = entry1->data;
         pcmk__schema_t *schema2 = entry2->data;
 
         return schema1->schema_index - schema2->schema_index;
     }
 }
 
 static bool
 validate_with(xmlNode *xml, pcmk__schema_t *schema,
               xmlRelaxNGValidityErrorFunc error_handler,
               void *error_handler_context)
 {
     bool valid = false;
     char *file = NULL;
     relaxng_ctx_cache_t **cache = NULL;
 
     if (schema == NULL) {
         return false;
     }
 
     if (schema->validator == pcmk__schema_validator_none) {
         return true;
     }
 
     if (pcmk__str_eq(schema->name, "pacemaker-next", pcmk__str_none)) {
         crm_warn("The pacemaker-next schema is deprecated and will be removed "
                  "in a future release.");
     }
 
     file = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng,
                                    schema->name);
 
     crm_trace("Validating with %s (type=%d)",
               pcmk__s(file, "missing schema"), schema->validator);
     switch (schema->validator) {
         case pcmk__schema_validator_rng:
             cache = (relaxng_ctx_cache_t **) &(schema->cache);
             valid = validate_with_relaxng(xml->doc, error_handler, error_handler_context, file, cache);
             break;
         default:
             crm_err("Unknown validator type: %d", schema->validator);
             break;
     }
 
     free(file);
     return valid;
 }
 
 static bool
 validate_with_silent(xmlNode *xml, pcmk__schema_t *schema)
 {
     bool rc, sl_backup = silent_logging;
     silent_logging = TRUE;
     rc = validate_with(xml, schema, (xmlRelaxNGValidityErrorFunc) xml_log, GUINT_TO_POINTER(LOG_ERR));
     silent_logging = sl_backup;
     return rc;
 }
 
 static void
 dump_file(const char *filename)
 {
 
     FILE *fp = NULL;
     int ch, line = 0;
 
     CRM_CHECK(filename != NULL, return);
 
     fp = fopen(filename, "r");
     if (fp == NULL) {
         crm_perror(LOG_ERR, "Could not open %s for reading", filename);
         return;
     }
 
     fprintf(stderr, "%4d ", ++line);
     do {
         ch = getc(fp);
         if (ch == EOF) {
             putc('\n', stderr);
             break;
         } else if (ch == '\n') {
             fprintf(stderr, "\n%4d ", ++line);
         } else {
             putc(ch, stderr);
         }
     } while (1);
 
     fclose(fp);
 }
 
 gboolean
 validate_xml_verbose(const xmlNode *xml_blob)
 {
     int fd = 0;
     xmlDoc *doc = NULL;
     xmlNode *xml = NULL;
     gboolean rc = FALSE;
     char *filename = NULL;
 
     filename = crm_strdup_printf("%s/cib-invalid.XXXXXX", pcmk__get_tmpdir());
 
     umask(S_IWGRP | S_IWOTH | S_IROTH);
     fd = mkstemp(filename);
     pcmk__xml_write_fd(xml_blob, filename, fd, false, NULL);
 
     dump_file(filename);
 
     doc = xmlReadFile(filename, NULL, 0);
     xml = xmlDocGetRootElement(doc);
     rc = pcmk__validate_xml(xml, NULL, NULL, NULL);
     free_xml(xml);
 
     unlink(filename);
     free(filename);
 
     return rc? TRUE : FALSE;
 }
 
-gboolean
-validate_xml(xmlNode *xml_blob, const char *validation, gboolean to_logs)
-{
-    bool rc = pcmk__validate_xml(xml_blob, validation,
-                                 to_logs? (xmlRelaxNGValidityErrorFunc) xml_log : NULL,
-                                 GUINT_TO_POINTER(LOG_ERR));
-    return rc? TRUE : FALSE;
-}
-
 bool
 pcmk__validate_xml(xmlNode *xml_blob, const char *validation,
                    xmlRelaxNGValidityErrorFunc error_handler,
                    void *error_handler_context)
 {
     GList *entry = NULL;
     pcmk__schema_t *schema = NULL;
 
     CRM_CHECK((xml_blob != NULL) && (xml_blob->doc != NULL), return false);
 
     if (validation == NULL) {
         validation = crm_element_value(xml_blob, PCMK_XA_VALIDATE_WITH);
     }
 
     if (validation == NULL) {
         bool valid = false;
 
         for (entry = known_schemas; entry != NULL; entry = entry->next) {
             schema = entry->data;
             if (validate_with(xml_blob, schema, NULL, NULL)) {
                 valid = true;
                 crm_xml_add(xml_blob, PCMK_XA_VALIDATE_WITH, schema->name);
                 crm_info("XML validated against %s", schema->name);
             }
         }
         return valid;
     }
 
     entry = pcmk__get_schema(validation);
     if (entry != NULL) {
         schema = entry->data;
         return validate_with(xml_blob, schema, error_handler,
                              error_handler_context);
     }
 
     crm_err("Unknown validator: %s", validation);
     return false;
 }
 
+/*!
+ * \internal
+ * \brief Validate XML using its configured schema (and send errors to logs)
+ *
+ * \param[in] xml  XML to validate
+ *
+ * \return true if XML validates, otherwise false
+ */
+bool
+pcmk__configured_schema_validates(xmlNode *xml)
+{
+    return pcmk__validate_xml(xml, NULL,
+                              (xmlRelaxNGValidityErrorFunc) xml_log,
+                              GUINT_TO_POINTER(LOG_ERR));
+}
+
 /* With this arrangement, an attempt to identify the message severity
    as explicitly signalled directly from XSLT is performed in rather
    a smart way (no reliance on formatting string + arguments being
    always specified as ["%s", purposeful_string], as it can also be
    ["%s: %s", some_prefix, purposeful_string] etc. so every argument
    pertaining %s specifier is investigated), and if such a mark found,
    the respective level is determined and, when the messages are to go
    to the native logs, the mark itself gets dropped
    (by the means of string shift).
 
    NOTE: whether the native logging is the right sink is decided per
          the ctx parameter -- NULL denotes this case, otherwise it
          carries a pointer to the numeric expression of the desired
          target logging level (messages with higher level will be
          suppressed)
 
    NOTE: on some architectures, this string shift may not have any
          effect, but that's an acceptable tradeoff
 
    The logging level for not explicitly designated messages
    (suspicious, likely internal errors or some runaways) is
    LOG_WARNING.
  */
 static void G_GNUC_PRINTF(2, 3)
 cib_upgrade_err(void *ctx, const char *fmt, ...)
 {
     va_list ap, aq;
     char *arg_cur;
 
     bool found = FALSE;
     const char *fmt_iter = fmt;
     uint8_t msg_log_level = LOG_WARNING;  /* default for runaway messages */
     const unsigned * log_level = (const unsigned *) ctx;
     enum {
         escan_seennothing,
         escan_seenpercent,
     } scan_state = escan_seennothing;
 
     va_start(ap, fmt);
     va_copy(aq, ap);
 
     while (!found && *fmt_iter != '\0') {
         /* while casing schema borrowed from libqb:qb_vsnprintf_serialize */
         switch (*fmt_iter++) {
         case '%':
             if (scan_state == escan_seennothing) {
                 scan_state = escan_seenpercent;
             } else if (scan_state == escan_seenpercent) {
                 scan_state = escan_seennothing;
             }
             break;
         case 's':
             if (scan_state == escan_seenpercent) {
                 scan_state = escan_seennothing;
                 arg_cur = va_arg(aq, char *);
                 if (arg_cur != NULL) {
                     switch (arg_cur[0]) {
                     case 'W':
                         if (!strncmp(arg_cur, "WARNING: ",
                                      sizeof("WARNING: ") - 1)) {
                             msg_log_level = LOG_WARNING;
                         }
                         if (ctx == NULL) {
                             memmove(arg_cur, arg_cur + sizeof("WARNING: ") - 1,
                                     strlen(arg_cur + sizeof("WARNING: ") - 1) + 1);
                         }
                         found = TRUE;
                         break;
                     case 'I':
                         if (!strncmp(arg_cur, "INFO: ",
                                      sizeof("INFO: ") - 1)) {
                             msg_log_level = LOG_INFO;
                         }
                         if (ctx == NULL) {
                             memmove(arg_cur, arg_cur + sizeof("INFO: ") - 1,
                                     strlen(arg_cur + sizeof("INFO: ") - 1) + 1);
                         }
                         found = TRUE;
                         break;
                     case 'D':
                         if (!strncmp(arg_cur, "DEBUG: ",
                                      sizeof("DEBUG: ") - 1)) {
                             msg_log_level = LOG_DEBUG;
                         }
                         if (ctx == NULL) {
                             memmove(arg_cur, arg_cur + sizeof("DEBUG: ") - 1,
                                     strlen(arg_cur + sizeof("DEBUG: ") - 1) + 1);
                         }
                         found = TRUE;
                         break;
                     }
                 }
             }
             break;
         case '#': case '-': case ' ': case '+': case '\'': case 'I': case '.':
         case '0': case '1': case '2': case '3': case '4':
         case '5': case '6': case '7': case '8': case '9':
         case '*':
             break;
         case 'l':
         case 'z':
         case 't':
         case 'j':
         case 'd': case 'i':
         case 'o':
         case 'u':
         case 'x': case 'X':
         case 'e': case 'E':
         case 'f': case 'F':
         case 'g': case 'G':
         case 'a': case 'A':
         case 'c':
         case 'p':
             if (scan_state == escan_seenpercent) {
                 (void) va_arg(aq, void *);  /* skip forward */
                 scan_state = escan_seennothing;
             }
             break;
         default:
             scan_state = escan_seennothing;
             break;
         }
     }
 
     if (log_level != NULL) {
         /* intention of the following offset is:
            cibadmin -V -> start showing INFO labelled messages */
         if (*log_level + 4 >= msg_log_level) {
             vfprintf(stderr, fmt, ap);
         }
     } else {
         PCMK__XML_LOG_BASE(msg_log_level, TRUE, 0, "CIB upgrade: ", fmt, ap);
     }
 
     va_end(aq);
     va_end(ap);
 }
 
 /*!
  * \internal
  * \brief Apply a single XSL transformation to given XML
  *
  * \param[in] xml        XML to transform
  * \param[in] transform  XSL name
  * \param[in] to_logs    If false, certain validation errors will be sent to
  *                       stderr rather than logged
  *
  * \return Transformed XML on success, otherwise NULL
  */
 static xmlNode *
 apply_transformation(const xmlNode *xml, const char *transform,
                      gboolean to_logs)
 {
     char *xform = NULL;
     xmlNode *out = NULL;
     xmlDocPtr res = NULL;
     xsltStylesheet *xslt = NULL;
 
     xform = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
                                     transform);
 
     /* for capturing, e.g., what's emitted via <xsl:message> */
     if (to_logs) {
         xsltSetGenericErrorFunc(NULL, cib_upgrade_err);
     } else {
         xsltSetGenericErrorFunc(&crm_log_level, cib_upgrade_err);
     }
 
     xslt = xsltParseStylesheetFile((pcmkXmlStr) xform);
     CRM_CHECK(xslt != NULL, goto cleanup);
 
     res = xsltApplyStylesheet(xslt, xml->doc, NULL);
     CRM_CHECK(res != NULL, goto cleanup);
 
     xsltSetGenericErrorFunc(NULL, NULL);  /* restore default one */
 
     out = xmlDocGetRootElement(res);
 
   cleanup:
     if (xslt) {
         xsltFreeStylesheet(xslt);
     }
 
     free(xform);
 
     return out;
 }
 
 /*!
  * \internal
  * \brief Perform all transformations needed to upgrade XML to next schema
  *
  * A schema upgrade can require up to three XSL transformations: an "enter"
  * transform, the main upgrade transform, and a "leave" transform. Perform
  * all needed transforms to upgrade given XML to the next schema.
  *
  * \param[in] original_xml  XML to transform
  * \param[in] schema_index  Index of schema that successfully validates
  *                          \p original_xml
  * \param[in] to_logs       If false, certain validation errors will be sent to
  *                          stderr rather than logged
  *
  * \return XML result of schema transforms if successful, otherwise NULL
  */
 static xmlNode *
 apply_upgrade(const xmlNode *original_xml, int schema_index, gboolean to_logs)
 {
     pcmk__schema_t *schema = g_list_nth_data(known_schemas, schema_index);
     pcmk__schema_t *upgraded_schema = g_list_nth_data(known_schemas,
                                                       schema_index + 1);
     bool transform_onleave = false;
     char *transform_leave;
     const xmlNode *xml = original_xml;
     xmlNode *upgrade = NULL;
     xmlNode *final = NULL;
     xmlRelaxNGValidityErrorFunc error_handler = NULL;
 
     CRM_ASSERT((schema != NULL) && (upgraded_schema != NULL));
 
     if (to_logs) {
         error_handler = (xmlRelaxNGValidityErrorFunc) xml_log;
     }
 
     transform_onleave = schema->transform_onleave;
     if (schema->transform_enter != NULL) {
         crm_debug("Upgrading schema from %s to %s: "
                   "applying pre-upgrade XSL transform %s",
                   schema->name, upgraded_schema->name, schema->transform_enter);
         upgrade = apply_transformation(xml, schema->transform_enter, to_logs);
         if (upgrade == NULL) {
             crm_warn("Pre-upgrade XSL transform %s failed, "
                      "will skip post-upgrade transform",
                      schema->transform_enter);
             transform_onleave = FALSE;
         } else {
             xml = upgrade;
         }
     }
 
 
     crm_debug("Upgrading schema from %s to %s: "
               "applying upgrade XSL transform %s",
               schema->name, upgraded_schema->name, schema->transform);
     final = apply_transformation(xml, schema->transform, to_logs);
     if (upgrade != xml) {
         free_xml(upgrade);
         upgrade = NULL;
     }
 
     if ((final != NULL) && transform_onleave) {
         upgrade = final;
         /* following condition ensured in add_schema_by_version */
         CRM_ASSERT(schema->transform_enter != NULL);
         transform_leave = strdup(schema->transform_enter);
         /* enter -> leave */
         memcpy(strrchr(transform_leave, '-') + 1, "leave", sizeof("leave") - 1);
         crm_debug("Upgrading schema from %s to %s: "
                   "applying post-upgrade XSL transform %s",
                   schema->name, upgraded_schema->name, transform_leave);
         final = apply_transformation(upgrade, transform_leave, to_logs);
         if (final == NULL) {
             crm_warn("Ignoring failure of post-upgrade XSL transform %s",
                      transform_leave);
             final = upgrade;
         } else {
             free_xml(upgrade);
         }
         free(transform_leave);
     }
 
     if (final == NULL) {
         return NULL;
     }
 
     // Ensure result validates with its new schema
     if (!validate_with(final, upgraded_schema, error_handler,
                        GUINT_TO_POINTER(LOG_ERR))) {
         crm_err("Schema upgrade from %s to %s failed: "
                 "XSL transform %s produced an invalid configuration",
                 schema->name, upgraded_schema->name, schema->transform);
         crm_log_xml_debug(final, "bad-transform-result");
         free_xml(final);
         return NULL;
     }
 
     crm_info("Schema upgrade from %s to %s succeeded",
              schema->name, upgraded_schema->name);
     return final;
 }
 
 const char *
 get_schema_name(int version)
 {
     pcmk__schema_t *schema = g_list_nth_data(known_schemas, version);
 
     return (schema != NULL)? schema->name : "unknown";
 }
 
 int
 get_schema_version(const char *name)
 {
     int lpc = 0;
 
     if (name == NULL) {
         name = PCMK_VALUE_NONE;
     }
 
     for (GList *iter = known_schemas; iter != NULL; iter = iter->next) {
         pcmk__schema_t *schema = iter->data;
 
         if (pcmk__str_eq(name, schema->name, pcmk__str_casei)) {
             return lpc;
         }
 
         lpc++;
     }
 
     return -1;
 }
 
 /*!
  * \internal
  * \brief Get the schema list entry corresponding to XML configuration
  *
  * \param[in] xml  CIB XML to check
  *
  * \return List entry of schema configured in \p xml
  */
 static GList *
 get_configured_schema(const xmlNode *xml)
 {
     const char *schema_name = crm_element_value(xml, PCMK_XA_VALIDATE_WITH);
 
     if (schema_name == NULL) {
         return NULL;
     }
     return pcmk__get_schema(schema_name);
 }
 
 /*!
  * \brief Update CIB XML to latest schema that validates it
  *
  * \param[in,out] xml              XML to update (may be freed and replaced
  *                                 after being transformed)
  * \param[in]     max_schema_name  If not NULL, do not update \p xml to any
  *                                 schema later than this one
  * \param[in]     transform        If false, do not update \p xml to any schema
  *                                 that requires an XSL transform
  * \param[in]     to_logs          If false, certain validation errors will be
  *                                 sent to stderr rather than logged
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__update_schema(xmlNode **xml, const char *max_schema_name, bool transform,
                     bool to_logs)
 {
     int max_stable_schemas = xml_latest_schema_index();
     int max_schema_index = 0;
     int rc = pcmk_rc_ok;
     GList *entry = NULL;
     pcmk__schema_t *best_schema = NULL;
     pcmk__schema_t *original_schema = NULL;
     xmlRelaxNGValidityErrorFunc error_handler = 
         to_logs ? (xmlRelaxNGValidityErrorFunc) xml_log : NULL;
 
     CRM_CHECK((xml != NULL) && (*xml != NULL) && ((*xml)->doc != NULL),
               return EINVAL);
 
     if (max_schema_name != NULL) {
         GList *max_entry = pcmk__get_schema(max_schema_name);
 
         if (max_entry != NULL) {
             pcmk__schema_t *max_schema = max_entry->data;
 
             max_schema_index = max_schema->schema_index;
         }
     }
     if ((max_schema_index < 1) || (max_schema_index > max_stable_schemas)) {
         max_schema_index = max_stable_schemas;
     }
 
     entry = get_configured_schema(*xml);
     if (entry == NULL) {
         entry = known_schemas;
     } else {
         original_schema = entry->data;
         if (original_schema->schema_index >= max_schema_index) {
             return pcmk_rc_ok;
         }
     }
 
     for (; entry != NULL; entry = entry->next) {
         pcmk__schema_t *current_schema = entry->data;
         xmlNode *upgrade = NULL;
 
         if (current_schema->schema_index > max_schema_index) {
             break;
         }
 
         if (!validate_with(*xml, current_schema, error_handler,
                            GUINT_TO_POINTER(LOG_ERR))) {
             crm_debug("Schema %s does not validate", current_schema->name);
             if (best_schema != NULL) {
                 /* we've satisfied the validation, no need to check further */
                 break;
             }
             rc = pcmk_rc_schema_validation;
             continue; // Try again with the next higher schema
         }
 
         crm_debug("Schema %s validates", current_schema->name);
         rc = pcmk_rc_ok;
         best_schema = current_schema;
         if (current_schema->schema_index == max_schema_index) {
             break; // No further transformations possible
         }
 
         if (!transform || (current_schema->transform == NULL)
             || validate_with_silent(*xml, entry->next->data)) {
             /* The next schema either doesn't require a transform or validates
              * successfully even without the transform. Skip the transform and
              * try the next schema with the same XML.
              */
             continue;
         }
 
         upgrade = apply_upgrade(*xml, current_schema->schema_index, to_logs);
         if (upgrade == NULL) {
             /* The transform failed, so this schema can't be used. Later
              * schemas are unlikely to validate, but try anyway until we
              * run out of options.
              */
             rc = pcmk_rc_transform_failed;
         } else {
             best_schema = current_schema;
             free_xml(*xml);
             *xml = upgrade;
         }
     }
 
     if (best_schema != NULL) {
         if ((original_schema == NULL)
             || (best_schema->schema_index > original_schema->schema_index)) {
             crm_info("%s the configuration schema to %s",
                      (transform? "Transformed" : "Upgraded"),
                      best_schema->name);
             crm_xml_add(*xml, PCMK_XA_VALIDATE_WITH, best_schema->name);
         }
     }
     return rc;
 }
 
 gboolean
 cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs)
 {
     gboolean rc = TRUE;
     char *original_schema_name = NULL;
     int version = 0;
     int orig_version = 0;
     pcmk__schema_t *x_0_schema = pcmk__find_x_0_schema()->data;
 
     original_schema_name = crm_element_value_copy(*xml, PCMK_XA_VALIDATE_WITH);
     version = get_schema_version(original_schema_name);
     orig_version = version;
     if (version < x_0_schema->schema_index) {
         // Current configuration schema is not acceptable, try to update
         xmlNode *converted = NULL;
         const char *new_schema_name = NULL;
 
         converted = pcmk__xml_copy(NULL, *xml);
         if (pcmk__update_schema(&converted, NULL, true, to_logs) == pcmk_rc_ok) {
             new_schema_name = crm_element_value(converted,
                                                 PCMK_XA_VALIDATE_WITH);
             version = get_schema_version(new_schema_name);
         } else {
             version = 0;
         }
 
         if (version < x_0_schema->schema_index) {
             // Updated configuration schema is still not acceptable
 
             if (version < orig_version || orig_version == -1) {
                 // We couldn't validate any schema at all
                 if (to_logs) {
                     pcmk__config_err("Cannot upgrade configuration (claiming "
                                      "%s schema) to at least %s because it "
                                      "does not validate with any schema from "
                                      "%s to the latest",
                                      pcmk__s(original_schema_name, "no"),
                                      x_0_schema->name,
                                      get_schema_name(orig_version));
                 } else {
                     fprintf(stderr, "Cannot upgrade configuration (claiming "
                                     "%s schema) to at least %s because it "
                                     "does not validate with any schema from "
                                     "%s to the latest\n",
                                     pcmk__s(original_schema_name, "no"),
                                     x_0_schema->name,
                                     get_schema_name(orig_version));
                 }
             } else {
                 // We updated configuration successfully, but still too low
                 if (to_logs) {
                     pcmk__config_err("Cannot upgrade configuration (claiming "
                                      "%s schema) to at least %s because it "
                                      "would not upgrade past %s",
                                      pcmk__s(original_schema_name, "no"),
                                      x_0_schema->name,
                                      pcmk__s(new_schema_name, "unspecified version"));
                 } else {
                     fprintf(stderr, "Cannot upgrade configuration (claiming "
                                     "%s schema) to at least %s because it "
                                     "would not upgrade past %s\n",
                                     pcmk__s(original_schema_name, "no"),
                                     x_0_schema->name,
                                     pcmk__s(new_schema_name, "unspecified version"));
                 }
             }
 
             free_xml(converted);
             converted = NULL;
             rc = FALSE;
 
         } else {
             // Updated configuration schema is acceptable
             free_xml(*xml);
             *xml = converted;
 
             if (version < xml_latest_schema_index()) {
                 if (to_logs) {
                     pcmk__config_warn("Configuration with %s schema was "
                                       "internally upgraded to acceptable (but "
                                       "not most recent) %s",
                                       pcmk__s(original_schema_name, "no"),
                                       get_schema_name(version));
                 }
             } else {
                 if (to_logs) {
                     crm_info("Configuration with %s schema was internally "
                              "upgraded to latest version %s",
                              pcmk__s(original_schema_name, "no"),
                              get_schema_name(version));
                 }
             }
         }
 
     } else if (version >= get_schema_version(PCMK_VALUE_NONE)) {
         // Schema validation is disabled
         if (to_logs) {
             pcmk__config_warn("Schema validation of configuration is disabled "
                               "(enabling is encouraged and prevents common "
                               "misconfigurations)");
 
         } else {
             fprintf(stderr, "Schema validation of configuration is disabled "
                             "(enabling is encouraged and prevents common "
                             "misconfigurations)\n");
         }
     }
 
     if (best_version) {
         *best_version = version;
     }
 
     free(original_schema_name);
     return rc;
 }
 
 /*!
  * \internal
  * \brief Return a list of all schema files and any associated XSLT files
  *        later than the given one
  * \brief Return a list of all schema versions later than the given one
  *
  * \param[in] schema The schema to compare against (for example,
  *                   "pacemaker-3.1.rng" or "pacemaker-3.1")
  *
  * \note The caller is responsible for freeing both the returned list and
  *       the elements of the list
  */
 GList *
 pcmk__schema_files_later_than(const char *name)
 {
     GList *lst = NULL;
     pcmk__schema_version_t ver;
 
     if (!version_from_filename(name, &ver)) {
         return lst;
     }
 
     for (GList *iter = g_list_nth(known_schemas, xml_latest_schema_index());
          iter != NULL; iter = iter->prev) {
         pcmk__schema_t *schema = iter->data;
         char *s = NULL;
 
         if (schema_cmp(ver, schema->version) != -1) {
             continue;
         }
 
         s = crm_strdup_printf("%s.rng", schema->name);
         lst = g_list_prepend(lst, s);
 
         if (schema->transform != NULL) {
             char *xform = crm_strdup_printf("%s.xsl", schema->transform);
             lst = g_list_prepend(lst, xform);
         }
 
         if (schema->transform_enter != NULL) {
             char *enter = crm_strdup_printf("%s.xsl", schema->transform_enter);
 
             lst = g_list_prepend(lst, enter);
 
             if (schema->transform_onleave) {
                 int last_dash = strrchr(enter, '-') - enter;
                 char *leave = crm_strdup_printf("%.*s-leave.xsl", last_dash, enter);
 
                 lst = g_list_prepend(lst, leave);
             }
         }
     }
 
     return lst;
 }
 
 static void
 append_href(xmlNode *xml, void *user_data)
 {
     GList **list = user_data;
     char *href = crm_element_value_copy(xml, "href");
 
     if (href == NULL) {
         return;
     }
     *list = g_list_prepend(*list, href);
 }
 
 static void
 external_refs_in_schema(GList **list, const char *contents)
 {
     /* local-name()= is needed to ignore the xmlns= setting at the top of
      * the XML file.  Otherwise, the xpath query will always return nothing.
      */
     const char *search = "//*[local-name()='externalRef'] | //*[local-name()='include']";
     xmlNode *xml = pcmk__xml_parse(contents);
 
     crm_foreach_xpath_result(xml, search, append_href, list);
     free_xml(xml);
 }
 
 static int
 read_file_contents(const char *file, char **contents)
 {
     int rc = pcmk_rc_ok;
     char *path = NULL;
 
     if (pcmk__ends_with(file, ".rng")) {
         path = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng, file);
     } else {
         path = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, file);
     }
 
     rc = pcmk__file_contents(path, contents);
 
     free(path);
     return rc;
 }
 
 static void
 add_schema_file_to_xml(xmlNode *parent, const char *file, GList **already_included)
 {
     char *contents = NULL;
     char *path = NULL;
     xmlNode *file_node = NULL;
     GList *includes = NULL;
     int rc = pcmk_rc_ok;
 
     /* If we already included this file, don't do so again. */
     if (g_list_find_custom(*already_included, file, (GCompareFunc) strcmp) != NULL) {
         return;
     }
 
     /* Ensure whatever file we were given has a suffix we know about.  If not,
      * just assume it's an RNG file.
      */
     if (!pcmk__ends_with(file, ".rng") && !pcmk__ends_with(file, ".xsl")) {
         path = crm_strdup_printf("%s.rng", file);
     } else {
         path = pcmk__str_copy(file);
     }
 
     rc = read_file_contents(path, &contents);
     if (rc != pcmk_rc_ok || contents == NULL) {
         crm_warn("Could not read schema file %s: %s", file, pcmk_rc_str(rc));
         free(path);
         return;
     }
 
     /* Create a new <file path="..."> node with the contents of the file
      * as a CDATA block underneath it.
      */
     file_node = pcmk__xe_create(parent, PCMK_XA_FILE);
     crm_xml_add(file_node, PCMK_XA_PATH, path);
     *already_included = g_list_prepend(*already_included, path);
 
     xmlAddChild(file_node, xmlNewCDataBlock(parent->doc, (pcmkXmlStr) contents,
                                             strlen(contents)));
 
     /* Scan the file for any <externalRef> or <include> nodes and build up
      * a list of the files they reference.
      */
     external_refs_in_schema(&includes, contents);
 
     /* For each referenced file, recurse to add it (and potentially anything it
      * references, ...) to the XML.
      */
     for (GList *iter = includes; iter != NULL; iter = iter->next) {
         add_schema_file_to_xml(parent, iter->data, already_included);
     }
 
     free(contents);
     g_list_free_full(includes, free);
 }
 
 /*!
  * \internal
  * \brief Add an XML schema file and all the files it references as children
  *        of a given XML node
  *
  * \param[in,out] parent            The parent XML node
  * \param[in] name                  The schema version to compare against
  *                                  (for example, "pacemaker-3.1" or "pacemaker-3.1.rng")
  * \param[in,out] already_included  A list of names that have already been added
  *                                  to the parent node.
  *
  * \note The caller is responsible for freeing both the returned list and
  *       the elements of the list
  */
 void
 pcmk__build_schema_xml_node(xmlNode *parent, const char *name, GList **already_included)
 {
     /* First, create an unattached node to add all the schema files to as children. */
     xmlNode *schema_node = pcmk__xe_create(NULL, PCMK__XA_SCHEMA);
 
     crm_xml_add(schema_node, PCMK_XA_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
  *        Pacemaker Remote node fetched from the cluster
  */
 const char *
 pcmk__remote_schema_dir(void)
 {
     const char *dir = pcmk__env_option(PCMK__ENV_REMOTE_SCHEMA_DIRECTORY);
 
     if (pcmk__str_empty(dir)) {
         return PCMK__REMOTE_SCHEMA_DIR;
     }
 
     return dir;
 }
 
 // Deprecated functions kept only for backward API compatibility
 // LCOV_EXCL_START
 
 #include <crm/common/schemas_compat.h>
 
 const char *
 xml_latest_schema(void)
 {
     return pcmk__highest_schema_name();
 }
 
 int
 update_validation(xmlNode **xml, int *best, int max, gboolean transform,
                   gboolean to_logs)
 {
     int rc = pcmk__update_schema(xml, get_schema_name(max), transform, to_logs);
 
     if ((best != NULL) && (xml != NULL) && (rc == pcmk_rc_ok)) {
         const char *schema_name = crm_element_value(*xml,
                                                     PCMK_XA_VALIDATE_WITH);
         GList *schema_entry = pcmk__get_schema(schema_name);
 
         if (schema_entry != NULL) {
             *best = ((pcmk__schema_t *)(schema_entry->data))->schema_index;
         }
     }
 
     return pcmk_rc2legacy(rc);
 }
 
+gboolean
+validate_xml(xmlNode *xml_blob, const char *validation, gboolean to_logs)
+{
+    bool rc = pcmk__validate_xml(xml_blob, validation,
+                                 to_logs? (xmlRelaxNGValidityErrorFunc) xml_log : NULL,
+                                 GUINT_TO_POINTER(LOG_ERR));
+    return rc? TRUE : FALSE;
+}
+
 // LCOV_EXCL_STOP
 // End deprecated API