summaryrefslogtreecommitdiffstats
path: root/lib/common/schemas.c
diff options
context:
space:
mode:
Diffstat (limited to 'lib/common/schemas.c')
-rw-r--r--lib/common/schemas.c1303
1 files changed, 1303 insertions, 0 deletions
diff --git a/lib/common/schemas.c b/lib/common/schemas.c
new file mode 100644
index 0000000..88a3051
--- /dev/null
+++ b/lib/common/schemas.c
@@ -0,0 +1,1303 @@
+/*
+ * Copyright 2004-2022 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/msg_xml.h>
+#include <crm/common/xml.h>
+#include <crm/common/xml_internal.h> /* PCMK__XML_LOG_BASE */
+
+typedef struct {
+ unsigned char v[2];
+} schema_version_t;
+
+#define SCHEMA_ZERO { .v = { 0, 0 } }
+
+#define schema_scanf(s, prefix, version, suffix) \
+ sscanf((s), prefix "%hhu.%hhu" suffix, &((version).v[0]), &((version).v[1]))
+
+#define schema_strdup_printf(prefix, version, suffix) \
+ crm_strdup_printf(prefix "%u.%u" suffix, (version).v[0], (version).v[1])
+
+typedef struct {
+ xmlRelaxNGPtr rng;
+ xmlRelaxNGValidCtxtPtr valid;
+ xmlRelaxNGParserCtxtPtr parser;
+} relaxng_ctx_cache_t;
+
+enum schema_validator_e {
+ schema_validator_none,
+ schema_validator_rng
+};
+
+struct schema_s {
+ char *name;
+ char *transform;
+ void *cache;
+ enum schema_validator_e validator;
+ int after_transform;
+ schema_version_t version;
+ char *transform_enter;
+ bool transform_onleave;
+};
+
+static struct schema_s *known_schemas = NULL;
+static int xml_schema_max = 0;
+static bool silent_logging = FALSE;
+
+static void
+xml_log(int priority, const char *fmt, ...)
+G_GNUC_PRINTF(2, 3);
+
+static void
+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)
+{
+ // @COMPAT: pacemaker-next is deprecated since 2.1.5
+ return xml_schema_max - 3; // index from 0, ignore "pacemaker-next"/"none"
+}
+
+static int
+xml_minimum_schema_index(void)
+{
+ static int best = 0;
+ if (best == 0) {
+ int lpc = 0;
+
+ best = xml_latest_schema_index();
+ for (lpc = best; lpc > 0; lpc--) {
+ if (known_schemas[lpc].version.v[0]
+ < known_schemas[best].version.v[0]) {
+ return best;
+ } else {
+ best = lpc;
+ }
+ }
+ best = xml_latest_schema_index();
+ }
+ return best;
+}
+
+const char *
+xml_latest_schema(void)
+{
+ return get_schema_name(xml_latest_schema_index());
+}
+
+static inline bool
+version_from_filename(const char *filename, schema_version_t *version)
+{
+ int rc = schema_scanf(filename, "pacemaker-", *version, ".rng");
+
+ return (rc == 2);
+}
+
+static int
+schema_filter(const struct dirent *a)
+{
+ int rc = 0;
+ 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_sort(const struct dirent **a, const struct dirent **b)
+{
+ schema_version_t a_version = SCHEMA_ZERO;
+ schema_version_t b_version = SCHEMA_ZERO;
+
+ if (!version_from_filename(a[0]->d_name, &a_version)
+ || !version_from_filename(b[0]->d_name, &b_version)) {
+ // Shouldn't be possible, but makes static analysis happy
+ return 0;
+ }
+
+ for (int i = 0; i < 2; ++i) {
+ if (a_version.v[i] < b_version.v[i]) {
+ return -1;
+ } else if (a_version.v[i] > b_version.v[i]) {
+ return 1;
+ }
+ }
+ return 0;
+}
+
+/*!
+ * \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 schema_validator_e validator, const schema_version_t *version,
+ const char *name, const char *transform,
+ const char *transform_enter, bool transform_onleave,
+ int after_transform)
+{
+ int last = xml_schema_max;
+ bool have_version = FALSE;
+
+ xml_schema_max++;
+ known_schemas = pcmk__realloc(known_schemas,
+ xml_schema_max * sizeof(struct schema_s));
+ CRM_ASSERT(known_schemas != NULL);
+ memset(known_schemas+last, 0, sizeof(struct schema_s));
+ known_schemas[last].validator = validator;
+ known_schemas[last].after_transform = after_transform;
+
+ for (int i = 0; i < 2; ++i) {
+ known_schemas[last].version.v[i] = version->v[i];
+ if (version->v[i]) {
+ have_version = TRUE;
+ }
+ }
+ if (have_version) {
+ known_schemas[last].name = schema_strdup_printf("pacemaker-", *version, "");
+ } else {
+ CRM_ASSERT(name);
+ schema_scanf(name, "%*[^-]-", known_schemas[last].version, "");
+ known_schemas[last].name = strdup(name);
+ }
+
+ if (transform) {
+ known_schemas[last].transform = strdup(transform);
+ }
+ if (transform_enter) {
+ known_schemas[last].transform_enter = strdup(transform_enter);
+ }
+ known_schemas[last].transform_onleave = transform_onleave;
+ if (after_transform == 0) {
+ after_transform = xml_schema_max; /* upgrade is a one-way */
+ }
+ known_schemas[last].after_transform = after_transform;
+
+ if (known_schemas[last].after_transform < 0) {
+ crm_debug("Added supported schema %d: %s",
+ last, known_schemas[last].name);
+
+ } else if (known_schemas[last].transform) {
+ crm_debug("Added supported schema %d: %s (upgrades to %d with %s.xsl)",
+ last, known_schemas[last].name,
+ known_schemas[last].after_transform,
+ known_schemas[last].transform);
+
+ } else {
+ crm_debug("Added supported schema %d: %s (upgrades to %d)",
+ last, known_schemas[last].name,
+ known_schemas[last].after_transform);
+ }
+}
+
+/*!
+ * \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 schema_version_t *version, int next,
+ 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;
+ next = -1;
+ rc = ENOENT;
+ }
+
+ add_schema(schema_validator_rng, version, NULL,
+ transform_upgrade, transform_enter, transform_onleave, next);
+
+ 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 schema_version_t zero = SCHEMA_ZERO;
+
+ wrap_libxslt(false);
+
+ max = scandir(base, &namelist, schema_filter, schema_sort);
+ 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;
+ int next = 0;
+ schema_version_t version = SCHEMA_ZERO;
+
+ if (!version_from_filename(namelist[lpc]->d_name, &version)) {
+ // Shouldn't be possible, but makes static analysis happy
+ crm_err("Skipping schema '%s': could not parse version",
+ namelist[lpc]->d_name);
+ continue;
+ }
+ if ((lpc + 1) < max) {
+ schema_version_t next_version = SCHEMA_ZERO;
+
+ if (version_from_filename(namelist[lpc+1]->d_name, &next_version)
+ && (version.v[0] < next_version.v[0])) {
+ transform_expected = TRUE;
+ }
+
+ } else {
+ next = -1;
+ }
+ if (add_schema_by_version(&version, next, transform_expected)
+ == ENOENT) {
+ break;
+ }
+ }
+
+ for (lpc = 0; lpc < max; lpc++) {
+ free(namelist[lpc]);
+ }
+ free(namelist);
+ }
+
+ // @COMPAT: Deprecated since 2.1.5
+ add_schema(schema_validator_rng, &zero, "pacemaker-next",
+ NULL, NULL, FALSE, -1);
+
+ add_schema(schema_validator_none, &zero, PCMK__VALUE_NONE,
+ NULL, NULL, FALSE, -1);
+}
+
+#if 0
+static void
+relaxng_invalid_stderr(void *userData, xmlErrorPtr error)
+{
+ /*
+ Structure xmlError
+ struct _xmlError {
+ int domain : What part of the library raised this er
+ int code : The error code, e.g. an xmlParserError
+ char * message : human-readable informative error messag
+ xmlErrorLevel level : how consequent is the error
+ char * file : the filename
+ int line : the line number if available
+ char * str1 : extra string information
+ char * str2 : extra string information
+ char * str3 : extra string information
+ int int1 : extra number information
+ int int2 : column number of the error or 0 if N/A
+ void * ctxt : the parser context if available
+ void * node : the node in the tree
+ }
+ */
+ crm_err("Structured error: line=%d, level=%d %s", error->line, error->level, error->message);
+}
+#endif
+
+static gboolean
+validate_with_relaxng(xmlDocPtr doc, gboolean to_logs, 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));
+
+ xmlLoadExtDtdDefaultValue = 1;
+ ctx->parser = xmlRelaxNGNewParserCtxt(relaxng_file);
+ CRM_CHECK(ctx->parser != NULL, goto cleanup);
+
+ if (to_logs) {
+ xmlRelaxNGSetParserErrors(ctx->parser,
+ (xmlRelaxNGValidityErrorFunc) xml_log,
+ (xmlRelaxNGValidityWarningFunc) xml_log,
+ GUINT_TO_POINTER(LOG_ERR));
+ } 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 (to_logs) {
+ xmlRelaxNGSetValidErrors(ctx->valid,
+ (xmlRelaxNGValidityErrorFunc) xml_log,
+ (xmlRelaxNGValidityWarningFunc) xml_log,
+ GUINT_TO_POINTER(LOG_ERR));
+ } else {
+ xmlRelaxNGSetValidErrors(ctx->valid,
+ (xmlRelaxNGValidityErrorFunc) fprintf,
+ (xmlRelaxNGValidityWarningFunc) fprintf,
+ stderr);
+ }
+ }
+
+ /* xmlRelaxNGSetValidStructuredErrors( */
+ /* valid, relaxng_invalid_stderr, valid); */
+
+ xmlLineNumbersDefault(1);
+ 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;
+}
+
+/*!
+ * \internal
+ * \brief Clean up global memory associated with XML schemas
+ */
+void
+crm_schema_cleanup(void)
+{
+ int lpc;
+ relaxng_ctx_cache_t *ctx = NULL;
+
+ for (lpc = 0; lpc < xml_schema_max; lpc++) {
+
+ switch (known_schemas[lpc].validator) {
+ case schema_validator_none: // not cached
+ break;
+ case schema_validator_rng: // cached
+ ctx = (relaxng_ctx_cache_t *) known_schemas[lpc].cache;
+ if (ctx == NULL) {
+ break;
+ }
+ if (ctx->parser != NULL) {
+ xmlRelaxNGFreeParserCtxt(ctx->parser);
+ }
+ if (ctx->valid != NULL) {
+ xmlRelaxNGFreeValidCtxt(ctx->valid);
+ }
+ if (ctx->rng != NULL) {
+ xmlRelaxNGFree(ctx->rng);
+ }
+ free(ctx);
+ known_schemas[lpc].cache = NULL;
+ break;
+ }
+ free(known_schemas[lpc].name);
+ free(known_schemas[lpc].transform);
+ free(known_schemas[lpc].transform_enter);
+ }
+ free(known_schemas);
+ known_schemas = NULL;
+
+ wrap_libxslt(true);
+}
+
+static gboolean
+validate_with(xmlNode *xml, int method, gboolean to_logs)
+{
+ xmlDocPtr doc = NULL;
+ gboolean valid = FALSE;
+ char *file = NULL;
+
+ if (method < 0) {
+ return FALSE;
+ }
+
+ if (known_schemas[method].validator == schema_validator_none) {
+ return TRUE;
+ }
+
+ CRM_CHECK(xml != NULL, return FALSE);
+
+ if (pcmk__str_eq(known_schemas[method].name, "pacemaker-next",
+ pcmk__str_none)) {
+ crm_warn("The pacemaker-next schema is deprecated and will be removed "
+ "in a future release.");
+ }
+
+ doc = getDocPtr(xml);
+ file = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng,
+ known_schemas[method].name);
+
+ crm_trace("Validating with %s (type=%d)",
+ pcmk__s(file, "missing schema"), known_schemas[method].validator);
+ switch (known_schemas[method].validator) {
+ case schema_validator_rng:
+ valid =
+ validate_with_relaxng(doc, to_logs, file,
+ (relaxng_ctx_cache_t **) & (known_schemas[method].cache));
+ break;
+ default:
+ crm_err("Unknown validator type: %d",
+ known_schemas[method].validator);
+ break;
+ }
+
+ free(file);
+ return valid;
+}
+
+static bool
+validate_with_silent(xmlNode *xml, int method)
+{
+ bool rc, sl_backup = silent_logging;
+ silent_logging = TRUE;
+ rc = validate_with(xml, method, TRUE);
+ 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(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 = xmlParseFile(filename);
+ 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)
+{
+ int version = 0;
+
+ if (validation == NULL) {
+ validation = crm_element_value(xml_blob, XML_ATTR_VALIDATION);
+ }
+
+ if (validation == NULL) {
+ int lpc = 0;
+ bool valid = FALSE;
+
+ for (lpc = 0; lpc < xml_schema_max; lpc++) {
+ if (validate_with(xml_blob, lpc, FALSE)) {
+ valid = TRUE;
+ crm_xml_add(xml_blob, XML_ATTR_VALIDATION,
+ known_schemas[lpc].name);
+ crm_info("XML validated against %s", known_schemas[lpc].name);
+ if(known_schemas[lpc].after_transform == 0) {
+ break;
+ }
+ }
+ }
+
+ return valid;
+ }
+
+ version = get_schema_version(validation);
+ if (strcmp(validation, PCMK__VALUE_NONE) == 0) {
+ return TRUE;
+ } else if (version < xml_schema_max) {
+ return validate_with(xml_blob, version, to_logs);
+ }
+
+ crm_err("Unknown validator: %s", validation);
+ return FALSE;
+}
+
+static void
+cib_upgrade_err(void *ctx, const char *fmt, ...)
+G_GNUC_PRINTF(2, 3);
+
+/* 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
+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);
+}
+
+
+/* Denotes temporary emergency fix for "xmldiff'ing not text-node-ready";
+ proper fix is most likely to teach __xml_diff_object and friends to
+ deal with XML_TEXT_NODE (and more?), i.e., those nodes currently
+ missing "_private" field (implicitly as NULL) which clashes with
+ unchecked accesses (e.g. in __xml_offset) -- the outcome may be that
+ those unexpected XML nodes will simply be ignored for the purpose of
+ diff'ing, or it may be made more robust, or per the user's preference
+ (which then may be exposed as crm_diff switch).
+
+ Said XML_TEXT_NODE may appear unexpectedly due to how upgrade-2.10.xsl
+ is arranged.
+
+ The emergency fix is simple: reparse XSLT output with blank-ignoring
+ parser. */
+#ifndef PCMK_SCHEMAS_EMERGENCY_XSLT
+#define PCMK_SCHEMAS_EMERGENCY_XSLT 1
+#endif
+
+static xmlNode *
+apply_transformation(xmlNode *xml, const char *transform, gboolean to_logs)
+{
+ char *xform = NULL;
+ xmlNode *out = NULL;
+ xmlDocPtr res = NULL;
+ xmlDocPtr doc = NULL;
+ xsltStylesheet *xslt = NULL;
+#if PCMK_SCHEMAS_EMERGENCY_XSLT != 0
+ xmlChar *emergency_result;
+ int emergency_txt_len;
+ int emergency_res;
+#endif
+
+ CRM_CHECK(xml != NULL, return FALSE);
+ doc = getDocPtr(xml);
+ xform = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
+ transform);
+
+ xmlLoadExtDtdDefaultValue = 1;
+ xmlSubstituteEntitiesDefault(1);
+
+ /* 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, doc, NULL);
+ CRM_CHECK(res != NULL, goto cleanup);
+
+ xsltSetGenericErrorFunc(NULL, NULL); /* restore default one */
+
+
+#if PCMK_SCHEMAS_EMERGENCY_XSLT != 0
+ emergency_res = xsltSaveResultToString(&emergency_result,
+ &emergency_txt_len, res, xslt);
+ xmlFreeDoc(res);
+ CRM_CHECK(emergency_res == 0, goto cleanup);
+ out = string2xml((const char *) emergency_result);
+ free(emergency_result);
+#else
+ out = xmlDocGetRootElement(res);
+#endif
+
+ 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 struct schema_s *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)
+{
+ if (version < 0 || version >= xml_schema_max) {
+ return "unknown";
+ }
+ return known_schemas[version].name;
+}
+
+int
+get_schema_version(const char *name)
+{
+ int lpc = 0;
+
+ if (name == NULL) {
+ name = PCMK__VALUE_NONE;
+ }
+ for (; lpc < xml_schema_max; lpc++) {
+ if (pcmk__str_eq(name, known_schemas[lpc].name, pcmk__str_casei)) {
+ return 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();
+ int lpc = 0, match = -1, rc = pcmk_ok;
+ int next = -1; /* -1 denotes "inactive" value */
+
+ CRM_CHECK(best != NULL, return -EINVAL);
+ *best = 0;
+
+ CRM_CHECK(xml_blob != NULL, return -EINVAL);
+ CRM_CHECK(*xml_blob != 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) {
+ crm_debug("Testing '%s' validation (%d of %d)",
+ known_schemas[lpc].name ? known_schemas[lpc].name : "<unset>",
+ lpc, max_stable_schemas);
+
+ if (validate_with(xml, lpc, to_logs) == FALSE) {
+ if (next != -1) {
+ crm_info("Configuration not valid for schema: %s",
+ known_schemas[lpc].name);
+ next = -1;
+ } else {
+ crm_trace("%s validation failed",
+ known_schemas[lpc].name ? known_schemas[lpc].name : "<unset>");
+ }
+ if (*best) {
+ /* we've satisfied the validation, no need to check further */
+ break;
+ }
+ rc = -pcmk_err_schema_validation;
+
+ } else {
+ if (next != -1) {
+ crm_debug("Configuration valid for schema: %s",
+ known_schemas[next].name);
+ next = -1;
+ }
+ rc = pcmk_ok;
+ }
+
+ if (rc == pcmk_ok) {
+ *best = lpc;
+ }
+
+ if (rc == pcmk_ok && transform) {
+ xmlNode *upgrade = NULL;
+ next = known_schemas[lpc].after_transform;
+
+ if (next <= lpc) {
+ /* There is no next version, or next would regress */
+ crm_trace("Stopping at %s", known_schemas[lpc].name);
+ break;
+
+ } else if (max > 0 && (lpc == max || next > max)) {
+ crm_trace("Upgrade limit reached at %s (lpc=%d, next=%d, max=%d)",
+ known_schemas[lpc].name, lpc, next, max);
+ break;
+
+ } else if (known_schemas[lpc].transform == NULL
+ /* possibly avoid transforming when readily valid
+ (in general more restricted when crossing the major
+ version boundary, as X.0 "transitional" version is
+ expected to be more strict than it's successors that
+ may re-allow constructs from previous major line) */
+ || validate_with_silent(xml, next)) {
+ crm_debug("%s-style configuration is also valid for %s",
+ known_schemas[lpc].name, known_schemas[next].name);
+
+ lpc = next;
+
+ } else {
+ crm_debug("Upgrading %s-style configuration to %s with %s.xsl",
+ known_schemas[lpc].name, known_schemas[next].name,
+ known_schemas[lpc].transform);
+
+ upgrade = apply_upgrade(xml, &known_schemas[lpc], to_logs);
+ if (upgrade == NULL) {
+ crm_err("Transformation %s.xsl failed",
+ known_schemas[lpc].transform);
+ rc = -pcmk_err_transform_failed;
+
+ } else if (validate_with(upgrade, next, to_logs)) {
+ crm_info("Transformation %s.xsl successful",
+ known_schemas[lpc].transform);
+ lpc = next;
+ *best = next;
+ free_xml(xml);
+ xml = upgrade;
+ rc = pcmk_ok;
+
+ } else {
+ crm_err("Transformation %s.xsl did not produce a valid configuration",
+ known_schemas[lpc].transform);
+ crm_log_xml_info(upgrade, "transform:bad");
+ free_xml(upgrade);
+ rc = -pcmk_err_schema_validation;
+ }
+ next = -1;
+ }
+ }
+
+ if (transform == FALSE || rc != pcmk_ok) {
+ /* we need some progress! */
+ lpc++;
+ }
+ }
+
+ if (*best > match && *best) {
+ crm_info("%s the configuration from %s to %s",
+ transform?"Transformed":"Upgraded",
+ value ? value : "<none>", known_schemas[*best].name);
+ crm_xml_add(xml, XML_ATTR_VALIDATION, known_schemas[*best].name);
+ }
+
+ *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 = xml_minimum_schema_index();
+
+ 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()) {
+ 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;
+}