diff --git a/include/crm/common/digest_internal.h b/include/crm/common/digest_internal.h index 45be1cf8b7..b5265deed2 100644 --- a/include/crm/common/digest_internal.h +++ b/include/crm/common/digest_internal.h @@ -1,54 +1,55 @@ /* * Copyright 2015-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__CRM_COMMON_DIGEST_INTERNAL__H #define PCMK__CRM_COMMON_DIGEST_INTERNAL__H /* * Internal-only functions to create digest strings from XML */ #include #include // xmlNode #ifdef __cplusplus extern "C" { #endif // Digest comparison results enum pcmk__digest_result { pcmk__digest_unknown, // No digest available for comparison pcmk__digest_match, // Digests match pcmk__digest_mismatch, // Any parameter changed (potentially reloadable) pcmk__digest_restart, // Parameters that require a restart changed }; // Information needed to compare operation digests typedef struct { enum pcmk__digest_result rc; // Result of digest comparison xmlNode *params_all; // All operation parameters xmlNode *params_secure; // Parameters marked private xmlNode *params_restart; // Parameters marked not reloadable char *digest_all_calc; // Digest of params_all char *digest_secure_calc; // Digest of params_secure char *digest_restart_calc; // Digest of params_restart } pcmk__op_digest_t; +char *pcmk__md5sum(const char *input); char *pcmk__digest_on_disk_cib(const xmlNode *input); char *pcmk__digest_op_params(const xmlNode *input); char *pcmk__digest_xml(const xmlNode *input, bool filter); bool pcmk__verify_digest(const xmlNode *input, const char *expected); #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_DIGEST_INTERNAL__H diff --git a/lib/common/cib_secrets.c b/lib/common/cib_secrets.c index 455c365d04..5e4da81a12 100644 --- a/lib/common/cib_secrets.c +++ b/lib/common/cib_secrets.c @@ -1,195 +1,195 @@ /* * Copyright 2011-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include #include /*! * \internal * \brief Read file contents into a string, with trailing whitespace removed * * \param[in] filename Name of file to read * * \return File contents as a string, or \c NULL on failure or empty file * * \note It would be simpler to call \c pcmk__file_contents() directly without * trimming trailing whitespace. However, this would change hashes for * existing value files that have trailing whitespace. Similarly, if we * trim trailing whitespace, it would make sense to trim leading * whitespace, but this changes existing hashes. */ static char * read_file_trimmed(const char *filename) { char *p = NULL; char *buf = NULL; int rc = pcmk__file_contents(filename, &buf); if (rc != pcmk_rc_ok) { crm_err("Failed to read %s: %s", filename, pcmk_rc_str(rc)); free(buf); return NULL; } if (buf == NULL) { crm_err("File %s is empty", filename); return NULL; } // Strip trailing white space for (p = buf + strlen(buf) - 1; (p >= buf) && isspace(*p); p--); *(p + 1) = '\0'; return buf; } /*! * \internal * \brief Read checksum from a file and compare against calculated checksum * * \param[in] filename File containing stored checksum * \param[in] secret_value String to calculate checksum from * \param[in] rsc_id Resource ID (for logging only) * \param[in] param Parameter name (for logging only) * * \return Standard Pacemaker return code */ static int validate_hash(const char *filename, const char *secret_value, const char *rsc_id, const char *param) { char *stored = NULL; char *calculated = NULL; int rc = pcmk_rc_ok; stored = read_file_trimmed(filename); if (stored == NULL) { crm_err("Could not read md5 sum for resource %s parameter '%s' from " "file '%s'", rsc_id, param, filename); rc = ENOENT; goto done; } - calculated = crm_md5sum(secret_value); + calculated = pcmk__md5sum(secret_value); if (calculated == NULL) { // Should be impossible rc = EINVAL; goto done; } crm_trace("Stored hash: %s, calculated hash: %s", stored, calculated); if (!pcmk__str_eq(stored, calculated, pcmk__str_casei)) { crm_err("Calculated md5 sum for resource %s parameter '%s' does not " "match stored md5 sum", rsc_id, param); rc = pcmk_rc_cib_corrupt; } done: free(stored); free(calculated); return rc; } /*! * \internal * \brief Read secret parameter values from file * * Given a table of resource parameters, if any of their values are the * magic string indicating a CIB secret, replace that string with the * secret read from the file appropriate to the given resource. * * \param[in] rsc_id Resource whose parameters are being checked * \param[in,out] params Resource parameters to check * * \return Standard Pacemaker return code */ int pcmk__substitute_secrets(const char *rsc_id, GHashTable *params) { GHashTableIter iter; char *param = NULL; char *value = NULL; GString *filename = NULL; gsize dir_len = 0; int rc = pcmk_rc_ok; if (params == NULL) { return pcmk_rc_ok; } // Some params are sent with operations, so we cannot cache secret params g_hash_table_iter_init(&iter, params); while (g_hash_table_iter_next(&iter, (gpointer *) ¶m, (gpointer *) &value)) { char *secret_value = NULL; int hash_rc = pcmk_rc_ok; if (!pcmk__str_eq(value, "lrm://", pcmk__str_none)) { // Not a secret parameter continue; } if (filename == NULL) { // First secret parameter. Fill in directory path for use with all. crm_debug("Replacing secret parameters for resource %s", rsc_id); filename = g_string_sized_new(128); pcmk__g_strcat(filename, PCMK__CIB_SECRETS_DIR "/", rsc_id, "/", NULL); dir_len = filename->len; } else { // Reset filename to the resource's secrets directory path g_string_truncate(filename, dir_len); } // Path to file containing secret value for this parameter g_string_append(filename, param); secret_value = read_file_trimmed(filename->str); if (secret_value == NULL) { crm_err("Secret value for resource %s parameter '%s' not found in " PCMK__CIB_SECRETS_DIR, rsc_id, param); rc = ENOENT; continue; } // Path to file containing md5 sum for this parameter g_string_append(filename, ".sign"); hash_rc = validate_hash(filename->str, secret_value, rsc_id, param); if (hash_rc != pcmk_rc_ok) { rc = hash_rc; free(secret_value); continue; } g_hash_table_iter_replace(&iter, (gpointer) secret_value); } if (filename != NULL) { g_string_free(filename, TRUE); } return rc; } diff --git a/lib/common/digest.c b/lib/common/digest.c index 1c23ddd456..3e8916d8f6 100644 --- a/lib/common/digest.c +++ b/lib/common/digest.c @@ -1,401 +1,441 @@ /* * Copyright 2015-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include // GString, etc. #include // gnutls_hash_fast(), gnutls_hash_get_len() #include // gnutls_strerror() #include #include #include "crmcommon_private.h" #define BEST_EFFORT_STATUS 0 /* * Pacemaker uses digests (MD5 hashes) of stringified XML to detect changes in * the CIB as a whole, a particular resource's agent parameters, and the device * parameters last used to unfence a particular node. * * "v2" digests hash pcmk__xml_string() directly, while less efficient "v1" * digests do the same with a prefixed space, suffixed newline, and optional * pre-sorting. * * On-disk CIB digests use v1 without sorting. * * Operation digests use v1 with sorting, and are stored in a resource's * operation history in the CIB status section. They come in three flavors: * - a digest of (nearly) all resource parameters and options, used to detect * any resource configuration change; * - a digest of resource parameters marked as nonreloadable, used to decide * whether a reload or full restart is needed after a configuration change; * - and a digest of resource parameters not marked as private, used in * simulations where private parameters have been removed from the input. * * Unfencing digests are set as node attributes, and are used to require * that nodes be unfenced again after a device's configuration changes. */ /*! * \internal * \brief Dump XML in a format used with v1 digests * * \param[in] xml Root of XML to dump * * \return Newly allocated buffer containing dumped XML */ static GString * dump_xml_for_digest(const xmlNode *xml) { GString *buffer = g_string_sized_new(1024); /* for compatibility with the old result which is used for v1 digests */ g_string_append_c(buffer, ' '); pcmk__xml_string(xml, 0, buffer, 0); g_string_append_c(buffer, '\n'); return buffer; } +/*! + * \internal + * \brief Compute an MD5 checksum for a given input string + * + * \param[in] input Input string (can be \c NULL) + * + * \return Newly allocated string containing MD5 checksum for \p input, or + * \c NULL on error or if \p input is \c NULL + * + * \note The caller is responsible for freeing the return value using \c free(). + */ +char * +pcmk__md5sum(const char *input) +{ + char *checksum = NULL; + gchar *checksum_g = NULL; + + if (input == NULL) { + return NULL; + } + + /* g_compute_checksum_for_string() returns NULL if the input string is + * empty. There are instances where we may want to hash an empty, but + * non-NULL, string, so here we just hardcode the result. + */ + if (pcmk__str_empty(input)) { + return pcmk__str_copy("d41d8cd98f00b204e9800998ecf8427e"); + } + + checksum_g = g_compute_checksum_for_string(G_CHECKSUM_MD5, input, -1); + if (checksum_g == NULL) { + crm_err("Failed to compute MD5 checksum for %s", input); + return NULL; + } + + // Make a copy just so that callers can use free() instead of g_free() + checksum = pcmk__str_copy(checksum_g); + g_free(checksum_g); + return checksum; +} + /*! * \internal * \brief Calculate and return v1 digest of XML tree * * \param[in] input Root of XML to digest * * \return Newly allocated string containing digest * * \note Example return value: "c048eae664dba840e1d2060f00299e9d" */ static char * calculate_xml_digest_v1(const xmlNode *input) { GString *buffer = dump_xml_for_digest(input); char *digest = NULL; // buffer->len > 2 for initial space and trailing newline CRM_CHECK(buffer->len > 2, g_string_free(buffer, TRUE); return NULL); - digest = crm_md5sum((const char *) buffer->str); - crm_log_xml_trace(input, "digest:source"); + digest = pcmk__md5sum(buffer->str); g_string_free(buffer, TRUE); return digest; } /*! * \internal * \brief Calculate and return the digest of a CIB, suitable for storing on disk * * \param[in] input Root of XML to digest * * \return Newly allocated string containing digest */ char * pcmk__digest_on_disk_cib(const xmlNode *input) { /* Always use the v1 format for on-disk digests. * * Switching to v2 affects even full-restart upgrades, so it would be a * compatibility nightmare. * * We only use this once at startup. All other invocations are in a * separate child process. */ return calculate_xml_digest_v1(input); } /*! * \internal * \brief Calculate and return digest of a \c PCMK_XE_PARAMETERS element * * This is intended for parameters of a resource operation (also known as * resource action). A \c PCMK_XE_PARAMETERS element from a different source * (for example, resource agent metadata) may have child elements, which are not * allowed here. * * The digest is invariant to changes in the order of XML attributes. * * \param[in] input XML element to digest (must have no children) * * \return Newly allocated string containing digest */ char * pcmk__digest_op_params(const xmlNode *input) { /* Switching to v2 digests would likely cause restarts during rolling * upgrades. * * @TODO Confirm this. Switch to v2 if safe, or drop this TODO otherwise. */ char *digest = NULL; xmlNode *sorted = NULL; pcmk__assert(input->children == NULL); sorted = pcmk__xe_create(NULL, (const char *) input->name); pcmk__xe_copy_attrs(sorted, input, pcmk__xaf_none); pcmk__xe_sort_attrs(sorted); digest = calculate_xml_digest_v1(sorted); pcmk__xml_free(sorted); return digest; } /*! * \internal * \brief Calculate and return the digest of an XML tree * * \param[in] xml XML tree to digest * \param[in] filter Whether to filter certain XML attributes * * \return Newly allocated string containing digest */ char * pcmk__digest_xml(const xmlNode *xml, bool filter) { /* @TODO Filtering accounts for significant CPU usage. Consider removing if * possible. */ char *digest = NULL; GString *buf = g_string_sized_new(1024); pcmk__xml_string(xml, (filter? pcmk__xml_fmt_filtered : 0), buf, 0); - digest = crm_md5sum(buf->str); + digest = pcmk__md5sum(buf->str); if (digest == NULL) { goto done; } pcmk__if_tracing( { char *trace_file = pcmk__assert_asprintf("digest-%s", digest); crm_trace("Saving %s.%s.%s to %s", pcmk__xe_get(xml, PCMK_XA_ADMIN_EPOCH), pcmk__xe_get(xml, PCMK_XA_EPOCH), pcmk__xe_get(xml, PCMK_XA_NUM_UPDATES), trace_file); pcmk__xml_write_temp_file(xml, "digest input", trace_file); free(trace_file); }, {} ); done: g_string_free(buf, TRUE); return digest; } /*! * \internal * \brief Check whether calculated digest of given XML matches expected digest * * \param[in] input Root of XML tree to digest * \param[in] expected Expected digest in on-disk format * * \return true if digests match, false on mismatch or error */ bool pcmk__verify_digest(const xmlNode *input, const char *expected) { char *calculated = NULL; bool passed; if (input != NULL) { calculated = pcmk__digest_on_disk_cib(input); if (calculated == NULL) { crm_err("Could not calculate digest for comparison"); return false; } } passed = pcmk__str_eq(expected, calculated, pcmk__str_casei); if (passed) { crm_trace("Digest comparison passed: %s", calculated); } else { crm_err("Digest comparison failed: expected %s, calculated %s", expected, calculated); } free(calculated); return passed; } /*! * \internal * \brief Check whether an XML attribute should be excluded from CIB digests * * \param[in] name XML attribute name * * \return true if XML attribute should be excluded from CIB digest calculation */ bool pcmk__xa_filterable(const char *name) { static const char *filter[] = { PCMK_XA_CRM_DEBUG_ORIGIN, PCMK_XA_CIB_LAST_WRITTEN, PCMK_XA_UPDATE_ORIGIN, PCMK_XA_UPDATE_CLIENT, PCMK_XA_UPDATE_USER, }; for (int i = 0; i < PCMK__NELEM(filter); i++) { if (strcmp(name, filter[i]) == 0) { return true; } } return false; } char * crm_md5sum(const char *buffer) { char *digest = NULL; gchar *raw_digest = NULL; /* g_compute_checksum_for_string returns NULL if the input string is empty. * There are instances where we may want to hash an empty, but non-NULL, * string so here we just hardcode the result. */ if (buffer == NULL) { return NULL; } else if (pcmk__str_empty(buffer)) { return pcmk__str_copy("d41d8cd98f00b204e9800998ecf8427e"); } raw_digest = g_compute_checksum_for_string(G_CHECKSUM_MD5, buffer, -1); if (raw_digest == NULL) { crm_err("Failed to calculate hash"); return NULL; } digest = pcmk__str_copy(raw_digest); g_free(raw_digest); crm_trace("Digest %s.", digest); return digest; } // Return true if a is an attribute that should be filtered static bool should_filter_for_digest(xmlAttrPtr a, void *user_data) { if (strncmp((const char *) a->name, CRM_META "_", sizeof(CRM_META " ") - 1) == 0) { return true; } return pcmk__str_any_of((const char *) a->name, PCMK_XA_ID, PCMK_XA_CRM_FEATURE_SET, PCMK__XA_OP_DIGEST, PCMK__META_ON_NODE, PCMK__META_ON_NODE_UUID, "pcmk_external_ip", NULL); } /*! * \internal * \brief Remove XML attributes not needed for operation digest * * \param[in,out] param_set XML with operation parameters */ void pcmk__filter_op_for_digest(xmlNode *param_set) { char *key = NULL; char *timeout = NULL; guint interval_ms = 0; if (param_set == NULL) { return; } /* Timeout is useful for recurring operation digests, so grab it before * removing meta-attributes */ key = crm_meta_name(PCMK_META_INTERVAL); pcmk__xe_get_guint(param_set, key, &interval_ms); free(key); key = NULL; if (interval_ms != 0) { key = crm_meta_name(PCMK_META_TIMEOUT); timeout = pcmk__xe_get_copy(param_set, key); } // Remove all CRM_meta_* attributes and certain other attributes pcmk__xe_remove_matching_attrs(param_set, false, should_filter_for_digest, NULL); // Add timeout back for recurring operation digests if (timeout != NULL) { pcmk__xe_set(param_set, key, timeout); } free(timeout); free(key); } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include #include char * calculate_on_disk_digest(xmlNode *input) { return calculate_xml_digest_v1(input); } char * calculate_operation_digest(xmlNode *input, const char *version) { xmlNode *sorted = sorted_xml(input, NULL, true); char *digest = calculate_xml_digest_v1(sorted); pcmk__xml_free(sorted); return digest; } char * calculate_xml_versioned_digest(xmlNode *input, gboolean sort, gboolean do_filter, const char *version) { if ((version == NULL) || (compare_version("3.0.5", version) > 0)) { xmlNode *sorted = NULL; char *digest = NULL; if (sort) { xmlNode *sorted = sorted_xml(input, NULL, true); input = sorted; } crm_trace("Using v1 digest algorithm for %s", pcmk__s(version, "unknown feature set")); digest = calculate_xml_digest_v1(input); pcmk__xml_free(sorted); return digest; } crm_trace("Using v2 digest algorithm for %s", version); return pcmk__digest_xml(input, do_filter); } // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/common/tests/digest/Makefile.am b/lib/common/tests/digest/Makefile.am index 223a5053d3..1210b22e07 100644 --- a/lib/common/tests/digest/Makefile.am +++ b/lib/common/tests/digest/Makefile.am @@ -1,17 +1,17 @@ # -# Copyright 2024 the Pacemaker project contributors +# Copyright 2024-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/tap.mk include $(top_srcdir)/mk/unittest.mk # Add "_test" to the end of all test program names to simplify .gitignore. -check_PROGRAMS = crm_md5sum_test +check_PROGRAMS = pcmk__md5sum_test TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/digest/crm_md5sum_test.c b/lib/common/tests/digest/pcmk__md5sum_test.c similarity index 79% rename from lib/common/tests/digest/crm_md5sum_test.c rename to lib/common/tests/digest/pcmk__md5sum_test.c index ece4ced2c3..e5a59f615c 100644 --- a/lib/common/tests/digest/crm_md5sum_test.c +++ b/lib/common/tests/digest/pcmk__md5sum_test.c @@ -1,31 +1,31 @@ /* - * Copyright 2024 the Pacemaker project contributors + * 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 static void null_arg_test(void **state) { - assert_null(crm_md5sum(NULL)); + assert_null(pcmk__md5sum(NULL)); } static void basic_usage_test(void **state) { - char *result = crm_md5sum("abcdefghijklmnopqrstuvwxyz"); + char *result = pcmk__md5sum("abcdefghijklmnopqrstuvwxyz"); assert_string_equal(result, "c3fcd3d76192e4007dfb496cca67e13b"); free(result); } PCMK__UNIT_TEST(NULL, NULL, cmocka_unit_test(null_arg_test), cmocka_unit_test(basic_usage_test)) diff --git a/tools/cibsecret.c b/tools/cibsecret.c index 6ab83af5c8..2252845d54 100644 --- a/tools/cibsecret.c +++ b/tools/cibsecret.c @@ -1,1219 +1,1218 @@ /* * 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 // EINVAL, ENODEV, ENOENT, ENOTCONN #include #include #include // setenv, unsetenv #include #include // LOG_DEBUG #include // umask, S_IRGRP, S_IROTH, ... #include // WEXITSTATUS #include // geteuid #include #include // xmlNode, xmlNodeGetContent #include // xmlFree #include // xmlChar #include // cib__signon_query #include #include -#include // crm_md5sum #include // crm_element_value, PCMK_XA_* #include // pcmk__query_node_name #define SUMMARY "cibsecret - manage sensitive information in Pacemaker CIB" #define LRM_MAGIC "lrm://" #define SSH_OPTS "-o StrictHostKeyChecking=no" 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 } }; /*! * \internal * \brief A function for running a command on remote hosts * * \param[in,out] out Output object * \param[in] nodes A list of remote hosts * \param[in] cmdline The command line to run */ typedef int (*rsh_fn_t)(pcmk__output_t *out, gchar **nodes, const char *cmdline); /*! * \internal * \brief A function for copying a file to remote hosts * * \param[in,out] out Output object * \param[in] nodes A list of remote hosts * \param[in] to The destination path on the remote host * \param[in] from The local file (or directory) to copy * * \note \p from can either be a single file or a directory. It cannot be * be multiple files in a space-separated string. If multiple files need * to be copied, either copy the entire directory at once or call this * function multiple times. */ typedef int (*rcp_fn_t)(pcmk__output_t *out, gchar **nodes, const char *to, const char *from); 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, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn, crm_exit_t *exit_code); }; static int run_cmdline(pcmk__output_t *out, const char *cmdline, char **standard_out) { int rc = pcmk_rc_ok; gboolean success = FALSE; GError *error = NULL; gchar *sout = NULL; gchar *serr = NULL; gint status; /* A failure here is a failure starting the program (for example, it doesn't * exist on the $PATH), not that it ran but exited with an error code. */ success = g_spawn_command_line_sync(cmdline, &sout, &serr, &status, &error); if (!success) { out->err(out, "%s", error->message); rc = pcmk_rc_error; goto done; } /* A failure here indicates that the program exited with a non-zero exit * code or due to a fatal signal. */ /* @FIXME @COMPAT g_spawn_check_exit_status is deprecated as of glib 2.70 * and is replaced with g_spawn_check_wait_status. */ success = g_spawn_check_exit_status(status, &error); if (!success) { out->err(out, "%s", error->message); out->subprocess_output(out, WEXITSTATUS(status), sout, serr); rc = pcmk_rc_error; } done: pcmk__str_update(standard_out, sout); g_free(sout); g_free(serr); g_clear_error(&error); return rc; } static int pssh(pcmk__output_t *out, gchar **nodes, const char *cmdline) { int rc = pcmk_rc_ok; char *s = NULL; gchar *hosts = g_strjoinv(" ", nodes); s = pcmk__assert_asprintf("pssh -i -H \"%s\" -x \"" SSH_OPTS "\" -- \"%s\"", hosts, cmdline); rc = run_cmdline(out, s, NULL); free(s); g_free(hosts); return rc; } static int pdsh(pcmk__output_t *out, gchar **nodes, const char *cmdline) { int rc = pcmk_rc_ok; char *s = NULL; gchar *hosts = g_strjoinv(",", nodes); s = pcmk__assert_asprintf("pdsh -w \"%s\" -- \"%s\"", hosts, cmdline); setenv("PDSH_SSH_ARGS_APPEND", SSH_OPTS, 1); rc = run_cmdline(out, s, NULL); unsetenv("PDSH_SSH_ARGS_APPEND"); free(s); g_free(hosts); return rc; } static int ssh(pcmk__output_t *out, gchar **nodes, const char *cmdline) { int rc = pcmk_rc_ok; for (gchar **node = nodes; *node != NULL; node++) { char *s = pcmk__assert_asprintf("ssh " SSH_OPTS " \"%s\" -- \"%s\"", *node, cmdline); rc = run_cmdline(out, s, NULL); free(s); if (rc != pcmk_rc_ok) { return rc; } } return rc; } static int pscp(pcmk__output_t *out, gchar **nodes, const char *to, const char *from) { int rc = pcmk_rc_ok; char *s = NULL; gchar *hosts = g_strjoinv(" ", nodes); s = pcmk__assert_asprintf("pscp.pssh -H \"%s\" -x \"-pr\" " "-x \"" SSH_OPTS "\" -- \"%s\" \"%s\"", hosts, from, to); rc = run_cmdline(out, s, NULL); free(s); g_free(hosts); return rc; } static int pdcp(pcmk__output_t *out, gchar **nodes, const char *to, const char *from) { int rc = pcmk_rc_ok; char *s = NULL; gchar *hosts = g_strjoinv(",", nodes); s = pcmk__assert_asprintf("pdcp -pr -w \"%s\" -- \"%s\" \"%s\"", hosts, from, to); setenv("PDSH_SSH_ARGS_APPEND", SSH_OPTS, 1); rc = run_cmdline(out, s, NULL); unsetenv("PDSH_SSH_ARGS_APPEND"); free(s); g_free(hosts); return rc; } static int scp(pcmk__output_t *out, gchar **nodes, const char *to, const char *from) { int rc = pcmk_rc_ok; for (gchar **node = nodes; *node != NULL; node++) { char *s = pcmk__assert_asprintf("scp -pqr " SSH_OPTS " \"%s\" " "\"%s:%s\"", from, *node, to); rc = run_cmdline(out, s, NULL); free(s); if (rc != pcmk_rc_ok) { return rc; } } return rc; } static gchar ** reachable_hosts(pcmk__output_t *out, GList *all) { GPtrArray *reachable = NULL; gchar *path = NULL; path = g_find_program_in_path("fping"); reachable = g_ptr_array_new(); if ((path == NULL) || (geteuid() != 0)) { for (GList *host = all; host != NULL; host = host->next) { int rc = pcmk_rc_ok; char *cmdline = pcmk__assert_asprintf("ping -c 2 -q %s", (char *) host->data); rc = run_cmdline(out, cmdline, NULL); free(cmdline); if (rc == pcmk_rc_ok) { g_ptr_array_add(reachable, g_strdup(host->data)); } } } else { GString *all_str = g_string_sized_new(64); gchar **parts = NULL; char *standard_out = NULL; char *cmdline = NULL; for (GList *host = all; host != NULL; host = host->next) { pcmk__add_word(&all_str, 64, host->data); } cmdline = pcmk__assert_asprintf("fping -a -q %s", all_str->str); run_cmdline(out, cmdline, &standard_out); parts = g_strsplit(standard_out, "\n", 0); for (gchar **p = parts; *p != NULL; p++) { if (pcmk__str_empty(*p)) { continue; } g_ptr_array_add(reachable, g_strdup(*p)); } free(cmdline); free(standard_out); g_string_free(all_str, TRUE); g_strfreev(parts); } g_free(path); g_ptr_array_add(reachable, NULL); return (char **) g_ptr_array_free(reachable, FALSE); } struct node_data { pcmk__output_t *out; char *local_node; const char *field; GList *all_nodes; }; static void node_iter_helper(xmlNode *result, void *user_data) { struct node_data *data = user_data; const char *uname = pcmk__xe_get(result, PCMK_XA_UNAME); const char *id = pcmk__xe_get(result, data->field); const char *name = pcmk__s(uname, id); /* Filter out the local node */ if (pcmk__str_eq(name, data->local_node, pcmk__str_null_matches)) { return; } data->all_nodes = g_list_append(data->all_nodes, g_strdup(name)); } static gchar ** get_live_peers(pcmk__output_t *out) { int rc = pcmk_rc_ok; xmlNode *xml_node = NULL; gchar **reachable = NULL; struct node_data nd = { .out = out, .all_nodes = NULL }; /* Get the local node name. */ rc = pcmk__query_node_name(out, 0, &(nd.local_node), 0); if (rc != pcmk_rc_ok) { out->err(out, "Could not get local node name"); goto done; } /* Get a list of all node names, filtering out the local node. */ rc = cib__signon_query(out, NULL, &xml_node); if (rc != pcmk_rc_ok) { out->err(out, "Could not get list of cluster nodes"); goto done; } nd.field = PCMK_XA_ID; pcmk__xpath_foreach_result(xml_node->doc, PCMK__XP_MEMBER_NODE_CONFIG, node_iter_helper, &nd); nd.field = PCMK_XA_VALUE; pcmk__xpath_foreach_result(xml_node->doc, PCMK__XP_GUEST_NODE_CONFIG, node_iter_helper, &nd); nd.field = PCMK_XA_ID; pcmk__xpath_foreach_result(xml_node->doc, PCMK__XP_REMOTE_NODE_CONFIG, node_iter_helper, &nd); if (nd.all_nodes == NULL) { goto done; } /* Get a list of all nodes that respond to pings */ reachable = reachable_hosts(out, nd.all_nodes); /* Warn the user about any that didn't respond to pings */ for (const GList *iter = nd.all_nodes; iter != NULL; iter = iter->next) { bool found = false; for (gchar **host = reachable; *host != NULL; host++) { if (pcmk__str_eq(iter->data, *host, pcmk__str_none)) { found = true; break; } } if (!found) { out->info(out, "Node %s is down - you'll need to update it " "with `cibsecret sync` later", (char *) iter->data); } } done: free(nd.local_node); free(xml_node); if (nd.all_nodes != NULL) { g_list_free_full(nd.all_nodes, g_free); } return reachable; } static int sync_one_file(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn, const char *path) { int rc = pcmk_rc_ok; gchar *dirname = NULL; gchar **peers = get_live_peers(out); gchar *peer_str = NULL; char *cmdline = NULL; if (peers == NULL) { return pcmk_rc_ok; } peer_str = g_strjoinv(" ", peers); if (pcmk__str_eq(remainder[0], "delete", pcmk__str_none)) { out->info(out, "Deleting %s from %s ...", path, peer_str); } else { out->info(out, "Syncing %s to %s ...", path, peer_str); } dirname = g_path_get_dirname(path); cmdline = pcmk__assert_asprintf("mkdir -p %s", dirname); rc = rsh_fn(out, peers, cmdline); if (rc != pcmk_rc_ok) { goto done; } if (g_file_test(path, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) { char *sign_path = NULL; rc = rcp_fn(out, peers, dirname, path); if (rc != pcmk_rc_ok) { goto done; } sign_path = pcmk__assert_asprintf("%s.sign", path); rc = rcp_fn(out, peers, dirname, sign_path); free(sign_path); } else { free(cmdline); cmdline = pcmk__assert_asprintf("rm -f %s %s.sign", path, path); rc = rsh_fn(out, peers, cmdline); } done: free(cmdline); g_free(dirname); g_strfreev(peers); g_free(peer_str); return rc; } static int check_cib_rsc(pcmk__output_t *out, const char *rsc) { int rc = pcmk_rc_ok; char *cmdline = NULL; if (no_cib) { return rc; } cmdline = pcmk__assert_asprintf("crm_resource -r %s -W", rsc); rc = run_cmdline(out, cmdline, NULL); free(cmdline); return rc; } static bool is_secret(const char *s) { if (no_cib) { /* Assume that the secret is in the CIB if we can't connect */ return true; } return pcmk__str_eq(s, LRM_MAGIC, pcmk__str_none); } static char * get_cib_param(pcmk__output_t *out, const char *rsc, const char *param) { int rc = pcmk_rc_ok; char *cmdline = NULL; char *standard_out = NULL; char *retval = NULL; char *xpath = NULL; xmlNode *xml = NULL; xmlNode *node = NULL; xmlChar *content = NULL; if (no_cib) { return NULL; } cmdline = pcmk__assert_asprintf("crm_resource -r %s -g %s --output-as=xml", rsc, param); rc = run_cmdline(out, cmdline, &standard_out); if (rc != pcmk_rc_ok) { goto done; } xml = pcmk__xml_parse(standard_out); if (xml == NULL) { goto done; } xpath = pcmk__assert_asprintf("//" PCMK_XE_ITEM "[@" PCMK_XA_NAME "='%s']", param); node = pcmk__xpath_find_one(xml->doc, xpath, LOG_DEBUG); if (node == NULL) { goto done; } content = xmlNodeGetContent(node); if (content != NULL) { retval = pcmk__str_copy((char *) content); xmlFree(content); } done: free(cmdline); free(standard_out); free(xpath); pcmk__xml_free(xml); return retval; } static int remove_cib_param(pcmk__output_t *out, const char *rsc, const char *param) { int rc = pcmk_rc_ok; char *cmdline = NULL; if (no_cib) { return rc; } cmdline = pcmk__assert_asprintf("crm_resource -r %s -d %s", rsc, param); rc = run_cmdline(out, cmdline, NULL); free(cmdline); return rc; } static int set_cib_param(pcmk__output_t *out, const char *rsc, const char *param, const char *value) { int rc = pcmk_rc_ok; char *cmdline = NULL; if (no_cib) { return rc; } cmdline = pcmk__assert_asprintf("crm_resource -r %s -p %s -v %s", rsc, param, value); rc = run_cmdline(out, cmdline, NULL); free(cmdline); return rc; } static char * local_files_get(const char *rsc, const char *param) { char *retval = NULL; char *lf_file = NULL; gchar *contents = NULL; lf_file = pcmk__assert_asprintf(PCMK__CIB_SECRETS_DIR "/%s/%s", rsc, param); if (g_file_get_contents(lf_file, &contents, NULL, NULL)) { contents = g_strchomp(contents); retval = pcmk__str_copy(contents); g_free(contents); } free(lf_file); return retval; } static char * local_files_getsum(const char *rsc, const char *param) { char *retval = NULL; char *lf_file = NULL; gchar *contents = NULL; lf_file = pcmk__assert_asprintf(PCMK__CIB_SECRETS_DIR "/%s/%s.sign", rsc, param); if (g_file_get_contents(lf_file, &contents, NULL, NULL)) { contents = g_strchomp(contents); retval = pcmk__str_copy(contents); g_free(contents); } free(lf_file); return retval; } static int local_files_remove(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn, const char *rsc, const char *param) { int rc = pcmk_rc_ok; char *lf_file = NULL; char *cmdline = NULL; lf_file = pcmk__assert_asprintf(PCMK__CIB_SECRETS_DIR "/%s/%s", rsc, param); cmdline = pcmk__assert_asprintf("rm -f %s %s.sign", lf_file, lf_file); rc = run_cmdline(out, cmdline, NULL); if (rc != pcmk_rc_ok) { goto done; } rc = sync_one_file(out, rsh_fn, rcp_fn, lf_file); done: free(lf_file); free(cmdline); return rc; } static int local_files_set(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn, const char *rsc, const char *param, const char *value) { char *contents = NULL; char *lf_dir = NULL; char *lf_file = NULL; char *sign_file = NULL; char *calc_sum = NULL; int rc = pcmk_rc_ok; lf_dir = pcmk__assert_asprintf(PCMK__CIB_SECRETS_DIR "/%s", rsc); if (g_mkdir_with_parents(lf_dir, 0700) != 0) { rc = errno; goto done; } lf_file = pcmk__assert_asprintf("%s/%s", lf_dir, param); contents = pcmk__assert_asprintf("%s\n", value); if (!g_file_set_contents(lf_file, contents, -1, NULL)) { rc = EIO; goto done; } free(contents); sign_file = pcmk__assert_asprintf("%s/%s.sign", lf_dir, param); - calc_sum = crm_md5sum(value); + calc_sum = pcmk__md5sum(value); contents = pcmk__assert_asprintf("%s\n", calc_sum); if (!g_file_set_contents(sign_file, contents, -1, NULL)) { rc = EIO; goto done; } rc = sync_one_file(out, rsh_fn, rcp_fn, lf_file); done: free(contents); free(calc_sum); free(sign_file); free(lf_dir); free(lf_file); return rc; } static int subcommand_check(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn, crm_exit_t *exit_code) { int rc = pcmk_rc_ok; const char *rsc = remainder[1]; const char *param = remainder[2]; char *value = NULL; char *calc_sum = NULL; char *local_sum = NULL; char *local_value = NULL; if (check_cib_rsc(out, rsc) != pcmk_rc_ok) { *exit_code = CRM_EX_NOSUCH; rc = ENODEV; goto done; } value = get_cib_param(out, rsc, param); if ((value == NULL) || !is_secret(value)) { out->err(out, "Resource %s parameter %s not set as secret, nothing to check", rsc, param); *exit_code = CRM_EX_CONFIG; rc = EINVAL; goto done; } local_sum = local_files_getsum(rsc, param); if (local_sum == NULL) { out->err(out, "No checksum for resource %s parameter %s", rsc, param); *exit_code = CRM_EX_OSFILE; rc = ENOENT; goto done; } local_value = local_files_get(rsc, param); if (local_value != NULL) { - calc_sum = crm_md5sum(local_value); + calc_sum = pcmk__md5sum(local_value); } if ((local_value == NULL) || !pcmk__str_eq(calc_sum, local_sum, pcmk__str_none)) { out->err(out, "Checksum mismatch for resource %s parameter %s", rsc, param); *exit_code = CRM_EX_DIGEST; rc = EINVAL; } done: free(local_sum); free(local_value); free(calc_sum); free(value); return rc; } static int subcommand_delete(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn, crm_exit_t *exit_code) { int rc = pcmk_rc_ok; const char *rsc = remainder[1]; const char *param = remainder[2]; if (check_cib_rsc(out, rsc) != pcmk_rc_ok) { *exit_code = CRM_EX_NOSUCH; rc = ENODEV; goto done; } rc = local_files_remove(out, rsh_fn, rcp_fn, rsc, param); if (rc != pcmk_rc_ok) { goto done; } rc = remove_cib_param(out, rsc, param); done: return rc; } static int subcommand_get(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn, crm_exit_t *exit_code) { int rc = subcommand_check(out, rsh_fn, rcp_fn, exit_code); char *value = NULL; const char *rsc = remainder[1]; const char *param = remainder[2]; if (rc != pcmk_rc_ok) { return rc; } value = local_files_get(rsc, param); pcmk__assert(value != NULL); out->info(out, "%s", value); free(value); 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, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn, crm_exit_t *exit_code) { int rc = pcmk_rc_ok; const char *rsc = remainder[1]; const char *param = remainder[2]; const char *value = remainder[3]; char *current = NULL; if (check_cib_rsc(out, rsc) != pcmk_rc_ok) { *exit_code = CRM_EX_NOSUCH; rc = ENODEV; goto done; } current = get_cib_param(out, rsc, param); if ((current != NULL) && !pcmk__str_any_of(current, LRM_MAGIC, value, NULL)) { out->err(out, "CIB value <%s> different for %s rsc parameter %s; please " "delete it first", current, rsc, param); *exit_code = CRM_EX_CONFIG; rc = EINVAL; goto done; } rc = local_files_set(out, rsh_fn, rcp_fn, rsc, param, value); if (rc != pcmk_rc_ok) { goto done; } rc = set_cib_param(out, rsc, param, LRM_MAGIC); done: free(current); return rc; } static int subcommand_stash(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn, crm_exit_t *exit_code) { int rc = pcmk_rc_ok; const char *rsc = remainder[1]; const char *param = remainder[2]; char *value = NULL; if (check_cib_rsc(out, rsc) != pcmk_rc_ok) { *exit_code = CRM_EX_NOSUCH; rc = ENODEV; goto done; } value = get_cib_param(out, rsc, param); if ((value == NULL) || is_secret(value)) { if (value == NULL) { out->err(out, "Nothing to stash for resource %s parameter %s", rsc, param); *exit_code = CRM_EX_NOSUCH; } else { out->err(out, "Resource %s parameter %s already set as secret", rsc, param); *exit_code = CRM_EX_EXISTS; } rc = EINVAL; goto done; } remainder = g_realloc(remainder, sizeof(gchar *) * 5); remainder[3] = g_strdup(value); remainder[4] = NULL; rc = subcommand_set(out, rsh_fn, rcp_fn, exit_code); done: free(value); return rc; } static int subcommand_sync(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn, crm_exit_t *exit_code) { int rc = pcmk_rc_ok; gchar *dirname = NULL; char *cmdline = NULL; gchar **peers = get_live_peers(out); gchar *peer_str = NULL; if (peers == NULL) { return pcmk_rc_ok; } peer_str = g_strjoinv(" ", peers); out->info(out, "Syncing %s to %s ...", PCMK__CIB_SECRETS_DIR, peer_str); g_free(peer_str); dirname = g_path_get_dirname(PCMK__CIB_SECRETS_DIR); rc = rsh_fn(out, peers, "rm -rf " PCMK__CIB_SECRETS_DIR); if (rc != pcmk_rc_ok) { *exit_code = CRM_EX_ERROR; goto done; } cmdline = pcmk__assert_asprintf("mkdir -p %s", dirname); rc = rsh_fn(out, peers, cmdline); free(cmdline); if (rc != pcmk_rc_ok) { *exit_code = CRM_EX_ERROR; goto done; } rc = rcp_fn(out, peers, dirname, PCMK__CIB_SECRETS_DIR); if (rc != pcmk_rc_ok) { *exit_code = CRM_EX_ERROR; } done: g_strfreev(peers); g_free(dirname); return rc; } static int subcommand_unstash(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn, crm_exit_t *exit_code) { int rc = pcmk_rc_ok; const char *rsc = remainder[1]; const char *param = remainder[2]; char *local_value = NULL; char *cib_value = NULL; local_value = local_files_get(rsc, param); if (local_value == NULL) { out->err(out, "Nothing to unstash for resource %s parameter %s", rsc, param); *exit_code = CRM_EX_NOSUCH; rc = EINVAL; goto done; } if (check_cib_rsc(out, rsc) != pcmk_rc_ok) { *exit_code = CRM_EX_NOSUCH; rc = ENODEV; goto done; } cib_value = get_cib_param(out, rsc, param); if (!is_secret(cib_value)) { out->info(out, "Resource %s parameter %s is not set as secret, but we " "have a local value so proceeding anyway", rsc, param); } rc = local_files_remove(out, rsh_fn, rcp_fn, rsc, param); if (rc != pcmk_rc_ok) { goto done; } rc = set_cib_param(out, rsc, param, local_value); done: free(cib_value); free(local_value); return rc; } 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 bool tools_installed(pcmk__output_t *out, rsh_fn_t *rsh_fn, rcp_fn_t *rcp_fn, GError **error) { gchar *path = NULL; path = g_find_program_in_path("pssh"); if (path != NULL) { g_free(path); *rsh_fn = pssh; *rcp_fn = pscp; return true; } path = g_find_program_in_path("pdsh"); if (path != NULL) { g_free(path); *rsh_fn = pdsh; *rcp_fn = pdcp; return true; } path = g_find_program_in_path("ssh"); if (path != NULL) { g_free(path); *rsh_fn = ssh; *rcp_fn = scp; return true; } out->err(out, "Please install one of pssh, pdsh, or ssh"); return false; } 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; rsh_fn_t rsh_fn; rcp_fn_t rcp_fn; 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; } /* Check that we have the tools necessary to manage secrets */ if (!tools_installed(out, &rsh_fn, &rcp_fn, &error)) { exit_code = CRM_EX_NOT_INSTALLED; 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, rsh_fn, rcp_fn, &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); }