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 <crm/common/xml.h>
13 :
14 : #include "crmcommon_private.h"
15 :
16 : /*!
17 : * \internal
18 : * \brief Output an XML patchset header
19 : *
20 : * This function parses a header from an XML patchset (an \p XML_ATTR_DIFF
21 : * element and its children).
22 : *
23 : * All header lines contain three integers separated by dots, of the form
24 : * <tt>{0}.{1}.{2}</tt>:
25 : * * \p {0}: \c PCMK_XA_ADMIN_EPOCH
26 : * * \p {1}: \c PCMK_XA_EPOCH
27 : * * \p {2}: \c PCMK_XA_NUM_UPDATES
28 : *
29 : * Lines containing \p "---" describe removals and end with the patch format
30 : * number. Lines containing \p "+++" describe additions and end with the patch
31 : * digest.
32 : *
33 : * \param[in,out] out Output object
34 : * \param[in] patchset XML patchset to output
35 : *
36 : * \return Standard Pacemaker return code
37 : *
38 : * \note This function produces output only for text-like formats.
39 : */
40 : static int
41 0 : xml_show_patchset_header(pcmk__output_t *out, const xmlNode *patchset)
42 : {
43 0 : int rc = pcmk_rc_no_output;
44 0 : int add[] = { 0, 0, 0 };
45 0 : int del[] = { 0, 0, 0 };
46 :
47 0 : xml_patch_versions(patchset, add, del);
48 :
49 0 : if ((add[0] != del[0]) || (add[1] != del[1]) || (add[2] != del[2])) {
50 0 : const char *fmt = crm_element_value(patchset, PCMK_XA_FORMAT);
51 0 : const char *digest = crm_element_value(patchset, PCMK__XA_DIGEST);
52 :
53 0 : out->info(out, "Diff: --- %d.%d.%d %s", del[0], del[1], del[2], fmt);
54 0 : rc = out->info(out, "Diff: +++ %d.%d.%d %s",
55 : add[0], add[1], add[2], digest);
56 :
57 0 : } else if ((add[0] != 0) || (add[1] != 0) || (add[2] != 0)) {
58 0 : rc = out->info(out, "Local-only Change: %d.%d.%d",
59 : add[0], add[1], add[2]);
60 : }
61 :
62 0 : return rc;
63 : }
64 :
65 : /*!
66 : * \internal
67 : * \brief Output a user-friendly form of XML additions or removals
68 : *
69 : * \param[in,out] out Output object
70 : * \param[in] prefix String to prepend to every line of output
71 : * \param[in] data XML node to output
72 : * \param[in] depth Current indentation level
73 : * \param[in] options Group of \p pcmk__xml_fmt_options flags
74 : *
75 : * \return Standard Pacemaker return code
76 : *
77 : * \note This function produces output only for text-like formats.
78 : */
79 : static int
80 0 : xml_show_patchset_v1_recursive(pcmk__output_t *out, const char *prefix,
81 : const xmlNode *data, int depth, uint32_t options)
82 : {
83 0 : if ((data->children == NULL)
84 0 : || (crm_element_value(data, PCMK__XA_CRM_DIFF_MARKER) != NULL)) {
85 :
86 : // Found a change; clear the pcmk__xml_fmt_diff_short option if set
87 0 : options &= ~pcmk__xml_fmt_diff_short;
88 :
89 0 : if (pcmk_is_set(options, pcmk__xml_fmt_diff_plus)) {
90 0 : prefix = PCMK__XML_PREFIX_CREATED;
91 : } else { // pcmk_is_set(options, pcmk__xml_fmt_diff_minus)
92 0 : prefix = PCMK__XML_PREFIX_DELETED;
93 : }
94 : }
95 :
96 0 : if (pcmk_is_set(options, pcmk__xml_fmt_diff_short)) {
97 0 : int rc = pcmk_rc_no_output;
98 :
99 : // Keep looking for the actual change
100 0 : for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL;
101 0 : child = pcmk__xml_next(child)) {
102 0 : int temp_rc = xml_show_patchset_v1_recursive(out, prefix, child,
103 : depth + 1, options);
104 0 : rc = pcmk__output_select_rc(rc, temp_rc);
105 : }
106 0 : return rc;
107 : }
108 :
109 0 : return pcmk__xml_show(out, prefix, data, depth,
110 : options
111 : |pcmk__xml_fmt_open
112 : |pcmk__xml_fmt_children
113 : |pcmk__xml_fmt_close);
114 : }
115 :
116 : /*!
117 : * \internal
118 : * \brief Output a user-friendly form of an XML patchset (format 1)
119 : *
120 : * This function parses an XML patchset (an \p XML_ATTR_DIFF element and its
121 : * children) into a user-friendly combined diff output.
122 : *
123 : * \param[in,out] out Output object
124 : * \param[in] patchset XML patchset to output
125 : * \param[in] options Group of \p pcmk__xml_fmt_options flags
126 : *
127 : * \return Standard Pacemaker return code
128 : *
129 : * \note This function produces output only for text-like formats.
130 : */
131 : static int
132 0 : xml_show_patchset_v1(pcmk__output_t *out, const xmlNode *patchset,
133 : uint32_t options)
134 : {
135 0 : const xmlNode *removed = NULL;
136 0 : const xmlNode *added = NULL;
137 0 : const xmlNode *child = NULL;
138 0 : bool is_first = true;
139 0 : int rc = xml_show_patchset_header(out, patchset);
140 :
141 : /* It's not clear whether "- " or "+ " ever does *not* get overridden by
142 : * PCMK__XML_PREFIX_DELETED or PCMK__XML_PREFIX_CREATED in practice.
143 : * However, v1 patchsets can only exist during rolling upgrades from
144 : * Pacemaker 1.1.11, so not worth worrying about.
145 : */
146 0 : removed = pcmk__xe_first_child(patchset, PCMK__XE_DIFF_REMOVED, NULL, NULL);
147 0 : for (child = pcmk__xml_first_child(removed); child != NULL;
148 0 : child = pcmk__xml_next(child)) {
149 0 : int temp_rc = xml_show_patchset_v1_recursive(out, "- ", child, 0,
150 : options
151 : |pcmk__xml_fmt_diff_minus);
152 0 : rc = pcmk__output_select_rc(rc, temp_rc);
153 :
154 0 : if (is_first) {
155 0 : is_first = false;
156 : } else {
157 0 : rc = pcmk__output_select_rc(rc, out->info(out, " --- "));
158 : }
159 : }
160 :
161 0 : is_first = true;
162 0 : added = pcmk__xe_first_child(patchset, PCMK__XE_DIFF_ADDED, NULL, NULL);
163 0 : for (child = pcmk__xml_first_child(added); child != NULL;
164 0 : child = pcmk__xml_next(child)) {
165 0 : int temp_rc = xml_show_patchset_v1_recursive(out, "+ ", child, 0,
166 : options
167 : |pcmk__xml_fmt_diff_plus);
168 0 : rc = pcmk__output_select_rc(rc, temp_rc);
169 :
170 0 : if (is_first) {
171 0 : is_first = false;
172 : } else {
173 0 : rc = pcmk__output_select_rc(rc, out->info(out, " +++ "));
174 : }
175 : }
176 :
177 0 : return rc;
178 : }
179 :
180 : /*!
181 : * \internal
182 : * \brief Output a user-friendly form of an XML patchset (format 2)
183 : *
184 : * This function parses an XML patchset (an \p XML_ATTR_DIFF element and its
185 : * children) into a user-friendly combined diff output.
186 : *
187 : * \param[in,out] out Output object
188 : * \param[in] patchset XML patchset to output
189 : *
190 : * \return Standard Pacemaker return code
191 : *
192 : * \note This function produces output only for text-like formats.
193 : */
194 : static int
195 0 : xml_show_patchset_v2(pcmk__output_t *out, const xmlNode *patchset)
196 : {
197 0 : int rc = xml_show_patchset_header(out, patchset);
198 0 : int temp_rc = pcmk_rc_no_output;
199 :
200 0 : for (const xmlNode *change = pcmk__xe_first_child(patchset, NULL, NULL,
201 : NULL);
202 0 : change != NULL; change = pcmk__xe_next(change)) {
203 :
204 0 : const char *op = crm_element_value(change, PCMK_XA_OPERATION);
205 0 : const char *xpath = crm_element_value(change, PCMK_XA_PATH);
206 :
207 0 : if (op == NULL) {
208 0 : continue;
209 : }
210 :
211 0 : if (strcmp(op, PCMK_VALUE_CREATE) == 0) {
212 0 : char *prefix = crm_strdup_printf(PCMK__XML_PREFIX_CREATED " %s: ",
213 : xpath);
214 :
215 0 : temp_rc = pcmk__xml_show(out, prefix, change->children, 0,
216 : pcmk__xml_fmt_pretty|pcmk__xml_fmt_open);
217 0 : rc = pcmk__output_select_rc(rc, temp_rc);
218 :
219 : // Overwrite all except the first two characters with spaces
220 0 : for (char *ch = prefix + 2; *ch != '\0'; ch++) {
221 0 : *ch = ' ';
222 : }
223 :
224 0 : temp_rc = pcmk__xml_show(out, prefix, change->children, 0,
225 : pcmk__xml_fmt_pretty
226 : |pcmk__xml_fmt_children
227 : |pcmk__xml_fmt_close);
228 0 : rc = pcmk__output_select_rc(rc, temp_rc);
229 0 : free(prefix);
230 :
231 0 : } else if (strcmp(op, PCMK_VALUE_MOVE) == 0) {
232 0 : const char *position = crm_element_value(change, PCMK_XE_POSITION);
233 :
234 0 : temp_rc = out->info(out,
235 : PCMK__XML_PREFIX_MOVED " %s moved to offset %s",
236 : xpath, position);
237 0 : rc = pcmk__output_select_rc(rc, temp_rc);
238 :
239 0 : } else if (strcmp(op, PCMK_VALUE_MODIFY) == 0) {
240 0 : xmlNode *clist = pcmk__xe_first_child(change, PCMK_XE_CHANGE_LIST,
241 : NULL, NULL);
242 0 : GString *buffer_set = NULL;
243 0 : GString *buffer_unset = NULL;
244 :
245 0 : for (const xmlNode *child = pcmk__xe_first_child(clist, NULL, NULL,
246 : NULL);
247 0 : child != NULL; child = pcmk__xe_next(child)) {
248 :
249 0 : const char *name = crm_element_value(child, PCMK_XA_NAME);
250 :
251 0 : op = crm_element_value(child, PCMK_XA_OPERATION);
252 0 : if (op == NULL) {
253 0 : continue;
254 : }
255 :
256 0 : if (strcmp(op, "set") == 0) {
257 0 : const char *value = crm_element_value(child, PCMK_XA_VALUE);
258 :
259 0 : pcmk__add_separated_word(&buffer_set, 256, "@", ", ");
260 0 : pcmk__g_strcat(buffer_set, name, "=", value, NULL);
261 :
262 0 : } else if (strcmp(op, "unset") == 0) {
263 0 : pcmk__add_separated_word(&buffer_unset, 256, "@", ", ");
264 0 : g_string_append(buffer_unset, name);
265 : }
266 : }
267 :
268 0 : if (buffer_set != NULL) {
269 0 : temp_rc = out->info(out, "+ %s: %s", xpath, buffer_set->str);
270 0 : rc = pcmk__output_select_rc(rc, temp_rc);
271 0 : g_string_free(buffer_set, TRUE);
272 : }
273 :
274 0 : if (buffer_unset != NULL) {
275 0 : temp_rc = out->info(out, "-- %s: %s",
276 0 : xpath, buffer_unset->str);
277 0 : rc = pcmk__output_select_rc(rc, temp_rc);
278 0 : g_string_free(buffer_unset, TRUE);
279 : }
280 :
281 0 : } else if (strcmp(op, PCMK_VALUE_DELETE) == 0) {
282 0 : int position = -1;
283 :
284 0 : crm_element_value_int(change, PCMK_XE_POSITION, &position);
285 0 : if (position >= 0) {
286 0 : temp_rc = out->info(out, "-- %s (%d)", xpath, position);
287 : } else {
288 0 : temp_rc = out->info(out, "-- %s", xpath);
289 : }
290 0 : rc = pcmk__output_select_rc(rc, temp_rc);
291 : }
292 : }
293 :
294 0 : return rc;
295 : }
296 :
297 : /*!
298 : * \internal
299 : * \brief Output a user-friendly form of an XML patchset
300 : *
301 : * This function parses an XML patchset (an \p XML_ATTR_DIFF element and its
302 : * children) into a user-friendly combined diff output.
303 : *
304 : * \param[in,out] out Output object
305 : * \param[in] args Message-specific arguments
306 : *
307 : * \return Standard Pacemaker return code
308 : *
309 : * \note \p args should contain the following:
310 : * -# XML patchset
311 : */
312 : PCMK__OUTPUT_ARGS("xml-patchset", "const xmlNode *")
313 : static int
314 0 : xml_patchset_default(pcmk__output_t *out, va_list args)
315 : {
316 0 : const xmlNode *patchset = va_arg(args, const xmlNode *);
317 :
318 0 : int format = 1;
319 :
320 0 : if (patchset == NULL) {
321 0 : crm_trace("Empty patch");
322 0 : return pcmk_rc_no_output;
323 : }
324 :
325 0 : crm_element_value_int(patchset, PCMK_XA_FORMAT, &format);
326 0 : switch (format) {
327 0 : case 1:
328 0 : return xml_show_patchset_v1(out, patchset, pcmk__xml_fmt_pretty);
329 0 : case 2:
330 0 : return xml_show_patchset_v2(out, patchset);
331 0 : default:
332 0 : crm_err("Unknown patch format: %d", format);
333 0 : return pcmk_rc_bad_xml_patch;
334 : }
335 : }
336 :
337 : /*!
338 : * \internal
339 : * \brief Output a user-friendly form of an XML patchset
340 : *
341 : * This function parses an XML patchset (an \p XML_ATTR_DIFF element and its
342 : * children) into a user-friendly combined diff output.
343 : *
344 : * \param[in,out] out Output object
345 : * \param[in] args Message-specific arguments
346 : *
347 : * \return Standard Pacemaker return code
348 : *
349 : * \note \p args should contain the following:
350 : * -# XML patchset
351 : */
352 : PCMK__OUTPUT_ARGS("xml-patchset", "const xmlNode *")
353 : static int
354 0 : xml_patchset_log(pcmk__output_t *out, va_list args)
355 : {
356 : static struct qb_log_callsite *patchset_cs = NULL;
357 :
358 0 : const xmlNode *patchset = va_arg(args, const xmlNode *);
359 :
360 0 : uint8_t log_level = pcmk__output_get_log_level(out);
361 0 : int format = 1;
362 :
363 0 : if (log_level == LOG_NEVER) {
364 0 : return pcmk_rc_no_output;
365 : }
366 :
367 0 : if (patchset == NULL) {
368 0 : crm_trace("Empty patch");
369 0 : return pcmk_rc_no_output;
370 : }
371 :
372 0 : if (patchset_cs == NULL) {
373 0 : patchset_cs = qb_log_callsite_get(__func__, __FILE__, "xml-patchset",
374 : log_level, __LINE__,
375 : crm_trace_nonlog);
376 : }
377 :
378 0 : if (!crm_is_callsite_active(patchset_cs, log_level, crm_trace_nonlog)) {
379 : // Nothing would be logged, so skip all the work
380 0 : return pcmk_rc_no_output;
381 : }
382 :
383 0 : crm_element_value_int(patchset, PCMK_XA_FORMAT, &format);
384 0 : switch (format) {
385 0 : case 1:
386 0 : if (log_level < LOG_DEBUG) {
387 0 : return xml_show_patchset_v1(out, patchset,
388 : pcmk__xml_fmt_pretty
389 : |pcmk__xml_fmt_diff_short);
390 : }
391 0 : return xml_show_patchset_v1(out, patchset, pcmk__xml_fmt_pretty);
392 0 : case 2:
393 0 : return xml_show_patchset_v2(out, patchset);
394 0 : default:
395 0 : crm_err("Unknown patch format: %d", format);
396 0 : return pcmk_rc_bad_xml_patch;
397 : }
398 : }
399 :
400 : /*!
401 : * \internal
402 : * \brief Output an XML patchset
403 : *
404 : * This function outputs an XML patchset (an \p XML_ATTR_DIFF element and its
405 : * children) without modification, as a CDATA block.
406 : *
407 : * \param[in,out] out Output object
408 : * \param[in] args Message-specific arguments
409 : *
410 : * \return Standard Pacemaker return code
411 : *
412 : * \note \p args should contain the following:
413 : * -# XML patchset
414 : */
415 : PCMK__OUTPUT_ARGS("xml-patchset", "const xmlNode *")
416 : static int
417 0 : xml_patchset_xml(pcmk__output_t *out, va_list args)
418 : {
419 0 : const xmlNode *patchset = va_arg(args, const xmlNode *);
420 :
421 0 : if (patchset != NULL) {
422 0 : GString *buf = g_string_sized_new(1024);
423 :
424 0 : pcmk__xml_string(patchset, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, buf,
425 : 0);
426 :
427 0 : out->output_xml(out, PCMK_XE_XML_PATCHSET, buf->str);
428 0 : g_string_free(buf, TRUE);
429 0 : return pcmk_rc_ok;
430 : }
431 0 : crm_trace("Empty patch");
432 0 : return pcmk_rc_no_output;
433 : }
434 :
435 : static pcmk__message_entry_t fmt_functions[] = {
436 : { "xml-patchset", "default", xml_patchset_default },
437 : { "xml-patchset", "log", xml_patchset_log },
438 : { "xml-patchset", "xml", xml_patchset_xml },
439 :
440 : { NULL, NULL, NULL }
441 : };
442 :
443 : /*!
444 : * \internal
445 : * \brief Register the formatting functions for XML patchsets
446 : *
447 : * \param[in,out] out Output object
448 : */
449 : void
450 0 : pcmk__register_patchset_messages(pcmk__output_t *out) {
451 0 : pcmk__register_messages(out, fmt_functions);
452 0 : }
453 :
454 : // Deprecated functions kept only for backward API compatibility
455 : // LCOV_EXCL_START
456 :
457 : #include <crm/common/xml_compat.h>
458 :
459 : void
460 : xml_log_patchset(uint8_t log_level, const char *function,
461 : const xmlNode *patchset)
462 : {
463 : /* This function has some duplication relative to the message functions.
464 : * This way, we can maintain the const xmlNode * in the signature. The
465 : * message functions must be non-const. They have to support XML output
466 : * objects, which must make a copy of a the patchset, requiring a non-const
467 : * function call.
468 : *
469 : * In contrast, this legacy function doesn't need to support XML output.
470 : */
471 : static struct qb_log_callsite *patchset_cs = NULL;
472 :
473 : pcmk__output_t *out = NULL;
474 : int format = 1;
475 : int rc = pcmk_rc_no_output;
476 :
477 : switch (log_level) {
478 : case LOG_NEVER:
479 : return;
480 : case LOG_STDOUT:
481 : CRM_CHECK(pcmk__text_output_new(&out, NULL) == pcmk_rc_ok, return);
482 : break;
483 : default:
484 : if (patchset_cs == NULL) {
485 : patchset_cs = qb_log_callsite_get(__func__, __FILE__,
486 : "xml-patchset", log_level,
487 : __LINE__, crm_trace_nonlog);
488 : }
489 : if (!crm_is_callsite_active(patchset_cs, log_level,
490 : crm_trace_nonlog)) {
491 : return;
492 : }
493 : CRM_CHECK(pcmk__log_output_new(&out) == pcmk_rc_ok, return);
494 : pcmk__output_set_log_level(out, log_level);
495 : break;
496 : }
497 :
498 : if (patchset == NULL) {
499 : // Should come after the LOG_NEVER check
500 : crm_trace("Empty patch");
501 : goto done;
502 : }
503 :
504 : crm_element_value_int(patchset, PCMK_XA_FORMAT, &format);
505 : switch (format) {
506 : case 1:
507 : if (log_level < LOG_DEBUG) {
508 : rc = xml_show_patchset_v1(out, patchset,
509 : pcmk__xml_fmt_pretty
510 : |pcmk__xml_fmt_diff_short);
511 : } else { // Note: LOG_STDOUT > LOG_DEBUG
512 : rc = xml_show_patchset_v1(out, patchset, pcmk__xml_fmt_pretty);
513 : }
514 : break;
515 : case 2:
516 : rc = xml_show_patchset_v2(out, patchset);
517 : break;
518 : default:
519 : crm_err("Unknown patch format: %d", format);
520 : rc = pcmk_rc_bad_xml_patch;
521 : break;
522 : }
523 :
524 : done:
525 : out->finish(out, pcmk_rc2exitc(rc), true, NULL);
526 : pcmk__output_free(out);
527 : }
528 :
529 : // LCOV_EXCL_STOP
530 : // End deprecated API
|