diff --git a/include/crm/common/xml_internal.h b/include/crm/common/xml_internal.h index 1d52bfbeb5..3e71f0d27e 100644 --- a/include/crm/common/xml_internal.h +++ b/include/crm/common/xml_internal.h @@ -1,598 +1,600 @@ /* * 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 // uint32_t #include #include #include /* transitively imports qblog.h */ #include #include #include // PCMK__XE_PROMOTABLE_LEGACY #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), // @COMPAT Can we start including text nodes unconditionally? //! 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), }; void pcmk__xml_init(void); void pcmk__xml_cleanup(void); 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(@" PCMK_XA_TYPE ") or @" PCMK_XA_TYPE "='" PCMK_VALUE_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 \ "[@" PCMK_XA_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 \ "[@" PCMK_XA_TYPE "='" PCMK_VALUE_REMOTE "']" \ "[@" PCMK_XA_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 "='" PCMK_VALUE_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_first_child(const xmlNode *parent, const char *node_name, const char *attr_n, const char *attr_v); +xmlNode *pcmk__xe_resolve_idref(xmlNode *xml, xmlNode *search); + void pcmk__xe_remove_attr(xmlNode *element, const char *name); bool pcmk__xe_remove_attr_cb(xmlNode *xml, void *user_data); void pcmk__xe_remove_matching_attrs(xmlNode *element, bool (*match)(xmlAttrPtr, void *), void *user_data); int pcmk__xe_delete_match(xmlNode *xml, xmlNode *search); int pcmk__xe_replace_match(xmlNode *xml, xmlNode *replace); int pcmk__xe_update_match(xmlNode *xml, xmlNode *update, uint32_t flags); GString *pcmk__element_xpath(const xmlNode *xml); /*! * \internal * \enum pcmk__xml_escape_type * \brief Indicators of which XML characters to escape * * XML allows the escaping of special characters by replacing them with entity * references (for example, """) or character references (for * example, " "). * * The special characters '&' (except as the beginning of an entity * reference) 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). '>' is allowed under most circumstances; we escape * it for safety and symmetry. * * 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 * * Attribute values are handled specially. * * 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. * * A conformant XML processor replaces a literal whitespace character (tab, * newline, carriage return, space) in an attribute value with a space * (\c '#x20') character. However, a reference to a whitespace character (for * example, \c " " for \c '\n') does not get replaced. * * For more details, see the "Attribute-Value Normalization" section of the * XML spec, currently section 3.3.3. Note that the default attribute type * is CDATA; we don't deal with NMTOKENS, etc.: * https://www.w3.org/TR/xml/#AVNormalize * * Pacemaker always delimits attribute values with double quotes, so there's no * need to escape single quotes. * * Newlines and tabs should be escaped in attribute values when XML is * serialized to text, so that future parsing preserves them rather than * normalizing them to spaces. * * We always escape carriage returns, so that they're not converted to spaces * during attribute-value normalization and because displaying them as literals * is messy. */ enum pcmk__xml_escape_type { /*! * For text nodes. * * Escape \c '<', \c '>', and \c '&' using entity references. * * Do not escape \c '\n' and \c '\t'. * * Escape other non-printing characters using character references. */ pcmk__xml_escape_text, /*! * For attribute values. * * Escape \c '<', \c '>', \c '&', and \c '"' using entity references. * * Escape \c '\n', \c '\t', and other non-printing characters using * character references. */ pcmk__xml_escape_attr, /* @COMPAT Drop escaping of at least '\n' and '\t' for * pcmk__xml_escape_attr_pretty when openstack-info, openstack-floating-ip, * and openstack-virtual-ip resource agents no longer depend on it. * * At time of writing, openstack-info may set a multiline value for the * openstack_ports node attribute. The other two agents query the value and * require it to be on one line with no spaces. */ /*! * For attribute values displayed in text output delimited by double quotes. * * Escape \c '\n' as \c "\\n" * * Escape \c '\r' as \c "\\r" * * Escape \c '\t' as \c "\\t" * * Escape \c '"' as \c "\\"" */ pcmk__xml_escape_attr_pretty, }; bool pcmk__xml_needs_escape(const char *text, enum pcmk__xml_escape_type type); char *pcmk__xml_escape(const char *text, enum pcmk__xml_escape_type type); /*! * \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 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__xe_create(xmlNode *parent, const char *name); void pcmk__xml_free(xmlNode *xml); xmlNode *pcmk__xml_copy(xmlNode *parent, xmlNode *src); xmlNode *pcmk__xe_next_same(const xmlNode *node); void pcmk__xe_set_content(xmlNode *node, const char *format, ...) G_GNUC_PRINTF(2, 3); /*! * \internal * \enum pcmk__xa_flags * \brief Flags for operations affecting XML attributes */ enum pcmk__xa_flags { //! Flag has no effect pcmk__xaf_none = 0U, //! Don't overwrite existing values pcmk__xaf_no_overwrite = (1U << 0), /*! * Treat values as score updates where possible (see * \c pcmk__xe_set_score()) */ pcmk__xaf_score_update = (1U << 1), }; int pcmk__xe_copy_attrs(xmlNode *target, const xmlNode *src, uint32_t flags); /*! * \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 * \brief Print an informational message if an xpath query returned multiple * items with the same ID. * * \param[in,out] out The output object * \param[in] search The xpath search result, most typically the result of * calling cib->cmds->query(). * \param[in] name The name searched for */ void pcmk__warn_multiple_name_matches(pcmk__output_t *out, xmlNode *search, const char *name); /* 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); bool pcmk__xml_tree_foreach(xmlNode *xml, bool (*fn)(xmlNode *, void *), void *user_data); static inline const char * pcmk__xml_attr_value(const xmlAttr *attr) { return ((attr == NULL) || (attr->children == NULL))? NULL : (const char *) attr->children->content; } // @COMPAT Remove when v1 patchsets are removed xmlNode *pcmk__diff_v1_xml_object(xmlNode *left, xmlNode *right, bool suppress); // @COMPAT Drop when PCMK__XE_PROMOTABLE_LEGACY is removed static inline const char * pcmk__map_element_name(const xmlNode *xml) { if (xml == NULL) { return NULL; } else if (pcmk__xe_is(xml, PCMK__XE_PROMOTABLE_LEGACY)) { return PCMK_XE_CLONE; } else { return (const char *) xml->name; } } #endif // PCMK__XML_INTERNAL__H diff --git a/lib/common/rules.c b/lib/common/rules.c index 32af83510d..cfe31b303f 100644 --- a/lib/common/rules.c +++ b/lib/common/rules.c @@ -1,1512 +1,1512 @@ /* * 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 // NULL, size_t #include // bool #include // isdigit() #include // regmatch_t #include // uint32_t #include // PRIu32 #include // gboolean, FALSE #include // xmlNode #include #include #include #include #include "crmcommon_private.h" /*! * \internal * \brief Get the condition type corresponding to given condition XML * * \param[in] condition Rule condition XML * * \return Condition type corresponding to \p condition */ enum expression_type pcmk__condition_type(const xmlNode *condition) { const char *name = NULL; // Expression types based on element name if (pcmk__xe_is(condition, PCMK_XE_DATE_EXPRESSION)) { return pcmk__condition_datetime; } else if (pcmk__xe_is(condition, PCMK_XE_RSC_EXPRESSION)) { return pcmk__condition_resource; } else if (pcmk__xe_is(condition, PCMK_XE_OP_EXPRESSION)) { return pcmk__condition_operation; } else if (pcmk__xe_is(condition, PCMK_XE_RULE)) { return pcmk__condition_rule; } else if (!pcmk__xe_is(condition, PCMK_XE_EXPRESSION)) { return pcmk__condition_unknown; } // Expression types based on node attribute name name = crm_element_value(condition, PCMK_XA_ATTRIBUTE); if (pcmk__str_any_of(name, CRM_ATTR_UNAME, CRM_ATTR_KIND, CRM_ATTR_ID, NULL)) { return pcmk__condition_location; } return pcmk__condition_attribute; } /*! * \internal * \brief Get parent XML element's ID for logging purposes * * \param[in] xml XML of a subelement * * \return ID of \p xml's parent for logging purposes (guaranteed non-NULL) */ static const char * loggable_parent_id(const xmlNode *xml) { // Default if called without parent (likely for unit testing) const char *parent_id = "implied"; if ((xml != NULL) && (xml->parent != NULL)) { parent_id = pcmk__xe_id(xml->parent); if (parent_id == NULL) { // Not possible with schema validation enabled parent_id = "without ID"; } } return parent_id; } /*! * \internal * \brief Get the moon phase corresponding to a given date/time * * \param[in] now Date/time to get moon phase for * * \return Phase of the moon corresponding to \p now, where 0 is the new moon * and 7 is the full moon * \deprecated This feature has been deprecated since 2.1.6. */ static int phase_of_the_moon(const crm_time_t *now) { /* As per the nethack rules: * - A moon period is 29.53058 days ~= 30 * - A year is 365.2422 days * - Number of days moon phase advances on first day of year compared to * preceding year is (365.2422 - 12 * 29.53058) ~= 11 * - Number of years until same phases fall on the same days of the month * is 18.6 ~= 19 * - Moon phase on first day of year (epact) ~= (11 * (year%19) + 29) % 30 * (29 as initial condition) * - Current phase in days = first day phase + days elapsed in year * - 6 moons ~= 177 days ~= 8 reported phases * 22 (+ 11/22 for rounding) */ uint32_t epact, diy, goldn; uint32_t y; crm_time_get_ordinal(now, &y, &diy); goldn = (y % 19) + 1; epact = (11 * goldn + 18) % 30; if (((epact == 25) && (goldn > 11)) || (epact == 24)) { epact++; } return (((((diy + epact) * 6) + 11) % 177) / 22) & 7; } /*! * \internal * \brief Check an integer value against a range from a date specification * * \param[in] date_spec XML of PCMK_XE_DATE_SPEC element to check * \param[in] id XML ID for logging purposes * \param[in] attr Name of XML attribute with range to check against * \param[in] value Value to compare against range * * \return Standard Pacemaker return code (specifically, pcmk_rc_before_range, * pcmk_rc_after_range, or pcmk_rc_ok to indicate that result is either * within range or undetermined) * \note We return pcmk_rc_ok for an undetermined result so we can continue * checking the next range attribute. */ static int check_range(const xmlNode *date_spec, const char *id, const char *attr, uint32_t value) { int rc = pcmk_rc_ok; const char *range = crm_element_value(date_spec, attr); long long low, high; if (range == NULL) { // Attribute not present goto bail; } if (pcmk__parse_ll_range(range, &low, &high) != pcmk_rc_ok) { // Invalid range /* @COMPAT When we can break behavioral backward compatibility, treat * the entire rule as not passing. */ pcmk__config_err("Ignoring " PCMK_XE_DATE_SPEC " %s attribute %s because '%s' is not a valid range", id, attr, range); } else if ((low != -1) && (value < low)) { rc = pcmk_rc_before_range; } else if ((high != -1) && (value > high)) { rc = pcmk_rc_after_range; } bail: crm_trace("Checked " PCMK_XE_DATE_SPEC " %s %s='%s' for %" PRIu32 ": %s", id, attr, pcmk__s(range, ""), value, pcmk_rc_str(rc)); return rc; } /*! * \internal * \brief Evaluate a date specification for a given date/time * * \param[in] date_spec XML of PCMK_XE_DATE_SPEC element to evaluate * \param[in] now Time to check * * \return Standard Pacemaker return code (specifically, EINVAL for NULL * arguments, pcmk_rc_ok if time matches specification, or * pcmk_rc_before_range, pcmk_rc_after_range, or pcmk_rc_op_unsatisfied * as appropriate to how time relates to specification) */ int pcmk__evaluate_date_spec(const xmlNode *date_spec, const crm_time_t *now) { const char *id = NULL; const char *parent_id = loggable_parent_id(date_spec); // Range attributes that can be specified for a PCMK_XE_DATE_SPEC element struct range { const char *attr; uint32_t value; } ranges[] = { { PCMK_XA_YEARS, 0U }, { PCMK_XA_MONTHS, 0U }, { PCMK_XA_MONTHDAYS, 0U }, { PCMK_XA_HOURS, 0U }, { PCMK_XA_MINUTES, 0U }, { PCMK_XA_SECONDS, 0U }, { PCMK_XA_YEARDAYS, 0U }, { PCMK_XA_WEEKYEARS, 0U }, { PCMK_XA_WEEKS, 0U }, { PCMK_XA_WEEKDAYS, 0U }, { PCMK__XA_MOON, 0U }, }; if ((date_spec == NULL) || (now == NULL)) { return EINVAL; } // Get specification ID (for logging) id = pcmk__xe_id(date_spec); if (pcmk__str_empty(id)) { // Not possible with schema validation enabled /* @COMPAT When we can break behavioral backward compatibility, * fail the specification */ pcmk__config_warn(PCMK_XE_DATE_SPEC " subelement of " PCMK_XE_DATE_EXPRESSION " %s has no " PCMK_XA_ID, parent_id); id = "without ID"; // for logging } // Year, month, day crm_time_get_gregorian(now, &(ranges[0].value), &(ranges[1].value), &(ranges[2].value)); // Hour, minute, second crm_time_get_timeofday(now, &(ranges[3].value), &(ranges[4].value), &(ranges[5].value)); // Year (redundant) and day of year crm_time_get_ordinal(now, &(ranges[0].value), &(ranges[6].value)); // Week year, week of week year, day of week crm_time_get_isoweek(now, &(ranges[7].value), &(ranges[8].value), &(ranges[9].value)); // Moon phase (deprecated) ranges[10].value = phase_of_the_moon(now); if (crm_element_value(date_spec, PCMK__XA_MOON) != NULL) { pcmk__config_warn("Support for '" PCMK__XA_MOON "' in " PCMK_XE_DATE_SPEC " elements (such as %s) is " "deprecated and will be removed in a future release " "of Pacemaker", id); } for (int i = 0; i < PCMK__NELEM(ranges); ++i) { int rc = check_range(date_spec, id, ranges[i].attr, ranges[i].value); if (rc != pcmk_rc_ok) { return rc; } } // All specified ranges passed, or none were given (also considered a pass) return pcmk_rc_ok; } #define ADD_COMPONENT(component) do { \ int sub_rc = pcmk__add_time_from_xml(*end, component, duration); \ if (sub_rc != pcmk_rc_ok) { \ /* @COMPAT return sub_rc when we can break compatibility */ \ pcmk__config_warn("Ignoring %s in " PCMK_XE_DURATION " %s " \ "because it is invalid", \ pcmk__time_component_attr(component), id); \ rc = sub_rc; \ } \ } while (0) /*! * \internal * \brief Given a duration and a start time, calculate the end time * * \param[in] duration XML of PCMK_XE_DURATION element * \param[in] start Start time * \param[out] end Where to store end time (\p *end must be NULL * initially) * * \return Standard Pacemaker return code * \note The caller is responsible for freeing \p *end using crm_time_free(). */ int pcmk__unpack_duration(const xmlNode *duration, const crm_time_t *start, crm_time_t **end) { int rc = pcmk_rc_ok; const char *id = NULL; const char *parent_id = loggable_parent_id(duration); if ((start == NULL) || (duration == NULL) || (end == NULL) || (*end != NULL)) { return EINVAL; } // Get duration ID (for logging) id = pcmk__xe_id(duration); if (pcmk__str_empty(id)) { // Not possible with schema validation enabled /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_unpack_error instead */ pcmk__config_warn(PCMK_XE_DURATION " subelement of " PCMK_XE_DATE_EXPRESSION " %s has no " PCMK_XA_ID, parent_id); id = "without ID"; } *end = pcmk_copy_time(start); ADD_COMPONENT(pcmk__time_years); ADD_COMPONENT(pcmk__time_months); ADD_COMPONENT(pcmk__time_weeks); ADD_COMPONENT(pcmk__time_days); ADD_COMPONENT(pcmk__time_hours); ADD_COMPONENT(pcmk__time_minutes); ADD_COMPONENT(pcmk__time_seconds); return rc; } /*! * \internal * \brief Evaluate a range check for a given date/time * * \param[in] date_expression XML of PCMK_XE_DATE_EXPRESSION element * \param[in] id Expression ID for logging purposes * \param[in] now Date/time to compare * \param[in,out] next_change If not NULL, set this to when the evaluation * will change, if known and earlier than the * original value * * \return Standard Pacemaker return code */ static int evaluate_in_range(const xmlNode *date_expression, const char *id, const crm_time_t *now, crm_time_t *next_change) { crm_time_t *start = NULL; crm_time_t *end = NULL; if (pcmk__xe_get_datetime(date_expression, PCMK_XA_START, &start) != pcmk_rc_ok) { /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_unpack_error */ pcmk__config_warn("Ignoring " PCMK_XA_START " in " PCMK_XE_DATE_EXPRESSION " %s because it is invalid", id); } if (pcmk__xe_get_datetime(date_expression, PCMK_XA_END, &end) != pcmk_rc_ok) { /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_unpack_error */ pcmk__config_warn("Ignoring " PCMK_XA_END " in " PCMK_XE_DATE_EXPRESSION " %s because it is invalid", id); } if ((start == NULL) && (end == NULL)) { // Not possible with schema validation enabled /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_unpack_error */ pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " "passing because in_range requires at least one of " PCMK_XA_START " or " PCMK_XA_END, id); return pcmk_rc_undetermined; } if (end == NULL) { xmlNode *duration = pcmk__xe_first_child(date_expression, PCMK_XE_DURATION, NULL, NULL); if (duration != NULL) { /* @COMPAT When we can break behavioral backward compatibility, * return the result of this if not OK */ pcmk__unpack_duration(duration, start, &end); } } if ((start != NULL) && (crm_time_compare(now, start) < 0)) { pcmk__set_time_if_earlier(next_change, start); crm_time_free(start); crm_time_free(end); return pcmk_rc_before_range; } if (end != NULL) { if (crm_time_compare(now, end) > 0) { crm_time_free(start); crm_time_free(end); return pcmk_rc_after_range; } // Evaluation doesn't change until second after end if (next_change != NULL) { crm_time_add_seconds(end, 1); pcmk__set_time_if_earlier(next_change, end); } } crm_time_free(start); crm_time_free(end); return pcmk_rc_within_range; } /*! * \internal * \brief Evaluate a greater-than check for a given date/time * * \param[in] date_expression XML of PCMK_XE_DATE_EXPRESSION element * \param[in] id Expression ID for logging purposes * \param[in] now Date/time to compare * \param[in,out] next_change If not NULL, set this to when the evaluation * will change, if known and earlier than the * original value * * \return Standard Pacemaker return code */ static int evaluate_gt(const xmlNode *date_expression, const char *id, const crm_time_t *now, crm_time_t *next_change) { crm_time_t *start = NULL; if (pcmk__xe_get_datetime(date_expression, PCMK_XA_START, &start) != pcmk_rc_ok) { /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_unpack_error */ pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " "passing because " PCMK_XA_START " is invalid", id); return pcmk_rc_undetermined; } if (start == NULL) { // Not possible with schema validation enabled /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_unpack_error */ pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " "passing because " PCMK_VALUE_GT " requires " PCMK_XA_START, id); return pcmk_rc_undetermined; } if (crm_time_compare(now, start) > 0) { crm_time_free(start); return pcmk_rc_within_range; } // Evaluation doesn't change until second after start time crm_time_add_seconds(start, 1); pcmk__set_time_if_earlier(next_change, start); crm_time_free(start); return pcmk_rc_before_range; } /*! * \internal * \brief Evaluate a less-than check for a given date/time * * \param[in] date_expression XML of PCMK_XE_DATE_EXPRESSION element * \param[in] id Expression ID for logging purposes * \param[in] now Date/time to compare * \param[in,out] next_change If not NULL, set this to when the evaluation * will change, if known and earlier than the * original value * * \return Standard Pacemaker return code */ static int evaluate_lt(const xmlNode *date_expression, const char *id, const crm_time_t *now, crm_time_t *next_change) { crm_time_t *end = NULL; if (pcmk__xe_get_datetime(date_expression, PCMK_XA_END, &end) != pcmk_rc_ok) { /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_unpack_error */ pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " "passing because " PCMK_XA_END " is invalid", id); return pcmk_rc_undetermined; } if (end == NULL) { // Not possible with schema validation enabled /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_unpack_error */ pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not " "passing because " PCMK_VALUE_GT " requires " PCMK_XA_END, id); return pcmk_rc_undetermined; } if (crm_time_compare(now, end) < 0) { pcmk__set_time_if_earlier(next_change, end); crm_time_free(end); return pcmk_rc_within_range; } crm_time_free(end); return pcmk_rc_after_range; } /*! * \internal * \brief Evaluate a rule's date expression for a given date/time * * \param[in] date_expression XML of a PCMK_XE_DATE_EXPRESSION element * \param[in] now Time to use for evaluation * \param[in,out] next_change If not NULL, set this to when the evaluation * will change, if known and earlier than the * original value * * \return Standard Pacemaker return code (unlike most other evaluation * functions, this can return either pcmk_rc_ok or pcmk_rc_within_range * on success) */ int pcmk__evaluate_date_expression(const xmlNode *date_expression, const crm_time_t *now, crm_time_t *next_change) { const char *id = NULL; const char *op = NULL; int rc = pcmk_rc_undetermined; if ((date_expression == NULL) || (now == NULL)) { return EINVAL; } // Get expression ID (for logging) id = pcmk__xe_id(date_expression); if (pcmk__str_empty(id)) { // Not possible with schema validation enabled /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_unpack_error */ pcmk__config_warn(PCMK_XE_DATE_EXPRESSION " element has no " PCMK_XA_ID); id = "without ID"; // for logging } op = crm_element_value(date_expression, PCMK_XA_OPERATION); if (pcmk__str_eq(op, PCMK_VALUE_IN_RANGE, pcmk__str_null_matches|pcmk__str_casei)) { rc = evaluate_in_range(date_expression, id, now, next_change); } else if (pcmk__str_eq(op, PCMK_VALUE_DATE_SPEC, pcmk__str_casei)) { xmlNode *date_spec = pcmk__xe_first_child(date_expression, PCMK_XE_DATE_SPEC, NULL, NULL); if (date_spec == NULL) { // Not possible with schema validation enabled /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_unpack_error */ pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s " "as not passing because " PCMK_VALUE_DATE_SPEC " operations require a " PCMK_XE_DATE_SPEC " subelement", id); } else { // @TODO set next_change appropriately rc = pcmk__evaluate_date_spec(date_spec, now); } } else if (pcmk__str_eq(op, PCMK_VALUE_GT, pcmk__str_casei)) { rc = evaluate_gt(date_expression, id, now, next_change); } else if (pcmk__str_eq(op, PCMK_VALUE_LT, pcmk__str_casei)) { rc = evaluate_lt(date_expression, id, now, next_change); } else { // Not possible with schema validation enabled /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_unpack_error */ pcmk__config_warn("Treating " PCMK_XE_DATE_EXPRESSION " %s as not passing because '%s' is not a valid " PCMK_XE_OPERATION, op); } crm_trace(PCMK_XE_DATE_EXPRESSION " %s (%s): %s (%d)", id, op, pcmk_rc_str(rc), rc); return rc; } /*! * \internal * \brief Go through submatches in a string, either counting how many bytes * would be needed for the expansion, or performing the expansion, * as requested * * \param[in] string String possibly containing submatch variables * \param[in] match String that matched the regular expression * \param[in] submatches Regular expression submatches (as set by regexec()) * \param[in] nmatches Number of entries in \p submatches[] * \param[out] expansion If not NULL, expand string here (must be * pre-allocated to appropriate size) * \param[out] nbytes If not NULL, set to size needed for expansion * * \return true if any expansion is needed, otherwise false */ static bool process_submatches(const char *string, const char *match, const regmatch_t submatches[], int nmatches, char *expansion, size_t *nbytes) { bool expanded = false; const char *src = string; if (nbytes != NULL) { *nbytes = 1; // Include space for terminator } while (*src != '\0') { int submatch = 0; size_t match_len = 0; if ((src[0] != '%') || !isdigit(src[1])) { /* src does not point to the first character of a %N sequence, * so expand this character as-is */ if (expansion != NULL) { *expansion++ = *src; } if (nbytes != NULL) { ++(*nbytes); } ++src; continue; } submatch = src[1] - '0'; src += 2; // Skip over %N sequence in source string expanded = true; // Expansion will be different from source // Omit sequence from expansion unless it has a non-empty match if ((nmatches <= submatch) // Not enough submatches || (submatches[submatch].rm_so < 0) // Pattern did not match || (submatches[submatch].rm_eo <= submatches[submatch].rm_so)) { // Match was empty continue; } match_len = submatches[submatch].rm_eo - submatches[submatch].rm_so; if (nbytes != NULL) { *nbytes += match_len; } if (expansion != NULL) { memcpy(expansion, match + submatches[submatch].rm_so, match_len); expansion += match_len; } } return expanded; } /*! * \internal * \brief Expand any regular expression submatches (%0-%9) in a string * * \param[in] string String possibly containing submatch variables * \param[in] match String that matched the regular expression * \param[in] submatches Regular expression submatches (as set by regexec()) * \param[in] nmatches Number of entries in \p submatches[] * * \return Newly allocated string identical to \p string with submatches * expanded on success, or NULL if no expansions were needed * \note The caller is responsible for freeing the result with free() */ char * pcmk__replace_submatches(const char *string, const char *match, const regmatch_t submatches[], int nmatches) { size_t nbytes = 0; char *result = NULL; if (pcmk__str_empty(string) || pcmk__str_empty(match)) { return NULL; // Nothing to expand } // Calculate how much space will be needed for expanded string if (!process_submatches(string, match, submatches, nmatches, NULL, &nbytes)) { return NULL; // No expansions needed } // Allocate enough space for expanded string result = pcmk__assert_alloc(nbytes, sizeof(char)); // Expand submatches (void) process_submatches(string, match, submatches, nmatches, result, NULL); return result; } /*! * \internal * \brief Parse a comparison type from a string * * \param[in] op String with comparison type (valid values are * \c PCMK_VALUE_DEFINED, \c PCMK_VALUE_NOT_DEFINED, * \c PCMK_VALUE_EQ, \c PCMK_VALUE_NE, * \c PCMK_VALUE_LT, \c PCMK_VALUE_LTE, * \c PCMK_VALUE_GT, or \c PCMK_VALUE_GTE) * * \return Comparison type corresponding to \p op */ enum pcmk__comparison pcmk__parse_comparison(const char *op) { if (pcmk__str_eq(op, PCMK_VALUE_DEFINED, pcmk__str_casei)) { return pcmk__comparison_defined; } else if (pcmk__str_eq(op, PCMK_VALUE_NOT_DEFINED, pcmk__str_casei)) { return pcmk__comparison_undefined; } else if (pcmk__str_eq(op, PCMK_VALUE_EQ, pcmk__str_casei)) { return pcmk__comparison_eq; } else if (pcmk__str_eq(op, PCMK_VALUE_NE, pcmk__str_casei)) { return pcmk__comparison_ne; } else if (pcmk__str_eq(op, PCMK_VALUE_LT, pcmk__str_casei)) { return pcmk__comparison_lt; } else if (pcmk__str_eq(op, PCMK_VALUE_LTE, pcmk__str_casei)) { return pcmk__comparison_lte; } else if (pcmk__str_eq(op, PCMK_VALUE_GT, pcmk__str_casei)) { return pcmk__comparison_gt; } else if (pcmk__str_eq(op, PCMK_VALUE_GTE, pcmk__str_casei)) { return pcmk__comparison_gte; } return pcmk__comparison_unknown; } /*! * \internal * \brief Parse a value type from a string * * \param[in] type String with value type (valid values are NULL, * \c PCMK_VALUE_STRING, \c PCMK_VALUE_INTEGER, * \c PCMK_VALUE_NUMBER, and \c PCMK_VALUE_VERSION) * \param[in] op Operation type (used only to select default) * \param[in] value1 First value being compared (used only to select default) * \param[in] value2 Second value being compared (used only to select default) */ enum pcmk__type pcmk__parse_type(const char *type, enum pcmk__comparison op, const char *value1, const char *value2) { if (type == NULL) { switch (op) { case pcmk__comparison_lt: case pcmk__comparison_lte: case pcmk__comparison_gt: case pcmk__comparison_gte: if (((value1 != NULL) && (strchr(value1, '.') != NULL)) || ((value2 != NULL) && (strchr(value2, '.') != NULL))) { return pcmk__type_number; } return pcmk__type_integer; default: return pcmk__type_string; } } if (pcmk__str_eq(type, PCMK_VALUE_STRING, pcmk__str_casei)) { return pcmk__type_string; } else if (pcmk__str_eq(type, PCMK_VALUE_INTEGER, pcmk__str_casei)) { return pcmk__type_integer; } else if (pcmk__str_eq(type, PCMK_VALUE_NUMBER, pcmk__str_casei)) { return pcmk__type_number; } else if (pcmk__str_eq(type, PCMK_VALUE_VERSION, pcmk__str_casei)) { return pcmk__type_version; } return pcmk__type_unknown; } /*! * \internal * \brief Compare two strings according to a given type * * \param[in] value1 String with first value to compare * \param[in] value2 String with second value to compare * \param[in] type How to interpret the values * * \return Standard comparison result (a negative integer if \p value1 is * lesser, 0 if the values are equal, and a positive integer if * \p value1 is greater) */ int pcmk__cmp_by_type(const char *value1, const char *value2, enum pcmk__type type) { // NULL compares as less than non-NULL if (value2 == NULL) { return (value1 == NULL)? 0 : 1; } if (value1 == NULL) { return -1; } switch (type) { case pcmk__type_string: return strcasecmp(value1, value2); case pcmk__type_integer: { long long integer1; long long integer2; if ((pcmk__scan_ll(value1, &integer1, 0LL) != pcmk_rc_ok) || (pcmk__scan_ll(value2, &integer2, 0LL) != pcmk_rc_ok)) { crm_warn("Comparing '%s' and '%s' as strings because " "invalid as integers", value1, value2); return strcasecmp(value1, value2); } return (integer1 < integer2)? -1 : (integer1 > integer2)? 1 : 0; } break; case pcmk__type_number: { double num1; double num2; if ((pcmk__scan_double(value1, &num1, NULL, NULL) != pcmk_rc_ok) || (pcmk__scan_double(value2, &num2, NULL, NULL) != pcmk_rc_ok)) { crm_warn("Comparing '%s' and '%s' as strings because invalid as " "numbers", value1, value2); return strcasecmp(value1, value2); } return (num1 < num2)? -1 : (num1 > num2)? 1 : 0; } break; case pcmk__type_version: return compare_version(value1, value2); default: // Invalid type return 0; } } /*! * \internal * \brief Parse a reference value source from a string * * \param[in] source String indicating reference value source * * \return Reference value source corresponding to \p source */ enum pcmk__reference_source pcmk__parse_source(const char *source) { if (pcmk__str_eq(source, PCMK_VALUE_LITERAL, pcmk__str_casei|pcmk__str_null_matches)) { return pcmk__source_literal; } else if (pcmk__str_eq(source, PCMK_VALUE_PARAM, pcmk__str_casei)) { return pcmk__source_instance_attrs; } else if (pcmk__str_eq(source, PCMK_VALUE_META, pcmk__str_casei)) { return pcmk__source_meta_attrs; } else { return pcmk__source_unknown; } } /*! * \internal * \brief Parse a boolean operator from a string * * \param[in] combine String indicating boolean operator * * \return Enumeration value corresponding to \p combine */ enum pcmk__combine pcmk__parse_combine(const char *combine) { if (pcmk__str_eq(combine, PCMK_VALUE_AND, pcmk__str_null_matches|pcmk__str_casei)) { return pcmk__combine_and; } else if (pcmk__str_eq(combine, PCMK_VALUE_OR, pcmk__str_casei)) { return pcmk__combine_or; } else { return pcmk__combine_unknown; } } /*! * \internal * \brief Get the result of a node attribute comparison for rule evaluation * * \param[in] actual Actual node attribute value * \param[in] reference Node attribute value from rule (ignored for * \p comparison of \c pcmk__comparison_defined or * \c pcmk__comparison_undefined) * \param[in] type How to interpret the values * \param[in] comparison How to compare the values * * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok if the * comparison passes, and some other value if it does not) */ static int evaluate_attr_comparison(const char *actual, const char *reference, enum pcmk__type type, enum pcmk__comparison comparison) { int cmp = 0; switch (comparison) { case pcmk__comparison_defined: return (actual != NULL)? pcmk_rc_ok : pcmk_rc_op_unsatisfied; case pcmk__comparison_undefined: return (actual == NULL)? pcmk_rc_ok : pcmk_rc_op_unsatisfied; default: break; } cmp = pcmk__cmp_by_type(actual, reference, type); switch (comparison) { case pcmk__comparison_eq: return (cmp == 0)? pcmk_rc_ok : pcmk_rc_op_unsatisfied; case pcmk__comparison_ne: return (cmp != 0)? pcmk_rc_ok : pcmk_rc_op_unsatisfied; default: break; } if ((actual == NULL) || (reference == NULL)) { return pcmk_rc_op_unsatisfied; // Comparison would be meaningless } switch (comparison) { case pcmk__comparison_lt: return (cmp < 0)? pcmk_rc_ok : pcmk_rc_after_range; case pcmk__comparison_lte: return (cmp <= 0)? pcmk_rc_ok : pcmk_rc_after_range; case pcmk__comparison_gt: return (cmp > 0)? pcmk_rc_ok : pcmk_rc_before_range; case pcmk__comparison_gte: return (cmp >= 0)? pcmk_rc_ok : pcmk_rc_before_range; default: // Not possible with schema validation enabled return pcmk_rc_op_unsatisfied; } } /*! * \internal * \brief Get a reference value from a configured source * * \param[in] value Value given in rule expression * \param[in] source Reference value source * \param[in] rule_input Values used to evaluate rule criteria */ static const char * value_from_source(const char *value, enum pcmk__reference_source source, const pcmk_rule_input_t *rule_input) { GHashTable *table = NULL; if (pcmk__str_empty(value)) { /* @COMPAT When we can break backward compatibility, drop this block so * empty strings are treated as such (there should never be an empty * string as an instance attribute or meta-attribute name, so those will * get NULL anyway, but it could matter for literal comparisons) */ return NULL; } switch (source) { case pcmk__source_literal: return value; case pcmk__source_instance_attrs: table = rule_input->rsc_params; break; case pcmk__source_meta_attrs: table = rule_input->rsc_meta; break; default: return NULL; // Not possible } if (table == NULL) { return NULL; } return (const char *) g_hash_table_lookup(table, value); } /*! * \internal * \brief Evaluate a node attribute rule expression * * \param[in] expression XML of a rule's PCMK_XE_EXPRESSION subelement * \param[in] rule_input Values used to evaluate rule criteria * * \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression * passes, some other value if it does not) */ int pcmk__evaluate_attr_expression(const xmlNode *expression, const pcmk_rule_input_t *rule_input) { const char *id = NULL; const char *op = NULL; const char *attr = NULL; const char *type_s = NULL; const char *value = NULL; const char *actual = NULL; const char *source_s = NULL; const char *reference = NULL; char *expanded_attr = NULL; int rc = pcmk_rc_ok; enum pcmk__type type = pcmk__type_unknown; enum pcmk__reference_source source = pcmk__source_unknown; enum pcmk__comparison comparison = pcmk__comparison_unknown; if ((expression == NULL) || (rule_input == NULL)) { return EINVAL; } // Get expression ID (for logging) id = pcmk__xe_id(expression); if (pcmk__str_empty(id)) { /* @COMPAT When we can break behavioral backward compatibility, * fail the expression */ pcmk__config_warn(PCMK_XE_EXPRESSION " element has no " PCMK_XA_ID); id = "without ID"; // for logging } /* Get name of node attribute to compare (expanding any %0-%9 to * regular expression submatches) */ attr = crm_element_value(expression, PCMK_XA_ATTRIBUTE); if (attr == NULL) { pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not passing " "because " PCMK_XA_ATTRIBUTE " was not specified", id); return pcmk_rc_unpack_error; } expanded_attr = pcmk__replace_submatches(attr, rule_input->rsc_id, rule_input->rsc_id_submatches, rule_input->rsc_id_nmatches); if (expanded_attr != NULL) { attr = expanded_attr; } // Get and validate operation op = crm_element_value(expression, PCMK_XA_OPERATION); comparison = pcmk__parse_comparison(op); if (comparison == pcmk__comparison_unknown) { // Not possible with schema validation enabled if (op == NULL) { pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not " "passing because it has no " PCMK_XA_OPERATION, id); } else { pcmk__config_err("Treating " PCMK_XE_EXPRESSION " %s as not " "passing because '%s' is not a valid " PCMK_XA_OPERATION, id, op); } rc = pcmk_rc_unpack_error; goto done; } // How reference value is obtained (literal, resource meta-attribute, etc.) source_s = crm_element_value(expression, PCMK_XA_VALUE_SOURCE); source = pcmk__parse_source(source_s); if (source == pcmk__source_unknown) { // Not possible with schema validation enabled // @COMPAT Fail expression once we can break backward compatibility pcmk__config_warn("Expression %s has invalid " PCMK_XA_VALUE_SOURCE " value '%s', using default " "('" PCMK_VALUE_LITERAL "')", id, source_s); source = pcmk__source_literal; } // Get and validate reference value value = crm_element_value(expression, PCMK_XA_VALUE); switch (comparison) { case pcmk__comparison_defined: case pcmk__comparison_undefined: if (value != NULL) { pcmk__config_warn("Ignoring " PCMK_XA_VALUE " in " PCMK_XE_EXPRESSION " %s because it is unused " "when " PCMK_XA_BOOLEAN_OP " is %s", id, op); } break; default: if (value == NULL) { pcmk__config_warn(PCMK_XE_EXPRESSION " %s has no " PCMK_XA_VALUE, id); } break; } reference = value_from_source(value, source, rule_input); // Get actual value of node attribute if (rule_input->node_attrs != NULL) { actual = g_hash_table_lookup(rule_input->node_attrs, attr); } // Get and validate value type (after expanding reference value) type_s = crm_element_value(expression, PCMK_XA_TYPE); type = pcmk__parse_type(type_s, comparison, actual, reference); if (type == pcmk__type_unknown) { /* Not possible with schema validation enabled * * @COMPAT When we can break behavioral backward compatibility, treat * the expression as not passing. */ pcmk__config_warn("Non-empty node attribute values will be treated as " "equal for " PCMK_XE_EXPRESSION " %s because '%s' " "is not a valid type", id, type); } rc = evaluate_attr_comparison(actual, reference, type, comparison); switch (comparison) { case pcmk__comparison_defined: case pcmk__comparison_undefined: crm_trace(PCMK_XE_EXPRESSION " %s result: %s (for attribute %s %s)", id, pcmk_rc_str(rc), attr, op); break; default: crm_trace(PCMK_XE_EXPRESSION " %s result: " "%s (attribute %s %s '%s' via %s source as %s type)", id, pcmk_rc_str(rc), attr, op, pcmk__s(reference, ""), pcmk__s(source_s, "default"), pcmk__s(type_s, "default")); break; } done: free(expanded_attr); return rc; } /*! * \internal * \brief Evaluate a resource rule expression * * \param[in] rsc_expression XML of rule's \c PCMK_XE_RSC_EXPRESSION subelement * \param[in] rule_input Values used to evaluate rule criteria * * \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression * passes, some other value if it does not) */ int pcmk__evaluate_rsc_expression(const xmlNode *rsc_expression, const pcmk_rule_input_t *rule_input) { const char *id = NULL; const char *standard = NULL; const char *provider = NULL; const char *type = NULL; if ((rsc_expression == NULL) || (rule_input == NULL)) { return EINVAL; } // Validate XML ID id = pcmk__xe_id(rsc_expression); if (pcmk__str_empty(id)) { // Not possible with schema validation enabled /* @COMPAT When we can break behavioral backward compatibility, * fail the expression */ pcmk__config_warn(PCMK_XE_RSC_EXPRESSION " has no " PCMK_XA_ID); id = "without ID"; // for logging } // Compare resource standard standard = crm_element_value(rsc_expression, PCMK_XA_CLASS); if ((standard != NULL) && !pcmk__str_eq(standard, rule_input->rsc_standard, pcmk__str_none)) { crm_trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because " "actual standard '%s' doesn't match '%s'", id, pcmk__s(rule_input->rsc_standard, ""), standard); return pcmk_rc_op_unsatisfied; } // Compare resource provider provider = crm_element_value(rsc_expression, PCMK_XA_PROVIDER); if ((provider != NULL) && !pcmk__str_eq(provider, rule_input->rsc_provider, pcmk__str_none)) { crm_trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because " "actual provider '%s' doesn't match '%s'", id, pcmk__s(rule_input->rsc_provider, ""), provider); return pcmk_rc_op_unsatisfied; } // Compare resource agent type type = crm_element_value(rsc_expression, PCMK_XA_TYPE); if ((type != NULL) && !pcmk__str_eq(type, rule_input->rsc_agent, pcmk__str_none)) { crm_trace(PCMK_XE_RSC_EXPRESSION " %s is unsatisfied because " "actual agent '%s' doesn't match '%s'", id, pcmk__s(rule_input->rsc_agent, ""), type); return pcmk_rc_op_unsatisfied; } crm_trace(PCMK_XE_RSC_EXPRESSION " %s is satisfied by %s%s%s:%s", id, pcmk__s(standard, ""), ((provider == NULL)? "" : ":"), pcmk__s(provider, ""), pcmk__s(type, "")); return pcmk_rc_ok; } /*! * \internal * \brief Evaluate an operation rule expression * * \param[in] op_expression XML of a rule's \c PCMK_XE_OP_EXPRESSION subelement * \param[in] rule_input Values used to evaluate rule criteria * * \return Standard Pacemaker return code (\c pcmk_rc_ok if the expression * is satisfied, some other value if it is not) */ int pcmk__evaluate_op_expression(const xmlNode *op_expression, const pcmk_rule_input_t *rule_input) { const char *id = NULL; const char *name = NULL; const char *interval_s = NULL; guint interval_ms = 0U; if ((op_expression == NULL) || (rule_input == NULL)) { return EINVAL; } // Get operation expression ID (for logging) id = pcmk__xe_id(op_expression); if (pcmk__str_empty(id)) { // Not possible with schema validation enabled /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_op_unsatisfied */ pcmk__config_warn(PCMK_XE_OP_EXPRESSION " element has no " PCMK_XA_ID); id = "without ID"; // for logging } // Validate operation name name = crm_element_value(op_expression, PCMK_XA_NAME); if (name == NULL) { // Not possible with schema validation enabled pcmk__config_warn("Treating " PCMK_XE_OP_EXPRESSION " %s as not " "passing because it has no " PCMK_XA_NAME, id); return pcmk_rc_unpack_error; } // Validate operation interval interval_s = crm_element_value(op_expression, PCMK_META_INTERVAL); if (pcmk_parse_interval_spec(interval_s, &interval_ms) != pcmk_rc_ok) { pcmk__config_warn("Treating " PCMK_XE_OP_EXPRESSION " %s as not " "passing because '%s' is not a valid interval", id, interval_s); return pcmk_rc_unpack_error; } // Compare operation name if (!pcmk__str_eq(name, rule_input->op_name, pcmk__str_none)) { crm_trace(PCMK_XE_OP_EXPRESSION " %s is unsatisfied because " "actual name '%s' doesn't match '%s'", id, pcmk__s(rule_input->op_name, ""), name); return pcmk_rc_op_unsatisfied; } // Compare operation interval (unspecified interval matches all) if ((interval_s != NULL) && (interval_ms != rule_input->op_interval_ms)) { crm_trace(PCMK_XE_OP_EXPRESSION " %s is unsatisfied because " "actual interval %s doesn't match %s", id, pcmk__readable_interval(rule_input->op_interval_ms), pcmk__readable_interval(interval_ms)); return pcmk_rc_op_unsatisfied; } crm_trace(PCMK_XE_OP_EXPRESSION " %s is satisfied (name %s, interval %s)", id, name, pcmk__readable_interval(rule_input->op_interval_ms)); return pcmk_rc_ok; } /*! * \internal * \brief Evaluate a rule condition * * \param[in,out] condition XML containing a rule condition (a subrule, or an * expression of any type) * \param[in] rule_input Values used to evaluate rule criteria * \param[out] next_change If not NULL, set to when evaluation will change * * \return Standard Pacemaker return code (\c pcmk_rc_ok if the condition * passes, some other value if it does not) */ int pcmk__evaluate_condition(xmlNode *condition, const pcmk_rule_input_t *rule_input, crm_time_t *next_change) { if ((condition == NULL) || (rule_input == NULL)) { return EINVAL; } switch (pcmk__condition_type(condition)) { case pcmk__condition_rule: return pcmk_evaluate_rule(condition, rule_input, next_change); case pcmk__condition_attribute: case pcmk__condition_location: return pcmk__evaluate_attr_expression(condition, rule_input); case pcmk__condition_datetime: { int rc = pcmk__evaluate_date_expression(condition, rule_input->now, next_change); return (rc == pcmk_rc_within_range)? pcmk_rc_ok : rc; } case pcmk__condition_resource: return pcmk__evaluate_rsc_expression(condition, rule_input); case pcmk__condition_operation: return pcmk__evaluate_op_expression(condition, rule_input); default: // Not possible with schema validation enabled pcmk__config_err("Treating rule condition %s as not passing " "because %s is not a valid condition type", pcmk__s(pcmk__xe_id(condition), "without ID"), (const char *) condition->name); return pcmk_rc_unpack_error; } } /*! * \brief Evaluate a single rule, including all its conditions * * \param[in,out] rule XML containing a rule definition or its id-ref * \param[in] rule_input Values used to evaluate rule criteria * \param[out] next_change If not NULL, set to when evaluation will change * * \return Standard Pacemaker return code (\c pcmk_rc_ok if the rule is * satisfied, some other value if it is not) */ int pcmk_evaluate_rule(xmlNode *rule, const pcmk_rule_input_t *rule_input, crm_time_t *next_change) { bool empty = true; int rc = pcmk_rc_ok; const char *id = NULL; const char *value = NULL; enum pcmk__combine combine = pcmk__combine_unknown; if ((rule == NULL) || (rule_input == NULL)) { return EINVAL; } - rule = expand_idref(rule, NULL); + rule = pcmk__xe_resolve_idref(rule, NULL); if (rule == NULL) { // Not possible with schema validation enabled; message already logged return pcmk_rc_unpack_error; } // Validate XML ID id = pcmk__xe_id(rule); if (pcmk__str_empty(id)) { /* @COMPAT When we can break behavioral backward compatibility, * fail the rule */ pcmk__config_warn(PCMK_XE_RULE " has no " PCMK_XA_ID); id = "without ID"; // for logging } value = crm_element_value(rule, PCMK_XA_BOOLEAN_OP); combine = pcmk__parse_combine(value); switch (combine) { case pcmk__combine_and: // For "and", rc defaults to success (reset on failure below) break; case pcmk__combine_or: // For "or", rc defaults to failure (reset on success below) rc = pcmk_rc_op_unsatisfied; break; default: /* @COMPAT When we can break behavioral backward compatibility, * return pcmk_rc_unpack_error */ pcmk__config_warn("Rule %s has invalid " PCMK_XA_BOOLEAN_OP " value '%s', using default '" PCMK_VALUE_AND "'", pcmk__xe_id(rule), value); combine = pcmk__combine_and; break; } // Evaluate each condition for (xmlNode *condition = pcmk__xe_first_child(rule, NULL, NULL, NULL); condition != NULL; condition = pcmk__xe_next(condition)) { empty = false; if (pcmk__evaluate_condition(condition, rule_input, next_change) == pcmk_rc_ok) { if (combine == pcmk__combine_or) { rc = pcmk_rc_ok; // Any pass is final for "or" break; } } else if (combine == pcmk__combine_and) { rc = pcmk_rc_op_unsatisfied; // Any failure is final for "and" break; } } if (empty) { // Not possible with schema validation enabled /* @COMPAT Currently, we don't actually ignore "or" rules because * rc is initialized to failure above in that case. When we can break * backward compatibility, reset rc to pcmk_rc_ok here. */ pcmk__config_warn("Ignoring rule %s because it contains no conditions", id); } crm_trace("Rule %s is %ssatisfied", id, ((rc == pcmk_rc_ok)? "" : "not ")); return rc; } /*! * \internal * \brief Evaluate all rules contained within an element * * \param[in,out] xml XML element possibly containing rule subelements * \param[in] rule_input Values used to evaluate rule criteria * \param[out] next_change If not NULL, set to when evaluation will change * * \return Standard Pacemaker return code (pcmk_rc_ok if there are no contained * rules or any contained rule passes, otherwise the result of the last * rule) * \deprecated On code paths leading to this function, the schema allows * multiple top-level rules only in the deprecated lifetime element * of location constraints. The code also allows multiple top-level * rules when unpacking attribute sets, but this is deprecated and * already prevented by schema validation. This function can be * dropped when support for those is dropped. */ int pcmk__evaluate_rules(xmlNode *xml, const pcmk_rule_input_t *rule_input, crm_time_t *next_change) { // If there are no rules, pass by default int rc = pcmk_rc_ok; bool have_rule = false; for (xmlNode *rule = pcmk__xe_first_child(xml, PCMK_XE_RULE, NULL, NULL); rule != NULL; rule = pcmk__xe_next_same(rule)) { if (have_rule) { pcmk__warn_once(pcmk__wo_multiple_rules, "Support for multiple top-level rules is " "deprecated (replace with a single rule containing " "the existing rules with " PCMK_XA_BOOLEAN_OP "set to " PCMK_VALUE_OR " instead)"); } else { have_rule = true; } rc = pcmk_evaluate_rule(rule, rule_input, next_change); if (rc == pcmk_rc_ok) { break; } } return rc; } diff --git a/lib/common/xml.c b/lib/common/xml.c index 30332d07cf..84b73662ad 100644 --- a/lib/common/xml.c +++ b/lib/common/xml.c @@ -1,2774 +1,2793 @@ /* * 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 // uint32_t #include #include #include #include // stat(), S_ISREG, etc. #include #include #include #include #include #include // PCMK__XML_LOG_BASE, etc. #include "crmcommon_private.h" /*! * \internal * \brief Apply a function to each XML node in a tree (pre-order, depth-first) * * \param[in,out] xml XML tree to traverse * \param[in,out] fn Function to call for each node (returns \c true to * continue traversing the tree or \c false to stop) * \param[in,out] user_data Argument to \p fn * * \return \c false if any \p fn call returned \c false, or \c true otherwise * * \note This function is recursive. */ bool pcmk__xml_tree_foreach(xmlNode *xml, bool (*fn)(xmlNode *, void *), void *user_data) { if (!fn(xml, user_data)) { return false; } for (xml = pcmk__xml_first_child(xml); xml != NULL; xml = pcmk__xml_next(xml)) { if (!pcmk__xml_tree_foreach(xml, fn, user_data)) { return false; } } return true; } 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); } /*! * \internal * \brief Clear flags on an XML node * * \param[in,out] xml XML node whose flags to reset * \param[in,out] user_data Ignored * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool reset_xml_node_flags(xmlNode *xml, void *user_data) { xml_node_private_t *nodepriv = xml->_private; if (nodepriv != NULL) { nodepriv->flags = pcmk__xf_none; } return true; } /*! * \internal * \brief Set the \c pcmk__xf_dirty and \c pcmk__xf_created flags on an XML node * * \param[in,out] xml Node whose flags to set * \param[in] user_data Ignored * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool mark_xml_dirty_created(xmlNode *xml, void *user_data) { xml_node_private_t *nodepriv = xml->_private; if (nodepriv != NULL) { pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created); } return true; } /*! * \internal * \brief Mark an XML tree as dirty and created, and mark its parents dirty * * Also mark the document dirty. * * \param[in,out] xml Tree to mark as dirty and created */ void pcmk__xml_mark_created(xmlNode *xml) { CRM_ASSERT(xml != NULL); if (!pcmk__tracking_xml_changes(xml, false)) { // Tracking is disabled for entire document return; } // Mark all parents and document dirty pcmk__mark_xml_node_dirty(xml); pcmk__xml_tree_foreach(xml, mark_xml_dirty_created, NULL); } #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; g_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 = pcmk__assert_alloc(1, sizeof(xml_doc_private_t)); 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 = pcmk__assert_alloc(1, sizeof(xml_node_private_t)); 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; } /*! * \internal * \brief Remove all attributes marked as deleted from an XML node * * \param[in,out] xml XML node whose deleted attributes to remove * \param[in,out] user_data Ignored * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool accept_attr_deletions(xmlNode *xml, void *user_data) { reset_xml_node_flags(xml, NULL); pcmk__xe_remove_matching_attrs(xml, pcmk__marked_as_deleted, NULL); return true; } /*! * \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_first_child(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; pcmk__xml_tree_foreach(top, accept_attr_deletions, NULL); } /*! * \internal * \brief Find first XML child element matching given criteria * * \param[in] parent XML element to search (can be \c NULL) * \param[in] node_name If not \c NULL, only match children of this type * \param[in] attr_n If not \c 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 \c NULL if none found */ xmlNode * pcmk__xe_first_child(const xmlNode *parent, const char *node_name, const char *attr_n, const char *attr_v) { xmlNode *child = NULL; const char *parent_name = ""; CRM_CHECK((attr_v == NULL) || (attr_n != NULL), return NULL); if (parent != NULL) { child = parent->children; while ((child != NULL) && (child->type != XML_ELEMENT_NODE)) { child = child->next; } parent_name = (const char *) parent->name; } for (; child != NULL; child = pcmk__xe_next(child)) { const char *value = NULL; if ((node_name != NULL) && !pcmk__xe_is(child, node_name)) { // Node name mismatch continue; } if (attr_n == NULL) { // No attribute match needed return child; } value = crm_element_value(child, attr_n); if ((attr_v == NULL) && (value != NULL)) { // attr_v == NULL: Attribute attr_n must be set (to any value) return child; } if ((attr_v != NULL) && (pcmk__str_eq(value, attr_v, pcmk__str_none))) { // attr_v != NULL: Attribute attr_n must be set to value attr_v return child; } } if (node_name == NULL) { node_name = "(any)"; // For logging } if (attr_n != NULL) { crm_trace("XML child node <%s %s=%s> not found in %s", node_name, attr_n, attr_v, parent_name); } else { crm_trace("XML child node <%s> not found in %s", node_name, parent_name); } return NULL; } /*! * \internal * \brief Set an XML attribute, expanding \c ++ and \c += where appropriate * * If \p target already has an attribute named \p name set to an integer value * and \p value is an addition assignment expression on \p name, then expand * \p value to an integer and set attribute \p name to the expanded value in * \p target. * * Otherwise, set attribute \p name on \p target using the literal \p value. * * The original attribute value in \p target and the number in an assignment * expression in \p value are parsed and added as scores (that is, their values * are capped at \c INFINITY and \c -INFINITY). For more details, refer to * \c char2score(). * * For example, suppose \p target has an attribute named \c "X" with value * \c "5", and that \p name is \c "X". * * If \p value is \c "X++", the new value of \c "X" in \p target is \c "6". * * If \p value is \c "X+=3", the new value of \c "X" in \p target is \c "8". * * If \p value is \c "val", the new value of \c "X" in \p target is \c "val". * * If \p value is \c "Y++", the new value of \c "X" in \p target is \c "Y++". * * \param[in,out] target XML node whose attribute to set * \param[in] name Name of the attribute to set * \param[in] value New value of attribute to set * * \return Standard Pacemaker return code (specifically, \c EINVAL on invalid * argument, or \c pcmk_rc_ok otherwise) */ int pcmk__xe_set_score(xmlNode *target, const char *name, const char *value) { const char *old_value = NULL; CRM_CHECK((target != NULL) && (name != NULL), return EINVAL); if (value == NULL) { return pcmk_rc_ok; } old_value = crm_element_value(target, name); // If no previous value, skip to default case and set the value unexpanded. if (old_value != NULL) { const char *n = name; const char *v = value; // Stop at first character that differs between name and value for (; (*n == *v) && (*n != '\0'); n++, v++); // If value begins with name followed by a "++" or "+=" if ((*n == '\0') && (*v++ == '+') && ((*v == '+') || (*v == '='))) { // If we're expanding ourselves, no previous value was set; use 0 int old_value_i = (old_value != value)? char2score(old_value) : 0; /* value="X++": new value of X is old_value + 1 * value="X+=Y": new value of X is old_value + Y (for some number Y) */ int add = (*v == '+')? 1 : char2score(++v); crm_xml_add_int(target, name, pcmk__add_scores(old_value_i, add)); return pcmk_rc_ok; } } // Default case: set the attribute unexpanded (with value treated literally) if (old_value != value) { crm_xml_add(target, name, value); } return pcmk_rc_ok; } /*! * \internal * \brief Copy XML attributes from a source element to a target element * * This is similar to \c xmlCopyPropList() except that attributes are marked * as dirty for change tracking purposes. * * \param[in,out] target XML element to receive copied attributes from \p src * \param[in] src XML element whose attributes to copy to \p target * \param[in] flags Group of enum pcmk__xa_flags * * \return Standard Pacemaker return code */ int pcmk__xe_copy_attrs(xmlNode *target, const xmlNode *src, uint32_t flags) { CRM_CHECK((src != NULL) && (target != NULL), return EINVAL); for (xmlAttr *attr = pcmk__xe_first_attr(src); attr != NULL; attr = attr->next) { const char *name = (const char *) attr->name; const char *value = pcmk__xml_attr_value(attr); if (pcmk_is_set(flags, pcmk__xaf_no_overwrite) && (crm_element_value(target, name) != NULL)) { continue; } if (pcmk_is_set(flags, pcmk__xaf_score_update)) { pcmk__xe_set_score(target, name, value); } else { crm_xml_add(target, name, value); } } return pcmk_rc_ok; } /*! * \internal * \brief Remove an XML attribute from an element * * \param[in,out] element XML element that owns \p attr * \param[in,out] attr XML attribute to remove from \p element * * \return Standard Pacemaker return code (\c EPERM if ACLs prevent removal of * attributes from \p element, or \c pcmk_rc_ok otherwise) */ static int remove_xe_attr(xmlNode *element, xmlAttr *attr) { if (attr == NULL) { return pcmk_rc_ok; } if (!pcmk__check_acl(element, NULL, pcmk__xf_acl_write)) { // ACLs apply to element, not to particular attributes crm_trace("ACLs prevent removal of attributes from %s element", (const char *) element->name); return EPERM; } if (pcmk__tracking_xml_changes(element, false)) { // Leave in place (marked for removal) until after diff is calculated set_parent_flag(element, pcmk__xf_dirty); pcmk__set_xml_flags((xml_node_private_t *) attr->_private, pcmk__xf_deleted); } else { xmlRemoveProp(attr); } return pcmk_rc_ok; } /*! * \internal * \brief Remove a named attribute from an XML element * * \param[in,out] element XML element to remove an attribute from * \param[in] name Name of attribute to remove */ void pcmk__xe_remove_attr(xmlNode *element, const char *name) { if (name != NULL) { remove_xe_attr(element, xmlHasProp(element, (pcmkXmlStr) name)); } } /*! * \internal * \brief Remove a named attribute from an XML element * * This is a wrapper for \c pcmk__xe_remove_attr() for use with * \c pcmk__xml_tree_foreach(). * * \param[in,out] xml XML element to remove an attribute from * \param[in] user_data Name of attribute to remove * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ bool pcmk__xe_remove_attr_cb(xmlNode *xml, void *user_data) { const char *name = user_data; pcmk__xe_remove_attr(xml, name); return true; } /*! * \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 (remove_xe_attr(element, a) != pcmk_rc_ok) { return; } } } } /*! * \internal * \brief Create a new XML element under a given parent * * \param[in,out] parent XML element that will be the new element's parent * (\c NULL to create a new XML document with the new * node as root) * \param[in] name Name of new element * * \return Newly created XML element (guaranteed not to be \c NULL) */ xmlNode * pcmk__xe_create(xmlNode *parent, const char *name) { xmlNode *node = NULL; CRM_ASSERT(!pcmk__str_empty(name)); if (parent == NULL) { xmlDoc *doc = xmlNewDoc(PCMK__XML_VERSION); pcmk__mem_assert(doc); node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL); pcmk__mem_assert(node); xmlDocSetRootElement(doc, node); } else { node = xmlNewChild(parent, NULL, (pcmkXmlStr) name, NULL); pcmk__mem_assert(node); } pcmk__xml_mark_created(node); return node; } /*! * \internal * \brief Set a formatted string as an XML node's content * * \param[in,out] node Node whose content to set * \param[in] format printf(3)-style format string * \param[in] ... Arguments for \p format * * \note This function escapes special characters. \c xmlNodeSetContent() does * not. */ G_GNUC_PRINTF(2, 3) void pcmk__xe_set_content(xmlNode *node, const char *format, ...) { if (node != NULL) { const char *content = NULL; char *buf = NULL; if (strchr(format, '%') == NULL) { // Nothing to format content = format; } else { va_list ap; va_start(ap, format); if (pcmk__str_eq(format, "%s", pcmk__str_none)) { // No need to make a copy content = va_arg(ap, const char *); } else { CRM_ASSERT(vasprintf(&buf, format, ap) >= 0); content = buf; } va_end(ap); } xmlNodeSetContent(node, (pcmkXmlStr) content); free(buf); } } /*! * \internal * \brief Free an XML tree if ACLs allow; track deletion if tracking is enabled * * If \p node is the root of its document, free the entire document. * * \param[in,out] node XML node to free * \param[in] position Position of \p node among its siblings for change * tracking (negative to calculate automatically if * needed) */ static void free_xml_with_position(xmlNode *node, int position) { xmlDoc *doc = NULL; xml_node_private_t *nodepriv = NULL; if (node == NULL) { return; } doc = node->doc; nodepriv = node->_private; if ((doc != NULL) && (xmlDocGetRootElement(doc) == node)) { /* @TODO Should we check ACLs first? Otherwise it seems like we could * free the root element without write permission. */ xmlFreeDoc(doc); return; } if (!pcmk__check_acl(node, NULL, pcmk__xf_acl_write)) { GString *xpath = NULL; pcmk__if_tracing({}, return); xpath = pcmk__element_xpath(node); qb_log_from_external_source(__func__, __FILE__, "Cannot remove %s %x", LOG_TRACE, __LINE__, 0, xpath->str, nodepriv->flags); g_string_free(xpath, TRUE); return; } if ((doc != NULL) && pcmk__tracking_xml_changes(node, false) && !pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { xml_doc_private_t *docpriv = doc->_private; GString *xpath = pcmk__element_xpath(node); if (xpath != NULL) { pcmk__deleted_xml_t *deleted_obj = NULL; crm_trace("Deleting %s %p from %p", xpath->str, node, doc); deleted_obj = pcmk__assert_alloc(1, sizeof(pcmk__deleted_xml_t)); deleted_obj->path = g_string_free(xpath, FALSE); deleted_obj->position = -1; // Record the position only for XML comments for now if (node->type == XML_COMMENT_NODE) { if (position >= 0) { deleted_obj->position = position; } else { deleted_obj->position = pcmk__xml_position(node, pcmk__xf_skip); } } docpriv->deleted_objs = g_list_append(docpriv->deleted_objs, deleted_obj); pcmk__set_xml_doc_flag(node, pcmk__xf_dirty); } } xmlUnlinkNode(node); xmlFreeNode(node); } /*! * \internal * \brief Free an XML tree if ACLs allow; track deletion if tracking is enabled * * If \p xml is the root of its document, free the entire document. * * \param[in,out] xml XML node to free */ void pcmk__xml_free(xmlNode *xml) { free_xml_with_position(xml, -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); pcmk__mem_assert(doc); copy = xmlDocCopyNode(src, doc, 1); pcmk__mem_assert(copy); xmlDocSetRootElement(doc, copy); } else { copy = xmlDocCopyNode(src, parent->doc, 1); pcmk__mem_assert(copy); xmlAddChild(parent, copy); } pcmk__xml_mark_created(copy); return copy; } /*! * \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: xmlUnlinkNode(iter); xmlFreeNode(iter); break; case XML_ELEMENT_NODE: /* Search it */ pcmk__strip_xml_text(iter); break; default: /* Leave it */ break; } iter = next; } } /*! * \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 Check whether a string has XML special characters that must be escaped * * See \c pcmk__xml_escape() and \c pcmk__xml_escape_type for more details. * * \param[in] text String to check * \param[in] type Type of escaping * * \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, enum pcmk__xml_escape_type type) { if (text == NULL) { return false; } while (*text != '\0') { switch (type) { case pcmk__xml_escape_text: switch (*text) { case '<': case '>': case '&': return true; case '\n': case '\t': break; default: if (g_ascii_iscntrl(*text)) { return true; } break; } break; case pcmk__xml_escape_attr: switch (*text) { case '<': case '>': case '&': case '"': return true; default: if (g_ascii_iscntrl(*text)) { return true; } break; } break; case pcmk__xml_escape_attr_pretty: switch (*text) { case '\n': case '\r': case '\t': case '"': return true; default: break; } break; default: // Invalid enum value CRM_ASSERT(false); break; } text = g_utf8_next_char(text); } return false; } /*! * \internal * \brief Replace special characters with their XML escape sequences * * \param[in] text Text to escape * \param[in] type Type of escaping * * \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 * \note The caller is responsible for freeing the return value using * \c g_free(). */ gchar * pcmk__xml_escape(const char *text, enum pcmk__xml_escape_type type) { GString *copy = NULL; if (text == NULL) { return NULL; } copy = g_string_sized_new(strlen(text)); while (*text != '\0') { // Don't escape any non-ASCII characters if ((*text & 0x80) != 0) { size_t bytes = g_utf8_next_char(text) - text; g_string_append_len(copy, text, bytes); text += bytes; continue; } switch (type) { case pcmk__xml_escape_text: switch (*text) { case '<': g_string_append(copy, PCMK__XML_ENTITY_LT); break; case '>': g_string_append(copy, PCMK__XML_ENTITY_GT); break; case '&': g_string_append(copy, PCMK__XML_ENTITY_AMP); break; case '\n': case '\t': g_string_append_c(copy, *text); break; default: if (g_ascii_iscntrl(*text)) { g_string_append_printf(copy, "&#x%.2X;", *text); } else { g_string_append_c(copy, *text); } break; } break; case pcmk__xml_escape_attr: switch (*text) { case '<': g_string_append(copy, PCMK__XML_ENTITY_LT); break; case '>': g_string_append(copy, PCMK__XML_ENTITY_GT); break; case '&': g_string_append(copy, PCMK__XML_ENTITY_AMP); break; case '"': g_string_append(copy, PCMK__XML_ENTITY_QUOT); break; default: if (g_ascii_iscntrl(*text)) { g_string_append_printf(copy, "&#x%.2X;", *text); } else { g_string_append_c(copy, *text); } break; } break; case pcmk__xml_escape_attr_pretty: switch (*text) { case '"': g_string_append(copy, "\\\""); break; case '\n': g_string_append(copy, "\\n"); break; case '\r': g_string_append(copy, "\\r"); break; case '\t': g_string_append(copy, "\\t"); break; default: g_string_append_c(copy, *text); break; } break; default: // Invalid enum value CRM_ASSERT(false); break; } text = g_utf8_next_char(text); } return g_string_free(copy, FALSE); } /*! * \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 remove_xe_attr(new_xml, attr); 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 pcmk__xml_tree_foreach(candidate, reset_xml_node_flags, NULL); // 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 " PCMK_XA_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 *old_child = NULL; xmlNode *new_child = NULL; xml_node_private_t *nodepriv = NULL; CRM_CHECK(new_xml != NULL, return); if (old_xml == NULL) { pcmk__xml_mark_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 (old_child = pcmk__xml_first_child(old_xml); old_child != NULL; old_child = pcmk__xml_next(old_child)) { new_child = pcmk__xml_match(new_xml, old_child, true); if (new_child != NULL) { mark_xml_changes(old_child, new_child, true); } else { mark_child_deleted(old_child, new_xml); } } // Check for moved or created children new_child = pcmk__xml_first_child(new_xml); while (new_child != NULL) { xmlNode *next = pcmk__xml_next(new_child); old_child = pcmk__xml_match(old_xml, new_child, true); if (old_child == NULL) { // This is a newly created child nodepriv = new_child->_private; pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); // May free new_child 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); } } new_child = next; } } 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 Merge one XML tree into another * * Here, "merge" means: * 1. Copy attribute values from \p update to the target, overwriting in case of * conflict. * 2. Descend through \p update and the target in parallel. At each level, for * each child of \p update, look for a matching child of the target. * a. For each child, if a match is found, go to step 1, recursively merging * the child of \p update into the child of the target. * b. Otherwise, copy the child of \p update as a child of the target. * * A match is defined as the first child of the same type within the target, * with: * * the \c PCMK_XA_ID attribute matching, if set in \p update; otherwise, * * the \c PCMK_XA_ID_REF attribute matching, if set in \p update * * This function does not delete any elements or attributes from the target. It * may add elements or overwrite attributes, as described above. * * \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 \c NULL) * \param[in] flags Group of enum pcmk__xa_flags * \param[in] as_diff If \c true, preserve order of attributes (deprecated * since 2.0.5) * * \note At least one of \p parent and \p target must be non-NULL. * \note This function is recursive. For the top-level call, \p parent is * \c NULL and \p target is not \c NULL. For recursive calls, \p target is * \c NULL and \p parent is not \c NULL. */ void pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update, uint32_t flags, bool as_diff) { /* @COMPAT Refactor further and staticize after v1 patchset deprecation. * * @COMPAT Drop as_diff argument when apply_xml_diff() is dropped. */ const char *update_name = NULL; const char *update_id_attr = NULL; const char *update_id_val = NULL; char *trace_s = NULL; crm_log_xml_trace(update, "update"); crm_log_xml_trace(target, "target"); CRM_CHECK(update != NULL, goto done); if (update->type == XML_COMMENT_NODE) { pcmk__xc_update(parent, target, update); goto done; } update_name = (const char *) update->name; CRM_CHECK(update_name != NULL, goto done); CRM_CHECK((target != NULL) || (parent != NULL), goto done); update_id_val = pcmk__xe_id(update); if (update_id_val != NULL) { update_id_attr = PCMK_XA_ID; } else { update_id_val = crm_element_value(update, PCMK_XA_ID_REF); if (update_id_val != NULL) { update_id_attr = PCMK_XA_ID_REF; } } pcmk__if_tracing( { if (update_id_attr != NULL) { trace_s = crm_strdup_printf("<%s %s=%s/>", update_name, update_id_attr, update_id_val); } else { trace_s = crm_strdup_printf("<%s/>", update_name); } }, {} ); if (target == NULL) { // Recursive call target = pcmk__xe_first_child(parent, update_name, update_id_attr, update_id_val); } if (target == NULL) { // Recursive call with no existing matching child target = pcmk__xe_create(parent, update_name); crm_trace("Added %s", pcmk__s(trace_s, update_name)); } else { // Either recursive call with match, or top-level call crm_trace("Found node %s to update", pcmk__s(trace_s, update_name)); } CRM_CHECK(pcmk__xe_is(target, (const char *) update->name), return); if (!as_diff) { pcmk__xe_copy_attrs(target, update, flags); } else { // Preserve order of attributes. Don't use pcmk__xe_copy_attrs(). 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 (xmlNode *child = pcmk__xml_first_child(update); child != NULL; child = pcmk__xml_next(child)) { crm_trace("Updating child of %s", pcmk__s(trace_s, update_name)); pcmk__xml_update(target, NULL, child, flags, as_diff); } crm_trace("Finished with %s", pcmk__s(trace_s, update_name)); done: free(trace_s); } /*! * \internal * \brief Delete an XML subtree if it matches a search element * * A match is defined as follows: * * \p xml and \p user_data are both element nodes of the same type. * * If \p user_data has attributes set, \p xml has those attributes set to the * same values. (\p xml may have additional attributes set to arbitrary * values.) * * \param[in,out] xml XML subtree to delete upon match * \param[in] user_data Search element * * \return \c true to continue traversing the tree, or \c false to stop (because * \p xml was deleted) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool delete_xe_if_matching(xmlNode *xml, void *user_data) { xmlNode *search = user_data; if (!pcmk__xe_is(search, (const char *) xml->name)) { // No match: either not both elements, or different element types return true; } for (const xmlAttr *attr = pcmk__xe_first_attr(search); attr != NULL; attr = attr->next) { const char *search_val = pcmk__xml_attr_value(attr); const char *xml_val = crm_element_value(xml, (const char *) attr->name); if (!pcmk__str_eq(search_val, xml_val, pcmk__str_casei)) { // No match: an attr in xml doesn't match the attr in search return true; } } crm_log_xml_trace(xml, "delete-match"); crm_log_xml_trace(search, "delete-search"); pcmk__xml_free(xml); // Found a match and deleted it; stop traversing tree return false; } /*! * \internal * \brief Search an XML tree depth-first and delete the first matching element * * This function does not attempt to match the tree root (\p xml). * * A match with a node \c node is defined as follows: * * \c node and \p search are both element nodes of the same type. * * If \p search has attributes set, \c node has those attributes set to the * same values. (\c node may have additional attributes set to arbitrary * values.) * * \param[in,out] xml XML subtree to search * \param[in] search Element to match against * * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on * successful deletion and an error code otherwise) */ int pcmk__xe_delete_match(xmlNode *xml, xmlNode *search) { // See @COMPAT comment in pcmk__xe_replace_match() CRM_CHECK((xml != NULL) && (search != NULL), return EINVAL); for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL; xml = pcmk__xe_next(xml)) { if (!pcmk__xml_tree_foreach(xml, delete_xe_if_matching, search)) { // Found and deleted an element return pcmk_rc_ok; } } // No match found in this subtree return ENXIO; } /*! * \internal * \brief Replace one XML node with a copy of another XML node * * This function handles change tracking and applies ACLs. * * \param[in,out] old XML node to replace * \param[in] new XML node to copy as replacement for \p old * * \note This frees \p old. */ static void replace_node(xmlNode *old, xmlNode *new) { new = xmlCopyNode(new, 1); pcmk__mem_assert(new); // May be unnecessary but avoids slight changes to some test outputs pcmk__xml_tree_foreach(new, reset_xml_node_flags, NULL); 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); } /*! * \internal * \brief Replace one XML subtree with a copy of another if the two match * * A match is defined as follows: * * \p xml and \p user_data are both element nodes of the same type. * * If \p user_data has the \c PCMK_XA_ID attribute set, then \p xml has * \c PCMK_XA_ID set to the same value. * * \param[in,out] xml XML subtree to replace with \p user_data upon match * \param[in] user_data XML to replace \p xml with a copy of upon match * * \return \c true to continue traversing the tree, or \c false to stop (because * \p xml was replaced by \p user_data) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool replace_xe_if_matching(xmlNode *xml, void *user_data) { xmlNode *replace = user_data; const char *xml_id = NULL; const char *replace_id = NULL; xml_id = pcmk__xe_id(xml); replace_id = pcmk__xe_id(replace); if (!pcmk__xe_is(replace, (const char *) xml->name)) { // No match: either not both elements, or different element types return true; } if ((replace_id != NULL) && !pcmk__str_eq(replace_id, xml_id, pcmk__str_none)) { // No match: ID was provided in replace and doesn't match xml's ID return true; } crm_log_xml_trace(xml, "replace-match"); crm_log_xml_trace(replace, "replace-with"); replace_node(xml, replace); // Found a match and replaced it; stop traversing tree return false; } /*! * \internal * \brief Search an XML tree depth-first and replace the first matching element * * This function does not attempt to match the tree root (\p xml). * * A match with a node \c node is defined as follows: * * \c node and \p replace are both element nodes of the same type. * * If \p replace has the \c PCMK_XA_ID attribute set, then \c node has * \c PCMK_XA_ID set to the same value. * * \param[in,out] xml XML tree to search * \param[in] replace XML to replace a matching element with a copy of * * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on * successful replacement and an error code otherwise) */ int pcmk__xe_replace_match(xmlNode *xml, xmlNode *replace) { /* @COMPAT Some of this behavior (like not matching the tree root, which is * allowed by pcmk__xe_update_match()) is questionable for general use but * required for backward compatibility by cib_process_replace() and * cib_process_delete(). Behavior can change at a major version release if * desired. */ CRM_CHECK((xml != NULL) && (replace != NULL), return EINVAL); for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL; xml = pcmk__xe_next(xml)) { if (!pcmk__xml_tree_foreach(xml, replace_xe_if_matching, replace)) { // Found and replaced an element return pcmk_rc_ok; } } // No match found in this subtree return ENXIO; } //! User data for \c update_xe_if_matching() struct update_data { xmlNode *update; //!< Update source uint32_t flags; //!< Group of enum pcmk__xa_flags }; /*! * \internal * \brief Update one XML subtree with another if the two match * * "Update" means to merge a source subtree into a target subtree (see * \c pcmk__xml_update()). * * A match is defined as follows: * * \p xml and \p user_data->update are both element nodes of the same type. * * \p xml and \p user_data->update have the same \c PCMK_XA_ID attribute * value, or \c PCMK_XA_ID is unset in both * * \param[in,out] xml XML subtree to update with \p user_data->update * upon match * \param[in] user_data struct update_data object * * \return \c true to continue traversing the tree, or \c false to stop (because * \p xml was updated by \p user_data->update) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool update_xe_if_matching(xmlNode *xml, void *user_data) { struct update_data *data = user_data; xmlNode *update = data->update; if (!pcmk__xe_is(update, (const char *) xml->name)) { // No match: either not both elements, or different element types return true; } if (!pcmk__str_eq(pcmk__xe_id(xml), pcmk__xe_id(update), pcmk__str_none)) { // No match: ID mismatch return true; } crm_log_xml_trace(xml, "update-match"); crm_log_xml_trace(update, "update-with"); pcmk__xml_update(NULL, xml, update, data->flags, false); // Found a match and replaced it; stop traversing tree return false; } /*! * \internal * \brief Search an XML tree depth-first and update the first matching element * * "Update" means to merge a source subtree into a target subtree (see * \c pcmk__xml_update()). * * A match with a node \c node is defined as follows: * * \c node and \p update are both element nodes of the same type. * * \c node and \p update have the same \c PCMK_XA_ID attribute value, or * \c PCMK_XA_ID is unset in both * * \param[in,out] xml XML tree to search * \param[in] update XML to update a matching element with * \param[in] flags Group of enum pcmk__xa_flags * * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on * successful update and an error code otherwise) */ int pcmk__xe_update_match(xmlNode *xml, xmlNode *update, uint32_t flags) { /* @COMPAT In pcmk__xe_delete_match() and pcmk__xe_replace_match(), we * compare IDs only if the equivalent of the update argument has an ID. * Here, we're stricter: we consider it a mismatch if only one element has * an ID attribute, or if both elements have IDs but they don't match. * * Perhaps we should align the behavior at a major version release. */ struct update_data data = { .update = update, .flags = flags, }; CRM_CHECK((xml != NULL) && (update != NULL), return EINVAL); if (!pcmk__xml_tree_foreach(xml, update_xe_if_matching, &data)) { // Found and updated an element return pcmk_rc_ok; } // No match found in this subtree return ENXIO; } 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 = pcmk__xe_create(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__xe_first_child(input, NULL, NULL, NULL); child != NULL; child = pcmk__xe_next(child)) { if (recursive) { sorted_xml(child, result, recursive); } else { pcmk__xml_copy(result, child); } } return result; } /*! * \internal * \brief Get next sibling XML element with the same name as a given element * * \param[in] node XML element to start from * * \return Next sibling XML element with same name */ xmlNode * pcmk__xe_next_same(const xmlNode *node) { for (xmlNode *match = pcmk__xe_next(node); match != NULL; match = pcmk__xe_next(match)) { if (pcmk__xe_is(match, (const char *) node->name)) { return match; } } return NULL; } /*! * \internal * \brief Initialize the Pacemaker XML environment * * Set an XML buffer allocation scheme, set XML node create and destroy * callbacks, and load schemas into the cache. */ void pcmk__xml_init(void) { // @TODO Try to find a better caller than crm_log_preinit() static bool initialized = false; if (!initialized) { initialized = true; /* Double the buffer size when the buffer needs to grow. The default * allocator XML_BUFFER_ALLOC_EXACT was found to cause poor performance * due to the number of reallocs. */ xmlSetBufferAllocationScheme(XML_BUFFER_ALLOC_DOUBLEIT); // Initialize private data at node creation xmlRegisterNodeDefault(new_private_data); // Free private data at node destruction xmlDeregisterNodeDefault(free_private_data); // Load schemas into the cache crm_schema_init(); } } /*! * \internal * \brief Tear down the Pacemaker XML environment * * Destroy schema cache and clean up memory allocated by libxml2. */ void pcmk__xml_cleanup(void) { crm_schema_cleanup(); xmlCleanupParser(); } +/*! + * \internal + * \brief Get the XML element whose \c PCMK_XA_ID matches an \c PCMK_XA_ID_REF + * + * \param[in] xml Element whose \c PCMK_XA_ID_REF attribute to check + * \param[in] search Node whose document to search for node with matching + * \c PCMK_XA_ID (\c NULL to use \p xml) + * + * \return If \p xml has a \c PCMK_XA_ID_REF attribute, node in + * search's document whose \c PCMK_XA_ID attribute matches; + * otherwise, \p xml + */ xmlNode * -expand_idref(xmlNode * input, xmlNode * top) +pcmk__xe_resolve_idref(xmlNode *xml, xmlNode *search) { char *xpath = NULL; const char *ref = NULL; xmlNode *result = NULL; - if (input == NULL) { + if (xml == NULL) { return NULL; } - ref = crm_element_value(input, PCMK_XA_ID_REF); + ref = crm_element_value(xml, PCMK_XA_ID_REF); if (ref == NULL) { - return input; + return xml; } - if (top == NULL) { - top = input; + if (search == NULL) { + search = xml; } - 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 + xpath = crm_strdup_printf("//%s[@" PCMK_XA_ID "='%s']", xml->name, ref); + result = get_xpath_object(xpath, search, 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); + xml->name, ref, xpath); } free(xpath); return result; } +xmlNode * +expand_idref(xmlNode * input, xmlNode * top) +{ + return pcmk__xe_resolve_idref(input, top); +} + 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(); free(ret); 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_first_child(parent, node_name, ((id == NULL)? id : PCMK_XA_ID), id); } void crm_destroy_xml(gpointer data) { pcmk__xml_free(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__xml_mark_created(child); return child; } int add_node_nocopy(xmlNode *parent, const char *name, xmlNode *child) { add_node_copy(parent, child); pcmk__xml_free(child); return 1; } gboolean xml_has_children(const xmlNode * xml_root) { if (xml_root != NULL && xml_root->children != NULL) { return TRUE; } return FALSE; } static char * replace_text(char *text, size_t *index, size_t *length, const char *replace) { // 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; } char * crm_xml_escape(const char *text) { size_t length = 0; char *copy = NULL; if (text == NULL) { return NULL; } length = strlen(text); copy = pcmk__str_copy(text); 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 = NULL; pcmk__mem_assert(doc); copy = xmlDocCopyNode(src, doc, 1); pcmk__mem_assert(copy); xmlDocSetRootElement(doc, copy); return copy; } xmlNode * create_xml_node(xmlNode *parent, const char *name) { // Like pcmk__xe_create(), but returns NULL on failure xmlNode *node = NULL; CRM_CHECK(!pcmk__str_empty(name), return NULL); if (parent == NULL) { xmlDoc *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__xml_mark_created(node); return node; } xmlNode * pcmk_create_xml_text_node(xmlNode *parent, const char *name, const char *content) { xmlNode *node = pcmk__xe_create(parent, name); pcmk__xe_set_content(node, "%s", 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__html_create(parent, element_name, id, class_name); pcmk__xe_set_content(node, "%s", text); return node; } xmlNode * first_named_child(const xmlNode *parent, const char *name) { return pcmk__xe_first_child(parent, name, NULL, NULL); } xmlNode * find_xml_node(const xmlNode *root, const char *search_path, gboolean must_find) { xmlNode *result = NULL; if (search_path == NULL) { crm_warn("Will never find "); return NULL; } result = pcmk__xe_first_child(root, search_path, NULL, NULL); if (must_find && (result == NULL)) { crm_warn("Could not find %s in %s", search_path, ((root != NULL)? (const char *) root->name : "")); } return result; } xmlNode * crm_next_same_xml(const xmlNode *sibling) { return pcmk__xe_next_same(sibling); } void xml_remove_prop(xmlNode * obj, const char *name) { pcmk__xe_remove_attr(obj, name); } gboolean replace_xml_child(xmlNode * parent, xmlNode * child, xmlNode * update, gboolean delete_only) { bool is_match = false; const char *child_id = NULL; const char *update_id = NULL; CRM_CHECK(child != NULL, return FALSE); CRM_CHECK(update != NULL, return FALSE); child_id = pcmk__xe_id(child); update_id = pcmk__xe_id(update); /* Match element name and (if provided in update XML) element ID. Don't * match search root (child is search root if parent == NULL). */ is_match = (parent != NULL) && pcmk__xe_is(update, (const char *) child->name) && ((update_id == NULL) || pcmk__str_eq(update_id, child_id, pcmk__str_none)); /* For deletion, match all attributes provided in update. A matching node * can have additional attributes, but values must match for provided ones. */ if (is_match && delete_only) { for (xmlAttr *attr = pcmk__xe_first_attr(update); attr != NULL; attr = attr->next) { const char *name = (const char *) attr->name; const char *update_val = pcmk__xml_attr_value(attr); const char *child_val = crm_element_value(child, name); if (!pcmk__str_eq(update_val, child_val, pcmk__str_casei)) { is_match = false; break; } } } if (is_match) { if (delete_only) { crm_log_xml_trace(child, "delete-match"); crm_log_xml_trace(update, "delete-search"); pcmk__xml_free(child); } else { crm_log_xml_trace(child, "replace-match"); crm_log_xml_trace(update, "replace-with"); replace_node(child, update); } return TRUE; } // Current node not a match; search the rest of the subtree depth-first parent = child; for (child = pcmk__xml_first_child(parent); child != NULL; child = pcmk__xml_next(child)) { // Only delete/replace the first match if (replace_xml_child(parent, child, update, delete_only)) { return TRUE; } } // No match found in this subtree return FALSE; } gboolean update_xml_child(xmlNode *child, xmlNode *to_update) { return pcmk__xe_update_match(child, to_update, pcmk__xaf_score_update) == pcmk_rc_ok; } 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 = pcmk__xe_create(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; } 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__xe_first_child(target, NULL, NULL, NULL); child != NULL; child = pcmk__xe_next(child)) { fix_plus_plus_recursive(child); } } 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; } } } } void expand_plus_plus(xmlNode * target, const char *name, const char *value) { pcmk__xe_set_score(target, name, value); } void crm_xml_init(void) { pcmk__xml_init(); } void crm_xml_cleanup(void) { pcmk__xml_cleanup(); } void pcmk_free_xml_subtree(xmlNode *xml) { xmlUnlinkNode(xml); // Detaches from parent and siblings xmlFreeNode(xml); // Frees } void free_xml(xmlNode *child) { pcmk__xml_free(child); } // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/pacemaker/pcmk_sched_colocation.c b/lib/pacemaker/pcmk_sched_colocation.c index e061c612dc..6279ad50f3 100644 --- a/lib/pacemaker/pcmk_sched_colocation.c +++ b/lib/pacemaker/pcmk_sched_colocation.c @@ -1,1935 +1,1935 @@ /* * 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 "crm/common/util.h" #include "crm/common/xml_internal.h" #include "crm/common/xml.h" #include "libpacemaker_private.h" // Used to temporarily mark a node as unusable #define INFINITY_HACK (PCMK_SCORE_INFINITY * -100) /*! * \internal * \brief Compare two colocations according to priority * * Compare two colocations according to the order in which they should be * considered, based on either their dependent resources or their primary * resources -- preferring (in order): * * Colocation that is not \c NULL * * Colocation whose resource has higher priority * * Colocation whose resource is of a higher-level variant * (bundle > clone > group > primitive) * * Colocation whose resource is promotable, if both are clones * * Colocation whose resource has lower ID in lexicographic order * * \param[in] colocation1 First colocation to compare * \param[in] colocation2 Second colocation to compare * \param[in] dependent If \c true, compare colocations by dependent * priority; otherwise compare them by primary priority * * \return A negative number if \p colocation1 should be considered first, * a positive number if \p colocation2 should be considered first, * or 0 if order doesn't matter */ static gint cmp_colocation_priority(const pcmk__colocation_t *colocation1, const pcmk__colocation_t *colocation2, bool dependent) { const pcmk_resource_t *rsc1 = NULL; const pcmk_resource_t *rsc2 = NULL; if (colocation1 == NULL) { return 1; } if (colocation2 == NULL) { return -1; } if (dependent) { rsc1 = colocation1->dependent; rsc2 = colocation2->dependent; CRM_ASSERT(colocation1->primary != NULL); } else { rsc1 = colocation1->primary; rsc2 = colocation2->primary; CRM_ASSERT(colocation1->dependent != NULL); } CRM_ASSERT((rsc1 != NULL) && (rsc2 != NULL)); if (rsc1->priority > rsc2->priority) { return -1; } if (rsc1->priority < rsc2->priority) { return 1; } // Process clones before primitives and groups if (rsc1->variant > rsc2->variant) { return -1; } if (rsc1->variant < rsc2->variant) { return 1; } /* @COMPAT scheduler <2.0.0: Process promotable clones before nonpromotable * clones (probably unnecessary, but avoids having to update regression * tests) */ if (pcmk__is_clone(rsc1)) { if (pcmk_is_set(rsc1->flags, pcmk_rsc_promotable) && !pcmk_is_set(rsc2->flags, pcmk_rsc_promotable)) { return -1; } if (!pcmk_is_set(rsc1->flags, pcmk_rsc_promotable) && pcmk_is_set(rsc2->flags, pcmk_rsc_promotable)) { return 1; } } return strcmp(rsc1->id, rsc2->id); } /*! * \internal * \brief Compare two colocations according to priority based on dependents * * Compare two colocations according to the order in which they should be * considered, based on their dependent resources -- preferring (in order): * * Colocation that is not \c NULL * * Colocation whose resource has higher priority * * Colocation whose resource is of a higher-level variant * (bundle > clone > group > primitive) * * Colocation whose resource is promotable, if both are clones * * Colocation whose resource has lower ID in lexicographic order * * \param[in] a First colocation to compare * \param[in] b Second colocation to compare * * \return A negative number if \p a should be considered first, * a positive number if \p b should be considered first, * or 0 if order doesn't matter */ static gint cmp_dependent_priority(gconstpointer a, gconstpointer b) { return cmp_colocation_priority(a, b, true); } /*! * \internal * \brief Compare two colocations according to priority based on primaries * * Compare two colocations according to the order in which they should be * considered, based on their primary resources -- preferring (in order): * * Colocation that is not \c NULL * * Colocation whose primary has higher priority * * Colocation whose primary is of a higher-level variant * (bundle > clone > group > primitive) * * Colocation whose primary is promotable, if both are clones * * Colocation whose primary has lower ID in lexicographic order * * \param[in] a First colocation to compare * \param[in] b Second colocation to compare * * \return A negative number if \p a should be considered first, * a positive number if \p b should be considered first, * or 0 if order doesn't matter */ static gint cmp_primary_priority(gconstpointer a, gconstpointer b) { return cmp_colocation_priority(a, b, false); } /*! * \internal * \brief Add a "this with" colocation constraint to a sorted list * * \param[in,out] list List of constraints to add \p colocation to * \param[in] colocation Colocation constraint to add to \p list * \param[in] rsc Resource whose colocations we're getting (for * logging only) * * \note The list will be sorted using cmp_primary_priority(). */ void pcmk__add_this_with(GList **list, const pcmk__colocation_t *colocation, const pcmk_resource_t *rsc) { CRM_ASSERT((list != NULL) && (colocation != NULL) && (rsc != NULL)); pcmk__rsc_trace(rsc, "Adding colocation %s (%s with %s using %s @%s) to " "'this with' list for %s", colocation->id, colocation->dependent->id, colocation->primary->id, colocation->node_attribute, pcmk_readable_score(colocation->score), rsc->id); *list = g_list_insert_sorted(*list, (gpointer) colocation, cmp_primary_priority); } /*! * \internal * \brief Add a list of "this with" colocation constraints to a list * * \param[in,out] list List of constraints to add \p addition to * \param[in] addition List of colocation constraints to add to \p list * \param[in] rsc Resource whose colocations we're getting (for * logging only) * * \note The lists must be pre-sorted by cmp_primary_priority(). */ void pcmk__add_this_with_list(GList **list, GList *addition, const pcmk_resource_t *rsc) { CRM_ASSERT((list != NULL) && (rsc != NULL)); pcmk__if_tracing( {}, // Always add each colocation individually if tracing { if (*list == NULL) { // Trivial case for efficiency if not tracing *list = g_list_copy(addition); return; } } ); for (const GList *iter = addition; iter != NULL; iter = iter->next) { pcmk__add_this_with(list, addition->data, rsc); } } /*! * \internal * \brief Add a "with this" colocation constraint to a sorted list * * \param[in,out] list List of constraints to add \p colocation to * \param[in] colocation Colocation constraint to add to \p list * \param[in] rsc Resource whose colocations we're getting (for * logging only) * * \note The list will be sorted using cmp_dependent_priority(). */ void pcmk__add_with_this(GList **list, const pcmk__colocation_t *colocation, const pcmk_resource_t *rsc) { CRM_ASSERT((list != NULL) && (colocation != NULL) && (rsc != NULL)); pcmk__rsc_trace(rsc, "Adding colocation %s (%s with %s using %s @%s) to " "'with this' list for %s", colocation->id, colocation->dependent->id, colocation->primary->id, colocation->node_attribute, pcmk_readable_score(colocation->score), rsc->id); *list = g_list_insert_sorted(*list, (gpointer) colocation, cmp_dependent_priority); } /*! * \internal * \brief Add a list of "with this" colocation constraints to a list * * \param[in,out] list List of constraints to add \p addition to * \param[in] addition List of colocation constraints to add to \p list * \param[in] rsc Resource whose colocations we're getting (for * logging only) * * \note The lists must be pre-sorted by cmp_dependent_priority(). */ void pcmk__add_with_this_list(GList **list, GList *addition, const pcmk_resource_t *rsc) { CRM_ASSERT((list != NULL) && (rsc != NULL)); pcmk__if_tracing( {}, // Always add each colocation individually if tracing { if (*list == NULL) { // Trivial case for efficiency if not tracing *list = g_list_copy(addition); return; } } ); for (const GList *iter = addition; iter != NULL; iter = iter->next) { pcmk__add_with_this(list, addition->data, rsc); } } /*! * \internal * \brief Add orderings necessary for an anti-colocation constraint * * \param[in,out] first_rsc One resource in an anti-colocation * \param[in] first_role Anti-colocation role of \p first_rsc * \param[in] then_rsc Other resource in the anti-colocation * \param[in] then_role Anti-colocation role of \p then_rsc */ static void anti_colocation_order(pcmk_resource_t *first_rsc, int first_role, pcmk_resource_t *then_rsc, int then_role) { const char *first_tasks[] = { NULL, NULL }; const char *then_tasks[] = { NULL, NULL }; /* Actions to make first_rsc lose first_role */ if (first_role == pcmk_role_promoted) { first_tasks[0] = PCMK_ACTION_DEMOTE; } else { first_tasks[0] = PCMK_ACTION_STOP; if (first_role == pcmk_role_unpromoted) { first_tasks[1] = PCMK_ACTION_PROMOTE; } } /* Actions to make then_rsc gain then_role */ if (then_role == pcmk_role_promoted) { then_tasks[0] = PCMK_ACTION_PROMOTE; } else { then_tasks[0] = PCMK_ACTION_START; if (then_role == pcmk_role_unpromoted) { then_tasks[1] = PCMK_ACTION_DEMOTE; } } for (int first_lpc = 0; (first_lpc <= 1) && (first_tasks[first_lpc] != NULL); first_lpc++) { for (int then_lpc = 0; (then_lpc <= 1) && (then_tasks[then_lpc] != NULL); then_lpc++) { pcmk__order_resource_actions(first_rsc, first_tasks[first_lpc], then_rsc, then_tasks[then_lpc], pcmk__ar_if_required_on_same_node); } } } /*! * \internal * \brief Add a new colocation constraint to scheduler data * * \param[in] id XML ID for this constraint * \param[in] node_attr Colocate by this attribute (NULL for #uname) * \param[in] score Constraint score * \param[in,out] dependent Resource to be colocated * \param[in,out] primary Resource to colocate \p dependent with * \param[in] dependent_role Current role of \p dependent * \param[in] primary_role Current role of \p primary * \param[in] flags Group of enum pcmk__coloc_flags */ void pcmk__new_colocation(const char *id, const char *node_attr, int score, pcmk_resource_t *dependent, pcmk_resource_t *primary, const char *dependent_role, const char *primary_role, uint32_t flags) { pcmk__colocation_t *new_con = NULL; CRM_CHECK(id != NULL, return); if ((dependent == NULL) || (primary == NULL)) { pcmk__config_err("Ignoring colocation '%s' because resource " "does not exist", id); return; } if (score == 0) { pcmk__rsc_trace(dependent, "Ignoring colocation '%s' (%s with %s) because score is 0", id, dependent->id, primary->id); return; } new_con = pcmk__assert_alloc(1, sizeof(pcmk__colocation_t)); if (pcmk__str_eq(dependent_role, PCMK_ROLE_STARTED, pcmk__str_null_matches|pcmk__str_casei)) { dependent_role = PCMK__ROLE_UNKNOWN; } if (pcmk__str_eq(primary_role, PCMK_ROLE_STARTED, pcmk__str_null_matches|pcmk__str_casei)) { primary_role = PCMK__ROLE_UNKNOWN; } new_con->id = id; new_con->dependent = dependent; new_con->primary = primary; new_con->score = score; new_con->dependent_role = pcmk_parse_role(dependent_role); new_con->primary_role = pcmk_parse_role(primary_role); new_con->node_attribute = pcmk__s(node_attr, CRM_ATTR_UNAME); new_con->flags = flags; pcmk__add_this_with(&(dependent->rsc_cons), new_con, dependent); pcmk__add_with_this(&(primary->rsc_cons_lhs), new_con, primary); dependent->cluster->colocation_constraints = g_list_prepend( dependent->cluster->colocation_constraints, new_con); if (score <= -PCMK_SCORE_INFINITY) { anti_colocation_order(dependent, new_con->dependent_role, primary, new_con->primary_role); anti_colocation_order(primary, new_con->primary_role, dependent, new_con->dependent_role); } } /*! * \internal * \brief Return the boolean influence corresponding to configuration * * \param[in] coloc_id Colocation XML ID (for error logging) * \param[in] rsc Resource involved in constraint (for default) * \param[in] influence_s String value of \c PCMK_XA_INFLUENCE option * * \return \c pcmk__coloc_influence if string evaluates true, or string is * \c NULL or invalid and resource's \c PCMK_META_CRITICAL option * evaluates true, otherwise \c pcmk__coloc_none */ static uint32_t unpack_influence(const char *coloc_id, const pcmk_resource_t *rsc, const char *influence_s) { if (influence_s != NULL) { int influence_i = 0; if (crm_str_to_boolean(influence_s, &influence_i) < 0) { pcmk__config_err("Constraint '%s' has invalid value for " PCMK_XA_INFLUENCE " (using default)", coloc_id); } else { return (influence_i == 0)? pcmk__coloc_none : pcmk__coloc_influence; } } if (pcmk_is_set(rsc->flags, pcmk_rsc_critical)) { return pcmk__coloc_influence; } return pcmk__coloc_none; } static void unpack_colocation_set(xmlNode *set, int score, const char *coloc_id, const char *influence_s, pcmk_scheduler_t *scheduler) { xmlNode *xml_rsc = NULL; pcmk_resource_t *other = NULL; pcmk_resource_t *resource = NULL; const char *set_id = pcmk__xe_id(set); const char *role = crm_element_value(set, PCMK_XA_ROLE); bool with_previous = false; int local_score = score; bool sequential = false; uint32_t flags = pcmk__coloc_none; const char *xml_rsc_id = NULL; const char *score_s = crm_element_value(set, PCMK_XA_SCORE); if (score_s) { local_score = char2score(score_s); } if (local_score == 0) { crm_trace("Ignoring colocation '%s' for set '%s' because score is 0", coloc_id, set_id); return; } /* @COMPAT The deprecated PCMK__XA_ORDERING attribute specifies whether * resources in a positive-score set are colocated with the previous or next * resource. */ if (pcmk__str_eq(crm_element_value(set, PCMK__XA_ORDERING), PCMK__VALUE_GROUP, pcmk__str_null_matches|pcmk__str_casei)) { with_previous = true; } else { pcmk__warn_once(pcmk__wo_set_ordering, "Support for '" PCMK__XA_ORDERING "' other than" " '" PCMK__VALUE_GROUP "' in " PCMK_XE_RESOURCE_SET " (such as %s) is deprecated and will be removed in a" " future release", set_id); } if ((pcmk__xe_get_bool_attr(set, PCMK_XA_SEQUENTIAL, &sequential) == pcmk_rc_ok) && !sequential) { return; } if (local_score > 0) { for (xml_rsc = pcmk__xe_first_child(set, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { xml_rsc_id = pcmk__xe_id(xml_rsc); resource = pcmk__find_constraint_resource(scheduler->resources, xml_rsc_id); if (resource == NULL) { // Should be possible only with validation disabled pcmk__config_err("Ignoring %s and later resources in set %s: " "No such resource", xml_rsc_id, set_id); return; } if (other != NULL) { flags = pcmk__coloc_explicit | unpack_influence(coloc_id, resource, influence_s); if (with_previous) { pcmk__rsc_trace(resource, "Colocating %s with %s in set %s", resource->id, other->id, set_id); pcmk__new_colocation(set_id, NULL, local_score, resource, other, role, role, flags); } else { pcmk__rsc_trace(resource, "Colocating %s with %s in set %s", other->id, resource->id, set_id); pcmk__new_colocation(set_id, NULL, local_score, other, resource, role, role, flags); } } other = resource; } } else { /* Anti-colocating with every prior resource is * the only way to ensure the intuitive result * (i.e. that no one in the set can run with anyone else in the set) */ for (xml_rsc = pcmk__xe_first_child(set, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { xmlNode *xml_rsc_with = NULL; xml_rsc_id = pcmk__xe_id(xml_rsc); resource = pcmk__find_constraint_resource(scheduler->resources, xml_rsc_id); if (resource == NULL) { // Should be possible only with validation disabled pcmk__config_err("Ignoring %s and later resources in set %s: " "No such resource", xml_rsc_id, set_id); return; } flags = pcmk__coloc_explicit | unpack_influence(coloc_id, resource, influence_s); for (xml_rsc_with = pcmk__xe_first_child(set, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc_with != NULL; xml_rsc_with = pcmk__xe_next_same(xml_rsc_with)) { xml_rsc_id = pcmk__xe_id(xml_rsc_with); if (pcmk__str_eq(resource->id, xml_rsc_id, pcmk__str_none)) { break; } other = pcmk__find_constraint_resource(scheduler->resources, xml_rsc_id); CRM_ASSERT(other != NULL); // We already processed it pcmk__new_colocation(set_id, NULL, local_score, resource, other, role, role, flags); } } } } /*! * \internal * \brief Colocate two resource sets relative to each other * * \param[in] id Colocation XML ID * \param[in] set1 Dependent set * \param[in] set2 Primary set * \param[in] score Colocation score * \param[in] influence_s Value of colocation's \c PCMK_XA_INFLUENCE * attribute * \param[in,out] scheduler Scheduler data */ static void colocate_rsc_sets(const char *id, const xmlNode *set1, const xmlNode *set2, int score, const char *influence_s, pcmk_scheduler_t *scheduler) { xmlNode *xml_rsc = NULL; pcmk_resource_t *rsc_1 = NULL; pcmk_resource_t *rsc_2 = NULL; const char *xml_rsc_id = NULL; const char *role_1 = crm_element_value(set1, PCMK_XA_ROLE); const char *role_2 = crm_element_value(set2, PCMK_XA_ROLE); int rc = pcmk_rc_ok; bool sequential = false; uint32_t flags = pcmk__coloc_none; if (score == 0) { crm_trace("Ignoring colocation '%s' between sets %s and %s " "because score is 0", id, pcmk__xe_id(set1), pcmk__xe_id(set2)); return; } rc = pcmk__xe_get_bool_attr(set1, PCMK_XA_SEQUENTIAL, &sequential); if ((rc != pcmk_rc_ok) || sequential) { // Get the first one xml_rsc = pcmk__xe_first_child(set1, PCMK_XE_RESOURCE_REF, NULL, NULL); if (xml_rsc != NULL) { xml_rsc_id = pcmk__xe_id(xml_rsc); rsc_1 = pcmk__find_constraint_resource(scheduler->resources, xml_rsc_id); if (rsc_1 == NULL) { // Should be possible only with validation disabled pcmk__config_err("Ignoring colocation of set %s with set %s " "because first resource %s not found", pcmk__xe_id(set1), pcmk__xe_id(set2), xml_rsc_id); return; } } } rc = pcmk__xe_get_bool_attr(set2, PCMK_XA_SEQUENTIAL, &sequential); if ((rc != pcmk_rc_ok) || sequential) { // Get the last one for (xml_rsc = pcmk__xe_first_child(set2, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { xml_rsc_id = pcmk__xe_id(xml_rsc); } rsc_2 = pcmk__find_constraint_resource(scheduler->resources, xml_rsc_id); if (rsc_2 == NULL) { // Should be possible only with validation disabled pcmk__config_err("Ignoring colocation of set %s with set %s " "because last resource %s not found", pcmk__xe_id(set1), pcmk__xe_id(set2), xml_rsc_id); return; } } if ((rsc_1 != NULL) && (rsc_2 != NULL)) { // Both sets are sequential flags = pcmk__coloc_explicit | unpack_influence(id, rsc_1, influence_s); pcmk__new_colocation(id, NULL, score, rsc_1, rsc_2, role_1, role_2, flags); } else if (rsc_1 != NULL) { // Only set1 is sequential flags = pcmk__coloc_explicit | unpack_influence(id, rsc_1, influence_s); for (xml_rsc = pcmk__xe_first_child(set2, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { xml_rsc_id = pcmk__xe_id(xml_rsc); rsc_2 = pcmk__find_constraint_resource(scheduler->resources, xml_rsc_id); if (rsc_2 == NULL) { // Should be possible only with validation disabled pcmk__config_err("Ignoring set %s colocation with resource %s " "in set %s: No such resource", pcmk__xe_id(set1), xml_rsc_id, pcmk__xe_id(set2)); continue; } pcmk__new_colocation(id, NULL, score, rsc_1, rsc_2, role_1, role_2, flags); } } else if (rsc_2 != NULL) { // Only set2 is sequential for (xml_rsc = pcmk__xe_first_child(set1, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { xml_rsc_id = pcmk__xe_id(xml_rsc); rsc_1 = pcmk__find_constraint_resource(scheduler->resources, xml_rsc_id); if (rsc_1 == NULL) { // Should be possible only with validation disabled pcmk__config_err("Ignoring colocation of set %s resource %s " "with set %s: No such resource", pcmk__xe_id(set1), xml_rsc_id, pcmk__xe_id(set2)); continue; } flags = pcmk__coloc_explicit | unpack_influence(id, rsc_1, influence_s); pcmk__new_colocation(id, NULL, score, rsc_1, rsc_2, role_1, role_2, flags); } } else { // Neither set is sequential for (xml_rsc = pcmk__xe_first_child(set1, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { xmlNode *xml_rsc_2 = NULL; xml_rsc_id = pcmk__xe_id(xml_rsc); rsc_1 = pcmk__find_constraint_resource(scheduler->resources, xml_rsc_id); if (rsc_1 == NULL) { // Should be possible only with validation disabled pcmk__config_err("Ignoring colocation of set %s resource %s " "with set %s: No such resource", pcmk__xe_id(set1), xml_rsc_id, pcmk__xe_id(set2)); continue; } flags = pcmk__coloc_explicit | unpack_influence(id, rsc_1, influence_s); for (xml_rsc_2 = pcmk__xe_first_child(set2, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc_2 != NULL; xml_rsc_2 = pcmk__xe_next_same(xml_rsc_2)) { xml_rsc_id = pcmk__xe_id(xml_rsc_2); rsc_2 = pcmk__find_constraint_resource(scheduler->resources, xml_rsc_id); if (rsc_2 == NULL) { // Should be possible only with validation disabled pcmk__config_err("Ignoring colocation of set %s resource " "%s with set %s resource %s: No such " "resource", pcmk__xe_id(set1), pcmk__xe_id(xml_rsc), pcmk__xe_id(set2), xml_rsc_id); continue; } pcmk__new_colocation(id, NULL, score, rsc_1, rsc_2, role_1, role_2, flags); } } } } static void unpack_simple_colocation(xmlNode *xml_obj, const char *id, const char *influence_s, pcmk_scheduler_t *scheduler) { int score_i = 0; uint32_t flags = pcmk__coloc_none; const char *score = crm_element_value(xml_obj, PCMK_XA_SCORE); const char *dependent_id = crm_element_value(xml_obj, PCMK_XA_RSC); const char *primary_id = crm_element_value(xml_obj, PCMK_XA_WITH_RSC); const char *dependent_role = crm_element_value(xml_obj, PCMK_XA_RSC_ROLE); const char *primary_role = crm_element_value(xml_obj, PCMK_XA_WITH_RSC_ROLE); const char *attr = crm_element_value(xml_obj, PCMK_XA_NODE_ATTRIBUTE); const char *primary_instance = NULL; const char *dependent_instance = NULL; pcmk_resource_t *primary = NULL; pcmk_resource_t *dependent = NULL; primary = pcmk__find_constraint_resource(scheduler->resources, primary_id); dependent = pcmk__find_constraint_resource(scheduler->resources, dependent_id); // @COMPAT: Deprecated since 2.1.5 primary_instance = crm_element_value(xml_obj, PCMK__XA_WITH_RSC_INSTANCE); dependent_instance = crm_element_value(xml_obj, PCMK__XA_RSC_INSTANCE); if (dependent_instance != NULL) { pcmk__warn_once(pcmk__wo_coloc_inst, "Support for " PCMK__XA_RSC_INSTANCE " is deprecated " "and will be removed in a future release"); } if (primary_instance != NULL) { pcmk__warn_once(pcmk__wo_coloc_inst, "Support for " PCMK__XA_WITH_RSC_INSTANCE " is " "deprecated and will be removed in a future release"); } if (dependent == NULL) { pcmk__config_err("Ignoring constraint '%s' because resource '%s' " "does not exist", id, dependent_id); return; } else if (primary == NULL) { pcmk__config_err("Ignoring constraint '%s' because resource '%s' " "does not exist", id, primary_id); return; } else if ((dependent_instance != NULL) && !pcmk__is_clone(dependent)) { pcmk__config_err("Ignoring constraint '%s' because resource '%s' " "is not a clone but instance '%s' was requested", id, dependent_id, dependent_instance); return; } else if ((primary_instance != NULL) && !pcmk__is_clone(primary)) { pcmk__config_err("Ignoring constraint '%s' because resource '%s' " "is not a clone but instance '%s' was requested", id, primary_id, primary_instance); return; } if (dependent_instance != NULL) { dependent = find_clone_instance(dependent, dependent_instance); if (dependent == NULL) { pcmk__config_warn("Ignoring constraint '%s' because resource '%s' " "does not have an instance '%s'", id, dependent_id, dependent_instance); return; } } if (primary_instance != NULL) { primary = find_clone_instance(primary, primary_instance); if (primary == NULL) { pcmk__config_warn("Ignoring constraint '%s' because resource '%s' " "does not have an instance '%s'", "'%s'", id, primary_id, primary_instance); return; } } if (pcmk__xe_attr_is_true(xml_obj, PCMK_XA_SYMMETRICAL)) { pcmk__config_warn("The colocation constraint " "'" PCMK_XA_SYMMETRICAL "' attribute has been " "removed"); } if (score) { score_i = char2score(score); } flags = pcmk__coloc_explicit | unpack_influence(id, dependent, influence_s); pcmk__new_colocation(id, attr, score_i, dependent, primary, dependent_role, primary_role, flags); } // \return Standard Pacemaker return code static int unpack_colocation_tags(xmlNode *xml_obj, xmlNode **expanded_xml, pcmk_scheduler_t *scheduler) { const char *id = NULL; const char *dependent_id = NULL; const char *primary_id = NULL; const char *dependent_role = NULL; const char *primary_role = NULL; pcmk_resource_t *dependent = NULL; pcmk_resource_t *primary = NULL; pcmk_tag_t *dependent_tag = NULL; pcmk_tag_t *primary_tag = NULL; xmlNode *dependent_set = NULL; xmlNode *primary_set = NULL; bool any_sets = false; *expanded_xml = NULL; CRM_CHECK(xml_obj != NULL, return EINVAL); id = pcmk__xe_id(xml_obj); if (id == NULL) { pcmk__config_err("Ignoring <%s> constraint without " PCMK_XA_ID, xml_obj->name); return pcmk_rc_unpack_error; } // Check whether there are any resource sets with template or tag references *expanded_xml = pcmk__expand_tags_in_sets(xml_obj, scheduler); if (*expanded_xml != NULL) { crm_log_xml_trace(*expanded_xml, "Expanded " PCMK_XE_RSC_COLOCATION); return pcmk_rc_ok; } dependent_id = crm_element_value(xml_obj, PCMK_XA_RSC); primary_id = crm_element_value(xml_obj, PCMK_XA_WITH_RSC); if ((dependent_id == NULL) || (primary_id == NULL)) { return pcmk_rc_ok; } if (!pcmk__valid_resource_or_tag(scheduler, dependent_id, &dependent, &dependent_tag)) { pcmk__config_err("Ignoring constraint '%s' because '%s' is not a " "valid resource or tag", id, dependent_id); return pcmk_rc_unpack_error; } if (!pcmk__valid_resource_or_tag(scheduler, primary_id, &primary, &primary_tag)) { pcmk__config_err("Ignoring constraint '%s' because '%s' is not a " "valid resource or tag", id, primary_id); return pcmk_rc_unpack_error; } if ((dependent != NULL) && (primary != NULL)) { /* Neither side references any template/tag. */ return pcmk_rc_ok; } if ((dependent_tag != NULL) && (primary_tag != NULL)) { // A colocation constraint between two templates/tags makes no sense pcmk__config_err("Ignoring constraint '%s' because two templates or " "tags cannot be colocated", id); return pcmk_rc_unpack_error; } dependent_role = crm_element_value(xml_obj, PCMK_XA_RSC_ROLE); primary_role = crm_element_value(xml_obj, PCMK_XA_WITH_RSC_ROLE); *expanded_xml = pcmk__xml_copy(NULL, xml_obj); /* Convert dependent's template/tag reference into constraint * PCMK_XE_RESOURCE_SET */ if (!pcmk__tag_to_set(*expanded_xml, &dependent_set, PCMK_XA_RSC, true, scheduler)) { pcmk__xml_free(*expanded_xml); *expanded_xml = NULL; return pcmk_rc_unpack_error; } if (dependent_set != NULL) { if (dependent_role != NULL) { /* Move PCMK_XA_RSC_ROLE into converted PCMK_XE_RESOURCE_SET as * PCMK_XA_ROLE */ crm_xml_add(dependent_set, PCMK_XA_ROLE, dependent_role); pcmk__xe_remove_attr(*expanded_xml, PCMK_XA_RSC_ROLE); } any_sets = true; } /* Convert primary's template/tag reference into constraint * PCMK_XE_RESOURCE_SET */ if (!pcmk__tag_to_set(*expanded_xml, &primary_set, PCMK_XA_WITH_RSC, true, scheduler)) { pcmk__xml_free(*expanded_xml); *expanded_xml = NULL; return pcmk_rc_unpack_error; } if (primary_set != NULL) { if (primary_role != NULL) { /* Move PCMK_XA_WITH_RSC_ROLE into converted PCMK_XE_RESOURCE_SET as * PCMK_XA_ROLE */ crm_xml_add(primary_set, PCMK_XA_ROLE, primary_role); pcmk__xe_remove_attr(*expanded_xml, PCMK_XA_WITH_RSC_ROLE); } any_sets = true; } if (any_sets) { crm_log_xml_trace(*expanded_xml, "Expanded " PCMK_XE_RSC_COLOCATION); } else { pcmk__xml_free(*expanded_xml); *expanded_xml = NULL; } return pcmk_rc_ok; } /*! * \internal * \brief Parse a colocation constraint from XML into scheduler data * * \param[in,out] xml_obj Colocation constraint XML to unpack * \param[in,out] scheduler Scheduler data to add constraint to */ void pcmk__unpack_colocation(xmlNode *xml_obj, pcmk_scheduler_t *scheduler) { int score_i = 0; xmlNode *set = NULL; xmlNode *last = NULL; xmlNode *orig_xml = NULL; xmlNode *expanded_xml = NULL; const char *id = crm_element_value(xml_obj, PCMK_XA_ID); const char *score = NULL; const char *influence_s = NULL; if (pcmk__str_empty(id)) { pcmk__config_err("Ignoring " PCMK_XE_RSC_COLOCATION " without " CRM_ATTR_ID); return; } if (unpack_colocation_tags(xml_obj, &expanded_xml, scheduler) != pcmk_rc_ok) { return; } if (expanded_xml != NULL) { orig_xml = xml_obj; xml_obj = expanded_xml; } score = crm_element_value(xml_obj, PCMK_XA_SCORE); if (score != NULL) { score_i = char2score(score); } influence_s = crm_element_value(xml_obj, PCMK_XA_INFLUENCE); for (set = pcmk__xe_first_child(xml_obj, PCMK_XE_RESOURCE_SET, NULL, NULL); set != NULL; set = pcmk__xe_next_same(set)) { - set = expand_idref(set, scheduler->input); + set = pcmk__xe_resolve_idref(set, scheduler->input); if (set == NULL) { // Configuration error, message already logged if (expanded_xml != NULL) { pcmk__xml_free(expanded_xml); } return; } if (pcmk__str_empty(pcmk__xe_id(set))) { pcmk__config_err("Ignoring " PCMK_XE_RESOURCE_SET " without " CRM_ATTR_ID); continue; } unpack_colocation_set(set, score_i, id, influence_s, scheduler); if (last != NULL) { colocate_rsc_sets(id, last, set, score_i, influence_s, scheduler); } last = set; } if (expanded_xml) { pcmk__xml_free(expanded_xml); xml_obj = orig_xml; } if (last == NULL) { unpack_simple_colocation(xml_obj, id, influence_s, scheduler); } } /*! * \internal * \brief Make actions of a given type unrunnable for a given resource * * \param[in,out] rsc Resource whose actions should be blocked * \param[in] task Name of action to block * \param[in] reason Unrunnable start action causing the block */ static void mark_action_blocked(pcmk_resource_t *rsc, const char *task, const pcmk_resource_t *reason) { GList *iter = NULL; char *reason_text = crm_strdup_printf("colocation with %s", reason->id); for (iter = rsc->actions; iter != NULL; iter = iter->next) { pcmk_action_t *action = iter->data; if (pcmk_is_set(action->flags, pcmk_action_runnable) && pcmk__str_eq(action->task, task, pcmk__str_none)) { pcmk__clear_action_flags(action, pcmk_action_runnable); pe_action_set_reason(action, reason_text, false); pcmk__block_colocation_dependents(action); pcmk__update_action_for_orderings(action, rsc->cluster); } } // If parent resource can't perform an action, neither can any children for (iter = rsc->children; iter != NULL; iter = iter->next) { mark_action_blocked((pcmk_resource_t *) (iter->data), task, reason); } free(reason_text); } /*! * \internal * \brief If an action is unrunnable, block any relevant dependent actions * * If a given action is an unrunnable start or promote, block the start or * promote actions of resources colocated with it, as appropriate to the * colocations' configured roles. * * \param[in,out] action Action to check */ void pcmk__block_colocation_dependents(pcmk_action_t *action) { GList *iter = NULL; GList *colocations = NULL; pcmk_resource_t *rsc = NULL; bool is_start = false; if (pcmk_is_set(action->flags, pcmk_action_runnable)) { return; // Only unrunnable actions block dependents } is_start = pcmk__str_eq(action->task, PCMK_ACTION_START, pcmk__str_none); if (!is_start && !pcmk__str_eq(action->task, PCMK_ACTION_PROMOTE, pcmk__str_none)) { return; // Only unrunnable starts and promotes block dependents } CRM_ASSERT(action->rsc != NULL); // Start and promote are resource actions /* If this resource is part of a collective resource, dependents are blocked * only if all instances of the collective are unrunnable, so check the * collective resource. */ rsc = uber_parent(action->rsc); if (rsc->parent != NULL) { rsc = rsc->parent; // Bundle } // Colocation fails only if entire primary can't reach desired role for (iter = rsc->children; iter != NULL; iter = iter->next) { pcmk_resource_t *child = iter->data; pcmk_action_t *child_action = find_first_action(child->actions, NULL, action->task, NULL); if ((child_action == NULL) || pcmk_is_set(child_action->flags, pcmk_action_runnable)) { crm_trace("Not blocking %s colocation dependents because " "at least %s has runnable %s", rsc->id, child->id, action->task); return; // At least one child can reach desired role } } crm_trace("Blocking %s colocation dependents due to unrunnable %s %s", rsc->id, action->rsc->id, action->task); // Check each colocation where this resource is primary colocations = pcmk__with_this_colocations(rsc); for (iter = colocations; iter != NULL; iter = iter->next) { pcmk__colocation_t *colocation = iter->data; if (colocation->score < PCMK_SCORE_INFINITY) { continue; // Only mandatory colocations block dependent } /* If the primary can't start, the dependent can't reach its colocated * role, regardless of what the primary or dependent colocation role is. * * If the primary can't be promoted, the dependent can't reach its * colocated role if the primary's colocation role is promoted. */ if (!is_start && (colocation->primary_role != pcmk_role_promoted)) { continue; } // Block the dependent from reaching its colocated role if (colocation->dependent_role == pcmk_role_promoted) { mark_action_blocked(colocation->dependent, PCMK_ACTION_PROMOTE, action->rsc); } else { mark_action_blocked(colocation->dependent, PCMK_ACTION_START, action->rsc); } } g_list_free(colocations); } /*! * \internal * \brief Get the resource to use for role comparisons * * A bundle replica includes a container and possibly an instance of the bundled * resource. The dependent in a "with bundle" colocation is colocated with a * particular bundle container. However, if the colocation includes a role, then * the role must be checked on the bundled resource instance inside the * container. The container itself will never be promoted; the bundled resource * may be. * * If the given resource is a bundle replica container, return the resource * inside it, if any. Otherwise, return the resource itself. * * \param[in] rsc Resource to check * * \return Resource to use for role comparisons */ static const pcmk_resource_t * get_resource_for_role(const pcmk_resource_t *rsc) { if (pcmk_is_set(rsc->flags, pcmk_rsc_replica_container)) { const pcmk_resource_t *child = pe__get_rsc_in_container(rsc); if (child != NULL) { return child; } } return rsc; } /*! * \internal * \brief Determine how a colocation constraint should affect a resource * * Colocation constraints have different effects at different points in the * scheduler sequence. Initially, they affect a resource's location; once that * is determined, then for promotable clones they can affect a resource * instance's role; after both are determined, the constraints no longer matter. * Given a specific colocation constraint, check what has been done so far to * determine what should be affected at the current point in the scheduler. * * \param[in] dependent Dependent resource in colocation * \param[in] primary Primary resource in colocation * \param[in] colocation Colocation constraint * \param[in] preview If true, pretend resources have already been assigned * * \return How colocation constraint should be applied at this point */ enum pcmk__coloc_affects pcmk__colocation_affects(const pcmk_resource_t *dependent, const pcmk_resource_t *primary, const pcmk__colocation_t *colocation, bool preview) { const pcmk_resource_t *dependent_role_rsc = NULL; const pcmk_resource_t *primary_role_rsc = NULL; CRM_ASSERT((dependent != NULL) && (primary != NULL) && (colocation != NULL)); if (!preview && pcmk_is_set(primary->flags, pcmk_rsc_unassigned)) { // Primary resource has not been assigned yet, so we can't do anything return pcmk__coloc_affects_nothing; } dependent_role_rsc = get_resource_for_role(dependent); primary_role_rsc = get_resource_for_role(primary); if ((colocation->dependent_role >= pcmk_role_unpromoted) && (dependent_role_rsc->parent != NULL) && pcmk_is_set(dependent_role_rsc->parent->flags, pcmk_rsc_promotable) && !pcmk_is_set(dependent_role_rsc->flags, pcmk_rsc_unassigned)) { /* This is a colocation by role, and the dependent is a promotable clone * that has already been assigned, so the colocation should now affect * the role. */ return pcmk__coloc_affects_role; } if (!preview && !pcmk_is_set(dependent->flags, pcmk_rsc_unassigned)) { /* The dependent resource has already been through assignment, so the * constraint no longer has any effect. Log an error if a mandatory * colocation constraint has been violated. */ const pcmk_node_t *primary_node = primary->allocated_to; if (dependent->allocated_to == NULL) { crm_trace("Skipping colocation '%s': %s will not run anywhere", colocation->id, dependent->id); } else if (colocation->score >= PCMK_SCORE_INFINITY) { // Dependent resource must colocate with primary resource if (!pcmk__same_node(primary_node, dependent->allocated_to)) { pcmk__sched_err("%s must be colocated with %s but is not " "(%s vs. %s)", dependent->id, primary->id, pcmk__node_name(dependent->allocated_to), pcmk__node_name(primary_node)); } } else if (colocation->score <= -PCMK_SCORE_INFINITY) { // Dependent resource must anti-colocate with primary resource if (pcmk__same_node(dependent->allocated_to, primary_node)) { pcmk__sched_err("%s and %s must be anti-colocated but are " "assigned to the same node (%s)", dependent->id, primary->id, pcmk__node_name(primary_node)); } } return pcmk__coloc_affects_nothing; } if ((colocation->dependent_role != pcmk_role_unknown) && (colocation->dependent_role != dependent_role_rsc->next_role)) { crm_trace("Skipping %scolocation '%s': dependent limited to %s role " "but %s next role is %s", ((colocation->score < 0)? "anti-" : ""), colocation->id, pcmk_role_text(colocation->dependent_role), dependent_role_rsc->id, pcmk_role_text(dependent_role_rsc->next_role)); return pcmk__coloc_affects_nothing; } if ((colocation->primary_role != pcmk_role_unknown) && (colocation->primary_role != primary_role_rsc->next_role)) { crm_trace("Skipping %scolocation '%s': primary limited to %s role " "but %s next role is %s", ((colocation->score < 0)? "anti-" : ""), colocation->id, pcmk_role_text(colocation->primary_role), primary_role_rsc->id, pcmk_role_text(primary_role_rsc->next_role)); return pcmk__coloc_affects_nothing; } return pcmk__coloc_affects_location; } /*! * \internal * \brief Apply colocation to dependent for assignment purposes * * Update the allowed node scores of the dependent resource in a colocation, * for the purposes of assigning it to a node. * * \param[in,out] dependent Dependent resource in colocation * \param[in] primary Primary resource in colocation * \param[in] colocation Colocation constraint */ void pcmk__apply_coloc_to_scores(pcmk_resource_t *dependent, const pcmk_resource_t *primary, const pcmk__colocation_t *colocation) { const char *attr = colocation->node_attribute; const char *value = NULL; GHashTable *work = NULL; GHashTableIter iter; pcmk_node_t *node = NULL; if (primary->allocated_to != NULL) { value = pcmk__colocation_node_attr(primary->allocated_to, attr, primary); } else if (colocation->score < 0) { // Nothing to do (anti-colocation with something that is not running) return; } work = pcmk__copy_node_table(dependent->allowed_nodes); g_hash_table_iter_init(&iter, work); while (g_hash_table_iter_next(&iter, NULL, (void **)&node)) { if (primary->allocated_to == NULL) { node->weight = pcmk__add_scores(-colocation->score, node->weight); pcmk__rsc_trace(dependent, "Applied %s to %s score on %s (now %s after " "subtracting %s because primary %s inactive)", colocation->id, dependent->id, pcmk__node_name(node), pcmk_readable_score(node->weight), pcmk_readable_score(colocation->score), primary->id); continue; } if (pcmk__str_eq(pcmk__colocation_node_attr(node, attr, dependent), value, pcmk__str_casei)) { /* Add colocation score only if optional (or minus infinity). A * mandatory colocation is a requirement rather than a preference, * so we don't need to consider it for relative assignment purposes. * The resource will simply be forbidden from running on the node if * the primary isn't active there (via the condition above). */ if (colocation->score < PCMK_SCORE_INFINITY) { node->weight = pcmk__add_scores(colocation->score, node->weight); pcmk__rsc_trace(dependent, "Applied %s to %s score on %s (now %s after " "adding %s)", colocation->id, dependent->id, pcmk__node_name(node), pcmk_readable_score(node->weight), pcmk_readable_score(colocation->score)); } continue; } if (colocation->score >= PCMK_SCORE_INFINITY) { /* Only mandatory colocations are relevant when the colocation * attribute doesn't match, because an attribute not matching is not * a negative preference -- the colocation is simply relevant only * where it matches. */ node->weight = -PCMK_SCORE_INFINITY; pcmk__rsc_trace(dependent, "Banned %s from %s because colocation %s attribute %s " "does not match", dependent->id, pcmk__node_name(node), colocation->id, attr); } } if ((colocation->score <= -PCMK_SCORE_INFINITY) || (colocation->score >= PCMK_SCORE_INFINITY) || pcmk__any_node_available(work)) { g_hash_table_destroy(dependent->allowed_nodes); dependent->allowed_nodes = work; work = NULL; } else { pcmk__rsc_info(dependent, "%s: Rolling back scores from %s (no available nodes)", dependent->id, primary->id); } if (work != NULL) { g_hash_table_destroy(work); } } /*! * \internal * \brief Apply colocation to dependent for role purposes * * Update the priority of the dependent resource in a colocation, for the * purposes of selecting its role * * \param[in,out] dependent Dependent resource in colocation * \param[in] primary Primary resource in colocation * \param[in] colocation Colocation constraint */ void pcmk__apply_coloc_to_priority(pcmk_resource_t *dependent, const pcmk_resource_t *primary, const pcmk__colocation_t *colocation) { const char *dependent_value = NULL; const char *primary_value = NULL; const char *attr = colocation->node_attribute; int score_multiplier = 1; const pcmk_resource_t *primary_role_rsc = NULL; CRM_ASSERT((dependent != NULL) && (primary != NULL) && (colocation != NULL)); if ((primary->allocated_to == NULL) || (dependent->allocated_to == NULL)) { return; } dependent_value = pcmk__colocation_node_attr(dependent->allocated_to, attr, dependent); primary_value = pcmk__colocation_node_attr(primary->allocated_to, attr, primary); primary_role_rsc = get_resource_for_role(primary); if (!pcmk__str_eq(dependent_value, primary_value, pcmk__str_casei)) { if ((colocation->score == PCMK_SCORE_INFINITY) && (colocation->dependent_role == pcmk_role_promoted)) { dependent->priority = -PCMK_SCORE_INFINITY; } return; } if ((colocation->primary_role != pcmk_role_unknown) && (colocation->primary_role != primary_role_rsc->next_role)) { return; } if (colocation->dependent_role == pcmk_role_unpromoted) { score_multiplier = -1; } dependent->priority = pcmk__add_scores(score_multiplier * colocation->score, dependent->priority); pcmk__rsc_trace(dependent, "Applied %s to %s promotion priority (now %s after %s %s)", colocation->id, dependent->id, pcmk_readable_score(dependent->priority), ((score_multiplier == 1)? "adding" : "subtracting"), pcmk_readable_score(colocation->score)); } /*! * \internal * \brief Find score of highest-scored node that matches colocation attribute * * \param[in] rsc Resource whose allowed nodes should be searched * \param[in] attr Colocation attribute name (must not be NULL) * \param[in] value Colocation attribute value to require */ static int best_node_score_matching_attr(const pcmk_resource_t *rsc, const char *attr, const char *value) { GHashTableIter iter; pcmk_node_t *node = NULL; int best_score = -PCMK_SCORE_INFINITY; const char *best_node = NULL; // Find best allowed node with matching attribute g_hash_table_iter_init(&iter, rsc->allowed_nodes); while (g_hash_table_iter_next(&iter, NULL, (void **) &node)) { if ((node->weight > best_score) && pcmk__node_available(node, false, false) && pcmk__str_eq(value, pcmk__colocation_node_attr(node, attr, rsc), pcmk__str_casei)) { best_score = node->weight; best_node = node->details->uname; } } if (!pcmk__str_eq(attr, CRM_ATTR_UNAME, pcmk__str_none)) { if (best_node == NULL) { crm_info("No allowed node for %s matches node attribute %s=%s", rsc->id, attr, value); } else { crm_info("Allowed node %s for %s had best score (%d) " "of those matching node attribute %s=%s", best_node, rsc->id, best_score, attr, value); } } return best_score; } /*! * \internal * \brief Check whether a resource is allowed only on a single node * * \param[in] rsc Resource to check * * \return \c true if \p rsc is allowed only on one node, otherwise \c false */ static bool allowed_on_one(const pcmk_resource_t *rsc) { GHashTableIter iter; pcmk_node_t *allowed_node = NULL; int allowed_nodes = 0; g_hash_table_iter_init(&iter, rsc->allowed_nodes); while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &allowed_node)) { if ((allowed_node->weight >= 0) && (++allowed_nodes > 1)) { pcmk__rsc_trace(rsc, "%s is allowed on multiple nodes", rsc->id); return false; } } pcmk__rsc_trace(rsc, "%s is allowed %s", rsc->id, ((allowed_nodes == 1)? "on a single node" : "nowhere")); return (allowed_nodes == 1); } /*! * \internal * \brief Add resource's colocation matches to current node assignment scores * * For each node in a given table, if any of a given resource's allowed nodes * have a matching value for the colocation attribute, add the highest of those * nodes' scores to the node's score. * * \param[in,out] nodes Table of nodes with assignment scores so far * \param[in] source_rsc Resource whose node scores to add * \param[in] target_rsc Resource on whose behalf to update \p nodes * \param[in] colocation Original colocation constraint (used to get * configured primary resource's stickiness, and * to get colocation node attribute; pass NULL to * ignore stickiness and use default attribute) * \param[in] factor Factor by which to multiply scores being added * \param[in] only_positive Whether to add only positive scores */ static void add_node_scores_matching_attr(GHashTable *nodes, const pcmk_resource_t *source_rsc, const pcmk_resource_t *target_rsc, const pcmk__colocation_t *colocation, float factor, bool only_positive) { GHashTableIter iter; pcmk_node_t *node = NULL; const char *attr = colocation->node_attribute; // Iterate through each node g_hash_table_iter_init(&iter, nodes); while (g_hash_table_iter_next(&iter, NULL, (void **)&node)) { float delta_f = 0; int delta = 0; int score = 0; int new_score = 0; const char *value = pcmk__colocation_node_attr(node, attr, target_rsc); score = best_node_score_matching_attr(source_rsc, attr, value); if ((factor < 0) && (score < 0)) { /* If the dependent is anti-colocated, we generally don't want the * primary to prefer nodes that the dependent avoids. That could * lead to unnecessary shuffling of the primary when the dependent * hits its migration threshold somewhere, for example. * * However, there are cases when it is desirable. If the dependent * can't run anywhere but where the primary is, it would be * worthwhile to move the primary for the sake of keeping the * dependent active. * * We can't know that exactly at this point since we don't know * where the primary will be assigned, but we can limit considering * the preference to when the dependent is allowed only on one node. * This is less than ideal for multiple reasons: * * - the dependent could be allowed on more than one node but have * anti-colocation primaries on each; * - the dependent could be a clone or bundle with multiple * instances, and the dependent as a whole is allowed on multiple * nodes but some instance still can't run * - the dependent has considered node-specific criteria such as * location constraints and stickiness by this point, but might * have other factors that end up disallowing a node * * but the alternative is making the primary move when it doesn't * need to. * * We also consider the primary's stickiness and influence, so the * user has some say in the matter. (This is the configured primary, * not a particular instance of the primary, but that doesn't matter * unless stickiness uses a rule to vary by node, and that seems * acceptable to ignore.) */ if ((colocation->primary->stickiness >= -score) || !pcmk__colocation_has_influence(colocation, NULL) || !allowed_on_one(colocation->dependent)) { crm_trace("%s: Filtering %d + %f * %d " "(double negative disallowed)", pcmk__node_name(node), node->weight, factor, score); continue; } } if (node->weight == INFINITY_HACK) { crm_trace("%s: Filtering %d + %f * %d (node was marked unusable)", pcmk__node_name(node), node->weight, factor, score); continue; } delta_f = factor * score; // Round the number; see http://c-faq.com/fp/round.html delta = (int) ((delta_f < 0)? (delta_f - 0.5) : (delta_f + 0.5)); /* Small factors can obliterate the small scores that are often actually * used in configurations. If the score and factor are nonzero, ensure * that the result is nonzero as well. */ if ((delta == 0) && (score != 0)) { if (factor > 0.0) { delta = 1; } else if (factor < 0.0) { delta = -1; } } new_score = pcmk__add_scores(delta, node->weight); if (only_positive && (new_score < 0) && (node->weight > 0)) { crm_trace("%s: Filtering %d + %f * %d = %d " "(negative disallowed, marking node unusable)", pcmk__node_name(node), node->weight, factor, score, new_score); node->weight = INFINITY_HACK; continue; } if (only_positive && (new_score < 0) && (node->weight == 0)) { crm_trace("%s: Filtering %d + %f * %d = %d (negative disallowed)", pcmk__node_name(node), node->weight, factor, score, new_score); continue; } crm_trace("%s: %d + %f * %d = %d", pcmk__node_name(node), node->weight, factor, score, new_score); node->weight = new_score; } } /*! * \internal * \brief Update nodes with scores of colocated resources' nodes * * Given a table of nodes and a resource, update the nodes' scores with the * scores of the best nodes matching the attribute used for each of the * resource's relevant colocations. * * \param[in,out] source_rsc Resource whose node scores to add * \param[in] target_rsc Resource on whose behalf to update \p *nodes * \param[in] log_id Resource ID for logs (if \c NULL, use * \p source_rsc ID) * \param[in,out] nodes Nodes to update (set initial contents to \c NULL * to copy allowed nodes from \p source_rsc) * \param[in] colocation Original colocation constraint (used to get * configured primary resource's stickiness, and * to get colocation node attribute; if \c NULL, * source_rsc's own matching node scores * will not be added, and \p *nodes must be \c NULL * as well) * \param[in] factor Incorporate scores multiplied by this factor * \param[in] flags Bitmask of enum pcmk__coloc_select values * * \note \c NULL \p target_rsc, \c NULL \p *nodes, \c NULL \p colocation, and * the \c pcmk__coloc_select_this_with flag are used together (and only by * \c cmp_resources()). * \note The caller remains responsible for freeing \p *nodes. * \note This is the shared implementation of * \c pcmk_assignment_methods_t:add_colocated_node_scores(). */ void pcmk__add_colocated_node_scores(pcmk_resource_t *source_rsc, const pcmk_resource_t *target_rsc, const char *log_id, GHashTable **nodes, const pcmk__colocation_t *colocation, float factor, uint32_t flags) { GHashTable *work = NULL; CRM_ASSERT((source_rsc != NULL) && (nodes != NULL) && ((colocation != NULL) || ((target_rsc == NULL) && (*nodes == NULL)))); if (log_id == NULL) { log_id = source_rsc->id; } // Avoid infinite recursion if (pcmk_is_set(source_rsc->flags, pcmk_rsc_updating_nodes)) { pcmk__rsc_info(source_rsc, "%s: Breaking dependency loop at %s", log_id, source_rsc->id); return; } pcmk__set_rsc_flags(source_rsc, pcmk_rsc_updating_nodes); if (*nodes == NULL) { work = pcmk__copy_node_table(source_rsc->allowed_nodes); target_rsc = source_rsc; } else { const bool pos = pcmk_is_set(flags, pcmk__coloc_select_nonnegative); pcmk__rsc_trace(source_rsc, "%s: Merging %s scores from %s (at %.6f)", log_id, (pos? "positive" : "all"), source_rsc->id, factor); work = pcmk__copy_node_table(*nodes); add_node_scores_matching_attr(work, source_rsc, target_rsc, colocation, factor, pos); } if (work == NULL) { pcmk__clear_rsc_flags(source_rsc, pcmk_rsc_updating_nodes); return; } if (pcmk__any_node_available(work)) { GList *colocations = NULL; if (pcmk_is_set(flags, pcmk__coloc_select_this_with)) { colocations = pcmk__this_with_colocations(source_rsc); pcmk__rsc_trace(source_rsc, "Checking additional %d optional '%s with' " "constraints", g_list_length(colocations), source_rsc->id); } else { colocations = pcmk__with_this_colocations(source_rsc); pcmk__rsc_trace(source_rsc, "Checking additional %d optional 'with %s' " "constraints", g_list_length(colocations), source_rsc->id); } flags |= pcmk__coloc_select_active; for (GList *iter = colocations; iter != NULL; iter = iter->next) { pcmk__colocation_t *constraint = iter->data; pcmk_resource_t *other = NULL; float other_factor = factor * constraint->score / (float) PCMK_SCORE_INFINITY; if (pcmk_is_set(flags, pcmk__coloc_select_this_with)) { other = constraint->primary; } else if (!pcmk__colocation_has_influence(constraint, NULL)) { continue; } else { other = constraint->dependent; } pcmk__rsc_trace(source_rsc, "Optionally merging score of '%s' constraint " "(%s with %s)", constraint->id, constraint->dependent->id, constraint->primary->id); other->cmds->add_colocated_node_scores(other, target_rsc, log_id, &work, constraint, other_factor, flags); pe__show_node_scores(true, NULL, log_id, work, source_rsc->cluster); } g_list_free(colocations); } else if (pcmk_is_set(flags, pcmk__coloc_select_active)) { pcmk__rsc_info(source_rsc, "%s: Rolling back optional scores from %s", log_id, source_rsc->id); g_hash_table_destroy(work); pcmk__clear_rsc_flags(source_rsc, pcmk_rsc_updating_nodes); return; } if (pcmk_is_set(flags, pcmk__coloc_select_nonnegative)) { pcmk_node_t *node = NULL; GHashTableIter iter; g_hash_table_iter_init(&iter, work); while (g_hash_table_iter_next(&iter, NULL, (void **)&node)) { if (node->weight == INFINITY_HACK) { node->weight = 1; } } } if (*nodes != NULL) { g_hash_table_destroy(*nodes); } *nodes = work; pcmk__clear_rsc_flags(source_rsc, pcmk_rsc_updating_nodes); } /*! * \internal * \brief Apply a "with this" colocation to a resource's allowed node scores * * \param[in,out] data Colocation to apply * \param[in,out] user_data Resource being assigned */ void pcmk__add_dependent_scores(gpointer data, gpointer user_data) { pcmk__colocation_t *colocation = data; pcmk_resource_t *target_rsc = user_data; pcmk_resource_t *source_rsc = colocation->dependent; const float factor = colocation->score / (float) PCMK_SCORE_INFINITY; uint32_t flags = pcmk__coloc_select_active; if (!pcmk__colocation_has_influence(colocation, NULL)) { return; } if (pcmk__is_clone(target_rsc)) { flags |= pcmk__coloc_select_nonnegative; } pcmk__rsc_trace(target_rsc, "%s: Incorporating attenuated %s assignment scores due " "to colocation %s", target_rsc->id, source_rsc->id, colocation->id); source_rsc->cmds->add_colocated_node_scores(source_rsc, target_rsc, source_rsc->id, &target_rsc->allowed_nodes, colocation, factor, flags); } /*! * \internal * \brief Exclude nodes from a dependent's node table if not in a given list * * Given a dependent resource in a colocation and a list of nodes where the * primary resource will run, set a node's score to \c -INFINITY in the * dependent's node table if not found in the primary nodes list. * * \param[in,out] dependent Dependent resource * \param[in] primary Primary resource (for logging only) * \param[in] colocation Colocation constraint (for logging only) * \param[in] primary_nodes List of nodes where the primary will have * unblocked instances in a suitable role * \param[in] merge_scores If \c true and a node is found in both \p table * and \p list, add the node's score in \p list to * the node's score in \p table */ void pcmk__colocation_intersect_nodes(pcmk_resource_t *dependent, const pcmk_resource_t *primary, const pcmk__colocation_t *colocation, const GList *primary_nodes, bool merge_scores) { GHashTableIter iter; pcmk_node_t *dependent_node = NULL; CRM_ASSERT((dependent != NULL) && (primary != NULL) && (colocation != NULL)); g_hash_table_iter_init(&iter, dependent->allowed_nodes); while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &dependent_node)) { const pcmk_node_t *primary_node = NULL; primary_node = pe_find_node_id(primary_nodes, dependent_node->details->id); if (primary_node == NULL) { dependent_node->weight = -PCMK_SCORE_INFINITY; pcmk__rsc_trace(dependent, "Banning %s from %s (no primary instance) for %s", dependent->id, pcmk__node_name(dependent_node), colocation->id); } else if (merge_scores) { dependent_node->weight = pcmk__add_scores(dependent_node->weight, primary_node->weight); pcmk__rsc_trace(dependent, "Added %s's score %s to %s's score for %s (now %s) " "for colocation %s", primary->id, pcmk_readable_score(primary_node->weight), dependent->id, pcmk__node_name(dependent_node), pcmk_readable_score(dependent_node->weight), colocation->id); } } } /*! * \internal * \brief Get all colocations affecting a resource as the primary * * \param[in] rsc Resource to get colocations for * * \return Newly allocated list of colocations affecting \p rsc as primary * * \note This is a convenience wrapper for the with_this_colocations() method. */ GList * pcmk__with_this_colocations(const pcmk_resource_t *rsc) { GList *list = NULL; rsc->cmds->with_this_colocations(rsc, rsc, &list); return list; } /*! * \internal * \brief Get all colocations affecting a resource as the dependent * * \param[in] rsc Resource to get colocations for * * \return Newly allocated list of colocations affecting \p rsc as dependent * * \note This is a convenience wrapper for the this_with_colocations() method. */ GList * pcmk__this_with_colocations(const pcmk_resource_t *rsc) { GList *list = NULL; rsc->cmds->this_with_colocations(rsc, rsc, &list); return list; } diff --git a/lib/pacemaker/pcmk_sched_location.c b/lib/pacemaker/pcmk_sched_location.c index 9cec56898f..b51dcd119b 100644 --- a/lib/pacemaker/pcmk_sched_location.c +++ b/lib/pacemaker/pcmk_sched_location.c @@ -1,726 +1,726 @@ /* * 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 "libpacemaker_private.h" static int get_node_score(const char *rule, const char *score, bool raw, pcmk_node_t *node, pcmk_resource_t *rsc) { int score_f = 0; if (score == NULL) { pcmk__config_warn("Rule %s: no score specified (assuming 0)", rule); } else if (raw) { score_f = char2score(score); } else { const char *target = NULL; const char *attr_score = NULL; target = g_hash_table_lookup(rsc->meta, PCMK_META_CONTAINER_ATTRIBUTE_TARGET); attr_score = pcmk__node_attr(node, score, target, pcmk__rsc_node_current); if (attr_score == NULL) { crm_debug("Rule %s: %s did not have a value for %s", rule, pcmk__node_name(node), score); score_f = -PCMK_SCORE_INFINITY; } else { crm_debug("Rule %s: %s had value %s for %s", rule, pcmk__node_name(node), attr_score, score); score_f = char2score(attr_score); } } return score_f; } /*! * \internal * \brief Parse a role configuration for a location constraint * * \param[in] role_spec Role specification * \param[out] role Where to store parsed role * * \return true if role specification is valid, otherwise false */ static bool parse_location_role(const char *role_spec, enum rsc_role_e *role) { if (role_spec == NULL) { *role = pcmk_role_unknown; return true; } *role = pcmk_parse_role(role_spec); switch (*role) { case pcmk_role_unknown: return false; case pcmk_role_started: case pcmk_role_unpromoted: /* Any promotable clone instance cannot be promoted without being in * the unpromoted role first. Therefore, any constraint for the * started or unpromoted role applies to every role. */ *role = pcmk_role_unknown; break; default: break; } return true; } /*! * \internal * \brief Generate a location constraint from a rule * * \param[in,out] rsc Resource that constraint is for * \param[in] rule_xml Rule XML (sub-element of location constraint) * \param[in] discovery Value of \c PCMK_XA_RESOURCE_DISCOVERY for * constraint * \param[out] next_change Where to set when rule evaluation will change * \param[in,out] rule_input Values used to evaluate rule criteria * (node-specific values will be overwritten by * this function) * * \return true if rule is valid, otherwise false */ static bool generate_location_rule(pcmk_resource_t *rsc, xmlNode *rule_xml, const char *discovery, crm_time_t *next_change, pcmk_rule_input_t *rule_input) { const char *rule_id = NULL; const char *score = NULL; const char *boolean = NULL; const char *role_spec = NULL; GList *iter = NULL; bool raw_score = true; bool score_allocated = false; pcmk__location_t *location_rule = NULL; enum rsc_role_e role = pcmk_role_unknown; enum pcmk__combine combine = pcmk__combine_unknown; - rule_xml = expand_idref(rule_xml, rsc->cluster->input); + rule_xml = pcmk__xe_resolve_idref(rule_xml, rsc->cluster->input); if (rule_xml == NULL) { return false; // Error already logged } rule_id = crm_element_value(rule_xml, PCMK_XA_ID); if (rule_id == NULL) { pcmk__config_err("Ignoring " PCMK_XE_RULE " without " PCMK_XA_ID " in location constraint"); return false; } boolean = crm_element_value(rule_xml, PCMK_XA_BOOLEAN_OP); role_spec = crm_element_value(rule_xml, PCMK_XA_ROLE); if (parse_location_role(role_spec, &role)) { crm_trace("Setting rule %s role filter to %s", rule_id, role_spec); } else { pcmk__config_err("Ignoring rule %s: Invalid " PCMK_XA_ROLE " '%s'", rule_id, role_spec); return false; } crm_trace("Processing location constraint rule %s", rule_id); score = crm_element_value(rule_xml, PCMK_XA_SCORE); if (score == NULL) { score = crm_element_value(rule_xml, PCMK_XA_SCORE_ATTRIBUTE); if (score != NULL) { raw_score = false; } } combine = pcmk__parse_combine(boolean); switch (combine) { case pcmk__combine_and: case pcmk__combine_or: break; default: /* @COMPAT When we can break behavioral backward compatibility, * return false */ pcmk__config_warn("Location constraint rule %s has invalid " PCMK_XA_BOOLEAN_OP " value '%s', using default " "'" PCMK_VALUE_AND "'", rule_id, boolean); combine = pcmk__combine_and; break; } location_rule = pcmk__new_location(rule_id, rsc, 0, discovery, NULL); CRM_CHECK(location_rule != NULL, return NULL); location_rule->role_filter = role; if ((rule_input->rsc_id != NULL) && (rule_input->rsc_id_nmatches > 0) && !raw_score) { char *result = pcmk__replace_submatches(score, rule_input->rsc_id, rule_input->rsc_id_submatches, rule_input->rsc_id_nmatches); if (result != NULL) { score = result; score_allocated = true; } } for (iter = rsc->cluster->nodes; iter != NULL; iter = iter->next) { pcmk_node_t *node = iter->data; rule_input->node_attrs = node->details->attrs; rule_input->rsc_params = pe_rsc_params(rsc, node, rsc->cluster); if (pcmk_evaluate_rule(rule_xml, rule_input, next_change) == pcmk_rc_ok) { pcmk_node_t *local = pe__copy_node(node); location_rule->nodes = g_list_prepend(location_rule->nodes, local); local->weight = get_node_score(rule_id, score, raw_score, node, rsc); crm_trace("%s has score %s after %s", pcmk__node_name(node), pcmk_readable_score(local->weight), rule_id); } } if (score_allocated) { free((char *)score); } if (location_rule->nodes == NULL) { crm_trace("No matching nodes for location constraint rule %s", rule_id); } else { crm_trace("Location constraint rule %s matched %d nodes", rule_id, g_list_length(location_rule->nodes)); } return true; } static void unpack_rsc_location(xmlNode *xml_obj, pcmk_resource_t *rsc, const char *role_spec, const char *score, char *rsc_id_match, int rsc_id_nmatches, regmatch_t *rsc_id_submatches) { const char *rsc_id = crm_element_value(xml_obj, PCMK_XA_RSC); const char *id = crm_element_value(xml_obj, PCMK_XA_ID); const char *node = crm_element_value(xml_obj, PCMK_XE_NODE); const char *discovery = crm_element_value(xml_obj, PCMK_XA_RESOURCE_DISCOVERY); if (rsc == NULL) { pcmk__config_warn("Ignoring constraint '%s' because resource '%s' " "does not exist", id, rsc_id); return; } if (score == NULL) { score = crm_element_value(xml_obj, PCMK_XA_SCORE); } if ((node != NULL) && (score != NULL)) { int score_i = char2score(score); pcmk_node_t *match = pcmk_find_node(rsc->cluster, node); enum rsc_role_e role = pcmk_role_unknown; pcmk__location_t *location = NULL; if (match == NULL) { crm_info("Ignoring location constraint %s " "because '%s' is not a known node", pcmk__s(id, "without ID"), node); return; } if (role_spec == NULL) { role_spec = crm_element_value(xml_obj, PCMK_XA_ROLE); } if (parse_location_role(role_spec, &role)) { crm_trace("Setting location constraint %s role filter: %s", id, role_spec); } else { /* @COMPAT The previous behavior of creating the constraint ignoring * the role is retained for now, but we should ignore the entire * constraint when we can break backward compatibility. */ pcmk__config_err("Ignoring role in constraint %s: " "Invalid value '%s'", id, role_spec); } location = pcmk__new_location(id, rsc, score_i, discovery, match); if (location == NULL) { return; // Error already logged } location->role_filter = role; } else { bool empty = true; crm_time_t *next_change = crm_time_new_undefined(); pcmk_rule_input_t rule_input = { .now = rsc->cluster->now, .rsc_meta = rsc->meta, .rsc_id = rsc_id_match, .rsc_id_submatches = rsc_id_submatches, .rsc_id_nmatches = rsc_id_nmatches, }; /* This loop is logically parallel to pcmk__evaluate_rules(), except * instead of checking whether any rule is active, we set up location * constraints for each active rule. * * @COMPAT When we can break backward compatibility, limit location * constraints to a single rule, for consistency with other contexts. * Since a rule may contain other rules, this does not prohibit any * existing use cases. */ for (xmlNode *rule_xml = pcmk__xe_first_child(xml_obj, PCMK_XE_RULE, NULL, NULL); rule_xml != NULL; rule_xml = pcmk__xe_next_same(rule_xml)) { if (generate_location_rule(rsc, rule_xml, discovery, next_change, &rule_input)) { if (empty) { empty = false; continue; } pcmk__warn_once(pcmk__wo_location_rules, "Support for multiple " PCMK_XE_RULE " elements in a location constraint is " "deprecated and will be removed in a future " "release (use a single new rule combining the " "previous rules with " PCMK_XA_BOOLEAN_OP " set to '" PCMK_VALUE_OR "' instead)"); } } if (empty) { pcmk__config_err("Ignoring constraint '%s' because it contains " "no valid rules", id); } /* If there is a point in the future when the evaluation of a rule will * change, make sure the scheduler is re-run by that time. */ if (crm_time_is_defined(next_change)) { time_t t = (time_t) crm_time_get_seconds_since_epoch(next_change); pe__update_recheck_time(t, rsc->cluster, "location rule evaluation"); } crm_time_free(next_change); } } static void unpack_simple_location(xmlNode *xml_obj, pcmk_scheduler_t *scheduler) { const char *id = crm_element_value(xml_obj, PCMK_XA_ID); const char *value = crm_element_value(xml_obj, PCMK_XA_RSC); if (value) { pcmk_resource_t *rsc; rsc = pcmk__find_constraint_resource(scheduler->resources, value); unpack_rsc_location(xml_obj, rsc, NULL, NULL, NULL, 0, NULL); } value = crm_element_value(xml_obj, PCMK_XA_RSC_PATTERN); if (value) { regex_t regex; bool invert = false; if (value[0] == '!') { value++; invert = true; } if (regcomp(®ex, value, REG_EXTENDED) != 0) { pcmk__config_err("Ignoring constraint '%s' because " PCMK_XA_RSC_PATTERN " has invalid value '%s'", id, value); return; } for (GList *iter = scheduler->resources; iter != NULL; iter = iter->next) { pcmk_resource_t *r = iter->data; int nregs = 0; regmatch_t *pmatch = NULL; int status; if (regex.re_nsub > 0) { nregs = regex.re_nsub + 1; } else { nregs = 1; } pmatch = pcmk__assert_alloc(nregs, sizeof(regmatch_t)); status = regexec(®ex, r->id, nregs, pmatch, 0); if (!invert && (status == 0)) { crm_debug("'%s' matched '%s' for %s", r->id, value, id); unpack_rsc_location(xml_obj, r, NULL, NULL, r->id, nregs, pmatch); } else if (invert && (status != 0)) { crm_debug("'%s' is an inverted match of '%s' for %s", r->id, value, id); unpack_rsc_location(xml_obj, r, NULL, NULL, NULL, 0, NULL); } else { crm_trace("'%s' does not match '%s' for %s", r->id, value, id); } free(pmatch); } regfree(®ex); } } // \return Standard Pacemaker return code static int unpack_location_tags(xmlNode *xml_obj, xmlNode **expanded_xml, pcmk_scheduler_t *scheduler) { const char *id = NULL; const char *rsc_id = NULL; const char *state = NULL; pcmk_resource_t *rsc = NULL; pcmk_tag_t *tag = NULL; xmlNode *rsc_set = NULL; *expanded_xml = NULL; CRM_CHECK(xml_obj != NULL, return EINVAL); id = pcmk__xe_id(xml_obj); if (id == NULL) { pcmk__config_err("Ignoring <%s> constraint without " PCMK_XA_ID, xml_obj->name); return pcmk_rc_unpack_error; } // Check whether there are any resource sets with template or tag references *expanded_xml = pcmk__expand_tags_in_sets(xml_obj, scheduler); if (*expanded_xml != NULL) { crm_log_xml_trace(*expanded_xml, "Expanded " PCMK_XE_RSC_LOCATION); return pcmk_rc_ok; } rsc_id = crm_element_value(xml_obj, PCMK_XA_RSC); if (rsc_id == NULL) { return pcmk_rc_ok; } if (!pcmk__valid_resource_or_tag(scheduler, rsc_id, &rsc, &tag)) { pcmk__config_err("Ignoring constraint '%s' because '%s' is not a " "valid resource or tag", id, rsc_id); return pcmk_rc_unpack_error; } else if (rsc != NULL) { // No template is referenced return pcmk_rc_ok; } state = crm_element_value(xml_obj, PCMK_XA_ROLE); *expanded_xml = pcmk__xml_copy(NULL, xml_obj); /* Convert any template or tag reference into constraint * PCMK_XE_RESOURCE_SET */ if (!pcmk__tag_to_set(*expanded_xml, &rsc_set, PCMK_XA_RSC, false, scheduler)) { pcmk__xml_free(*expanded_xml); *expanded_xml = NULL; return pcmk_rc_unpack_error; } if (rsc_set != NULL) { if (state != NULL) { /* Move PCMK_XA_RSC_ROLE into converted PCMK_XE_RESOURCE_SET as * PCMK_XA_ROLE attribute */ crm_xml_add(rsc_set, PCMK_XA_ROLE, state); pcmk__xe_remove_attr(*expanded_xml, PCMK_XA_ROLE); } crm_log_xml_trace(*expanded_xml, "Expanded " PCMK_XE_RSC_LOCATION); } else { // No sets pcmk__xml_free(*expanded_xml); *expanded_xml = NULL; } return pcmk_rc_ok; } // \return Standard Pacemaker return code static int unpack_location_set(xmlNode *location, xmlNode *set, pcmk_scheduler_t *scheduler) { xmlNode *xml_rsc = NULL; pcmk_resource_t *resource = NULL; const char *set_id; const char *role; const char *local_score; CRM_CHECK(set != NULL, return EINVAL); set_id = pcmk__xe_id(set); if (set_id == NULL) { pcmk__config_err("Ignoring " PCMK_XE_RESOURCE_SET " without " PCMK_XA_ID " in constraint '%s'", pcmk__s(pcmk__xe_id(location), "(missing ID)")); return pcmk_rc_unpack_error; } role = crm_element_value(set, PCMK_XA_ROLE); local_score = crm_element_value(set, PCMK_XA_SCORE); for (xml_rsc = pcmk__xe_first_child(set, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { resource = pcmk__find_constraint_resource(scheduler->resources, pcmk__xe_id(xml_rsc)); if (resource == NULL) { pcmk__config_err("%s: No resource found for %s", set_id, pcmk__xe_id(xml_rsc)); return pcmk_rc_unpack_error; } unpack_rsc_location(location, resource, role, local_score, NULL, 0, NULL); } return pcmk_rc_ok; } void pcmk__unpack_location(xmlNode *xml_obj, pcmk_scheduler_t *scheduler) { xmlNode *set = NULL; bool any_sets = false; xmlNode *orig_xml = NULL; xmlNode *expanded_xml = NULL; if (unpack_location_tags(xml_obj, &expanded_xml, scheduler) != pcmk_rc_ok) { return; } if (expanded_xml) { orig_xml = xml_obj; xml_obj = expanded_xml; } for (set = pcmk__xe_first_child(xml_obj, PCMK_XE_RESOURCE_SET, NULL, NULL); set != NULL; set = pcmk__xe_next_same(set)) { any_sets = true; - set = expand_idref(set, scheduler->input); + set = pcmk__xe_resolve_idref(set, scheduler->input); if ((set == NULL) // Configuration error, message already logged || (unpack_location_set(xml_obj, set, scheduler) != pcmk_rc_ok)) { if (expanded_xml) { pcmk__xml_free(expanded_xml); } return; } } if (expanded_xml) { pcmk__xml_free(expanded_xml); xml_obj = orig_xml; } if (!any_sets) { unpack_simple_location(xml_obj, scheduler); } } /*! * \internal * \brief Add a new location constraint to scheduler data * * \param[in] id XML ID of location constraint * \param[in,out] rsc Resource in location constraint * \param[in] node_score Constraint score * \param[in] discover_mode Resource discovery option for constraint * \param[in] node Node in constraint (or NULL if rule-based) * * \return Newly allocated location constraint on success, otherwise NULL * \note The result will be added to the cluster (via \p rsc) and should not be * freed separately. */ pcmk__location_t * pcmk__new_location(const char *id, pcmk_resource_t *rsc, int node_score, const char *discover_mode, pcmk_node_t *node) { pcmk__location_t *new_con = NULL; CRM_CHECK((node != NULL) || (node_score == 0), return NULL); if (id == NULL) { pcmk__config_err("Invalid constraint: no ID specified"); return NULL; } if (rsc == NULL) { pcmk__config_err("Invalid constraint %s: no resource specified", id); return NULL; } new_con = pcmk__assert_alloc(1, sizeof(pcmk__location_t)); new_con->id = pcmk__str_copy(id); new_con->rsc = rsc; new_con->nodes = NULL; new_con->role_filter = pcmk_role_unknown; if (pcmk__str_eq(discover_mode, PCMK_VALUE_ALWAYS, pcmk__str_null_matches|pcmk__str_casei)) { new_con->discover_mode = pcmk_probe_always; } else if (pcmk__str_eq(discover_mode, PCMK_VALUE_NEVER, pcmk__str_casei)) { new_con->discover_mode = pcmk_probe_never; } else if (pcmk__str_eq(discover_mode, PCMK_VALUE_EXCLUSIVE, pcmk__str_casei)) { new_con->discover_mode = pcmk_probe_exclusive; rsc->exclusive_discover = TRUE; } else { pcmk__config_err("Invalid " PCMK_XA_RESOURCE_DISCOVERY " value %s " "in location constraint", discover_mode); } if (node != NULL) { pcmk_node_t *copy = pe__copy_node(node); copy->weight = node_score; new_con->nodes = g_list_prepend(NULL, copy); } rsc->cluster->placement_constraints = g_list_prepend( rsc->cluster->placement_constraints, new_con); rsc->rsc_location = g_list_prepend(rsc->rsc_location, new_con); return new_con; } /*! * \internal * \brief Apply all location constraints * * \param[in,out] scheduler Scheduler data */ void pcmk__apply_locations(pcmk_scheduler_t *scheduler) { for (GList *iter = scheduler->placement_constraints; iter != NULL; iter = iter->next) { pcmk__location_t *location = iter->data; location->rsc->cmds->apply_location(location->rsc, location); } } /*! * \internal * \brief Apply a location constraint to a resource's allowed node scores * * \param[in,out] rsc Resource to apply constraint to * \param[in,out] location Location constraint to apply * * \note This does not consider the resource's children, so the resource's * apply_location() method should be used instead in most cases. */ void pcmk__apply_location(pcmk_resource_t *rsc, pcmk__location_t *location) { bool need_role = false; CRM_ASSERT((rsc != NULL) && (location != NULL)); // If a role was specified, ensure constraint is applicable need_role = (location->role_filter > pcmk_role_unknown); if (need_role && (location->role_filter != rsc->next_role)) { pcmk__rsc_trace(rsc, "Not applying %s to %s because role will be %s not %s", location->id, rsc->id, pcmk_role_text(rsc->next_role), pcmk_role_text(location->role_filter)); return; } if (location->nodes == NULL) { pcmk__rsc_trace(rsc, "Not applying %s to %s because no nodes match", location->id, rsc->id); return; } pcmk__rsc_trace(rsc, "Applying %s%s%s to %s", location->id, (need_role? " for role " : ""), (need_role? pcmk_role_text(location->role_filter) : ""), rsc->id); for (GList *iter = location->nodes; iter != NULL; iter = iter->next) { pcmk_node_t *node = iter->data; pcmk_node_t *allowed_node = g_hash_table_lookup(rsc->allowed_nodes, node->details->id); if (allowed_node == NULL) { pcmk__rsc_trace(rsc, "* = %d on %s", node->weight, pcmk__node_name(node)); allowed_node = pe__copy_node(node); g_hash_table_insert(rsc->allowed_nodes, (gpointer) allowed_node->details->id, allowed_node); } else { pcmk__rsc_trace(rsc, "* + %d on %s", node->weight, pcmk__node_name(node)); allowed_node->weight = pcmk__add_scores(allowed_node->weight, node->weight); } if (allowed_node->rsc_discover_mode < location->discover_mode) { if (location->discover_mode == pcmk_probe_exclusive) { rsc->exclusive_discover = TRUE; } /* exclusive > never > always... always is default */ allowed_node->rsc_discover_mode = location->discover_mode; } } } diff --git a/lib/pacemaker/pcmk_sched_ordering.c b/lib/pacemaker/pcmk_sched_ordering.c index 83d3759dab..5141c53038 100644 --- a/lib/pacemaker/pcmk_sched_ordering.c +++ b/lib/pacemaker/pcmk_sched_ordering.c @@ -1,1531 +1,1531 @@ /* * 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 // PRIx32 #include #include #include #include #include "libpacemaker_private.h" enum pe_order_kind { pe_order_kind_optional, pe_order_kind_mandatory, pe_order_kind_serialize, }; enum ordering_symmetry { ordering_asymmetric, // the only relation in an asymmetric ordering ordering_symmetric, // the normal relation in a symmetric ordering ordering_symmetric_inverse, // the inverse relation in a symmetric ordering }; #define EXPAND_CONSTRAINT_IDREF(__set, __rsc, __name) do { \ __rsc = pcmk__find_constraint_resource(scheduler->resources, \ __name); \ if (__rsc == NULL) { \ pcmk__config_err("%s: No resource found for %s", __set, __name);\ return pcmk_rc_unpack_error; \ } \ } while (0) static const char * invert_action(const char *action) { if (pcmk__str_eq(action, PCMK_ACTION_START, pcmk__str_none)) { return PCMK_ACTION_STOP; } else if (pcmk__str_eq(action, PCMK_ACTION_STOP, pcmk__str_none)) { return PCMK_ACTION_START; } else if (pcmk__str_eq(action, PCMK_ACTION_PROMOTE, pcmk__str_none)) { return PCMK_ACTION_DEMOTE; } else if (pcmk__str_eq(action, PCMK_ACTION_DEMOTE, pcmk__str_none)) { return PCMK_ACTION_PROMOTE; } else if (pcmk__str_eq(action, PCMK_ACTION_PROMOTED, pcmk__str_none)) { return PCMK_ACTION_DEMOTED; } else if (pcmk__str_eq(action, PCMK_ACTION_DEMOTED, pcmk__str_none)) { return PCMK_ACTION_PROMOTED; } else if (pcmk__str_eq(action, PCMK_ACTION_RUNNING, pcmk__str_none)) { return PCMK_ACTION_STOPPED; } else if (pcmk__str_eq(action, PCMK_ACTION_STOPPED, pcmk__str_none)) { return PCMK_ACTION_RUNNING; } pcmk__config_warn("Unknown action '%s' specified in order constraint", action); return NULL; } static enum pe_order_kind get_ordering_type(const xmlNode *xml_obj) { enum pe_order_kind kind_e = pe_order_kind_mandatory; const char *kind = crm_element_value(xml_obj, PCMK_XA_KIND); if (kind == NULL) { const char *score = crm_element_value(xml_obj, PCMK_XA_SCORE); kind_e = pe_order_kind_mandatory; if (score) { // @COMPAT deprecated informally since 1.0.7, formally since 2.0.1 int score_i = char2score(score); if (score_i == 0) { kind_e = pe_order_kind_optional; } pcmk__warn_once(pcmk__wo_order_score, "Support for '" PCMK_XA_SCORE "' in " PCMK_XE_RSC_ORDER " is deprecated and will be " "removed in a future release " "(use '" PCMK_XA_KIND "' instead)"); } } else if (pcmk__str_eq(kind, PCMK_VALUE_MANDATORY, pcmk__str_none)) { kind_e = pe_order_kind_mandatory; } else if (pcmk__str_eq(kind, PCMK_VALUE_OPTIONAL, pcmk__str_none)) { kind_e = pe_order_kind_optional; } else if (pcmk__str_eq(kind, PCMK_VALUE_SERIALIZE, pcmk__str_none)) { kind_e = pe_order_kind_serialize; } else { pcmk__config_err("Resetting '" PCMK_XA_KIND "' for constraint %s to " "'" PCMK_VALUE_MANDATORY "' because '%s' is not valid", pcmk__s(pcmk__xe_id(xml_obj), "missing ID"), kind); } return kind_e; } /*! * \internal * \brief Get ordering symmetry from XML * * \param[in] xml_obj Ordering XML * \param[in] parent_kind Default ordering kind * \param[in] parent_symmetrical_s Parent element's \c PCMK_XA_SYMMETRICAL * setting, if any * * \retval ordering_symmetric Ordering is symmetric * \retval ordering_asymmetric Ordering is asymmetric */ static enum ordering_symmetry get_ordering_symmetry(const xmlNode *xml_obj, enum pe_order_kind parent_kind, const char *parent_symmetrical_s) { int rc = pcmk_rc_ok; bool symmetric = false; enum pe_order_kind kind = parent_kind; // Default to parent's kind // Check ordering XML for explicit kind if ((crm_element_value(xml_obj, PCMK_XA_KIND) != NULL) || (crm_element_value(xml_obj, PCMK_XA_SCORE) != NULL)) { kind = get_ordering_type(xml_obj); } // Check ordering XML (and parent) for explicit PCMK_XA_SYMMETRICAL setting rc = pcmk__xe_get_bool_attr(xml_obj, PCMK_XA_SYMMETRICAL, &symmetric); if (rc != pcmk_rc_ok && parent_symmetrical_s != NULL) { symmetric = crm_is_true(parent_symmetrical_s); rc = pcmk_rc_ok; } if (rc == pcmk_rc_ok) { if (symmetric) { if (kind == pe_order_kind_serialize) { pcmk__config_warn("Ignoring " PCMK_XA_SYMMETRICAL " for '%s' because not valid with " PCMK_XA_KIND " of '" PCMK_VALUE_SERIALIZE "'", pcmk__xe_id(xml_obj)); } else { return ordering_symmetric; } } return ordering_asymmetric; } // Use default symmetry if (kind == pe_order_kind_serialize) { return ordering_asymmetric; } return ordering_symmetric; } /*! * \internal * \brief Get ordering flags appropriate to ordering kind * * \param[in] kind Ordering kind * \param[in] first Action name for 'first' action * \param[in] symmetry This ordering's symmetry role * * \return Minimal ordering flags appropriate to \p kind */ static uint32_t ordering_flags_for_kind(enum pe_order_kind kind, const char *first, enum ordering_symmetry symmetry) { uint32_t flags = pcmk__ar_none; // so we trace-log all flags set switch (kind) { case pe_order_kind_optional: pcmk__set_relation_flags(flags, pcmk__ar_ordered); break; case pe_order_kind_serialize: /* This flag is not used anywhere directly but means the relation * will not match an equality comparison against pcmk__ar_none or * pcmk__ar_ordered. */ pcmk__set_relation_flags(flags, pcmk__ar_serialize); break; case pe_order_kind_mandatory: pcmk__set_relation_flags(flags, pcmk__ar_ordered); switch (symmetry) { case ordering_asymmetric: pcmk__set_relation_flags(flags, pcmk__ar_asymmetric); break; case ordering_symmetric: pcmk__set_relation_flags(flags, pcmk__ar_first_implies_then); if (pcmk__strcase_any_of(first, PCMK_ACTION_START, PCMK_ACTION_PROMOTE, NULL)) { pcmk__set_relation_flags(flags, pcmk__ar_unrunnable_first_blocks); } break; case ordering_symmetric_inverse: pcmk__set_relation_flags(flags, pcmk__ar_then_implies_first); break; } break; } return flags; } /*! * \internal * \brief Find resource corresponding to ID specified in ordering * * \param[in] xml Ordering XML * \param[in] resource_attr XML attribute name for resource ID * \param[in] instance_attr XML attribute name for instance number. * This option is deprecated and will be removed in a * future release. * \param[in] scheduler Scheduler data * * \return Resource corresponding to \p id, or NULL if none */ static pcmk_resource_t * get_ordering_resource(const xmlNode *xml, const char *resource_attr, const char *instance_attr, const pcmk_scheduler_t *scheduler) { // @COMPAT: instance_attr and instance_id variables deprecated since 2.1.5 pcmk_resource_t *rsc = NULL; const char *rsc_id = crm_element_value(xml, resource_attr); const char *instance_id = crm_element_value(xml, instance_attr); if (rsc_id == NULL) { pcmk__config_err("Ignoring constraint '%s' without %s", pcmk__xe_id(xml), resource_attr); return NULL; } rsc = pcmk__find_constraint_resource(scheduler->resources, rsc_id); if (rsc == NULL) { pcmk__config_err("Ignoring constraint '%s' because resource '%s' " "does not exist", pcmk__xe_id(xml), rsc_id); return NULL; } if (instance_id != NULL) { pcmk__warn_once(pcmk__wo_order_inst, "Support for " PCMK__XA_FIRST_INSTANCE " and " PCMK__XA_THEN_INSTANCE " is deprecated and will be " "removed in a future release."); if (!pcmk__is_clone(rsc)) { pcmk__config_err("Ignoring constraint '%s' because resource '%s' " "is not a clone but instance '%s' was requested", pcmk__xe_id(xml), rsc_id, instance_id); return NULL; } rsc = find_clone_instance(rsc, instance_id); if (rsc == NULL) { pcmk__config_err("Ignoring constraint '%s' because resource '%s' " "does not have an instance '%s'", pcmk__xe_id(xml), rsc_id, instance_id); return NULL; } } return rsc; } /*! * \internal * \brief Determine minimum number of 'first' instances required in ordering * * \param[in] rsc 'First' resource in ordering * \param[in] xml Ordering XML * * \return Minimum 'first' instances required (or 0 if not applicable) */ static int get_minimum_first_instances(const pcmk_resource_t *rsc, const xmlNode *xml) { const char *clone_min = NULL; bool require_all = false; if (!pcmk__is_clone(rsc)) { return 0; } clone_min = g_hash_table_lookup(rsc->meta, PCMK_META_CLONE_MIN); if (clone_min != NULL) { int clone_min_int = 0; pcmk__scan_min_int(clone_min, &clone_min_int, 0); return clone_min_int; } /* @COMPAT 1.1.13: * PCMK_XA_REQUIRE_ALL=PCMK_VALUE_FALSE is deprecated equivalent of * PCMK_META_CLONE_MIN=1 */ if (pcmk__xe_get_bool_attr(xml, PCMK_XA_REQUIRE_ALL, &require_all) != ENODATA) { pcmk__warn_once(pcmk__wo_require_all, "Support for " PCMK_XA_REQUIRE_ALL " in ordering " "constraints is deprecated and will be removed in a " "future release (use " PCMK_META_CLONE_MIN " clone " "meta-attribute instead)"); if (!require_all) { return 1; } } return 0; } /*! * \internal * \brief Create orderings for a constraint with \c PCMK_META_CLONE_MIN > 0 * * \param[in] id Ordering ID * \param[in,out] rsc_first 'First' resource in ordering (a clone) * \param[in] action_first 'First' action in ordering * \param[in] rsc_then 'Then' resource in ordering * \param[in] action_then 'Then' action in ordering * \param[in] flags Ordering flags * \param[in] clone_min Minimum required instances of 'first' */ static void clone_min_ordering(const char *id, pcmk_resource_t *rsc_first, const char *action_first, pcmk_resource_t *rsc_then, const char *action_then, uint32_t flags, int clone_min) { // Create a pseudo-action for when the minimum instances are active char *task = crm_strdup_printf(PCMK_ACTION_CLONE_ONE_OR_MORE ":%s", id); pcmk_action_t *clone_min_met = get_pseudo_op(task, rsc_first->cluster); free(task); /* Require the pseudo-action to have the required number of actions to be * considered runnable before allowing the pseudo-action to be runnable. */ clone_min_met->required_runnable_before = clone_min; pcmk__set_action_flags(clone_min_met, pcmk_action_min_runnable); // Order the actions for each clone instance before the pseudo-action for (GList *iter = rsc_first->children; iter != NULL; iter = iter->next) { pcmk_resource_t *child = iter->data; pcmk__new_ordering(child, pcmk__op_key(child->id, action_first, 0), NULL, NULL, NULL, clone_min_met, pcmk__ar_min_runnable |pcmk__ar_first_implies_then_graphed, rsc_first->cluster); } // Order "then" action after the pseudo-action (if runnable) pcmk__new_ordering(NULL, NULL, clone_min_met, rsc_then, pcmk__op_key(rsc_then->id, action_then, 0), NULL, flags|pcmk__ar_unrunnable_first_blocks, rsc_first->cluster); } /*! * \internal * \brief Update ordering flags for restart-type=restart * * \param[in] rsc 'Then' resource in ordering * \param[in] kind Ordering kind * \param[in] flag Ordering flag to set (when applicable) * \param[in,out] flags Ordering flag set to update * * \compat The \c PCMK__META_RESTART_TYPE resource meta-attribute is deprecated. * Eventually, it will be removed, and \c pe_restart_ignore will be the * only behavior, at which time this can just be removed entirely. */ #define handle_restart_type(rsc, kind, flag, flags) do { \ if (((kind) == pe_order_kind_optional) \ && ((rsc)->restart_type == pe_restart_restart)) { \ pcmk__set_relation_flags((flags), (flag)); \ } \ } while (0) /*! * \internal * \brief Create new ordering for inverse of symmetric constraint * * \param[in] id Ordering ID (for logging only) * \param[in] kind Ordering kind * \param[in] rsc_first 'First' resource in ordering (a clone) * \param[in] action_first 'First' action in ordering * \param[in,out] rsc_then 'Then' resource in ordering * \param[in] action_then 'Then' action in ordering */ static void inverse_ordering(const char *id, enum pe_order_kind kind, pcmk_resource_t *rsc_first, const char *action_first, pcmk_resource_t *rsc_then, const char *action_then) { action_then = invert_action(action_then); action_first = invert_action(action_first); if ((action_then == NULL) || (action_first == NULL)) { pcmk__config_warn("Cannot invert constraint '%s' " "(please specify inverse manually)", id); } else { uint32_t flags = ordering_flags_for_kind(kind, action_first, ordering_symmetric_inverse); handle_restart_type(rsc_then, kind, pcmk__ar_then_implies_first, flags); pcmk__order_resource_actions(rsc_then, action_then, rsc_first, action_first, flags); } } static void unpack_simple_rsc_order(xmlNode *xml_obj, pcmk_scheduler_t *scheduler) { pcmk_resource_t *rsc_then = NULL; pcmk_resource_t *rsc_first = NULL; int min_required_before = 0; enum pe_order_kind kind = pe_order_kind_mandatory; uint32_t flags = pcmk__ar_none; enum ordering_symmetry symmetry; const char *action_then = NULL; const char *action_first = NULL; const char *id = NULL; CRM_CHECK(xml_obj != NULL, return); id = crm_element_value(xml_obj, PCMK_XA_ID); if (id == NULL) { pcmk__config_err("Ignoring <%s> constraint without " PCMK_XA_ID, xml_obj->name); return; } rsc_first = get_ordering_resource(xml_obj, PCMK_XA_FIRST, PCMK__XA_FIRST_INSTANCE, scheduler); if (rsc_first == NULL) { return; } rsc_then = get_ordering_resource(xml_obj, PCMK_XA_THEN, PCMK__XA_THEN_INSTANCE, scheduler); if (rsc_then == NULL) { return; } action_first = crm_element_value(xml_obj, PCMK_XA_FIRST_ACTION); if (action_first == NULL) { action_first = PCMK_ACTION_START; } action_then = crm_element_value(xml_obj, PCMK_XA_THEN_ACTION); if (action_then == NULL) { action_then = action_first; } kind = get_ordering_type(xml_obj); symmetry = get_ordering_symmetry(xml_obj, kind, NULL); flags = ordering_flags_for_kind(kind, action_first, symmetry); handle_restart_type(rsc_then, kind, pcmk__ar_first_implies_then, flags); /* If there is a minimum number of instances that must be runnable before * the 'then' action is runnable, we use a pseudo-action for convenience: * minimum number of clone instances have runnable actions -> * pseudo-action is runnable -> dependency is runnable. */ min_required_before = get_minimum_first_instances(rsc_first, xml_obj); if (min_required_before > 0) { clone_min_ordering(id, rsc_first, action_first, rsc_then, action_then, flags, min_required_before); } else { pcmk__order_resource_actions(rsc_first, action_first, rsc_then, action_then, flags); } if (symmetry == ordering_symmetric) { inverse_ordering(id, kind, rsc_first, action_first, rsc_then, action_then); } } /*! * \internal * \brief Create a new ordering between two actions * * \param[in,out] first_rsc Resource for 'first' action (if NULL and * \p first_action is a resource action, that * resource will be used) * \param[in,out] first_action_task Action key for 'first' action (if NULL and * \p first_action is not NULL, its UUID will * be used) * \param[in,out] first_action 'first' action (if NULL, \p first_rsc and * \p first_action_task must be set) * * \param[in] then_rsc Resource for 'then' action (if NULL and * \p then_action is a resource action, that * resource will be used) * \param[in,out] then_action_task Action key for 'then' action (if NULL and * \p then_action is not NULL, its UUID will * be used) * \param[in] then_action 'then' action (if NULL, \p then_rsc and * \p then_action_task must be set) * * \param[in] flags Group of enum pcmk__action_relation_flags * \param[in,out] sched Scheduler data to add ordering to * * \note This function takes ownership of first_action_task and * then_action_task, which do not need to be freed by the caller. */ void pcmk__new_ordering(pcmk_resource_t *first_rsc, char *first_action_task, pcmk_action_t *first_action, pcmk_resource_t *then_rsc, char *then_action_task, pcmk_action_t *then_action, uint32_t flags, pcmk_scheduler_t *sched) { pcmk__action_relation_t *order = NULL; // One of action or resource must be specified for each side CRM_CHECK(((first_action != NULL) || (first_rsc != NULL)) && ((then_action != NULL) || (then_rsc != NULL)), free(first_action_task); free(then_action_task); return); if ((first_rsc == NULL) && (first_action != NULL)) { first_rsc = first_action->rsc; } if ((then_rsc == NULL) && (then_action != NULL)) { then_rsc = then_action->rsc; } order = pcmk__assert_alloc(1, sizeof(pcmk__action_relation_t)); order->id = sched->order_id++; order->flags = flags; order->rsc1 = first_rsc; order->rsc2 = then_rsc; order->action1 = first_action; order->action2 = then_action; order->task1 = first_action_task; order->task2 = then_action_task; if ((order->task1 == NULL) && (first_action != NULL)) { order->task1 = strdup(first_action->uuid); } if ((order->task2 == NULL) && (then_action != NULL)) { order->task2 = strdup(then_action->uuid); } if ((order->rsc1 == NULL) && (first_action != NULL)) { order->rsc1 = first_action->rsc; } if ((order->rsc2 == NULL) && (then_action != NULL)) { order->rsc2 = then_action->rsc; } pcmk__rsc_trace(first_rsc, "Created ordering %d for %s then %s", (sched->order_id - 1), pcmk__s(order->task1, "an underspecified action"), pcmk__s(order->task2, "an underspecified action")); sched->ordering_constraints = g_list_prepend(sched->ordering_constraints, order); pcmk__order_migration_equivalents(order); } /*! * \brief Unpack a set in an ordering constraint * * \param[in] set Set XML to unpack * \param[in] parent_kind \c PCMK_XE_RSC_ORDER XML \c PCMK_XA_KIND * attribute * \param[in] parent_symmetrical_s \c PCMK_XE_RSC_ORDER XML * \c PCMK_XA_SYMMETRICAL attribute * \param[in,out] scheduler Scheduler data * * \return Standard Pacemaker return code */ static int unpack_order_set(const xmlNode *set, enum pe_order_kind parent_kind, const char *parent_symmetrical_s, pcmk_scheduler_t *scheduler) { GList *set_iter = NULL; GList *resources = NULL; pcmk_resource_t *last = NULL; pcmk_resource_t *resource = NULL; int local_kind = parent_kind; bool sequential = false; uint32_t flags = pcmk__ar_ordered; enum ordering_symmetry symmetry; char *key = NULL; const char *id = pcmk__xe_id(set); const char *action = crm_element_value(set, PCMK_XA_ACTION); const char *sequential_s = crm_element_value(set, PCMK_XA_SEQUENTIAL); const char *kind_s = crm_element_value(set, PCMK_XA_KIND); if (action == NULL) { action = PCMK_ACTION_START; } if (kind_s) { local_kind = get_ordering_type(set); } if (sequential_s == NULL) { sequential_s = "1"; } sequential = crm_is_true(sequential_s); symmetry = get_ordering_symmetry(set, parent_kind, parent_symmetrical_s); flags = ordering_flags_for_kind(local_kind, action, symmetry); for (const xmlNode *xml_rsc = pcmk__xe_first_child(set, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { EXPAND_CONSTRAINT_IDREF(id, resource, pcmk__xe_id(xml_rsc)); resources = g_list_append(resources, resource); } if (pcmk__list_of_1(resources)) { crm_trace("Single set: %s", id); goto done; } set_iter = resources; while (set_iter != NULL) { resource = (pcmk_resource_t *) set_iter->data; set_iter = set_iter->next; key = pcmk__op_key(resource->id, action, 0); if (local_kind == pe_order_kind_serialize) { /* Serialize before everything that comes after */ for (GList *iter = set_iter; iter != NULL; iter = iter->next) { pcmk_resource_t *then_rsc = iter->data; char *then_key = pcmk__op_key(then_rsc->id, action, 0); pcmk__new_ordering(resource, strdup(key), NULL, then_rsc, then_key, NULL, flags, scheduler); } } else if (sequential) { if (last != NULL) { pcmk__order_resource_actions(last, action, resource, action, flags); } last = resource; } free(key); } if (symmetry == ordering_asymmetric) { goto done; } last = NULL; action = invert_action(action); flags = ordering_flags_for_kind(local_kind, action, ordering_symmetric_inverse); set_iter = resources; while (set_iter != NULL) { resource = (pcmk_resource_t *) set_iter->data; set_iter = set_iter->next; if (sequential) { if (last != NULL) { pcmk__order_resource_actions(resource, action, last, action, flags); } last = resource; } } done: g_list_free(resources); return pcmk_rc_ok; } /*! * \brief Order two resource sets relative to each other * * \param[in] id Ordering ID (for logging) * \param[in] set1 First listed set * \param[in] set2 Second listed set * \param[in] kind Ordering kind * \param[in,out] scheduler Scheduler data * \param[in] symmetry Which ordering symmetry applies to this relation * * \return Standard Pacemaker return code */ static int order_rsc_sets(const char *id, const xmlNode *set1, const xmlNode *set2, enum pe_order_kind kind, pcmk_scheduler_t *scheduler, enum ordering_symmetry symmetry) { const xmlNode *xml_rsc = NULL; const xmlNode *xml_rsc_2 = NULL; pcmk_resource_t *rsc_1 = NULL; pcmk_resource_t *rsc_2 = NULL; const char *action_1 = crm_element_value(set1, PCMK_XA_ACTION); const char *action_2 = crm_element_value(set2, PCMK_XA_ACTION); uint32_t flags = pcmk__ar_none; bool require_all = true; (void) pcmk__xe_get_bool_attr(set1, PCMK_XA_REQUIRE_ALL, &require_all); if (action_1 == NULL) { action_1 = PCMK_ACTION_START; } if (action_2 == NULL) { action_2 = PCMK_ACTION_START; } if (symmetry == ordering_symmetric_inverse) { action_1 = invert_action(action_1); action_2 = invert_action(action_2); } if (pcmk__str_eq(PCMK_ACTION_STOP, action_1, pcmk__str_none) || pcmk__str_eq(PCMK_ACTION_DEMOTE, action_1, pcmk__str_none)) { /* Assuming: A -> ( B || C) -> D * The one-or-more logic only applies during the start/promote phase. * During shutdown neither B nor can shutdown until D is down, so simply * turn require_all back on. */ require_all = true; } flags = ordering_flags_for_kind(kind, action_1, symmetry); /* If we have an unordered set1, whether it is sequential or not is * irrelevant in regards to set2. */ if (!require_all) { char *task = crm_strdup_printf(PCMK_ACTION_ONE_OR_MORE ":%s", pcmk__xe_id(set1)); pcmk_action_t *unordered_action = get_pseudo_op(task, scheduler); free(task); pcmk__set_action_flags(unordered_action, pcmk_action_min_runnable); for (xml_rsc = pcmk__xe_first_child(set1, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { EXPAND_CONSTRAINT_IDREF(id, rsc_1, pcmk__xe_id(xml_rsc)); /* Add an ordering constraint between every element in set1 and the * pseudo action. If any action in set1 is runnable the pseudo * action will be runnable. */ pcmk__new_ordering(rsc_1, pcmk__op_key(rsc_1->id, action_1, 0), NULL, NULL, NULL, unordered_action, pcmk__ar_min_runnable |pcmk__ar_first_implies_then_graphed, scheduler); } for (xml_rsc_2 = pcmk__xe_first_child(set2, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc_2 != NULL; xml_rsc_2 = pcmk__xe_next_same(xml_rsc_2)) { EXPAND_CONSTRAINT_IDREF(id, rsc_2, pcmk__xe_id(xml_rsc_2)); /* Add an ordering constraint between the pseudo-action and every * element in set2. If the pseudo-action is runnable, every action * in set2 will be runnable. */ pcmk__new_ordering(NULL, NULL, unordered_action, rsc_2, pcmk__op_key(rsc_2->id, action_2, 0), NULL, flags|pcmk__ar_unrunnable_first_blocks, scheduler); } return pcmk_rc_ok; } if (pcmk__xe_attr_is_true(set1, PCMK_XA_SEQUENTIAL)) { if (symmetry == ordering_symmetric_inverse) { // Get the first one xml_rsc = pcmk__xe_first_child(set1, PCMK_XE_RESOURCE_REF, NULL, NULL); if (xml_rsc != NULL) { EXPAND_CONSTRAINT_IDREF(id, rsc_1, pcmk__xe_id(xml_rsc)); } } else { // Get the last one const char *rid = NULL; for (xml_rsc = pcmk__xe_first_child(set1, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { rid = pcmk__xe_id(xml_rsc); } EXPAND_CONSTRAINT_IDREF(id, rsc_1, rid); } } if (pcmk__xe_attr_is_true(set2, PCMK_XA_SEQUENTIAL)) { if (symmetry == ordering_symmetric_inverse) { // Get the last one const char *rid = NULL; for (xml_rsc = pcmk__xe_first_child(set2, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { rid = pcmk__xe_id(xml_rsc); } EXPAND_CONSTRAINT_IDREF(id, rsc_2, rid); } else { // Get the first one xml_rsc = pcmk__xe_first_child(set2, PCMK_XE_RESOURCE_REF, NULL, NULL); if (xml_rsc != NULL) { EXPAND_CONSTRAINT_IDREF(id, rsc_2, pcmk__xe_id(xml_rsc)); } } } if ((rsc_1 != NULL) && (rsc_2 != NULL)) { pcmk__order_resource_actions(rsc_1, action_1, rsc_2, action_2, flags); } else if (rsc_1 != NULL) { for (xml_rsc = pcmk__xe_first_child(set2, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { EXPAND_CONSTRAINT_IDREF(id, rsc_2, pcmk__xe_id(xml_rsc)); pcmk__order_resource_actions(rsc_1, action_1, rsc_2, action_2, flags); } } else if (rsc_2 != NULL) { for (xml_rsc = pcmk__xe_first_child(set1, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { EXPAND_CONSTRAINT_IDREF(id, rsc_1, pcmk__xe_id(xml_rsc)); pcmk__order_resource_actions(rsc_1, action_1, rsc_2, action_2, flags); } } else { for (xml_rsc = pcmk__xe_first_child(set1, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { EXPAND_CONSTRAINT_IDREF(id, rsc_1, pcmk__xe_id(xml_rsc)); for (xmlNode *xml_rsc_2 = pcmk__xe_first_child(set2, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc_2 != NULL; xml_rsc_2 = pcmk__xe_next_same(xml_rsc_2)) { EXPAND_CONSTRAINT_IDREF(id, rsc_2, pcmk__xe_id(xml_rsc_2)); pcmk__order_resource_actions(rsc_1, action_1, rsc_2, action_2, flags); } } } return pcmk_rc_ok; } /*! * \internal * \brief If an ordering constraint uses resource tags, expand them * * \param[in,out] xml_obj Ordering constraint XML * \param[out] expanded_xml Equivalent XML with tags expanded * \param[in] scheduler Scheduler data * * \return Standard Pacemaker return code (specifically, pcmk_rc_ok on success, * and pcmk_rc_unpack_error on invalid configuration) */ static int unpack_order_tags(xmlNode *xml_obj, xmlNode **expanded_xml, const pcmk_scheduler_t *scheduler) { const char *id_first = NULL; const char *id_then = NULL; const char *action_first = NULL; const char *action_then = NULL; pcmk_resource_t *rsc_first = NULL; pcmk_resource_t *rsc_then = NULL; pcmk_tag_t *tag_first = NULL; pcmk_tag_t *tag_then = NULL; xmlNode *rsc_set_first = NULL; xmlNode *rsc_set_then = NULL; bool any_sets = false; // Check whether there are any resource sets with template or tag references *expanded_xml = pcmk__expand_tags_in_sets(xml_obj, scheduler); if (*expanded_xml != NULL) { crm_log_xml_trace(*expanded_xml, "Expanded " PCMK_XE_RSC_ORDER); return pcmk_rc_ok; } id_first = crm_element_value(xml_obj, PCMK_XA_FIRST); id_then = crm_element_value(xml_obj, PCMK_XA_THEN); if ((id_first == NULL) || (id_then == NULL)) { return pcmk_rc_ok; } if (!pcmk__valid_resource_or_tag(scheduler, id_first, &rsc_first, &tag_first)) { pcmk__config_err("Ignoring constraint '%s' because '%s' is not a " "valid resource or tag", pcmk__xe_id(xml_obj), id_first); return pcmk_rc_unpack_error; } if (!pcmk__valid_resource_or_tag(scheduler, id_then, &rsc_then, &tag_then)) { pcmk__config_err("Ignoring constraint '%s' because '%s' is not a " "valid resource or tag", pcmk__xe_id(xml_obj), id_then); return pcmk_rc_unpack_error; } if ((rsc_first != NULL) && (rsc_then != NULL)) { // Neither side references a template or tag return pcmk_rc_ok; } action_first = crm_element_value(xml_obj, PCMK_XA_FIRST_ACTION); action_then = crm_element_value(xml_obj, PCMK_XA_THEN_ACTION); *expanded_xml = pcmk__xml_copy(NULL, xml_obj); /* Convert template/tag reference in PCMK_XA_FIRST into constraint * PCMK_XE_RESOURCE_SET */ if (!pcmk__tag_to_set(*expanded_xml, &rsc_set_first, PCMK_XA_FIRST, true, scheduler)) { pcmk__xml_free(*expanded_xml); *expanded_xml = NULL; return pcmk_rc_unpack_error; } if (rsc_set_first != NULL) { if (action_first != NULL) { /* Move PCMK_XA_FIRST_ACTION into converted PCMK_XE_RESOURCE_SET as * PCMK_XA_ACTION */ crm_xml_add(rsc_set_first, PCMK_XA_ACTION, action_first); pcmk__xe_remove_attr(*expanded_xml, PCMK_XA_FIRST_ACTION); } any_sets = true; } /* Convert template/tag reference in PCMK_XA_THEN into constraint * PCMK_XE_RESOURCE_SET */ if (!pcmk__tag_to_set(*expanded_xml, &rsc_set_then, PCMK_XA_THEN, true, scheduler)) { pcmk__xml_free(*expanded_xml); *expanded_xml = NULL; return pcmk_rc_unpack_error; } if (rsc_set_then != NULL) { if (action_then != NULL) { /* Move PCMK_XA_THEN_ACTION into converted PCMK_XE_RESOURCE_SET as * PCMK_XA_ACTION */ crm_xml_add(rsc_set_then, PCMK_XA_ACTION, action_then); pcmk__xe_remove_attr(*expanded_xml, PCMK_XA_THEN_ACTION); } any_sets = true; } if (any_sets) { crm_log_xml_trace(*expanded_xml, "Expanded " PCMK_XE_RSC_ORDER); } else { pcmk__xml_free(*expanded_xml); *expanded_xml = NULL; } return pcmk_rc_ok; } /*! * \internal * \brief Unpack ordering constraint XML * * \param[in,out] xml_obj Ordering constraint XML to unpack * \param[in,out] scheduler Scheduler data */ void pcmk__unpack_ordering(xmlNode *xml_obj, pcmk_scheduler_t *scheduler) { xmlNode *set = NULL; xmlNode *last = NULL; xmlNode *orig_xml = NULL; xmlNode *expanded_xml = NULL; const char *id = crm_element_value(xml_obj, PCMK_XA_ID); const char *invert = crm_element_value(xml_obj, PCMK_XA_SYMMETRICAL); enum pe_order_kind kind = get_ordering_type(xml_obj); enum ordering_symmetry symmetry = get_ordering_symmetry(xml_obj, kind, NULL); // Expand any resource tags in the constraint XML if (unpack_order_tags(xml_obj, &expanded_xml, scheduler) != pcmk_rc_ok) { return; } if (expanded_xml != NULL) { orig_xml = xml_obj; xml_obj = expanded_xml; } // If the constraint has resource sets, unpack them for (set = pcmk__xe_first_child(xml_obj, PCMK_XE_RESOURCE_SET, NULL, NULL); set != NULL; set = pcmk__xe_next_same(set)) { - set = expand_idref(set, scheduler->input); + set = pcmk__xe_resolve_idref(set, scheduler->input); if ((set == NULL) // Configuration error, message already logged || (unpack_order_set(set, kind, invert, scheduler) != pcmk_rc_ok)) { if (expanded_xml != NULL) { pcmk__xml_free(expanded_xml); } return; } if (last != NULL) { if (order_rsc_sets(id, last, set, kind, scheduler, symmetry) != pcmk_rc_ok) { if (expanded_xml != NULL) { pcmk__xml_free(expanded_xml); } return; } if ((symmetry == ordering_symmetric) && (order_rsc_sets(id, set, last, kind, scheduler, ordering_symmetric_inverse) != pcmk_rc_ok)) { if (expanded_xml != NULL) { pcmk__xml_free(expanded_xml); } return; } } last = set; } if (expanded_xml) { pcmk__xml_free(expanded_xml); xml_obj = orig_xml; } // If the constraint has no resource sets, unpack it as a simple ordering if (last == NULL) { return unpack_simple_rsc_order(xml_obj, scheduler); } } static bool ordering_is_invalid(pcmk_action_t *action, pcmk__related_action_t *input) { /* Prevent user-defined ordering constraints between resources * running in a guest node and the resource that defines that node. */ if (!pcmk_is_set(input->type, pcmk__ar_guest_allowed) && (input->action->rsc != NULL) && pcmk__rsc_corresponds_to_guest(action->rsc, input->action->node)) { pcmk__config_warn("Invalid ordering constraint between %s and %s", input->action->rsc->id, action->rsc->id); return true; } /* If there's an order like * "rscB_stop node2"-> "load_stopped_node2" -> "rscA_migrate_to node1" * * then rscA is being migrated from node1 to node2, while rscB is being * migrated from node2 to node1. If there would be a graph loop, * break the order "load_stopped_node2" -> "rscA_migrate_to node1". */ if (((uint32_t) input->type == pcmk__ar_if_on_same_node_or_target) && (action->rsc != NULL) && pcmk__str_eq(action->task, PCMK_ACTION_MIGRATE_TO, pcmk__str_none) && pcmk__graph_has_loop(action, action, input)) { return true; } return false; } void pcmk__disable_invalid_orderings(pcmk_scheduler_t *scheduler) { for (GList *iter = scheduler->actions; iter != NULL; iter = iter->next) { pcmk_action_t *action = (pcmk_action_t *) iter->data; pcmk__related_action_t *input = NULL; for (GList *input_iter = action->actions_before; input_iter != NULL; input_iter = input_iter->next) { input = input_iter->data; if (ordering_is_invalid(action, input)) { input->type = (enum pe_ordering) pcmk__ar_none; } } } } /*! * \internal * \brief Order stops on a node before the node's shutdown * * \param[in,out] node Node being shut down * \param[in] shutdown_op Shutdown action for node */ void pcmk__order_stops_before_shutdown(pcmk_node_t *node, pcmk_action_t *shutdown_op) { for (GList *iter = node->details->data_set->actions; iter != NULL; iter = iter->next) { pcmk_action_t *action = (pcmk_action_t *) iter->data; // Only stops on the node shutting down are relevant if (!pcmk__same_node(action->node, node) || !pcmk__str_eq(action->task, PCMK_ACTION_STOP, pcmk__str_none)) { continue; } // Resources and nodes in maintenance mode won't be touched if (pcmk_is_set(action->rsc->flags, pcmk_rsc_maintenance)) { pcmk__rsc_trace(action->rsc, "Not ordering %s before shutdown of %s because " "resource in maintenance mode", action->uuid, pcmk__node_name(node)); continue; } else if (node->details->maintenance) { pcmk__rsc_trace(action->rsc, "Not ordering %s before shutdown of %s because " "node in maintenance mode", action->uuid, pcmk__node_name(node)); continue; } /* Don't touch a resource that is unmanaged or blocked, to avoid * blocking the shutdown (though if another action depends on this one, * we may still end up blocking) */ if (!pcmk_any_flags_set(action->rsc->flags, pcmk_rsc_managed|pcmk_rsc_blocked)) { pcmk__rsc_trace(action->rsc, "Not ordering %s before shutdown of %s because " "resource is unmanaged or blocked", action->uuid, pcmk__node_name(node)); continue; } pcmk__rsc_trace(action->rsc, "Ordering %s before shutdown of %s", action->uuid, pcmk__node_name(node)); pcmk__clear_action_flags(action, pcmk_action_optional); pcmk__new_ordering(action->rsc, NULL, action, NULL, strdup(PCMK_ACTION_DO_SHUTDOWN), shutdown_op, pcmk__ar_ordered|pcmk__ar_unrunnable_first_blocks, node->details->data_set); } } /*! * \brief Find resource actions matching directly or as child * * \param[in] rsc Resource to check * \param[in] original_key Action key to search for (possibly referencing * parent of \rsc) * * \return Newly allocated list of matching actions * \note It is the caller's responsibility to free the result with g_list_free() */ static GList * find_actions_by_task(const pcmk_resource_t *rsc, const char *original_key) { // Search under given task key directly GList *list = find_actions(rsc->actions, original_key, NULL); if (list == NULL) { // Search again using this resource's ID char *key = NULL; char *task = NULL; guint interval_ms = 0; CRM_CHECK(parse_op_key(original_key, NULL, &task, &interval_ms), return NULL); key = pcmk__op_key(rsc->id, task, interval_ms); list = find_actions(rsc->actions, key, NULL); free(key); free(task); } return list; } /*! * \internal * \brief Order relevant resource actions after a given action * * \param[in,out] first_action Action to order after (or NULL if none runnable) * \param[in] rsc Resource whose actions should be ordered * \param[in,out] order Ordering constraint being applied */ static void order_resource_actions_after(pcmk_action_t *first_action, const pcmk_resource_t *rsc, pcmk__action_relation_t *order) { GList *then_actions = NULL; uint32_t flags = pcmk__ar_none; CRM_CHECK((rsc != NULL) && (order != NULL), return); flags = order->flags; pcmk__rsc_trace(rsc, "Applying ordering %d for 'then' resource %s", order->id, rsc->id); if (order->action2 != NULL) { then_actions = g_list_prepend(NULL, order->action2); } else { then_actions = find_actions_by_task(rsc, order->task2); } if (then_actions == NULL) { pcmk__rsc_trace(rsc, "Ignoring ordering %d: no %s actions found for %s", order->id, order->task2, rsc->id); return; } if ((first_action != NULL) && (first_action->rsc == rsc) && pcmk_is_set(first_action->flags, pcmk_action_migration_abort)) { pcmk__rsc_trace(rsc, "Detected dangling migration ordering (%s then %s %s)", first_action->uuid, order->task2, rsc->id); pcmk__clear_relation_flags(flags, pcmk__ar_first_implies_then); } if ((first_action == NULL) && !pcmk_is_set(flags, pcmk__ar_first_implies_then)) { pcmk__rsc_debug(rsc, "Ignoring ordering %d for %s: No first action found", order->id, rsc->id); g_list_free(then_actions); return; } for (GList *iter = then_actions; iter != NULL; iter = iter->next) { pcmk_action_t *then_action_iter = (pcmk_action_t *) iter->data; if (first_action != NULL) { order_actions(first_action, then_action_iter, flags); } else { pcmk__clear_action_flags(then_action_iter, pcmk_action_runnable); crm_warn("%s of %s is unrunnable because there is no %s of %s " "to order it after", then_action_iter->task, rsc->id, order->task1, order->rsc1->id); } } g_list_free(then_actions); } static void rsc_order_first(pcmk_resource_t *first_rsc, pcmk__action_relation_t *order) { GList *first_actions = NULL; pcmk_action_t *first_action = order->action1; pcmk_resource_t *then_rsc = order->rsc2; CRM_ASSERT(first_rsc != NULL); pcmk__rsc_trace(first_rsc, "Applying ordering constraint %d (first: %s)", order->id, first_rsc->id); if (first_action != NULL) { first_actions = g_list_prepend(NULL, first_action); } else { first_actions = find_actions_by_task(first_rsc, order->task1); } if ((first_actions == NULL) && (first_rsc == then_rsc)) { pcmk__rsc_trace(first_rsc, "Ignoring constraint %d: first (%s for %s) not found", order->id, order->task1, first_rsc->id); } else if (first_actions == NULL) { char *key = NULL; char *op_type = NULL; guint interval_ms = 0; parse_op_key(order->task1, NULL, &op_type, &interval_ms); key = pcmk__op_key(first_rsc->id, op_type, interval_ms); if ((first_rsc->fns->state(first_rsc, TRUE) == pcmk_role_stopped) && pcmk__str_eq(op_type, PCMK_ACTION_STOP, pcmk__str_none)) { free(key); pcmk__rsc_trace(first_rsc, "Ignoring constraint %d: first (%s for %s) " "not found", order->id, order->task1, first_rsc->id); } else if ((first_rsc->fns->state(first_rsc, TRUE) == pcmk_role_unpromoted) && pcmk__str_eq(op_type, PCMK_ACTION_DEMOTE, pcmk__str_none)) { free(key); pcmk__rsc_trace(first_rsc, "Ignoring constraint %d: first (%s for %s) " "not found", order->id, order->task1, first_rsc->id); } else { pcmk__rsc_trace(first_rsc, "Creating first (%s for %s) for constraint %d ", order->task1, first_rsc->id, order->id); first_action = custom_action(first_rsc, key, op_type, NULL, TRUE, first_rsc->cluster); first_actions = g_list_prepend(NULL, first_action); } free(op_type); } if (then_rsc == NULL) { if (order->action2 == NULL) { pcmk__rsc_trace(first_rsc, "Ignoring constraint %d: then not found", order->id); return; } then_rsc = order->action2->rsc; } for (GList *iter = first_actions; iter != NULL; iter = iter->next) { first_action = iter->data; if (then_rsc == NULL) { order_actions(first_action, order->action2, order->flags); } else { order_resource_actions_after(first_action, then_rsc, order); } } g_list_free(first_actions); } // GFunc to call pcmk__block_colocation_dependents() static void block_colocation_dependents(gpointer data, gpointer user_data) { pcmk__block_colocation_dependents(data); } // GFunc to call pcmk__update_action_for_orderings() static void update_action_for_orderings(gpointer data, gpointer user_data) { pcmk__update_action_for_orderings((pcmk_action_t *) data, (pcmk_scheduler_t *) user_data); } /*! * \internal * \brief Apply all ordering constraints * * \param[in,out] sched Scheduler data */ void pcmk__apply_orderings(pcmk_scheduler_t *sched) { crm_trace("Applying ordering constraints"); /* Ordering constraints need to be processed in the order they were created. * rsc_order_first() and order_resource_actions_after() require the relevant * actions to already exist in some cases, but rsc_order_first() will create * the 'first' action in certain cases. Thus calling rsc_order_first() can * change the behavior of later-created orderings. * * Also, g_list_append() should be avoided for performance reasons, so we * prepend orderings when creating them and reverse the list here. * * @TODO This is brittle and should be carefully redesigned so that the * order of creation doesn't matter, and the reverse becomes unneeded. */ sched->ordering_constraints = g_list_reverse(sched->ordering_constraints); for (GList *iter = sched->ordering_constraints; iter != NULL; iter = iter->next) { pcmk__action_relation_t *order = iter->data; pcmk_resource_t *rsc = order->rsc1; if (rsc != NULL) { rsc_order_first(rsc, order); continue; } rsc = order->rsc2; if (rsc != NULL) { order_resource_actions_after(order->action1, rsc, order); } else { crm_trace("Applying ordering constraint %d (non-resource actions)", order->id); order_actions(order->action1, order->action2, order->flags); } } g_list_foreach(sched->actions, block_colocation_dependents, NULL); crm_trace("Ordering probes"); pcmk__order_probes(sched); crm_trace("Updating %d actions", g_list_length(sched->actions)); g_list_foreach(sched->actions, update_action_for_orderings, sched); pcmk__disable_invalid_orderings(sched); } /*! * \internal * \brief Order a given action after each action in a given list * * \param[in,out] after "After" action * \param[in,out] list List of "before" actions */ void pcmk__order_after_each(pcmk_action_t *after, GList *list) { const char *after_desc = (after->task == NULL)? after->uuid : after->task; for (GList *iter = list; iter != NULL; iter = iter->next) { pcmk_action_t *before = (pcmk_action_t *) iter->data; const char *before_desc = before->task? before->task : before->uuid; crm_debug("Ordering %s on %s before %s on %s", before_desc, pcmk__node_name(before->node), after_desc, pcmk__node_name(after->node)); order_actions(before, after, pcmk__ar_ordered); } } /*! * \internal * \brief Order promotions and demotions for restarts of a clone or bundle * * \param[in,out] rsc Clone or bundle to order */ void pcmk__promotable_restart_ordering(pcmk_resource_t *rsc) { // Order start and promote after all instances are stopped pcmk__order_resource_actions(rsc, PCMK_ACTION_STOPPED, rsc, PCMK_ACTION_START, pcmk__ar_ordered); pcmk__order_resource_actions(rsc, PCMK_ACTION_STOPPED, rsc, PCMK_ACTION_PROMOTE, pcmk__ar_ordered); // Order stop, start, and promote after all instances are demoted pcmk__order_resource_actions(rsc, PCMK_ACTION_DEMOTED, rsc, PCMK_ACTION_STOP, pcmk__ar_ordered); pcmk__order_resource_actions(rsc, PCMK_ACTION_DEMOTED, rsc, PCMK_ACTION_START, pcmk__ar_ordered); pcmk__order_resource_actions(rsc, PCMK_ACTION_DEMOTED, rsc, PCMK_ACTION_PROMOTE, pcmk__ar_ordered); // Order promote after all instances are started pcmk__order_resource_actions(rsc, PCMK_ACTION_RUNNING, rsc, PCMK_ACTION_PROMOTE, pcmk__ar_ordered); // Order demote after all instances are demoted pcmk__order_resource_actions(rsc, PCMK_ACTION_DEMOTE, rsc, PCMK_ACTION_DEMOTED, pcmk__ar_ordered); } diff --git a/lib/pacemaker/pcmk_sched_tickets.c b/lib/pacemaker/pcmk_sched_tickets.c index 41871b048c..c542bd2b5f 100644 --- a/lib/pacemaker/pcmk_sched_tickets.c +++ b/lib/pacemaker/pcmk_sched_tickets.c @@ -1,533 +1,533 @@ /* * 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 "libpacemaker_private.h" enum loss_ticket_policy { loss_ticket_stop, loss_ticket_demote, loss_ticket_fence, loss_ticket_freeze }; typedef struct { const char *id; pcmk_resource_t *rsc; pcmk_ticket_t *ticket; enum loss_ticket_policy loss_policy; int role; } rsc_ticket_t; /*! * \brief Check whether a ticket constraint matches a resource by role * * \param[in] rsc_ticket Ticket constraint * \param[in] rsc Resource to compare with ticket * * \param[in] true if constraint has no role or resource's role matches * constraint's, otherwise false */ static bool ticket_role_matches(const pcmk_resource_t *rsc, const rsc_ticket_t *rsc_ticket) { if ((rsc_ticket->role == pcmk_role_unknown) || (rsc_ticket->role == rsc->role)) { return true; } pcmk__rsc_trace(rsc, "Skipping constraint: \"%s\" state filter", pcmk_role_text(rsc_ticket->role)); return false; } /*! * \brief Create location constraints and fencing as needed for a ticket * * \param[in,out] rsc Resource affected by ticket * \param[in] rsc_ticket Ticket */ static void constraints_for_ticket(pcmk_resource_t *rsc, const rsc_ticket_t *rsc_ticket) { GList *iter = NULL; CRM_CHECK((rsc != NULL) && (rsc_ticket != NULL), return); if (rsc_ticket->ticket->granted && !rsc_ticket->ticket->standby) { return; } if (rsc->children) { pcmk__rsc_trace(rsc, "Processing ticket dependencies from %s", rsc->id); for (iter = rsc->children; iter != NULL; iter = iter->next) { constraints_for_ticket((pcmk_resource_t *) iter->data, rsc_ticket); } return; } pcmk__rsc_trace(rsc, "%s: Processing ticket dependency on %s (%s, %s)", rsc->id, rsc_ticket->ticket->id, rsc_ticket->id, pcmk_role_text(rsc_ticket->role)); if (!rsc_ticket->ticket->granted && (rsc->running_on != NULL)) { switch (rsc_ticket->loss_policy) { case loss_ticket_stop: resource_location(rsc, NULL, -PCMK_SCORE_INFINITY, "__loss_of_ticket__", rsc->cluster); break; case loss_ticket_demote: // Promotion score will be set to -INFINITY in promotion_order() if (rsc_ticket->role != pcmk_role_promoted) { resource_location(rsc, NULL, -PCMK_SCORE_INFINITY, "__loss_of_ticket__", rsc->cluster); } break; case loss_ticket_fence: if (!ticket_role_matches(rsc, rsc_ticket)) { return; } resource_location(rsc, NULL, -PCMK_SCORE_INFINITY, "__loss_of_ticket__", rsc->cluster); for (iter = rsc->running_on; iter != NULL; iter = iter->next) { pe_fence_node(rsc->cluster, (pcmk_node_t *) iter->data, "deadman ticket was lost", FALSE); } break; case loss_ticket_freeze: if (!ticket_role_matches(rsc, rsc_ticket)) { return; } if (rsc->running_on != NULL) { pcmk__clear_rsc_flags(rsc, pcmk_rsc_managed); pcmk__set_rsc_flags(rsc, pcmk_rsc_blocked); } break; } } else if (!rsc_ticket->ticket->granted) { if ((rsc_ticket->role != pcmk_role_promoted) || (rsc_ticket->loss_policy == loss_ticket_stop)) { resource_location(rsc, NULL, -PCMK_SCORE_INFINITY, "__no_ticket__", rsc->cluster); } } else if (rsc_ticket->ticket->standby) { if ((rsc_ticket->role != pcmk_role_promoted) || (rsc_ticket->loss_policy == loss_ticket_stop)) { resource_location(rsc, NULL, -PCMK_SCORE_INFINITY, "__ticket_standby__", rsc->cluster); } } } static void rsc_ticket_new(const char *id, pcmk_resource_t *rsc, pcmk_ticket_t *ticket, const char *state, const char *loss_policy) { rsc_ticket_t *new_rsc_ticket = NULL; if (rsc == NULL) { pcmk__config_err("Ignoring ticket '%s' because resource " "does not exist", id); return; } new_rsc_ticket = calloc(1, sizeof(rsc_ticket_t)); if (new_rsc_ticket == NULL) { return; } if (pcmk__str_eq(state, PCMK_ROLE_STARTED, pcmk__str_null_matches|pcmk__str_casei)) { state = PCMK__ROLE_UNKNOWN; } new_rsc_ticket->id = id; new_rsc_ticket->ticket = ticket; new_rsc_ticket->rsc = rsc; new_rsc_ticket->role = pcmk_parse_role(state); if (pcmk__str_eq(loss_policy, PCMK_VALUE_FENCE, pcmk__str_casei)) { if (pcmk_is_set(rsc->cluster->flags, pcmk_sched_fencing_enabled)) { new_rsc_ticket->loss_policy = loss_ticket_fence; } else { pcmk__config_err("Resetting '" PCMK_XA_LOSS_POLICY "' " "for ticket '%s' to '" PCMK_VALUE_STOP "' " "because fencing is not configured", ticket->id); loss_policy = PCMK_VALUE_STOP; } } if (new_rsc_ticket->loss_policy == loss_ticket_fence) { crm_debug("On loss of ticket '%s': Fence the nodes running %s (%s)", new_rsc_ticket->ticket->id, new_rsc_ticket->rsc->id, pcmk_role_text(new_rsc_ticket->role)); } else if (pcmk__str_eq(loss_policy, PCMK_VALUE_FREEZE, pcmk__str_casei)) { crm_debug("On loss of ticket '%s': Freeze %s (%s)", new_rsc_ticket->ticket->id, new_rsc_ticket->rsc->id, pcmk_role_text(new_rsc_ticket->role)); new_rsc_ticket->loss_policy = loss_ticket_freeze; } else if (pcmk__str_eq(loss_policy, PCMK_VALUE_DEMOTE, pcmk__str_casei)) { crm_debug("On loss of ticket '%s': Demote %s (%s)", new_rsc_ticket->ticket->id, new_rsc_ticket->rsc->id, pcmk_role_text(new_rsc_ticket->role)); new_rsc_ticket->loss_policy = loss_ticket_demote; } else if (pcmk__str_eq(loss_policy, PCMK_VALUE_STOP, pcmk__str_casei)) { crm_debug("On loss of ticket '%s': Stop %s (%s)", new_rsc_ticket->ticket->id, new_rsc_ticket->rsc->id, pcmk_role_text(new_rsc_ticket->role)); new_rsc_ticket->loss_policy = loss_ticket_stop; } else { if (new_rsc_ticket->role == pcmk_role_promoted) { crm_debug("On loss of ticket '%s': Default to demote %s (%s)", new_rsc_ticket->ticket->id, new_rsc_ticket->rsc->id, pcmk_role_text(new_rsc_ticket->role)); new_rsc_ticket->loss_policy = loss_ticket_demote; } else { crm_debug("On loss of ticket '%s': Default to stop %s (%s)", new_rsc_ticket->ticket->id, new_rsc_ticket->rsc->id, pcmk_role_text(new_rsc_ticket->role)); new_rsc_ticket->loss_policy = loss_ticket_stop; } } pcmk__rsc_trace(rsc, "%s (%s) ==> %s", rsc->id, pcmk_role_text(new_rsc_ticket->role), ticket->id); rsc->rsc_tickets = g_list_append(rsc->rsc_tickets, new_rsc_ticket); rsc->cluster->ticket_constraints = g_list_append( rsc->cluster->ticket_constraints, new_rsc_ticket); if (!(new_rsc_ticket->ticket->granted) || new_rsc_ticket->ticket->standby) { constraints_for_ticket(rsc, new_rsc_ticket); } } // \return Standard Pacemaker return code static int unpack_rsc_ticket_set(xmlNode *set, pcmk_ticket_t *ticket, const char *loss_policy, pcmk_scheduler_t *scheduler) { const char *set_id = NULL; const char *role = NULL; CRM_CHECK(set != NULL, return EINVAL); CRM_CHECK(ticket != NULL, return EINVAL); set_id = pcmk__xe_id(set); if (set_id == NULL) { pcmk__config_err("Ignoring <" PCMK_XE_RESOURCE_SET "> without " PCMK_XA_ID); return pcmk_rc_unpack_error; } role = crm_element_value(set, PCMK_XA_ROLE); for (xmlNode *xml_rsc = pcmk__xe_first_child(set, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { pcmk_resource_t *resource = NULL; resource = pcmk__find_constraint_resource(scheduler->resources, pcmk__xe_id(xml_rsc)); if (resource == NULL) { pcmk__config_err("%s: No resource found for %s", set_id, pcmk__xe_id(xml_rsc)); return pcmk_rc_unpack_error; } pcmk__rsc_trace(resource, "Resource '%s' depends on ticket '%s'", resource->id, ticket->id); rsc_ticket_new(set_id, resource, ticket, role, loss_policy); } return pcmk_rc_ok; } static void unpack_simple_rsc_ticket(xmlNode *xml_obj, pcmk_scheduler_t *scheduler) { const char *id = NULL; const char *ticket_str = crm_element_value(xml_obj, PCMK_XA_TICKET); const char *loss_policy = crm_element_value(xml_obj, PCMK_XA_LOSS_POLICY); pcmk_ticket_t *ticket = NULL; const char *rsc_id = crm_element_value(xml_obj, PCMK_XA_RSC); const char *state = crm_element_value(xml_obj, PCMK_XA_RSC_ROLE); // @COMPAT: Deprecated since 2.1.5 const char *instance = crm_element_value(xml_obj, PCMK__XA_RSC_INSTANCE); pcmk_resource_t *rsc = NULL; if (instance != NULL) { pcmk__warn_once(pcmk__wo_coloc_inst, "Support for " PCMK__XA_RSC_INSTANCE " is deprecated " "and will be removed in a future release"); } CRM_CHECK(xml_obj != NULL, return); id = pcmk__xe_id(xml_obj); if (id == NULL) { pcmk__config_err("Ignoring <%s> constraint without " PCMK_XA_ID, xml_obj->name); return; } if (ticket_str == NULL) { pcmk__config_err("Ignoring constraint '%s' without ticket specified", id); return; } else { ticket = g_hash_table_lookup(scheduler->tickets, ticket_str); } if (ticket == NULL) { pcmk__config_err("Ignoring constraint '%s' because ticket '%s' " "does not exist", id, ticket_str); return; } if (rsc_id == NULL) { pcmk__config_err("Ignoring constraint '%s' without resource", id); return; } else { rsc = pcmk__find_constraint_resource(scheduler->resources, rsc_id); } if (rsc == NULL) { pcmk__config_err("Ignoring constraint '%s' because resource '%s' " "does not exist", id, rsc_id); return; } else if ((instance != NULL) && !pcmk__is_clone(rsc)) { pcmk__config_err("Ignoring constraint '%s' because resource '%s' " "is not a clone but instance '%s' was requested", id, rsc_id, instance); return; } if (instance != NULL) { rsc = find_clone_instance(rsc, instance); if (rsc == NULL) { pcmk__config_warn("Ignoring constraint '%s' because resource '%s' " "does not have an instance '%s'", "'%s'", id, rsc_id, instance); return; } } rsc_ticket_new(id, rsc, ticket, state, loss_policy); } // \return Standard Pacemaker return code static int unpack_rsc_ticket_tags(xmlNode *xml_obj, xmlNode **expanded_xml, pcmk_scheduler_t *scheduler) { const char *id = NULL; const char *rsc_id = NULL; const char *state = NULL; pcmk_resource_t *rsc = NULL; pcmk_tag_t *tag = NULL; xmlNode *rsc_set = NULL; *expanded_xml = NULL; CRM_CHECK(xml_obj != NULL, return EINVAL); id = pcmk__xe_id(xml_obj); if (id == NULL) { pcmk__config_err("Ignoring <%s> constraint without " PCMK_XA_ID, xml_obj->name); return pcmk_rc_unpack_error; } // Check whether there are any resource sets with template or tag references *expanded_xml = pcmk__expand_tags_in_sets(xml_obj, scheduler); if (*expanded_xml != NULL) { crm_log_xml_trace(*expanded_xml, "Expanded rsc_ticket"); return pcmk_rc_ok; } rsc_id = crm_element_value(xml_obj, PCMK_XA_RSC); if (rsc_id == NULL) { return pcmk_rc_ok; } if (!pcmk__valid_resource_or_tag(scheduler, rsc_id, &rsc, &tag)) { pcmk__config_err("Ignoring constraint '%s' because '%s' is not a " "valid resource or tag", id, rsc_id); return pcmk_rc_unpack_error; } else if (rsc != NULL) { // No template or tag is referenced return pcmk_rc_ok; } state = crm_element_value(xml_obj, PCMK_XA_RSC_ROLE); *expanded_xml = pcmk__xml_copy(NULL, xml_obj); /* Convert any template or tag reference in "rsc" into ticket * PCMK_XE_RESOURCE_SET */ if (!pcmk__tag_to_set(*expanded_xml, &rsc_set, PCMK_XA_RSC, false, scheduler)) { pcmk__xml_free(*expanded_xml); *expanded_xml = NULL; return pcmk_rc_unpack_error; } if (rsc_set != NULL) { if (state != NULL) { /* Move PCMK_XA_RSC_ROLE into converted PCMK_XE_RESOURCE_SET as a * PCMK_XA_ROLE attribute */ crm_xml_add(rsc_set, PCMK_XA_ROLE, state); pcmk__xe_remove_attr(*expanded_xml, PCMK_XA_RSC_ROLE); } } else { pcmk__xml_free(*expanded_xml); *expanded_xml = NULL; } return pcmk_rc_ok; } void pcmk__unpack_rsc_ticket(xmlNode *xml_obj, pcmk_scheduler_t *scheduler) { xmlNode *set = NULL; bool any_sets = false; const char *id = NULL; const char *ticket_str = NULL; pcmk_ticket_t *ticket = NULL; xmlNode *orig_xml = NULL; xmlNode *expanded_xml = NULL; CRM_CHECK(xml_obj != NULL, return); id = pcmk__xe_id(xml_obj); if (id == NULL) { pcmk__config_err("Ignoring <%s> constraint without " PCMK_XA_ID, xml_obj->name); return; } if (scheduler->tickets == NULL) { scheduler->tickets = pcmk__strkey_table(free, destroy_ticket); } ticket_str = crm_element_value(xml_obj, PCMK_XA_TICKET); if (ticket_str == NULL) { pcmk__config_err("Ignoring constraint '%s' without ticket", id); return; } else { ticket = g_hash_table_lookup(scheduler->tickets, ticket_str); } if (ticket == NULL) { ticket = ticket_new(ticket_str, scheduler); if (ticket == NULL) { return; } } if (unpack_rsc_ticket_tags(xml_obj, &expanded_xml, scheduler) != pcmk_rc_ok) { return; } if (expanded_xml != NULL) { orig_xml = xml_obj; xml_obj = expanded_xml; } for (set = pcmk__xe_first_child(xml_obj, PCMK_XE_RESOURCE_SET, NULL, NULL); set != NULL; set = pcmk__xe_next_same(set)) { const char *loss_policy = NULL; any_sets = true; - set = expand_idref(set, scheduler->input); + set = pcmk__xe_resolve_idref(set, scheduler->input); loss_policy = crm_element_value(xml_obj, PCMK_XA_LOSS_POLICY); if ((set == NULL) // Configuration error, message already logged || (unpack_rsc_ticket_set(set, ticket, loss_policy, scheduler) != pcmk_rc_ok)) { if (expanded_xml != NULL) { pcmk__xml_free(expanded_xml); } return; } } if (expanded_xml) { pcmk__xml_free(expanded_xml); xml_obj = orig_xml; } if (!any_sets) { unpack_simple_rsc_ticket(xml_obj, scheduler); } } /*! * \internal * \brief Ban resource from a node if it doesn't have a promotion ticket * * If a resource has tickets for the promoted role, and the ticket is either not * granted or set to standby, then ban the resource from all nodes. * * \param[in,out] rsc Resource to check */ void pcmk__require_promotion_tickets(pcmk_resource_t *rsc) { for (GList *item = rsc->rsc_tickets; item != NULL; item = item->next) { rsc_ticket_t *rsc_ticket = (rsc_ticket_t *) item->data; if ((rsc_ticket->role == pcmk_role_promoted) && (!rsc_ticket->ticket->granted || rsc_ticket->ticket->standby)) { resource_location(rsc, NULL, -PCMK_SCORE_INFINITY, "__stateful_without_ticket__", rsc->cluster); } } } diff --git a/lib/pengine/complex.c b/lib/pengine/complex.c index 974b25ead6..c33c3d7aba 100644 --- a/lib/pengine/complex.c +++ b/lib/pengine/complex.c @@ -1,1262 +1,1262 @@ /* * 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 "pe_status_private.h" void populate_hash(xmlNode * nvpair_list, GHashTable * hash, const char **attrs, int attrs_length); static pcmk_node_t *active_node(const pcmk_resource_t *rsc, unsigned int *count_all, unsigned int *count_clean); pcmk_rsc_methods_t resource_class_functions[] = { { native_unpack, native_find_rsc, native_parameter, NULL, native_active, native_resource_state, native_location, native_free, pe__count_common, pe__native_is_filtered, active_node, pe__primitive_max_per_node, }, { group_unpack, native_find_rsc, native_parameter, NULL, group_active, group_resource_state, native_location, group_free, pe__count_common, pe__group_is_filtered, active_node, pe__group_max_per_node, }, { clone_unpack, native_find_rsc, native_parameter, NULL, clone_active, clone_resource_state, native_location, clone_free, pe__count_common, pe__clone_is_filtered, active_node, pe__clone_max_per_node, }, { pe__unpack_bundle, native_find_rsc, native_parameter, NULL, pe__bundle_active, pe__bundle_resource_state, native_location, pe__free_bundle, pe__count_bundle, pe__bundle_is_filtered, pe__bundle_active_node, pe__bundle_max_per_node, } }; static enum pe_obj_types get_resource_type(const char *name) { if (pcmk__str_eq(name, PCMK_XE_PRIMITIVE, pcmk__str_casei)) { return pcmk_rsc_variant_primitive; } else if (pcmk__str_eq(name, PCMK_XE_GROUP, pcmk__str_casei)) { return pcmk_rsc_variant_group; } else if (pcmk__str_eq(name, PCMK_XE_CLONE, pcmk__str_casei)) { return pcmk_rsc_variant_clone; } else if (pcmk__str_eq(name, PCMK__XE_PROMOTABLE_LEGACY, pcmk__str_casei)) { // @COMPAT deprecated since 2.0.0 return pcmk_rsc_variant_clone; } else if (pcmk__str_eq(name, PCMK_XE_BUNDLE, pcmk__str_casei)) { return pcmk_rsc_variant_bundle; } return pcmk_rsc_variant_unknown; } /*! * \internal * \brief Insert a meta-attribute if not already present * * \param[in] key Meta-attribute name * \param[in] value Meta-attribute value to add if not already present * \param[in,out] table Meta-attribute hash table to insert into * * \note This is like pcmk__insert_meta() except it won't overwrite existing * values. */ static void dup_attr(gpointer key, gpointer value, gpointer user_data) { GHashTable *table = user_data; CRM_CHECK((key != NULL) && (table != NULL), return); if (pcmk__str_eq((const char *) value, "#default", pcmk__str_casei)) { // @COMPAT Deprecated since 2.1.8 pcmk__config_warn("Support for setting meta-attributes (such as %s) to " "the explicit value '#default' is deprecated and " "will be removed in a future release", (const char *) key); } else if ((value != NULL) && (g_hash_table_lookup(table, key) == NULL)) { pcmk__insert_dup(table, (const char *) key, (const char *) value); } } static void expand_parents_fixed_nvpairs(pcmk_resource_t *rsc, pe_rule_eval_data_t *rule_data, GHashTable *meta_hash, pcmk_scheduler_t *scheduler) { GHashTable *parent_orig_meta = pcmk__strkey_table(free, free); pcmk_resource_t *p = rsc->parent; if (p == NULL) { return ; } /* Search all parent resources, get the fixed value of * PCMK_XE_META_ATTRIBUTES set only in the original xml, and stack it in the * hash table. The fixed value of the lower parent resource takes precedence * and is not overwritten. */ while(p != NULL) { /* A hash table for comparison is generated, including the id-ref. */ pe__unpack_dataset_nvpairs(p->xml, PCMK_XE_META_ATTRIBUTES, rule_data, parent_orig_meta, NULL, FALSE, scheduler); p = p->parent; } if (parent_orig_meta != NULL) { // This will not overwrite any values already existing for child g_hash_table_foreach(parent_orig_meta, dup_attr, meta_hash); } if (parent_orig_meta != NULL) { g_hash_table_destroy(parent_orig_meta); } return ; } void get_meta_attributes(GHashTable * meta_hash, pcmk_resource_t * rsc, pcmk_node_t *node, pcmk_scheduler_t *scheduler) { pe_rsc_eval_data_t rsc_rule_data = { .standard = crm_element_value(rsc->xml, PCMK_XA_CLASS), .provider = crm_element_value(rsc->xml, PCMK_XA_PROVIDER), .agent = crm_element_value(rsc->xml, PCMK_XA_TYPE) }; pe_rule_eval_data_t rule_data = { .node_hash = NULL, .now = scheduler->now, .match_data = NULL, .rsc_data = &rsc_rule_data, .op_data = NULL }; if (node) { /* @COMPAT Support for node attribute expressions in rules for * meta-attributes is deprecated. When we can break behavioral backward * compatibility, drop this block. */ rule_data.node_hash = node->details->attrs; } for (xmlAttrPtr a = pcmk__xe_first_attr(rsc->xml); a != NULL; a = a->next) { if (a->children != NULL) { dup_attr((gpointer) a->name, (gpointer) a->children->content, meta_hash); } } pe__unpack_dataset_nvpairs(rsc->xml, PCMK_XE_META_ATTRIBUTES, &rule_data, meta_hash, NULL, FALSE, scheduler); /* Set the PCMK_XE_META_ATTRIBUTES explicitly set in the parent resource to * the hash table of the child resource. If it is already explicitly set as * a child, it will not be overwritten. */ if (rsc->parent != NULL) { expand_parents_fixed_nvpairs(rsc, &rule_data, meta_hash, scheduler); } /* check the defaults */ pe__unpack_dataset_nvpairs(scheduler->rsc_defaults, PCMK_XE_META_ATTRIBUTES, &rule_data, meta_hash, NULL, FALSE, scheduler); /* If there is PCMK_XE_META_ATTRIBUTES that the parent resource has not * explicitly set, set a value that is not set from PCMK_XE_RSC_DEFAULTS * either. The values already set up to this point will not be overwritten. */ if (rsc->parent) { g_hash_table_foreach(rsc->parent->meta, dup_attr, meta_hash); } } void get_rsc_attributes(GHashTable *meta_hash, const pcmk_resource_t *rsc, const pcmk_node_t *node, pcmk_scheduler_t *scheduler) { pe_rule_eval_data_t rule_data = { .node_hash = NULL, .now = scheduler->now, .match_data = NULL, .rsc_data = NULL, .op_data = NULL }; if (node) { rule_data.node_hash = node->details->attrs; } pe__unpack_dataset_nvpairs(rsc->xml, PCMK_XE_INSTANCE_ATTRIBUTES, &rule_data, meta_hash, NULL, FALSE, scheduler); /* set anything else based on the parent */ if (rsc->parent != NULL) { get_rsc_attributes(meta_hash, rsc->parent, node, scheduler); } else { if (pcmk__xe_first_child(scheduler->rsc_defaults, PCMK_XE_INSTANCE_ATTRIBUTES, NULL, NULL) != NULL) { /* Not possible with schema validation enabled * * @COMPAT Drop support when we can break behavioral * backward compatibility */ pcmk__warn_once(pcmk__wo_instance_defaults, "Support for " PCMK_XE_INSTANCE_ATTRIBUTES " in " PCMK_XE_RSC_DEFAULTS " is deprecated and will be " "removed in a future release"); } /* and finally check the defaults */ pe__unpack_dataset_nvpairs(scheduler->rsc_defaults, PCMK_XE_INSTANCE_ATTRIBUTES, &rule_data, meta_hash, NULL, FALSE, scheduler); } } static char * template_op_key(xmlNode * op) { const char *name = crm_element_value(op, PCMK_XA_NAME); const char *role = crm_element_value(op, PCMK_XA_ROLE); char *key = NULL; if ((role == NULL) || pcmk__strcase_any_of(role, PCMK_ROLE_STARTED, PCMK_ROLE_UNPROMOTED, PCMK__ROLE_UNPROMOTED_LEGACY, NULL)) { role = PCMK__ROLE_UNKNOWN; } key = crm_strdup_printf("%s-%s", name, role); return key; } static gboolean unpack_template(xmlNode *xml_obj, xmlNode **expanded_xml, pcmk_scheduler_t *scheduler) { xmlNode *cib_resources = NULL; xmlNode *template = NULL; xmlNode *new_xml = NULL; xmlNode *child_xml = NULL; xmlNode *rsc_ops = NULL; xmlNode *template_ops = NULL; const char *template_ref = NULL; const char *id = NULL; if (xml_obj == NULL) { pcmk__config_err("No resource object for template unpacking"); return FALSE; } template_ref = crm_element_value(xml_obj, PCMK_XA_TEMPLATE); if (template_ref == NULL) { return TRUE; } id = pcmk__xe_id(xml_obj); if (id == NULL) { pcmk__config_err("'%s' object must have a id", xml_obj->name); return FALSE; } if (pcmk__str_eq(template_ref, id, pcmk__str_none)) { pcmk__config_err("The resource object '%s' should not reference itself", id); return FALSE; } cib_resources = get_xpath_object("//" PCMK_XE_RESOURCES, scheduler->input, LOG_TRACE); if (cib_resources == NULL) { pcmk__config_err("No resources configured"); return FALSE; } template = pcmk__xe_first_child(cib_resources, PCMK_XE_TEMPLATE, PCMK_XA_ID, template_ref); if (template == NULL) { pcmk__config_err("No template named '%s'", template_ref); return FALSE; } new_xml = pcmk__xml_copy(NULL, template); xmlNodeSetName(new_xml, xml_obj->name); crm_xml_add(new_xml, PCMK_XA_ID, id); crm_xml_add(new_xml, PCMK__META_CLONE, crm_element_value(xml_obj, PCMK__META_CLONE)); template_ops = pcmk__xe_first_child(new_xml, PCMK_XE_OPERATIONS, NULL, NULL); for (child_xml = pcmk__xe_first_child(xml_obj, NULL, NULL, NULL); child_xml != NULL; child_xml = pcmk__xe_next(child_xml)) { xmlNode *new_child = pcmk__xml_copy(new_xml, child_xml); if (pcmk__xe_is(new_child, PCMK_XE_OPERATIONS)) { rsc_ops = new_child; } } if (template_ops && rsc_ops) { xmlNode *op = NULL; GHashTable *rsc_ops_hash = pcmk__strkey_table(free, NULL); for (op = pcmk__xe_first_child(rsc_ops, NULL, NULL, NULL); op != NULL; op = pcmk__xe_next(op)) { char *key = template_op_key(op); g_hash_table_insert(rsc_ops_hash, key, op); } for (op = pcmk__xe_first_child(template_ops, NULL, NULL, NULL); op != NULL; op = pcmk__xe_next(op)) { char *key = template_op_key(op); if (g_hash_table_lookup(rsc_ops_hash, key) == NULL) { pcmk__xml_copy(rsc_ops, op); } free(key); } if (rsc_ops_hash) { g_hash_table_destroy(rsc_ops_hash); } pcmk__xml_free(template_ops); } /*pcmk__xml_free(*expanded_xml); */ *expanded_xml = new_xml; #if 0 /* Disable multi-level templates for now */ if (!unpack_template(new_xml, expanded_xml, scheduler)) { pcmk__xml_free(*expanded_xml); *expanded_xml = NULL; return FALSE; } #endif return TRUE; } static gboolean add_template_rsc(xmlNode *xml_obj, pcmk_scheduler_t *scheduler) { const char *template_ref = NULL; const char *id = NULL; if (xml_obj == NULL) { pcmk__config_err("No resource object for processing resource list " "of template"); return FALSE; } template_ref = crm_element_value(xml_obj, PCMK_XA_TEMPLATE); if (template_ref == NULL) { return TRUE; } id = pcmk__xe_id(xml_obj); if (id == NULL) { pcmk__config_err("'%s' object must have a id", xml_obj->name); return FALSE; } if (pcmk__str_eq(template_ref, id, pcmk__str_none)) { pcmk__config_err("The resource object '%s' should not reference itself", id); return FALSE; } if (add_tag_ref(scheduler->template_rsc_sets, template_ref, id) == FALSE) { return FALSE; } return TRUE; } static bool detect_promotable(pcmk_resource_t *rsc) { const char *promotable = g_hash_table_lookup(rsc->meta, PCMK_META_PROMOTABLE); if (crm_is_true(promotable)) { return TRUE; } // @COMPAT deprecated since 2.0.0 if (pcmk__xe_is(rsc->xml, PCMK__XE_PROMOTABLE_LEGACY)) { pcmk__warn_once(pcmk__wo_master_element, "Support for <" PCMK__XE_PROMOTABLE_LEGACY "> (such " "as in %s) is deprecated and will be removed in a " "future release. Use <" PCMK_XE_CLONE "> with a " PCMK_META_PROMOTABLE " meta-attribute instead.", rsc->id); pcmk__insert_dup(rsc->meta, PCMK_META_PROMOTABLE, PCMK_VALUE_TRUE); return TRUE; } return FALSE; } static void free_params_table(gpointer data) { g_hash_table_destroy((GHashTable *) data); } /*! * \brief Get a table of resource parameters * * \param[in,out] rsc Resource to query * \param[in] node Node for evaluating rules (NULL for defaults) * \param[in,out] scheduler Scheduler data * * \return Hash table containing resource parameter names and values * (or NULL if \p rsc or \p scheduler is NULL) * \note The returned table will be destroyed when the resource is freed, so * callers should not destroy it. */ GHashTable * pe_rsc_params(pcmk_resource_t *rsc, const pcmk_node_t *node, pcmk_scheduler_t *scheduler) { GHashTable *params_on_node = NULL; /* A NULL node is used to request the resource's default parameters * (not evaluated for node), but we always want something non-NULL * as a hash table key. */ const char *node_name = ""; // Sanity check if ((rsc == NULL) || (scheduler == NULL)) { return NULL; } if ((node != NULL) && (node->details->uname != NULL)) { node_name = node->details->uname; } // Find the parameter table for given node if (rsc->parameter_cache == NULL) { rsc->parameter_cache = pcmk__strikey_table(free, free_params_table); } else { params_on_node = g_hash_table_lookup(rsc->parameter_cache, node_name); } // If none exists yet, create one with parameters evaluated for node if (params_on_node == NULL) { params_on_node = pcmk__strkey_table(free, free); get_rsc_attributes(params_on_node, rsc, node, scheduler); g_hash_table_insert(rsc->parameter_cache, strdup(node_name), params_on_node); } return params_on_node; } /*! * \internal * \brief Unpack a resource's \c PCMK_META_REQUIRES meta-attribute * * \param[in,out] rsc Resource being unpacked * \param[in] value Value of \c PCMK_META_REQUIRES meta-attribute * \param[in] is_default Whether \p value was selected by default */ static void unpack_requires(pcmk_resource_t *rsc, const char *value, bool is_default) { if (pcmk__str_eq(value, PCMK_VALUE_NOTHING, pcmk__str_casei)) { } else if (pcmk__str_eq(value, PCMK_VALUE_QUORUM, pcmk__str_casei)) { pcmk__set_rsc_flags(rsc, pcmk_rsc_needs_quorum); } else if (pcmk__str_eq(value, PCMK_VALUE_FENCING, pcmk__str_casei)) { pcmk__set_rsc_flags(rsc, pcmk_rsc_needs_fencing); if (!pcmk_is_set(rsc->cluster->flags, pcmk_sched_fencing_enabled)) { pcmk__config_warn("%s requires fencing but fencing is disabled", rsc->id); } } else if (pcmk__str_eq(value, PCMK_VALUE_UNFENCING, pcmk__str_casei)) { if (pcmk_is_set(rsc->flags, pcmk_rsc_fence_device)) { pcmk__config_warn("Resetting \"" PCMK_META_REQUIRES "\" for %s " "to \"" PCMK_VALUE_QUORUM "\" because fencing " "devices cannot require unfencing", rsc->id); unpack_requires(rsc, PCMK_VALUE_QUORUM, true); return; } else if (!pcmk_is_set(rsc->cluster->flags, pcmk_sched_fencing_enabled)) { pcmk__config_warn("Resetting \"" PCMK_META_REQUIRES "\" for %s " "to \"" PCMK_VALUE_QUORUM "\" because fencing is " "disabled", rsc->id); unpack_requires(rsc, PCMK_VALUE_QUORUM, true); return; } else { pcmk__set_rsc_flags(rsc, pcmk_rsc_needs_fencing |pcmk_rsc_needs_unfencing); } } else { const char *orig_value = value; if (pcmk_is_set(rsc->flags, pcmk_rsc_fence_device)) { value = PCMK_VALUE_QUORUM; } else if (pcmk__is_primitive(rsc) && xml_contains_remote_node(rsc->xml)) { value = PCMK_VALUE_QUORUM; } else if (pcmk_is_set(rsc->cluster->flags, pcmk_sched_enable_unfencing)) { value = PCMK_VALUE_UNFENCING; } else if (pcmk_is_set(rsc->cluster->flags, pcmk_sched_fencing_enabled)) { value = PCMK_VALUE_FENCING; } else if (rsc->cluster->no_quorum_policy == pcmk_no_quorum_ignore) { value = PCMK_VALUE_NOTHING; } else { value = PCMK_VALUE_QUORUM; } if (orig_value != NULL) { pcmk__config_err("Resetting '" PCMK_META_REQUIRES "' for %s " "to '%s' because '%s' is not valid", rsc->id, value, orig_value); } unpack_requires(rsc, value, true); return; } pcmk__rsc_trace(rsc, "\tRequired to start: %s%s", value, (is_default? " (default)" : "")); } static void warn_about_deprecated_classes(pcmk_resource_t *rsc) { const char *std = crm_element_value(rsc->xml, PCMK_XA_CLASS); if (pcmk__str_eq(std, PCMK_RESOURCE_CLASS_UPSTART, pcmk__str_none)) { pcmk__warn_once(pcmk__wo_upstart, "Support for Upstart resources (such as %s) is " "deprecated and will be removed in a future release", rsc->id); } else if (pcmk__str_eq(std, PCMK_RESOURCE_CLASS_NAGIOS, pcmk__str_none)) { pcmk__warn_once(pcmk__wo_nagios, "Support for Nagios resources (such as %s) is " "deprecated and will be removed in a future release", rsc->id); } } /*! * \internal * \brief Unpack configuration XML for a given resource * * Unpack the XML object containing a resource's configuration into a new * \c pcmk_resource_t object. * * \param[in] xml_obj XML node containing the resource's configuration * \param[out] rsc Where to store the unpacked resource information * \param[in] parent Resource's parent, if any * \param[in,out] scheduler Scheduler data * * \return Standard Pacemaker return code * \note If pcmk_rc_ok is returned, \p *rsc is guaranteed to be non-NULL, and * the caller is responsible for freeing it using its variant-specific * free() method. Otherwise, \p *rsc is guaranteed to be NULL. */ int pe__unpack_resource(xmlNode *xml_obj, pcmk_resource_t **rsc, pcmk_resource_t *parent, pcmk_scheduler_t *scheduler) { xmlNode *expanded_xml = NULL; xmlNode *ops = NULL; const char *value = NULL; const char *id = NULL; bool guest_node = false; bool remote_node = false; pe_rule_eval_data_t rule_data = { .node_hash = NULL, .now = NULL, .match_data = NULL, .rsc_data = NULL, .op_data = NULL }; CRM_CHECK(rsc != NULL, return EINVAL); CRM_CHECK((xml_obj != NULL) && (scheduler != NULL), *rsc = NULL; return EINVAL); rule_data.now = scheduler->now; crm_log_xml_trace(xml_obj, "[raw XML]"); id = crm_element_value(xml_obj, PCMK_XA_ID); if (id == NULL) { pcmk__config_err("Ignoring <%s> configuration without " PCMK_XA_ID, xml_obj->name); return pcmk_rc_unpack_error; } if (unpack_template(xml_obj, &expanded_xml, scheduler) == FALSE) { return pcmk_rc_unpack_error; } *rsc = calloc(1, sizeof(pcmk_resource_t)); if (*rsc == NULL) { pcmk__sched_err("Unable to allocate memory for resource '%s'", id); return ENOMEM; } (*rsc)->cluster = scheduler; if (expanded_xml) { crm_log_xml_trace(expanded_xml, "[expanded XML]"); (*rsc)->xml = expanded_xml; (*rsc)->orig_xml = xml_obj; } else { (*rsc)->xml = xml_obj; (*rsc)->orig_xml = NULL; } /* Do not use xml_obj from here on, use (*rsc)->xml in case templates are involved */ (*rsc)->parent = parent; ops = pcmk__xe_first_child((*rsc)->xml, PCMK_XE_OPERATIONS, NULL, NULL); - (*rsc)->ops_xml = expand_idref(ops, scheduler->input); + (*rsc)->ops_xml = pcmk__xe_resolve_idref(ops, scheduler->input); (*rsc)->variant = get_resource_type((const char *) (*rsc)->xml->name); if ((*rsc)->variant == pcmk_rsc_variant_unknown) { pcmk__config_err("Ignoring resource '%s' of unknown type '%s'", id, (*rsc)->xml->name); common_free(*rsc); *rsc = NULL; return pcmk_rc_unpack_error; } (*rsc)->meta = pcmk__strkey_table(free, free); (*rsc)->allowed_nodes = pcmk__strkey_table(NULL, free); (*rsc)->known_on = pcmk__strkey_table(NULL, free); value = crm_element_value((*rsc)->xml, PCMK__META_CLONE); if (value) { (*rsc)->id = crm_strdup_printf("%s:%s", id, value); pcmk__insert_meta(*rsc, PCMK__META_CLONE, value); } else { (*rsc)->id = strdup(id); } warn_about_deprecated_classes(*rsc); (*rsc)->fns = &resource_class_functions[(*rsc)->variant]; get_meta_attributes((*rsc)->meta, *rsc, NULL, scheduler); (*rsc)->parameters = pe_rsc_params(*rsc, NULL, scheduler); // \deprecated (*rsc)->flags = 0; pcmk__set_rsc_flags(*rsc, pcmk_rsc_runnable|pcmk_rsc_unassigned); if (!pcmk_is_set(scheduler->flags, pcmk_sched_in_maintenance)) { pcmk__set_rsc_flags(*rsc, pcmk_rsc_managed); } (*rsc)->rsc_cons = NULL; (*rsc)->rsc_tickets = NULL; (*rsc)->actions = NULL; (*rsc)->role = pcmk_role_stopped; (*rsc)->next_role = pcmk_role_unknown; (*rsc)->recovery_type = pcmk_multiply_active_restart; (*rsc)->stickiness = 0; (*rsc)->migration_threshold = PCMK_SCORE_INFINITY; (*rsc)->failure_timeout = 0; value = g_hash_table_lookup((*rsc)->meta, PCMK_META_PRIORITY); (*rsc)->priority = char2score(value); value = g_hash_table_lookup((*rsc)->meta, PCMK_META_CRITICAL); if ((value == NULL) || crm_is_true(value)) { pcmk__set_rsc_flags(*rsc, pcmk_rsc_critical); } value = g_hash_table_lookup((*rsc)->meta, PCMK_META_NOTIFY); if (crm_is_true(value)) { pcmk__set_rsc_flags(*rsc, pcmk_rsc_notify); } if (xml_contains_remote_node((*rsc)->xml)) { (*rsc)->is_remote_node = TRUE; if (g_hash_table_lookup((*rsc)->meta, PCMK__META_CONTAINER)) { guest_node = true; } else { remote_node = true; } } value = g_hash_table_lookup((*rsc)->meta, PCMK_META_ALLOW_MIGRATE); if (crm_is_true(value)) { pcmk__set_rsc_flags(*rsc, pcmk_rsc_migratable); } else if ((value == NULL) && remote_node) { /* By default, we want remote nodes to be able * to float around the cluster without having to stop all the * resources within the remote-node before moving. Allowing * migration support enables this feature. If this ever causes * problems, migration support can be explicitly turned off with * PCMK_META_ALLOW_MIGRATE=false. */ pcmk__set_rsc_flags(*rsc, pcmk_rsc_migratable); } value = g_hash_table_lookup((*rsc)->meta, PCMK_META_IS_MANAGED); if (value != NULL) { if (pcmk__str_eq(PCMK_VALUE_DEFAULT, value, pcmk__str_casei)) { // @COMPAT Deprecated since 2.1.8 pcmk__config_warn("Support for setting " PCMK_META_IS_MANAGED " to the explicit value '" PCMK_VALUE_DEFAULT "' is deprecated and will be removed in a " "future release (just leave it unset)"); } else if (crm_is_true(value)) { pcmk__set_rsc_flags(*rsc, pcmk_rsc_managed); } else { pcmk__clear_rsc_flags(*rsc, pcmk_rsc_managed); } } value = g_hash_table_lookup((*rsc)->meta, PCMK_META_MAINTENANCE); if (crm_is_true(value)) { pcmk__clear_rsc_flags(*rsc, pcmk_rsc_managed); pcmk__set_rsc_flags(*rsc, pcmk_rsc_maintenance); } if (pcmk_is_set(scheduler->flags, pcmk_sched_in_maintenance)) { pcmk__clear_rsc_flags(*rsc, pcmk_rsc_managed); pcmk__set_rsc_flags(*rsc, pcmk_rsc_maintenance); } if (pcmk__is_clone(pe__const_top_resource(*rsc, false))) { value = g_hash_table_lookup((*rsc)->meta, PCMK_META_GLOBALLY_UNIQUE); if (crm_is_true(value)) { pcmk__set_rsc_flags(*rsc, pcmk_rsc_unique); } if (detect_promotable(*rsc)) { pcmk__set_rsc_flags(*rsc, pcmk_rsc_promotable); } } else { pcmk__set_rsc_flags(*rsc, pcmk_rsc_unique); } // @COMPAT Deprecated meta-attribute value = g_hash_table_lookup((*rsc)->meta, PCMK__META_RESTART_TYPE); if (pcmk__str_eq(value, PCMK_VALUE_RESTART, pcmk__str_casei)) { (*rsc)->restart_type = pe_restart_restart; pcmk__rsc_trace(*rsc, "%s dependency restart handling: restart", (*rsc)->id); pcmk__warn_once(pcmk__wo_restart_type, "Support for " PCMK__META_RESTART_TYPE " is deprecated " "and will be removed in a future release"); } else { (*rsc)->restart_type = pe_restart_ignore; pcmk__rsc_trace(*rsc, "%s dependency restart handling: ignore", (*rsc)->id); } value = g_hash_table_lookup((*rsc)->meta, PCMK_META_MULTIPLE_ACTIVE); if (pcmk__str_eq(value, PCMK_VALUE_STOP_ONLY, pcmk__str_casei)) { (*rsc)->recovery_type = pcmk_multiply_active_stop; pcmk__rsc_trace(*rsc, "%s multiple running resource recovery: stop only", (*rsc)->id); } else if (pcmk__str_eq(value, PCMK_VALUE_BLOCK, pcmk__str_casei)) { (*rsc)->recovery_type = pcmk_multiply_active_block; pcmk__rsc_trace(*rsc, "%s multiple running resource recovery: block", (*rsc)->id); } else if (pcmk__str_eq(value, PCMK_VALUE_STOP_UNEXPECTED, pcmk__str_casei)) { (*rsc)->recovery_type = pcmk_multiply_active_unexpected; pcmk__rsc_trace(*rsc, "%s multiple running resource recovery: " "stop unexpected instances", (*rsc)->id); } else { // PCMK_VALUE_STOP_START if (!pcmk__str_eq(value, PCMK_VALUE_STOP_START, pcmk__str_casei|pcmk__str_null_matches)) { pcmk__config_warn("%s is not a valid value for " PCMK_META_MULTIPLE_ACTIVE ", using default of " "\"" PCMK_VALUE_STOP_START "\"", value); } (*rsc)->recovery_type = pcmk_multiply_active_restart; pcmk__rsc_trace(*rsc, "%s multiple running resource recovery: stop/start", (*rsc)->id); } value = g_hash_table_lookup((*rsc)->meta, PCMK_META_RESOURCE_STICKINESS); if (value != NULL) { if (pcmk__str_eq(PCMK_VALUE_DEFAULT, value, pcmk__str_casei)) { // @COMPAT Deprecated since 2.1.8 pcmk__config_warn("Support for setting " PCMK_META_RESOURCE_STICKINESS " to the explicit value '" PCMK_VALUE_DEFAULT "' is deprecated and will be removed in a " "future release (just leave it unset)"); } else { (*rsc)->stickiness = char2score(value); } } value = g_hash_table_lookup((*rsc)->meta, PCMK_META_MIGRATION_THRESHOLD); if (value != NULL) { if (pcmk__str_eq(PCMK_VALUE_DEFAULT, value, pcmk__str_casei)) { // @COMPAT Deprecated since 2.1.8 pcmk__config_warn("Support for setting " PCMK_META_MIGRATION_THRESHOLD " to the explicit value '" PCMK_VALUE_DEFAULT "' is deprecated and will be removed in a " "future release (just leave it unset)"); } else { (*rsc)->migration_threshold = char2score(value); if ((*rsc)->migration_threshold < 0) { /* @COMPAT We use 1 here to preserve previous behavior, but this * should probably use the default (INFINITY) or 0 (to disable) * instead. */ pcmk__warn_once(pcmk__wo_neg_threshold, PCMK_META_MIGRATION_THRESHOLD " must be non-negative, using 1 instead"); (*rsc)->migration_threshold = 1; } } } if (pcmk__str_eq(crm_element_value((*rsc)->xml, PCMK_XA_CLASS), PCMK_RESOURCE_CLASS_STONITH, pcmk__str_casei)) { pcmk__set_scheduler_flags(scheduler, pcmk_sched_have_fencing); pcmk__set_rsc_flags(*rsc, pcmk_rsc_fence_device); } value = g_hash_table_lookup((*rsc)->meta, PCMK_META_REQUIRES); unpack_requires(*rsc, value, false); value = g_hash_table_lookup((*rsc)->meta, PCMK_META_FAILURE_TIMEOUT); if (value != NULL) { guint interval_ms = 0U; // Stored as seconds pcmk_parse_interval_spec(value, &interval_ms); (*rsc)->failure_timeout = (int) (interval_ms / 1000); } if (remote_node) { GHashTable *params = pe_rsc_params(*rsc, NULL, scheduler); /* Grabbing the value now means that any rules based on node attributes * will evaluate to false, so such rules should not be used with * PCMK_REMOTE_RA_RECONNECT_INTERVAL. * * @TODO Evaluate per node before using */ value = g_hash_table_lookup(params, PCMK_REMOTE_RA_RECONNECT_INTERVAL); if (value) { /* reconnect delay works by setting failure_timeout and preventing the * connection from starting until the failure is cleared. */ pcmk_parse_interval_spec(value, &((*rsc)->remote_reconnect_ms)); /* We want to override any default failure_timeout in use when remote * PCMK_REMOTE_RA_RECONNECT_INTERVAL is in use. */ (*rsc)->failure_timeout = (*rsc)->remote_reconnect_ms / 1000; } } get_target_role(*rsc, &((*rsc)->next_role)); pcmk__rsc_trace(*rsc, "%s desired next state: %s", (*rsc)->id, ((*rsc)->next_role == pcmk_role_unknown)? "default" : pcmk_role_text((*rsc)->next_role)); if ((*rsc)->fns->unpack(*rsc, scheduler) == FALSE) { (*rsc)->fns->free(*rsc); *rsc = NULL; return pcmk_rc_unpack_error; } if (pcmk_is_set(scheduler->flags, pcmk_sched_symmetric_cluster)) { // This tag must stay exactly the same because it is tested elsewhere resource_location(*rsc, NULL, 0, "symmetric_default", scheduler); } else if (guest_node) { /* remote resources tied to a container resource must always be allowed * to opt-in to the cluster. Whether the connection resource is actually * allowed to be placed on a node is dependent on the container resource */ resource_location(*rsc, NULL, 0, "remote_connection_default", scheduler); } pcmk__rsc_trace(*rsc, "%s action notification: %s", (*rsc)->id, pcmk_is_set((*rsc)->flags, pcmk_rsc_notify)? "required" : "not required"); (*rsc)->utilization = pcmk__strkey_table(free, free); pe__unpack_dataset_nvpairs((*rsc)->xml, PCMK_XE_UTILIZATION, &rule_data, (*rsc)->utilization, NULL, FALSE, scheduler); if (expanded_xml) { if (add_template_rsc(xml_obj, scheduler) == FALSE) { (*rsc)->fns->free(*rsc); *rsc = NULL; return pcmk_rc_unpack_error; } } return pcmk_rc_ok; } gboolean is_parent(pcmk_resource_t *child, pcmk_resource_t *rsc) { pcmk_resource_t *parent = child; if (parent == NULL || rsc == NULL) { return FALSE; } while (parent->parent != NULL) { if (parent->parent == rsc) { return TRUE; } parent = parent->parent; } return FALSE; } pcmk_resource_t * uber_parent(pcmk_resource_t *rsc) { pcmk_resource_t *parent = rsc; if (parent == NULL) { return NULL; } while ((parent->parent != NULL) && !pcmk__is_bundle(parent->parent)) { parent = parent->parent; } return parent; } /*! * \internal * \brief Get the topmost parent of a resource as a const pointer * * \param[in] rsc Resource to check * \param[in] include_bundle If true, go all the way to bundle * * \return \p NULL if \p rsc is NULL, \p rsc if \p rsc has no parent, * the bundle if \p rsc is bundled and \p include_bundle is true, * otherwise the topmost parent of \p rsc up to a clone */ const pcmk_resource_t * pe__const_top_resource(const pcmk_resource_t *rsc, bool include_bundle) { const pcmk_resource_t *parent = rsc; if (parent == NULL) { return NULL; } while (parent->parent != NULL) { if (!include_bundle && pcmk__is_bundle(parent->parent)) { break; } parent = parent->parent; } return parent; } void common_free(pcmk_resource_t * rsc) { if (rsc == NULL) { return; } pcmk__rsc_trace(rsc, "Freeing %s %d", rsc->id, rsc->variant); g_list_free(rsc->rsc_cons); g_list_free(rsc->rsc_cons_lhs); g_list_free(rsc->rsc_tickets); g_list_free(rsc->dangling_migrations); if (rsc->parameter_cache != NULL) { g_hash_table_destroy(rsc->parameter_cache); } if (rsc->meta != NULL) { g_hash_table_destroy(rsc->meta); } if (rsc->utilization != NULL) { g_hash_table_destroy(rsc->utilization); } if ((rsc->parent == NULL) && pcmk_is_set(rsc->flags, pcmk_rsc_removed)) { pcmk__xml_free(rsc->xml); rsc->xml = NULL; pcmk__xml_free(rsc->orig_xml); rsc->orig_xml = NULL; /* if rsc->orig_xml, then rsc->xml is an expanded xml from a template */ } else if (rsc->orig_xml) { pcmk__xml_free(rsc->xml); rsc->xml = NULL; } if (rsc->running_on) { g_list_free(rsc->running_on); rsc->running_on = NULL; } if (rsc->known_on) { g_hash_table_destroy(rsc->known_on); rsc->known_on = NULL; } if (rsc->actions) { g_list_free(rsc->actions); rsc->actions = NULL; } if (rsc->allowed_nodes) { g_hash_table_destroy(rsc->allowed_nodes); rsc->allowed_nodes = NULL; } g_list_free(rsc->fillers); g_list_free(rsc->rsc_location); pcmk__rsc_trace(rsc, "Resource freed"); free(rsc->id); free(rsc->clone_name); free(rsc->allocated_to); free(rsc->variant_opaque); free(rsc->pending_task); free(rsc); } /*! * \internal * \brief Count a node and update most preferred to it as appropriate * * \param[in] rsc An active resource * \param[in] node A node that \p rsc is active on * \param[in,out] active This will be set to \p node if \p node is more * preferred than the current value * \param[in,out] count_all If not NULL, this will be incremented * \param[in,out] count_clean If not NULL, this will be incremented if \p node * is online and clean * * \return true if the count should continue, or false if sufficiently known */ bool pe__count_active_node(const pcmk_resource_t *rsc, pcmk_node_t *node, pcmk_node_t **active, unsigned int *count_all, unsigned int *count_clean) { bool keep_looking = false; bool is_happy = false; CRM_CHECK((rsc != NULL) && (node != NULL) && (active != NULL), return false); is_happy = node->details->online && !node->details->unclean; if (count_all != NULL) { ++*count_all; } if ((count_clean != NULL) && is_happy) { ++*count_clean; } if ((count_all != NULL) || (count_clean != NULL)) { keep_looking = true; // We're counting, so go through entire list } if (rsc->partial_migration_source != NULL) { if (pcmk__same_node(node, rsc->partial_migration_source)) { *active = node; // This is the migration source } else { keep_looking = true; } } else if (!pcmk_is_set(rsc->flags, pcmk_rsc_needs_fencing)) { if (is_happy && ((*active == NULL) || !(*active)->details->online || (*active)->details->unclean)) { *active = node; // This is the first clean node } else { keep_looking = true; } } if (*active == NULL) { *active = node; // This is the first node checked } return keep_looking; } // Shared implementation of pcmk_rsc_methods_t:active_node() static pcmk_node_t * active_node(const pcmk_resource_t *rsc, unsigned int *count_all, unsigned int *count_clean) { pcmk_node_t *active = NULL; if (count_all != NULL) { *count_all = 0; } if (count_clean != NULL) { *count_clean = 0; } if (rsc == NULL) { return NULL; } for (GList *iter = rsc->running_on; iter != NULL; iter = iter->next) { if (!pe__count_active_node(rsc, (pcmk_node_t *) iter->data, &active, count_all, count_clean)) { break; // Don't waste time iterating if we don't have to } } return active; } /*! * \brief * \internal Find and count active nodes according to \c PCMK_META_REQUIRES * * \param[in] rsc Resource to check * \param[out] count If not NULL, will be set to count of active nodes * * \return An active node (or NULL if resource is not active anywhere) * * \note This is a convenience wrapper for active_node() where the count of all * active nodes or only clean active nodes is desired according to the * \c PCMK_META_REQUIRES meta-attribute. */ pcmk_node_t * pe__find_active_requires(const pcmk_resource_t *rsc, unsigned int *count) { if (rsc == NULL) { if (count != NULL) { *count = 0; } return NULL; } else if (pcmk_is_set(rsc->flags, pcmk_rsc_needs_fencing)) { return rsc->fns->active_node(rsc, count, NULL); } else { return rsc->fns->active_node(rsc, NULL, count); } } void pe__count_common(pcmk_resource_t *rsc) { if (rsc->children != NULL) { for (GList *item = rsc->children; item != NULL; item = item->next) { ((pcmk_resource_t *) item->data)->fns->count(item->data); } } else if (!pcmk_is_set(rsc->flags, pcmk_rsc_removed) || (rsc->role > pcmk_role_stopped)) { rsc->cluster->ninstances++; if (pe__resource_is_disabled(rsc)) { rsc->cluster->disabled_resources++; } if (pcmk_is_set(rsc->flags, pcmk_rsc_blocked)) { rsc->cluster->blocked_resources++; } } } /*! * \internal * \brief Update a resource's next role * * \param[in,out] rsc Resource to be updated * \param[in] role Resource's new next role * \param[in] why Human-friendly reason why role is changing (for logs) */ void pe__set_next_role(pcmk_resource_t *rsc, enum rsc_role_e role, const char *why) { CRM_ASSERT((rsc != NULL) && (why != NULL)); if (rsc->next_role != role) { pcmk__rsc_trace(rsc, "Resetting next role for %s from %s to %s (%s)", rsc->id, pcmk_role_text(rsc->next_role), pcmk_role_text(role), why); rsc->next_role = role; } } diff --git a/lib/pengine/rules.c b/lib/pengine/rules.c index e0ecad1051..ade1a9e79b 100644 --- a/lib/pengine/rules.c +++ b/lib/pengine/rules.c @@ -1,321 +1,321 @@ /* * 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 CRM_TRACE_INIT_DATA(pe_rules); /*! * \internal * \brief Map pe_rule_eval_data_t to pcmk_rule_input_t * * \param[out] new New data struct * \param[in] old Old data struct */ static void map_rule_input(pcmk_rule_input_t *new, const pe_rule_eval_data_t *old) { if (old == NULL) { return; } new->now = old->now; new->node_attrs = old->node_hash; if (old->rsc_data != NULL) { new->rsc_standard = old->rsc_data->standard; new->rsc_provider = old->rsc_data->provider; new->rsc_agent = old->rsc_data->agent; } if (old->match_data != NULL) { new->rsc_params = old->match_data->params; new->rsc_meta = old->match_data->meta; if (old->match_data->re != NULL) { new->rsc_id = old->match_data->re->string; new->rsc_id_submatches = old->match_data->re->pmatch; new->rsc_id_nmatches = old->match_data->re->nregs; } } if (old->op_data != NULL) { new->op_name = old->op_data->op_name; new->op_interval_ms = old->op_data->interval; } } static gint sort_pairs(gconstpointer a, gconstpointer b, gpointer user_data) { const xmlNode *pair_a = a; const xmlNode *pair_b = b; pcmk__nvpair_unpack_t *unpack_data = user_data; const char *score = NULL; int score_a = 0; int score_b = 0; if (a == NULL && b == NULL) { return 0; } else if (a == NULL) { return 1; } else if (b == NULL) { return -1; } if (pcmk__str_eq(pcmk__xe_id(pair_a), unpack_data->first_id, pcmk__str_none)) { return -1; } else if (pcmk__str_eq(pcmk__xe_id(pair_b), unpack_data->first_id, pcmk__str_none)) { return 1; } score = crm_element_value(pair_a, PCMK_XA_SCORE); score_a = char2score(score); score = crm_element_value(pair_b, PCMK_XA_SCORE); score_b = char2score(score); /* If we're overwriting values, we want lowest score first, so the highest * score is processed last; if we're not overwriting values, we want highest * score first, so nothing else overwrites it. */ if (score_a < score_b) { return unpack_data->overwrite? -1 : 1; } else if (score_a > score_b) { return unpack_data->overwrite? 1 : -1; } return 0; } static void populate_hash(xmlNode *nvpair_list, GHashTable *hash, bool overwrite) { const char *name = NULL; const char *value = NULL; const char *old_value = NULL; xmlNode *list = nvpair_list; xmlNode *an_attr = NULL; if (pcmk__xe_is(list->children, PCMK__XE_ATTRIBUTES)) { list = list->children; } for (an_attr = pcmk__xe_first_child(list, NULL, NULL, NULL); an_attr != NULL; an_attr = pcmk__xe_next(an_attr)) { if (pcmk__xe_is(an_attr, PCMK_XE_NVPAIR)) { - xmlNode *ref_nvpair = expand_idref(an_attr, NULL); + xmlNode *ref_nvpair = pcmk__xe_resolve_idref(an_attr, NULL); name = crm_element_value(an_attr, PCMK_XA_NAME); if ((name == NULL) && (ref_nvpair != NULL)) { name = crm_element_value(ref_nvpair, PCMK_XA_NAME); } value = crm_element_value(an_attr, PCMK_XA_VALUE); if ((value == NULL) && (ref_nvpair != NULL)) { value = crm_element_value(ref_nvpair, PCMK_XA_VALUE); } if (name == NULL || value == NULL) { continue; } old_value = g_hash_table_lookup(hash, name); if (pcmk__str_eq(value, "#default", pcmk__str_casei)) { // @COMPAT Deprecated since 2.1.8 pcmk__config_warn("Support for setting meta-attributes (such " "as %s) to the explicit value '#default' is " "deprecated and will be removed in a future " "release", name); if (old_value) { crm_trace("Letting %s default (removing explicit value \"%s\")", name, value); g_hash_table_remove(hash, name); } continue; } else if (old_value == NULL) { crm_trace("Setting %s=\"%s\"", name, value); pcmk__insert_dup(hash, name, value); } else if (overwrite) { crm_trace("Setting %s=\"%s\" (overwriting old value \"%s\")", name, value, old_value); pcmk__insert_dup(hash, name, value); } } } } static void unpack_attr_set(gpointer data, gpointer user_data) { xmlNode *pair = data; pcmk__nvpair_unpack_t *unpack_data = user_data; if (pcmk__evaluate_rules(pair, &(unpack_data->rule_input), unpack_data->next_change) != pcmk_rc_ok) { return; } crm_trace("Adding name/value pairs from %s %s overwrite", pcmk__xe_id(pair), (unpack_data->overwrite? "with" : "without")); populate_hash(pair, unpack_data->values, unpack_data->overwrite); } /*! * \internal * \brief Create a sorted list of nvpair blocks * * \param[in] xml_obj XML element containing blocks of nvpair elements * \param[in] set_name If not NULL, only get blocks of this element * * \return List of XML blocks of name/value pairs */ static GList * make_pairs(const xmlNode *xml_obj, const char *set_name) { GList *unsorted = NULL; if (xml_obj == NULL) { return NULL; } for (xmlNode *attr_set = pcmk__xe_first_child(xml_obj, NULL, NULL, NULL); attr_set != NULL; attr_set = pcmk__xe_next(attr_set)) { if ((set_name == NULL) || pcmk__xe_is(attr_set, set_name)) { - xmlNode *expanded_attr_set = expand_idref(attr_set, NULL); + xmlNode *expanded_attr_set = pcmk__xe_resolve_idref(attr_set, NULL); if (expanded_attr_set == NULL) { continue; // Not possible with schema validation enabled } unsorted = g_list_prepend(unsorted, expanded_attr_set); } } return unsorted; } /*! * \brief Extract nvpair blocks contained by an XML element into a hash table * * \param[in,out] top Ignored * \param[in] xml_obj XML element containing blocks of nvpair elements * \param[in] set_name If not NULL, only use blocks of this element * \param[in] rule_data Matching parameters to use when unpacking * \param[out] hash Where to store extracted name/value pairs * \param[in] always_first If not NULL, process block with this ID first * \param[in] overwrite Whether to replace existing values with same name * \param[out] next_change If not NULL, set to when evaluation will change */ void pe_eval_nvpairs(xmlNode *top, const xmlNode *xml_obj, const char *set_name, const pe_rule_eval_data_t *rule_data, GHashTable *hash, const char *always_first, gboolean overwrite, crm_time_t *next_change) { GList *pairs = make_pairs(xml_obj, set_name); if (pairs) { pcmk__nvpair_unpack_t data = { .values = hash, .first_id = always_first, .overwrite = overwrite, .next_change = next_change, }; map_rule_input(&(data.rule_input), rule_data); pairs = g_list_sort_with_data(pairs, sort_pairs, &data); g_list_foreach(pairs, unpack_attr_set, &data); g_list_free(pairs); } } /*! * \brief Extract nvpair blocks contained by an XML element into a hash table * * \param[in,out] top Ignored * \param[in] xml_obj XML element containing blocks of nvpair elements * \param[in] set_name Element name to identify nvpair blocks * \param[in] node_hash Node attributes to use when evaluating rules * \param[out] hash Where to store extracted name/value pairs * \param[in] always_first If not NULL, process block with this ID first * \param[in] overwrite Whether to replace existing values with same name * \param[in] now Time to use when evaluating rules * \param[out] next_change If not NULL, set to when evaluation will change */ void pe_unpack_nvpairs(xmlNode *top, const xmlNode *xml_obj, const char *set_name, GHashTable *node_hash, GHashTable *hash, const char *always_first, gboolean overwrite, crm_time_t *now, crm_time_t *next_change) { pe_rule_eval_data_t rule_data = { .node_hash = node_hash, .now = now, .match_data = NULL, .rsc_data = NULL, .op_data = NULL }; pe_eval_nvpairs(NULL, xml_obj, set_name, &rule_data, hash, always_first, overwrite, next_change); } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include static gboolean pe_test_rule(xmlNode *rule, GHashTable *node_hash, enum rsc_role_e role, crm_time_t *now, crm_time_t *next_change, pe_match_data_t *match_data) { pcmk_rule_input_t rule_input = { .node_attrs = node_hash, .now = now, }; if (match_data != NULL) { rule_input.rsc_params = match_data->params; rule_input.rsc_meta = match_data->meta; if (match_data->re != NULL) { rule_input.rsc_id = match_data->re->string; rule_input.rsc_id_submatches = match_data->re->pmatch; rule_input.rsc_id_nmatches = match_data->re->nregs; } } return pcmk_evaluate_rule(rule, &rule_input, next_change) == pcmk_rc_ok; } gboolean test_rule(xmlNode * rule, GHashTable * node_hash, enum rsc_role_e role, crm_time_t * now) { return pe_test_rule(rule, node_hash, role, now, NULL, NULL); } // LCOV_EXCL_STOP // End deprecated API