Page Menu
Home
ClusterLabs Projects
Search
Configure Global Search
Log In
Files
F4185678
crm_shadow.c
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
38 KB
Referenced Files
None
Subscribers
None
crm_shadow.c
View Options
/*
* Copyright 2004-2024 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/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] args Message-specific arguments
*
* \return Standard Pacemaker return code
*
* \note \p args should contain the following:
* -# 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] args Message-specific arguments
*
* \return Standard Pacemaker return code
*
* \note \p args should contain the following:
* -# 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] args Message-specific arguments
*
* \return Standard Pacemaker return code
*
* \note \p args should contain the following:
* -# 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 *", "const xmlNode *",
"const xmlNode *", "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 *);
const xmlNode *content = va_arg(args, const xmlNode *);
const xmlNode *diff = va_arg(args, const xmlNode *);
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) {
GString *buf = g_string_sized_new(1024);
gchar *str = NULL;
pcmk__xml_string(content, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text,
buf, 0);
str = g_string_free(buf, FALSE);
str = pcmk__trim(str);
if (!pcmk__str_empty(str)) {
out->info(out, "%s", str);
}
g_free(str);
} 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] args Message-specific arguments
*
* \return Standard Pacemaker return code
*
* \note \p args should contain the following:
* -# 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 *", "const xmlNode *",
"const xmlNode *", "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 *);
const xmlNode *content = va_arg(args, const xmlNode *);
const xmlNode *diff = va_arg(args, const xmlNode *);
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)) {
GString *buf = g_string_sized_new(1024);
gchar *str = NULL;
pcmk__xml_string(content, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text,
buf, 0);
str = g_string_free(buf, FALSE);
str = pcmk__trim(str);
rc = out->info(out, "%s", str);
g_free(str);
}
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] args Message-specific arguments
*
* \return Standard Pacemaker return code
*
* \note \p args should contain the following:
* -# 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 *", "const xmlNode *",
"const xmlNode *", "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 *);
const xmlNode *content = va_arg(args, const xmlNode *);
const xmlNode *diff = va_arg(args, const xmlNode *);
enum shadow_disp_flags flags G_GNUC_UNUSED =
(enum shadow_disp_flags) va_arg(args, int);
pcmk__output_xml_create_parent(out, PCMK_XE_SHADOW,
PCMK_XA_INSTANCE, instance,
PCMK_XA_FILE, filename,
NULL);
if (content != NULL) {
GString *buf = g_string_sized_new(1024);
pcmk__xml_string(content, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, buf,
0);
out->output_xml(out, PCMK_XE_CONTENT, buf->str);
g_string_free(buf, TRUE);
}
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)) {
int rc = errno;
exit_code = pcmk_rc2exitc(rc);
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"Could not access shadow instance '%s': %s",
options.instance, strerror(rc));
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 = cib__signon_attempts(*real_cib, cib_command, 5);
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 = pcmk__xml_read(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] 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(const xmlNode *xml, const char *filename, bool reset,
GError **error)
{
int rc = pcmk__xml_write_file(xml, filename, false);
if (rc != pcmk_rc_ok) {
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;
}
/*!
* \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, PCMK_XE_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 = PCMK_XE_CONFIGURATION;
section_xml = pcmk__xe_first_child(input, section, NULL, NULL);
}
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);
pcmk__xml_free(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, PCMK_XA_VALIDATE_WITH, options.validate_with);
out->info(out, "Created new %s configuration",
crm_element_value(output, PCMK_XA_VALIDATE_WITH));
if (write_shadow_file(output, filename, false, error) != pcmk_rc_ok) {
goto done;
}
shadow_setup(out, false, error);
done:
free(filename);
pcmk__xml_free(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);
pcmk__xml_free(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;
pcmk__xml_free(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;
bool quiet_orig = out->quiet;
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);
pcmk__log_xml_changes(LOG_INFO, new_config);
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);
pcmk__xml_free(old_config);
pcmk__xml_free(new_config);
pcmk__xml_free(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);
}
File Metadata
Details
Attached
Mime Type
text/x-c
Expires
Thu, Jun 5, 11:12 PM (4 h, 32 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1811479
Default Alt Text
crm_shadow.c (38 KB)
Attached To
Mode
rP Pacemaker
Attached
Detach File
Event Timeline
Log In to Comment