diff options
Diffstat (limited to '')
-rw-r--r-- | tools/crm_shadow.c | 1322 |
1 files changed, 1322 insertions, 0 deletions
diff --git a/tools/crm_shadow.c b/tools/crm_shadow.c new file mode 100644 index 0000000..ef69502 --- /dev/null +++ b/tools/crm_shadow.c @@ -0,0 +1,1322 @@ +/* + * Copyright 2004-2023 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 <crm_internal.h> + +#include <stdio.h> +#include <unistd.h> + +#include <sys/param.h> +#include <crm/crm.h> +#include <sys/stat.h> +#include <sys/types.h> + +#include <stdlib.h> +#include <errno.h> +#include <fcntl.h> +#include <crm/msg_xml.h> + +#include <crm/common/cmdline_internal.h> +#include <crm/common/ipc.h> +#include <crm/common/output_internal.h> +#include <crm/common/xml.h> + +#include <crm/cib.h> +#include <crm/cib/internal.h> + +#define SUMMARY "perform Pacemaker configuration changes in a sandbox\n\n" \ + "This command sets up an environment in which " \ + "configuration tools (cibadmin,\n" \ + "crm_resource, etc.) work offline instead of against a " \ + "live cluster, allowing\n" \ + "changes to be previewed and tested for side effects." + +#define INDENT " " + +enum shadow_command { + shadow_cmd_none = 0, + shadow_cmd_which, + shadow_cmd_display, + shadow_cmd_diff, + shadow_cmd_file, + shadow_cmd_create, + shadow_cmd_create_empty, + shadow_cmd_commit, + shadow_cmd_delete, + shadow_cmd_edit, + shadow_cmd_reset, + shadow_cmd_switch, +}; + +/*! + * \internal + * \enum shadow_disp_flags + * \brief Bit flags to control which fields of shadow CIB info are displayed + * + * \note Ignored for XML output. + */ +enum shadow_disp_flags { + shadow_disp_instance = (1 << 0), + shadow_disp_file = (1 << 1), + shadow_disp_content = (1 << 2), + shadow_disp_diff = (1 << 3), +}; + +static crm_exit_t exit_code = CRM_EX_OK; + +static struct { + enum shadow_command cmd; + int cmd_options; + char *instance; + gboolean force; + gboolean batch; + gboolean full_upload; + gchar *validate_with; +} options = { + .cmd_options = cib_sync_call, +}; + +/*! + * \internal + * \brief Display an instruction to the user + * + * \param[in,out] out Output object + * \param[in] ... Message arguments + * + * \return Standard Pacemaker return code + * + * \note The variadic message arguments are of the following format: + * -# Instructional message + */ +PCMK__OUTPUT_ARGS("instruction", "const char *") +static int +instruction_default(pcmk__output_t *out, va_list args) +{ + const char *msg = va_arg(args, const char *); + + if (msg == NULL) { + return pcmk_rc_no_output; + } + return out->info(out, "%s", msg); +} + +/*! + * \internal + * \brief Display an instruction to the user + * + * \param[in,out] out Output object + * \param[in] ... Message arguments + * + * \return Standard Pacemaker return code + * + * \note The variadic message arguments are of the following format: + * -# Instructional message + */ +PCMK__OUTPUT_ARGS("instruction", "const char *") +static int +instruction_xml(pcmk__output_t *out, va_list args) +{ + const char *msg = va_arg(args, const char *); + + if (msg == NULL) { + return pcmk_rc_no_output; + } + pcmk__output_create_xml_text_node(out, "instruction", msg); + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Display information about a shadow CIB instance + * + * \param[in,out] out Output object + * \param[in] ... Message arguments + * + * \return Standard Pacemaker return code + * + * \note The variadic message arguments are of the following format: + * -# Instance name (can be \p NULL) + * -# Shadow file name (can be \p NULL) + * -# Shadow file content (can be \p NULL) + * -# Patchset containing the changes in the shadow CIB (can be \p NULL) + * -# Group of \p shadow_disp_flags indicating which fields to display + */ +PCMK__OUTPUT_ARGS("shadow", "const char *", "const char *", "xmlNodePtr", + "xmlNodePtr", "enum shadow_disp_flags") +static int +shadow_default(pcmk__output_t *out, va_list args) +{ + const char *instance = va_arg(args, const char *); + const char *filename = va_arg(args, const char *); + xmlNodePtr content = va_arg(args, xmlNodePtr); + xmlNodePtr diff = va_arg(args, xmlNodePtr); + enum shadow_disp_flags flags = (enum shadow_disp_flags) va_arg(args, int); + + int rc = pcmk_rc_no_output; + + if (pcmk_is_set(flags, shadow_disp_instance)) { + rc = out->info(out, "Instance: %s", pcmk__s(instance, "<unknown>")); + } + if (pcmk_is_set(flags, shadow_disp_file)) { + rc = out->info(out, "File name: %s", pcmk__s(filename, "<unknown>")); + } + if (pcmk_is_set(flags, shadow_disp_content)) { + rc = out->info(out, "Content:"); + + if (content != NULL) { + char *buf = pcmk__trim(dump_xml_formatted_with_text(content)); + + if (!pcmk__str_empty(buf)) { + out->info(out, "%s", buf); + } + free(buf); + + } else { + out->info(out, "<unknown>"); + } + } + if (pcmk_is_set(flags, shadow_disp_diff)) { + rc = out->info(out, "Diff:"); + + if (diff != NULL) { + out->message(out, "xml-patchset", diff); + } else { + out->info(out, "<empty>"); + } + } + + return rc; +} + +/*! + * \internal + * \brief Display information about a shadow CIB instance + * + * \param[in,out] out Output object + * \param[in] ... Message arguments + * + * \return Standard Pacemaker return code + * + * \note The variadic message arguments are of the following format: + * -# Instance name (can be \p NULL) + * -# Shadow file name (can be \p NULL) + * -# Shadow file content (can be \p NULL) + * -# Patchset containing the changes in the shadow CIB (can be \p NULL) + * -# Group of \p shadow_disp_flags indicating which fields to display + */ +PCMK__OUTPUT_ARGS("shadow", "const char *", "const char *", "xmlNodePtr", + "xmlNodePtr", "enum shadow_disp_flags") +static int +shadow_text(pcmk__output_t *out, va_list args) +{ + if (!out->is_quiet(out)) { + return shadow_default(out, args); + + } else { + const char *instance = va_arg(args, const char *); + const char *filename = va_arg(args, const char *); + xmlNodePtr content = va_arg(args, xmlNodePtr); + xmlNodePtr diff = va_arg(args, xmlNodePtr); + enum shadow_disp_flags flags = (enum shadow_disp_flags) va_arg(args, int); + + int rc = pcmk_rc_no_output; + bool quiet_orig = out->quiet; + + /* We have to disable quiet mode for the "xml-patchset" message if we + * call it, so we might as well do so for this whole section. + */ + out->quiet = false; + + if (pcmk_is_set(flags, shadow_disp_instance) && (instance != NULL)) { + rc = out->info(out, "%s", instance); + } + if (pcmk_is_set(flags, shadow_disp_file) && (filename != NULL)) { + rc = out->info(out, "%s", filename); + } + if (pcmk_is_set(flags, shadow_disp_content) && (content != NULL)) { + char *buf = pcmk__trim(dump_xml_formatted_with_text(content)); + + rc = out->info(out, "%s", pcmk__trim(buf)); + free(buf); + } + if (pcmk_is_set(flags, shadow_disp_diff) && (diff != NULL)) { + rc = out->message(out, "xml-patchset", diff); + } + + out->quiet = quiet_orig; + return rc; + } +} + +/*! + * \internal + * \brief Display information about a shadow CIB instance + * + * \param[in,out] out Output object + * \param[in] ... Message arguments + * + * \return Standard Pacemaker return code + * + * \note The variadic message arguments are of the following format: + * -# Instance name (can be \p NULL) + * -# Shadow file name (can be \p NULL) + * -# Shadow file content (can be \p NULL) + * -# Patchset containing the changes in the shadow CIB (can be \p NULL) + * -# Group of \p shadow_disp_flags indicating which fields to display + * (ignored) + */ +PCMK__OUTPUT_ARGS("shadow", "const char *", "const char *", "xmlNodePtr", + "xmlNodePtr", "enum shadow_disp_flags") +static int +shadow_xml(pcmk__output_t *out, va_list args) +{ + const char *instance = va_arg(args, const char *); + const char *filename = va_arg(args, const char *); + xmlNodePtr content = va_arg(args, xmlNodePtr); + xmlNodePtr diff = va_arg(args, xmlNodePtr); + enum shadow_disp_flags flags G_GNUC_UNUSED = + (enum shadow_disp_flags) va_arg(args, int); + + pcmk__output_xml_create_parent(out, "shadow", + "instance", instance, + "file", filename, + NULL); + + if (content != NULL) { + char *buf = dump_xml_formatted_with_text(content); + + out->output_xml(out, "content", buf); + free(buf); + } + + if (diff != NULL) { + out->message(out, "xml-patchset", diff); + } + + pcmk__output_xml_pop_parent(out); + return pcmk_rc_ok; +} + +static const pcmk__supported_format_t formats[] = { + PCMK__SUPPORTED_FORMAT_NONE, + PCMK__SUPPORTED_FORMAT_TEXT, + PCMK__SUPPORTED_FORMAT_XML, + { NULL, NULL, NULL } +}; + +static const pcmk__message_entry_t fmt_functions[] = { + { "instruction", "default", instruction_default }, + { "instruction", "xml", instruction_xml }, + { "shadow", "default", shadow_default }, + { "shadow", "text", shadow_text }, + { "shadow", "xml", shadow_xml }, + + { NULL, NULL, NULL } +}; + +/*! + * \internal + * \brief Set the error when \p --force is not passed with a dangerous command + * + * \param[in] reason Why command is dangerous + * \param[in] for_shadow If true, command is dangerous to the shadow file. + * Otherwise, command is dangerous to the active + * cluster. + * \param[in] show_mismatch If true and the supplied shadow instance is not + * the same as the active shadow instance, report + * this + * \param[out] error Where to store error + */ +static void +set_danger_error(const char *reason, bool for_shadow, bool show_mismatch, + GError **error) +{ + const char *active = getenv("CIB_shadow"); + char *full = NULL; + + if (show_mismatch + && !pcmk__str_eq(active, options.instance, pcmk__str_null_matches)) { + + full = crm_strdup_printf("%s.\nAdditionally, the supplied shadow " + "instance (%s) is not the same as the active " + "one (%s)", + reason, options.instance, active); + reason = full; + } + + g_set_error(error, PCMK__EXITC_ERROR, exit_code, + "%s%sTo prevent accidental destruction of the %s, the --force " + "flag is required in order to proceed.", + pcmk__s(reason, ""), ((reason != NULL)? ".\n" : ""), + (for_shadow? "shadow file" : "cluster")); + free(full); +} + +/*! + * \internal + * \brief Get the active shadow instance from the environment + * + * This sets \p options.instance to the value of the \p CIB_shadow env variable. + * + * \param[out] error Where to store error + */ +static int +get_instance_from_env(GError **error) +{ + int rc = pcmk_rc_ok; + + pcmk__str_update(&options.instance, getenv("CIB_shadow")); + if (options.instance == NULL) { + rc = ENXIO; + exit_code = pcmk_rc2exitc(rc); + g_set_error(error, PCMK__EXITC_ERROR, exit_code, + "No active shadow configuration defined"); + } + return rc; +} + +/*! + * \internal + * \brief Validate that the shadow file does or does not exist, as appropriate + * + * \param[in] filename Absolute path of shadow file + * \param[in] should_exist Whether the shadow file is expected to exist + * \param[out] error Where to store error + * + * \return Standard Pacemaker return code + */ +static int +check_file_exists(const char *filename, bool should_exist, GError **error) +{ + struct stat buf; + + if (!should_exist && (stat(filename, &buf) == 0)) { + char *reason = crm_strdup_printf("A shadow instance '%s' already " + "exists", options.instance); + + exit_code = CRM_EX_CANTCREAT; + set_danger_error(reason, true, false, error); + free(reason); + return EEXIST; + } + + if (should_exist && (stat(filename, &buf) < 0)) { + // @COMPAT: Use pcmk_rc2exitc(errno)? + exit_code = CRM_EX_NOSUCH; + g_set_error(error, PCMK__EXITC_ERROR, exit_code, + "Could not access shadow instance '%s': %s", + options.instance, strerror(errno)); + return errno; + } + + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Connect to the "real" (non-shadow) CIB + * + * \param[out] real_cib Where to store CIB connection + * \param[out] error Where to store error + * + * \return Standard Pacemaker return code + */ +static int +connect_real_cib(cib_t **real_cib, GError **error) +{ + int rc = pcmk_rc_ok; + + *real_cib = cib_new_no_shadow(); + if (*real_cib == NULL) { + rc = ENOMEM; + exit_code = pcmk_rc2exitc(rc); + g_set_error(error, PCMK__EXITC_ERROR, exit_code, + "Could not create a CIB connection object"); + return rc; + } + + rc = (*real_cib)->cmds->signon(*real_cib, crm_system_name, cib_command); + rc = pcmk_legacy2rc(rc); + if (rc != pcmk_rc_ok) { + exit_code = pcmk_rc2exitc(rc); + g_set_error(error, PCMK__EXITC_ERROR, exit_code, + "Could not connect to CIB: %s", pcmk_rc_str(rc)); + } + return rc; +} + +/*! + * \internal + * \brief Query the "real" (non-shadow) CIB and store the result + * + * \param[out] output Where to store query output + * \param[out] error Where to store error + * + * \return Standard Pacemaker return code + */ +static int +query_real_cib(xmlNode **output, GError **error) +{ + cib_t *real_cib = NULL; + int rc = connect_real_cib(&real_cib, error); + + if (rc != pcmk_rc_ok) { + goto done; + } + + rc = real_cib->cmds->query(real_cib, NULL, output, options.cmd_options); + rc = pcmk_legacy2rc(rc); + if (rc != pcmk_rc_ok) { + exit_code = pcmk_rc2exitc(rc); + g_set_error(error, PCMK__EXITC_ERROR, exit_code, + "Could not query the non-shadow CIB: %s", pcmk_rc_str(rc)); + } + +done: + cib_delete(real_cib); + return rc; +} + +/*! + * \internal + * \brief Read XML from the given file + * + * \param[in] filename Path of input file + * \param[out] output Where to store XML read from \p filename + * \param[out] error Where to store error + * + * \return Standard Pacemaker return code + */ +static int +read_xml(const char *filename, xmlNode **output, GError **error) +{ + int rc = pcmk_rc_ok; + + *output = filename2xml(filename); + if (*output == NULL) { + rc = pcmk_rc_no_input; + exit_code = pcmk_rc2exitc(rc); + g_set_error(error, PCMK__EXITC_ERROR, exit_code, + "Could not parse XML from input file '%s'", filename); + } + return rc; +} + +/*! + * \internal + * \brief Write the shadow XML to a file + * + * \param[in,out] xml Shadow XML + * \param[in] filename Name of destination file + * \param[in] reset Whether the write is a reset (for logging only) + * \param[out] error Where to store error + */ +static int +write_shadow_file(xmlNode *xml, const char *filename, bool reset, + GError **error) +{ + int rc = write_xml_file(xml, filename, FALSE); + + if (rc < 0) { + rc = pcmk_legacy2rc(rc); + exit_code = pcmk_rc2exitc(rc); + g_set_error(error, PCMK__EXITC_ERROR, exit_code, + "Could not %s the shadow instance '%s': %s", + reset? "reset" : "create", options.instance, + pcmk_rc_str(rc)); + return rc; + } + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Create a shell prompt based on the given shadow instance name + * + * \return Newly created prompt + * + * \note The caller is responsible for freeing the return value using \p free(). + */ +static inline char * +get_shadow_prompt(void) +{ + return crm_strdup_printf("shadow[%.40s] # ", options.instance); +} + +/*! + * \internal + * \brief Set up environment variables for a shadow instance + * + * \param[in,out] out Output object + * \param[in] do_switch If true, switch to an existing instance (logging + * only) + * \param[out] error Where to store error + */ +static void +shadow_setup(pcmk__output_t *out, bool do_switch, GError **error) +{ + const char *active = getenv("CIB_shadow"); + const char *prompt = getenv("PS1"); + const char *shell = getenv("SHELL"); + char *new_prompt = get_shadow_prompt(); + + if (pcmk__str_eq(active, options.instance, pcmk__str_none) + && pcmk__str_eq(new_prompt, prompt, pcmk__str_none)) { + // CIB_shadow and prompt environment variables are already set up + goto done; + } + + if (!options.batch && (shell != NULL)) { + out->info(out, "Setting up shadow instance"); + setenv("PS1", new_prompt, 1); + setenv("CIB_shadow", options.instance, 1); + + out->message(out, "instruction", + "Press Ctrl+D to exit the crm_shadow shell"); + + if (pcmk__str_eq(shell, "(^|/)bash$", pcmk__str_regex)) { + execl(shell, shell, "--norc", "--noprofile", NULL); + } else { + execl(shell, shell, NULL); + } + + exit_code = pcmk_rc2exitc(errno); + g_set_error(error, PCMK__EXITC_ERROR, exit_code, + "Failed to launch shell '%s': %s", + shell, pcmk_rc_str(errno)); + + } else { + char *msg = NULL; + const char *prefix = "A new shadow instance was created. To begin " + "using it"; + + if (do_switch) { + prefix = "To switch to the named shadow instance"; + } + + msg = crm_strdup_printf("%s, enter the following into your shell:\n" + "\texport CIB_shadow=%s", + prefix, options.instance); + out->message(out, "instruction", msg); + free(msg); + } + +done: + free(new_prompt); +} + +/*! + * \internal + * \brief Remind the user to clean up the shadow environment + * + * \param[in,out] out Output object + */ +static void +shadow_teardown(pcmk__output_t *out) +{ + const char *active = getenv("CIB_shadow"); + const char *prompt = getenv("PS1"); + + if (pcmk__str_eq(active, options.instance, pcmk__str_none)) { + char *our_prompt = get_shadow_prompt(); + + if (pcmk__str_eq(prompt, our_prompt, pcmk__str_none)) { + out->message(out, "instruction", + "Press Ctrl+D to exit the crm_shadow shell"); + + } else { + out->message(out, "instruction", + "Remember to unset the CIB_shadow variable by " + "entering the following into your shell:\n" + "\tunset CIB_shadow"); + } + free(our_prompt); + } +} + +/*! + * \internal + * \brief Commit the shadow file contents to the active cluster + * + * \param[out] error Where to store error + */ +static void +commit_shadow_file(GError **error) +{ + char *filename = NULL; + cib_t *real_cib = NULL; + + xmlNodePtr input = NULL; + xmlNodePtr section_xml = NULL; + const char *section = NULL; + + int rc = pcmk_rc_ok; + + if (!options.force) { + const char *reason = "The commit command overwrites the active cluster " + "configuration"; + + exit_code = CRM_EX_USAGE; + set_danger_error(reason, false, true, error); + return; + } + + filename = get_shadow_file(options.instance); + if (check_file_exists(filename, true, error) != pcmk_rc_ok) { + goto done; + } + + if (connect_real_cib(&real_cib, error) != pcmk_rc_ok) { + goto done; + } + + if (read_xml(filename, &input, error) != pcmk_rc_ok) { + goto done; + } + + section_xml = input; + + if (!options.full_upload) { + section = XML_CIB_TAG_CONFIGURATION; + section_xml = first_named_child(input, section); + } + + rc = real_cib->cmds->replace(real_cib, section, section_xml, + options.cmd_options); + rc = pcmk_legacy2rc(rc); + + if (rc != pcmk_rc_ok) { + exit_code = pcmk_rc2exitc(rc); + g_set_error(error, PCMK__EXITC_ERROR, exit_code, + "Could not commit shadow instance '%s' to the CIB: %s", + options.instance, pcmk_rc_str(rc)); + } + +done: + free(filename); + cib_delete(real_cib); + free_xml(input); +} + +/*! + * \internal + * \brief Create a new empty shadow instance + * + * \param[in,out] out Output object + * \param[out] error Where to store error + * + * \note If \p --force is given, we try to write the file regardless of whether + * it already exists. + */ +static void +create_shadow_empty(pcmk__output_t *out, GError **error) +{ + char *filename = get_shadow_file(options.instance); + xmlNode *output = NULL; + + if (!options.force + && (check_file_exists(filename, false, error) != pcmk_rc_ok)) { + goto done; + } + + output = createEmptyCib(0); + crm_xml_add(output, XML_ATTR_VALIDATION, options.validate_with); + out->info(out, "Created new %s configuration", + crm_element_value(output, XML_ATTR_VALIDATION)); + + if (write_shadow_file(output, filename, false, error) != pcmk_rc_ok) { + goto done; + } + shadow_setup(out, false, error); + +done: + free(filename); + free_xml(output); +} + +/*! + * \internal + * \brief Create a shadow instance based on the active CIB + * + * \param[in,out] out Output object + * \param[in] reset If true, overwrite the given existing shadow instance. + * Otherwise, create a new shadow instance with the given + * name. + * \param[out] error Where to store error + * + * \note If \p --force is given, we try to write the file regardless of whether + * it already exists. + */ +static void +create_shadow_from_cib(pcmk__output_t *out, bool reset, GError **error) +{ + char *filename = get_shadow_file(options.instance); + xmlNode *output = NULL; + + if (!options.force) { + if (reset) { + /* @COMPAT: Reset is dangerous to the shadow file, but to preserve + * compatibility we can't require --force unless there's a mismatch. + * At a compatibility break, call set_danger_error() with for_shadow + * and show_mismatch set to true. + */ + const char *local = getenv("CIB_shadow"); + + if (!pcmk__str_eq(local, options.instance, pcmk__str_null_matches)) { + exit_code = CRM_EX_USAGE; + g_set_error(error, PCMK__EXITC_ERROR, exit_code, + "The supplied shadow instance (%s) is not the same " + "as the active one (%s).\n" + "To prevent accidental destruction of the shadow " + "file, the --force flag is required in order to " + "proceed.", + options.instance, local); + goto done; + } + } + + if (check_file_exists(filename, reset, error) != pcmk_rc_ok) { + goto done; + } + } + + if (query_real_cib(&output, error) != pcmk_rc_ok) { + goto done; + } + + if (write_shadow_file(output, filename, reset, error) != pcmk_rc_ok) { + goto done; + } + shadow_setup(out, false, error); + +done: + free(filename); + free_xml(output); +} + +/*! + * \internal + * \brief Delete the shadow file + * + * \param[in,out] out Output object + * \param[out] error Where to store error + */ +static void +delete_shadow_file(pcmk__output_t *out, GError **error) +{ + char *filename = NULL; + + if (!options.force) { + const char *reason = "The delete command removes the specified shadow " + "file"; + + exit_code = CRM_EX_USAGE; + set_danger_error(reason, true, true, error); + return; + } + + filename = get_shadow_file(options.instance); + + if ((unlink(filename) < 0) && (errno != ENOENT)) { + exit_code = pcmk_rc2exitc(errno); + g_set_error(error, PCMK__EXITC_ERROR, exit_code, + "Could not remove shadow instance '%s': %s", + options.instance, strerror(errno)); + } else { + shadow_teardown(out); + } + free(filename); +} + +/*! + * \internal + * \brief Open the shadow file in a text editor + * + * \param[out] error Where to store error + * + * \note The \p EDITOR environment variable must be set. + */ +static void +edit_shadow_file(GError **error) +{ + char *filename = NULL; + const char *editor = NULL; + + if (get_instance_from_env(error) != pcmk_rc_ok) { + return; + } + + filename = get_shadow_file(options.instance); + if (check_file_exists(filename, true, error) != pcmk_rc_ok) { + goto done; + } + + editor = getenv("EDITOR"); + if (editor == NULL) { + exit_code = CRM_EX_NOT_CONFIGURED; + g_set_error(error, PCMK__EXITC_ERROR, exit_code, + "No value for EDITOR defined"); + goto done; + } + + execlp(editor, "--", filename, NULL); + exit_code = CRM_EX_OSFILE; + g_set_error(error, PCMK__EXITC_ERROR, exit_code, + "Could not invoke EDITOR (%s %s): %s", + editor, filename, strerror(errno)); + +done: + free(filename); +} + +/*! + * \internal + * \brief Show the contents of the active shadow instance + * + * \param[in,out] out Output object + * \param[out] error Where to store error + */ +static void +show_shadow_contents(pcmk__output_t *out, GError **error) +{ + char *filename = NULL; + + if (get_instance_from_env(error) != pcmk_rc_ok) { + return; + } + + filename = get_shadow_file(options.instance); + + if (check_file_exists(filename, true, error) == pcmk_rc_ok) { + xmlNode *output = NULL; + bool quiet_orig = out->quiet; + + if (read_xml(filename, &output, error) != pcmk_rc_ok) { + goto done; + } + + out->quiet = true; + out->message(out, "shadow", + options.instance, NULL, output, NULL, shadow_disp_content); + out->quiet = quiet_orig; + + free_xml(output); + } + +done: + free(filename); +} + +/*! + * \internal + * \brief Show the changes in the active shadow instance + * + * \param[in,out] out Output object + * \param[out] error Where to store error + */ +static void +show_shadow_diff(pcmk__output_t *out, GError **error) +{ + char *filename = NULL; + xmlNodePtr old_config = NULL; + xmlNodePtr new_config = NULL; + xmlNodePtr diff = NULL; + pcmk__output_t *logger_out = NULL; + bool quiet_orig = out->quiet; + int rc = pcmk_rc_ok; + + if (get_instance_from_env(error) != pcmk_rc_ok) { + return; + } + + filename = get_shadow_file(options.instance); + if (check_file_exists(filename, true, error) != pcmk_rc_ok) { + goto done; + } + + if (query_real_cib(&old_config, error) != pcmk_rc_ok) { + goto done; + } + + if (read_xml(filename, &new_config, error) != pcmk_rc_ok) { + goto done; + } + xml_track_changes(new_config, NULL, new_config, false); + xml_calculate_changes(old_config, new_config); + diff = xml_create_patchset(0, old_config, new_config, NULL, false); + + rc = pcmk__log_output_new(&logger_out); + if (rc != pcmk_rc_ok) { + exit_code = pcmk_rc2exitc(rc); + g_set_error(error, PCMK__EXITC_ERROR, exit_code, + "Could not create logger object: %s", pcmk_rc_str(rc)); + goto done; + } + pcmk__output_set_log_level(logger_out, LOG_INFO); + rc = pcmk__xml_show_changes(logger_out, new_config); + logger_out->finish(logger_out, pcmk_rc2exitc(rc), true, NULL); + pcmk__output_free(logger_out); + + xml_accept_changes(new_config); + + out->quiet = true; + out->message(out, "shadow", + options.instance, NULL, NULL, diff, shadow_disp_diff); + out->quiet = quiet_orig; + + if (diff != NULL) { + /* @COMPAT: Exit with CRM_EX_DIGEST? This is not really an error; we + * just want to indicate that there are differences (as the diff command + * does). + */ + exit_code = CRM_EX_ERROR; + } + +done: + free(filename); + free_xml(old_config); + free_xml(new_config); + free_xml(diff); +} + +/*! + * \internal + * \brief Show the absolute path of the active shadow instance + * + * \param[in,out] out Output object + * \param[out] error Where to store error + */ +static void +show_shadow_filename(pcmk__output_t *out, GError **error) +{ + if (get_instance_from_env(error) == pcmk_rc_ok) { + char *filename = get_shadow_file(options.instance); + bool quiet_orig = out->quiet; + + out->quiet = true; + out->message(out, "shadow", + options.instance, filename, NULL, NULL, shadow_disp_file); + out->quiet = quiet_orig; + + free(filename); + } +} + +/*! + * \internal + * \brief Show the active shadow instance + * + * \param[in,out] out Output object + * \param[out] error Where to store error + */ +static void +show_shadow_instance(pcmk__output_t *out, GError **error) +{ + if (get_instance_from_env(error) == pcmk_rc_ok) { + bool quiet_orig = out->quiet; + + out->quiet = true; + out->message(out, "shadow", + options.instance, NULL, NULL, NULL, shadow_disp_instance); + out->quiet = quiet_orig; + } +} + +/*! + * \internal + * \brief Switch to the given shadow instance + * + * \param[in,out] out Output object + * \param[out] error Where to store error + */ +static void +switch_shadow_instance(pcmk__output_t *out, GError **error) +{ + char *filename = NULL; + + filename = get_shadow_file(options.instance); + if (check_file_exists(filename, true, error) == pcmk_rc_ok) { + shadow_setup(out, true, error); + } + free(filename); +} + +static gboolean +command_cb(const gchar *option_name, const gchar *optarg, gpointer data, + GError **error) +{ + if (pcmk__str_any_of(option_name, "-w", "--which", NULL)) { + options.cmd = shadow_cmd_which; + + } else if (pcmk__str_any_of(option_name, "-p", "--display", NULL)) { + options.cmd = shadow_cmd_display; + + } else if (pcmk__str_any_of(option_name, "-d", "--diff", NULL)) { + options.cmd = shadow_cmd_diff; + + } else if (pcmk__str_any_of(option_name, "-F", "--file", NULL)) { + options.cmd = shadow_cmd_file; + + } else if (pcmk__str_any_of(option_name, "-c", "--create", NULL)) { + options.cmd = shadow_cmd_create; + + } else if (pcmk__str_any_of(option_name, "-e", "--create-empty", NULL)) { + options.cmd = shadow_cmd_create_empty; + + } else if (pcmk__str_any_of(option_name, "-C", "--commit", NULL)) { + options.cmd = shadow_cmd_commit; + + } else if (pcmk__str_any_of(option_name, "-D", "--delete", NULL)) { + options.cmd = shadow_cmd_delete; + + } else if (pcmk__str_any_of(option_name, "-E", "--edit", NULL)) { + options.cmd = shadow_cmd_edit; + + } else if (pcmk__str_any_of(option_name, "-r", "--reset", NULL)) { + options.cmd = shadow_cmd_reset; + + } else if (pcmk__str_any_of(option_name, "-s", "--switch", NULL)) { + options.cmd = shadow_cmd_switch; + + } else { + // Should be impossible + return FALSE; + } + + // optarg may be NULL and that's okay + pcmk__str_update(&options.instance, optarg); + return TRUE; +} + +static GOptionEntry query_entries[] = { + { "which", 'w', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, + "Indicate the active shadow copy", NULL }, + + { "display", 'p', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, + "Display the contents of the active shadow copy", NULL }, + + { "diff", 'd', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, + "Display the changes in the active shadow copy", NULL }, + + { "file", 'F', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, + "Display the location of the active shadow copy file", NULL }, + + { NULL } +}; + +static GOptionEntry command_entries[] = { + { "create", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb, + "Create the named shadow copy of the active cluster configuration", + "name" }, + + { "create-empty", 'e', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, + command_cb, + "Create the named shadow copy with an empty cluster configuration.\n" + INDENT "Optional: --validate-with", "name" }, + + { "commit", 'C', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb, + "Upload the contents of the named shadow copy to the cluster", "name" }, + + { "delete", 'D', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb, + "Delete the contents of the named shadow copy", "name" }, + + { "edit", 'E', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, + "Edit the contents of the active shadow copy with your favorite $EDITOR", + NULL }, + + { "reset", 'r', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb, + "Recreate named shadow copy from the active cluster configuration", + "name" }, + + { "switch", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb, + "(Advanced) Switch to the named shadow copy", "name" }, + + { NULL } +}; + +static GOptionEntry addl_entries[] = { + { "force", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.force, + "(Advanced) Force the action to be performed", NULL }, + + { "batch", 'b', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.batch, + "(Advanced) Don't spawn a new shell", NULL }, + + { "all", 'a', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.full_upload, + "(Advanced) Upload entire CIB, including status, with --commit", NULL }, + + { "validate-with", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, + &options.validate_with, + "(Advanced) Create an older configuration version", NULL }, + + { NULL } +}; + +static GOptionContext * +build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) +{ + const char *desc = NULL; + GOptionContext *context = NULL; + + desc = "Examples:\n\n" + "Create a blank shadow configuration:\n\n" + "\t# crm_shadow --create-empty myShadow\n\n" + "Create a shadow configuration from the running cluster\n\n" + "\t# crm_shadow --create myShadow\n\n" + "Display the current shadow configuration:\n\n" + "\t# crm_shadow --display\n\n" + "Discard the current shadow configuration (named myShadow):\n\n" + "\t# crm_shadow --delete myShadow --force\n\n" + "Upload current shadow configuration (named myShadow) to running " + "cluster:\n\n" + "\t# crm_shadow --commit myShadow\n\n"; + + context = pcmk__build_arg_context(args, "text (default), xml", group, + "<query>|<command>"); + g_option_context_set_description(context, desc); + + pcmk__add_arg_group(context, "queries", "Queries:", + "Show query help", query_entries); + pcmk__add_arg_group(context, "commands", "Commands:", + "Show command help", command_entries); + pcmk__add_arg_group(context, "additional", "Additional Options:", + "Show additional options", addl_entries); + return context; +} + +int +main(int argc, char **argv) +{ + int rc = pcmk_rc_ok; + 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, "CDcersv"); + GOptionContext *context = build_arg_context(args, &output_group); + + crm_log_preinit(NULL, argc, argv); + + pcmk__register_formats(output_group, formats); + + if (!g_option_context_parse_strv(context, &processed_args, &error)) { + exit_code = CRM_EX_USAGE; + goto done; + } + + 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 (g_strv_length(processed_args) > 1) { + gchar *help = g_option_context_get_help(context, TRUE, NULL); + GString *extra = g_string_sized_new(128); + + for (int lpc = 1; processed_args[lpc] != NULL; lpc++) { + if (extra->len > 0) { + g_string_append_c(extra, ' '); + } + g_string_append(extra, processed_args[lpc]); + } + + exit_code = CRM_EX_USAGE; + g_set_error(&error, PCMK__EXITC_ERROR, exit_code, + "non-option ARGV-elements: %s\n\n%s", extra->str, help); + g_free(help); + g_string_free(extra, TRUE); + goto done; + } + + if (args->version) { + out->version(out, false); + goto done; + } + + pcmk__register_messages(out, fmt_functions); + + if (options.cmd == shadow_cmd_none) { + // @COMPAT: Create a default command if other tools have one + gchar *help = g_option_context_get_help(context, TRUE, NULL); + + exit_code = CRM_EX_USAGE; + g_set_error(&error, PCMK__EXITC_ERROR, exit_code, + "Must specify a query or command option\n\n%s", help); + g_free(help); + goto done; + } + + pcmk__cli_init_logging("crm_shadow", args->verbosity); + + if (args->verbosity > 0) { + cib__set_call_options(options.cmd_options, crm_system_name, + cib_verbose); + } + + // Run the command + switch (options.cmd) { + case shadow_cmd_commit: + commit_shadow_file(&error); + break; + case shadow_cmd_create: + create_shadow_from_cib(out, false, &error); + break; + case shadow_cmd_create_empty: + create_shadow_empty(out, &error); + break; + case shadow_cmd_reset: + create_shadow_from_cib(out, true, &error); + break; + case shadow_cmd_delete: + delete_shadow_file(out, &error); + break; + case shadow_cmd_diff: + show_shadow_diff(out, &error); + break; + case shadow_cmd_display: + show_shadow_contents(out, &error); + break; + case shadow_cmd_edit: + edit_shadow_file(&error); + break; + case shadow_cmd_file: + show_shadow_filename(out, &error); + break; + case shadow_cmd_switch: + switch_shadow_instance(out, &error); + break; + case shadow_cmd_which: + show_shadow_instance(out, &error); + break; + default: + // Should never reach this point + break; + } + +done: + g_strfreev(processed_args); + pcmk__free_arg_context(context); + + pcmk__output_and_clear_error(&error, out); + + free(options.instance); + g_free(options.validate_with); + + if (out != NULL) { + out->finish(out, exit_code, true, NULL); + pcmk__output_free(out); + } + + crm_exit(exit_code); +} |