diff --git a/tools/Makefile.am b/tools/Makefile.am index 5a7b3825bd..62b3bc0359 100644 --- a/tools/Makefile.am +++ b/tools/Makefile.am @@ -1,158 +1,159 @@ # # Copyright 2004-2019 the Pacemaker project contributors # # The version control history for this file may have further details. # # This source code is licensed under the GNU General Public License version 2 # or later (GPLv2+) WITHOUT ANY WARRANTY. # include $(top_srcdir)/Makefile.common if BUILD_SYSTEMD systemdsystemunit_DATA = crm_mon.service endif noinst_HEADERS = crm_mon.h crm_resource.h pcmkdir = $(datadir)/$(PACKAGE) pcmk_DATA = report.common report.collector sbin_SCRIPTS = crm_report crm_standby crm_master crm_failcount if BUILD_CIBSECRETS sbin_SCRIPTS += cibsecret endif noinst_SCRIPTS = pcmk_simtimes -EXTRA_DIST = crm_mon.sysconfig \ +EXTRA_DIST = crm_diff.8.inc \ + crm_mon.sysconfig \ crm_mon.8.inc \ crm_node.8.inc \ stonith_admin.8.inc sbin_PROGRAMS = attrd_updater \ cibadmin \ crmadmin \ crm_simulate \ crm_attribute \ crm_diff \ crm_error \ crm_mon \ crm_node \ crm_resource \ crm_rule \ crm_shadow \ crm_verify \ crm_ticket \ iso8601 \ stonith_admin if BUILD_SERVICELOG sbin_PROGRAMS += notifyServicelogEvent endif if BUILD_OPENIPMI_SERVICELOG sbin_PROGRAMS += ipmiservicelogd endif ## SOURCES # A few tools are just thin wrappers around crm_attribute. # This makes their help get updated when crm_attribute changes # (see Makefile.common). MAN8DEPS = crm_attribute crmadmin_SOURCES = crmadmin.c crmadmin_LDADD = $(top_builddir)/lib/pengine/libpe_status.la \ $(top_builddir)/lib/cib/libcib.la \ $(top_builddir)/lib/common/libcrmcommon.la crm_error_SOURCES = crm_error.c crm_error_LDADD = $(top_builddir)/lib/common/libcrmcommon.la cibadmin_SOURCES = cibadmin.c cibadmin_LDADD = $(top_builddir)/lib/cib/libcib.la \ $(top_builddir)/lib/common/libcrmcommon.la crm_shadow_SOURCES = crm_shadow.c crm_shadow_LDADD = $(top_builddir)/lib/cib/libcib.la \ $(top_builddir)/lib/common/libcrmcommon.la crm_node_SOURCES = crm_node.c crm_node_LDADD = $(top_builddir)/lib/cib/libcib.la \ $(top_builddir)/lib/common/libcrmcommon.la crm_simulate_SOURCES = crm_simulate.c crm_simulate_LDADD = $(top_builddir)/lib/pengine/libpe_status.la \ $(top_builddir)/lib/pacemaker/libpacemaker.la \ $(top_builddir)/lib/cib/libcib.la \ $(top_builddir)/lib/common/libcrmcommon.la crm_diff_SOURCES = crm_diff.c crm_diff_LDADD = $(top_builddir)/lib/common/libcrmcommon.la crm_mon_SOURCES = crm_mon.c crm_mon_curses.c crm_mon_output.c crm_mon_print.c crm_mon_runtime.c crm_mon_xml.c crm_mon_LDADD = $(top_builddir)/lib/pengine/libpe_status.la \ $(top_builddir)/lib/fencing/libstonithd.la \ $(top_builddir)/lib/pacemaker/libpacemaker.la \ $(top_builddir)/lib/cib/libcib.la \ $(top_builddir)/lib/common/libcrmcommon.la \ $(CURSESLIBS) crm_verify_SOURCES = crm_verify.c crm_verify_LDADD = $(top_builddir)/lib/pengine/libpe_status.la \ $(top_builddir)/lib/pacemaker/libpacemaker.la \ $(top_builddir)/lib/cib/libcib.la \ $(top_builddir)/lib/common/libcrmcommon.la crm_attribute_SOURCES = crm_attribute.c crm_attribute_LDADD = $(top_builddir)/lib/cluster/libcrmcluster.la \ $(top_builddir)/lib/cib/libcib.la \ $(top_builddir)/lib/common/libcrmcommon.la crm_resource_SOURCES = crm_resource.c crm_resource_ban.c crm_resource_runtime.c crm_resource_print.c crm_resource_LDADD = $(top_builddir)/lib/pengine/libpe_rules.la \ $(top_builddir)/lib/fencing/libstonithd.la \ $(top_builddir)/lib/lrmd/liblrmd.la \ $(top_builddir)/lib/services/libcrmservice.la \ $(top_builddir)/lib/pengine/libpe_status.la \ $(top_builddir)/lib/pacemaker/libpacemaker.la \ $(top_builddir)/lib/cib/libcib.la \ $(top_builddir)/lib/common/libcrmcommon.la crm_rule_SOURCES = crm_rule.c crm_rule_LDADD = $(top_builddir)/lib/cib/libcib.la \ $(top_builddir)/lib/pengine/libpe_rules.la \ $(top_builddir)/lib/pengine/libpe_status.la \ $(top_builddir)/lib/common/libcrmcommon.la iso8601_SOURCES = iso8601.c iso8601_LDADD = $(top_builddir)/lib/common/libcrmcommon.la attrd_updater_SOURCES = attrd_updater.c attrd_updater_LDADD = $(top_builddir)/lib/common/libcrmcommon.la crm_ticket_SOURCES = crm_ticket.c crm_ticket_LDADD = $(top_builddir)/lib/pengine/libpe_rules.la \ $(top_builddir)/lib/pengine/libpe_status.la \ $(top_builddir)/lib/pacemaker/libpacemaker.la \ $(top_builddir)/lib/cib/libcib.la \ $(top_builddir)/lib/common/libcrmcommon.la stonith_admin_SOURCES = stonith_admin.c stonith_admin_LDADD = $(top_builddir)/lib/common/libcrmcommon.la \ $(top_builddir)/lib/cib/libcib.la \ $(top_builddir)/lib/pengine/libpe_status.la \ $(top_builddir)/lib/fencing/libstonithd.la if BUILD_SERVICELOG notifyServicelogEvent_SOURCES = notifyServicelogEvent.c notifyServicelogEvent_CFLAGS = $(SERVICELOG_CFLAGS) notifyServicelogEvent_LDADD = $(top_builddir)/lib/common/libcrmcommon.la $(SERVICELOG_LIBS) endif if BUILD_OPENIPMI_SERVICELOG ipmiservicelogd_SOURCES = ipmiservicelogd.c ipmiservicelogd_CFLAGS = $(OPENIPMI_SERVICELOG_CFLAGS) $(SERVICELOG_CFLAGS) ipmiservicelogd_LDFLAGS = $(top_builddir)/lib/common/libcrmcommon.la $(OPENIPMI_SERVICELOG_LIBS) $(SERVICELOG_LIBS) endif CLEANFILES = $(man8_MANS) diff --git a/tools/crm_diff.8.inc b/tools/crm_diff.8.inc new file mode 100644 index 0000000000..4229cbf8f3 --- /dev/null +++ b/tools/crm_diff.8.inc @@ -0,0 +1,5 @@ +[synopsis] +crm_diff original_xml operation [options] + +/as a patch/ +.SH OPTIONS diff --git a/tools/crm_diff.c b/tools/crm_diff.c index 6a03040129..8722646ea5 100644 --- a/tools/crm_diff.c +++ b/tools/crm_diff.c @@ -1,351 +1,394 @@ /* * Copyright 2005-2018 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include +#include #include #include #include -/* *INDENT-OFF* */ -static struct crm_option long_options[] = { - /* Top-level Options */ - {"help", 0, 0, '?', "\t\tThis text"}, - {"version", 0, 0, '$', "\t\tVersion information" }, - {"verbose", 0, 0, 'V', "\t\tIncrease debug output\n"}, - - {"-spacer-", 1, 0, '-', "\nOriginal XML:"}, - {"original", 1, 0, 'o', "\tXML is contained in the named file"}, - {"original-string", 1, 0, 'O', "XML is contained in the supplied string"}, - - {"-spacer-", 1, 0, '-', "\nOperation:"}, - {"new", 1, 0, 'n', "\tCompare the original XML to the contents of the named file"}, - {"new-string", 1, 0, 'N', "\tCompare the original XML to the contents of the supplied string"}, - {"patch", 1, 0, 'p', "\tPatch the original XML with the contents of the named file"}, - - {"-spacer-", 1, 0, '-', "\nAdditional Options:"}, - {"cib", 0, 0, 'c', "\t\tCompare/patch the inputs as a CIB (includes versions details)"}, - {"stdin", 0, 0, 's', NULL, 1}, - {"no-version", 0, 0, 'u', "\tGenerate the difference without versions details"}, - {"-spacer-", 1, 0, '-', "\nExamples:", pcmk_option_paragraph}, - {"-spacer-", 1, 0, '-', "Obtain the two different configuration files by running cibadmin on the two cluster setups to compare:", pcmk_option_paragraph}, - {"-spacer-", 1, 0, '-', " cibadmin --query > cib-old.xml", pcmk_option_example}, - {"-spacer-", 1, 0, '-', " cibadmin --query > cib-new.xml", pcmk_option_example}, - {"-spacer-", 1, 0, '-', "Calculate and save the difference between the two files:", pcmk_option_paragraph}, - {"-spacer-", 1, 0, '-', " crm_diff --original cib-old.xml --new cib-new.xml > patch.xml", pcmk_option_example }, - {"-spacer-", 1, 0, '-', "Apply the patch to the original file:", pcmk_option_paragraph }, - {"-spacer-", 1, 0, '-', " crm_diff --original cib-old.xml --patch patch.xml > updated.xml", pcmk_option_example }, - {"-spacer-", 1, 0, '-', "Apply the patch to the running cluster:", pcmk_option_paragraph }, - {"-spacer-", 1, 0, '-', " cibadmin --patch patch.xml", pcmk_option_example }, - - {0, 0, 0, 0} +#define SUMMARY "Compare two Pacemaker configurations (in XML format) to produce a custom diff-like output, " \ + "or apply such an output as a patch" + +struct { + gboolean apply; + gboolean as_cib; + gboolean no_version; + gboolean raw_1; + gboolean raw_2; + gboolean use_stdin; + char *xml_file_1; + char *xml_file_2; +} 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 GOptionEntry original_xml_entries[] = { + { "original", 'o', 0, G_OPTION_ARG_STRING, &options.xml_file_1, + "XML is contained in the named file", + "FILE" }, + { "original-string", 'O', 0, G_OPTION_ARG_CALLBACK, original_string_cb, + "XML is contained in the supplied string", + "STRING" }, + + { NULL } }; -/* *INDENT-ON* */ + +static GOptionEntry operation_entries[] = { + { "new", 'n', 0, G_OPTION_ARG_STRING, &options.xml_file_2, + "Compare the original XML to the contents of the named file", + "FILE" }, + { "new-string", 'N', 0, G_OPTION_ARG_CALLBACK, new_string_cb, + "Compare the original XML with the contents of the supplied string", + "STRING" }, + { "patch", 'p', 0, G_OPTION_ARG_CALLBACK, patch_cb, + "Patch the original XML with the contents of the named file", + "FILE" }, + + { NULL } +}; + +static GOptionEntry addl_entries[] = { + { "cib", 'c', 0, G_OPTION_ARG_NONE, &options.as_cib, + "Compare/patch the inputs as a CIB (includes versions details)", + NULL }, + { "stdin", 's', 0, G_OPTION_ARG_NONE, &options.use_stdin, + "", + NULL }, + { "no-version", 'u', 0, G_OPTION_ARG_NONE, &options.no_version, + "Generate the difference without versions details", + NULL }, + + { NULL } +}; + +gboolean +new_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { + options.raw_2 = TRUE; + + if (options.xml_file_2 != NULL) { + free(options.xml_file_2); + } + + options.xml_file_2 = strdup(optarg); + return TRUE; +} + +gboolean +original_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { + options.raw_1 = TRUE; + + if (options.xml_file_1 != NULL) { + free(options.xml_file_1); + } + + options.xml_file_1 = strdup(optarg); + return TRUE; +} + +gboolean +patch_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { + options.apply = TRUE; + + if (options.xml_file_2 != NULL) { + free(options.xml_file_2); + } + + options.xml_file_2 = strdup(optarg); + return TRUE; +} static void print_patch(xmlNode *patch) { char *buffer = dump_xml_formatted(patch); printf("%s\n", crm_str(buffer)); free(buffer); fflush(stdout); } static int apply_patch(xmlNode *input, xmlNode *patch, gboolean as_cib) { int rc; xmlNode *output = copy_xml(input); rc = xml_apply_patchset(output, patch, as_cib); if (rc != pcmk_ok) { fprintf(stderr, "Could not apply patch: %s\n", pcmk_strerror(rc)); free_xml(output); return rc; } if (output != NULL) { const char *version; char *buffer; print_patch(output); version = crm_element_value(output, XML_ATTR_CRM_VERSION); buffer = calculate_xml_versioned_digest(output, FALSE, TRUE, version); crm_trace("Digest: %s\n", crm_str(buffer)); free(buffer); free_xml(output); } return pcmk_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; xml_patch_versions(patch, add, del); fmt = crm_element_value(patch, "format"); digest = crm_element_value(patch, XML_ATTR_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); } } static void strip_patch_cib_version(xmlNode *patch, const char **vfields, size_t nvfields) { int format = 1; crm_element_value_int(patch, "format", &format); if (format == 2) { xmlNode *version_xml = find_xml_node(patch, "version", FALSE); if (version_xml) { free_xml(version_xml); } } else { int i = 0; const char *tags[] = { XML_TAG_DIFF_REMOVED, XML_TAG_DIFF_ADDED, }; for (i = 0; i < DIMOF(tags); i++) { xmlNode *tmp = NULL; int lpc; tmp = find_xml_node(patch, tags[i], FALSE); if (tmp) { for (lpc = 0; lpc < nvfields; lpc++) { xml_remove_prop(tmp, vfields[lpc]); } tmp = find_xml_node(tmp, XML_TAG_CIB, FALSE); if (tmp) { for (lpc = 0; lpc < nvfields; lpc++) { xml_remove_prop(tmp, vfields[lpc]); } } } } } } static int generate_patch(xmlNode *object_1, xmlNode *object_2, const char *xml_file_2, gboolean as_cib, gboolean no_version) { xmlNode *output = NULL; const char *vfields[] = { XML_ATTR_GENERATION_ADMIN, XML_ATTR_GENERATION, XML_ATTR_NUMUPDATES, }; /* If we're ignoring the version, make the version information * identical, so it isn't detected as a change. */ if (no_version) { int lpc; for (lpc = 0; lpc < DIMOF(vfields); lpc++) { crm_copy_xml_element(object_1, object_2, vfields[lpc]); } } xml_track_changes(object_2, NULL, object_2, FALSE); if(as_cib) { xml_calculate_significant_changes(object_1, object_2); } else { xml_calculate_changes(object_1, object_2); } crm_log_xml_debug(object_2, (xml_file_2? xml_file_2: "target")); output = xml_create_patchset(0, object_1, object_2, NULL, FALSE); xml_log_changes(LOG_INFO, __FUNCTION__, object_2); xml_accept_changes(object_2); if (output == NULL) { return pcmk_ok; } patchset_process_digest(output, object_1, object_2, as_cib); if (as_cib) { log_patch_cib_versions(output); } else if (no_version) { strip_patch_cib_version(output, vfields, DIMOF(vfields)); } xml_log_patchset(LOG_NOTICE, __FUNCTION__, output); print_patch(output); free_xml(output); return -pcmk_err_generic; } +static GOptionContext * +build_arg_context(pcmk__common_args_t *args) { + GOptionContext *context = NULL; + + const char *description = "*Examples*\n\n" + "Obtain the two different configuration files by running cibadmin on the two cluster setups to compare:\n\n" + "\tcibadmin --query > cib-old.xml\n" + "\tcibadmin --query > cib-new.xml\n\n" + "Calculate and save the difference between the two files:\n\n" + "\tcrm_diff --original cib-old.xml --new cib-new.xml > patch.xml\n\n" + "Apply the patch to the original file:\n\n" + "\tcrm_diff --original cib-old.xml --patch patch.xml > updated.xml\n\n" + "Apply the patch to the running cluster:\n\n" + "\tcibadmin --patch patch.xml\n"; + + context = pcmk__build_arg_context(args, NULL, NULL); + g_option_context_set_description(context, description); + + pcmk__add_arg_group(context, "xml", "Original XML:", + "Show original XML options", original_xml_entries); + pcmk__add_arg_group(context, "operation", "Operation:", + "Show operation options", operation_entries); + pcmk__add_arg_group(context, "additional", "Additional Options:", + "Show additional options", addl_entries); + return context; +} + int main(int argc, char **argv) { - gboolean apply = FALSE; - gboolean raw_1 = FALSE; - gboolean raw_2 = FALSE; - gboolean use_stdin = FALSE; - gboolean as_cib = FALSE; - gboolean no_version = FALSE; - int argerr = 0; - int flag; int rc = pcmk_ok; xmlNode *object_1 = NULL; xmlNode *object_2 = NULL; - const char *xml_file_1 = NULL; - const char *xml_file_2 = NULL; - int option_index = 0; + pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY); + + GError *error = NULL; + GOptionContext *context = NULL; + gchar **processed_args = NULL; + + context = build_arg_context(args); crm_log_cli_init("crm_diff"); - crm_set_options(NULL, "original_xml operation [options]", long_options, - "crm_diff can compare two Pacemaker configurations (in XML format) to\n" - "produce a custom diff-like output, or apply such an output as a patch\n"); - if (argc < 2) { - crm_help('?', CRM_EX_USAGE); - } + processed_args = pcmk__cmdline_preproc(argc, argv, "nopNO"); - while (1) { - flag = crm_get_option(argc, argv, &option_index); - if (flag == -1) - break; - - switch (flag) { - case 'o': - xml_file_1 = optarg; - break; - case 'O': - xml_file_1 = optarg; - raw_1 = TRUE; - break; - case 'n': - xml_file_2 = optarg; - break; - case 'N': - xml_file_2 = optarg; - raw_2 = TRUE; - break; - case 'p': - xml_file_2 = optarg; - apply = TRUE; - break; - case 's': - use_stdin = TRUE; - break; - case 'c': - as_cib = TRUE; - break; - case 'u': - no_version = TRUE; - break; - case 'V': - crm_bump_log_level(argc, argv); - break; - case '?': - case '$': - crm_help(flag, CRM_EX_OK); - break; - default: - printf("Argument %c (0%o) is not (yet?) supported\n", flag, flag); - ++argerr; - break; - } + if (!g_option_context_parse_strv(context, &processed_args, &error)) { + fprintf(stderr, "%s: %s\n", g_get_prgname(), error->message); + rc = CRM_EX_USAGE; + goto done; } - if (optind < argc) { - printf("non-option ARGV-elements: "); - while (optind < argc) - printf("%s ", argv[optind++]); - printf("\n"); + for (int i = 0; i < args->verbosity; i++) { + crm_bump_log_level(argc, argv); } - if (optind > argc) { - ++argerr; + if (args->version) { + /* FIXME: When crm_diff is converted to use formatted output, this can go. */ + crm_help('v', CRM_EX_USAGE); } - if (argerr) { - crm_help('?', CRM_EX_USAGE); + if (optind > argc) { + fprintf(stderr, "%s", g_option_context_get_help(context, TRUE, NULL)); + rc = CRM_EX_USAGE; + goto done; } - if (apply && no_version) { + if (options.apply && options.no_version) { fprintf(stderr, "warning: -u/--no-version ignored with -p/--patch\n"); - } else if (as_cib && no_version) { + } else if (options.as_cib && options.no_version) { fprintf(stderr, "error: -u/--no-version incompatible with -c/--cib\n"); - return CRM_EX_USAGE; + rc = CRM_EX_USAGE; + goto done; } - if (raw_1) { - object_1 = string2xml(xml_file_1); + if (options.raw_1) { + object_1 = string2xml(options.xml_file_1); - } else if (use_stdin) { + } else if (options.use_stdin) { fprintf(stderr, "Input first XML fragment:"); object_1 = stdin2xml(); - } else if (xml_file_1 != NULL) { - object_1 = filename2xml(xml_file_1); + } else if (options.xml_file_1 != NULL) { + object_1 = filename2xml(options.xml_file_1); } - if (raw_2) { - object_2 = string2xml(xml_file_2); + if (options.raw_2) { + object_2 = string2xml(options.xml_file_2); - } else if (use_stdin) { + } else if (options.use_stdin) { fprintf(stderr, "Input second XML fragment:"); object_2 = stdin2xml(); - } else if (xml_file_2 != NULL) { - object_2 = filename2xml(xml_file_2); + } else if (options.xml_file_2 != NULL) { + object_2 = filename2xml(options.xml_file_2); } if (object_1 == NULL) { fprintf(stderr, "Could not parse the first XML fragment\n"); - return CRM_EX_DATAERR; + rc = CRM_EX_DATAERR; + goto done; } if (object_2 == NULL) { fprintf(stderr, "Could not parse the second XML fragment\n"); - return CRM_EX_DATAERR; + rc = CRM_EX_DATAERR; + goto done; } - if (apply) { - rc = apply_patch(object_1, object_2, as_cib); + if (options.apply) { + rc = apply_patch(object_1, object_2, options.as_cib); } else { - rc = generate_patch(object_1, object_2, xml_file_2, as_cib, no_version); + rc = generate_patch(object_1, object_2, options.xml_file_2, options.as_cib, options.no_version); } +done: + pcmk__free_arg_context(context); + free(options.xml_file_1); + free(options.xml_file_2); free_xml(object_1); free_xml(object_2); return crm_errno2exit(rc); }