diff options
Diffstat (limited to 'lib/common/schemas.c')
-rw-r--r-- | lib/common/schemas.c | 1561 |
1 files changed, 1032 insertions, 529 deletions
diff --git a/lib/common/schemas.c b/lib/common/schemas.c index b3c09eb..16f2ccb 100644 --- a/lib/common/schemas.c +++ b/lib/common/schemas.c @@ -1,5 +1,5 @@ /* - * Copyright 2004-2023 the Pacemaker project contributors + * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -22,19 +22,13 @@ #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; +#include "crmcommon_private.h" #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]) @@ -44,31 +38,11 @@ typedef struct { 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 GList *known_schemas = NULL; +static bool initialized = false; static bool silent_logging = FALSE; -static void -xml_log(int priority, const char *fmt, ...) -G_GNUC_PRINTF(2, 3); - -static void +static void G_GNUC_PRINTF(2, 3) xml_log(int priority, const char *fmt, ...) { va_list ap; @@ -84,50 +58,114 @@ xml_log(int priority, const char *fmt, ...) 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" + /* This function assumes that crm_schema_init() has been called beforehand, + * so we have at least three schemas (one real schema, the "pacemaker-next" + * schema, and the "none" schema). + * + * @COMPAT: pacemaker-next is deprecated since 2.1.5 and none since 2.1.8. + * Update this when we drop those. + */ + return g_list_length(known_schemas) - 3; } -static int -xml_minimum_schema_index(void) +/*! + * \internal + * \brief Return the schema entry of the highest-versioned schema + * + * \return Schema entry of highest-versioned schema (or NULL on error) + */ +static GList * +get_highest_schema(void) { - 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; + /* The highest numerically versioned schema is the one before pacemaker-next + * + * @COMPAT pacemaker-next is deprecated since 2.1.5 + */ + GList *entry = pcmk__get_schema("pacemaker-next"); + + CRM_ASSERT((entry != NULL) && (entry->prev != NULL)); + return entry->prev; } +/*! + * \internal + * \brief Return the name of the highest-versioned schema + * + * \return Name of highest-versioned schema (or NULL on error) + */ const char * -xml_latest_schema(void) +pcmk__highest_schema_name(void) { - return get_schema_name(xml_latest_schema_index()); + GList *entry = get_highest_schema(); + + return ((pcmk__schema_t *)(entry->data))->name; } -static inline bool -version_from_filename(const char *filename, schema_version_t *version) +/*! + * \internal + * \brief Find first entry of highest major schema version series + * + * \return Schema entry of first schema with highest major version + */ +GList * +pcmk__find_x_0_schema(void) { - int rc = schema_scanf(filename, "pacemaker-", *version, ".rng"); +#if defined(PCMK__UNIT_TESTING) + /* If we're unit testing, this can't be static because it'll stick + * around from one test run to the next. It needs to be cleared out + * every time. + */ + GList *x_0_entry = NULL; +#else + static GList *x_0_entry = NULL; +#endif + + pcmk__schema_t *highest_schema = NULL; + + if (x_0_entry != NULL) { + return x_0_entry; + } + x_0_entry = get_highest_schema(); + highest_schema = x_0_entry->data; + + for (GList *iter = x_0_entry->prev; iter != NULL; iter = iter->prev) { + pcmk__schema_t *schema = iter->data; + + /* We've found a schema in an older major version series. Return + * the index of the first one in the same major version series as + * the highest schema. + */ + if (schema->version.v[0] < highest_schema->version.v[0]) { + x_0_entry = iter->next; + break; + } + + /* We're out of list to examine. This probably means there was only + * one major version series, so return the first schema entry. + */ + if (iter->prev == NULL) { + x_0_entry = known_schemas->data; + break; + } + } + return x_0_entry; +} - return (rc == 2); +static inline bool +version_from_filename(const char *filename, pcmk__schema_version_t *version) +{ + if (pcmk__ends_with(filename, ".rng")) { + return sscanf(filename, "pacemaker-%hhu.%hhu.rng", &(version->v[0]), &(version->v[1])) == 2; + } else { + return sscanf(filename, "pacemaker-%hhu.%hhu", &(version->v[0]), &(version->v[1])) == 2; + } } static int schema_filter(const struct dirent *a) { int rc = 0; - schema_version_t version = SCHEMA_ZERO; + pcmk__schema_version_t version = SCHEMA_ZERO; if (strstr(a->d_name, "pacemaker-") != a->d_name) { /* crm_trace("%s - wrong prefix", a->d_name); */ @@ -147,17 +185,8 @@ schema_filter(const struct dirent *a) } static int -schema_sort(const struct dirent **a, const struct dirent **b) +schema_cmp(pcmk__schema_version_t a_version, pcmk__schema_version_t b_version) { - 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; @@ -168,6 +197,21 @@ schema_sort(const struct dirent **a, const struct dirent **b) 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. @@ -176,63 +220,35 @@ schema_sort(const struct dirent **a, const struct dirent **b) * through \c add_schema_by_version. */ static void -add_schema(enum schema_validator_e validator, const schema_version_t *version, +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, - int after_transform) + const char *transform_enter, bool transform_onleave) { - int last = xml_schema_max; - bool have_version = FALSE; + pcmk__schema_t *schema = NULL; - 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; + schema = pcmk__assert_alloc(1, sizeof(pcmk__schema_t)); - 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, ""); + schema->validator = validator; + schema->version.v[0] = version->v[0]; + schema->version.v[1] = version->v[1]; + schema->transform_onleave = transform_onleave; + // schema->schema_index is set after all schemas are loaded and sorted + + if (version->v[0] || version->v[1]) { + schema->name = schema_strdup_printf("pacemaker-", *version, ""); } else { - CRM_ASSERT(name); - schema_scanf(name, "%*[^-]-", known_schemas[last].version, ""); - known_schemas[last].name = strdup(name); + schema->name = pcmk__str_copy(name); } if (transform) { - known_schemas[last].transform = strdup(transform); + schema->transform = pcmk__str_copy(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 */ + schema->transform_enter = pcmk__str_copy(transform_enter); } - 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); - } + known_schemas = g_list_prepend(known_schemas, schema); } /*! @@ -264,8 +280,7 @@ add_schema(enum schema_validator_e validator, const schema_version_t *version, * . name convention: (see "upgrade-enter") */ static int -add_schema_by_version(const schema_version_t *version, int next, - bool transform_expected) +add_schema_by_version(const pcmk__schema_version_t *version, bool transform_expected) { bool transform_onleave = FALSE; int rc = pcmk_rc_ok; @@ -321,12 +336,11 @@ add_schema_by_version(const schema_version_t *version, int next, 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); + add_schema(pcmk__schema_validator_rng, version, NULL, + transform_upgrade, transform_enter, transform_onleave); free(transform_upgrade); free(transform_enter); @@ -366,6 +380,85 @@ wrap_libxslt(bool finalize) } } +void +pcmk__load_schemas_from_dir(const char *dir) +{ + int lpc, max; + struct dirent **namelist = NULL; + + max = scandir(dir, &namelist, schema_filter, schema_cmp_directory); + if (max < 0) { + crm_warn("Could not load schemas from %s: %s", dir, strerror(errno)); + return; + } + + for (lpc = 0; lpc < max; lpc++) { + bool transform_expected = false; + pcmk__schema_version_t version = SCHEMA_ZERO; + + if (!version_from_filename(namelist[lpc]->d_name, &version)) { + // Shouldn't be possible, but makes static analysis happy + crm_warn("Skipping schema '%s': could not parse version", + namelist[lpc]->d_name); + continue; + } + if ((lpc + 1) < max) { + pcmk__schema_version_t next_version = SCHEMA_ZERO; + + if (version_from_filename(namelist[lpc+1]->d_name, &next_version) + && (version.v[0] < next_version.v[0])) { + transform_expected = true; + } + } + + if (add_schema_by_version(&version, transform_expected) != pcmk_rc_ok) { + break; + } + } + + for (lpc = 0; lpc < max; lpc++) { + free(namelist[lpc]); + } + + free(namelist); +} + +static gint +schema_sort_GCompareFunc(gconstpointer a, gconstpointer b) +{ + const pcmk__schema_t *schema_a = a; + const pcmk__schema_t *schema_b = b; + + // @COMPAT pacemaker-next is deprecated since 2.1.5 and none since 2.1.8 + if (pcmk__str_eq(schema_a->name, "pacemaker-next", pcmk__str_none)) { + if (pcmk__str_eq(schema_b->name, PCMK_VALUE_NONE, pcmk__str_none)) { + return -1; + } else { + return 1; + } + } else if (pcmk__str_eq(schema_a->name, PCMK_VALUE_NONE, pcmk__str_none)) { + return 1; + } else if (pcmk__str_eq(schema_b->name, "pacemaker-next", pcmk__str_none)) { + return -1; + } else { + return schema_cmp(schema_a->version, schema_b->version); + } +} + +/*! + * \internal + * \brief Sort the list of known schemas such that all pacemaker-X.Y are in + * version order, then pacemaker-next, then none + * + * This function should be called whenever additional schemas are loaded using + * pcmk__load_schemas_from_dir(), after the initial sets in crm_schema_init(). + */ +void +pcmk__sort_schemas(void) +{ + known_schemas = g_list_sort(known_schemas, schema_sort_GCompareFunc); +} + /*! * \internal * \brief Load pacemaker schemas into cache @@ -376,79 +469,67 @@ wrap_libxslt(bool finalize) void crm_schema_init(void) { - int lpc, max; - char *base = pcmk__xml_artefact_root(pcmk__xml_artefact_ns_legacy_rng); - struct dirent **namelist = NULL; - const schema_version_t zero = SCHEMA_ZERO; + if (!initialized) { + const char *remote_schema_dir = pcmk__remote_schema_dir(); + char *base = pcmk__xml_artefact_root(pcmk__xml_artefact_ns_legacy_rng); + const pcmk__schema_version_t zero = SCHEMA_ZERO; + int schema_index = 0; - wrap_libxslt(false); + initialized = true; - max = scandir(base, &namelist, schema_filter, schema_sort); - if (max < 0) { - crm_notice("scandir(%s) failed: %s (%d)", base, strerror(errno), errno); - free(base); + wrap_libxslt(false); - } else { + pcmk__load_schemas_from_dir(base); + pcmk__load_schemas_from_dir(remote_schema_dir); 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; - } + // @COMPAT: Deprecated since 2.1.5 + add_schema(pcmk__schema_validator_rng, &zero, "pacemaker-next", NULL, + NULL, FALSE); + // @COMPAT Deprecated since 2.1.8 + add_schema(pcmk__schema_validator_none, &zero, PCMK_VALUE_NONE, NULL, + NULL, FALSE); + + /* add_schema() prepends items to the list, so in the simple case, this + * just reverses the list. However if there were any remote schemas, + * sorting is necessary. + */ + pcmk__sort_schemas(); + + // Now set the schema indexes and log the final result + for (GList *iter = known_schemas; iter != NULL; iter = iter->next) { + pcmk__schema_t *schema = iter->data; + + if (schema->transform == NULL) { + crm_debug("Loaded schema %d: %s", schema_index, schema->name); } else { - next = -1; - } - if (add_schema_by_version(&version, next, transform_expected) - == ENOENT) { - break; + crm_debug("Loaded schema %d: %s (upgrades with %s.xsl)", + schema_index, schema->name, schema->transform); } + schema->schema_index = schema_index++; } - - 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); } -static gboolean -validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler, void *error_handler_context, const char *relaxng_file, +static bool +validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler, + void *error_handler_context, const char *relaxng_file, relaxng_ctx_cache_t **cached_ctx) { int rc = 0; - gboolean valid = TRUE; + bool valid = true; relaxng_ctx_cache_t *ctx = NULL; - CRM_CHECK(doc != NULL, return FALSE); - CRM_CHECK(relaxng_file != NULL, return FALSE); + 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 = pcmk__assert_alloc(1, sizeof(relaxng_ctx_cache_t)); ctx->parser = xmlRelaxNGNewParserCtxt(relaxng_file); CRM_CHECK(ctx->parser != NULL, goto cleanup); @@ -488,7 +569,7 @@ validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler, rc = xmlRelaxNGValidateDoc(ctx->valid, doc); if (rc > 0) { - valid = FALSE; + valid = false; } else if (rc < 0) { crm_err("Internal libxml error during validation"); @@ -515,6 +596,45 @@ validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler, 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); + free(schema); +} + /*! * \internal * \brief Clean up global memory associated with XML schemas @@ -522,62 +642,86 @@ validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler, void crm_schema_cleanup(void) { - int lpc; - relaxng_ctx_cache_t *ctx = NULL; + if (known_schemas != NULL) { + g_list_free_full(known_schemas, free_schema); + known_schemas = NULL; + } + initialized = false; - for (lpc = 0; lpc < xml_schema_max; lpc++) { + wrap_libxslt(true); +} - 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; +/*! + * \internal + * \brief Get schema list entry corresponding to a schema name + * + * \param[in] name Name of schema to get + * + * \return Schema list entry corresponding to \p name, or NULL if unknown + */ +GList * +pcmk__get_schema(const char *name) +{ + // @COMPAT Not specifying a schema name is deprecated since 2.1.8 + if (name == NULL) { + name = PCMK_VALUE_NONE; + } + for (GList *iter = known_schemas; iter != NULL; iter = iter->next) { + pcmk__schema_t *schema = iter->data; + + if (pcmk__str_eq(name, schema->name, pcmk__str_casei)) { + return iter; } - free(known_schemas[lpc].name); - free(known_schemas[lpc].transform); - free(known_schemas[lpc].transform_enter); } - free(known_schemas); - known_schemas = NULL; + return NULL; +} - wrap_libxslt(true); +/*! + * \internal + * \brief Compare two schema version numbers given the schema names + * + * \param[in] schema1 Name of first schema to compare + * \param[in] schema2 Name of second schema to compare + * + * \return Standard comparison result (negative integer if \p schema1 has the + * lower version number, positive integer if \p schema1 has the higher + * version number, of 0 if the version numbers are equal) + */ +int +pcmk__cmp_schemas_by_name(const char *schema1_name, const char *schema2_name) +{ + GList *entry1 = pcmk__get_schema(schema1_name); + GList *entry2 = pcmk__get_schema(schema2_name); + + if (entry1 == NULL) { + return (entry2 == NULL)? 0 : -1; + + } else if (entry2 == NULL) { + return 1; + + } else { + pcmk__schema_t *schema1 = entry1->data; + pcmk__schema_t *schema2 = entry2->data; + + return schema1->schema_index - schema2->schema_index; + } } -static gboolean -validate_with(xmlNode *xml, int method, xmlRelaxNGValidityErrorFunc error_handler, void* error_handler_context) +static bool +validate_with(xmlNode *xml, pcmk__schema_t *schema, + xmlRelaxNGValidityErrorFunc error_handler, + void *error_handler_context) { - gboolean valid = FALSE; + bool valid = false; char *file = NULL; - struct schema_s *schema = NULL; relaxng_ctx_cache_t **cache = NULL; - if (method < 0) { - return FALSE; - } - - schema = &(known_schemas[method]); - if (schema->validator == schema_validator_none) { - return TRUE; + if (schema == NULL) { + return false; } - 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."); + if (schema->validator == pcmk__schema_validator_none) { + return true; } file = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng, @@ -586,13 +730,12 @@ validate_with(xmlNode *xml, int method, xmlRelaxNGValidityErrorFunc error_handle crm_trace("Validating with %s (type=%d)", pcmk__s(file, "missing schema"), schema->validator); switch (schema->validator) { - case schema_validator_rng: + 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", - known_schemas[method].validator); + crm_err("Unknown validator type: %d", schema->validator); break; } @@ -601,124 +744,74 @@ validate_with(xmlNode *xml, int method, xmlRelaxNGValidityErrorFunc error_handle } static bool -validate_with_silent(xmlNode *xml, int method) +validate_with_silent(xmlNode *xml, pcmk__schema_t *schema) { bool rc, sl_backup = silent_logging; silent_logging = TRUE; - rc = validate_with(xml, method, (xmlRelaxNGValidityErrorFunc) xml_log, GUINT_TO_POINTER(LOG_ERR)); + 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) +bool +pcmk__validate_xml(xmlNode *xml_blob, const char *validation, + xmlRelaxNGValidityErrorFunc error_handler, + void *error_handler_context) { + GList *entry = NULL; + pcmk__schema_t *schema = NULL; - 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); + CRM_CHECK((xml_blob != NULL) && (xml_blob->doc != NULL), return false); if (validation == NULL) { - validation = crm_element_value(xml_blob, XML_ATTR_VALIDATION); + validation = crm_element_value(xml_blob, PCMK_XA_VALIDATE_WITH); } + pcmk__warn_if_schema_deprecated(validation); + // @COMPAT Not specifying a schema name is deprecated since 2.1.8 if (validation == NULL) { - int lpc = 0; - bool valid = FALSE; - - for (lpc = 0; lpc < xml_schema_max; lpc++) { - if (validate_with(xml_blob, lpc, NULL, NULL)) { - 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; - } + bool valid = false; + + for (entry = known_schemas; entry != NULL; entry = entry->next) { + schema = entry->data; + if (validate_with(xml_blob, schema, NULL, NULL)) { + valid = true; + crm_xml_add(xml_blob, PCMK_XA_VALIDATE_WITH, schema->name); + crm_info("XML validated against %s", schema->name); } } - return valid; } - 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, error_handler, error_handler_context); + entry = pcmk__get_schema(validation); + if (entry == NULL) { + pcmk__config_err("Cannot validate CIB with " PCMK_XA_VALIDATE_WITH + " set to an unknown schema such as '%s' (manually" + " edit to use a known schema)", + validation); + return false; } - crm_err("Unknown validator: %s", validation); - return FALSE; + schema = entry->data; + return validate_with(xml_blob, schema, error_handler, + error_handler_context); } -static void -cib_upgrade_err(void *ctx, const char *fmt, ...) -G_GNUC_PRINTF(2, 3); +/*! + * \internal + * \brief Validate XML using its configured schema (and send errors to logs) + * + * \param[in] xml XML to validate + * + * \return true if XML validates, otherwise false + */ +bool +pcmk__configured_schema_validates(xmlNode *xml) +{ + return pcmk__validate_xml(xml, NULL, + (xmlRelaxNGValidityErrorFunc) xml_log, + GUINT_TO_POINTER(LOG_ERR)); +} /* With this arrangement, an attempt to identify the message severity as explicitly signalled directly from XSLT is performed in rather @@ -743,7 +836,7 @@ G_GNUC_PRINTF(2, 3); (suspicious, likely internal errors or some runaways) is LOG_WARNING. */ -static void +static void G_GNUC_PRINTF(2, 3) cib_upgrade_err(void *ctx, const char *fmt, ...) { va_list ap, aq; @@ -858,8 +951,20 @@ cib_upgrade_err(void *ctx, const char *fmt, ...) va_end(ap); } +/*! + * \internal + * \brief Apply a single XSL transformation to given XML + * + * \param[in] xml XML to transform + * \param[in] transform XSL name + * \param[in] to_logs If false, certain validation errors will be sent to + * stderr rather than logged + * + * \return Transformed XML on success, otherwise NULL + */ static xmlNode * -apply_transformation(xmlNode *xml, const char *transform, gboolean to_logs) +apply_transformation(const xmlNode *xml, const char *transform, + gboolean to_logs) { char *xform = NULL; xmlNode *out = NULL; @@ -898,52 +1003,79 @@ apply_transformation(xmlNode *xml, const char *transform, gboolean to_logs) /*! * \internal - * \brief Possibly full enter->upgrade->leave trip per internal bookkeeping. + * \brief Perform all transformations needed to upgrade XML to next schema + * + * A schema upgrade can require up to three XSL transformations: an "enter" + * transform, the main upgrade transform, and a "leave" transform. Perform + * all needed transforms to upgrade given XML to the next schema. + * + * \param[in] original_xml XML to transform + * \param[in] schema_index Index of schema that successfully validates + * \p original_xml + * \param[in] to_logs If false, certain validation errors will be sent to + * stderr rather than logged * - * \note Only emits warnings about enter/leave phases in case of issues. + * \return XML result of schema transforms if successful, otherwise NULL */ static xmlNode * -apply_upgrade(xmlNode *xml, const struct schema_s *schema, gboolean to_logs) +apply_upgrade(const xmlNode *original_xml, int schema_index, gboolean to_logs) { - bool transform_onleave = schema->transform_onleave; + pcmk__schema_t *schema = g_list_nth_data(known_schemas, schema_index); + pcmk__schema_t *upgraded_schema = g_list_nth_data(known_schemas, + schema_index + 1); + bool transform_onleave = false; char *transform_leave; - xmlNode *upgrade = NULL, - *final = NULL; + const xmlNode *xml = original_xml; + xmlNode *upgrade = NULL; + xmlNode *final = NULL; + xmlRelaxNGValidityErrorFunc error_handler = NULL; - if (schema->transform_enter) { - crm_debug("Upgrading %s-style configuration, pre-upgrade phase with %s.xsl", - schema->name, schema->transform_enter); + CRM_ASSERT((schema != NULL) && (upgraded_schema != NULL)); + + if (to_logs) { + error_handler = (xmlRelaxNGValidityErrorFunc) xml_log; + } + + transform_onleave = schema->transform_onleave; + if (schema->transform_enter != NULL) { + crm_debug("Upgrading schema from %s to %s: " + "applying pre-upgrade XSL transform %s", + schema->name, upgraded_schema->name, schema->transform_enter); upgrade = apply_transformation(xml, schema->transform_enter, to_logs); if (upgrade == NULL) { - crm_warn("Upgrade-enter transformation %s.xsl failed", + crm_warn("Pre-upgrade XSL transform %s failed, " + "will skip post-upgrade transform", schema->transform_enter); transform_onleave = FALSE; + } else { + xml = upgrade; } } - 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); + + crm_debug("Upgrading schema from %s to %s: " + "applying upgrade XSL transform %s", + schema->name, upgraded_schema->name, schema->transform); + final = apply_transformation(xml, schema->transform, to_logs); if (upgrade != xml) { free_xml(upgrade); upgrade = NULL; } - if (final != NULL && transform_onleave) { + 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); + crm_debug("Upgrading schema from %s to %s: " + "applying post-upgrade XSL transform %s", + schema->name, upgraded_schema->name, transform_leave); final = apply_transformation(upgrade, transform_leave, to_logs); if (final == NULL) { - crm_warn("Upgrade-leave transformation %s.xsl failed", transform_leave); + crm_warn("Ignoring failure of post-upgrade XSL transform %s", + transform_leave); final = upgrade; } else { free_xml(upgrade); @@ -951,290 +1083,661 @@ apply_upgrade(xmlNode *xml, const struct schema_s *schema, gboolean to_logs) free(transform_leave); } - return final; -} + if (final == NULL) { + return NULL; + } -const char * -get_schema_name(int version) -{ - if (version < 0 || version >= xml_schema_max) { - return "unknown"; + // Ensure result validates with its new schema + if (!validate_with(final, upgraded_schema, error_handler, + GUINT_TO_POINTER(LOG_ERR))) { + crm_err("Schema upgrade from %s to %s failed: " + "XSL transform %s produced an invalid configuration", + schema->name, upgraded_schema->name, schema->transform); + crm_log_xml_debug(final, "bad-transform-result"); + free_xml(final); + return NULL; } - return known_schemas[version].name; + + crm_info("Schema upgrade from %s to %s succeeded", + schema->name, upgraded_schema->name); + return final; } -int -get_schema_version(const char *name) +/*! + * \internal + * \brief Get the schema list entry corresponding to XML configuration + * + * \param[in] xml CIB XML to check + * + * \return List entry of schema configured in \p xml + */ +static GList * +get_configured_schema(const xmlNode *xml) { - int lpc = 0; + const char *schema_name = crm_element_value(xml, PCMK_XA_VALIDATE_WITH); - if (name == NULL) { - name = PCMK__VALUE_NONE; + pcmk__warn_if_schema_deprecated(schema_name); + if (schema_name == NULL) { + return NULL; } - for (; lpc < xml_schema_max; lpc++) { - if (pcmk__str_eq(name, known_schemas[lpc].name, pcmk__str_casei)) { - return lpc; - } - } - return -1; + return pcmk__get_schema(schema_name); } -/* set which validation to use */ +/*! + * \brief Update CIB XML to latest schema that validates it + * + * \param[in,out] xml XML to update (may be freed and replaced + * after being transformed) + * \param[in] max_schema_name If not NULL, do not update \p xml to any + * schema later than this one + * \param[in] transform If false, do not update \p xml to any schema + * that requires an XSL transform + * \param[in] to_logs If false, certain validation errors will be + * sent to stderr rather than logged + * + * \return Standard Pacemaker return code + */ int -update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, - gboolean to_logs) +pcmk__update_schema(xmlNode **xml, const char *max_schema_name, bool transform, + bool 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 */ + int max_schema_index = 0; + int rc = pcmk_rc_ok; + GList *entry = NULL; + pcmk__schema_t *best_schema = NULL; + pcmk__schema_t *original_schema = NULL; xmlRelaxNGValidityErrorFunc error_handler = to_logs ? (xmlRelaxNGValidityErrorFunc) xml_log : NULL; - CRM_CHECK(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); + CRM_CHECK((xml != NULL) && (*xml != NULL) && ((*xml)->doc != NULL), + return EINVAL); - if (value != NULL) { - match = get_schema_version(value); + if (max_schema_name != NULL) { + GList *max_entry = pcmk__get_schema(max_schema_name); - lpc = match; - if (lpc >= 0 && transform == FALSE) { - *best = lpc++; + if (max_entry != NULL) { + pcmk__schema_t *max_schema = max_entry->data; - } else if (lpc < 0) { - crm_debug("Unknown validation schema"); - lpc = 0; + max_schema_index = max_schema->schema_index; } } + if ((max_schema_index < 1) || (max_schema_index > max_stable_schemas)) { + max_schema_index = max_stable_schemas; + } - if (match >= max_stable_schemas) { - /* nothing to do */ - free(value); - *best = match; - return pcmk_ok; + entry = get_configured_schema(*xml); + if (entry == NULL) { + // @COMPAT Not specifying a schema name is deprecated since 2.1.8 + entry = known_schemas; + } else { + original_schema = entry->data; + if (original_schema->schema_index >= max_schema_index) { + return pcmk_rc_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); + for (; entry != NULL; entry = entry->next) { + pcmk__schema_t *current_schema = entry->data; + xmlNode *upgrade = NULL; - if (validate_with(xml, lpc, error_handler, GUINT_TO_POINTER(LOG_ERR)) == 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) { + if (current_schema->schema_index > max_schema_index) { + break; + } + + if (!validate_with(*xml, current_schema, error_handler, + GUINT_TO_POINTER(LOG_ERR))) { + crm_debug("Schema %s does not validate", current_schema->name); + if (best_schema != NULL) { /* we've satisfied the validation, no need to check further */ break; } - rc = -pcmk_err_schema_validation; - - } else { - if (next != -1) { - crm_debug("Configuration valid for schema: %s", - known_schemas[next].name); - next = -1; - } - rc = pcmk_ok; + rc = pcmk_rc_schema_validation; + continue; // Try again with the next higher schema } - if (rc == pcmk_ok) { - *best = lpc; + crm_debug("Schema %s validates", current_schema->name); + rc = pcmk_rc_ok; + best_schema = current_schema; + if (current_schema->schema_index == max_schema_index) { + break; // No further transformations possible } - if (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, error_handler, GUINT_TO_POINTER(LOG_ERR))) { - 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 || (current_schema->transform == NULL) + || validate_with_silent(*xml, entry->next->data)) { + /* The next schema either doesn't require a transform or validates + * successfully even without the transform. Skip the transform and + * try the next schema with the same XML. + */ + continue; } - if (transform == FALSE || rc != pcmk_ok) { - /* we need some progress! */ - lpc++; + upgrade = apply_upgrade(*xml, current_schema->schema_index, to_logs); + if (upgrade == NULL) { + /* The transform failed, so this schema can't be used. Later + * schemas are unlikely to validate, but try anyway until we + * run out of options. + */ + rc = pcmk_rc_transform_failed; + } else { + best_schema = current_schema; + free_xml(*xml); + *xml = upgrade; } } - if (*best > 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); + if (best_schema != NULL) { + if ((original_schema == NULL) + || (best_schema->schema_index > original_schema->schema_index)) { + crm_info("%s the configuration schema to %s", + (transform? "Transformed" : "Upgraded"), + best_schema->name); + crm_xml_add(*xml, PCMK_XA_VALIDATE_WITH, best_schema->name); + } } - - *xml_blob = xml; - free(value); return rc; } -gboolean -cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs) +/*! + * \brief Update XML from its configured schema to the latest major series + * + * \param[in,out] xml XML to update + * \param[in] to_logs If false, certain validation errors will be + * sent to stderr rather than logged + * + * \return Standard Pacemaker return code + */ +int +pcmk_update_configured_schema(xmlNode **xml, bool 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 rc = pcmk_rc_ok; + char *original_schema_name = NULL; + + // @COMPAT Not specifying a schema name is deprecated since 2.1.8 + const char *effective_original_name = "the first"; + + int orig_version = -1; + pcmk__schema_t *x_0_schema = pcmk__find_x_0_schema()->data; + GList *entry = NULL; + + CRM_CHECK(xml != NULL, return EINVAL); - int version = get_schema_version(value); - int orig_version = version; - int min_version = xml_minimum_schema_index(); + original_schema_name = crm_element_value_copy(*xml, PCMK_XA_VALIDATE_WITH); + pcmk__warn_if_schema_deprecated(original_schema_name); + entry = pcmk__get_schema(original_schema_name); + if (entry != NULL) { + pcmk__schema_t *original_schema = entry->data; - if (version < min_version) { + effective_original_name = original_schema->name; + orig_version = original_schema->schema_index; + } + + if (orig_version < x_0_schema->schema_index) { // Current configuration schema is not acceptable, try to update xmlNode *converted = NULL; + const char *new_schema_name = NULL; + pcmk__schema_t *schema = NULL; + + entry = NULL; + converted = pcmk__xml_copy(NULL, *xml); + if (pcmk__update_schema(&converted, NULL, true, to_logs) == pcmk_rc_ok) { + new_schema_name = crm_element_value(converted, + PCMK_XA_VALIDATE_WITH); + entry = pcmk__get_schema(new_schema_name); + } + schema = (entry == NULL)? NULL : entry->data; - converted = copy_xml(*xml); - update_validation(&converted, &version, 0, TRUE, to_logs); - - value = crm_element_value(converted, XML_ATTR_VALIDATION); - if (version < min_version) { + if ((schema == NULL) + || (schema->schema_index < x_0_schema->schema_index)) { // Updated configuration schema is still not acceptable - if (version < orig_version || orig_version == -1) { + if ((orig_version == -1) || (schema == NULL) + || (schema->schema_index < orig_version)) { // 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 " + "%s schema) 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()); + "%s to the latest", + pcmk__s(original_schema_name, "no"), + x_0_schema->name, effective_original_name); } else { fprintf(stderr, "Cannot upgrade configuration (claiming " - "schema %s) to at least %s because it " + "%s schema) 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()); + "%s to the latest\n", + pcmk__s(original_schema_name, "no"), + x_0_schema->name, effective_original_name); } } 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 " + "%s schema) to at least %s because it " "would not upgrade past %s", - orig_value, - get_schema_name(min_version), - pcmk__s(value, "unspecified version")); + pcmk__s(original_schema_name, "no"), + x_0_schema->name, + pcmk__s(new_schema_name, "unspecified version")); } else { fprintf(stderr, "Cannot upgrade configuration (claiming " - "schema %s) to at least %s because it " + "%s schema) to at least %s because it " "would not upgrade past %s\n", - orig_value, - get_schema_name(min_version), - pcmk__s(value, "unspecified version")); + pcmk__s(original_schema_name, "no"), + x_0_schema->name, + pcmk__s(new_schema_name, "unspecified version")); } } free_xml(converted); converted = NULL; - rc = FALSE; + rc = pcmk_rc_transform_failed; } else { // Updated configuration schema is acceptable free_xml(*xml); *xml = converted; - if (version < xml_latest_schema_index()) { + if (schema->schema_index < xml_latest_schema_index()) { if (to_logs) { - pcmk__config_warn("Configuration with schema %s was " + pcmk__config_warn("Configuration with %s schema 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)); + pcmk__s(original_schema_name, "no"), + schema->name); } + } else if (to_logs) { + crm_info("Configuration with %s schema was internally " + "upgraded to latest version %s", + pcmk__s(original_schema_name, "no"), + schema->name); } } - } 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 { + // @COMPAT the none schema is deprecated since 2.1.8 + pcmk__schema_t *none_schema = NULL; + + entry = pcmk__get_schema(PCMK_VALUE_NONE); + CRM_ASSERT((entry != NULL) && (entry->data != NULL)); + + none_schema = entry->data; + if (!to_logs && (orig_version >= none_schema->schema_index)) { + fprintf(stderr, "Schema validation of configuration is " + "disabled (support for " PCMK_XA_VALIDATE_WITH + " set to \"" PCMK_VALUE_NONE "\" is deprecated" + " and will be removed in a future release)\n"); + } + } - } else { - fprintf(stderr, "Schema validation of configuration is disabled " - "(enabling is encouraged and prevents common " - "misconfigurations)\n"); + free(original_schema_name); + return rc; +} + +/*! + * \internal + * \brief Return a list of all schema files and any associated XSLT files + * later than the given one + * \brief Return a list of all schema versions later than the given one + * + * \param[in] schema The schema to compare against (for example, + * "pacemaker-3.1.rng" or "pacemaker-3.1") + * + * \note The caller is responsible for freeing both the returned list and + * the elements of the list + */ +GList * +pcmk__schema_files_later_than(const char *name) +{ + GList *lst = NULL; + pcmk__schema_version_t ver; + + if (!version_from_filename(name, &ver)) { + return lst; + } + + for (GList *iter = g_list_nth(known_schemas, xml_latest_schema_index()); + iter != NULL; iter = iter->prev) { + pcmk__schema_t *schema = iter->data; + char *s = NULL; + + if (schema_cmp(ver, schema->version) != -1) { + continue; + } + + s = crm_strdup_printf("%s.rng", schema->name); + lst = g_list_prepend(lst, s); + + if (schema->transform != NULL) { + char *xform = crm_strdup_printf("%s.xsl", schema->transform); + lst = g_list_prepend(lst, xform); + } + + if (schema->transform_enter != NULL) { + char *enter = crm_strdup_printf("%s.xsl", schema->transform_enter); + + lst = g_list_prepend(lst, enter); + + if (schema->transform_onleave) { + int last_dash = strrchr(enter, '-') - enter; + char *leave = crm_strdup_printf("%.*s-leave.xsl", last_dash, enter); + + lst = g_list_prepend(lst, leave); + } } } - if (best_version) { - *best_version = version; + return lst; +} + +static void +append_href(xmlNode *xml, void *user_data) +{ + GList **list = user_data; + char *href = crm_element_value_copy(xml, "href"); + + if (href == NULL) { + return; } + *list = g_list_prepend(*list, href); +} + +static void +external_refs_in_schema(GList **list, const char *contents) +{ + /* local-name()= is needed to ignore the xmlns= setting at the top of + * the XML file. Otherwise, the xpath query will always return nothing. + */ + const char *search = "//*[local-name()='externalRef'] | //*[local-name()='include']"; + xmlNode *xml = pcmk__xml_parse(contents); - free(orig_value); + crm_foreach_xpath_result(xml, search, append_href, list); + free_xml(xml); +} + +static int +read_file_contents(const char *file, char **contents) +{ + int rc = pcmk_rc_ok; + char *path = NULL; + + if (pcmk__ends_with(file, ".rng")) { + path = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng, file); + } else { + path = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, file); + } + + rc = pcmk__file_contents(path, contents); + + free(path); return rc; } + +static void +add_schema_file_to_xml(xmlNode *parent, const char *file, GList **already_included) +{ + char *contents = NULL; + char *path = NULL; + xmlNode *file_node = NULL; + GList *includes = NULL; + int rc = pcmk_rc_ok; + + /* If we already included this file, don't do so again. */ + if (g_list_find_custom(*already_included, file, (GCompareFunc) strcmp) != NULL) { + return; + } + + /* Ensure whatever file we were given has a suffix we know about. If not, + * just assume it's an RNG file. + */ + if (!pcmk__ends_with(file, ".rng") && !pcmk__ends_with(file, ".xsl")) { + path = crm_strdup_printf("%s.rng", file); + } else { + path = pcmk__str_copy(file); + } + + rc = read_file_contents(path, &contents); + if (rc != pcmk_rc_ok || contents == NULL) { + crm_warn("Could not read schema file %s: %s", file, pcmk_rc_str(rc)); + free(path); + return; + } + + /* Create a new <file path="..."> node with the contents of the file + * as a CDATA block underneath it. + */ + file_node = pcmk__xe_create(parent, PCMK_XA_FILE); + crm_xml_add(file_node, PCMK_XA_PATH, path); + *already_included = g_list_prepend(*already_included, path); + + xmlAddChild(file_node, xmlNewCDataBlock(parent->doc, (pcmkXmlStr) contents, + strlen(contents))); + + /* Scan the file for any <externalRef> or <include> nodes and build up + * a list of the files they reference. + */ + external_refs_in_schema(&includes, contents); + + /* For each referenced file, recurse to add it (and potentially anything it + * references, ...) to the XML. + */ + for (GList *iter = includes; iter != NULL; iter = iter->next) { + add_schema_file_to_xml(parent, iter->data, already_included); + } + + free(contents); + g_list_free_full(includes, free); +} + +/*! + * \internal + * \brief Add an XML schema file and all the files it references as children + * of a given XML node + * + * \param[in,out] parent The parent XML node + * \param[in] name The schema version to compare against + * (for example, "pacemaker-3.1" or "pacemaker-3.1.rng") + * \param[in,out] already_included A list of names that have already been added + * to the parent node. + * + * \note The caller is responsible for freeing both the returned list and + * the elements of the list + */ +void +pcmk__build_schema_xml_node(xmlNode *parent, const char *name, GList **already_included) +{ + xmlNode *schema_node = pcmk__xe_create(parent, PCMK__XA_SCHEMA); + + crm_xml_add(schema_node, PCMK_XA_VERSION, name); + add_schema_file_to_xml(schema_node, name, already_included); + + if (schema_node->children == NULL) { + // Not needed if empty. May happen if name was invalid, for example. + free_xml(schema_node); + } +} + +/*! + * \internal + * \brief Return the directory containing any extra schema files that a + * Pacemaker Remote node fetched from the cluster + */ +const char * +pcmk__remote_schema_dir(void) +{ + const char *dir = pcmk__env_option(PCMK__ENV_REMOTE_SCHEMA_DIRECTORY); + + if (pcmk__str_empty(dir)) { + return PCMK__REMOTE_SCHEMA_DIR; + } + + return dir; +} + +/*! + * \internal + * \brief Warn if a given validation schema is deprecated + * + * \param[in] Schema name to check + */ +void +pcmk__warn_if_schema_deprecated(const char *schema) +{ + if ((schema == NULL) || + pcmk__strcase_any_of(schema, "pacemaker-next", PCMK_VALUE_NONE, NULL)) { + pcmk__config_warn("Support for " PCMK_XA_VALIDATE_WITH "='%s' is " + "deprecated and will be removed in a future release " + "without the possibility of upgrades (manually edit " + "to use a supported schema)", pcmk__s(schema, "")); + } +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include <crm/common/xml_compat.h> + +const char * +xml_latest_schema(void) +{ + return pcmk__highest_schema_name(); +} + +const char * +get_schema_name(int version) +{ + pcmk__schema_t *schema = g_list_nth_data(known_schemas, version); + + return (schema != NULL)? schema->name : "unknown"; +} + +int +get_schema_version(const char *name) +{ + int lpc = 0; + + if (name == NULL) { + name = PCMK_VALUE_NONE; + } + + for (GList *iter = known_schemas; iter != NULL; iter = iter->next) { + pcmk__schema_t *schema = iter->data; + + if (pcmk__str_eq(name, schema->name, pcmk__str_casei)) { + return lpc; + } + + lpc++; + } + + return -1; +} + +int +update_validation(xmlNode **xml, int *best, int max, gboolean transform, + gboolean to_logs) +{ + int rc = pcmk__update_schema(xml, get_schema_name(max), transform, to_logs); + + if ((best != NULL) && (xml != NULL) && (rc == pcmk_rc_ok)) { + const char *schema_name = crm_element_value(*xml, + PCMK_XA_VALIDATE_WITH); + GList *schema_entry = pcmk__get_schema(schema_name); + + if (schema_entry != NULL) { + *best = ((pcmk__schema_t *)(schema_entry->data))->schema_index; + } + } + + return pcmk_rc2legacy(rc); +} + +gboolean +validate_xml(xmlNode *xml_blob, const char *validation, gboolean to_logs) +{ + bool rc = pcmk__validate_xml(xml_blob, validation, + to_logs? (xmlRelaxNGValidityErrorFunc) xml_log : NULL, + GUINT_TO_POINTER(LOG_ERR)); + return rc? TRUE : FALSE; +} + +static void +dump_file(const char *filename) +{ + + FILE *fp = NULL; + int ch, line = 0; + + CRM_CHECK(filename != NULL, return); + + fp = fopen(filename, "r"); + if (fp == NULL) { + crm_perror(LOG_ERR, "Could not open %s for reading", filename); + return; + } + + fprintf(stderr, "%4d ", ++line); + do { + ch = getc(fp); + if (ch == EOF) { + putc('\n', stderr); + break; + } else if (ch == '\n') { + fprintf(stderr, "\n%4d ", ++line); + } else { + putc(ch, stderr); + } + } while (1); + + fclose(fp); +} + +gboolean +validate_xml_verbose(const xmlNode *xml_blob) +{ + int fd = 0; + xmlDoc *doc = NULL; + xmlNode *xml = NULL; + gboolean rc = FALSE; + char *filename = NULL; + + filename = crm_strdup_printf("%s/cib-invalid.XXXXXX", pcmk__get_tmpdir()); + + umask(S_IWGRP | S_IWOTH | S_IROTH); + fd = mkstemp(filename); + pcmk__xml_write_fd(xml_blob, filename, fd, false, NULL); + + dump_file(filename); + + doc = xmlReadFile(filename, NULL, 0); + xml = xmlDocGetRootElement(doc); + rc = pcmk__validate_xml(xml, NULL, NULL, NULL); + free_xml(xml); + + unlink(filename); + free(filename); + + return rc? TRUE : FALSE; +} + +gboolean +cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs) +{ + int rc = pcmk_update_configured_schema(xml, to_logs); + + if (best_version != NULL) { + const char *name = crm_element_value(*xml, PCMK_XA_VALIDATE_WITH); + + if (name == NULL) { + *best_version = -1; + } else { + GList *entry = pcmk__get_schema(name); + pcmk__schema_t *schema = (entry == NULL)? NULL : entry->data; + + *best_version = (schema == NULL)? -1 : schema->schema_index; + } + } + return (rc == pcmk_rc_ok)? TRUE: FALSE; +} + +// LCOV_EXCL_STOP +// End deprecated API |