diff options
Diffstat (limited to 'lib/common/schemas.c')
-rw-r--r-- | lib/common/schemas.c | 1303 |
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; +} |