Line data Source code
1 : /*
2 : * Copyright 2004-2024 the Pacemaker project contributors
3 : *
4 : * The version control history for this file may have further details.
5 : *
6 : * This source code is licensed under the GNU Lesser General Public License
7 : * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
8 : */
9 :
10 : #include <crm_internal.h>
11 :
12 : #include <stdio.h>
13 : #include <string.h>
14 : #include <dirent.h>
15 : #include <errno.h>
16 : #include <sys/stat.h>
17 : #include <stdarg.h>
18 :
19 : #include <libxml/relaxng.h>
20 : #include <libxslt/xslt.h>
21 : #include <libxslt/transform.h>
22 : #include <libxslt/security.h>
23 : #include <libxslt/xsltutils.h>
24 :
25 : #include <crm/common/xml.h>
26 : #include <crm/common/xml_internal.h> /* PCMK__XML_LOG_BASE */
27 :
28 : #include "crmcommon_private.h"
29 :
30 : #define SCHEMA_ZERO { .v = { 0, 0 } }
31 :
32 : #define schema_strdup_printf(prefix, version, suffix) \
33 : crm_strdup_printf(prefix "%u.%u" suffix, (version).v[0], (version).v[1])
34 :
35 : typedef struct {
36 : xmlRelaxNGPtr rng;
37 : xmlRelaxNGValidCtxtPtr valid;
38 : xmlRelaxNGParserCtxtPtr parser;
39 : } relaxng_ctx_cache_t;
40 :
41 : static GList *known_schemas = NULL;
42 : static bool initialized = false;
43 : static bool silent_logging = FALSE;
44 :
45 : static void G_GNUC_PRINTF(2, 3)
46 0 : xml_log(int priority, const char *fmt, ...)
47 : {
48 : va_list ap;
49 :
50 0 : va_start(ap, fmt);
51 0 : if (silent_logging == FALSE) {
52 : /* XXX should not this enable dechunking as well? */
53 0 : PCMK__XML_LOG_BASE(priority, FALSE, 0, NULL, fmt, ap);
54 : }
55 0 : va_end(ap);
56 0 : }
57 :
58 : static int
59 4 : xml_latest_schema_index(void)
60 : {
61 : /* This function assumes that crm_schema_init() has been called beforehand,
62 : * so we have at least three schemas (one real schema, the "pacemaker-next"
63 : * schema, and the "none" schema).
64 : *
65 : * @COMPAT: pacemaker-next is deprecated since 2.1.5 and none since 2.1.8.
66 : * Update this when we drop those.
67 : */
68 4 : return g_list_length(known_schemas) - 3;
69 : }
70 :
71 : /*!
72 : * \internal
73 : * \brief Return the schema entry of the highest-versioned schema
74 : *
75 : * \return Schema entry of highest-versioned schema (or NULL on error)
76 : */
77 : static GList *
78 3 : get_highest_schema(void)
79 : {
80 : /* The highest numerically versioned schema is the one before pacemaker-next
81 : *
82 : * @COMPAT pacemaker-next is deprecated since 2.1.5
83 : */
84 3 : GList *entry = pcmk__get_schema("pacemaker-next");
85 :
86 3 : CRM_ASSERT((entry != NULL) && (entry->prev != NULL));
87 3 : return entry->prev;
88 : }
89 :
90 : /*!
91 : * \internal
92 : * \brief Return the name of the highest-versioned schema
93 : *
94 : * \return Name of highest-versioned schema (or NULL on error)
95 : */
96 : const char *
97 0 : pcmk__highest_schema_name(void)
98 : {
99 0 : GList *entry = get_highest_schema();
100 :
101 0 : return ((pcmk__schema_t *)(entry->data))->name;
102 : }
103 :
104 : /*!
105 : * \internal
106 : * \brief Find first entry of highest major schema version series
107 : *
108 : * \return Schema entry of first schema with highest major version
109 : */
110 : GList *
111 3 : pcmk__find_x_0_schema(void)
112 : {
113 : #if defined(PCMK__UNIT_TESTING)
114 : /* If we're unit testing, this can't be static because it'll stick
115 : * around from one test run to the next. It needs to be cleared out
116 : * every time.
117 : */
118 3 : GList *x_0_entry = NULL;
119 : #else
120 : static GList *x_0_entry = NULL;
121 : #endif
122 :
123 3 : pcmk__schema_t *highest_schema = NULL;
124 :
125 3 : if (x_0_entry != NULL) {
126 0 : return x_0_entry;
127 : }
128 3 : x_0_entry = get_highest_schema();
129 3 : highest_schema = x_0_entry->data;
130 :
131 22 : for (GList *iter = x_0_entry->prev; iter != NULL; iter = iter->prev) {
132 22 : pcmk__schema_t *schema = iter->data;
133 :
134 : /* We've found a schema in an older major version series. Return
135 : * the index of the first one in the same major version series as
136 : * the highest schema.
137 : */
138 22 : if (schema->version.v[0] < highest_schema->version.v[0]) {
139 3 : x_0_entry = iter->next;
140 3 : break;
141 : }
142 :
143 : /* We're out of list to examine. This probably means there was only
144 : * one major version series, so return the first schema entry.
145 : */
146 19 : if (iter->prev == NULL) {
147 0 : x_0_entry = known_schemas->data;
148 0 : break;
149 : }
150 : }
151 3 : return x_0_entry;
152 : }
153 :
154 : static inline bool
155 11058 : version_from_filename(const char *filename, pcmk__schema_version_t *version)
156 : {
157 11058 : if (pcmk__ends_with(filename, ".rng")) {
158 11053 : return sscanf(filename, "pacemaker-%hhu.%hhu.rng", &(version->v[0]), &(version->v[1])) == 2;
159 : } else {
160 5 : return sscanf(filename, "pacemaker-%hhu.%hhu", &(version->v[0]), &(version->v[1])) == 2;
161 : }
162 : }
163 :
164 : static int
165 0 : schema_filter(const struct dirent *a)
166 : {
167 0 : int rc = 0;
168 0 : pcmk__schema_version_t version = SCHEMA_ZERO;
169 :
170 0 : if (strstr(a->d_name, "pacemaker-") != a->d_name) {
171 : /* crm_trace("%s - wrong prefix", a->d_name); */
172 :
173 0 : } else if (!pcmk__ends_with_ext(a->d_name, ".rng")) {
174 : /* crm_trace("%s - wrong suffix", a->d_name); */
175 :
176 0 : } else if (!version_from_filename(a->d_name, &version)) {
177 : /* crm_trace("%s - wrong format", a->d_name); */
178 :
179 : } else {
180 : /* crm_debug("%s - candidate", a->d_name); */
181 0 : rc = 1;
182 : }
183 :
184 0 : return rc;
185 : }
186 :
187 : static int
188 6916 : schema_cmp(pcmk__schema_version_t a_version, pcmk__schema_version_t b_version)
189 : {
190 11504 : for (int i = 0; i < 2; ++i) {
191 11501 : if (a_version.v[i] < b_version.v[i]) {
192 1768 : return -1;
193 9733 : } else if (a_version.v[i] > b_version.v[i]) {
194 5145 : return 1;
195 : }
196 : }
197 3 : return 0;
198 : }
199 :
200 : static int
201 0 : schema_cmp_directory(const struct dirent **a, const struct dirent **b)
202 : {
203 0 : pcmk__schema_version_t a_version = SCHEMA_ZERO;
204 0 : pcmk__schema_version_t b_version = SCHEMA_ZERO;
205 :
206 0 : if (!version_from_filename(a[0]->d_name, &a_version)
207 0 : || !version_from_filename(b[0]->d_name, &b_version)) {
208 : // Shouldn't be possible, but makes static analysis happy
209 0 : return 0;
210 : }
211 :
212 0 : return schema_cmp(a_version, b_version);
213 : }
214 :
215 : /*!
216 : * \internal
217 : * \brief Add given schema + auxiliary data to internal bookkeeping.
218 : *
219 : * \note When providing \p version, should not be called directly but
220 : * through \c add_schema_by_version.
221 : */
222 : static void
223 1373 : add_schema(enum pcmk__schema_validator validator, const pcmk__schema_version_t *version,
224 : const char *name, const char *transform,
225 : const char *transform_enter, bool transform_onleave)
226 : {
227 1373 : pcmk__schema_t *schema = NULL;
228 :
229 1373 : schema = pcmk__assert_alloc(1, sizeof(pcmk__schema_t));
230 :
231 1373 : schema->validator = validator;
232 1373 : schema->version.v[0] = version->v[0];
233 1373 : schema->version.v[1] = version->v[1];
234 1373 : schema->transform_onleave = transform_onleave;
235 : // schema->schema_index is set after all schemas are loaded and sorted
236 :
237 1373 : if (version->v[0] || version->v[1]) {
238 1169 : schema->name = schema_strdup_printf("pacemaker-", *version, "");
239 : } else {
240 204 : schema->name = pcmk__str_copy(name);
241 : }
242 :
243 1373 : if (transform) {
244 98 : schema->transform = pcmk__str_copy(transform);
245 : }
246 :
247 1373 : if (transform_enter) {
248 48 : schema->transform_enter = pcmk__str_copy(transform_enter);
249 : }
250 :
251 1373 : known_schemas = g_list_prepend(known_schemas, schema);
252 1373 : }
253 :
254 : /*!
255 : * \internal
256 : * \brief Add version-specified schema + auxiliary data to internal bookkeeping.
257 : * \return Standard Pacemaker return value (the only possible values are
258 : * \c ENOENT when no upgrade schema is associated, or \c pcmk_rc_ok otherwise.
259 : *
260 : * \note There's no reliance on the particular order of schemas entering here.
261 : *
262 : * \par A bit of theory
263 : * We track 3 XSLT stylesheets that differ per usage:
264 : * - "upgrade":
265 : * . sparsely spread over the sequence of all available schemas,
266 : * as they are only relevant when major version of the schema
267 : * is getting bumped -- in that case, it MUST be set
268 : * . name convention: upgrade-X.Y.xsl
269 : * - "upgrade-enter":
270 : * . may only accompany "upgrade" occurrence, but doesn't need to
271 : * be present anytime such one is, i.e., it MAY not be set when
272 : * "upgrade" is
273 : * . name convention: upgrade-X.Y-enter.xsl,
274 : * when not present: upgrade-enter.xsl
275 : * - "upgrade-leave":
276 : * . like "upgrade-enter", but SHOULD be present whenever
277 : * "upgrade-enter" is (and vice versa, but that's only
278 : * to prevent confusion based on observing the files,
279 : * it would get ignored regardless)
280 : * . name convention: (see "upgrade-enter")
281 : */
282 : static int
283 1169 : add_schema_by_version(const pcmk__schema_version_t *version, bool transform_expected)
284 : {
285 1169 : bool transform_onleave = FALSE;
286 1169 : int rc = pcmk_rc_ok;
287 : struct stat s;
288 1169 : char *xslt = NULL,
289 1169 : *transform_upgrade = NULL,
290 1169 : *transform_enter = NULL;
291 :
292 : /* prologue for further transform_expected handling */
293 1169 : if (transform_expected) {
294 : /* check if there's suitable "upgrade" stylesheet */
295 98 : transform_upgrade = schema_strdup_printf("upgrade-", *version, );
296 98 : xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
297 : transform_upgrade);
298 : }
299 :
300 1169 : if (!transform_expected) {
301 : /* jump directly to the end */
302 :
303 98 : } else if (stat(xslt, &s) == 0) {
304 : /* perhaps there's also a targeted "upgrade-enter" stylesheet */
305 98 : transform_enter = schema_strdup_printf("upgrade-", *version, "-enter");
306 98 : free(xslt);
307 98 : xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
308 : transform_enter);
309 98 : if (stat(xslt, &s) != 0) {
310 : /* or initially, at least a generic one */
311 50 : crm_debug("Upgrade-enter transform %s.xsl not found", xslt);
312 50 : free(xslt);
313 50 : free(transform_enter);
314 50 : transform_enter = strdup("upgrade-enter");
315 50 : xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
316 : transform_enter);
317 50 : if (stat(xslt, &s) != 0) {
318 50 : crm_debug("Upgrade-enter transform %s.xsl not found, either", xslt);
319 50 : free(xslt);
320 50 : xslt = NULL;
321 : }
322 : }
323 : /* xslt contains full path to "upgrade-enter" stylesheet */
324 98 : if (xslt != NULL) {
325 : /* then there should be "upgrade-leave" counterpart (enter->leave) */
326 48 : memcpy(strrchr(xslt, '-') + 1, "leave", sizeof("leave") - 1);
327 48 : transform_onleave = (stat(xslt, &s) == 0);
328 48 : free(xslt);
329 : } else {
330 50 : free(transform_enter);
331 50 : transform_enter = NULL;
332 : }
333 :
334 : } else {
335 0 : crm_err("Upgrade transform %s not found", xslt);
336 0 : free(xslt);
337 0 : free(transform_upgrade);
338 0 : transform_upgrade = NULL;
339 0 : rc = ENOENT;
340 : }
341 :
342 1169 : add_schema(pcmk__schema_validator_rng, version, NULL,
343 : transform_upgrade, transform_enter, transform_onleave);
344 :
345 1169 : free(transform_upgrade);
346 1169 : free(transform_enter);
347 :
348 1169 : return rc;
349 : }
350 :
351 : static void
352 160 : wrap_libxslt(bool finalize)
353 : {
354 : static xsltSecurityPrefsPtr secprefs;
355 160 : int ret = 0;
356 :
357 : /* security framework preferences */
358 160 : if (!finalize) {
359 102 : CRM_ASSERT(secprefs == NULL);
360 102 : secprefs = xsltNewSecurityPrefs();
361 102 : ret = xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_WRITE_FILE,
362 : xsltSecurityForbid)
363 102 : | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_CREATE_DIRECTORY,
364 : xsltSecurityForbid)
365 102 : | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_READ_NETWORK,
366 : xsltSecurityForbid)
367 102 : | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_WRITE_NETWORK,
368 : xsltSecurityForbid);
369 102 : if (ret != 0) {
370 0 : return;
371 : }
372 : } else {
373 58 : xsltFreeSecurityPrefs(secprefs);
374 58 : secprefs = NULL;
375 : }
376 :
377 : /* cleanup only */
378 160 : if (finalize) {
379 58 : xsltCleanupGlobals();
380 : }
381 : }
382 :
383 : void
384 0 : pcmk__load_schemas_from_dir(const char *dir)
385 : {
386 : int lpc, max;
387 0 : struct dirent **namelist = NULL;
388 :
389 0 : max = scandir(dir, &namelist, schema_filter, schema_cmp_directory);
390 0 : if (max < 0) {
391 0 : crm_warn("Could not load schemas from %s: %s", dir, strerror(errno));
392 0 : return;
393 : }
394 :
395 0 : for (lpc = 0; lpc < max; lpc++) {
396 0 : bool transform_expected = false;
397 0 : pcmk__schema_version_t version = SCHEMA_ZERO;
398 :
399 0 : if (!version_from_filename(namelist[lpc]->d_name, &version)) {
400 : // Shouldn't be possible, but makes static analysis happy
401 0 : crm_warn("Skipping schema '%s': could not parse version",
402 : namelist[lpc]->d_name);
403 0 : continue;
404 : }
405 0 : if ((lpc + 1) < max) {
406 0 : pcmk__schema_version_t next_version = SCHEMA_ZERO;
407 :
408 0 : if (version_from_filename(namelist[lpc+1]->d_name, &next_version)
409 0 : && (version.v[0] < next_version.v[0])) {
410 0 : transform_expected = true;
411 : }
412 : }
413 :
414 0 : if (add_schema_by_version(&version, transform_expected) != pcmk_rc_ok) {
415 0 : break;
416 : }
417 : }
418 :
419 0 : for (lpc = 0; lpc < max; lpc++) {
420 0 : free(namelist[lpc]);
421 : }
422 :
423 0 : free(namelist);
424 : }
425 :
426 : static gint
427 0 : schema_sort_GCompareFunc(gconstpointer a, gconstpointer b)
428 : {
429 0 : const pcmk__schema_t *schema_a = a;
430 0 : const pcmk__schema_t *schema_b = b;
431 :
432 : // @COMPAT pacemaker-next is deprecated since 2.1.5 and none since 2.1.8
433 0 : if (pcmk__str_eq(schema_a->name, "pacemaker-next", pcmk__str_none)) {
434 0 : if (pcmk__str_eq(schema_b->name, PCMK_VALUE_NONE, pcmk__str_none)) {
435 0 : return -1;
436 : } else {
437 0 : return 1;
438 : }
439 0 : } else if (pcmk__str_eq(schema_a->name, PCMK_VALUE_NONE, pcmk__str_none)) {
440 0 : return 1;
441 0 : } else if (pcmk__str_eq(schema_b->name, "pacemaker-next", pcmk__str_none)) {
442 0 : return -1;
443 : } else {
444 0 : return schema_cmp(schema_a->version, schema_b->version);
445 : }
446 : }
447 :
448 : /*!
449 : * \internal
450 : * \brief Sort the list of known schemas such that all pacemaker-X.Y are in
451 : * version order, then pacemaker-next, then none
452 : *
453 : * This function should be called whenever additional schemas are loaded using
454 : * pcmk__load_schemas_from_dir(), after the initial sets in crm_schema_init().
455 : */
456 : void
457 0 : pcmk__sort_schemas(void)
458 : {
459 0 : known_schemas = g_list_sort(known_schemas, schema_sort_GCompareFunc);
460 0 : }
461 :
462 : /*!
463 : * \internal
464 : * \brief Load pacemaker schemas into cache
465 : *
466 : * \note This currently also serves as an entry point for the
467 : * generic initialization of the libxslt library.
468 : */
469 : void
470 103 : crm_schema_init(void)
471 : {
472 103 : if (!initialized) {
473 102 : const char *remote_schema_dir = pcmk__remote_schema_dir();
474 102 : char *base = pcmk__xml_artefact_root(pcmk__xml_artefact_ns_legacy_rng);
475 102 : const pcmk__schema_version_t zero = SCHEMA_ZERO;
476 102 : int schema_index = 0;
477 :
478 102 : initialized = true;
479 :
480 102 : wrap_libxslt(false);
481 :
482 102 : pcmk__load_schemas_from_dir(base);
483 102 : pcmk__load_schemas_from_dir(remote_schema_dir);
484 :
485 : // @COMPAT: Deprecated since 2.1.5
486 102 : add_schema(pcmk__schema_validator_rng, &zero, "pacemaker-next", NULL,
487 : NULL, FALSE);
488 :
489 : // @COMPAT Deprecated since 2.1.8
490 102 : add_schema(pcmk__schema_validator_none, &zero, PCMK_VALUE_NONE, NULL,
491 : NULL, FALSE);
492 :
493 : /* add_schema() prepends items to the list, so in the simple case, this
494 : * just reverses the list. However if there were any remote schemas,
495 : * sorting is necessary.
496 : */
497 102 : pcmk__sort_schemas();
498 :
499 : // Now set the schema indexes and log the final result
500 1475 : for (GList *iter = known_schemas; iter != NULL; iter = iter->next) {
501 1373 : pcmk__schema_t *schema = iter->data;
502 :
503 1373 : if (schema->transform == NULL) {
504 1275 : crm_debug("Loaded schema %d: %s", schema_index, schema->name);
505 : } else {
506 98 : crm_debug("Loaded schema %d: %s (upgrades with %s.xsl)",
507 : schema_index, schema->name, schema->transform);
508 : }
509 1373 : schema->schema_index = schema_index++;
510 : }
511 : }
512 103 : }
513 :
514 : static bool
515 0 : validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler,
516 : void *error_handler_context, const char *relaxng_file,
517 : relaxng_ctx_cache_t **cached_ctx)
518 : {
519 0 : int rc = 0;
520 0 : bool valid = true;
521 0 : relaxng_ctx_cache_t *ctx = NULL;
522 :
523 0 : CRM_CHECK(doc != NULL, return false);
524 0 : CRM_CHECK(relaxng_file != NULL, return false);
525 :
526 0 : if (cached_ctx && *cached_ctx) {
527 0 : ctx = *cached_ctx;
528 :
529 : } else {
530 0 : crm_debug("Creating RNG parser context");
531 0 : ctx = pcmk__assert_alloc(1, sizeof(relaxng_ctx_cache_t));
532 :
533 0 : ctx->parser = xmlRelaxNGNewParserCtxt(relaxng_file);
534 0 : CRM_CHECK(ctx->parser != NULL, goto cleanup);
535 :
536 0 : if (error_handler) {
537 0 : xmlRelaxNGSetParserErrors(ctx->parser,
538 : (xmlRelaxNGValidityErrorFunc) error_handler,
539 : (xmlRelaxNGValidityWarningFunc) error_handler,
540 : error_handler_context);
541 : } else {
542 0 : xmlRelaxNGSetParserErrors(ctx->parser,
543 : (xmlRelaxNGValidityErrorFunc) fprintf,
544 : (xmlRelaxNGValidityWarningFunc) fprintf,
545 : stderr);
546 : }
547 :
548 0 : ctx->rng = xmlRelaxNGParse(ctx->parser);
549 0 : CRM_CHECK(ctx->rng != NULL,
550 : crm_err("Could not find/parse %s", relaxng_file);
551 : goto cleanup);
552 :
553 0 : ctx->valid = xmlRelaxNGNewValidCtxt(ctx->rng);
554 0 : CRM_CHECK(ctx->valid != NULL, goto cleanup);
555 :
556 0 : if (error_handler) {
557 0 : xmlRelaxNGSetValidErrors(ctx->valid,
558 : (xmlRelaxNGValidityErrorFunc) error_handler,
559 : (xmlRelaxNGValidityWarningFunc) error_handler,
560 : error_handler_context);
561 : } else {
562 0 : xmlRelaxNGSetValidErrors(ctx->valid,
563 : (xmlRelaxNGValidityErrorFunc) fprintf,
564 : (xmlRelaxNGValidityWarningFunc) fprintf,
565 : stderr);
566 : }
567 : }
568 :
569 0 : rc = xmlRelaxNGValidateDoc(ctx->valid, doc);
570 0 : if (rc > 0) {
571 0 : valid = false;
572 :
573 0 : } else if (rc < 0) {
574 0 : crm_err("Internal libxml error during validation");
575 : }
576 :
577 0 : cleanup:
578 :
579 0 : if (cached_ctx) {
580 0 : *cached_ctx = ctx;
581 :
582 : } else {
583 0 : if (ctx->parser != NULL) {
584 0 : xmlRelaxNGFreeParserCtxt(ctx->parser);
585 : }
586 0 : if (ctx->valid != NULL) {
587 0 : xmlRelaxNGFreeValidCtxt(ctx->valid);
588 : }
589 0 : if (ctx->rng != NULL) {
590 0 : xmlRelaxNGFree(ctx->rng);
591 : }
592 0 : free(ctx);
593 : }
594 :
595 0 : return valid;
596 : }
597 :
598 : static void
599 0 : free_schema(gpointer data)
600 : {
601 0 : pcmk__schema_t *schema = data;
602 0 : relaxng_ctx_cache_t *ctx = NULL;
603 :
604 0 : switch (schema->validator) {
605 0 : case pcmk__schema_validator_none: // not cached
606 0 : break;
607 :
608 0 : case pcmk__schema_validator_rng: // cached
609 0 : ctx = (relaxng_ctx_cache_t *) schema->cache;
610 0 : if (ctx == NULL) {
611 0 : break;
612 : }
613 :
614 0 : if (ctx->parser != NULL) {
615 0 : xmlRelaxNGFreeParserCtxt(ctx->parser);
616 : }
617 :
618 0 : if (ctx->valid != NULL) {
619 0 : xmlRelaxNGFreeValidCtxt(ctx->valid);
620 : }
621 :
622 0 : if (ctx->rng != NULL) {
623 0 : xmlRelaxNGFree(ctx->rng);
624 : }
625 :
626 0 : free(ctx);
627 0 : schema->cache = NULL;
628 0 : break;
629 : }
630 :
631 0 : free(schema->name);
632 0 : free(schema->transform);
633 0 : free(schema->transform_enter);
634 0 : }
635 :
636 : /*!
637 : * \internal
638 : * \brief Clean up global memory associated with XML schemas
639 : */
640 : void
641 0 : crm_schema_cleanup(void)
642 : {
643 0 : g_list_free_full(known_schemas, free_schema);
644 0 : known_schemas = NULL;
645 0 : initialized = false;
646 :
647 0 : wrap_libxslt(true);
648 0 : }
649 :
650 : /*!
651 : * \internal
652 : * \brief Get schema list entry corresponding to a schema name
653 : *
654 : * \param[in] name Name of schema to get
655 : *
656 : * \return Schema list entry corresponding to \p name, or NULL if unknown
657 : */
658 : GList *
659 173 : pcmk__get_schema(const char *name)
660 : {
661 : // @COMPAT Not specifying a schema name is deprecated since 2.1.8
662 173 : if (name == NULL) {
663 9 : name = PCMK_VALUE_NONE;
664 : }
665 3237 : for (GList *iter = known_schemas; iter != NULL; iter = iter->next) {
666 3225 : pcmk__schema_t *schema = iter->data;
667 :
668 3225 : if (pcmk__str_eq(name, schema->name, pcmk__str_casei)) {
669 161 : return iter;
670 : }
671 : }
672 12 : return NULL;
673 : }
674 :
675 : /*!
676 : * \internal
677 : * \brief Compare two schema version numbers given the schema names
678 : *
679 : * \param[in] schema1 Name of first schema to compare
680 : * \param[in] schema2 Name of second schema to compare
681 : *
682 : * \return Standard comparison result (negative integer if \p schema1 has the
683 : * lower version number, positive integer if \p schema1 has the higher
684 : * version number, of 0 if the version numbers are equal)
685 : */
686 : int
687 26 : pcmk__cmp_schemas_by_name(const char *schema1_name, const char *schema2_name)
688 : {
689 26 : GList *entry1 = pcmk__get_schema(schema1_name);
690 26 : GList *entry2 = pcmk__get_schema(schema2_name);
691 :
692 26 : if (entry1 == NULL) {
693 4 : return (entry2 == NULL)? 0 : -1;
694 :
695 22 : } else if (entry2 == NULL) {
696 3 : return 1;
697 :
698 : } else {
699 19 : pcmk__schema_t *schema1 = entry1->data;
700 19 : pcmk__schema_t *schema2 = entry2->data;
701 :
702 19 : return schema1->schema_index - schema2->schema_index;
703 : }
704 : }
705 :
706 : static bool
707 0 : validate_with(xmlNode *xml, pcmk__schema_t *schema,
708 : xmlRelaxNGValidityErrorFunc error_handler,
709 : void *error_handler_context)
710 : {
711 0 : bool valid = false;
712 0 : char *file = NULL;
713 0 : relaxng_ctx_cache_t **cache = NULL;
714 :
715 0 : if (schema == NULL) {
716 0 : return false;
717 : }
718 :
719 0 : if (schema->validator == pcmk__schema_validator_none) {
720 0 : return true;
721 : }
722 :
723 0 : file = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng,
724 0 : schema->name);
725 :
726 0 : crm_trace("Validating with %s (type=%d)",
727 : pcmk__s(file, "missing schema"), schema->validator);
728 0 : switch (schema->validator) {
729 0 : case pcmk__schema_validator_rng:
730 0 : cache = (relaxng_ctx_cache_t **) &(schema->cache);
731 0 : valid = validate_with_relaxng(xml->doc, error_handler, error_handler_context, file, cache);
732 0 : break;
733 0 : default:
734 0 : crm_err("Unknown validator type: %d", schema->validator);
735 0 : break;
736 : }
737 :
738 0 : free(file);
739 0 : return valid;
740 : }
741 :
742 : static bool
743 0 : validate_with_silent(xmlNode *xml, pcmk__schema_t *schema)
744 : {
745 0 : bool rc, sl_backup = silent_logging;
746 0 : silent_logging = TRUE;
747 0 : rc = validate_with(xml, schema, (xmlRelaxNGValidityErrorFunc) xml_log, GUINT_TO_POINTER(LOG_ERR));
748 0 : silent_logging = sl_backup;
749 0 : return rc;
750 : }
751 :
752 : bool
753 0 : pcmk__validate_xml(xmlNode *xml_blob, const char *validation,
754 : xmlRelaxNGValidityErrorFunc error_handler,
755 : void *error_handler_context)
756 : {
757 0 : GList *entry = NULL;
758 0 : pcmk__schema_t *schema = NULL;
759 :
760 0 : CRM_CHECK((xml_blob != NULL) && (xml_blob->doc != NULL), return false);
761 :
762 0 : if (validation == NULL) {
763 0 : validation = crm_element_value(xml_blob, PCMK_XA_VALIDATE_WITH);
764 : }
765 0 : pcmk__warn_if_schema_deprecated(validation);
766 :
767 : // @COMPAT Not specifying a schema name is deprecated since 2.1.8
768 0 : if (validation == NULL) {
769 0 : bool valid = false;
770 :
771 0 : for (entry = known_schemas; entry != NULL; entry = entry->next) {
772 0 : schema = entry->data;
773 0 : if (validate_with(xml_blob, schema, NULL, NULL)) {
774 0 : valid = true;
775 0 : crm_xml_add(xml_blob, PCMK_XA_VALIDATE_WITH, schema->name);
776 0 : crm_info("XML validated against %s", schema->name);
777 : }
778 : }
779 0 : return valid;
780 : }
781 :
782 0 : entry = pcmk__get_schema(validation);
783 0 : if (entry == NULL) {
784 0 : pcmk__config_err("Cannot validate CIB with " PCMK_XA_VALIDATE_WITH
785 : " set to an unknown schema such as '%s' (manually"
786 : " edit to use a known schema)",
787 : validation);
788 0 : return false;
789 : }
790 :
791 0 : schema = entry->data;
792 0 : return validate_with(xml_blob, schema, error_handler,
793 : error_handler_context);
794 : }
795 :
796 : /*!
797 : * \internal
798 : * \brief Validate XML using its configured schema (and send errors to logs)
799 : *
800 : * \param[in] xml XML to validate
801 : *
802 : * \return true if XML validates, otherwise false
803 : */
804 : bool
805 0 : pcmk__configured_schema_validates(xmlNode *xml)
806 : {
807 0 : return pcmk__validate_xml(xml, NULL,
808 : (xmlRelaxNGValidityErrorFunc) xml_log,
809 : GUINT_TO_POINTER(LOG_ERR));
810 : }
811 :
812 : /* With this arrangement, an attempt to identify the message severity
813 : as explicitly signalled directly from XSLT is performed in rather
814 : a smart way (no reliance on formatting string + arguments being
815 : always specified as ["%s", purposeful_string], as it can also be
816 : ["%s: %s", some_prefix, purposeful_string] etc. so every argument
817 : pertaining %s specifier is investigated), and if such a mark found,
818 : the respective level is determined and, when the messages are to go
819 : to the native logs, the mark itself gets dropped
820 : (by the means of string shift).
821 :
822 : NOTE: whether the native logging is the right sink is decided per
823 : the ctx parameter -- NULL denotes this case, otherwise it
824 : carries a pointer to the numeric expression of the desired
825 : target logging level (messages with higher level will be
826 : suppressed)
827 :
828 : NOTE: on some architectures, this string shift may not have any
829 : effect, but that's an acceptable tradeoff
830 :
831 : The logging level for not explicitly designated messages
832 : (suspicious, likely internal errors or some runaways) is
833 : LOG_WARNING.
834 : */
835 : static void G_GNUC_PRINTF(2, 3)
836 0 : cib_upgrade_err(void *ctx, const char *fmt, ...)
837 : {
838 : va_list ap, aq;
839 : char *arg_cur;
840 :
841 0 : bool found = FALSE;
842 0 : const char *fmt_iter = fmt;
843 0 : uint8_t msg_log_level = LOG_WARNING; /* default for runaway messages */
844 0 : const unsigned * log_level = (const unsigned *) ctx;
845 : enum {
846 : escan_seennothing,
847 : escan_seenpercent,
848 0 : } scan_state = escan_seennothing;
849 :
850 0 : va_start(ap, fmt);
851 0 : va_copy(aq, ap);
852 :
853 0 : while (!found && *fmt_iter != '\0') {
854 : /* while casing schema borrowed from libqb:qb_vsnprintf_serialize */
855 0 : switch (*fmt_iter++) {
856 0 : case '%':
857 0 : if (scan_state == escan_seennothing) {
858 0 : scan_state = escan_seenpercent;
859 0 : } else if (scan_state == escan_seenpercent) {
860 0 : scan_state = escan_seennothing;
861 : }
862 0 : break;
863 0 : case 's':
864 0 : if (scan_state == escan_seenpercent) {
865 0 : scan_state = escan_seennothing;
866 0 : arg_cur = va_arg(aq, char *);
867 0 : if (arg_cur != NULL) {
868 0 : switch (arg_cur[0]) {
869 0 : case 'W':
870 0 : if (!strncmp(arg_cur, "WARNING: ",
871 : sizeof("WARNING: ") - 1)) {
872 0 : msg_log_level = LOG_WARNING;
873 : }
874 0 : if (ctx == NULL) {
875 0 : memmove(arg_cur, arg_cur + sizeof("WARNING: ") - 1,
876 0 : strlen(arg_cur + sizeof("WARNING: ") - 1) + 1);
877 : }
878 0 : found = TRUE;
879 0 : break;
880 0 : case 'I':
881 0 : if (!strncmp(arg_cur, "INFO: ",
882 : sizeof("INFO: ") - 1)) {
883 0 : msg_log_level = LOG_INFO;
884 : }
885 0 : if (ctx == NULL) {
886 0 : memmove(arg_cur, arg_cur + sizeof("INFO: ") - 1,
887 0 : strlen(arg_cur + sizeof("INFO: ") - 1) + 1);
888 : }
889 0 : found = TRUE;
890 0 : break;
891 0 : case 'D':
892 0 : if (!strncmp(arg_cur, "DEBUG: ",
893 : sizeof("DEBUG: ") - 1)) {
894 0 : msg_log_level = LOG_DEBUG;
895 : }
896 0 : if (ctx == NULL) {
897 0 : memmove(arg_cur, arg_cur + sizeof("DEBUG: ") - 1,
898 0 : strlen(arg_cur + sizeof("DEBUG: ") - 1) + 1);
899 : }
900 0 : found = TRUE;
901 0 : break;
902 : }
903 : }
904 : }
905 0 : break;
906 0 : case '#': case '-': case ' ': case '+': case '\'': case 'I': case '.':
907 : case '0': case '1': case '2': case '3': case '4':
908 : case '5': case '6': case '7': case '8': case '9':
909 : case '*':
910 0 : break;
911 0 : case 'l':
912 : case 'z':
913 : case 't':
914 : case 'j':
915 : case 'd': case 'i':
916 : case 'o':
917 : case 'u':
918 : case 'x': case 'X':
919 : case 'e': case 'E':
920 : case 'f': case 'F':
921 : case 'g': case 'G':
922 : case 'a': case 'A':
923 : case 'c':
924 : case 'p':
925 0 : if (scan_state == escan_seenpercent) {
926 0 : (void) va_arg(aq, void *); /* skip forward */
927 0 : scan_state = escan_seennothing;
928 : }
929 0 : break;
930 0 : default:
931 0 : scan_state = escan_seennothing;
932 0 : break;
933 : }
934 : }
935 :
936 0 : if (log_level != NULL) {
937 : /* intention of the following offset is:
938 : cibadmin -V -> start showing INFO labelled messages */
939 0 : if (*log_level + 4 >= msg_log_level) {
940 0 : vfprintf(stderr, fmt, ap);
941 : }
942 : } else {
943 0 : PCMK__XML_LOG_BASE(msg_log_level, TRUE, 0, "CIB upgrade: ", fmt, ap);
944 : }
945 :
946 0 : va_end(aq);
947 0 : va_end(ap);
948 0 : }
949 :
950 : /*!
951 : * \internal
952 : * \brief Apply a single XSL transformation to given XML
953 : *
954 : * \param[in] xml XML to transform
955 : * \param[in] transform XSL name
956 : * \param[in] to_logs If false, certain validation errors will be sent to
957 : * stderr rather than logged
958 : *
959 : * \return Transformed XML on success, otherwise NULL
960 : */
961 : static xmlNode *
962 0 : apply_transformation(const xmlNode *xml, const char *transform,
963 : gboolean to_logs)
964 : {
965 0 : char *xform = NULL;
966 0 : xmlNode *out = NULL;
967 0 : xmlDocPtr res = NULL;
968 0 : xsltStylesheet *xslt = NULL;
969 :
970 0 : xform = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
971 : transform);
972 :
973 : /* for capturing, e.g., what's emitted via <xsl:message> */
974 0 : if (to_logs) {
975 0 : xsltSetGenericErrorFunc(NULL, cib_upgrade_err);
976 : } else {
977 0 : xsltSetGenericErrorFunc(&crm_log_level, cib_upgrade_err);
978 : }
979 :
980 0 : xslt = xsltParseStylesheetFile((pcmkXmlStr) xform);
981 0 : CRM_CHECK(xslt != NULL, goto cleanup);
982 :
983 0 : res = xsltApplyStylesheet(xslt, xml->doc, NULL);
984 0 : CRM_CHECK(res != NULL, goto cleanup);
985 :
986 0 : xsltSetGenericErrorFunc(NULL, NULL); /* restore default one */
987 :
988 0 : out = xmlDocGetRootElement(res);
989 :
990 0 : cleanup:
991 0 : if (xslt) {
992 0 : xsltFreeStylesheet(xslt);
993 : }
994 :
995 0 : free(xform);
996 :
997 0 : return out;
998 : }
999 :
1000 : /*!
1001 : * \internal
1002 : * \brief Perform all transformations needed to upgrade XML to next schema
1003 : *
1004 : * A schema upgrade can require up to three XSL transformations: an "enter"
1005 : * transform, the main upgrade transform, and a "leave" transform. Perform
1006 : * all needed transforms to upgrade given XML to the next schema.
1007 : *
1008 : * \param[in] original_xml XML to transform
1009 : * \param[in] schema_index Index of schema that successfully validates
1010 : * \p original_xml
1011 : * \param[in] to_logs If false, certain validation errors will be sent to
1012 : * stderr rather than logged
1013 : *
1014 : * \return XML result of schema transforms if successful, otherwise NULL
1015 : */
1016 : static xmlNode *
1017 0 : apply_upgrade(const xmlNode *original_xml, int schema_index, gboolean to_logs)
1018 : {
1019 0 : pcmk__schema_t *schema = g_list_nth_data(known_schemas, schema_index);
1020 0 : pcmk__schema_t *upgraded_schema = g_list_nth_data(known_schemas,
1021 0 : schema_index + 1);
1022 0 : bool transform_onleave = false;
1023 : char *transform_leave;
1024 0 : const xmlNode *xml = original_xml;
1025 0 : xmlNode *upgrade = NULL;
1026 0 : xmlNode *final = NULL;
1027 0 : xmlRelaxNGValidityErrorFunc error_handler = NULL;
1028 :
1029 0 : CRM_ASSERT((schema != NULL) && (upgraded_schema != NULL));
1030 :
1031 0 : if (to_logs) {
1032 0 : error_handler = (xmlRelaxNGValidityErrorFunc) xml_log;
1033 : }
1034 :
1035 0 : transform_onleave = schema->transform_onleave;
1036 0 : if (schema->transform_enter != NULL) {
1037 0 : crm_debug("Upgrading schema from %s to %s: "
1038 : "applying pre-upgrade XSL transform %s",
1039 : schema->name, upgraded_schema->name, schema->transform_enter);
1040 0 : upgrade = apply_transformation(xml, schema->transform_enter, to_logs);
1041 0 : if (upgrade == NULL) {
1042 0 : crm_warn("Pre-upgrade XSL transform %s failed, "
1043 : "will skip post-upgrade transform",
1044 : schema->transform_enter);
1045 0 : transform_onleave = FALSE;
1046 : } else {
1047 0 : xml = upgrade;
1048 : }
1049 : }
1050 :
1051 :
1052 0 : crm_debug("Upgrading schema from %s to %s: "
1053 : "applying upgrade XSL transform %s",
1054 : schema->name, upgraded_schema->name, schema->transform);
1055 0 : final = apply_transformation(xml, schema->transform, to_logs);
1056 0 : if (upgrade != xml) {
1057 0 : free_xml(upgrade);
1058 0 : upgrade = NULL;
1059 : }
1060 :
1061 0 : if ((final != NULL) && transform_onleave) {
1062 0 : upgrade = final;
1063 : /* following condition ensured in add_schema_by_version */
1064 0 : CRM_ASSERT(schema->transform_enter != NULL);
1065 0 : transform_leave = strdup(schema->transform_enter);
1066 : /* enter -> leave */
1067 0 : memcpy(strrchr(transform_leave, '-') + 1, "leave", sizeof("leave") - 1);
1068 0 : crm_debug("Upgrading schema from %s to %s: "
1069 : "applying post-upgrade XSL transform %s",
1070 : schema->name, upgraded_schema->name, transform_leave);
1071 0 : final = apply_transformation(upgrade, transform_leave, to_logs);
1072 0 : if (final == NULL) {
1073 0 : crm_warn("Ignoring failure of post-upgrade XSL transform %s",
1074 : transform_leave);
1075 0 : final = upgrade;
1076 : } else {
1077 0 : free_xml(upgrade);
1078 : }
1079 0 : free(transform_leave);
1080 : }
1081 :
1082 0 : if (final == NULL) {
1083 0 : return NULL;
1084 : }
1085 :
1086 : // Ensure result validates with its new schema
1087 0 : if (!validate_with(final, upgraded_schema, error_handler,
1088 : GUINT_TO_POINTER(LOG_ERR))) {
1089 0 : crm_err("Schema upgrade from %s to %s failed: "
1090 : "XSL transform %s produced an invalid configuration",
1091 : schema->name, upgraded_schema->name, schema->transform);
1092 0 : crm_log_xml_debug(final, "bad-transform-result");
1093 0 : free_xml(final);
1094 0 : return NULL;
1095 : }
1096 :
1097 0 : crm_info("Schema upgrade from %s to %s succeeded",
1098 : schema->name, upgraded_schema->name);
1099 0 : return final;
1100 : }
1101 :
1102 : /*!
1103 : * \internal
1104 : * \brief Get the schema list entry corresponding to XML configuration
1105 : *
1106 : * \param[in] xml CIB XML to check
1107 : *
1108 : * \return List entry of schema configured in \p xml
1109 : */
1110 : static GList *
1111 0 : get_configured_schema(const xmlNode *xml)
1112 : {
1113 0 : const char *schema_name = crm_element_value(xml, PCMK_XA_VALIDATE_WITH);
1114 :
1115 0 : pcmk__warn_if_schema_deprecated(schema_name);
1116 0 : if (schema_name == NULL) {
1117 0 : return NULL;
1118 : }
1119 0 : return pcmk__get_schema(schema_name);
1120 : }
1121 :
1122 : /*!
1123 : * \brief Update CIB XML to latest schema that validates it
1124 : *
1125 : * \param[in,out] xml XML to update (may be freed and replaced
1126 : * after being transformed)
1127 : * \param[in] max_schema_name If not NULL, do not update \p xml to any
1128 : * schema later than this one
1129 : * \param[in] transform If false, do not update \p xml to any schema
1130 : * that requires an XSL transform
1131 : * \param[in] to_logs If false, certain validation errors will be
1132 : * sent to stderr rather than logged
1133 : *
1134 : * \return Standard Pacemaker return code
1135 : */
1136 : int
1137 0 : pcmk__update_schema(xmlNode **xml, const char *max_schema_name, bool transform,
1138 : bool to_logs)
1139 : {
1140 0 : int max_stable_schemas = xml_latest_schema_index();
1141 0 : int max_schema_index = 0;
1142 0 : int rc = pcmk_rc_ok;
1143 0 : GList *entry = NULL;
1144 0 : pcmk__schema_t *best_schema = NULL;
1145 0 : pcmk__schema_t *original_schema = NULL;
1146 0 : xmlRelaxNGValidityErrorFunc error_handler =
1147 0 : to_logs ? (xmlRelaxNGValidityErrorFunc) xml_log : NULL;
1148 :
1149 0 : CRM_CHECK((xml != NULL) && (*xml != NULL) && ((*xml)->doc != NULL),
1150 : return EINVAL);
1151 :
1152 0 : if (max_schema_name != NULL) {
1153 0 : GList *max_entry = pcmk__get_schema(max_schema_name);
1154 :
1155 0 : if (max_entry != NULL) {
1156 0 : pcmk__schema_t *max_schema = max_entry->data;
1157 :
1158 0 : max_schema_index = max_schema->schema_index;
1159 : }
1160 : }
1161 0 : if ((max_schema_index < 1) || (max_schema_index > max_stable_schemas)) {
1162 0 : max_schema_index = max_stable_schemas;
1163 : }
1164 :
1165 0 : entry = get_configured_schema(*xml);
1166 0 : if (entry == NULL) {
1167 : // @COMPAT Not specifying a schema name is deprecated since 2.1.8
1168 0 : entry = known_schemas;
1169 : } else {
1170 0 : original_schema = entry->data;
1171 0 : if (original_schema->schema_index >= max_schema_index) {
1172 0 : return pcmk_rc_ok;
1173 : }
1174 : }
1175 :
1176 0 : for (; entry != NULL; entry = entry->next) {
1177 0 : pcmk__schema_t *current_schema = entry->data;
1178 0 : xmlNode *upgrade = NULL;
1179 :
1180 0 : if (current_schema->schema_index > max_schema_index) {
1181 0 : break;
1182 : }
1183 :
1184 0 : if (!validate_with(*xml, current_schema, error_handler,
1185 : GUINT_TO_POINTER(LOG_ERR))) {
1186 0 : crm_debug("Schema %s does not validate", current_schema->name);
1187 0 : if (best_schema != NULL) {
1188 : /* we've satisfied the validation, no need to check further */
1189 0 : break;
1190 : }
1191 0 : rc = pcmk_rc_schema_validation;
1192 0 : continue; // Try again with the next higher schema
1193 : }
1194 :
1195 0 : crm_debug("Schema %s validates", current_schema->name);
1196 0 : rc = pcmk_rc_ok;
1197 0 : best_schema = current_schema;
1198 0 : if (current_schema->schema_index == max_schema_index) {
1199 0 : break; // No further transformations possible
1200 : }
1201 :
1202 0 : if (!transform || (current_schema->transform == NULL)
1203 0 : || validate_with_silent(*xml, entry->next->data)) {
1204 : /* The next schema either doesn't require a transform or validates
1205 : * successfully even without the transform. Skip the transform and
1206 : * try the next schema with the same XML.
1207 : */
1208 0 : continue;
1209 : }
1210 :
1211 0 : upgrade = apply_upgrade(*xml, current_schema->schema_index, to_logs);
1212 0 : if (upgrade == NULL) {
1213 : /* The transform failed, so this schema can't be used. Later
1214 : * schemas are unlikely to validate, but try anyway until we
1215 : * run out of options.
1216 : */
1217 0 : rc = pcmk_rc_transform_failed;
1218 : } else {
1219 0 : best_schema = current_schema;
1220 0 : free_xml(*xml);
1221 0 : *xml = upgrade;
1222 : }
1223 : }
1224 :
1225 0 : if (best_schema != NULL) {
1226 0 : if ((original_schema == NULL)
1227 0 : || (best_schema->schema_index > original_schema->schema_index)) {
1228 0 : crm_info("%s the configuration schema to %s",
1229 : (transform? "Transformed" : "Upgraded"),
1230 : best_schema->name);
1231 0 : crm_xml_add(*xml, PCMK_XA_VALIDATE_WITH, best_schema->name);
1232 : }
1233 : }
1234 0 : return rc;
1235 : }
1236 :
1237 : /*!
1238 : * \internal
1239 : * \brief Update XML from its configured schema to the latest major series
1240 : *
1241 : * \param[in,out] xml XML to update
1242 : * \param[in] to_logs If false, certain validation errors will be
1243 : * sent to stderr rather than logged
1244 : *
1245 : * \return true if XML was successfully updated, otherwise false
1246 : */
1247 : bool
1248 0 : pcmk__update_configured_schema(xmlNode **xml, bool to_logs)
1249 : {
1250 0 : bool rc = true;
1251 0 : char *original_schema_name = NULL;
1252 :
1253 : // @COMPAT Not specifying a schema name is deprecated since 2.1.8
1254 0 : const char *effective_original_name = "the first";
1255 :
1256 0 : int orig_version = -1;
1257 0 : pcmk__schema_t *x_0_schema = pcmk__find_x_0_schema()->data;
1258 0 : GList *entry = NULL;
1259 :
1260 0 : original_schema_name = crm_element_value_copy(*xml, PCMK_XA_VALIDATE_WITH);
1261 0 : pcmk__warn_if_schema_deprecated(original_schema_name);
1262 0 : entry = pcmk__get_schema(original_schema_name);
1263 0 : if (entry != NULL) {
1264 0 : pcmk__schema_t *original_schema = entry->data;
1265 :
1266 0 : effective_original_name = original_schema->name;
1267 0 : orig_version = original_schema->schema_index;
1268 : }
1269 :
1270 0 : if (orig_version < x_0_schema->schema_index) {
1271 : // Current configuration schema is not acceptable, try to update
1272 0 : xmlNode *converted = NULL;
1273 0 : const char *new_schema_name = NULL;
1274 0 : pcmk__schema_t *schema = NULL;
1275 :
1276 0 : entry = NULL;
1277 0 : converted = pcmk__xml_copy(NULL, *xml);
1278 0 : if (pcmk__update_schema(&converted, NULL, true, to_logs) == pcmk_rc_ok) {
1279 0 : new_schema_name = crm_element_value(converted,
1280 : PCMK_XA_VALIDATE_WITH);
1281 0 : entry = pcmk__get_schema(new_schema_name);
1282 : }
1283 0 : schema = (entry == NULL)? NULL : entry->data;
1284 :
1285 0 : if ((schema == NULL)
1286 0 : || (schema->schema_index < x_0_schema->schema_index)) {
1287 : // Updated configuration schema is still not acceptable
1288 :
1289 0 : if ((orig_version == -1) || (schema == NULL)
1290 0 : || (schema->schema_index < orig_version)) {
1291 : // We couldn't validate any schema at all
1292 0 : if (to_logs) {
1293 0 : pcmk__config_err("Cannot upgrade configuration (claiming "
1294 : "%s schema) to at least %s because it "
1295 : "does not validate with any schema from "
1296 : "%s to the latest",
1297 : pcmk__s(original_schema_name, "no"),
1298 : x_0_schema->name, effective_original_name);
1299 : } else {
1300 0 : fprintf(stderr, "Cannot upgrade configuration (claiming "
1301 : "%s schema) to at least %s because it "
1302 : "does not validate with any schema from "
1303 : "%s to the latest\n",
1304 : pcmk__s(original_schema_name, "no"),
1305 : x_0_schema->name, effective_original_name);
1306 : }
1307 : } else {
1308 : // We updated configuration successfully, but still too low
1309 0 : if (to_logs) {
1310 0 : pcmk__config_err("Cannot upgrade configuration (claiming "
1311 : "%s schema) to at least %s because it "
1312 : "would not upgrade past %s",
1313 : pcmk__s(original_schema_name, "no"),
1314 : x_0_schema->name,
1315 : pcmk__s(new_schema_name, "unspecified version"));
1316 : } else {
1317 0 : fprintf(stderr, "Cannot upgrade configuration (claiming "
1318 : "%s schema) to at least %s because it "
1319 : "would not upgrade past %s\n",
1320 : pcmk__s(original_schema_name, "no"),
1321 : x_0_schema->name,
1322 : pcmk__s(new_schema_name, "unspecified version"));
1323 : }
1324 : }
1325 :
1326 0 : free_xml(converted);
1327 0 : converted = NULL;
1328 0 : rc = false;
1329 :
1330 : } else {
1331 : // Updated configuration schema is acceptable
1332 0 : free_xml(*xml);
1333 0 : *xml = converted;
1334 :
1335 0 : if (schema->schema_index < xml_latest_schema_index()) {
1336 0 : if (to_logs) {
1337 0 : pcmk__config_warn("Configuration with %s schema was "
1338 : "internally upgraded to acceptable (but "
1339 : "not most recent) %s",
1340 : pcmk__s(original_schema_name, "no"),
1341 : schema->name);
1342 : }
1343 0 : } else if (to_logs) {
1344 0 : crm_info("Configuration with %s schema was internally "
1345 : "upgraded to latest version %s",
1346 : pcmk__s(original_schema_name, "no"),
1347 : schema->name);
1348 : }
1349 : }
1350 :
1351 : } else {
1352 : // @COMPAT the none schema is deprecated since 2.1.8
1353 0 : pcmk__schema_t *none_schema = NULL;
1354 :
1355 0 : entry = pcmk__get_schema(PCMK_VALUE_NONE);
1356 0 : CRM_ASSERT((entry != NULL) && (entry->data != NULL));
1357 :
1358 0 : none_schema = entry->data;
1359 0 : if (!to_logs && (orig_version >= none_schema->schema_index)) {
1360 0 : fprintf(stderr, "Schema validation of configuration is "
1361 : "disabled (support for " PCMK_XA_VALIDATE_WITH
1362 : " set to \"" PCMK_VALUE_NONE "\" is deprecated"
1363 : " and will be removed in a future release)\n");
1364 : }
1365 : }
1366 :
1367 0 : free(original_schema_name);
1368 0 : return rc;
1369 : }
1370 :
1371 : /*!
1372 : * \internal
1373 : * \brief Return a list of all schema files and any associated XSLT files
1374 : * later than the given one
1375 : * \brief Return a list of all schema versions later than the given one
1376 : *
1377 : * \param[in] schema The schema to compare against (for example,
1378 : * "pacemaker-3.1.rng" or "pacemaker-3.1")
1379 : *
1380 : * \note The caller is responsible for freeing both the returned list and
1381 : * the elements of the list
1382 : */
1383 : GList *
1384 6 : pcmk__schema_files_later_than(const char *name)
1385 : {
1386 6 : GList *lst = NULL;
1387 : pcmk__schema_version_t ver;
1388 :
1389 6 : if (!version_from_filename(name, &ver)) {
1390 2 : return lst;
1391 : }
1392 :
1393 4 : for (GList *iter = g_list_nth(known_schemas, xml_latest_schema_index());
1394 64 : iter != NULL; iter = iter->prev) {
1395 60 : pcmk__schema_t *schema = iter->data;
1396 60 : char *s = NULL;
1397 :
1398 60 : if (schema_cmp(ver, schema->version) != -1) {
1399 33 : continue;
1400 : }
1401 :
1402 27 : s = crm_strdup_printf("%s.rng", schema->name);
1403 27 : lst = g_list_prepend(lst, s);
1404 :
1405 27 : if (schema->transform != NULL) {
1406 4 : char *xform = crm_strdup_printf("%s.xsl", schema->transform);
1407 4 : lst = g_list_prepend(lst, xform);
1408 : }
1409 :
1410 27 : if (schema->transform_enter != NULL) {
1411 3 : char *enter = crm_strdup_printf("%s.xsl", schema->transform_enter);
1412 :
1413 3 : lst = g_list_prepend(lst, enter);
1414 :
1415 3 : if (schema->transform_onleave) {
1416 3 : int last_dash = strrchr(enter, '-') - enter;
1417 3 : char *leave = crm_strdup_printf("%.*s-leave.xsl", last_dash, enter);
1418 :
1419 3 : lst = g_list_prepend(lst, leave);
1420 : }
1421 : }
1422 : }
1423 :
1424 4 : return lst;
1425 : }
1426 :
1427 : static void
1428 0 : append_href(xmlNode *xml, void *user_data)
1429 : {
1430 0 : GList **list = user_data;
1431 0 : char *href = crm_element_value_copy(xml, "href");
1432 :
1433 0 : if (href == NULL) {
1434 0 : return;
1435 : }
1436 0 : *list = g_list_prepend(*list, href);
1437 : }
1438 :
1439 : static void
1440 33 : external_refs_in_schema(GList **list, const char *contents)
1441 : {
1442 : /* local-name()= is needed to ignore the xmlns= setting at the top of
1443 : * the XML file. Otherwise, the xpath query will always return nothing.
1444 : */
1445 33 : const char *search = "//*[local-name()='externalRef'] | //*[local-name()='include']";
1446 33 : xmlNode *xml = pcmk__xml_parse(contents);
1447 :
1448 33 : crm_foreach_xpath_result(xml, search, append_href, list);
1449 33 : free_xml(xml);
1450 33 : }
1451 :
1452 : static int
1453 34 : read_file_contents(const char *file, char **contents)
1454 : {
1455 34 : int rc = pcmk_rc_ok;
1456 34 : char *path = NULL;
1457 :
1458 34 : if (pcmk__ends_with(file, ".rng")) {
1459 34 : path = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng, file);
1460 : } else {
1461 0 : path = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, file);
1462 : }
1463 :
1464 34 : rc = pcmk__file_contents(path, contents);
1465 :
1466 34 : free(path);
1467 34 : return rc;
1468 : }
1469 :
1470 : static void
1471 88 : add_schema_file_to_xml(xmlNode *parent, const char *file, GList **already_included)
1472 : {
1473 88 : char *contents = NULL;
1474 88 : char *path = NULL;
1475 88 : xmlNode *file_node = NULL;
1476 88 : GList *includes = NULL;
1477 88 : int rc = pcmk_rc_ok;
1478 :
1479 : /* If we already included this file, don't do so again. */
1480 88 : if (g_list_find_custom(*already_included, file, (GCompareFunc) strcmp) != NULL) {
1481 55 : return;
1482 : }
1483 :
1484 : /* Ensure whatever file we were given has a suffix we know about. If not,
1485 : * just assume it's an RNG file.
1486 : */
1487 34 : if (!pcmk__ends_with(file, ".rng") && !pcmk__ends_with(file, ".xsl")) {
1488 4 : path = crm_strdup_printf("%s.rng", file);
1489 : } else {
1490 30 : path = pcmk__str_copy(file);
1491 : }
1492 :
1493 34 : rc = read_file_contents(path, &contents);
1494 34 : if (rc != pcmk_rc_ok || contents == NULL) {
1495 1 : crm_warn("Could not read schema file %s: %s", file, pcmk_rc_str(rc));
1496 1 : free(path);
1497 1 : return;
1498 : }
1499 :
1500 : /* Create a new <file path="..."> node with the contents of the file
1501 : * as a CDATA block underneath it.
1502 : */
1503 33 : file_node = pcmk__xe_create(parent, PCMK_XA_FILE);
1504 33 : crm_xml_add(file_node, PCMK_XA_PATH, path);
1505 33 : *already_included = g_list_prepend(*already_included, path);
1506 :
1507 33 : xmlAddChild(file_node, xmlNewCDataBlock(parent->doc, (pcmkXmlStr) contents,
1508 33 : strlen(contents)));
1509 :
1510 : /* Scan the file for any <externalRef> or <include> nodes and build up
1511 : * a list of the files they reference.
1512 : */
1513 33 : external_refs_in_schema(&includes, contents);
1514 :
1515 : /* For each referenced file, recurse to add it (and potentially anything it
1516 : * references, ...) to the XML.
1517 : */
1518 117 : for (GList *iter = includes; iter != NULL; iter = iter->next) {
1519 84 : add_schema_file_to_xml(parent, iter->data, already_included);
1520 : }
1521 :
1522 33 : free(contents);
1523 33 : g_list_free_full(includes, free);
1524 : }
1525 :
1526 : /*!
1527 : * \internal
1528 : * \brief Add an XML schema file and all the files it references as children
1529 : * of a given XML node
1530 : *
1531 : * \param[in,out] parent The parent XML node
1532 : * \param[in] name The schema version to compare against
1533 : * (for example, "pacemaker-3.1" or "pacemaker-3.1.rng")
1534 : * \param[in,out] already_included A list of names that have already been added
1535 : * to the parent node.
1536 : *
1537 : * \note The caller is responsible for freeing both the returned list and
1538 : * the elements of the list
1539 : */
1540 : void
1541 4 : pcmk__build_schema_xml_node(xmlNode *parent, const char *name, GList **already_included)
1542 : {
1543 : /* First, create an unattached node to add all the schema files to as children. */
1544 4 : xmlNode *schema_node = pcmk__xe_create(NULL, PCMK__XA_SCHEMA);
1545 :
1546 4 : crm_xml_add(schema_node, PCMK_XA_VERSION, name);
1547 4 : add_schema_file_to_xml(schema_node, name, already_included);
1548 :
1549 : /* Then, if we actually added any children, attach the node to parent. If
1550 : * we did not add any children (for instance, name was invalid), this prevents
1551 : * us from returning a document with additional empty children.
1552 : */
1553 4 : if (schema_node->children != NULL) {
1554 3 : xmlAddChild(parent, schema_node);
1555 : } else {
1556 1 : free_xml(schema_node);
1557 : }
1558 4 : }
1559 :
1560 : /*!
1561 : * \internal
1562 : * \brief Return the directory containing any extra schema files that a
1563 : * Pacemaker Remote node fetched from the cluster
1564 : */
1565 : const char *
1566 0 : pcmk__remote_schema_dir(void)
1567 : {
1568 0 : const char *dir = pcmk__env_option(PCMK__ENV_REMOTE_SCHEMA_DIRECTORY);
1569 :
1570 0 : if (pcmk__str_empty(dir)) {
1571 0 : return PCMK__REMOTE_SCHEMA_DIR;
1572 : }
1573 :
1574 0 : return dir;
1575 : }
1576 :
1577 : /*!
1578 : * \internal
1579 : * \brief Warn if a given validation schema is deprecated
1580 : *
1581 : * \param[in] Schema name to check
1582 : */
1583 : void
1584 0 : pcmk__warn_if_schema_deprecated(const char *schema)
1585 : {
1586 0 : if ((schema == NULL) ||
1587 0 : pcmk__strcase_any_of(schema, "pacemaker-next", PCMK_VALUE_NONE, NULL)) {
1588 0 : pcmk__config_warn("Support for " PCMK_XA_VALIDATE_WITH "='%s' is "
1589 : "deprecated and will be removed in a future release "
1590 : "without the possibility of upgrades (manually edit "
1591 : "to use a supported schema)", pcmk__s(schema, ""));
1592 : }
1593 0 : }
1594 :
1595 : // Deprecated functions kept only for backward API compatibility
1596 : // LCOV_EXCL_START
1597 :
1598 : #include <crm/common/xml_compat.h>
1599 :
1600 : const char *
1601 : xml_latest_schema(void)
1602 : {
1603 : return pcmk__highest_schema_name();
1604 : }
1605 :
1606 : const char *
1607 : get_schema_name(int version)
1608 : {
1609 : pcmk__schema_t *schema = g_list_nth_data(known_schemas, version);
1610 :
1611 : return (schema != NULL)? schema->name : "unknown";
1612 : }
1613 :
1614 : int
1615 : get_schema_version(const char *name)
1616 : {
1617 : int lpc = 0;
1618 :
1619 : if (name == NULL) {
1620 : name = PCMK_VALUE_NONE;
1621 : }
1622 :
1623 : for (GList *iter = known_schemas; iter != NULL; iter = iter->next) {
1624 : pcmk__schema_t *schema = iter->data;
1625 :
1626 : if (pcmk__str_eq(name, schema->name, pcmk__str_casei)) {
1627 : return lpc;
1628 : }
1629 :
1630 : lpc++;
1631 : }
1632 :
1633 : return -1;
1634 : }
1635 :
1636 : int
1637 : update_validation(xmlNode **xml, int *best, int max, gboolean transform,
1638 : gboolean to_logs)
1639 : {
1640 : int rc = pcmk__update_schema(xml, get_schema_name(max), transform, to_logs);
1641 :
1642 : if ((best != NULL) && (xml != NULL) && (rc == pcmk_rc_ok)) {
1643 : const char *schema_name = crm_element_value(*xml,
1644 : PCMK_XA_VALIDATE_WITH);
1645 : GList *schema_entry = pcmk__get_schema(schema_name);
1646 :
1647 : if (schema_entry != NULL) {
1648 : *best = ((pcmk__schema_t *)(schema_entry->data))->schema_index;
1649 : }
1650 : }
1651 :
1652 : return pcmk_rc2legacy(rc);
1653 : }
1654 :
1655 : gboolean
1656 : validate_xml(xmlNode *xml_blob, const char *validation, gboolean to_logs)
1657 : {
1658 : bool rc = pcmk__validate_xml(xml_blob, validation,
1659 : to_logs? (xmlRelaxNGValidityErrorFunc) xml_log : NULL,
1660 : GUINT_TO_POINTER(LOG_ERR));
1661 : return rc? TRUE : FALSE;
1662 : }
1663 :
1664 : static void
1665 : dump_file(const char *filename)
1666 : {
1667 :
1668 : FILE *fp = NULL;
1669 : int ch, line = 0;
1670 :
1671 : CRM_CHECK(filename != NULL, return);
1672 :
1673 : fp = fopen(filename, "r");
1674 : if (fp == NULL) {
1675 : crm_perror(LOG_ERR, "Could not open %s for reading", filename);
1676 : return;
1677 : }
1678 :
1679 : fprintf(stderr, "%4d ", ++line);
1680 : do {
1681 : ch = getc(fp);
1682 : if (ch == EOF) {
1683 : putc('\n', stderr);
1684 : break;
1685 : } else if (ch == '\n') {
1686 : fprintf(stderr, "\n%4d ", ++line);
1687 : } else {
1688 : putc(ch, stderr);
1689 : }
1690 : } while (1);
1691 :
1692 : fclose(fp);
1693 : }
1694 :
1695 : gboolean
1696 : validate_xml_verbose(const xmlNode *xml_blob)
1697 : {
1698 : int fd = 0;
1699 : xmlDoc *doc = NULL;
1700 : xmlNode *xml = NULL;
1701 : gboolean rc = FALSE;
1702 : char *filename = NULL;
1703 :
1704 : filename = crm_strdup_printf("%s/cib-invalid.XXXXXX", pcmk__get_tmpdir());
1705 :
1706 : umask(S_IWGRP | S_IWOTH | S_IROTH);
1707 : fd = mkstemp(filename);
1708 : pcmk__xml_write_fd(xml_blob, filename, fd, false, NULL);
1709 :
1710 : dump_file(filename);
1711 :
1712 : doc = xmlReadFile(filename, NULL, 0);
1713 : xml = xmlDocGetRootElement(doc);
1714 : rc = pcmk__validate_xml(xml, NULL, NULL, NULL);
1715 : free_xml(xml);
1716 :
1717 : unlink(filename);
1718 : free(filename);
1719 :
1720 : return rc? TRUE : FALSE;
1721 : }
1722 :
1723 : gboolean
1724 : cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs)
1725 : {
1726 : bool rc = pcmk__update_configured_schema(xml, to_logs);
1727 :
1728 : if (best_version != NULL) {
1729 : const char *name = crm_element_value(*xml, PCMK_XA_VALIDATE_WITH);
1730 :
1731 : if (name == NULL) {
1732 : *best_version = -1;
1733 : } else {
1734 : GList *entry = pcmk__get_schema(name);
1735 : pcmk__schema_t *schema = (entry == NULL)? NULL : entry->data;
1736 :
1737 : *best_version = (schema == NULL)? -1 : schema->schema_index;
1738 : }
1739 : }
1740 : return rc? TRUE: FALSE;
1741 : }
1742 :
1743 : // LCOV_EXCL_STOP
1744 : // End deprecated API
|