diff --git a/cts/cli/crm_diff_new.xml b/cts/cli/crm_diff_new.xml index 7fa0cbbd1ba..be9e9aba7fa 100644 --- a/cts/cli/crm_diff_new.xml +++ b/cts/cli/crm_diff_new.xml @@ -1,4 +1,4 @@ - + diff --git a/cts/cli/crm_diff_patchset.xml b/cts/cli/crm_diff_patchset.xml new file mode 100644 index 00000000000..7cb38288d6e --- /dev/null +++ b/cts/cli/crm_diff_patchset.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cts/cli/regression.crm_diff.exp b/cts/cli/regression.crm_diff.exp index 6b1a7109dda..01e859842f0 100644 --- a/cts/cli/regression.crm_diff.exp +++ b/cts/cli/regression.crm_diff.exp @@ -1,4 +1,4 @@ -=#=#=#= Begin test: Create an XML patchset =#=#=#= +=#=#=#= Begin test: Create an XML patchset from files =#=#=#= @@ -62,5 +62,438 @@ -=#=#=#= End test: Create an XML patchset - Error occurred (1) =#=#=#= -* Passed: crm_diff - Create an XML patchset +=#=#=#= End test: Create an XML patchset from files - Error occurred (1) =#=#=#= +* Passed: crm_diff - Create an XML patchset from files +=#=#=#= Begin test: Create an XML patchset from strings =#=#=#= + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +=#=#=#= End test: Create an XML patchset from strings - Error occurred (1) =#=#=#= +* Passed: crm_diff - Create an XML patchset from strings +=#=#=#= Begin test: Create an XML patchset from old file, new string =#=#=#= + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +=#=#=#= End test: Create an XML patchset from old file, new string - Error occurred (1) =#=#=#= +* Passed: crm_diff - Create an XML patchset from old file, new string +=#=#=#= Begin test: Create an XML patchset from old string, new file =#=#=#= + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +=#=#=#= End test: Create an XML patchset from old string, new file - Error occurred (1) =#=#=#= +* Passed: crm_diff - Create an XML patchset from old string, new file +=#=#=#= Begin test: Create an XML patchset from files =#=#=#= + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +=#=#=#= End test: Create an XML patchset from files - Error occurred (1) =#=#=#= +* Passed: crm_diff - Create an XML patchset from files +=#=#=#= Begin test: Create an XML patchset from files =#=#=#= + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +=#=#=#= End test: Create an XML patchset from files - Error occurred (1) =#=#=#= +* Passed: crm_diff - Create an XML patchset from files +=#=#=#= Begin test: Create an XML patchset from files =#=#=#= +crm_diff: -u/--no-version incompatible with -c/--cib +=#=#=#= End test: Create an XML patchset from files - Incorrect usage (64) =#=#=#= +* Passed: crm_diff - Create an XML patchset from files +=#=#=#= Begin test: Apply an XML patchset to a file =#=#=#= + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +=#=#=#= End test: Apply an XML patchset to a file - OK (0) =#=#=#= +* Passed: crm_diff - Apply an XML patchset to a file +=#=#=#= Begin test: Apply an XML patchset to a string =#=#=#= + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +=#=#=#= End test: Apply an XML patchset to a string - OK (0) =#=#=#= +* Passed: crm_diff - Apply an XML patchset to a string diff --git a/cts/cts-cli.in b/cts/cts-cli.in index b2d2218f773..d594a6f612a 100644 --- a/cts/cts-cli.in +++ b/cts/cts-cli.in @@ -450,7 +450,7 @@ class Test: def _log_test_failed(self, app, rc): """Log a message when a test fails.""" - self._output.append(f"* Failed (rc={rc:.3d}): {app:<23} - {self.desc}") + self._output.append(f"* Failed (rc={rc:03d}): {app:<23} - {self.desc}") def _log_test_passed(self, app): """Log a message when a test passes.""" @@ -2385,10 +2385,46 @@ class CrmDiffRegressionTest(RegressionTest): @property def tests(self): """A list of Test instances to be run as part of this regression test.""" + + old_file = f"{cts_cli_data}/crm_diff_old.xml" + new_file = f"{cts_cli_data}/crm_diff_new.xml" + patch_file = f"{cts_cli_data}/crm_diff_patchset.xml" + + with open(f"{cts_cli_data}/crm_diff_old.xml", "r") as file: + old_str = f"'{file.read()}'" + with open(f"{cts_cli_data}/crm_diff_new.xml", "r") as file: + new_str = f"'{file.read()}'" + return [ - Test("Create an XML patchset", - f"crm_diff -o {cts_cli_data}/crm_diff_old.xml -n {cts_cli_data}/crm_diff_new.xml", - expected_rc=ExitStatus.ERROR) + Test("Create an XML patchset from files", + f"crm_diff -o {old_file} -n {new_file}", + expected_rc=ExitStatus.ERROR), + Test("Create an XML patchset from strings", + f"crm_diff -O {old_str} -N {new_str}", + expected_rc=ExitStatus.ERROR), + Test("Create an XML patchset from old file, new string", + f"crm_diff -o {old_file} -N {new_str}", + expected_rc=ExitStatus.ERROR), + Test("Create an XML patchset from old string, new file", + f"crm_diff -O {old_str} -n {new_file}", + expected_rc=ExitStatus.ERROR), + + Test("Create an XML patchset from files", + f"crm_diff -o {old_file} -n {new_file} --cib", + expected_rc=ExitStatus.ERROR), + Test("Create an XML patchset from files", + f"crm_diff -o {old_file} -n {new_file} --no-version", + expected_rc=ExitStatus.ERROR), + Test("Create an XML patchset from files", + f"crm_diff -o {old_file} -n {new_file} --cib --no-version", + expected_rc=ExitStatus.USAGE), + + Test("Apply an XML patchset to a file", + f"crm_diff -o {old_file} -p {patch_file}"), + Test("Apply an XML patchset to a string", + f"crm_diff -O {old_str} -p {patch_file}"), + Test("Apply an XML patchset to a file", + f"crm_diff -o {old_file} -p {patch_file} --cib"), ] diff --git a/daemons/controld/controld_te_actions.c b/daemons/controld/controld_te_actions.c index 1df83e82b08..fba2463d1e0 100644 --- a/daemons/controld/controld_te_actions.c +++ b/daemons/controld/controld_te_actions.c @@ -1,5 +1,5 @@ /* - * Copyright 2004-2024 the Pacemaker project contributors + * Copyright 2004-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -299,10 +299,11 @@ controld_record_action_event(pcmk__graph_action_t *action, rsc = pcmk__xe_create(rsc, PCMK__XE_LRM_RESOURCE); crm_xml_add(rsc, PCMK_XA_ID, rsc_id); - - crm_copy_xml_element(action_rsc, rsc, PCMK_XA_TYPE); - crm_copy_xml_element(action_rsc, rsc, PCMK_XA_CLASS); - crm_copy_xml_element(action_rsc, rsc, PCMK_XA_PROVIDER); + crm_xml_add(rsc, PCMK_XA_TYPE, crm_element_value(action_rsc, PCMK_XA_TYPE)); + crm_xml_add(rsc, PCMK_XA_CLASS, + crm_element_value(action_rsc, PCMK_XA_CLASS)); + crm_xml_add(rsc, PCMK_XA_PROVIDER, + crm_element_value(action_rsc, PCMK_XA_PROVIDER)); pcmk__create_history_xml(rsc, op, CRM_FEATURE_SET, target_rc, target, __func__); diff --git a/include/crm/common/xml.h b/include/crm/common/xml.h index a4cdcde69d9..b5a5b08f0e5 100644 --- a/include/crm/common/xml.h +++ b/include/crm/common/xml.h @@ -35,7 +35,8 @@ extern "C" { xmlNode *xml_create_patchset( int format, xmlNode *source, xmlNode *target, bool *config, bool manage_version); -int xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version); +int xml_apply_patchset(xmlNode *xml, const xmlNode *patchset, + bool check_version); #ifdef __cplusplus } diff --git a/include/crm/common/xml_element.h b/include/crm/common/xml_element.h index c49603b5454..7487cce1bf8 100644 --- a/include/crm/common/xml_element.h +++ b/include/crm/common/xml_element.h @@ -1,5 +1,5 @@ /* - * Copyright 2004-2024 the Pacemaker project contributors + * Copyright 2004-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -42,24 +42,6 @@ int crm_element_value_timeval(const xmlNode *data, const char *name_sec, const char *name_usec, struct timeval *dest); char *crm_element_value_copy(const xmlNode *data, const char *name); -/*! - * \brief Copy an element from one XML object to another - * - * \param[in] obj1 Source XML - * \param[in,out] obj2 Destination XML - * \param[in] element Name of element to copy - * - * \return Pointer to copied value (from source) - */ -static inline const char * -crm_copy_xml_element(const xmlNode *obj1, xmlNode *obj2, const char *element) -{ - const char *value = crm_element_value(obj1, element); - - crm_xml_add(obj2, element, value); - return value; -} - #ifdef __cplusplus } #endif diff --git a/include/crm/common/xml_element_compat.h b/include/crm/common/xml_element_compat.h index 7c6e1f1c841..50e677afbcb 100644 --- a/include/crm/common/xml_element_compat.h +++ b/include/crm/common/xml_element_compat.h @@ -1,5 +1,5 @@ /* - * Copyright 2004-2024 the Pacemaker project contributors + * Copyright 2004-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -35,6 +35,10 @@ void crm_xml_set_id(xmlNode *xml, const char *format, ...) G_GNUC_PRINTF(2, 3); //! \deprecated Do not use xmlNode *sorted_xml(xmlNode *input, xmlNode *parent, gboolean recursive); +//! \deprecated Do not use +const char *crm_copy_xml_element(const xmlNode *obj1, xmlNode *obj2, + const char *element); + #ifdef __cplusplus } #endif diff --git a/include/crm/common/xml_names.h b/include/crm/common/xml_names.h index 6dfd23af8b5..2e937660dac 100644 --- a/include/crm/common/xml_names.h +++ b/include/crm/common/xml_names.h @@ -1,5 +1,5 @@ /* - * Copyright 2004-2024 the Pacemaker project contributors + * Copyright 2004-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -157,6 +157,7 @@ extern "C" { #define PCMK_XE_PACEMAKERD "pacemakerd" #define PCMK_XE_PARAMETER "parameter" #define PCMK_XE_PARAMETERS "parameters" +#define PCMK_XE_PATCHSET "patchset" #define PCMK_XE_PERIOD "period" #define PCMK_XE_PODMAN "podman" #define PCMK_XE_PORT_MAPPING "port-mapping" @@ -214,6 +215,7 @@ extern "C" { #define PCMK_XE_TIMING "timing" #define PCMK_XE_TIMINGS "timings" #define PCMK_XE_TRANSITION "transition" +#define PCMK_XE_UPDATED "updated" #define PCMK_XE_UTILIZATION "utilization" #define PCMK_XE_UTILIZATIONS "utilizations" #define PCMK_XE_VALIDATE "validate" diff --git a/include/crm/crm.h b/include/crm/crm.h index b8a213cb4d0..a819902cc9b 100644 --- a/include/crm/crm.h +++ b/include/crm/crm.h @@ -63,7 +63,7 @@ extern "C" { * >=3.2.0: DC supports PCMK_EXEC_INVALID and PCMK_EXEC_NOT_CONNECTED * >=3.19.0: DC supports PCMK__CIB_REQUEST_COMMIT_TRANSACT */ -#define CRM_FEATURE_SET "3.20.1" +#define CRM_FEATURE_SET "3.20.2" /* Pacemaker's CPG protocols use fixed-width binary fields for the sender and * recipient of a CPG message. This imposes an arbitrary limit on cluster node diff --git a/include/crm_internal.h b/include/crm_internal.h index 891f2e5b686..337d309b991 100644 --- a/include/crm_internal.h +++ b/include/crm_internal.h @@ -1,5 +1,5 @@ /* - * Copyright 2006-2024 the Pacemaker project contributors + * Copyright 2006-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -38,6 +38,7 @@ #include #include +#include #include #include #include diff --git a/lib/common/digest.c b/lib/common/digest.c index 3479f8a4258..b9213bd5608 100644 --- a/lib/common/digest.c +++ b/lib/common/digest.c @@ -176,6 +176,9 @@ pcmk__digest_xml(const xmlNode *xml, bool filter) pcmk__xml_string(xml, (filter? pcmk__xml_fmt_filtered : 0), buf, 0); digest = crm_md5sum(buf->str); + if (digest == NULL) { + goto done; + } pcmk__if_tracing( { @@ -192,6 +195,8 @@ pcmk__digest_xml(const xmlNode *xml, bool filter) }, {} ); + +done: g_string_free(buf, TRUE); return digest; } diff --git a/lib/common/patchset.c b/lib/common/patchset.c index 3ed81f0e3af..81c6521bb51 100644 --- a/lib/common/patchset.c +++ b/lib/common/patchset.c @@ -26,6 +26,12 @@ #include // CRM_XML_LOG_BASE, etc. #include "crmcommon_private.h" +static const char *const vfields[] = { + PCMK_XA_ADMIN_EPOCH, + PCMK_XA_EPOCH, + PCMK_XA_NUM_UPDATES, +}; + /* Add changes for specified XML to patchset. * For patchset format, refer to diff schema. */ @@ -185,11 +191,6 @@ xml_create_patchset_v2(xmlNode *source, xmlNode *target) xmlNode *v = NULL; xmlNode *version = NULL; xmlNode *patchset = NULL; - const char *vfields[] = { - PCMK_XA_ADMIN_EPOCH, - PCMK_XA_EPOCH, - PCMK_XA_NUM_UPDATES, - }; pcmk__assert(target != NULL); @@ -335,12 +336,6 @@ int pcmk__xml_patchset_versions(const xmlNode *patchset, int source[3], int target[3]) { - static const char *const vfields[] = { - PCMK_XA_ADMIN_EPOCH, - PCMK_XA_EPOCH, - PCMK_XA_NUM_UPDATES, - }; - int format = 0; const xmlNode *version = NULL; const xmlNode *source_xml = NULL; @@ -413,80 +408,92 @@ pcmk__xml_patchset_versions(const xmlNode *patchset, int source[3], * \internal * \brief Check whether patchset can be applied to current CIB * - * \param[in] xml Root of current CIB + * \param[in] cib_root Root of current CIB * \param[in] patchset Patchset to check * * \return Standard Pacemaker return code */ static int -xml_patch_version_check(const xmlNode *xml, const xmlNode *patchset) +check_patchset_versions(const xmlNode *cib_root, const xmlNode *patchset) { - int lpc = 0; - bool changed = FALSE; - - int this[] = { 0, 0, 0 }; - int add[] = { 0, 0, 0 }; - int del[] = { 0, 0, 0 }; + int current[] = { 0, 0, 0 }; + int source[] = { 0, 0, 0 }; + int target[] = { 0, 0, 0 }; int rc = pcmk_rc_ok; - const char *vfields[] = { - PCMK_XA_ADMIN_EPOCH, - PCMK_XA_EPOCH, - PCMK_XA_NUM_UPDATES, - }; - - for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { - crm_element_value_int(xml, vfields[lpc], &(this[lpc])); - crm_trace("Got %d for this[%s]", this[lpc], vfields[lpc]); - if (this[lpc] < 0) { - this[lpc] = 0; + for (int i = 0; i < PCMK__NELEM(vfields); i++) { + /* @COMPAT We should probably fail with EINVAL for negative or invalid. + * + * Preserve behavior for xml_apply_patchset(). Use new behavior in a + * future replacement. + */ + if (crm_element_value_int(cib_root, vfields[i], &(current[i])) == 0) { + crm_trace("Got %d for current[%s]%s", + current[i], vfields[i], + ((current[i] < 0)? ", using 0" : "")); + } else { + crm_debug("Failed to get value for current[%s], using 0", + vfields[i]); + } + if (current[i] < 0) { + current[i] = 0; } } - /* Set some defaults in case nothing is present */ - add[0] = this[0]; - add[1] = this[1]; - add[2] = this[2] + 1; - for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { - del[lpc] = this[lpc]; + /* Set some defaults in case nothing is present. + * + * @COMPAT We should probably skip this step, and fail immediately below if + * target[i] < source[i]. + * + * Preserve behavior for xml_apply_patchset(). Use new behavior in a future + * replacement. + */ + target[0] = current[0]; + target[1] = current[1]; + target[2] = current[2] + 1; + for (int i = 0; i < PCMK__NELEM(vfields); i++) { + source[i] = current[i]; } - rc = pcmk__xml_patchset_versions(patchset, del, add); + rc = pcmk__xml_patchset_versions(patchset, source, target); if (rc != pcmk_rc_ok) { return rc; } - for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { - if (this[lpc] < del[lpc]) { - crm_debug("Current %s is too low (%d.%d.%d < %d.%d.%d --> %d.%d.%d)", - vfields[lpc], this[0], this[1], this[2], - del[0], del[1], del[2], add[0], add[1], add[2]); + // Ensure current version matches patchset source version + for (int i = 0; i < PCMK__NELEM(vfields); i++) { + if (current[i] < source[i]) { + crm_debug("Current %s is too low " + "(%d.%d.%d < %d.%d.%d --> %d.%d.%d)", + vfields[i], current[0], current[1], current[2], + source[0], source[1], source[2], + target[0], target[1], target[2]); return pcmk_rc_diff_resync; - - } else if (this[lpc] > del[lpc]) { - crm_info("Current %s is too high (%d.%d.%d > %d.%d.%d --> %d.%d.%d) %p", - vfields[lpc], this[0], this[1], this[2], - del[0], del[1], del[2], add[0], add[1], add[2], patchset); + } + if (current[i] > source[i]) { + crm_info("Current %s is too high " + "(%d.%d.%d > %d.%d.%d --> %d.%d.%d)", + vfields[i], current[0], current[1], current[2], + source[0], source[1], source[2], + target[0], target[1], target[2]); crm_log_xml_info(patchset, "OldPatch"); return pcmk_rc_old_data; } } - for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { - if (add[lpc] > del[lpc]) { - changed = TRUE; + // Ensure target version is newer than source version + for (int i = 0; i < PCMK__NELEM(vfields); i++) { + if (target[i] > source[i]) { + crm_debug("Can apply patch %d.%d.%d to %d.%d.%d", + target[0], target[1], target[2], + current[0], current[1], current[2]); + return pcmk_rc_ok; } } - if (!changed) { - crm_notice("Versions did not change in patch %d.%d.%d", - add[0], add[1], add[2]); - return pcmk_rc_old_data; - } - - crm_debug("Can apply patch %d.%d.%d to %d.%d.%d", - add[0], add[1], add[2], this[0], this[1], this[2]); - return pcmk_rc_ok; + crm_notice("Versions did not change in patch %d.%d.%d", + target[0], target[1], target[2]); + return pcmk_rc_old_data; } // Return first child matching element name and optionally id or position @@ -825,7 +832,7 @@ apply_v2_patchset(xmlNode *xml, const xmlNode *patchset) } int -xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version) +xml_apply_patchset(xmlNode *xml, const xmlNode *patchset, bool check_version) { int format = 1; int rc = pcmk_ok; @@ -839,7 +846,7 @@ xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version) pcmk__log_xml_patchset(LOG_TRACE, patchset); if (check_version) { - rc = pcmk_rc2legacy(xml_patch_version_check(xml, patchset)); + rc = pcmk_rc2legacy(check_patchset_versions(xml, patchset)); if (rc != pcmk_ok) { return rc; } @@ -968,12 +975,6 @@ pcmk__cib_element_in_patchset(const xmlNode *patchset, const char *element) bool xml_patch_versions(const xmlNode *patchset, int add[3], int del[3]) { - static const char *const vfields[] = { - PCMK_XA_ADMIN_EPOCH, - PCMK_XA_EPOCH, - PCMK_XA_NUM_UPDATES, - }; - const xmlNode *version = pcmk__xe_first_child(patchset, PCMK_XE_VERSION, NULL, NULL); const xmlNode *source = pcmk__xe_first_child(version, PCMK_XE_SOURCE, NULL, diff --git a/lib/common/xml_display.c b/lib/common/xml_display.c index d886d34c4bb..d6865f7474a 100644 --- a/lib/common/xml_display.c +++ b/lib/common/xml_display.c @@ -1,5 +1,5 @@ /* - * Copyright 2004-2024 the Pacemaker project contributors + * Copyright 2004-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -111,7 +111,8 @@ show_xml_element(pcmk__output_t *out, GString *buffer, const char *prefix, const char *p_value = pcmk__xml_attr_value(attr); gchar *p_copy = NULL; - if (pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) { + if ((nodepriv == NULL) + || pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) { continue; } @@ -265,6 +266,10 @@ show_xml_changes_recursive(pcmk__output_t *out, const xmlNode *data, int depth, int rc = pcmk_rc_no_output; int temp_rc = pcmk_rc_no_output; + if (nodepriv == NULL) { + return pcmk_rc_no_output; + } + if (pcmk_all_flags_set(nodepriv->flags, pcmk__xf_dirty|pcmk__xf_created)) { // Newly created return pcmk__xml_show(out, PCMK__XML_PREFIX_CREATED, data, depth, diff --git a/lib/common/xml_element.c b/lib/common/xml_element.c index 3b97e0f37c2..1875efd50ab 100644 --- a/lib/common/xml_element.c +++ b/lib/common/xml_element.c @@ -1599,5 +1599,14 @@ sorted_xml(xmlNode *input, xmlNode *parent, gboolean recursive) return result; } +const char * +crm_copy_xml_element(const xmlNode *obj1, xmlNode *obj2, const char *element) +{ + const char *value = crm_element_value(obj1, element); + + crm_xml_add(obj2, element, value); + return value; +} + // LCOV_EXCL_STOP // End deprecated API diff --git a/tools/crm_diff.c b/tools/crm_diff.c index 173e87f5cee..239afc4c0d8 100644 --- a/tools/crm_diff.c +++ b/tools/crm_diff.c @@ -9,59 +9,85 @@ #include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#define SUMMARY "Compare two Pacemaker configurations (in XML format) to produce a custom diff-like output, " \ - "or apply such an output as a patch" +#include // bool, true +#include // NULL, printf(), etc. +#include // free() + +#include // GOption, etc. +#include // xmlNode + +#include // xml_{create,apply}_patchset() + +#define SUMMARY "Compare two Pacemaker configurations (in XML format) to " \ + "produce a custom diff-like output, or apply such an output " \ + "as a patch" +#define INDENT " " struct { - gboolean apply; + gchar *source_file; + gchar *target_file; + gchar *source_string; + gchar *target_string; + bool patch; gboolean as_cib; gboolean no_version; - gboolean raw_original; - gboolean raw_new; - gboolean use_stdin; - char *xml_file_original; - char *xml_file_new; + gboolean use_stdin; //!< \deprecated } options; -gboolean new_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); -gboolean original_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); -gboolean patch_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); +static gboolean +patch_cb(const gchar *option_name, const gchar *optarg, gpointer data, + GError **error) +{ + options.patch = true; + g_free(options.target_file); + options.target_file = g_strdup(optarg); + return TRUE; +} +/* @COMPAT Use last-one-wins for original/new/patch input sources + * + * @COMPAT Precedence is --original-string > --stdin > --original. --stdin is + * now deprecated and hidden, so we don't mention it in the help text. + */ static GOptionEntry original_xml_entries[] = { - { "original", 'o', 0, G_OPTION_ARG_STRING, &options.xml_file_original, - "XML is contained in the named file", + { "original", 'o', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, + &options.source_file, + "XML is contained in the named file. Currently --original-string\n" + INDENT "overrides this. In a future release, the last one specified\n" + INDENT "will be used.", "FILE" }, - { "original-string", 'O', 0, G_OPTION_ARG_CALLBACK, original_string_cb, - "XML is contained in the supplied string", + { "original-string", 'O', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, + &options.source_string, + "XML is contained in the supplied string. Currently this takes\n" + INDENT "precedence over --original. In a future release, the last one\n" + INDENT "specified will be used.", "STRING" }, { NULL } }; +/* @COMPAT Precedence is --original-string > --stdin > --original. --stdin is + * now deprecated and hidden, so we don't mention it in the help text. + */ static GOptionEntry operation_entries[] = { - { "new", 'n', 0, G_OPTION_ARG_STRING, &options.xml_file_new, - "Compare the original XML to the contents of the named file", + { "new", 'n', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.target_file, + "Compare the original XML to the contents of the named file. Currently\n" + INDENT "--new-string overrides this. In a future release, the last one\n" + INDENT "specified will be used.", "FILE" }, - { "new-string", 'N', 0, G_OPTION_ARG_CALLBACK, new_string_cb, - "Compare the original XML with the contents of the supplied string", + { "new-string", 'N', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, + &options.target_string, + "Compare the original XML with the contents of the supplied string.\n" + INDENT "Currently this takes precedence over --patch and --new. In a\n" + INDENT "future release, the last one specified will be used.", "STRING" }, - { "patch", 'p', 0, G_OPTION_ARG_CALLBACK, patch_cb, - "Patch the original XML with the contents of the named file", + { "patch", 'p', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, patch_cb, + "Patch the original XML with the contents of the named file. Currently\n" + INDENT "--new-string and (if specified later) --new override the input\n" + INDENT "source specified here. In a future release, the last one\n" + INDENT "specified will be used. Note: even if this input source is\n" + INDENT "overridden, the input source will be applied as a patch to the\n" + INDENT "original XML.", "FILE" }, { NULL } @@ -75,148 +101,98 @@ static GOptionEntry addl_entries[] = { * we don't update the target versions in the patchset, and we don't drop * the versions from the patchset unless --no-version is given. */ - { "cib", 'c', 0, G_OPTION_ARG_NONE, &options.as_cib, - "Compare/patch the inputs as a CIB (includes versions details)", + { "cib", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.as_cib, + "Compare/patch the inputs as a CIB (includes version details)", NULL }, - { "stdin", 's', 0, G_OPTION_ARG_NONE, &options.use_stdin, - "", + { "no-version", 'u', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, + &options.no_version, + "Generate the difference without version details", NULL }, - { "no-version", 'u', 0, G_OPTION_ARG_NONE, &options.no_version, - "Generate the difference without versions details", + + // @COMPAT Deprecated + { "stdin", 's', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.use_stdin, + "Get the original XML and new (or patch) XML from stdin. Currently\n" + INDENT "--original-string and --new-string override this for original\n" + INDENT "and new/patch XML, respectively. In a future release, the last\n" + INDENT "one specified will be used.", NULL }, { NULL } }; -gboolean -new_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { - options.raw_new = TRUE; - pcmk__str_update(&options.xml_file_new, optarg); - return TRUE; -} - -gboolean -original_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { - options.raw_original = TRUE; - pcmk__str_update(&options.xml_file_original, optarg); - return TRUE; -} - -gboolean -patch_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { - options.apply = TRUE; - pcmk__str_update(&options.xml_file_new, optarg); - return TRUE; -} - -static void -print_patch(xmlNode *patch) -{ - GString *buffer = g_string_sized_new(1024); - - pcmk__xml_string(patch, pcmk__xml_fmt_pretty, buffer, 0); - - printf("%s", buffer->str); - g_string_free(buffer, TRUE); - fflush(stdout); -} - -// \return Standard Pacemaker return code -static int -apply_patch(xmlNode *input, xmlNode *patch, gboolean as_cib) -{ - xmlNode *output = pcmk__xml_copy(NULL, input); - int rc = xml_apply_patchset(output, patch, as_cib); - - rc = pcmk_legacy2rc(rc); - if (rc != pcmk_rc_ok) { - fprintf(stderr, "Could not apply patch: %s\n", pcmk_rc_str(rc)); - pcmk__xml_free(output); - return rc; - } - - if (output != NULL) { - char *buffer; - - print_patch(output); - - buffer = pcmk__digest_xml(output, true); - crm_trace("Digest: %s", pcmk__s(buffer, "\n")); - free(buffer); - pcmk__xml_free(output); - } - return pcmk_rc_ok; -} - -static void -log_patch_cib_versions(xmlNode *patch) -{ - int add[] = { 0, 0, 0 }; - int del[] = { 0, 0, 0 }; - - const char *fmt = NULL; - const char *digest = NULL; - - pcmk__xml_patchset_versions(patch, del, add); - fmt = crm_element_value(patch, PCMK_XA_FORMAT); - digest = crm_element_value(patch, PCMK__XA_DIGEST); - - if (add[2] != del[2] || add[1] != del[1] || add[0] != del[0]) { - crm_info("Patch: --- %d.%d.%d %s", del[0], del[1], del[2], fmt); - crm_info("Patch: +++ %d.%d.%d %s", add[0], add[1], add[2], digest); - } -} - -// \return Standard Pacemaker return code +/*! + * \internal + * \brief Create an XML patchset from the given source and target XML trees + * + * \param[in,out] out Output object + * \param[in,out] source Source XML + * \param[in,out] target Target XML + * \param[in] as_cib If \c true, treat the XML trees as CIBs. In + * particular, ignore attribute position changes, + * include the target digest in the patchset, and log + * the source and target CIB versions. + * \param[in] no_version If \c true, ignore changes to the CIB version + * (must be \c false if \p as_cib is \c true) + * + * \return Standard Pacemaker return code + */ static int -generate_patch(xmlNode *object_original, xmlNode *object_new, const char *xml_file_new, - gboolean as_cib, gboolean no_version) +generate_patch(pcmk__output_t *out, xmlNode *source, xmlNode *target, + bool as_cib, bool no_version) { - const char *vfields[] = { + static const char *const vfields[] = { PCMK_XA_ADMIN_EPOCH, PCMK_XA_EPOCH, PCMK_XA_NUM_UPDATES, }; - xmlNode *output = NULL; + xmlNode *patchset = NULL; + GString *buffer = NULL; - /* If we're ignoring the version, make the version information - * identical, so it isn't detected as a change. */ - if (no_version) { - int lpc; + // Currently impossible; just a reminder for when we move to libpacemaker + pcmk__assert(!as_cib || !no_version); - for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { - crm_copy_xml_element(object_original, object_new, vfields[lpc]); + /* If we're ignoring the version, make the version information identical, so + * it isn't detected as a change. + */ + if (no_version) { + for (int i = 0; i < PCMK__NELEM(vfields); i++) { + crm_xml_add(target, vfields[i], + crm_element_value(source, vfields[i])); } } if (as_cib) { - pcmk__xml_doc_set_flags(object_new->doc, pcmk__xf_ignore_attr_pos); + pcmk__xml_doc_set_flags(target->doc, pcmk__xf_ignore_attr_pos); } - pcmk__xml_mark_changes(object_original, object_new); - crm_log_xml_debug(object_new, (xml_file_new? xml_file_new: "target")); + pcmk__xml_mark_changes(source, target); + crm_log_xml_debug(target, "target"); - output = xml_create_patchset(0, object_original, object_new, NULL, FALSE); + patchset = xml_create_patchset(0, source, target, NULL, false); - pcmk__log_xml_changes(LOG_INFO, object_new); - pcmk__xml_commit_changes(object_new->doc); + pcmk__log_xml_changes(LOG_INFO, target); + pcmk__xml_commit_changes(target->doc); - if (output == NULL) { + if (patchset == NULL) { return pcmk_rc_ok; // No changes } if (as_cib) { - pcmk__xml_patchset_add_digest(output, object_new); - log_patch_cib_versions(output); + pcmk__xml_patchset_add_digest(patchset, target); } else if (no_version) { - pcmk__xml_free(pcmk__xe_first_child(output, PCMK_XE_VERSION, NULL, + pcmk__xml_free(pcmk__xe_first_child(patchset, PCMK_XE_VERSION, NULL, NULL)); } - pcmk__log_xml_patchset(LOG_NOTICE, output); - print_patch(output); - pcmk__xml_free(output); + pcmk__log_xml_patchset(LOG_NOTICE, patchset); + + buffer = g_string_sized_new(1024); + pcmk__xml_string(patchset, pcmk__xml_fmt_pretty, buffer, 0); + out->output_xml(out, PCMK_XE_PATCHSET, buffer->str); + + pcmk__xml_free(patchset); + g_string_free(buffer, TRUE); /* pcmk_rc_error means there's a non-empty diff. * @COMPAT Choose a more descriptive return code, like one that maps to @@ -225,8 +201,17 @@ generate_patch(xmlNode *object_original, xmlNode *object_new, const char *xml_fi return pcmk_rc_error; } +static const pcmk__supported_format_t formats[] = { + PCMK__SUPPORTED_FORMAT_NONE, + PCMK__SUPPORTED_FORMAT_TEXT, + PCMK__SUPPORTED_FORMAT_XML, + + { NULL, NULL, NULL } +}; + static GOptionContext * -build_arg_context(pcmk__common_args_t *args) { +build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) +{ GOptionContext *context = NULL; const char *description = "Examples:\n\n" @@ -240,7 +225,7 @@ build_arg_context(pcmk__common_args_t *args) { "Apply the patch to the running cluster:\n\n" "\t# cibadmin --patch -x patch.xml\n"; - context = pcmk__build_arg_context(args, NULL, NULL, NULL); + context = pcmk__build_arg_context(args, "text (default), xml", group, NULL); g_option_context_set_description(context, description); pcmk__add_arg_group(context, "xml", "Original XML:", @@ -255,17 +240,22 @@ build_arg_context(pcmk__common_args_t *args) { int main(int argc, char **argv) { - xmlNode *object_original = NULL; - xmlNode *object_new = NULL; - crm_exit_t exit_code = CRM_EX_OK; + int rc = pcmk_rc_ok; + + xmlNode *source = NULL; + xmlNode *target = NULL; + + pcmk__output_t *out = NULL; + GError *error = NULL; + GOptionGroup *output_group = NULL; pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY); gchar **processed_args = pcmk__cmdline_preproc(argv, "nopNO"); - GOptionContext *context = build_arg_context(args); + GOptionContext *context = build_arg_context(args, &output_group); - int rc = pcmk_rc_ok; + pcmk__register_formats(output_group, formats); if (!g_option_context_parse_strv(context, &processed_args, &error)) { exit_code = CRM_EX_USAGE; @@ -274,69 +264,113 @@ main(int argc, char **argv) pcmk__cli_init_logging("crm_diff", args->verbosity); - if (args->version) { - g_strfreev(processed_args); - pcmk__free_arg_context(context); - /* FIXME: When crm_diff is converted to use formatted output, this can go. */ - pcmk__cli_help('v'); + rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv); + if (rc != pcmk_rc_ok) { + exit_code = CRM_EX_ERROR; + g_set_error(&error, PCMK__EXITC_ERROR, exit_code, + "Error creating output format %s: %s", args->output_ty, + pcmk_rc_str(rc)); + goto done; } - if (options.apply && options.no_version) { - fprintf(stderr, "warning: -u/--no-version ignored with -p/--patch\n"); - } else if (options.as_cib && options.no_version) { - fprintf(stderr, "error: -u/--no-version incompatible with -c/--cib\n"); - exit_code = CRM_EX_USAGE; + if (args->version) { + out->version(out, false); goto done; } - if (options.raw_original) { - object_original = pcmk__xml_parse(options.xml_file_original); + if (options.no_version) { + if (options.as_cib) { + exit_code = CRM_EX_USAGE; + g_set_error(&error, PCMK__EXITC_ERROR, exit_code, + "-u/--no-version incompatible with -c/--cib"); + goto done; + } + if (options.patch) { + out->err(out, "Warning: -u/--no-version ignored with -p/--patch"); + } + } + + if (options.source_string != NULL) { + source = pcmk__xml_parse(options.source_string); } else if (options.use_stdin) { - fprintf(stderr, "Input first XML fragment:"); - object_original = pcmk__xml_read(NULL); + source = pcmk__xml_read(NULL); + + } else if (options.source_file != NULL) { + source = pcmk__xml_read(options.source_file); - } else if (options.xml_file_original != NULL) { - object_original = pcmk__xml_read(options.xml_file_original); + } else { + exit_code = CRM_EX_USAGE; + g_set_error(&error, PCMK__EXITC_ERROR, exit_code, + "Either --original or --original-string must be specified"); + goto done; } - if (options.raw_new) { - object_new = pcmk__xml_parse(options.xml_file_new); + if (options.target_string != NULL) { + target = pcmk__xml_parse(options.target_string); } else if (options.use_stdin) { - fprintf(stderr, "Input second XML fragment:"); - object_new = pcmk__xml_read(NULL); + target = pcmk__xml_read(NULL); + + } else if (options.target_file != NULL) { + target = pcmk__xml_read(options.target_file); - } else if (options.xml_file_new != NULL) { - object_new = pcmk__xml_read(options.xml_file_new); + } else { + exit_code = CRM_EX_USAGE; + g_set_error(&error, PCMK__EXITC_ERROR, exit_code, + "Either --new, --new-string, or --patch must be specified"); + goto done; } - if (object_original == NULL) { - fprintf(stderr, "Could not parse the first XML fragment\n"); + if (source == NULL) { exit_code = CRM_EX_DATAERR; + g_set_error(&error, PCMK__EXITC_ERROR, exit_code, + "Failed to parse original XML"); goto done; } - if (object_new == NULL) { - fprintf(stderr, "Could not parse the second XML fragment\n"); + if (target == NULL) { exit_code = CRM_EX_DATAERR; + g_set_error(&error, PCMK__EXITC_ERROR, exit_code, + "Failed to parse %s XML", (options.patch? "patch" : "new")); goto done; } - if (options.apply) { - rc = apply_patch(object_original, object_new, options.as_cib); + if (options.patch) { + rc = xml_apply_patchset(source, target, options.as_cib); + rc = pcmk_legacy2rc(rc); + if (rc != pcmk_rc_ok) { + g_set_error(&error, PCMK__RC_ERROR, rc, + "Could not apply patch: %s", pcmk_rc_str(rc)); + } else { + GString *buffer = g_string_sized_new(1024); + + pcmk__xml_string(source, pcmk__xml_fmt_pretty, buffer, 0); + out->output_xml(out, PCMK_XE_UPDATED, buffer->str); + g_string_free(buffer, TRUE); + } + } else { - rc = generate_patch(object_original, object_new, options.xml_file_new, options.as_cib, options.no_version); + rc = generate_patch(out, source, target, options.as_cib, + options.no_version); } exit_code = pcmk_rc2exitc(rc); done: g_strfreev(processed_args); pcmk__free_arg_context(context); - free(options.xml_file_original); - free(options.xml_file_new); - pcmk__xml_free(object_original); - pcmk__xml_free(object_new); - pcmk__output_and_clear_error(&error, NULL); + g_free(options.source_file); + g_free(options.target_file); + g_free(options.source_string); + g_free(options.target_string); + pcmk__xml_free(source); + pcmk__xml_free(target); + + pcmk__output_and_clear_error(&error, out); + + if (out != NULL) { + out->finish(out, exit_code, true, NULL); + pcmk__output_free(out); + } crm_exit(exit_code); } diff --git a/xml/Makefile.am b/xml/Makefile.am index dcbd9db0547..5b218a91484 100644 --- a/xml/Makefile.am +++ b/xml/Makefile.am @@ -55,6 +55,7 @@ version_pairs_last = $(wordlist \ # Names of API schemas that form the choices for pacemaker-result content API_request_base = command-output \ crm_attribute \ + crm_diff \ crm_error \ crm_mon \ crm_node \ diff --git a/xml/api/crm_diff-2.39.rng b/xml/api/crm_diff-2.39.rng new file mode 100644 index 00000000000..296539c24ba --- /dev/null +++ b/xml/api/crm_diff-2.39.rng @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +