From bee6fa1a1f2f063b9dabedc562e27ba561334ecf Mon Sep 17 00:00:00 2001 From: Chris Lumens Date: Wed, 13 Dec 2023 09:46:43 -0500 Subject: [PATCH] Rebase on upstream 2.1.7-rc4 release - Use systemd-sysusers to create user/group - Pacemaker Remote nodes can validate against later schema versions - Related: RHEL-17225 - Resolves: RHEL-7665 --- 001-schema-glib.patch | 2334 +++++++++++++++++++++++++++++++++++++ 002-schema-transfer.patch | 1986 +++++++++++++++++++++++++++++++ pacemaker.spec | 29 +- pacemaker.sysusers | 3 + sources | 2 +- 5 files changed, 4344 insertions(+), 10 deletions(-) create mode 100644 001-schema-glib.patch create mode 100644 002-schema-transfer.patch create mode 100644 pacemaker.sysusers diff --git a/001-schema-glib.patch b/001-schema-glib.patch new file mode 100644 index 0000000..c38d1d8 --- /dev/null +++ b/001-schema-glib.patch @@ -0,0 +1,2334 @@ +From a59d703de97a49a27564f572dac52b455b356ba9 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Wed, 25 Oct 2023 12:14:57 -0400 +Subject: [PATCH 01/20] Refactor: libcrmcommon: Remove prototypes for internal + functions. + +These are only here to give a place to put the G_GNUC_PRINTF attribute, +but that can go in the function definition itself. +--- + lib/common/schemas.c | 12 ++---------- + 1 file changed, 2 insertions(+), 10 deletions(-) + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index b3c09eb..a85438c 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -64,11 +64,7 @@ static struct schema_s *known_schemas = NULL; + static int xml_schema_max = 0; + static bool silent_logging = FALSE; + +-static void +-xml_log(int priority, const char *fmt, ...) +-G_GNUC_PRINTF(2, 3); +- +-static void ++static void G_GNUC_PRINTF(2, 3) + xml_log(int priority, const char *fmt, ...) + { + va_list ap; +@@ -716,10 +712,6 @@ pcmk__validate_xml(xmlNode *xml_blob, const char *validation, xmlRelaxNGValidity + return FALSE; + } + +-static void +-cib_upgrade_err(void *ctx, const char *fmt, ...) +-G_GNUC_PRINTF(2, 3); +- + /* With this arrangement, an attempt to identify the message severity + as explicitly signalled directly from XSLT is performed in rather + a smart way (no reliance on formatting string + arguments being +@@ -743,7 +735,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; +-- +2.31.1 + +From 3d50aeebce74e520606036ec7db8b5c70fe327b5 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Wed, 25 Oct 2023 12:54:37 -0400 +Subject: [PATCH 02/20] Refactor: libcrmcommon: validate_with should take a + schema as argument. + +...instead of taking an index, and then finding that in the +known_schemas array. +--- + lib/common/schemas.c | 25 ++++++++++++------------- + 1 file changed, 12 insertions(+), 13 deletions(-) + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index a85438c..f1f86f4 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -555,18 +555,16 @@ crm_schema_cleanup(void) + } + + static gboolean +-validate_with(xmlNode *xml, int method, xmlRelaxNGValidityErrorFunc error_handler, void* error_handler_context) ++validate_with(xmlNode *xml, struct schema_s *schema, xmlRelaxNGValidityErrorFunc error_handler, void* error_handler_context) + { + gboolean valid = FALSE; + char *file = NULL; +- struct schema_s *schema = NULL; + relaxng_ctx_cache_t **cache = NULL; + +- if (method < 0) { ++ if (schema == NULL) { + return FALSE; + } + +- schema = &(known_schemas[method]); + if (schema->validator == schema_validator_none) { + return TRUE; + } +@@ -587,8 +585,7 @@ validate_with(xmlNode *xml, int method, xmlRelaxNGValidityErrorFunc error_handle + 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; + } + +@@ -597,11 +594,11 @@ validate_with(xmlNode *xml, int method, xmlRelaxNGValidityErrorFunc error_handle + } + + static bool +-validate_with_silent(xmlNode *xml, int method) ++validate_with_silent(xmlNode *xml, struct schema_s *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; + } +@@ -687,7 +684,7 @@ pcmk__validate_xml(xmlNode *xml_blob, const char *validation, xmlRelaxNGValidity + bool valid = FALSE; + + for (lpc = 0; lpc < xml_schema_max; lpc++) { +- if (validate_with(xml_blob, lpc, NULL, NULL)) { ++ if (validate_with(xml_blob, &known_schemas[lpc], NULL, NULL)) { + valid = TRUE; + crm_xml_add(xml_blob, XML_ATTR_VALIDATION, + known_schemas[lpc].name); +@@ -705,7 +702,8 @@ pcmk__validate_xml(xmlNode *xml_blob, const char *validation, xmlRelaxNGValidity + 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); ++ return validate_with(xml_blob, version >= 0 ? &known_schemas[version] : NULL, ++ error_handler, error_handler_context); + } + + crm_err("Unknown validator: %s", validation); +@@ -1019,7 +1017,7 @@ update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, + known_schemas[lpc].name ? known_schemas[lpc].name : "", + lpc, max_stable_schemas); + +- if (validate_with(xml, lpc, error_handler, GUINT_TO_POINTER(LOG_ERR)) == FALSE) { ++ if (validate_with(xml, &known_schemas[lpc], error_handler, GUINT_TO_POINTER(LOG_ERR)) == FALSE) { + if (next != -1) { + crm_info("Configuration not valid for schema: %s", + known_schemas[lpc].name); +@@ -1067,7 +1065,7 @@ update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, + 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)) { ++ || validate_with_silent(xml, next >= 0 ? &known_schemas[next] : NULL)) { + crm_debug("%s-style configuration is also valid for %s", + known_schemas[lpc].name, known_schemas[next].name); + +@@ -1084,7 +1082,8 @@ update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, + known_schemas[lpc].transform); + rc = -pcmk_err_transform_failed; + +- } else if (validate_with(upgrade, next, error_handler, GUINT_TO_POINTER(LOG_ERR))) { ++ } else if (validate_with(upgrade, next >= 0 ? &known_schemas[next] : NULL, ++ error_handler, GUINT_TO_POINTER(LOG_ERR))) { + crm_info("Transformation %s.xsl successful", + known_schemas[lpc].transform); + lpc = next; +-- +2.31.1 + +From 26a17af650a842659f57c9e58185c290c30a3fb3 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Wed, 25 Oct 2023 14:33:11 -0400 +Subject: [PATCH 03/20] Refactor: libcrmcommon: Break schema freeing out into a + function. + +--- + lib/common/schemas.c | 69 +++++++++++++++++++++++++------------------- + 1 file changed, 40 insertions(+), 29 deletions(-) + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index f1f86f4..c21b9ae 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -511,6 +511,43 @@ validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler, + return valid; + } + ++static void ++free_schema(struct schema_s *schema) ++{ ++ relaxng_ctx_cache_t *ctx = NULL; ++ ++ switch (schema->validator) { ++ case schema_validator_none: // not cached ++ break; ++ ++ case 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 +@@ -518,36 +555,10 @@ validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler, + void + crm_schema_cleanup(void) + { +- int lpc; +- relaxng_ctx_cache_t *ctx = NULL; +- +- for (lpc = 0; lpc < xml_schema_max; lpc++) { +- +- switch (known_schemas[lpc].validator) { +- case schema_validator_none: // not cached +- break; +- case schema_validator_rng: // cached +- ctx = (relaxng_ctx_cache_t *) known_schemas[lpc].cache; +- if (ctx == NULL) { +- break; +- } +- if (ctx->parser != NULL) { +- xmlRelaxNGFreeParserCtxt(ctx->parser); +- } +- if (ctx->valid != NULL) { +- xmlRelaxNGFreeValidCtxt(ctx->valid); +- } +- if (ctx->rng != NULL) { +- xmlRelaxNGFree(ctx->rng); +- } +- free(ctx); +- known_schemas[lpc].cache = NULL; +- break; +- } +- free(known_schemas[lpc].name); +- free(known_schemas[lpc].transform); +- free(known_schemas[lpc].transform_enter); ++ for (int lpc = 0; lpc < xml_schema_max; lpc++) { ++ free_schema(&known_schemas[lpc]); + } ++ + free(known_schemas); + known_schemas = NULL; + +-- +2.31.1 + +From fdf66811c23d93715fcd34e16eb58fce5f4294d7 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Wed, 25 Oct 2023 14:36:07 -0400 +Subject: [PATCH 04/20] Refactor: libcrmcommon: Clean up add_schema a bit. + +* Use true/false instead of TRUE/FALSE. + +* Call CRM_ASSERT after all the strdups. + +* There's no need to have a for loop over a two element list. If the + version number struct ever changes, plenty of other places will have + to be changed as well so this isn't saving us much. +--- + lib/common/schemas.c | 20 +++++++++++++------- + 1 file changed, 13 insertions(+), 7 deletions(-) + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index c21b9ae..17cd21f 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -178,7 +178,7 @@ add_schema(enum schema_validator_e validator, const schema_version_t *version, + int after_transform) + { + int last = xml_schema_max; +- bool have_version = FALSE; ++ bool have_version = false; + + xml_schema_max++; + known_schemas = pcmk__realloc(known_schemas, +@@ -188,26 +188,32 @@ add_schema(enum schema_validator_e validator, const schema_version_t *version, + known_schemas[last].validator = validator; + known_schemas[last].after_transform = after_transform; + +- for (int i = 0; i < 2; ++i) { +- known_schemas[last].version.v[i] = version->v[i]; +- if (version->v[i]) { +- have_version = TRUE; +- } ++ known_schemas[last].version.v[0] = version->v[0]; ++ known_schemas[last].version.v[1] = version->v[1]; ++ ++ if (version->v[0] || version->v[1]) { ++ have_version = true; + } ++ + if (have_version) { + known_schemas[last].name = schema_strdup_printf("pacemaker-", *version, ""); + } else { +- CRM_ASSERT(name); ++ CRM_ASSERT(name != NULL); + schema_scanf(name, "%*[^-]-", known_schemas[last].version, ""); + known_schemas[last].name = strdup(name); ++ CRM_ASSERT(known_schemas[last].name != NULL); + } + + if (transform) { + known_schemas[last].transform = strdup(transform); ++ CRM_ASSERT(known_schemas[last].transform != NULL); + } ++ + if (transform_enter) { + known_schemas[last].transform_enter = strdup(transform_enter); ++ CRM_ASSERT(known_schemas[last].transform_enter != NULL); + } ++ + known_schemas[last].transform_onleave = transform_onleave; + if (after_transform == 0) { + after_transform = xml_schema_max; /* upgrade is a one-way */ +-- +2.31.1 + +From ef89e0536fae09036a657cf651da1eed75356054 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Wed, 25 Oct 2023 15:16:11 -0400 +Subject: [PATCH 05/20] Refactor: libcrmcommon: Use pcmk__s in schemas.c where + possible. + +--- + lib/common/schemas.c | 8 ++++---- + 1 file changed, 4 insertions(+), 4 deletions(-) + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index 17cd21f..fca81e4 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -1031,7 +1031,7 @@ update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, + + while (lpc <= max_stable_schemas) { + crm_debug("Testing '%s' validation (%d of %d)", +- known_schemas[lpc].name ? known_schemas[lpc].name : "", ++ pcmk__s(known_schemas[lpc].name, ""), + lpc, max_stable_schemas); + + if (validate_with(xml, &known_schemas[lpc], error_handler, GUINT_TO_POINTER(LOG_ERR)) == FALSE) { +@@ -1041,7 +1041,7 @@ update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, + next = -1; + } else { + crm_trace("%s validation failed", +- known_schemas[lpc].name ? known_schemas[lpc].name : ""); ++ pcmk__s(known_schemas[lpc].name, "")); + } + if (*best) { + /* we've satisfied the validation, no need to check further */ +@@ -1128,8 +1128,8 @@ update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, + + if (*best > match && *best) { + crm_info("%s the configuration from %s to %s", +- transform?"Transformed":"Upgraded", +- value ? value : "", known_schemas[*best].name); ++ transform?"Transformed":"Upgraded", pcmk__s(value, ""), ++ known_schemas[*best].name); + crm_xml_add(xml, XML_ATTR_VALIDATION, known_schemas[*best].name); + } + +-- +2.31.1 + +From 574c3c1f5ca00514eff77b927821b695c980a683 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Wed, 25 Oct 2023 15:20:04 -0400 +Subject: [PATCH 06/20] Refactor: libcrmcommon: Use a schema variable in + update_validation. + +This just gets rid of a ton of references to the known_schemas array, +making it easier to replace that array with something else in a future +commit. +--- + lib/common/schemas.c | 38 +++++++++++++++++--------------------- + 1 file changed, 17 insertions(+), 21 deletions(-) + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index fca81e4..888a473 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -1030,18 +1030,18 @@ update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, + } + + while (lpc <= max_stable_schemas) { ++ struct schema_s *schema = &known_schemas[lpc]; ++ + crm_debug("Testing '%s' validation (%d of %d)", +- pcmk__s(known_schemas[lpc].name, ""), +- lpc, max_stable_schemas); ++ pcmk__s(schema->name, ""), lpc, max_stable_schemas); + +- if (validate_with(xml, &known_schemas[lpc], error_handler, GUINT_TO_POINTER(LOG_ERR)) == FALSE) { ++ if (validate_with(xml, schema, error_handler, GUINT_TO_POINTER(LOG_ERR)) == FALSE) { + if (next != -1) { + crm_info("Configuration not valid for schema: %s", +- known_schemas[lpc].name); ++ schema->name); + next = -1; + } else { +- crm_trace("%s validation failed", +- pcmk__s(known_schemas[lpc].name, "")); ++ crm_trace("%s validation failed", pcmk__s(schema->name, "")); + } + if (*best) { + /* we've satisfied the validation, no need to check further */ +@@ -1051,8 +1051,7 @@ update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, + + } else { + if (next != -1) { +- crm_debug("Configuration valid for schema: %s", +- known_schemas[next].name); ++ crm_debug("Configuration valid for schema: %s", schema->name); + next = -1; + } + rc = pcmk_ok; +@@ -1064,19 +1063,19 @@ update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, + + if (rc == pcmk_ok && transform) { + xmlNode *upgrade = NULL; +- next = known_schemas[lpc].after_transform; ++ next = schema->after_transform; + + if (next <= lpc) { + /* There is no next version, or next would regress */ +- crm_trace("Stopping at %s", known_schemas[lpc].name); ++ crm_trace("Stopping at %s", schema->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); ++ schema->name, lpc, next, max); + break; + +- } else if (known_schemas[lpc].transform == NULL ++ } else 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 +@@ -1084,25 +1083,22 @@ update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, + may re-allow constructs from previous major line) */ + || validate_with_silent(xml, next >= 0 ? &known_schemas[next] : NULL)) { + crm_debug("%s-style configuration is also valid for %s", +- known_schemas[lpc].name, known_schemas[next].name); ++ schema->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); ++ schema->name, known_schemas[next].name, schema->transform); + +- upgrade = apply_upgrade(xml, &known_schemas[lpc], to_logs); ++ upgrade = apply_upgrade(xml, schema, to_logs); + if (upgrade == NULL) { +- crm_err("Transformation %s.xsl failed", +- known_schemas[lpc].transform); ++ crm_err("Transformation %s.xsl failed", schema->transform); + rc = -pcmk_err_transform_failed; + + } else if (validate_with(upgrade, next >= 0 ? &known_schemas[next] : NULL, + error_handler, GUINT_TO_POINTER(LOG_ERR))) { +- crm_info("Transformation %s.xsl successful", +- known_schemas[lpc].transform); ++ crm_info("Transformation %s.xsl successful", schema->transform); + lpc = next; + *best = next; + free_xml(xml); +@@ -1111,7 +1107,7 @@ update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, + + } else { + crm_err("Transformation %s.xsl did not produce a valid configuration", +- known_schemas[lpc].transform); ++ schema->transform); + crm_log_xml_info(upgrade, "transform:bad"); + free_xml(upgrade); + rc = -pcmk_err_schema_validation; +-- +2.31.1 + +From dc724c940014fbe60aa506d8acb652b2dd5dce90 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Wed, 25 Oct 2023 15:31:27 -0400 +Subject: [PATCH 07/20] Refactor: libcrmcommon: Use a variable for the next + schema, too. + +This gets rid of further references to known_schemas update_validation, +also with the purpose of making it easier to change the implementation +of that array. +--- + lib/common/schemas.c | 20 +++++++++++++------- + 1 file changed, 13 insertions(+), 7 deletions(-) + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index 888a473..8e5c22e 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -1063,41 +1063,47 @@ update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, + + if (rc == pcmk_ok && transform) { + xmlNode *upgrade = NULL; ++ struct schema_s *next_schema = NULL; + next = schema->after_transform; + + if (next <= lpc) { + /* There is no next version, or next would regress */ + crm_trace("Stopping at %s", schema->name); + break; ++ } + +- } else if (max > 0 && (lpc == max || next > max)) { ++ 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 = &known_schemas[next]; ++ CRM_ASSERT(next_schema != NULL); + +- } else if (schema->transform == NULL ++ if (schema->transform == NULL + /* possibly avoid transforming when readily valid + (in general more restricted when crossing the major + version boundary, as X.0 "transitional" version is + expected to be more strict than it's successors that + may re-allow constructs from previous major line) */ +- || validate_with_silent(xml, next >= 0 ? &known_schemas[next] : NULL)) { ++ || validate_with_silent(xml, next_schema)) { + crm_debug("%s-style configuration is also valid for %s", +- schema->name, known_schemas[next].name); ++ schema->name, next_schema->name); + + lpc = next; + + } else { + crm_debug("Upgrading %s-style configuration to %s with %s.xsl", +- schema->name, known_schemas[next].name, schema->transform); ++ 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 >= 0 ? &known_schemas[next] : NULL, +- error_handler, GUINT_TO_POINTER(LOG_ERR))) { ++ } 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; +-- +2.31.1 + +From 0664f7e327c612d5602515ecf4bb32fb7c3503f6 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Wed, 25 Oct 2023 16:09:30 -0400 +Subject: [PATCH 08/20] Refactor: libcrmcommon: Add pcmk__dump_known_schemas. + +The debug logging in add_schema isn't necessarily all that useful. +Typically, schema adding happens in crm_log_preinit which means it +happens before logging is set up, so nothing that we log actually goes +anywhere. + +This function does the same thing but can be called where needed. +--- + include/crm/common/xml_internal.h | 3 +++ + lib/common/schemas.c | 21 +++++++++++++++++++++ + 2 files changed, 24 insertions(+) + +diff --git a/include/crm/common/xml_internal.h b/include/crm/common/xml_internal.h +index ddb4384..f319856 100644 +--- a/include/crm/common/xml_internal.h ++++ b/include/crm/common/xml_internal.h +@@ -441,8 +441,11 @@ pcmk__xml_attr_value(const xmlAttr *attr) + : (const char *) attr->children->content; + } + ++ + gboolean pcmk__validate_xml(xmlNode *xml_blob, const char *validation, + xmlRelaxNGValidityErrorFunc error_handler, + void *error_handler_context); + ++void pcmk__log_known_schemas(void); ++ + #endif // PCMK__XML_INTERNAL__H +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index 8e5c22e..41ca138 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -1248,3 +1248,24 @@ cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs) + free(orig_value); + return rc; + } ++ ++void ++pcmk__log_known_schemas(void) ++{ ++ for (int lpc = 0; lpc < xml_schema_max; lpc++) { ++ if (known_schemas[lpc].after_transform < 0) { ++ crm_debug("known_schemas[%d] => %s", lpc, known_schemas[lpc].name); ++ ++ } else if (known_schemas[lpc].transform != NULL) { ++ crm_debug("known_schemas[%d] => %s (upgrades to %d with %s.xsl)", ++ lpc, known_schemas[lpc].name, ++ known_schemas[lpc].after_transform, ++ known_schemas[lpc].transform); ++ ++ } else { ++ crm_debug("known_schemas[%d] => %s (upgrades to %d)", ++ lpc, known_schemas[lpc].name, ++ known_schemas[lpc].after_transform); ++ } ++ } ++} +-- +2.31.1 + +From 423a28fd5c2b71945d75b68b168a607279d795f7 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Wed, 25 Oct 2023 17:22:53 -0400 +Subject: [PATCH 09/20] Refactor: libcrmcommon: Store known_schemas as a GList. + +Instead of managing our own array with realloc, use GList and the +various glib list functions. + +In many places, this makes the code easier to follow - we can simply +iterate over the list and do something on each node. In other places, +we're still relying on list indices too much to help. Those spots can +probably be cleaned up in future commits. +--- + lib/common/schemas.c | 181 ++++++++++++++++++++++++++----------------- + 1 file changed, 108 insertions(+), 73 deletions(-) + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index 41ca138..6e6f32e 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -60,7 +60,7 @@ struct schema_s { + bool transform_onleave; + }; + +-static struct schema_s *known_schemas = NULL; ++static GList *known_schemas = NULL; + static int xml_schema_max = 0; + static bool silent_logging = FALSE; + +@@ -81,27 +81,45 @@ 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" ++ // FIXME: This function assumes at least three schemas have been added ++ // before it has been called for the first time. ++ return g_list_length(known_schemas) - 3; // index from 0, ignore "pacemaker-next"/"none" + } + + static int + xml_minimum_schema_index(void) + { + static int best = 0; +- if (best == 0) { +- int lpc = 0; +- +- best = xml_latest_schema_index(); +- for (lpc = best; lpc > 0; lpc--) { +- if (known_schemas[lpc].version.v[0] +- < known_schemas[best].version.v[0]) { +- return best; +- } else { +- best = lpc; +- } ++ struct schema_s *best_schema = NULL; ++ GList *last_real_ele = NULL; ++ ++ if (best != 0) { ++ return best; ++ } ++ ++ best = xml_latest_schema_index(); ++ ++ /* We can't just use g_list_last here because "pacemaker-next" and "none" ++ * are stored at the end of the list. We need to start several elements ++ * back, at the last real schema. ++ */ ++ last_real_ele = g_list_nth(known_schemas, best); ++ best_schema = last_real_ele->data; ++ ++ for (GList *iter = last_real_ele; iter != NULL; iter = iter->prev) { ++ struct schema_s *schema = iter->data; ++ ++ if (schema->version.v[0] < best_schema->version.v[0]) { ++ return best; ++ } else { ++ best--; + } +- best = xml_latest_schema_index(); + } ++ ++ /* If we never found a schema that meets the above criteria, default to ++ * the last one. ++ */ ++ best = xml_latest_schema_index(); + return best; + } + +@@ -177,63 +195,61 @@ add_schema(enum schema_validator_e validator, const schema_version_t *version, + const char *transform_enter, bool transform_onleave, + int after_transform) + { ++ struct schema_s *schema = NULL; + int last = xml_schema_max; + bool have_version = false; + + xml_schema_max++; +- known_schemas = pcmk__realloc(known_schemas, +- xml_schema_max * sizeof(struct schema_s)); +- CRM_ASSERT(known_schemas != NULL); +- memset(known_schemas+last, 0, sizeof(struct schema_s)); +- known_schemas[last].validator = validator; +- known_schemas[last].after_transform = after_transform; + +- known_schemas[last].version.v[0] = version->v[0]; +- known_schemas[last].version.v[1] = version->v[1]; ++ schema = calloc(1, sizeof(struct schema_s)); ++ CRM_ASSERT(schema != NULL); ++ ++ schema->validator = validator; ++ schema->after_transform = after_transform; ++ schema->version.v[0] = version->v[0]; ++ schema->version.v[1] = version->v[1]; + + if (version->v[0] || version->v[1]) { + have_version = true; + } + + if (have_version) { +- known_schemas[last].name = schema_strdup_printf("pacemaker-", *version, ""); ++ schema->name = schema_strdup_printf("pacemaker-", *version, ""); + } else { + CRM_ASSERT(name != NULL); +- schema_scanf(name, "%*[^-]-", known_schemas[last].version, ""); +- known_schemas[last].name = strdup(name); +- CRM_ASSERT(known_schemas[last].name != NULL); ++ schema_scanf(name, "%*[^-]-", schema->version, ""); ++ schema->name = strdup(name); ++ CRM_ASSERT(schema->name != NULL); + } + + if (transform) { +- known_schemas[last].transform = strdup(transform); +- CRM_ASSERT(known_schemas[last].transform != NULL); ++ schema->transform = strdup(transform); ++ CRM_ASSERT(schema->transform != NULL); + } + + if (transform_enter) { +- known_schemas[last].transform_enter = strdup(transform_enter); +- CRM_ASSERT(known_schemas[last].transform_enter != NULL); ++ schema->transform_enter = strdup(transform_enter); ++ CRM_ASSERT(schema->transform_enter != NULL); + } + +- known_schemas[last].transform_onleave = transform_onleave; ++ schema->transform_onleave = transform_onleave; + if (after_transform == 0) { + after_transform = xml_schema_max; /* upgrade is a one-way */ + } +- known_schemas[last].after_transform = after_transform; ++ schema->after_transform = after_transform; + +- if (known_schemas[last].after_transform < 0) { +- crm_debug("Added supported schema %d: %s", +- last, known_schemas[last].name); ++ known_schemas = g_list_append(known_schemas, schema); + +- } else if (known_schemas[last].transform) { ++ if (schema->after_transform < 0) { ++ crm_debug("Added supported schema %d: %s", last, schema->name); ++ ++ } else if (schema->transform != NULL) { + 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); ++ last, schema->name, schema->after_transform, schema->transform); + + } else { + crm_debug("Added supported schema %d: %s (upgrades to %d)", +- last, known_schemas[last].name, +- known_schemas[last].after_transform); ++ last, schema->name, schema->after_transform); + } + } + +@@ -518,8 +534,9 @@ validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler, + } + + static void +-free_schema(struct schema_s *schema) ++free_schema(gpointer data) + { ++ struct schema_s *schema = data; + relaxng_ctx_cache_t *ctx = NULL; + + switch (schema->validator) { +@@ -561,11 +578,7 @@ free_schema(struct schema_s *schema) + void + crm_schema_cleanup(void) + { +- for (int lpc = 0; lpc < xml_schema_max; lpc++) { +- free_schema(&known_schemas[lpc]); +- } +- +- free(known_schemas); ++ g_list_free_full(known_schemas, free_schema); + known_schemas = NULL; + + wrap_libxslt(true); +@@ -697,16 +710,17 @@ pcmk__validate_xml(xmlNode *xml_blob, const char *validation, xmlRelaxNGValidity + } + + if (validation == NULL) { +- int lpc = 0; + bool valid = FALSE; + +- for (lpc = 0; lpc < xml_schema_max; lpc++) { +- if (validate_with(xml_blob, &known_schemas[lpc], NULL, NULL)) { ++ for (GList *iter = known_schemas; iter != NULL; iter = iter->next) { ++ struct schema_s *schema = iter->data; ++ ++ if (validate_with(xml_blob, schema, 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) { ++ crm_xml_add(xml_blob, XML_ATTR_VALIDATION, schema->name); ++ crm_info("XML validated against %s", schema->name); ++ ++ if (schema->after_transform == 0) { + break; + } + } +@@ -719,8 +733,9 @@ pcmk__validate_xml(xmlNode *xml_blob, const char *validation, xmlRelaxNGValidity + if (strcmp(validation, PCMK__VALUE_NONE) == 0) { + return TRUE; + } else if (version < xml_schema_max) { +- return validate_with(xml_blob, version >= 0 ? &known_schemas[version] : NULL, +- error_handler, error_handler_context); ++ struct schema_s *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); +@@ -964,10 +979,13 @@ apply_upgrade(xmlNode *xml, const struct schema_s *schema, gboolean to_logs) + const char * + get_schema_name(int version) + { +- if (version < 0 || version >= xml_schema_max) { ++ struct schema_s *schema = g_list_nth_data(known_schemas, version); ++ ++ if (schema == NULL) { + return "unknown"; + } +- return known_schemas[version].name; ++ ++ return schema->name; + } + + int +@@ -978,11 +996,17 @@ get_schema_version(const char *name) + if (name == NULL) { + name = PCMK__VALUE_NONE; + } +- for (; lpc < xml_schema_max; lpc++) { +- if (pcmk__str_eq(name, known_schemas[lpc].name, pcmk__str_casei)) { ++ ++ for (GList *iter = known_schemas; iter != NULL; iter = iter->next) { ++ struct schema_s *schema = iter->data; ++ ++ if (pcmk__str_eq(name, schema->name, pcmk__str_casei)) { + return lpc; + } ++ ++ lpc++; + } ++ + return -1; + } + +@@ -1030,7 +1054,12 @@ update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, + } + + while (lpc <= max_stable_schemas) { +- struct schema_s *schema = &known_schemas[lpc]; ++ /* 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. ++ */ ++ struct schema_s *schema = g_list_nth_data(known_schemas, lpc); + + crm_debug("Testing '%s' validation (%d of %d)", + pcmk__s(schema->name, ""), lpc, max_stable_schemas); +@@ -1078,7 +1107,7 @@ update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, + break; + } + +- next_schema = &known_schemas[next]; ++ next_schema = g_list_nth_data(known_schemas, next); + CRM_ASSERT(next_schema != NULL); + + if (schema->transform == NULL +@@ -1129,10 +1158,12 @@ update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, + } + + if (*best > match && *best) { ++ struct schema_s *best_schema = g_list_nth_data(known_schemas, *best); ++ + crm_info("%s the configuration from %s to %s", + transform?"Transformed":"Upgraded", pcmk__s(value, ""), +- known_schemas[*best].name); +- crm_xml_add(xml, XML_ATTR_VALIDATION, known_schemas[*best].name); ++ best_schema->name); ++ crm_xml_add(xml, XML_ATTR_VALIDATION, best_schema->name); + } + + *xml_blob = xml; +@@ -1252,20 +1283,24 @@ cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs) + void + pcmk__log_known_schemas(void) + { +- for (int lpc = 0; lpc < xml_schema_max; lpc++) { +- if (known_schemas[lpc].after_transform < 0) { +- crm_debug("known_schemas[%d] => %s", lpc, known_schemas[lpc].name); ++ int lpc = 0; + +- } else if (known_schemas[lpc].transform != NULL) { ++ for (GList *iter = known_schemas; iter != NULL; iter = iter->next) { ++ struct schema_s *schema = iter->data; ++ ++ if (schema->after_transform < 0) { ++ crm_debug("known_schemas[%d] => %s", lpc, schema->name); ++ ++ } else if (schema->transform != NULL) { + crm_debug("known_schemas[%d] => %s (upgrades to %d with %s.xsl)", +- lpc, known_schemas[lpc].name, +- known_schemas[lpc].after_transform, +- known_schemas[lpc].transform); ++ lpc, schema->name, schema->after_transform, ++ schema->transform); + + } else { + crm_debug("known_schemas[%d] => %s (upgrades to %d)", +- lpc, known_schemas[lpc].name, +- known_schemas[lpc].after_transform); ++ lpc, schema->name, schema->after_transform); + } ++ ++ lpc++; + } + } +-- +2.31.1 + +From 33cfcc0d98603e04dde15d69acd46823679405f0 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Wed, 25 Oct 2023 17:34:25 -0400 +Subject: [PATCH 10/20] Refactor: libcrmcommon: Get rid of xml_schema_max. + +This is just the length of the known_schemas list. +--- + lib/common/schemas.c | 9 +++------ + 1 file changed, 3 insertions(+), 6 deletions(-) + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index 6e6f32e..d4ce68e 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -61,7 +61,6 @@ struct schema_s { + }; + + static GList *known_schemas = NULL; +-static int xml_schema_max = 0; + static bool silent_logging = FALSE; + + static void G_GNUC_PRINTF(2, 3) +@@ -196,11 +195,9 @@ add_schema(enum schema_validator_e validator, const schema_version_t *version, + int after_transform) + { + struct schema_s *schema = NULL; +- int last = xml_schema_max; ++ int last = g_list_length(known_schemas); + bool have_version = false; + +- xml_schema_max++; +- + schema = calloc(1, sizeof(struct schema_s)); + CRM_ASSERT(schema != NULL); + +@@ -234,7 +231,7 @@ add_schema(enum schema_validator_e validator, const schema_version_t *version, + + schema->transform_onleave = transform_onleave; + if (after_transform == 0) { +- after_transform = xml_schema_max; /* upgrade is a one-way */ ++ after_transform = last + 1; /* upgrade is a one-way */ + } + schema->after_transform = after_transform; + +@@ -732,7 +729,7 @@ pcmk__validate_xml(xmlNode *xml_blob, const char *validation, xmlRelaxNGValidity + version = get_schema_version(validation); + if (strcmp(validation, PCMK__VALUE_NONE) == 0) { + return TRUE; +- } else if (version < xml_schema_max) { ++ } else if (version < g_list_length(known_schemas)) { + struct schema_s *schema = g_list_nth_data(known_schemas, version); + return validate_with(xml_blob, schema, error_handler, + error_handler_context); +-- +2.31.1 + +From 6ace4c912d34f495ab5f52500628c82fb1533256 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Thu, 26 Oct 2023 12:52:14 -0400 +Subject: [PATCH 11/20] Refactor: libcrmcommon: Rename + xml_minimum_schema_index. + +This function's name is unclear. It actually returns the most recent +X.0 schema index. The new name is pretty bad, but I think it's at least +clear. + +And then while I'm at it, rewrite it to make it more clear. Just +iterate the known_schemas list, looking for the right .0 one. This code +does not get run very often, and it caches its result, so there's no +need to do the reverse traversal with a lagging index. +--- + lib/common/schemas.c | 41 +++++++++++++++++++++++++---------------- + 1 file changed, 25 insertions(+), 16 deletions(-) + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index d4ce68e..55519e8 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -85,39 +85,48 @@ xml_latest_schema_index(void) + return g_list_length(known_schemas) - 3; // index from 0, ignore "pacemaker-next"/"none" + } + ++/* Return the index of the most recent X.0 schema. */ + static int +-xml_minimum_schema_index(void) ++xml_find_x_0_schema_index(void) + { + static int best = 0; ++ int i = 0; + struct schema_s *best_schema = NULL; +- GList *last_real_ele = NULL; + + if (best != 0) { + return best; + } + ++ /* Get the most recent schema so we can look at its version number. */ + best = xml_latest_schema_index(); ++ best_schema = g_list_nth(known_schemas, best)->data; + +- /* We can't just use g_list_last here because "pacemaker-next" and "none" +- * are stored at the end of the list. We need to start several elements +- * back, at the last real schema. ++ /* Iterate over the schema list until we find a schema with the same major ++ * version as best, and with a minor version number of 0. ++ * ++ * This assumes that the first schema in a major series is always X.0, ++ * which seems like a safe assumption. + */ +- last_real_ele = g_list_nth(known_schemas, best); +- best_schema = last_real_ele->data; +- +- for (GList *iter = last_real_ele; iter != NULL; iter = iter->prev) { ++ for (GList *iter = known_schemas; iter != NULL; iter = iter->next) { + struct schema_s *schema = iter->data; + +- if (schema->version.v[0] < best_schema->version.v[0]) { ++ /* If we hit the initial best schema, the only things left in the list ++ * are "pacemaker-next" and "none" which aren't worth checking. ++ */ ++ if (schema == best_schema) { ++ break; ++ } ++ ++ if (schema->version.v[0] == best_schema->version.v[0] && ++ schema->version.v[1] == 0) { ++ best = i; + return best; +- } else { +- best--; + } ++ ++ i++; + } + +- /* If we never found a schema that meets the above criteria, default to +- * the last one. +- */ ++ /* If we got here, we never found a match. Just return the latest. */ + best = xml_latest_schema_index(); + return best; + } +@@ -1177,7 +1186,7 @@ cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs) + + int version = get_schema_version(value); + int orig_version = version; +- int min_version = xml_minimum_schema_index(); ++ int min_version = xml_find_x_0_schema_index(); + + if (version < min_version) { + // Current configuration schema is not acceptable, try to update +-- +2.31.1 + +From 7df1d2df9e8710183735b19947a885bb129e523e Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Thu, 26 Oct 2023 14:14:02 -0400 +Subject: [PATCH 12/20] Refactor: libcrmcommon: Remove an unnecessary check in + validate_xml. + +I believe that add_schema ensures after_transform is never 0 - it's +either negative, or some positive non-zero value. So this check should +be pointless. +--- + lib/common/schemas.c | 4 ---- + 1 file changed, 4 deletions(-) + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index 55519e8..cfb83dd 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -725,10 +725,6 @@ pcmk__validate_xml(xmlNode *xml_blob, const char *validation, xmlRelaxNGValidity + valid = TRUE; + crm_xml_add(xml_blob, XML_ATTR_VALIDATION, schema->name); + crm_info("XML validated against %s", schema->name); +- +- if (schema->after_transform == 0) { +- break; +- } + } + } + +-- +2.31.1 + +From dbf94f5a3c146992fb60c231a7eda21271b62b99 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Fri, 27 Oct 2023 10:49:49 -0400 +Subject: [PATCH 13/20] Refactor: libcrmcommon: Change how schema upgrade + versions are handled + +...in update_validation. Schemas always either upgrade to the next one +in the list, or do not upgrade. The latter only happens when we get to +the last real version and the next one is pacemaker-next/none. + +With that change made, we also need to change the conditional. There's +no need to check that the upgrade will regress. We only need to check +that we've run off the end of the list of real schema versions. + +A future commit will remove the after_transform variable entirely, but +since this is its most visible and complicated use, splitting this into +a separate commit seems worth it. +--- + lib/common/schemas.c | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index cfb83dd..3ebdf1c 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -1095,10 +1095,10 @@ update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, + if (rc == pcmk_ok && transform) { + xmlNode *upgrade = NULL; + struct schema_s *next_schema = NULL; +- next = schema->after_transform; ++ next = lpc+1; + +- if (next <= lpc) { +- /* There is no next version, or next would regress */ ++ if (next > max_stable_schemas) { ++ /* There is no next version */ + crm_trace("Stopping at %s", schema->name); + break; + } +-- +2.31.1 + +From b2da7aaba5a1afe9d4f56989c0b81dd55abaf1b8 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Fri, 27 Oct 2023 11:00:12 -0400 +Subject: [PATCH 14/20] Refactor: libcrmcommon: Get rid of after_transform. + +As stated in the previous commit, schemas always just upgrade to the +next one in the list. There's no need to keep track of that fact, so +get rid of the variable that held it. This then allows us to get rid of +all the places that value was being set and passed around. +--- + lib/common/schemas.c | 54 +++++++++++++------------------------------- + 1 file changed, 16 insertions(+), 38 deletions(-) + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index 3ebdf1c..e33d3c7 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -54,7 +54,6 @@ struct schema_s { + char *transform; + void *cache; + enum schema_validator_e validator; +- int after_transform; + schema_version_t version; + char *transform_enter; + bool transform_onleave; +@@ -200,8 +199,7 @@ schema_sort(const struct dirent **a, const struct dirent **b) + static void + add_schema(enum schema_validator_e validator, const schema_version_t *version, + const char *name, const char *transform, +- const char *transform_enter, bool transform_onleave, +- int after_transform) ++ const char *transform_enter, bool transform_onleave) + { + struct schema_s *schema = NULL; + int last = g_list_length(known_schemas); +@@ -211,9 +209,9 @@ add_schema(enum schema_validator_e validator, const schema_version_t *version, + CRM_ASSERT(schema != NULL); + + schema->validator = validator; +- schema->after_transform = after_transform; + 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]) { + have_version = true; +@@ -238,24 +236,14 @@ add_schema(enum schema_validator_e validator, const schema_version_t *version, + CRM_ASSERT(schema->transform_enter != NULL); + } + +- schema->transform_onleave = transform_onleave; +- if (after_transform == 0) { +- after_transform = last + 1; /* upgrade is a one-way */ +- } +- schema->after_transform = after_transform; +- + known_schemas = g_list_append(known_schemas, schema); + +- if (schema->after_transform < 0) { +- crm_debug("Added supported schema %d: %s", last, schema->name); +- +- } else if (schema->transform != NULL) { +- crm_debug("Added supported schema %d: %s (upgrades to %d with %s.xsl)", +- last, schema->name, schema->after_transform, schema->transform); ++ 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 (upgrades to %d)", +- last, schema->name, schema->after_transform); ++ crm_debug("Added supported schema %d: %s", last, schema->name); + } + } + +@@ -288,8 +276,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 schema_version_t *version, bool transform_expected) + { + bool transform_onleave = FALSE; + int rc = pcmk_rc_ok; +@@ -345,12 +332,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); ++ transform_upgrade, transform_enter, transform_onleave); + + free(transform_upgrade); + free(transform_enter); +@@ -416,7 +402,6 @@ crm_schema_init(void) + 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)) { +@@ -432,11 +417,9 @@ crm_schema_init(void) + && (version.v[0] < next_version.v[0])) { + transform_expected = TRUE; + } +- +- } else { +- next = -1; + } +- if (add_schema_by_version(&version, next, transform_expected) ++ ++ if (add_schema_by_version(&version, transform_expected) + == ENOENT) { + break; + } +@@ -450,10 +433,10 @@ crm_schema_init(void) + + // @COMPAT: Deprecated since 2.1.5 + add_schema(schema_validator_rng, &zero, "pacemaker-next", +- NULL, NULL, FALSE, -1); ++ NULL, NULL, FALSE); + + add_schema(schema_validator_none, &zero, PCMK__VALUE_NONE, +- NULL, NULL, FALSE, -1); ++ NULL, NULL, FALSE); + } + + static gboolean +@@ -1290,17 +1273,12 @@ pcmk__log_known_schemas(void) + for (GList *iter = known_schemas; iter != NULL; iter = iter->next) { + struct schema_s *schema = iter->data; + +- if (schema->after_transform < 0) { +- crm_debug("known_schemas[%d] => %s", lpc, schema->name); +- +- } else if (schema->transform != NULL) { +- crm_debug("known_schemas[%d] => %s (upgrades to %d with %s.xsl)", +- lpc, schema->name, schema->after_transform, +- schema->transform); ++ 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 (upgrades to %d)", +- lpc, schema->name, schema->after_transform); ++ crm_debug("known_schemas[%d] => %s", lpc, schema->name); + } + + lpc++; +-- +2.31.1 + +From 645bb233e52b9f5f559ffcd354b2f4ef0bcdee90 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Mon, 6 Nov 2023 08:22:04 -0500 +Subject: [PATCH 15/20] Refactor: libcrmcommon: Remove unnecessary schema code. + +The block that sets the version if we didn't previously do so is no +longer necessary. This block only executes if the version parameter is +all zeros, which at the moment is only "pacemaker-next" and "none". We +could probably guarantee this will continue to be the case. + +Additionally, I don't see that this would even do anything useful +anymore. Scanning the name for a version number is going to fail for +"pacemaker-next" and "none". So really, this block was just handling +the possibility that we passed in no version number but that the name +contained a number. + +And with that done, there's only one more spot using schema_scanf so we +can just replace that with a call to sscanf. +--- + lib/common/schemas.c | 14 +------------- + 1 file changed, 1 insertion(+), 13 deletions(-) + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index e33d3c7..7b91f71 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -32,9 +32,6 @@ typedef struct { + + #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]) + +@@ -139,9 +136,7 @@ xml_latest_schema(void) + static inline bool + version_from_filename(const char *filename, schema_version_t *version) + { +- int rc = schema_scanf(filename, "pacemaker-", *version, ".rng"); +- +- return (rc == 2); ++ return sscanf(filename, "pacemaker-%hhu.%hhu.rng", &(version->v[0]), &(version->v[1])) == 2; + } + + static int +@@ -203,7 +198,6 @@ add_schema(enum schema_validator_e validator, const schema_version_t *version, + { + struct schema_s *schema = NULL; + int last = g_list_length(known_schemas); +- bool have_version = false; + + schema = calloc(1, sizeof(struct schema_s)); + CRM_ASSERT(schema != NULL); +@@ -214,14 +208,8 @@ add_schema(enum schema_validator_e validator, const schema_version_t *version, + schema->transform_onleave = transform_onleave; + + if (version->v[0] || version->v[1]) { +- have_version = true; +- } +- +- if (have_version) { + schema->name = schema_strdup_printf("pacemaker-", *version, ""); + } else { +- CRM_ASSERT(name != NULL); +- schema_scanf(name, "%*[^-]-", schema->version, ""); + schema->name = strdup(name); + CRM_ASSERT(schema->name != NULL); + } +-- +2.31.1 + +From 0943f1ff0a9e72e88c5a234a32bb83d0f2e02c84 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Fri, 17 Nov 2023 09:42:55 -0500 +Subject: [PATCH 16/20] Refactor: libcrmcommon: Improve + xml_find_x_0_schema_index. + +* Lots of comments to explain how it works. + +* Walk the list backwards, stopping on the first one in the major + version series. This means the first one no longer has to be X.0. + +* Require that known_schemas be non-NULL. + +* Don't use the returned index to also mean we've found something since + that means if the index we actually want to return is 0, the function + will have to run every time. +--- + lib/common/schemas.c | 62 +++++++++++++++++++++++++++++--------------- + 1 file changed, 41 insertions(+), 21 deletions(-) + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index 7b91f71..466ad5a 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -85,45 +85,65 @@ xml_latest_schema_index(void) + static int + xml_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. ++ */ ++ static bool found = false; + static int best = 0; +- int i = 0; ++ int i; ++ GList *best_node = NULL; + struct schema_s *best_schema = NULL; + +- if (best != 0) { ++ 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_schema = g_list_nth(known_schemas, best)->data; ++ best_node = g_list_nth(known_schemas, best); ++ best_schema = best_node->data; ++ ++ /* If this is a singleton list, we're done. */ ++ if (pcmk__list_of_1(known_schemas)) { ++ goto done; ++ } + +- /* Iterate over the schema list until we find a schema with the same major +- * version as best, and with a minor version number of 0. +- * +- * This assumes that the first schema in a major series is always X.0, +- * which seems like a safe assumption. ++ /* 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. + */ +- for (GList *iter = known_schemas; iter != NULL; iter = iter->next) { ++ i = best; ++ ++ for (GList *iter = best_node->prev; iter != NULL; iter = iter->prev) { + struct schema_s *schema = iter->data; + +- /* If we hit the initial best schema, the only things left in the list +- * are "pacemaker-next" and "none" which aren't worth checking. ++ /* 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 == best_schema) { +- break; +- } +- +- if (schema->version.v[0] == best_schema->version.v[0] && +- schema->version.v[1] == 0) { ++ if (schema->version.v[0] < best_schema->version.v[0]) { + best = i; +- return best; ++ 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++; ++ i--; + } + +- /* If we got here, we never found a match. Just return the latest. */ +- best = xml_latest_schema_index(); ++done: ++ found = true; + return best; + } + +-- +2.31.1 + +From eeeb36338f48d40f9f15a51c18aeca533b6c260d Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Fri, 17 Nov 2023 11:18:34 -0500 +Subject: [PATCH 17/20] Refactor: libcrmcommon: Add a parameter to a couple + schema functions. + +Instead of assuming known_schemas, pass the list to use as a parameter. +--- + lib/common/schemas.c | 22 +++++++++++----------- + 1 file changed, 11 insertions(+), 11 deletions(-) + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index 466ad5a..9d98695 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -73,17 +73,17 @@ xml_log(int priority, const char *fmt, ...) + } + + static int +-xml_latest_schema_index(void) ++xml_latest_schema_index(GList *schemas) + { + // @COMPAT: pacemaker-next is deprecated since 2.1.5 + // FIXME: This function assumes at least three schemas have been added + // before it has been called for the first time. +- return g_list_length(known_schemas) - 3; // index from 0, ignore "pacemaker-next"/"none" ++ return g_list_length(schemas) - 3; // index from 0, ignore "pacemaker-next"/"none" + } + + /* Return the index of the most recent X.0 schema. */ + static int +-xml_find_x_0_schema_index(void) ++xml_find_x_0_schema_index(GList *schemas) + { + /* We can't just use best to determine whether we've found the index + * or not. What if we have a very long list of schemas all in the +@@ -100,15 +100,15 @@ xml_find_x_0_schema_index(void) + return best; + } + +- CRM_ASSERT(known_schemas != NULL); ++ CRM_ASSERT(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 = xml_latest_schema_index(schemas); ++ best_node = g_list_nth(schemas, best); + best_schema = best_node->data; + + /* If this is a singleton list, we're done. */ +- if (pcmk__list_of_1(known_schemas)) { ++ if (pcmk__list_of_1(schemas)) { + goto done; + } + +@@ -150,7 +150,7 @@ done: + const char * + xml_latest_schema(void) + { +- return get_schema_name(xml_latest_schema_index()); ++ return get_schema_name(xml_latest_schema_index(known_schemas)); + } + + static inline bool +@@ -1010,7 +1010,7 @@ update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, + { + xmlNode *xml = NULL; + char *value = NULL; +- int max_stable_schemas = xml_latest_schema_index(); ++ int max_stable_schemas = xml_latest_schema_index(known_schemas); + int lpc = 0, match = -1, rc = pcmk_ok; + int next = -1; /* -1 denotes "inactive" value */ + xmlRelaxNGValidityErrorFunc error_handler = +@@ -1173,7 +1173,7 @@ cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs) + + int version = get_schema_version(value); + int orig_version = version; +- int min_version = xml_find_x_0_schema_index(); ++ int min_version = xml_find_x_0_schema_index(known_schemas); + + if (version < min_version) { + // Current configuration schema is not acceptable, try to update +@@ -1235,7 +1235,7 @@ cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs) + free_xml(*xml); + *xml = converted; + +- if (version < xml_latest_schema_index()) { ++ if (version < xml_latest_schema_index(known_schemas)) { + if (to_logs) { + pcmk__config_warn("Configuration with schema %s was " + "internally upgraded to acceptable (but " +-- +2.31.1 + +From 12e7b982da61c6cc6cf01164d45bb8f7b0255a8a Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Fri, 17 Nov 2023 11:30:18 -0500 +Subject: [PATCH 18/20] Refactor: libcrmcommon: Rename several schema-related + types. + +Give them pcmk__ names indicating they are private. This is in +preparation for moving them out into a header file. +--- + lib/common/schemas.c | 80 ++++++++++++++++++++++---------------------- + 1 file changed, 40 insertions(+), 40 deletions(-) + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index 9d98695..cf8f325 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -28,7 +28,7 @@ + + typedef struct { + unsigned char v[2]; +-} schema_version_t; ++} pcmk__schema_version_t; + + #define SCHEMA_ZERO { .v = { 0, 0 } } + +@@ -41,20 +41,20 @@ typedef struct { + xmlRelaxNGParserCtxtPtr parser; + } relaxng_ctx_cache_t; + +-enum schema_validator_e { +- schema_validator_none, +- schema_validator_rng ++enum pcmk__schema_validator { ++ pcmk__schema_validator_none, ++ pcmk__schema_validator_rng + }; + +-struct schema_s { ++typedef struct { + char *name; + char *transform; + void *cache; +- enum schema_validator_e validator; +- schema_version_t version; ++ enum pcmk__schema_validator validator; ++ pcmk__schema_version_t version; + char *transform_enter; + bool transform_onleave; +-}; ++} pcmk__schema_t; + + static GList *known_schemas = NULL; + static bool silent_logging = FALSE; +@@ -94,7 +94,7 @@ xml_find_x_0_schema_index(GList *schemas) + static int best = 0; + int i; + GList *best_node = NULL; +- struct schema_s *best_schema = NULL; ++ pcmk__schema_t *best_schema = NULL; + + if (found) { + return best; +@@ -121,7 +121,7 @@ xml_find_x_0_schema_index(GList *schemas) + i = best; + + for (GList *iter = best_node->prev; iter != NULL; iter = iter->prev) { +- struct schema_s *schema = iter->data; ++ 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 +@@ -154,7 +154,7 @@ xml_latest_schema(void) + } + + static inline bool +-version_from_filename(const char *filename, schema_version_t *version) ++version_from_filename(const char *filename, pcmk__schema_version_t *version) + { + return sscanf(filename, "pacemaker-%hhu.%hhu.rng", &(version->v[0]), &(version->v[1])) == 2; + } +@@ -163,7 +163,7 @@ 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); */ +@@ -185,8 +185,8 @@ schema_filter(const struct dirent *a) + static int + schema_sort(const struct dirent **a, const struct dirent **b) + { +- schema_version_t a_version = SCHEMA_ZERO; +- schema_version_t b_version = SCHEMA_ZERO; ++ 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)) { +@@ -212,14 +212,14 @@ 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) + { +- struct schema_s *schema = NULL; ++ pcmk__schema_t *schema = NULL; + int last = g_list_length(known_schemas); + +- schema = calloc(1, sizeof(struct schema_s)); ++ schema = calloc(1, sizeof(pcmk__schema_t)); + CRM_ASSERT(schema != NULL); + + schema->validator = validator; +@@ -284,7 +284,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, 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; +@@ -343,7 +343,7 @@ add_schema_by_version(const schema_version_t *version, bool transform_expected) + rc = ENOENT; + } + +- add_schema(schema_validator_rng, version, NULL, ++ add_schema(pcmk__schema_validator_rng, version, NULL, + transform_upgrade, transform_enter, transform_onleave); + + free(transform_upgrade); +@@ -397,7 +397,7 @@ 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; ++ const pcmk__schema_version_t zero = SCHEMA_ZERO; + + wrap_libxslt(false); + +@@ -410,7 +410,7 @@ crm_schema_init(void) + free(base); + for (lpc = 0; lpc < max; lpc++) { + bool transform_expected = FALSE; +- schema_version_t version = SCHEMA_ZERO; ++ 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 +@@ -419,7 +419,7 @@ crm_schema_init(void) + continue; + } + if ((lpc + 1) < max) { +- schema_version_t next_version = SCHEMA_ZERO; ++ 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])) { +@@ -440,10 +440,10 @@ crm_schema_init(void) + } + + // @COMPAT: Deprecated since 2.1.5 +- add_schema(schema_validator_rng, &zero, "pacemaker-next", ++ add_schema(pcmk__schema_validator_rng, &zero, "pacemaker-next", + NULL, NULL, FALSE); + +- add_schema(schema_validator_none, &zero, PCMK__VALUE_NONE, ++ add_schema(pcmk__schema_validator_none, &zero, PCMK__VALUE_NONE, + NULL, NULL, FALSE); + } + +@@ -533,14 +533,14 @@ validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler, + static void + free_schema(gpointer data) + { +- struct schema_s *schema = data; ++ pcmk__schema_t *schema = data; + relaxng_ctx_cache_t *ctx = NULL; + + switch (schema->validator) { +- case schema_validator_none: // not cached ++ case pcmk__schema_validator_none: // not cached + break; + +- case schema_validator_rng: // cached ++ case pcmk__schema_validator_rng: // cached + ctx = (relaxng_ctx_cache_t *) schema->cache; + if (ctx == NULL) { + break; +@@ -582,7 +582,7 @@ crm_schema_cleanup(void) + } + + static gboolean +-validate_with(xmlNode *xml, struct schema_s *schema, xmlRelaxNGValidityErrorFunc error_handler, void* error_handler_context) ++validate_with(xmlNode *xml, pcmk__schema_t *schema, xmlRelaxNGValidityErrorFunc error_handler, void* error_handler_context) + { + gboolean valid = FALSE; + char *file = NULL; +@@ -592,7 +592,7 @@ validate_with(xmlNode *xml, struct schema_s *schema, xmlRelaxNGValidityErrorFunc + return FALSE; + } + +- if (schema->validator == schema_validator_none) { ++ if (schema->validator == pcmk__schema_validator_none) { + return TRUE; + } + +@@ -607,7 +607,7 @@ validate_with(xmlNode *xml, struct schema_s *schema, xmlRelaxNGValidityErrorFunc + 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; +@@ -621,7 +621,7 @@ validate_with(xmlNode *xml, struct schema_s *schema, xmlRelaxNGValidityErrorFunc + } + + static bool +-validate_with_silent(xmlNode *xml, struct schema_s *schema) ++validate_with_silent(xmlNode *xml, pcmk__schema_t *schema) + { + bool rc, sl_backup = silent_logging; + silent_logging = TRUE; +@@ -710,7 +710,7 @@ pcmk__validate_xml(xmlNode *xml_blob, const char *validation, xmlRelaxNGValidity + bool valid = FALSE; + + for (GList *iter = known_schemas; iter != NULL; iter = iter->next) { +- struct schema_s *schema = iter->data; ++ pcmk__schema_t *schema = iter->data; + + if (validate_with(xml_blob, schema, NULL, NULL)) { + valid = TRUE; +@@ -726,7 +726,7 @@ pcmk__validate_xml(xmlNode *xml_blob, const char *validation, xmlRelaxNGValidity + if (strcmp(validation, PCMK__VALUE_NONE) == 0) { + return TRUE; + } else if (version < g_list_length(known_schemas)) { +- struct schema_s *schema = g_list_nth_data(known_schemas, version); ++ pcmk__schema_t *schema = g_list_nth_data(known_schemas, version); + return validate_with(xml_blob, schema, error_handler, + error_handler_context); + } +@@ -918,7 +918,7 @@ apply_transformation(xmlNode *xml, const char *transform, gboolean to_logs) + * \note Only emits warnings about enter/leave phases in case of issues. + */ + static xmlNode * +-apply_upgrade(xmlNode *xml, const struct schema_s *schema, gboolean to_logs) ++apply_upgrade(xmlNode *xml, const pcmk__schema_t *schema, gboolean to_logs) + { + bool transform_onleave = schema->transform_onleave; + char *transform_leave; +@@ -972,7 +972,7 @@ apply_upgrade(xmlNode *xml, const struct schema_s *schema, gboolean to_logs) + const char * + get_schema_name(int version) + { +- struct schema_s *schema = g_list_nth_data(known_schemas, version); ++ pcmk__schema_t *schema = g_list_nth_data(known_schemas, version); + + if (schema == NULL) { + return "unknown"; +@@ -991,7 +991,7 @@ get_schema_version(const char *name) + } + + for (GList *iter = known_schemas; iter != NULL; iter = iter->next) { +- struct schema_s *schema = iter->data; ++ pcmk__schema_t *schema = iter->data; + + if (pcmk__str_eq(name, schema->name, pcmk__str_casei)) { + return lpc; +@@ -1052,7 +1052,7 @@ update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, + * easier than trying to get all the loop indices we're using here + * sorted out and working correctly. + */ +- struct schema_s *schema = g_list_nth_data(known_schemas, lpc); ++ pcmk__schema_t *schema = g_list_nth_data(known_schemas, lpc); + + crm_debug("Testing '%s' validation (%d of %d)", + pcmk__s(schema->name, ""), lpc, max_stable_schemas); +@@ -1085,7 +1085,7 @@ update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, + + if (rc == pcmk_ok && transform) { + xmlNode *upgrade = NULL; +- struct schema_s *next_schema = NULL; ++ pcmk__schema_t *next_schema = NULL; + next = lpc+1; + + if (next > max_stable_schemas) { +@@ -1151,7 +1151,7 @@ update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, + } + + if (*best > match && *best) { +- struct schema_s *best_schema = g_list_nth_data(known_schemas, *best); ++ pcmk__schema_t *best_schema = g_list_nth_data(known_schemas, *best); + + crm_info("%s the configuration from %s to %s", + transform?"Transformed":"Upgraded", pcmk__s(value, ""), +@@ -1279,7 +1279,7 @@ pcmk__log_known_schemas(void) + int lpc = 0; + + for (GList *iter = known_schemas; iter != NULL; iter = iter->next) { +- struct schema_s *schema = iter->data; ++ pcmk__schema_t *schema = iter->data; + + if (schema->transform != NULL) { + crm_debug("known_schemas[%d] => %s (upgrades with %s.xsl)", +-- +2.31.1 + +From 97b3fa3462039d4d7bdad6c6ff328a5124977e5f Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Fri, 17 Nov 2023 11:32:55 -0500 +Subject: [PATCH 19/20] Refactor: libcrmcommon: Make various schema stuff + non-static. + +This is the minimum amount necessary to make the function unit testable. +None of this is intended to ever become public. +--- + lib/common/crmcommon_private.h | 26 ++++++++++++++++++++++++++ + lib/common/schemas.c | 25 ++++--------------------- + 2 files changed, 30 insertions(+), 21 deletions(-) + +diff --git a/lib/common/crmcommon_private.h b/lib/common/crmcommon_private.h +index 121d663..6ab9de1 100644 +--- a/lib/common/crmcommon_private.h ++++ b/lib/common/crmcommon_private.h +@@ -283,4 +283,30 @@ void pcmk__register_patchset_messages(pcmk__output_t *out); + #define PCMK__PW_BUFFER_LEN 500 + + ++/* ++ * Schemas ++ */ ++typedef struct { ++ unsigned char v[2]; ++} pcmk__schema_version_t; ++ ++enum pcmk__schema_validator { ++ pcmk__schema_validator_none, ++ pcmk__schema_validator_rng ++}; ++ ++typedef struct { ++ char *name; ++ char *transform; ++ void *cache; ++ enum pcmk__schema_validator validator; ++ pcmk__schema_version_t version; ++ char *transform_enter; ++ bool transform_onleave; ++} pcmk__schema_t; ++ ++G_GNUC_INTERNAL ++int pcmk__find_x_0_schema_index(GList *schemas); ++ ++ + #endif // CRMCOMMON_PRIVATE__H +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index cf8f325..83334b4 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -26,9 +26,7 @@ + #include + #include /* PCMK__XML_LOG_BASE */ + +-typedef struct { +- unsigned char v[2]; +-} pcmk__schema_version_t; ++#include "crmcommon_private.h" + + #define SCHEMA_ZERO { .v = { 0, 0 } } + +@@ -41,21 +39,6 @@ typedef struct { + xmlRelaxNGParserCtxtPtr parser; + } relaxng_ctx_cache_t; + +-enum pcmk__schema_validator { +- pcmk__schema_validator_none, +- pcmk__schema_validator_rng +-}; +- +-typedef struct { +- char *name; +- char *transform; +- void *cache; +- enum pcmk__schema_validator validator; +- pcmk__schema_version_t version; +- char *transform_enter; +- bool transform_onleave; +-} pcmk__schema_t; +- + static GList *known_schemas = NULL; + static bool silent_logging = FALSE; + +@@ -82,8 +65,8 @@ xml_latest_schema_index(GList *schemas) + } + + /* Return the index of the most recent X.0 schema. */ +-static int +-xml_find_x_0_schema_index(GList *schemas) ++int ++pcmk__find_x_0_schema_index(GList *schemas) + { + /* We can't just use best to determine whether we've found the index + * or not. What if we have a very long list of schemas all in the +@@ -1173,7 +1156,7 @@ cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs) + + int version = get_schema_version(value); + int orig_version = version; +- int min_version = xml_find_x_0_schema_index(known_schemas); ++ int min_version = pcmk__find_x_0_schema_index(known_schemas); + + if (version < min_version) { + // Current configuration schema is not acceptable, try to update +-- +2.31.1 + +From c4c093c0785e06ca3371556b19ede1d5b44d090a Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Fri, 17 Nov 2023 12:39:53 -0500 +Subject: [PATCH 20/20] Test: libcrmcommon: Add unit tests for + pcmk__xml_find_x_0_schema_index. + +This requires making various things in the function conditional, which I +kind of hate. But, it all comes down to the fact that when we are +running for real, we're adding the pacemaker-next/none schemas with +crm_schema_init. + +When we are unit testing, we aren't doing any of that. The only +"schemas" we have are the ones we are adding directly. So, the list has +two items fewer than the real function expects. I think this is okay +and doesn't totally invalidating the testing. +--- + configure.ac | 1 + + lib/common/schemas.c | 33 +++++- + lib/common/tests/Makefile.am | 1 + + lib/common/tests/schemas/Makefile.am | 16 +++ + .../pcmk__xml_find_x_0_schema_index_test.c | 112 ++++++++++++++++++ + 5 files changed, 161 insertions(+), 2 deletions(-) + create mode 100644 lib/common/tests/schemas/Makefile.am + create mode 100644 lib/common/tests/schemas/pcmk__xml_find_x_0_schema_index_test.c + +diff --git a/configure.ac b/configure.ac +index 6bff02e..9eb7539 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -2153,6 +2153,7 @@ AC_CONFIG_FILES(Makefile \ + lib/common/tests/output/Makefile \ + lib/common/tests/procfs/Makefile \ + lib/common/tests/results/Makefile \ ++ lib/common/tests/schemas/Makefile \ + lib/common/tests/scores/Makefile \ + lib/common/tests/strings/Makefile \ + lib/common/tests/utils/Makefile \ +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index 83334b4..372e872 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -60,8 +60,13 @@ xml_latest_schema_index(GList *schemas) + { + // @COMPAT: pacemaker-next is deprecated since 2.1.5 + // FIXME: This function assumes at least three schemas have been added +- // before it has been called for the first time. ++ // before it has been called for the first time, which is only the case ++ // if we are not unit testing. ++#if defined(PCMK__UNIT_TESTING) ++ return g_list_length(schemas) - 1; // index from 0 ++#else + return g_list_length(schemas) - 3; // index from 0, ignore "pacemaker-next"/"none" ++#endif + } + + /* Return the index of the most recent X.0 schema. */ +@@ -73,8 +78,17 @@ pcmk__find_x_0_schema_index(GList *schemas) + * 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; +@@ -90,10 +104,25 @@ pcmk__find_x_0_schema_index(GList *schemas) + best_node = g_list_nth(schemas, best); + best_schema = best_node->data; + +- /* If this is a singleton list, we're done. */ ++ /* If we are unit testing, we don't add the pacemaker-next/none schemas ++ * to the list because we're not using the standard schema adding ++ * functions. Thus, a singleton list means we're done. ++ * ++ * On the other hand, if we are running as usually, we have those two ++ * schemas added to the list. A list of length three actually only has ++ * one useful schema. So we're still done. ++ * ++ * @COMPAT Change this when we stop adding those schemas. ++ */ ++#if defined(PCMK__UNIT_TESTING) + if (pcmk__list_of_1(schemas)) { + goto done; + } ++#else ++ if (g_list_length(schemas) == 3) { ++ goto done; ++ } ++#endif + + /* Start comparing the list from the node before the best schema (there's + * no point in comparing something to itself). Then, 'i' is an index +diff --git a/lib/common/tests/Makefile.am b/lib/common/tests/Makefile.am +index c0407e5..22fb32e 100644 +--- a/lib/common/tests/Makefile.am ++++ b/lib/common/tests/Makefile.am +@@ -21,6 +21,7 @@ SUBDIRS = \ + options \ + output \ + results \ ++ schemas \ + scores \ + strings \ + utils \ +diff --git a/lib/common/tests/schemas/Makefile.am b/lib/common/tests/schemas/Makefile.am +new file mode 100644 +index 0000000..5f485b3 +--- /dev/null ++++ b/lib/common/tests/schemas/Makefile.am +@@ -0,0 +1,16 @@ ++# ++# Copyright 2023 the Pacemaker project contributors ++# ++# The version control history for this file may have further details. ++# ++# This source code is licensed under the GNU General Public License version 2 ++# or later (GPLv2+) WITHOUT ANY WARRANTY. ++# ++ ++include $(top_srcdir)/mk/tap.mk ++include $(top_srcdir)/mk/unittest.mk ++ ++# Add "_test" to the end of all test program names to simplify .gitignore. ++check_PROGRAMS = pcmk__xml_find_x_0_schema_index_test ++ ++TESTS = $(check_PROGRAMS) +diff --git a/lib/common/tests/schemas/pcmk__xml_find_x_0_schema_index_test.c b/lib/common/tests/schemas/pcmk__xml_find_x_0_schema_index_test.c +new file mode 100644 +index 0000000..9f16ba1 +--- /dev/null ++++ b/lib/common/tests/schemas/pcmk__xml_find_x_0_schema_index_test.c +@@ -0,0 +1,112 @@ ++/* ++ * Copyright 2023 the Pacemaker project contributors ++ * ++ * The version control history for this file may have further details. ++ * ++ * This source code is licensed under the GNU General Public License version 2 ++ * or later (GPLv2+) WITHOUT ANY WARRANTY. ++ */ ++ ++#include ++#include ++ ++#include ++ ++#include "crmcommon_private.h" ++ ++static pcmk__schema_t * ++mk_schema(const char *name, unsigned char x, unsigned char y) ++{ ++ pcmk__schema_t *schema = malloc(sizeof(pcmk__schema_t)); ++ ++ schema->name = strdup(name); ++ schema->version.v[0] = x; ++ schema->version.v[1] = y; ++ return schema; ++} ++ ++static void ++free_schema(void *data) ++{ ++ pcmk__schema_t *schema = data; ++ free(schema->name); ++ free(schema); ++} ++ ++static void ++empty_schema_list(void **state) ++{ ++ pcmk__assert_asserts(pcmk__find_x_0_schema_index(NULL)); ++} ++ ++static void ++singleton_schema_list(void **state) ++{ ++ GList *schemas = NULL; ++ ++ schemas = g_list_append(schemas, mk_schema("pacemaker-1.0", 1, 0)); ++ assert_int_equal(0, pcmk__find_x_0_schema_index(schemas)); ++ g_list_free_full(schemas, free_schema); ++} ++ ++static void ++one_major_version(void **state) ++{ ++ GList *schemas = NULL; ++ ++ schemas = g_list_append(schemas, mk_schema("pacemaker-1.0", 1, 0)); ++ schemas = g_list_append(schemas, mk_schema("pacemaker-1.2", 1, 2)); ++ schemas = g_list_append(schemas, mk_schema("pacemaker-1.3", 1, 3)); ++ assert_int_equal(0, pcmk__find_x_0_schema_index(schemas)); ++ g_list_free_full(schemas, free_schema); ++} ++ ++static void ++first_version_is_not_0(void **state) ++{ ++ GList *schemas = NULL; ++ ++ schemas = g_list_append(schemas, mk_schema("pacemaker-1.1", 1, 1)); ++ schemas = g_list_append(schemas, mk_schema("pacemaker-1.2", 1, 2)); ++ schemas = g_list_append(schemas, mk_schema("pacemaker-1.3", 1, 3)); ++ assert_int_equal(0, pcmk__find_x_0_schema_index(schemas)); ++ g_list_free_full(schemas, free_schema); ++} ++ ++static void ++multiple_major_versions(void **state) ++{ ++ GList *schemas = NULL; ++ ++ schemas = g_list_append(schemas, mk_schema("pacemaker-1.0", 1, 0)); ++ schemas = g_list_append(schemas, mk_schema("pacemaker-1.1", 1, 1)); ++ schemas = g_list_append(schemas, mk_schema("pacemaker-2.0", 2, 0)); ++ assert_int_equal(2, pcmk__find_x_0_schema_index(schemas)); ++ g_list_free_full(schemas, free_schema); ++} ++ ++static void ++many_versions(void **state) ++{ ++ GList *schemas = NULL; ++ ++ schemas = g_list_append(schemas, mk_schema("pacemaker-1.0", 1, 0)); ++ schemas = g_list_append(schemas, mk_schema("pacemaker-1.1", 1, 1)); ++ schemas = g_list_append(schemas, mk_schema("pacemaker-1.2", 1, 2)); ++ schemas = g_list_append(schemas, mk_schema("pacemaker-2.0", 2, 0)); ++ schemas = g_list_append(schemas, mk_schema("pacemaker-2.1", 2, 1)); ++ schemas = g_list_append(schemas, mk_schema("pacemaker-2.2", 2, 2)); ++ schemas = g_list_append(schemas, mk_schema("pacemaker-3.0", 3, 0)); ++ schemas = g_list_append(schemas, mk_schema("pacemaker-3.1", 3, 1)); ++ schemas = g_list_append(schemas, mk_schema("pacemaker-3.2", 3, 2)); ++ assert_int_equal(6, pcmk__find_x_0_schema_index(schemas)); ++ g_list_free_full(schemas, free_schema); ++} ++ ++PCMK__UNIT_TEST(NULL, NULL, ++ cmocka_unit_test(empty_schema_list), ++ cmocka_unit_test(singleton_schema_list), ++ cmocka_unit_test(one_major_version), ++ cmocka_unit_test(first_version_is_not_0), ++ cmocka_unit_test(multiple_major_versions), ++ cmocka_unit_test(many_versions)) +-- +2.31.1 + diff --git a/002-schema-transfer.patch b/002-schema-transfer.patch new file mode 100644 index 0000000..9c1c05b --- /dev/null +++ b/002-schema-transfer.patch @@ -0,0 +1,1986 @@ +From 9e0c58dc3b949e4eadfa43364e60677478b7aa0f Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Thu, 7 Sep 2023 16:48:52 -0400 +Subject: [PATCH 01/15] Refactor: libcrmcommon: Break schema version comparison + out... + +...into its own function. Then, wrap this function with +schema_sort_directory so existing code doesn't need to be changed. This +allows us to use the version comparison elsewhere. +--- + lib/common/schemas.c | 26 ++++++++++++++++---------- + 1 file changed, 16 insertions(+), 10 deletions(-) + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index 372e87223..b3ff05917 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -195,7 +195,20 @@ 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) ++{ ++ 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; +@@ -206,14 +219,7 @@ schema_sort(const struct dirent **a, const struct dirent **b) + return 0; + } + +- for (int i = 0; i < 2; ++i) { +- if (a_version.v[i] < b_version.v[i]) { +- return -1; +- } else if (a_version.v[i] > b_version.v[i]) { +- return 1; +- } +- } +- return 0; ++ return schema_cmp(a_version, b_version); + } + + /*! +@@ -413,7 +419,7 @@ crm_schema_init(void) + + wrap_libxslt(false); + +- max = scandir(base, &namelist, schema_filter, schema_sort); ++ max = scandir(base, &namelist, schema_filter, schema_cmp_directory); + if (max < 0) { + crm_notice("scandir(%s) failed: %s (%d)", base, strerror(errno), errno); + free(base); +-- +2.41.0 + +From e7d7c33eb8329c3c50fe648133cbb7651c1ecb9d Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Wed, 29 Nov 2023 09:37:31 -0500 +Subject: [PATCH 02/15] Feature: libcrmcommon: Add + pcmk__schema_files_later_than. + +This function takes in a schema version and returns a list of all RNG +and XSLT files from all schema versions more recent than that one. + +Also, add unit tests for the new function. +--- + include/crm/common/internal.h | 1 + + lib/common/schemas.c | 62 ++++++++++- + lib/common/tests/schemas/Makefile.am | 42 ++++++- + .../pcmk__schema_files_later_than_test.c | 104 ++++++++++++++++++ + 4 files changed, 207 insertions(+), 2 deletions(-) + create mode 100644 lib/common/tests/schemas/pcmk__schema_files_later_than_test.c + +diff --git a/include/crm/common/internal.h b/include/crm/common/internal.h +index 307860636..a3cd455dc 100644 +--- a/include/crm/common/internal.h ++++ b/include/crm/common/internal.h +@@ -134,6 +134,7 @@ bool pcmk__procfs_has_pids(void); + void crm_schema_init(void); + void crm_schema_cleanup(void); + ++GList *pcmk__schema_files_later_than(const char *name); + + /* internal functions related to process IDs (from pid.c) */ + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index b3ff05917..1c60738ec 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -168,7 +168,11 @@ xml_latest_schema(void) + static inline bool + version_from_filename(const char *filename, pcmk__schema_version_t *version) + { +- return sscanf(filename, "pacemaker-%hhu.%hhu.rng", &(version->v[0]), &(version->v[1])) == 2; ++ if (pcmk__ends_with(filename, ".rng")) { ++ return sscanf(filename, "pacemaker-%hhu.%hhu.rng", &(version->v[0]), &(version->v[1])) == 2; ++ } else { ++ return sscanf(filename, "pacemaker-%hhu.%hhu", &(version->v[0]), &(version->v[1])) == 2; ++ } + } + + static int +@@ -1291,6 +1295,62 @@ cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs) + return rc; + } + ++/*! ++ * \internal ++ * \brief Return a list of all schema files and any associated XSLT files ++ * later than the given one ++ * \brief Return a list of all schema versions later than the given one ++ * ++ * \param[in] schema The schema to compare against (for example, ++ * "pacemaker-3.1.rng" or "pacemaker-3.1") ++ * ++ * \note The caller is responsible for freeing both the returned list and ++ * the elements of the list ++ */ ++GList * ++pcmk__schema_files_later_than(const char *name) ++{ ++ GList *lst = NULL; ++ pcmk__schema_version_t ver; ++ ++ if (!version_from_filename(name, &ver)) { ++ return lst; ++ } ++ ++ for (GList *iter = g_list_nth(known_schemas, xml_latest_schema_index(known_schemas)); ++ iter != NULL; iter = iter->prev) { ++ pcmk__schema_t *schema = iter->data; ++ char *s = NULL; ++ ++ if (schema_cmp(ver, schema->version) != -1) { ++ continue; ++ } ++ ++ s = crm_strdup_printf("%s.rng", schema->name); ++ lst = g_list_prepend(lst, s); ++ ++ if (schema->transform != NULL) { ++ char *xform = crm_strdup_printf("%s.xsl", schema->transform); ++ lst = g_list_prepend(lst, xform); ++ } ++ ++ if (schema->transform_enter != NULL) { ++ char *enter = crm_strdup_printf("%s.xsl", schema->transform_enter); ++ ++ lst = g_list_prepend(lst, enter); ++ ++ if (schema->transform_onleave) { ++ int last_dash = strrchr(enter, '-') - enter; ++ char *leave = crm_strdup_printf("%.*s-leave.xsl", last_dash, enter); ++ ++ lst = g_list_prepend(lst, leave); ++ } ++ } ++ } ++ ++ return lst; ++} ++ + void + pcmk__log_known_schemas(void) + { +diff --git a/lib/common/tests/schemas/Makefile.am b/lib/common/tests/schemas/Makefile.am +index 5f485b3e9..b5c5e7f3c 100644 +--- a/lib/common/tests/schemas/Makefile.am ++++ b/lib/common/tests/schemas/Makefile.am +@@ -10,7 +10,47 @@ + include $(top_srcdir)/mk/tap.mk + include $(top_srcdir)/mk/unittest.mk + ++CFLAGS += -DPCMK__TEST_SCHEMA_DIR='"$(abs_builddir)/schemas"' ++ + # Add "_test" to the end of all test program names to simplify .gitignore. +-check_PROGRAMS = pcmk__xml_find_x_0_schema_index_test ++check_PROGRAMS = pcmk__schema_files_later_than_test \ ++ pcmk__xml_find_x_0_schema_index_test + + TESTS = $(check_PROGRAMS) ++ ++$(TESTS): setup-schema-dir ++ ++# Set up a temporary schemas/ directory containing only some of the full set of ++# pacemaker schema files. This lets us know exactly how many schemas are present, ++# allowing us to write tests without having to make changes when new schemas are ++# added. ++# ++# This directory contains the following: ++# ++# * pacemaker-next.rng - Used to verify that this sorts before all versions ++# * upgrade-*.xsl - Required by various schema versions ++# * pacemaker-[0-9]*.rng - We're only pulling in 15 schemas, which is enough ++# to get everything through pacemaker-3.0.rng. This ++# includes 2.10, needed so we can check that versions ++# are compared as numbers instead of strings. ++# * other RNG files - This catches everything except the pacemaker-*rng ++# files. These files are included by the top-level ++# pacemaker-*rng files, so we need them for tests. ++# This will glob more than we need, but the extra ones ++# won't get in the way. ++.PHONY: setup-schema-dir ++setup-schema-dir: ++ $(MKDIR_P) schemas ++ ( cd schemas ; \ ++ ln -sf $(abs_top_builddir)/xml/pacemaker-next.rng . ; \ ++ ln -sf $(abs_top_builddir)/xml/upgrade-*.xsl . ; \ ++ for f in $(shell ls -1v $(abs_top_builddir)/xml/pacemaker-[0-9]*.rng | head -15); do \ ++ ln -sf $$f $$(basename $$f); \ ++ done ; \ ++ for f in $(shell ls -1 $(top_srcdir)/xml/*.rng | grep -v pacemaker); do \ ++ ln -sf ../$$f $$(basename $$f); \ ++ done ) ++ ++.PHONY: clean-local ++clean-local: ++ -rm -rf schemas +diff --git a/lib/common/tests/schemas/pcmk__schema_files_later_than_test.c b/lib/common/tests/schemas/pcmk__schema_files_later_than_test.c +new file mode 100644 +index 000000000..50b5f4334 +--- /dev/null ++++ b/lib/common/tests/schemas/pcmk__schema_files_later_than_test.c +@@ -0,0 +1,104 @@ ++/* ++ * Copyright 2023 the Pacemaker project contributors ++ * ++ * The version control history for this file may have further details. ++ * ++ * This source code is licensed under the GNU General Public License version 2 ++ * or later (GPLv2+) WITHOUT ANY WARRANTY. ++ */ ++ ++#include ++ ++#include ++#include ++ ++#include ++ ++static int ++setup(void **state) { ++ setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR, 1); ++ crm_schema_init(); ++ return 0; ++} ++ ++static int ++teardown(void **state) { ++ crm_schema_cleanup(); ++ unsetenv("PCMK_schema_directory"); ++ return 0; ++} ++ ++static void ++invalid_name(void **state) ++{ ++ assert_null(pcmk__schema_files_later_than("xyz")); ++ assert_null(pcmk__schema_files_later_than("pacemaker-")); ++} ++ ++static void ++valid_name(void **state) ++{ ++ GList *schemas = NULL; ++ ++ schemas = pcmk__schema_files_later_than("pacemaker-1.0"); ++ assert_int_equal(g_list_length(schemas), 18); ++ /* There is no "pacemaker-1.1". */ ++ assert_string_equal("pacemaker-1.2.rng", g_list_nth_data(schemas, 0)); ++ assert_string_equal("upgrade-1.3.xsl", g_list_nth_data(schemas, 1)); ++ assert_string_equal("pacemaker-1.3.rng", g_list_nth_data(schemas, 2)); ++ assert_string_equal("pacemaker-2.0.rng", g_list_nth_data(schemas, 3)); ++ assert_string_equal("pacemaker-2.1.rng", g_list_nth_data(schemas, 4)); ++ assert_string_equal("pacemaker-2.2.rng", g_list_nth_data(schemas, 5)); ++ assert_string_equal("pacemaker-2.3.rng", g_list_nth_data(schemas, 6)); ++ assert_string_equal("pacemaker-2.4.rng", g_list_nth_data(schemas, 7)); ++ assert_string_equal("pacemaker-2.5.rng", g_list_nth_data(schemas, 8)); ++ assert_string_equal("pacemaker-2.6.rng", g_list_nth_data(schemas, 9)); ++ assert_string_equal("pacemaker-2.7.rng", g_list_nth_data(schemas, 10)); ++ assert_string_equal("pacemaker-2.8.rng", g_list_nth_data(schemas, 11)); ++ assert_string_equal("pacemaker-2.9.rng", g_list_nth_data(schemas, 12)); ++ assert_string_equal("upgrade-2.10-leave.xsl", g_list_nth_data(schemas, 13)); ++ assert_string_equal("upgrade-2.10-enter.xsl", g_list_nth_data(schemas, 14)); ++ assert_string_equal("upgrade-2.10.xsl", g_list_nth_data(schemas, 15)); ++ assert_string_equal("pacemaker-2.10.rng", g_list_nth_data(schemas, 16)); ++ assert_string_equal("pacemaker-3.0.rng", g_list_nth_data(schemas, 17)); ++ g_list_free_full(schemas, free); ++ ++ /* Adding .rng to the end of the schema we're requesting is also valid. */ ++ schemas = pcmk__schema_files_later_than("pacemaker-2.0.rng"); ++ assert_int_equal(g_list_length(schemas), 14); ++ assert_string_equal("pacemaker-2.1.rng", g_list_nth_data(schemas, 0)); ++ assert_string_equal("pacemaker-2.2.rng", g_list_nth_data(schemas, 1)); ++ assert_string_equal("pacemaker-2.3.rng", g_list_nth_data(schemas, 2)); ++ assert_string_equal("pacemaker-2.4.rng", g_list_nth_data(schemas, 3)); ++ assert_string_equal("pacemaker-2.5.rng", g_list_nth_data(schemas, 4)); ++ assert_string_equal("pacemaker-2.6.rng", g_list_nth_data(schemas, 5)); ++ assert_string_equal("pacemaker-2.7.rng", g_list_nth_data(schemas, 6)); ++ assert_string_equal("pacemaker-2.8.rng", g_list_nth_data(schemas, 7)); ++ assert_string_equal("pacemaker-2.9.rng", g_list_nth_data(schemas, 8)); ++ assert_string_equal("upgrade-2.10-leave.xsl", g_list_nth_data(schemas, 9)); ++ assert_string_equal("upgrade-2.10-enter.xsl", g_list_nth_data(schemas, 10)); ++ assert_string_equal("upgrade-2.10.xsl", g_list_nth_data(schemas, 11)); ++ assert_string_equal("pacemaker-2.10.rng", g_list_nth_data(schemas, 12)); ++ assert_string_equal("pacemaker-3.0.rng", g_list_nth_data(schemas, 13)); ++ g_list_free_full(schemas, free); ++ ++ /* Check that "pacemaker-2.10" counts as later than "pacemaker-2.9". */ ++ schemas = pcmk__schema_files_later_than("pacemaker-2.9"); ++ assert_int_equal(g_list_length(schemas), 5); ++ assert_string_equal("upgrade-2.10-leave.xsl", g_list_nth_data(schemas, 0)); ++ assert_string_equal("upgrade-2.10-enter.xsl", g_list_nth_data(schemas, 1)); ++ assert_string_equal("upgrade-2.10.xsl", g_list_nth_data(schemas, 2)); ++ assert_string_equal("pacemaker-2.10.rng", g_list_nth_data(schemas, 3)); ++ assert_string_equal("pacemaker-3.0.rng", g_list_nth_data(schemas, 4)); ++ g_list_free_full(schemas, free); ++ ++ /* And then something way in the future that will never apply due to our ++ * special schema directory. ++ */ ++ schemas = pcmk__schema_files_later_than("pacemaker-9.0"); ++ assert_null(schemas); ++} ++ ++PCMK__UNIT_TEST(setup, teardown, ++ cmocka_unit_test(invalid_name), ++ cmocka_unit_test(valid_name)) +-- +2.41.0 + +From 76859d61f4d35e1f8b7c35d8766d3d0c123d4552 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Mon, 11 Sep 2023 08:56:22 -0400 +Subject: [PATCH 03/15] Refactor: libcrmcommon: Allow more specs in + pcmk__xml_artefact_path. + +If the given filespec already has a .rng or .xsl extension, don't add an +additional one. This allows reusing this function to grab files given +in another schema file's externalRef links without modification. +--- + lib/common/xml.c | 12 ++++++++++-- + 1 file changed, 10 insertions(+), 2 deletions(-) + +diff --git a/lib/common/xml.c b/lib/common/xml.c +index 53ebff770..142b501df 100644 +--- a/lib/common/xml.c ++++ b/lib/common/xml.c +@@ -2618,11 +2618,19 @@ pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec) + switch (ns) { + case pcmk__xml_artefact_ns_legacy_rng: + case pcmk__xml_artefact_ns_base_rng: +- ret = crm_strdup_printf("%s/%s.rng", base, filespec); ++ if (pcmk__ends_with(filespec, ".rng")) { ++ ret = crm_strdup_printf("%s/%s", base, filespec); ++ } else { ++ ret = crm_strdup_printf("%s/%s.rng", base, filespec); ++ } + break; + case pcmk__xml_artefact_ns_legacy_xslt: + case pcmk__xml_artefact_ns_base_xslt: +- ret = crm_strdup_printf("%s/%s.xsl", base, filespec); ++ if (pcmk__ends_with(filespec, ".xsl")) { ++ ret = crm_strdup_printf("%s/%s", base, filespec); ++ } else { ++ ret = crm_strdup_printf("%s/%s.xsl", base, filespec); ++ } + break; + default: + crm_err("XML artefact family specified as %u not recognized", ns); +-- +2.41.0 + +From 0d702bba5b50e1eede201853c7680a2517fade9f Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Mon, 18 Sep 2023 12:37:46 -0400 +Subject: [PATCH 04/15] Feature: build: Add PCMK__REMOTE_SCHEMA_DIR to + configure.ac. + +This is a new subdirectory where schema files downloaded to a remote +executor can be stored. + +Also, add support for an environment variable that can override this +compile-time setting and explain it in the documentation. +--- + configure.ac | 5 +++++ + doc/sphinx/Pacemaker_Explained/local-options.rst | 12 ++++++++++++ + etc/sysconfig/pacemaker.in | 7 +++++++ + include/crm/common/options_internal.h | 1 + + 4 files changed, 25 insertions(+) + +diff --git a/configure.ac b/configure.ac +index 17cee41e9..bd548200c 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -838,6 +838,11 @@ AC_DEFINE_UNQUOTED([CRM_SCHEMA_DIRECTORY], ["$CRM_SCHEMA_DIRECTORY"], + [Location for the Pacemaker Relax-NG Schema]) + AC_SUBST(CRM_SCHEMA_DIRECTORY) + ++PCMK__REMOTE_SCHEMA_DIR="${localstatedir}/lib/pacemaker/schemas" ++AC_DEFINE_UNQUOTED([PCMK__REMOTE_SCHEMA_DIR], ["$PCMK__REMOTE_SCHEMA_DIR"], ++ [Location to store Relax-NG Schema files on remote nodes]) ++AC_SUBST(PCMK__REMOTE_SCHEMA_DIR) ++ + CRM_CORE_DIR="${localstatedir}/lib/pacemaker/cores" + AC_DEFINE_UNQUOTED([CRM_CORE_DIR], ["$CRM_CORE_DIR"], + [Directory Pacemaker daemons should change to (without systemd, core files will go here)]) +diff --git a/doc/sphinx/Pacemaker_Explained/local-options.rst b/doc/sphinx/Pacemaker_Explained/local-options.rst +index 91eda6632..118256247 100644 +--- a/doc/sphinx/Pacemaker_Explained/local-options.rst ++++ b/doc/sphinx/Pacemaker_Explained/local-options.rst +@@ -478,6 +478,18 @@ environment variables when Pacemaker daemons start up. + - *Advanced Use Only:* Specify an alternate location for RNG schemas and + XSL transforms. + ++ * - .. _pcmk_remote_schema_directory: ++ ++ .. index:: ++ pair:: node option; PCMK_remote_schema_directory ++ ++ PCMK_remote_schema_directory ++ - :ref:`text ` ++ - |PCMK__REMOTE_SCHEMA_DIR| ++ - *Advanced Use Only:* Specify an alternate location on Pacemaker Remote ++ nodes for storing newer RNG schemas and XSL transforms fetched from ++ the cluster. ++ + * - .. _pcmk_valgrind_enabled: + + .. index:: +diff --git a/etc/sysconfig/pacemaker.in b/etc/sysconfig/pacemaker.in +index 0c3609d8e..487a104a7 100644 +--- a/etc/sysconfig/pacemaker.in ++++ b/etc/sysconfig/pacemaker.in +@@ -339,6 +339,13 @@ + # + # Default: PCMK_schema_directory="@CRM_SCHEMA_DIRECTORY@" + ++# PCMK_remote_schema_directory (Advanced Use Only) ++# ++# Specify an alternate location on Pacemaker Remote nodes for storing newer ++# RNG schemas and XSL transforms fetched from the cluster. ++# ++# Default: PCMK_remote_schema_directory="@PCMK__REMOTE_SCHEMA_DIR@" ++ + # G_SLICE (Advanced Use Only) + # + # Affect the behavior of glib's memory allocator. Setting to "always-malloc" +diff --git a/include/crm/common/options_internal.h b/include/crm/common/options_internal.h +index 5c561fd1f..a9316ca33 100644 +--- a/include/crm/common/options_internal.h ++++ b/include/crm/common/options_internal.h +@@ -95,6 +95,7 @@ bool pcmk__valid_sbd_timeout(const char *value); + #define PCMK__ENV_PANIC_ACTION "panic_action" + #define PCMK__ENV_PHYSICAL_HOST "physical_host" + #define PCMK__ENV_REMOTE_ADDRESS "remote_address" ++#define PCMK__ENV_REMOTE_SCHEMA_DIR "remote_schema_directory" + #define PCMK__ENV_REMOTE_PID1 "remote_pid1" + #define PCMK__ENV_REMOTE_PORT "remote_port" + #define PCMK__ENV_RESPAWNED "respawned" +-- +2.41.0 + +From 16d46de389f33a8b29cbd74ee2c9077f28029446 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Wed, 6 Dec 2023 12:38:12 -0500 +Subject: [PATCH 05/15] Feature: libcrmcommon: Add pcmk__remote_schema_dir. + +This function checks both the environment (thus, +/etc/sysconfig/pacemaker as well) and ./configure options for the +location where any additional schema files should be stored. +--- + include/crm/common/xml_internal.h | 1 + + lib/common/schemas.c | 17 +++++++++++++++++ + 2 files changed, 18 insertions(+) + +diff --git a/include/crm/common/xml_internal.h b/include/crm/common/xml_internal.h +index f319856c8..cb27ec6b2 100644 +--- a/include/crm/common/xml_internal.h ++++ b/include/crm/common/xml_internal.h +@@ -447,5 +447,6 @@ gboolean pcmk__validate_xml(xmlNode *xml_blob, const char *validation, + void *error_handler_context); + + void pcmk__log_known_schemas(void); ++const char *pcmk__remote_schema_dir(void); + + #endif // PCMK__XML_INTERNAL__H +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index 1c60738ec..c03d80036 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -1351,6 +1351,23 @@ pcmk__schema_files_later_than(const char *name) + return lst; + } + ++/*! ++ * \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_DIR); ++ ++ if (pcmk__str_empty(dir)) { ++ return PCMK__REMOTE_SCHEMA_DIR; ++ } ++ ++ return dir; ++} ++ + void + pcmk__log_known_schemas(void) + { +-- +2.41.0 + +From d2ed95651ea7c44153a34ba009494c70928319d7 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Tue, 19 Sep 2023 11:40:59 -0400 +Subject: [PATCH 06/15] Refactor: libcrmcommon: Look in more dirs in + pcmk__xml_artefact_path. + +If the directory returned by pcmk__xml_artefact_root doesn't contain the +file we were looking for, fall back on PCMK__REMOTE_SCHEMA_DIR. If that +still doesn't contain the file, return NULL. +--- + lib/common/xml.c | 32 +++++++++++++++++++++++++------- + 1 file changed, 25 insertions(+), 7 deletions(-) + +diff --git a/lib/common/xml.c b/lib/common/xml.c +index 142b501df..a1b1291f9 100644 +--- a/lib/common/xml.c ++++ b/lib/common/xml.c +@@ -10,6 +10,7 @@ + #include + + #include ++#include + #include + #include + #include +@@ -2610,33 +2611,50 @@ pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns) + return ret; + } + +-char * +-pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec) ++static char * ++find_artefact(enum pcmk__xml_artefact_ns ns, const char *path, const char *filespec) + { +- char *base = pcmk__xml_artefact_root(ns), *ret = NULL; ++ char *ret = NULL; + + switch (ns) { + case pcmk__xml_artefact_ns_legacy_rng: + case pcmk__xml_artefact_ns_base_rng: + if (pcmk__ends_with(filespec, ".rng")) { +- ret = crm_strdup_printf("%s/%s", base, filespec); ++ ret = crm_strdup_printf("%s/%s", path, filespec); + } else { +- ret = crm_strdup_printf("%s/%s.rng", base, filespec); ++ ret = crm_strdup_printf("%s/%s.rng", path, filespec); + } + break; + case pcmk__xml_artefact_ns_legacy_xslt: + case pcmk__xml_artefact_ns_base_xslt: + if (pcmk__ends_with(filespec, ".xsl")) { +- ret = crm_strdup_printf("%s/%s", base, filespec); ++ ret = crm_strdup_printf("%s/%s", path, filespec); + } else { +- ret = crm_strdup_printf("%s/%s.xsl", base, filespec); ++ ret = crm_strdup_printf("%s/%s.xsl", path, filespec); + } + break; + default: + crm_err("XML artefact family specified as %u not recognized", ns); + } ++ ++ return ret; ++} ++ ++char * ++pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec) ++{ ++ struct stat sb; ++ char *base = pcmk__xml_artefact_root(ns); ++ char *ret = NULL; ++ ++ ret = find_artefact(ns, base, filespec); + free(base); + ++ if (stat(ret, &sb) != 0 || !S_ISREG(sb.st_mode)) { ++ const char *remote_schema_dir = pcmk__remote_schema_dir(); ++ ret = find_artefact(ns, remote_schema_dir, filespec); ++ } ++ + return ret; + } + +-- +2.41.0 + +From 9f4da4102a40f7dbfe0d78c7735c2801ba852f4b Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Mon, 11 Sep 2023 09:17:18 -0400 +Subject: [PATCH 07/15] Feature: libcrmcommon: Add XML attrs needed for schema + file transfer. + +Also, move PCMK__XA_CONN_HOST to be alphabetized. +--- + include/crm_internal.h | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + +diff --git a/include/crm_internal.h b/include/crm_internal.h +index 71a0f7efa..3bc8d096a 100644 +--- a/include/crm_internal.h ++++ b/include/crm_internal.h +@@ -81,17 +81,21 @@ + #define PCMK__XA_CONFIG_ERRORS "config-errors" + #define PCMK__XA_CONFIG_WARNINGS "config-warnings" + #define PCMK__XA_CONFIRM "confirm" ++#define PCMK__XA_CONN_HOST "connection_host" + #define PCMK__XA_CRMD "crmd" + #define PCMK__XA_EXPECTED "expected" ++#define PCMK__XA_FILE "file" + #define PCMK__XA_GRAPH_ERRORS "graph-errors" + #define PCMK__XA_GRAPH_WARNINGS "graph-warnings" + #define PCMK__XA_IN_CCM "in_ccm" + #define PCMK__XA_JOIN "join" + #define PCMK__XA_MODE "mode" + #define PCMK__XA_NODE_START_STATE "node_start_state" ++#define PCMK__XA_PATH "path" ++#define PCMK__XA_SCHEMA "schema" ++#define PCMK__XA_SCHEMAS "schemas" + #define PCMK__XA_TASK "task" + #define PCMK__XA_UPTIME "uptime" +-#define PCMK__XA_CONN_HOST "connection_host" + + + /* +-- +2.41.0 + +From 3384643bd4572ad03e183b16d5cc84fe69599380 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Mon, 11 Sep 2023 10:46:26 -0400 +Subject: [PATCH 08/15] Feature: libcrmcommon: Add pcmk__build_schema_xml_node. + +This function adds a given RNG or XSLT file and all of the files they +refer to as children of a given XML node. + +Also, add unit tests for the new function. +--- + include/crm/common/internal.h | 3 + + lib/common/schemas.c | 144 +++++++++++++++++ + lib/common/tests/schemas/Makefile.am | 3 +- + .../pcmk__build_schema_xml_node_test.c | 149 ++++++++++++++++++ + 4 files changed, 298 insertions(+), 1 deletion(-) + create mode 100644 lib/common/tests/schemas/pcmk__build_schema_xml_node_test.c + +diff --git a/include/crm/common/internal.h b/include/crm/common/internal.h +index a3cd455dc..318003efe 100644 +--- a/include/crm/common/internal.h ++++ b/include/crm/common/internal.h +@@ -135,6 +135,9 @@ void crm_schema_init(void); + void crm_schema_cleanup(void); + + GList *pcmk__schema_files_later_than(const char *name); ++void pcmk__build_schema_xml_node(xmlNode *parent, const char *name, ++ GList **already_included); ++ + + /* internal functions related to process IDs (from pid.c) */ + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index c03d80036..1bcdff031 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -1351,6 +1351,150 @@ pcmk__schema_files_later_than(const char *name) + return lst; + } + ++static void ++append_href(xmlNode *xml, void *user_data) ++{ ++ GList **list = user_data; ++ const char *href = crm_element_value(xml, "href"); ++ char *s = NULL; ++ ++ if (href == NULL) { ++ return; ++ } ++ ++ s = strdup(href); ++ CRM_ASSERT(s != NULL); ++ *list = g_list_prepend(*list, s); ++} ++ ++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 = string2xml(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 = strdup(file); ++ CRM_ASSERT(path != NULL); ++ } ++ ++ 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 = create_xml_node(parent, PCMK__XA_FILE); ++ if (file_node == NULL) { ++ free(contents); ++ free(path); ++ return; ++ } ++ ++ 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 = create_xml_node(NULL, PCMK__XA_SCHEMA); ++ ++ crm_xml_add(schema_node, XML_ATTR_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 +diff --git a/lib/common/tests/schemas/Makefile.am b/lib/common/tests/schemas/Makefile.am +index b5c5e7f3c..8854eb264 100644 +--- a/lib/common/tests/schemas/Makefile.am ++++ b/lib/common/tests/schemas/Makefile.am +@@ -13,7 +13,8 @@ include $(top_srcdir)/mk/unittest.mk + CFLAGS += -DPCMK__TEST_SCHEMA_DIR='"$(abs_builddir)/schemas"' + + # Add "_test" to the end of all test program names to simplify .gitignore. +-check_PROGRAMS = pcmk__schema_files_later_than_test \ ++check_PROGRAMS = pcmk__build_schema_xml_node_test \ ++ pcmk__schema_files_later_than_test \ + pcmk__xml_find_x_0_schema_index_test + + TESTS = $(check_PROGRAMS) +diff --git a/lib/common/tests/schemas/pcmk__build_schema_xml_node_test.c b/lib/common/tests/schemas/pcmk__build_schema_xml_node_test.c +new file mode 100644 +index 000000000..1f5cb6ce1 +--- /dev/null ++++ b/lib/common/tests/schemas/pcmk__build_schema_xml_node_test.c +@@ -0,0 +1,149 @@ ++/* ++ * Copyright 2023 the Pacemaker project contributors ++ * ++ * The version control history for this file may have further details. ++ * ++ * This source code is licensed under the GNU General Public License version 2 ++ * or later (GPLv2+) WITHOUT ANY WARRANTY. ++ */ ++ ++#include ++ ++#include ++#include ++#include ++ ++#include ++ ++const char *rngs1[] = { "pacemaker-3.0.rng", "status-1.0.rng", "alerts-2.10.rng", ++ "nvset-2.9.rng", "score.rng", "rule-2.9.rng", ++ "tags-1.3.rng", "acls-2.0.rng", "fencing-2.4.rng", ++ "constraints-3.0.rng", "resources-3.0.rng", "nvset-3.0.rng", ++ "nodes-3.0.rng", "options-3.0.rng", NULL }; ++ ++const char *rngs2[] = { "pacemaker-2.0.rng", "status-1.0.rng", "tags-1.3.rng", ++ "acls-2.0.rng", "fencing-1.2.rng", "constraints-1.2.rng", ++ "rule.rng", "score.rng", "resources-1.3.rng", ++ "nvset-1.3.rng", "nodes-1.3.rng", "options-1.0.rng", ++ "nvset.rng", "cib-1.2.rng", NULL }; ++ ++const char *rngs3[] = { "pacemaker-2.1.rng", "constraints-2.1.rng", NULL }; ++ ++static int ++setup(void **state) { ++ setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR, 1); ++ crm_schema_init(); ++ return 0; ++} ++ ++static int ++teardown(void **state) { ++ crm_schema_cleanup(); ++ unsetenv("PCMK_schema_directory"); ++ return 0; ++} ++ ++static void ++invalid_name(void **state) ++{ ++ GList *already_included = NULL; ++ xmlNode *parent = create_xml_node(NULL, PCMK__XA_SCHEMAS); ++ ++ pcmk__build_schema_xml_node(parent, "pacemaker-9.0", &already_included); ++ assert_null(parent->children); ++ assert_null(already_included); ++ free_xml(parent); ++} ++ ++static void ++single_schema(void **state) ++{ ++ GList *already_included = NULL; ++ xmlNode *parent = create_xml_node(NULL, PCMK__XA_SCHEMAS); ++ xmlNode *schema_node = NULL; ++ xmlNode *file_node = NULL; ++ int i = 0; ++ ++ pcmk__build_schema_xml_node(parent, "pacemaker-3.0", &already_included); ++ ++ assert_non_null(already_included); ++ assert_non_null(parent->children); ++ ++ /* Test that the result looks like this: ++ * ++ * ++ * ++ * CDATA ++ * CDATA ++ * ... ++ * ++ * ++ */ ++ schema_node = pcmk__xml_first_child(parent); ++ assert_string_equal("pacemaker-3.0", crm_element_value(schema_node, XML_ATTR_VERSION)); ++ ++ file_node = pcmk__xml_first_child(schema_node); ++ while (file_node != NULL && rngs1[i] != NULL) { ++ assert_string_equal(rngs1[i], crm_element_value(file_node, PCMK__XA_PATH)); ++ assert_int_equal(pcmk__xml_first_child(file_node)->type, XML_CDATA_SECTION_NODE); ++ ++ file_node = pcmk__xml_next(file_node); ++ i++; ++ } ++ ++ g_list_free_full(already_included, free); ++ free_xml(parent); ++} ++ ++static void ++multiple_schemas(void **state) ++{ ++ GList *already_included = NULL; ++ xmlNode *parent = create_xml_node(NULL, PCMK__XA_SCHEMAS); ++ xmlNode *schema_node = NULL; ++ xmlNode *file_node = NULL; ++ int i = 0; ++ ++ pcmk__build_schema_xml_node(parent, "pacemaker-2.0", &already_included); ++ pcmk__build_schema_xml_node(parent, "pacemaker-2.1", &already_included); ++ ++ assert_non_null(already_included); ++ assert_non_null(parent->children); ++ ++ /* Like single_schema, but make sure files aren't included multiple times ++ * when the function is called repeatedly. ++ */ ++ schema_node = pcmk__xml_first_child(parent); ++ assert_string_equal("pacemaker-2.0", crm_element_value(schema_node, XML_ATTR_VERSION)); ++ ++ file_node = pcmk__xml_first_child(schema_node); ++ while (file_node != NULL && rngs2[i] != NULL) { ++ assert_string_equal(rngs2[i], crm_element_value(file_node, PCMK__XA_PATH)); ++ assert_int_equal(pcmk__xml_first_child(file_node)->type, XML_CDATA_SECTION_NODE); ++ ++ file_node = pcmk__xml_next(file_node); ++ i++; ++ } ++ ++ schema_node = pcmk__xml_next(schema_node); ++ assert_string_equal("pacemaker-2.1", crm_element_value(schema_node, XML_ATTR_VERSION)); ++ ++ file_node = pcmk__xml_first_child(schema_node); ++ i = 0; ++ ++ while (file_node != NULL && rngs3[i] != NULL) { ++ assert_string_equal(rngs3[i], crm_element_value(file_node, PCMK__XA_PATH)); ++ assert_int_equal(pcmk__xml_first_child(file_node)->type, XML_CDATA_SECTION_NODE); ++ ++ file_node = pcmk__xml_next(file_node); ++ i++; ++ } ++ ++ g_list_free_full(already_included, free); ++ free_xml(parent); ++} ++ ++PCMK__UNIT_TEST(setup, teardown, ++ cmocka_unit_test(invalid_name), ++ cmocka_unit_test(single_schema), ++ cmocka_unit_test(multiple_schemas)) +-- +2.41.0 + +From 036eb9f59326962ed2d1f2f4af88b20755a046d5 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Thu, 14 Sep 2023 10:02:16 -0400 +Subject: [PATCH 09/15] Feature: daemons: Add a new fetch_schemas CIB command. + +This command wraps pcmk__schema_files_later_than and +pcmk__build_schema_xml_node to produce a new CIB command that takes in a +minimum schema version and returns a big XML message containing all the +schema files after that version. +--- + daemons/based/based_messages.c | 43 +++++++++++++++++++++++++++++++++ + daemons/based/based_operation.c | 1 + + daemons/based/pacemaker-based.h | 4 +++ + include/crm/cib/cib_types.h | 3 +++ + include/crm/cib/internal.h | 2 ++ + lib/cib/cib_client.c | 15 ++++++++++++ + lib/cib/cib_ops.c | 3 +++ + 7 files changed, 71 insertions(+) + +diff --git a/daemons/based/based_messages.c b/daemons/based/based_messages.c +index 35d639a89..a87d9ac2e 100644 +--- a/daemons/based/based_messages.c ++++ b/daemons/based/based_messages.c +@@ -478,3 +478,46 @@ cib_process_commit_transaction(const char *op, int options, const char *section, + } + return pcmk_rc2legacy(rc); + } ++ ++int ++cib_process_schemas(const char *op, int options, const char *section, xmlNode *req, ++ xmlNode *input, xmlNode *existing_cib, xmlNode **result_cib, ++ xmlNode **answer) ++{ ++ xmlNode *data = NULL; ++ const char *after_ver = NULL; ++ GList *schemas = NULL; ++ GList *already_included = NULL; ++ ++ *answer = create_xml_node(NULL, PCMK__XA_SCHEMAS); ++ ++ data = get_message_xml(req, F_CIB_CALLDATA); ++ if (data == NULL) { ++ crm_warn("No data specified in request"); ++ return -EPROTO; ++ } ++ ++ after_ver = crm_element_value(data, XML_ATTR_VERSION); ++ if (after_ver == NULL) { ++ crm_warn("No version specified in request"); ++ return -EPROTO; ++ } ++ ++ /* The client requested all schemas after the latest one we know about, which ++ * means the client is fully up-to-date. Return a properly formatted reply ++ * with no schemas. ++ */ ++ if (pcmk__str_eq(after_ver, xml_latest_schema(), pcmk__str_none)) { ++ return pcmk_ok; ++ } ++ ++ schemas = pcmk__schema_files_later_than(after_ver); ++ ++ for (GList *iter = schemas; iter != NULL; iter = iter->next) { ++ pcmk__build_schema_xml_node(*answer, iter->data, &already_included); ++ } ++ ++ g_list_free_full(schemas, free); ++ g_list_free_full(already_included, free); ++ return pcmk_ok; ++} +diff --git a/daemons/based/based_operation.c b/daemons/based/based_operation.c +index 736d425e3..8dd07af93 100644 +--- a/daemons/based/based_operation.c ++++ b/daemons/based/based_operation.c +@@ -35,6 +35,7 @@ static const cib__op_fn_t cib_op_functions[] = { + [cib__op_sync_all] = cib_process_sync, + [cib__op_sync_one] = cib_process_sync_one, + [cib__op_upgrade] = cib_process_upgrade_server, ++ [cib__op_schemas] = cib_process_schemas, + }; + + /*! +diff --git a/daemons/based/pacemaker-based.h b/daemons/based/pacemaker-based.h +index 33c7642c5..de24779ac 100644 +--- a/daemons/based/pacemaker-based.h ++++ b/daemons/based/pacemaker-based.h +@@ -122,6 +122,10 @@ int cib_process_commit_transaction(const char *op, int options, + const char *section, xmlNode *req, + xmlNode *input, xmlNode *existing_cib, + xmlNode **result_cib, xmlNode **answer); ++int cib_process_schemas(const char *op, int options, const char *section, ++ xmlNode *req, xmlNode *input, xmlNode *existing_cib, ++ xmlNode **result_cib, xmlNode **answer); ++ + void send_sync_request(const char *host); + int sync_our_cib(xmlNode *request, gboolean all); + +diff --git a/include/crm/cib/cib_types.h b/include/crm/cib/cib_types.h +index a803311c2..bebe770ed 100644 +--- a/include/crm/cib/cib_types.h ++++ b/include/crm/cib/cib_types.h +@@ -324,6 +324,9 @@ typedef struct cib_api_operations_s { + * processing requests + */ + void (*set_user)(cib_t *cib, const char *user); ++ ++ int (*fetch_schemas)(cib_t *cib, xmlNode **output_data, const char *after_ver, ++ int call_options); + } cib_api_operations_t; + + struct cib_s { +diff --git a/include/crm/cib/internal.h b/include/crm/cib/internal.h +index 20059ec7e..9d54d52b7 100644 +--- a/include/crm/cib/internal.h ++++ b/include/crm/cib/internal.h +@@ -32,6 +32,7 @@ + #define PCMK__CIB_REQUEST_NOOP "noop" + #define PCMK__CIB_REQUEST_SHUTDOWN "cib_shutdown_req" + #define PCMK__CIB_REQUEST_COMMIT_TRANSACT "cib_commit_transact" ++#define PCMK__CIB_REQUEST_SCHEMAS "cib_schemas" + + # define F_CIB_CLIENTID "cib_clientid" + # define F_CIB_CALLOPTS "cib_callopt" +@@ -110,6 +111,7 @@ enum cib__op_type { + cib__op_sync_all, + cib__op_sync_one, + cib__op_upgrade, ++ cib__op_schemas, + }; + + gboolean cib_diff_version_details(xmlNode * diff, int *admin_epoch, int *epoch, int *updates, +diff --git a/lib/cib/cib_client.c b/lib/cib/cib_client.c +index 32e1f83c5..a2fcabbca 100644 +--- a/lib/cib/cib_client.c ++++ b/lib/cib/cib_client.c +@@ -451,6 +451,19 @@ cib_client_end_transaction(cib_t *cib, bool commit, int call_options) + return rc; + } + ++static int ++cib_client_fetch_schemas(cib_t *cib, xmlNode **output_data, const char *after_ver, ++ int call_options) ++{ ++ xmlNode *data = create_xml_node(NULL, PCMK__XA_SCHEMA); ++ ++ crm_xml_add(data, XML_ATTR_VERSION, after_ver); ++ ++ return cib_internal_op(cib, PCMK__CIB_REQUEST_SCHEMAS, NULL, NULL, data, ++ output_data, call_options, NULL); ++ ++} ++ + static void + cib_client_set_user(cib_t *cib, const char *user) + { +@@ -736,6 +749,8 @@ cib_new_variant(void) + + new_cib->cmds->set_user = cib_client_set_user; + ++ new_cib->cmds->fetch_schemas = cib_client_fetch_schemas; ++ + return new_cib; + } + +diff --git a/lib/cib/cib_ops.c b/lib/cib/cib_ops.c +index c324304b9..2165d8af3 100644 +--- a/lib/cib/cib_ops.c ++++ b/lib/cib/cib_ops.c +@@ -127,6 +127,9 @@ static const cib__operation_t cib_ops[] = { + |cib__op_attr_writes_through + |cib__op_attr_transaction + }, ++ { ++ PCMK__CIB_REQUEST_SCHEMAS, cib__op_schemas, cib__op_attr_local ++ } + }; + + /*! +-- +2.41.0 + +From e8076b4a387ee758508f0683739b3e880f79db47 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Thu, 14 Sep 2023 15:48:31 -0400 +Subject: [PATCH 10/15] Refactor: libcrmcommon: Add + pcmk__load_schemas_from_dir. + +This just moves the bulk of crm_schema_init out into its own function, +allowing us to call it multiple times as other schema directories +appear. + +There is no similar function for unloading schemas from a directory. If +you want to do that, you'll have to use crm_schema_cleanup to unload +everything and start over. + +This function has not been tested for the possibility that the same +schema files exist in multiple directories. It is assumed that it won't +be used like that. +--- + include/crm/common/internal.h | 1 + + lib/common/schemas.c | 83 +++++++++++++++++++---------------- + 2 files changed, 45 insertions(+), 39 deletions(-) + +diff --git a/include/crm/common/internal.h b/include/crm/common/internal.h +index 318003efe..542d0a67c 100644 +--- a/include/crm/common/internal.h ++++ b/include/crm/common/internal.h +@@ -134,6 +134,7 @@ bool pcmk__procfs_has_pids(void); + void crm_schema_init(void); + void crm_schema_cleanup(void); + ++void pcmk__load_schemas_from_dir(const char *dir); + GList *pcmk__schema_files_later_than(const char *name); + void pcmk__build_schema_xml_node(xmlNode *parent, const char *name, + GList **already_included); +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index 1bcdff031..a0c844131 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -406,6 +406,49 @@ 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); ++} ++ + /*! + * \internal + * \brief Load pacemaker schemas into cache +@@ -416,50 +459,12 @@ 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 pcmk__schema_version_t zero = SCHEMA_ZERO; + + wrap_libxslt(false); + +- max = scandir(base, &namelist, schema_filter, schema_cmp_directory); +- if (max < 0) { +- crm_notice("scandir(%s) failed: %s (%d)", base, strerror(errno), errno); +- free(base); +- +- } else { +- free(base); +- for (lpc = 0; lpc < max; lpc++) { +- bool transform_expected = FALSE; +- pcmk__schema_version_t version = SCHEMA_ZERO; +- +- if (!version_from_filename(namelist[lpc]->d_name, &version)) { +- // Shouldn't be possible, but makes static analysis happy +- crm_err("Skipping schema '%s': could not parse version", +- namelist[lpc]->d_name); +- continue; +- } +- if ((lpc + 1) < max) { +- pcmk__schema_version_t next_version = SCHEMA_ZERO; +- +- if (version_from_filename(namelist[lpc+1]->d_name, &next_version) +- && (version.v[0] < next_version.v[0])) { +- transform_expected = TRUE; +- } +- } +- +- if (add_schema_by_version(&version, transform_expected) +- == ENOENT) { +- break; +- } +- } +- +- for (lpc = 0; lpc < max; lpc++) { +- free(namelist[lpc]); +- } +- free(namelist); +- } ++ pcmk__load_schemas_from_dir(base); + + // @COMPAT: Deprecated since 2.1.5 + add_schema(pcmk__schema_validator_rng, &zero, "pacemaker-next", +-- +2.41.0 + +From 5b40f0227b33edd6be65515dcc1af4eb656b78e7 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Thu, 14 Sep 2023 11:24:36 -0400 +Subject: [PATCH 11/15] Feature: daemons: Download newer schema files to a + remote executor. + +If the remote executor supports an older version of the CIB schema than +the rest of the cluster, various operations could fail due to the schema +not validating on the remote node. + +Instead, ask the CIB manager for the updated schema files, store them in +a separate directory on the remote node, and add them to the list of +known schema files. These can then be used just like the packaged +schema files. + +Note that while this is a fairly large patch, it's really just a lot of +boring IO and file management code, which requires a lot of error +handling. + +Fixes T182 +--- + daemons/execd/Makefile.am | 4 +- + daemons/execd/execd_commands.c | 6 + + daemons/execd/pacemaker-execd.h | 1 + + daemons/execd/remoted_schemas.c | 282 ++++++++++++++++++++++++++++++++ + 4 files changed, 292 insertions(+), 1 deletion(-) + create mode 100644 daemons/execd/remoted_schemas.c + +diff --git a/daemons/execd/Makefile.am b/daemons/execd/Makefile.am +index ab8544f9d..ce0e16126 100644 +--- a/daemons/execd/Makefile.am ++++ b/daemons/execd/Makefile.am +@@ -44,12 +44,14 @@ pacemaker_remoted_LDFLAGS = $(LDFLAGS_HARDENED_EXE) + + pacemaker_remoted_LDADD = $(top_builddir)/lib/fencing/libstonithd.la + pacemaker_remoted_LDADD += $(top_builddir)/lib/services/libcrmservice.la ++pacemaker_remoted_LDADD += $(top_builddir)/lib/cib/libcib.la + pacemaker_remoted_LDADD += $(top_builddir)/lib/lrmd/liblrmd.la + pacemaker_remoted_LDADD += $(top_builddir)/lib/common/libcrmcommon.la + pacemaker_remoted_SOURCES = $(pacemaker_execd_SOURCES) \ + remoted_tls.c \ + remoted_pidone.c \ +- remoted_proxy.c ++ remoted_proxy.c \ ++ remoted_schemas.c + endif + + cts_exec_helper_LDADD = $(top_builddir)/lib/pengine/libpe_status.la +diff --git a/daemons/execd/execd_commands.c b/daemons/execd/execd_commands.c +index cf4503a25..1601efb0b 100644 +--- a/daemons/execd/execd_commands.c ++++ b/daemons/execd/execd_commands.c +@@ -1493,9 +1493,15 @@ process_lrmd_signon(pcmk__client_t *client, xmlNode *request, int call_id, + if ((client->remote != NULL) + && pcmk_is_set(client->flags, + pcmk__client_tls_handshake_complete)) { ++ const char *op = crm_element_value(request, F_LRMD_OPERATION); + + // This is a remote connection from a cluster node's controller + ipc_proxy_add_provider(client); ++ ++ /* If this was a register operation, also ask for new schema files. */ ++ if (pcmk__str_eq(op, CRM_OP_REGISTER, pcmk__str_none)) { ++ remoted_request_cib_schema_files(); ++ } + } else { + rc = -EACCES; + } +diff --git a/daemons/execd/pacemaker-execd.h b/daemons/execd/pacemaker-execd.h +index 9c1d173f5..6fb8ef440 100644 +--- a/daemons/execd/pacemaker-execd.h ++++ b/daemons/execd/pacemaker-execd.h +@@ -101,6 +101,7 @@ void ipc_proxy_forward_client(pcmk__client_t *client, xmlNode *xml); + pcmk__client_t *ipc_proxy_get_provider(void); + int ipc_proxy_shutdown_req(pcmk__client_t *ipc_proxy); + void remoted_spawn_pidone(int argc, char **argv, char **envp); ++void remoted_request_cib_schema_files(void); + #endif + + int process_lrmd_alert_exec(pcmk__client_t *client, uint32_t id, +diff --git a/daemons/execd/remoted_schemas.c b/daemons/execd/remoted_schemas.c +new file mode 100644 +index 000000000..d501fa495 +--- /dev/null ++++ b/daemons/execd/remoted_schemas.c +@@ -0,0 +1,282 @@ ++/* ++ * Copyright 2023 the Pacemaker project contributors ++ * ++ * The version control history for this file may have further details. ++ * ++ * This source code is licensed under the GNU 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 "pacemaker-execd.h" ++ ++static pid_t schema_fetch_pid = 0; ++ ++static int ++rm_files(const char *pathname, const struct stat *sbuf, int type, struct FTW *ftwb) ++{ ++ /* Don't delete PCMK__REMOTE_SCHEMA_DIR . */ ++ if (ftwb->level == 0) { ++ return 0; ++ } ++ ++ if (remove(pathname) != 0) { ++ int rc = errno; ++ crm_err("Could not remove %s: %s", pathname, pcmk_rc_str(rc)); ++ return -1; ++ } ++ ++ return 0; ++} ++ ++static void ++clean_up_extra_schema_files(void) ++{ ++ const char *remote_schema_dir = pcmk__remote_schema_dir(); ++ struct stat sb; ++ int rc; ++ ++ rc = stat(remote_schema_dir, &sb); ++ ++ if (rc == -1) { ++ if (errno == ENOENT) { ++ /* If the directory doesn't exist, try to make it first. */ ++ if (mkdir(remote_schema_dir, 0755) != 0) { ++ rc = errno; ++ crm_err("Could not create directory for schemas: %s", ++ pcmk_rc_str(rc)); ++ } ++ ++ } else { ++ rc = errno; ++ crm_err("Could not create directory for schemas: %s", ++ pcmk_rc_str(rc)); ++ } ++ ++ } else if (!S_ISDIR(sb.st_mode)) { ++ /* If something exists with the same name that's not a directory, that's ++ * an error. ++ */ ++ crm_err("%s already exists but is not a directory", remote_schema_dir); ++ ++ } else { ++ /* It's a directory - clear it out so we can download potentially new ++ * schema files. ++ */ ++ rc = nftw(remote_schema_dir, rm_files, 10, FTW_DEPTH|FTW_MOUNT|FTW_PHYS); ++ ++ if (rc != 0) { ++ crm_err("Could not remove %s: %s", remote_schema_dir, pcmk_rc_str(rc)); ++ } ++ } ++} ++ ++static void ++write_extra_schema_file(xmlNode *xml, void *user_data) ++{ ++ const char *remote_schema_dir = pcmk__remote_schema_dir(); ++ const char *file = NULL; ++ char *path = NULL; ++ int rc; ++ ++ file = crm_element_value(xml, PCMK__XA_PATH); ++ if (file == NULL) { ++ crm_warn("No destination path given in schema request"); ++ return; ++ } ++ ++ path = crm_strdup_printf("%s/%s", remote_schema_dir, file); ++ ++ /* The schema is a CDATA node, which is a child of the node. Traverse ++ * all children and look for the first CDATA child. There can't be more than ++ * one because we only have one file attribute on the parent. ++ */ ++ for (xmlNode *child = xml->children; child != NULL; child = child->next) { ++ FILE *stream = NULL; ++ ++ if (child->type != XML_CDATA_SECTION_NODE) { ++ continue; ++ } ++ ++ stream = fopen(path, "w+"); ++ if (stream == NULL) { ++ crm_warn("Could not write schema file %s: %s", path, strerror(errno)); ++ } else { ++ rc = fprintf(stream, "%s", child->content); ++ ++ if (rc < 0) { ++ crm_warn("Could not write schema file %s: %s", path, strerror(errno)); ++ } ++ ++ fclose(stream); ++ } ++ ++ break; ++ } ++ ++ free(path); ++} ++ ++static void ++get_schema_files(void) ++{ ++ int rc = pcmk_rc_ok; ++ cib_t *cib = NULL; ++ xmlNode *reply; ++ ++ cib = cib_new(); ++ if (cib == NULL) { ++ _exit(ENOTCONN); ++ } ++ ++ rc = cib->cmds->signon(cib, crm_system_name, cib_query); ++ if (rc != pcmk_ok) { ++ crm_err("Could not connect to the CIB manager: %s", pcmk_strerror(rc)); ++ _exit(pcmk_rc2exitc(rc)); ++ } ++ ++ rc = cib->cmds->fetch_schemas(cib, &reply, xml_latest_schema(), cib_sync_call); ++ if (rc != pcmk_ok) { ++ crm_err("Could not get schema files: %s", pcmk_strerror(rc)); ++ rc = pcmk_legacy2rc(rc); ++ ++ } else if (reply->children != NULL) { ++ /* The returned document looks something like this: ++ * ++ * ++ * ++ * ++ * ++ * ++ * ++ * ++ * ... ++ * ++ * ++ * ++ * ... ++ * ++ * ++ * ++ * ++ * All the and tags are really just there for organizing ++ * the XML a little better. What we really care about are the nodes, ++ * and specifically the path attributes and the CDATA children (not shown) ++ * of each. We can use an xpath query to reach down and get all the ++ * nodes at once. ++ * ++ * If we already have the latest schema version, or we asked for one later ++ * than what the cluster supports, we'll get back an empty node, ++ * so all this will continue to work. It just won't do anything. ++ */ ++ crm_foreach_xpath_result(reply, "//" PCMK__XA_FILE, write_extra_schema_file, NULL); ++ } ++ ++ cib__clean_up_connection(&cib); ++ _exit(pcmk_rc2exitc(rc)); ++} ++ ++/* Load any additional schema files when the child is finished fetching and ++ * saving them to disk. ++ */ ++static void ++get_schema_files_complete(mainloop_child_t *p, pid_t pid, int core, int signo, int exitcode) ++{ ++ const char *errmsg = "Could not load additional schema files"; ++ ++ if ((signo == 0) && (exitcode == 0)) { ++ const char *remote_schema_dir = pcmk__remote_schema_dir(); ++ ++ /* Don't just crm_schema_init here because that will load the base ++ * schemas again too. Instead just load the things we fetched. ++ */ ++ pcmk__load_schemas_from_dir(remote_schema_dir); ++ crm_info("Fetching extra schema files completed successfully"); ++ ++ } else { ++ if (signo == 0) { ++ crm_err("%s: process %d exited %d", errmsg, (int) pid, exitcode); ++ ++ } else { ++ crm_err("%s: process %d terminated with signal %d (%s)%s", ++ errmsg, (int) pid, signo, strsignal(signo), ++ (core? " and dumped core" : "")); ++ } ++ ++ /* Clean up any incomplete schema data we might have been downloading when ++ * the process timed out or crashed. We don't need to do any extra cleanup ++ * because we never loaded the extra schemas, and we don't need to call ++ * crm_schema_init because that was called in remoted_request_cib_schema_files ++ * before this function. ++ */ ++ clean_up_extra_schema_files(); ++ } ++} ++ ++void ++remoted_request_cib_schema_files(void) ++{ ++ pid_t pid; ++ int rc; ++ ++ /* If a previous schema-fetch process is still running when we're called ++ * again, it's hung. Attempt to kill it before cleaning up the extra ++ * directory. ++ */ ++ if (schema_fetch_pid != 0) { ++ if (mainloop_child_kill(schema_fetch_pid) == FALSE) { ++ crm_warn("Unable to kill pre-existing schema-fetch process"); ++ return; ++ } ++ ++ schema_fetch_pid = 0; ++ } ++ ++ /* Clean up any extra schema files we downloaded from a previous cluster ++ * connection. After the files are gone, we need to wipe them from ++ * known_schemas, but there's no opposite operation for add_schema(). ++ * ++ * Instead, unload all the schemas. This means we'll also forget about all ++ * the installed schemas as well, which means that xml_latest_schema() will ++ * fail. So we need to load the base schemas right now. ++ */ ++ clean_up_extra_schema_files(); ++ crm_schema_cleanup(); ++ crm_schema_init(); ++ ++ crm_info("Fetching extra schema files from cluster"); ++ pid = fork(); ++ ++ switch (pid) { ++ case -1: { ++ rc = errno; ++ crm_warn("Could not spawn process to get schema files: %s", pcmk_rc_str(rc)); ++ break; ++ } ++ ++ case 0: ++ /* child */ ++ get_schema_files(); ++ break; ++ ++ default: ++ /* parent */ ++ schema_fetch_pid = pid; ++ mainloop_child_add_with_flags(pid, 5 * 60 * 1000, "schema-fetch", NULL, ++ mainloop_leave_pid_group, ++ get_schema_files_complete); ++ break; ++ } ++} +-- +2.41.0 + +From b05fef32cf1f9063e01db5108f95386be329d778 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Wed, 29 Nov 2023 10:04:57 -0500 +Subject: [PATCH 12/15] Feature: libcrmcommon: Load additional schema files in + crm_schema_init. + +If the /var/lib/pacemaker/schemas directory exists, load any extra +schemas from it when we init. This makes them available for command +line programs to use. +--- + lib/common/schemas.c | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index a0c844131..68d79cfc7 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -459,12 +459,14 @@ pcmk__load_schemas_from_dir(const char *dir) + 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", +-- +2.41.0 + +From 7454a8400238301848cc6694f5413fdb19d5834e Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Fri, 29 Sep 2023 10:41:51 -0400 +Subject: [PATCH 13/15] Refactor: daemons: Remove redundant includes from + remoted_tls.c. + +--- + daemons/execd/remoted_tls.c | 2 -- + 1 file changed, 2 deletions(-) + +diff --git a/daemons/execd/remoted_tls.c b/daemons/execd/remoted_tls.c +index 23a2dcf45..978edfdd3 100644 +--- a/daemons/execd/remoted_tls.c ++++ b/daemons/execd/remoted_tls.c +@@ -12,8 +12,6 @@ + #include + #include + +-#include +-#include + #include + #include + #include +-- +2.41.0 + +From c6b5f73a013515d6acd818d348d011e38f3d8e0e Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Fri, 27 Oct 2023 15:23:41 -0400 +Subject: [PATCH 14/15] Refactor: daemons: Keep XML schemas sorted. + +Now that we can add schemas multiple times (including after next/none +have been added in the initial set), we've broken the assumption that +schemas are always sorted by scandir. + +This introduces a function that can be called wherever to keep them +sorted. It should be called pretty much whenever +pcmk__load_schemas_from_dir is called. +--- + daemons/execd/remoted_schemas.c | 1 + + include/crm/common/xml_internal.h | 1 + + lib/common/schemas.c | 40 +++++++++++++++++++++++++++++++ + 3 files changed, 42 insertions(+) + +diff --git a/daemons/execd/remoted_schemas.c b/daemons/execd/remoted_schemas.c +index d501fa495..eed43dfa9 100644 +--- a/daemons/execd/remoted_schemas.c ++++ b/daemons/execd/remoted_schemas.c +@@ -203,6 +203,7 @@ get_schema_files_complete(mainloop_child_t *p, pid_t pid, int core, int signo, i + * schemas again too. Instead just load the things we fetched. + */ + pcmk__load_schemas_from_dir(remote_schema_dir); ++ pcmk__sort_schemas(); + crm_info("Fetching extra schema files completed successfully"); + + } else { +diff --git a/include/crm/common/xml_internal.h b/include/crm/common/xml_internal.h +index cb27ec6b2..9b50f4e72 100644 +--- a/include/crm/common/xml_internal.h ++++ b/include/crm/common/xml_internal.h +@@ -448,5 +448,6 @@ gboolean pcmk__validate_xml(xmlNode *xml_blob, const char *validation, + + void pcmk__log_known_schemas(void); + const char *pcmk__remote_schema_dir(void); ++void pcmk__sort_schemas(void); + + #endif // PCMK__XML_INTERNAL__H +diff --git a/lib/common/schemas.c b/lib/common/schemas.c +index 68d79cfc7..fd6202f62 100644 +--- a/lib/common/schemas.c ++++ b/lib/common/schemas.c +@@ -449,6 +449,41 @@ pcmk__load_schemas_from_dir(const char *dir) + 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, "none", pcmk__str_none)) { ++ return -1; ++ } else { ++ return 1; ++ } ++ } else if (pcmk__str_eq(schema_a->name, "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 +@@ -474,6 +509,11 @@ crm_schema_init(void) + + 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 +-- +2.41.0 + +From d6a535ca43db202bd01a2e085b58722ed1abcdb0 Mon Sep 17 00:00:00 2001 +From: Chris Lumens +Date: Mon, 30 Oct 2023 12:59:13 -0400 +Subject: [PATCH 15/15] Feature: daemons: Only ask for schemas if supported by + the server + +We can use LRMD_PROTOCOL_VERSION in the handshake to determine what the +server supports, similar to what is being done in attrd. Add a macro to +compare the version we were given with the known minimum version that +supports the schema transfer command. + +Additionally, introduce LRMD_COMPATIBLE_PROTOCOL which is just the major +version number required for the connection to succeed. This gets rid of +the need for LRMD_MIN_PROTOCOL_VERSION, which can now be deprecated. + +And then since I wasn't sure compare_version would work if you give it a +full version number and just a major version, add a unit test for that. +--- + daemons/execd/execd_commands.c | 11 +++++---- + include/crm/lrmd.h | 23 +++++++++++++++---- + lib/common/tests/utils/compare_version_test.c | 5 +++- + 3 files changed, 30 insertions(+), 9 deletions(-) + +diff --git a/daemons/execd/execd_commands.c b/daemons/execd/execd_commands.c +index 1601efb0b..4ec4d03d6 100644 +--- a/daemons/execd/execd_commands.c ++++ b/daemons/execd/execd_commands.c +@@ -1482,9 +1482,9 @@ process_lrmd_signon(pcmk__client_t *client, xmlNode *request, int call_id, + const char *protocol_version = crm_element_value(request, F_LRMD_PROTOCOL_VERSION); + const char *start_state = pcmk__env_option(PCMK__ENV_NODE_START_STATE); + +- if (compare_version(protocol_version, LRMD_MIN_PROTOCOL_VERSION) < 0) { ++ if (compare_version(protocol_version, LRMD_COMPATIBLE_PROTOCOL) < 0) { + crm_err("Cluster API version must be greater than or equal to %s, not %s", +- LRMD_MIN_PROTOCOL_VERSION, protocol_version); ++ LRMD_COMPATIBLE_PROTOCOL, protocol_version); + rc = -EPROTO; + } + +@@ -1498,8 +1498,11 @@ process_lrmd_signon(pcmk__client_t *client, xmlNode *request, int call_id, + // This is a remote connection from a cluster node's controller + ipc_proxy_add_provider(client); + +- /* If this was a register operation, also ask for new schema files. */ +- if (pcmk__str_eq(op, CRM_OP_REGISTER, pcmk__str_none)) { ++ /* If this was a register operation, also ask for new schema files but ++ * only if it's supported by the protocol version. ++ */ ++ if (pcmk__str_eq(op, CRM_OP_REGISTER, pcmk__str_none) && ++ LRMD_SUPPORTS_SCHEMA_XFER(protocol_version)) { + remoted_request_cib_schema_files(); + } + } else { +diff --git a/include/crm/lrmd.h b/include/crm/lrmd.h +index 0c5a40b62..948a3b1fc 100644 +--- a/include/crm/lrmd.h ++++ b/include/crm/lrmd.h +@@ -33,12 +33,27 @@ typedef struct lrmd_key_value_s { + struct lrmd_key_value_s *next; + } lrmd_key_value_t; + +-/* This should be bumped every time there is an incompatible change that +- * prevents older clients from connecting to this version of the server. ++/* The major version should be bumped every time there is an incompatible ++ * change that prevents older clients from connecting to this version of ++ * the server. The minor version indicates feature support. ++ * ++ * Protocol Pacemaker Significant changes ++ * -------- --------- ------------------- ++ * 1.2 2.1.7 PCMK__CIB_REQUEST_SCHEMAS + */ +-#define LRMD_PROTOCOL_VERSION "1.1" ++#define LRMD_PROTOCOL_VERSION "1.2" ++ ++#define LRMD_SUPPORTS_SCHEMA_XFER(x) (compare_version((x), "1.2") >= 0) + +-/* This is the version that the client version will actually be compared ++/* The major protocol version the client and server both need to support for ++ * the connection to be successful. This should only ever be the major ++ * version - not a complete version number. ++ */ ++#define LRMD_COMPATIBLE_PROTOCOL "1" ++ ++/* \deprecated Do not use (will be removed in a future release) ++ * ++ * This is the version that the client version will actually be compared + * against. This should be identical to LRMD_PROTOCOL_VERSION. However, we + * accidentally bumped LRMD_PROTOCOL_VERSION in 6424a647 (1.1.15) when we didn't + * need to, so for now it's different. If we ever have a truly incompatible +diff --git a/lib/common/tests/utils/compare_version_test.c b/lib/common/tests/utils/compare_version_test.c +index 35ebb63c6..d191f4abb 100644 +--- a/lib/common/tests/utils/compare_version_test.c ++++ b/lib/common/tests/utils/compare_version_test.c +@@ -1,5 +1,5 @@ + /* +- * Copyright 2022 the Pacemaker project contributors ++ * Copyright 2022-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * +@@ -46,6 +46,9 @@ shorter_versions(void **state) + { + assert_int_equal(compare_version("1.0", "1.0.1"), -1); + assert_int_equal(compare_version("1.0.1", "1.0"), 1); ++ assert_int_equal(compare_version("1.0", "1"), 0); ++ assert_int_equal(compare_version("1", "1.2"), -1); ++ assert_int_equal(compare_version("1.2", "1"), 1); + } + + PCMK__UNIT_TEST(NULL, NULL, +-- +2.41.0 + diff --git a/pacemaker.spec b/pacemaker.spec index 0b782a7..a0ed7c5 100644 --- a/pacemaker.spec +++ b/pacemaker.spec @@ -36,10 +36,10 @@ ## can be incremented to build packages reliably considered "newer" ## than previously built packages with the same pcmkversion) %global pcmkversion 2.1.7 -%global specversion 1 +%global specversion 2 ## Upstream commit (full commit ID, abbreviated commit ID, or tag) to build -%global commit 7534cc50aefbf3c161c7ed258daa1019a94d5079 +%global commit c858c13cb79431b5c8e5dda3ca44dd305fce946c ## Since git v2.11, the extent of abbreviation is autoscaled by default ## (used to be constant of 7), so we need to convey it for non-tags, too. @@ -153,7 +153,6 @@ %global pkgname_procps procps %global pkgname_glue_libs libglue %global pkgname_pcmk_libs lib%{name}3 -%global hacluster_id 90 %else %global pkgname_libtool_devel libtool-ltdl-devel %global pkgname_libtool_devel_arch libtool-ltdl-devel%{?_isa} @@ -165,7 +164,6 @@ %global pkgname_procps procps-ng %global pkgname_glue_libs cluster-glue-libs %global pkgname_pcmk_libs %{name}-libs -%global hacluster_id 189 %endif ## Distro-specific configuration choices @@ -246,9 +244,11 @@ Url: https://www.clusterlabs.org/ # You can use "spectool -s 0 pacemaker.spec" (rpmdevtools) to show final URL. Source0: https://codeload.github.com/%{github_owner}/%{name}/tar.gz/%{archive_github_url} Source1: https://codeload.github.com/%{github_owner}/%{nagios_name}/tar.gz/%{nagios_archive_github_url} +Source2: pacemaker.sysusers # upstream commits -#Patch001: 001-xxxx.patch +Patch001: 001-schema-glib.patch +Patch002: 002-schema-transfer.patch Requires: resource-agents Requires: %{pkgname_pcmk_libs}%{?_isa} = %{version}-%{release} @@ -331,6 +331,10 @@ BuildRequires: inkscape BuildRequires: %{python_name}-sphinx %endif +# Creation of Users / Groups +BuildRequires: systemd-rpm-macros +%{?sysusers_requires_compat} + # Booth requires this Provides: pacemaker-ticket-support = 2.0 @@ -616,6 +620,8 @@ find %{buildroot} -name '*.la' -type f -print0 | xargs -0 rm -f rm -f %{buildroot}/%{_sbindir}/fence_legacy rm -f %{buildroot}/%{_mandir}/man8/fence_legacy.* +install -p -D -m 0644 %{SOURCE2} %{buildroot}%{_sysusersdir}/pacemaker.conf + # For now, don't package the servicelog-related binaries built only for # ppc64le when certain dependencies are installed. If they get more exercise by # advanced users, we can reconsider. @@ -691,10 +697,7 @@ fi %systemd_postun_with_restart crm_mon.service %pre -n %{pkgname_pcmk_libs} -# @TODO Use sysusers.d: -# https://fedoraproject.org/wiki/Changes/Adopting_sysusers.d_format -getent group %{gname} >/dev/null || groupadd -r %{gname} -g %{hacluster_id} -getent passwd %{uname} >/dev/null || useradd -r -g %{gname} -u %{hacluster_id} -s /sbin/nologin -c "cluster user" %{uname} +%sysusers_create_compat %{SOURCE2} exit 0 %ldconfig_scriptlets -n %{pkgname_pcmk_libs} @@ -815,6 +818,7 @@ exit 0 %dir %attr (770, %{uname}, %{gname}) %{_var}/log/pacemaker/bundles %files -n %{pkgname_pcmk_libs} %{?with_nls:-f %{name}.lang} +%{_sysusersdir}/pacemaker.conf %{_libdir}/libcib.so.* %{_libdir}/liblrmd.so.* %{_libdir}/libcrmservice.so.* @@ -903,6 +907,13 @@ exit 0 %license %{nagios_name}-%{nagios_hash}/COPYING %changelog +* Wed Dec 13 2023 Chris Lumens - 2.1.7-2 +- Rebase on upstream 2.1.7-rc4 release +- Use systemd-sysusers to create user/group +- Pacemaker Remote nodes can validate against later schema versions +- Related: RHEL-17225 +- Resolves: RHEL-7665 + * Wed Nov 22 2023 Chris Lumens - 2.1.7-1 - Rebase on upstream 2.1.7-rc2 release - Resolves: RHEL-7682 diff --git a/pacemaker.sysusers b/pacemaker.sysusers new file mode 100644 index 0000000..daadb96 --- /dev/null +++ b/pacemaker.sysusers @@ -0,0 +1,3 @@ +#Type Name ID GECOS Home directory Shell +g haclient 189 +u hacluster 189:haclient "cluster user" /var/lib/pacemaker /sbin/nologin diff --git a/sources b/sources index c018296..35a9f36 100644 --- a/sources +++ b/sources @@ -1,2 +1,2 @@ SHA512 (nagios-agents-metadata-105ab8a7b2c16b9a29cf1c1596b80136eeef332b.tar.gz) = 11ddeb48a4929e7642b6dfa9c7962aa1d7a1af1c569830f55ed6cd6773abac13377317327bc1db8411c8077884f83f81cc54d746c834b63a99fa6dc219b5caad -SHA512 (pacemaker-7534cc50a.tar.gz) = 70703c68e3249a2fcc4d88d53b78a2a990163ec59b32fb4c5c6993cb53943900279017f529f9bf29a06e59fd67bb2d631719ed5a41848b01a54497caf90e0b20 +SHA512 (pacemaker-c858c13cb.tar.gz) = 67a6669bb42dd7adcde2a99155086746a95a37262a14b6ca2c4f8f2706ba572ef2a3715cc40bbeb6988ae6a8979ed8ce208c890934ea0eb5d8448d84510cf3ce