diff --git a/cts/cli/regression.upgrade.exp b/cts/cli/regression.upgrade.exp
index 847769e324..dd1bd32f96 100644
--- a/cts/cli/regression.upgrade.exp
+++ b/cts/cli/regression.upgrade.exp
@@ -1,167 +1,166 @@
Created new pacemaker configuration
A new shadow instance was created. To begin using it, enter the following into your shell:
export CIB_shadow=cts-cli
=#=#=#= Begin test: Set stonith-enabled=false =#=#=#=
=#=#=#= Current cib after: Set stonith-enabled=false =#=#=#=
=#=#=#= End test: Set stonith-enabled=false - OK (0) =#=#=#=
* Passed: crm_attribute - Set stonith-enabled=false
=#=#=#= Begin test: Configure the initial resource =#=#=#=
=#=#=#= Current cib after: Configure the initial resource =#=#=#=
=#=#=#= End test: Configure the initial resource - OK (0) =#=#=#=
* Passed: cibadmin - Configure the initial resource
=#=#=#= Begin test: Upgrade to latest CIB schema (trigger 2.10.xsl + the wrapping) =#=#=#=
update_validation debug: Testing 'pacemaker-2.10' validation (13 of X)
-update_validation debug: Upgrading pacemaker-2.10-style configuration to pacemaker-3.0 with upgrade-2.10.xsl
-apply_upgrade debug: Upgrading pacemaker-2.10-style configuration, pre-upgrade phase with upgrade-2.10-enter.xsl
-apply_upgrade debug: Upgrading pacemaker-2.10-style configuration, main phase with upgrade-2.10.xsl
+apply_upgrade debug: Upgrading schema from pacemaker-2.10 to pacemaker-3.0: applying pre-upgrade XSL transform upgrade-2.10-enter
+apply_upgrade debug: Upgrading schema from pacemaker-2.10 to pacemaker-3.0: applying upgrade XSL transform upgrade-2.10
INFO: Resources-operation instance_attributes: mySmartFuse-monitor-inputpower (rsc=mySmartFuse, meta=mySmartFuse-inputpower-instanceparams): dropping requires
INFO: Resources-operation instance_attributes: ... only start/promote operation taken into account
INFO: Resources-operation instance_attributes: mySmartFuse-monitor-outputpower (rsc=mySmartFuse, meta=mySmartFuse-outputpower-instanceparams): dropping requires
INFO: Resources-operation instance_attributes: ... only start/promote operation taken into account
-apply_upgrade debug: Upgrading pacemaker-2.10-style configuration, post-upgrade phase with upgrade-2.10-leave.xsl
+apply_upgrade debug: Upgrading schema from pacemaker-2.10 to pacemaker-3.0: applying post-upgrade XSL transform upgrade-2.10-leave
DEBUG: instance_attributes: original element pointed to with @id-ref (mySmartFuse-outputpower-instanceparams) disappeared during upgrade
-update_validation info: Transformation upgrade-2.10.xsl successful
+apply_upgrade info: Schema upgrade from pacemaker-2.10 to pacemaker-3.0 succeeded
update_validation debug: Testing 'pacemaker-3.0' validation (14 of X)
update_validation debug: pacemaker-3.0-style configuration is also valid for pacemaker-3.1
update_validation debug: Testing 'pacemaker-3.1' validation (15 of X)
update_validation debug: Configuration valid for schema: pacemaker-3.1
update_validation debug: pacemaker-3.1-style configuration is also valid for pacemaker-3.2
update_validation debug: Testing 'pacemaker-3.2' validation (16 of X)
update_validation debug: Configuration valid for schema: pacemaker-3.2
update_validation debug: pacemaker-3.2-style configuration is also valid for pacemaker-3.3
update_validation debug: Testing 'pacemaker-3.3' validation (17 of X)
update_validation debug: Configuration valid for schema: pacemaker-3.3
update_validation debug: pacemaker-3.3-style configuration is also valid for pacemaker-3.4
update_validation debug: Testing 'pacemaker-3.4' validation (18 of X)
update_validation debug: Configuration valid for schema: pacemaker-3.4
update_validation debug: pacemaker-3.4-style configuration is also valid for pacemaker-3.5
update_validation debug: Testing 'pacemaker-3.5' validation (19 of X)
update_validation debug: Configuration valid for schema: pacemaker-3.5
update_validation debug: pacemaker-3.5-style configuration is also valid for pacemaker-3.6
update_validation debug: Testing 'pacemaker-3.6' validation (20 of X)
update_validation debug: Configuration valid for schema: pacemaker-3.6
update_validation debug: pacemaker-3.6-style configuration is also valid for pacemaker-3.7
update_validation debug: Testing 'pacemaker-3.7' validation (21 of X)
update_validation debug: Configuration valid for schema: pacemaker-3.7
update_validation debug: pacemaker-3.7-style configuration is also valid for pacemaker-3.8
update_validation debug: Testing 'pacemaker-3.8' validation (22 of X)
update_validation debug: Configuration valid for schema: pacemaker-3.8
update_validation debug: pacemaker-3.8-style configuration is also valid for pacemaker-3.9
update_validation debug: Testing 'pacemaker-3.9' validation (23 of X)
update_validation debug: Configuration valid for schema: pacemaker-3.9
update_validation debug: pacemaker-3.9-style configuration is also valid for pacemaker-3.10
update_validation debug: Testing 'pacemaker-3.10' validation (24 of X)
update_validation debug: Configuration valid for schema: pacemaker-3.10
update_validation trace: Stopping at pacemaker-3.10
update_validation info: Transformed the configuration from pacemaker-2.10 to pacemaker-3.10
=#=#=#= Current cib after: Upgrade to latest CIB schema (trigger 2.10.xsl + the wrapping) =#=#=#=
=#=#=#= End test: Upgrade to latest CIB schema (trigger 2.10.xsl + the wrapping) - OK (0) =#=#=#=
* Passed: cibadmin - Upgrade to latest CIB schema (trigger 2.10.xsl + the wrapping)
=#=#=#= Begin test: Query a resource instance attribute (shall survive) =#=#=#=
outputpower
=#=#=#= Current cib after: Query a resource instance attribute (shall survive) =#=#=#=
=#=#=#= End test: Query a resource instance attribute (shall survive) - OK (0) =#=#=#=
* Passed: crm_resource - Query a resource instance attribute (shall survive)
diff --git a/lib/common/schemas.c b/lib/common/schemas.c
index 28b8b5b6f5..886c1ac51c 100644
--- a/lib/common/schemas.c
+++ b/lib/common/schemas.c
@@ -1,1549 +1,1618 @@
/*
* Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include /* PCMK__XML_LOG_BASE */
#include "crmcommon_private.h"
#define SCHEMA_ZERO { .v = { 0, 0 } }
#define schema_strdup_printf(prefix, version, suffix) \
crm_strdup_printf(prefix "%u.%u" suffix, (version).v[0], (version).v[1])
typedef struct {
xmlRelaxNGPtr rng;
xmlRelaxNGValidCtxtPtr valid;
xmlRelaxNGParserCtxtPtr parser;
} relaxng_ctx_cache_t;
static GList *known_schemas = NULL;
static bool silent_logging = FALSE;
static void G_GNUC_PRINTF(2, 3)
xml_log(int priority, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
if (silent_logging == FALSE) {
/* XXX should not this enable dechunking as well? */
PCMK__XML_LOG_BASE(priority, FALSE, 0, NULL, fmt, ap);
}
va_end(ap);
}
static int
xml_latest_schema_index(void)
{
/* This function assumes that crm_schema_init() has been called beforehand,
* so we have at least three schemas (one real schema, the "pacemaker-next"
* schema, and the "none" schema).
*
* @COMPAT: pacemaker-next is deprecated since 2.1.5.
* Update this when we drop that schema.
*/
return g_list_length(known_schemas) - 3;
}
/* Return the index of the most recent X.0 schema. */
int
pcmk__find_x_0_schema_index(void)
{
/* We can't just use best to determine whether we've found the index
* or not. What if we have a very long list of schemas all in the
* same major version series? We'd return 0 for that, which means
* we would still run this function every time.
*/
#if defined(PCMK__UNIT_TESTING)
/* If we're unit testing, these can't be static because they'll stick
* around from one test run to the next. They need to be cleared out
* every time.
*/
bool found = false;
int best = 0;
#else
static bool found = false;
static int best = 0;
#endif
int i;
GList *best_node = NULL;
pcmk__schema_t *best_schema = NULL;
if (found) {
return best;
}
CRM_ASSERT(known_schemas != NULL);
/* Get the most recent schema so we can look at its version number. */
best = xml_latest_schema_index();
best_node = g_list_nth(known_schemas, best);
best_schema = best_node->data;
/* The "pacemaker-next" and "none" schemas are added to the real schemas,
* so a list of length three actually only has one useful schema.
*
* @COMPAT pacemaker-next is deprecated since 2.1.5.
* Update this when we drop that schema.
*/
if (g_list_length(known_schemas) == 3) {
goto done;
}
/* Start comparing the list from the node before the best schema (there's
* no point in comparing something to itself). Then, 'i' is an index
* starting at the best schema and will always point at the node after
* 'iter'. This makes it the value we want to return when we find what
* we're looking for.
*/
i = best;
for (GList *iter = best_node->prev; iter != NULL; iter = iter->prev) {
pcmk__schema_t *schema = iter->data;
/* We've found a schema in an older major version series. Return
* the index of the first one in the same major version series as
* the best schema.
*/
if (schema->version.v[0] < best_schema->version.v[0]) {
best = i;
goto done;
/* We're out of list to examine. This probably means there was only
* one major version series, so return index 0.
*/
} else if (iter->prev == NULL) {
best = 0;
goto done;
}
i--;
}
done:
found = true;
return best;
}
const char *
xml_latest_schema(void)
{
return get_schema_name(xml_latest_schema_index());
}
static inline bool
version_from_filename(const char *filename, pcmk__schema_version_t *version)
{
if (pcmk__ends_with(filename, ".rng")) {
return sscanf(filename, "pacemaker-%hhu.%hhu.rng", &(version->v[0]), &(version->v[1])) == 2;
} else {
return sscanf(filename, "pacemaker-%hhu.%hhu", &(version->v[0]), &(version->v[1])) == 2;
}
}
static int
schema_filter(const struct dirent *a)
{
int rc = 0;
pcmk__schema_version_t version = SCHEMA_ZERO;
if (strstr(a->d_name, "pacemaker-") != a->d_name) {
/* crm_trace("%s - wrong prefix", a->d_name); */
} else if (!pcmk__ends_with_ext(a->d_name, ".rng")) {
/* crm_trace("%s - wrong suffix", a->d_name); */
} else if (!version_from_filename(a->d_name, &version)) {
/* crm_trace("%s - wrong format", a->d_name); */
} else {
/* crm_debug("%s - candidate", a->d_name); */
rc = 1;
}
return rc;
}
static int
schema_cmp(pcmk__schema_version_t a_version, pcmk__schema_version_t b_version)
{
for (int i = 0; i < 2; ++i) {
if (a_version.v[i] < b_version.v[i]) {
return -1;
} else if (a_version.v[i] > b_version.v[i]) {
return 1;
}
}
return 0;
}
static int
schema_cmp_directory(const struct dirent **a, const struct dirent **b)
{
pcmk__schema_version_t a_version = SCHEMA_ZERO;
pcmk__schema_version_t b_version = SCHEMA_ZERO;
if (!version_from_filename(a[0]->d_name, &a_version)
|| !version_from_filename(b[0]->d_name, &b_version)) {
// Shouldn't be possible, but makes static analysis happy
return 0;
}
return schema_cmp(a_version, b_version);
}
/*!
* \internal
* \brief Add given schema + auxiliary data to internal bookkeeping.
*
* \note When providing \p version, should not be called directly but
* through \c add_schema_by_version.
*/
static void
add_schema(enum pcmk__schema_validator validator, const pcmk__schema_version_t *version,
const char *name, const char *transform,
const char *transform_enter, bool transform_onleave)
{
pcmk__schema_t *schema = NULL;
int last = g_list_length(known_schemas);
schema = pcmk__assert_alloc(1, sizeof(pcmk__schema_t));
schema->validator = validator;
schema->version.v[0] = version->v[0];
schema->version.v[1] = version->v[1];
schema->transform_onleave = transform_onleave;
if (version->v[0] || version->v[1]) {
schema->name = schema_strdup_printf("pacemaker-", *version, "");
} else {
schema->name = pcmk__str_copy(name);
}
if (transform) {
schema->transform = pcmk__str_copy(transform);
}
if (transform_enter) {
schema->transform_enter = pcmk__str_copy(transform_enter);
}
known_schemas = g_list_append(known_schemas, schema);
if (schema->transform != NULL) {
crm_debug("Added supported schema %d: %s (upgrades with %s.xsl)",
last, schema->name, schema->transform);
} else {
crm_debug("Added supported schema %d: %s", last, schema->name);
}
}
/*!
* \internal
* \brief Add version-specified schema + auxiliary data to internal bookkeeping.
* \return Standard Pacemaker return value (the only possible values are
* \c ENOENT when no upgrade schema is associated, or \c pcmk_rc_ok otherwise.
*
* \note There's no reliance on the particular order of schemas entering here.
*
* \par A bit of theory
* We track 3 XSLT stylesheets that differ per usage:
* - "upgrade":
* . sparsely spread over the sequence of all available schemas,
* as they are only relevant when major version of the schema
* is getting bumped -- in that case, it MUST be set
* . name convention: upgrade-X.Y.xsl
* - "upgrade-enter":
* . may only accompany "upgrade" occurrence, but doesn't need to
* be present anytime such one is, i.e., it MAY not be set when
* "upgrade" is
* . name convention: upgrade-X.Y-enter.xsl,
* when not present: upgrade-enter.xsl
* - "upgrade-leave":
* . like "upgrade-enter", but SHOULD be present whenever
* "upgrade-enter" is (and vice versa, but that's only
* to prevent confusion based on observing the files,
* it would get ignored regardless)
* . name convention: (see "upgrade-enter")
*/
static int
add_schema_by_version(const pcmk__schema_version_t *version, bool transform_expected)
{
bool transform_onleave = FALSE;
int rc = pcmk_rc_ok;
struct stat s;
char *xslt = NULL,
*transform_upgrade = NULL,
*transform_enter = NULL;
/* prologue for further transform_expected handling */
if (transform_expected) {
/* check if there's suitable "upgrade" stylesheet */
transform_upgrade = schema_strdup_printf("upgrade-", *version, );
xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
transform_upgrade);
}
if (!transform_expected) {
/* jump directly to the end */
} else if (stat(xslt, &s) == 0) {
/* perhaps there's also a targeted "upgrade-enter" stylesheet */
transform_enter = schema_strdup_printf("upgrade-", *version, "-enter");
free(xslt);
xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
transform_enter);
if (stat(xslt, &s) != 0) {
/* or initially, at least a generic one */
crm_debug("Upgrade-enter transform %s.xsl not found", xslt);
free(xslt);
free(transform_enter);
transform_enter = strdup("upgrade-enter");
xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
transform_enter);
if (stat(xslt, &s) != 0) {
crm_debug("Upgrade-enter transform %s.xsl not found, either", xslt);
free(xslt);
xslt = NULL;
}
}
/* xslt contains full path to "upgrade-enter" stylesheet */
if (xslt != NULL) {
/* then there should be "upgrade-leave" counterpart (enter->leave) */
memcpy(strrchr(xslt, '-') + 1, "leave", sizeof("leave") - 1);
transform_onleave = (stat(xslt, &s) == 0);
free(xslt);
} else {
free(transform_enter);
transform_enter = NULL;
}
} else {
crm_err("Upgrade transform %s not found", xslt);
free(xslt);
free(transform_upgrade);
transform_upgrade = NULL;
rc = ENOENT;
}
add_schema(pcmk__schema_validator_rng, version, NULL,
transform_upgrade, transform_enter, transform_onleave);
free(transform_upgrade);
free(transform_enter);
return rc;
}
static void
wrap_libxslt(bool finalize)
{
static xsltSecurityPrefsPtr secprefs;
int ret = 0;
/* security framework preferences */
if (!finalize) {
CRM_ASSERT(secprefs == NULL);
secprefs = xsltNewSecurityPrefs();
ret = xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_WRITE_FILE,
xsltSecurityForbid)
| xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_CREATE_DIRECTORY,
xsltSecurityForbid)
| xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_READ_NETWORK,
xsltSecurityForbid)
| xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_WRITE_NETWORK,
xsltSecurityForbid);
if (ret != 0) {
return;
}
} else {
xsltFreeSecurityPrefs(secprefs);
secprefs = NULL;
}
/* cleanup only */
if (finalize) {
xsltCleanupGlobals();
}
}
void
pcmk__load_schemas_from_dir(const char *dir)
{
int lpc, max;
struct dirent **namelist = NULL;
max = scandir(dir, &namelist, schema_filter, schema_cmp_directory);
if (max < 0) {
crm_warn("Could not load schemas from %s: %s", dir, strerror(errno));
return;
}
for (lpc = 0; lpc < max; lpc++) {
bool transform_expected = false;
pcmk__schema_version_t version = SCHEMA_ZERO;
if (!version_from_filename(namelist[lpc]->d_name, &version)) {
// Shouldn't be possible, but makes static analysis happy
crm_warn("Skipping schema '%s': could not parse version",
namelist[lpc]->d_name);
continue;
}
if ((lpc + 1) < max) {
pcmk__schema_version_t next_version = SCHEMA_ZERO;
if (version_from_filename(namelist[lpc+1]->d_name, &next_version)
&& (version.v[0] < next_version.v[0])) {
transform_expected = true;
}
}
if (add_schema_by_version(&version, transform_expected) != pcmk_rc_ok) {
break;
}
}
for (lpc = 0; lpc < max; lpc++) {
free(namelist[lpc]);
}
free(namelist);
}
static gint
schema_sort_GCompareFunc(gconstpointer a, gconstpointer b)
{
const pcmk__schema_t *schema_a = a;
const pcmk__schema_t *schema_b = b;
if (pcmk__str_eq(schema_a->name, "pacemaker-next", pcmk__str_none)) {
if (pcmk__str_eq(schema_b->name, PCMK_VALUE_NONE, pcmk__str_none)) {
return -1;
} else {
return 1;
}
} else if (pcmk__str_eq(schema_a->name, PCMK_VALUE_NONE, pcmk__str_none)) {
return 1;
} else if (pcmk__str_eq(schema_b->name, "pacemaker-next", pcmk__str_none)) {
return -1;
} else {
return schema_cmp(schema_a->version, schema_b->version);
}
}
/*!
* \internal
* \brief Sort the list of known schemas such that all pacemaker-X.Y are in
* version order, then pacemaker-next, then none
*
* This function should be called whenever additional schemas are loaded using
* pcmk__load_schemas_from_dir(), after the initial sets in crm_schema_init().
*/
void
pcmk__sort_schemas(void)
{
known_schemas = g_list_sort(known_schemas, schema_sort_GCompareFunc);
}
/*!
* \internal
* \brief Load pacemaker schemas into cache
*
* \note This currently also serves as an entry point for the
* generic initialization of the libxslt library.
*/
void
crm_schema_init(void)
{
const char *remote_schema_dir = pcmk__remote_schema_dir();
char *base = pcmk__xml_artefact_root(pcmk__xml_artefact_ns_legacy_rng);
const pcmk__schema_version_t zero = SCHEMA_ZERO;
wrap_libxslt(false);
pcmk__load_schemas_from_dir(base);
pcmk__load_schemas_from_dir(remote_schema_dir);
// @COMPAT: Deprecated since 2.1.5
add_schema(pcmk__schema_validator_rng, &zero, "pacemaker-next",
NULL, NULL, FALSE);
add_schema(pcmk__schema_validator_none, &zero, PCMK_VALUE_NONE,
NULL, NULL, FALSE);
/* This shouldn't be strictly necessary, but we'll do it here just in case
* there's anything in PCMK__REMOTE_SCHEMA_DIR that messes up the order.
*/
pcmk__sort_schemas();
}
static gboolean
validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler, void *error_handler_context, const char *relaxng_file,
relaxng_ctx_cache_t **cached_ctx)
{
int rc = 0;
gboolean valid = TRUE;
relaxng_ctx_cache_t *ctx = NULL;
CRM_CHECK(doc != NULL, return FALSE);
CRM_CHECK(relaxng_file != NULL, return FALSE);
if (cached_ctx && *cached_ctx) {
ctx = *cached_ctx;
} else {
crm_debug("Creating RNG parser context");
ctx = pcmk__assert_alloc(1, sizeof(relaxng_ctx_cache_t));
ctx->parser = xmlRelaxNGNewParserCtxt(relaxng_file);
CRM_CHECK(ctx->parser != NULL, goto cleanup);
if (error_handler) {
xmlRelaxNGSetParserErrors(ctx->parser,
(xmlRelaxNGValidityErrorFunc) error_handler,
(xmlRelaxNGValidityWarningFunc) error_handler,
error_handler_context);
} else {
xmlRelaxNGSetParserErrors(ctx->parser,
(xmlRelaxNGValidityErrorFunc) fprintf,
(xmlRelaxNGValidityWarningFunc) fprintf,
stderr);
}
ctx->rng = xmlRelaxNGParse(ctx->parser);
CRM_CHECK(ctx->rng != NULL,
crm_err("Could not find/parse %s", relaxng_file);
goto cleanup);
ctx->valid = xmlRelaxNGNewValidCtxt(ctx->rng);
CRM_CHECK(ctx->valid != NULL, goto cleanup);
if (error_handler) {
xmlRelaxNGSetValidErrors(ctx->valid,
(xmlRelaxNGValidityErrorFunc) error_handler,
(xmlRelaxNGValidityWarningFunc) error_handler,
error_handler_context);
} else {
xmlRelaxNGSetValidErrors(ctx->valid,
(xmlRelaxNGValidityErrorFunc) fprintf,
(xmlRelaxNGValidityWarningFunc) fprintf,
stderr);
}
}
rc = xmlRelaxNGValidateDoc(ctx->valid, doc);
if (rc > 0) {
valid = FALSE;
} else if (rc < 0) {
crm_err("Internal libxml error during validation");
}
cleanup:
if (cached_ctx) {
*cached_ctx = ctx;
} else {
if (ctx->parser != NULL) {
xmlRelaxNGFreeParserCtxt(ctx->parser);
}
if (ctx->valid != NULL) {
xmlRelaxNGFreeValidCtxt(ctx->valid);
}
if (ctx->rng != NULL) {
xmlRelaxNGFree(ctx->rng);
}
free(ctx);
}
return valid;
}
static void
free_schema(gpointer data)
{
pcmk__schema_t *schema = data;
relaxng_ctx_cache_t *ctx = NULL;
switch (schema->validator) {
case pcmk__schema_validator_none: // not cached
break;
case pcmk__schema_validator_rng: // cached
ctx = (relaxng_ctx_cache_t *) schema->cache;
if (ctx == NULL) {
break;
}
if (ctx->parser != NULL) {
xmlRelaxNGFreeParserCtxt(ctx->parser);
}
if (ctx->valid != NULL) {
xmlRelaxNGFreeValidCtxt(ctx->valid);
}
if (ctx->rng != NULL) {
xmlRelaxNGFree(ctx->rng);
}
free(ctx);
schema->cache = NULL;
break;
}
free(schema->name);
free(schema->transform);
free(schema->transform_enter);
}
/*!
* \internal
* \brief Clean up global memory associated with XML schemas
*/
void
crm_schema_cleanup(void)
{
g_list_free_full(known_schemas, free_schema);
known_schemas = NULL;
wrap_libxslt(true);
}
static gboolean
validate_with(xmlNode *xml, pcmk__schema_t *schema, xmlRelaxNGValidityErrorFunc error_handler, void* error_handler_context)
{
gboolean valid = FALSE;
char *file = NULL;
relaxng_ctx_cache_t **cache = NULL;
if (schema == NULL) {
return FALSE;
}
if (schema->validator == pcmk__schema_validator_none) {
return TRUE;
}
if (pcmk__str_eq(schema->name, "pacemaker-next", pcmk__str_none)) {
crm_warn("The pacemaker-next schema is deprecated and will be removed "
"in a future release.");
}
file = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng,
schema->name);
crm_trace("Validating with %s (type=%d)",
pcmk__s(file, "missing schema"), schema->validator);
switch (schema->validator) {
case pcmk__schema_validator_rng:
cache = (relaxng_ctx_cache_t **) &(schema->cache);
valid = validate_with_relaxng(xml->doc, error_handler, error_handler_context, file, cache);
break;
default:
crm_err("Unknown validator type: %d", schema->validator);
break;
}
free(file);
return valid;
}
static bool
validate_with_silent(xmlNode *xml, pcmk__schema_t *schema)
{
bool rc, sl_backup = silent_logging;
silent_logging = TRUE;
rc = validate_with(xml, schema, (xmlRelaxNGValidityErrorFunc) xml_log, GUINT_TO_POINTER(LOG_ERR));
silent_logging = sl_backup;
return rc;
}
static void
dump_file(const char *filename)
{
FILE *fp = NULL;
int ch, line = 0;
CRM_CHECK(filename != NULL, return);
fp = fopen(filename, "r");
if (fp == NULL) {
crm_perror(LOG_ERR, "Could not open %s for reading", filename);
return;
}
fprintf(stderr, "%4d ", ++line);
do {
ch = getc(fp);
if (ch == EOF) {
putc('\n', stderr);
break;
} else if (ch == '\n') {
fprintf(stderr, "\n%4d ", ++line);
} else {
putc(ch, stderr);
}
} while (1);
fclose(fp);
}
gboolean
validate_xml_verbose(const xmlNode *xml_blob)
{
int fd = 0;
xmlDoc *doc = NULL;
xmlNode *xml = NULL;
gboolean rc = FALSE;
char *filename = NULL;
filename = crm_strdup_printf("%s/cib-invalid.XXXXXX", pcmk__get_tmpdir());
umask(S_IWGRP | S_IWOTH | S_IROTH);
fd = mkstemp(filename);
pcmk__xml_write_fd(xml_blob, filename, fd, false, NULL);
dump_file(filename);
doc = xmlReadFile(filename, NULL, 0);
xml = xmlDocGetRootElement(doc);
rc = validate_xml(xml, NULL, FALSE);
free_xml(xml);
unlink(filename);
free(filename);
return rc;
}
gboolean
validate_xml(xmlNode *xml_blob, const char *validation, gboolean to_logs)
{
return pcmk__validate_xml(xml_blob, validation, to_logs ? (xmlRelaxNGValidityErrorFunc) xml_log : NULL, GUINT_TO_POINTER(LOG_ERR));
}
gboolean
pcmk__validate_xml(xmlNode *xml_blob, const char *validation, xmlRelaxNGValidityErrorFunc error_handler, void* error_handler_context)
{
int version = 0;
CRM_CHECK((xml_blob != NULL) && (xml_blob->doc != NULL), return FALSE);
if (validation == NULL) {
validation = crm_element_value(xml_blob, PCMK_XA_VALIDATE_WITH);
}
if (validation == NULL) {
bool valid = FALSE;
for (GList *iter = known_schemas; iter != NULL; iter = iter->next) {
pcmk__schema_t *schema = iter->data;
if (validate_with(xml_blob, schema, NULL, NULL)) {
valid = TRUE;
crm_xml_add(xml_blob, 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 < g_list_length(known_schemas)) {
pcmk__schema_t *schema = g_list_nth_data(known_schemas, version);
return validate_with(xml_blob, schema, error_handler,
error_handler_context);
}
crm_err("Unknown validator: %s", validation);
return FALSE;
}
/* With this arrangement, an attempt to identify the message severity
as explicitly signalled directly from XSLT is performed in rather
a smart way (no reliance on formatting string + arguments being
always specified as ["%s", purposeful_string], as it can also be
["%s: %s", some_prefix, purposeful_string] etc. so every argument
pertaining %s specifier is investigated), and if such a mark found,
the respective level is determined and, when the messages are to go
to the native logs, the mark itself gets dropped
(by the means of string shift).
NOTE: whether the native logging is the right sink is decided per
the ctx parameter -- NULL denotes this case, otherwise it
carries a pointer to the numeric expression of the desired
target logging level (messages with higher level will be
suppressed)
NOTE: on some architectures, this string shift may not have any
effect, but that's an acceptable tradeoff
The logging level for not explicitly designated messages
(suspicious, likely internal errors or some runaways) is
LOG_WARNING.
*/
static void G_GNUC_PRINTF(2, 3)
cib_upgrade_err(void *ctx, const char *fmt, ...)
{
va_list ap, aq;
char *arg_cur;
bool found = FALSE;
const char *fmt_iter = fmt;
uint8_t msg_log_level = LOG_WARNING; /* default for runaway messages */
const unsigned * log_level = (const unsigned *) ctx;
enum {
escan_seennothing,
escan_seenpercent,
} scan_state = escan_seennothing;
va_start(ap, fmt);
va_copy(aq, ap);
while (!found && *fmt_iter != '\0') {
/* while casing schema borrowed from libqb:qb_vsnprintf_serialize */
switch (*fmt_iter++) {
case '%':
if (scan_state == escan_seennothing) {
scan_state = escan_seenpercent;
} else if (scan_state == escan_seenpercent) {
scan_state = escan_seennothing;
}
break;
case 's':
if (scan_state == escan_seenpercent) {
scan_state = escan_seennothing;
arg_cur = va_arg(aq, char *);
if (arg_cur != NULL) {
switch (arg_cur[0]) {
case 'W':
if (!strncmp(arg_cur, "WARNING: ",
sizeof("WARNING: ") - 1)) {
msg_log_level = LOG_WARNING;
}
if (ctx == NULL) {
memmove(arg_cur, arg_cur + sizeof("WARNING: ") - 1,
strlen(arg_cur + sizeof("WARNING: ") - 1) + 1);
}
found = TRUE;
break;
case 'I':
if (!strncmp(arg_cur, "INFO: ",
sizeof("INFO: ") - 1)) {
msg_log_level = LOG_INFO;
}
if (ctx == NULL) {
memmove(arg_cur, arg_cur + sizeof("INFO: ") - 1,
strlen(arg_cur + sizeof("INFO: ") - 1) + 1);
}
found = TRUE;
break;
case 'D':
if (!strncmp(arg_cur, "DEBUG: ",
sizeof("DEBUG: ") - 1)) {
msg_log_level = LOG_DEBUG;
}
if (ctx == NULL) {
memmove(arg_cur, arg_cur + sizeof("DEBUG: ") - 1,
strlen(arg_cur + sizeof("DEBUG: ") - 1) + 1);
}
found = TRUE;
break;
}
}
}
break;
case '#': case '-': case ' ': case '+': case '\'': case 'I': case '.':
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
case '*':
break;
case 'l':
case 'z':
case 't':
case 'j':
case 'd': case 'i':
case 'o':
case 'u':
case 'x': case 'X':
case 'e': case 'E':
case 'f': case 'F':
case 'g': case 'G':
case 'a': case 'A':
case 'c':
case 'p':
if (scan_state == escan_seenpercent) {
(void) va_arg(aq, void *); /* skip forward */
scan_state = escan_seennothing;
}
break;
default:
scan_state = escan_seennothing;
break;
}
}
if (log_level != NULL) {
/* intention of the following offset is:
cibadmin -V -> start showing INFO labelled messages */
if (*log_level + 4 >= msg_log_level) {
vfprintf(stderr, fmt, ap);
}
} else {
PCMK__XML_LOG_BASE(msg_log_level, TRUE, 0, "CIB upgrade: ", fmt, ap);
}
va_end(aq);
va_end(ap);
}
+/*!
+ * \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;
xmlDocPtr res = NULL;
xsltStylesheet *xslt = NULL;
xform = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
transform);
/* for capturing, e.g., what's emitted via */
if (to_logs) {
xsltSetGenericErrorFunc(NULL, cib_upgrade_err);
} else {
xsltSetGenericErrorFunc(&crm_log_level, cib_upgrade_err);
}
xslt = xsltParseStylesheetFile((pcmkXmlStr) xform);
CRM_CHECK(xslt != NULL, goto cleanup);
res = xsltApplyStylesheet(xslt, xml->doc, NULL);
CRM_CHECK(res != NULL, goto cleanup);
xsltSetGenericErrorFunc(NULL, NULL); /* restore default one */
out = xmlDocGetRootElement(res);
cleanup:
if (xslt) {
xsltFreeStylesheet(xslt);
}
free(xform);
return out;
}
/*!
* \internal
- * \brief Possibly full enter->upgrade->leave trip per internal bookkeeping.
+ * \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.
*
- * \note Only emits warnings about enter/leave phases in case of issues.
+ * \param[in] original_xml XML to transform
+ * \param[in] schema_index Index of schema that successfully validates
+ * \p original_xml
+ * \param[in] to_logs If false, certain validation errors will be sent to
+ * stderr rather than logged
+ *
+ * \return XML result of schema transforms if successful, otherwise NULL
*/
static xmlNode *
-apply_upgrade(xmlNode *xml, const pcmk__schema_t *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;
+
+ CRM_ASSERT((schema != NULL) && (upgraded_schema != NULL));
- if (schema->transform_enter) {
- crm_debug("Upgrading %s-style configuration, pre-upgrade phase with %s.xsl",
- schema->name, schema->transform_enter);
+ 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);
}
free(transform_leave);
}
+ if (final == NULL) {
+ return NULL;
+ }
+
+ // Ensure result validates with its new schema
+ if (!validate_with(final, upgraded_schema, error_handler,
+ GUINT_TO_POINTER(LOG_ERR))) {
+ crm_err("Schema upgrade from %s to %s failed: "
+ "XSL transform %s produced an invalid configuration",
+ schema->name, upgraded_schema->name, schema->transform);
+ crm_log_xml_debug(final, "bad-transform-result");
+ free_xml(final);
+ return NULL;
+ }
+
+ crm_info("Schema upgrade from %s to %s succeeded",
+ schema->name, upgraded_schema->name);
return final;
}
const char *
get_schema_name(int version)
{
pcmk__schema_t *schema = g_list_nth_data(known_schemas, version);
return (schema != NULL)? schema->name : "unknown";
}
int
get_schema_version(const char *name)
{
int lpc = 0;
if (name == NULL) {
name = PCMK_VALUE_NONE;
}
for (GList *iter = known_schemas; iter != NULL; iter = iter->next) {
pcmk__schema_t *schema = iter->data;
if (pcmk__str_eq(name, schema->name, pcmk__str_casei)) {
return lpc;
}
lpc++;
}
return -1;
}
/* set which validation to use */
+/*!
+ * \brief Update CIB XML to latest schema that validates it
+ *
+ * \param[in,out] xml_blob XML to update (may be freed and replaced after
+ * being transformed)
+ * \param[out] best If not NULL, set to schema index of latest schema
+ * that validates \p xml_blob
+ * \param[in] max If positive, do not update \p xml_blob to any
+ * schema past this index
+ * \param[in] transform If false, do not update \p xml_blob 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 Legacy Pacemaker return code
+ */
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 local_best = 0;
int next = -1; /* -1 denotes "inactive" value */
xmlRelaxNGValidityErrorFunc error_handler =
to_logs ? (xmlRelaxNGValidityErrorFunc) xml_log : NULL;
- CRM_CHECK(best != NULL, return -EINVAL);
- *best = 0;
+ if (best != NULL) {
+ *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, PCMK_XA_VALIDATE_WITH);
if (value != NULL) {
match = get_schema_version(value);
+ if (match >= max_stable_schemas) {
+ // No higher version is available
+ free(value);
+ if (best != NULL) {
+ *best = match;
+ }
+ return pcmk_ok;
+ }
lpc = match;
if (lpc >= 0 && transform == FALSE) {
- *best = lpc++;
+ local_best = lpc++;
} else if (lpc < 0) {
crm_debug("Unknown validation schema");
lpc = 0;
}
}
- if (match >= max_stable_schemas) {
- /* nothing to do */
- free(value);
- *best = match;
- return pcmk_ok;
- }
-
while (lpc <= max_stable_schemas) {
/* FIXME: This will cause us to walk the known_schemas list every time
* this loop iterates, which is not ideal. However, for now it's a lot
* easier than trying to get all the loop indices we're using here
* sorted out and working correctly.
*/
pcmk__schema_t *schema = g_list_nth_data(known_schemas, lpc);
+ pcmk__schema_t *next_schema = NULL;
+ xmlNode *upgrade = NULL;
crm_debug("Testing '%s' validation (%d of %d)",
pcmk__s(schema->name, ""), lpc, max_stable_schemas);
if (validate_with(xml, schema, error_handler, GUINT_TO_POINTER(LOG_ERR)) == FALSE) {
if (next != -1) {
crm_info("Configuration not valid for schema: %s",
schema->name);
next = -1;
} else {
crm_trace("%s validation failed", pcmk__s(schema->name, ""));
}
- if (*best) {
+ if (local_best > 0) {
/* we've satisfied the validation, no need to check further */
break;
}
rc = -pcmk_err_schema_validation;
-
- } else {
- if (next != -1) {
- crm_debug("Configuration valid for schema: %s", schema->name);
- next = -1;
- }
- rc = pcmk_ok;
+ lpc++; // Try again with the next higher schema
+ continue;
}
- if (rc == pcmk_ok) {
- *best = lpc;
+ if (next != -1) {
+ crm_debug("Configuration valid for schema: %s", schema->name);
+ next = -1;
}
+ rc = pcmk_ok;
+ local_best = lpc;
- if (rc == pcmk_ok && transform) {
- xmlNode *upgrade = NULL;
- pcmk__schema_t *next_schema = NULL;
- next = lpc+1;
-
- if (next > max_stable_schemas) {
- /* There is no next version */
- crm_trace("Stopping at %s", schema->name);
- break;
- }
-
- if (max > 0 && (lpc == max || next > max)) {
- crm_trace("Upgrade limit reached at %s (lpc=%d, next=%d, max=%d)",
- schema->name, lpc, next, max);
- break;
- }
-
- next_schema = g_list_nth_data(known_schemas, next);
- CRM_ASSERT(next_schema != NULL);
+ if (!transform) {
+ /* Validation with this schema succeeded. We are not doing
+ * transforms, so try the next schema using the same XML.
+ */
+ lpc++;
+ continue;
+ }
- if (schema->transform == NULL
- /* possibly avoid transforming when readily valid
- (in general more restricted when crossing the major
- version boundary, as X.0 "transitional" version is
- expected to be more strict than it's successors that
- may re-allow constructs from previous major line) */
- || validate_with_silent(xml, next_schema)) {
- crm_debug("%s-style configuration is also valid for %s",
- schema->name, next_schema->name);
+ next = lpc+1;
- lpc = next;
+ if (next > max_stable_schemas) {
+ /* There is no next version */
+ crm_trace("Stopping at %s", schema->name);
+ break;
+ }
- } else {
- crm_debug("Upgrading %s-style configuration to %s with %s.xsl",
- schema->name, next_schema->name, schema->transform);
-
- upgrade = apply_upgrade(xml, schema, to_logs);
- if (upgrade == NULL) {
- crm_err("Transformation %s.xsl failed", schema->transform);
- rc = -pcmk_err_transform_failed;
-
- } else if (validate_with(upgrade, next_schema, error_handler,
- GUINT_TO_POINTER(LOG_ERR))) {
- crm_info("Transformation %s.xsl successful", schema->transform);
- lpc = next;
- *best = next;
- free_xml(xml);
- xml = upgrade;
- rc = pcmk_ok;
+ if (max > 0 && (lpc == max || next > max)) {
+ crm_trace("Upgrade limit reached at %s (lpc=%d, next=%d, max=%d)",
+ schema->name, lpc, next, max);
+ break;
+ }
- } else {
- crm_err("Transformation %s.xsl did not produce a valid configuration",
- schema->transform);
- crm_log_xml_info(upgrade, "transform:bad");
- free_xml(upgrade);
- rc = -pcmk_err_schema_validation;
- }
- next = -1;
- }
+ next_schema = g_list_nth_data(known_schemas, next);
+ CRM_ASSERT(next_schema != NULL);
+
+ if ((schema->transform == NULL)
+ || validate_with_silent(xml, next_schema)) {
+ /* The next schema either doesn't require a transform, or validates
+ * successfully even without doing the transform. We can skip the
+ * transform and use it with the same XML in the next iteration.
+ */
+ crm_debug("%s-style configuration is also valid for %s",
+ schema->name, next_schema->name);
+ lpc = next;
+ continue;
}
- if (transform == FALSE || rc != pcmk_ok) {
- /* we need some progress! */
+ upgrade = apply_upgrade(xml, lpc, to_logs);
+ if (upgrade == NULL) {
+ rc = -pcmk_err_transform_failed;
+ } else {
+ lpc = next;
+ local_best = next;
+ free_xml(xml);
+ xml = upgrade;
+ }
+ next = -1;
+ if (rc != pcmk_ok) {
+ /* 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.
+ */
lpc++;
}
}
- if (*best > match && *best) {
- pcmk__schema_t *best_schema = g_list_nth_data(known_schemas, *best);
+ if ((local_best > 0) && (local_best > match)) {
+ pcmk__schema_t *best_schema = g_list_nth_data(known_schemas,
+ local_best);
crm_info("%s the configuration from %s to %s",
transform?"Transformed":"Upgraded", pcmk__s(value, ""),
best_schema->name);
crm_xml_add(xml, PCMK_XA_VALIDATE_WITH, best_schema->name);
}
*xml_blob = xml;
free(value);
+
+ if (best != NULL) {
+ *best = local_best;
+ }
return rc;
}
gboolean
cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs)
{
gboolean rc = TRUE;
const char *value = crm_element_value(*xml, PCMK_XA_VALIDATE_WITH);
char *const orig_value = strdup(value == NULL ? "(none)" : value);
int version = get_schema_version(value);
int orig_version = version;
int min_version = pcmk__find_x_0_schema_index();
if (version < min_version) {
// Current configuration schema is not acceptable, try to update
xmlNode *converted = NULL;
converted = pcmk__xml_copy(NULL, *xml);
update_validation(&converted, &version, 0, TRUE, to_logs);
value = crm_element_value(converted, PCMK_XA_VALIDATE_WITH);
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;
}
/*!
* \internal
* \brief Return a list of all schema files and any associated XSLT files
* later than the given one
* \brief Return a list of all schema versions later than the given one
*
* \param[in] schema The schema to compare against (for example,
* "pacemaker-3.1.rng" or "pacemaker-3.1")
*
* \note The caller is responsible for freeing both the returned list and
* the elements of the list
*/
GList *
pcmk__schema_files_later_than(const char *name)
{
GList *lst = NULL;
pcmk__schema_version_t ver;
if (!version_from_filename(name, &ver)) {
return lst;
}
for (GList *iter = g_list_nth(known_schemas, xml_latest_schema_index());
iter != NULL; iter = iter->prev) {
pcmk__schema_t *schema = iter->data;
char *s = NULL;
if (schema_cmp(ver, schema->version) != -1) {
continue;
}
s = crm_strdup_printf("%s.rng", schema->name);
lst = g_list_prepend(lst, s);
if (schema->transform != NULL) {
char *xform = crm_strdup_printf("%s.xsl", schema->transform);
lst = g_list_prepend(lst, xform);
}
if (schema->transform_enter != NULL) {
char *enter = crm_strdup_printf("%s.xsl", schema->transform_enter);
lst = g_list_prepend(lst, enter);
if (schema->transform_onleave) {
int last_dash = strrchr(enter, '-') - enter;
char *leave = crm_strdup_printf("%.*s-leave.xsl", last_dash, enter);
lst = g_list_prepend(lst, leave);
}
}
}
return lst;
}
static void
append_href(xmlNode *xml, void *user_data)
{
GList **list = user_data;
char *href = crm_element_value_copy(xml, "href");
if (href == NULL) {
return;
}
*list = g_list_prepend(*list, href);
}
static void
external_refs_in_schema(GList **list, const char *contents)
{
/* local-name()= is needed to ignore the xmlns= setting at the top of
* the XML file. Otherwise, the xpath query will always return nothing.
*/
const char *search = "//*[local-name()='externalRef'] | //*[local-name()='include']";
xmlNode *xml = pcmk__xml_parse(contents);
crm_foreach_xpath_result(xml, search, append_href, list);
free_xml(xml);
}
static int
read_file_contents(const char *file, char **contents)
{
int rc = pcmk_rc_ok;
char *path = NULL;
if (pcmk__ends_with(file, ".rng")) {
path = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng, file);
} else {
path = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, file);
}
rc = pcmk__file_contents(path, contents);
free(path);
return rc;
}
static void
add_schema_file_to_xml(xmlNode *parent, const char *file, GList **already_included)
{
char *contents = NULL;
char *path = NULL;
xmlNode *file_node = NULL;
GList *includes = NULL;
int rc = pcmk_rc_ok;
/* If we already included this file, don't do so again. */
if (g_list_find_custom(*already_included, file, (GCompareFunc) strcmp) != NULL) {
return;
}
/* Ensure whatever file we were given has a suffix we know about. If not,
* just assume it's an RNG file.
*/
if (!pcmk__ends_with(file, ".rng") && !pcmk__ends_with(file, ".xsl")) {
path = crm_strdup_printf("%s.rng", file);
} else {
path = pcmk__str_copy(file);
}
rc = read_file_contents(path, &contents);
if (rc != pcmk_rc_ok || contents == NULL) {
crm_warn("Could not read schema file %s: %s", file, pcmk_rc_str(rc));
free(path);
return;
}
/* Create a new 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 or nodes and build up
* a list of the files they reference.
*/
external_refs_in_schema(&includes, contents);
/* For each referenced file, recurse to add it (and potentially anything it
* references, ...) to the XML.
*/
for (GList *iter = includes; iter != NULL; iter = iter->next) {
add_schema_file_to_xml(parent, iter->data, already_included);
}
free(contents);
g_list_free_full(includes, free);
}
/*!
* \internal
* \brief Add an XML schema file and all the files it references as children
* of a given XML node
*
* \param[in,out] parent The parent XML node
* \param[in] name The schema version to compare against
* (for example, "pacemaker-3.1" or "pacemaker-3.1.rng")
* \param[in,out] already_included A list of names that have already been added
* to the parent node.
*
* \note The caller is responsible for freeing both the returned list and
* the elements of the list
*/
void
pcmk__build_schema_xml_node(xmlNode *parent, const char *name, GList **already_included)
{
/* First, create an unattached node to add all the schema files to as children. */
xmlNode *schema_node = pcmk__xe_create(NULL, PCMK__XA_SCHEMA);
crm_xml_add(schema_node, PCMK_XA_VERSION, name);
add_schema_file_to_xml(schema_node, name, already_included);
/* Then, if we actually added any children, attach the node to parent. If
* we did not add any children (for instance, name was invalid), this prevents
* us from returning a document with additional empty children.
*/
if (schema_node->children != NULL) {
xmlAddChild(parent, schema_node);
} else {
free_xml(schema_node);
}
}
/*!
* \internal
* \brief Return the directory containing any extra schema files that a
* Pacemaker Remote node fetched from the cluster
*/
const char *
pcmk__remote_schema_dir(void)
{
const char *dir = pcmk__env_option(PCMK__ENV_REMOTE_SCHEMA_DIRECTORY);
if (pcmk__str_empty(dir)) {
return PCMK__REMOTE_SCHEMA_DIR;
}
return dir;
}
void
pcmk__log_known_schemas(void)
{
int lpc = 0;
for (GList *iter = known_schemas; iter != NULL; iter = iter->next) {
pcmk__schema_t *schema = iter->data;
if (schema->transform != NULL) {
crm_debug("known_schemas[%d] => %s (upgrades with %s.xsl)",
lpc, schema->name, schema->transform);
} else {
crm_debug("known_schemas[%d] => %s", lpc, schema->name);
}
lpc++;
}
}
diff --git a/tools/cibadmin.c b/tools/cibadmin.c
index e7c14b8a1f..7837564425 100644
--- a/tools/cibadmin.c
+++ b/tools/cibadmin.c
@@ -1,954 +1,953 @@
/*
* Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#define SUMMARY "query and edit the Pacemaker configuration"
#define INDENT " "
enum cibadmin_section_type {
cibadmin_section_all = 0,
cibadmin_section_scope,
cibadmin_section_xpath,
};
static int request_id = 0;
static cib_t *the_cib = NULL;
static GMainLoop *mainloop = NULL;
static crm_exit_t exit_code = CRM_EX_OK;
static struct {
const char *cib_action;
int cmd_options;
enum cibadmin_section_type section_type;
char *cib_section;
char *validate_with;
gint message_timeout_sec;
enum pcmk__acl_render_how acl_render_mode;
gchar *cib_user;
gchar *dest_node;
gchar *input_file;
gchar *input_xml;
gboolean input_stdin;
bool delete_all;
gboolean allow_create;
gboolean force;
gboolean get_node_path;
gboolean local;
gboolean no_children;
gboolean sync_call;
/* @COMPAT: For "-!" version option. Not advertised nor marked as
* deprecated, but accepted.
*/
gboolean extended_version;
//! \deprecated
gboolean no_bcast;
} options;
int do_init(void);
static int do_work(xmlNode *input, xmlNode **output);
void cibadmin_op_callback(xmlNode *msg, int call_id, int rc, xmlNode *output,
void *user_data);
static void
print_xml_output(xmlNode * xml)
{
if (!xml) {
return;
} else if (xml->type != XML_ELEMENT_NODE) {
return;
}
if (pcmk_is_set(options.cmd_options, cib_xpath_address)) {
const char *id = crm_element_value(xml, PCMK_XA_ID);
if (pcmk__xe_is(xml, PCMK__XE_XPATH_QUERY)) {
xmlNode *child = NULL;
for (child = xml->children; child; child = child->next) {
print_xml_output(child);
}
} else if (id) {
printf("%s\n", id);
}
} else {
GString *buf = g_string_sized_new(1024);
pcmk__xml_string(xml, pcmk__xml_fmt_pretty, buf, 0);
fprintf(stdout, "%s", buf->str);
g_string_free(buf, TRUE);
}
}
// Upgrade requested but already at latest schema
static void
report_schema_unchanged(void)
{
const char *err = pcmk_rc_str(pcmk_rc_schema_unchanged);
crm_info("Upgrade unnecessary: %s\n", err);
printf("Upgrade unnecessary: %s\n", err);
exit_code = CRM_EX_OK;
}
/*!
* \internal
* \brief Check whether the current CIB action is dangerous
* \return true if \p options.cib_action is dangerous, or false otherwise
*/
static inline bool
cib_action_is_dangerous(void)
{
return options.no_bcast || options.delete_all
|| pcmk__str_any_of(options.cib_action,
PCMK__CIB_REQUEST_UPGRADE,
PCMK__CIB_REQUEST_ERASE,
NULL);
}
/*!
* \internal
* \brief Determine whether the given CIB scope is valid for \p cibadmin
*
* \param[in] scope Scope to validate
*
* \return true if \p scope is valid, or false otherwise
* \note An invalid scope applies the operation to the entire CIB.
*/
static inline bool
scope_is_valid(const char *scope)
{
return pcmk__str_any_of(scope,
PCMK_XE_CONFIGURATION,
PCMK_XE_NODES,
PCMK_XE_RESOURCES,
PCMK_XE_CONSTRAINTS,
PCMK_XE_CRM_CONFIG,
PCMK_XE_RSC_DEFAULTS,
PCMK_XE_OP_DEFAULTS,
PCMK_XE_ACLS,
PCMK_XE_FENCING_TOPOLOGY,
PCMK_XE_TAGS,
PCMK_XE_ALERTS,
PCMK_XE_STATUS,
NULL);
}
static gboolean
command_cb(const gchar *option_name, const gchar *optarg, gpointer data,
GError **error)
{
options.delete_all = false;
if (pcmk__str_any_of(option_name, "-u", "--upgrade", NULL)) {
options.cib_action = PCMK__CIB_REQUEST_UPGRADE;
} else if (pcmk__str_any_of(option_name, "-Q", "--query", NULL)) {
options.cib_action = PCMK__CIB_REQUEST_QUERY;
} else if (pcmk__str_any_of(option_name, "-E", "--erase", NULL)) {
options.cib_action = PCMK__CIB_REQUEST_ERASE;
} else if (pcmk__str_any_of(option_name, "-B", "--bump", NULL)) {
options.cib_action = PCMK__CIB_REQUEST_BUMP;
} else if (pcmk__str_any_of(option_name, "-C", "--create", NULL)) {
options.cib_action = PCMK__CIB_REQUEST_CREATE;
} else if (pcmk__str_any_of(option_name, "-M", "--modify", NULL)) {
options.cib_action = PCMK__CIB_REQUEST_MODIFY;
} else if (pcmk__str_any_of(option_name, "-P", "--patch", NULL)) {
options.cib_action = PCMK__CIB_REQUEST_APPLY_PATCH;
} else if (pcmk__str_any_of(option_name, "-R", "--replace", NULL)) {
options.cib_action = PCMK__CIB_REQUEST_REPLACE;
} else if (pcmk__str_any_of(option_name, "-D", "--delete", NULL)) {
options.cib_action = PCMK__CIB_REQUEST_DELETE;
} else if (pcmk__str_any_of(option_name, "-d", "--delete-all", NULL)) {
options.cib_action = PCMK__CIB_REQUEST_DELETE;
options.delete_all = true;
} else if (pcmk__str_any_of(option_name, "-a", "--empty", NULL)) {
options.cib_action = "empty";
pcmk__str_update(&options.validate_with, optarg);
} else if (pcmk__str_any_of(option_name, "-5", "--md5-sum", NULL)) {
options.cib_action = "md5-sum";
} else if (pcmk__str_any_of(option_name, "-6", "--md5-sum-versioned",
NULL)) {
options.cib_action = "md5-sum-versioned";
} else {
// Should be impossible
return FALSE;
}
return TRUE;
}
static gboolean
show_access_cb(const gchar *option_name, const gchar *optarg, gpointer data,
GError **error)
{
if (pcmk__str_eq(optarg, "auto", pcmk__str_null_matches)) {
options.acl_render_mode = pcmk__acl_render_default;
} else if (g_strcmp0(optarg, "namespace") == 0) {
options.acl_render_mode = pcmk__acl_render_namespace;
} else if (g_strcmp0(optarg, "text") == 0) {
options.acl_render_mode = pcmk__acl_render_text;
} else if (g_strcmp0(optarg, "color") == 0) {
options.acl_render_mode = pcmk__acl_render_color;
} else {
g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
"Invalid value '%s' for option '%s'",
optarg, option_name);
return FALSE;
}
return TRUE;
}
static gboolean
section_cb(const gchar *option_name, const gchar *optarg, gpointer data,
GError **error)
{
if (pcmk__str_any_of(option_name, "-o", "--scope", NULL)) {
options.section_type = cibadmin_section_scope;
} else if (pcmk__str_any_of(option_name, "-A", "--xpath", NULL)) {
options.section_type = cibadmin_section_xpath;
} else {
// Should be impossible
return FALSE;
}
pcmk__str_update(&options.cib_section, optarg);
return TRUE;
}
static GOptionEntry command_entries[] = {
{ "upgrade", 'u', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Upgrade the configuration to the latest syntax", NULL },
{ "query", 'Q', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Query the contents of the CIB", NULL },
{ "erase", 'E', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Erase the contents of the whole CIB", NULL },
{ "bump", 'B', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Increase the CIB's epoch value by 1", NULL },
{ "create", 'C', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Create an object in the CIB (will fail if object already exists)",
NULL },
{ "modify", 'M', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Find object somewhere in CIB's XML tree and update it (fails if object "
"does not exist unless -c is also specified)",
NULL },
{ "patch", 'P', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Supply an update in the form of an XML diff (see crm_diff(8))", NULL },
{ "replace", 'R', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Recursively replace an object in the CIB", NULL },
{ "delete", 'D', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Delete first object matching supplied criteria (for example, "
"<" PCMK_XE_OP " " PCMK_XA_ID "=\"rsc1_op1\" "
PCMK_XA_NAME "=\"monitor\"/>).\n"
INDENT "The XML element name and all attributes must match in order for "
"the element to be deleted.",
NULL },
{ "delete-all", 'd', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
command_cb,
"When used with --xpath, remove all matching objects in the "
"configuration instead of just the first one",
NULL },
{ "empty", 'a', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK,
command_cb,
"Output an empty CIB. Accepts an optional schema name argument to use as "
"the " PCMK_XA_VALIDATE_WITH " value.\n"
INDENT "If no schema is given, the latest will be used.",
"[schema]" },
{ "md5-sum", '5', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Calculate the on-disk CIB digest", NULL },
{ "md5-sum-versioned", '6', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
command_cb, "Calculate an on-the-wire versioned CIB digest", NULL },
{ NULL }
};
static GOptionEntry data_entries[] = {
/* @COMPAT: These arguments should be last-wins. We can have an enum option
* that stores the input type, along with a single string option that stores
* the XML string for --xml-text, filename for --xml-file, or NULL for
* --xml-pipe.
*/
{ "xml-text", 'X', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING,
&options.input_xml, "Retrieve XML from the supplied string", "value" },
{ "xml-file", 'x', G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME,
&options.input_file, "Retrieve XML from the named file", "value" },
{ "xml-pipe", 'p', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
&options.input_stdin, "Retrieve XML from stdin", NULL },
{ NULL }
};
static GOptionEntry addl_entries[] = {
{ "force", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.force,
"Force the action to be performed", NULL },
{ "timeout", 't', G_OPTION_FLAG_NONE, G_OPTION_ARG_INT,
&options.message_timeout_sec,
"Time (in seconds) to wait before declaring the operation failed",
"value" },
{ "user", 'U', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.cib_user,
"Run the command with permissions of the named user (valid only for the "
"root and " CRM_DAEMON_USER " accounts)", "value" },
{ "sync-call", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
&options.sync_call, "Wait for call to complete before returning", NULL },
{ "local", 'l', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.local,
"Command takes effect locally (should be used only for queries)", NULL },
{ "scope", 'o', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, section_cb,
"Limit scope of operation to specific section of CIB\n"
INDENT "Valid values: " PCMK_XE_CONFIGURATION ", " PCMK_XE_NODES
", " PCMK_XE_RESOURCES ", " PCMK_XE_CONSTRAINTS
", " PCMK_XE_CRM_CONFIG ", " PCMK_XE_RSC_DEFAULTS ",\n"
INDENT " " PCMK_XE_OP_DEFAULTS ", " PCMK_XE_ACLS
", " PCMK_XE_FENCING_TOPOLOGY ", " PCMK_XE_TAGS ", " PCMK_XE_ALERTS
", " PCMK_XE_STATUS "\n"
INDENT "If both --scope/-o and --xpath/-a are specified, the last one to "
"appear takes effect",
"value" },
{ "xpath", 'A', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, section_cb,
"A valid XPath to use instead of --scope/-o\n"
INDENT "If both --scope/-o and --xpath/-a are specified, the last one to "
"appear takes effect",
"value" },
{ "node-path", 'e', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
&options.get_node_path,
"When performing XPath queries, return paths of any matches found\n"
INDENT "(for example, "
"\"/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION
"/" PCMK_XE_RESOURCES "/" PCMK_XE_CLONE
"[@" PCMK_XA_ID "='dummy-clone']"
"/" PCMK_XE_PRIMITIVE "[@" PCMK_XA_ID "='dummy']\")",
NULL },
{ "show-access", 'S', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK,
show_access_cb,
"Whether to use syntax highlighting for ACLs (with -Q/--query and "
"-U/--user)\n"
INDENT "Allowed values: 'color' (default for terminal), 'text' (plain text, "
"default for non-terminal),\n"
INDENT " 'namespace', or 'auto' (use default value)\n"
INDENT "Default value: 'auto'",
"[value]" },
{ "allow-create", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
&options.allow_create,
"(Advanced) Allow target of --modify/-M to be created if it does not "
"exist",
NULL },
{ "no-children", 'n', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
&options.no_children,
"(Advanced) When querying an object, do not include its children in the "
"result",
NULL },
{ "node", 'N', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.dest_node,
"(Advanced) Send command to the specified host", "value" },
// @COMPAT: Deprecated
{ "no-bcast", 'b', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE,
&options.no_bcast, "deprecated", NULL },
// @COMPAT: Deprecated
{ "host", 'h', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING,
&options.dest_node, "deprecated", NULL },
{ NULL }
};
static GOptionContext *
build_arg_context(pcmk__common_args_t *args)
{
const char *desc = NULL;
GOptionContext *context = NULL;
GOptionEntry extra_prog_entries[] = {
// @COMPAT: Deprecated
{ "extended-version", '!', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE,
&options.extended_version, "deprecated", NULL },
{ NULL }
};
desc = "Examples:\n\n"
"Query the configuration from the local node:\n\n"
"\t# cibadmin --query --local\n\n"
"Query just the cluster options configuration:\n\n"
"\t# cibadmin --query --scope " PCMK_XE_CRM_CONFIG "\n\n"
"Query all '" PCMK_META_TARGET_ROLE "' settings:\n\n"
"\t# cibadmin --query --xpath "
"\"//" PCMK_XE_NVPAIR
"[@" PCMK_XA_NAME "='" PCMK_META_TARGET_ROLE"']\"\n\n"
"Remove all '" PCMK_META_IS_MANAGED "' settings:\n\n"
"\t# cibadmin --delete-all --xpath "
"\"//" PCMK_XE_NVPAIR
"[@" PCMK_XA_NAME "='" PCMK_META_IS_MANAGED "']\"\n\n"
"Remove the resource named 'old':\n\n"
"\t# cibadmin --delete --xml-text "
"'<" PCMK_XE_PRIMITIVE " " PCMK_XA_ID "=\"old\"/>'\n\n"
"Remove all resources from the configuration:\n\n"
"\t# cibadmin --replace --scope " PCMK_XE_RESOURCES
" --xml-text '<" PCMK_XE_RESOURCES "/>'\n\n"
"Replace complete configuration with contents of "
"$HOME/pacemaker.xml:\n\n"
"\t# cibadmin --replace --xml-file $HOME/pacemaker.xml\n\n"
"Replace " PCMK_XE_CONSTRAINTS " section of configuration with "
"contents of $HOME/constraints.xml:\n\n"
"\t# cibadmin --replace --scope " PCMK_XE_CONSTRAINTS
" --xml-file $HOME/constraints.xml\n\n"
"Increase configuration version to prevent old configurations from "
"being loaded accidentally:\n\n"
"\t# cibadmin --modify --xml-text "
"'<" PCMK_XE_CIB " " PCMK_XA_ADMIN_EPOCH
"=\"" PCMK_XA_ADMIN_EPOCH "++\"/>'\n\n"
"Edit the configuration with your favorite $EDITOR:\n\n"
"\t# cibadmin --query > $HOME/local.xml\n\n"
"\t# $EDITOR $HOME/local.xml\n\n"
"\t# cibadmin --replace --xml-file $HOME/local.xml\n\n"
"Assuming terminal, render configuration in color (green for "
"writable, blue for readable, red for\n"
"denied) to visualize permissions for user tony:\n\n"
"\t# cibadmin --show-access=color --query --user tony | less -r\n\n"
"SEE ALSO:\n"
" crm(8), pcs(8), crm_shadow(8), crm_diff(8)\n";
context = pcmk__build_arg_context(args, NULL, NULL, "");
g_option_context_set_description(context, desc);
pcmk__add_main_args(context, extra_prog_entries);
pcmk__add_arg_group(context, "commands", "Commands:", "Show command help",
command_entries);
pcmk__add_arg_group(context, "data", "Data:", "Show data help",
data_entries);
pcmk__add_arg_group(context, "additional", "Additional Options:",
"Show additional options", addl_entries);
return context;
}
int
main(int argc, char **argv)
{
int rc = pcmk_rc_ok;
const char *source = NULL;
xmlNode *output = NULL;
xmlNode *input = NULL;
gchar *acl_cred = NULL;
GError *error = NULL;
pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
gchar **processed_args = pcmk__cmdline_preproc(argv, "ANSUXhotx");
GOptionContext *context = build_arg_context(args);
if (!g_option_context_parse_strv(context, &processed_args, &error)) {
exit_code = CRM_EX_USAGE;
goto done;
}
if (g_strv_length(processed_args) > 1) {
gchar *help = g_option_context_get_help(context, TRUE, NULL);
GString *extra = g_string_sized_new(128);
for (int lpc = 1; processed_args[lpc] != NULL; lpc++) {
if (extra->len > 0) {
g_string_append_c(extra, ' ');
}
g_string_append(extra, processed_args[lpc]);
}
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"non-option ARGV-elements: %s\n\n%s", extra->str, help);
g_free(help);
g_string_free(extra, TRUE);
goto done;
}
if (args->version || options.extended_version) {
g_strfreev(processed_args);
pcmk__free_arg_context(context);
/* FIXME: When cibadmin is converted to use formatted output, this can
* be replaced by out->version with the appropriate boolean flag.
*
* options.extended_version is deprecated and will be removed in a
* future release.
*/
pcmk__cli_help(options.extended_version? '!' : 'v');
}
/* At LOG_ERR, stderr for CIB calls is rather verbose. Several lines like
*
* (func@file:line) error: CIB failures
*
* In cibadmin we explicitly output the XML portion without the prefixes. So
* we default to LOG_CRIT.
*/
pcmk__cli_init_logging("cibadmin", 0);
set_crm_log_level(LOG_CRIT);
if (args->verbosity > 0) {
cib__set_call_options(options.cmd_options, crm_system_name,
cib_verbose);
for (int i = 0; i < args->verbosity; i++) {
crm_bump_log_level(argc, argv);
}
}
if (options.cib_action == NULL) {
// @COMPAT: Create a default command if other tools have one
gchar *help = g_option_context_get_help(context, TRUE, NULL);
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Must specify a command option\n\n%s", help);
g_free(help);
goto done;
}
if (strcmp(options.cib_action, "empty") == 0) {
// Output an empty CIB
GString *buf = g_string_sized_new(1024);
output = createEmptyCib(1);
crm_xml_add(output, PCMK_XA_VALIDATE_WITH, options.validate_with);
pcmk__xml_string(output, pcmk__xml_fmt_pretty, buf, 0);
fprintf(stdout, "%s", buf->str);
g_string_free(buf, TRUE);
goto done;
}
if (cib_action_is_dangerous() && !options.force) {
exit_code = CRM_EX_UNSAFE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"The supplied command is considered dangerous. To prevent "
"accidental destruction of the cluster, the --force flag "
"is required in order to proceed.");
goto done;
}
if (options.message_timeout_sec < 1) {
// Set default timeout
options.message_timeout_sec = 30;
}
if (options.section_type == cibadmin_section_xpath) {
// Enable getting section by XPath
cib__set_call_options(options.cmd_options, crm_system_name,
cib_xpath);
} else if (options.section_type == cibadmin_section_scope) {
if (!scope_is_valid(options.cib_section)) {
// @COMPAT: Consider requiring --force to proceed
fprintf(stderr,
"Invalid value '%s' for '--scope'. Operation will apply "
"to the entire CIB.\n", options.cib_section);
}
}
if (options.allow_create) {
// Allow target of --modify/-M to be created if it does not exist
cib__set_call_options(options.cmd_options, crm_system_name,
cib_can_create);
}
if (options.delete_all) {
// With cibadmin_section_xpath, remove all matching objects
cib__set_call_options(options.cmd_options, crm_system_name,
cib_multiple);
}
if (options.get_node_path) {
/* Enable getting node path of XPath query matches.
* Meaningful only if options.section_type == cibadmin_section_xpath.
*/
cib__set_call_options(options.cmd_options, crm_system_name,
cib_xpath_address);
}
if (options.local) {
// Configure command to take effect only locally
cib__set_call_options(options.cmd_options, crm_system_name,
cib_scope_local);
}
// @COMPAT: Deprecated option
if (options.no_bcast) {
// Configure command to take effect only locally and not to broadcast
cib__set_call_options(options.cmd_options, crm_system_name,
cib_inhibit_bcast|cib_scope_local);
}
if (options.no_children) {
// When querying an object, don't include its children in the result
cib__set_call_options(options.cmd_options, crm_system_name,
cib_no_children);
}
if (options.sync_call
|| (options.acl_render_mode != pcmk__acl_render_none)) {
/* Wait for call to complete before returning.
*
* The ACL render modes work only with sync calls due to differences in
* output handling between sync/async. It shouldn't matter to the user
* whether the call is synchronous; for a CIB query, we have to wait for
* the result in order to display it in any case.
*/
cib__set_call_options(options.cmd_options, crm_system_name,
cib_sync_call);
}
if (options.input_file != NULL) {
input = pcmk__xml_read(options.input_file);
source = options.input_file;
} else if (options.input_xml != NULL) {
input = pcmk__xml_parse(options.input_xml);
source = "input string";
} else if (options.input_stdin) {
input = pcmk__xml_read(NULL);
source = "STDIN";
} else if (options.acl_render_mode != pcmk__acl_render_none) {
char *username = pcmk__uid2username(geteuid());
bool required = pcmk_acl_required(username);
free(username);
if (required) {
if (options.force) {
fprintf(stderr, "The supplied command can provide skewed"
" result since it is run under user that also"
" gets guarded per ACLs on their own right."
" Continuing since --force flag was"
" provided.\n");
} else {
exit_code = CRM_EX_UNSAFE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"The supplied command can provide skewed result "
"since it is run under user that also gets guarded "
"per ACLs in their own right. To accept the risk "
"of such a possible distortion (without even "
"knowing it at this time), use the --force flag.");
goto done;
}
}
if (options.cib_user == NULL) {
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"The supplied command requires -U user specified.");
goto done;
}
/* We already stopped/warned ACL-controlled users about consequences.
*
* Note: acl_cred takes ownership of options.cib_user here.
* options.cib_user is set to NULL so that the CIB is obtained as the
* user running the cibadmin command. The CIB must be obtained as a user
* with full permissions in order to show the CIB correctly annotated
* for the options.cib_user's permissions.
*/
acl_cred = options.cib_user;
options.cib_user = NULL;
}
if (input != NULL) {
crm_log_xml_debug(input, "[admin input]");
} else if (source != NULL) {
exit_code = CRM_EX_CONFIG;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Couldn't parse input from %s.", source);
goto done;
}
if (pcmk__str_eq(options.cib_action, "md5-sum", pcmk__str_casei)) {
char *digest = NULL;
if (input == NULL) {
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Please supply XML to process with -X, -x, or -p");
goto done;
}
digest = calculate_on_disk_digest(input);
fprintf(stderr, "Digest: ");
fprintf(stdout, "%s\n", pcmk__s(digest, ""));
free(digest);
goto done;
} else if (strcmp(options.cib_action, "md5-sum-versioned") == 0) {
char *digest = NULL;
const char *version = NULL;
if (input == NULL) {
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Please supply XML to process with -X, -x, or -p");
goto done;
}
version = crm_element_value(input, PCMK_XA_CRM_FEATURE_SET);
digest = calculate_xml_versioned_digest(input, FALSE, TRUE, version);
fprintf(stderr, "Versioned (%s) digest: ", version);
fprintf(stdout, "%s\n", pcmk__s(digest, ""));
free(digest);
goto done;
}
rc = do_init();
if (rc != pcmk_ok) {
rc = pcmk_legacy2rc(rc);
exit_code = pcmk_rc2exitc(rc);
crm_err("Init failed, could not perform requested operations: %s",
pcmk_rc_str(rc));
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Init failed, could not perform requested operations: %s",
pcmk_rc_str(rc));
goto done;
}
rc = do_work(input, &output);
if (rc > 0) {
/* wait for the reply by creating a mainloop and running it until
* the callbacks are invoked...
*/
request_id = rc;
the_cib->cmds->register_callback(the_cib, request_id,
options.message_timeout_sec, FALSE,
NULL, "cibadmin_op_callback",
cibadmin_op_callback);
mainloop = g_main_loop_new(NULL, FALSE);
crm_trace("%s waiting for reply from the local CIB", crm_system_name);
crm_info("Starting mainloop");
g_main_loop_run(mainloop);
} else if ((rc == -pcmk_err_schema_unchanged)
&& (strcmp(options.cib_action,
PCMK__CIB_REQUEST_UPGRADE) == 0)) {
report_schema_unchanged();
} else if (rc < 0) {
rc = pcmk_legacy2rc(rc);
crm_err("Call failed: %s", pcmk_rc_str(rc));
fprintf(stderr, "Call failed: %s\n", pcmk_rc_str(rc));
if (rc == pcmk_rc_schema_validation) {
if (strcmp(options.cib_action, PCMK__CIB_REQUEST_UPGRADE) == 0) {
xmlNode *obj = NULL;
- int version = 0;
if (the_cib->cmds->query(the_cib, NULL, &obj,
options.cmd_options) == pcmk_ok) {
- update_validation(&obj, &version, 0, TRUE, FALSE);
+ update_validation(&obj, NULL, 0, TRUE, FALSE);
}
free_xml(obj);
} else if (output) {
validate_xml_verbose(output);
}
}
exit_code = pcmk_rc2exitc(rc);
}
if ((output != NULL)
&& (options.acl_render_mode != pcmk__acl_render_none)) {
xmlDoc *acl_evaled_doc;
rc = pcmk__acl_annotate_permissions(acl_cred, output->doc, &acl_evaled_doc);
if (rc == pcmk_rc_ok) {
xmlChar *rendered = NULL;
rc = pcmk__acl_evaled_render(acl_evaled_doc,
options.acl_render_mode, &rendered);
if (rc != pcmk_rc_ok) {
exit_code = CRM_EX_CONFIG;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Could not render evaluated access: %s",
pcmk_rc_str(rc));
goto done;
}
printf("%s\n", (char *) rendered);
free(rendered);
} else {
exit_code = CRM_EX_CONFIG;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Could not evaluate access per request (%s, error: %s)",
acl_cred, pcmk_rc_str(rc));
goto done;
}
} else if (output != NULL) {
print_xml_output(output);
}
crm_trace("%s exiting normally", crm_system_name);
done:
g_strfreev(processed_args);
pcmk__free_arg_context(context);
g_free(options.cib_user);
g_free(options.dest_node);
g_free(options.input_file);
g_free(options.input_xml);
free(options.cib_section);
free(options.validate_with);
g_free(acl_cred);
free_xml(input);
free_xml(output);
rc = cib__clean_up_connection(&the_cib);
if (exit_code == CRM_EX_OK) {
exit_code = pcmk_rc2exitc(rc);
}
pcmk__output_and_clear_error(&error, NULL);
crm_exit(exit_code);
}
static int
do_work(xmlNode *input, xmlNode **output)
{
/* construct the request */
the_cib->call_timeout = options.message_timeout_sec;
if ((strcmp(options.cib_action, PCMK__CIB_REQUEST_REPLACE) == 0)
&& pcmk__xe_is(input, PCMK_XE_CIB)) {
xmlNode *status = pcmk_find_cib_element(input, PCMK_XE_STATUS);
if (status == NULL) {
pcmk__xe_create(input, PCMK_XE_STATUS);
}
}
crm_trace("Passing \"%s\" to variant_op...", options.cib_action);
return cib_internal_op(the_cib, options.cib_action, options.dest_node,
options.cib_section, input, output,
options.cmd_options, options.cib_user);
}
int
do_init(void)
{
int rc = pcmk_ok;
the_cib = cib_new();
rc = the_cib->cmds->signon(the_cib, crm_system_name, cib_command);
if (rc != pcmk_ok) {
crm_err("Could not connect to the CIB: %s", pcmk_strerror(rc));
fprintf(stderr, "Could not connect to the CIB: %s\n",
pcmk_strerror(rc));
}
return rc;
}
void
cibadmin_op_callback(xmlNode * msg, int call_id, int rc, xmlNode * output, void *user_data)
{
rc = pcmk_legacy2rc(rc);
exit_code = pcmk_rc2exitc(rc);
if (rc == pcmk_rc_schema_unchanged) {
report_schema_unchanged();
} else if (rc != pcmk_rc_ok) {
crm_warn("Call %s failed: %s " CRM_XS " rc=%d",
options.cib_action, pcmk_rc_str(rc), rc);
fprintf(stderr, "Call %s failed: %s\n",
options.cib_action, pcmk_rc_str(rc));
print_xml_output(output);
} else if ((strcmp(options.cib_action, PCMK__CIB_REQUEST_QUERY) == 0)
&& (output == NULL)) {
crm_err("Query returned no output");
crm_log_xml_err(msg, "no output");
} else if (output == NULL) {
crm_info("Call passed");
} else {
crm_info("Call passed");
print_xml_output(output);
}
if (call_id == request_id) {
g_main_loop_quit(mainloop);
} else {
crm_info("Message was not the response we were looking for (%d vs. %d)",
call_id, request_id);
}
}
diff --git a/xml/best-match.sh b/xml/best-match.sh
index 580c29fadf..02c41f085f 100755
--- a/xml/best-match.sh
+++ b/xml/best-match.sh
@@ -1,98 +1,105 @@
#!/bin/sh
#
+# Copyright 2014-2024 the Pacemaker project contributors
+#
+# The version control history for this file may have further details.
+#
+# This source code is licensed under the GNU General Public License version 2
+# or later (GPLv2+) WITHOUT ANY WARRANTY.
+#
+
# Find the (sub-)schema that best matches a desired version.
#
# Version numbers are assumed to be in the format X.Y,
# where X and Y are integers, and Y is no more than 3 digits,
# or the special value "next".
-#
# (Sub-)schema name (e.g. "resources")
base="$1"; shift
# Desired version (e.g. "1.0" or "next")
target="$1"; shift
# If not empty, append the best match as an XML externalRef to this file
# (otherwise, just echo the best match). Using readlink allows building
# from a different directory.
destination="$(readlink -f "$1")"; shift
# Arbitrary text to print before XML (generally spaces to indent)
prefix="$1"; shift
# Allow building from a different directory
cd "$(dirname $0)"
list_candidates() {
- ls -1 "${1}.rng" "${1}"-[0-9]*.rng 2>/dev/null
+ ls -1 "${1}.rng" "${1}"-[0-9]*.rng "${1}"-next.rng 2>/dev/null
}
version_from_filename() {
vff_filename="$1"
case "$vff_filename" in
*-*.rng)
echo "$vff_filename" | sed -e 's/.*-\(.*\).rng/\1/'
;;
*)
# special case for bare ${base}.rng, no -0.1's around anyway
echo 0.1
;;
esac
}
filename_from_version() {
ffv_version="$1"
ffv_base="$2"
if [ "$ffv_version" = "0.1" ]; then
echo "${ffv_base}.rng"
else
echo "${ffv_base}-${ffv_version}.rng"
fi
}
# Convert version string (e.g. 2.10) into integer (e.g. 2010) for comparisons
int_version() {
echo "$1" | awk -F. '{ printf("%d%03d\n", $1,$2); }';
}
best="0.0"
for rng in $(list_candidates "${base}"); do
case ${rng} in
${base}-${target}.rng)
# We found exactly what was requested
best=${target}
break
;;
*-next.rng)
# "Next" schemas cannot be a best match unless directly requested
;;
*)
v=$(version_from_filename "${rng}")
if [ $(int_version "${v}") -gt $(int_version "${best}") ]; then
# This version beats the previous best match
if [ "${target}" = "next" ]; then
best=${v}
elif [ $(int_version "${v}") -lt $(int_version "${target}") ]; then
# This value is best only if it's still less than the target
best=${v}
fi
fi
;;
esac
done
if [ "$best" != "0.0" ]; then
found=$(filename_from_version "$best" "$base")
if [ -z "$destination" ]; then
echo "$(basename $found)"
else
echo "${prefix}" >> "$destination"
fi
exit 0
fi
exit 1