diff --git a/lib/common/cib_secrets.c b/lib/common/cib_secrets.c index 455c365d04..ad9010ebf5 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; + gchar *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); + g_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/crmcommon_private.h b/lib/common/crmcommon_private.h index a2c04f9cfc..45a8489100 100644 --- a/lib/common/crmcommon_private.h +++ b/lib/common/crmcommon_private.h @@ -1,479 +1,484 @@ /* * Copyright 2018-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__COMMON_CRMCOMMON_PRIVATE__H #define PCMK__COMMON_CRMCOMMON_PRIVATE__H /* This header is for the sole use of libcrmcommon, so that functions can be * declared with G_GNUC_INTERNAL for efficiency. */ #include // uint8_t, uint32_t #include // bool #include // size_t #include // G_GNUC_INTERNAL, G_GNUC_PRINTF, gchar, etc. #include // xmlNode, xmlAttr #include // xmlChar #include // struct qb_ipc_response_header #include // pcmk_ipc_api_t, crm_ipc_t, etc. #include // crm_time_t #include // LOG_NEVER #include // mainloop_io_t #include // pcmk__output_t #include // crm_exit_t #include // pcmk_rule_input_t #include // enum pcmk__xml_flags #ifdef __cplusplus extern "C" { #endif // Decent chunk size for processing large amounts of data #define PCMK__BUFFER_SIZE 4096 #if defined(PCMK__UNIT_TESTING) #undef G_GNUC_INTERNAL #define G_GNUC_INTERNAL #endif /*! * \internal * \brief Information about an XML node that was deleted * * When change tracking is enabled and we delete an XML node using * \c pcmk__xml_free(), we free it and add its path and position to a list in * its document's private data. This allows us to display changes, generate * patchsets, etc. * * Note that this does not happen when deleting an XML attribute using * \c pcmk__xa_remove(). In that case: * * If \c force is \c true, we remove the attribute without any tracking. * * If \c force is \c false, we mark the attribute as deleted but leave it in * place until we commit changes. */ typedef struct pcmk__deleted_xml_s { gchar *path; //!< XPath expression identifying the deleted node int position; //!< Position of the deleted node among its siblings } pcmk__deleted_xml_t; /*! * \internal * \brief Private data for an XML node */ typedef struct xml_node_private_s { uint32_t check; //!< Magic number for checking integrity uint32_t flags; //!< Group of enum pcmk__xml_flags xmlNode *match; //!< Pointer to matching node (defined by caller) } xml_node_private_t; /*! * \internal * \brief Private data for an XML document */ typedef struct xml_doc_private_s { uint32_t check; //!< Magic number for checking integrity uint32_t flags; //!< Group of enum pcmk__xml_flags char *acl_user; //!< User affected by \c acls (for logging) //! ACLs to check requested changes against (list of \c xml_acl_t) GList *acls; //! XML nodes marked as deleted (list of \c pcmk__deleted_xml_t) GList *deleted_objs; } xml_doc_private_t; // XML private data magic numbers #define PCMK__XML_DOC_PRIVATE_MAGIC 0x81726354UL #define PCMK__XML_NODE_PRIVATE_MAGIC 0x54637281UL // XML entity references #define PCMK__XML_ENTITY_AMP "&" #define PCMK__XML_ENTITY_GT ">" #define PCMK__XML_ENTITY_LT "<" #define PCMK__XML_ENTITY_QUOT """ #define pcmk__set_xml_flags(xml_priv, flags_to_set) do { \ (xml_priv)->flags = pcmk__set_flags_as(__func__, __LINE__, \ LOG_NEVER, "XML", "XML node", (xml_priv)->flags, \ (flags_to_set), #flags_to_set); \ } while (0) #define pcmk__clear_xml_flags(xml_priv, flags_to_clear) do { \ (xml_priv)->flags = pcmk__clear_flags_as(__func__, __LINE__, \ LOG_NEVER, "XML", "XML node", (xml_priv)->flags, \ (flags_to_clear), #flags_to_clear); \ } while (0) G_GNUC_INTERNAL const char *pcmk__xml_element_type_text(xmlElementType type); G_GNUC_INTERNAL bool pcmk__xml_reset_node_flags(xmlNode *xml, void *user_data); G_GNUC_INTERNAL void pcmk__xml_set_parent_flags(xmlNode *xml, uint64_t flags); G_GNUC_INTERNAL void pcmk__xml_new_private_data(xmlNode *xml); G_GNUC_INTERNAL void pcmk__xml_free_private_data(xmlNode *xml); G_GNUC_INTERNAL void pcmk__xml_free_node(xmlNode *xml); G_GNUC_INTERNAL xmlDoc *pcmk__xml_new_doc(void); G_GNUC_INTERNAL int pcmk__xml_position(const xmlNode *xml, enum pcmk__xml_flags ignore_if_set); G_GNUC_INTERNAL bool pcmk__xc_matches(const xmlNode *comment1, const xmlNode *comment2); G_GNUC_INTERNAL void pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update); G_GNUC_INTERNAL void pcmk__free_acls(GList *acls); G_GNUC_INTERNAL void pcmk__unpack_acl(xmlNode *source, xmlNode *target, const char *user); G_GNUC_INTERNAL bool pcmk__is_user_in_group(const char *user, const char *group); G_GNUC_INTERNAL void pcmk__apply_acl(xmlNode *xml); G_GNUC_INTERNAL void pcmk__apply_creation_acl(xmlNode *xml, bool check_top); G_GNUC_INTERNAL int pcmk__xa_remove(xmlAttr *attr, bool force); G_GNUC_INTERNAL void pcmk__mark_xml_attr_dirty(xmlAttr *a); G_GNUC_INTERNAL bool pcmk__xa_filterable(const char *name); G_GNUC_INTERNAL void pcmk__log_xmllib_err(void *ctx, const char *fmt, ...) G_GNUC_PRINTF(2, 3); G_GNUC_INTERNAL void pcmk__mark_xml_node_dirty(xmlNode *xml); G_GNUC_INTERNAL bool pcmk__marked_as_deleted(xmlAttrPtr a, void *user_data); G_GNUC_INTERNAL void pcmk__dump_xml_attr(const xmlAttr *attr, GString *buffer); G_GNUC_INTERNAL int pcmk__xe_set_score(xmlNode *target, const char *name, const char *value); G_GNUC_INTERNAL bool pcmk__xml_is_name_start_char(const char *utf8, int *len); G_GNUC_INTERNAL bool pcmk__xml_is_name_char(const char *utf8, int *len); /* * Date/times */ // For use with pcmk__add_time_from_xml() enum pcmk__time_component { pcmk__time_unknown, pcmk__time_years, pcmk__time_months, pcmk__time_weeks, pcmk__time_days, pcmk__time_hours, pcmk__time_minutes, pcmk__time_seconds, }; G_GNUC_INTERNAL const char *pcmk__time_component_attr(enum pcmk__time_component component); G_GNUC_INTERNAL int pcmk__add_time_from_xml(crm_time_t *t, enum pcmk__time_component component, const xmlNode *xml); G_GNUC_INTERNAL void pcmk__set_time_if_earlier(crm_time_t *target, const crm_time_t *source); +/* + * Digests + */ + +char *pcmk__md5sum(const char *input); /* * IPC */ #define PCMK__IPC_VERSION 1 #define PCMK__CONTROLD_API_MAJOR "1" #define PCMK__CONTROLD_API_MINOR "0" // IPC behavior that varies by daemon typedef struct pcmk__ipc_methods_s { /*! * \internal * \brief Allocate any private data needed by daemon IPC * * \param[in,out] api IPC API connection * * \return Standard Pacemaker return code */ int (*new_data)(pcmk_ipc_api_t *api); /*! * \internal * \brief Free any private data used by daemon IPC * * \param[in,out] api_data Data allocated by new_data() method */ void (*free_data)(void *api_data); /*! * \internal * \brief Perform daemon-specific handling after successful connection * * Some daemons require clients to register before sending any other * commands. The controller requires a CRM_OP_HELLO (with no reply), and * the CIB manager, executor, and fencer require a CRM_OP_REGISTER (with a * reply). Ideally this would be consistent across all daemons, but for now * this allows each to do its own authorization. * * \param[in,out] api IPC API connection * * \return Standard Pacemaker return code */ int (*post_connect)(pcmk_ipc_api_t *api); /*! * \internal * \brief Check whether an IPC request results in a reply * * \param[in,out] api IPC API connection * \param[in] request IPC request XML * * \return true if request would result in an IPC reply, false otherwise */ bool (*reply_expected)(pcmk_ipc_api_t *api, const xmlNode *request); /*! * \internal * \brief Perform daemon-specific handling of an IPC message * * \param[in,out] api IPC API connection * \param[in,out] msg Message read from IPC connection * * \return true if more IPC reply messages should be expected */ bool (*dispatch)(pcmk_ipc_api_t *api, xmlNode *msg); /*! * \internal * \brief Perform daemon-specific handling of an IPC disconnect * * \param[in,out] api IPC API connection */ void (*post_disconnect)(pcmk_ipc_api_t *api); } pcmk__ipc_methods_t; // Implementation of pcmk_ipc_api_t struct pcmk_ipc_api_s { enum pcmk_ipc_server server; // Daemon this IPC API instance is for enum pcmk_ipc_dispatch dispatch_type; // How replies should be dispatched crm_ipc_t *ipc; // IPC connection mainloop_io_t *mainloop_io; // If using mainloop, I/O source for IPC bool free_on_disconnect; // Whether disconnect should free object pcmk_ipc_callback_t cb; // Caller-registered callback (if any) void *user_data; // Caller-registered data (if any) void *api_data; // For daemon-specific use pcmk__ipc_methods_t *cmds; // Behavior that varies by daemon }; typedef struct pcmk__ipc_header_s { struct qb_ipc_response_header qb; uint32_t size; uint32_t flags; uint8_t version; uint16_t part_id; // If this is a multipart message, which part is this? } pcmk__ipc_header_t; G_GNUC_INTERNAL int pcmk__send_ipc_request(pcmk_ipc_api_t *api, const xmlNode *request); G_GNUC_INTERNAL void pcmk__call_ipc_callback(pcmk_ipc_api_t *api, enum pcmk_ipc_event event_type, crm_exit_t status, void *event_data); G_GNUC_INTERNAL bool pcmk__valid_ipc_header(const pcmk__ipc_header_t *header); G_GNUC_INTERNAL pcmk__ipc_methods_t *pcmk__attrd_api_methods(void); G_GNUC_INTERNAL pcmk__ipc_methods_t *pcmk__controld_api_methods(void); G_GNUC_INTERNAL pcmk__ipc_methods_t *pcmk__pacemakerd_api_methods(void); G_GNUC_INTERNAL pcmk__ipc_methods_t *pcmk__schedulerd_api_methods(void); /* * Logging */ //! XML is newly created #define PCMK__XML_PREFIX_CREATED "++" //! XML has been deleted #define PCMK__XML_PREFIX_DELETED "--" //! XML has been modified #define PCMK__XML_PREFIX_MODIFIED "+ " //! XML has been moved #define PCMK__XML_PREFIX_MOVED "+~" /* * Output */ G_GNUC_INTERNAL int pcmk__bare_output_new(pcmk__output_t **out, const char *fmt_name, const char *filename, char **argv); G_GNUC_INTERNAL void pcmk__register_option_messages(pcmk__output_t *out); G_GNUC_INTERNAL void pcmk__register_patchset_messages(pcmk__output_t *out); G_GNUC_INTERNAL bool pcmk__output_text_get_fancy(pcmk__output_t *out); /* * Rules */ // How node attribute values may be compared in rules enum pcmk__comparison { pcmk__comparison_unknown, pcmk__comparison_defined, pcmk__comparison_undefined, pcmk__comparison_eq, pcmk__comparison_ne, pcmk__comparison_lt, pcmk__comparison_lte, pcmk__comparison_gt, pcmk__comparison_gte, }; // How node attribute values may be parsed in rules enum pcmk__type { pcmk__type_unknown, pcmk__type_string, pcmk__type_integer, pcmk__type_number, pcmk__type_version, }; // Where to obtain reference value for a node attribute comparison enum pcmk__reference_source { pcmk__source_unknown, pcmk__source_literal, pcmk__source_instance_attrs, pcmk__source_meta_attrs, }; G_GNUC_INTERNAL enum pcmk__comparison pcmk__parse_comparison(const char *op); G_GNUC_INTERNAL enum pcmk__type pcmk__parse_type(const char *type, enum pcmk__comparison op, const char *value1, const char *value2); G_GNUC_INTERNAL enum pcmk__reference_source pcmk__parse_source(const char *source); G_GNUC_INTERNAL int pcmk__cmp_by_type(const char *value1, const char *value2, enum pcmk__type type); G_GNUC_INTERNAL int pcmk__unpack_duration(const xmlNode *duration, const crm_time_t *start, crm_time_t **end); G_GNUC_INTERNAL int pcmk__evaluate_date_spec(const xmlNode *date_spec, const crm_time_t *now); G_GNUC_INTERNAL int pcmk__evaluate_attr_expression(const xmlNode *expression, const pcmk_rule_input_t *rule_input); G_GNUC_INTERNAL int pcmk__evaluate_rsc_expression(const xmlNode *expr, const pcmk_rule_input_t *rule_input); G_GNUC_INTERNAL int pcmk__evaluate_op_expression(const xmlNode *expr, const pcmk_rule_input_t *rule_input); /* * Utils */ #define PCMK__PW_BUFFER_LEN 500 /* * Schemas */ typedef struct { unsigned char v[2]; } pcmk__schema_version_t; enum pcmk__schema_validator { pcmk__schema_validator_none, pcmk__schema_validator_rng }; typedef struct { int schema_index; char *name; /*! * List of XSLT stylesheets for upgrading from this schema version to the * next one. Sorted by the order in which they should be applied to the CIB. */ GList *transforms; void *cache; enum pcmk__schema_validator validator; pcmk__schema_version_t version; } pcmk__schema_t; G_GNUC_INTERNAL GList *pcmk__find_x_0_schema(void); #ifdef __cplusplus } #endif #endif // PCMK__COMMON_CRMCOMMON_PRIVATE__H diff --git a/lib/common/digest.c b/lib/common/digest.c index 1c23ddd456..ad2a3ef9b6 100644 --- a/lib/common/digest.c +++ b/lib/common/digest.c @@ -1,401 +1,448 @@ /* * 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); + gchar *digest_g = NULL; 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_g = pcmk__md5sum(buffer->str); + digest = pcmk__str_copy(digest_g); g_string_free(buffer, TRUE); + g_free(digest_g); 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); + gchar *digest_g = NULL; + char *digest = NULL; pcmk__xml_string(xml, (filter? pcmk__xml_fmt_filtered : 0), buf, 0); - digest = crm_md5sum(buf->str); - if (digest == NULL) { + digest_g = pcmk__md5sum(buf->str); + if (digest_g == NULL) { goto done; } + digest = pcmk__str_copy(digest_g); + 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); + g_free(digest_g); 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 70% rename from lib/common/tests/digest/crm_md5sum_test.c rename to lib/common/tests/digest/pcmk__md5sum_test.c index ece4ced2c3..c9f6db1bae 100644 --- a/lib/common/tests/digest/crm_md5sum_test.c +++ b/lib/common/tests/digest/pcmk__md5sum_test.c @@ -1,31 +1,33 @@ /* - * 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 +#include "crmcommon_private.h" // pcmk__md5sum() + 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"); + gchar *result = pcmk__md5sum("abcdefghijklmnopqrstuvwxyz"); assert_string_equal(result, "c3fcd3d76192e4007dfb496cca67e13b"); - free(result); + g_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); }