diff --git a/devel/scratch.c b/devel/scratch.c index bd0358f5a0..d503382abd 100644 --- a/devel/scratch.c +++ b/devel/scratch.c @@ -1,53 +1,53 @@ /* - * Copyright 2004-2021 the Pacemaker project contributors + * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include #define OPTARGS "X:" int main(int argc, char **argv) { int flag; xmlNode *top = NULL; const char *xml_file = NULL; crm_log_init(NULL, LOG_TRACE, FALSE, TRUE, argc, argv, FALSE); while (1) { flag = getopt(argc, argv, OPTARGS); if (flag == -1) break; switch (flag) { case 'X': xml_file = optarg; break; default: printf("Unknown option: -%c\n", flag); break; } } - top = filename2xml(xml_file); + top = pcmk__xml_read(xml_file); free_xml(top); return 0; } diff --git a/include/crm/common/xml_internal.h b/include/crm/common/xml_internal.h index 6ef0fc7c5c..97eab7e98a 100644 --- a/include/crm/common/xml_internal.h +++ b/include/crm/common/xml_internal.h @@ -1,477 +1,485 @@ /* * Copyright 2017-2024 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__XML_INTERNAL__H # define PCMK__XML_INTERNAL__H /* * Internal-only wrappers for and extensions to libxml2 (libxslt) */ # include # include # include # include /* transitively imports qblog.h */ # include # include /*! * \brief Base for directing lib{xml2,xslt} log into standard libqb backend * * This macro implements the core of what can be needed for directing * libxml2 or libxslt error messaging into standard, preconfigured * libqb-backed log stream. * * It's a bit unfortunate that libxml2 (and more sparsely, also libxslt) * emits a single message by chunks (location is emitted separatedly from * the message itself), so we have to take the effort to combine these * chunks back to single message. Whether to do this or not is driven * with \p dechunk toggle. * * The form of a macro was chosen for implicit deriving of __FILE__, etc. * and also because static dechunking buffer should be differentiated per * library (here we assume different functions referring to this macro * will not ever be using both at once), preferably also per-library * context of use to avoid clashes altogether. * * Note that we cannot use qb_logt, because callsite data have to be known * at the moment of compilation, which it is not always the case -- xml_log * (and unfortunately there's no clear explanation of the fail to compile). * * Also note that there's no explicit guard against said libraries producing * never-newline-terminated chunks (which would just keep consuming memory), * as it's quite improbable. Termination of the program in between the * same-message chunks will raise a flag with valgrind and the likes, though. * * And lastly, regarding how dechunking combines with other non-message * parameters -- for \p priority, most important running specification * wins (possibly elevated to LOG_ERR in case of nonconformance with the * newline-termination "protocol"), \p dechunk is expected to always be * on once it was at the start, and the rest (\p postemit and \p prefix) * are picked directly from the last chunk entry finalizing the message * (also reasonable to always have it the same with all related entries). * * \param[in] priority Syslog priority for the message to be logged * \param[in] dechunk Whether to dechunk new-line terminated message * \param[in] postemit Code to be executed once message is sent out * \param[in] prefix How to prefix the message or NULL for raw passing * \param[in] fmt Format string as with printf-like functions * \param[in] ap Variable argument list to supplement \p fmt format string */ #define PCMK__XML_LOG_BASE(priority, dechunk, postemit, prefix, fmt, ap) \ do { \ if (!(dechunk) && (prefix) == NULL) { /* quick pass */ \ qb_log_from_external_source_va(__func__, __FILE__, (fmt), \ (priority), __LINE__, 0, (ap)); \ (void) (postemit); \ } else { \ int CXLB_len = 0; \ char *CXLB_buf = NULL; \ static int CXLB_buffer_len = 0; \ static char *CXLB_buffer = NULL; \ static uint8_t CXLB_priority = 0; \ \ CXLB_len = vasprintf(&CXLB_buf, (fmt), (ap)); \ \ if (CXLB_len <= 0 || CXLB_buf[CXLB_len - 1] == '\n' || !(dechunk)) { \ if (CXLB_len < 0) { \ CXLB_buf = (char *) "LOG CORRUPTION HAZARD"; /*we don't modify*/\ CXLB_priority = QB_MIN(CXLB_priority, LOG_ERR); \ } else if (CXLB_len > 0 /* && (dechunk) */ \ && CXLB_buf[CXLB_len - 1] == '\n') { \ CXLB_buf[CXLB_len - 1] = '\0'; \ } \ if (CXLB_buffer) { \ qb_log_from_external_source(__func__, __FILE__, "%s%s%s", \ CXLB_priority, __LINE__, 0, \ (prefix) != NULL ? (prefix) : "", \ CXLB_buffer, CXLB_buf); \ free(CXLB_buffer); \ } else { \ qb_log_from_external_source(__func__, __FILE__, "%s%s", \ (priority), __LINE__, 0, \ (prefix) != NULL ? (prefix) : "", \ CXLB_buf); \ } \ if (CXLB_len < 0) { \ CXLB_buf = NULL; /* restore temporary override */ \ } \ CXLB_buffer = NULL; \ CXLB_buffer_len = 0; \ (void) (postemit); \ \ } else if (CXLB_buffer == NULL) { \ CXLB_buffer_len = CXLB_len; \ CXLB_buffer = CXLB_buf; \ CXLB_buf = NULL; \ CXLB_priority = (priority); /* remember as a running severest */ \ \ } else { \ CXLB_buffer = realloc(CXLB_buffer, 1 + CXLB_buffer_len + CXLB_len); \ memcpy(CXLB_buffer + CXLB_buffer_len, CXLB_buf, CXLB_len); \ CXLB_buffer_len += CXLB_len; \ CXLB_buffer[CXLB_buffer_len] = '\0'; \ CXLB_priority = QB_MIN(CXLB_priority, (priority)); /* severest? */ \ } \ free(CXLB_buf); \ } \ } while (0) /* * \enum pcmk__xml_fmt_options * \brief Bit flags to control format in XML logs and dumps */ enum pcmk__xml_fmt_options { //! Exclude certain XML attributes (for calculating digests) pcmk__xml_fmt_filtered = (1 << 0), //! Include indentation and newlines pcmk__xml_fmt_pretty = (1 << 1), //! Include the opening tag of an XML element, and include XML comments pcmk__xml_fmt_open = (1 << 3), //! Include the children of an XML element pcmk__xml_fmt_children = (1 << 4), //! Include the closing tag of an XML element pcmk__xml_fmt_close = (1 << 5), //! Include XML text nodes pcmk__xml_fmt_text = (1 << 6), // @COMPAT Remove when v1 patchsets are removed //! Log a created XML subtree pcmk__xml_fmt_diff_plus = (1 << 7), // @COMPAT Remove when v1 patchsets are removed //! Log a removed XML subtree pcmk__xml_fmt_diff_minus = (1 << 8), // @COMPAT Remove when v1 patchsets are removed //! Log a minimal version of an XML diff (only showing the changes) pcmk__xml_fmt_diff_short = (1 << 9), }; int pcmk__xml_show(pcmk__output_t *out, const char *prefix, const xmlNode *data, int depth, uint32_t options); int pcmk__xml_show_changes(pcmk__output_t *out, const xmlNode *xml); /* XML search strings for guest, remote and pacemaker_remote nodes */ /* search string to find CIB resources entries for cluster nodes */ #define PCMK__XP_MEMBER_NODE_CONFIG \ "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_NODES \ "/" PCMK_XE_NODE "[not(@type) or @type='member']" /* search string to find CIB resources entries for guest nodes */ #define PCMK__XP_GUEST_NODE_CONFIG \ "//" PCMK_XE_CIB "//" PCMK_XE_CONFIGURATION "//" PCMK_XE_PRIMITIVE \ "//" PCMK_XE_META_ATTRIBUTES "//" PCMK_XE_NVPAIR \ "[@name='" PCMK_META_REMOTE_NODE "']" /* search string to find CIB resources entries for remote nodes */ #define PCMK__XP_REMOTE_NODE_CONFIG \ "//" PCMK_XE_CIB "//" PCMK_XE_CONFIGURATION "//" PCMK_XE_PRIMITIVE \ "[@type='remote'][@provider='pacemaker']" /* search string to find CIB node status entries for pacemaker_remote nodes */ #define PCMK__XP_REMOTE_NODE_STATUS \ "//" PCMK_XE_CIB "//" PCMK_XE_STATUS "//" PCMK__XE_NODE_STATE \ "[@" PCMK_XA_REMOTE_NODE "='true']" /*! * \internal * \brief Serialize XML (using libxml) into provided descriptor * * \param[in] fd File descriptor to (piece-wise) write to * \param[in] cur XML subtree to proceed * * \return a standard Pacemaker return code */ int pcmk__xml2fd(int fd, xmlNode *cur); enum pcmk__xml_artefact_ns { pcmk__xml_artefact_ns_legacy_rng = 1, pcmk__xml_artefact_ns_legacy_xslt, pcmk__xml_artefact_ns_base_rng, pcmk__xml_artefact_ns_base_xslt, }; void pcmk__strip_xml_text(xmlNode *xml); const char *pcmk__xe_add_last_written(xmlNode *xe); xmlNode *pcmk__xe_match(const xmlNode *parent, const char *node_name, const char *attr_n, const char *attr_v); void pcmk__xe_remove_matching_attrs(xmlNode *element, bool (*match)(xmlAttrPtr, void *), void *user_data); GString *pcmk__element_xpath(const xmlNode *xml); bool pcmk__xml_needs_escape(const char *text, bool escape_quote); char *pcmk__xml_escape(const char *text, bool escape_quote); /*! * \internal * \brief Get the root directory to scan XML artefacts of given kind for * * \param[in] ns governs the hierarchy nesting against the inherent root dir * * \return root directory to scan XML artefacts of given kind for */ char * pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns); /*! * \internal * \brief Get the fully unwrapped path to particular XML artifact (RNG/XSLT) * * \param[in] ns denotes path forming details (parent dir, suffix) * \param[in] filespec symbolic file specification to be combined with * #artefact_ns to form the final path * \return unwrapped path to particular XML artifact (RNG/XSLT) */ char *pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec); /*! * \internal * \brief Retrieve the value of the \c PCMK_XA_ID XML attribute * * \param[in] xml XML element to check * * \return Value of the \c PCMK_XA_ID attribute (may be \c NULL) */ static inline const char * pcmk__xe_id(const xmlNode *xml) { return crm_element_value(xml, PCMK_XA_ID); } /*! * \internal * \brief Check whether an XML element is of a particular type * * \param[in] xml XML element to compare * \param[in] name XML element name to compare * * \return \c true if \p xml is of type \p name, otherwise \c false */ static inline bool pcmk__xe_is(const xmlNode *xml, const char *name) { return (xml != NULL) && (xml->name != NULL) && (name != NULL) && (strcmp((const char *) xml->name, name) == 0); } /*! * \internal * \brief Return first non-text child node of an XML node * * \param[in] parent XML node to check * * \return First non-text child node of \p parent (or NULL if none) */ static inline xmlNode * pcmk__xml_first_child(const xmlNode *parent) { xmlNode *child = (parent? parent->children : NULL); while (child && (child->type == XML_TEXT_NODE)) { child = child->next; } return child; } /*! * \internal * \brief Return next non-text sibling node of an XML node * * \param[in] child XML node to check * * \return Next non-text sibling of \p child (or NULL if none) */ static inline xmlNode * pcmk__xml_next(const xmlNode *child) { xmlNode *next = (child? child->next : NULL); while (next && (next->type == XML_TEXT_NODE)) { next = next->next; } return next; } /*! * \internal * \brief Return first non-text child element of an XML node * * \param[in] parent XML node to check * * \return First child element of \p parent (or NULL if none) */ static inline xmlNode * pcmk__xe_first_child(const xmlNode *parent) { xmlNode *child = (parent? parent->children : NULL); while (child && (child->type != XML_ELEMENT_NODE)) { child = child->next; } return child; } /*! * \internal * \brief Return next non-text sibling element of an XML element * * \param[in] child XML element to check * * \return Next sibling element of \p child (or NULL if none) */ static inline xmlNode * pcmk__xe_next(const xmlNode *child) { xmlNode *next = child? child->next : NULL; while (next && (next->type != XML_ELEMENT_NODE)) { next = next->next; } return next; } xmlNode *pcmk__xml_copy(xmlNode *parent, xmlNode *src); void pcmk__xe_set_content(xmlNode *node, const char *content); /*! * \internal * \brief Like pcmk__xe_set_props, but takes a va_list instead of * arguments directly. * * \param[in,out] node XML to add attributes to * \param[in] pairs NULL-terminated list of name/value pairs to add */ void pcmk__xe_set_propv(xmlNodePtr node, va_list pairs); /*! * \internal * \brief Add a NULL-terminated list of name/value pairs to the given * XML node as properties. * * \param[in,out] node XML node to add properties to * \param[in] ... NULL-terminated list of name/value pairs * * \note A NULL name terminates the arguments; a NULL value will be skipped. */ void pcmk__xe_set_props(xmlNodePtr node, ...) G_GNUC_NULL_TERMINATED; /*! * \internal * \brief Get first attribute of an XML element * * \param[in] xe XML element to check * * \return First attribute of \p xe (or NULL if \p xe is NULL or has none) */ static inline xmlAttr * pcmk__xe_first_attr(const xmlNode *xe) { return (xe == NULL)? NULL : xe->properties; } /*! * \internal * \brief Extract the ID attribute from an XML element * * \param[in] xpath String to search * \param[in] node Node to get the ID for * * \return ID attribute of \p node in xpath string \p xpath */ char * pcmk__xpath_node_id(const char *xpath, const char *node); /* internal XML-related utilities */ enum xml_private_flags { pcmk__xf_none = 0x0000, pcmk__xf_dirty = 0x0001, pcmk__xf_deleted = 0x0002, pcmk__xf_created = 0x0004, pcmk__xf_modified = 0x0008, pcmk__xf_tracking = 0x0010, pcmk__xf_processed = 0x0020, pcmk__xf_skip = 0x0040, pcmk__xf_moved = 0x0080, pcmk__xf_acl_enabled = 0x0100, pcmk__xf_acl_read = 0x0200, pcmk__xf_acl_write = 0x0400, pcmk__xf_acl_deny = 0x0800, pcmk__xf_acl_create = 0x1000, pcmk__xf_acl_denied = 0x2000, pcmk__xf_lazy = 0x4000, }; void pcmk__set_xml_doc_flag(xmlNode *xml, enum xml_private_flags flag); /*! * \internal * \brief Iterate over child elements of \p xml * * This function iterates over the children of \p xml, performing the * callback function \p handler on each node. If the callback returns * a value other than pcmk_rc_ok, the iteration stops and the value is * returned. It is therefore possible that not all children will be * visited. * * \param[in,out] xml The starting XML node. Can be NULL. * \param[in] child_element_name The name that the node must match in order * for \p handler to be run. If NULL, all * child elements will match. * \param[in] handler The callback function. * \param[in,out] userdata User data to pass to the callback function. * Can be NULL. * * \return Standard Pacemaker return code */ int pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name, int (*handler)(xmlNode *xml, void *userdata), void *userdata); static inline const char * pcmk__xml_attr_value(const xmlAttr *attr) { return ((attr == NULL) || (attr->children == NULL))? NULL : (const char *) attr->children->content; } gboolean pcmk__validate_xml(xmlNode *xml_blob, const char *validation, xmlRelaxNGValidityErrorFunc error_handler, void *error_handler_context); void pcmk__log_known_schemas(void); const char *pcmk__remote_schema_dir(void); void pcmk__sort_schemas(void); + +/* + * I/O + */ + +xmlNode *pcmk__xml_read(const char *filename); + + // @COMPAT Remove when v1 patchsets are removed xmlNode *pcmk__diff_v1_xml_object(xmlNode *left, xmlNode *right, bool suppress); #endif // PCMK__XML_INTERNAL__H diff --git a/lib/cib/cib_file.c b/lib/cib/cib_file.c index 34a37bf806..b972a27751 100644 --- a/lib/cib/cib_file.c +++ b/lib/cib/cib_file.c @@ -1,1177 +1,1177 @@ /* * Original copyright 2004 International Business Machines * Later changes copyright 2008-2024 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 #include #include #include #include #include #define CIB_SERIES "cib" #define CIB_SERIES_MAX 100 #define CIB_SERIES_BZIP FALSE /* Must be false because archived copies are created with hard links */ #define CIB_LIVE_NAME CIB_SERIES ".xml" // key: client ID (const char *) -> value: client (cib_t *) static GHashTable *client_table = NULL; enum cib_file_flags { cib_file_flag_dirty = (1 << 0), cib_file_flag_live = (1 << 1), }; typedef struct cib_file_opaque_s { char *id; char *filename; uint32_t flags; // Group of enum cib_file_flags xmlNode *cib_xml; } cib_file_opaque_t; static int cib_file_process_commit_transaction(const char *op, int options, const char *section, xmlNode *req, xmlNode *input, xmlNode *existing_cib, xmlNode **result_cib, xmlNode **answer); /*! * \internal * \brief Add a CIB file client to client table * * \param[in] cib CIB client */ static void register_client(const cib_t *cib) { cib_file_opaque_t *private = cib->variant_opaque; if (client_table == NULL) { client_table = pcmk__strkey_table(NULL, NULL); } g_hash_table_insert(client_table, private->id, (gpointer) cib); } /*! * \internal * \brief Remove a CIB file client from client table * * \param[in] cib CIB client */ static void unregister_client(const cib_t *cib) { cib_file_opaque_t *private = cib->variant_opaque; if (client_table == NULL) { return; } g_hash_table_remove(client_table, private->id); /* @COMPAT: Add to crm_exit() when libcib and libcrmcommon are merged, * instead of destroying the client table when there are no more clients. */ if (g_hash_table_size(client_table) == 0) { g_hash_table_destroy(client_table); client_table = NULL; } } /*! * \internal * \brief Look up a CIB file client by its ID * * \param[in] client_id CIB client ID * * \return CIB client with matching ID if found, or \p NULL otherwise */ static cib_t * get_client(const char *client_id) { if (client_table == NULL) { return NULL; } return g_hash_table_lookup(client_table, (gpointer) client_id); } static const cib__op_fn_t cib_op_functions[] = { [cib__op_apply_patch] = cib_process_diff, [cib__op_bump] = cib_process_bump, [cib__op_commit_transact] = cib_file_process_commit_transaction, [cib__op_create] = cib_process_create, [cib__op_delete] = cib_process_delete, [cib__op_erase] = cib_process_erase, [cib__op_modify] = cib_process_modify, [cib__op_query] = cib_process_query, [cib__op_replace] = cib_process_replace, [cib__op_upgrade] = cib_process_upgrade, }; /* cib_file_backup() and cib_file_write_with_digest() need to chown the * written files only in limited circumstances, so these variables allow * that to be indicated without affecting external callers */ static uid_t cib_file_owner = 0; static uid_t cib_file_group = 0; static gboolean cib_do_chown = FALSE; #define cib_set_file_flags(cibfile, flags_to_set) do { \ (cibfile)->flags = pcmk__set_flags_as(__func__, __LINE__, \ LOG_TRACE, "CIB file", \ cibfile->filename, \ (cibfile)->flags, \ (flags_to_set), \ #flags_to_set); \ } while (0) #define cib_clear_file_flags(cibfile, flags_to_clear) do { \ (cibfile)->flags = pcmk__clear_flags_as(__func__, __LINE__, \ LOG_TRACE, "CIB file", \ cibfile->filename, \ (cibfile)->flags, \ (flags_to_clear), \ #flags_to_clear); \ } while (0) /*! * \internal * \brief Get the function that performs a given CIB file operation * * \param[in] operation Operation whose function to look up * * \return Function that performs \p operation for a CIB file client */ static cib__op_fn_t file_get_op_function(const cib__operation_t *operation) { enum cib__op_type type = operation->type; CRM_ASSERT(type >= 0); if (type >= PCMK__NELEM(cib_op_functions)) { return NULL; } return cib_op_functions[type]; } /*! * \internal * \brief Check whether a file is the live CIB * * \param[in] filename Name of file to check * * \return TRUE if file exists and its real path is same as live CIB's */ static gboolean cib_file_is_live(const char *filename) { gboolean same = FALSE; if (filename != NULL) { // Canonicalize file names for true comparison char *real_filename = NULL; if (pcmk__real_path(filename, &real_filename) == pcmk_rc_ok) { char *real_livename = NULL; if (pcmk__real_path(CRM_CONFIG_DIR "/" CIB_LIVE_NAME, &real_livename) == pcmk_rc_ok) { same = !strcmp(real_filename, real_livename); free(real_livename); } free(real_filename); } } return same; } static int cib_file_process_request(cib_t *cib, xmlNode *request, xmlNode **output) { int rc = pcmk_ok; const cib__operation_t *operation = NULL; cib__op_fn_t op_function = NULL; int call_id = 0; int call_options = cib_none; const char *op = crm_element_value(request, PCMK__XA_CIB_OP); const char *section = crm_element_value(request, PCMK__XA_CIB_SECTION); xmlNode *data = get_message_xml(request, PCMK__XA_CIB_CALLDATA); bool changed = false; bool read_only = false; xmlNode *result_cib = NULL; xmlNode *cib_diff = NULL; cib_file_opaque_t *private = cib->variant_opaque; // We error checked these in callers cib__get_operation(op, &operation); op_function = file_get_op_function(operation); crm_element_value_int(request, PCMK__XA_CIB_CALLID, &call_id); crm_element_value_int(request, PCMK__XA_CIB_CALLOPT, &call_options); read_only = !pcmk_is_set(operation->flags, cib__op_attr_modifies); // Mirror the logic in prepare_input() in pacemaker-based if ((section != NULL) && pcmk__xe_is(data, PCMK_XE_CIB)) { data = pcmk_find_cib_element(data, section); } rc = cib_perform_op(cib, op, call_options, op_function, read_only, section, request, data, true, &changed, &private->cib_xml, &result_cib, &cib_diff, output); if (pcmk_is_set(call_options, cib_transaction)) { /* The rest of the logic applies only to the transaction as a whole, not * to individual requests. */ goto done; } if (rc == -pcmk_err_schema_validation) { validate_xml_verbose(result_cib); } else if ((rc == pcmk_ok) && !read_only) { pcmk__log_xml_patchset(LOG_DEBUG, cib_diff); if (result_cib != private->cib_xml) { free_xml(private->cib_xml); private->cib_xml = result_cib; } cib_set_file_flags(private, cib_file_flag_dirty); } // Global operation callback (deprecated) if (cib->op_callback != NULL) { cib->op_callback(NULL, call_id, rc, *output); } done: if ((result_cib != private->cib_xml) && (result_cib != *output)) { free_xml(result_cib); } free_xml(cib_diff); return rc; } static int cib_file_perform_op_delegate(cib_t *cib, const char *op, const char *host, const char *section, xmlNode *data, xmlNode **output_data, int call_options, const char *user_name) { int rc = pcmk_ok; xmlNode *request = NULL; xmlNode *output = NULL; cib_file_opaque_t *private = cib->variant_opaque; const cib__operation_t *operation = NULL; crm_info("Handling %s operation for %s as %s", pcmk__s(op, "invalid"), pcmk__s(section, "entire CIB"), pcmk__s(user_name, "default user")); if (output_data != NULL) { *output_data = NULL; } if (cib->state == cib_disconnected) { return -ENOTCONN; } rc = cib__get_operation(op, &operation); rc = pcmk_rc2legacy(rc); if (rc != pcmk_ok) { // @COMPAT: At compatibility break, use rc directly return -EPROTONOSUPPORT; } if (file_get_op_function(operation) == NULL) { // @COMPAT: At compatibility break, use EOPNOTSUPP crm_err("Operation %s is not supported by CIB file clients", op); return -EPROTONOSUPPORT; } cib__set_call_options(call_options, "file operation", cib_no_mtime); rc = cib__create_op(cib, op, host, section, data, call_options, user_name, NULL, &request); if (rc != pcmk_ok) { return rc; } crm_xml_add(request, PCMK_XE_ACL_TARGET, user_name); crm_xml_add(request, PCMK__XA_CIB_CLIENTID, private->id); if (pcmk_is_set(call_options, cib_transaction)) { rc = cib__extend_transaction(cib, request); goto done; } rc = cib_file_process_request(cib, request, &output); if ((output_data != NULL) && (output != NULL)) { if (output->doc == private->cib_xml->doc) { *output_data = pcmk__xml_copy(NULL, output); } else { *output_data = output; } } done: if ((output != NULL) && (output->doc != private->cib_xml->doc) && ((output_data == NULL) || (output != *output_data))) { free_xml(output); } free_xml(request); return rc; } /*! * \internal * \brief Read CIB from disk and validate it against XML schema * * \param[in] filename Name of file to read CIB from * \param[out] output Where to store the read CIB XML * * \return pcmk_ok on success, * -ENXIO if file does not exist (or stat() otherwise fails), or * -pcmk_err_schema_validation if XML doesn't parse or validate * \note If filename is the live CIB, this will *not* verify its digest, * though that functionality would be trivial to add here. * Also, this will *not* verify that the file is writable, * because some callers might not need to write. */ static int load_file_cib(const char *filename, xmlNode **output) { struct stat buf; xmlNode *root = NULL; /* Ensure file is readable */ if (strcmp(filename, "-") && (stat(filename, &buf) < 0)) { return -ENXIO; } /* Parse XML from file */ - root = filename2xml(filename); + root = pcmk__xml_read(filename); if (root == NULL) { return -pcmk_err_schema_validation; } /* Add a status section if not already present */ if (find_xml_node(root, PCMK_XE_STATUS, FALSE) == NULL) { create_xml_node(root, PCMK_XE_STATUS); } /* Validate XML against its specified schema */ if (validate_xml(root, NULL, TRUE) == FALSE) { const char *schema = crm_element_value(root, PCMK_XA_VALIDATE_WITH); crm_err("CIB does not validate against %s, or that schema is unknown", schema); free_xml(root); return -pcmk_err_schema_validation; } /* Remember the parsed XML for later use */ *output = root; return pcmk_ok; } static int cib_file_signon(cib_t *cib, const char *name, enum cib_conn_type type) { int rc = pcmk_ok; cib_file_opaque_t *private = cib->variant_opaque; if (private->filename == NULL) { rc = -EINVAL; } else { rc = load_file_cib(private->filename, &private->cib_xml); } if (rc == pcmk_ok) { crm_debug("Opened connection to local file '%s' for %s", private->filename, name); cib->state = cib_connected_command; cib->type = cib_command; register_client(cib); } else { crm_info("Connection to local file '%s' for %s (client %s) failed: %s", private->filename, name, private->id, pcmk_strerror(rc)); } return rc; } /*! * \internal * \brief Write out the in-memory CIB to a live CIB file * * \param[in] cib_root Root of XML tree to write * \param[in,out] path Full path to file to write * * \return 0 on success, -1 on failure */ static int cib_file_write_live(xmlNode *cib_root, char *path) { uid_t uid = geteuid(); struct passwd *daemon_pwent; char *sep = strrchr(path, '/'); const char *cib_dirname, *cib_filename; int rc = 0; /* Get the desired uid/gid */ errno = 0; daemon_pwent = getpwnam(CRM_DAEMON_USER); if (daemon_pwent == NULL) { crm_perror(LOG_ERR, "Could not find %s user", CRM_DAEMON_USER); return -1; } /* If we're root, we can change the ownership; * if we're daemon, anything we create will be OK; * otherwise, block access so we don't create wrong owner */ if ((uid != 0) && (uid != daemon_pwent->pw_uid)) { crm_perror(LOG_ERR, "Must be root or %s to modify live CIB", CRM_DAEMON_USER); return 0; } /* fancy footwork to separate dirname from filename * (we know the canonical name maps to the live CIB, * but the given name might be relative, or symlinked) */ if (sep == NULL) { /* no directory component specified */ cib_dirname = "./"; cib_filename = path; } else if (sep == path) { /* given name is in / */ cib_dirname = "/"; cib_filename = path + 1; } else { /* typical case; split given name into parts */ *sep = '\0'; cib_dirname = path; cib_filename = sep + 1; } /* if we're root, we want to update the file ownership */ if (uid == 0) { cib_file_owner = daemon_pwent->pw_uid; cib_file_group = daemon_pwent->pw_gid; cib_do_chown = TRUE; } /* write the file */ if (cib_file_write_with_digest(cib_root, cib_dirname, cib_filename) != pcmk_ok) { rc = -1; } /* turn off file ownership changes, for other callers */ if (uid == 0) { cib_do_chown = FALSE; } /* undo fancy stuff */ if ((sep != NULL) && (*sep == '\0')) { *sep = '/'; } return rc; } /*! * \internal * \brief Sign-off method for CIB file variants * * This will write the file to disk if needed, and free the in-memory CIB. If * the file is the live CIB, it will compute and write a signature as well. * * \param[in,out] cib CIB object to sign off * * \return pcmk_ok on success, pcmk_err_generic on failure * \todo This method should refuse to write the live CIB if the CIB manager is * running. */ static int cib_file_signoff(cib_t *cib) { int rc = pcmk_ok; cib_file_opaque_t *private = cib->variant_opaque; crm_debug("Disconnecting from the CIB manager"); cib->state = cib_disconnected; cib->type = cib_no_connection; unregister_client(cib); cib->cmds->end_transaction(cib, false, cib_none); /* If the in-memory CIB has been changed, write it to disk */ if (pcmk_is_set(private->flags, cib_file_flag_dirty)) { /* If this is the live CIB, write it out with a digest */ if (pcmk_is_set(private->flags, cib_file_flag_live)) { if (cib_file_write_live(private->cib_xml, private->filename) < 0) { rc = pcmk_err_generic; } /* Otherwise, it's a simple write */ } else { gboolean do_bzip = pcmk__ends_with_ext(private->filename, ".bz2"); if (write_xml_file(private->cib_xml, private->filename, do_bzip) <= 0) { rc = pcmk_err_generic; } } if (rc == pcmk_ok) { crm_info("Wrote CIB to %s", private->filename); cib_clear_file_flags(private, cib_file_flag_dirty); } else { crm_err("Could not write CIB to %s", private->filename); } } /* Free the in-memory CIB */ free_xml(private->cib_xml); private->cib_xml = NULL; return rc; } static int cib_file_free(cib_t *cib) { int rc = pcmk_ok; if (cib->state != cib_disconnected) { rc = cib_file_signoff(cib); } if (rc == pcmk_ok) { cib_file_opaque_t *private = cib->variant_opaque; free(private->id); free(private->filename); free(private); free(cib->cmds); free(cib->user); free(cib); } else { fprintf(stderr, "Couldn't sign off: %d\n", rc); } return rc; } static int cib_file_inputfd(cib_t *cib) { return -EPROTONOSUPPORT; } static int cib_file_register_notification(cib_t *cib, const char *callback, int enabled) { return -EPROTONOSUPPORT; } static int cib_file_set_connection_dnotify(cib_t *cib, void (*dnotify) (gpointer user_data)) { return -EPROTONOSUPPORT; } /*! * \internal * \brief Get the given CIB connection's unique client identifier * * \param[in] cib CIB connection * \param[out] async_id If not \p NULL, where to store asynchronous client ID * \param[out] sync_id If not \p NULL, where to store synchronous client ID * * \return Legacy Pacemaker return code * * \note This is the \p cib_file variant implementation of * \p cib_api_operations_t:client_id(). */ static int cib_file_client_id(const cib_t *cib, const char **async_id, const char **sync_id) { cib_file_opaque_t *private = cib->variant_opaque; if (async_id != NULL) { *async_id = private->id; } if (sync_id != NULL) { *sync_id = private->id; } return pcmk_ok; } cib_t * cib_file_new(const char *cib_location) { cib_file_opaque_t *private = NULL; cib_t *cib = cib_new_variant(); if (cib == NULL) { return NULL; } private = calloc(1, sizeof(cib_file_opaque_t)); if (private == NULL) { free(cib); return NULL; } private->id = crm_generate_uuid(); cib->variant = cib_file; cib->variant_opaque = private; if (cib_location == NULL) { cib_location = getenv("CIB_file"); CRM_CHECK(cib_location != NULL, return NULL); // Shouldn't be possible } private->flags = 0; if (cib_file_is_live(cib_location)) { cib_set_file_flags(private, cib_file_flag_live); crm_trace("File %s detected as live CIB", cib_location); } private->filename = strdup(cib_location); /* assign variant specific ops */ cib->delegate_fn = cib_file_perform_op_delegate; cib->cmds->signon = cib_file_signon; cib->cmds->signoff = cib_file_signoff; cib->cmds->free = cib_file_free; cib->cmds->inputfd = cib_file_inputfd; // Deprecated method cib->cmds->register_notification = cib_file_register_notification; cib->cmds->set_connection_dnotify = cib_file_set_connection_dnotify; cib->cmds->client_id = cib_file_client_id; return cib; } /*! * \internal * \brief Compare the calculated digest of an XML tree against a signature file * * \param[in] root Root of XML tree to compare * \param[in] sigfile Name of signature file containing digest to compare * * \return TRUE if digests match or signature file does not exist, else FALSE */ static gboolean cib_file_verify_digest(xmlNode *root, const char *sigfile) { gboolean passed = FALSE; char *expected; int rc = pcmk__file_contents(sigfile, &expected); switch (rc) { case pcmk_rc_ok: if (expected == NULL) { crm_err("On-disk digest at %s is empty", sigfile); return FALSE; } break; case ENOENT: crm_warn("No on-disk digest present at %s", sigfile); return TRUE; default: crm_err("Could not read on-disk digest from %s: %s", sigfile, pcmk_rc_str(rc)); return FALSE; } passed = pcmk__verify_digest(root, expected); free(expected); return passed; } /*! * \internal * \brief Read an XML tree from a file and verify its digest * * \param[in] filename Name of XML file to read * \param[in] sigfile Name of signature file containing digest to compare * \param[out] root If non-NULL, will be set to pointer to parsed XML tree * * \return 0 if file was successfully read, parsed and verified, otherwise: * -errno on stat() failure, * -pcmk_err_cib_corrupt if file size is 0 or XML is not parseable, or * -pcmk_err_cib_modified if digests do not match * \note If root is non-NULL, it is the caller's responsibility to free *root on * successful return. */ int cib_file_read_and_verify(const char *filename, const char *sigfile, xmlNode **root) { int s_res; struct stat buf; char *local_sigfile = NULL; xmlNode *local_root = NULL; CRM_ASSERT(filename != NULL); if (root) { *root = NULL; } /* Verify that file exists and its size is nonzero */ s_res = stat(filename, &buf); if (s_res < 0) { crm_perror(LOG_WARNING, "Could not verify cluster configuration file %s", filename); return -errno; } else if (buf.st_size == 0) { crm_warn("Cluster configuration file %s is corrupt (size is zero)", filename); return -pcmk_err_cib_corrupt; } /* Parse XML */ - local_root = filename2xml(filename); + local_root = pcmk__xml_read(filename); if (local_root == NULL) { crm_warn("Cluster configuration file %s is corrupt (unparseable as XML)", filename); return -pcmk_err_cib_corrupt; } /* If sigfile is not specified, use original file name plus .sig */ if (sigfile == NULL) { sigfile = local_sigfile = crm_strdup_printf("%s.sig", filename); } /* Verify that digests match */ if (cib_file_verify_digest(local_root, sigfile) == FALSE) { free(local_sigfile); free_xml(local_root); return -pcmk_err_cib_modified; } free(local_sigfile); if (root) { *root = local_root; } else { free_xml(local_root); } return pcmk_ok; } /*! * \internal * \brief Back up a CIB * * \param[in] cib_dirname Directory containing CIB file and backups * \param[in] cib_filename Name (relative to cib_dirname) of CIB file to back up * * \return 0 on success, -1 on error */ static int cib_file_backup(const char *cib_dirname, const char *cib_filename) { int rc = 0; unsigned int seq; char *cib_path = crm_strdup_printf("%s/%s", cib_dirname, cib_filename); char *cib_digest = crm_strdup_printf("%s.sig", cib_path); char *backup_path; char *backup_digest; // Determine backup and digest file names if (pcmk__read_series_sequence(cib_dirname, CIB_SERIES, &seq) != pcmk_rc_ok) { // @TODO maybe handle errors better ... seq = 0; } backup_path = pcmk__series_filename(cib_dirname, CIB_SERIES, seq, CIB_SERIES_BZIP); backup_digest = crm_strdup_printf("%s.sig", backup_path); /* Remove the old backups if they exist */ unlink(backup_path); unlink(backup_digest); /* Back up the CIB, by hard-linking it to the backup name */ if ((link(cib_path, backup_path) < 0) && (errno != ENOENT)) { crm_perror(LOG_ERR, "Could not archive %s by linking to %s", cib_path, backup_path); rc = -1; /* Back up the CIB signature similarly */ } else if ((link(cib_digest, backup_digest) < 0) && (errno != ENOENT)) { crm_perror(LOG_ERR, "Could not archive %s by linking to %s", cib_digest, backup_digest); rc = -1; /* Update the last counter and ensure everything is sync'd to media */ } else { pcmk__write_series_sequence(cib_dirname, CIB_SERIES, ++seq, CIB_SERIES_MAX); if (cib_do_chown) { int rc2; if ((chown(backup_path, cib_file_owner, cib_file_group) < 0) && (errno != ENOENT)) { crm_perror(LOG_ERR, "Could not set owner of %s", backup_path); rc = -1; } if ((chown(backup_digest, cib_file_owner, cib_file_group) < 0) && (errno != ENOENT)) { crm_perror(LOG_ERR, "Could not set owner of %s", backup_digest); rc = -1; } rc2 = pcmk__chown_series_sequence(cib_dirname, CIB_SERIES, cib_file_owner, cib_file_group); if (rc2 != pcmk_rc_ok) { crm_err("Could not set owner of sequence file in %s: %s", cib_dirname, pcmk_rc_str(rc2)); rc = -1; } } pcmk__sync_directory(cib_dirname); crm_info("Archived previous version as %s", backup_path); } free(cib_path); free(cib_digest); free(backup_path); free(backup_digest); return rc; } /*! * \internal * \brief Prepare CIB XML to be written to disk * * Set \c PCMK_XA_NUM_UPDATES to 0, set \c PCMK_XA_CIB_LAST_WRITTEN to the * current timestamp, and strip out the status section. * * \param[in,out] root Root of CIB XML tree * * \return void */ static void cib_file_prepare_xml(xmlNode *root) { xmlNode *cib_status_root = NULL; /* Always write out with num_updates=0 and current last-written timestamp */ crm_xml_add(root, PCMK_XA_NUM_UPDATES, "0"); pcmk__xe_add_last_written(root); /* Delete status section before writing to file, because * we discard it on startup anyway, and users get confused by it */ cib_status_root = find_xml_node(root, PCMK_XE_STATUS, TRUE); CRM_LOG_ASSERT(cib_status_root != NULL); if (cib_status_root != NULL) { free_xml(cib_status_root); } } /*! * \internal * \brief Write CIB to disk, along with a signature file containing its digest * * \param[in,out] cib_root Root of XML tree to write * \param[in] cib_dirname Directory containing CIB and signature files * \param[in] cib_filename Name (relative to cib_dirname) of file to write * * \return pcmk_ok on success, * pcmk_err_cib_modified if existing cib_filename doesn't match digest, * pcmk_err_cib_backup if existing cib_filename couldn't be backed up, * or pcmk_err_cib_save if new cib_filename couldn't be saved */ int cib_file_write_with_digest(xmlNode *cib_root, const char *cib_dirname, const char *cib_filename) { int exit_rc = pcmk_ok; int rc, fd; char *digest = NULL; /* Detect CIB version for diagnostic purposes */ const char *epoch = crm_element_value(cib_root, PCMK_XA_EPOCH); const char *admin_epoch = crm_element_value(cib_root, PCMK_XA_ADMIN_EPOCH); /* Determine full CIB and signature pathnames */ char *cib_path = crm_strdup_printf("%s/%s", cib_dirname, cib_filename); char *digest_path = crm_strdup_printf("%s.sig", cib_path); /* Create temporary file name patterns for writing out CIB and signature */ char *tmp_cib = crm_strdup_printf("%s/cib.XXXXXX", cib_dirname); char *tmp_digest = crm_strdup_printf("%s/cib.XXXXXX", cib_dirname); CRM_ASSERT((cib_path != NULL) && (digest_path != NULL) && (tmp_cib != NULL) && (tmp_digest != NULL)); /* Ensure the admin didn't modify the existing CIB underneath us */ crm_trace("Reading cluster configuration file %s", cib_path); rc = cib_file_read_and_verify(cib_path, NULL, NULL); if ((rc != pcmk_ok) && (rc != -ENOENT)) { crm_err("%s was manually modified while the cluster was active!", cib_path); exit_rc = pcmk_err_cib_modified; goto cleanup; } /* Back up the existing CIB */ if (cib_file_backup(cib_dirname, cib_filename) < 0) { exit_rc = pcmk_err_cib_backup; goto cleanup; } crm_debug("Writing CIB to disk"); umask(S_IWGRP | S_IWOTH | S_IROTH); cib_file_prepare_xml(cib_root); /* Write the CIB to a temporary file, so we can deploy (near) atomically */ fd = mkstemp(tmp_cib); if (fd < 0) { crm_perror(LOG_ERR, "Couldn't open temporary file %s for writing CIB", tmp_cib); exit_rc = pcmk_err_cib_save; goto cleanup; } /* Protect the temporary file */ if (fchmod(fd, S_IRUSR | S_IWUSR) < 0) { crm_perror(LOG_ERR, "Couldn't protect temporary file %s for writing CIB", tmp_cib); exit_rc = pcmk_err_cib_save; goto cleanup; } if (cib_do_chown && (fchown(fd, cib_file_owner, cib_file_group) < 0)) { crm_perror(LOG_ERR, "Couldn't protect temporary file %s for writing CIB", tmp_cib); exit_rc = pcmk_err_cib_save; goto cleanup; } /* Write out the CIB */ if (write_xml_fd(cib_root, tmp_cib, fd, FALSE) <= 0) { crm_err("Changes couldn't be written to %s", tmp_cib); exit_rc = pcmk_err_cib_save; goto cleanup; } /* Calculate CIB digest */ digest = calculate_on_disk_digest(cib_root); CRM_ASSERT(digest != NULL); crm_info("Wrote version %s.%s.0 of the CIB to disk (digest: %s)", (admin_epoch ? admin_epoch : "0"), (epoch ? epoch : "0"), digest); /* Write the CIB digest to a temporary file */ fd = mkstemp(tmp_digest); if (fd < 0) { crm_perror(LOG_ERR, "Could not create temporary file for CIB digest"); exit_rc = pcmk_err_cib_save; goto cleanup; } if (cib_do_chown && (fchown(fd, cib_file_owner, cib_file_group) < 0)) { crm_perror(LOG_ERR, "Couldn't protect temporary file %s for writing CIB", tmp_cib); exit_rc = pcmk_err_cib_save; close(fd); goto cleanup; } rc = pcmk__write_sync(fd, digest); if (rc != pcmk_rc_ok) { crm_err("Could not write digest to %s: %s", tmp_digest, pcmk_rc_str(rc)); exit_rc = pcmk_err_cib_save; close(fd); goto cleanup; } close(fd); crm_debug("Wrote digest %s to disk", digest); /* Verify that what we wrote is sane */ crm_info("Reading cluster configuration file %s (digest: %s)", tmp_cib, tmp_digest); rc = cib_file_read_and_verify(tmp_cib, tmp_digest, NULL); CRM_ASSERT(rc == 0); /* Rename temporary files to live, and sync directory changes to media */ crm_debug("Activating %s", tmp_cib); if (rename(tmp_cib, cib_path) < 0) { crm_perror(LOG_ERR, "Couldn't rename %s as %s", tmp_cib, cib_path); exit_rc = pcmk_err_cib_save; } if (rename(tmp_digest, digest_path) < 0) { crm_perror(LOG_ERR, "Couldn't rename %s as %s", tmp_digest, digest_path); exit_rc = pcmk_err_cib_save; } pcmk__sync_directory(cib_dirname); cleanup: free(cib_path); free(digest_path); free(digest); free(tmp_digest); free(tmp_cib); return exit_rc; } /*! * \internal * \brief Process requests in a CIB transaction * * Stop when a request fails or when all requests have been processed. * * \param[in,out] cib CIB client * \param[in,out] transaction CIB transaction * * \return Standard Pacemaker return code */ static int cib_file_process_transaction_requests(cib_t *cib, xmlNode *transaction) { cib_file_opaque_t *private = cib->variant_opaque; for (xmlNode *request = first_named_child(transaction, PCMK__XE_CIB_COMMAND); request != NULL; request = crm_next_same_xml(request)) { xmlNode *output = NULL; const char *op = crm_element_value(request, PCMK__XA_CIB_OP); int rc = cib_file_process_request(cib, request, &output); rc = pcmk_legacy2rc(rc); if (rc != pcmk_rc_ok) { crm_err("Aborting transaction for CIB file client (%s) on file " "'%s' due to failed %s request: %s", private->id, private->filename, op, pcmk_rc_str(rc)); crm_log_xml_info(request, "Failed request"); return rc; } crm_trace("Applied %s request to transaction working CIB for CIB file " "client (%s) on file '%s'", op, private->id, private->filename); crm_log_xml_trace(request, "Successful request"); } return pcmk_rc_ok; } /*! * \internal * \brief Commit a given CIB file client's transaction to a working CIB copy * * \param[in,out] cib CIB file client * \param[in] transaction CIB transaction * \param[in,out] result_cib Where to store result CIB * * \return Standard Pacemaker return code * * \note The caller is responsible for replacing the \p cib argument's * \p private->cib_xml with \p result_cib on success, and for freeing * \p result_cib using \p free_xml() on failure. */ static int cib_file_commit_transaction(cib_t *cib, xmlNode *transaction, xmlNode **result_cib) { int rc = pcmk_rc_ok; cib_file_opaque_t *private = cib->variant_opaque; xmlNode *saved_cib = private->cib_xml; CRM_CHECK(pcmk__xe_is(transaction, PCMK__XE_CIB_TRANSACTION), return pcmk_rc_no_transaction); /* *result_cib should be a copy of private->cib_xml (created by * cib_perform_op()). If not, make a copy now. Change tracking isn't * strictly required here because: * * Each request in the transaction will have changes tracked and ACLs * checked if appropriate. * * cib_perform_op() will infer changes for the commit request at the end. */ CRM_CHECK((*result_cib != NULL) && (*result_cib != private->cib_xml), *result_cib = pcmk__xml_copy(NULL, private->cib_xml)); crm_trace("Committing transaction for CIB file client (%s) on file '%s' to " "working CIB", private->id, private->filename); // Apply all changes to a working copy of the CIB private->cib_xml = *result_cib; rc = cib_file_process_transaction_requests(cib, transaction); crm_trace("Transaction commit %s for CIB file client (%s) on file '%s'", ((rc == pcmk_rc_ok)? "succeeded" : "failed"), private->id, private->filename); /* Some request types (for example, erase) may have freed private->cib_xml * (the working copy) and pointed it at a new XML object. In that case, it * follows that *result_cib (the working copy) was freed. * * Point *result_cib at the updated working copy stored in private->cib_xml. */ *result_cib = private->cib_xml; // Point private->cib_xml back to the unchanged original copy private->cib_xml = saved_cib; return rc; } static int cib_file_process_commit_transaction(const char *op, int options, const char *section, xmlNode *req, xmlNode *input, xmlNode *existing_cib, xmlNode **result_cib, xmlNode **answer) { int rc = pcmk_rc_ok; const char *client_id = crm_element_value(req, PCMK__XA_CIB_CLIENTID); cib_t *cib = NULL; CRM_CHECK(client_id != NULL, return -EINVAL); cib = get_client(client_id); CRM_CHECK(cib != NULL, return -EINVAL); rc = cib_file_commit_transaction(cib, input, result_cib); if (rc != pcmk_rc_ok) { cib_file_opaque_t *private = cib->variant_opaque; crm_err("Could not commit transaction for CIB file client (%s) on " "file '%s': %s", private->id, private->filename, pcmk_rc_str(rc)); } return pcmk_rc2legacy(rc); } diff --git a/lib/common/xml.c b/lib/common/xml.c index 77ebac31af..6fe038fcac 100644 --- a/lib/common/xml.c +++ b/lib/common/xml.c @@ -1,3004 +1,3029 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU 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 #include /* xmlAllocOutputBuffer */ #include #include #include // PCMK__XML_LOG_BASE, etc. #include "crmcommon_private.h" // Define this as 1 in development to get insanely verbose trace messages #ifndef XML_PARSER_DEBUG #define XML_PARSER_DEBUG 0 #endif /* @TODO XML_PARSE_RECOVER allows some XML errors to be silently worked around * by libxml2, which is potentially ambiguous and dangerous. We should drop it * when we can break backward compatibility with configurations that might be * relying on it (i.e. pacemaker 3.0.0). * * It might be a good idea to have a transitional period where we first try * parsing without XML_PARSE_RECOVER, and if that fails, try parsing again with * it, logging a warning if it succeeds. */ #define PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER (XML_PARSE_NOBLANKS) #define PCMK__XML_PARSE_OPTS_WITH_RECOVER (XML_PARSE_NOBLANKS | XML_PARSE_RECOVER) bool pcmk__tracking_xml_changes(xmlNode *xml, bool lazy) { if(xml == NULL || xml->doc == NULL || xml->doc->_private == NULL) { return FALSE; } else if (!pcmk_is_set(((xml_doc_private_t *)xml->doc->_private)->flags, pcmk__xf_tracking)) { return FALSE; } else if (lazy && !pcmk_is_set(((xml_doc_private_t *)xml->doc->_private)->flags, pcmk__xf_lazy)) { return FALSE; } return TRUE; } static inline void set_parent_flag(xmlNode *xml, long flag) { for(; xml; xml = xml->parent) { xml_node_private_t *nodepriv = xml->_private; if (nodepriv == NULL) { /* During calls to xmlDocCopyNode(), _private will be unset for parent nodes */ } else { pcmk__set_xml_flags(nodepriv, flag); } } } void pcmk__set_xml_doc_flag(xmlNode *xml, enum xml_private_flags flag) { if(xml && xml->doc && xml->doc->_private){ /* During calls to xmlDocCopyNode(), xml->doc may be unset */ xml_doc_private_t *docpriv = xml->doc->_private; pcmk__set_xml_flags(docpriv, flag); } } // Mark document, element, and all element's parents as changed void pcmk__mark_xml_node_dirty(xmlNode *xml) { pcmk__set_xml_doc_flag(xml, pcmk__xf_dirty); set_parent_flag(xml, pcmk__xf_dirty); } // Clear flags on XML node and its children static void reset_xml_node_flags(xmlNode *xml) { xmlNode *cIter = NULL; xml_node_private_t *nodepriv = xml->_private; if (nodepriv) { nodepriv->flags = 0; } for (cIter = pcmk__xml_first_child(xml); cIter != NULL; cIter = pcmk__xml_next(cIter)) { reset_xml_node_flags(cIter); } } // Set xpf_created flag on XML node and any children void pcmk__mark_xml_created(xmlNode *xml) { xmlNode *cIter = NULL; xml_node_private_t *nodepriv = NULL; CRM_ASSERT(xml != NULL); nodepriv = xml->_private; if (nodepriv && pcmk__tracking_xml_changes(xml, FALSE)) { if (!pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { pcmk__set_xml_flags(nodepriv, pcmk__xf_created); pcmk__mark_xml_node_dirty(xml); } for (cIter = pcmk__xml_first_child(xml); cIter != NULL; cIter = pcmk__xml_next(cIter)) { pcmk__mark_xml_created(cIter); } } } #define XML_DOC_PRIVATE_MAGIC 0x81726354UL #define XML_NODE_PRIVATE_MAGIC 0x54637281UL // Free an XML object previously marked as deleted static void free_deleted_object(void *data) { if(data) { pcmk__deleted_xml_t *deleted_obj = data; free(deleted_obj->path); free(deleted_obj); } } // Free and NULL user, ACLs, and deleted objects in an XML node's private data static void reset_xml_private_data(xml_doc_private_t *docpriv) { if (docpriv != NULL) { CRM_ASSERT(docpriv->check == XML_DOC_PRIVATE_MAGIC); free(docpriv->user); docpriv->user = NULL; if (docpriv->acls != NULL) { pcmk__free_acls(docpriv->acls); docpriv->acls = NULL; } if(docpriv->deleted_objs) { g_list_free_full(docpriv->deleted_objs, free_deleted_object); docpriv->deleted_objs = NULL; } } } // Free all private data associated with an XML node static void free_private_data(xmlNode *node) { /* Note: This function frees private data assosciated with an XML node, unless the function is being called as a result of internal XSLT cleanup. That could happen through, for example, the following chain of function calls: xsltApplyStylesheetInternal -> xsltFreeTransformContext -> xsltFreeRVTs -> xmlFreeDoc And in that case, the node would fulfill three conditions: 1. It would be a standalone document (i.e. it wouldn't be part of a document) 2. It would have a space-prefixed name (for reference, please see xsltInternals.h: XSLT_MARK_RES_TREE_FRAG) 3. It would carry its own payload in the _private field. We do not free data in this circumstance to avoid a failed assertion on the XML_*_PRIVATE_MAGIC later. */ if (node->name == NULL || node->name[0] != ' ') { if (node->_private) { if (node->type == XML_DOCUMENT_NODE) { reset_xml_private_data(node->_private); } else { CRM_ASSERT(((xml_node_private_t *) node->_private)->check == XML_NODE_PRIVATE_MAGIC); /* nothing dynamically allocated nested */ } free(node->_private); node->_private = NULL; } } } // Allocate and initialize private data for an XML node static void new_private_data(xmlNode *node) { switch (node->type) { case XML_DOCUMENT_NODE: { xml_doc_private_t *docpriv = NULL; docpriv = calloc(1, sizeof(xml_doc_private_t)); CRM_ASSERT(docpriv != NULL); docpriv->check = XML_DOC_PRIVATE_MAGIC; /* Flags will be reset if necessary when tracking is enabled */ pcmk__set_xml_flags(docpriv, pcmk__xf_dirty|pcmk__xf_created); node->_private = docpriv; break; } case XML_ELEMENT_NODE: case XML_ATTRIBUTE_NODE: case XML_COMMENT_NODE: { xml_node_private_t *nodepriv = NULL; nodepriv = calloc(1, sizeof(xml_node_private_t)); CRM_ASSERT(nodepriv != NULL); nodepriv->check = XML_NODE_PRIVATE_MAGIC; /* Flags will be reset if necessary when tracking is enabled */ pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created); node->_private = nodepriv; if (pcmk__tracking_xml_changes(node, FALSE)) { /* XML_ELEMENT_NODE doesn't get picked up here, node->doc is * not hooked up at the point we are called */ pcmk__mark_xml_node_dirty(node); } break; } case XML_TEXT_NODE: case XML_DTD_NODE: case XML_CDATA_SECTION_NODE: break; default: /* Ignore */ crm_trace("Ignoring %p %d", node, node->type); CRM_LOG_ASSERT(node->type == XML_ELEMENT_NODE); break; } } void xml_track_changes(xmlNode * xml, const char *user, xmlNode *acl_source, bool enforce_acls) { xml_accept_changes(xml); crm_trace("Tracking changes%s to %p", enforce_acls?" with ACLs":"", xml); pcmk__set_xml_doc_flag(xml, pcmk__xf_tracking); if(enforce_acls) { if(acl_source == NULL) { acl_source = xml; } pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_enabled); pcmk__unpack_acl(acl_source, xml, user); pcmk__apply_acl(xml); } } bool xml_tracking_changes(xmlNode * xml) { return (xml != NULL) && (xml->doc != NULL) && (xml->doc->_private != NULL) && pcmk_is_set(((xml_doc_private_t *)(xml->doc->_private))->flags, pcmk__xf_tracking); } bool xml_document_dirty(xmlNode *xml) { return (xml != NULL) && (xml->doc != NULL) && (xml->doc->_private != NULL) && pcmk_is_set(((xml_doc_private_t *)(xml->doc->_private))->flags, pcmk__xf_dirty); } /*! * \internal * \brief Return ordinal position of an XML node among its siblings * * \param[in] xml XML node to check * \param[in] ignore_if_set Don't count siblings with this flag set * * \return Ordinal position of \p xml (starting with 0) */ int pcmk__xml_position(const xmlNode *xml, enum xml_private_flags ignore_if_set) { int position = 0; for (const xmlNode *cIter = xml; cIter->prev; cIter = cIter->prev) { xml_node_private_t *nodepriv = ((xmlNode*)cIter->prev)->_private; if (!pcmk_is_set(nodepriv->flags, ignore_if_set)) { position++; } } return position; } // Remove all attributes marked as deleted from an XML node static void accept_attr_deletions(xmlNode *xml) { // Clear XML node's flags ((xml_node_private_t *) xml->_private)->flags = pcmk__xf_none; // Remove this XML node's attributes that were marked as deleted pcmk__xe_remove_matching_attrs(xml, pcmk__marked_as_deleted, NULL); // Recursively do the same for this XML node's children for (xmlNodePtr cIter = pcmk__xml_first_child(xml); cIter != NULL; cIter = pcmk__xml_next(cIter)) { accept_attr_deletions(cIter); } } /*! * \internal * \brief Find first child XML node matching another given XML node * * \param[in] haystack XML whose children should be checked * \param[in] needle XML to match (comment content or element name and ID) * \param[in] exact If true and needle is a comment, position must match */ xmlNode * pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle, bool exact) { CRM_CHECK(needle != NULL, return NULL); if (needle->type == XML_COMMENT_NODE) { return pcmk__xc_match(haystack, needle, exact); } else { const char *id = pcmk__xe_id(needle); const char *attr = (id == NULL)? NULL : PCMK_XA_ID; return pcmk__xe_match(haystack, (const char *) needle->name, attr, id); } } void xml_accept_changes(xmlNode * xml) { xmlNode *top = NULL; xml_doc_private_t *docpriv = NULL; if(xml == NULL) { return; } crm_trace("Accepting changes to %p", xml); docpriv = xml->doc->_private; top = xmlDocGetRootElement(xml->doc); reset_xml_private_data(xml->doc->_private); if (!pcmk_is_set(docpriv->flags, pcmk__xf_dirty)) { docpriv->flags = pcmk__xf_none; return; } docpriv->flags = pcmk__xf_none; accept_attr_deletions(top); } xmlNode * find_xml_node(const xmlNode *root, const char *search_path, gboolean must_find) { xmlNode *a_child = NULL; const char *name = (root == NULL)? "" : (const char *) root->name; if (search_path == NULL) { crm_warn("Will never find "); return NULL; } for (a_child = pcmk__xml_first_child(root); a_child != NULL; a_child = pcmk__xml_next(a_child)) { if (strcmp((const char *)a_child->name, search_path) == 0) { return a_child; } } if (must_find) { crm_warn("Could not find %s in %s.", search_path, name); } else if (root != NULL) { crm_trace("Could not find %s in %s.", search_path, name); } else { crm_trace("Could not find %s in .", search_path); } return NULL; } #define attr_matches(c, n, v) pcmk__str_eq(crm_element_value((c), (n)), \ (v), pcmk__str_none) /*! * \internal * \brief Find first XML child element matching given criteria * * \param[in] parent XML element to search * \param[in] node_name If not NULL, only match children of this type * \param[in] attr_n If not NULL, only match children with an attribute * of this name. * \param[in] attr_v If \p attr_n and this are not NULL, only match children * with an attribute named \p attr_n and this value * * \return Matching XML child element, or NULL if none found */ xmlNode * pcmk__xe_match(const xmlNode *parent, const char *node_name, const char *attr_n, const char *attr_v) { CRM_CHECK(parent != NULL, return NULL); CRM_CHECK(attr_v == NULL || attr_n != NULL, return NULL); for (xmlNode *child = pcmk__xml_first_child(parent); child != NULL; child = pcmk__xml_next(child)) { if (((node_name == NULL) || pcmk__xe_is(child, node_name)) && ((attr_n == NULL) || (attr_v == NULL && xmlHasProp(child, (pcmkXmlStr) attr_n)) || (attr_v != NULL && attr_matches(child, attr_n, attr_v)))) { return child; } } crm_trace("XML child node <%s%s%s%s%s> not found in %s", (node_name? node_name : "(any)"), (attr_n? " " : ""), (attr_n? attr_n : ""), (attr_n? "=" : ""), (attr_n? attr_v : ""), (const char *) parent->name); return NULL; } void copy_in_properties(xmlNode *target, const xmlNode *src) { if (src == NULL) { crm_warn("No node to copy properties from"); } else if (target == NULL) { crm_err("No node to copy properties into"); } else { for (xmlAttrPtr a = pcmk__xe_first_attr(src); a != NULL; a = a->next) { const char *p_name = (const char *) a->name; const char *p_value = pcmk__xml_attr_value(a); expand_plus_plus(target, p_name, p_value); if (xml_acl_denied(target)) { crm_trace("Cannot copy %s=%s to %s", p_name, p_value, target->name); return; } } } return; } /*! * \brief Parse integer assignment statements on this node and all its child * nodes * * \param[in,out] target Root XML node to be processed * * \note This function is recursive */ void fix_plus_plus_recursive(xmlNode *target) { /* TODO: Remove recursion and use xpath searches for value++ */ xmlNode *child = NULL; for (xmlAttrPtr a = pcmk__xe_first_attr(target); a != NULL; a = a->next) { const char *p_name = (const char *) a->name; const char *p_value = pcmk__xml_attr_value(a); expand_plus_plus(target, p_name, p_value); } for (child = pcmk__xml_first_child(target); child != NULL; child = pcmk__xml_next(child)) { fix_plus_plus_recursive(child); } } /*! * \brief Update current XML attribute value per parsed integer assignment statement * * \param[in,out] target an XML node, containing a XML attribute that is * initialized to some numeric value, to be processed * \param[in] name name of the XML attribute, e.g. X, whose value * should be updated * \param[in] value assignment statement, e.g. "X++" or * "X+=5", to be applied to the initialized value. * * \note The original XML attribute value is treated as 0 if non-numeric and * truncated to be an integer if decimal-point-containing. * \note The final XML attribute value is truncated to not exceed 1000000. * \note Undefined behavior if unexpected input. */ void expand_plus_plus(xmlNode * target, const char *name, const char *value) { int offset = 1; int name_len = 0; int int_value = 0; int value_len = 0; const char *old_value = NULL; if (target == NULL || value == NULL || name == NULL) { return; } old_value = crm_element_value(target, name); if (old_value == NULL) { /* if no previous value, set unexpanded */ goto set_unexpanded; } else if (strstr(value, name) != value) { goto set_unexpanded; } name_len = strlen(name); value_len = strlen(value); if (value_len < (name_len + 2) || value[name_len] != '+' || (value[name_len + 1] != '+' && value[name_len + 1] != '=')) { goto set_unexpanded; } /* if we are expanding ourselves, * then no previous value was set and leave int_value as 0 */ if (old_value != value) { int_value = char2score(old_value); } if (value[name_len + 1] != '+') { const char *offset_s = value + (name_len + 2); offset = char2score(offset_s); } int_value += offset; if (int_value > PCMK_SCORE_INFINITY) { int_value = PCMK_SCORE_INFINITY; } crm_xml_add_int(target, name, int_value); return; set_unexpanded: if (old_value == value) { /* the old value is already set, nothing to do */ return; } crm_xml_add(target, name, value); return; } /*! * \internal * \brief Remove an XML element's attributes that match some criteria * * \param[in,out] element XML element to modify * \param[in] match If not NULL, only remove attributes for which * this function returns true * \param[in,out] user_data Data to pass to \p match */ void pcmk__xe_remove_matching_attrs(xmlNode *element, bool (*match)(xmlAttrPtr, void *), void *user_data) { xmlAttrPtr next = NULL; for (xmlAttrPtr a = pcmk__xe_first_attr(element); a != NULL; a = next) { next = a->next; // Grab now because attribute might get removed if ((match == NULL) || match(a, user_data)) { if (!pcmk__check_acl(element, NULL, pcmk__xf_acl_write)) { crm_trace("ACLs prevent removal of attributes (%s and " "possibly others) from %s element", (const char *) a->name, (const char *) element->name); return; // ACLs apply to element, not particular attributes } if (pcmk__tracking_xml_changes(element, false)) { // Leave (marked for removal) until after diff is calculated set_parent_flag(element, pcmk__xf_dirty); pcmk__set_xml_flags((xml_node_private_t *) a->_private, pcmk__xf_deleted); } else { xmlRemoveProp(a); } } } } xmlNode * create_xml_node(xmlNode * parent, const char *name) { xmlDoc *doc = NULL; xmlNode *node = NULL; if (pcmk__str_empty(name)) { CRM_CHECK(name != NULL && name[0] == 0, return NULL); return NULL; } if (parent == NULL) { doc = xmlNewDoc(PCMK__XML_VERSION); if (doc == NULL) { return NULL; } node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL); if (node == NULL) { xmlFreeDoc(doc); return NULL; } xmlDocSetRootElement(doc, node); } else { node = xmlNewChild(parent, NULL, (pcmkXmlStr) name, NULL); if (node == NULL) { return NULL; } } pcmk__mark_xml_created(node); return node; } /*! * \internal * \brief Set a given string as an XML node's content * * \param[in,out] node Node whose content to set * \param[in] content String to set as the content * * \note \c xmlNodeSetContent() does not escape special characters. */ void pcmk__xe_set_content(xmlNode *node, const char *content) { if (node != NULL) { char *escaped = pcmk__xml_escape(content, false); xmlNodeSetContent(node, (pcmkXmlStr) escaped); free(escaped); } } xmlNode * pcmk_create_xml_text_node(xmlNode * parent, const char *name, const char *content) { xmlNode *node = create_xml_node(parent, name); pcmk__xe_set_content(node, content); return node; } xmlNode * pcmk_create_html_node(xmlNode * parent, const char *element_name, const char *id, const char *class_name, const char *text) { xmlNode *node = pcmk_create_xml_text_node(parent, element_name, text); if (class_name != NULL) { crm_xml_add(node, PCMK_XA_CLASS, class_name); } if (id != NULL) { crm_xml_add(node, PCMK_XA_ID, id); } return node; } /*! * Free an XML element and all of its children, removing it from its parent * * \param[in,out] xml XML element to free */ void pcmk_free_xml_subtree(xmlNode *xml) { xmlUnlinkNode(xml); // Detaches from parent and siblings xmlFreeNode(xml); // Frees } static void free_xml_with_position(xmlNode * child, int position) { if (child != NULL) { xmlNode *top = NULL; xmlDoc *doc = child->doc; xml_node_private_t *nodepriv = child->_private; xml_doc_private_t *docpriv = NULL; if (doc != NULL) { top = xmlDocGetRootElement(doc); } if (doc != NULL && top == child) { /* Free everything */ xmlFreeDoc(doc); } else if (pcmk__check_acl(child, NULL, pcmk__xf_acl_write) == FALSE) { GString *xpath = NULL; pcmk__if_tracing({}, return); xpath = pcmk__element_xpath(child); qb_log_from_external_source(__func__, __FILE__, "Cannot remove %s %x", LOG_TRACE, __LINE__, 0, (const char *) xpath->str, nodepriv->flags); g_string_free(xpath, TRUE); return; } else { if (doc && pcmk__tracking_xml_changes(child, FALSE) && !pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { GString *xpath = pcmk__element_xpath(child); if (xpath != NULL) { pcmk__deleted_xml_t *deleted_obj = NULL; crm_trace("Deleting %s %p from %p", (const char *) xpath->str, child, doc); deleted_obj = calloc(1, sizeof(pcmk__deleted_xml_t)); deleted_obj->path = strdup((const char *) xpath->str); CRM_ASSERT(deleted_obj->path != NULL); g_string_free(xpath, TRUE); deleted_obj->position = -1; /* Record the "position" only for XML comments for now */ if (child->type == XML_COMMENT_NODE) { if (position >= 0) { deleted_obj->position = position; } else { deleted_obj->position = pcmk__xml_position(child, pcmk__xf_skip); } } docpriv = doc->_private; docpriv->deleted_objs = g_list_append(docpriv->deleted_objs, deleted_obj); pcmk__set_xml_doc_flag(child, pcmk__xf_dirty); } } pcmk_free_xml_subtree(child); } } } void free_xml(xmlNode * child) { free_xml_with_position(child, -1); } /*! * \internal * \brief Make a deep copy of an XML node under a given parent * * \param[in,out] parent XML element that will be the copy's parent (\c NULL * to create a new XML document with the copy as root) * \param[in] src XML node to copy * * \return Deep copy of \p src, or \c NULL if \p src is \c NULL */ xmlNode * pcmk__xml_copy(xmlNode *parent, xmlNode *src) { xmlNode *copy = NULL; if (src == NULL) { return NULL; } if (parent == NULL) { xmlDoc *doc = NULL; // The copy will be the root element of a new document CRM_ASSERT(src->type == XML_ELEMENT_NODE); doc = xmlNewDoc(PCMK__XML_VERSION); CRM_ASSERT(doc != NULL); copy = xmlDocCopyNode(src, doc, 1); CRM_ASSERT(copy != NULL); xmlDocSetRootElement(doc, copy); } else { copy = xmlDocCopyNode(src, parent->doc, 1); CRM_ASSERT(copy != NULL); xmlAddChild(parent, copy); } pcmk__mark_xml_created(copy); return copy; } xmlNode * string2xml(const char *input) { xmlNode *xml = NULL; xmlDocPtr output = NULL; xmlParserCtxtPtr ctxt = NULL; const xmlError *last_error = NULL; if (input == NULL) { crm_err("Can't parse NULL input"); return NULL; } /* create a parser context */ ctxt = xmlNewParserCtxt(); CRM_CHECK(ctxt != NULL, return NULL); xmlCtxtResetLastError(ctxt); xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err); output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL, PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER); if (output == NULL) { output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL, PCMK__XML_PARSE_OPTS_WITH_RECOVER); if (output) { crm_warn("Successfully recovered from XML errors " "(note: a future release will treat this as a fatal failure)"); } } if (output) { xml = xmlDocGetRootElement(output); } last_error = xmlCtxtGetLastError(ctxt); if (last_error && last_error->code != XML_ERR_OK) { /* crm_abort(__FILE__,__func__,__LINE__, "last_error->code != XML_ERR_OK", TRUE, TRUE); */ /* * http://xmlsoft.org/html/libxml-xmlerror.html#xmlErrorLevel * http://xmlsoft.org/html/libxml-xmlerror.html#xmlParserErrors */ crm_warn("Parsing failed (domain=%d, level=%d, code=%d): %s", last_error->domain, last_error->level, last_error->code, last_error->message); if (last_error->code == XML_ERR_DOCUMENT_EMPTY) { CRM_LOG_ASSERT("Cannot parse an empty string"); } else if (last_error->code != XML_ERR_DOCUMENT_END) { crm_err("Couldn't%s parse %d chars: %s", xml ? " fully" : "", (int)strlen(input), input); if (xml != NULL) { crm_log_xml_err(xml, "Partial"); } } else { int len = strlen(input); int lpc = 0; while(lpc < len) { crm_warn("Parse error[+%.3d]: %.80s", lpc, input+lpc); lpc += 80; } CRM_LOG_ASSERT("String parsing error"); } } xmlFreeParserCtxt(ctxt); return xml; } xmlNode * stdin2xml(void) { size_t data_length = 0; size_t read_chars = 0; char *xml_buffer = NULL; xmlNode *xml_obj = NULL; do { xml_buffer = pcmk__realloc(xml_buffer, data_length + PCMK__BUFFER_SIZE); read_chars = fread(xml_buffer + data_length, 1, PCMK__BUFFER_SIZE, stdin); data_length += read_chars; } while (read_chars == PCMK__BUFFER_SIZE); if (data_length == 0) { crm_warn("No XML supplied on stdin"); free(xml_buffer); return NULL; } xml_buffer[data_length] = '\0'; xml_obj = string2xml(xml_buffer); free(xml_buffer); crm_log_xml_trace(xml_obj, "Created fragment"); return xml_obj; } static char * decompress_file(const char *filename) { char *buffer = NULL; int rc = 0; size_t length = 0, read_len = 0; BZFILE *bz_file = NULL; FILE *input = fopen(filename, "r"); if (input == NULL) { crm_perror(LOG_ERR, "Could not open %s for reading", filename); return NULL; } bz_file = BZ2_bzReadOpen(&rc, input, 0, 0, NULL, 0); rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { crm_err("Could not prepare to read compressed %s: %s " CRM_XS " rc=%d", filename, pcmk_rc_str(rc), rc); BZ2_bzReadClose(&rc, bz_file); fclose(input); return NULL; } rc = BZ_OK; // cppcheck seems not to understand the abort-logic in pcmk__realloc // cppcheck-suppress memleak while (rc == BZ_OK) { buffer = pcmk__realloc(buffer, PCMK__BUFFER_SIZE + length + 1); read_len = BZ2_bzRead(&rc, bz_file, buffer + length, PCMK__BUFFER_SIZE); crm_trace("Read %ld bytes from file: %d", (long)read_len, rc); if (rc == BZ_OK || rc == BZ_STREAM_END) { length += read_len; } } buffer[length] = '\0'; rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { crm_err("Could not read compressed %s: %s " CRM_XS " rc=%d", filename, pcmk_rc_str(rc), rc); free(buffer); buffer = NULL; } BZ2_bzReadClose(&rc, bz_file); fclose(input); return buffer; } /*! * \internal * \brief Remove XML text nodes from specified XML and all its children * * \param[in,out] xml XML to strip text from */ void pcmk__strip_xml_text(xmlNode *xml) { xmlNode *iter = xml->children; while (iter) { xmlNode *next = iter->next; switch (iter->type) { case XML_TEXT_NODE: /* Remove it */ pcmk_free_xml_subtree(iter); break; case XML_ELEMENT_NODE: /* Search it */ pcmk__strip_xml_text(iter); break; default: /* Leave it */ break; } iter = next; } } +/*! + * \internal + * \brief Parse XML from a file + * + * \param[in] filename Name of file containing XML (\c NULL or \c "-" for + * \c stdin); if \p filename ends in \c ".bz2", the file + * will be decompressed using \c bzip2 + * + * \return XML tree parsed from the given file; may be \c NULL or only partial + * on error + */ xmlNode * -filename2xml(const char *filename) +pcmk__xml_read(const char *filename) { + static const char *recovered = "Successfully recovered from XML errors " + "(note: a future release will treat this as " + "a fatal failure)"; xmlNode *xml = NULL; xmlDocPtr output = NULL; bool uncompressed = true; - xmlParserCtxtPtr ctxt = NULL; + xmlParserCtxt *ctxt = NULL; const xmlError *last_error = NULL; - /* create a parser context */ + // Create a parser context ctxt = xmlNewParserCtxt(); CRM_CHECK(ctxt != NULL, return NULL); xmlCtxtResetLastError(ctxt); xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err); - if (filename) { + if (filename != NULL) { uncompressed = !pcmk__ends_with_ext(filename, ".bz2"); } if (pcmk__str_eq(filename, "-", pcmk__str_null_matches)) { - /* STDIN_FILENO == fileno(stdin) */ output = xmlCtxtReadFd(ctxt, STDIN_FILENO, "unknown.xml", NULL, PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER); if (output == NULL) { output = xmlCtxtReadFd(ctxt, STDIN_FILENO, "unknown.xml", NULL, PCMK__XML_PARSE_OPTS_WITH_RECOVER); - if (output) { - crm_warn("Successfully recovered from XML errors " - "(note: a future release will treat this as a fatal failure)"); + if (output != NULL) { + crm_warn("%s", recovered); } } } else if (uncompressed) { output = xmlCtxtReadFile(ctxt, filename, NULL, PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER); if (output == NULL) { output = xmlCtxtReadFile(ctxt, filename, NULL, PCMK__XML_PARSE_OPTS_WITH_RECOVER); - if (output) { - crm_warn("Successfully recovered from XML errors " - "(note: a future release will treat this as a fatal failure)"); + if (output != NULL) { + crm_warn("%s", recovered); } - } + } } else { char *input = decompress_file(filename); output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL, PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER); if (output == NULL) { output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL, PCMK__XML_PARSE_OPTS_WITH_RECOVER); - if (output) { - crm_warn("Successfully recovered from XML errors " - "(note: a future release will treat this as a fatal failure)"); + if (output != NULL) { + crm_warn("%s", recovered); } - } + } free(input); } - if (output && (xml = xmlDocGetRootElement(output))) { - pcmk__strip_xml_text(xml); + if (output != NULL) { + xml = xmlDocGetRootElement(output); + if (xml != NULL) { + /* @TODO Should we really be stripping out text? This seems like an + * overly broad way to get rid of whitespace, if that's the goal. + * Text nodes may be invalid in most or all Pacemaker inputs, but + * stripping them in a generic "parse XML from file" function may + * not be the best way to ignore them. + */ + pcmk__strip_xml_text(xml); + } } last_error = xmlCtxtGetLastError(ctxt); - if (last_error && last_error->code != XML_ERR_OK) { - /* crm_abort(__FILE__,__func__,__LINE__, "last_error->code != XML_ERR_OK", TRUE, TRUE); */ - /* - * http://xmlsoft.org/html/libxml-xmlerror.html#xmlErrorLevel - * http://xmlsoft.org/html/libxml-xmlerror.html#xmlParserErrors + if ((last_error != NULL) && (last_error->code != XML_ERR_OK)) { + /* crm_abort(__FILE__,__func__,__LINE__, + * "last_error->code != XML_ERR_OK", TRUE, TRUE); */ crm_err("Parsing failed (domain=%d, level=%d, code=%d): %s", - last_error->domain, last_error->level, last_error->code, last_error->message); + last_error->domain, last_error->level, last_error->code, + last_error->message); - if (last_error && last_error->code != XML_ERR_OK) { - crm_err("Couldn't%s parse %s", xml ? " fully" : "", filename); - if (xml != NULL) { - crm_log_xml_err(xml, "Partial"); - } + if (xml != NULL) { + // @COMPAT At 3.0.0, free xml and return NULL + crm_err("Couldn't fully parse %s", filename); + crm_log_xml_err(xml, "Partial"); + } else { + crm_err("Couldn't parse %s", filename); } } xmlFreeParserCtxt(ctxt); return xml; } +xmlNode * +filename2xml(const char *filename) +{ + return pcmk__xml_read(filename); +} + /*! * \internal * \brief Add a "last written" attribute to an XML element, set to current time * * \param[in,out] xe XML element to add attribute to * * \return Value that was set, or NULL on error */ const char * pcmk__xe_add_last_written(xmlNode *xe) { char *now_s = pcmk__epoch2str(NULL, 0); const char *result = NULL; result = crm_xml_add(xe, PCMK_XA_CIB_LAST_WRITTEN, pcmk__s(now_s, "Could not determine current time")); free(now_s); return result; } /*! * \brief Sanitize a string so it is usable as an XML ID * * \param[in,out] id String to sanitize */ void crm_xml_sanitize_id(char *id) { char *c; for (c = id; *c; ++c) { /* @TODO Sanitize more comprehensively */ switch (*c) { case ':': case '#': *c = '.'; } } } /*! * \brief Set the ID of an XML element using a format * * \param[in,out] xml XML element * \param[in] fmt printf-style format * \param[in] ... any arguments required by format */ void crm_xml_set_id(xmlNode *xml, const char *format, ...) { va_list ap; int len = 0; char *id = NULL; /* equivalent to crm_strdup_printf() */ va_start(ap, format); len = vasprintf(&id, format, ap); va_end(ap); CRM_ASSERT(len > 0); crm_xml_sanitize_id(id); crm_xml_add(xml, PCMK_XA_ID, id); free(id); } /*! * \internal * \brief Write XML to a file stream * * \param[in] xml XML to write * \param[in] filename Name of file being written (for logging only) * \param[in,out] stream Open file stream corresponding to filename * \param[in] compress Whether to compress XML before writing * \param[out] nbytes Number of bytes written * * \return Standard Pacemaker return code */ static int write_xml_stream(const xmlNode *xml, const char *filename, FILE *stream, bool compress, unsigned int *nbytes) { int rc = pcmk_rc_ok; char *buffer = NULL; *nbytes = 0; crm_log_xml_trace(xml, "writing"); buffer = dump_xml_formatted(xml); CRM_CHECK(buffer && strlen(buffer), crm_log_xml_warn(xml, "formatting failed"); rc = pcmk_rc_error; goto bail); if (compress) { unsigned int in = 0; BZFILE *bz_file = NULL; rc = BZ_OK; bz_file = BZ2_bzWriteOpen(&rc, stream, 5, 0, 30); rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { crm_warn("Not compressing %s: could not prepare file stream: %s " CRM_XS " rc=%d", filename, pcmk_rc_str(rc), rc); } else { BZ2_bzWrite(&rc, bz_file, buffer, strlen(buffer)); rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { crm_warn("Not compressing %s: could not compress data: %s " CRM_XS " rc=%d errno=%d", filename, pcmk_rc_str(rc), rc, errno); } } if (rc == pcmk_rc_ok) { BZ2_bzWriteClose(&rc, bz_file, 0, &in, nbytes); rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { crm_warn("Not compressing %s: could not write compressed data: %s " CRM_XS " rc=%d errno=%d", filename, pcmk_rc_str(rc), rc, errno); *nbytes = 0; // retry without compression } else { crm_trace("Compressed XML for %s from %u bytes to %u", filename, in, *nbytes); } } rc = pcmk_rc_ok; // Either true, or we'll retry without compression } if (*nbytes == 0) { rc = fprintf(stream, "%s", buffer); if (rc < 0) { rc = errno; crm_perror(LOG_ERR, "writing %s", filename); } else { *nbytes = (unsigned int) rc; rc = pcmk_rc_ok; } } bail: if (fflush(stream) != 0) { rc = errno; crm_perror(LOG_ERR, "flushing %s", filename); } /* Don't report error if the file does not support synchronization */ if (fsync(fileno(stream)) < 0 && errno != EROFS && errno != EINVAL) { rc = errno; crm_perror(LOG_ERR, "synchronizing %s", filename); } fclose(stream); crm_trace("Saved %d bytes to %s as XML", *nbytes, filename); free(buffer); return rc; } /*! * \brief Write XML to a file descriptor * * \param[in] xml XML to write * \param[in] filename Name of file being written (for logging only) * \param[in] fd Open file descriptor corresponding to filename * \param[in] compress Whether to compress XML before writing * * \return Number of bytes written on success, -errno otherwise */ int write_xml_fd(const xmlNode *xml, const char *filename, int fd, gboolean compress) { FILE *stream = NULL; unsigned int nbytes = 0; int rc = pcmk_rc_ok; CRM_CHECK((xml != NULL) && (fd > 0), return -EINVAL); stream = fdopen(fd, "w"); if (stream == NULL) { return -errno; } rc = write_xml_stream(xml, filename, stream, compress, &nbytes); if (rc != pcmk_rc_ok) { return pcmk_rc2legacy(rc); } return (int) nbytes; } /*! * \brief Write XML to a file * * \param[in] xml XML to write * \param[in] filename Name of file to write * \param[in] compress Whether to compress XML before writing * * \return Number of bytes written on success, -errno otherwise */ int write_xml_file(const xmlNode *xml, const char *filename, gboolean compress) { FILE *stream = NULL; unsigned int nbytes = 0; int rc = pcmk_rc_ok; CRM_CHECK((xml != NULL) && (filename != NULL), return -EINVAL); stream = fopen(filename, "w"); if (stream == NULL) { return -errno; } rc = write_xml_stream(xml, filename, stream, compress, &nbytes); if (rc != pcmk_rc_ok) { return pcmk_rc2legacy(rc); } return (int) nbytes; } /*! * \internal * \brief Get consecutive bytes encoding non-ASCII UTF-8 characters * * \param[in] text String to check * * \return Number of non-ASCII UTF-8 bytes at the beginning of \p text */ static size_t utf8_bytes(const char *text) { // Total number of consecutive bytes containing UTF-8 characters size_t c_bytes = 0; if (text == NULL) { return 0; } /* UTF-8 uses one to four 8-bit bytes per character. The first byte * indicates the width of the character. A byte beginning with a '0' bit is * a one-byte ASCII character. * * A C byte is 8 bits on most systems, but this is not guaranteed. * * Count until we find an ASCII character or an invalid byte. Check bytes * aligned with the C byte boundary. */ for (const uint8_t *utf8_byte = (const uint8_t *) text; (*utf8_byte & 0x80) != 0; utf8_byte = (const uint8_t *) (text + c_bytes)) { size_t utf8_bits = 0; if ((*utf8_byte & 0xf0) == 0xf0) { // Four-byte character (first byte: 11110xxx) utf8_bits = 32; } else if ((*utf8_byte & 0xe0) == 0xe0) { // Three-byte character (first byte: 1110xxxx) utf8_bits = 24; } else if ((*utf8_byte & 0xc0) == 0xc0) { // Two-byte character (first byte: 110xxxxx) utf8_bits = 16; } else { crm_warn("Found invalid UTF-8 character %.2x", (unsigned char) *utf8_byte); return c_bytes; } c_bytes += utf8_bits / CHAR_BIT; #if (CHAR_BIT != 8) // Coverity complains about dead code without this CPP guard if ((utf8_bits % CHAR_BIT) > 0) { c_bytes++; } #endif // CHAR_BIT != 8 } return c_bytes; } /*! * \internal * \brief Replace a character in a dynamically allocated string, reallocating * memory * * \param[in,out] text String to replace a character in * \param[in,out] index Index of character to replace with new string; on * return, reset to index of end of replacement string * \param[in,out] length Length of \p text * \param[in] replace String to replace character at \p index with (must * not be empty) * * \return \p text, with the character at \p index replaced by \p replace */ static char * replace_text(char *text, size_t *index, size_t *length, const char *replace) { /* @TODO Replace with GString? Or at least copy char-by-char, escaping * characters as needed, instead of shifting characters on every replacement */ // We have space for 1 char already size_t offset = strlen(replace) - 1; if (offset > 0) { *length += offset; text = pcmk__realloc(text, *length + 1); // Shift characters to the right to make room for the replacement string for (size_t i = *length; i > (*index + offset); i--) { text[i] = text[i - offset]; } } // Replace the character at index by the replacement string memcpy(text + *index, replace, offset + 1); // Reset index to the end of replacement string *index += offset; return text; } /*! * \internal * \brief Check whether a string has XML special characters that must be escaped * * See \c pcmk__xml_escape() for more details. * * \param[in] text String to check * \param[in] escape_quote If \c true, double quotes must be escaped * * \return \c true if \p text has special characters that need to be escaped, or * \c false otherwise */ bool pcmk__xml_needs_escape(const char *text, bool escape_quote) { size_t length = 0; if (text == NULL) { return false; } length = strlen(text); for (size_t index = 0; index < length; index++) { // Don't escape any non-ASCII characters index += utf8_bytes(&(text[index])); switch (text[index]) { case '\0': // Reached end of string by skipping UTF-8 bytes return false; case '<': return true; case '>': // Not necessary, but for symmetry with '<' return true; case '&': return true; case '"': if (escape_quote) { return true; } break; case '\n': case '\t': // Don't escape newline or tab break; default: if ((text[index] < 0x20) || (text[index] >= 0x7f)) { // Escape non-printing characters return true; } break; } } return false; } /*! * \internal * \brief Replace special characters with their XML escape sequences * * XML allows the escaping of special characters by replacing them with entity * references (for example, """) or character references (for * example, " "). * * The special characters '<' and '&' are not allowed in their * literal forms in XML character data. Character data is non-markup text (for * example, the content of a text node). * * Additionally, if an attribute value is delimited by single quotes, then * single quotes must be escaped within the value. Similarly, if an attribute * value is delimited by double quotes, then double quotes must be escaped * within the value. * * For more details, see the "Character Data and Markup" section of the XML * spec, currently section 2.4: * https://www.w3.org/TR/xml/#dt-markup * * Pacemaker always delimits attribute values with double quotes, so this * function doesn't escape single quotes. * * \param[in] text Text to escape * \param[in] escape_quote If \c true, escape double quotes (should be enabled * for attribute values) * * \return Newly allocated string equivalent to \p text but with special * characters replaced with XML escape sequences (or \c NULL if \p text * is \c NULL). If \p text is not \c NULL, the return value is * guaranteed not to be \c NULL. * * \note There are libxml functions that purport to do this: * \c xmlEncodeEntitiesReentrant() and \c xmlEncodeSpecialChars(). * However, their escaping is incomplete. See: * https://discourse.gnome.org/t/intended-use-of-xmlencodeentitiesreentrant-vs-xmlencodespecialchars/19252 */ char * pcmk__xml_escape(const char *text, bool escape_quote) { size_t length = 0; char *copy = NULL; char buf[32] = { '\0', }; if (text == NULL) { return NULL; } length = strlen(text); pcmk__str_update(©, text); for (size_t index = 0; index < length; index++) { // Don't escape any non-ASCII characters index += utf8_bytes(&(copy[index])); switch (copy[index]) { case '\0': // Reached end of string by skipping UTF-8 bytes break; case '<': copy = replace_text(copy, &index, &length, "<"); break; case '>': // Not necessary, but for symmetry with '<' copy = replace_text(copy, &index, &length, ">"); break; case '&': copy = replace_text(copy, &index, &length, "&"); break; case '"': if (escape_quote) { copy = replace_text(copy, &index, &length, """); } break; case '\n': case '\t': // Don't escape newlines and tabs break; default: if ((copy[index] < 0x20) || (copy[index] >= 0x7f)) { // Escape non-printing characters snprintf(buf, sizeof(buf), "&#%.2x;", copy[index]); copy = replace_text(copy, &index, &length, buf); } break; } } return copy; } /*! * \internal * \brief Append a string representation of an XML element to a buffer * * \param[in] data XML whose representation to append * \param[in] options Group of \p pcmk__xml_fmt_options flags * \param[in,out] buffer Where to append the content (must not be \p NULL) * \param[in] depth Current indentation level */ static void dump_xml_element(const xmlNode *data, uint32_t options, GString *buffer, int depth) { bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); bool filtered = pcmk_is_set(options, pcmk__xml_fmt_filtered); int spaces = pretty? (2 * depth) : 0; for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } pcmk__g_strcat(buffer, "<", data->name, NULL); for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL; attr = attr->next) { if (!filtered || !pcmk__xa_filterable((const char *) (attr->name))) { pcmk__dump_xml_attr(attr, buffer); } } if (data->children == NULL) { g_string_append(buffer, "/>"); } else { g_string_append_c(buffer, '>'); } if (pretty) { g_string_append_c(buffer, '\n'); } if (data->children) { for (const xmlNode *child = data->children; child != NULL; child = child->next) { pcmk__xml2text(child, options, buffer, depth + 1); } for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } pcmk__g_strcat(buffer, "name, ">", NULL); if (pretty) { g_string_append_c(buffer, '\n'); } } } /*! * \internal * \brief Append XML text content to a buffer * * \param[in] data XML whose content to append * \param[in] options Group of \p xml_log_options flags * \param[in,out] buffer Where to append the content (must not be \p NULL) * \param[in] depth Current indentation level */ static void dump_xml_text(const xmlNode *data, uint32_t options, GString *buffer, int depth) { bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); int spaces = pretty? (2 * depth) : 0; const char *content = (const char *) data->content; char *content_esc = NULL; if (pcmk__xml_needs_escape(content, false)) { content_esc = pcmk__xml_escape(content, false); content = content_esc; } for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } g_string_append(buffer, content); if (pretty) { g_string_append_c(buffer, '\n'); } free(content_esc); } /*! * \internal * \brief Append XML CDATA content to a buffer * * \param[in] data XML whose content to append * \param[in] options Group of \p pcmk__xml_fmt_options flags * \param[in,out] buffer Where to append the content (must not be \p NULL) * \param[in] depth Current indentation level */ static void dump_xml_cdata(const xmlNode *data, uint32_t options, GString *buffer, int depth) { bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); int spaces = pretty? (2 * depth) : 0; for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } pcmk__g_strcat(buffer, "content, "]]>", NULL); if (pretty) { g_string_append_c(buffer, '\n'); } } /*! * \internal * \brief Append an XML comment to a buffer * * \param[in] data XML whose content to append * \param[in] options Group of \p pcmk__xml_fmt_options flags * \param[in,out] buffer Where to append the content (must not be \p NULL) * \param[in] depth Current indentation level */ static void dump_xml_comment(const xmlNode *data, uint32_t options, GString *buffer, int depth) { bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); int spaces = pretty? (2 * depth) : 0; for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } pcmk__g_strcat(buffer, "", NULL); if (pretty) { g_string_append_c(buffer, '\n'); } } /*! * \internal * \brief Get a string representation of an XML element type * * \param[in] type XML element type * * \return String representation of \p type */ static const char * xml_element_type2str(xmlElementType type) { static const char *const element_type_names[] = { [XML_ELEMENT_NODE] = "element", [XML_ATTRIBUTE_NODE] = "attribute", [XML_TEXT_NODE] = "text", [XML_CDATA_SECTION_NODE] = "CDATA section", [XML_ENTITY_REF_NODE] = "entity reference", [XML_ENTITY_NODE] = "entity", [XML_PI_NODE] = "PI", [XML_COMMENT_NODE] = "comment", [XML_DOCUMENT_NODE] = "document", [XML_DOCUMENT_TYPE_NODE] = "document type", [XML_DOCUMENT_FRAG_NODE] = "document fragment", [XML_NOTATION_NODE] = "notation", [XML_HTML_DOCUMENT_NODE] = "HTML document", [XML_DTD_NODE] = "DTD", [XML_ELEMENT_DECL] = "element declaration", [XML_ATTRIBUTE_DECL] = "attribute declaration", [XML_ENTITY_DECL] = "entity declaration", [XML_NAMESPACE_DECL] = "namespace declaration", [XML_XINCLUDE_START] = "XInclude start", [XML_XINCLUDE_END] = "XInclude end", }; if ((type < 0) || (type >= PCMK__NELEM(element_type_names))) { return "unrecognized type"; } return element_type_names[type]; } /*! * \internal * \brief Create a text representation of an XML object * * \param[in] data XML to convert * \param[in] options Group of \p pcmk__xml_fmt_options flags * \param[in,out] buffer Where to store the text (must not be \p NULL) * \param[in] depth Current indentation level */ void pcmk__xml2text(const xmlNode *data, uint32_t options, GString *buffer, int depth) { if (data == NULL) { crm_trace("Nothing to dump"); return; } CRM_ASSERT(buffer != NULL); CRM_CHECK(depth >= 0, depth = 0); switch(data->type) { case XML_ELEMENT_NODE: /* Handle below */ dump_xml_element(data, options, buffer, depth); break; case XML_TEXT_NODE: if (pcmk_is_set(options, pcmk__xml_fmt_text)) { dump_xml_text(data, options, buffer, depth); } break; case XML_COMMENT_NODE: dump_xml_comment(data, options, buffer, depth); break; case XML_CDATA_SECTION_NODE: dump_xml_cdata(data, options, buffer, depth); break; default: crm_warn("Cannot convert XML %s node to text " CRM_XS " type=%d", xml_element_type2str(data->type), data->type); break; } } char * dump_xml_formatted_with_text(const xmlNode *xml) { /* libxml's xmlNodeDumpOutput() would work here since we're not specifically * filtering out any nodes. However, use pcmk__xml2text() for consistency, * to escape attribute values, and to allow a const argument. */ char *buffer = NULL; GString *g_buffer = g_string_sized_new(1024); pcmk__xml2text(xml, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, g_buffer, 0); pcmk__str_update(&buffer, g_buffer->str); g_string_free(g_buffer, TRUE); return buffer; } char * dump_xml_formatted(const xmlNode *xml) { char *buffer = NULL; GString *g_buffer = g_string_sized_new(1024); pcmk__xml2text(xml, pcmk__xml_fmt_pretty, g_buffer, 0); pcmk__str_update(&buffer, g_buffer->str); g_string_free(g_buffer, TRUE); return buffer; } char * dump_xml_unformatted(const xmlNode *xml) { char *buffer = NULL; GString *g_buffer = g_string_sized_new(1024); pcmk__xml2text(xml, 0, g_buffer, 0); pcmk__str_update(&buffer, g_buffer->str); g_string_free(g_buffer, TRUE); return buffer; } int pcmk__xml2fd(int fd, xmlNode *cur) { bool success; xmlOutputBuffer *fd_out = xmlOutputBufferCreateFd(fd, NULL); CRM_ASSERT(fd_out != NULL); xmlNodeDumpOutput(fd_out, cur->doc, cur, 0, pcmk__xml_fmt_pretty, NULL); success = xmlOutputBufferWrite(fd_out, sizeof("\n") - 1, "\n") != -1; success = xmlOutputBufferClose(fd_out) != -1 && success; if (!success) { return EIO; } fsync(fd); return pcmk_rc_ok; } void xml_remove_prop(xmlNode * obj, const char *name) { if (crm_element_value(obj, name) == NULL) { return; } if (pcmk__check_acl(obj, NULL, pcmk__xf_acl_write) == FALSE) { crm_trace("Cannot remove %s from %s", name, obj->name); } else if (pcmk__tracking_xml_changes(obj, FALSE)) { /* Leave in place (marked for removal) until after the diff is calculated */ xmlAttr *attr = xmlHasProp(obj, (pcmkXmlStr) name); xml_node_private_t *nodepriv = attr->_private; set_parent_flag(obj, pcmk__xf_dirty); pcmk__set_xml_flags(nodepriv, pcmk__xf_deleted); } else { xmlUnsetProp(obj, (pcmkXmlStr) name); } } void save_xml_to_file(const xmlNode *xml, const char *desc, const char *filename) { char *f = NULL; if (filename == NULL) { char *uuid = crm_generate_uuid(); f = crm_strdup_printf("%s/%s", pcmk__get_tmpdir(), uuid); filename = f; free(uuid); } crm_info("Saving %s to %s", desc, filename); write_xml_file(xml, filename, FALSE); free(f); } /*! * \internal * \brief Set a flag on all attributes of an XML element * * \param[in,out] xml XML node to set flags on * \param[in] flag XML private flag to set */ static void set_attrs_flag(xmlNode *xml, enum xml_private_flags flag) { for (xmlAttr *attr = pcmk__xe_first_attr(xml); attr; attr = attr->next) { pcmk__set_xml_flags((xml_node_private_t *) (attr->_private), flag); } } /*! * \internal * \brief Add an XML attribute to a node, marked as deleted * * When calculating XML changes, we need to know when an attribute has been * deleted. Add the attribute back to the new XML, so that we can check the * removal against ACLs, and mark it as deleted for later removal after * differences have been calculated. * * \param[in,out] new_xml XML to modify * \param[in] element Name of XML element that changed (for logging) * \param[in] attr_name Name of attribute that was deleted * \param[in] old_value Value of attribute that was deleted */ static void mark_attr_deleted(xmlNode *new_xml, const char *element, const char *attr_name, const char *old_value) { xml_doc_private_t *docpriv = new_xml->doc->_private; xmlAttr *attr = NULL; xml_node_private_t *nodepriv; // Prevent the dirty flag being set recursively upwards pcmk__clear_xml_flags(docpriv, pcmk__xf_tracking); // Restore the old value (and the tracking flag) attr = xmlSetProp(new_xml, (pcmkXmlStr) attr_name, (pcmkXmlStr) old_value); pcmk__set_xml_flags(docpriv, pcmk__xf_tracking); // Reset flags (so the attribute doesn't appear as newly created) nodepriv = attr->_private; nodepriv->flags = 0; // Check ACLs and mark restored value for later removal xml_remove_prop(new_xml, attr_name); crm_trace("XML attribute %s=%s was removed from %s", attr_name, old_value, element); } /* * \internal * \brief Check ACLs for a changed XML attribute */ static void mark_attr_changed(xmlNode *new_xml, const char *element, const char *attr_name, const char *old_value) { char *vcopy = crm_element_value_copy(new_xml, attr_name); crm_trace("XML attribute %s was changed from '%s' to '%s' in %s", attr_name, old_value, vcopy, element); // Restore the original value xmlSetProp(new_xml, (pcmkXmlStr) attr_name, (pcmkXmlStr) old_value); // Change it back to the new value, to check ACLs crm_xml_add(new_xml, attr_name, vcopy); free(vcopy); } /*! * \internal * \brief Mark an XML attribute as having changed position * * \param[in,out] new_xml XML to modify * \param[in] element Name of XML element that changed (for logging) * \param[in,out] old_attr Attribute that moved, in original XML * \param[in,out] new_attr Attribute that moved, in \p new_xml * \param[in] p_old Ordinal position of \p old_attr in original XML * \param[in] p_new Ordinal position of \p new_attr in \p new_xml */ static void mark_attr_moved(xmlNode *new_xml, const char *element, xmlAttr *old_attr, xmlAttr *new_attr, int p_old, int p_new) { xml_node_private_t *nodepriv = new_attr->_private; crm_trace("XML attribute %s moved from position %d to %d in %s", old_attr->name, p_old, p_new, element); // Mark document, element, and all element's parents as changed pcmk__mark_xml_node_dirty(new_xml); // Mark attribute as changed pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_moved); nodepriv = (p_old > p_new)? old_attr->_private : new_attr->_private; pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); } /*! * \internal * \brief Calculate differences in all previously existing XML attributes * * \param[in,out] old_xml Original XML to compare * \param[in,out] new_xml New XML to compare */ static void xml_diff_old_attrs(xmlNode *old_xml, xmlNode *new_xml) { xmlAttr *attr_iter = pcmk__xe_first_attr(old_xml); while (attr_iter != NULL) { const char *name = (const char *) attr_iter->name; xmlAttr *old_attr = attr_iter; xmlAttr *new_attr = xmlHasProp(new_xml, attr_iter->name); const char *old_value = pcmk__xml_attr_value(attr_iter); attr_iter = attr_iter->next; if (new_attr == NULL) { mark_attr_deleted(new_xml, (const char *) old_xml->name, name, old_value); } else { xml_node_private_t *nodepriv = new_attr->_private; int new_pos = pcmk__xml_position((xmlNode*) new_attr, pcmk__xf_skip); int old_pos = pcmk__xml_position((xmlNode*) old_attr, pcmk__xf_skip); const char *new_value = crm_element_value(new_xml, name); // This attribute isn't new pcmk__clear_xml_flags(nodepriv, pcmk__xf_created); if (strcmp(new_value, old_value) != 0) { mark_attr_changed(new_xml, (const char *) old_xml->name, name, old_value); } else if ((old_pos != new_pos) && !pcmk__tracking_xml_changes(new_xml, TRUE)) { mark_attr_moved(new_xml, (const char *) old_xml->name, old_attr, new_attr, old_pos, new_pos); } } } } /*! * \internal * \brief Check all attributes in new XML for creation * * For each of a given XML element's attributes marked as newly created, accept * (and mark as dirty) or reject the creation according to ACLs. * * \param[in,out] new_xml XML to check */ static void mark_created_attrs(xmlNode *new_xml) { xmlAttr *attr_iter = pcmk__xe_first_attr(new_xml); while (attr_iter != NULL) { xmlAttr *new_attr = attr_iter; xml_node_private_t *nodepriv = attr_iter->_private; attr_iter = attr_iter->next; if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { const char *attr_name = (const char *) new_attr->name; crm_trace("Created new attribute %s=%s in %s", attr_name, pcmk__xml_attr_value(new_attr), new_xml->name); /* Check ACLs (we can't use the remove-then-create trick because it * would modify the attribute position). */ if (pcmk__check_acl(new_xml, attr_name, pcmk__xf_acl_write)) { pcmk__mark_xml_attr_dirty(new_attr); } else { // Creation was not allowed, so remove the attribute xmlUnsetProp(new_xml, new_attr->name); } } } } /*! * \internal * \brief Calculate differences in attributes between two XML nodes * * \param[in,out] old_xml Original XML to compare * \param[in,out] new_xml New XML to compare */ static void xml_diff_attrs(xmlNode *old_xml, xmlNode *new_xml) { set_attrs_flag(new_xml, pcmk__xf_created); // cleared later if not really new xml_diff_old_attrs(old_xml, new_xml); mark_created_attrs(new_xml); } /*! * \internal * \brief Add an XML child element to a node, marked as deleted * * When calculating XML changes, we need to know when a child element has been * deleted. Add the child back to the new XML, so that we can check the removal * against ACLs, and mark it as deleted for later removal after differences have * been calculated. * * \param[in,out] old_child Child element from original XML * \param[in,out] new_parent New XML to add marked copy to */ static void mark_child_deleted(xmlNode *old_child, xmlNode *new_parent) { // Re-create the child element so we can check ACLs xmlNode *candidate = pcmk__xml_copy(new_parent, old_child); // Clear flags on new child and its children reset_xml_node_flags(candidate); // Check whether ACLs allow the deletion pcmk__apply_acl(xmlDocGetRootElement(candidate->doc)); // Remove the child again (which will track it in document's deleted_objs) free_xml_with_position(candidate, pcmk__xml_position(old_child, pcmk__xf_skip)); if (pcmk__xml_match(new_parent, old_child, true) == NULL) { pcmk__set_xml_flags((xml_node_private_t *) (old_child->_private), pcmk__xf_skip); } } static void mark_child_moved(xmlNode *old_child, xmlNode *new_parent, xmlNode *new_child, int p_old, int p_new) { xml_node_private_t *nodepriv = new_child->_private; crm_trace("Child element %s with id='%s' moved from position %d to %d under %s", new_child->name, pcmk__s(pcmk__xe_id(new_child), ""), p_old, p_new, new_parent->name); pcmk__mark_xml_node_dirty(new_parent); pcmk__set_xml_flags(nodepriv, pcmk__xf_moved); if (p_old > p_new) { nodepriv = old_child->_private; } else { nodepriv = new_child->_private; } pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); } // Given original and new XML, mark new XML portions that have changed static void mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top) { xmlNode *cIter = NULL; xml_node_private_t *nodepriv = NULL; CRM_CHECK(new_xml != NULL, return); if (old_xml == NULL) { pcmk__mark_xml_created(new_xml); pcmk__apply_creation_acl(new_xml, check_top); return; } nodepriv = new_xml->_private; CRM_CHECK(nodepriv != NULL, return); if(nodepriv->flags & pcmk__xf_processed) { /* Avoid re-comparing nodes */ return; } pcmk__set_xml_flags(nodepriv, pcmk__xf_processed); xml_diff_attrs(old_xml, new_xml); // Check for differences in the original children for (cIter = pcmk__xml_first_child(old_xml); cIter != NULL; ) { xmlNode *old_child = cIter; xmlNode *new_child = pcmk__xml_match(new_xml, cIter, true); cIter = pcmk__xml_next(cIter); if(new_child) { mark_xml_changes(old_child, new_child, TRUE); } else { mark_child_deleted(old_child, new_xml); } } // Check for moved or created children for (cIter = pcmk__xml_first_child(new_xml); cIter != NULL; ) { xmlNode *new_child = cIter; xmlNode *old_child = pcmk__xml_match(old_xml, cIter, true); cIter = pcmk__xml_next(cIter); if(old_child == NULL) { // This is a newly created child nodepriv = new_child->_private; pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); mark_xml_changes(old_child, new_child, TRUE); } else { /* Check for movement, we already checked for differences */ int p_new = pcmk__xml_position(new_child, pcmk__xf_skip); int p_old = pcmk__xml_position(old_child, pcmk__xf_skip); if(p_old != p_new) { mark_child_moved(old_child, new_xml, new_child, p_old, p_new); } } } } void xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml) { pcmk__set_xml_doc_flag(new_xml, pcmk__xf_lazy); xml_calculate_changes(old_xml, new_xml); } // Called functions may set the \p pcmk__xf_skip flag on parts of \p old_xml void xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml) { CRM_CHECK((old_xml != NULL) && (new_xml != NULL) && pcmk__xe_is(old_xml, (const char *) new_xml->name) && pcmk__str_eq(pcmk__xe_id(old_xml), pcmk__xe_id(new_xml), pcmk__str_none), return); if(xml_tracking_changes(new_xml) == FALSE) { xml_track_changes(new_xml, NULL, NULL, FALSE); } mark_xml_changes(old_xml, new_xml, FALSE); } /*! * \internal * \brief Find a comment with matching content in specified XML * * \param[in] root XML to search * \param[in] search_comment Comment whose content should be searched for * \param[in] exact If true, comment must also be at same position */ xmlNode * pcmk__xc_match(const xmlNode *root, const xmlNode *search_comment, bool exact) { xmlNode *a_child = NULL; int search_offset = pcmk__xml_position(search_comment, pcmk__xf_skip); CRM_CHECK(search_comment->type == XML_COMMENT_NODE, return NULL); for (a_child = pcmk__xml_first_child(root); a_child != NULL; a_child = pcmk__xml_next(a_child)) { if (exact) { int offset = pcmk__xml_position(a_child, pcmk__xf_skip); xml_node_private_t *nodepriv = a_child->_private; if (offset < search_offset) { continue; } else if (offset > search_offset) { return NULL; } if (pcmk_is_set(nodepriv->flags, pcmk__xf_skip)) { continue; } } if (a_child->type == XML_COMMENT_NODE && pcmk__str_eq((const char *)a_child->content, (const char *)search_comment->content, pcmk__str_casei)) { return a_child; } else if (exact) { return NULL; } } return NULL; } /*! * \internal * \brief Make one XML comment match another (in content) * * \param[in,out] parent If \p target is NULL and this is not, add or update * comment child of this XML node that matches \p update * \param[in,out] target If not NULL, update this XML comment node * \param[in] update Make comment content match this (must not be NULL) * * \note At least one of \parent and \target must be non-NULL */ void pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update) { CRM_CHECK(update != NULL, return); CRM_CHECK(update->type == XML_COMMENT_NODE, return); if (target == NULL) { target = pcmk__xc_match(parent, update, false); } if (target == NULL) { pcmk__xml_copy(parent, update); } else if (!pcmk__str_eq((const char *)target->content, (const char *)update->content, pcmk__str_casei)) { xmlFree(target->content); target->content = xmlStrdup(update->content); } } /*! * \internal * \brief Make one XML tree match another (in children and attributes) * * \param[in,out] parent If \p target is NULL and this is not, add or update * child of this XML node that matches \p update * \param[in,out] target If not NULL, update this XML * \param[in] update Make the desired XML match this (must not be NULL) * \param[in] as_diff If false, expand "++" when making attributes match * * \note At least one of \p parent and \p target must be non-NULL */ void pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update, bool as_diff) { xmlNode *a_child = NULL; const char *object_name = NULL, *object_href = NULL, *object_href_val = NULL; #if XML_PARSER_DEBUG crm_log_xml_trace(update, "update:"); crm_log_xml_trace(target, "target:"); #endif CRM_CHECK(update != NULL, return); if (update->type == XML_COMMENT_NODE) { pcmk__xc_update(parent, target, update); return; } object_name = (const char *) update->name; object_href_val = pcmk__xe_id(update); if (object_href_val != NULL) { object_href = PCMK_XA_ID; } else { object_href_val = crm_element_value(update, PCMK_XA_ID_REF); object_href = (object_href_val == NULL)? NULL : PCMK_XA_ID_REF; } CRM_CHECK(object_name != NULL, return); CRM_CHECK(target != NULL || parent != NULL, return); if (target == NULL) { target = pcmk__xe_match(parent, object_name, object_href, object_href_val); } if (target == NULL) { target = create_xml_node(parent, object_name); CRM_CHECK(target != NULL, return); #if XML_PARSER_DEBUG crm_trace("Added <%s%s%s%s%s/>", pcmk__s(object_name, ""), object_href ? " " : "", object_href ? object_href : "", object_href ? "=" : "", object_href ? object_href_val : ""); } else { crm_trace("Found node <%s%s%s%s%s/> to update", pcmk__s(object_name, ""), object_href ? " " : "", object_href ? object_href : "", object_href ? "=" : "", object_href ? object_href_val : ""); #endif } CRM_CHECK(pcmk__xe_is(target, (const char *) update->name), return); if (as_diff == FALSE) { /* So that expand_plus_plus() gets called */ copy_in_properties(target, update); } else { /* No need for expand_plus_plus(), just raw speed */ for (xmlAttrPtr a = pcmk__xe_first_attr(update); a != NULL; a = a->next) { const char *p_value = pcmk__xml_attr_value(a); /* Remove it first so the ordering of the update is preserved */ xmlUnsetProp(target, a->name); xmlSetProp(target, a->name, (pcmkXmlStr) p_value); } } for (a_child = pcmk__xml_first_child(update); a_child != NULL; a_child = pcmk__xml_next(a_child)) { #if XML_PARSER_DEBUG crm_trace("Updating child <%s%s%s%s%s/>", pcmk__s(object_name, ""), object_href ? " " : "", object_href ? object_href : "", object_href ? "=" : "", object_href ? object_href_val : ""); #endif pcmk__xml_update(target, NULL, a_child, as_diff); } #if XML_PARSER_DEBUG crm_trace("Finished with <%s%s%s%s%s/>", pcmk__s(object_name, ""), object_href ? " " : "", object_href ? object_href : "", object_href ? "=" : "", object_href ? object_href_val : ""); #endif } gboolean update_xml_child(xmlNode * child, xmlNode * to_update) { gboolean can_update = TRUE; xmlNode *child_of_child = NULL; CRM_CHECK(child != NULL, return FALSE); CRM_CHECK(to_update != NULL, return FALSE); if (!pcmk__xe_is(to_update, (const char *) child->name)) { can_update = FALSE; } else if (!pcmk__str_eq(pcmk__xe_id(to_update), pcmk__xe_id(child), pcmk__str_none)) { can_update = FALSE; } else if (can_update) { #if XML_PARSER_DEBUG crm_log_xml_trace(child, "Update match found..."); #endif pcmk__xml_update(NULL, child, to_update, false); } for (child_of_child = pcmk__xml_first_child(child); child_of_child != NULL; child_of_child = pcmk__xml_next(child_of_child)) { /* only update the first one */ if (can_update) { break; } can_update = update_xml_child(child_of_child, to_update); } return can_update; } int find_xml_children(xmlNode ** children, xmlNode * root, const char *tag, const char *field, const char *value, gboolean search_matches) { int match_found = 0; CRM_CHECK(root != NULL, return FALSE); CRM_CHECK(children != NULL, return FALSE); if ((tag != NULL) && !pcmk__xe_is(root, tag)) { } else if (value != NULL && !pcmk__str_eq(value, crm_element_value(root, field), pcmk__str_casei)) { } else { if (*children == NULL) { *children = create_xml_node(NULL, __func__); } pcmk__xml_copy(*children, root); match_found = 1; } if (search_matches || match_found == 0) { xmlNode *child = NULL; for (child = pcmk__xml_first_child(root); child != NULL; child = pcmk__xml_next(child)) { match_found += find_xml_children(children, child, tag, field, value, search_matches); } } return match_found; } gboolean replace_xml_child(xmlNode * parent, xmlNode * child, xmlNode * update, gboolean delete_only) { gboolean can_delete = FALSE; xmlNode *child_of_child = NULL; const char *up_id = NULL; const char *child_id = NULL; const char *right_val = NULL; CRM_CHECK(child != NULL, return FALSE); CRM_CHECK(update != NULL, return FALSE); up_id = pcmk__xe_id(update); child_id = pcmk__xe_id(child); if (up_id == NULL || (child_id && strcmp(child_id, up_id) == 0)) { can_delete = TRUE; } if (!pcmk__xe_is(update, (const char *) child->name)) { can_delete = FALSE; } if (can_delete && delete_only) { for (xmlAttrPtr a = pcmk__xe_first_attr(update); a != NULL; a = a->next) { const char *p_name = (const char *) a->name; const char *p_value = pcmk__xml_attr_value(a); right_val = crm_element_value(child, p_name); if (!pcmk__str_eq(p_value, right_val, pcmk__str_casei)) { can_delete = FALSE; } } } if (can_delete && parent != NULL) { crm_log_xml_trace(child, "Delete match found..."); if (delete_only || update == NULL) { free_xml(child); } else { xmlNode *old = child; xmlNode *new = xmlCopyNode(update, 1); CRM_ASSERT(new != NULL); // May be unnecessary but avoids slight changes to some test outputs reset_xml_node_flags(new); old = xmlReplaceNode(old, new); if (xml_tracking_changes(new)) { // Replaced sections may have included relevant ACLs pcmk__apply_acl(new); } xml_calculate_changes(old, new); xmlFreeNode(old); } return TRUE; } else if (can_delete) { crm_log_xml_debug(child, "Cannot delete the search root"); can_delete = FALSE; } child_of_child = pcmk__xml_first_child(child); while (child_of_child) { xmlNode *next = pcmk__xml_next(child_of_child); can_delete = replace_xml_child(child, child_of_child, update, delete_only); /* only delete the first one */ if (can_delete) { child_of_child = NULL; } else { child_of_child = next; } } return can_delete; } xmlNode * sorted_xml(xmlNode *input, xmlNode *parent, gboolean recursive) { xmlNode *child = NULL; GSList *nvpairs = NULL; xmlNode *result = NULL; CRM_CHECK(input != NULL, return NULL); result = create_xml_node(parent, (const char *) input->name); nvpairs = pcmk_xml_attrs2nvpairs(input); nvpairs = pcmk_sort_nvpairs(nvpairs); pcmk_nvpairs2xml_attrs(nvpairs, result); pcmk_free_nvpairs(nvpairs); for (child = pcmk__xml_first_child(input); child != NULL; child = pcmk__xml_next(child)) { if (recursive) { sorted_xml(child, result, recursive); } else { pcmk__xml_copy(result, child); } } return result; } xmlNode * first_named_child(const xmlNode *parent, const char *name) { xmlNode *match = NULL; for (match = pcmk__xe_first_child(parent); match != NULL; match = pcmk__xe_next(match)) { /* * name == NULL gives first child regardless of name; this is * semantically incorrect in this function, but may be necessary * due to prior use of xml_child_iter_filter */ if ((name == NULL) || pcmk__xe_is(match, name)) { return match; } } return NULL; } /*! * \brief Get next instance of same XML tag * * \param[in] sibling XML tag to start from * * \return Next sibling XML tag with same name */ xmlNode * crm_next_same_xml(const xmlNode *sibling) { xmlNode *match = pcmk__xe_next(sibling); while (match != NULL) { if (pcmk__xe_is(match, (const char *) sibling->name)) { return match; } match = pcmk__xe_next(match); } return NULL; } void crm_xml_init(void) { static bool init = true; if(init) { init = false; /* The default allocator XML_BUFFER_ALLOC_EXACT does far too many * pcmk__realloc()s and it can take upwards of 18 seconds (yes, seconds) * to dump a 28kb tree which XML_BUFFER_ALLOC_DOUBLEIT can do in * less than 1 second. */ xmlSetBufferAllocationScheme(XML_BUFFER_ALLOC_DOUBLEIT); /* Populate and free the _private field when nodes are created and destroyed */ xmlDeregisterNodeDefault(free_private_data); xmlRegisterNodeDefault(new_private_data); crm_schema_init(); } } void crm_xml_cleanup(void) { crm_schema_cleanup(); xmlCleanupParser(); } #define XPATH_MAX 512 xmlNode * expand_idref(xmlNode * input, xmlNode * top) { char *xpath = NULL; const char *ref = NULL; xmlNode *result = NULL; if (input == NULL) { return NULL; } ref = crm_element_value(input, PCMK_XA_ID_REF); if (ref == NULL) { return input; } if (top == NULL) { top = input; } xpath = crm_strdup_printf("//%s[@" PCMK_XA_ID "='%s']", input->name, ref); result = get_xpath_object(xpath, top, LOG_DEBUG); if (result == NULL) { // Not possible with schema validation enabled pcmk__config_err("Ignoring invalid %s configuration: " PCMK_XA_ID_REF " '%s' does not reference " "a valid object " CRM_XS " xpath=%s", input->name, ref, xpath); } free(xpath); return result; } char * pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns) { static const char *base = NULL; char *ret = NULL; if (base == NULL) { base = pcmk__env_option(PCMK__ENV_SCHEMA_DIRECTORY); } if (pcmk__str_empty(base)) { base = CRM_SCHEMA_DIRECTORY; } switch (ns) { case pcmk__xml_artefact_ns_legacy_rng: case pcmk__xml_artefact_ns_legacy_xslt: ret = strdup(base); break; case pcmk__xml_artefact_ns_base_rng: case pcmk__xml_artefact_ns_base_xslt: ret = crm_strdup_printf("%s/base", base); break; default: crm_err("XML artefact family specified as %u not recognized", ns); } return ret; } static char * find_artefact(enum pcmk__xml_artefact_ns ns, const char *path, const char *filespec) { char *ret = NULL; switch (ns) { case pcmk__xml_artefact_ns_legacy_rng: case pcmk__xml_artefact_ns_base_rng: if (pcmk__ends_with(filespec, ".rng")) { ret = crm_strdup_printf("%s/%s", path, filespec); } else { ret = crm_strdup_printf("%s/%s.rng", path, filespec); } break; case pcmk__xml_artefact_ns_legacy_xslt: case pcmk__xml_artefact_ns_base_xslt: if (pcmk__ends_with(filespec, ".xsl")) { ret = crm_strdup_printf("%s/%s", path, filespec); } else { ret = crm_strdup_printf("%s/%s.xsl", path, filespec); } break; default: crm_err("XML artefact family specified as %u not recognized", ns); } return ret; } char * pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec) { struct stat sb; char *base = pcmk__xml_artefact_root(ns); char *ret = NULL; ret = find_artefact(ns, base, filespec); free(base); if (stat(ret, &sb) != 0 || !S_ISREG(sb.st_mode)) { const char *remote_schema_dir = pcmk__remote_schema_dir(); ret = find_artefact(ns, remote_schema_dir, filespec); } return ret; } void pcmk__xe_set_propv(xmlNodePtr node, va_list pairs) { while (true) { const char *name, *value; name = va_arg(pairs, const char *); if (name == NULL) { return; } value = va_arg(pairs, const char *); if (value != NULL) { crm_xml_add(node, name, value); } } } void pcmk__xe_set_props(xmlNodePtr node, ...) { va_list pairs; va_start(pairs, node); pcmk__xe_set_propv(node, pairs); va_end(pairs); } int pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name, int (*handler)(xmlNode *xml, void *userdata), void *userdata) { xmlNode *children = (xml? xml->children : NULL); CRM_ASSERT(handler != NULL); for (xmlNode *node = children; node != NULL; node = node->next) { if ((node->type == XML_ELEMENT_NODE) && ((child_element_name == NULL) || pcmk__xe_is(node, child_element_name))) { int rc = handler(node, userdata); if (rc != pcmk_rc_ok) { return rc; } } } return pcmk_rc_ok; } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include xmlNode * find_entity(xmlNode *parent, const char *node_name, const char *id) { return pcmk__xe_match(parent, node_name, ((id == NULL)? id : PCMK_XA_ID), id); } void crm_destroy_xml(gpointer data) { free_xml(data); } xmlDoc * getDocPtr(xmlNode *node) { xmlDoc *doc = NULL; CRM_CHECK(node != NULL, return NULL); doc = node->doc; if (doc == NULL) { doc = xmlNewDoc(PCMK__XML_VERSION); xmlDocSetRootElement(doc, node); } return doc; } xmlNode * add_node_copy(xmlNode *parent, xmlNode *src_node) { xmlNode *child = NULL; CRM_CHECK((parent != NULL) && (src_node != NULL), return NULL); child = xmlDocCopyNode(src_node, parent->doc, 1); if (child == NULL) { return NULL; } xmlAddChild(parent, child); pcmk__mark_xml_created(child); return child; } int add_node_nocopy(xmlNode *parent, const char *name, xmlNode *child) { add_node_copy(parent, child); free_xml(child); return 1; } gboolean xml_has_children(const xmlNode * xml_root) { if (xml_root != NULL && xml_root->children != NULL) { return TRUE; } return FALSE; } char * crm_xml_escape(const char *text) { size_t length = 0; char *copy = NULL; if (text == NULL) { return NULL; } length = strlen(text); copy = strdup(text); CRM_ASSERT(copy != NULL); for (size_t index = 0; index <= length; index++) { if(copy[index] & 0x80 && copy[index+1] & 0x80){ index++; continue; } switch (copy[index]) { case 0: // Sanity only; loop should stop at the last non-null byte break; case '<': copy = replace_text(copy, &index, &length, "<"); break; case '>': copy = replace_text(copy, &index, &length, ">"); break; case '"': copy = replace_text(copy, &index, &length, """); break; case '\'': copy = replace_text(copy, &index, &length, "'"); break; case '&': copy = replace_text(copy, &index, &length, "&"); break; case '\t': /* Might as well just expand to a few spaces... */ copy = replace_text(copy, &index, &length, " "); break; case '\n': copy = replace_text(copy, &index, &length, "\\n"); break; case '\r': copy = replace_text(copy, &index, &length, "\\r"); break; default: /* Check for and replace non-printing characters with their octal equivalent */ if(copy[index] < ' ' || copy[index] > '~') { char *replace = crm_strdup_printf("\\%.3o", copy[index]); copy = replace_text(copy, &index, &length, replace); free(replace); } } } return copy; } xmlNode * copy_xml(xmlNode *src) { xmlDoc *doc = xmlNewDoc(PCMK__XML_VERSION); xmlNode *copy = xmlDocCopyNode(src, doc, 1); CRM_ASSERT(copy != NULL); xmlDocSetRootElement(doc, copy); return copy; } // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/pacemaker/pcmk_simulate.c b/lib/pacemaker/pcmk_simulate.c index b750a51083..47f434a36e 100644 --- a/lib/pacemaker/pcmk_simulate.c +++ b/lib/pacemaker/pcmk_simulate.c @@ -1,1011 +1,1011 @@ /* * Copyright 2021-2024 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 "libpacemaker_private.h" static pcmk__output_t *out = NULL; static cib_t *fake_cib = NULL; static GList *fake_resource_list = NULL; static const GList *fake_op_fail_list = NULL; static void set_effective_date(pcmk_scheduler_t *scheduler, bool print_original, const char *use_date); /*! * \internal * \brief Create an action name for use in a dot graph * * \param[in] action Action to create name for * \param[in] verbose If true, add action ID to name * * \return Newly allocated string with action name * \note It is the caller's responsibility to free the result. */ static char * create_action_name(const pcmk_action_t *action, bool verbose) { char *action_name = NULL; const char *prefix = ""; const char *action_host = NULL; const char *clone_name = NULL; const char *task = action->task; if (action->node != NULL) { action_host = action->node->details->uname; } else if (!pcmk_is_set(action->flags, pcmk_action_pseudo)) { action_host = ""; } if (pcmk__str_eq(action->task, PCMK_ACTION_CANCEL, pcmk__str_none)) { prefix = "Cancel "; task = action->cancel_task; } if (action->rsc != NULL) { clone_name = action->rsc->clone_name; } if (clone_name != NULL) { char *key = NULL; guint interval_ms = 0; if (pcmk__guint_from_hash(action->meta, PCMK_META_INTERVAL, 0, &interval_ms) != pcmk_rc_ok) { interval_ms = 0; } if (pcmk__strcase_any_of(action->task, PCMK_ACTION_NOTIFY, PCMK_ACTION_NOTIFIED, NULL)) { const char *n_type = g_hash_table_lookup(action->meta, "notify_key_type"); const char *n_task = g_hash_table_lookup(action->meta, "notify_key_operation"); CRM_ASSERT(n_type != NULL); CRM_ASSERT(n_task != NULL); key = pcmk__notify_key(clone_name, n_type, n_task); } else { key = pcmk__op_key(clone_name, task, interval_ms); } if (action_host != NULL) { action_name = crm_strdup_printf("%s%s %s", prefix, key, action_host); } else { action_name = crm_strdup_printf("%s%s", prefix, key); } free(key); } else if (pcmk__str_eq(action->task, PCMK_ACTION_STONITH, pcmk__str_none)) { const char *op = g_hash_table_lookup(action->meta, PCMK__META_STONITH_ACTION); action_name = crm_strdup_printf("%s%s '%s' %s", prefix, action->task, op, action_host); } else if (action->rsc && action_host) { action_name = crm_strdup_printf("%s%s %s", prefix, action->uuid, action_host); } else if (action_host) { action_name = crm_strdup_printf("%s%s %s", prefix, action->task, action_host); } else { action_name = crm_strdup_printf("%s", action->uuid); } if (verbose) { char *with_id = crm_strdup_printf("%s (%d)", action_name, action->id); free(action_name); action_name = with_id; } return action_name; } /*! * \internal * \brief Display the status of a cluster * * \param[in,out] scheduler Scheduler data * \param[in] show_opts How to modify display (as pcmk_show_opt_e flags) * \param[in] section_opts Sections to display (as pcmk_section_e flags) * \param[in] title What to use as list title * \param[in] print_spacer Whether to display a spacer first */ static void print_cluster_status(pcmk_scheduler_t *scheduler, uint32_t show_opts, uint32_t section_opts, const char *title, bool print_spacer) { pcmk__output_t *out = scheduler->priv; GList *all = NULL; crm_exit_t stonith_rc = 0; enum pcmk_pacemakerd_state state = pcmk_pacemakerd_state_invalid; section_opts |= pcmk_section_nodes | pcmk_section_resources; show_opts |= pcmk_show_inactive_rscs | pcmk_show_failed_detail; all = g_list_prepend(all, (gpointer) "*"); PCMK__OUTPUT_SPACER_IF(out, print_spacer); out->begin_list(out, NULL, NULL, "%s", title); out->message(out, "cluster-status", scheduler, state, stonith_rc, NULL, pcmk__fence_history_none, section_opts, show_opts, NULL, all, all); out->end_list(out); g_list_free(all); } /*! * \internal * \brief Display a summary of all actions scheduled in a transition * * \param[in,out] scheduler Scheduler data (fully scheduled) * \param[in] print_spacer Whether to display a spacer first */ static void print_transition_summary(pcmk_scheduler_t *scheduler, bool print_spacer) { pcmk__output_t *out = scheduler->priv; PCMK__OUTPUT_SPACER_IF(out, print_spacer); out->begin_list(out, NULL, NULL, "Transition Summary"); pcmk__output_actions(scheduler); out->end_list(out); } /*! * \internal * \brief Reset scheduler input, output, date, and flags * * \param[in,out] scheduler Scheduler data * \param[in] input What to set as cluster input * \param[in] out What to set as cluster output object * \param[in] use_date What to set as cluster's current timestamp * \param[in] flags Group of enum pcmk_scheduler_flags to set */ static void reset(pcmk_scheduler_t *scheduler, xmlNodePtr input, pcmk__output_t *out, const char *use_date, unsigned int flags) { scheduler->input = input; scheduler->priv = out; set_effective_date(scheduler, true, use_date); if (pcmk_is_set(flags, pcmk_sim_sanitized)) { pcmk__set_scheduler_flags(scheduler, pcmk_sched_sanitized); } if (pcmk_is_set(flags, pcmk_sim_show_scores)) { pcmk__set_scheduler_flags(scheduler, pcmk_sched_output_scores); } if (pcmk_is_set(flags, pcmk_sim_show_utilization)) { pcmk__set_scheduler_flags(scheduler, pcmk_sched_show_utilization); } } /*! * \brief Write out a file in dot(1) format describing the actions that will * be taken by the scheduler in response to an input CIB file. * * \param[in,out] scheduler Scheduler data * \param[in] dot_file The filename to write * \param[in] all_actions Write all actions, even those that are optional * or are on unmanaged resources * \param[in] verbose Add extra information, such as action IDs, to the * output * * \return Standard Pacemaker return code */ static int write_sim_dotfile(pcmk_scheduler_t *scheduler, const char *dot_file, bool all_actions, bool verbose) { GList *iter = NULL; FILE *dot_strm = fopen(dot_file, "w"); if (dot_strm == NULL) { return errno; } fprintf(dot_strm, " digraph \"g\" {\n"); for (iter = scheduler->actions; iter != NULL; iter = iter->next) { pcmk_action_t *action = (pcmk_action_t *) iter->data; const char *style = "dashed"; const char *font = "black"; const char *color = "black"; char *action_name = create_action_name(action, verbose); if (pcmk_is_set(action->flags, pcmk_action_pseudo)) { font = "orange"; } if (pcmk_is_set(action->flags, pcmk_action_added_to_graph)) { style = PCMK__VALUE_BOLD; color = "green"; } else if ((action->rsc != NULL) && !pcmk_is_set(action->rsc->flags, pcmk_rsc_managed)) { color = "red"; font = "purple"; if (!all_actions) { goto do_not_write; } } else if (pcmk_is_set(action->flags, pcmk_action_optional)) { color = "blue"; if (!all_actions) { goto do_not_write; } } else { color = "red"; CRM_LOG_ASSERT(!pcmk_is_set(action->flags, pcmk_action_runnable)); } pcmk__set_action_flags(action, pcmk_action_added_to_graph); fprintf(dot_strm, "\"%s\" [ style=%s color=\"%s\" fontcolor=\"%s\"]\n", action_name, style, color, font); do_not_write: free(action_name); } for (iter = scheduler->actions; iter != NULL; iter = iter->next) { pcmk_action_t *action = (pcmk_action_t *) iter->data; for (GList *before_iter = action->actions_before; before_iter != NULL; before_iter = before_iter->next) { pcmk__related_action_t *before = before_iter->data; char *before_name = NULL; char *after_name = NULL; const char *style = "dashed"; bool optional = true; if (before->state == pe_link_dumped) { optional = false; style = PCMK__VALUE_BOLD; } else if ((uint32_t) before->type == pcmk__ar_none) { continue; } else if (pcmk_is_set(before->action->flags, pcmk_action_added_to_graph) && pcmk_is_set(action->flags, pcmk_action_added_to_graph) && (uint32_t) before->type != pcmk__ar_if_on_same_node_or_target) { optional = false; } if (all_actions || !optional) { before_name = create_action_name(before->action, verbose); after_name = create_action_name(action, verbose); fprintf(dot_strm, "\"%s\" -> \"%s\" [ style = %s]\n", before_name, after_name, style); free(before_name); free(after_name); } } } fprintf(dot_strm, "}\n"); fflush(dot_strm); fclose(dot_strm); return pcmk_rc_ok; } /*! * \brief Profile the configuration updates and scheduler actions in a single * CIB file, printing the profiling timings. * * \note \p scheduler->priv must have been set to a valid \p pcmk__output_t * object before this function is called. * * \param[in] xml_file The CIB file to profile * \param[in] repeat Number of times to run * \param[in,out] scheduler Scheduler data * \param[in] use_date The date to set the cluster's time to (may be NULL) */ static void profile_file(const char *xml_file, long long repeat, pcmk_scheduler_t *scheduler, const char *use_date) { pcmk__output_t *out = scheduler->priv; xmlNode *cib_object = NULL; clock_t start = 0; clock_t end; unsigned long long scheduler_flags = pcmk_sched_no_compat; CRM_ASSERT(out != NULL); - cib_object = filename2xml(xml_file); + cib_object = pcmk__xml_read(xml_file); start = clock(); if (pcmk_find_cib_element(cib_object, PCMK_XE_STATUS) == NULL) { create_xml_node(cib_object, PCMK_XE_STATUS); } if (cli_config_update(&cib_object, NULL, FALSE) == FALSE) { free_xml(cib_object); return; } if (validate_xml(cib_object, NULL, FALSE) != TRUE) { free_xml(cib_object); return; } if (pcmk_is_set(scheduler->flags, pcmk_sched_output_scores)) { scheduler_flags |= pcmk_sched_output_scores; } if (pcmk_is_set(scheduler->flags, pcmk_sched_show_utilization)) { scheduler_flags |= pcmk_sched_show_utilization; } for (int i = 0; i < repeat; ++i) { xmlNode *input = cib_object; if (repeat > 1) { input = pcmk__xml_copy(NULL, cib_object); } scheduler->input = input; set_effective_date(scheduler, false, use_date); pcmk__schedule_actions(input, scheduler_flags, scheduler); pe_reset_working_set(scheduler); } end = clock(); out->message(out, "profile", xml_file, start, end); } void pcmk__profile_dir(const char *dir, long long repeat, pcmk_scheduler_t *scheduler, const char *use_date) { pcmk__output_t *out = scheduler->priv; struct dirent **namelist; int file_num = scandir(dir, &namelist, 0, alphasort); CRM_ASSERT(out != NULL); if (file_num > 0) { struct stat prop; char buffer[FILENAME_MAX]; out->begin_list(out, NULL, NULL, "Timings"); while (file_num--) { if ('.' == namelist[file_num]->d_name[0]) { free(namelist[file_num]); continue; } else if (!pcmk__ends_with_ext(namelist[file_num]->d_name, ".xml")) { free(namelist[file_num]); continue; } snprintf(buffer, sizeof(buffer), "%s/%s", dir, namelist[file_num]->d_name); if (stat(buffer, &prop) == 0 && S_ISREG(prop.st_mode)) { profile_file(buffer, repeat, scheduler, use_date); } free(namelist[file_num]); } free(namelist); out->end_list(out); } } /*! * \brief Set the date of the cluster, either to the value given by * \p use_date, or to the \c PCMK_XA_EXECUTION_DATE value in the CIB. * * \note \p scheduler->priv must have been set to a valid \p pcmk__output_t * object before this function is called. * * \param[in,out] scheduler Scheduler data * \param[in] print_original If \p true, the \c PCMK_XA_EXECUTION_DATE * should also be printed * \param[in] use_date The date to set the cluster's time to * (may be NULL) */ static void set_effective_date(pcmk_scheduler_t *scheduler, bool print_original, const char *use_date) { pcmk__output_t *out = scheduler->priv; time_t original_date = 0; CRM_ASSERT(out != NULL); crm_element_value_epoch(scheduler->input, PCMK_XA_EXECUTION_DATE, &original_date); if (use_date) { scheduler->now = crm_time_new(use_date); out->info(out, "Setting effective cluster time: %s", use_date); crm_time_log(LOG_NOTICE, "Pretending 'now' is", scheduler->now, crm_time_log_date | crm_time_log_timeofday); } else if (original_date != 0) { scheduler->now = pcmk__copy_timet(original_date); if (print_original) { char *when = crm_time_as_string(scheduler->now, crm_time_log_date|crm_time_log_timeofday); out->info(out, "Using the original execution date of: %s", when); free(when); } } } /*! * \internal * \brief Simulate successfully executing a pseudo-action in a graph * * \param[in,out] graph Graph to update with pseudo-action result * \param[in,out] action Pseudo-action to simulate executing * * \return Standard Pacemaker return code */ static int simulate_pseudo_action(pcmk__graph_t *graph, pcmk__graph_action_t *action) { const char *node = crm_element_value(action->xml, PCMK__META_ON_NODE); const char *task = crm_element_value(action->xml, PCMK__XA_OPERATION_KEY); pcmk__set_graph_action_flags(action, pcmk__graph_action_confirmed); out->message(out, "inject-pseudo-action", node, task); pcmk__update_graph(graph, action); return pcmk_rc_ok; } /*! * \internal * \brief Simulate executing a resource action in a graph * * \param[in,out] graph Graph to update with resource action result * \param[in,out] action Resource action to simulate executing * * \return Standard Pacemaker return code */ static int simulate_resource_action(pcmk__graph_t *graph, pcmk__graph_action_t *action) { int rc; lrmd_event_data_t *op = NULL; int target_outcome = PCMK_OCF_OK; const char *rtype = NULL; const char *rclass = NULL; const char *resource = NULL; const char *rprovider = NULL; const char *resource_config_name = NULL; const char *operation = crm_element_value(action->xml, PCMK_XA_OPERATION); const char *target_rc_s = crm_meta_value(action->params, PCMK__META_OP_TARGET_RC); xmlNode *cib_node = NULL; xmlNode *cib_resource = NULL; xmlNode *action_rsc = first_named_child(action->xml, PCMK_XE_PRIMITIVE); char *node = crm_element_value_copy(action->xml, PCMK__META_ON_NODE); char *uuid = NULL; const char *router_node = crm_element_value(action->xml, PCMK__XA_ROUTER_NODE); // Certain actions don't need to be displayed or history entries if (pcmk__str_eq(operation, CRM_OP_REPROBE, pcmk__str_none)) { crm_debug("No history injection for %s op on %s", operation, node); goto done; // Confirm action and update graph } if (action_rsc == NULL) { // Shouldn't be possible crm_log_xml_err(action->xml, "Bad"); free(node); return EPROTO; } /* A resource might be known by different names in the configuration and in * the action (for example, a clone instance). Grab the configuration name * (which is preferred when writing history), and if necessary, the instance * name. */ resource_config_name = crm_element_value(action_rsc, PCMK_XA_ID); if (resource_config_name == NULL) { // Shouldn't be possible crm_log_xml_err(action->xml, "No ID"); free(node); return EPROTO; } resource = resource_config_name; if (pe_find_resource(fake_resource_list, resource) == NULL) { const char *longname = crm_element_value(action_rsc, PCMK__XA_LONG_ID); if ((longname != NULL) && (pe_find_resource(fake_resource_list, longname) != NULL)) { resource = longname; } } // Certain actions need to be displayed but don't need history entries if (pcmk__strcase_any_of(operation, PCMK_ACTION_DELETE, PCMK_ACTION_META_DATA, NULL)) { out->message(out, "inject-rsc-action", resource, operation, node, (guint) 0); goto done; // Confirm action and update graph } rclass = crm_element_value(action_rsc, PCMK_XA_CLASS); rtype = crm_element_value(action_rsc, PCMK_XA_TYPE); rprovider = crm_element_value(action_rsc, PCMK_XA_PROVIDER); pcmk__scan_min_int(target_rc_s, &target_outcome, 0); CRM_ASSERT(fake_cib->cmds->query(fake_cib, NULL, NULL, cib_sync_call|cib_scope_local) == pcmk_ok); // Ensure the action node is in the CIB uuid = crm_element_value_copy(action->xml, PCMK__META_ON_NODE_UUID); cib_node = pcmk__inject_node(fake_cib, node, ((router_node == NULL)? uuid: node)); free(uuid); CRM_ASSERT(cib_node != NULL); // Add a history entry for the action cib_resource = pcmk__inject_resource_history(out, cib_node, resource, resource_config_name, rclass, rtype, rprovider); if (cib_resource == NULL) { crm_err("Could not simulate action %d history for resource %s", action->id, resource); free(node); free_xml(cib_node); return EINVAL; } // Simulate and display an executor event for the action result op = pcmk__event_from_graph_action(cib_resource, action, PCMK_EXEC_DONE, target_outcome, "User-injected result"); out->message(out, "inject-rsc-action", resource, op->op_type, node, op->interval_ms); // Check whether action is in a list of desired simulated failures for (const GList *iter = fake_op_fail_list; iter != NULL; iter = iter->next) { const char *spec = (const char *) iter->data; char *key = NULL; const char *match_name = NULL; // Allow user to specify anonymous clone with or without instance number key = crm_strdup_printf(PCMK__OP_FMT "@%s=", resource, op->op_type, op->interval_ms, node); if (strncasecmp(key, spec, strlen(key)) == 0) { match_name = resource; } free(key); // If not found, try the resource's name in the configuration if ((match_name == NULL) && (strcmp(resource, resource_config_name) != 0)) { key = crm_strdup_printf(PCMK__OP_FMT "@%s=", resource_config_name, op->op_type, op->interval_ms, node); if (strncasecmp(key, spec, strlen(key)) == 0) { match_name = resource_config_name; } free(key); } if (match_name == NULL) { continue; // This failed action entry doesn't match } // ${match_name}_${task}_${interval_in_ms}@${node}=${rc} rc = sscanf(spec, "%*[^=]=%d", (int *) &op->rc); if (rc != 1) { out->err(out, "Invalid failed operation '%s' " "(result code must be integer)", spec); continue; // Keep checking other list entries } out->info(out, "Pretending action %d failed with rc=%d", action->id, op->rc); pcmk__set_graph_action_flags(action, pcmk__graph_action_failed); graph->abort_priority = PCMK_SCORE_INFINITY; pcmk__inject_failcount(out, cib_node, match_name, op->op_type, op->interval_ms, op->rc); break; } pcmk__inject_action_result(cib_resource, op, target_outcome); lrmd_free_event(op); rc = fake_cib->cmds->modify(fake_cib, PCMK_XE_STATUS, cib_node, cib_sync_call|cib_scope_local); CRM_ASSERT(rc == pcmk_ok); done: free(node); free_xml(cib_node); pcmk__set_graph_action_flags(action, pcmk__graph_action_confirmed); pcmk__update_graph(graph, action); return pcmk_rc_ok; } /*! * \internal * \brief Simulate successfully executing a cluster action * * \param[in,out] graph Graph to update with action result * \param[in,out] action Cluster action to simulate * * \return Standard Pacemaker return code */ static int simulate_cluster_action(pcmk__graph_t *graph, pcmk__graph_action_t *action) { const char *node = crm_element_value(action->xml, PCMK__META_ON_NODE); const char *task = crm_element_value(action->xml, PCMK_XA_OPERATION); xmlNode *rsc = first_named_child(action->xml, PCMK_XE_PRIMITIVE); pcmk__set_graph_action_flags(action, pcmk__graph_action_confirmed); out->message(out, "inject-cluster-action", node, task, rsc); pcmk__update_graph(graph, action); return pcmk_rc_ok; } /*! * \internal * \brief Simulate successfully executing a fencing action * * \param[in,out] graph Graph to update with action result * \param[in,out] action Fencing action to simulate * * \return Standard Pacemaker return code */ static int simulate_fencing_action(pcmk__graph_t *graph, pcmk__graph_action_t *action) { const char *op = crm_meta_value(action->params, PCMK__META_STONITH_ACTION); char *target = crm_element_value_copy(action->xml, PCMK__META_ON_NODE); out->message(out, "inject-fencing-action", target, op); if (!pcmk__str_eq(op, PCMK_ACTION_ON, pcmk__str_casei)) { int rc = pcmk_ok; GString *xpath = g_string_sized_new(512); // Set node state to offline xmlNode *cib_node = pcmk__inject_node_state_change(fake_cib, target, false); CRM_ASSERT(cib_node != NULL); crm_xml_add(cib_node, PCMK_XA_CRM_DEBUG_ORIGIN, __func__); rc = fake_cib->cmds->replace(fake_cib, PCMK_XE_STATUS, cib_node, cib_sync_call|cib_scope_local); CRM_ASSERT(rc == pcmk_ok); // Simulate controller clearing node's resource history and attributes pcmk__g_strcat(xpath, "//" PCMK__XE_NODE_STATE "[@" PCMK_XA_UNAME "='", target, "']/" PCMK__XE_LRM, NULL); fake_cib->cmds->remove(fake_cib, (const char *) xpath->str, NULL, cib_xpath|cib_sync_call|cib_scope_local); g_string_truncate(xpath, 0); pcmk__g_strcat(xpath, "//" PCMK__XE_NODE_STATE "[@" PCMK_XA_UNAME "='", target, "']" "/" PCMK__XE_TRANSIENT_ATTRIBUTES, NULL); fake_cib->cmds->remove(fake_cib, (const char *) xpath->str, NULL, cib_xpath|cib_sync_call|cib_scope_local); free_xml(cib_node); g_string_free(xpath, TRUE); } pcmk__set_graph_action_flags(action, pcmk__graph_action_confirmed); pcmk__update_graph(graph, action); free(target); return pcmk_rc_ok; } enum pcmk__graph_status pcmk__simulate_transition(pcmk_scheduler_t *scheduler, cib_t *cib, const GList *op_fail_list) { pcmk__graph_t *transition = NULL; enum pcmk__graph_status graph_rc; pcmk__graph_functions_t simulation_fns = { simulate_pseudo_action, simulate_resource_action, simulate_cluster_action, simulate_fencing_action, }; out = scheduler->priv; fake_cib = cib; fake_op_fail_list = op_fail_list; if (!out->is_quiet(out)) { out->begin_list(out, NULL, NULL, "Executing Cluster Transition"); } pcmk__set_graph_functions(&simulation_fns); transition = pcmk__unpack_graph(scheduler->graph, crm_system_name); pcmk__log_graph(LOG_DEBUG, transition); fake_resource_list = scheduler->resources; do { graph_rc = pcmk__execute_graph(transition); } while (graph_rc == pcmk__graph_active); fake_resource_list = NULL; if (graph_rc != pcmk__graph_complete) { out->err(out, "Transition failed: %s", pcmk__graph_status2text(graph_rc)); pcmk__log_graph(LOG_ERR, transition); out->err(out, "An invalid transition was produced"); } pcmk__free_graph(transition); if (!out->is_quiet(out)) { // If not quiet, we'll need the resulting CIB for later display xmlNode *cib_object = NULL; int rc = fake_cib->cmds->query(fake_cib, NULL, &cib_object, cib_sync_call|cib_scope_local); CRM_ASSERT(rc == pcmk_ok); pe_reset_working_set(scheduler); scheduler->input = cib_object; out->end_list(out); } return graph_rc; } int pcmk__simulate(pcmk_scheduler_t *scheduler, pcmk__output_t *out, const pcmk_injections_t *injections, unsigned int flags, uint32_t section_opts, const char *use_date, const char *input_file, const char *graph_file, const char *dot_file) { int printed = pcmk_rc_no_output; int rc = pcmk_rc_ok; xmlNodePtr input = NULL; cib_t *cib = NULL; rc = cib__signon_query(out, &cib, &input); if (rc != pcmk_rc_ok) { goto simulate_done; } reset(scheduler, input, out, use_date, flags); cluster_status(scheduler); if ((cib->variant == cib_native) && pcmk_is_set(section_opts, pcmk_section_times)) { if (pcmk__our_nodename == NULL) { // Currently used only in the times section pcmk__query_node_name(out, 0, &pcmk__our_nodename, 0); } scheduler->localhost = pcmk__our_nodename; } if (!out->is_quiet(out)) { const bool show_pending = pcmk_is_set(flags, pcmk_sim_show_pending); if (pcmk_is_set(scheduler->flags, pcmk_sched_in_maintenance)) { printed = out->message(out, "maint-mode", scheduler->flags); } if (scheduler->disabled_resources || scheduler->blocked_resources) { PCMK__OUTPUT_SPACER_IF(out, printed == pcmk_rc_ok); printed = out->info(out, "%d of %d resource instances DISABLED and " "%d BLOCKED from further action due to failure", scheduler->disabled_resources, scheduler->ninstances, scheduler->blocked_resources); } /* Most formatted output headers use caps for each word, but this one * only has the first word capitalized for compatibility with pcs. */ print_cluster_status(scheduler, (show_pending? pcmk_show_pending : 0), section_opts, "Current cluster status", (printed == pcmk_rc_ok)); printed = pcmk_rc_ok; } // If the user requested any injections, handle them if ((injections->node_down != NULL) || (injections->node_fail != NULL) || (injections->node_up != NULL) || (injections->op_inject != NULL) || (injections->ticket_activate != NULL) || (injections->ticket_grant != NULL) || (injections->ticket_revoke != NULL) || (injections->ticket_standby != NULL) || (injections->watchdog != NULL)) { PCMK__OUTPUT_SPACER_IF(out, printed == pcmk_rc_ok); pcmk__inject_scheduler_input(scheduler, cib, injections); printed = pcmk_rc_ok; rc = cib->cmds->query(cib, NULL, &input, cib_sync_call); if (rc != pcmk_rc_ok) { rc = pcmk_legacy2rc(rc); goto simulate_done; } cleanup_calculations(scheduler); reset(scheduler, input, out, use_date, flags); cluster_status(scheduler); } if (input_file != NULL) { rc = write_xml_file(input, input_file, FALSE); if (rc < 0) { rc = pcmk_legacy2rc(rc); goto simulate_done; } } if (pcmk_any_flags_set(flags, pcmk_sim_process | pcmk_sim_simulate)) { pcmk__output_t *logger_out = NULL; unsigned long long scheduler_flags = pcmk_sched_no_compat; if (pcmk_is_set(scheduler->flags, pcmk_sched_output_scores)) { scheduler_flags |= pcmk_sched_output_scores; } if (pcmk_is_set(scheduler->flags, pcmk_sched_show_utilization)) { scheduler_flags |= pcmk_sched_show_utilization; } if (pcmk_all_flags_set(scheduler->flags, pcmk_sched_output_scores |pcmk_sched_show_utilization)) { PCMK__OUTPUT_SPACER_IF(out, printed == pcmk_rc_ok); out->begin_list(out, NULL, NULL, "Assignment Scores and Utilization Information"); printed = pcmk_rc_ok; } else if (pcmk_is_set(scheduler->flags, pcmk_sched_output_scores)) { PCMK__OUTPUT_SPACER_IF(out, printed == pcmk_rc_ok); out->begin_list(out, NULL, NULL, "Assignment Scores"); printed = pcmk_rc_ok; } else if (pcmk_is_set(scheduler->flags, pcmk_sched_show_utilization)) { PCMK__OUTPUT_SPACER_IF(out, printed == pcmk_rc_ok); out->begin_list(out, NULL, NULL, "Utilization Information"); printed = pcmk_rc_ok; } else { rc = pcmk__log_output_new(&logger_out); if (rc != pcmk_rc_ok) { goto simulate_done; } pe__register_messages(logger_out); pcmk__register_lib_messages(logger_out); scheduler->priv = logger_out; } pcmk__schedule_actions(input, scheduler_flags, scheduler); if (logger_out == NULL) { out->end_list(out); } else { logger_out->finish(logger_out, CRM_EX_OK, true, NULL); pcmk__output_free(logger_out); scheduler->priv = out; } input = NULL; /* Don't try and free it twice */ if (graph_file != NULL) { rc = write_xml_file(scheduler->graph, graph_file, FALSE); if (rc < 0) { rc = pcmk_rc_graph_error; goto simulate_done; } } if (dot_file != NULL) { rc = write_sim_dotfile(scheduler, dot_file, pcmk_is_set(flags, pcmk_sim_all_actions), pcmk_is_set(flags, pcmk_sim_verbose)); if (rc != pcmk_rc_ok) { rc = pcmk_rc_dot_error; goto simulate_done; } } if (!out->is_quiet(out)) { print_transition_summary(scheduler, printed == pcmk_rc_ok); } } rc = pcmk_rc_ok; if (!pcmk_is_set(flags, pcmk_sim_simulate)) { goto simulate_done; } PCMK__OUTPUT_SPACER_IF(out, printed == pcmk_rc_ok); if (pcmk__simulate_transition(scheduler, cib, injections->op_fail) != pcmk__graph_complete) { rc = pcmk_rc_invalid_transition; } if (out->is_quiet(out)) { goto simulate_done; } set_effective_date(scheduler, true, use_date); if (pcmk_is_set(flags, pcmk_sim_show_scores)) { pcmk__set_scheduler_flags(scheduler, pcmk_sched_output_scores); } if (pcmk_is_set(flags, pcmk_sim_show_utilization)) { pcmk__set_scheduler_flags(scheduler, pcmk_sched_show_utilization); } cluster_status(scheduler); print_cluster_status(scheduler, 0, section_opts, "Revised Cluster Status", true); simulate_done: cib__clean_up_connection(&cib); return rc; } int pcmk_simulate(xmlNodePtr *xml, pcmk_scheduler_t *scheduler, const pcmk_injections_t *injections, unsigned int flags, unsigned int section_opts, const char *use_date, const char *input_file, const char *graph_file, const char *dot_file) { pcmk__output_t *out = NULL; int rc = pcmk_rc_ok; rc = pcmk__xml_output_new(&out, xml); if (rc != pcmk_rc_ok) { return rc; } pe__register_messages(out); pcmk__register_lib_messages(out); rc = pcmk__simulate(scheduler, out, injections, flags, section_opts, use_date, input_file, graph_file, dot_file); pcmk__xml_output_finish(out, pcmk_rc2exitc(rc), xml); return rc; } diff --git a/lib/pacemaker/pcmk_verify.c b/lib/pacemaker/pcmk_verify.c index bc914c880a..01a9323264 100644 --- a/lib/pacemaker/pcmk_verify.c +++ b/lib/pacemaker/pcmk_verify.c @@ -1,166 +1,166 @@ /* * Copyright 2023-2024 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 "libpacemaker_private.h" int pcmk__parse_cib(pcmk__output_t *out, const char *cib_source, xmlNodePtr *cib_object) { int rc = pcmk_rc_ok; if (cib_source == NULL) { crm_info("Reading XML from: live cluster"); rc = cib__signon_query(out, NULL, cib_object); } else if (cib_source[0] == '<') { *cib_object = string2xml(cib_source); if (*cib_object == NULL) { rc = ENODATA; } } else if (strcmp(cib_source, "-") == 0) { *cib_object = stdin2xml(); if (*cib_object == NULL) { rc = ENODATA; } } else { - *cib_object = filename2xml(cib_source); + *cib_object = pcmk__xml_read(cib_source); if (*cib_object == NULL) { rc = ENODATA; } } return rc; } int pcmk__verify(pcmk_scheduler_t *scheduler, pcmk__output_t *out, xmlNode *cib_object) { int rc = pcmk_rc_ok; xmlNode *status = NULL; xmlNode *cib_object_copy = NULL; if (!pcmk__xe_is(cib_object, PCMK_XE_CIB)) { rc = EBADMSG; out->err(out, "This tool can only check complete configurations (i.e. those starting with )."); goto verify_done; } status = pcmk_find_cib_element(cib_object, PCMK_XE_STATUS); if (status == NULL) { create_xml_node(cib_object, PCMK_XE_STATUS); } if (pcmk__validate_xml(cib_object, NULL, (xmlRelaxNGValidityErrorFunc) out->err, out) == FALSE) { crm_config_error = TRUE; rc = pcmk_rc_schema_validation; goto verify_done; } else if (cli_config_update(&cib_object, NULL, FALSE) == FALSE) { crm_config_error = TRUE; out->err(out, "The cluster will NOT be able to use this configuration.\n" "Please manually update the configuration to conform to the %s syntax.", xml_latest_schema()); rc = pcmk_rc_schema_validation; goto verify_done; } /* Process the configuration to set crm_config_error/crm_config_warning. * * @TODO Some parts of the configuration are unpacked only when needed (for * example, action configuration), so we aren't necessarily checking those. */ if (cib_object != NULL) { unsigned long long flags = pcmk_sched_no_counts|pcmk_sched_no_compat; if (status == NULL) { // No status available, so do minimal checks flags |= pcmk_sched_validate_only; } cib_object_copy = pcmk__xml_copy(NULL, cib_object); /* The scheduler takes ownership of the XML object and potentially * frees it later. We want the caller of pcmk__verify to retain * ownership of the passed-in XML object, hence we pass in a copy * to the scheduler. */ pcmk__schedule_actions(cib_object_copy, flags, scheduler); } verify_done: if (crm_config_error) { rc = pcmk_rc_schema_validation; pcmk__config_err("CIB did not pass schema validation"); } else if (crm_config_warning) { rc = pcmk_rc_schema_validation; } return rc; } int pcmk_verify(xmlNodePtr *xml, const char *cib_source) { pcmk_scheduler_t *scheduler = NULL; pcmk__output_t *out = NULL; int rc = pcmk_rc_ok; xmlNode *cib_object = NULL; rc = pcmk__xml_output_new(&out, xml); if (rc != pcmk_rc_ok) { return rc; } pe__register_messages(out); pcmk__register_lib_messages(out); rc = pcmk__parse_cib(out, cib_source, &cib_object); if (rc != pcmk_rc_ok) { out->err(out, "Couldn't parse input"); goto done; } scheduler = pe_new_working_set(); if (scheduler == NULL) { rc = errno; out->err(out, "Could not allocate scheduler data: %s", pcmk_rc_str(rc)); goto done; } scheduler->priv = out; rc = pcmk__verify(scheduler, out, cib_object); pe_free_working_set(scheduler); done: pcmk__xml_output_finish(out, pcmk_rc2exitc(rc), xml); if (cib_object != NULL) { free_xml(cib_object); } return rc; } diff --git a/lib/pengine/tests/native/native_find_rsc_test.c b/lib/pengine/tests/native/native_find_rsc_test.c index 1de10bf971..172279b44c 100644 --- a/lib/pengine/tests/native/native_find_rsc_test.c +++ b/lib/pengine/tests/native/native_find_rsc_test.c @@ -1,917 +1,917 @@ /* - * Copyright 2022-2023 the Pacemaker project contributors + * Copyright 2022-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include xmlNode *input = NULL; pcmk_scheduler_t *scheduler = NULL; pcmk_node_t *cluster01, *cluster02, *httpd_bundle_0; pcmk_resource_t *exim_group, *inactive_group; pcmk_resource_t *promotable_clone, *inactive_clone; pcmk_resource_t *httpd_bundle, *mysql_clone_group; static int setup(void **state) { char *path = NULL; crm_xml_init(); path = crm_strdup_printf("%s/crm_mon.xml", getenv("PCMK_CTS_CLI_DIR")); - input = filename2xml(path); + input = pcmk__xml_read(path); free(path); if (input == NULL) { return 1; } scheduler = pe_new_working_set(); if (scheduler == NULL) { return 1; } pcmk__set_scheduler_flags(scheduler, pcmk_sched_no_counts|pcmk_sched_no_compat); scheduler->input = input; cluster_status(scheduler); /* Get references to the cluster nodes so we don't have to find them repeatedly. */ cluster01 = pe_find_node(scheduler->nodes, "cluster01"); cluster02 = pe_find_node(scheduler->nodes, "cluster02"); httpd_bundle_0 = pe_find_node(scheduler->nodes, "httpd-bundle-0"); /* Get references to several resources we use frequently. */ for (GList *iter = scheduler->resources; iter != NULL; iter = iter->next) { pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data; if (strcmp(rsc->id, "exim-group") == 0) { exim_group = rsc; } else if (strcmp(rsc->id, "httpd-bundle") == 0) { httpd_bundle = rsc; } else if (strcmp(rsc->id, "inactive-clone") == 0) { inactive_clone = rsc; } else if (strcmp(rsc->id, "inactive-group") == 0) { inactive_group = rsc; } else if (strcmp(rsc->id, "mysql-clone-group") == 0) { mysql_clone_group = rsc; } else if (strcmp(rsc->id, "promotable-clone") == 0) { promotable_clone = rsc; } } return 0; } static int teardown(void **state) { pe_free_working_set(scheduler); return 0; } static void bad_args(void **state) { pcmk_resource_t *rsc = g_list_first(scheduler->resources)->data; char *id = rsc->id; char *name = NULL; assert_non_null(rsc); assert_null(native_find_rsc(NULL, "dummy", NULL, 0)); assert_null(native_find_rsc(rsc, NULL, NULL, 0)); /* No resources exist with these names. */ name = crm_strdup_printf("%sX", rsc->id); assert_null(native_find_rsc(rsc, name, NULL, 0)); free(name); name = crm_strdup_printf("x%s", rsc->id); assert_null(native_find_rsc(rsc, name, NULL, 0)); free(name); name = g_ascii_strup(rsc->id, -1); assert_null(native_find_rsc(rsc, name, NULL, 0)); g_free(name); /* Fails because resource ID is NULL. */ rsc->id = NULL; assert_null(native_find_rsc(rsc, id, NULL, 0)); rsc->id = id; } static void primitive_rsc(void **state) { pcmk_resource_t *dummy = NULL; /* Find the "dummy" resource, which is the only one with that ID in the set. */ for (GList *iter = scheduler->resources; iter != NULL; iter = iter->next) { pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data; if (strcmp(rsc->id, "dummy") == 0) { dummy = rsc; break; } } assert_non_null(dummy); /* Passes because NULL was passed for node, regardless of flags. */ assert_ptr_equal(dummy, native_find_rsc(dummy, "dummy", NULL, 0)); assert_ptr_equal(dummy, native_find_rsc(dummy, "dummy", NULL, pcmk_rsc_match_current_node)); /* Fails because resource is not a clone (nor cloned). */ assert_null(native_find_rsc(dummy, "dummy", NULL, pcmk_rsc_match_clone_only)); assert_null(native_find_rsc(dummy, "dummy", cluster02, pcmk_rsc_match_clone_only)); /* Fails because dummy is not running on cluster01, even with the right flags. */ assert_null(native_find_rsc(dummy, "dummy", cluster01, pcmk_rsc_match_current_node)); // Fails because pcmk_rsc_match_current_node is required if a node is given assert_null(native_find_rsc(dummy, "dummy", cluster02, 0)); /* Passes because dummy is running on cluster02. */ assert_ptr_equal(dummy, native_find_rsc(dummy, "dummy", cluster02, pcmk_rsc_match_current_node)); } static void group_rsc(void **state) { assert_non_null(exim_group); /* Passes because NULL was passed for node, regardless of flags. */ assert_ptr_equal(exim_group, native_find_rsc(exim_group, "exim-group", NULL, 0)); assert_ptr_equal(exim_group, native_find_rsc(exim_group, "exim-group", NULL, pcmk_rsc_match_current_node)); /* Fails because resource is not a clone (nor cloned). */ assert_null(native_find_rsc(exim_group, "exim-group", NULL, pcmk_rsc_match_clone_only)); assert_null(native_find_rsc(exim_group, "exim-group", cluster01, pcmk_rsc_match_clone_only)); /* Fails because none of exim-group's children are running on cluster01, even with the right flags. */ assert_null(native_find_rsc(exim_group, "exim-group", cluster01, pcmk_rsc_match_current_node)); // Fails because pcmk_rsc_match_current_node is required if a node is given assert_null(native_find_rsc(exim_group, "exim-group", cluster01, 0)); /* Passes because one of exim-group's children is running on cluster02. */ assert_ptr_equal(exim_group, native_find_rsc(exim_group, "exim-group", cluster02, pcmk_rsc_match_current_node)); } static void inactive_group_rsc(void **state) { assert_non_null(inactive_group); /* Passes because NULL was passed for node, regardless of flags. */ assert_ptr_equal(inactive_group, native_find_rsc(inactive_group, "inactive-group", NULL, 0)); assert_ptr_equal(inactive_group, native_find_rsc(inactive_group, "inactive-group", NULL, pcmk_rsc_match_current_node)); /* Fails because resource is not a clone (nor cloned). */ assert_null(native_find_rsc(inactive_group, "inactive-group", NULL, pcmk_rsc_match_clone_only)); assert_null(native_find_rsc(inactive_group, "inactive-group", cluster01, pcmk_rsc_match_clone_only)); /* Fails because none of inactive-group's children are running. */ assert_null(native_find_rsc(inactive_group, "inactive-group", cluster01, pcmk_rsc_match_current_node)); assert_null(native_find_rsc(inactive_group, "inactive-group", cluster02, pcmk_rsc_match_current_node)); } static void group_member_rsc(void **state) { pcmk_resource_t *public_ip = NULL; /* Find the "Public-IP" resource, a member of "exim-group". */ for (GList *iter = exim_group->children; iter != NULL; iter = iter->next) { pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data; if (strcmp(rsc->id, "Public-IP") == 0) { public_ip = rsc; break; } } assert_non_null(public_ip); /* Passes because NULL was passed for node, regardless of flags. */ assert_ptr_equal(public_ip, native_find_rsc(public_ip, "Public-IP", NULL, 0)); assert_ptr_equal(public_ip, native_find_rsc(public_ip, "Public-IP", NULL, pcmk_rsc_match_current_node)); /* Fails because resource is not a clone (nor cloned). */ assert_null(native_find_rsc(public_ip, "Public-IP", NULL, pcmk_rsc_match_clone_only)); assert_null(native_find_rsc(public_ip, "Public-IP", cluster02, pcmk_rsc_match_clone_only)); /* Fails because Public-IP is not running on cluster01, even with the right flags. */ assert_null(native_find_rsc(public_ip, "Public-IP", cluster01, pcmk_rsc_match_current_node)); // Fails because pcmk_rsc_match_current_node is required if a node is given assert_null(native_find_rsc(public_ip, "Public-IP", cluster02, 0)); /* Passes because Public-IP is running on cluster02. */ assert_ptr_equal(public_ip, native_find_rsc(public_ip, "Public-IP", cluster02, pcmk_rsc_match_current_node)); } static void inactive_group_member_rsc(void **state) { pcmk_resource_t *inactive_dummy_1 = NULL; /* Find the "inactive-dummy-1" resource, a member of "inactive-group". */ for (GList *iter = inactive_group->children; iter != NULL; iter = iter->next) { pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data; if (strcmp(rsc->id, "inactive-dummy-1") == 0) { inactive_dummy_1 = rsc; break; } } assert_non_null(inactive_dummy_1); /* Passes because NULL was passed for node, regardless of flags. */ assert_ptr_equal(inactive_dummy_1, native_find_rsc(inactive_dummy_1, "inactive-dummy-1", NULL, 0)); assert_ptr_equal(inactive_dummy_1, native_find_rsc(inactive_dummy_1, "inactive-dummy-1", NULL, pcmk_rsc_match_current_node)); /* Fails because resource is not a clone (nor cloned). */ assert_null(native_find_rsc(inactive_dummy_1, "inactive-dummy-1", NULL, pcmk_rsc_match_clone_only)); assert_null(native_find_rsc(inactive_dummy_1, "inactive-dummy-1", cluster01, pcmk_rsc_match_clone_only)); /* Fails because inactive-dummy-1 is not running. */ assert_null(native_find_rsc(inactive_dummy_1, "inactive-dummy-1", cluster01, pcmk_rsc_match_current_node)); assert_null(native_find_rsc(inactive_dummy_1, "inactive-dummy-1", cluster02, pcmk_rsc_match_current_node)); } static void clone_rsc(void **state) { assert_non_null(promotable_clone); /* Passes because NULL was passed for node, regardless of flags. */ assert_ptr_equal(promotable_clone, native_find_rsc(promotable_clone, "promotable-clone", NULL, 0)); assert_ptr_equal(promotable_clone, native_find_rsc(promotable_clone, "promotable-clone", NULL, pcmk_rsc_match_current_node)); assert_ptr_equal(promotable_clone, native_find_rsc(promotable_clone, "promotable-clone", NULL, pcmk_rsc_match_clone_only)); // Fails because pcmk_rsc_match_current_node is required if a node is given assert_null(native_find_rsc(promotable_clone, "promotable-clone", cluster01, 0)); /* Passes because one of ping-clone's children is running on cluster01. */ assert_ptr_equal(promotable_clone, native_find_rsc(promotable_clone, "promotable-clone", cluster01, pcmk_rsc_match_current_node)); // Fails because pcmk_rsc_match_current_node is required if a node is given assert_null(native_find_rsc(promotable_clone, "promotable-clone", cluster02, 0)); /* Passes because one of ping_clone's children is running on cluster02. */ assert_ptr_equal(promotable_clone, native_find_rsc(promotable_clone, "promotable-clone", cluster02, pcmk_rsc_match_current_node)); // Passes for previous reasons, plus includes pcmk_rsc_match_clone_only assert_ptr_equal(promotable_clone, native_find_rsc(promotable_clone, "promotable-clone", cluster01, pcmk_rsc_match_clone_only |pcmk_rsc_match_current_node)); assert_ptr_equal(promotable_clone, native_find_rsc(promotable_clone, "promotable-clone", cluster02, pcmk_rsc_match_clone_only |pcmk_rsc_match_current_node)); } static void inactive_clone_rsc(void **state) { assert_non_null(inactive_clone); /* Passes because NULL was passed for node, regardless of flags. */ assert_ptr_equal(inactive_clone, native_find_rsc(inactive_clone, "inactive-clone", NULL, 0)); assert_ptr_equal(inactive_clone, native_find_rsc(inactive_clone, "inactive-clone", NULL, pcmk_rsc_match_current_node)); assert_ptr_equal(inactive_clone, native_find_rsc(inactive_clone, "inactive-clone", NULL, pcmk_rsc_match_clone_only)); /* Fails because none of inactive-clone's children are running. */ assert_null(native_find_rsc(inactive_clone, "inactive-clone", cluster01, pcmk_rsc_match_current_node |pcmk_rsc_match_clone_only)); assert_null(native_find_rsc(inactive_clone, "inactive-clone", cluster02, pcmk_rsc_match_current_node |pcmk_rsc_match_clone_only)); } static void clone_instance_rsc(void **state) { pcmk_resource_t *promotable_0 = NULL; pcmk_resource_t *promotable_1 = NULL; /* Find the "promotable-rsc:0" and "promotable-rsc:1" resources, members of "promotable-clone". */ for (GList *iter = promotable_clone->children; iter != NULL; iter = iter->next) { pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data; if (strcmp(rsc->id, "promotable-rsc:0") == 0) { promotable_0 = rsc; } else if (strcmp(rsc->id, "promotable-rsc:1") == 0) { promotable_1 = rsc; } } assert_non_null(promotable_0); assert_non_null(promotable_1); /* Passes because NULL was passed for node, regardless of flags. */ assert_ptr_equal(promotable_0, native_find_rsc(promotable_0, "promotable-rsc:0", NULL, 0)); assert_ptr_equal(promotable_0, native_find_rsc(promotable_0, "promotable-rsc:0", NULL, pcmk_rsc_match_current_node)); assert_ptr_equal(promotable_1, native_find_rsc(promotable_1, "promotable-rsc:1", NULL, 0)); assert_ptr_equal(promotable_1, native_find_rsc(promotable_1, "promotable-rsc:1", NULL, pcmk_rsc_match_current_node)); // Fails because pcmk_rsc_match_current_node is required if a node is given assert_null(native_find_rsc(promotable_0, "promotable-rsc:0", cluster02, 0)); assert_null(native_find_rsc(promotable_1, "promotable-rsc:1", cluster01, 0)); /* Check that the resource is running on the node we expect. */ assert_ptr_equal(promotable_0, native_find_rsc(promotable_0, "promotable-rsc:0", cluster02, pcmk_rsc_match_current_node)); assert_null(native_find_rsc(promotable_0, "promotable-rsc:0", cluster01, pcmk_rsc_match_current_node)); assert_ptr_equal(promotable_1, native_find_rsc(promotable_1, "promotable-rsc:1", cluster01, pcmk_rsc_match_current_node)); assert_null(native_find_rsc(promotable_1, "promotable-rsc:1", cluster02, pcmk_rsc_match_current_node)); /* Passes because NULL was passed for node and primitive name was given, with correct flags. */ assert_ptr_equal(promotable_0, native_find_rsc(promotable_0, "promotable-rsc", NULL, pcmk_rsc_match_clone_only)); // Passes because pcmk_rsc_match_basename matches any instance's base name assert_ptr_equal(promotable_0, native_find_rsc(promotable_0, "promotable-rsc", NULL, pcmk_rsc_match_basename)); assert_ptr_equal(promotable_1, native_find_rsc(promotable_1, "promotable-rsc", NULL, pcmk_rsc_match_basename)); // Passes because pcmk_rsc_match_anon_basename matches assert_ptr_equal(promotable_0, native_find_rsc(promotable_0, "promotable-rsc", NULL, pcmk_rsc_match_anon_basename)); assert_ptr_equal(promotable_1, native_find_rsc(promotable_1, "promotable-rsc", NULL, pcmk_rsc_match_anon_basename)); /* Check that the resource is running on the node we expect. */ assert_ptr_equal(promotable_0, native_find_rsc(promotable_0, "promotable-rsc", cluster02, pcmk_rsc_match_basename |pcmk_rsc_match_current_node)); assert_ptr_equal(promotable_0, native_find_rsc(promotable_0, "promotable-rsc", cluster02, pcmk_rsc_match_anon_basename |pcmk_rsc_match_current_node)); assert_null(native_find_rsc(promotable_0, "promotable-rsc", cluster01, pcmk_rsc_match_basename |pcmk_rsc_match_current_node)); assert_null(native_find_rsc(promotable_0, "promotable-rsc", cluster01, pcmk_rsc_match_anon_basename |pcmk_rsc_match_current_node)); assert_ptr_equal(promotable_1, native_find_rsc(promotable_1, "promotable-rsc", cluster01, pcmk_rsc_match_basename |pcmk_rsc_match_current_node)); assert_ptr_equal(promotable_1, native_find_rsc(promotable_1, "promotable-rsc", cluster01, pcmk_rsc_match_anon_basename |pcmk_rsc_match_current_node)); assert_null(native_find_rsc(promotable_1, "promotable-rsc", cluster02, pcmk_rsc_match_basename |pcmk_rsc_match_current_node)); assert_null(native_find_rsc(promotable_1, "promotable-rsc", cluster02, pcmk_rsc_match_anon_basename |pcmk_rsc_match_current_node)); /* Fails because incorrect flags were given along with primitive name. */ assert_null(native_find_rsc(promotable_0, "promotable-rsc", NULL, pcmk_rsc_match_current_node)); assert_null(native_find_rsc(promotable_1, "promotable-rsc", NULL, pcmk_rsc_match_current_node)); /* And then we check failure possibilities again, except passing promotable_clone * instead of promotable_X as the first argument to native_find_rsc. */ // Fails because pcmk_rsc_match_current_node is required if a node is given assert_null(native_find_rsc(promotable_clone, "promotable-rsc:0", cluster02, 0)); assert_null(native_find_rsc(promotable_clone, "promotable-rsc:1", cluster01, 0)); /* Check that the resource is running on the node we expect. */ assert_ptr_equal(promotable_0, native_find_rsc(promotable_clone, "promotable-rsc:0", cluster02, pcmk_rsc_match_current_node)); assert_ptr_equal(promotable_0, native_find_rsc(promotable_clone, "promotable-rsc", cluster02, pcmk_rsc_match_basename |pcmk_rsc_match_current_node)); assert_ptr_equal(promotable_0, native_find_rsc(promotable_clone, "promotable-rsc", cluster02, pcmk_rsc_match_anon_basename |pcmk_rsc_match_current_node)); assert_ptr_equal(promotable_1, native_find_rsc(promotable_clone, "promotable-rsc:1", cluster01, pcmk_rsc_match_current_node)); assert_ptr_equal(promotable_1, native_find_rsc(promotable_clone, "promotable-rsc", cluster01, pcmk_rsc_match_basename |pcmk_rsc_match_current_node)); assert_ptr_equal(promotable_1, native_find_rsc(promotable_clone, "promotable-rsc", cluster01, pcmk_rsc_match_anon_basename |pcmk_rsc_match_current_node)); } static void renamed_rsc(void **state) { pcmk_resource_t *promotable_0 = NULL; pcmk_resource_t *promotable_1 = NULL; /* Find the "promotable-rsc:0" and "promotable-rsc:1" resources, members of "promotable-clone". */ for (GList *iter = promotable_clone->children; iter != NULL; iter = iter->next) { pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data; if (strcmp(rsc->id, "promotable-rsc:0") == 0) { promotable_0 = rsc; } else if (strcmp(rsc->id, "promotable-rsc:1") == 0) { promotable_1 = rsc; } } assert_non_null(promotable_0); assert_non_null(promotable_1); // Passes because pcmk_rsc_match_history means base name matches clone_name assert_ptr_equal(promotable_0, native_find_rsc(promotable_0, "promotable-rsc", NULL, pcmk_rsc_match_history)); assert_ptr_equal(promotable_1, native_find_rsc(promotable_1, "promotable-rsc", NULL, pcmk_rsc_match_history)); } static void bundle_rsc(void **state) { assert_non_null(httpd_bundle); /* Passes because NULL was passed for node, regardless of flags. */ assert_ptr_equal(httpd_bundle, native_find_rsc(httpd_bundle, "httpd-bundle", NULL, 0)); assert_ptr_equal(httpd_bundle, native_find_rsc(httpd_bundle, "httpd-bundle", NULL, pcmk_rsc_match_current_node)); /* Fails because resource is not a clone (nor cloned). */ assert_null(native_find_rsc(httpd_bundle, "httpd-bundle", NULL, pcmk_rsc_match_clone_only)); assert_null(native_find_rsc(httpd_bundle, "httpd-bundle", cluster01, pcmk_rsc_match_clone_only)); // Fails because pcmk_rsc_match_current_node is required if a node is given assert_null(native_find_rsc(httpd_bundle, "httpd-bundle", cluster01, 0)); /* Passes because one of httpd_bundle's children is running on cluster01. */ assert_ptr_equal(httpd_bundle, native_find_rsc(httpd_bundle, "httpd-bundle", cluster01, pcmk_rsc_match_current_node)); } static bool bundle_first_replica(pcmk__bundle_replica_t *replica, void *user_data) { pcmk_resource_t *ip_0 = replica->ip; pcmk_resource_t *child_0 = replica->child; pcmk_resource_t *container_0 = replica->container; pcmk_resource_t *remote_0 = replica->remote; assert_non_null(ip_0); assert_non_null(child_0); assert_non_null(container_0); assert_non_null(remote_0); /* Passes because NULL was passed for node, regardless of flags. */ assert_ptr_equal(ip_0, native_find_rsc(ip_0, "httpd-bundle-ip-192.168.122.131", NULL, 0)); assert_ptr_equal(child_0, native_find_rsc(child_0, "httpd:0", NULL, 0)); assert_ptr_equal(container_0, native_find_rsc(container_0, "httpd-bundle-docker-0", NULL, 0)); assert_ptr_equal(remote_0, native_find_rsc(remote_0, "httpd-bundle-0", NULL, 0)); // Fails because pcmk_rsc_match_current_node is required if a node is given assert_null(native_find_rsc(ip_0, "httpd-bundle-ip-192.168.122.131", cluster01, 0)); assert_null(native_find_rsc(child_0, "httpd:0", httpd_bundle_0, 0)); assert_null(native_find_rsc(container_0, "httpd-bundle-docker-0", cluster01, 0)); assert_null(native_find_rsc(remote_0, "httpd-bundle-0", cluster01, 0)); /* Check that the resource is running on the node we expect. */ assert_ptr_equal(ip_0, native_find_rsc(ip_0, "httpd-bundle-ip-192.168.122.131", cluster01, pcmk_rsc_match_current_node)); assert_null(native_find_rsc(ip_0, "httpd-bundle-ip-192.168.122.131", cluster02, pcmk_rsc_match_current_node)); assert_null(native_find_rsc(ip_0, "httpd-bundle-ip-192.168.122.131", httpd_bundle_0, pcmk_rsc_match_current_node)); assert_ptr_equal(child_0, native_find_rsc(child_0, "httpd:0", httpd_bundle_0, pcmk_rsc_match_current_node)); assert_null(native_find_rsc(child_0, "httpd:0", cluster01, pcmk_rsc_match_current_node)); assert_null(native_find_rsc(child_0, "httpd:0", cluster02, pcmk_rsc_match_current_node)); assert_ptr_equal(container_0, native_find_rsc(container_0, "httpd-bundle-docker-0", cluster01, pcmk_rsc_match_current_node)); assert_null(native_find_rsc(container_0, "httpd-bundle-docker-0", cluster02, pcmk_rsc_match_current_node)); assert_null(native_find_rsc(container_0, "httpd-bundle-docker-0", httpd_bundle_0, pcmk_rsc_match_current_node)); assert_ptr_equal(remote_0, native_find_rsc(remote_0, "httpd-bundle-0", cluster01, pcmk_rsc_match_current_node)); assert_null(native_find_rsc(remote_0, "httpd-bundle-0", cluster02, pcmk_rsc_match_current_node)); assert_null(native_find_rsc(remote_0, "httpd-bundle-0", httpd_bundle_0, pcmk_rsc_match_current_node)); // Passes because pcmk_rsc_match_basename matches any replica's base name assert_ptr_equal(child_0, native_find_rsc(child_0, "httpd", NULL, pcmk_rsc_match_basename)); // Passes because pcmk_rsc_match_anon_basename matches assert_ptr_equal(child_0, native_find_rsc(child_0, "httpd", NULL, pcmk_rsc_match_anon_basename)); /* Check that the resource is running on the node we expect. */ assert_ptr_equal(child_0, native_find_rsc(child_0, "httpd", httpd_bundle_0, pcmk_rsc_match_basename |pcmk_rsc_match_current_node)); assert_ptr_equal(child_0, native_find_rsc(child_0, "httpd", httpd_bundle_0, pcmk_rsc_match_anon_basename |pcmk_rsc_match_current_node)); assert_null(native_find_rsc(child_0, "httpd", cluster01, pcmk_rsc_match_basename |pcmk_rsc_match_current_node)); assert_null(native_find_rsc(child_0, "httpd", cluster01, pcmk_rsc_match_anon_basename |pcmk_rsc_match_current_node)); assert_null(native_find_rsc(child_0, "httpd", cluster02, pcmk_rsc_match_basename |pcmk_rsc_match_current_node)); assert_null(native_find_rsc(child_0, "httpd", cluster02, pcmk_rsc_match_anon_basename |pcmk_rsc_match_current_node)); /* Fails because incorrect flags were given along with base name. */ assert_null(native_find_rsc(child_0, "httpd", NULL, pcmk_rsc_match_current_node)); /* And then we check failure possibilities again, except passing httpd-bundle * instead of X_0 as the first argument to native_find_rsc. */ // Fails because pcmk_rsc_match_current_node is required if a node is given assert_null(native_find_rsc(httpd_bundle, "httpd-bundle-ip-192.168.122.131", cluster01, 0)); assert_null(native_find_rsc(httpd_bundle, "httpd:0", httpd_bundle_0, 0)); assert_null(native_find_rsc(httpd_bundle, "httpd-bundle-docker-0", cluster01, 0)); assert_null(native_find_rsc(httpd_bundle, "httpd-bundle-0", cluster01, 0)); /* Check that the resource is running on the node we expect. */ assert_ptr_equal(ip_0, native_find_rsc(httpd_bundle, "httpd-bundle-ip-192.168.122.131", cluster01, pcmk_rsc_match_current_node)); assert_ptr_equal(child_0, native_find_rsc(httpd_bundle, "httpd:0", httpd_bundle_0, pcmk_rsc_match_current_node)); assert_ptr_equal(container_0, native_find_rsc(httpd_bundle, "httpd-bundle-docker-0", cluster01, pcmk_rsc_match_current_node)); assert_ptr_equal(remote_0, native_find_rsc(httpd_bundle, "httpd-bundle-0", cluster01, pcmk_rsc_match_current_node)); return false; // Do not iterate through any further replicas } static void bundle_replica_rsc(void **state) { pe__foreach_bundle_replica(httpd_bundle, bundle_first_replica, NULL); } static void clone_group_rsc(void **rsc) { assert_non_null(mysql_clone_group); /* Passes because NULL was passed for node, regardless of flags. */ assert_ptr_equal(mysql_clone_group, native_find_rsc(mysql_clone_group, "mysql-clone-group", NULL, 0)); assert_ptr_equal(mysql_clone_group, native_find_rsc(mysql_clone_group, "mysql-clone-group", NULL, pcmk_rsc_match_current_node)); assert_ptr_equal(mysql_clone_group, native_find_rsc(mysql_clone_group, "mysql-clone-group", NULL, pcmk_rsc_match_clone_only)); // Fails because pcmk_rsc_match_current_node is required if a node is given assert_null(native_find_rsc(mysql_clone_group, "mysql-clone-group", cluster01, 0)); /* Passes because one of mysql-clone-group's children is running on cluster01. */ assert_ptr_equal(mysql_clone_group, native_find_rsc(mysql_clone_group, "mysql-clone-group", cluster01, pcmk_rsc_match_current_node)); // Fails because pcmk_rsc_match_current_node is required if a node is given assert_null(native_find_rsc(mysql_clone_group, "mysql-clone-group", cluster02, 0)); /* Passes because one of mysql-clone-group's children is running on cluster02. */ assert_ptr_equal(mysql_clone_group, native_find_rsc(mysql_clone_group, "mysql-clone-group", cluster02, pcmk_rsc_match_current_node)); // Passes for previous reasons, plus includes pcmk_rsc_match_clone_only assert_ptr_equal(mysql_clone_group, native_find_rsc(mysql_clone_group, "mysql-clone-group", cluster01, pcmk_rsc_match_clone_only |pcmk_rsc_match_current_node)); assert_ptr_equal(mysql_clone_group, native_find_rsc(mysql_clone_group, "mysql-clone-group", cluster02, pcmk_rsc_match_clone_only |pcmk_rsc_match_current_node)); } static void clone_group_instance_rsc(void **rsc) { pcmk_resource_t *mysql_group_0 = NULL; pcmk_resource_t *mysql_group_1 = NULL; /* Find the "mysql-group:0" and "mysql-group:1" resources, members of "mysql-clone-group". */ for (GList *iter = mysql_clone_group->children; iter != NULL; iter = iter->next) { pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data; if (strcmp(rsc->id, "mysql-group:0") == 0) { mysql_group_0 = rsc; } else if (strcmp(rsc->id, "mysql-group:1") == 0) { mysql_group_1 = rsc; } } assert_non_null(mysql_group_0); assert_non_null(mysql_group_1); /* Passes because NULL was passed for node, regardless of flags. */ assert_ptr_equal(mysql_group_0, native_find_rsc(mysql_group_0, "mysql-group:0", NULL, 0)); assert_ptr_equal(mysql_group_0, native_find_rsc(mysql_group_0, "mysql-group:0", NULL, pcmk_rsc_match_current_node)); assert_ptr_equal(mysql_group_1, native_find_rsc(mysql_group_1, "mysql-group:1", NULL, 0)); assert_ptr_equal(mysql_group_1, native_find_rsc(mysql_group_1, "mysql-group:1", NULL, pcmk_rsc_match_current_node)); // Fails because pcmk_rsc_match_current_node is required if a node is given assert_null(native_find_rsc(mysql_group_0, "mysql-group:0", cluster02, 0)); assert_null(native_find_rsc(mysql_group_1, "mysql-group:1", cluster01, 0)); /* Check that the resource is running on the node we expect. */ assert_ptr_equal(mysql_group_0, native_find_rsc(mysql_group_0, "mysql-group:0", cluster02, pcmk_rsc_match_current_node)); assert_null(native_find_rsc(mysql_group_0, "mysql-group:0", cluster01, pcmk_rsc_match_current_node)); assert_ptr_equal(mysql_group_1, native_find_rsc(mysql_group_1, "mysql-group:1", cluster01, pcmk_rsc_match_current_node)); assert_null(native_find_rsc(mysql_group_1, "mysql-group:1", cluster02, pcmk_rsc_match_current_node)); /* Passes because NULL was passed for node and base name was given, with correct flags. */ assert_ptr_equal(mysql_group_0, native_find_rsc(mysql_group_0, "mysql-group" , NULL, pcmk_rsc_match_clone_only)); // Passes because pcmk_rsc_match_basename matches any base name assert_ptr_equal(mysql_group_0, native_find_rsc(mysql_group_0, "mysql-group" , NULL, pcmk_rsc_match_basename)); assert_ptr_equal(mysql_group_1, native_find_rsc(mysql_group_1, "mysql-group" , NULL, pcmk_rsc_match_basename)); // Passes because pcmk_rsc_match_anon_basename matches assert_ptr_equal(mysql_group_0, native_find_rsc(mysql_group_0, "mysql-group" , NULL, pcmk_rsc_match_anon_basename)); assert_ptr_equal(mysql_group_1, native_find_rsc(mysql_group_1, "mysql-group" , NULL, pcmk_rsc_match_anon_basename)); /* Check that the resource is running on the node we expect. */ assert_ptr_equal(mysql_group_0, native_find_rsc(mysql_group_0, "mysql-group", cluster02, pcmk_rsc_match_basename |pcmk_rsc_match_current_node)); assert_ptr_equal(mysql_group_0, native_find_rsc(mysql_group_0, "mysql-group", cluster02, pcmk_rsc_match_anon_basename |pcmk_rsc_match_current_node)); assert_null(native_find_rsc(mysql_group_0, "mysql-group", cluster01, pcmk_rsc_match_basename |pcmk_rsc_match_current_node)); assert_null(native_find_rsc(mysql_group_0, "mysql-group", cluster01, pcmk_rsc_match_anon_basename |pcmk_rsc_match_current_node)); assert_ptr_equal(mysql_group_1, native_find_rsc(mysql_group_1, "mysql-group", cluster01, pcmk_rsc_match_basename |pcmk_rsc_match_current_node)); assert_ptr_equal(mysql_group_1, native_find_rsc(mysql_group_1, "mysql-group", cluster01, pcmk_rsc_match_anon_basename |pcmk_rsc_match_current_node)); assert_null(native_find_rsc(mysql_group_1, "mysql-group", cluster02, pcmk_rsc_match_basename |pcmk_rsc_match_current_node)); assert_null(native_find_rsc(mysql_group_1, "mysql-group", cluster02, pcmk_rsc_match_anon_basename |pcmk_rsc_match_current_node)); /* Fails because incorrect flags were given along with base name. */ assert_null(native_find_rsc(mysql_group_0, "mysql-group", NULL, pcmk_rsc_match_current_node)); assert_null(native_find_rsc(mysql_group_1, "mysql-group", NULL, pcmk_rsc_match_current_node)); /* And then we check failure possibilities again, except passing mysql_clone_group * instead of mysql_group_X as the first argument to native_find_rsc. */ // Fails because pcmk_rsc_match_current_node is required if a node is given assert_null(native_find_rsc(mysql_clone_group, "mysql-group:0", cluster02, 0)); assert_null(native_find_rsc(mysql_clone_group, "mysql-group:1", cluster01, 0)); /* Check that the resource is running on the node we expect. */ assert_ptr_equal(mysql_group_0, native_find_rsc(mysql_clone_group, "mysql-group:0", cluster02, pcmk_rsc_match_current_node)); assert_ptr_equal(mysql_group_0, native_find_rsc(mysql_clone_group, "mysql-group", cluster02, pcmk_rsc_match_basename |pcmk_rsc_match_current_node)); assert_ptr_equal(mysql_group_0, native_find_rsc(mysql_clone_group, "mysql-group", cluster02, pcmk_rsc_match_anon_basename |pcmk_rsc_match_current_node)); assert_ptr_equal(mysql_group_1, native_find_rsc(mysql_clone_group, "mysql-group:1", cluster01, pcmk_rsc_match_current_node)); assert_ptr_equal(mysql_group_1, native_find_rsc(mysql_clone_group, "mysql-group", cluster01, pcmk_rsc_match_basename |pcmk_rsc_match_current_node)); assert_ptr_equal(mysql_group_1, native_find_rsc(mysql_clone_group, "mysql-group", cluster01, pcmk_rsc_match_anon_basename |pcmk_rsc_match_current_node)); } static void clone_group_member_rsc(void **state) { pcmk_resource_t *mysql_proxy = NULL; /* Find the "mysql-proxy" resource, a member of "mysql-group". */ for (GList *iter = mysql_clone_group->children; iter != NULL; iter = iter->next) { pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data; if (strcmp(rsc->id, "mysql-group:0") == 0) { for (GList *iter2 = rsc->children; iter2 != NULL; iter2 = iter2->next) { pcmk_resource_t *child = (pcmk_resource_t *) iter2->data; if (strcmp(child->id, "mysql-proxy:0") == 0) { mysql_proxy = child; break; } } break; } } assert_non_null(mysql_proxy); /* Passes because NULL was passed for node, regardless of flags. */ assert_ptr_equal(mysql_proxy, native_find_rsc(mysql_proxy, "mysql-proxy:0", NULL, 0)); assert_ptr_equal(mysql_proxy, native_find_rsc(mysql_proxy, "mysql-proxy:0", NULL, pcmk_rsc_match_current_node)); /* Passes because resource's parent is a clone. */ assert_ptr_equal(mysql_proxy, native_find_rsc(mysql_proxy, "mysql-proxy:0", NULL, pcmk_rsc_match_clone_only)); assert_ptr_equal(mysql_proxy, native_find_rsc(mysql_proxy, "mysql-proxy:0", cluster02, pcmk_rsc_match_clone_only |pcmk_rsc_match_current_node)); /* Fails because mysql-proxy:0 is not running on cluster01, even with the right flags. */ assert_null(native_find_rsc(mysql_proxy, "mysql-proxy:0", cluster01, pcmk_rsc_match_current_node)); // Fails because pcmk_rsc_match_current_node is required if a node is given assert_null(native_find_rsc(mysql_proxy, "mysql-proxy:0", cluster02, 0)); /* Passes because mysql-proxy:0 is running on cluster02. */ assert_ptr_equal(mysql_proxy, native_find_rsc(mysql_proxy, "mysql-proxy:0", cluster02, pcmk_rsc_match_current_node)); } /* TODO: Add tests for finding on assigned node (passing a node without * pcmk_rsc_match_current_node, after scheduling, for a resource that is * starting/stopping/moving. */ PCMK__UNIT_TEST(setup, teardown, cmocka_unit_test(bad_args), cmocka_unit_test(primitive_rsc), cmocka_unit_test(group_rsc), cmocka_unit_test(inactive_group_rsc), cmocka_unit_test(group_member_rsc), cmocka_unit_test(inactive_group_member_rsc), cmocka_unit_test(clone_rsc), cmocka_unit_test(inactive_clone_rsc), cmocka_unit_test(clone_instance_rsc), cmocka_unit_test(renamed_rsc), cmocka_unit_test(bundle_rsc), cmocka_unit_test(bundle_replica_rsc), cmocka_unit_test(clone_group_rsc), cmocka_unit_test(clone_group_instance_rsc), cmocka_unit_test(clone_group_member_rsc)) diff --git a/lib/pengine/tests/native/pe_base_name_eq_test.c b/lib/pengine/tests/native/pe_base_name_eq_test.c index f6081ba445..68112bb4d0 100644 --- a/lib/pengine/tests/native/pe_base_name_eq_test.c +++ b/lib/pengine/tests/native/pe_base_name_eq_test.c @@ -1,150 +1,150 @@ /* - * Copyright 2022-2023 the Pacemaker project contributors + * Copyright 2022-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include xmlNode *input = NULL; pcmk_scheduler_t *scheduler = NULL; pcmk_resource_t *exim_group, *promotable_0, *promotable_1, *dummy; pcmk_resource_t *httpd_bundle, *mysql_group_0, *mysql_group_1; static int setup(void **state) { char *path = NULL; crm_xml_init(); path = crm_strdup_printf("%s/crm_mon.xml", getenv("PCMK_CTS_CLI_DIR")); - input = filename2xml(path); + input = pcmk__xml_read(path); free(path); if (input == NULL) { return 1; } scheduler = pe_new_working_set(); if (scheduler == NULL) { return 1; } pcmk__set_scheduler_flags(scheduler, pcmk_sched_no_counts|pcmk_sched_no_compat); scheduler->input = input; cluster_status(scheduler); /* Get references to several resources we use frequently. */ for (GList *iter = scheduler->resources; iter != NULL; iter = iter->next) { pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data; if (strcmp(rsc->id, "dummy") == 0) { dummy = rsc; } else if (strcmp(rsc->id, "exim-group") == 0) { exim_group = rsc; } else if (strcmp(rsc->id, "httpd-bundle") == 0) { httpd_bundle = rsc; } else if (strcmp(rsc->id, "mysql-clone-group") == 0) { for (GList *iter = rsc->children; iter != NULL; iter = iter->next) { pcmk_resource_t *child = (pcmk_resource_t *) iter->data; if (strcmp(child->id, "mysql-group:0") == 0) { mysql_group_0 = child; } else if (strcmp(child->id, "mysql-group:1") == 0) { mysql_group_1 = child; } } } else if (strcmp(rsc->id, "promotable-clone") == 0) { for (GList *iter = rsc->children; iter != NULL; iter = iter->next) { pcmk_resource_t *child = (pcmk_resource_t *) iter->data; if (strcmp(child->id, "promotable-rsc:0") == 0) { promotable_0 = child; } else if (strcmp(child->id, "promotable-rsc:1") == 0) { promotable_1 = child; } } } } return 0; } static int teardown(void **state) { pe_free_working_set(scheduler); return 0; } static void bad_args(void **state) { char *id = dummy->id; assert_false(pe_base_name_eq(NULL, "dummy")); assert_false(pe_base_name_eq(dummy, NULL)); dummy->id = NULL; assert_false(pe_base_name_eq(dummy, "dummy")); dummy->id = id; } static void primitive_rsc(void **state) { assert_true(pe_base_name_eq(dummy, "dummy")); assert_false(pe_base_name_eq(dummy, "DUMMY")); assert_false(pe_base_name_eq(dummy, "dUmMy")); assert_false(pe_base_name_eq(dummy, "dummy0")); assert_false(pe_base_name_eq(dummy, "dummy:0")); } static void group_rsc(void **state) { assert_true(pe_base_name_eq(exim_group, "exim-group")); assert_false(pe_base_name_eq(exim_group, "EXIM-GROUP")); assert_false(pe_base_name_eq(exim_group, "exim-group0")); assert_false(pe_base_name_eq(exim_group, "exim-group:0")); assert_false(pe_base_name_eq(exim_group, "Public-IP")); } static void clone_rsc(void **state) { assert_true(pe_base_name_eq(promotable_0, "promotable-rsc")); assert_true(pe_base_name_eq(promotable_1, "promotable-rsc")); assert_false(pe_base_name_eq(promotable_0, "promotable-rsc:0")); assert_false(pe_base_name_eq(promotable_1, "promotable-rsc:1")); assert_false(pe_base_name_eq(promotable_0, "PROMOTABLE-RSC")); assert_false(pe_base_name_eq(promotable_1, "PROMOTABLE-RSC")); assert_false(pe_base_name_eq(promotable_0, "Promotable-rsc")); assert_false(pe_base_name_eq(promotable_1, "Promotable-rsc")); } static void bundle_rsc(void **state) { assert_true(pe_base_name_eq(httpd_bundle, "httpd-bundle")); assert_false(pe_base_name_eq(httpd_bundle, "HTTPD-BUNDLE")); assert_false(pe_base_name_eq(httpd_bundle, "httpd")); assert_false(pe_base_name_eq(httpd_bundle, "httpd-docker-0")); } PCMK__UNIT_TEST(setup, teardown, cmocka_unit_test(bad_args), cmocka_unit_test(primitive_rsc), cmocka_unit_test(group_rsc), cmocka_unit_test(clone_rsc), cmocka_unit_test(bundle_rsc)) diff --git a/tools/cibadmin.c b/tools/cibadmin.c index c2234400f8..50573be243 100644 --- a/tools/cibadmin.c +++ b/tools/cibadmin.c @@ -1,950 +1,950 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #define SUMMARY "query and edit the Pacemaker configuration" #define INDENT " " enum cibadmin_section_type { cibadmin_section_all = 0, cibadmin_section_scope, cibadmin_section_xpath, }; static int request_id = 0; static cib_t *the_cib = NULL; static GMainLoop *mainloop = NULL; static crm_exit_t exit_code = CRM_EX_OK; static struct { const char *cib_action; int cmd_options; enum cibadmin_section_type section_type; char *cib_section; char *validate_with; gint message_timeout_sec; enum pcmk__acl_render_how acl_render_mode; gchar *cib_user; gchar *dest_node; gchar *input_file; gchar *input_xml; gboolean input_stdin; bool delete_all; gboolean allow_create; gboolean force; gboolean get_node_path; gboolean local; gboolean no_children; gboolean sync_call; /* @COMPAT: For "-!" version option. Not advertised nor marked as * deprecated, but accepted. */ gboolean extended_version; //! \deprecated gboolean no_bcast; } options; int do_init(void); static int do_work(xmlNode *input, xmlNode **output); void cibadmin_op_callback(xmlNode *msg, int call_id, int rc, xmlNode *output, void *user_data); static void print_xml_output(xmlNode * xml) { if (!xml) { return; } else if (xml->type != XML_ELEMENT_NODE) { return; } if (pcmk_is_set(options.cmd_options, cib_xpath_address)) { const char *id = crm_element_value(xml, PCMK_XA_ID); if (pcmk__xe_is(xml, PCMK__XE_XPATH_QUERY)) { xmlNode *child = NULL; for (child = xml->children; child; child = child->next) { print_xml_output(child); } } else if (id) { printf("%s\n", id); } } else { char *buffer = dump_xml_formatted(xml); fprintf(stdout, "%s", buffer); free(buffer); } } // Upgrade requested but already at latest schema static void report_schema_unchanged(void) { const char *err = pcmk_rc_str(pcmk_rc_schema_unchanged); crm_info("Upgrade unnecessary: %s\n", err); printf("Upgrade unnecessary: %s\n", err); exit_code = CRM_EX_OK; } /*! * \internal * \brief Check whether the current CIB action is dangerous * \return true if \p options.cib_action is dangerous, or false otherwise */ static inline bool cib_action_is_dangerous(void) { return options.no_bcast || options.delete_all || pcmk__str_any_of(options.cib_action, PCMK__CIB_REQUEST_UPGRADE, PCMK__CIB_REQUEST_ERASE, NULL); } /*! * \internal * \brief Determine whether the given CIB scope is valid for \p cibadmin * * \param[in] scope Scope to validate * * \return true if \p scope is valid, or false otherwise * \note An invalid scope applies the operation to the entire CIB. */ static inline bool scope_is_valid(const char *scope) { return pcmk__str_any_of(scope, PCMK_XE_CONFIGURATION, PCMK_XE_NODES, PCMK_XE_RESOURCES, PCMK_XE_CONSTRAINTS, PCMK_XE_CRM_CONFIG, PCMK_XE_RSC_DEFAULTS, PCMK_XE_OP_DEFAULTS, PCMK_XE_ACLS, PCMK_XE_FENCING_TOPOLOGY, PCMK_XE_TAGS, PCMK_XE_ALERTS, PCMK_XE_STATUS, NULL); } static gboolean command_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.delete_all = false; if (pcmk__str_any_of(option_name, "-u", "--upgrade", NULL)) { options.cib_action = PCMK__CIB_REQUEST_UPGRADE; } else if (pcmk__str_any_of(option_name, "-Q", "--query", NULL)) { options.cib_action = PCMK__CIB_REQUEST_QUERY; } else if (pcmk__str_any_of(option_name, "-E", "--erase", NULL)) { options.cib_action = PCMK__CIB_REQUEST_ERASE; } else if (pcmk__str_any_of(option_name, "-B", "--bump", NULL)) { options.cib_action = PCMK__CIB_REQUEST_BUMP; } else if (pcmk__str_any_of(option_name, "-C", "--create", NULL)) { options.cib_action = PCMK__CIB_REQUEST_CREATE; } else if (pcmk__str_any_of(option_name, "-M", "--modify", NULL)) { options.cib_action = PCMK__CIB_REQUEST_MODIFY; } else if (pcmk__str_any_of(option_name, "-P", "--patch", NULL)) { options.cib_action = PCMK__CIB_REQUEST_APPLY_PATCH; } else if (pcmk__str_any_of(option_name, "-R", "--replace", NULL)) { options.cib_action = PCMK__CIB_REQUEST_REPLACE; } else if (pcmk__str_any_of(option_name, "-D", "--delete", NULL)) { options.cib_action = PCMK__CIB_REQUEST_DELETE; } else if (pcmk__str_any_of(option_name, "-d", "--delete-all", NULL)) { options.cib_action = PCMK__CIB_REQUEST_DELETE; options.delete_all = true; } else if (pcmk__str_any_of(option_name, "-a", "--empty", NULL)) { options.cib_action = "empty"; pcmk__str_update(&options.validate_with, optarg); } else if (pcmk__str_any_of(option_name, "-5", "--md5-sum", NULL)) { options.cib_action = "md5-sum"; } else if (pcmk__str_any_of(option_name, "-6", "--md5-sum-versioned", NULL)) { options.cib_action = "md5-sum-versioned"; } else { // Should be impossible return FALSE; } return TRUE; } static gboolean show_access_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { if (pcmk__str_eq(optarg, "auto", pcmk__str_null_matches)) { options.acl_render_mode = pcmk__acl_render_default; } else if (g_strcmp0(optarg, "namespace") == 0) { options.acl_render_mode = pcmk__acl_render_namespace; } else if (g_strcmp0(optarg, "text") == 0) { options.acl_render_mode = pcmk__acl_render_text; } else if (g_strcmp0(optarg, "color") == 0) { options.acl_render_mode = pcmk__acl_render_color; } else { g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_USAGE, "Invalid value '%s' for option '%s'", optarg, option_name); return FALSE; } return TRUE; } static gboolean section_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { if (pcmk__str_any_of(option_name, "-o", "--scope", NULL)) { options.section_type = cibadmin_section_scope; } else if (pcmk__str_any_of(option_name, "-A", "--xpath", NULL)) { options.section_type = cibadmin_section_xpath; } else { // Should be impossible return FALSE; } pcmk__str_update(&options.cib_section, optarg); return TRUE; } static GOptionEntry command_entries[] = { { "upgrade", 'u', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Upgrade the configuration to the latest syntax", NULL }, { "query", 'Q', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Query the contents of the CIB", NULL }, { "erase", 'E', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Erase the contents of the whole CIB", NULL }, { "bump", 'B', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Increase the CIB's epoch value by 1", NULL }, { "create", 'C', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Create an object in the CIB (will fail if object already exists)", NULL }, { "modify", 'M', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Find object somewhere in CIB's XML tree and update it (fails if object " "does not exist unless -c is also specified)", NULL }, { "patch", 'P', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Supply an update in the form of an XML diff (see crm_diff(8))", NULL }, { "replace", 'R', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Recursively replace an object in the CIB", NULL }, { "delete", 'D', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Delete first object matching supplied criteria (for example, " "<" PCMK_XE_OP " " PCMK_XA_ID "=\"rsc1_op1\" " PCMK_XA_NAME "=\"monitor\"/>).\n" INDENT "The XML element name and all attributes must match in order for " "the element to be deleted.", NULL }, { "delete-all", 'd', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "When used with --xpath, remove all matching objects in the " "configuration instead of just the first one", NULL }, { "empty", 'a', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Output an empty CIB. Accepts an optional schema name argument to use as " "the " PCMK_XA_VALIDATE_WITH " value.\n" INDENT "If no schema is given, the latest will be used.", "[schema]" }, { "md5-sum", '5', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Calculate the on-disk CIB digest", NULL }, { "md5-sum-versioned", '6', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Calculate an on-the-wire versioned CIB digest", NULL }, { NULL } }; static GOptionEntry data_entries[] = { /* @COMPAT: These arguments should be last-wins. We can have an enum option * that stores the input type, along with a single string option that stores * the XML string for --xml-text, filename for --xml-file, or NULL for * --xml-pipe. */ { "xml-text", 'X', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.input_xml, "Retrieve XML from the supplied string", "value" }, { "xml-file", 'x', G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &options.input_file, "Retrieve XML from the named file", "value" }, { "xml-pipe", 'p', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.input_stdin, "Retrieve XML from stdin", NULL }, { NULL } }; static GOptionEntry addl_entries[] = { { "force", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.force, "Force the action to be performed", NULL }, { "timeout", 't', G_OPTION_FLAG_NONE, G_OPTION_ARG_INT, &options.message_timeout_sec, "Time (in seconds) to wait before declaring the operation failed", "value" }, { "user", 'U', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.cib_user, "Run the command with permissions of the named user (valid only for the " "root and " CRM_DAEMON_USER " accounts)", "value" }, { "sync-call", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.sync_call, "Wait for call to complete before returning", NULL }, { "local", 'l', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.local, "Command takes effect locally (should be used only for queries)", NULL }, { "scope", 'o', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, section_cb, "Limit scope of operation to specific section of CIB\n" INDENT "Valid values: " PCMK_XE_CONFIGURATION ", " PCMK_XE_NODES ", " PCMK_XE_RESOURCES ", " PCMK_XE_CONSTRAINTS ", " PCMK_XE_CRM_CONFIG ", " PCMK_XE_RSC_DEFAULTS ",\n" INDENT " " PCMK_XE_OP_DEFAULTS ", " PCMK_XE_ACLS ", " PCMK_XE_FENCING_TOPOLOGY ", " PCMK_XE_TAGS ", " PCMK_XE_ALERTS ", " PCMK_XE_STATUS "\n" INDENT "If both --scope/-o and --xpath/-a are specified, the last one to " "appear takes effect", "value" }, { "xpath", 'A', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, section_cb, "A valid XPath to use instead of --scope/-o\n" INDENT "If both --scope/-o and --xpath/-a are specified, the last one to " "appear takes effect", "value" }, { "node-path", 'e', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.get_node_path, "When performing XPath queries, return paths of any matches found\n" INDENT "(for example, " "\"/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_RESOURCES "/" PCMK_XE_CLONE "[@" PCMK_XA_ID "='dummy-clone']" "/" PCMK_XE_PRIMITIVE "[@" PCMK_XA_ID "='dummy']\")", NULL }, { "show-access", 'S', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, show_access_cb, "Whether to use syntax highlighting for ACLs (with -Q/--query and " "-U/--user)\n" INDENT "Allowed values: 'color' (default for terminal), 'text' (plain text, " "default for non-terminal),\n" INDENT " 'namespace', or 'auto' (use default value)\n" INDENT "Default value: 'auto'", "[value]" }, { "allow-create", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.allow_create, "(Advanced) Allow target of --modify/-M to be created if it does not " "exist", NULL }, { "no-children", 'n', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.no_children, "(Advanced) When querying an object, do not include its children in the " "result", NULL }, { "node", 'N', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.dest_node, "(Advanced) Send command to the specified host", "value" }, // @COMPAT: Deprecated { "no-bcast", 'b', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.no_bcast, "deprecated", NULL }, // @COMPAT: Deprecated { "host", 'h', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &options.dest_node, "deprecated", NULL }, { NULL } }; static GOptionContext * build_arg_context(pcmk__common_args_t *args) { const char *desc = NULL; GOptionContext *context = NULL; GOptionEntry extra_prog_entries[] = { // @COMPAT: Deprecated { "extended-version", '!', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.extended_version, "deprecated", NULL }, { NULL } }; desc = "Examples:\n\n" "Query the configuration from the local node:\n\n" "\t# cibadmin --query --local\n\n" "Query just the cluster options configuration:\n\n" "\t# cibadmin --query --scope " PCMK_XE_CRM_CONFIG "\n\n" "Query all '" PCMK_META_TARGET_ROLE "' settings:\n\n" "\t# cibadmin --query --xpath " "\"//" PCMK_XE_NVPAIR "[@" PCMK_XA_NAME "='" PCMK_META_TARGET_ROLE"']\"\n\n" "Remove all '" PCMK_META_IS_MANAGED "' settings:\n\n" "\t# cibadmin --delete-all --xpath " "\"//" PCMK_XE_NVPAIR "[@" PCMK_XA_NAME "='" PCMK_META_IS_MANAGED "']\"\n\n" "Remove the resource named 'old':\n\n" "\t# cibadmin --delete --xml-text " "'<" PCMK_XE_PRIMITIVE " " PCMK_XA_ID "=\"old\"/>'\n\n" "Remove all resources from the configuration:\n\n" "\t# cibadmin --replace --scope " PCMK_XE_RESOURCES " --xml-text '<" PCMK_XE_RESOURCES "/>'\n\n" "Replace complete configuration with contents of " "$HOME/pacemaker.xml:\n\n" "\t# cibadmin --replace --xml-file $HOME/pacemaker.xml\n\n" "Replace " PCMK_XE_CONSTRAINTS " section of configuration with " "contents of $HOME/constraints.xml:\n\n" "\t# cibadmin --replace --scope " PCMK_XE_CONSTRAINTS " --xml-file $HOME/constraints.xml\n\n" "Increase configuration version to prevent old configurations from " "being loaded accidentally:\n\n" "\t# cibadmin --modify --xml-text " "'<" PCMK_XE_CIB " " PCMK_XA_ADMIN_EPOCH "=\"" PCMK_XA_ADMIN_EPOCH "++\"/>'\n\n" "Edit the configuration with your favorite $EDITOR:\n\n" "\t# cibadmin --query > $HOME/local.xml\n\n" "\t# $EDITOR $HOME/local.xml\n\n" "\t# cibadmin --replace --xml-file $HOME/local.xml\n\n" "Assuming terminal, render configuration in color (green for " "writable, blue for readable, red for\n" "denied) to visualize permissions for user tony:\n\n" "\t# cibadmin --show-access=color --query --user tony | less -r\n\n" "SEE ALSO:\n" " crm(8), pcs(8), crm_shadow(8), crm_diff(8)\n"; context = pcmk__build_arg_context(args, NULL, NULL, ""); g_option_context_set_description(context, desc); pcmk__add_main_args(context, extra_prog_entries); pcmk__add_arg_group(context, "commands", "Commands:", "Show command help", command_entries); pcmk__add_arg_group(context, "data", "Data:", "Show data help", data_entries); pcmk__add_arg_group(context, "additional", "Additional Options:", "Show additional options", addl_entries); return context; } int main(int argc, char **argv) { int rc = pcmk_rc_ok; const char *source = NULL; xmlNode *output = NULL; xmlNode *input = NULL; gchar *acl_cred = NULL; GError *error = NULL; pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY); gchar **processed_args = pcmk__cmdline_preproc(argv, "ANSUXhotx"); GOptionContext *context = build_arg_context(args); if (!g_option_context_parse_strv(context, &processed_args, &error)) { exit_code = CRM_EX_USAGE; goto done; } if (g_strv_length(processed_args) > 1) { gchar *help = g_option_context_get_help(context, TRUE, NULL); GString *extra = g_string_sized_new(128); for (int lpc = 1; processed_args[lpc] != NULL; lpc++) { if (extra->len > 0) { g_string_append_c(extra, ' '); } g_string_append(extra, processed_args[lpc]); } exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "non-option ARGV-elements: %s\n\n%s", extra->str, help); g_free(help); g_string_free(extra, TRUE); goto done; } if (args->version || options.extended_version) { g_strfreev(processed_args); pcmk__free_arg_context(context); /* FIXME: When cibadmin is converted to use formatted output, this can * be replaced by out->version with the appropriate boolean flag. * * options.extended_version is deprecated and will be removed in a * future release. */ pcmk__cli_help(options.extended_version? '!' : 'v'); } /* At LOG_ERR, stderr for CIB calls is rather verbose. Several lines like * * (func@file:line) error: CIB failures * * In cibadmin we explicitly output the XML portion without the prefixes. So * we default to LOG_CRIT. */ pcmk__cli_init_logging("cibadmin", 0); set_crm_log_level(LOG_CRIT); if (args->verbosity > 0) { cib__set_call_options(options.cmd_options, crm_system_name, cib_verbose); for (int i = 0; i < args->verbosity; i++) { crm_bump_log_level(argc, argv); } } if (options.cib_action == NULL) { // @COMPAT: Create a default command if other tools have one gchar *help = g_option_context_get_help(context, TRUE, NULL); exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must specify a command option\n\n%s", help); g_free(help); goto done; } if (strcmp(options.cib_action, "empty") == 0) { // Output an empty CIB char *buf = NULL; output = createEmptyCib(1); crm_xml_add(output, PCMK_XA_VALIDATE_WITH, options.validate_with); buf = dump_xml_formatted(output); fprintf(stdout, "%s", buf); free(buf); goto done; } if (cib_action_is_dangerous() && !options.force) { exit_code = CRM_EX_UNSAFE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "The supplied command is considered dangerous. To prevent " "accidental destruction of the cluster, the --force flag " "is required in order to proceed."); goto done; } if (options.message_timeout_sec < 1) { // Set default timeout options.message_timeout_sec = 30; } if (options.section_type == cibadmin_section_xpath) { // Enable getting section by XPath cib__set_call_options(options.cmd_options, crm_system_name, cib_xpath); } else if (options.section_type == cibadmin_section_scope) { if (!scope_is_valid(options.cib_section)) { // @COMPAT: Consider requiring --force to proceed fprintf(stderr, "Invalid value '%s' for '--scope'. Operation will apply " "to the entire CIB.\n", options.cib_section); } } if (options.allow_create) { // Allow target of --modify/-M to be created if it does not exist cib__set_call_options(options.cmd_options, crm_system_name, cib_can_create); } if (options.delete_all) { // With cibadmin_section_xpath, remove all matching objects cib__set_call_options(options.cmd_options, crm_system_name, cib_multiple); } if (options.get_node_path) { /* Enable getting node path of XPath query matches. * Meaningful only if options.section_type == cibadmin_section_xpath. */ cib__set_call_options(options.cmd_options, crm_system_name, cib_xpath_address); } if (options.local) { // Configure command to take effect only locally cib__set_call_options(options.cmd_options, crm_system_name, cib_scope_local); } // @COMPAT: Deprecated option if (options.no_bcast) { // Configure command to take effect only locally and not to broadcast cib__set_call_options(options.cmd_options, crm_system_name, cib_inhibit_bcast|cib_scope_local); } if (options.no_children) { // When querying an object, don't include its children in the result cib__set_call_options(options.cmd_options, crm_system_name, cib_no_children); } if (options.sync_call || (options.acl_render_mode != pcmk__acl_render_none)) { /* Wait for call to complete before returning. * * The ACL render modes work only with sync calls due to differences in * output handling between sync/async. It shouldn't matter to the user * whether the call is synchronous; for a CIB query, we have to wait for * the result in order to display it in any case. */ cib__set_call_options(options.cmd_options, crm_system_name, cib_sync_call); } if (options.input_file != NULL) { - input = filename2xml(options.input_file); + input = pcmk__xml_read(options.input_file); source = options.input_file; } else if (options.input_xml != NULL) { input = string2xml(options.input_xml); source = "input string"; } else if (options.input_stdin) { source = "STDIN"; input = stdin2xml(); } else if (options.acl_render_mode != pcmk__acl_render_none) { char *username = pcmk__uid2username(geteuid()); bool required = pcmk_acl_required(username); free(username); if (required) { if (options.force) { fprintf(stderr, "The supplied command can provide skewed" " result since it is run under user that also" " gets guarded per ACLs on their own right." " Continuing since --force flag was" " provided.\n"); } else { exit_code = CRM_EX_UNSAFE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "The supplied command can provide skewed result " "since it is run under user that also gets guarded " "per ACLs in their own right. To accept the risk " "of such a possible distortion (without even " "knowing it at this time), use the --force flag."); goto done; } } if (options.cib_user == NULL) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "The supplied command requires -U user specified."); goto done; } /* We already stopped/warned ACL-controlled users about consequences. * * Note: acl_cred takes ownership of options.cib_user here. * options.cib_user is set to NULL so that the CIB is obtained as the * user running the cibadmin command. The CIB must be obtained as a user * with full permissions in order to show the CIB correctly annotated * for the options.cib_user's permissions. */ acl_cred = options.cib_user; options.cib_user = NULL; } if (input != NULL) { crm_log_xml_debug(input, "[admin input]"); } else if (source != NULL) { exit_code = CRM_EX_CONFIG; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Couldn't parse input from %s.", source); goto done; } if (pcmk__str_eq(options.cib_action, "md5-sum", pcmk__str_casei)) { char *digest = NULL; if (input == NULL) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Please supply XML to process with -X, -x, or -p"); goto done; } digest = calculate_on_disk_digest(input); fprintf(stderr, "Digest: "); fprintf(stdout, "%s\n", pcmk__s(digest, "")); free(digest); goto done; } else if (strcmp(options.cib_action, "md5-sum-versioned") == 0) { char *digest = NULL; const char *version = NULL; if (input == NULL) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Please supply XML to process with -X, -x, or -p"); goto done; } version = crm_element_value(input, PCMK_XA_CRM_FEATURE_SET); digest = calculate_xml_versioned_digest(input, FALSE, TRUE, version); fprintf(stderr, "Versioned (%s) digest: ", version); fprintf(stdout, "%s\n", pcmk__s(digest, "")); free(digest); goto done; } rc = do_init(); if (rc != pcmk_ok) { rc = pcmk_legacy2rc(rc); exit_code = pcmk_rc2exitc(rc); crm_err("Init failed, could not perform requested operations: %s", pcmk_rc_str(rc)); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Init failed, could not perform requested operations: %s", pcmk_rc_str(rc)); goto done; } rc = do_work(input, &output); if (rc > 0) { /* wait for the reply by creating a mainloop and running it until * the callbacks are invoked... */ request_id = rc; the_cib->cmds->register_callback(the_cib, request_id, options.message_timeout_sec, FALSE, NULL, "cibadmin_op_callback", cibadmin_op_callback); mainloop = g_main_loop_new(NULL, FALSE); crm_trace("%s waiting for reply from the local CIB", crm_system_name); crm_info("Starting mainloop"); g_main_loop_run(mainloop); } else if ((rc == -pcmk_err_schema_unchanged) && (strcmp(options.cib_action, PCMK__CIB_REQUEST_UPGRADE) == 0)) { report_schema_unchanged(); } else if (rc < 0) { rc = pcmk_legacy2rc(rc); crm_err("Call failed: %s", pcmk_rc_str(rc)); fprintf(stderr, "Call failed: %s\n", pcmk_rc_str(rc)); if (rc == pcmk_rc_schema_validation) { if (strcmp(options.cib_action, PCMK__CIB_REQUEST_UPGRADE) == 0) { xmlNode *obj = NULL; int version = 0; if (the_cib->cmds->query(the_cib, NULL, &obj, options.cmd_options) == pcmk_ok) { update_validation(&obj, &version, 0, TRUE, FALSE); } free_xml(obj); } else if (output) { validate_xml_verbose(output); } } exit_code = pcmk_rc2exitc(rc); } if ((output != NULL) && (options.acl_render_mode != pcmk__acl_render_none)) { xmlDoc *acl_evaled_doc; rc = pcmk__acl_annotate_permissions(acl_cred, output->doc, &acl_evaled_doc); if (rc == pcmk_rc_ok) { xmlChar *rendered = NULL; rc = pcmk__acl_evaled_render(acl_evaled_doc, options.acl_render_mode, &rendered); if (rc != pcmk_rc_ok) { exit_code = CRM_EX_CONFIG; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not render evaluated access: %s", pcmk_rc_str(rc)); goto done; } printf("%s\n", (char *) rendered); free(rendered); } else { exit_code = CRM_EX_CONFIG; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not evaluate access per request (%s, error: %s)", acl_cred, pcmk_rc_str(rc)); goto done; } } else if (output != NULL) { print_xml_output(output); } crm_trace("%s exiting normally", crm_system_name); done: g_strfreev(processed_args); pcmk__free_arg_context(context); g_free(options.cib_user); g_free(options.dest_node); g_free(options.input_file); g_free(options.input_xml); free(options.cib_section); free(options.validate_with); g_free(acl_cred); free_xml(input); free_xml(output); rc = cib__clean_up_connection(&the_cib); if (exit_code == CRM_EX_OK) { exit_code = pcmk_rc2exitc(rc); } pcmk__output_and_clear_error(&error, NULL); crm_exit(exit_code); } static int do_work(xmlNode *input, xmlNode **output) { /* construct the request */ the_cib->call_timeout = options.message_timeout_sec; if ((strcmp(options.cib_action, PCMK__CIB_REQUEST_REPLACE) == 0) && pcmk__xe_is(input, PCMK_XE_CIB)) { xmlNode *status = pcmk_find_cib_element(input, PCMK_XE_STATUS); if (status == NULL) { create_xml_node(input, PCMK_XE_STATUS); } } crm_trace("Passing \"%s\" to variant_op...", options.cib_action); return cib_internal_op(the_cib, options.cib_action, options.dest_node, options.cib_section, input, output, options.cmd_options, options.cib_user); } int do_init(void) { int rc = pcmk_ok; the_cib = cib_new(); rc = the_cib->cmds->signon(the_cib, crm_system_name, cib_command); if (rc != pcmk_ok) { crm_err("Could not connect to the CIB: %s", pcmk_strerror(rc)); fprintf(stderr, "Could not connect to the CIB: %s\n", pcmk_strerror(rc)); } return rc; } void cibadmin_op_callback(xmlNode * msg, int call_id, int rc, xmlNode * output, void *user_data) { rc = pcmk_legacy2rc(rc); exit_code = pcmk_rc2exitc(rc); if (rc == pcmk_rc_schema_unchanged) { report_schema_unchanged(); } else if (rc != pcmk_rc_ok) { crm_warn("Call %s failed: %s " CRM_XS " rc=%d", options.cib_action, pcmk_rc_str(rc), rc); fprintf(stderr, "Call %s failed: %s\n", options.cib_action, pcmk_rc_str(rc)); print_xml_output(output); } else if ((strcmp(options.cib_action, PCMK__CIB_REQUEST_QUERY) == 0) && (output == NULL)) { crm_err("Query returned no output"); crm_log_xml_err(msg, "no output"); } else if (output == NULL) { crm_info("Call passed"); } else { crm_info("Call passed"); print_xml_output(output); } if (call_id == request_id) { g_main_loop_quit(mainloop); } else { crm_info("Message was not the response we were looking for (%d vs. %d)", call_id, request_id); } } diff --git a/tools/crm_diff.c b/tools/crm_diff.c index 547a926bcd..236617921b 100644 --- a/tools/crm_diff.c +++ b/tools/crm_diff.c @@ -1,379 +1,379 @@ /* * Copyright 2005-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define SUMMARY "Compare two Pacemaker configurations (in XML format) to produce a custom diff-like output, " \ "or apply such an output as a patch" struct { gboolean apply; gboolean as_cib; gboolean no_version; gboolean raw_1; gboolean raw_2; gboolean use_stdin; char *xml_file_1; char *xml_file_2; } options; gboolean new_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean original_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean patch_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); static GOptionEntry original_xml_entries[] = { { "original", 'o', 0, G_OPTION_ARG_STRING, &options.xml_file_1, "XML is contained in the named file", "FILE" }, { "original-string", 'O', 0, G_OPTION_ARG_CALLBACK, original_string_cb, "XML is contained in the supplied string", "STRING" }, { NULL } }; static GOptionEntry operation_entries[] = { { "new", 'n', 0, G_OPTION_ARG_STRING, &options.xml_file_2, "Compare the original XML to the contents of the named file", "FILE" }, { "new-string", 'N', 0, G_OPTION_ARG_CALLBACK, new_string_cb, "Compare the original XML with the contents of the supplied string", "STRING" }, { "patch", 'p', 0, G_OPTION_ARG_CALLBACK, patch_cb, "Patch the original XML with the contents of the named file", "FILE" }, { NULL } }; static GOptionEntry addl_entries[] = { { "cib", 'c', 0, G_OPTION_ARG_NONE, &options.as_cib, "Compare/patch the inputs as a CIB (includes versions details)", NULL }, { "stdin", 's', 0, G_OPTION_ARG_NONE, &options.use_stdin, "", NULL }, { "no-version", 'u', 0, G_OPTION_ARG_NONE, &options.no_version, "Generate the difference without versions details", NULL }, { NULL } }; gboolean new_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.raw_2 = TRUE; pcmk__str_update(&options.xml_file_2, optarg); return TRUE; } gboolean original_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.raw_1 = TRUE; pcmk__str_update(&options.xml_file_1, optarg); return TRUE; } gboolean patch_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.apply = TRUE; pcmk__str_update(&options.xml_file_2, optarg); return TRUE; } static void print_patch(xmlNode *patch) { char *buffer = dump_xml_formatted(patch); printf("%s", buffer); free(buffer); fflush(stdout); } // \return Standard Pacemaker return code static int apply_patch(xmlNode *input, xmlNode *patch, gboolean as_cib) { xmlNode *output = pcmk__xml_copy(NULL, input); int rc = xml_apply_patchset(output, patch, as_cib); rc = pcmk_legacy2rc(rc); if (rc != pcmk_rc_ok) { fprintf(stderr, "Could not apply patch: %s\n", pcmk_rc_str(rc)); free_xml(output); return rc; } if (output != NULL) { const char *version; char *buffer; print_patch(output); version = crm_element_value(output, PCMK_XA_CRM_FEATURE_SET); buffer = calculate_xml_versioned_digest(output, FALSE, TRUE, version); crm_trace("Digest: %s", pcmk__s(buffer, "\n")); free(buffer); free_xml(output); } return pcmk_rc_ok; } static void log_patch_cib_versions(xmlNode *patch) { int add[] = { 0, 0, 0 }; int del[] = { 0, 0, 0 }; const char *fmt = NULL; const char *digest = NULL; xml_patch_versions(patch, add, del); fmt = crm_element_value(patch, PCMK_XA_FORMAT); digest = crm_element_value(patch, PCMK__XA_DIGEST); if (add[2] != del[2] || add[1] != del[1] || add[0] != del[0]) { crm_info("Patch: --- %d.%d.%d %s", del[0], del[1], del[2], fmt); crm_info("Patch: +++ %d.%d.%d %s", add[0], add[1], add[2], digest); } } static void strip_patch_cib_version(xmlNode *patch, const char **vfields, size_t nvfields) { int format = 1; crm_element_value_int(patch, PCMK_XA_FORMAT, &format); if (format == 2) { xmlNode *version_xml = find_xml_node(patch, PCMK_XE_VERSION, FALSE); if (version_xml) { free_xml(version_xml); } } else { int i = 0; const char *tags[] = { PCMK__XE_DIFF_REMOVED, PCMK__XE_DIFF_ADDED, }; for (i = 0; i < PCMK__NELEM(tags); i++) { xmlNode *tmp = NULL; int lpc; tmp = find_xml_node(patch, tags[i], FALSE); if (tmp) { for (lpc = 0; lpc < nvfields; lpc++) { xml_remove_prop(tmp, vfields[lpc]); } tmp = find_xml_node(tmp, PCMK_XE_CIB, FALSE); if (tmp) { for (lpc = 0; lpc < nvfields; lpc++) { xml_remove_prop(tmp, vfields[lpc]); } } } } } } // \return Standard Pacemaker return code static int generate_patch(xmlNode *object_1, xmlNode *object_2, const char *xml_file_2, gboolean as_cib, gboolean no_version) { const char *vfields[] = { PCMK_XA_ADMIN_EPOCH, PCMK_XA_EPOCH, PCMK_XA_NUM_UPDATES, }; xmlNode *output = NULL; /* If we're ignoring the version, make the version information * identical, so it isn't detected as a change. */ if (no_version) { int lpc; for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { crm_copy_xml_element(object_1, object_2, vfields[lpc]); } } xml_track_changes(object_2, NULL, object_2, FALSE); if(as_cib) { xml_calculate_significant_changes(object_1, object_2); } else { xml_calculate_changes(object_1, object_2); } crm_log_xml_debug(object_2, (xml_file_2? xml_file_2: "target")); output = xml_create_patchset(0, object_1, object_2, NULL, FALSE); pcmk__log_xml_changes(LOG_INFO, object_2); xml_accept_changes(object_2); if (output == NULL) { return pcmk_rc_ok; // No changes } patchset_process_digest(output, object_1, object_2, as_cib); if (as_cib) { log_patch_cib_versions(output); } else if (no_version) { strip_patch_cib_version(output, vfields, PCMK__NELEM(vfields)); } pcmk__log_xml_patchset(LOG_NOTICE, output); print_patch(output); free_xml(output); /* pcmk_rc_error means there's a non-empty diff. * @COMPAT Choose a more descriptive return code, like one that maps to * CRM_EX_DIGEST? */ return pcmk_rc_error; } static GOptionContext * build_arg_context(pcmk__common_args_t *args) { GOptionContext *context = NULL; const char *description = "Examples:\n\n" "Obtain the two different configuration files by running cibadmin on the two cluster setups to compare:\n\n" "\t# cibadmin --query > cib-old.xml\n\n" "\t# cibadmin --query > cib-new.xml\n\n" "Calculate and save the difference between the two files:\n\n" "\t# crm_diff --original cib-old.xml --new cib-new.xml > patch.xml\n\n" "Apply the patch to the original file:\n\n" "\t# crm_diff --original cib-old.xml --patch patch.xml > updated.xml\n\n" "Apply the patch to the running cluster:\n\n" "\t# cibadmin --patch -x patch.xml\n"; context = pcmk__build_arg_context(args, NULL, NULL, NULL); g_option_context_set_description(context, description); pcmk__add_arg_group(context, "xml", "Original XML:", "Show original XML options", original_xml_entries); pcmk__add_arg_group(context, "operation", "Operation:", "Show operation options", operation_entries); pcmk__add_arg_group(context, "additional", "Additional Options:", "Show additional options", addl_entries); return context; } int main(int argc, char **argv) { xmlNode *object_1 = NULL; xmlNode *object_2 = NULL; crm_exit_t exit_code = CRM_EX_OK; GError *error = NULL; pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY); gchar **processed_args = pcmk__cmdline_preproc(argv, "nopNO"); GOptionContext *context = build_arg_context(args); int rc = pcmk_rc_ok; if (!g_option_context_parse_strv(context, &processed_args, &error)) { exit_code = CRM_EX_USAGE; goto done; } pcmk__cli_init_logging("crm_diff", args->verbosity); if (args->version) { g_strfreev(processed_args); pcmk__free_arg_context(context); /* FIXME: When crm_diff is converted to use formatted output, this can go. */ pcmk__cli_help('v'); } if (options.apply && options.no_version) { fprintf(stderr, "warning: -u/--no-version ignored with -p/--patch\n"); } else if (options.as_cib && options.no_version) { fprintf(stderr, "error: -u/--no-version incompatible with -c/--cib\n"); exit_code = CRM_EX_USAGE; goto done; } if (options.raw_1) { object_1 = string2xml(options.xml_file_1); } else if (options.use_stdin) { fprintf(stderr, "Input first XML fragment:"); object_1 = stdin2xml(); } else if (options.xml_file_1 != NULL) { - object_1 = filename2xml(options.xml_file_1); + object_1 = pcmk__xml_read(options.xml_file_1); } if (options.raw_2) { object_2 = string2xml(options.xml_file_2); } else if (options.use_stdin) { fprintf(stderr, "Input second XML fragment:"); object_2 = stdin2xml(); } else if (options.xml_file_2 != NULL) { - object_2 = filename2xml(options.xml_file_2); + object_2 = pcmk__xml_read(options.xml_file_2); } if (object_1 == NULL) { fprintf(stderr, "Could not parse the first XML fragment\n"); exit_code = CRM_EX_DATAERR; goto done; } if (object_2 == NULL) { fprintf(stderr, "Could not parse the second XML fragment\n"); exit_code = CRM_EX_DATAERR; goto done; } if (options.apply) { rc = apply_patch(object_1, object_2, options.as_cib); } else { rc = generate_patch(object_1, object_2, options.xml_file_2, options.as_cib, options.no_version); } exit_code = pcmk_rc2exitc(rc); done: g_strfreev(processed_args); pcmk__free_arg_context(context); free(options.xml_file_1); free(options.xml_file_2); free_xml(object_1); free_xml(object_2); pcmk__output_and_clear_error(&error, NULL); crm_exit(exit_code); } diff --git a/tools/crm_resource.c b/tools/crm_resource.c index d51d412646..9514229080 100644 --- a/tools/crm_resource.c +++ b/tools/crm_resource.c @@ -1,2101 +1,2101 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define SUMMARY "crm_resource - perform tasks related to Pacemaker cluster resources" enum rsc_command { cmd_none = 0, // No command option given (yet) cmd_ban, cmd_cleanup, cmd_clear, cmd_colocations, cmd_cts, cmd_delete, cmd_delete_param, cmd_digests, cmd_execute_agent, cmd_fail, cmd_get_param, cmd_get_property, cmd_list_active_ops, cmd_list_agents, cmd_list_all_ops, cmd_list_alternatives, cmd_list_instances, cmd_list_providers, cmd_list_resources, cmd_list_standards, cmd_locate, cmd_metadata, cmd_move, cmd_query_raw_xml, cmd_query_xml, cmd_refresh, cmd_restart, cmd_set_param, cmd_set_property, cmd_wait, cmd_why, }; struct { enum rsc_command rsc_cmd; // crm_resource command to perform // Infrastructure that given command needs to work gboolean require_cib; // Whether command requires CIB IPC int cib_options; // Options to use with CIB IPC calls gboolean require_crmd; // Whether command requires controller IPC gboolean require_scheduler; // Whether command requires scheduler data gboolean require_resource; // Whether command requires resource specified gboolean require_node; // Whether command requires node specified int find_flags; // Flags to use when searching for resource // Command-line option values gchar *rsc_id; // Value of --resource gchar *rsc_type; // Value of --resource-type gboolean force; // --force was given gboolean clear_expired; // --expired was given gboolean recursive; // --recursive was given gboolean promoted_role_only; // --promoted was given gchar *host_uname; // Value of --node gchar *interval_spec; // Value of --interval gchar *move_lifetime; // Value of --lifetime gchar *operation; // Value of --operation const char *attr_set_type; // Instance, meta, utilization, or element attribute gchar *prop_id; // --nvpair (attribute XML ID) char *prop_name; // Attribute name gchar *prop_set; // --set-name (attribute block XML ID) gchar *prop_value; // --parameter-value (attribute value) long long timeout_ms; // Parsed from --timeout value char *agent_spec; // Standard and/or provider and/or agent gchar *xml_file; // Value of (deprecated) --xml-file int check_level; // Optional value of --validate or --force-check // Resource configuration specified via command-line arguments gboolean cmdline_config; // Resource configuration was via arguments char *v_agent; // Value of --agent char *v_class; // Value of --class char *v_provider; // Value of --provider GHashTable *cmdline_params; // Resource parameters specified // Positional command-line arguments gchar **remainder; // Positional arguments as given GHashTable *override_params; // Resource parameter values that override config } options = { .attr_set_type = PCMK_XE_INSTANCE_ATTRIBUTES, .check_level = -1, .cib_options = cib_sync_call, .require_cib = TRUE, .require_scheduler = TRUE, .require_resource = TRUE, }; #if 0 // @COMPAT @TODO enable this at next backward compatibility break #define SET_COMMAND(cmd) do { \ if (options.rsc_cmd != cmd_none) { \ g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_USAGE, \ "Only one command option may be specified"); \ return FALSE; \ } \ options.rsc_cmd = (cmd); \ } while (0) #else #define SET_COMMAND(cmd) do { \ if (options.rsc_cmd != cmd_none) { \ reset_options(); \ } \ options.rsc_cmd = (cmd); \ } while (0) #endif gboolean agent_provider_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean attr_set_type_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean class_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean cleanup_refresh_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean delete_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean expired_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean list_agents_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean list_providers_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean list_standards_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean list_alternatives_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean metadata_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean option_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean fail_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean flag_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean get_param_prop_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean list_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean set_delete_param_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean set_prop_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean timeout_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean validate_or_force_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean restart_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean digests_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean wait_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); gboolean why_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error); static crm_exit_t exit_code = CRM_EX_OK; static pcmk__output_t *out = NULL; static pcmk__common_args_t *args = NULL; // Things that should be cleaned up on exit static GError *error = NULL; static GMainLoop *mainloop = NULL; static cib_t *cib_conn = NULL; static pcmk_ipc_api_t *controld_api = NULL; static pcmk_scheduler_t *scheduler = NULL; #define MESSAGE_TIMEOUT_S 60 #define INDENT " " static pcmk__supported_format_t formats[] = { PCMK__SUPPORTED_FORMAT_NONE, PCMK__SUPPORTED_FORMAT_TEXT, PCMK__SUPPORTED_FORMAT_XML, { NULL, NULL, NULL } }; // Clean up and exit static crm_exit_t bye(crm_exit_t ec) { pcmk__output_and_clear_error(&error, out); if (out != NULL) { out->finish(out, ec, true, NULL); pcmk__output_free(out); } pcmk__unregister_formats(); if (cib_conn != NULL) { cib_t *save_cib_conn = cib_conn; cib_conn = NULL; // Ensure we can't free this twice cib__clean_up_connection(&save_cib_conn); } if (controld_api != NULL) { pcmk_ipc_api_t *save_controld_api = controld_api; controld_api = NULL; // Ensure we can't free this twice pcmk_free_ipc_api(save_controld_api); } if (mainloop != NULL) { g_main_loop_unref(mainloop); mainloop = NULL; } pe_free_working_set(scheduler); scheduler = NULL; crm_exit(ec); return ec; } static void quit_main_loop(crm_exit_t ec) { exit_code = ec; if (mainloop != NULL) { GMainLoop *mloop = mainloop; mainloop = NULL; // Don't re-enter this block pcmk_quit_main_loop(mloop, 10); g_main_loop_unref(mloop); } } static gboolean resource_ipc_timeout(gpointer data) { // Start with newline because "Waiting for ..." message doesn't have one if (error != NULL) { g_clear_error(&error); } g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_TIMEOUT, _("Aborting because no messages received in %d seconds"), MESSAGE_TIMEOUT_S); quit_main_loop(CRM_EX_TIMEOUT); return FALSE; } static void controller_event_callback(pcmk_ipc_api_t *api, enum pcmk_ipc_event event_type, crm_exit_t status, void *event_data, void *user_data) { switch (event_type) { case pcmk_ipc_event_disconnect: if (exit_code == CRM_EX_DISCONNECT) { // Unexpected crm_info("Connection to controller was terminated"); } quit_main_loop(exit_code); break; case pcmk_ipc_event_reply: if (status != CRM_EX_OK) { out->err(out, "Error: bad reply from controller: %s", crm_exit_str(status)); pcmk_disconnect_ipc(api); quit_main_loop(status); } else { if ((pcmk_controld_api_replies_expected(api) == 0) && mainloop && g_main_loop_is_running(mainloop)) { out->info(out, "... got reply (done)"); crm_debug("Got all the replies we expected"); pcmk_disconnect_ipc(api); quit_main_loop(CRM_EX_OK); } else { out->info(out, "... got reply"); } } break; default: break; } } static void start_mainloop(pcmk_ipc_api_t *capi) { unsigned int count = pcmk_controld_api_replies_expected(capi); if (count > 0) { out->info(out, "Waiting for %u %s from the controller", count, pcmk__plural_alt(count, "reply", "replies")); exit_code = CRM_EX_DISCONNECT; // For unexpected disconnects mainloop = g_main_loop_new(NULL, FALSE); g_timeout_add(MESSAGE_TIMEOUT_S * 1000, resource_ipc_timeout, NULL); g_main_loop_run(mainloop); } } static int compare_id(gconstpointer a, gconstpointer b) { return strcmp((const char *)a, (const char *)b); } static GList * build_constraint_list(xmlNode *root) { GList *retval = NULL; xmlNode *cib_constraints = NULL; xmlXPathObjectPtr xpathObj = NULL; int ndx = 0; cib_constraints = pcmk_find_cib_element(root, PCMK_XE_CONSTRAINTS); xpathObj = xpath_search(cib_constraints, "//" PCMK_XE_RSC_LOCATION); for (ndx = 0; ndx < numXpathResults(xpathObj); ndx++) { xmlNode *match = getXpathResult(xpathObj, ndx); retval = g_list_insert_sorted(retval, (gpointer) pcmk__xe_id(match), compare_id); } freeXpathObject(xpathObj); return retval; } /* short option letters still available: eEJkKXyYZ */ static GOptionEntry query_entries[] = { { "list", 'L', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, list_cb, "List all cluster resources with status", NULL }, { "list-raw", 'l', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, list_cb, "List IDs of all instantiated resources (individual members\n" INDENT "rather than groups etc.)", NULL }, { "list-cts", 'c', G_OPTION_FLAG_HIDDEN|G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, list_cb, NULL, NULL }, { "list-operations", 'O', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, list_cb, "List active resource operations, optionally filtered by\n" INDENT "--resource and/or --node", NULL }, { "list-all-operations", 'o', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, list_cb, "List all resource operations, optionally filtered by\n" INDENT "--resource and/or --node", NULL }, { "list-standards", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, list_standards_cb, "List supported standards", NULL }, { "list-ocf-providers", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, list_providers_cb, "List all available OCF providers", NULL }, { "list-agents", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, list_agents_cb, "List all agents available for the named standard and/or provider", "STD:PROV" }, { "list-ocf-alternatives", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, list_alternatives_cb, "List all available providers for the named OCF agent", "AGENT" }, { "show-metadata", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, metadata_cb, "Show the metadata for the named class:provider:agent", "SPEC" }, { "query-xml", 'q', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, flag_cb, "Show XML configuration of resource (after any template expansion)", NULL }, { "query-xml-raw", 'w', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, flag_cb, "Show XML configuration of resource (before any template expansion)", NULL }, { "get-parameter", 'g', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, get_param_prop_cb, "Display named parameter for resource (use instance attribute\n" INDENT "unless --element, --meta, or --utilization is specified)", "PARAM" }, { "get-property", 'G', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_CALLBACK, get_param_prop_cb, "Display named property of resource ('class', 'type', or 'provider') " "(requires --resource)", "PROPERTY" }, { "locate", 'W', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, flag_cb, "Show node(s) currently running resource", NULL }, { "constraints", 'a', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, flag_cb, "Display the location and colocation constraints that apply to a\n" INDENT "resource, and if --recursive is specified, to the resources\n" INDENT "directly or indirectly involved in those colocations.\n" INDENT "If the named resource is part of a group, or a clone or\n" INDENT "bundle instance, constraints for the collective resource\n" INDENT "will be shown unless --force is given.", NULL }, { "stack", 'A', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, flag_cb, "Equivalent to --constraints --recursive", NULL }, { "why", 'Y', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, why_cb, "Show why resources are not running, optionally filtered by\n" INDENT "--resource and/or --node", NULL }, { NULL } }; static GOptionEntry command_entries[] = { { "validate", 0, G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, validate_or_force_cb, "Validate resource configuration by calling agent's validate-all\n" INDENT "action. The configuration may be specified either by giving an\n" INDENT "existing resource name with -r, or by specifying --class,\n" INDENT "--agent, and --provider arguments, along with any number of\n" INDENT "--option arguments. An optional LEVEL argument can be given\n" INDENT "to control the level of checking performed.", "LEVEL" }, { "cleanup", 'C', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, cleanup_refresh_cb, "If resource has any past failures, clear its history and fail\n" INDENT "count. Optionally filtered by --resource, --node, --operation\n" INDENT "and --interval (otherwise all). --operation and --interval\n" INDENT "apply to fail counts, but entire history is always clear, to\n" INDENT "allow current state to be rechecked. If the named resource is\n" INDENT "part of a group, or one numbered instance of a clone or bundled\n" INDENT "resource, the clean-up applies to the whole collective resource\n" INDENT "unless --force is given.", NULL }, { "refresh", 'R', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, cleanup_refresh_cb, "Delete resource's history (including failures) so its current state\n" INDENT "is rechecked. Optionally filtered by --resource and --node\n" INDENT "(otherwise all). If the named resource is part of a group, or one\n" INDENT "numbered instance of a clone or bundled resource, the refresh\n" INDENT "applies to the whole collective resource unless --force is given.", NULL }, { "set-parameter", 'p', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, set_delete_param_cb, "Set named parameter for resource (requires -v). Use instance\n" INDENT "attribute unless --element, --meta, or --utilization is " "specified.", "PARAM" }, { "delete-parameter", 'd', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, set_delete_param_cb, "Delete named parameter for resource. Use instance attribute\n" INDENT "unless --element, --meta or, --utilization is specified.", "PARAM" }, { "set-property", 'S', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_CALLBACK, set_prop_cb, "Set named property of resource ('class', 'type', or 'provider') " "(requires -r, -t, -v)", "PROPERTY" }, { NULL } }; static GOptionEntry location_entries[] = { { "move", 'M', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, flag_cb, "Create a constraint to move resource. If --node is specified,\n" INDENT "the constraint will be to move to that node, otherwise it\n" INDENT "will be to ban the current node. Unless --force is specified\n" INDENT "this will return an error if the resource is already running\n" INDENT "on the specified node. If --force is specified, this will\n" INDENT "always ban the current node.\n" INDENT "Optional: --lifetime, --promoted. NOTE: This may prevent the\n" INDENT "resource from running on its previous location until the\n" INDENT "implicit constraint expires or is removed with --clear.", NULL }, { "ban", 'B', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, flag_cb, "Create a constraint to keep resource off a node.\n" INDENT "Optional: --node, --lifetime, --promoted.\n" INDENT "NOTE: This will prevent the resource from running on the\n" INDENT "affected node until the implicit constraint expires or is\n" INDENT "removed with --clear. If --node is not specified, it defaults\n" INDENT "to the node currently running the resource for primitives\n" INDENT "and groups, or the promoted instance of promotable clones with\n" INDENT PCMK_META_PROMOTED_MAX "=1 (all other situations result in an\n" INDENT "error as there is no sane default).", NULL }, { "clear", 'U', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, flag_cb, "Remove all constraints created by the --ban and/or --move\n" INDENT "commands. Requires: --resource. Optional: --node, --promoted,\n" INDENT "--expired. If --node is not specified, all constraints created\n" INDENT "by --ban and --move will be removed for the named resource. If\n" INDENT "--node and --force are specified, any constraint created by\n" INDENT "--move will be cleared, even if it is not for the specified\n" INDENT "node. If --expired is specified, only those constraints whose\n" INDENT "lifetimes have expired will be removed.", NULL }, { "expired", 'e', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, expired_cb, "Modifies the --clear argument to remove constraints with\n" INDENT "expired lifetimes.", NULL }, { "lifetime", 'u', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.move_lifetime, "Lifespan (as ISO 8601 duration) of created constraints (with\n" INDENT "-B, -M) see https://en.wikipedia.org/wiki/ISO_8601#Durations)", "TIMESPEC" }, { "promoted", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.promoted_role_only, "Limit scope of command to promoted role (with -B, -M, -U). For\n" INDENT "-B and -M, previously promoted instances may remain\n" INDENT "active in the unpromoted role.", NULL }, // Deprecated since 2.1.0 { "master", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.promoted_role_only, "Deprecated: Use --promoted instead", NULL }, { NULL } }; static GOptionEntry advanced_entries[] = { { "delete", 'D', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, delete_cb, "(Advanced) Delete a resource from the CIB. Required: -t", NULL }, { "fail", 'F', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, fail_cb, "(Advanced) Tell the cluster this resource has failed", NULL }, { "restart", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, restart_cb, "(Advanced) Tell the cluster to restart this resource and\n" INDENT "anything that depends on it", NULL }, { "wait", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, wait_cb, "(Advanced) Wait until the cluster settles into a stable state", NULL }, { "digests", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, digests_cb, "(Advanced) Show parameter hashes that Pacemaker uses to detect\n" INDENT "configuration changes (only accurate if there is resource\n" INDENT "history on the specified node). Required: --resource, --node.\n" INDENT "Optional: any NAME=VALUE parameters will be used to override\n" INDENT "the configuration (to see what the hash would be with those\n" INDENT "changes).", NULL }, { "force-demote", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, validate_or_force_cb, "(Advanced) Bypass the cluster and demote a resource on the local\n" INDENT "node. Unless --force is specified, this will refuse to do so if\n" INDENT "the cluster believes the resource is a clone instance already\n" INDENT "running on the local node.", NULL }, { "force-stop", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, validate_or_force_cb, "(Advanced) Bypass the cluster and stop a resource on the local node", NULL }, { "force-start", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, validate_or_force_cb, "(Advanced) Bypass the cluster and start a resource on the local\n" INDENT "node. Unless --force is specified, this will refuse to do so if\n" INDENT "the cluster believes the resource is a clone instance already\n" INDENT "running on the local node.", NULL }, { "force-promote", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, validate_or_force_cb, "(Advanced) Bypass the cluster and promote a resource on the local\n" INDENT "node. Unless --force is specified, this will refuse to do so if\n" INDENT "the cluster believes the resource is a clone instance already\n" INDENT "running on the local node.", NULL }, { "force-check", 0, G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, validate_or_force_cb, "(Advanced) Bypass the cluster and check the state of a resource on\n" INDENT "the local node. An optional LEVEL argument can be given\n" INDENT "to control the level of checking performed.", "LEVEL" }, { NULL } }; static GOptionEntry addl_entries[] = { { "node", 'N', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.host_uname, "Node name", "NAME" }, { "recursive", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.recursive, "Follow colocation chains when using --set-parameter or --constraints", NULL }, { "resource-type", 't', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.rsc_type, "Resource XML element (primitive, group, etc.) (with -D)", "ELEMENT" }, { "parameter-value", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.prop_value, "Value to use with -p", "PARAM" }, { "meta", 'm', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, attr_set_type_cb, "Use resource meta-attribute instead of instance attribute\n" INDENT "(with -p, -g, -d)", NULL }, { "utilization", 'z', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, attr_set_type_cb, "Use resource utilization attribute instead of instance attribute\n" INDENT "(with -p, -g, -d)", NULL }, { "element", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, attr_set_type_cb, "Use resource element attribute instead of instance attribute\n" INDENT "(with -p, -g, -d)", NULL }, { "operation", 'n', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.operation, "Operation to clear instead of all (with -C -r)", "OPERATION" }, { "interval", 'I', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.interval_spec, "Interval of operation to clear (default 0) (with -C -r -n)", "N" }, { "class", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, class_cb, "The standard the resource agent conforms to (for example, ocf).\n" INDENT "Use with --agent, --provider, --option, and --validate.", "CLASS" }, { "agent", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, agent_provider_cb, "The agent to use (for example, IPaddr). Use with --class,\n" INDENT "--provider, --option, and --validate.", "AGENT" }, { "provider", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, agent_provider_cb, "The vendor that supplies the resource agent (for example,\n" INDENT "heartbeat). Use with --class, --agent, --option, and --validate.", "PROVIDER" }, { "option", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, option_cb, "Specify a device configuration parameter as NAME=VALUE (may be\n" INDENT "specified multiple times). Use with --validate and without the\n" INDENT "-r option.", "PARAM" }, { "set-name", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.prop_set, "(Advanced) XML ID of attributes element to use (with -p, -d)", "ID" }, { "nvpair", 'i', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.prop_id, "(Advanced) XML ID of nvpair element to use (with -p, -d)", "ID" }, { "timeout", 'T', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, timeout_cb, "(Advanced) Abort if command does not finish in this time (with\n" INDENT "--restart, --wait, --force-*)", "N" }, { "force", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.force, "Force the action to be performed. See help for individual commands for\n" INDENT "additional behavior.", NULL }, { "xml-file", 'x', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_FILENAME, &options.xml_file, NULL, "FILE" }, { "host-uname", 'H', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &options.host_uname, NULL, "HOST" }, { NULL } }; static void reset_options(void) { options.require_crmd = FALSE; options.require_node = FALSE; options.require_cib = TRUE; options.require_scheduler = TRUE; options.require_resource = TRUE; options.find_flags = 0; } gboolean agent_provider_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.cmdline_config = TRUE; options.require_resource = FALSE; if (pcmk__str_eq(option_name, "--provider", pcmk__str_casei)) { pcmk__str_update(&options.v_provider, optarg); } else { pcmk__str_update(&options.v_agent, optarg); } return TRUE; } gboolean attr_set_type_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { if (pcmk__str_any_of(option_name, "-m", "--meta", NULL)) { options.attr_set_type = PCMK_XE_META_ATTRIBUTES; } else if (pcmk__str_any_of(option_name, "-z", "--utilization", NULL)) { options.attr_set_type = PCMK_XE_UTILIZATION; } else if (pcmk__str_eq(option_name, "--element", pcmk__str_casei)) { options.attr_set_type = ATTR_SET_ELEMENT; } return TRUE; } gboolean class_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { pcmk__str_update(&options.v_class, optarg); options.cmdline_config = TRUE; options.require_resource = FALSE; return TRUE; } gboolean cleanup_refresh_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { if (pcmk__str_any_of(option_name, "-C", "--cleanup", NULL)) { SET_COMMAND(cmd_cleanup); } else { SET_COMMAND(cmd_refresh); } options.require_resource = FALSE; if (getenv("CIB_file") == NULL) { options.require_crmd = TRUE; } options.find_flags = pcmk_rsc_match_history|pcmk_rsc_match_anon_basename; return TRUE; } gboolean delete_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { SET_COMMAND(cmd_delete); options.require_scheduler = FALSE; options.find_flags = pcmk_rsc_match_history|pcmk_rsc_match_basename; return TRUE; } gboolean expired_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.clear_expired = TRUE; options.require_resource = FALSE; return TRUE; } static void get_agent_spec(const gchar *optarg) { options.require_cib = FALSE; options.require_scheduler = FALSE; options.require_resource = FALSE; pcmk__str_update(&options.agent_spec, optarg); } gboolean list_agents_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { SET_COMMAND(cmd_list_agents); get_agent_spec(optarg); return TRUE; } gboolean list_providers_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { SET_COMMAND(cmd_list_providers); get_agent_spec(optarg); return TRUE; } gboolean list_standards_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { SET_COMMAND(cmd_list_standards); options.require_cib = FALSE; options.require_scheduler = FALSE; options.require_resource = FALSE; return TRUE; } gboolean list_alternatives_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { SET_COMMAND(cmd_list_alternatives); get_agent_spec(optarg); return TRUE; } gboolean metadata_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { SET_COMMAND(cmd_metadata); get_agent_spec(optarg); return TRUE; } gboolean option_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { char *name = NULL; char *value = NULL; if (pcmk__scan_nvpair(optarg, &name, &value) != 2) { return FALSE; } if (options.cmdline_params == NULL) { options.cmdline_params = pcmk__strkey_table(free, free); } g_hash_table_replace(options.cmdline_params, name, value); return TRUE; } gboolean fail_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { SET_COMMAND(cmd_fail); options.require_crmd = TRUE; options.require_node = TRUE; return TRUE; } gboolean flag_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { if (pcmk__str_any_of(option_name, "-U", "--clear", NULL)) { SET_COMMAND(cmd_clear); options.find_flags = pcmk_rsc_match_history |pcmk_rsc_match_anon_basename; } else if (pcmk__str_any_of(option_name, "-B", "--ban", NULL)) { SET_COMMAND(cmd_ban); options.find_flags = pcmk_rsc_match_history |pcmk_rsc_match_anon_basename; } else if (pcmk__str_any_of(option_name, "-M", "--move", NULL)) { SET_COMMAND(cmd_move); options.find_flags = pcmk_rsc_match_history |pcmk_rsc_match_anon_basename; } else if (pcmk__str_any_of(option_name, "-q", "--query-xml", NULL)) { SET_COMMAND(cmd_query_xml); options.find_flags = pcmk_rsc_match_history|pcmk_rsc_match_basename; } else if (pcmk__str_any_of(option_name, "-w", "--query-xml-raw", NULL)) { SET_COMMAND(cmd_query_raw_xml); options.find_flags = pcmk_rsc_match_history|pcmk_rsc_match_basename; } else if (pcmk__str_any_of(option_name, "-W", "--locate", NULL)) { SET_COMMAND(cmd_locate); options.find_flags = pcmk_rsc_match_history |pcmk_rsc_match_anon_basename; } else if (pcmk__str_any_of(option_name, "-a", "--constraints", NULL)) { SET_COMMAND(cmd_colocations); options.find_flags = pcmk_rsc_match_history |pcmk_rsc_match_anon_basename; } else if (pcmk__str_any_of(option_name, "-A", "--stack", NULL)) { SET_COMMAND(cmd_colocations); options.find_flags = pcmk_rsc_match_history |pcmk_rsc_match_anon_basename; options.recursive = TRUE; } return TRUE; } gboolean get_param_prop_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { if (pcmk__str_any_of(option_name, "-g", "--get-parameter", NULL)) { SET_COMMAND(cmd_get_param); } else { SET_COMMAND(cmd_get_property); } pcmk__str_update(&options.prop_name, optarg); options.find_flags = pcmk_rsc_match_history|pcmk_rsc_match_basename; return TRUE; } gboolean list_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { if (pcmk__str_any_of(option_name, "-c", "--list-cts", NULL)) { SET_COMMAND(cmd_cts); } else if (pcmk__str_any_of(option_name, "-L", "--list", NULL)) { SET_COMMAND(cmd_list_resources); } else if (pcmk__str_any_of(option_name, "-l", "--list-raw", NULL)) { SET_COMMAND(cmd_list_instances); } else if (pcmk__str_any_of(option_name, "-O", "--list-operations", NULL)) { SET_COMMAND(cmd_list_active_ops); } else { SET_COMMAND(cmd_list_all_ops); } options.require_resource = FALSE; return TRUE; } gboolean set_delete_param_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { if (pcmk__str_any_of(option_name, "-p", "--set-parameter", NULL)) { SET_COMMAND(cmd_set_param); } else { SET_COMMAND(cmd_delete_param); } pcmk__str_update(&options.prop_name, optarg); options.find_flags = pcmk_rsc_match_history|pcmk_rsc_match_basename; return TRUE; } gboolean set_prop_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { SET_COMMAND(cmd_set_property); options.require_scheduler = FALSE; pcmk__str_update(&options.prop_name, optarg); options.find_flags = pcmk_rsc_match_history|pcmk_rsc_match_basename; return TRUE; } gboolean timeout_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.timeout_ms = crm_get_msec(optarg); if (options.timeout_ms < 0) { crm_warn("Ignoring invalid timeout '%s'", optarg); options.timeout_ms = 0; } else { options.timeout_ms = QB_MIN(options.timeout_ms, INT_MAX); } return TRUE; } gboolean validate_or_force_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { SET_COMMAND(cmd_execute_agent); if (options.operation) { g_free(options.operation); } options.operation = g_strdup(option_name + 2); // skip "--" options.find_flags = pcmk_rsc_match_history|pcmk_rsc_match_anon_basename; if (options.override_params == NULL) { options.override_params = pcmk__strkey_table(free, free); } if (optarg != NULL) { if (pcmk__scan_min_int(optarg, &options.check_level, 0) != pcmk_rc_ok) { g_set_error(error, G_OPTION_ERROR, CRM_EX_INVALID_PARAM, _("Invalid check level setting: %s"), optarg); return FALSE; } } return TRUE; } gboolean restart_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { SET_COMMAND(cmd_restart); options.find_flags = pcmk_rsc_match_history|pcmk_rsc_match_anon_basename; return TRUE; } gboolean digests_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { SET_COMMAND(cmd_digests); options.find_flags = pcmk_rsc_match_history|pcmk_rsc_match_anon_basename; if (options.override_params == NULL) { options.override_params = pcmk__strkey_table(free, free); } options.require_node = TRUE; options.require_scheduler = TRUE; return TRUE; } gboolean wait_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { SET_COMMAND(cmd_wait); options.require_resource = FALSE; options.require_scheduler = FALSE; return TRUE; } gboolean why_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { SET_COMMAND(cmd_why); options.require_resource = FALSE; options.find_flags = pcmk_rsc_match_history|pcmk_rsc_match_anon_basename; return TRUE; } static int ban_or_move(pcmk__output_t *out, pcmk_resource_t *rsc, const char *move_lifetime) { int rc = pcmk_rc_ok; pcmk_node_t *current = NULL; unsigned int nactive = 0; CRM_CHECK(rsc != NULL, return EINVAL); current = pe__find_active_requires(rsc, &nactive); if (nactive == 1) { rc = cli_resource_ban(out, options.rsc_id, current->details->uname, move_lifetime, cib_conn, options.cib_options, options.promoted_role_only, PCMK__ROLE_PROMOTED); } else if (pcmk_is_set(rsc->flags, pcmk_rsc_promotable)) { int count = 0; GList *iter = NULL; current = NULL; for(iter = rsc->children; iter; iter = iter->next) { pcmk_resource_t *child = (pcmk_resource_t *)iter->data; enum rsc_role_e child_role = child->fns->state(child, TRUE); if (child_role == pcmk_role_promoted) { count++; current = pcmk__current_node(child); } } if(count == 1 && current) { rc = cli_resource_ban(out, options.rsc_id, current->details->uname, move_lifetime, cib_conn, options.cib_options, options.promoted_role_only, PCMK__ROLE_PROMOTED); } else { rc = EINVAL; g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE, _("Resource '%s' not moved: active in %d locations (promoted in %d).\n" "To prevent '%s' from running on a specific location, " "specify a node." "To prevent '%s' from being promoted at a specific " "location, specify a node and the --promoted option."), options.rsc_id, nactive, count, options.rsc_id, options.rsc_id); } } else { rc = EINVAL; g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE, _("Resource '%s' not moved: active in %d locations.\n" "To prevent '%s' from running on a specific location, " "specify a node."), options.rsc_id, nactive, options.rsc_id); } return rc; } static void cleanup(pcmk__output_t *out, pcmk_resource_t *rsc, pcmk_node_t *node) { int rc = pcmk_rc_ok; if (options.force == FALSE) { rsc = uber_parent(rsc); } crm_debug("Erasing failures of %s (%s requested) on %s", rsc->id, options.rsc_id, (options.host_uname? options.host_uname: "all nodes")); rc = cli_resource_delete(controld_api, options.host_uname, rsc, options.operation, options.interval_spec, TRUE, scheduler, options.force); if ((rc == pcmk_rc_ok) && !out->is_quiet(out)) { // Show any reasons why resource might stay stopped cli_resource_check(out, rsc, node); } if (rc == pcmk_rc_ok) { start_mainloop(controld_api); } } static int clear_constraints(pcmk__output_t *out, xmlNodePtr *cib_xml_copy) { GList *before = NULL; GList *after = NULL; GList *remaining = NULL; GList *ele = NULL; pcmk_node_t *dest = NULL; int rc = pcmk_rc_ok; if (!out->is_quiet(out)) { before = build_constraint_list(scheduler->input); } if (options.clear_expired) { rc = cli_resource_clear_all_expired(scheduler->input, cib_conn, options.cib_options, options.rsc_id, options.host_uname, options.promoted_role_only); } else if (options.host_uname) { dest = pe_find_node(scheduler->nodes, options.host_uname); if (dest == NULL) { rc = pcmk_rc_node_unknown; if (!out->is_quiet(out)) { g_list_free(before); } return rc; } rc = cli_resource_clear(options.rsc_id, dest->details->uname, NULL, cib_conn, options.cib_options, TRUE, options.force); } else { rc = cli_resource_clear(options.rsc_id, NULL, scheduler->nodes, cib_conn, options.cib_options, TRUE, options.force); } if (!out->is_quiet(out)) { rc = cib_conn->cmds->query(cib_conn, NULL, cib_xml_copy, cib_scope_local | cib_sync_call); rc = pcmk_legacy2rc(rc); if (rc != pcmk_rc_ok) { g_set_error(&error, PCMK__RC_ERROR, rc, _("Could not get modified CIB: %s\n"), pcmk_rc_str(rc)); g_list_free(before); free_xml(*cib_xml_copy); *cib_xml_copy = NULL; return rc; } scheduler->input = *cib_xml_copy; cluster_status(scheduler); after = build_constraint_list(scheduler->input); remaining = pcmk__subtract_lists(before, after, (GCompareFunc) strcmp); for (ele = remaining; ele != NULL; ele = ele->next) { out->info(out, "Removing constraint: %s", (char *) ele->data); } g_list_free(before); g_list_free(after); g_list_free(remaining); } return rc; } static int initialize_scheduler_data(xmlNodePtr *cib_xml_copy) { int rc = pcmk_rc_ok; if (options.xml_file != NULL) { - *cib_xml_copy = filename2xml(options.xml_file); + *cib_xml_copy = pcmk__xml_read(options.xml_file); if (*cib_xml_copy == NULL) { rc = pcmk_rc_cib_corrupt; } } else { rc = cib_conn->cmds->query(cib_conn, NULL, cib_xml_copy, cib_scope_local | cib_sync_call); rc = pcmk_legacy2rc(rc); } if (rc == pcmk_rc_ok) { scheduler = pe_new_working_set(); if (scheduler == NULL) { rc = ENOMEM; } else { pcmk__set_scheduler_flags(scheduler, pcmk_sched_no_counts |pcmk_sched_no_compat); scheduler->priv = out; rc = update_scheduler_input(scheduler, cib_xml_copy); } } if (rc != pcmk_rc_ok) { free_xml(*cib_xml_copy); *cib_xml_copy = NULL; return rc; } cluster_status(scheduler); return pcmk_rc_ok; } static int refresh(pcmk__output_t *out) { int rc = pcmk_rc_ok; const char *router_node = options.host_uname; int attr_options = pcmk__node_attr_none; if (options.host_uname) { pcmk_node_t *node = pe_find_node(scheduler->nodes, options.host_uname); if (pcmk__is_pacemaker_remote_node(node)) { node = pcmk__current_node(node->details->remote_rsc); if (node == NULL) { rc = ENXIO; g_set_error(&error, PCMK__RC_ERROR, rc, _("No cluster connection to Pacemaker Remote node %s detected"), options.host_uname); return rc; } router_node = node->details->uname; attr_options |= pcmk__node_attr_remote; } } if (controld_api == NULL) { out->info(out, "Dry run: skipping clean-up of %s due to CIB_file", options.host_uname? options.host_uname : "all nodes"); rc = pcmk_rc_ok; return rc; } crm_debug("Re-checking the state of all resources on %s", options.host_uname?options.host_uname:"all nodes"); rc = pcmk__attrd_api_clear_failures(NULL, options.host_uname, NULL, NULL, NULL, NULL, attr_options); if (pcmk_controld_api_reprobe(controld_api, options.host_uname, router_node) == pcmk_rc_ok) { start_mainloop(controld_api); } return rc; } static void refresh_resource(pcmk__output_t *out, pcmk_resource_t *rsc, pcmk_node_t *node) { int rc = pcmk_rc_ok; if (options.force == FALSE) { rsc = uber_parent(rsc); } crm_debug("Re-checking the state of %s (%s requested) on %s", rsc->id, options.rsc_id, (options.host_uname? options.host_uname: "all nodes")); rc = cli_resource_delete(controld_api, options.host_uname, rsc, NULL, 0, FALSE, scheduler, options.force); if ((rc == pcmk_rc_ok) && !out->is_quiet(out)) { // Show any reasons why resource might stay stopped cli_resource_check(out, rsc, node); } if (rc == pcmk_rc_ok) { start_mainloop(controld_api); } } static int set_property(void) { int rc = pcmk_rc_ok; xmlNode *msg_data = NULL; if (pcmk__str_empty(options.rsc_type)) { g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE, _("Must specify -t with resource type")); rc = ENXIO; return rc; } else if (pcmk__str_empty(options.prop_value)) { g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE, _("Must supply -v with new value")); rc = ENXIO; return rc; } CRM_LOG_ASSERT(options.prop_name != NULL); msg_data = create_xml_node(NULL, options.rsc_type); crm_xml_add(msg_data, PCMK_XA_ID, options.rsc_id); crm_xml_add(msg_data, options.prop_name, options.prop_value); rc = cib_conn->cmds->modify(cib_conn, PCMK_XE_RESOURCES, msg_data, options.cib_options); rc = pcmk_legacy2rc(rc); free_xml(msg_data); return rc; } static int show_metadata(pcmk__output_t *out, const char *agent_spec) { int rc = pcmk_rc_ok; char *standard = NULL; char *provider = NULL; char *type = NULL; char *metadata = NULL; lrmd_t *lrmd_conn = NULL; rc = lrmd__new(&lrmd_conn, NULL, NULL, 0); if (rc != pcmk_rc_ok) { g_set_error(&error, PCMK__RC_ERROR, rc, _("Could not create executor connection")); lrmd_api_delete(lrmd_conn); return rc; } rc = crm_parse_agent_spec(agent_spec, &standard, &provider, &type); rc = pcmk_legacy2rc(rc); if (rc == pcmk_rc_ok) { rc = lrmd_conn->cmds->get_metadata(lrmd_conn, standard, provider, type, &metadata, 0); rc = pcmk_legacy2rc(rc); if (metadata) { out->output_xml(out, PCMK_XE_METADATA, metadata); free(metadata); } else { /* We were given a validly formatted spec, but it doesn't necessarily * match up with anything that exists. Use ENXIO as the return code * here because that maps to an exit code of CRM_EX_NOSUCH, which * probably is the most common reason to get here. */ rc = ENXIO; g_set_error(&error, PCMK__RC_ERROR, rc, _("Metadata query for %s failed: %s"), agent_spec, pcmk_rc_str(rc)); } } else { rc = ENXIO; g_set_error(&error, PCMK__RC_ERROR, rc, _("'%s' is not a valid agent specification"), agent_spec); } lrmd_api_delete(lrmd_conn); return rc; } static void validate_cmdline_config(void) { // Cannot use both --resource and command-line resource configuration if (options.rsc_id != NULL) { g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE, _("--resource cannot be used with --class, --agent, and --provider")); // Not all commands support command-line resource configuration } else if (options.rsc_cmd != cmd_execute_agent) { g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE, _("--class, --agent, and --provider can only be used with " "--validate and --force-*")); // Not all of --class, --agent, and --provider need to be given. Not all // classes support the concept of a provider. Check that what we were given // is valid. } else if (pcmk__str_eq(options.v_class, "stonith", pcmk__str_none)) { if (options.v_provider != NULL) { g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE, _("stonith does not support providers")); } else if (stonith_agent_exists(options.v_agent, 0) == FALSE) { g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE, _("%s is not a known stonith agent"), options.v_agent ? options.v_agent : ""); } } else if (resources_agent_exists(options.v_class, options.v_provider, options.v_agent) == FALSE) { g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE, _("%s:%s:%s is not a known resource"), options.v_class ? options.v_class : "", options.v_provider ? options.v_provider : "", options.v_agent ? options.v_agent : ""); } if (error != NULL) { return; } if (options.cmdline_params == NULL) { options.cmdline_params = pcmk__strkey_table(free, free); } options.require_resource = FALSE; options.require_scheduler = FALSE; options.require_cib = FALSE; } static GOptionContext * build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) { GOptionContext *context = NULL; GOptionEntry extra_prog_entries[] = { { "quiet", 'Q', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &(args->quiet), "Be less descriptive in output.", NULL }, { "resource", 'r', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.rsc_id, "Resource ID", "ID" }, { G_OPTION_REMAINING, 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING_ARRAY, &options.remainder, NULL, NULL }, { NULL } }; const char *description = "Examples:\n\n" "List the available OCF agents:\n\n" "\t# crm_resource --list-agents ocf\n\n" "List the available OCF agents from the linux-ha project:\n\n" "\t# crm_resource --list-agents ocf:heartbeat\n\n" "Move 'myResource' to a specific node:\n\n" "\t# crm_resource --resource myResource --move --node altNode\n\n" "Allow (but not force) 'myResource' to move back to its original " "location:\n\n" "\t# crm_resource --resource myResource --clear\n\n" "Stop 'myResource' (and anything that depends on it):\n\n" "\t# crm_resource --resource myResource --set-parameter " PCMK_META_TARGET_ROLE "--meta --parameter-value Stopped\n\n" "Tell the cluster not to manage 'myResource' (the cluster will not " "attempt to start or stop the\n" "resource under any circumstances; useful when performing maintenance " "tasks on a resource):\n\n" "\t# crm_resource --resource myResource --set-parameter " PCMK_META_IS_MANAGED "--meta --parameter-value false\n\n" "Erase the operation history of 'myResource' on 'aNode' (the cluster " "will 'forget' the existing\n" "resource state, including any errors, and attempt to recover the" "resource; useful when a resource\n" "had failed permanently and has been repaired by an administrator):\n\n" "\t# crm_resource --resource myResource --cleanup --node aNode\n\n"; context = pcmk__build_arg_context(args, "text (default), xml", group, NULL); g_option_context_set_description(context, description); /* Add the -Q option, which cannot be part of the globally supported options * because some tools use that flag for something else. */ pcmk__add_main_args(context, extra_prog_entries); pcmk__add_arg_group(context, "queries", "Queries:", "Show query help", query_entries); pcmk__add_arg_group(context, "commands", "Commands:", "Show command help", command_entries); pcmk__add_arg_group(context, "locations", "Locations:", "Show location help", location_entries); pcmk__add_arg_group(context, "advanced", "Advanced:", "Show advanced option help", advanced_entries); pcmk__add_arg_group(context, "additional", "Additional Options:", "Show additional options", addl_entries); return context; } int main(int argc, char **argv) { xmlNode *cib_xml_copy = NULL; pcmk_resource_t *rsc = NULL; pcmk_node_t *node = NULL; int rc = pcmk_rc_ok; GOptionGroup *output_group = NULL; gchar **processed_args = NULL; GOptionContext *context = NULL; /* * Parse command line arguments */ args = pcmk__new_common_args(SUMMARY); processed_args = pcmk__cmdline_preproc(argv, "GHINSTdginpstuvx"); context = build_arg_context(args, &output_group); 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("crm_resource", 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; } pe__register_messages(out); crm_resource_register_messages(out); lrmd__register_messages(out); pcmk__register_lib_messages(out); out->quiet = args->quiet; crm_log_args(argc, argv); /* * Validate option combinations */ // If the user didn't explicitly specify a command, list resources if (options.rsc_cmd == cmd_none) { options.rsc_cmd = cmd_list_resources; options.require_resource = FALSE; } // --expired without --clear/-U doesn't make sense if (options.clear_expired && (options.rsc_cmd != cmd_clear)) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, _("--expired requires --clear or -U")); goto done; } if ((options.remainder != NULL) && (options.override_params != NULL)) { // Commands that use positional arguments will create override_params for (gchar **s = options.remainder; *s; s++) { char *name = calloc(1, strlen(*s)); char *value = calloc(1, strlen(*s)); int rc = sscanf(*s, "%[^=]=%s", name, value); if (rc == 2) { g_hash_table_replace(options.override_params, name, value); } else { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, _("Error parsing '%s' as a name=value pair"), argv[optind]); free(value); free(name); goto done; } } } else if (options.remainder != NULL) { gchar **strv = NULL; gchar *msg = NULL; int i = 1; int len = 0; for (gchar **s = options.remainder; *s; s++) { len++; } CRM_ASSERT(len > 0); /* Add 1 for the strv[0] string below, and add another 1 for the NULL * at the end of the array so g_strjoinv knows when to stop. */ strv = calloc(len+2, sizeof(char *)); strv[0] = strdup("non-option ARGV-elements:\n"); for (gchar **s = options.remainder; *s; s++) { strv[i] = crm_strdup_printf("[%d of %d] %s\n", i, len, *s); i++; } strv[i] = NULL; exit_code = CRM_EX_USAGE; msg = g_strjoinv("", strv); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "%s", msg); g_free(msg); /* Don't try to free the last element, which is just NULL. */ for(i = 0; i < len+1; i++) { free(strv[i]); } free(strv); goto done; } if (pcmk__str_eq(args->output_ty, "xml", pcmk__str_none)) { switch (options.rsc_cmd) { /* These are the only commands that have historically used the * elements in their XML schema. For all others, use the simple list * argument. */ case cmd_get_param: case cmd_get_property: case cmd_list_instances: case cmd_list_standards: pcmk__output_enable_list_element(out); break; default: break; } } else if (pcmk__str_eq(args->output_ty, "text", pcmk__str_null_matches)) { switch (options.rsc_cmd) { case cmd_colocations: case cmd_list_resources: pcmk__output_text_set_fancy(out, true); break; default: break; } } if (args->version) { out->version(out, false); goto done; } if (options.cmdline_config) { /* A resource configuration was given on the command line. Sanity-check * the values and set error if they don't make sense. */ validate_cmdline_config(); if (error != NULL) { exit_code = CRM_EX_USAGE; goto done; } } else if (options.cmdline_params != NULL) { // @COMPAT @TODO error out here when we can break backward compatibility g_hash_table_destroy(options.cmdline_params); options.cmdline_params = NULL; } if (options.require_resource && (options.rsc_id == NULL)) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, _("Must supply a resource id with -r")); goto done; } if (options.require_node && (options.host_uname == NULL)) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, _("Must supply a node name with -N")); goto done; } /* * Set up necessary connections */ if (options.find_flags && options.rsc_id) { options.require_scheduler = TRUE; } // Establish a connection to the CIB if needed if (options.require_cib) { cib_conn = cib_new(); if ((cib_conn == NULL) || (cib_conn->cmds == NULL)) { exit_code = CRM_EX_DISCONNECT; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, _("Could not create CIB connection")); goto done; } rc = cib_conn->cmds->signon(cib_conn, crm_system_name, cib_command); rc = pcmk_legacy2rc(rc); if (rc != pcmk_rc_ok) { exit_code = pcmk_rc2exitc(rc); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, _("Could not connect to the CIB: %s"), pcmk_rc_str(rc)); goto done; } } // Populate scheduler data from XML file if specified or CIB query otherwise if (options.require_scheduler) { rc = initialize_scheduler_data(&cib_xml_copy); if (rc != pcmk_rc_ok) { exit_code = pcmk_rc2exitc(rc); goto done; } } // If command requires that resource exist if specified, find it if (options.find_flags && options.rsc_id) { rsc = pe_find_resource_with_flags(scheduler->resources, options.rsc_id, options.find_flags); if (rsc == NULL) { exit_code = CRM_EX_NOSUCH; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, _("Resource '%s' not found"), options.rsc_id); goto done; } /* The --ban, --clear, --move, and --restart commands do not work with * instances of clone resourcs. */ if (pcmk__is_clone(rsc->parent) && (strchr(options.rsc_id, ':') != NULL) && ((options.rsc_cmd == cmd_ban) || (options.rsc_cmd == cmd_clear) || (options.rsc_cmd == cmd_move) || (options.rsc_cmd == cmd_restart))) { exit_code = CRM_EX_INVALID_PARAM; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, _("Cannot operate on clone resource instance '%s'"), options.rsc_id); goto done; } } // If user supplied a node name, check whether it exists if ((options.host_uname != NULL) && (scheduler != NULL)) { node = pe_find_node(scheduler->nodes, options.host_uname); if (node == NULL) { exit_code = CRM_EX_NOSUCH; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, _("Node '%s' not found"), options.host_uname); goto done; } } // Establish a connection to the controller if needed if (options.require_crmd) { rc = pcmk_new_ipc_api(&controld_api, pcmk_ipc_controld); if (rc != pcmk_rc_ok) { exit_code = pcmk_rc2exitc(rc); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, _("Error connecting to the controller: %s"), pcmk_rc_str(rc)); goto done; } pcmk_register_ipc_callback(controld_api, controller_event_callback, NULL); rc = pcmk__connect_ipc(controld_api, pcmk_ipc_dispatch_main, 5); if (rc != pcmk_rc_ok) { exit_code = pcmk_rc2exitc(rc); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, _("Error connecting to %s: %s"), pcmk_ipc_name(controld_api, true), pcmk_rc_str(rc)); goto done; } } /* * Handle requested command */ switch (options.rsc_cmd) { case cmd_list_resources: { GList *all = NULL; uint32_t show_opts = pcmk_show_inactive_rscs | pcmk_show_rsc_only | pcmk_show_pending; all = g_list_prepend(all, (gpointer) "*"); rc = out->message(out, "resource-list", scheduler, show_opts, true, all, all, false); g_list_free(all); if (rc == pcmk_rc_no_output) { rc = ENXIO; } break; } case cmd_list_instances: rc = out->message(out, "resource-names-list", scheduler->resources); if (rc != pcmk_rc_ok) { rc = ENXIO; } break; case cmd_list_alternatives: rc = pcmk__list_alternatives(out, options.agent_spec); break; case cmd_list_agents: rc = pcmk__list_agents(out, options.agent_spec); break; case cmd_list_standards: rc = pcmk__list_standards(out); break; case cmd_list_providers: rc = pcmk__list_providers(out, options.agent_spec); break; case cmd_metadata: rc = show_metadata(out, options.agent_spec); break; case cmd_restart: /* We don't pass scheduler because rsc needs to stay valid for the * entire lifetime of cli_resource_restart(), but it will reset and * update the scheduler data multiple times, so it needs to use its * own copy. */ rc = cli_resource_restart(out, rsc, node, options.move_lifetime, options.timeout_ms, cib_conn, options.cib_options, options.promoted_role_only, options.force); break; case cmd_wait: rc = wait_till_stable(out, options.timeout_ms, cib_conn); break; case cmd_execute_agent: if (options.cmdline_config) { exit_code = cli_resource_execute_from_params(out, NULL, options.v_class, options.v_provider, options.v_agent, options.operation, options.cmdline_params, options.override_params, options.timeout_ms, args->verbosity, options.force, options.check_level); } else { exit_code = cli_resource_execute(rsc, options.rsc_id, options.operation, options.override_params, options.timeout_ms, cib_conn, scheduler, args->verbosity, options.force, options.check_level); } goto done; case cmd_digests: node = pe_find_node(scheduler->nodes, options.host_uname); if (node == NULL) { rc = pcmk_rc_node_unknown; } else { rc = pcmk__resource_digests(out, rsc, node, options.override_params); } break; case cmd_colocations: rc = out->message(out, "locations-and-colocations", rsc, options.recursive, (bool) options.force); break; case cmd_cts: rc = pcmk_rc_ok; g_list_foreach(scheduler->resources, (GFunc) cli_resource_print_cts, out); cli_resource_print_cts_constraints(scheduler); break; case cmd_fail: rc = cli_resource_fail(controld_api, options.host_uname, options.rsc_id, scheduler); if (rc == pcmk_rc_ok) { start_mainloop(controld_api); } break; case cmd_list_active_ops: rc = cli_resource_print_operations(options.rsc_id, options.host_uname, TRUE, scheduler); break; case cmd_list_all_ops: rc = cli_resource_print_operations(options.rsc_id, options.host_uname, FALSE, scheduler); break; case cmd_locate: { GList *nodes = cli_resource_search(rsc, options.rsc_id, scheduler); rc = out->message(out, "resource-search-list", nodes, options.rsc_id); g_list_free_full(nodes, free); break; } case cmd_query_xml: rc = cli_resource_print(rsc, scheduler, true); break; case cmd_query_raw_xml: rc = cli_resource_print(rsc, scheduler, false); break; case cmd_why: if ((options.host_uname != NULL) && (node == NULL)) { rc = pcmk_rc_node_unknown; } else { rc = out->message(out, "resource-reasons-list", scheduler->resources, rsc, node); } break; case cmd_clear: rc = clear_constraints(out, &cib_xml_copy); break; case cmd_move: if (options.host_uname == NULL) { rc = ban_or_move(out, rsc, options.move_lifetime); } else { rc = cli_resource_move(rsc, options.rsc_id, options.host_uname, options.move_lifetime, cib_conn, options.cib_options, scheduler, options.promoted_role_only, options.force); } if (rc == EINVAL) { exit_code = CRM_EX_USAGE; goto done; } break; case cmd_ban: if (options.host_uname == NULL) { rc = ban_or_move(out, rsc, options.move_lifetime); } else if (node == NULL) { rc = pcmk_rc_node_unknown; } else { rc = cli_resource_ban(out, options.rsc_id, node->details->uname, options.move_lifetime, cib_conn, options.cib_options, options.promoted_role_only, PCMK__ROLE_PROMOTED); } if (rc == EINVAL) { exit_code = CRM_EX_USAGE; goto done; } break; case cmd_get_property: rc = out->message(out, "property-list", rsc, options.prop_name); if (rc == pcmk_rc_no_output) { rc = ENXIO; } break; case cmd_set_property: rc = set_property(); break; case cmd_get_param: { unsigned int count = 0; GHashTable *params = NULL; pcmk_node_t *current = rsc->fns->active_node(rsc, &count, NULL); bool free_params = true; const char* value = NULL; if (count > 1) { out->err(out, "%s is active on more than one node," " returning the default value for %s", rsc->id, pcmk__s(options.prop_name, "unspecified property")); current = NULL; } crm_debug("Looking up %s in %s", options.prop_name, rsc->id); if (pcmk__str_eq(options.attr_set_type, PCMK_XE_INSTANCE_ATTRIBUTES, pcmk__str_none)) { params = pe_rsc_params(rsc, current, scheduler); free_params = false; value = g_hash_table_lookup(params, options.prop_name); } else if (pcmk__str_eq(options.attr_set_type, PCMK_XE_META_ATTRIBUTES, pcmk__str_none)) { params = pcmk__strkey_table(free, free); get_meta_attributes(params, rsc, current, scheduler); value = g_hash_table_lookup(params, options.prop_name); } else if (pcmk__str_eq(options.attr_set_type, ATTR_SET_ELEMENT, pcmk__str_none)) { value = crm_element_value(rsc->xml, options.prop_name); free_params = false; } else { params = pcmk__strkey_table(free, free); pe__unpack_dataset_nvpairs(rsc->xml, PCMK_XE_UTILIZATION, NULL, params, NULL, FALSE, scheduler); value = g_hash_table_lookup(params, options.prop_name); } rc = out->message(out, "attribute-list", rsc, options.prop_name, value); if (free_params) { g_hash_table_destroy(params); } break; } case cmd_set_param: if (pcmk__str_empty(options.prop_value)) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, _("You need to supply a value with the -v option")); goto done; } /* coverity[var_deref_model] False positive */ rc = cli_resource_update_attribute(rsc, options.rsc_id, options.prop_set, options.attr_set_type, options.prop_id, options.prop_name, options.prop_value, options.recursive, cib_conn, options.cib_options, options.force); break; case cmd_delete_param: /* coverity[var_deref_model] False positive */ rc = cli_resource_delete_attribute(rsc, options.rsc_id, options.prop_set, options.attr_set_type, options.prop_id, options.prop_name, cib_conn, options.cib_options, options.force); break; case cmd_cleanup: if (rsc == NULL) { rc = cli_cleanup_all(controld_api, options.host_uname, options.operation, options.interval_spec, scheduler); if (rc == pcmk_rc_ok) { start_mainloop(controld_api); } } else { cleanup(out, rsc, node); } break; case cmd_refresh: if (rsc == NULL) { rc = refresh(out); } else { refresh_resource(out, rsc, node); } break; case cmd_delete: /* rsc_id was already checked for NULL much earlier when validating * command line arguments. */ if (options.rsc_type == NULL) { // @COMPAT @TODO change this to exit_code = CRM_EX_USAGE rc = ENXIO; g_set_error(&error, PCMK__RC_ERROR, rc, _("You need to specify a resource type with -t")); } else { rc = pcmk__resource_delete(cib_conn, options.cib_options, options.rsc_id, options.rsc_type); if (rc != pcmk_rc_ok) { g_set_error(&error, PCMK__RC_ERROR, rc, _("Could not delete resource %s: %s"), options.rsc_id, pcmk_rc_str(rc)); } } break; default: exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, _("Unimplemented command: %d"), (int) options.rsc_cmd); goto done; } /* Convert rc into an exit code. */ if (rc != pcmk_rc_ok && rc != pcmk_rc_no_output) { exit_code = pcmk_rc2exitc(rc); } /* * Clean up and exit */ done: /* When we get here, exit_code has been set one of two ways - either at one of * the spots where there's a "goto done" (which itself could have happened either * directly or by calling pcmk_rc2exitc), or just up above after any of the break * statements. * * Thus, we can use just exit_code here to decide what to do. */ if (exit_code != CRM_EX_OK && exit_code != CRM_EX_USAGE) { if (error != NULL) { char *msg = crm_strdup_printf("%s\nError performing operation: %s", error->message, crm_exit_str(exit_code)); g_clear_error(&error); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "%s", msg); free(msg); } else { g_set_error(&error, PCMK__EXITC_ERROR, exit_code, _("Error performing operation: %s"), crm_exit_str(exit_code)); } } g_free(options.host_uname); g_free(options.interval_spec); g_free(options.move_lifetime); g_free(options.operation); g_free(options.prop_id); free(options.prop_name); g_free(options.prop_set); g_free(options.prop_value); g_free(options.rsc_id); g_free(options.rsc_type); free(options.agent_spec); free(options.v_agent); free(options.v_class); free(options.v_provider); g_free(options.xml_file); g_strfreev(options.remainder); if (options.override_params != NULL) { g_hash_table_destroy(options.override_params); } /* options.cmdline_params does not need to be destroyed here. See the * comments in cli_resource_execute_from_params. */ g_strfreev(processed_args); g_option_context_free(context); return bye(exit_code); } diff --git a/tools/crm_shadow.c b/tools/crm_shadow.c index c85bde4d72..4655777714 100644 --- a/tools/crm_shadow.c +++ b/tools/crm_shadow.c @@ -1,1308 +1,1308 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define SUMMARY "perform Pacemaker configuration changes in a sandbox\n\n" \ "This command sets up an environment in which " \ "configuration tools (cibadmin,\n" \ "crm_resource, etc.) work offline instead of against a " \ "live cluster, allowing\n" \ "changes to be previewed and tested for side effects." #define INDENT " " enum shadow_command { shadow_cmd_none = 0, shadow_cmd_which, shadow_cmd_display, shadow_cmd_diff, shadow_cmd_file, shadow_cmd_create, shadow_cmd_create_empty, shadow_cmd_commit, shadow_cmd_delete, shadow_cmd_edit, shadow_cmd_reset, shadow_cmd_switch, }; /*! * \internal * \enum shadow_disp_flags * \brief Bit flags to control which fields of shadow CIB info are displayed * * \note Ignored for XML output. */ enum shadow_disp_flags { shadow_disp_instance = (1 << 0), shadow_disp_file = (1 << 1), shadow_disp_content = (1 << 2), shadow_disp_diff = (1 << 3), }; static crm_exit_t exit_code = CRM_EX_OK; static struct { enum shadow_command cmd; int cmd_options; char *instance; gboolean force; gboolean batch; gboolean full_upload; gchar *validate_with; } options = { .cmd_options = cib_sync_call, }; /*! * \internal * \brief Display an instruction to the user * * \param[in,out] out Output object * \param[in] args Message-specific arguments * * \return Standard Pacemaker return code * * \note \p args should contain the following: * -# Instructional message */ PCMK__OUTPUT_ARGS("instruction", "const char *") static int instruction_default(pcmk__output_t *out, va_list args) { const char *msg = va_arg(args, const char *); if (msg == NULL) { return pcmk_rc_no_output; } return out->info(out, "%s", msg); } /*! * \internal * \brief Display an instruction to the user * * \param[in,out] out Output object * \param[in] args Message-specific arguments * * \return Standard Pacemaker return code * * \note \p args should contain the following: * -# Instructional message */ PCMK__OUTPUT_ARGS("instruction", "const char *") static int instruction_xml(pcmk__output_t *out, va_list args) { const char *msg = va_arg(args, const char *); if (msg == NULL) { return pcmk_rc_no_output; } pcmk__output_create_xml_text_node(out, "instruction", msg); return pcmk_rc_ok; } /*! * \internal * \brief Display information about a shadow CIB instance * * \param[in,out] out Output object * \param[in] args Message-specific arguments * * \return Standard Pacemaker return code * * \note \p args should contain the following: * -# Instance name (can be \p NULL) * -# Shadow file name (can be \p NULL) * -# Shadow file content (can be \p NULL) * -# Patchset containing the changes in the shadow CIB (can be \p NULL) * -# Group of \p shadow_disp_flags indicating which fields to display */ PCMK__OUTPUT_ARGS("shadow", "const char *", "const char *", "const xmlNode *", "const xmlNode *", "enum shadow_disp_flags") static int shadow_default(pcmk__output_t *out, va_list args) { const char *instance = va_arg(args, const char *); const char *filename = va_arg(args, const char *); const xmlNode *content = va_arg(args, const xmlNode *); const xmlNode *diff = va_arg(args, const xmlNode *); enum shadow_disp_flags flags = (enum shadow_disp_flags) va_arg(args, int); int rc = pcmk_rc_no_output; if (pcmk_is_set(flags, shadow_disp_instance)) { rc = out->info(out, "Instance: %s", pcmk__s(instance, "")); } if (pcmk_is_set(flags, shadow_disp_file)) { rc = out->info(out, "File name: %s", pcmk__s(filename, "")); } if (pcmk_is_set(flags, shadow_disp_content)) { rc = out->info(out, "Content:"); if (content != NULL) { char *buf = pcmk__trim(dump_xml_formatted_with_text(content)); if (!pcmk__str_empty(buf)) { out->info(out, "%s", buf); } free(buf); } else { out->info(out, ""); } } if (pcmk_is_set(flags, shadow_disp_diff)) { rc = out->info(out, "Diff:"); if (diff != NULL) { out->message(out, "xml-patchset", diff); } else { out->info(out, ""); } } return rc; } /*! * \internal * \brief Display information about a shadow CIB instance * * \param[in,out] out Output object * \param[in] args Message-specific arguments * * \return Standard Pacemaker return code * * \note \p args should contain the following: * -# Instance name (can be \p NULL) * -# Shadow file name (can be \p NULL) * -# Shadow file content (can be \p NULL) * -# Patchset containing the changes in the shadow CIB (can be \p NULL) * -# Group of \p shadow_disp_flags indicating which fields to display */ PCMK__OUTPUT_ARGS("shadow", "const char *", "const char *", "const xmlNode *", "const xmlNode *", "enum shadow_disp_flags") static int shadow_text(pcmk__output_t *out, va_list args) { if (!out->is_quiet(out)) { return shadow_default(out, args); } else { const char *instance = va_arg(args, const char *); const char *filename = va_arg(args, const char *); const xmlNode *content = va_arg(args, const xmlNode *); const xmlNode *diff = va_arg(args, const xmlNode *); enum shadow_disp_flags flags = (enum shadow_disp_flags) va_arg(args, int); int rc = pcmk_rc_no_output; bool quiet_orig = out->quiet; /* We have to disable quiet mode for the "xml-patchset" message if we * call it, so we might as well do so for this whole section. */ out->quiet = false; if (pcmk_is_set(flags, shadow_disp_instance) && (instance != NULL)) { rc = out->info(out, "%s", instance); } if (pcmk_is_set(flags, shadow_disp_file) && (filename != NULL)) { rc = out->info(out, "%s", filename); } if (pcmk_is_set(flags, shadow_disp_content) && (content != NULL)) { char *buf = pcmk__trim(dump_xml_formatted_with_text(content)); rc = out->info(out, "%s", pcmk__trim(buf)); free(buf); } if (pcmk_is_set(flags, shadow_disp_diff) && (diff != NULL)) { rc = out->message(out, "xml-patchset", diff); } out->quiet = quiet_orig; return rc; } } /*! * \internal * \brief Display information about a shadow CIB instance * * \param[in,out] out Output object * \param[in] args Message-specific arguments * * \return Standard Pacemaker return code * * \note \p args should contain the following: * -# Instance name (can be \p NULL) * -# Shadow file name (can be \p NULL) * -# Shadow file content (can be \p NULL) * -# Patchset containing the changes in the shadow CIB (can be \p NULL) * -# Group of \p shadow_disp_flags indicating which fields to display * (ignored) */ PCMK__OUTPUT_ARGS("shadow", "const char *", "const char *", "const xmlNode *", "const xmlNode *", "enum shadow_disp_flags") static int shadow_xml(pcmk__output_t *out, va_list args) { const char *instance = va_arg(args, const char *); const char *filename = va_arg(args, const char *); const xmlNode *content = va_arg(args, const xmlNode *); const xmlNode *diff = va_arg(args, const xmlNode *); enum shadow_disp_flags flags G_GNUC_UNUSED = (enum shadow_disp_flags) va_arg(args, int); pcmk__output_xml_create_parent(out, PCMK_XE_SHADOW, PCMK_XA_INSTANCE, instance, PCMK_XA_FILE, filename, NULL); if (content != NULL) { char *buf = dump_xml_formatted_with_text(content); out->output_xml(out, PCMK_XE_CONTENT, buf); free(buf); } if (diff != NULL) { out->message(out, "xml-patchset", diff); } pcmk__output_xml_pop_parent(out); return pcmk_rc_ok; } static const pcmk__supported_format_t formats[] = { PCMK__SUPPORTED_FORMAT_NONE, PCMK__SUPPORTED_FORMAT_TEXT, PCMK__SUPPORTED_FORMAT_XML, { NULL, NULL, NULL } }; static const pcmk__message_entry_t fmt_functions[] = { { "instruction", "default", instruction_default }, { "instruction", "xml", instruction_xml }, { "shadow", "default", shadow_default }, { "shadow", "text", shadow_text }, { "shadow", "xml", shadow_xml }, { NULL, NULL, NULL } }; /*! * \internal * \brief Set the error when \p --force is not passed with a dangerous command * * \param[in] reason Why command is dangerous * \param[in] for_shadow If true, command is dangerous to the shadow file. * Otherwise, command is dangerous to the active * cluster. * \param[in] show_mismatch If true and the supplied shadow instance is not * the same as the active shadow instance, report * this * \param[out] error Where to store error */ static void set_danger_error(const char *reason, bool for_shadow, bool show_mismatch, GError **error) { const char *active = getenv("CIB_shadow"); char *full = NULL; if (show_mismatch && !pcmk__str_eq(active, options.instance, pcmk__str_null_matches)) { full = crm_strdup_printf("%s.\nAdditionally, the supplied shadow " "instance (%s) is not the same as the active " "one (%s)", reason, options.instance, active); reason = full; } g_set_error(error, PCMK__EXITC_ERROR, exit_code, "%s%sTo prevent accidental destruction of the %s, the --force " "flag is required in order to proceed.", pcmk__s(reason, ""), ((reason != NULL)? ".\n" : ""), (for_shadow? "shadow file" : "cluster")); free(full); } /*! * \internal * \brief Get the active shadow instance from the environment * * This sets \p options.instance to the value of the \p CIB_shadow env variable. * * \param[out] error Where to store error */ static int get_instance_from_env(GError **error) { int rc = pcmk_rc_ok; pcmk__str_update(&options.instance, getenv("CIB_shadow")); if (options.instance == NULL) { rc = ENXIO; exit_code = pcmk_rc2exitc(rc); g_set_error(error, PCMK__EXITC_ERROR, exit_code, "No active shadow configuration defined"); } return rc; } /*! * \internal * \brief Validate that the shadow file does or does not exist, as appropriate * * \param[in] filename Absolute path of shadow file * \param[in] should_exist Whether the shadow file is expected to exist * \param[out] error Where to store error * * \return Standard Pacemaker return code */ static int check_file_exists(const char *filename, bool should_exist, GError **error) { struct stat buf; if (!should_exist && (stat(filename, &buf) == 0)) { char *reason = crm_strdup_printf("A shadow instance '%s' already " "exists", options.instance); exit_code = CRM_EX_CANTCREAT; set_danger_error(reason, true, false, error); free(reason); return EEXIST; } if (should_exist && (stat(filename, &buf) < 0)) { // @COMPAT: Use pcmk_rc2exitc(errno)? exit_code = CRM_EX_NOSUCH; g_set_error(error, PCMK__EXITC_ERROR, exit_code, "Could not access shadow instance '%s': %s", options.instance, strerror(errno)); return errno; } return pcmk_rc_ok; } /*! * \internal * \brief Connect to the "real" (non-shadow) CIB * * \param[out] real_cib Where to store CIB connection * \param[out] error Where to store error * * \return Standard Pacemaker return code */ static int connect_real_cib(cib_t **real_cib, GError **error) { int rc = pcmk_rc_ok; *real_cib = cib_new_no_shadow(); if (*real_cib == NULL) { rc = ENOMEM; exit_code = pcmk_rc2exitc(rc); g_set_error(error, PCMK__EXITC_ERROR, exit_code, "Could not create a CIB connection object"); return rc; } rc = (*real_cib)->cmds->signon(*real_cib, crm_system_name, cib_command); rc = pcmk_legacy2rc(rc); if (rc != pcmk_rc_ok) { exit_code = pcmk_rc2exitc(rc); g_set_error(error, PCMK__EXITC_ERROR, exit_code, "Could not connect to CIB: %s", pcmk_rc_str(rc)); } return rc; } /*! * \internal * \brief Query the "real" (non-shadow) CIB and store the result * * \param[out] output Where to store query output * \param[out] error Where to store error * * \return Standard Pacemaker return code */ static int query_real_cib(xmlNode **output, GError **error) { cib_t *real_cib = NULL; int rc = connect_real_cib(&real_cib, error); if (rc != pcmk_rc_ok) { goto done; } rc = real_cib->cmds->query(real_cib, NULL, output, options.cmd_options); rc = pcmk_legacy2rc(rc); if (rc != pcmk_rc_ok) { exit_code = pcmk_rc2exitc(rc); g_set_error(error, PCMK__EXITC_ERROR, exit_code, "Could not query the non-shadow CIB: %s", pcmk_rc_str(rc)); } done: cib_delete(real_cib); return rc; } /*! * \internal * \brief Read XML from the given file * * \param[in] filename Path of input file * \param[out] output Where to store XML read from \p filename * \param[out] error Where to store error * * \return Standard Pacemaker return code */ static int read_xml(const char *filename, xmlNode **output, GError **error) { int rc = pcmk_rc_ok; - *output = filename2xml(filename); + *output = pcmk__xml_read(filename); if (*output == NULL) { rc = pcmk_rc_no_input; exit_code = pcmk_rc2exitc(rc); g_set_error(error, PCMK__EXITC_ERROR, exit_code, "Could not parse XML from input file '%s'", filename); } return rc; } /*! * \internal * \brief Write the shadow XML to a file * * \param[in] xml Shadow XML * \param[in] filename Name of destination file * \param[in] reset Whether the write is a reset (for logging only) * \param[out] error Where to store error */ static int write_shadow_file(const xmlNode *xml, const char *filename, bool reset, GError **error) { int rc = write_xml_file(xml, filename, FALSE); if (rc < 0) { rc = pcmk_legacy2rc(rc); exit_code = pcmk_rc2exitc(rc); g_set_error(error, PCMK__EXITC_ERROR, exit_code, "Could not %s the shadow instance '%s': %s", reset? "reset" : "create", options.instance, pcmk_rc_str(rc)); return rc; } return pcmk_rc_ok; } /*! * \internal * \brief Create a shell prompt based on the given shadow instance name * * \return Newly created prompt * * \note The caller is responsible for freeing the return value using \p free(). */ static inline char * get_shadow_prompt(void) { return crm_strdup_printf("shadow[%.40s] # ", options.instance); } /*! * \internal * \brief Set up environment variables for a shadow instance * * \param[in,out] out Output object * \param[in] do_switch If true, switch to an existing instance (logging * only) * \param[out] error Where to store error */ static void shadow_setup(pcmk__output_t *out, bool do_switch, GError **error) { const char *active = getenv("CIB_shadow"); const char *prompt = getenv("PS1"); const char *shell = getenv("SHELL"); char *new_prompt = get_shadow_prompt(); if (pcmk__str_eq(active, options.instance, pcmk__str_none) && pcmk__str_eq(new_prompt, prompt, pcmk__str_none)) { // CIB_shadow and prompt environment variables are already set up goto done; } if (!options.batch && (shell != NULL)) { out->info(out, "Setting up shadow instance"); setenv("PS1", new_prompt, 1); setenv("CIB_shadow", options.instance, 1); out->message(out, PCMK_XE_INSTRUCTION, "Press Ctrl+D to exit the crm_shadow shell"); if (pcmk__str_eq(shell, "(^|/)bash$", pcmk__str_regex)) { execl(shell, shell, "--norc", "--noprofile", NULL); } else { execl(shell, shell, NULL); } exit_code = pcmk_rc2exitc(errno); g_set_error(error, PCMK__EXITC_ERROR, exit_code, "Failed to launch shell '%s': %s", shell, pcmk_rc_str(errno)); } else { char *msg = NULL; const char *prefix = "A new shadow instance was created. To begin " "using it"; if (do_switch) { prefix = "To switch to the named shadow instance"; } msg = crm_strdup_printf("%s, enter the following into your shell:\n" "\texport CIB_shadow=%s", prefix, options.instance); out->message(out, "instruction", msg); free(msg); } done: free(new_prompt); } /*! * \internal * \brief Remind the user to clean up the shadow environment * * \param[in,out] out Output object */ static void shadow_teardown(pcmk__output_t *out) { const char *active = getenv("CIB_shadow"); const char *prompt = getenv("PS1"); if (pcmk__str_eq(active, options.instance, pcmk__str_none)) { char *our_prompt = get_shadow_prompt(); if (pcmk__str_eq(prompt, our_prompt, pcmk__str_none)) { out->message(out, "instruction", "Press Ctrl+D to exit the crm_shadow shell"); } else { out->message(out, "instruction", "Remember to unset the CIB_shadow variable by " "entering the following into your shell:\n" "\tunset CIB_shadow"); } free(our_prompt); } } /*! * \internal * \brief Commit the shadow file contents to the active cluster * * \param[out] error Where to store error */ static void commit_shadow_file(GError **error) { char *filename = NULL; cib_t *real_cib = NULL; xmlNodePtr input = NULL; xmlNodePtr section_xml = NULL; const char *section = NULL; int rc = pcmk_rc_ok; if (!options.force) { const char *reason = "The commit command overwrites the active cluster " "configuration"; exit_code = CRM_EX_USAGE; set_danger_error(reason, false, true, error); return; } filename = get_shadow_file(options.instance); if (check_file_exists(filename, true, error) != pcmk_rc_ok) { goto done; } if (connect_real_cib(&real_cib, error) != pcmk_rc_ok) { goto done; } if (read_xml(filename, &input, error) != pcmk_rc_ok) { goto done; } section_xml = input; if (!options.full_upload) { section = PCMK_XE_CONFIGURATION; section_xml = first_named_child(input, section); } rc = real_cib->cmds->replace(real_cib, section, section_xml, options.cmd_options); rc = pcmk_legacy2rc(rc); if (rc != pcmk_rc_ok) { exit_code = pcmk_rc2exitc(rc); g_set_error(error, PCMK__EXITC_ERROR, exit_code, "Could not commit shadow instance '%s' to the CIB: %s", options.instance, pcmk_rc_str(rc)); } done: free(filename); cib_delete(real_cib); free_xml(input); } /*! * \internal * \brief Create a new empty shadow instance * * \param[in,out] out Output object * \param[out] error Where to store error * * \note If \p --force is given, we try to write the file regardless of whether * it already exists. */ static void create_shadow_empty(pcmk__output_t *out, GError **error) { char *filename = get_shadow_file(options.instance); xmlNode *output = NULL; if (!options.force && (check_file_exists(filename, false, error) != pcmk_rc_ok)) { goto done; } output = createEmptyCib(0); crm_xml_add(output, PCMK_XA_VALIDATE_WITH, options.validate_with); out->info(out, "Created new %s configuration", crm_element_value(output, PCMK_XA_VALIDATE_WITH)); if (write_shadow_file(output, filename, false, error) != pcmk_rc_ok) { goto done; } shadow_setup(out, false, error); done: free(filename); free_xml(output); } /*! * \internal * \brief Create a shadow instance based on the active CIB * * \param[in,out] out Output object * \param[in] reset If true, overwrite the given existing shadow instance. * Otherwise, create a new shadow instance with the given * name. * \param[out] error Where to store error * * \note If \p --force is given, we try to write the file regardless of whether * it already exists. */ static void create_shadow_from_cib(pcmk__output_t *out, bool reset, GError **error) { char *filename = get_shadow_file(options.instance); xmlNode *output = NULL; if (!options.force) { if (reset) { /* @COMPAT: Reset is dangerous to the shadow file, but to preserve * compatibility we can't require --force unless there's a mismatch. * At a compatibility break, call set_danger_error() with for_shadow * and show_mismatch set to true. */ const char *local = getenv("CIB_shadow"); if (!pcmk__str_eq(local, options.instance, pcmk__str_null_matches)) { exit_code = CRM_EX_USAGE; g_set_error(error, PCMK__EXITC_ERROR, exit_code, "The supplied shadow instance (%s) is not the same " "as the active one (%s).\n" "To prevent accidental destruction of the shadow " "file, the --force flag is required in order to " "proceed.", options.instance, local); goto done; } } if (check_file_exists(filename, reset, error) != pcmk_rc_ok) { goto done; } } if (query_real_cib(&output, error) != pcmk_rc_ok) { goto done; } if (write_shadow_file(output, filename, reset, error) != pcmk_rc_ok) { goto done; } shadow_setup(out, false, error); done: free(filename); free_xml(output); } /*! * \internal * \brief Delete the shadow file * * \param[in,out] out Output object * \param[out] error Where to store error */ static void delete_shadow_file(pcmk__output_t *out, GError **error) { char *filename = NULL; if (!options.force) { const char *reason = "The delete command removes the specified shadow " "file"; exit_code = CRM_EX_USAGE; set_danger_error(reason, true, true, error); return; } filename = get_shadow_file(options.instance); if ((unlink(filename) < 0) && (errno != ENOENT)) { exit_code = pcmk_rc2exitc(errno); g_set_error(error, PCMK__EXITC_ERROR, exit_code, "Could not remove shadow instance '%s': %s", options.instance, strerror(errno)); } else { shadow_teardown(out); } free(filename); } /*! * \internal * \brief Open the shadow file in a text editor * * \param[out] error Where to store error * * \note The \p EDITOR environment variable must be set. */ static void edit_shadow_file(GError **error) { char *filename = NULL; const char *editor = NULL; if (get_instance_from_env(error) != pcmk_rc_ok) { return; } filename = get_shadow_file(options.instance); if (check_file_exists(filename, true, error) != pcmk_rc_ok) { goto done; } editor = getenv("EDITOR"); if (editor == NULL) { exit_code = CRM_EX_NOT_CONFIGURED; g_set_error(error, PCMK__EXITC_ERROR, exit_code, "No value for EDITOR defined"); goto done; } execlp(editor, "--", filename, NULL); exit_code = CRM_EX_OSFILE; g_set_error(error, PCMK__EXITC_ERROR, exit_code, "Could not invoke EDITOR (%s %s): %s", editor, filename, strerror(errno)); done: free(filename); } /*! * \internal * \brief Show the contents of the active shadow instance * * \param[in,out] out Output object * \param[out] error Where to store error */ static void show_shadow_contents(pcmk__output_t *out, GError **error) { char *filename = NULL; if (get_instance_from_env(error) != pcmk_rc_ok) { return; } filename = get_shadow_file(options.instance); if (check_file_exists(filename, true, error) == pcmk_rc_ok) { xmlNode *output = NULL; bool quiet_orig = out->quiet; if (read_xml(filename, &output, error) != pcmk_rc_ok) { goto done; } out->quiet = true; out->message(out, "shadow", options.instance, NULL, output, NULL, shadow_disp_content); out->quiet = quiet_orig; free_xml(output); } done: free(filename); } /*! * \internal * \brief Show the changes in the active shadow instance * * \param[in,out] out Output object * \param[out] error Where to store error */ static void show_shadow_diff(pcmk__output_t *out, GError **error) { char *filename = NULL; xmlNodePtr old_config = NULL; xmlNodePtr new_config = NULL; xmlNodePtr diff = NULL; bool quiet_orig = out->quiet; if (get_instance_from_env(error) != pcmk_rc_ok) { return; } filename = get_shadow_file(options.instance); if (check_file_exists(filename, true, error) != pcmk_rc_ok) { goto done; } if (query_real_cib(&old_config, error) != pcmk_rc_ok) { goto done; } if (read_xml(filename, &new_config, error) != pcmk_rc_ok) { goto done; } xml_track_changes(new_config, NULL, new_config, false); xml_calculate_changes(old_config, new_config); diff = xml_create_patchset(0, old_config, new_config, NULL, false); pcmk__log_xml_changes(LOG_INFO, new_config); xml_accept_changes(new_config); out->quiet = true; out->message(out, "shadow", options.instance, NULL, NULL, diff, shadow_disp_diff); out->quiet = quiet_orig; if (diff != NULL) { /* @COMPAT: Exit with CRM_EX_DIGEST? This is not really an error; we * just want to indicate that there are differences (as the diff command * does). */ exit_code = CRM_EX_ERROR; } done: free(filename); free_xml(old_config); free_xml(new_config); free_xml(diff); } /*! * \internal * \brief Show the absolute path of the active shadow instance * * \param[in,out] out Output object * \param[out] error Where to store error */ static void show_shadow_filename(pcmk__output_t *out, GError **error) { if (get_instance_from_env(error) == pcmk_rc_ok) { char *filename = get_shadow_file(options.instance); bool quiet_orig = out->quiet; out->quiet = true; out->message(out, "shadow", options.instance, filename, NULL, NULL, shadow_disp_file); out->quiet = quiet_orig; free(filename); } } /*! * \internal * \brief Show the active shadow instance * * \param[in,out] out Output object * \param[out] error Where to store error */ static void show_shadow_instance(pcmk__output_t *out, GError **error) { if (get_instance_from_env(error) == pcmk_rc_ok) { bool quiet_orig = out->quiet; out->quiet = true; out->message(out, "shadow", options.instance, NULL, NULL, NULL, shadow_disp_instance); out->quiet = quiet_orig; } } /*! * \internal * \brief Switch to the given shadow instance * * \param[in,out] out Output object * \param[out] error Where to store error */ static void switch_shadow_instance(pcmk__output_t *out, GError **error) { char *filename = NULL; filename = get_shadow_file(options.instance); if (check_file_exists(filename, true, error) == pcmk_rc_ok) { shadow_setup(out, true, error); } free(filename); } static gboolean command_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { if (pcmk__str_any_of(option_name, "-w", "--which", NULL)) { options.cmd = shadow_cmd_which; } else if (pcmk__str_any_of(option_name, "-p", "--display", NULL)) { options.cmd = shadow_cmd_display; } else if (pcmk__str_any_of(option_name, "-d", "--diff", NULL)) { options.cmd = shadow_cmd_diff; } else if (pcmk__str_any_of(option_name, "-F", "--file", NULL)) { options.cmd = shadow_cmd_file; } else if (pcmk__str_any_of(option_name, "-c", "--create", NULL)) { options.cmd = shadow_cmd_create; } else if (pcmk__str_any_of(option_name, "-e", "--create-empty", NULL)) { options.cmd = shadow_cmd_create_empty; } else if (pcmk__str_any_of(option_name, "-C", "--commit", NULL)) { options.cmd = shadow_cmd_commit; } else if (pcmk__str_any_of(option_name, "-D", "--delete", NULL)) { options.cmd = shadow_cmd_delete; } else if (pcmk__str_any_of(option_name, "-E", "--edit", NULL)) { options.cmd = shadow_cmd_edit; } else if (pcmk__str_any_of(option_name, "-r", "--reset", NULL)) { options.cmd = shadow_cmd_reset; } else if (pcmk__str_any_of(option_name, "-s", "--switch", NULL)) { options.cmd = shadow_cmd_switch; } else { // Should be impossible return FALSE; } // optarg may be NULL and that's okay pcmk__str_update(&options.instance, optarg); return TRUE; } static GOptionEntry query_entries[] = { { "which", 'w', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Indicate the active shadow copy", NULL }, { "display", 'p', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Display the contents of the active shadow copy", NULL }, { "diff", 'd', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Display the changes in the active shadow copy", NULL }, { "file", 'F', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Display the location of the active shadow copy file", NULL }, { NULL } }; static GOptionEntry command_entries[] = { { "create", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb, "Create the named shadow copy of the active cluster configuration", "name" }, { "create-empty", 'e', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb, "Create the named shadow copy with an empty cluster configuration.\n" INDENT "Optional: --validate-with", "name" }, { "commit", 'C', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb, "Upload the contents of the named shadow copy to the cluster", "name" }, { "delete", 'D', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb, "Delete the contents of the named shadow copy", "name" }, { "edit", 'E', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Edit the contents of the active shadow copy with your favorite $EDITOR", NULL }, { "reset", 'r', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb, "Recreate named shadow copy from the active cluster configuration", "name" }, { "switch", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb, "(Advanced) Switch to the named shadow copy", "name" }, { NULL } }; static GOptionEntry addl_entries[] = { { "force", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.force, "(Advanced) Force the action to be performed", NULL }, { "batch", 'b', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.batch, "(Advanced) Don't spawn a new shell", NULL }, { "all", 'a', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.full_upload, "(Advanced) Upload entire CIB, including status, with --commit", NULL }, { "validate-with", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.validate_with, "(Advanced) Create an older configuration version", NULL }, { NULL } }; static GOptionContext * build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) { const char *desc = NULL; GOptionContext *context = NULL; desc = "Examples:\n\n" "Create a blank shadow configuration:\n\n" "\t# crm_shadow --create-empty myShadow\n\n" "Create a shadow configuration from the running cluster\n\n" "\t# crm_shadow --create myShadow\n\n" "Display the current shadow configuration:\n\n" "\t# crm_shadow --display\n\n" "Discard the current shadow configuration (named myShadow):\n\n" "\t# crm_shadow --delete myShadow --force\n\n" "Upload current shadow configuration (named myShadow) to running " "cluster:\n\n" "\t# crm_shadow --commit myShadow\n\n"; context = pcmk__build_arg_context(args, "text (default), xml", group, "|"); g_option_context_set_description(context, desc); pcmk__add_arg_group(context, "queries", "Queries:", "Show query help", query_entries); pcmk__add_arg_group(context, "commands", "Commands:", "Show command help", command_entries); pcmk__add_arg_group(context, "additional", "Additional Options:", "Show additional options", addl_entries); return context; } int main(int argc, char **argv) { int rc = pcmk_rc_ok; pcmk__output_t *out = NULL; GError *error = NULL; GOptionGroup *output_group = NULL; pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY); gchar **processed_args = pcmk__cmdline_preproc(argv, "CDcersv"); GOptionContext *context = build_arg_context(args, &output_group); crm_log_preinit(NULL, argc, argv); pcmk__register_formats(output_group, formats); if (!g_option_context_parse_strv(context, &processed_args, &error)) { exit_code = CRM_EX_USAGE; goto done; } rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv); if (rc != pcmk_rc_ok) { exit_code = CRM_EX_ERROR; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Error creating output format %s: %s", args->output_ty, pcmk_rc_str(rc)); goto done; } if (g_strv_length(processed_args) > 1) { gchar *help = g_option_context_get_help(context, TRUE, NULL); GString *extra = g_string_sized_new(128); for (int lpc = 1; processed_args[lpc] != NULL; lpc++) { if (extra->len > 0) { g_string_append_c(extra, ' '); } g_string_append(extra, processed_args[lpc]); } exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "non-option ARGV-elements: %s\n\n%s", extra->str, help); g_free(help); g_string_free(extra, TRUE); goto done; } if (args->version) { out->version(out, false); goto done; } pcmk__register_messages(out, fmt_functions); if (options.cmd == shadow_cmd_none) { // @COMPAT: Create a default command if other tools have one gchar *help = g_option_context_get_help(context, TRUE, NULL); exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must specify a query or command option\n\n%s", help); g_free(help); goto done; } pcmk__cli_init_logging("crm_shadow", args->verbosity); if (args->verbosity > 0) { cib__set_call_options(options.cmd_options, crm_system_name, cib_verbose); } // Run the command switch (options.cmd) { case shadow_cmd_commit: commit_shadow_file(&error); break; case shadow_cmd_create: create_shadow_from_cib(out, false, &error); break; case shadow_cmd_create_empty: create_shadow_empty(out, &error); break; case shadow_cmd_reset: create_shadow_from_cib(out, true, &error); break; case shadow_cmd_delete: delete_shadow_file(out, &error); break; case shadow_cmd_diff: show_shadow_diff(out, &error); break; case shadow_cmd_display: show_shadow_contents(out, &error); break; case shadow_cmd_edit: edit_shadow_file(&error); break; case shadow_cmd_file: show_shadow_filename(out, &error); break; case shadow_cmd_switch: switch_shadow_instance(out, &error); break; case shadow_cmd_which: show_shadow_instance(out, &error); break; default: // Should never reach this point break; } done: g_strfreev(processed_args); pcmk__free_arg_context(context); pcmk__output_and_clear_error(&error, out); free(options.instance); g_free(options.validate_with); if (out != NULL) { out->finish(out, exit_code, true, NULL); pcmk__output_free(out); } crm_exit(exit_code); } diff --git a/tools/crm_simulate.c b/tools/crm_simulate.c index 48d3f1968d..5ac870821c 100644 --- a/tools/crm_simulate.c +++ b/tools/crm_simulate.c @@ -1,589 +1,589 @@ /* * Copyright 2009-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define SUMMARY "crm_simulate - simulate a Pacemaker cluster's response to events" struct { char *dot_file; char *graph_file; gchar *input_file; pcmk_injections_t *injections; unsigned int flags; gchar *output_file; long long repeat; gboolean store; gchar *test_dir; char *use_date; char *xml_file; } options = { .flags = pcmk_sim_show_pending | pcmk_sim_sanitized, .repeat = 1 }; uint32_t section_opts = 0; char *temp_shadow = NULL; crm_exit_t exit_code = CRM_EX_OK; #define INDENT " " static pcmk__supported_format_t formats[] = { PCMK__SUPPORTED_FORMAT_NONE, PCMK__SUPPORTED_FORMAT_TEXT, PCMK__SUPPORTED_FORMAT_XML, { NULL, NULL, NULL } }; static gboolean all_actions_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.flags |= pcmk_sim_all_actions; return TRUE; } static gboolean attrs_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { section_opts |= pcmk_section_attributes; return TRUE; } static gboolean failcounts_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { section_opts |= pcmk_section_failcounts | pcmk_section_failures; return TRUE; } static gboolean in_place_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.store = TRUE; options.flags |= pcmk_sim_process | pcmk_sim_simulate; return TRUE; } static gboolean live_check_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { if (options.xml_file) { free(options.xml_file); } options.xml_file = NULL; options.flags &= ~pcmk_sim_sanitized; return TRUE; } static gboolean node_down_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.injections->node_down = g_list_append(options.injections->node_down, g_strdup(optarg)); return TRUE; } static gboolean node_fail_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.injections->node_fail = g_list_append(options.injections->node_fail, g_strdup(optarg)); return TRUE; } static gboolean node_up_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { pcmk__simulate_node_config = true; options.injections->node_up = g_list_append(options.injections->node_up, g_strdup(optarg)); return TRUE; } static gboolean op_fail_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.flags |= pcmk_sim_process | pcmk_sim_simulate; options.injections->op_fail = g_list_append(options.injections->op_fail, g_strdup(optarg)); return TRUE; } static gboolean op_inject_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.injections->op_inject = g_list_append(options.injections->op_inject, g_strdup(optarg)); return TRUE; } static gboolean pending_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.flags |= pcmk_sim_show_pending; return TRUE; } static gboolean process_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.flags |= pcmk_sim_process; return TRUE; } static gboolean quorum_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { pcmk__str_update(&options.injections->quorum, optarg); return TRUE; } static gboolean save_dotfile_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.flags |= pcmk_sim_process; pcmk__str_update(&options.dot_file, optarg); return TRUE; } static gboolean save_graph_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.flags |= pcmk_sim_process; pcmk__str_update(&options.graph_file, optarg); return TRUE; } static gboolean show_scores_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.flags |= pcmk_sim_process | pcmk_sim_show_scores; return TRUE; } static gboolean simulate_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.flags |= pcmk_sim_process | pcmk_sim_simulate; return TRUE; } static gboolean ticket_activate_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.injections->ticket_activate = g_list_append(options.injections->ticket_activate, g_strdup(optarg)); return TRUE; } static gboolean ticket_grant_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.injections->ticket_grant = g_list_append(options.injections->ticket_grant, g_strdup(optarg)); return TRUE; } static gboolean ticket_revoke_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.injections->ticket_revoke = g_list_append(options.injections->ticket_revoke, g_strdup(optarg)); return TRUE; } static gboolean ticket_standby_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.injections->ticket_standby = g_list_append(options.injections->ticket_standby, g_strdup(optarg)); return TRUE; } static gboolean utilization_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.flags |= pcmk_sim_process | pcmk_sim_show_utilization; return TRUE; } static gboolean watchdog_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { pcmk__str_update(&options.injections->watchdog, optarg); return TRUE; } static gboolean xml_file_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { pcmk__str_update(&options.xml_file, optarg); options.flags |= pcmk_sim_sanitized; return TRUE; } static gboolean xml_pipe_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { pcmk__str_update(&options.xml_file, "-"); options.flags |= pcmk_sim_sanitized; return TRUE; } static GOptionEntry operation_entries[] = { { "run", 'R', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, process_cb, "Process the supplied input and show what actions the cluster will take in response", NULL }, { "simulate", 'S', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, simulate_cb, "Like --run, but also simulate taking those actions and show the resulting new status", NULL }, { "in-place", 'X', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, in_place_cb, "Like --simulate, but also store the results back to the input file", NULL }, { "show-attrs", 'A', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, attrs_cb, "Show node attributes", NULL }, { "show-failcounts", 'c', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, failcounts_cb, "Show resource fail counts", NULL }, { "show-scores", 's', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, show_scores_cb, "Show allocation scores", NULL }, { "show-utilization", 'U', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, utilization_cb, "Show utilization information", NULL }, { "profile", 'P', 0, G_OPTION_ARG_FILENAME, &options.test_dir, "Process all the XML files in the named directory to create profiling data", "DIR" }, { "repeat", 'N', 0, G_OPTION_ARG_INT, &options.repeat, "With --profile, repeat each test N times and print timings", "N" }, /* Deprecated */ { "pending", 'j', G_OPTION_FLAG_NO_ARG|G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_CALLBACK, pending_cb, "Display pending state if '" PCMK_META_RECORD_PENDING "' is enabled", NULL }, { NULL } }; static GOptionEntry synthetic_entries[] = { { "node-up", 'u', 0, G_OPTION_ARG_CALLBACK, node_up_cb, "Simulate bringing a node online", "NODE" }, { "node-down", 'd', 0, G_OPTION_ARG_CALLBACK, node_down_cb, "Simulate taking a node offline", "NODE" }, { "node-fail", 'f', 0, G_OPTION_ARG_CALLBACK, node_fail_cb, "Simulate a node failing", "NODE" }, { "op-inject", 'i', 0, G_OPTION_ARG_CALLBACK, op_inject_cb, "Generate a failure for the cluster to react to in the simulation.\n" INDENT "See `Operation Specification` help for more information.", "OPSPEC" }, { "op-fail", 'F', 0, G_OPTION_ARG_CALLBACK, op_fail_cb, "If the specified task occurs during the simulation, have it fail with return code ${rc}.\n" INDENT "The transition will normally stop at the failed action.\n" INDENT "Save the result with --save-output and re-run with --xml-file.\n" INDENT "See `Operation Specification` help for more information.", "OPSPEC" }, { "set-datetime", 't', 0, G_OPTION_ARG_STRING, &options.use_date, "Set date/time (ISO 8601 format, see https://en.wikipedia.org/wiki/ISO_8601)", "DATETIME" }, { "quorum", 'q', 0, G_OPTION_ARG_CALLBACK, quorum_cb, "Set to '1' (or 'true') to indicate cluster has quorum", "QUORUM" }, { "watchdog", 'w', 0, G_OPTION_ARG_CALLBACK, watchdog_cb, "Set to '1' (or 'true') to indicate cluster has an active watchdog device", "DEVICE" }, { "ticket-grant", 'g', 0, G_OPTION_ARG_CALLBACK, ticket_grant_cb, "Simulate granting a ticket", "TICKET" }, { "ticket-revoke", 'r', 0, G_OPTION_ARG_CALLBACK, ticket_revoke_cb, "Simulate revoking a ticket", "TICKET" }, { "ticket-standby", 'b', 0, G_OPTION_ARG_CALLBACK, ticket_standby_cb, "Simulate making a ticket standby", "TICKET" }, { "ticket-activate", 'e', 0, G_OPTION_ARG_CALLBACK, ticket_activate_cb, "Simulate activating a ticket", "TICKET" }, { NULL } }; static GOptionEntry artifact_entries[] = { { "save-input", 'I', 0, G_OPTION_ARG_FILENAME, &options.input_file, "Save the input configuration to the named file", "FILE" }, { "save-output", 'O', 0, G_OPTION_ARG_FILENAME, &options.output_file, "Save the output configuration to the named file", "FILE" }, { "save-graph", 'G', 0, G_OPTION_ARG_CALLBACK, save_graph_cb, "Save the transition graph (XML format) to the named file", "FILE" }, { "save-dotfile", 'D', 0, G_OPTION_ARG_CALLBACK, save_dotfile_cb, "Save the transition graph (DOT format) to the named file", "FILE" }, { "all-actions", 'a', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, all_actions_cb, "Display all possible actions in DOT graph (even if not part of transition)", NULL }, { NULL } }; static GOptionEntry source_entries[] = { { "live-check", 'L', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, live_check_cb, "Connect to CIB manager and use the current CIB contents as input", NULL }, { "xml-file", 'x', 0, G_OPTION_ARG_CALLBACK, xml_file_cb, "Retrieve XML from the named file", "FILE" }, { "xml-pipe", 'p', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, xml_pipe_cb, "Retrieve XML from stdin", NULL }, { NULL } }; static int setup_input(pcmk__output_t *out, const char *input, const char *output, GError **error) { int rc = pcmk_rc_ok; xmlNode *cib_object = NULL; char *local_output = NULL; if (input == NULL) { /* Use live CIB */ rc = cib__signon_query(out, NULL, &cib_object); if (rc != pcmk_rc_ok) { // cib__signon_query() outputs any relevant error return rc; } } else if (pcmk__str_eq(input, "-", pcmk__str_casei)) { - cib_object = filename2xml(NULL); + cib_object = pcmk__xml_read(NULL); } else { - cib_object = filename2xml(input); + cib_object = pcmk__xml_read(input); } if (pcmk_find_cib_element(cib_object, PCMK_XE_STATUS) == NULL) { create_xml_node(cib_object, PCMK_XE_STATUS); } if (cli_config_update(&cib_object, NULL, FALSE) == FALSE) { free_xml(cib_object); return pcmk_rc_transform_failed; } if (validate_xml(cib_object, NULL, FALSE) != TRUE) { free_xml(cib_object); return pcmk_rc_schema_validation; } if (output == NULL) { char *pid = pcmk__getpid_s(); local_output = get_shadow_file(pid); temp_shadow = strdup(local_output); output = local_output; free(pid); } rc = write_xml_file(cib_object, output, FALSE); free_xml(cib_object); cib_object = NULL; if (rc < 0) { rc = pcmk_legacy2rc(rc); g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_CANTCREAT, "Could not create '%s': %s", output, pcmk_rc_str(rc)); return rc; } else { setenv("CIB_file", output, 1); free(local_output); return pcmk_rc_ok; } } static GOptionContext * build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) { GOptionContext *context = NULL; GOptionEntry extra_prog_entries[] = { { "quiet", 'Q', 0, G_OPTION_ARG_NONE, &(args->quiet), "Display only essential output", NULL }, { NULL } }; const char *description = "Operation Specification:\n\n" "The OPSPEC in any command line option is of the form\n" "${resource}_${task}_${interval_in_ms}@${node}=${rc}\n" "(memcached_monitor_20000@bart.example.com=7, for example).\n" "${rc} is an OCF return code. For more information on these\n" "return codes, refer to https://clusterlabs.org/pacemaker/doc/2.1/Pacemaker_Administration/html/agents.html#ocf-return-codes\n\n" "Examples:\n\n" "Pretend a recurring monitor action found memcached stopped on node\n" "fred.example.com and, during recovery, that the memcached stop\n" "action failed:\n\n" "\tcrm_simulate -LS --op-inject memcached:0_monitor_20000@bart.example.com=7 " "--op-fail memcached:0_stop_0@fred.example.com=1 --save-output /tmp/memcached-test.xml\n\n" "Now see what the reaction to the stop failed would be:\n\n" "\tcrm_simulate -S --xml-file /tmp/memcached-test.xml\n\n"; context = pcmk__build_arg_context(args, "text (default), xml", group, NULL); pcmk__add_main_args(context, extra_prog_entries); g_option_context_set_description(context, description); pcmk__add_arg_group(context, "operations", "Operations:", "Show operations options", operation_entries); pcmk__add_arg_group(context, "synthetic", "Synthetic Cluster Events:", "Show synthetic cluster event options", synthetic_entries); pcmk__add_arg_group(context, "artifact", "Artifact Options:", "Show artifact options", artifact_entries); pcmk__add_arg_group(context, "source", "Data Source:", "Show data source options", source_entries); return context; } int main(int argc, char **argv) { int rc = pcmk_rc_ok; pcmk_scheduler_t *scheduler = NULL; 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, "bdefgiqrtuwxDFGINOP"); GOptionContext *context = build_arg_context(args, &output_group); options.injections = calloc(1, sizeof(pcmk_injections_t)); if (options.injections == NULL) { rc = ENOMEM; goto done; } /* This must come before g_option_context_parse_strv. */ options.xml_file = strdup("-"); 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("crm_simulate", args->verbosity); rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv); if (rc != pcmk_rc_ok) { fprintf(stderr, "Error creating output format %s: %s\n", args->output_ty, pcmk_rc_str(rc)); exit_code = CRM_EX_ERROR; goto done; } if (pcmk__str_eq(args->output_ty, "text", pcmk__str_null_matches) && !pcmk_is_set(options.flags, pcmk_sim_show_scores) && !pcmk_is_set(options.flags, pcmk_sim_show_utilization)) { pcmk__output_text_set_fancy(out, true); } pe__register_messages(out); pcmk__register_lib_messages(out); out->quiet = args->quiet; if (args->version) { out->version(out, false); goto done; } if (args->verbosity > 0) { options.flags |= pcmk_sim_verbose; #ifdef PCMK__COMPAT_2_0 /* Redirect stderr to stdout so we can grep the output */ close(STDERR_FILENO); dup2(STDOUT_FILENO, STDERR_FILENO); #endif } scheduler = pe_new_working_set(); if (scheduler == NULL) { rc = ENOMEM; g_set_error(&error, PCMK__RC_ERROR, rc, "Could not allocate scheduler data"); goto done; } if (pcmk_is_set(options.flags, pcmk_sim_show_scores)) { pcmk__set_scheduler_flags(scheduler, pcmk_sched_output_scores); } if (pcmk_is_set(options.flags, pcmk_sim_show_utilization)) { pcmk__set_scheduler_flags(scheduler, pcmk_sched_show_utilization); } pcmk__set_scheduler_flags(scheduler, pcmk_sched_no_compat); if (options.test_dir != NULL) { scheduler->priv = out; pcmk__profile_dir(options.test_dir, options.repeat, scheduler, options.use_date); rc = pcmk_rc_ok; goto done; } rc = setup_input(out, options.xml_file, options.store? options.xml_file : options.output_file, &error); if (rc != pcmk_rc_ok) { goto done; } rc = pcmk__simulate(scheduler, out, options.injections, options.flags, section_opts, options.use_date, options.input_file, options.graph_file, options.dot_file); done: pcmk__output_and_clear_error(&error, NULL); /* There sure is a lot to free in options. */ free(options.dot_file); free(options.graph_file); g_free(options.input_file); g_free(options.output_file); g_free(options.test_dir); free(options.use_date); free(options.xml_file); pcmk_free_injections(options.injections); pcmk__free_arg_context(context); g_strfreev(processed_args); if (scheduler != NULL) { pe_free_working_set(scheduler); } fflush(stderr); if (temp_shadow) { unlink(temp_shadow); free(temp_shadow); } if (rc != pcmk_rc_ok) { exit_code = pcmk_rc2exitc(rc); } if (out != NULL) { out->finish(out, exit_code, true, NULL); pcmk__output_free(out); } pcmk__unregister_formats(); crm_exit(exit_code); } diff --git a/tools/crm_ticket.c b/tools/crm_ticket.c index 71f5cb588b..eba243dc57 100644 --- a/tools/crm_ticket.c +++ b/tools/crm_ticket.c @@ -1,1014 +1,1014 @@ /* * Copyright 2012-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include GError *error = NULL; #define SUMMARY "Perform tasks related to cluster tickets\n\n" \ "Allows ticket attributes to be queried, modified and deleted." struct { gchar *attr_default; gchar *attr_id; char *attr_name; char *attr_value; gboolean force; char *get_attr_name; gboolean quiet; gchar *set_name; char ticket_cmd; gchar *ticket_id; gchar *xml_file; } options = { .ticket_cmd = 'S' }; GList *attr_delete; GHashTable *attr_set; bool modified = false; int cib_options = cib_sync_call; #define INDENT " " static gboolean attr_value_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { pcmk__str_update(&options.attr_value, optarg); if (!options.attr_name || !options.attr_value) { return TRUE; } pcmk__insert_dup(attr_set, options.attr_name, options.attr_value); pcmk__str_update(&options.attr_name, NULL); pcmk__str_update(&options.attr_value, NULL); modified = true; return TRUE; } static gboolean command_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { if (pcmk__str_any_of(option_name, "--info", "-l", NULL)) { options.ticket_cmd = 'l'; } else if (pcmk__str_any_of(option_name, "--details", "-L", NULL)) { options.ticket_cmd = 'L'; } else if (pcmk__str_any_of(option_name, "--raw", "-w", NULL)) { options.ticket_cmd = 'w'; } else if (pcmk__str_any_of(option_name, "--query-xml", "-q", NULL)) { options.ticket_cmd = 'q'; } else if (pcmk__str_any_of(option_name, "--constraints", "-c", NULL)) { options.ticket_cmd = 'c'; } else if (pcmk__str_any_of(option_name, "--cleanup", "-C", NULL)) { options.ticket_cmd = 'C'; } return TRUE; } static gboolean delete_attr_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { attr_delete = g_list_append(attr_delete, strdup(optarg)); modified = true; return TRUE; } static gboolean get_attr_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { pcmk__str_update(&options.get_attr_name, optarg); options.ticket_cmd = 'G'; return TRUE; } static gboolean grant_standby_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { if (pcmk__str_any_of(option_name, "--grant", "-g", NULL)) { pcmk__insert_dup(attr_set, PCMK__XA_GRANTED, PCMK_VALUE_TRUE); modified = true; } else if (pcmk__str_any_of(option_name, "--revoke", "-r", NULL)) { pcmk__insert_dup(attr_set, PCMK__XA_GRANTED, PCMK_VALUE_FALSE); modified = true; } else if (pcmk__str_any_of(option_name, "--standby", "-s", NULL)) { pcmk__insert_dup(attr_set, PCMK_XA_STANDBY, PCMK_VALUE_TRUE); modified = true; } else if (pcmk__str_any_of(option_name, "--activate", "-a", NULL)) { pcmk__insert_dup(attr_set, PCMK_XA_STANDBY, PCMK_VALUE_FALSE); modified = true; } return TRUE; } static gboolean set_attr_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { pcmk__str_update(&options.attr_name, optarg); if (!options.attr_name || !options.attr_value) { return TRUE; } pcmk__insert_dup(attr_set, options.attr_name, options.attr_value); pcmk__str_update(&options.attr_name, NULL); pcmk__str_update(&options.attr_value, NULL); modified = true; return TRUE; } static GOptionEntry query_entries[] = { { "info", 'l', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Display the information of ticket(s)", NULL }, { "details", 'L', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Display the details of ticket(s)", NULL }, { "raw", 'w', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Display the IDs of ticket(s)", NULL }, { "query-xml", 'q', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Query the XML of ticket(s)", NULL }, { "constraints", 'c', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Display the " PCMK_XE_RSC_TICKET " constraints that apply to ticket(s)", NULL }, { NULL } }; static GOptionEntry command_entries[] = { { "grant", 'g', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, grant_standby_cb, "Grant a ticket to this cluster site", NULL }, { "revoke", 'r', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, grant_standby_cb, "Revoke a ticket from this cluster site", NULL }, { "standby", 's', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, grant_standby_cb, "Tell this cluster site this ticket is standby", NULL }, { "activate", 'a', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, grant_standby_cb, "Tell this cluster site this ticket is active", NULL }, { NULL } }; static GOptionEntry advanced_entries[] = { { "get-attr", 'G', 0, G_OPTION_ARG_CALLBACK, get_attr_cb, "Display the named attribute for a ticket", "ATTRIBUTE" }, { "set-attr", 'S', 0, G_OPTION_ARG_CALLBACK, set_attr_cb, "Set the named attribute for a ticket", "ATTRIBUTE" }, { "delete-attr", 'D', 0, G_OPTION_ARG_CALLBACK, delete_attr_cb, "Delete the named attribute for a ticket", "ATTRIBUTE" }, { "cleanup", 'C', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Delete all state of a ticket at this cluster site", NULL }, { NULL} }; static GOptionEntry addl_entries[] = { { "attr-value", 'v', 0, G_OPTION_ARG_CALLBACK, attr_value_cb, "Attribute value to use with -S", "VALUE" }, { "default", 'd', 0, G_OPTION_ARG_STRING, &options.attr_default, "(Advanced) Default attribute value to display if none is found\n" INDENT "(for use with -G)", "VALUE" }, { "force", 'f', 0, G_OPTION_ARG_NONE, &options.force, "(Advanced) Force the action to be performed", NULL }, { "ticket", 't', 0, G_OPTION_ARG_STRING, &options.ticket_id, "Ticket ID", "ID" }, { "xml-file", 'x', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &options.xml_file, NULL, NULL }, { NULL } }; static GOptionEntry deprecated_entries[] = { { "set-name", 'n', 0, G_OPTION_ARG_STRING, &options.set_name, "(Advanced) ID of the " PCMK_XE_INSTANCE_ATTRIBUTES " object to change", "ID" }, { "nvpair", 'i', 0, G_OPTION_ARG_STRING, &options.attr_id, "(Advanced) ID of the nvpair object to change/delete", "ID" }, { "quiet", 'Q', 0, G_OPTION_ARG_NONE, &options.quiet, "Print only the value on stdout", NULL }, { NULL } }; static pcmk_ticket_t * find_ticket(gchar *ticket_id, pcmk_scheduler_t *scheduler) { return g_hash_table_lookup(scheduler->tickets, ticket_id); } static void print_date(time_t time) { int lpc = 0; char date_str[26]; asctime_r(localtime(&time), date_str); for (; lpc < 26; lpc++) { if (date_str[lpc] == '\n') { date_str[lpc] = 0; } } fprintf(stdout, "'%s'", date_str); } static void print_ticket(pcmk_ticket_t *ticket, bool raw, bool details) { if (raw) { fprintf(stdout, "%s\n", ticket->id); return; } fprintf(stdout, "%s\t%s %s", ticket->id, ticket->granted ? "granted" : "revoked", ticket->standby ? "[standby]" : " "); if (details && g_hash_table_size(ticket->state) > 0) { GHashTableIter iter; const char *name = NULL; const char *value = NULL; int lpc = 0; fprintf(stdout, " ("); g_hash_table_iter_init(&iter, ticket->state); while (g_hash_table_iter_next(&iter, (void **)&name, (void **)&value)) { if (lpc > 0) { fprintf(stdout, ", "); } fprintf(stdout, "%s=", name); if (pcmk__str_any_of(name, PCMK_XA_LAST_GRANTED, "expires", NULL)) { long long time_ll; pcmk__scan_ll(value, &time_ll, 0); print_date((time_t) time_ll); } else { fprintf(stdout, "%s", value); } lpc++; } fprintf(stdout, ")\n"); } else { if (ticket->last_granted > -1) { fprintf(stdout, " " PCMK_XA_LAST_GRANTED "="); print_date(ticket->last_granted); } fprintf(stdout, "\n"); } return; } static void print_ticket_list(pcmk_scheduler_t *scheduler, bool raw, bool details) { GHashTableIter iter; pcmk_ticket_t *ticket = NULL; g_hash_table_iter_init(&iter, scheduler->tickets); while (g_hash_table_iter_next(&iter, NULL, (void **)&ticket)) { print_ticket(ticket, raw, details); } } static int find_ticket_state(cib_t * the_cib, gchar *ticket_id, xmlNode ** ticket_state_xml) { int rc = pcmk_rc_ok; xmlNode *xml_search = NULL; GString *xpath = NULL; CRM_ASSERT(ticket_state_xml != NULL); *ticket_state_xml = NULL; xpath = g_string_sized_new(1024); g_string_append(xpath, "/" PCMK_XE_CIB "/" PCMK_XE_STATUS "/" PCMK_XE_TICKETS); if (ticket_id != NULL) { pcmk__g_strcat(xpath, "/" PCMK__XE_TICKET_STATE "[@" PCMK_XA_ID "=\"", ticket_id, "\"]", NULL); } rc = the_cib->cmds->query(the_cib, (const char *) xpath->str, &xml_search, cib_sync_call | cib_scope_local | cib_xpath); rc = pcmk_legacy2rc(rc); g_string_free(xpath, TRUE); if (rc != pcmk_rc_ok) { return rc; } crm_log_xml_debug(xml_search, "Match"); if (xml_search->children != NULL) { if (ticket_id) { fprintf(stdout, "Multiple " PCMK__XE_TICKET_STATE "s match ticket_id=%s\n", ticket_id); } *ticket_state_xml = xml_search; } else { *ticket_state_xml = xml_search; } return rc; } static int find_ticket_constraints(cib_t * the_cib, gchar *ticket_id, xmlNode ** ticket_cons_xml) { int rc = pcmk_rc_ok; xmlNode *xml_search = NULL; GString *xpath = NULL; const char *xpath_base = NULL; CRM_ASSERT(ticket_cons_xml != NULL); *ticket_cons_xml = NULL; xpath_base = pcmk_cib_xpath_for(PCMK_XE_CONSTRAINTS); if (xpath_base == NULL) { crm_err(PCMK_XE_CONSTRAINTS " CIB element not known (bug?)"); return -ENOMSG; } xpath = g_string_sized_new(1024); pcmk__g_strcat(xpath, xpath_base, "/" PCMK_XE_RSC_TICKET, NULL); if (ticket_id != NULL) { pcmk__g_strcat(xpath, "[@" PCMK_XA_TICKET "=\"", ticket_id, "\"]", NULL); } rc = the_cib->cmds->query(the_cib, (const char *) xpath->str, &xml_search, cib_sync_call | cib_scope_local | cib_xpath); rc = pcmk_legacy2rc(rc); g_string_free(xpath, TRUE); if (rc != pcmk_rc_ok) { return rc; } crm_log_xml_debug(xml_search, "Match"); *ticket_cons_xml = xml_search; return rc; } static int dump_ticket_xml(cib_t * the_cib, gchar *ticket_id) { int rc = pcmk_rc_ok; xmlNode *state_xml = NULL; rc = find_ticket_state(the_cib, ticket_id, &state_xml); if (state_xml == NULL) { return rc; } fprintf(stdout, "State XML:\n"); if (state_xml) { char *state_xml_str = NULL; state_xml_str = dump_xml_formatted(state_xml); fprintf(stdout, "\n%s", state_xml_str); free_xml(state_xml); free(state_xml_str); } return rc; } static int dump_constraints(cib_t * the_cib, gchar *ticket_id) { int rc = pcmk_rc_ok; xmlNode *cons_xml = NULL; char *cons_xml_str = NULL; rc = find_ticket_constraints(the_cib, ticket_id, &cons_xml); if (cons_xml == NULL) { return rc; } cons_xml_str = dump_xml_formatted(cons_xml); fprintf(stdout, "Constraints XML:\n\n%s", cons_xml_str); free_xml(cons_xml); free(cons_xml_str); return rc; } static int get_ticket_state_attr(gchar *ticket_id, const char *attr_name, const char **attr_value, pcmk_scheduler_t *scheduler) { pcmk_ticket_t *ticket = NULL; CRM_ASSERT(attr_value != NULL); *attr_value = NULL; ticket = g_hash_table_lookup(scheduler->tickets, ticket_id); if (ticket == NULL) { return ENXIO; } *attr_value = g_hash_table_lookup(ticket->state, attr_name); if (*attr_value == NULL) { return ENXIO; } return pcmk_rc_ok; } static void ticket_warning(gchar *ticket_id, const char *action) { GString *warning = g_string_sized_new(1024); const char *word = NULL; CRM_ASSERT(action != NULL); if (strcmp(action, "grant") == 0) { pcmk__g_strcat(warning, "This command cannot help you verify whether '", ticket_id, "' has been already granted elsewhere.\n", NULL); word = "to"; } else { pcmk__g_strcat(warning, "Revoking '", ticket_id, "' can trigger the specified " "'" PCMK_XA_LOSS_POLICY "'(s) " "relating to '", ticket_id, "'.\n\n" "You can check that with:\n" "crm_ticket --ticket ", ticket_id, " --constraints\n\n" "Otherwise before revoking '", ticket_id, "', " "you may want to make '", ticket_id, "' " "standby with:\n" "crm_ticket --ticket ", ticket_id, " --standby\n\n", NULL); word = "from"; } pcmk__g_strcat(warning, "If you really want to ", action, " '", ticket_id, "' ", word, " this site now, and you know what you are doing,\n" "please specify --force.", NULL); fprintf(stdout, "%s\n", (const char *) warning->str); g_string_free(warning, TRUE); } static bool allow_modification(gchar *ticket_id) { const char *value = NULL; GList *list_iter = NULL; if (options.force) { return true; } if (g_hash_table_lookup_extended(attr_set, PCMK__XA_GRANTED, NULL, (gpointer *) &value)) { if (crm_is_true(value)) { ticket_warning(ticket_id, "grant"); return false; } else { ticket_warning(ticket_id, "revoke"); return false; } } for(list_iter = attr_delete; list_iter; list_iter = list_iter->next) { const char *key = (const char *)list_iter->data; if (pcmk__str_eq(key, PCMK__XA_GRANTED, pcmk__str_none)) { ticket_warning(ticket_id, "revoke"); return false; } } return true; } static int modify_ticket_state(gchar *ticket_id, cib_t *cib, pcmk_scheduler_t *scheduler) { int rc = pcmk_rc_ok; xmlNode *xml_top = NULL; xmlNode *ticket_state_xml = NULL; bool found = false; GList *list_iter = NULL; GHashTableIter hash_iter; char *key = NULL; char *value = NULL; pcmk_ticket_t *ticket = NULL; rc = find_ticket_state(cib, ticket_id, &ticket_state_xml); if (rc == pcmk_rc_ok) { crm_debug("Found a match state for ticket: id=%s", ticket_id); xml_top = ticket_state_xml; found = true; } else if (rc != ENXIO) { return rc; } else if (g_hash_table_size(attr_set) == 0){ return pcmk_rc_ok; } else { xmlNode *xml_obj = NULL; xml_top = create_xml_node(NULL, PCMK_XE_STATUS); xml_obj = create_xml_node(xml_top, PCMK_XE_TICKETS); ticket_state_xml = create_xml_node(xml_obj, PCMK__XE_TICKET_STATE); crm_xml_add(ticket_state_xml, PCMK_XA_ID, ticket_id); } for(list_iter = attr_delete; list_iter; list_iter = list_iter->next) { const char *key = (const char *)list_iter->data; xml_remove_prop(ticket_state_xml, key); } ticket = find_ticket(ticket_id, scheduler); g_hash_table_iter_init(&hash_iter, attr_set); while (g_hash_table_iter_next(&hash_iter, (gpointer *) & key, (gpointer *) & value)) { crm_xml_add(ticket_state_xml, key, value); if (pcmk__str_eq(key, PCMK__XA_GRANTED, pcmk__str_none) && (ticket == NULL || ticket->granted == FALSE) && crm_is_true(value)) { char *now = pcmk__ttoa(time(NULL)); crm_xml_add(ticket_state_xml, PCMK_XA_LAST_GRANTED, now); free(now); } } if (found && (attr_delete != NULL)) { crm_log_xml_debug(xml_top, "Replace"); rc = cib->cmds->replace(cib, PCMK_XE_STATUS, ticket_state_xml, cib_options); rc = pcmk_legacy2rc(rc); } else { crm_log_xml_debug(xml_top, "Update"); rc = cib->cmds->modify(cib, PCMK_XE_STATUS, xml_top, cib_options); rc = pcmk_legacy2rc(rc); } free_xml(xml_top); return rc; } static int delete_ticket_state(gchar *ticket_id, cib_t * cib) { xmlNode *ticket_state_xml = NULL; int rc = pcmk_rc_ok; rc = find_ticket_state(cib, ticket_id, &ticket_state_xml); if (rc == ENXIO) { return pcmk_rc_ok; } else if (rc != pcmk_rc_ok) { return rc; } crm_log_xml_debug(ticket_state_xml, "Delete"); rc = cib->cmds->remove(cib, PCMK_XE_STATUS, ticket_state_xml, cib_options); rc = pcmk_legacy2rc(rc); if (rc == pcmk_rc_ok) { fprintf(stdout, "Cleaned up %s\n", ticket_id); } free_xml(ticket_state_xml); return rc; } static GOptionContext * build_arg_context(pcmk__common_args_t *args) { GOptionContext *context = NULL; const char *description = "Examples:\n\n" "Display the info of tickets:\n\n" "\tcrm_ticket --info\n\n" "Display the detailed info of tickets:\n\n" "\tcrm_ticket --details\n\n" "Display the XML of 'ticketA':\n\n" "\tcrm_ticket --ticket ticketA --query-xml\n\n" "Display the " PCMK_XE_RSC_TICKET " constraints that apply to 'ticketA':\n\n" "\tcrm_ticket --ticket ticketA --constraints\n\n" "Grant 'ticketA' to this cluster site:\n\n" "\tcrm_ticket --ticket ticketA --grant\n\n" "Revoke 'ticketA' from this cluster site:\n\n" "\tcrm_ticket --ticket ticketA --revoke\n\n" "Make 'ticketA' standby (the cluster site will treat a granted\n" "'ticketA' as 'standby', and the dependent resources will be\n" "stopped or demoted gracefully without triggering loss-policies):\n\n" "\tcrm_ticket --ticket ticketA --standby\n\n" "Activate 'ticketA' from being standby:\n\n" "\tcrm_ticket --ticket ticketA --activate\n\n" "Get the value of the 'granted' attribute for 'ticketA':\n\n" "\tcrm_ticket --ticket ticketA --get-attr granted\n\n" "Set the value of the 'standby' attribute for 'ticketA':\n\n" "\tcrm_ticket --ticket ticketA --set-attr standby --attr-value true\n\n" "Delete the 'granted' attribute for 'ticketA':\n\n" "\tcrm_ticket --ticket ticketA --delete-attr granted\n\n" "Erase the operation history of 'ticketA' at this cluster site,\n" "causing the cluster site to 'forget' the existing ticket state:\n\n" "\tcrm_ticket --ticket ticketA --cleanup\n\n"; context = pcmk__build_arg_context(args, NULL, NULL, NULL); g_option_context_set_description(context, description); pcmk__add_arg_group(context, "queries", "Queries:", "Show queries", query_entries); pcmk__add_arg_group(context, "commands", "Commands:", "Show command options", command_entries); pcmk__add_arg_group(context, "advanced", "Advanced Options:", "Show advanced options", advanced_entries); pcmk__add_arg_group(context, "additional", "Additional Options:", "Show additional options", addl_entries); pcmk__add_arg_group(context, "deprecated", "Deprecated Options:", "Show deprecated options", deprecated_entries); return context; } int main(int argc, char **argv) { pcmk_scheduler_t *scheduler = NULL; xmlNode *cib_xml_copy = NULL; cib_t *cib_conn = NULL; crm_exit_t exit_code = CRM_EX_OK; int rc = pcmk_rc_ok; pcmk__common_args_t *args = NULL; GOptionContext *context = NULL; gchar **processed_args = NULL; attr_set = pcmk__strkey_table(free, free); attr_delete = NULL; args = pcmk__new_common_args(SUMMARY); context = build_arg_context(args); processed_args = pcmk__cmdline_preproc(argv, "dintvxCDGS"); if (!g_option_context_parse_strv(context, &processed_args, &error)) { exit_code = CRM_EX_USAGE; goto done; } pcmk__cli_init_logging("crm_ticket", args->verbosity); if (args->version) { g_strfreev(processed_args); pcmk__free_arg_context(context); /* FIXME: When crm_ticket is converted to use formatted output, this can go. */ pcmk__cli_help('v'); } scheduler = pe_new_working_set(); if (scheduler == NULL) { rc = errno; exit_code = pcmk_rc2exitc(rc); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not allocate scheduler data: %s", pcmk_rc_str(rc)); goto done; } pcmk__set_scheduler_flags(scheduler, pcmk_sched_no_counts|pcmk_sched_no_compat); cib_conn = cib_new(); if (cib_conn == NULL) { exit_code = CRM_EX_DISCONNECT; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not connect to the CIB manager"); goto done; } rc = cib_conn->cmds->signon(cib_conn, crm_system_name, cib_command); rc = pcmk_legacy2rc(rc); if (rc != pcmk_rc_ok) { exit_code = pcmk_rc2exitc(rc); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not connect to the CIB: %s", pcmk_rc_str(rc)); goto done; } if (options.xml_file != NULL) { - cib_xml_copy = filename2xml(options.xml_file); + cib_xml_copy = pcmk__xml_read(options.xml_file); } else { rc = cib_conn->cmds->query(cib_conn, NULL, &cib_xml_copy, cib_scope_local | cib_sync_call); rc = pcmk_legacy2rc(rc); if (rc != pcmk_rc_ok) { exit_code = pcmk_rc2exitc(rc); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not get local CIB: %s", pcmk_rc_str(rc)); goto done; } } if (!cli_config_update(&cib_xml_copy, NULL, FALSE)) { exit_code = CRM_EX_CONFIG; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not update local CIB to latest schema version"); goto done; } scheduler->input = cib_xml_copy; scheduler->now = crm_time_new(NULL); cluster_status(scheduler); /* For recording the tickets that are referenced in PCMK_XE_RSC_TICKET * constraints but have never been granted yet. */ pcmk__unpack_constraints(scheduler); if (options.ticket_cmd == 'l' || options.ticket_cmd == 'L' || options.ticket_cmd == 'w') { bool raw = false; bool details = false; if (options.ticket_cmd == 'L') { details = true; } else if (options.ticket_cmd == 'w') { raw = true; } if (options.ticket_id) { pcmk_ticket_t *ticket = find_ticket(options.ticket_id, scheduler); if (ticket == NULL) { exit_code = CRM_EX_NOSUCH; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "No such ticket '%s'", options.ticket_id); goto done; } print_ticket(ticket, raw, details); } else { print_ticket_list(scheduler, raw, details); } } else if (options.ticket_cmd == 'q') { rc = dump_ticket_xml(cib_conn, options.ticket_id); exit_code = pcmk_rc2exitc(rc); if (rc != pcmk_rc_ok) { g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not query ticket XML: %s", pcmk_rc_str(rc)); } } else if (options.ticket_cmd == 'c') { rc = dump_constraints(cib_conn, options.ticket_id); exit_code = pcmk_rc2exitc(rc); if (rc != pcmk_rc_ok) { g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not show ticket constraints: %s", pcmk_rc_str(rc)); } } else if (options.ticket_cmd == 'G') { const char *value = NULL; if (options.ticket_id == NULL) { exit_code = CRM_EX_NOSUCH; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply ticket ID with -t"); goto done; } rc = get_ticket_state_attr(options.ticket_id, options.get_attr_name, &value, scheduler); if (rc == pcmk_rc_ok) { fprintf(stdout, "%s\n", value); } else if (rc == ENXIO && options.attr_default) { fprintf(stdout, "%s\n", options.attr_default); rc = pcmk_rc_ok; } exit_code = pcmk_rc2exitc(rc); } else if (options.ticket_cmd == 'C') { if (options.ticket_id == NULL) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply ticket ID with -t"); goto done; } if (options.force == FALSE) { pcmk_ticket_t *ticket = NULL; ticket = find_ticket(options.ticket_id, scheduler); if (ticket == NULL) { exit_code = CRM_EX_NOSUCH; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "No such ticket '%s'", options.ticket_id); goto done; } if (ticket->granted) { ticket_warning(options.ticket_id, "revoke"); exit_code = CRM_EX_INSUFFICIENT_PRIV; goto done; } } rc = delete_ticket_state(options.ticket_id, cib_conn); exit_code = pcmk_rc2exitc(rc); if (rc != pcmk_rc_ok) { g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not clean up ticket: %s", pcmk_rc_str(rc)); } } else if (modified) { if (options.ticket_id == NULL) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply ticket ID with -t"); goto done; } if (options.attr_value && (pcmk__str_empty(options.attr_name))) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply attribute name with -S for -v %s", options.attr_value); goto done; } if (options.attr_name && (pcmk__str_empty(options.attr_value))) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply attribute value with -v for -S %s", options.attr_value); goto done; } if (!allow_modification(options.ticket_id)) { exit_code = CRM_EX_INSUFFICIENT_PRIV; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Ticket modification not allowed"); goto done; } rc = modify_ticket_state(options.ticket_id, cib_conn, scheduler); exit_code = pcmk_rc2exitc(rc); if (rc != pcmk_rc_ok) { g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not modify ticket: %s", pcmk_rc_str(rc)); } } else if (options.ticket_cmd == 'S') { /* Correct usage was handled in the "if (modified)" block above, so * this is just for reporting usage errors */ if (pcmk__str_empty(options.attr_name)) { // We only get here if ticket_cmd was left as default exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply a command"); goto done; } if (options.ticket_id == NULL) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply ticket ID with -t"); goto done; } if (pcmk__str_empty(options.attr_value)) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply value with -v for -S %s", options.attr_name); goto done; } } else { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Unknown command: %c", options.ticket_cmd); } done: if (attr_set) { g_hash_table_destroy(attr_set); } attr_set = NULL; if (attr_delete) { g_list_free_full(attr_delete, free); } attr_delete = NULL; pe_free_working_set(scheduler); scheduler = NULL; cib__clean_up_connection(&cib_conn); g_strfreev(processed_args); pcmk__free_arg_context(context); g_free(options.attr_default); g_free(options.attr_id); free(options.attr_name); free(options.attr_value); free(options.get_attr_name); g_free(options.set_name); g_free(options.ticket_id); g_free(options.xml_file); pcmk__output_and_clear_error(&error, NULL); crm_exit(exit_code); }