/src/pacemaker/lib/common/patchset.c
Line | Count | Source |
1 | | /* |
2 | | * Copyright 2004-2025 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 <stdbool.h> |
13 | | #include <stdio.h> |
14 | | #include <sys/types.h> |
15 | | #include <unistd.h> |
16 | | #include <time.h> |
17 | | #include <string.h> |
18 | | #include <stdlib.h> |
19 | | #include <stdarg.h> |
20 | | #include <bzlib.h> |
21 | | |
22 | | #include <libxml/tree.h> // xmlNode |
23 | | |
24 | | #include <crm/crm.h> |
25 | | #include <crm/common/cib_internal.h> |
26 | | #include <crm/common/xml.h> |
27 | | #include <crm/common/xml_internal.h> // CRM_XML_LOG_BASE, etc. |
28 | | #include "crmcommon_private.h" |
29 | | |
30 | | static const char *const vfields[] = { |
31 | | PCMK_XA_ADMIN_EPOCH, |
32 | | PCMK_XA_EPOCH, |
33 | | PCMK_XA_NUM_UPDATES, |
34 | | }; |
35 | | |
36 | | /* Add changes for specified XML to patchset. |
37 | | * For patchset format, refer to diff schema. |
38 | | */ |
39 | | static void |
40 | | add_xml_changes_to_patchset(xmlNode *xml, xmlNode *patchset) |
41 | 0 | { |
42 | 0 | xmlNode *cIter = NULL; |
43 | 0 | xmlAttr *pIter = NULL; |
44 | 0 | xmlNode *change = NULL; |
45 | 0 | xml_node_private_t *nodepriv = xml->_private; |
46 | 0 | const char *value = NULL; |
47 | |
|
48 | 0 | if (nodepriv == NULL) { |
49 | | /* Elements that shouldn't occur in a CIB don't have _private set. They |
50 | | * should be stripped out, ignored, or have an error thrown by any code |
51 | | * that processes their parent, so we ignore any changes to them. |
52 | | */ |
53 | 0 | return; |
54 | 0 | } |
55 | | |
56 | | // If this XML node is new, just report that |
57 | 0 | if ((patchset != NULL) && pcmk__is_set(nodepriv->flags, pcmk__xf_created)) { |
58 | 0 | GString *xpath = pcmk__element_xpath(xml->parent); |
59 | |
|
60 | 0 | if (xpath != NULL) { |
61 | 0 | int position = pcmk__xml_position(xml, pcmk__xf_deleted); |
62 | |
|
63 | 0 | change = pcmk__xe_create(patchset, PCMK_XE_CHANGE); |
64 | |
|
65 | 0 | pcmk__xe_set(change, PCMK_XA_OPERATION, PCMK_VALUE_CREATE); |
66 | 0 | pcmk__xe_set(change, PCMK_XA_PATH, (const char *) xpath->str); |
67 | 0 | pcmk__xe_set_int(change, PCMK_XE_POSITION, position); |
68 | 0 | pcmk__xml_copy(change, xml); |
69 | 0 | g_string_free(xpath, TRUE); |
70 | 0 | } |
71 | |
|
72 | 0 | return; |
73 | 0 | } |
74 | | |
75 | | // Check each of the XML node's attributes for changes |
76 | 0 | for (pIter = pcmk__xe_first_attr(xml); pIter != NULL; |
77 | 0 | pIter = pIter->next) { |
78 | 0 | xmlNode *attr = NULL; |
79 | |
|
80 | 0 | nodepriv = pIter->_private; |
81 | 0 | if (!pcmk__any_flags_set(nodepriv->flags, |
82 | 0 | pcmk__xf_deleted|pcmk__xf_dirty)) { |
83 | 0 | continue; |
84 | 0 | } |
85 | | |
86 | 0 | if (change == NULL) { |
87 | 0 | GString *xpath = pcmk__element_xpath(xml); |
88 | |
|
89 | 0 | if (xpath != NULL) { |
90 | 0 | change = pcmk__xe_create(patchset, PCMK_XE_CHANGE); |
91 | |
|
92 | 0 | pcmk__xe_set(change, PCMK_XA_OPERATION, PCMK_VALUE_MODIFY); |
93 | 0 | pcmk__xe_set(change, PCMK_XA_PATH, (const char *) xpath->str); |
94 | |
|
95 | 0 | change = pcmk__xe_create(change, PCMK_XE_CHANGE_LIST); |
96 | 0 | g_string_free(xpath, TRUE); |
97 | 0 | } |
98 | 0 | } |
99 | |
|
100 | 0 | attr = pcmk__xe_create(change, PCMK_XE_CHANGE_ATTR); |
101 | |
|
102 | 0 | pcmk__xe_set(attr, PCMK_XA_NAME, (const char *) pIter->name); |
103 | 0 | if (nodepriv->flags & pcmk__xf_deleted) { |
104 | 0 | pcmk__xe_set(attr, PCMK_XA_OPERATION, "unset"); |
105 | |
|
106 | 0 | } else { |
107 | 0 | pcmk__xe_set(attr, PCMK_XA_OPERATION, "set"); |
108 | |
|
109 | 0 | value = pcmk__xml_attr_value(pIter); |
110 | 0 | pcmk__xe_set(attr, PCMK_XA_VALUE, value); |
111 | 0 | } |
112 | 0 | } |
113 | |
|
114 | 0 | if (change) { |
115 | 0 | xmlNode *result = NULL; |
116 | |
|
117 | 0 | change = pcmk__xe_create(change->parent, PCMK_XE_CHANGE_RESULT); |
118 | 0 | result = pcmk__xe_create(change, (const char *)xml->name); |
119 | |
|
120 | 0 | for (pIter = pcmk__xe_first_attr(xml); pIter != NULL; |
121 | 0 | pIter = pIter->next) { |
122 | 0 | nodepriv = pIter->_private; |
123 | 0 | if (!pcmk__is_set(nodepriv->flags, pcmk__xf_deleted)) { |
124 | 0 | value = pcmk__xe_get(xml, (const char *) pIter->name); |
125 | 0 | pcmk__xe_set(result, (const char *)pIter->name, value); |
126 | 0 | } |
127 | 0 | } |
128 | 0 | } |
129 | | |
130 | | // Now recursively do the same for each child node of this node |
131 | 0 | for (cIter = pcmk__xml_first_child(xml); cIter != NULL; |
132 | 0 | cIter = pcmk__xml_next(cIter)) { |
133 | 0 | add_xml_changes_to_patchset(cIter, patchset); |
134 | 0 | } |
135 | |
|
136 | 0 | nodepriv = xml->_private; |
137 | 0 | if ((patchset != NULL) && pcmk__is_set(nodepriv->flags, pcmk__xf_moved)) { |
138 | 0 | GString *xpath = pcmk__element_xpath(xml); |
139 | |
|
140 | 0 | pcmk__trace("%s.%s moved to position %d", xml->name, pcmk__xe_id(xml), |
141 | 0 | pcmk__xml_position(xml, pcmk__xf_skip)); |
142 | | |
143 | 0 | if (xpath != NULL) { |
144 | 0 | change = pcmk__xe_create(patchset, PCMK_XE_CHANGE); |
145 | |
|
146 | 0 | pcmk__xe_set(change, PCMK_XA_OPERATION, PCMK_VALUE_MOVE); |
147 | 0 | pcmk__xe_set(change, PCMK_XA_PATH, (const char *) xpath->str); |
148 | 0 | pcmk__xe_set_int(change, PCMK_XE_POSITION, |
149 | 0 | pcmk__xml_position(xml, pcmk__xf_deleted)); |
150 | 0 | g_string_free(xpath, TRUE); |
151 | 0 | } |
152 | 0 | } |
153 | 0 | } |
154 | | |
155 | | static bool |
156 | | is_config_change(xmlNode *xml) |
157 | 0 | { |
158 | 0 | GList *gIter = NULL; |
159 | 0 | xml_node_private_t *nodepriv = NULL; |
160 | 0 | xml_doc_private_t *docpriv; |
161 | 0 | xmlNode *config = pcmk__xe_first_child(xml, PCMK_XE_CONFIGURATION, NULL, |
162 | 0 | NULL); |
163 | |
|
164 | 0 | if (config) { |
165 | 0 | nodepriv = config->_private; |
166 | 0 | } |
167 | 0 | if ((nodepriv != NULL) && pcmk__is_set(nodepriv->flags, pcmk__xf_dirty)) { |
168 | 0 | return TRUE; |
169 | 0 | } |
170 | | |
171 | 0 | if ((xml->doc != NULL) && (xml->doc->_private != NULL)) { |
172 | 0 | docpriv = xml->doc->_private; |
173 | 0 | for (gIter = docpriv->deleted_objs; gIter; gIter = gIter->next) { |
174 | 0 | pcmk__deleted_xml_t *deleted_obj = gIter->data; |
175 | |
|
176 | 0 | if (strstr(deleted_obj->path, |
177 | 0 | "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION) != NULL) { |
178 | 0 | return TRUE; |
179 | 0 | } |
180 | 0 | } |
181 | 0 | } |
182 | 0 | return FALSE; |
183 | 0 | } |
184 | | |
185 | | static xmlNode * |
186 | | xml_create_patchset_v2(xmlNode *source, xmlNode *target) |
187 | 0 | { |
188 | 0 | int lpc = 0; |
189 | 0 | GList *gIter = NULL; |
190 | 0 | xml_doc_private_t *docpriv; |
191 | |
|
192 | 0 | xmlNode *v = NULL; |
193 | 0 | xmlNode *version = NULL; |
194 | 0 | xmlNode *patchset = NULL; |
195 | |
|
196 | 0 | pcmk__assert(target != NULL); |
197 | |
|
198 | 0 | if (!pcmk__xml_doc_all_flags_set(target->doc, pcmk__xf_dirty)) { |
199 | 0 | return NULL; |
200 | 0 | } |
201 | | |
202 | 0 | pcmk__assert(target->doc != NULL); |
203 | 0 | docpriv = target->doc->_private; |
204 | |
|
205 | 0 | patchset = pcmk__xe_create(NULL, PCMK_XE_DIFF); |
206 | 0 | pcmk__xe_set_int(patchset, PCMK_XA_FORMAT, 2); |
207 | |
|
208 | 0 | version = pcmk__xe_create(patchset, PCMK_XE_VERSION); |
209 | |
|
210 | 0 | v = pcmk__xe_create(version, PCMK_XE_SOURCE); |
211 | 0 | for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { |
212 | 0 | const char *value = pcmk__xe_get(source, vfields[lpc]); |
213 | |
|
214 | 0 | if (value == NULL) { |
215 | 0 | value = "1"; |
216 | 0 | } |
217 | 0 | pcmk__xe_set(v, vfields[lpc], value); |
218 | 0 | } |
219 | |
|
220 | 0 | v = pcmk__xe_create(version, PCMK_XE_TARGET); |
221 | 0 | for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { |
222 | 0 | const char *value = pcmk__xe_get(target, vfields[lpc]); |
223 | |
|
224 | 0 | if (value == NULL) { |
225 | 0 | value = "1"; |
226 | 0 | } |
227 | 0 | pcmk__xe_set(v, vfields[lpc], value); |
228 | 0 | } |
229 | |
|
230 | 0 | for (gIter = docpriv->deleted_objs; gIter; gIter = gIter->next) { |
231 | 0 | pcmk__deleted_xml_t *deleted_obj = gIter->data; |
232 | 0 | xmlNode *change = pcmk__xe_create(patchset, PCMK_XE_CHANGE); |
233 | |
|
234 | 0 | pcmk__xe_set(change, PCMK_XA_OPERATION, PCMK_VALUE_DELETE); |
235 | 0 | pcmk__xe_set(change, PCMK_XA_PATH, deleted_obj->path); |
236 | 0 | if (deleted_obj->position >= 0) { |
237 | 0 | pcmk__xe_set_int(change, PCMK_XE_POSITION, deleted_obj->position); |
238 | 0 | } |
239 | 0 | } |
240 | |
|
241 | 0 | add_xml_changes_to_patchset(target, patchset); |
242 | 0 | return patchset; |
243 | 0 | } |
244 | | |
245 | | xmlNode * |
246 | | xml_create_patchset(int format, xmlNode *source, xmlNode *target, |
247 | | bool *config_changed, bool manage_version) |
248 | 0 | { |
249 | 0 | bool local_config_changed = false; |
250 | |
|
251 | 0 | if (format == 0) { |
252 | 0 | format = 2; |
253 | 0 | } |
254 | 0 | if (format != 2) { |
255 | 0 | pcmk__err("Unknown patch format: %d", format); |
256 | 0 | return NULL; |
257 | 0 | } |
258 | | |
259 | 0 | xml_acl_disable(target); |
260 | 0 | if ((target == NULL) |
261 | 0 | || !pcmk__xml_doc_all_flags_set(target->doc, pcmk__xf_dirty)) { |
262 | |
|
263 | 0 | pcmk__trace("No change %d", format); |
264 | 0 | return NULL; |
265 | 0 | } |
266 | | |
267 | 0 | if (config_changed == NULL) { |
268 | 0 | config_changed = &local_config_changed; |
269 | 0 | } |
270 | 0 | *config_changed = is_config_change(target); |
271 | |
|
272 | 0 | if (manage_version) { |
273 | 0 | int counter = 0; |
274 | |
|
275 | 0 | if (*config_changed) { |
276 | 0 | pcmk__xe_set(target, PCMK_XA_NUM_UPDATES, "0"); |
277 | |
|
278 | 0 | pcmk__xe_get_int(target, PCMK_XA_EPOCH, &counter); |
279 | 0 | pcmk__xe_set_int(target, PCMK_XA_EPOCH, counter + 1); |
280 | |
|
281 | 0 | } else { |
282 | 0 | pcmk__xe_get_int(target, PCMK_XA_NUM_UPDATES, &counter); |
283 | 0 | pcmk__xe_set_int(target, PCMK_XA_NUM_UPDATES, counter + 1); |
284 | 0 | } |
285 | 0 | } |
286 | |
|
287 | 0 | return xml_create_patchset_v2(source, target); |
288 | 0 | } |
289 | | |
290 | | /*! |
291 | | * \internal |
292 | | * \brief Add a digest of a patchset's target XML to the patchset |
293 | | * |
294 | | * \param[in,out] patchset XML patchset |
295 | | * \param[in] target Target XML |
296 | | */ |
297 | | void |
298 | | pcmk__xml_patchset_add_digest(xmlNode *patchset, const xmlNode *target) |
299 | 0 | { |
300 | 0 | char *digest = NULL; |
301 | |
|
302 | 0 | CRM_CHECK((patchset != NULL) && (target != NULL), return); |
303 | | |
304 | | /* If tracking is enabled and the document is dirty, we could get an |
305 | | * incorrect digest. Call pcmk__xml_commit_changes() before calling this. |
306 | | */ |
307 | 0 | CRM_CHECK(!pcmk__xml_doc_all_flags_set(target->doc, pcmk__xf_dirty), |
308 | 0 | return); |
309 | | |
310 | 0 | digest = pcmk__digest_xml(target, true); |
311 | |
|
312 | 0 | pcmk__xe_set(patchset, PCMK_XA_DIGEST, digest); |
313 | 0 | free(digest); |
314 | 0 | } |
315 | | |
316 | | /*! |
317 | | * \internal |
318 | | * \brief Get the source and target CIB versions from an XML patchset |
319 | | * |
320 | | * Each output object will contain, in order, the following version fields from |
321 | | * the source and target, respectively: |
322 | | * * \c PCMK_XA_ADMIN_EPOCH |
323 | | * * \c PCMK_XA_EPOCH |
324 | | * * \c PCMK_XA_NUM_UPDATES |
325 | | * |
326 | | * If source versions or target versions are absent from the patchset, then |
327 | | * \p source and \p target (respectively) are left unmodified. This is not |
328 | | * treated as an error. An unparsable version is an error, however. |
329 | | * |
330 | | * \param[in] patchset XML patchset |
331 | | * \param[out] source Where to store versions from source CIB |
332 | | * \param[out] target Where to store versions from target CIB |
333 | | * |
334 | | * \return Standard Pacemaker return code |
335 | | */ |
336 | | int |
337 | | pcmk__xml_patchset_versions(const xmlNode *patchset, int source[3], |
338 | | int target[3]) |
339 | 0 | { |
340 | 0 | int format = 0; |
341 | 0 | const xmlNode *version = NULL; |
342 | 0 | const xmlNode *source_xml = NULL; |
343 | 0 | const xmlNode *target_xml = NULL; |
344 | |
|
345 | 0 | CRM_CHECK((patchset != NULL) && (source != NULL) && (target != NULL), |
346 | 0 | return EINVAL); |
347 | | |
348 | 0 | pcmk__xe_get_int(patchset, PCMK_XA_FORMAT, &format); |
349 | 0 | if (format != 2) { |
350 | 0 | pcmk__err("Unknown patch format: %d", format); |
351 | 0 | return EINVAL; |
352 | 0 | } |
353 | | |
354 | 0 | version = pcmk__xe_first_child(patchset, PCMK_XE_VERSION, NULL, NULL); |
355 | 0 | source_xml = pcmk__xe_first_child(version, PCMK_XE_SOURCE, NULL, NULL); |
356 | 0 | target_xml = pcmk__xe_first_child(version, PCMK_XE_TARGET, NULL, NULL); |
357 | | |
358 | | /* @COMPAT Consider requiring source_xml and target_xml to be non-NULL. As |
359 | | * of pcs version 0.10.8, pcs creates a patchset using crm_diff |
360 | | * --no-version. The behavior and documentation of the crm_diff options |
361 | | * --cib and --no-version are questionable and should be re-examined. Even |
362 | | * without --no-version, crm_diff does not update the target version in the |
363 | | * generated patchset. So a diff based on a manual CIB XML edit is likely to |
364 | | * have unchanged version numbers. (Pacemaker tools bump the CIB versions |
365 | | * automatically when editing the CIB.) |
366 | | * |
367 | | * Until then, we may be applying a patchset that has no version info. We |
368 | | * will allow either source version or target version to be missing (even |
369 | | * though both should be present or both should be missing). However, return |
370 | | * an error if any of the three vfields is missing from a source or target |
371 | | * version element that is present. That level of sanity check should be |
372 | | * okay. |
373 | | * |
374 | | * We leave the destination arrays unmodified in case of absent versions, |
375 | | * instead of setting them to some default value like { 0, 0, 0 }. |
376 | | * xml_patch_version_check() sets its own defaults in case of absent |
377 | | * versions. |
378 | | */ |
379 | 0 | for (int i = 0; i < PCMK__NELEM(vfields); i++) { |
380 | 0 | if (source_xml != NULL) { |
381 | 0 | if (pcmk__xe_get_int(source_xml, vfields[i], |
382 | 0 | &(source[i])) != pcmk_rc_ok) { |
383 | 0 | return EINVAL; |
384 | 0 | } |
385 | 0 | pcmk__trace("Got source[%s]=%d", vfields[i], source[i]); |
386 | |
|
387 | 0 | } else { |
388 | 0 | pcmk__trace("No source versions found; keeping source[%s]=%d", |
389 | 0 | vfields[i], source[i]); |
390 | 0 | } |
391 | | |
392 | 0 | if (target_xml != NULL) { |
393 | 0 | if (pcmk__xe_get_int(target_xml, vfields[i], |
394 | 0 | &(target[i])) != pcmk_rc_ok) { |
395 | 0 | return EINVAL; |
396 | 0 | } |
397 | 0 | pcmk__trace("Got target[%s]=%d", vfields[i], target[i]); |
398 | |
|
399 | 0 | } else { |
400 | 0 | pcmk__trace("No target versions found; keeping target[%s]=%d", |
401 | 0 | vfields[i], target[i]); |
402 | 0 | } |
403 | 0 | } |
404 | | |
405 | 0 | return pcmk_rc_ok; |
406 | 0 | } |
407 | | |
408 | | /*! |
409 | | * \internal |
410 | | * \brief Check whether patchset can be applied to current CIB |
411 | | * |
412 | | * \param[in] cib_root Root of current CIB |
413 | | * \param[in] patchset Patchset to check |
414 | | * |
415 | | * \return Standard Pacemaker return code |
416 | | */ |
417 | | static int |
418 | | check_patchset_versions(const xmlNode *cib_root, const xmlNode *patchset) |
419 | 0 | { |
420 | 0 | int current[] = { 0, 0, 0 }; |
421 | 0 | int source[] = { 0, 0, 0 }; |
422 | 0 | int target[] = { 0, 0, 0 }; |
423 | 0 | int rc = pcmk_rc_ok; |
424 | |
|
425 | 0 | for (int i = 0; i < PCMK__NELEM(vfields); i++) { |
426 | | /* @COMPAT We should probably fail with EINVAL for negative or invalid. |
427 | | * |
428 | | * Preserve behavior for xml_apply_patchset(). Use new behavior in a |
429 | | * future replacement. |
430 | | */ |
431 | 0 | if (pcmk__xe_get_int(cib_root, vfields[i], |
432 | 0 | &(current[i])) == pcmk_rc_ok) { |
433 | 0 | pcmk__trace("Got %d for current[%s]%s", current[i], vfields[i], |
434 | 0 | ((current[i] < 0)? ", using 0" : "")); |
435 | 0 | } else { |
436 | 0 | pcmk__debug("Failed to get value for current[%s], using 0", |
437 | 0 | vfields[i]); |
438 | 0 | } |
439 | 0 | if (current[i] < 0) { |
440 | 0 | current[i] = 0; |
441 | 0 | } |
442 | 0 | } |
443 | | |
444 | | /* Set some defaults in case nothing is present. |
445 | | * |
446 | | * @COMPAT We should probably skip this step, and fail immediately below if |
447 | | * target[i] < source[i]. |
448 | | * |
449 | | * Preserve behavior for xml_apply_patchset(). Use new behavior in a future |
450 | | * replacement. |
451 | | */ |
452 | 0 | target[0] = current[0]; |
453 | 0 | target[1] = current[1]; |
454 | 0 | target[2] = current[2] + 1; |
455 | 0 | for (int i = 0; i < PCMK__NELEM(vfields); i++) { |
456 | 0 | source[i] = current[i]; |
457 | 0 | } |
458 | |
|
459 | 0 | rc = pcmk__xml_patchset_versions(patchset, source, target); |
460 | 0 | if (rc != pcmk_rc_ok) { |
461 | 0 | return rc; |
462 | 0 | } |
463 | | |
464 | | // Ensure current version matches patchset source version |
465 | 0 | for (int i = 0; i < PCMK__NELEM(vfields); i++) { |
466 | 0 | if (current[i] < source[i]) { |
467 | 0 | pcmk__debug("Current %s is too low " |
468 | 0 | "(%d.%d.%d < %d.%d.%d --> %d.%d.%d)", |
469 | 0 | vfields[i], current[0], current[1], current[2], |
470 | 0 | source[0], source[1], source[2], |
471 | 0 | target[0], target[1], target[2]); |
472 | 0 | return pcmk_rc_diff_resync; |
473 | 0 | } |
474 | 0 | if (current[i] > source[i]) { |
475 | 0 | pcmk__info("Current %s is too high " |
476 | 0 | "(%d.%d.%d > %d.%d.%d --> %d.%d.%d)", |
477 | 0 | vfields[i], current[0], current[1], current[2], |
478 | 0 | source[0], source[1], source[2], |
479 | 0 | target[0], target[1], target[2]); |
480 | 0 | pcmk__log_xml_info(patchset, "OldPatch"); |
481 | 0 | return pcmk_rc_old_data; |
482 | 0 | } |
483 | 0 | } |
484 | | |
485 | | // Ensure target version is newer than source version |
486 | 0 | for (int i = 0; i < PCMK__NELEM(vfields); i++) { |
487 | 0 | if (target[i] > source[i]) { |
488 | 0 | pcmk__debug("Can apply patch %d.%d.%d to %d.%d.%d", |
489 | 0 | target[0], target[1], target[2], |
490 | 0 | current[0], current[1], current[2]); |
491 | 0 | return pcmk_rc_ok; |
492 | 0 | } |
493 | 0 | } |
494 | | |
495 | 0 | pcmk__notice("Versions did not change in patch %d.%d.%d", |
496 | 0 | target[0], target[1], target[2]); |
497 | 0 | return pcmk_rc_old_data; |
498 | 0 | } |
499 | | |
500 | | // Return first child matching element name and optionally id or position |
501 | | static xmlNode * |
502 | | first_matching_xml_child(const xmlNode *parent, const char *name, |
503 | | const char *id, int position) |
504 | 0 | { |
505 | 0 | xmlNode *cIter = NULL; |
506 | |
|
507 | 0 | for (cIter = pcmk__xml_first_child(parent); cIter != NULL; |
508 | 0 | cIter = pcmk__xml_next(cIter)) { |
509 | 0 | if (strcmp((const char *) cIter->name, name) != 0) { |
510 | 0 | continue; |
511 | 0 | } else if (id) { |
512 | 0 | const char *cid = pcmk__xe_id(cIter); |
513 | |
|
514 | 0 | if ((cid == NULL) || (strcmp(cid, id) != 0)) { |
515 | 0 | continue; |
516 | 0 | } |
517 | 0 | } |
518 | | |
519 | | // "position" makes sense only for XML comments for now |
520 | 0 | if ((cIter->type == XML_COMMENT_NODE) |
521 | 0 | && (position >= 0) |
522 | 0 | && (pcmk__xml_position(cIter, pcmk__xf_skip) != position)) { |
523 | 0 | continue; |
524 | 0 | } |
525 | | |
526 | 0 | return cIter; |
527 | 0 | } |
528 | 0 | return NULL; |
529 | 0 | } |
530 | | |
531 | | /*! |
532 | | * \internal |
533 | | * \brief Simplified, more efficient alternative to pcmk__xpath_find_one() |
534 | | * |
535 | | * \param[in] top Root of XML to search |
536 | | * \param[in] key Search xpath |
537 | | * \param[in] target_position If deleting, where to delete |
538 | | * |
539 | | * \return XML child matching xpath if found, NULL otherwise |
540 | | * |
541 | | * \note This only works on simplified xpaths found in v2 patchset diffs, |
542 | | * i.e. the only allowed search predicate is [@id='XXX']. |
543 | | */ |
544 | | static xmlNode * |
545 | | search_v2_xpath(const xmlNode *top, const char *key, int target_position) |
546 | 0 | { |
547 | 0 | xmlNode *target = (xmlNode *) top->doc; |
548 | 0 | const char *current = key; |
549 | 0 | char *section; |
550 | 0 | char *remainder; |
551 | 0 | char *id; |
552 | 0 | char *tag; |
553 | 0 | int rc; |
554 | 0 | size_t key_len; |
555 | |
|
556 | 0 | CRM_CHECK(key != NULL, return NULL); |
557 | 0 | key_len = strlen(key); |
558 | | |
559 | | /* These are scanned from key after a slash, so they can't be bigger |
560 | | * than key_len - 1 characters plus a null terminator. |
561 | | */ |
562 | |
|
563 | 0 | remainder = pcmk__assert_alloc(key_len, sizeof(char)); |
564 | 0 | section = pcmk__assert_alloc(key_len, sizeof(char)); |
565 | 0 | id = pcmk__assert_alloc(key_len, sizeof(char)); |
566 | 0 | tag = pcmk__assert_alloc(key_len, sizeof(char)); |
567 | |
|
568 | 0 | do { |
569 | | // Look for /NEXT_COMPONENT/REMAINING_COMPONENTS |
570 | 0 | rc = sscanf(current, "/%[^/]%s", section, remainder); |
571 | 0 | if (rc > 0) { |
572 | | // Separate FIRST_COMPONENT into TAG[@id='ID'] |
573 | 0 | int f = sscanf(section, "%[^[][@" PCMK_XA_ID "='%[^']", tag, id); |
574 | 0 | int current_position = -1; |
575 | | |
576 | | /* The target position is for the final component tag, so only use |
577 | | * it if there is nothing left to search after this component. |
578 | | */ |
579 | 0 | if ((rc == 1) && (target_position >= 0)) { |
580 | 0 | current_position = target_position; |
581 | 0 | } |
582 | |
|
583 | 0 | switch (f) { |
584 | 0 | case 1: |
585 | 0 | target = first_matching_xml_child(target, tag, NULL, |
586 | 0 | current_position); |
587 | 0 | break; |
588 | 0 | case 2: |
589 | 0 | target = first_matching_xml_child(target, tag, id, |
590 | 0 | current_position); |
591 | 0 | break; |
592 | 0 | default: |
593 | | // This should not be possible |
594 | 0 | target = NULL; |
595 | 0 | break; |
596 | 0 | } |
597 | 0 | current = remainder; |
598 | 0 | } |
599 | | |
600 | | // Continue if something remains to search, and we've matched so far |
601 | 0 | } while ((rc == 2) && target); |
602 | | |
603 | 0 | if (target) { |
604 | 0 | pcmk__if_tracing( |
605 | 0 | { |
606 | 0 | char *path = (char *) xmlGetNodePath(target); |
607 | |
|
608 | 0 | pcmk__trace("Found %s for %s", path, key); |
609 | 0 | free(path); |
610 | 0 | }, |
611 | 0 | {} |
612 | 0 | ); |
613 | 0 | } else { |
614 | 0 | pcmk__debug("No match for %s", key); |
615 | 0 | } |
616 | | |
617 | 0 | free(remainder); |
618 | 0 | free(section); |
619 | 0 | free(tag); |
620 | 0 | free(id); |
621 | 0 | return target; |
622 | 0 | } |
623 | | |
624 | | typedef struct xml_change_obj_s { |
625 | | const xmlNode *change; |
626 | | xmlNode *match; |
627 | | } xml_change_obj_t; |
628 | | |
629 | | static gint |
630 | | sort_change_obj_by_position(gconstpointer a, gconstpointer b) |
631 | 0 | { |
632 | 0 | const xml_change_obj_t *change_obj_a = a; |
633 | 0 | const xml_change_obj_t *change_obj_b = b; |
634 | 0 | int position_a = -1; |
635 | 0 | int position_b = -1; |
636 | |
|
637 | 0 | pcmk__xe_get_int(change_obj_a->change, PCMK_XE_POSITION, &position_a); |
638 | 0 | pcmk__xe_get_int(change_obj_b->change, PCMK_XE_POSITION, &position_b); |
639 | |
|
640 | 0 | if (position_a < position_b) { |
641 | 0 | return -1; |
642 | |
|
643 | 0 | } else if (position_a > position_b) { |
644 | 0 | return 1; |
645 | 0 | } |
646 | | |
647 | 0 | return 0; |
648 | 0 | } |
649 | | |
650 | | /*! |
651 | | * \internal |
652 | | * \brief Apply a version 2 patchset to an XML node |
653 | | * |
654 | | * \param[in,out] xml XML to apply patchset to |
655 | | * \param[in] patchset Patchset to apply |
656 | | * |
657 | | * \return Standard Pacemaker return code |
658 | | */ |
659 | | static int |
660 | | apply_v2_patchset(xmlNode *xml, const xmlNode *patchset) |
661 | 0 | { |
662 | 0 | int rc = pcmk_rc_ok; |
663 | 0 | const xmlNode *change = NULL; |
664 | 0 | GList *change_objs = NULL; |
665 | 0 | GList *gIter = NULL; |
666 | |
|
667 | 0 | for (change = pcmk__xml_first_child(patchset); change != NULL; |
668 | 0 | change = pcmk__xml_next(change)) { |
669 | 0 | xmlNode *match = NULL; |
670 | 0 | const char *op = pcmk__xe_get(change, PCMK_XA_OPERATION); |
671 | 0 | const char *xpath = pcmk__xe_get(change, PCMK_XA_PATH); |
672 | 0 | int position = -1; |
673 | |
|
674 | 0 | if (op == NULL) { |
675 | 0 | continue; |
676 | 0 | } |
677 | | |
678 | 0 | pcmk__trace("Processing %s %s", change->name, op); |
679 | | |
680 | | /* PCMK_VALUE_DELETE changes for XML comments are generated with |
681 | | * PCMK_XE_POSITION |
682 | | */ |
683 | 0 | if (strcmp(op, PCMK_VALUE_DELETE) == 0) { |
684 | 0 | pcmk__xe_get_int(change, PCMK_XE_POSITION, &position); |
685 | 0 | } |
686 | 0 | match = search_v2_xpath(xml, xpath, position); |
687 | 0 | pcmk__trace("Performing %s on %s with %p", op, xpath, match); |
688 | | |
689 | 0 | if ((match == NULL) && (strcmp(op, PCMK_VALUE_DELETE) == 0)) { |
690 | 0 | pcmk__debug("No %s match for %s in %p", op, xpath, xml->doc); |
691 | 0 | continue; |
692 | |
|
693 | 0 | } else if (match == NULL) { |
694 | 0 | pcmk__err("No %s match for %s in %p", op, xpath, xml->doc); |
695 | 0 | rc = pcmk_rc_diff_failed; |
696 | 0 | continue; |
697 | |
|
698 | 0 | } else if (pcmk__str_any_of(op, |
699 | 0 | PCMK_VALUE_CREATE, PCMK_VALUE_MOVE, NULL)) { |
700 | | // Delay the adding of a PCMK_VALUE_CREATE object |
701 | 0 | xml_change_obj_t *change_obj = |
702 | 0 | pcmk__assert_alloc(1, sizeof(xml_change_obj_t)); |
703 | |
|
704 | 0 | change_obj->change = change; |
705 | 0 | change_obj->match = match; |
706 | |
|
707 | 0 | change_objs = g_list_append(change_objs, change_obj); |
708 | |
|
709 | 0 | if (strcmp(op, PCMK_VALUE_MOVE) == 0) { |
710 | | // Temporarily put the PCMK_VALUE_MOVE object after the last sibling |
711 | 0 | if ((match->parent != NULL) && (match->parent->last != NULL)) { |
712 | 0 | xmlAddNextSibling(match->parent->last, match); |
713 | 0 | } |
714 | 0 | } |
715 | |
|
716 | 0 | } else if (strcmp(op, PCMK_VALUE_DELETE) == 0) { |
717 | 0 | pcmk__xml_free(match); |
718 | |
|
719 | 0 | } else if (strcmp(op, PCMK_VALUE_MODIFY) == 0) { |
720 | 0 | const xmlNode *child = pcmk__xe_first_child(change, |
721 | 0 | PCMK_XE_CHANGE_RESULT, |
722 | 0 | NULL, NULL); |
723 | 0 | const xmlNode *attrs = pcmk__xml_first_child(child); |
724 | |
|
725 | 0 | if (attrs == NULL) { |
726 | 0 | rc = ENOMSG; |
727 | 0 | continue; |
728 | 0 | } |
729 | | |
730 | | // Remove all attributes |
731 | 0 | pcmk__xe_remove_matching_attrs(match, false, NULL, NULL); |
732 | |
|
733 | 0 | for (xmlAttrPtr pIter = pcmk__xe_first_attr(attrs); pIter != NULL; |
734 | 0 | pIter = pIter->next) { |
735 | 0 | const char *name = (const char *) pIter->name; |
736 | 0 | const char *value = pcmk__xml_attr_value(pIter); |
737 | |
|
738 | 0 | pcmk__xe_set(match, name, value); |
739 | 0 | } |
740 | |
|
741 | 0 | } else { |
742 | 0 | pcmk__err("Unknown operation: %s", op); |
743 | 0 | rc = pcmk_rc_diff_failed; |
744 | 0 | } |
745 | 0 | } |
746 | | |
747 | | // Changes should be generated in the right order. Double checking. |
748 | 0 | change_objs = g_list_sort(change_objs, sort_change_obj_by_position); |
749 | |
|
750 | 0 | for (gIter = change_objs; gIter; gIter = gIter->next) { |
751 | 0 | xml_change_obj_t *change_obj = gIter->data; |
752 | 0 | xmlNode *match = change_obj->match; |
753 | 0 | const char *op = NULL; |
754 | 0 | const char *xpath = NULL; |
755 | |
|
756 | 0 | change = change_obj->change; |
757 | |
|
758 | 0 | op = pcmk__xe_get(change, PCMK_XA_OPERATION); |
759 | 0 | xpath = pcmk__xe_get(change, PCMK_XA_PATH); |
760 | |
|
761 | 0 | pcmk__trace("Continue performing %s on %s with %p", op, xpath, match); |
762 | | |
763 | 0 | if (strcmp(op, PCMK_VALUE_CREATE) == 0) { |
764 | 0 | int position = 0; |
765 | 0 | xmlNode *child = NULL; |
766 | 0 | xmlNode *match_child = NULL; |
767 | |
|
768 | 0 | match_child = match->children; |
769 | 0 | pcmk__xe_get_int(change, PCMK_XE_POSITION, &position); |
770 | |
|
771 | 0 | while ((match_child != NULL) |
772 | 0 | && (position != pcmk__xml_position(match_child, pcmk__xf_skip))) { |
773 | 0 | match_child = match_child->next; |
774 | 0 | } |
775 | |
|
776 | 0 | child = pcmk__xml_copy(match, change->children); |
777 | |
|
778 | 0 | if (match_child != NULL) { |
779 | 0 | pcmk__trace("Adding %s at position %d", child->name, position); |
780 | 0 | xmlAddPrevSibling(match_child, child); |
781 | |
|
782 | 0 | } else { |
783 | 0 | pcmk__trace("Adding %s at position %d (end)", child->name, |
784 | 0 | position); |
785 | 0 | } |
786 | |
|
787 | 0 | } else if (strcmp(op, PCMK_VALUE_MOVE) == 0) { |
788 | 0 | int position = 0; |
789 | |
|
790 | 0 | pcmk__xe_get_int(change, PCMK_XE_POSITION, &position); |
791 | 0 | if (position != pcmk__xml_position(match, pcmk__xf_skip)) { |
792 | 0 | xmlNode *match_child = NULL; |
793 | 0 | int p = position; |
794 | |
|
795 | 0 | if (p > pcmk__xml_position(match, pcmk__xf_skip)) { |
796 | 0 | p++; // Skip ourselves |
797 | 0 | } |
798 | |
|
799 | 0 | pcmk__assert(match->parent != NULL); |
800 | 0 | match_child = match->parent->children; |
801 | |
|
802 | 0 | while ((match_child != NULL) |
803 | 0 | && (p != pcmk__xml_position(match_child, pcmk__xf_skip))) { |
804 | 0 | match_child = match_child->next; |
805 | 0 | } |
806 | |
|
807 | 0 | pcmk__trace("Moving %s to position %d (was %d, prev %p, %s %p)", |
808 | 0 | match->name, position, |
809 | 0 | pcmk__xml_position(match, pcmk__xf_skip), |
810 | 0 | match->prev, |
811 | 0 | ((match_child != NULL)? "next" : "last"), |
812 | 0 | ((match_child != NULL)? match_child |
813 | 0 | : match->parent->last)); |
814 | | |
815 | 0 | if (match_child) { |
816 | 0 | xmlAddPrevSibling(match_child, match); |
817 | |
|
818 | 0 | } else { |
819 | 0 | pcmk__assert(match->parent->last != NULL); |
820 | 0 | xmlAddNextSibling(match->parent->last, match); |
821 | 0 | } |
822 | |
|
823 | 0 | } else { |
824 | 0 | pcmk__trace("%s is already in position %d", match->name, |
825 | 0 | position); |
826 | 0 | } |
827 | | |
828 | 0 | if (position != pcmk__xml_position(match, pcmk__xf_skip)) { |
829 | 0 | pcmk__err("Moved %s.%s to position %d instead of %d (%p)", |
830 | 0 | match->name, pcmk__xe_id(match), |
831 | 0 | pcmk__xml_position(match, pcmk__xf_skip), |
832 | 0 | position, match->prev); |
833 | 0 | rc = pcmk_rc_diff_failed; |
834 | 0 | } |
835 | 0 | } |
836 | 0 | } |
837 | | |
838 | 0 | g_list_free_full(change_objs, free); |
839 | 0 | return rc; |
840 | 0 | } |
841 | | |
842 | | int |
843 | | xml_apply_patchset(xmlNode *xml, const xmlNode *patchset, bool check_version) |
844 | 0 | { |
845 | 0 | int format = 1; |
846 | 0 | int rc = pcmk_ok; |
847 | 0 | xmlNode *old = NULL; |
848 | 0 | const char *digest = NULL; |
849 | |
|
850 | 0 | if (patchset == NULL) { |
851 | 0 | return rc; |
852 | 0 | } |
853 | | |
854 | 0 | pcmk__log_xml_patchset(LOG_TRACE, patchset); |
855 | | |
856 | 0 | if (check_version) { |
857 | 0 | rc = pcmk_rc2legacy(check_patchset_versions(xml, patchset)); |
858 | 0 | if (rc != pcmk_ok) { |
859 | 0 | return rc; |
860 | 0 | } |
861 | 0 | } |
862 | | |
863 | 0 | digest = pcmk__xe_get(patchset, PCMK_XA_DIGEST); |
864 | 0 | if (digest != NULL) { |
865 | | /* Make original XML available for logging in case result doesn't have |
866 | | * expected digest |
867 | | */ |
868 | 0 | pcmk__if_tracing(old = pcmk__xml_copy(NULL, xml), {}); |
869 | 0 | } |
870 | |
|
871 | 0 | if (rc == pcmk_ok) { |
872 | 0 | pcmk__xe_get_int(patchset, PCMK_XA_FORMAT, &format); |
873 | |
|
874 | 0 | if (format != 2) { |
875 | 0 | pcmk__err("Unknown patch format: %d", format); |
876 | 0 | rc = -EINVAL; |
877 | |
|
878 | 0 | } else { |
879 | 0 | rc = pcmk_rc2legacy(apply_v2_patchset(xml, patchset)); |
880 | 0 | } |
881 | 0 | } |
882 | |
|
883 | 0 | if ((rc == pcmk_ok) && (digest != NULL)) { |
884 | 0 | char *new_digest = NULL; |
885 | |
|
886 | 0 | new_digest = pcmk__digest_xml(xml, true); |
887 | 0 | if (!pcmk__str_eq(new_digest, digest, pcmk__str_casei)) { |
888 | 0 | pcmk__info("v%d digest mis-match: expected %s, calculated %s", |
889 | 0 | format, digest, new_digest); |
890 | 0 | rc = -pcmk_err_diff_failed; |
891 | 0 | pcmk__if_tracing( |
892 | 0 | { |
893 | 0 | pcmk__xml_write_temp_file(old, "PatchDigest:input", NULL); |
894 | 0 | pcmk__xml_write_temp_file(xml, "PatchDigest:result", NULL); |
895 | 0 | pcmk__xml_write_temp_file(patchset, "PatchDigest:diff", |
896 | 0 | NULL); |
897 | 0 | }, |
898 | 0 | {} |
899 | 0 | ); |
900 | |
|
901 | 0 | } else { |
902 | 0 | pcmk__trace("v%d digest matched: expected %s, calculated %s", |
903 | 0 | format, digest, new_digest); |
904 | 0 | } |
905 | 0 | free(new_digest); |
906 | 0 | } |
907 | 0 | pcmk__xml_free(old); |
908 | 0 | return rc; |
909 | 0 | } |
910 | | |
911 | | /*! |
912 | | * \internal |
913 | | * \brief Check whether a given CIB element was modified in a CIB patchset |
914 | | * |
915 | | * \param[in] patchset CIB XML patchset |
916 | | * \param[in] element XML tag of CIB element to check (\c NULL is equivalent |
917 | | * to \c PCMK_XE_CIB). Supported values include any CIB |
918 | | * element supported by \c pcmk__cib_abs_xpath_for(). |
919 | | * |
920 | | * \retval \c true if \p element was modified |
921 | | * \retval \c false otherwise |
922 | | */ |
923 | | bool |
924 | | pcmk__cib_element_in_patchset(const xmlNode *patchset, const char *element) |
925 | 0 | { |
926 | 0 | const char *element_xpath = pcmk__cib_abs_xpath_for(element); |
927 | 0 | const char *parent_xpath = pcmk_cib_parent_name_for(element); |
928 | 0 | char *element_regex = NULL; |
929 | 0 | bool rc = false; |
930 | 0 | int format = 1; |
931 | |
|
932 | 0 | pcmk__assert(patchset != NULL); |
933 | |
|
934 | 0 | pcmk__xe_get_int(patchset, PCMK_XA_FORMAT, &format); |
935 | 0 | if (format != 2) { |
936 | 0 | pcmk__warn("Unknown patch format: %d", format); |
937 | 0 | return false; |
938 | 0 | } |
939 | | |
940 | 0 | CRM_CHECK(element_xpath != NULL, return false); // Unsupported element |
941 | | |
942 | | /* Matches if and only if element_xpath is part of a changed path |
943 | | * (supported values for element never contain XML IDs with schema |
944 | | * validation enabled) |
945 | | * |
946 | | * @TODO Use POSIX word boundary instead of (/|$), if it works: |
947 | | * https://www.regular-expressions.info/wordboundaries.html. |
948 | | */ |
949 | 0 | element_regex = pcmk__assert_asprintf("^%s(/|$)", element_xpath); |
950 | |
|
951 | 0 | for (const xmlNode *change = pcmk__xe_first_child(patchset, PCMK_XE_CHANGE, |
952 | 0 | NULL, NULL); |
953 | 0 | change != NULL; change = pcmk__xe_next(change, PCMK_XE_CHANGE)) { |
954 | |
|
955 | 0 | const char *op = pcmk__xe_get(change, PCMK_XA_OPERATION); |
956 | 0 | const char *diff_xpath = pcmk__xe_get(change, PCMK_XA_PATH); |
957 | |
|
958 | 0 | if (pcmk__str_eq(diff_xpath, element_regex, pcmk__str_regex)) { |
959 | | // Change to an existing element |
960 | 0 | rc = true; |
961 | 0 | break; |
962 | 0 | } |
963 | | |
964 | 0 | if (pcmk__str_eq(op, PCMK_VALUE_CREATE, pcmk__str_none) |
965 | 0 | && pcmk__str_eq(diff_xpath, parent_xpath, pcmk__str_none) |
966 | 0 | && pcmk__xe_is(pcmk__xe_first_child(change, NULL, NULL, NULL), |
967 | 0 | element)) { |
968 | | // Newly added element |
969 | 0 | rc = true; |
970 | 0 | break; |
971 | 0 | } |
972 | 0 | } |
973 | |
|
974 | 0 | free(element_regex); |
975 | 0 | return rc; |
976 | 0 | } |
977 | | |
978 | | // Deprecated functions kept only for backward API compatibility |
979 | | // LCOV_EXCL_START |
980 | | |
981 | | #include <crm/common/xml_compat.h> |
982 | | |
983 | | // Return value of true means failure; false means success |
984 | | bool |
985 | | xml_patch_versions(const xmlNode *patchset, int add[3], int del[3]) |
986 | 0 | { |
987 | 0 | const xmlNode *version = pcmk__xe_first_child(patchset, PCMK_XE_VERSION, |
988 | 0 | NULL, NULL); |
989 | 0 | const xmlNode *source = pcmk__xe_first_child(version, PCMK_XE_SOURCE, NULL, |
990 | 0 | NULL); |
991 | 0 | const xmlNode *target = pcmk__xe_first_child(version, PCMK_XE_TARGET, NULL, |
992 | 0 | NULL); |
993 | 0 | int format = 1; |
994 | |
|
995 | 0 | pcmk__xe_get_int(patchset, PCMK_XA_FORMAT, &format); |
996 | 0 | if (format != 2) { |
997 | 0 | pcmk__err("Unknown patch format: %d", format); |
998 | 0 | return true; |
999 | 0 | } |
1000 | | |
1001 | 0 | if (source != NULL) { |
1002 | 0 | for (int i = 0; i < PCMK__NELEM(vfields); i++) { |
1003 | 0 | pcmk__xe_get_int(source, vfields[i], &(del[i])); |
1004 | 0 | pcmk__trace("Got %d for del[%s]", del[i], vfields[i]); |
1005 | 0 | } |
1006 | 0 | } |
1007 | | |
1008 | 0 | if (target != NULL) { |
1009 | 0 | for (int i = 0; i < PCMK__NELEM(vfields); i++) { |
1010 | 0 | pcmk__xe_get_int(target, vfields[i], &(add[i])); |
1011 | 0 | pcmk__trace("Got %d for add[%s]", add[i], vfields[i]); |
1012 | 0 | } |
1013 | 0 | } |
1014 | 0 | return false; |
1015 | 0 | } |
1016 | | |
1017 | | void |
1018 | | patchset_process_digest(xmlNode *patch, const xmlNode *source, |
1019 | | const xmlNode *target, bool with_digest) |
1020 | 0 | { |
1021 | 0 | char *digest = NULL; |
1022 | |
|
1023 | 0 | if ((patch == NULL) || (source == NULL) || (target == NULL) |
1024 | 0 | || !with_digest) { |
1025 | 0 | return; |
1026 | 0 | } |
1027 | | |
1028 | 0 | CRM_LOG_ASSERT(!pcmk__xml_doc_all_flags_set(target->doc, pcmk__xf_dirty)); |
1029 | |
|
1030 | 0 | digest = pcmk__digest_xml(target, true); |
1031 | |
|
1032 | 0 | pcmk__xe_set(patch, PCMK_XA_DIGEST, digest); |
1033 | 0 | free(digest); |
1034 | |
|
1035 | 0 | return; |
1036 | 0 | } |
1037 | | |
1038 | | // LCOV_EXCL_STOP |
1039 | | // End deprecated API |