diff --git a/.gitignore b/.gitignore index b698574d4f..3c58131602 100644 --- a/.gitignore +++ b/.gitignore @@ -1,354 +1,355 @@ # # Copyright 2011-2023 the Pacemaker project contributors # # The version control history for this file may have further details. # # This source code is licensed under the GNU General Public License version 2 # or later (GPLv2+) WITHOUT ANY WARRANTY. # # Common conventions for files that should be ignored *~ *.bz2 *.diff *.orig *.patch *.rej *.sed *.swp *.tar.gz *.tgz \#* .\#* logs # libtool artifacts *.la *.lo .libs libltdl libtool libtool.m4 ltdl.m4 /m4/argz.m4 /m4/ltargz.m4 /m4/ltoptions.m4 /m4/ltsugar.m4 /m4/ltversion.m4 /m4/lt~obsolete.m4 # autotools artifacts .deps .dirstamp Makefile Makefile.in aclocal.m4 autoconf autoheader autom4te.cache/ automake /confdefs.h config.log config.status configure /conftest* # gettext artifacts /ABOUT-NLS /m4/codeset.m4 /m4/fcntl-o.m4 /m4/gettext.m4 /m4/glibc2.m4 /m4/glibc21.m4 /m4/iconv.m4 /m4/intdiv0.m4 /m4/intl.m4 /m4/intldir.m4 /m4/intlmacosx.m4 /m4/intmax.m4 /m4/inttypes-pri.m4 /m4/inttypes_h.m4 /m4/lcmessage.m4 /m4/lib-ld.m4 /m4/lib-link.m4 /m4/lib-prefix.m4 /m4/lock.m4 /m4/longlong.m4 /m4/nls.m4 /m4/po.m4 /m4/printf-posix.m4 /m4/progtest.m4 /m4/size_max.m4 /m4/stdint_h.m4 /m4/threadlib.m4 /m4/uintmax_t.m4 /m4/visibility.m4 /m4/wchar_t.m4 /m4/wint_t.m4 /m4/xsize.m4 /po/*.gmo /po/*.header /po/*.pot /po/*.sin /po/Makefile.in.in /po/Makevars.template /po/POTFILES /po/Rules-quot /po/stamp-po # configure targets /agents/ocf/ClusterMon /agents/ocf/Dummy /agents/ocf/HealthCPU /agents/ocf/HealthIOWait /agents/ocf/HealthSMART /agents/ocf/Stateful /agents/ocf/SysInfo /agents/ocf/attribute /agents/ocf/controld /agents/ocf/ifspeed /agents/ocf/o2cb /agents/ocf/ping /agents/ocf/remote /agents/stonith/fence_legacy /agents/stonith/fence_watchdog /cts/benchmark/clubench /cts/cluster_test /cts/cts /cts/cts-attrd /cts/cts-cli /cts/cts-exec /cts/cts-fencing /cts/cts-lab /cts/cts-log-watcher /cts/cts-regression /cts/cts-scheduler /cts/lab/CTS.py /cts/support/LSBDummy /cts/support/cts-support /cts/support/fence_dummy /cts/support/pacemaker-cts-dummyd /cts/support/pacemaker-cts-dummyd@.service /daemons/execd/pacemaker_remote /daemons/execd/pacemaker_remote.service /daemons/fenced/fence_legacy /daemons/fenced/fence_watchdog /daemons/pacemakerd/pacemaker.combined.upstart /daemons/pacemakerd/pacemaker.service /daemons/pacemakerd/pacemaker.upstart /doc/Doxyfile /etc/init.d/pacemaker /etc/logrotate.d/pacemaker /etc/sysconfig/pacemaker /include/config.h /include/config.h.in /include/crm_config.h /maint/bumplibs /python/pacemaker/buildoptions.py /python/setup.py /tools/cluster-clean /tools/cluster-helper /tools/cluster-init /tools/cibsecret /tools/crm_error /tools/crm_failcount /tools/crm_master /tools/crm_mon.service /tools/crm_mon.upstart /tools/crm_report /tools/crm_rule /tools/crm_standby /tools/pcmk_simtimes /tools/report.collector /tools/report.common /xml/version-diff.sh # Compiled targets and intermediary files *.o *.pc *.pyc /daemons/attrd/pacemaker-attrd /daemons/based/pacemaker-based /daemons/controld/pacemaker-controld /daemons/execd/cts-exec-helper /daemons/execd/pacemaker-execd /daemons/execd/pacemaker-remoted /daemons/fenced/cts-fence-helper /daemons/fenced/pacemaker-fenced /daemons/pacemakerd/pacemakerd /daemons/schedulerd/pacemaker-schedulerd /devel/scratch /lib/gnu/stdalign.h /tools/attrd_updater /tools/cibadmin /tools/crmadmin /tools/crm_attribute /tools/crm_diff /tools/crm_mon /tools/crm_node /tools/crm_resource /tools/crm_shadow /tools/crm_simulate /tools/crm_ticket /tools/crm_verify /tools/iso8601 /tools/stonith_admin # Generated XML schema files /xml/crm_mon.rng /xml/pacemaker*.rng /xml/versions.rng /xml/api/api-result*.rng # Working directories for make dist and make export /pacemaker-[a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9] # Documentation build targets and intermediary files *.7 *.7.xml *.7.html *.8 *.8.xml *.8.html GPATH GRTAGS GTAGS TAGS /daemons/fenced/pacemaker-fenced.xml /daemons/schedulerd/pacemaker-schedulerd.xml /doc/.ABI-build /doc/HTML /doc/abi_dumps /doc/abi-check /doc/api/ /doc/compat_reports /doc/crm_fencing.html /doc/sphinx/*/_build /doc/sphinx/*/conf.py /doc/sphinx/*/generated /doc/sphinx/build-2.1.txt /doc/sphinx/shared/images/*.png # Test artifacts (from unit tests, regression tests, static analysis, etc.) *.coverity *.gcda *.gcno coverity-* pacemaker_*.info /coverage /cppcheck.out /cts/scheduler/*.ref /cts/scheduler/*.up /cts/scheduler/*.up.err /cts/scheduler/bug-rh-1097457.log /cts/scheduler/bug-rh-1097457.trs /cts/scheduler/shadow.* /cts/test-suite.log /lib/*/tests/*/*.log /lib/*/tests/*/*_test /lib/*/tests/*/*.trs +/lib/common/tests/schemas/schemas /xml/test-*/*.up /xml/test-*/*.up.err /xml/assets/*.rng /xml/assets/diffview.js /xml/assets/xmlcatalog /test/_test_file.c # Packaging artifacts *.rpm /pacemaker.spec /rpm/[A-LN-Z]* /rpm/build.counter /rpm/mock # Project maintainer artifacts /maint/gnulib /maint/mocked/based /maint/testcc_helper.cc /maint/testcc_*_h # Formerly built files (helps when jumping back and forth in checkout) /.ABI-build /Doxyfile /HTML /abi_dumps /abi-check /build.counter /compat_reports /compile /cts/.regression.failed.diff /attrd /cib /config.guess /config.sub /coverage.sh /crmd /cts/CTS.py /cts/CTSlab.py /cts/CTSvars.py /cts/HBDummy /cts/LSBDummy /cts/OCFIPraTest.py /cts/cts-coverage /cts/cts-support /cts/fence_dummy /cts/lab/CTSlab.py /cts/lab/CTSvars.py /cts/lab/OCFIPraTest.py /cts/lab/cluster_test /cts/lab/cts /cts/lab/cts-log-watcher /cts/lxc_autogen.sh /cts/pacemaker-cts-dummyd /cts/pacemaker-cts-dummyd@.service /daemons/based/cibmon /daemons/fenced/fence_legacy /daemons/fenced/fence_watchdog /daemons/pacemakerd/pacemaker /depcomp /doc/*.build /doc/*/en-US/Ap-*.xml /doc/*/en-US/Ch-*.xml /doc/*/publican.cfg /doc/*/publish /doc/*/tmp/** /doc/Clusters_from_Scratch.txt /doc/Pacemaker_Explained.txt /doc/acls.html /doc/publican-catalog* /doc/shared/en-US/*.xml /doc/shared/en-US/images/pcmk-*.png /doc/shared/en-US/images/Policy-Engine-*.png /extra/*/* /fencing /include/stamp-* /install-sh /lib/common/md5.c /lib/common/tests/flags/pcmk__clear_flags_as /lib/common/tests/flags/pcmk__set_flags_as /lib/common/tests/flags/pcmk_all_flags_set /lib/common/tests/flags/pcmk_any_flags_set /lib/common/tests/operations/parse_op_key /lib/common/tests/strings/pcmk__btoa /lib/common/tests/strings/pcmk__parse_ll_range /lib/common/tests/strings/pcmk__scan_double /lib/common/tests/strings/pcmk__str_any_of /lib/common/tests/strings/pcmk__strcmp /lib/common/tests/strings/pcmk__char_in_any_str /lib/common/tests/utils/pcmk_str_is_infinity /lib/common/tests/utils/pcmk_str_is_minus_infinity /lib/gnu/libgnu.a /lib/pengine/tests/rules/pe_cron_range_satisfied /lrmd /ltmain.sh /mcp /missing /mock /pacemaker-*.spec /pengine /py-compile /scratch /tools/cluster-init /test-driver /xml/crm.dtd ylwrap diff --git a/include/crm/common/internal.h b/include/crm/common/internal.h index 3078606368..a3cd455dc5 100644 --- a/include/crm/common/internal.h +++ b/include/crm/common/internal.h @@ -1,341 +1,342 @@ /* * Copyright 2015-2023 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #ifndef CRM_COMMON_INTERNAL__H #define CRM_COMMON_INTERNAL__H #include // pid_t, getpid() #include // bool #include // uint8_t, uint64_t #include // guint, GList, GHashTable #include // xmlNode #include // crm_strdup_printf() #include // do_crm_log_unlikely(), etc. #include // mainloop_io_t, struct ipc_client_callbacks #include #include #include #include #include #include #include #include #include /* This says whether the current application is a Pacemaker daemon or not, * and is used to change default logging settings such as whether to log to * stderr, etc., as well as a few other details such as whether blackbox signal * handling is enabled. * * It is set when logging is initialized, and does not need to be set directly. */ extern bool pcmk__is_daemon; //! Node name of the local node extern char *pcmk__our_nodename; // Number of elements in a statically defined array #define PCMK__NELEM(a) ((int) (sizeof(a)/sizeof(a[0])) ) #if SUPPORT_CIBSECRETS /* internal CIB utilities (from cib_secrets.c) */ int pcmk__substitute_secrets(const char *rsc_id, GHashTable *params); #endif /* internal main loop utilities (from mainloop.c) */ int pcmk__add_mainloop_ipc(crm_ipc_t *ipc, int priority, void *userdata, const struct ipc_client_callbacks *callbacks, mainloop_io_t **source); guint pcmk__mainloop_timer_get_period(const mainloop_timer_t *timer); /* internal node-related XML utilities (from nodes.c) */ /*! * \internal * \brief Add local node name and ID to an XML node * * \param[in,out] request XML node to modify * \param[in] node The local node's name * \param[in] nodeid The local node's ID (can be 0) */ void pcmk__xe_add_node(xmlNode *xml, const char *node, int nodeid); /* internal name/value utilities (from nvpair.c) */ int pcmk__scan_nvpair(const char *input, char **name, char **value); char *pcmk__format_nvpair(const char *name, const char *value, const char *units); /*! * \internal * \brief Add a boolean attribute to an XML node. * * \param[in,out] node XML node to add attributes to * \param[in] name XML attribute to create * \param[in] value Value to give to the attribute */ void pcmk__xe_set_bool_attr(xmlNodePtr node, const char *name, bool value); /*! * \internal * \brief Extract a boolean attribute's value from an XML element * * \param[in] node XML node to get attribute from * \param[in] name XML attribute to get * * \return True if the given \p name is an attribute on \p node and has * the value "true", False in all other cases */ bool pcmk__xe_attr_is_true(const xmlNode *node, const char *name); /*! * \internal * \brief Extract a boolean attribute's value from an XML element, with * error checking * * \param[in] node XML node to get attribute from * \param[in] name XML attribute to get * \param[out] value Destination for the value of the attribute * * \return EINVAL if \p name or \p value are NULL, ENODATA if \p node is * NULL or the attribute does not exist, pcmk_rc_unknown_format * if the attribute is not a boolean, and pcmk_rc_ok otherwise. * * \note \p value only has any meaning if the return value is pcmk_rc_ok. */ int pcmk__xe_get_bool_attr(const xmlNode *node, const char *name, bool *value); /* internal procfs utilities (from procfs.c) */ pid_t pcmk__procfs_pid_of(const char *name); unsigned int pcmk__procfs_num_cores(void); int pcmk__procfs_pid2path(pid_t pid, char path[], size_t path_size); bool pcmk__procfs_has_pids(void); /* internal XML schema functions (from xml.c) */ void crm_schema_init(void); void crm_schema_cleanup(void); +GList *pcmk__schema_files_later_than(const char *name); /* internal functions related to process IDs (from pid.c) */ /*! * \internal * \brief Check whether process exists (by PID and optionally executable path) * * \param[in] pid PID of process to check * \param[in] daemon If not NULL, path component to match with procfs entry * * \return Standard Pacemaker return code * \note Particular return codes of interest include pcmk_rc_ok for alive, * ESRCH for process is not alive (verified by kill and/or executable path * match), EACCES for caller unable or not allowed to check. A result of * "alive" is less reliable when \p daemon is not provided or procfs is * not available, since there is no guarantee that the PID has not been * recycled for another process. * \note This function cannot be used to verify \e authenticity of the process. */ int pcmk__pid_active(pid_t pid, const char *daemon); int pcmk__read_pidfile(const char *filename, pid_t *pid); int pcmk__pidfile_matches(const char *filename, pid_t expected_pid, const char *expected_name, pid_t *pid); int pcmk__lock_pidfile(const char *filename, const char *name); // bitwise arithmetic utilities /*! * \internal * \brief Set specified flags in a flag group * * \param[in] function Function name of caller * \param[in] line Line number of caller * \param[in] log_level Log a message at this level * \param[in] flag_type Label describing this flag group (for logging) * \param[in] target Name of object whose flags these are (for logging) * \param[in] flag_group Flag group being manipulated * \param[in] flags Which flags in the group should be set * \param[in] flags_str Readable equivalent of \p flags (for logging) * * \return Possibly modified flag group */ static inline uint64_t pcmk__set_flags_as(const char *function, int line, uint8_t log_level, const char *flag_type, const char *target, uint64_t flag_group, uint64_t flags, const char *flags_str) { uint64_t result = flag_group | flags; if (result != flag_group) { do_crm_log_unlikely(log_level, "%s flags %#.8llx (%s) for %s set by %s:%d", ((flag_type == NULL)? "Group of" : flag_type), (unsigned long long) flags, ((flags_str == NULL)? "flags" : flags_str), ((target == NULL)? "target" : target), function, line); } return result; } /*! * \internal * \brief Clear specified flags in a flag group * * \param[in] function Function name of caller * \param[in] line Line number of caller * \param[in] log_level Log a message at this level * \param[in] flag_type Label describing this flag group (for logging) * \param[in] target Name of object whose flags these are (for logging) * \param[in] flag_group Flag group being manipulated * \param[in] flags Which flags in the group should be cleared * \param[in] flags_str Readable equivalent of \p flags (for logging) * * \return Possibly modified flag group */ static inline uint64_t pcmk__clear_flags_as(const char *function, int line, uint8_t log_level, const char *flag_type, const char *target, uint64_t flag_group, uint64_t flags, const char *flags_str) { uint64_t result = flag_group & ~flags; if (result != flag_group) { do_crm_log_unlikely(log_level, "%s flags %#.8llx (%s) for %s cleared by %s:%d", ((flag_type == NULL)? "Group of" : flag_type), (unsigned long long) flags, ((flags_str == NULL)? "flags" : flags_str), ((target == NULL)? "target" : target), function, line); } return result; } // miscellaneous utilities (from utils.c) void pcmk__daemonize(const char *name, const char *pidfile); void pcmk__panic(const char *origin); pid_t pcmk__locate_sbd(void); void pcmk__sleep_ms(unsigned int ms); extern int pcmk__score_red; extern int pcmk__score_green; extern int pcmk__score_yellow; /*! * \internal * \brief Resize a dynamically allocated memory block * * \param[in] ptr Memory block to resize (or NULL to allocate new memory) * \param[in] size New size of memory block in bytes (must be > 0) * * \return Pointer to resized memory block * * \note This asserts on error, so the result is guaranteed to be non-NULL * (which is the main advantage of this over directly using realloc()). */ static inline void * pcmk__realloc(void *ptr, size_t size) { void *new_ptr; // realloc(p, 0) can replace free(p) but this wrapper can't CRM_ASSERT(size > 0); new_ptr = realloc(ptr, size); if (new_ptr == NULL) { free(ptr); abort(); } return new_ptr; } static inline char * pcmk__getpid_s(void) { return crm_strdup_printf("%lu", (unsigned long) getpid()); } // More efficient than g_list_length(list) == 1 static inline bool pcmk__list_of_1(GList *list) { return list && (list->next == NULL); } // More efficient than g_list_length(list) > 1 static inline bool pcmk__list_of_multiple(GList *list) { return list && (list->next != NULL); } /* convenience functions for failure-related node attributes */ #define PCMK__FAIL_COUNT_PREFIX "fail-count" #define PCMK__LAST_FAILURE_PREFIX "last-failure" /*! * \internal * \brief Generate a failure-related node attribute name for a resource * * \param[in] prefix Start of attribute name * \param[in] rsc_id Resource name * \param[in] op Operation name * \param[in] interval_ms Operation interval * * \return Newly allocated string with attribute name * * \note Failure attributes are named like PREFIX-RSC#OP_INTERVAL (for example, * "fail-count-myrsc#monitor_30000"). The '#' is used because it is not * a valid character in a resource ID, to reliably distinguish where the * operation name begins. The '_' is used simply to be more comparable to * action labels like "myrsc_monitor_30000". */ static inline char * pcmk__fail_attr_name(const char *prefix, const char *rsc_id, const char *op, guint interval_ms) { CRM_CHECK(prefix && rsc_id && op, return NULL); return crm_strdup_printf("%s-%s#%s_%u", prefix, rsc_id, op, interval_ms); } static inline char * pcmk__failcount_name(const char *rsc_id, const char *op, guint interval_ms) { return pcmk__fail_attr_name(PCMK__FAIL_COUNT_PREFIX, rsc_id, op, interval_ms); } static inline char * pcmk__lastfailure_name(const char *rsc_id, const char *op, guint interval_ms) { return pcmk__fail_attr_name(PCMK__LAST_FAILURE_PREFIX, rsc_id, op, interval_ms); } // internal resource agent functions (from agents.c) int pcmk__effective_rc(int rc); #endif /* CRM_COMMON_INTERNAL__H */ diff --git a/lib/common/schemas.c b/lib/common/schemas.c index b3ff059174..1c60738ecc 100644 --- a/lib/common/schemas.c +++ b/lib/common/schemas.c @@ -1,1312 +1,1372 @@ /* * Copyright 2004-2023 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* 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(GList *schemas) { // @COMPAT: pacemaker-next is deprecated since 2.1.5 // FIXME: This function assumes at least three schemas have been added // before it has been called for the first time, which is only the case // if we are not unit testing. #if defined(PCMK__UNIT_TESTING) return g_list_length(schemas) - 1; // index from 0 #else return g_list_length(schemas) - 3; // index from 0, ignore "pacemaker-next"/"none" #endif } /* Return the index of the most recent X.0 schema. */ int pcmk__find_x_0_schema_index(GList *schemas) { /* We can't just use best to determine whether we've found the index * or not. What if we have a very long list of schemas all in the * same major version series? We'd return 0 for that, which means * we would still run this function every time. */ #if defined(PCMK__UNIT_TESTING) /* If we're unit testing, these can't be static because they'll stick * around from one test run to the next. They need to be cleared out * every time. */ bool found = false; int best = 0; #else static bool found = false; static int best = 0; #endif int i; GList *best_node = NULL; pcmk__schema_t *best_schema = NULL; if (found) { return best; } CRM_ASSERT(schemas != NULL); /* Get the most recent schema so we can look at its version number. */ best = xml_latest_schema_index(schemas); best_node = g_list_nth(schemas, best); best_schema = best_node->data; /* If we are unit testing, we don't add the pacemaker-next/none schemas * to the list because we're not using the standard schema adding * functions. Thus, a singleton list means we're done. * * On the other hand, if we are running as usually, we have those two * schemas added to the list. A list of length three actually only has * one useful schema. So we're still done. * * @COMPAT Change this when we stop adding those schemas. */ #if defined(PCMK__UNIT_TESTING) if (pcmk__list_of_1(schemas)) { goto done; } #else if (g_list_length(schemas) == 3) { goto done; } #endif /* Start comparing the list from the node before the best schema (there's * no point in comparing something to itself). Then, 'i' is an index * starting at the best schema and will always point at the node after * 'iter'. This makes it the value we want to return when we find what * we're looking for. */ i = best; for (GList *iter = best_node->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 best schema. */ if (schema->version.v[0] < best_schema->version.v[0]) { best = i; goto done; /* We're out of list to examine. This probably means there was only * one major version series, so return index 0. */ } else if (iter->prev == NULL) { best = 0; goto done; } i--; } done: found = true; return best; } const char * xml_latest_schema(void) { return get_schema_name(xml_latest_schema_index(known_schemas)); } static inline bool version_from_filename(const char *filename, pcmk__schema_version_t *version) { - return sscanf(filename, "pacemaker-%hhu.%hhu.rng", &(version->v[0]), &(version->v[1])) == 2; + if (pcmk__ends_with(filename, ".rng")) { + return sscanf(filename, "pacemaker-%hhu.%hhu.rng", &(version->v[0]), &(version->v[1])) == 2; + } else { + return sscanf(filename, "pacemaker-%hhu.%hhu", &(version->v[0]), &(version->v[1])) == 2; + } } static int 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; int last = g_list_length(known_schemas); schema = calloc(1, sizeof(pcmk__schema_t)); CRM_ASSERT(schema != NULL); schema->validator = validator; schema->version.v[0] = version->v[0]; schema->version.v[1] = version->v[1]; schema->transform_onleave = transform_onleave; if (version->v[0] || version->v[1]) { schema->name = schema_strdup_printf("pacemaker-", *version, ""); } else { schema->name = strdup(name); CRM_ASSERT(schema->name != NULL); } if (transform) { schema->transform = strdup(transform); CRM_ASSERT(schema->transform != NULL); } if (transform_enter) { schema->transform_enter = strdup(transform_enter); CRM_ASSERT(schema->transform_enter != NULL); } known_schemas = g_list_append(known_schemas, schema); if (schema->transform != NULL) { crm_debug("Added supported schema %d: %s (upgrades with %s.xsl)", last, schema->name, schema->transform); } else { crm_debug("Added supported schema %d: %s", last, schema->name); } } /*! * \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(); } } /*! * \internal * \brief Load pacemaker schemas into cache * * \note This currently also serves as an entry point for the * generic initialization of the libxslt library. */ void crm_schema_init(void) { int lpc, max; char *base = pcmk__xml_artefact_root(pcmk__xml_artefact_ns_legacy_rng); struct dirent **namelist = NULL; const pcmk__schema_version_t zero = SCHEMA_ZERO; wrap_libxslt(false); max = scandir(base, &namelist, schema_filter, schema_cmp_directory); if (max < 0) { crm_notice("scandir(%s) failed: %s (%d)", base, strerror(errno), errno); free(base); } else { free(base); for (lpc = 0; lpc < max; lpc++) { bool transform_expected = FALSE; pcmk__schema_version_t version = SCHEMA_ZERO; if (!version_from_filename(namelist[lpc]->d_name, &version)) { // Shouldn't be possible, but makes static analysis happy crm_err("Skipping schema '%s': could not parse version", namelist[lpc]->d_name); continue; } if ((lpc + 1) < max) { pcmk__schema_version_t next_version = SCHEMA_ZERO; if (version_from_filename(namelist[lpc+1]->d_name, &next_version) && (version.v[0] < next_version.v[0])) { transform_expected = TRUE; } } if (add_schema_by_version(&version, transform_expected) == ENOENT) { break; } } for (lpc = 0; lpc < max; lpc++) { free(namelist[lpc]); } free(namelist); } // @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); } static gboolean validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler, void *error_handler_context, const char *relaxng_file, relaxng_ctx_cache_t **cached_ctx) { int rc = 0; gboolean valid = TRUE; relaxng_ctx_cache_t *ctx = NULL; CRM_CHECK(doc != NULL, return FALSE); CRM_CHECK(relaxng_file != NULL, return FALSE); if (cached_ctx && *cached_ctx) { ctx = *cached_ctx; } else { crm_debug("Creating RNG parser context"); ctx = calloc(1, sizeof(relaxng_ctx_cache_t)); 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); } static gboolean validate_with(xmlNode *xml, pcmk__schema_t *schema, xmlRelaxNGValidityErrorFunc error_handler, void* error_handler_context) { gboolean 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); write_xml_fd(xml_blob, filename, fd, FALSE); dump_file(filename); doc = xmlReadFile(filename, NULL, 0); xml = xmlDocGetRootElement(doc); rc = validate_xml(xml, NULL, FALSE); free_xml(xml); unlink(filename); free(filename); return rc; } gboolean validate_xml(xmlNode *xml_blob, const char *validation, gboolean to_logs) { return pcmk__validate_xml(xml_blob, validation, to_logs ? (xmlRelaxNGValidityErrorFunc) xml_log : NULL, GUINT_TO_POINTER(LOG_ERR)); } gboolean pcmk__validate_xml(xmlNode *xml_blob, const char *validation, xmlRelaxNGValidityErrorFunc error_handler, void* error_handler_context) { int version = 0; CRM_CHECK((xml_blob != NULL) && (xml_blob->doc != NULL), return FALSE); if (validation == NULL) { validation = crm_element_value(xml_blob, XML_ATTR_VALIDATION); } if (validation == NULL) { bool valid = FALSE; for (GList *iter = known_schemas; iter != NULL; iter = iter->next) { pcmk__schema_t *schema = iter->data; if (validate_with(xml_blob, schema, NULL, NULL)) { valid = TRUE; crm_xml_add(xml_blob, XML_ATTR_VALIDATION, schema->name); crm_info("XML validated against %s", schema->name); } } return valid; } version = get_schema_version(validation); if (strcmp(validation, PCMK__VALUE_NONE) == 0) { return TRUE; } else if (version < g_list_length(known_schemas)) { pcmk__schema_t *schema = g_list_nth_data(known_schemas, version); return validate_with(xml_blob, schema, error_handler, error_handler_context); } crm_err("Unknown validator: %s", validation); return FALSE; } /* 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); } static xmlNode * apply_transformation(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 */ 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 Possibly full enter->upgrade->leave trip per internal bookkeeping. * * \note Only emits warnings about enter/leave phases in case of issues. */ static xmlNode * apply_upgrade(xmlNode *xml, const pcmk__schema_t *schema, gboolean to_logs) { bool transform_onleave = schema->transform_onleave; char *transform_leave; xmlNode *upgrade = NULL, *final = NULL; if (schema->transform_enter) { crm_debug("Upgrading %s-style configuration, pre-upgrade phase with %s.xsl", schema->name, schema->transform_enter); upgrade = apply_transformation(xml, schema->transform_enter, to_logs); if (upgrade == NULL) { crm_warn("Upgrade-enter transformation %s.xsl failed", schema->transform_enter); transform_onleave = FALSE; } } if (upgrade == NULL) { upgrade = xml; } crm_debug("Upgrading %s-style configuration, main phase with %s.xsl", schema->name, schema->transform); final = apply_transformation(upgrade, schema->transform, to_logs); if (upgrade != xml) { free_xml(upgrade); upgrade = NULL; } if (final != NULL && transform_onleave) { upgrade = final; /* following condition ensured in add_schema_by_version */ CRM_ASSERT(schema->transform_enter != NULL); transform_leave = strdup(schema->transform_enter); /* enter -> leave */ memcpy(strrchr(transform_leave, '-') + 1, "leave", sizeof("leave") - 1); crm_debug("Upgrading %s-style configuration, post-upgrade phase with %s.xsl", schema->name, transform_leave); final = apply_transformation(upgrade, transform_leave, to_logs); if (final == NULL) { crm_warn("Upgrade-leave transformation %s.xsl failed", transform_leave); final = upgrade; } else { free_xml(upgrade); } free(transform_leave); } return final; } const char * get_schema_name(int version) { pcmk__schema_t *schema = g_list_nth_data(known_schemas, version); if (schema == NULL) { return "unknown"; } return schema->name; } 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; } /* set which validation to use */ int update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, gboolean to_logs) { xmlNode *xml = NULL; char *value = NULL; int max_stable_schemas = xml_latest_schema_index(known_schemas); int lpc = 0, match = -1, rc = pcmk_ok; int next = -1; /* -1 denotes "inactive" value */ xmlRelaxNGValidityErrorFunc error_handler = to_logs ? (xmlRelaxNGValidityErrorFunc) xml_log : NULL; CRM_CHECK(best != NULL, return -EINVAL); *best = 0; CRM_CHECK((xml_blob != NULL) && (*xml_blob != NULL) && ((*xml_blob)->doc != NULL), return -EINVAL); xml = *xml_blob; value = crm_element_value_copy(xml, XML_ATTR_VALIDATION); if (value != NULL) { match = get_schema_version(value); lpc = match; if (lpc >= 0 && transform == FALSE) { *best = lpc++; } else if (lpc < 0) { crm_debug("Unknown validation schema"); lpc = 0; } } if (match >= max_stable_schemas) { /* nothing to do */ free(value); *best = match; return pcmk_ok; } while (lpc <= max_stable_schemas) { /* FIXME: This will cause us to walk the known_schemas list every time * this loop iterates, which is not ideal. However, for now it's a lot * easier than trying to get all the loop indices we're using here * sorted out and working correctly. */ pcmk__schema_t *schema = g_list_nth_data(known_schemas, lpc); crm_debug("Testing '%s' validation (%d of %d)", pcmk__s(schema->name, ""), lpc, max_stable_schemas); if (validate_with(xml, schema, error_handler, GUINT_TO_POINTER(LOG_ERR)) == FALSE) { if (next != -1) { crm_info("Configuration not valid for schema: %s", schema->name); next = -1; } else { crm_trace("%s validation failed", pcmk__s(schema->name, "")); } if (*best) { /* we've satisfied the validation, no need to check further */ break; } rc = -pcmk_err_schema_validation; } else { if (next != -1) { crm_debug("Configuration valid for schema: %s", schema->name); next = -1; } rc = pcmk_ok; } if (rc == pcmk_ok) { *best = lpc; } if (rc == pcmk_ok && transform) { xmlNode *upgrade = NULL; pcmk__schema_t *next_schema = NULL; next = lpc+1; if (next > max_stable_schemas) { /* There is no next version */ crm_trace("Stopping at %s", schema->name); break; } if (max > 0 && (lpc == max || next > max)) { crm_trace("Upgrade limit reached at %s (lpc=%d, next=%d, max=%d)", schema->name, lpc, next, max); break; } next_schema = g_list_nth_data(known_schemas, next); CRM_ASSERT(next_schema != NULL); if (schema->transform == NULL /* possibly avoid transforming when readily valid (in general more restricted when crossing the major version boundary, as X.0 "transitional" version is expected to be more strict than it's successors that may re-allow constructs from previous major line) */ || validate_with_silent(xml, next_schema)) { crm_debug("%s-style configuration is also valid for %s", schema->name, next_schema->name); lpc = next; } else { crm_debug("Upgrading %s-style configuration to %s with %s.xsl", schema->name, next_schema->name, schema->transform); upgrade = apply_upgrade(xml, schema, to_logs); if (upgrade == NULL) { crm_err("Transformation %s.xsl failed", schema->transform); rc = -pcmk_err_transform_failed; } else if (validate_with(upgrade, next_schema, error_handler, GUINT_TO_POINTER(LOG_ERR))) { crm_info("Transformation %s.xsl successful", schema->transform); lpc = next; *best = next; free_xml(xml); xml = upgrade; rc = pcmk_ok; } else { crm_err("Transformation %s.xsl did not produce a valid configuration", schema->transform); crm_log_xml_info(upgrade, "transform:bad"); free_xml(upgrade); rc = -pcmk_err_schema_validation; } next = -1; } } if (transform == FALSE || rc != pcmk_ok) { /* we need some progress! */ lpc++; } } if (*best > match && *best) { pcmk__schema_t *best_schema = g_list_nth_data(known_schemas, *best); crm_info("%s the configuration from %s to %s", transform?"Transformed":"Upgraded", pcmk__s(value, ""), best_schema->name); crm_xml_add(xml, XML_ATTR_VALIDATION, best_schema->name); } *xml_blob = xml; free(value); return rc; } gboolean cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs) { gboolean rc = TRUE; const char *value = crm_element_value(*xml, XML_ATTR_VALIDATION); char *const orig_value = strdup(value == NULL ? "(none)" : value); int version = get_schema_version(value); int orig_version = version; int min_version = pcmk__find_x_0_schema_index(known_schemas); if (version < min_version) { // Current configuration schema is not acceptable, try to update xmlNode *converted = NULL; converted = copy_xml(*xml); update_validation(&converted, &version, 0, TRUE, to_logs); value = crm_element_value(converted, XML_ATTR_VALIDATION); if (version < min_version) { // Updated configuration schema is still not acceptable if (version < orig_version || orig_version == -1) { // We couldn't validate any schema at all if (to_logs) { pcmk__config_err("Cannot upgrade configuration (claiming " "schema %s) to at least %s because it " "does not validate with any schema from " "%s to %s", orig_value, get_schema_name(min_version), get_schema_name(orig_version), xml_latest_schema()); } else { fprintf(stderr, "Cannot upgrade configuration (claiming " "schema %s) to at least %s because it " "does not validate with any schema from " "%s to %s\n", orig_value, get_schema_name(min_version), get_schema_name(orig_version), xml_latest_schema()); } } else { // We updated configuration successfully, but still too low if (to_logs) { pcmk__config_err("Cannot upgrade configuration (claiming " "schema %s) to at least %s because it " "would not upgrade past %s", orig_value, get_schema_name(min_version), pcmk__s(value, "unspecified version")); } else { fprintf(stderr, "Cannot upgrade configuration (claiming " "schema %s) to at least %s because it " "would not upgrade past %s\n", orig_value, get_schema_name(min_version), pcmk__s(value, "unspecified version")); } } free_xml(converted); converted = NULL; rc = FALSE; } else { // Updated configuration schema is acceptable free_xml(*xml); *xml = converted; if (version < xml_latest_schema_index(known_schemas)) { if (to_logs) { pcmk__config_warn("Configuration with schema %s was " "internally upgraded to acceptable (but " "not most recent) %s", orig_value, get_schema_name(version)); } } else { if (to_logs) { crm_info("Configuration with schema %s was internally " "upgraded to latest version %s", orig_value, get_schema_name(version)); } } } } else if (version >= get_schema_version(PCMK__VALUE_NONE)) { // Schema validation is disabled if (to_logs) { pcmk__config_warn("Schema validation of configuration is disabled " "(enabling is encouraged and prevents common " "misconfigurations)"); } else { fprintf(stderr, "Schema validation of configuration is disabled " "(enabling is encouraged and prevents common " "misconfigurations)\n"); } } if (best_version) { *best_version = version; } free(orig_value); return rc; } +/*! + * \internal + * \brief Return a list of all schema files and any associated XSLT files + * later than the given one + * \brief Return a list of all schema versions later than the given one + * + * \param[in] schema The schema to compare against (for example, + * "pacemaker-3.1.rng" or "pacemaker-3.1") + * + * \note The caller is responsible for freeing both the returned list and + * the elements of the list + */ +GList * +pcmk__schema_files_later_than(const char *name) +{ + GList *lst = NULL; + pcmk__schema_version_t ver; + + if (!version_from_filename(name, &ver)) { + return lst; + } + + for (GList *iter = g_list_nth(known_schemas, xml_latest_schema_index(known_schemas)); + iter != NULL; iter = iter->prev) { + pcmk__schema_t *schema = iter->data; + char *s = NULL; + + if (schema_cmp(ver, schema->version) != -1) { + continue; + } + + s = crm_strdup_printf("%s.rng", schema->name); + lst = g_list_prepend(lst, s); + + if (schema->transform != NULL) { + char *xform = crm_strdup_printf("%s.xsl", schema->transform); + lst = g_list_prepend(lst, xform); + } + + if (schema->transform_enter != NULL) { + char *enter = crm_strdup_printf("%s.xsl", schema->transform_enter); + + lst = g_list_prepend(lst, enter); + + if (schema->transform_onleave) { + int last_dash = strrchr(enter, '-') - enter; + char *leave = crm_strdup_printf("%.*s-leave.xsl", last_dash, enter); + + lst = g_list_prepend(lst, leave); + } + } + } + + return lst; +} + void pcmk__log_known_schemas(void) { int lpc = 0; for (GList *iter = known_schemas; iter != NULL; iter = iter->next) { pcmk__schema_t *schema = iter->data; if (schema->transform != NULL) { crm_debug("known_schemas[%d] => %s (upgrades with %s.xsl)", lpc, schema->name, schema->transform); } else { crm_debug("known_schemas[%d] => %s", lpc, schema->name); } lpc++; } } diff --git a/lib/common/tests/schemas/Makefile.am b/lib/common/tests/schemas/Makefile.am index 5f485b3e9b..b5c5e7f3c9 100644 --- a/lib/common/tests/schemas/Makefile.am +++ b/lib/common/tests/schemas/Makefile.am @@ -1,16 +1,56 @@ # # Copyright 2023 the Pacemaker project contributors # # The version control history for this file may have further details. # # This source code is licensed under the GNU General Public License version 2 # or later (GPLv2+) WITHOUT ANY WARRANTY. # include $(top_srcdir)/mk/tap.mk include $(top_srcdir)/mk/unittest.mk +CFLAGS += -DPCMK__TEST_SCHEMA_DIR='"$(abs_builddir)/schemas"' + # Add "_test" to the end of all test program names to simplify .gitignore. -check_PROGRAMS = pcmk__xml_find_x_0_schema_index_test +check_PROGRAMS = pcmk__schema_files_later_than_test \ + pcmk__xml_find_x_0_schema_index_test TESTS = $(check_PROGRAMS) + +$(TESTS): setup-schema-dir + +# Set up a temporary schemas/ directory containing only some of the full set of +# pacemaker schema files. This lets us know exactly how many schemas are present, +# allowing us to write tests without having to make changes when new schemas are +# added. +# +# This directory contains the following: +# +# * pacemaker-next.rng - Used to verify that this sorts before all versions +# * upgrade-*.xsl - Required by various schema versions +# * pacemaker-[0-9]*.rng - We're only pulling in 15 schemas, which is enough +# to get everything through pacemaker-3.0.rng. This +# includes 2.10, needed so we can check that versions +# are compared as numbers instead of strings. +# * other RNG files - This catches everything except the pacemaker-*rng +# files. These files are included by the top-level +# pacemaker-*rng files, so we need them for tests. +# This will glob more than we need, but the extra ones +# won't get in the way. +.PHONY: setup-schema-dir +setup-schema-dir: + $(MKDIR_P) schemas + ( cd schemas ; \ + ln -sf $(abs_top_builddir)/xml/pacemaker-next.rng . ; \ + ln -sf $(abs_top_builddir)/xml/upgrade-*.xsl . ; \ + for f in $(shell ls -1v $(abs_top_builddir)/xml/pacemaker-[0-9]*.rng | head -15); do \ + ln -sf $$f $$(basename $$f); \ + done ; \ + for f in $(shell ls -1 $(top_srcdir)/xml/*.rng | grep -v pacemaker); do \ + ln -sf ../$$f $$(basename $$f); \ + done ) + +.PHONY: clean-local +clean-local: + -rm -rf schemas diff --git a/lib/common/tests/schemas/pcmk__schema_files_later_than_test.c b/lib/common/tests/schemas/pcmk__schema_files_later_than_test.c new file mode 100644 index 0000000000..50b5f43343 --- /dev/null +++ b/lib/common/tests/schemas/pcmk__schema_files_later_than_test.c @@ -0,0 +1,104 @@ +/* + * Copyright 2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +#include + +static int +setup(void **state) { + setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR, 1); + crm_schema_init(); + return 0; +} + +static int +teardown(void **state) { + crm_schema_cleanup(); + unsetenv("PCMK_schema_directory"); + return 0; +} + +static void +invalid_name(void **state) +{ + assert_null(pcmk__schema_files_later_than("xyz")); + assert_null(pcmk__schema_files_later_than("pacemaker-")); +} + +static void +valid_name(void **state) +{ + GList *schemas = NULL; + + schemas = pcmk__schema_files_later_than("pacemaker-1.0"); + assert_int_equal(g_list_length(schemas), 18); + /* There is no "pacemaker-1.1". */ + assert_string_equal("pacemaker-1.2.rng", g_list_nth_data(schemas, 0)); + assert_string_equal("upgrade-1.3.xsl", g_list_nth_data(schemas, 1)); + assert_string_equal("pacemaker-1.3.rng", g_list_nth_data(schemas, 2)); + assert_string_equal("pacemaker-2.0.rng", g_list_nth_data(schemas, 3)); + assert_string_equal("pacemaker-2.1.rng", g_list_nth_data(schemas, 4)); + assert_string_equal("pacemaker-2.2.rng", g_list_nth_data(schemas, 5)); + assert_string_equal("pacemaker-2.3.rng", g_list_nth_data(schemas, 6)); + assert_string_equal("pacemaker-2.4.rng", g_list_nth_data(schemas, 7)); + assert_string_equal("pacemaker-2.5.rng", g_list_nth_data(schemas, 8)); + assert_string_equal("pacemaker-2.6.rng", g_list_nth_data(schemas, 9)); + assert_string_equal("pacemaker-2.7.rng", g_list_nth_data(schemas, 10)); + assert_string_equal("pacemaker-2.8.rng", g_list_nth_data(schemas, 11)); + assert_string_equal("pacemaker-2.9.rng", g_list_nth_data(schemas, 12)); + assert_string_equal("upgrade-2.10-leave.xsl", g_list_nth_data(schemas, 13)); + assert_string_equal("upgrade-2.10-enter.xsl", g_list_nth_data(schemas, 14)); + assert_string_equal("upgrade-2.10.xsl", g_list_nth_data(schemas, 15)); + assert_string_equal("pacemaker-2.10.rng", g_list_nth_data(schemas, 16)); + assert_string_equal("pacemaker-3.0.rng", g_list_nth_data(schemas, 17)); + g_list_free_full(schemas, free); + + /* Adding .rng to the end of the schema we're requesting is also valid. */ + schemas = pcmk__schema_files_later_than("pacemaker-2.0.rng"); + assert_int_equal(g_list_length(schemas), 14); + assert_string_equal("pacemaker-2.1.rng", g_list_nth_data(schemas, 0)); + assert_string_equal("pacemaker-2.2.rng", g_list_nth_data(schemas, 1)); + assert_string_equal("pacemaker-2.3.rng", g_list_nth_data(schemas, 2)); + assert_string_equal("pacemaker-2.4.rng", g_list_nth_data(schemas, 3)); + assert_string_equal("pacemaker-2.5.rng", g_list_nth_data(schemas, 4)); + assert_string_equal("pacemaker-2.6.rng", g_list_nth_data(schemas, 5)); + assert_string_equal("pacemaker-2.7.rng", g_list_nth_data(schemas, 6)); + assert_string_equal("pacemaker-2.8.rng", g_list_nth_data(schemas, 7)); + assert_string_equal("pacemaker-2.9.rng", g_list_nth_data(schemas, 8)); + assert_string_equal("upgrade-2.10-leave.xsl", g_list_nth_data(schemas, 9)); + assert_string_equal("upgrade-2.10-enter.xsl", g_list_nth_data(schemas, 10)); + assert_string_equal("upgrade-2.10.xsl", g_list_nth_data(schemas, 11)); + assert_string_equal("pacemaker-2.10.rng", g_list_nth_data(schemas, 12)); + assert_string_equal("pacemaker-3.0.rng", g_list_nth_data(schemas, 13)); + g_list_free_full(schemas, free); + + /* Check that "pacemaker-2.10" counts as later than "pacemaker-2.9". */ + schemas = pcmk__schema_files_later_than("pacemaker-2.9"); + assert_int_equal(g_list_length(schemas), 5); + assert_string_equal("upgrade-2.10-leave.xsl", g_list_nth_data(schemas, 0)); + assert_string_equal("upgrade-2.10-enter.xsl", g_list_nth_data(schemas, 1)); + assert_string_equal("upgrade-2.10.xsl", g_list_nth_data(schemas, 2)); + assert_string_equal("pacemaker-2.10.rng", g_list_nth_data(schemas, 3)); + assert_string_equal("pacemaker-3.0.rng", g_list_nth_data(schemas, 4)); + g_list_free_full(schemas, free); + + /* And then something way in the future that will never apply due to our + * special schema directory. + */ + schemas = pcmk__schema_files_later_than("pacemaker-9.0"); + assert_null(schemas); +} + +PCMK__UNIT_TEST(setup, teardown, + cmocka_unit_test(invalid_name), + cmocka_unit_test(valid_name))