diff --git a/tools/Makefile.am b/tools/Makefile.am index a5350c2144..b012c7d99d 100644 --- a/tools/Makefile.am +++ b/tools/Makefile.am @@ -1,151 +1,158 @@ # -# Copyright 2004-2023 the Pacemaker project contributors +# Copyright 2004-2025 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)/mk/common.mk include $(top_srcdir)/mk/man.mk 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 = cluster-clean \ cluster-helper \ pcmk_simtimes EXTRA_DIST = $(wildcard *.inc) \ fix-manpages 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_CIBSECRETS +sbin_PROGRAMS += cibsecret +endif + ## SOURCES # A few tools are just thin wrappers around crm_attribute. # This makes their help get updated when crm_attribute changes # (see mk/common.mk). MAN8DEPS = crm_attribute crmadmin_SOURCES = crmadmin.c crmadmin_LDADD = $(top_builddir)/lib/pacemaker/libpacemaker.la crmadmin_LDADD += $(top_builddir)/lib/pengine/libpe_status.la crmadmin_LDADD += $(top_builddir)/lib/cib/libcib.la crmadmin_LDADD += $(top_builddir)/lib/common/libcrmcommon.la crm_error_SOURCES = crm_error.c crm_error_LDADD = $(top_builddir)/lib/pacemaker/libpacemaker.la crm_error_LDADD += $(top_builddir)/lib/common/libcrmcommon.la cibadmin_SOURCES = cibadmin.c cibadmin_LDADD = $(top_builddir)/lib/pacemaker/libpacemaker.la cibadmin_LDADD += $(top_builddir)/lib/cib/libcib.la cibadmin_LDADD += $(top_builddir)/lib/common/libcrmcommon.la +if BUILD_CIBSECRETS +cibsecret_SOURCES = cibsecret.c +cibsecret_LDADD = $(top_builddir)/lib/common/libcrmcommon.la +endif + crm_shadow_SOURCES = crm_shadow.c crm_shadow_LDADD = $(top_builddir)/lib/cib/libcib.la crm_shadow_LDADD += $(top_builddir)/lib/common/libcrmcommon.la crm_node_SOURCES = crm_node.c crm_node_LDADD = $(top_builddir)/lib/pacemaker/libpacemaker.la crm_node_LDADD += $(top_builddir)/lib/cib/libcib.la crm_node_LDADD += $(top_builddir)/lib/common/libcrmcommon.la crm_simulate_SOURCES = crm_simulate.c crm_simulate_LDADD = $(top_builddir)/lib/pacemaker/libpacemaker.la crm_simulate_LDADD += $(top_builddir)/lib/pengine/libpe_status.la crm_simulate_LDADD += $(top_builddir)/lib/cib/libcib.la crm_simulate_LDADD += $(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_LDADD = $(top_builddir)/lib/pacemaker/libpacemaker.la crm_mon_LDADD += $(top_builddir)/lib/pengine/libpe_status.la crm_mon_LDADD += $(top_builddir)/lib/fencing/libstonithd.la crm_mon_LDADD += $(top_builddir)/lib/cib/libcib.la crm_mon_LDADD += $(top_builddir)/lib/common/libcrmcommon.la crm_mon_LDADD += $(CURSES_LIBS) crm_verify_SOURCES = crm_verify.c crm_verify_LDADD = $(top_builddir)/lib/pacemaker/libpacemaker.la crm_verify_LDADD += $(top_builddir)/lib/pengine/libpe_status.la crm_verify_LDADD += $(top_builddir)/lib/cib/libcib.la crm_verify_LDADD += $(top_builddir)/lib/common/libcrmcommon.la crm_attribute_SOURCES = crm_attribute.c crm_attribute_LDADD = $(top_builddir)/lib/pacemaker/libpacemaker.la crm_attribute_LDADD += $(top_builddir)/lib/cib/libcib.la crm_attribute_LDADD += $(top_builddir)/lib/common/libcrmcommon.la crm_resource_SOURCES = crm_resource.c \ crm_resource_ban.c \ crm_resource_print.c \ crm_resource_runtime.c crm_resource_LDADD = $(top_builddir)/lib/pacemaker/libpacemaker.la crm_resource_LDADD += $(top_builddir)/lib/pengine/libpe_status.la crm_resource_LDADD += $(top_builddir)/lib/cib/libcib.la crm_resource_LDADD += $(top_builddir)/lib/lrmd/liblrmd.la crm_resource_LDADD += $(top_builddir)/lib/fencing/libstonithd.la crm_resource_LDADD += $(top_builddir)/lib/services/libcrmservice.la crm_resource_LDADD += $(top_builddir)/lib/common/libcrmcommon.la crm_rule_SOURCES = crm_rule.c crm_rule_LDADD = $(top_builddir)/lib/pacemaker/libpacemaker.la crm_rule_LDADD += $(top_builddir)/lib/pengine/libpe_status.la crm_rule_LDADD += $(top_builddir)/lib/cib/libcib.la crm_rule_LDADD += $(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/pacemaker/libpacemaker.la attrd_updater_LDADD += $(top_builddir)/lib/common/libcrmcommon.la crm_ticket_SOURCES = crm_ticket.c crm_ticket_LDADD = $(top_builddir)/lib/pacemaker/libpacemaker.la crm_ticket_LDADD += $(top_builddir)/lib/pengine/libpe_status.la crm_ticket_LDADD += $(top_builddir)/lib/cib/libcib.la crm_ticket_LDADD += $(top_builddir)/lib/common/libcrmcommon.la stonith_admin_SOURCES = stonith_admin.c stonith_admin_LDADD = $(top_builddir)/lib/pacemaker/libpacemaker.la stonith_admin_LDADD += $(top_builddir)/lib/pengine/libpe_status.la stonith_admin_LDADD += $(top_builddir)/lib/cib/libcib.la stonith_admin_LDADD += $(top_builddir)/lib/fencing/libstonithd.la stonith_admin_LDADD += $(top_builddir)/lib/common/libcrmcommon.la CLEANFILES = $(man8_MANS) diff --git a/tools/cibsecret.8.inc b/tools/cibsecret.8.inc new file mode 100644 index 0000000000..099d53f2ce --- /dev/null +++ b/tools/cibsecret.8.inc @@ -0,0 +1,11 @@ +[synopsis] +cibsecret [OPTION?] [options] + +/in Pacemaker CIB/ +.SH OPTIONS + +/for better performance./ +.SH SUBCOMMANDS + +/Make a sensitive resource parameter/ +.SH KNOWN LIMITATIONS diff --git a/tools/cibsecret.c b/tools/cibsecret.c new file mode 100644 index 0000000000..2b01ae00fe --- /dev/null +++ b/tools/cibsecret.c @@ -0,0 +1,316 @@ +/* + * Copyright 2025 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 // umask, S_IRGRP, S_IROTH, ... + +#include + +#include +#include + +#define SUMMARY "cibsecret - manage sensitive information in Pacemaker CIB" + +static gchar **remainder = NULL; +static gboolean no_cib = FALSE; + +static GOptionEntry entries[] = { + { "no-cib", 'C', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &no_cib, + "Don't read or write the CIB", + NULL }, + + { G_OPTION_REMAINING, 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING_ARRAY, &remainder, + NULL, + NULL }, + + { NULL } +}; + +struct subcommand_entry { + const char *name; + int args; + const char *usage; + bool requires_cib; + /* The shell version of cibsecret exited with a wide variety of error codes + * for all sorts of situations. Our standard Pacemaker return codes don't + * really line up with what it was doing - either we don't have a code with + * the right name, or we have one that doesn't map to the right exit code, + * etc. + * + * For backwards compatibility, the subcommand handler functions will + * return a standard Pacemaker so other functions here know what to do, but + * it will also take exit_code as an out parameter for the subcommands to + * set and for us to exit with. + */ + int (*handler)(pcmk__output_t *out, crm_exit_t *exit_code); +}; + +static int +subcommand_check(pcmk__output_t *out, crm_exit_t *exit_code) +{ + return pcmk_rc_ok; +} + +static int +subcommand_delete(pcmk__output_t *out, crm_exit_t *exit_code) +{ + return pcmk_rc_ok; +} + +static int +subcommand_get(pcmk__output_t *out, crm_exit_t *exit_code) +{ + return pcmk_rc_ok; +} + +/* The previous shell implementation of cibsecret allowed passing the value + * to set (what would be remainder[3] here) via stdin, which we do not support + * here at the moment. + */ +static int +subcommand_set(pcmk__output_t *out, crm_exit_t *exit_code) +{ + return pcmk_rc_ok; +} + +static int +subcommand_stash(pcmk__output_t *out, crm_exit_t *exit_code) +{ + return pcmk_rc_ok; +} + +static int +subcommand_sync(pcmk__output_t *out, crm_exit_t *exit_code) +{ + return pcmk_rc_ok; +} + +static int +subcommand_unstash(pcmk__output_t *out, crm_exit_t *exit_code) +{ + return pcmk_rc_ok; +} + +static struct subcommand_entry subcommand_table[] = { + { "check", 2, "check ", false, + subcommand_check }, + { "delete", 2, "delete ", false, + subcommand_delete }, + { "get", 2, "get ", false, + subcommand_get }, + { "set", 3, "set ", false, + subcommand_set }, + { "stash", 2, "stash ", true, + subcommand_stash }, + { "sync", 0, "sync", false, subcommand_sync }, + { "unstash", 2, "unstash ", true, + subcommand_unstash }, + { NULL }, +}; + +static 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, GOptionGroup **group) { + const char *desc = NULL; + GOptionContext *context = NULL; + + desc = "This command manages sensitive resource parameter values that should not be\n" + "stored directly in Pacemaker's Cluster Information Base (CIB). Such values\n" + "are handled by storing a special string directly in the CIB that tells\n" + "Pacemaker to look in a separate, protected file for the actual value.\n\n" + + "The secret files are not encrypted, but protected by file system permissions\n" + "such that only root can read or modify them.\n\n" + + "Since the secret files are stored locally, they must be synchronized across all\n" + "cluster nodes. This command handles the synchronization using (in order of\n" + "preference) pssh, pdsh, or ssh, so one of those must be installed. Before\n" + "synchronizing, this command will ping the cluster nodes to determine which are\n" + "alive, using fping if it is installed, otherwise the ping command. Installing\n" + "fping is strongly recommended for better performance.\n\n" + + "Commands and their parameters:\n\n" + "check \n" + "\tVerify that the locally stored value of a sensitive resource parameter\n" + "\tmatches its locally stored MD5 hash.\n\n" + "delete \n" + "\tRemove a sensitive resource parameter value.\n\n" + "get \n" + "\tDisplay the locally stored value of a sensitive resource parameter.\n\n" + "set \n" + "\tSet the value of a sensitive resource parameter.\n\n" + "stash \n" + "\tMake a non-sensitive resource parameter that is already in the CIB\n" + "\tsensitive (move its value to a locally stored and protected file).\n" + "\tThis may not be used with -C.\n\n" + "sync\n" + "\tCopy all locally stored secrets to all other nodes.\n\n" + "unstash \n" + "\tMake a sensitive resource parameter that is already in the CIB\n" + "\tnon-sensitive (move its value from the locally stored file to the CIB).\n" + "\tThis may not be used with -C.\n\n\n" + + "Known limitations:\n\n" + + "This command can only be run from full cluster nodes (not Pacemaker Remote\n" + "nodes).\n\n" + + "Changes are not atomic, so the cluster may use different values while a\n" + "change is in progress. To avoid problems, it is recommended to put the\n" + "cluster in maintenance mode when making changes with this command.\n\n" + + "Changes in secret values do not trigger an agent reload or restart of the\n" + "affected resource, since they do not change the CIB. If a response is\n" + "desired before the next cluster recheck interval, any CIB change (such as\n" + "setting a node attribute) will trigger it.\n\n" + + "If any node is down when changes to secrets are made, or a new node is\n" + "later added to the cluster, it may have different values when it joins the\n" + "cluster, before 'cibsecret sync' is run. To avoid this, it is recommended to\n" + "run the sync command (from another node) before starting Pacemaker on the\n" + "node.\n\n" + + "Examples:\n\n" + + "# cibsecret set ipmi_node1 passwd SecreT_PASS\n\n" + "# cibsecret get ipmi_node1 passwd\n\n" + "# cibsecret check ipmi_node1 passwd\n\n" + "# cibsecret stash ipmi_node2 passwd\n\n" + "# cibsecret sync\n"; + + context = pcmk__build_arg_context(args, "text (default), xml", group, + " [options]"); + g_option_context_set_description(context, desc); + pcmk__add_main_args(context, entries); + return context; +} + +int +main(int argc, char **argv) +{ + crm_exit_t exit_code = CRM_EX_OK; + 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, NULL); + GOptionContext *context = build_arg_context(args, &output_group); + + struct subcommand_entry cmd; + + pcmk__register_formats(output_group, formats); + if (!g_option_context_parse_strv(context, &processed_args, &error)) { + exit_code = CRM_EX_USAGE; + goto done; + } + + pcmk__cli_init_logging("cibsecret", args->verbosity); + + 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 (args->version) { + out->version(out); + goto done; + } + + /* No subcommand was given */ + if ((remainder == NULL) || (g_strv_length(remainder) == 0)) { + 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 command option\n\n%s", help); + g_free(help); + goto done; + } + + /* Traverse the subcommand table looking for a match. */ + for (int i = 0; i < PCMK__NELEM(subcommand_table); i++) { + cmd = subcommand_table[i]; + + if (!pcmk__str_eq(remainder[0], cmd.name, pcmk__str_none)) { + continue; + } + + /* We found a match. Check that enough arguments were given and + * display a usage message if not. The "+ 1" is because the table + * entry lists how many arguments the subcommand takes, which does not + * include the subcommand itself. + */ + if (g_strv_length(remainder) != cmd.args + 1) { + exit_code = CRM_EX_USAGE; + g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Usage: %s", + cmd.usage); + goto done; + } + + /* We've found the subcommand handler and it's used correctly. */ + break; + } + + /* If we didn't find a match, a valid subcommand wasn't given. */ + if (cmd.name == NULL) { + exit_code = CRM_EX_USAGE; + g_set_error(&error, PCMK__EXITC_ERROR, exit_code, + "Invalid subcommand given; valid subcommands: " + "check, delete, get, set, stash, sync, unstash"); + goto done; + } + + /* Set a default umask so files we create are only accessible by the + * cluster user. + */ + umask(S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IWOTH | S_IXOTH); + + /* Call the subcommand handler. If the handler fails, it will have already + * set exit_code to the reason why so there's no need to worry with + * additional error checking here at the moment. + */ + if (cmd.requires_cib && no_cib) { + out->err(out, "No access to Pacemaker, %s not supported", cmd.name); + exit_code = CRM_EX_USAGE; + goto done; + } + + cmd.handler(out, &exit_code); + + done: + g_strfreev(processed_args); + g_strfreev(remainder); + pcmk__free_arg_context(context); + + pcmk__output_and_clear_error(&error, out); + + if (out != NULL) { + out->finish(out, exit_code, true, NULL); + pcmk__output_free(out); + } + pcmk__unregister_formats(); + crm_exit(exit_code); +} diff --git a/tools/fix-manpages b/tools/fix-manpages index f1f6f0d445..109a0f141d 100644 --- a/tools/fix-manpages +++ b/tools/fix-manpages @@ -1,33 +1,33 @@ # Because tools/*.8.inc include a synopsis, the following line removes # a redundant Usage: header from the man page and the couple lines after # it. /.SS "Usage:"/,+3d # The tools/*.8.inc files also include some additional section headers # on a per-tool basis. These section headers will get printed out as # .SH lines, but then the header from the --help-all output will also # get turned into groff. For instance, the following will be in the # man page for NOTES: # # .SH NOTES # .PP # Notes: # .PP # # The following block looks for any of those additional headers. The # 'n' command puts the next line in the pattern space, the two 'N' # commands append the next two lines, and then the 'd' command deletes # them. So basically, this just deletes # # .PP # Notes: # .PP # # This leaves the --help-all output looking good and removes redundant # stuff from the man page. Feel free to add additional headers here. # Not all tools will have all headers. -/.SH NOTES\|.SH INTERACTIVE USE\|.SH OPERATION SPECIFICATION\|.SH OUTPUT CONTROL\|.SH TIME SPECIFICATION/{ n +/.SH NOTES\|.SH INTERACTIVE USE\|.SH OPERATION SPECIFICATION\|.SH OUTPUT CONTROL\|.SH TIME SPECIFICATION\|.SH SUBCOMMANDS\|.SH KNOWN LIMITATIONS/{ n N N d }