diff --git a/lib/common/xml.c b/lib/common/xml.c index a1b1291f99..c9a07eed43 100644 --- a/lib/common/xml.c +++ b/lib/common/xml.c @@ -1,2762 +1,2763 @@ /* * Copyright 2004-2023 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include #include #include /* xmlAllocOutputBuffer */ #include #include #include #include // PCMK__XML_LOG_BASE, etc. #include "crmcommon_private.h" // Define this as 1 in development to get insanely verbose trace messages #ifndef XML_PARSER_DEBUG #define XML_PARSER_DEBUG 0 #endif /* @TODO XML_PARSE_RECOVER allows some XML errors to be silently worked around * by libxml2, which is potentially ambiguous and dangerous. We should drop it * when we can break backward compatibility with configurations that might be * relying on it (i.e. pacemaker 3.0.0). * * It might be a good idea to have a transitional period where we first try * parsing without XML_PARSE_RECOVER, and if that fails, try parsing again with * it, logging a warning if it succeeds. */ #define PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER (XML_PARSE_NOBLANKS) #define PCMK__XML_PARSE_OPTS_WITH_RECOVER (XML_PARSE_NOBLANKS | XML_PARSE_RECOVER) bool pcmk__tracking_xml_changes(xmlNode *xml, bool lazy) { if(xml == NULL || xml->doc == NULL || xml->doc->_private == NULL) { return FALSE; } else if (!pcmk_is_set(((xml_doc_private_t *)xml->doc->_private)->flags, pcmk__xf_tracking)) { return FALSE; } else if (lazy && !pcmk_is_set(((xml_doc_private_t *)xml->doc->_private)->flags, pcmk__xf_lazy)) { return FALSE; } return TRUE; } static inline void set_parent_flag(xmlNode *xml, long flag) { for(; xml; xml = xml->parent) { xml_node_private_t *nodepriv = xml->_private; if (nodepriv == NULL) { /* During calls to xmlDocCopyNode(), _private will be unset for parent nodes */ } else { pcmk__set_xml_flags(nodepriv, flag); } } } void pcmk__set_xml_doc_flag(xmlNode *xml, enum xml_private_flags flag) { if(xml && xml->doc && xml->doc->_private){ /* During calls to xmlDocCopyNode(), xml->doc may be unset */ xml_doc_private_t *docpriv = xml->doc->_private; pcmk__set_xml_flags(docpriv, flag); } } // Mark document, element, and all element's parents as changed void pcmk__mark_xml_node_dirty(xmlNode *xml) { pcmk__set_xml_doc_flag(xml, pcmk__xf_dirty); set_parent_flag(xml, pcmk__xf_dirty); } // Clear flags on XML node and its children static void reset_xml_node_flags(xmlNode *xml) { xmlNode *cIter = NULL; xml_node_private_t *nodepriv = xml->_private; if (nodepriv) { nodepriv->flags = 0; } for (cIter = pcmk__xml_first_child(xml); cIter != NULL; cIter = pcmk__xml_next(cIter)) { reset_xml_node_flags(cIter); } } // Set xpf_created flag on XML node and any children void pcmk__mark_xml_created(xmlNode *xml) { xmlNode *cIter = NULL; xml_node_private_t *nodepriv = NULL; CRM_ASSERT(xml != NULL); nodepriv = xml->_private; if (nodepriv && pcmk__tracking_xml_changes(xml, FALSE)) { if (!pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { pcmk__set_xml_flags(nodepriv, pcmk__xf_created); pcmk__mark_xml_node_dirty(xml); } for (cIter = pcmk__xml_first_child(xml); cIter != NULL; cIter = pcmk__xml_next(cIter)) { pcmk__mark_xml_created(cIter); } } } #define XML_DOC_PRIVATE_MAGIC 0x81726354UL #define XML_NODE_PRIVATE_MAGIC 0x54637281UL // Free an XML object previously marked as deleted static void free_deleted_object(void *data) { if(data) { pcmk__deleted_xml_t *deleted_obj = data; free(deleted_obj->path); free(deleted_obj); } } // Free and NULL user, ACLs, and deleted objects in an XML node's private data static void reset_xml_private_data(xml_doc_private_t *docpriv) { if (docpriv != NULL) { CRM_ASSERT(docpriv->check == XML_DOC_PRIVATE_MAGIC); free(docpriv->user); docpriv->user = NULL; if (docpriv->acls != NULL) { pcmk__free_acls(docpriv->acls); docpriv->acls = NULL; } if(docpriv->deleted_objs) { g_list_free_full(docpriv->deleted_objs, free_deleted_object); docpriv->deleted_objs = NULL; } } } // Free all private data associated with an XML node static void free_private_data(xmlNode *node) { /* Note: This function frees private data assosciated with an XML node, unless the function is being called as a result of internal XSLT cleanup. That could happen through, for example, the following chain of function calls: xsltApplyStylesheetInternal -> xsltFreeTransformContext -> xsltFreeRVTs -> xmlFreeDoc And in that case, the node would fulfill three conditions: 1. It would be a standalone document (i.e. it wouldn't be part of a document) 2. It would have a space-prefixed name (for reference, please see xsltInternals.h: XSLT_MARK_RES_TREE_FRAG) 3. It would carry its own payload in the _private field. We do not free data in this circumstance to avoid a failed assertion on the XML_*_PRIVATE_MAGIC later. */ if (node->name == NULL || node->name[0] != ' ') { if (node->_private) { if (node->type == XML_DOCUMENT_NODE) { reset_xml_private_data(node->_private); } else { CRM_ASSERT(((xml_node_private_t *) node->_private)->check == XML_NODE_PRIVATE_MAGIC); /* nothing dynamically allocated nested */ } free(node->_private); node->_private = NULL; } } } // Allocate and initialize private data for an XML node static void new_private_data(xmlNode *node) { switch (node->type) { case XML_DOCUMENT_NODE: { xml_doc_private_t *docpriv = NULL; docpriv = calloc(1, sizeof(xml_doc_private_t)); CRM_ASSERT(docpriv != NULL); docpriv->check = XML_DOC_PRIVATE_MAGIC; /* Flags will be reset if necessary when tracking is enabled */ pcmk__set_xml_flags(docpriv, pcmk__xf_dirty|pcmk__xf_created); node->_private = docpriv; break; } case XML_ELEMENT_NODE: case XML_ATTRIBUTE_NODE: case XML_COMMENT_NODE: { xml_node_private_t *nodepriv = NULL; nodepriv = calloc(1, sizeof(xml_node_private_t)); CRM_ASSERT(nodepriv != NULL); nodepriv->check = XML_NODE_PRIVATE_MAGIC; /* Flags will be reset if necessary when tracking is enabled */ pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created); node->_private = nodepriv; if (pcmk__tracking_xml_changes(node, FALSE)) { /* XML_ELEMENT_NODE doesn't get picked up here, node->doc is * not hooked up at the point we are called */ pcmk__mark_xml_node_dirty(node); } break; } case XML_TEXT_NODE: case XML_DTD_NODE: case XML_CDATA_SECTION_NODE: break; default: /* Ignore */ crm_trace("Ignoring %p %d", node, node->type); CRM_LOG_ASSERT(node->type == XML_ELEMENT_NODE); break; } } void xml_track_changes(xmlNode * xml, const char *user, xmlNode *acl_source, bool enforce_acls) { xml_accept_changes(xml); crm_trace("Tracking changes%s to %p", enforce_acls?" with ACLs":"", xml); pcmk__set_xml_doc_flag(xml, pcmk__xf_tracking); if(enforce_acls) { if(acl_source == NULL) { acl_source = xml; } pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_enabled); pcmk__unpack_acl(acl_source, xml, user); pcmk__apply_acl(xml); } } bool xml_tracking_changes(xmlNode * xml) { return (xml != NULL) && (xml->doc != NULL) && (xml->doc->_private != NULL) && pcmk_is_set(((xml_doc_private_t *)(xml->doc->_private))->flags, pcmk__xf_tracking); } bool xml_document_dirty(xmlNode *xml) { return (xml != NULL) && (xml->doc != NULL) && (xml->doc->_private != NULL) && pcmk_is_set(((xml_doc_private_t *)(xml->doc->_private))->flags, pcmk__xf_dirty); } /*! * \internal * \brief Return ordinal position of an XML node among its siblings * * \param[in] xml XML node to check * \param[in] ignore_if_set Don't count siblings with this flag set * * \return Ordinal position of \p xml (starting with 0) */ int pcmk__xml_position(const xmlNode *xml, enum xml_private_flags ignore_if_set) { int position = 0; for (const xmlNode *cIter = xml; cIter->prev; cIter = cIter->prev) { xml_node_private_t *nodepriv = ((xmlNode*)cIter->prev)->_private; if (!pcmk_is_set(nodepriv->flags, ignore_if_set)) { position++; } } return position; } // Remove all attributes marked as deleted from an XML node static void accept_attr_deletions(xmlNode *xml) { // Clear XML node's flags ((xml_node_private_t *) xml->_private)->flags = pcmk__xf_none; // Remove this XML node's attributes that were marked as deleted pcmk__xe_remove_matching_attrs(xml, pcmk__marked_as_deleted, NULL); // Recursively do the same for this XML node's children for (xmlNodePtr cIter = pcmk__xml_first_child(xml); cIter != NULL; cIter = pcmk__xml_next(cIter)) { accept_attr_deletions(cIter); } } /*! * \internal * \brief Find first child XML node matching another given XML node * * \param[in] haystack XML whose children should be checked * \param[in] needle XML to match (comment content or element name and ID) * \param[in] exact If true and needle is a comment, position must match */ xmlNode * pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle, bool exact) { CRM_CHECK(needle != NULL, return NULL); if (needle->type == XML_COMMENT_NODE) { return pcmk__xc_match(haystack, needle, exact); } else { const char *id = ID(needle); const char *attr = (id == NULL)? NULL : XML_ATTR_ID; return pcmk__xe_match(haystack, (const char *) needle->name, attr, id); } } void xml_accept_changes(xmlNode * xml) { xmlNode *top = NULL; xml_doc_private_t *docpriv = NULL; if(xml == NULL) { return; } crm_trace("Accepting changes to %p", xml); docpriv = xml->doc->_private; top = xmlDocGetRootElement(xml->doc); reset_xml_private_data(xml->doc->_private); if (!pcmk_is_set(docpriv->flags, pcmk__xf_dirty)) { docpriv->flags = pcmk__xf_none; return; } docpriv->flags = pcmk__xf_none; accept_attr_deletions(top); } xmlNode * find_xml_node(const xmlNode *root, const char *search_path, gboolean must_find) { xmlNode *a_child = NULL; const char *name = (root == NULL)? "" : (const char *) root->name; if (search_path == NULL) { crm_warn("Will never find "); return NULL; } for (a_child = pcmk__xml_first_child(root); a_child != NULL; a_child = pcmk__xml_next(a_child)) { if (strcmp((const char *)a_child->name, search_path) == 0) { return a_child; } } if (must_find) { crm_warn("Could not find %s in %s.", search_path, name); } else if (root != NULL) { crm_trace("Could not find %s in %s.", search_path, name); } else { crm_trace("Could not find %s in .", search_path); } return NULL; } #define attr_matches(c, n, v) pcmk__str_eq(crm_element_value((c), (n)), \ (v), pcmk__str_none) /*! * \internal * \brief Find first XML child element matching given criteria * * \param[in] parent XML element to search * \param[in] node_name If not NULL, only match children of this type * \param[in] attr_n If not NULL, only match children with an attribute * of this name. * \param[in] attr_v If \p attr_n and this are not NULL, only match children * with an attribute named \p attr_n and this value * * \return Matching XML child element, or NULL if none found */ xmlNode * pcmk__xe_match(const xmlNode *parent, const char *node_name, const char *attr_n, const char *attr_v) { CRM_CHECK(parent != NULL, return NULL); CRM_CHECK(attr_v == NULL || attr_n != NULL, return NULL); for (xmlNode *child = pcmk__xml_first_child(parent); child != NULL; child = pcmk__xml_next(child)) { if (pcmk__str_eq(node_name, (const char *) (child->name), pcmk__str_null_matches) && ((attr_n == NULL) || (attr_v == NULL && xmlHasProp(child, (pcmkXmlStr) attr_n)) || (attr_v != NULL && attr_matches(child, attr_n, attr_v)))) { return child; } } crm_trace("XML child node <%s%s%s%s%s> not found in %s", (node_name? node_name : "(any)"), (attr_n? " " : ""), (attr_n? attr_n : ""), (attr_n? "=" : ""), (attr_n? attr_v : ""), (const char *) parent->name); return NULL; } void copy_in_properties(xmlNode *target, const xmlNode *src) { if (src == NULL) { crm_warn("No node to copy properties from"); } else if (target == NULL) { crm_err("No node to copy properties into"); } else { for (xmlAttrPtr a = pcmk__xe_first_attr(src); a != NULL; a = a->next) { const char *p_name = (const char *) a->name; const char *p_value = pcmk__xml_attr_value(a); expand_plus_plus(target, p_name, p_value); if (xml_acl_denied(target)) { crm_trace("Cannot copy %s=%s to %s", p_name, p_value, target->name); return; } } } return; } /*! * \brief Parse integer assignment statements on this node and all its child * nodes * * \param[in,out] target Root XML node to be processed * * \note This function is recursive */ void fix_plus_plus_recursive(xmlNode *target) { /* TODO: Remove recursion and use xpath searches for value++ */ xmlNode *child = NULL; for (xmlAttrPtr a = pcmk__xe_first_attr(target); a != NULL; a = a->next) { const char *p_name = (const char *) a->name; const char *p_value = pcmk__xml_attr_value(a); expand_plus_plus(target, p_name, p_value); } for (child = pcmk__xml_first_child(target); child != NULL; child = pcmk__xml_next(child)) { fix_plus_plus_recursive(child); } } /*! * \brief Update current XML attribute value per parsed integer assignment statement * * \param[in,out] target an XML node, containing a XML attribute that is * initialized to some numeric value, to be processed * \param[in] name name of the XML attribute, e.g. X, whose value * should be updated * \param[in] value assignment statement, e.g. "X++" or * "X+=5", to be applied to the initialized value. * * \note The original XML attribute value is treated as 0 if non-numeric and * truncated to be an integer if decimal-point-containing. * \note The final XML attribute value is truncated to not exceed 1000000. * \note Undefined behavior if unexpected input. */ void expand_plus_plus(xmlNode * target, const char *name, const char *value) { int offset = 1; int name_len = 0; int int_value = 0; int value_len = 0; const char *old_value = NULL; if (target == NULL || value == NULL || name == NULL) { return; } old_value = crm_element_value(target, name); if (old_value == NULL) { /* if no previous value, set unexpanded */ goto set_unexpanded; } else if (strstr(value, name) != value) { goto set_unexpanded; } name_len = strlen(name); value_len = strlen(value); if (value_len < (name_len + 2) || value[name_len] != '+' || (value[name_len + 1] != '+' && value[name_len + 1] != '=')) { goto set_unexpanded; } /* if we are expanding ourselves, * then no previous value was set and leave int_value as 0 */ if (old_value != value) { int_value = char2score(old_value); } if (value[name_len + 1] != '+') { const char *offset_s = value + (name_len + 2); offset = char2score(offset_s); } int_value += offset; if (int_value > INFINITY) { int_value = (int)INFINITY; } crm_xml_add_int(target, name, int_value); return; set_unexpanded: if (old_value == value) { /* the old value is already set, nothing to do */ return; } crm_xml_add(target, name, value); return; } /*! * \internal * \brief Remove an XML element's attributes that match some criteria * * \param[in,out] element XML element to modify * \param[in] match If not NULL, only remove attributes for which * this function returns true * \param[in,out] user_data Data to pass to \p match */ void pcmk__xe_remove_matching_attrs(xmlNode *element, bool (*match)(xmlAttrPtr, void *), void *user_data) { xmlAttrPtr next = NULL; for (xmlAttrPtr a = pcmk__xe_first_attr(element); a != NULL; a = next) { next = a->next; // Grab now because attribute might get removed if ((match == NULL) || match(a, user_data)) { if (!pcmk__check_acl(element, NULL, pcmk__xf_acl_write)) { crm_trace("ACLs prevent removal of attributes (%s and " "possibly others) from %s element", (const char *) a->name, (const char *) element->name); return; // ACLs apply to element, not particular attributes } if (pcmk__tracking_xml_changes(element, false)) { // Leave (marked for removal) until after diff is calculated set_parent_flag(element, pcmk__xf_dirty); pcmk__set_xml_flags((xml_node_private_t *) a->_private, pcmk__xf_deleted); } else { xmlRemoveProp(a); } } } } xmlNode * add_node_copy(xmlNode * parent, xmlNode * src_node) { xmlNode *child = NULL; CRM_CHECK((parent != NULL) && (src_node != NULL), return NULL); child = xmlDocCopyNode(src_node, parent->doc, 1); if (child == NULL) { return NULL; } xmlAddChild(parent, child); pcmk__mark_xml_created(child); return child; } xmlNode * create_xml_node(xmlNode * parent, const char *name) { xmlDoc *doc = NULL; xmlNode *node = NULL; if (pcmk__str_empty(name)) { CRM_CHECK(name != NULL && name[0] == 0, return NULL); return NULL; } if (parent == NULL) { doc = xmlNewDoc((pcmkXmlStr) "1.0"); if (doc == NULL) { return NULL; } node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL); if (node == NULL) { xmlFreeDoc(doc); return NULL; } xmlDocSetRootElement(doc, node); } else { node = xmlNewChild(parent, NULL, (pcmkXmlStr) name, NULL); if (node == NULL) { return NULL; } } pcmk__mark_xml_created(node); return node; } xmlNode * pcmk_create_xml_text_node(xmlNode * parent, const char *name, const char *content) { xmlNode *node = create_xml_node(parent, name); if (node != NULL) { xmlNodeSetContent(node, (pcmkXmlStr) content); } return node; } xmlNode * pcmk_create_html_node(xmlNode * parent, const char *element_name, const char *id, const char *class_name, const char *text) { xmlNode *node = pcmk_create_xml_text_node(parent, element_name, text); if (class_name != NULL) { crm_xml_add(node, "class", class_name); } if (id != NULL) { crm_xml_add(node, "id", id); } return node; } /*! * Free an XML element and all of its children, removing it from its parent * * \param[in,out] xml XML element to free */ void pcmk_free_xml_subtree(xmlNode *xml) { xmlUnlinkNode(xml); // Detaches from parent and siblings xmlFreeNode(xml); // Frees } static void free_xml_with_position(xmlNode * child, int position) { if (child != NULL) { xmlNode *top = NULL; xmlDoc *doc = child->doc; xml_node_private_t *nodepriv = child->_private; xml_doc_private_t *docpriv = NULL; if (doc != NULL) { top = xmlDocGetRootElement(doc); } if (doc != NULL && top == child) { /* Free everything */ xmlFreeDoc(doc); } else if (pcmk__check_acl(child, NULL, pcmk__xf_acl_write) == FALSE) { GString *xpath = NULL; pcmk__if_tracing({}, return); xpath = pcmk__element_xpath(child); qb_log_from_external_source(__func__, __FILE__, "Cannot remove %s %x", LOG_TRACE, __LINE__, 0, (const char *) xpath->str, nodepriv->flags); g_string_free(xpath, TRUE); return; } else { if (doc && pcmk__tracking_xml_changes(child, FALSE) && !pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { GString *xpath = pcmk__element_xpath(child); if (xpath != NULL) { pcmk__deleted_xml_t *deleted_obj = NULL; crm_trace("Deleting %s %p from %p", (const char *) xpath->str, child, doc); deleted_obj = calloc(1, sizeof(pcmk__deleted_xml_t)); deleted_obj->path = strdup((const char *) xpath->str); CRM_ASSERT(deleted_obj->path != NULL); g_string_free(xpath, TRUE); deleted_obj->position = -1; /* Record the "position" only for XML comments for now */ if (child->type == XML_COMMENT_NODE) { if (position >= 0) { deleted_obj->position = position; } else { deleted_obj->position = pcmk__xml_position(child, pcmk__xf_skip); } } docpriv = doc->_private; docpriv->deleted_objs = g_list_append(docpriv->deleted_objs, deleted_obj); pcmk__set_xml_doc_flag(child, pcmk__xf_dirty); } } pcmk_free_xml_subtree(child); } } } void free_xml(xmlNode * child) { free_xml_with_position(child, -1); } xmlNode * copy_xml(xmlNode * src) { xmlDoc *doc = xmlNewDoc((pcmkXmlStr) "1.0"); xmlNode *copy = xmlDocCopyNode(src, doc, 1); CRM_ASSERT(copy != NULL); xmlDocSetRootElement(doc, copy); return copy; } xmlNode * string2xml(const char *input) { xmlNode *xml = NULL; xmlDocPtr output = NULL; xmlParserCtxtPtr ctxt = NULL; const xmlError *last_error = NULL; if (input == NULL) { crm_err("Can't parse NULL input"); return NULL; } /* create a parser context */ ctxt = xmlNewParserCtxt(); CRM_CHECK(ctxt != NULL, return NULL); xmlCtxtResetLastError(ctxt); xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err); output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL, PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER); if (output == NULL) { output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL, PCMK__XML_PARSE_OPTS_WITH_RECOVER); if (output) { crm_warn("Successfully recovered from XML errors " "(note: a future release will treat this as a fatal failure)"); } } if (output) { xml = xmlDocGetRootElement(output); } last_error = xmlCtxtGetLastError(ctxt); if (last_error && last_error->code != XML_ERR_OK) { /* crm_abort(__FILE__,__func__,__LINE__, "last_error->code != XML_ERR_OK", TRUE, TRUE); */ /* * http://xmlsoft.org/html/libxml-xmlerror.html#xmlErrorLevel * http://xmlsoft.org/html/libxml-xmlerror.html#xmlParserErrors */ crm_warn("Parsing failed (domain=%d, level=%d, code=%d): %s", last_error->domain, last_error->level, last_error->code, last_error->message); if (last_error->code == XML_ERR_DOCUMENT_EMPTY) { CRM_LOG_ASSERT("Cannot parse an empty string"); } else if (last_error->code != XML_ERR_DOCUMENT_END) { crm_err("Couldn't%s parse %d chars: %s", xml ? " fully" : "", (int)strlen(input), input); if (xml != NULL) { crm_log_xml_err(xml, "Partial"); } } else { int len = strlen(input); int lpc = 0; while(lpc < len) { crm_warn("Parse error[+%.3d]: %.80s", lpc, input+lpc); lpc += 80; } CRM_LOG_ASSERT("String parsing error"); } } xmlFreeParserCtxt(ctxt); return xml; } xmlNode * stdin2xml(void) { size_t data_length = 0; size_t read_chars = 0; char *xml_buffer = NULL; xmlNode *xml_obj = NULL; do { xml_buffer = pcmk__realloc(xml_buffer, data_length + PCMK__BUFFER_SIZE); read_chars = fread(xml_buffer + data_length, 1, PCMK__BUFFER_SIZE, stdin); data_length += read_chars; } while (read_chars == PCMK__BUFFER_SIZE); if (data_length == 0) { crm_warn("No XML supplied on stdin"); free(xml_buffer); return NULL; } xml_buffer[data_length] = '\0'; xml_obj = string2xml(xml_buffer); free(xml_buffer); crm_log_xml_trace(xml_obj, "Created fragment"); return xml_obj; } static char * decompress_file(const char *filename) { char *buffer = NULL; int rc = 0; size_t length = 0, read_len = 0; BZFILE *bz_file = NULL; FILE *input = fopen(filename, "r"); if (input == NULL) { crm_perror(LOG_ERR, "Could not open %s for reading", filename); return NULL; } bz_file = BZ2_bzReadOpen(&rc, input, 0, 0, NULL, 0); rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { crm_err("Could not prepare to read compressed %s: %s " CRM_XS " rc=%d", filename, pcmk_rc_str(rc), rc); BZ2_bzReadClose(&rc, bz_file); fclose(input); return NULL; } rc = BZ_OK; // cppcheck seems not to understand the abort-logic in pcmk__realloc // cppcheck-suppress memleak while (rc == BZ_OK) { buffer = pcmk__realloc(buffer, PCMK__BUFFER_SIZE + length + 1); read_len = BZ2_bzRead(&rc, bz_file, buffer + length, PCMK__BUFFER_SIZE); crm_trace("Read %ld bytes from file: %d", (long)read_len, rc); if (rc == BZ_OK || rc == BZ_STREAM_END) { length += read_len; } } buffer[length] = '\0'; rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { crm_err("Could not read compressed %s: %s " CRM_XS " rc=%d", filename, pcmk_rc_str(rc), rc); free(buffer); buffer = NULL; } BZ2_bzReadClose(&rc, bz_file); fclose(input); return buffer; } /*! * \internal * \brief Remove XML text nodes from specified XML and all its children * * \param[in,out] xml XML to strip text from */ void pcmk__strip_xml_text(xmlNode *xml) { xmlNode *iter = xml->children; while (iter) { xmlNode *next = iter->next; switch (iter->type) { case XML_TEXT_NODE: /* Remove it */ pcmk_free_xml_subtree(iter); break; case XML_ELEMENT_NODE: /* Search it */ pcmk__strip_xml_text(iter); break; default: /* Leave it */ break; } iter = next; } } xmlNode * filename2xml(const char *filename) { xmlNode *xml = NULL; xmlDocPtr output = NULL; bool uncompressed = true; xmlParserCtxtPtr ctxt = NULL; const xmlError *last_error = NULL; /* create a parser context */ ctxt = xmlNewParserCtxt(); CRM_CHECK(ctxt != NULL, return NULL); xmlCtxtResetLastError(ctxt); xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err); if (filename) { uncompressed = !pcmk__ends_with_ext(filename, ".bz2"); } if (pcmk__str_eq(filename, "-", pcmk__str_null_matches)) { /* STDIN_FILENO == fileno(stdin) */ output = xmlCtxtReadFd(ctxt, STDIN_FILENO, "unknown.xml", NULL, PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER); if (output == NULL) { output = xmlCtxtReadFd(ctxt, STDIN_FILENO, "unknown.xml", NULL, PCMK__XML_PARSE_OPTS_WITH_RECOVER); if (output) { crm_warn("Successfully recovered from XML errors " "(note: a future release will treat this as a fatal failure)"); } } } else if (uncompressed) { output = xmlCtxtReadFile(ctxt, filename, NULL, PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER); if (output == NULL) { output = xmlCtxtReadFile(ctxt, filename, NULL, PCMK__XML_PARSE_OPTS_WITH_RECOVER); if (output) { crm_warn("Successfully recovered from XML errors " "(note: a future release will treat this as a fatal failure)"); } } } else { char *input = decompress_file(filename); output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL, PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER); if (output == NULL) { output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL, PCMK__XML_PARSE_OPTS_WITH_RECOVER); if (output) { crm_warn("Successfully recovered from XML errors " "(note: a future release will treat this as a fatal failure)"); } } free(input); } if (output && (xml = xmlDocGetRootElement(output))) { pcmk__strip_xml_text(xml); } last_error = xmlCtxtGetLastError(ctxt); if (last_error && last_error->code != XML_ERR_OK) { /* crm_abort(__FILE__,__func__,__LINE__, "last_error->code != XML_ERR_OK", TRUE, TRUE); */ /* * http://xmlsoft.org/html/libxml-xmlerror.html#xmlErrorLevel * http://xmlsoft.org/html/libxml-xmlerror.html#xmlParserErrors */ crm_err("Parsing failed (domain=%d, level=%d, code=%d): %s", last_error->domain, last_error->level, last_error->code, last_error->message); if (last_error && last_error->code != XML_ERR_OK) { crm_err("Couldn't%s parse %s", xml ? " fully" : "", filename); if (xml != NULL) { crm_log_xml_err(xml, "Partial"); } } } xmlFreeParserCtxt(ctxt); return xml; } /*! * \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, XML_CIB_ATTR_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, XML_ATTR_ID, id); free(id); } /*! * \internal * \brief Write XML to a file stream * * \param[in] xml XML to write * \param[in] filename Name of file being written (for logging only) * \param[in,out] stream Open file stream corresponding to filename * \param[in] compress Whether to compress XML before writing * \param[out] nbytes Number of bytes written * * \return Standard Pacemaker return code */ static int write_xml_stream(const xmlNode *xml, const char *filename, FILE *stream, bool compress, unsigned int *nbytes) { int rc = pcmk_rc_ok; char *buffer = NULL; *nbytes = 0; crm_log_xml_trace(xml, "writing"); buffer = dump_xml_formatted(xml); CRM_CHECK(buffer && strlen(buffer), crm_log_xml_warn(xml, "formatting failed"); rc = pcmk_rc_error; goto bail); if (compress) { unsigned int in = 0; BZFILE *bz_file = NULL; rc = BZ_OK; bz_file = BZ2_bzWriteOpen(&rc, stream, 5, 0, 30); rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { crm_warn("Not compressing %s: could not prepare file stream: %s " CRM_XS " rc=%d", filename, pcmk_rc_str(rc), rc); } else { BZ2_bzWrite(&rc, bz_file, buffer, strlen(buffer)); rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { crm_warn("Not compressing %s: could not compress data: %s " CRM_XS " rc=%d errno=%d", filename, pcmk_rc_str(rc), rc, errno); } } if (rc == pcmk_rc_ok) { BZ2_bzWriteClose(&rc, bz_file, 0, &in, nbytes); rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { crm_warn("Not compressing %s: could not write compressed data: %s " CRM_XS " rc=%d errno=%d", filename, pcmk_rc_str(rc), rc, errno); *nbytes = 0; // retry without compression } else { crm_trace("Compressed XML for %s from %u bytes to %u", filename, in, *nbytes); } } rc = pcmk_rc_ok; // Either true, or we'll retry without compression } if (*nbytes == 0) { rc = fprintf(stream, "%s", buffer); if (rc < 0) { rc = errno; crm_perror(LOG_ERR, "writing %s", filename); } else { *nbytes = (unsigned int) rc; rc = pcmk_rc_ok; } } bail: if (fflush(stream) != 0) { rc = errno; crm_perror(LOG_ERR, "flushing %s", filename); } /* Don't report error if the file does not support synchronization */ if (fsync(fileno(stream)) < 0 && errno != EROFS && errno != EINVAL) { rc = errno; crm_perror(LOG_ERR, "synchronizing %s", filename); } fclose(stream); crm_trace("Saved %d bytes to %s as XML", *nbytes, filename); free(buffer); return rc; } /*! * \brief Write XML to a file descriptor * * \param[in] xml XML to write * \param[in] filename Name of file being written (for logging only) * \param[in] fd Open file descriptor corresponding to filename * \param[in] compress Whether to compress XML before writing * * \return Number of bytes written on success, -errno otherwise */ int write_xml_fd(const xmlNode *xml, const char *filename, int fd, gboolean compress) { FILE *stream = NULL; unsigned int nbytes = 0; int rc = pcmk_rc_ok; CRM_CHECK((xml != NULL) && (fd > 0), return -EINVAL); stream = fdopen(fd, "w"); if (stream == NULL) { return -errno; } rc = write_xml_stream(xml, filename, stream, compress, &nbytes); if (rc != pcmk_rc_ok) { return pcmk_rc2legacy(rc); } return (int) nbytes; } /*! * \brief Write XML to a file * * \param[in] xml XML to write * \param[in] filename Name of file to write * \param[in] compress Whether to compress XML before writing * * \return Number of bytes written on success, -errno otherwise */ int write_xml_file(const xmlNode *xml, const char *filename, gboolean compress) { FILE *stream = NULL; unsigned int nbytes = 0; int rc = pcmk_rc_ok; CRM_CHECK((xml != NULL) && (filename != NULL), return -EINVAL); stream = fopen(filename, "w"); if (stream == NULL) { return -errno; } rc = write_xml_stream(xml, filename, stream, compress, &nbytes); if (rc != pcmk_rc_ok) { return pcmk_rc2legacy(rc); } return (int) nbytes; } // Replace a portion of a dynamically allocated string (reallocating memory) static char * replace_text(char *text, int start, size_t *length, const char *replace) { size_t offset = strlen(replace) - 1; // We have space for 1 char already *length += offset; text = pcmk__realloc(text, *length); for (size_t lpc = (*length) - 1; lpc > (start + offset); lpc--) { text[lpc] = text[lpc - offset]; } memcpy(text + start, replace, offset + 1); return text; } /*! * \brief Replace special characters with their XML escape sequences * * \param[in] text Text to escape * * \return Newly allocated string equivalent to \p text but with special * characters replaced with XML escape sequences (or NULL if \p text * is NULL) */ char * crm_xml_escape(const char *text) { size_t length; char *copy; /* * When xmlCtxtReadDoc() parses < and friends in a * value, it converts them to their human readable * form. * * If one uses xmlNodeDump() to convert it back to a * string, all is well, because special characters are * converted back to their escape sequences. * * However xmlNodeDump() is randomly dog slow, even with the same * input. So we need to replicate the escaping in our custom * version so that the result can be re-parsed by xmlCtxtReadDoc() * when necessary. */ if (text == NULL) { return NULL; } length = 1 + strlen(text); copy = strdup(text); CRM_ASSERT(copy != NULL); for (size_t index = 0; index < length; index++) { if(copy[index] & 0x80 && copy[index+1] & 0x80){ index++; break; } switch (copy[index]) { case 0: 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; } /*! * \internal * \brief Append a string representation of an XML element to a buffer * * \param[in] data XML whose representation to append * \param[in] options Group of \p pcmk__xml_fmt_options flags * \param[in,out] buffer Where to append the content (must not be \p NULL) * \param[in] depth Current indentation level */ static void dump_xml_element(const xmlNode *data, uint32_t options, GString *buffer, int depth) { bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); bool filtered = pcmk_is_set(options, pcmk__xml_fmt_filtered); int spaces = pretty? (2 * depth) : 0; for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } pcmk__g_strcat(buffer, "<", data->name, NULL); for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL; attr = attr->next) { if (!filtered || !pcmk__xa_filterable((const char *) (attr->name))) { pcmk__dump_xml_attr(attr, buffer); } } if (data->children == NULL) { g_string_append(buffer, "/>"); } else { g_string_append_c(buffer, '>'); } if (pretty) { g_string_append_c(buffer, '\n'); } if (data->children) { for (const xmlNode *child = data->children; child != NULL; child = child->next) { pcmk__xml2text(child, options, buffer, depth + 1); } for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } pcmk__g_strcat(buffer, "name, ">", NULL); if (pretty) { g_string_append_c(buffer, '\n'); } } } /*! * \internal * \brief Append XML text content to a buffer * * \param[in] data XML whose content to append * \param[in] options Group of \p xml_log_options flags * \param[in,out] buffer Where to append the content (must not be \p NULL) * \param[in] depth Current indentation level */ static void dump_xml_text(const xmlNode *data, uint32_t options, GString *buffer, int depth) { /* @COMPAT: Remove when log_data_element() is removed. There are no internal * code paths to this, except through the deprecated log_data_element(). */ bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); int spaces = pretty? (2 * depth) : 0; for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } g_string_append(buffer, (const gchar *) data->content); if (pretty) { g_string_append_c(buffer, '\n'); } } /*! * \internal * \brief Append XML CDATA content to a buffer * * \param[in] data XML whose content to append * \param[in] options Group of \p pcmk__xml_fmt_options flags * \param[in,out] buffer Where to append the content (must not be \p NULL) * \param[in] depth Current indentation level */ static void dump_xml_cdata(const xmlNode *data, uint32_t options, GString *buffer, int depth) { bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); int spaces = pretty? (2 * depth) : 0; for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } pcmk__g_strcat(buffer, "content, "]]>", NULL); if (pretty) { g_string_append_c(buffer, '\n'); } } /*! * \internal * \brief Append an XML comment to a buffer * * \param[in] data XML whose content to append * \param[in] options Group of \p pcmk__xml_fmt_options flags * \param[in,out] buffer Where to append the content (must not be \p NULL) * \param[in] depth Current indentation level */ static void dump_xml_comment(const xmlNode *data, uint32_t options, GString *buffer, int depth) { bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); int spaces = pretty? (2 * depth) : 0; for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } pcmk__g_strcat(buffer, "", NULL); if (pretty) { g_string_append_c(buffer, '\n'); } } /*! * \internal * \brief Get a string representation of an XML element type * * \param[in] type XML element type * * \return String representation of \p type */ static const char * xml_element_type2str(xmlElementType type) { static const char *const element_type_names[] = { [XML_ELEMENT_NODE] = "element", [XML_ATTRIBUTE_NODE] = "attribute", [XML_TEXT_NODE] = "text", [XML_CDATA_SECTION_NODE] = "CDATA section", [XML_ENTITY_REF_NODE] = "entity reference", [XML_ENTITY_NODE] = "entity", [XML_PI_NODE] = "PI", [XML_COMMENT_NODE] = "comment", [XML_DOCUMENT_NODE] = "document", [XML_DOCUMENT_TYPE_NODE] = "document type", [XML_DOCUMENT_FRAG_NODE] = "document fragment", [XML_NOTATION_NODE] = "notation", [XML_HTML_DOCUMENT_NODE] = "HTML document", [XML_DTD_NODE] = "DTD", [XML_ELEMENT_DECL] = "element declaration", [XML_ATTRIBUTE_DECL] = "attribute declaration", [XML_ENTITY_DECL] = "entity declaration", [XML_NAMESPACE_DECL] = "namespace declaration", [XML_XINCLUDE_START] = "XInclude start", [XML_XINCLUDE_END] = "XInclude end", }; if ((type < 0) || (type >= PCMK__NELEM(element_type_names))) { return "unrecognized type"; } return element_type_names[type]; } /*! * \internal * \brief Create a text representation of an XML object * * \param[in] data XML to convert * \param[in] options Group of \p pcmk__xml_fmt_options flags * \param[in,out] buffer Where to store the text (must not be \p NULL) * \param[in] depth Current indentation level */ void pcmk__xml2text(const xmlNode *data, uint32_t options, GString *buffer, int depth) { if (data == NULL) { crm_trace("Nothing to dump"); return; } CRM_ASSERT(buffer != NULL); CRM_CHECK(depth >= 0, depth = 0); switch(data->type) { case XML_ELEMENT_NODE: /* Handle below */ dump_xml_element(data, options, buffer, depth); break; case XML_TEXT_NODE: if (pcmk_is_set(options, pcmk__xml_fmt_text)) { dump_xml_text(data, options, buffer, depth); } break; case XML_COMMENT_NODE: dump_xml_comment(data, options, buffer, depth); break; case XML_CDATA_SECTION_NODE: dump_xml_cdata(data, options, buffer, depth); break; default: crm_warn("Cannot convert XML %s node to text " CRM_XS " type=%d", xml_element_type2str(data->type), data->type); break; } } char * dump_xml_formatted_with_text(const xmlNode *xml) { /* libxml's xmlNodeDumpOutput() would work here since we're not specifically * filtering out any nodes. However, use pcmk__xml2text() for consistency, * to escape attribute values, and to allow a const argument. */ char *buffer = NULL; GString *g_buffer = g_string_sized_new(1024); pcmk__xml2text(xml, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, g_buffer, 0); pcmk__str_update(&buffer, g_buffer->str); g_string_free(g_buffer, TRUE); return buffer; } char * dump_xml_formatted(const xmlNode *xml) { char *buffer = NULL; GString *g_buffer = g_string_sized_new(1024); pcmk__xml2text(xml, pcmk__xml_fmt_pretty, g_buffer, 0); pcmk__str_update(&buffer, g_buffer->str); g_string_free(g_buffer, TRUE); return buffer; } char * dump_xml_unformatted(const xmlNode *xml) { char *buffer = NULL; GString *g_buffer = g_string_sized_new(1024); pcmk__xml2text(xml, 0, g_buffer, 0); pcmk__str_update(&buffer, g_buffer->str); g_string_free(g_buffer, TRUE); return buffer; } int pcmk__xml2fd(int fd, xmlNode *cur) { bool success; xmlOutputBuffer *fd_out = xmlOutputBufferCreateFd(fd, NULL); CRM_ASSERT(fd_out != NULL); xmlNodeDumpOutput(fd_out, cur->doc, cur, 0, pcmk__xml_fmt_pretty, NULL); success = xmlOutputBufferWrite(fd_out, sizeof("\n") - 1, "\n") != -1; success = xmlOutputBufferClose(fd_out) != -1 && success; if (!success) { return EIO; } fsync(fd); return pcmk_rc_ok; } void xml_remove_prop(xmlNode * obj, const char *name) { if (crm_element_value(obj, name) == NULL) { return; } if (pcmk__check_acl(obj, NULL, pcmk__xf_acl_write) == FALSE) { crm_trace("Cannot remove %s from %s", name, obj->name); } else if (pcmk__tracking_xml_changes(obj, FALSE)) { /* Leave in place (marked for removal) until after the diff is calculated */ xmlAttr *attr = xmlHasProp(obj, (pcmkXmlStr) name); xml_node_private_t *nodepriv = attr->_private; set_parent_flag(obj, pcmk__xf_dirty); pcmk__set_xml_flags(nodepriv, pcmk__xf_deleted); } else { xmlUnsetProp(obj, (pcmkXmlStr) name); } } void save_xml_to_file(const xmlNode *xml, const char *desc, const char *filename) { char *f = NULL; if (filename == NULL) { char *uuid = crm_generate_uuid(); f = crm_strdup_printf("%s/%s", pcmk__get_tmpdir(), uuid); filename = f; free(uuid); } crm_info("Saving %s to %s", desc, filename); write_xml_file(xml, filename, FALSE); free(f); } /*! * \internal * \brief Set a flag on all attributes of an XML element * * \param[in,out] xml XML node to set flags on * \param[in] flag XML private flag to set */ static void set_attrs_flag(xmlNode *xml, enum xml_private_flags flag) { for (xmlAttr *attr = pcmk__xe_first_attr(xml); attr; attr = attr->next) { pcmk__set_xml_flags((xml_node_private_t *) (attr->_private), flag); } } /*! * \internal * \brief Add an XML attribute to a node, marked as deleted * * When calculating XML changes, we need to know when an attribute has been * deleted. Add the attribute back to the new XML, so that we can check the * removal against ACLs, and mark it as deleted for later removal after * differences have been calculated. * * \param[in,out] new_xml XML to modify * \param[in] element Name of XML element that changed (for logging) * \param[in] attr_name Name of attribute that was deleted * \param[in] old_value Value of attribute that was deleted */ static void mark_attr_deleted(xmlNode *new_xml, const char *element, const char *attr_name, const char *old_value) { xml_doc_private_t *docpriv = new_xml->doc->_private; xmlAttr *attr = NULL; xml_node_private_t *nodepriv; // Prevent the dirty flag being set recursively upwards pcmk__clear_xml_flags(docpriv, pcmk__xf_tracking); // Restore the old value (and the tracking flag) attr = xmlSetProp(new_xml, (pcmkXmlStr) attr_name, (pcmkXmlStr) old_value); pcmk__set_xml_flags(docpriv, pcmk__xf_tracking); // Reset flags (so the attribute doesn't appear as newly created) nodepriv = attr->_private; nodepriv->flags = 0; // Check ACLs and mark restored value for later removal xml_remove_prop(new_xml, attr_name); crm_trace("XML attribute %s=%s was removed from %s", attr_name, old_value, element); } /* * \internal * \brief Check ACLs for a changed XML attribute */ static void mark_attr_changed(xmlNode *new_xml, const char *element, const char *attr_name, const char *old_value) { char *vcopy = crm_element_value_copy(new_xml, attr_name); crm_trace("XML attribute %s was changed from '%s' to '%s' in %s", attr_name, old_value, vcopy, element); // Restore the original value xmlSetProp(new_xml, (pcmkXmlStr) attr_name, (pcmkXmlStr) old_value); // Change it back to the new value, to check ACLs crm_xml_add(new_xml, attr_name, vcopy); free(vcopy); } /*! * \internal * \brief Mark an XML attribute as having changed position * * \param[in,out] new_xml XML to modify * \param[in] element Name of XML element that changed (for logging) * \param[in,out] old_attr Attribute that moved, in original XML * \param[in,out] new_attr Attribute that moved, in \p new_xml * \param[in] p_old Ordinal position of \p old_attr in original XML * \param[in] p_new Ordinal position of \p new_attr in \p new_xml */ static void mark_attr_moved(xmlNode *new_xml, const char *element, xmlAttr *old_attr, xmlAttr *new_attr, int p_old, int p_new) { xml_node_private_t *nodepriv = new_attr->_private; crm_trace("XML attribute %s moved from position %d to %d in %s", old_attr->name, p_old, p_new, element); // Mark document, element, and all element's parents as changed pcmk__mark_xml_node_dirty(new_xml); // Mark attribute as changed pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_moved); nodepriv = (p_old > p_new)? old_attr->_private : new_attr->_private; pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); } /*! * \internal * \brief Calculate differences in all previously existing XML attributes * * \param[in,out] old_xml Original XML to compare * \param[in,out] new_xml New XML to compare */ static void xml_diff_old_attrs(xmlNode *old_xml, xmlNode *new_xml) { xmlAttr *attr_iter = pcmk__xe_first_attr(old_xml); while (attr_iter != NULL) { const char *name = (const char *) attr_iter->name; xmlAttr *old_attr = attr_iter; xmlAttr *new_attr = xmlHasProp(new_xml, attr_iter->name); const char *old_value = pcmk__xml_attr_value(attr_iter); attr_iter = attr_iter->next; if (new_attr == NULL) { mark_attr_deleted(new_xml, (const char *) old_xml->name, name, old_value); } else { xml_node_private_t *nodepriv = new_attr->_private; int new_pos = pcmk__xml_position((xmlNode*) new_attr, pcmk__xf_skip); int old_pos = pcmk__xml_position((xmlNode*) old_attr, pcmk__xf_skip); const char *new_value = crm_element_value(new_xml, name); // This attribute isn't new pcmk__clear_xml_flags(nodepriv, pcmk__xf_created); if (strcmp(new_value, old_value) != 0) { mark_attr_changed(new_xml, (const char *) old_xml->name, name, old_value); } else if ((old_pos != new_pos) && !pcmk__tracking_xml_changes(new_xml, TRUE)) { mark_attr_moved(new_xml, (const char *) old_xml->name, old_attr, new_attr, old_pos, new_pos); } } } } /*! * \internal * \brief Check all attributes in new XML for creation * * For each of a given XML element's attributes marked as newly created, accept * (and mark as dirty) or reject the creation according to ACLs. * * \param[in,out] new_xml XML to check */ static void mark_created_attrs(xmlNode *new_xml) { xmlAttr *attr_iter = pcmk__xe_first_attr(new_xml); while (attr_iter != NULL) { xmlAttr *new_attr = attr_iter; xml_node_private_t *nodepriv = attr_iter->_private; attr_iter = attr_iter->next; if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { const char *attr_name = (const char *) new_attr->name; crm_trace("Created new attribute %s=%s in %s", attr_name, pcmk__xml_attr_value(new_attr), new_xml->name); /* Check ACLs (we can't use the remove-then-create trick because it * would modify the attribute position). */ if (pcmk__check_acl(new_xml, attr_name, pcmk__xf_acl_write)) { pcmk__mark_xml_attr_dirty(new_attr); } else { // Creation was not allowed, so remove the attribute xmlUnsetProp(new_xml, new_attr->name); } } } } /*! * \internal * \brief Calculate differences in attributes between two XML nodes * * \param[in,out] old_xml Original XML to compare * \param[in,out] new_xml New XML to compare */ static void xml_diff_attrs(xmlNode *old_xml, xmlNode *new_xml) { set_attrs_flag(new_xml, pcmk__xf_created); // cleared later if not really new xml_diff_old_attrs(old_xml, new_xml); mark_created_attrs(new_xml); } /*! * \internal * \brief Add an XML child element to a node, marked as deleted * * When calculating XML changes, we need to know when a child element has been * deleted. Add the child back to the new XML, so that we can check the removal * against ACLs, and mark it as deleted for later removal after differences have * been calculated. * * \param[in,out] old_child Child element from original XML * \param[in,out] new_parent New XML to add marked copy to */ static void mark_child_deleted(xmlNode *old_child, xmlNode *new_parent) { // Re-create the child element so we can check ACLs xmlNode *candidate = add_node_copy(new_parent, old_child); // Clear flags on new child and its children reset_xml_node_flags(candidate); // Check whether ACLs allow the deletion pcmk__apply_acl(xmlDocGetRootElement(candidate->doc)); // Remove the child again (which will track it in document's deleted_objs) free_xml_with_position(candidate, pcmk__xml_position(old_child, pcmk__xf_skip)); if (pcmk__xml_match(new_parent, old_child, true) == NULL) { pcmk__set_xml_flags((xml_node_private_t *) (old_child->_private), pcmk__xf_skip); } } static void mark_child_moved(xmlNode *old_child, xmlNode *new_parent, xmlNode *new_child, int p_old, int p_new) { xml_node_private_t *nodepriv = new_child->_private; crm_trace("Child element %s with id='%s' moved from position %d to %d under %s", new_child->name, (ID(new_child)? ID(new_child) : ""), p_old, p_new, new_parent->name); pcmk__mark_xml_node_dirty(new_parent); pcmk__set_xml_flags(nodepriv, pcmk__xf_moved); if (p_old > p_new) { nodepriv = old_child->_private; } else { nodepriv = new_child->_private; } pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); } // Given original and new XML, mark new XML portions that have changed static void mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top) { xmlNode *cIter = NULL; xml_node_private_t *nodepriv = NULL; CRM_CHECK(new_xml != NULL, return); if (old_xml == NULL) { pcmk__mark_xml_created(new_xml); pcmk__apply_creation_acl(new_xml, check_top); return; } nodepriv = new_xml->_private; CRM_CHECK(nodepriv != NULL, return); if(nodepriv->flags & pcmk__xf_processed) { /* Avoid re-comparing nodes */ return; } pcmk__set_xml_flags(nodepriv, pcmk__xf_processed); xml_diff_attrs(old_xml, new_xml); // Check for differences in the original children for (cIter = pcmk__xml_first_child(old_xml); cIter != NULL; ) { xmlNode *old_child = cIter; xmlNode *new_child = pcmk__xml_match(new_xml, cIter, true); cIter = pcmk__xml_next(cIter); if(new_child) { mark_xml_changes(old_child, new_child, TRUE); } else { mark_child_deleted(old_child, new_xml); } } // Check for moved or created children for (cIter = pcmk__xml_first_child(new_xml); cIter != NULL; ) { xmlNode *new_child = cIter; xmlNode *old_child = pcmk__xml_match(old_xml, cIter, true); cIter = pcmk__xml_next(cIter); if(old_child == NULL) { // This is a newly created child nodepriv = new_child->_private; pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); mark_xml_changes(old_child, new_child, TRUE); } else { /* Check for movement, we already checked for differences */ int p_new = pcmk__xml_position(new_child, pcmk__xf_skip); int p_old = pcmk__xml_position(old_child, pcmk__xf_skip); if(p_old != p_new) { mark_child_moved(old_child, new_xml, new_child, p_old, p_new); } } } } void xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml) { pcmk__set_xml_doc_flag(new_xml, pcmk__xf_lazy); xml_calculate_changes(old_xml, new_xml); } // Called functions may set the \p pcmk__xf_skip flag on parts of \p old_xml void xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml) { CRM_CHECK((old_xml != NULL) && (new_xml != NULL) && pcmk__xe_is(old_xml, (const char *) new_xml->name) && pcmk__str_eq(ID(old_xml), 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); } gboolean can_prune_leaf(xmlNode * xml_node) { xmlNode *cIter = NULL; gboolean can_prune = TRUE; CRM_CHECK(xml_node != NULL, return FALSE); if (pcmk__strcase_any_of((const char *) xml_node->name, XML_TAG_RESOURCE_REF, XML_CIB_TAG_OBJ_REF, XML_ACL_TAG_ROLE_REF, XML_ACL_TAG_ROLE_REFv1, NULL)) { return FALSE; } for (xmlAttrPtr a = pcmk__xe_first_attr(xml_node); a != NULL; a = a->next) { const char *p_name = (const char *) a->name; if (strcmp(p_name, XML_ATTR_ID) == 0) { continue; } can_prune = FALSE; } cIter = pcmk__xml_first_child(xml_node); while (cIter) { xmlNode *child = cIter; cIter = pcmk__xml_next(cIter); if (can_prune_leaf(child)) { free_xml(child); } else { can_prune = FALSE; } } return can_prune; } /*! * \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) { add_node_copy(parent, update); } else if (!pcmk__str_eq((const char *)target->content, (const char *)update->content, pcmk__str_casei)) { xmlFree(target->content); target->content = xmlStrdup(update->content); } } /*! * \internal * \brief Make one XML tree match another (in children and attributes) * * \param[in,out] parent If \p target is NULL and this is not, add or update * child of this XML node that matches \p update * \param[in,out] target If not NULL, update this XML * \param[in] update Make the desired XML match this (must not be NULL) * \param[in] as_diff If false, expand "++" when making attributes match * * \note At least one of \p parent and \p target must be non-NULL */ void pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update, bool as_diff) { xmlNode *a_child = NULL; const char *object_name = NULL, *object_href = NULL, *object_href_val = NULL; #if XML_PARSER_DEBUG crm_log_xml_trace(update, "update:"); crm_log_xml_trace(target, "target:"); #endif CRM_CHECK(update != NULL, return); if (update->type == XML_COMMENT_NODE) { pcmk__xc_update(parent, target, update); return; } object_name = (const char *) update->name; object_href_val = ID(update); if (object_href_val != NULL) { object_href = XML_ATTR_ID; } else { object_href_val = crm_element_value(update, XML_ATTR_IDREF); object_href = (object_href_val == NULL) ? NULL : XML_ATTR_IDREF; } CRM_CHECK(object_name != NULL, return); CRM_CHECK(target != NULL || parent != NULL, return); if (target == NULL) { target = pcmk__xe_match(parent, object_name, object_href, object_href_val); } if (target == NULL) { target = create_xml_node(parent, object_name); CRM_CHECK(target != NULL, return); #if XML_PARSER_DEBUG crm_trace("Added <%s%s%s%s%s/>", pcmk__s(object_name, ""), object_href ? " " : "", object_href ? object_href : "", object_href ? "=" : "", object_href ? object_href_val : ""); } else { crm_trace("Found node <%s%s%s%s%s/> to update", pcmk__s(object_name, ""), object_href ? " " : "", object_href ? object_href : "", object_href ? "=" : "", object_href ? object_href_val : ""); #endif } CRM_CHECK(pcmk__xe_is(target, (const char *) update->name), return); if (as_diff == FALSE) { /* So that expand_plus_plus() gets called */ copy_in_properties(target, update); } else { /* No need for expand_plus_plus(), just raw speed */ for (xmlAttrPtr a = pcmk__xe_first_attr(update); a != NULL; a = a->next) { const char *p_value = pcmk__xml_attr_value(a); /* Remove it first so the ordering of the update is preserved */ xmlUnsetProp(target, a->name); xmlSetProp(target, a->name, (pcmkXmlStr) p_value); } } for (a_child = pcmk__xml_first_child(update); a_child != NULL; a_child = pcmk__xml_next(a_child)) { #if XML_PARSER_DEBUG crm_trace("Updating child <%s%s%s%s%s/>", pcmk__s(object_name, ""), object_href ? " " : "", object_href ? object_href : "", object_href ? "=" : "", object_href ? object_href_val : ""); #endif pcmk__xml_update(target, NULL, a_child, as_diff); } #if XML_PARSER_DEBUG crm_trace("Finished with <%s%s%s%s%s/>", pcmk__s(object_name, ""), object_href ? " " : "", object_href ? object_href : "", object_href ? "=" : "", object_href ? object_href_val : ""); #endif } gboolean update_xml_child(xmlNode * child, xmlNode * to_update) { gboolean can_update = TRUE; xmlNode *child_of_child = NULL; CRM_CHECK(child != NULL, return FALSE); CRM_CHECK(to_update != NULL, return FALSE); if (!pcmk__xe_is(to_update, (const char *) child->name)) { can_update = FALSE; } else if (!pcmk__str_eq(ID(to_update), ID(child), pcmk__str_none)) { can_update = FALSE; } else if (can_update) { #if XML_PARSER_DEBUG crm_log_xml_trace(child, "Update match found..."); #endif pcmk__xml_update(NULL, child, to_update, false); } for (child_of_child = pcmk__xml_first_child(child); child_of_child != NULL; child_of_child = pcmk__xml_next(child_of_child)) { /* only update the first one */ if (can_update) { break; } can_update = update_xml_child(child_of_child, to_update); } return can_update; } int find_xml_children(xmlNode ** children, xmlNode * root, const char *tag, const char *field, const char *value, gboolean search_matches) { int match_found = 0; CRM_CHECK(root != NULL, return FALSE); CRM_CHECK(children != NULL, return FALSE); if ((tag != NULL) && !pcmk__xe_is(root, tag)) { } else if (value != NULL && !pcmk__str_eq(value, crm_element_value(root, field), pcmk__str_casei)) { } else { if (*children == NULL) { *children = create_xml_node(NULL, __func__); } add_node_copy(*children, root); match_found = 1; } if (search_matches || match_found == 0) { xmlNode *child = NULL; for (child = pcmk__xml_first_child(root); child != NULL; child = pcmk__xml_next(child)) { match_found += find_xml_children(children, child, tag, field, value, search_matches); } } return match_found; } gboolean replace_xml_child(xmlNode * parent, xmlNode * child, xmlNode * update, gboolean delete_only) { gboolean can_delete = FALSE; xmlNode *child_of_child = NULL; const char *up_id = NULL; const char *child_id = NULL; const char *right_val = NULL; CRM_CHECK(child != NULL, return FALSE); CRM_CHECK(update != NULL, return FALSE); up_id = ID(update); child_id = ID(child); if (up_id == NULL || (child_id && strcmp(child_id, up_id) == 0)) { can_delete = TRUE; } if (!pcmk__xe_is(update, (const char *) child->name)) { can_delete = FALSE; } if (can_delete && delete_only) { for (xmlAttrPtr a = pcmk__xe_first_attr(update); a != NULL; a = a->next) { const char *p_name = (const char *) a->name; const char *p_value = pcmk__xml_attr_value(a); right_val = crm_element_value(child, p_name); if (!pcmk__str_eq(p_value, right_val, pcmk__str_casei)) { can_delete = FALSE; } } } if (can_delete && parent != NULL) { crm_log_xml_trace(child, "Delete match found..."); if (delete_only || update == NULL) { free_xml(child); } else { xmlNode *old = child; xmlNode *new = xmlCopyNode(update, 1); CRM_ASSERT(new != NULL); // May be unnecessary but avoids slight changes to some test outputs reset_xml_node_flags(new); old = xmlReplaceNode(old, new); if (xml_tracking_changes(new)) { // Replaced sections may have included relevant ACLs pcmk__apply_acl(new); } xml_calculate_changes(old, new); xmlFreeNode(old); } return TRUE; } else if (can_delete) { crm_log_xml_debug(child, "Cannot delete the search root"); can_delete = FALSE; } child_of_child = pcmk__xml_first_child(child); while (child_of_child) { xmlNode *next = pcmk__xml_next(child_of_child); can_delete = replace_xml_child(child, child_of_child, update, delete_only); /* only delete the first one */ if (can_delete) { child_of_child = NULL; } else { child_of_child = next; } } return can_delete; } xmlNode * sorted_xml(xmlNode *input, xmlNode *parent, gboolean recursive) { xmlNode *child = NULL; GSList *nvpairs = NULL; xmlNode *result = NULL; CRM_CHECK(input != NULL, return NULL); result = create_xml_node(parent, (const char *) input->name); nvpairs = pcmk_xml_attrs2nvpairs(input); nvpairs = pcmk_sort_nvpairs(nvpairs); pcmk_nvpairs2xml_attrs(nvpairs, result); pcmk_free_nvpairs(nvpairs); for (child = pcmk__xml_first_child(input); child != NULL; child = pcmk__xml_next(child)) { if (recursive) { sorted_xml(child, result, recursive); } else { add_node_copy(result, child); } } return result; } xmlNode * first_named_child(const xmlNode *parent, const char *name) { xmlNode *match = NULL; for (match = pcmk__xe_first_child(parent); match != NULL; match = pcmk__xe_next(match)) { /* * name == NULL gives first child regardless of name; this is * semantically incorrect in this function, but may be necessary * due to prior use of xml_child_iter_filter */ if (pcmk__str_eq(name, (const char *)match->name, pcmk__str_null_matches)) { return match; } } return NULL; } /*! * \brief Get next instance of same XML tag * * \param[in] sibling XML tag to start from * * \return Next sibling XML tag with same name */ xmlNode * crm_next_same_xml(const xmlNode *sibling) { xmlNode *match = pcmk__xe_next(sibling); while (match != NULL) { if (pcmk__xe_is(match, (const char *) sibling->name)) { return match; } match = pcmk__xe_next(match); } return NULL; } void crm_xml_init(void) { static bool init = true; if(init) { init = false; /* The default allocator XML_BUFFER_ALLOC_EXACT does far too many * pcmk__realloc()s and it can take upwards of 18 seconds (yes, seconds) * to dump a 28kb tree which XML_BUFFER_ALLOC_DOUBLEIT can do in * less than 1 second. */ xmlSetBufferAllocationScheme(XML_BUFFER_ALLOC_DOUBLEIT); /* Populate and free the _private field when nodes are created and destroyed */ xmlDeregisterNodeDefault(free_private_data); xmlRegisterNodeDefault(new_private_data); crm_schema_init(); } } void crm_xml_cleanup(void) { crm_schema_cleanup(); xmlCleanupParser(); } #define XPATH_MAX 512 xmlNode * expand_idref(xmlNode * input, xmlNode * top) { const char *ref = NULL; xmlNode *result = input; if (result == NULL) { return NULL; } else if (top == NULL) { top = input; } ref = crm_element_value(result, XML_ATTR_IDREF); if (ref != NULL) { char *xpath_string = crm_strdup_printf("//%s[@" XML_ATTR_ID "='%s']", result->name, ref); - result = get_xpath_object(xpath_string, top, LOG_ERR); - if (result == NULL) { + result = get_xpath_object(xpath_string, top, LOG_DEBUG); + if (result == NULL) { // Not possible with schema validation enabled char *nodePath = (char *)xmlGetNodePath(top); - crm_err("No match for %s found in %s: Invalid configuration", - xpath_string, pcmk__s(nodePath, "unrecognizable path")); + pcmk__config_err("Ignoring invalid %s configuration: " + XML_ATTR_IDREF " '%s' does not reference " + "a valid object", result->name, ref); free(nodePath); } free(xpath_string); } return result; } char * pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns) { static const char *base = NULL; char *ret = NULL; if (base == NULL) { base = pcmk__env_option(PCMK__ENV_SCHEMA_DIRECTORY); } if (pcmk__str_empty(base)) { base = CRM_SCHEMA_DIRECTORY; } switch (ns) { case pcmk__xml_artefact_ns_legacy_rng: case pcmk__xml_artefact_ns_legacy_xslt: ret = strdup(base); break; case pcmk__xml_artefact_ns_base_rng: case pcmk__xml_artefact_ns_base_xslt: ret = crm_strdup_printf("%s/base", base); break; default: crm_err("XML artefact family specified as %u not recognized", ns); } return ret; } static char * find_artefact(enum pcmk__xml_artefact_ns ns, const char *path, const char *filespec) { char *ret = NULL; switch (ns) { case pcmk__xml_artefact_ns_legacy_rng: case pcmk__xml_artefact_ns_base_rng: if (pcmk__ends_with(filespec, ".rng")) { ret = crm_strdup_printf("%s/%s", path, filespec); } else { ret = crm_strdup_printf("%s/%s.rng", path, filespec); } break; case pcmk__xml_artefact_ns_legacy_xslt: case pcmk__xml_artefact_ns_base_xslt: if (pcmk__ends_with(filespec, ".xsl")) { ret = crm_strdup_printf("%s/%s", path, filespec); } else { ret = crm_strdup_printf("%s/%s.xsl", path, filespec); } break; default: crm_err("XML artefact family specified as %u not recognized", ns); } return ret; } char * pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec) { struct stat sb; char *base = pcmk__xml_artefact_root(ns); char *ret = NULL; ret = find_artefact(ns, base, filespec); free(base); if (stat(ret, &sb) != 0 || !S_ISREG(sb.st_mode)) { const char *remote_schema_dir = pcmk__remote_schema_dir(); ret = find_artefact(ns, remote_schema_dir, filespec); } return ret; } void pcmk__xe_set_propv(xmlNodePtr node, va_list pairs) { while (true) { const char *name, *value; name = va_arg(pairs, const char *); if (name == NULL) { return; } value = va_arg(pairs, const char *); if (value != NULL) { crm_xml_add(node, name, value); } } } void pcmk__xe_set_props(xmlNodePtr node, ...) { va_list pairs; va_start(pairs, node); pcmk__xe_set_propv(node, pairs); va_end(pairs); } int pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name, int (*handler)(xmlNode *xml, void *userdata), void *userdata) { xmlNode *children = (xml? xml->children : NULL); CRM_ASSERT(handler != NULL); for (xmlNode *node = children; node != NULL; node = node->next) { if (node->type == XML_ELEMENT_NODE && pcmk__str_eq(child_element_name, (const char *) node->name, pcmk__str_null_matches)) { int rc = handler(node, userdata); if (rc != pcmk_rc_ok) { return rc; } } } return pcmk_rc_ok; } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include xmlNode * find_entity(xmlNode *parent, const char *node_name, const char *id) { return pcmk__xe_match(parent, node_name, ((id == NULL)? id : XML_ATTR_ID), id); } void crm_destroy_xml(gpointer data) { free_xml(data); } xmlDoc * getDocPtr(xmlNode *node) { xmlDoc *doc = NULL; CRM_CHECK(node != NULL, return NULL); doc = node->doc; if (doc == NULL) { doc = xmlNewDoc((pcmkXmlStr) "1.0"); xmlDocSetRootElement(doc, node); } return doc; } int add_node_nocopy(xmlNode *parent, const char *name, xmlNode *child) { add_node_copy(parent, child); free_xml(child); return 1; } gboolean xml_has_children(const xmlNode * xml_root) { if (xml_root != NULL && xml_root->children != NULL) { return TRUE; } return FALSE; } // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/pacemaker/pcmk_sched_location.c b/lib/pacemaker/pcmk_sched_location.c index 9730b47a5f..dbcfdb6133 100644 --- a/lib/pacemaker/pcmk_sched_location.c +++ b/lib/pacemaker/pcmk_sched_location.c @@ -1,715 +1,714 @@ /* * Copyright 2004-2023 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" 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_err("Rule %s: no score specified (assuming 0)", rule); } else if (raw) { score_f = char2score(score); } else { const char *attr_score = NULL; attr_score = pe__node_attribute_calculated(node, score, rsc, pcmk__rsc_node_current, false); if (attr_score == NULL) { crm_debug("Rule %s: %s did not have a value for %s", rule, pe__node_name(node), score); score_f = -INFINITY; } else { crm_debug("Rule %s: %s had value %s for %s", rule, pe__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 = text2role(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 resource-discovery for constraint * \param[out] next_change Where to set when rule evaluation will change * \param[in] re_match_data Regular expression submatches * * \return New location constraint if rule is valid, otherwise NULL */ static pcmk__location_t * generate_location_rule(pcmk_resource_t *rsc, xmlNode *rule_xml, const char *discovery, crm_time_t *next_change, pe_re_match_data_t *re_match_data) { const char *rule_id = NULL; const char *score = NULL; const char *boolean = NULL; const char *role_spec = NULL; GList *iter = NULL; GList *nodes = NULL; bool do_and = true; bool accept = true; bool raw_score = true; bool score_allocated = false; pcmk__location_t *location_rule = NULL; enum rsc_role_e role = pcmk_role_unknown; rule_xml = expand_idref(rule_xml, rsc->cluster->input); if (rule_xml == NULL) { - pcmk__config_err("Ignoring rule %s: Invalid " XML_ATTR_IDREF, rule_id); - return NULL; + return NULL; // Error already logged } rule_id = crm_element_value(rule_xml, XML_ATTR_ID); boolean = crm_element_value(rule_xml, XML_RULE_ATTR_BOOLEAN_OP); role_spec = crm_element_value(rule_xml, XML_RULE_ATTR_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 " XML_RULE_ATTR_ROLE " '%s'", rule_id, role_spec); return NULL; } crm_trace("Processing location constraint rule %s", rule_id); score = crm_element_value(rule_xml, XML_RULE_ATTR_SCORE); if (score == NULL) { score = crm_element_value(rule_xml, XML_RULE_ATTR_SCORE_ATTRIBUTE); if (score != NULL) { raw_score = false; } } if (pcmk__str_eq(boolean, "or", pcmk__str_casei)) { do_and = false; } location_rule = pcmk__new_location(rule_id, rsc, 0, discovery, NULL); if (location_rule == NULL) { return NULL; // Error already logged } location_rule->role_filter = role; if ((re_match_data != NULL) && (re_match_data->nregs > 0) && (re_match_data->pmatch[0].rm_so != -1) && !raw_score) { char *result = pe_expand_re_matches(score, re_match_data); if (result != NULL) { score = result; score_allocated = true; } } if (do_and) { nodes = pcmk__copy_node_list(rsc->cluster->nodes, true); for (iter = nodes; iter != NULL; iter = iter->next) { pcmk_node_t *node = iter->data; node->weight = get_node_score(rule_id, score, raw_score, node, rsc); } } for (iter = rsc->cluster->nodes; iter != NULL; iter = iter->next) { int score_f = 0; pcmk_node_t *node = iter->data; pe_match_data_t match_data = { .re = re_match_data, .params = pe_rsc_params(rsc, node, rsc->cluster), .meta = rsc->meta, }; accept = pe_test_rule(rule_xml, node->details->attrs, pcmk_role_unknown, rsc->cluster->now, next_change, &match_data); crm_trace("Rule %s %s on %s", ID(rule_xml), accept? "passed" : "failed", pe__node_name(node)); score_f = get_node_score(rule_id, score, raw_score, node, rsc); if (accept) { pcmk_node_t *local = pe_find_node_id(nodes, node->details->id); if ((local == NULL) && do_and) { continue; } else if (local == NULL) { local = pe__copy_node(node); nodes = g_list_append(nodes, local); } if (!do_and) { local->weight = pcmk__add_scores(local->weight, score_f); } crm_trace("%s has score %s after %s", pe__node_name(node), pcmk_readable_score(local->weight), rule_id); } else if (do_and && !accept) { // Remove it pcmk_node_t *delete = pe_find_node_id(nodes, node->details->id); if (delete != NULL) { nodes = g_list_remove(nodes, delete); crm_trace("%s did not match", pe__node_name(node)); } free(delete); } } if (score_allocated) { free((char *)score); } location_rule->nodes = nodes; if (location_rule->nodes == NULL) { crm_trace("No matching nodes for location constraint rule %s", rule_id); return NULL; } else { crm_trace("Location constraint rule %s matched %d nodes", rule_id, g_list_length(location_rule->nodes)); } return location_rule; } static void unpack_rsc_location(xmlNode *xml_obj, pcmk_resource_t *rsc, const char *role_spec, const char *score, pe_re_match_data_t *re_match_data) { pcmk__location_t *location = NULL; const char *rsc_id = crm_element_value(xml_obj, XML_LOC_ATTR_SOURCE); const char *id = crm_element_value(xml_obj, XML_ATTR_ID); const char *node = crm_element_value(xml_obj, XML_CIB_TAG_NODE); const char *discovery = crm_element_value(xml_obj, XML_LOCATION_ATTR_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, XML_RULE_ATTR_SCORE); } if ((node != NULL) && (score != NULL)) { int score_i = char2score(score); pcmk_node_t *match = pe_find_node(rsc->cluster->nodes, node); enum rsc_role_e role = pcmk_role_unknown; if (!match) { return; } if (role_spec == NULL) { role_spec = crm_element_value(xml_obj, XML_RULE_ATTR_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); } 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(); /* This loop is logically parallel to pe_evaluate_rules(), except * instead of checking whether any rule is active, we set up location * constraints for each active rule. */ for (xmlNode *rule_xml = first_named_child(xml_obj, XML_TAG_RULE); rule_xml != NULL; rule_xml = crm_next_same_xml(rule_xml)) { empty = false; crm_trace("Unpacking %s/%s", id, ID(rule_xml)); generate_location_rule(rsc, rule_xml, discovery, next_change, re_match_data); } if (empty) { pcmk__config_err("Ignoring constraint '%s' because it contains " "no 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, XML_ATTR_ID); const char *value = crm_element_value(xml_obj, XML_LOC_ATTR_SOURCE); if (value) { pcmk_resource_t *rsc; rsc = pcmk__find_constraint_resource(scheduler->resources, value); unpack_rsc_location(xml_obj, rsc, NULL, NULL, NULL); } value = crm_element_value(xml_obj, XML_LOC_ATTR_SOURCE_PATTERN); if (value) { regex_t *r_patt = calloc(1, sizeof(regex_t)); bool invert = false; if (value[0] == '!') { value++; invert = true; } if (regcomp(r_patt, value, REG_EXTENDED) != 0) { pcmk__config_err("Ignoring constraint '%s' because " XML_LOC_ATTR_SOURCE_PATTERN " has invalid value '%s'", id, value); free(r_patt); 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 (r_patt->re_nsub > 0) { nregs = r_patt->re_nsub + 1; } else { nregs = 1; } pmatch = calloc(nregs, sizeof(regmatch_t)); status = regexec(r_patt, r->id, nregs, pmatch, 0); if (!invert && (status == 0)) { pe_re_match_data_t re_match_data = { .string = r->id, .nregs = nregs, .pmatch = pmatch }; crm_debug("'%s' matched '%s' for %s", r->id, value, id); unpack_rsc_location(xml_obj, r, NULL, NULL, &re_match_data); } 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); } else { crm_trace("'%s' does not match '%s' for %s", r->id, value, id); } free(pmatch); } regfree(r_patt); free(r_patt); } } // \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 = ID(xml_obj); if (id == NULL) { pcmk__config_err("Ignoring <%s> constraint without " XML_ATTR_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_location"); return pcmk_rc_ok; } rsc_id = crm_element_value(xml_obj, XML_LOC_ATTR_SOURCE); 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, XML_RULE_ATTR_ROLE); *expanded_xml = copy_xml(xml_obj); // Convert any template or tag reference into constraint resource_set if (!pcmk__tag_to_set(*expanded_xml, &rsc_set, XML_LOC_ATTR_SOURCE, false, scheduler)) { free_xml(*expanded_xml); *expanded_xml = NULL; return pcmk_rc_unpack_error; } if (rsc_set != NULL) { if (state != NULL) { // Move "rsc-role" into converted resource_set as "role" attribute crm_xml_add(rsc_set, "role", state); xml_remove_prop(*expanded_xml, XML_RULE_ATTR_ROLE); } crm_log_xml_trace(*expanded_xml, "Expanded rsc_location"); } else { // No sets free_xml(*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 = ID(set); if (set_id == NULL) { pcmk__config_err("Ignoring " XML_CONS_TAG_RSC_SET " without " XML_ATTR_ID " in constraint '%s'", pcmk__s(ID(location), "(missing ID)")); return pcmk_rc_unpack_error; } role = crm_element_value(set, "role"); local_score = crm_element_value(set, XML_RULE_ATTR_SCORE); for (xml_rsc = first_named_child(set, XML_TAG_RESOURCE_REF); xml_rsc != NULL; xml_rsc = crm_next_same_xml(xml_rsc)) { resource = pcmk__find_constraint_resource(scheduler->resources, ID(xml_rsc)); if (resource == NULL) { pcmk__config_err("%s: No resource found for %s", set_id, ID(xml_rsc)); return pcmk_rc_unpack_error; } unpack_rsc_location(location, resource, role, local_score, 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 = first_named_child(xml_obj, XML_CONS_TAG_RSC_SET); set != NULL; set = crm_next_same_xml(set)) { any_sets = true; set = expand_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) { free_xml(expanded_xml); } return; } } if (expanded_xml) { free_xml(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 * \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; if (id == NULL) { pcmk__config_err("Invalid constraint: no ID specified"); return NULL; } else if (rsc == NULL) { pcmk__config_err("Invalid constraint %s: no resource specified", id); return NULL; } else if (node == NULL) { CRM_CHECK(node_score == 0, return NULL); } new_con = calloc(1, sizeof(pcmk__location_t)); if (new_con != NULL) { new_con->id = strdup(id); new_con->rsc = rsc; new_con->nodes = NULL; new_con->role_filter = pcmk_role_unknown; if (pcmk__str_eq(discover_mode, "always", pcmk__str_null_matches|pcmk__str_casei)) { new_con->discover_mode = pcmk_probe_always; } else if (pcmk__str_eq(discover_mode, "never", pcmk__str_casei)) { new_con->discover_mode = pcmk_probe_never; } else if (pcmk__str_eq(discover_mode, "exclusive", pcmk__str_casei)) { new_con->discover_mode = pcmk_probe_exclusive; rsc->exclusive_discover = TRUE; } else { pcmk__config_err("Invalid " XML_LOCATION_ATTR_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, role2text(rsc->next_role), role2text(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? role2text(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, pe__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, pe__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/pengine/rules.c b/lib/pengine/rules.c index 4e16020a07..ef26661d74 100644 --- a/lib/pengine/rules.c +++ b/lib/pengine/rules.c @@ -1,1338 +1,1345 @@ /* * Copyright 2004-2023 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 CRM_TRACE_INIT_DATA(pe_rules); /*! * \brief Evaluate any rules contained by given XML element * * \param[in,out] xml XML element to check for rules * \param[in] node_hash Node attributes to use to evaluate expressions * \param[in] now Time to use when evaluating expressions * \param[out] next_change If not NULL, set to when evaluation will change * * \return TRUE if no rules, or any of rules present is in effect, else FALSE */ gboolean pe_evaluate_rules(xmlNode *ruleset, GHashTable *node_hash, crm_time_t *now, crm_time_t *next_change) { pe_rule_eval_data_t rule_data = { .node_hash = node_hash, .role = pcmk_role_unknown, .now = now, .match_data = NULL, .rsc_data = NULL, .op_data = NULL }; return pe_eval_rules(ruleset, &rule_data, next_change); } 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) { pe_rule_eval_data_t rule_data = { .node_hash = node_hash, .role = role, .now = now, .match_data = match_data, .rsc_data = NULL, .op_data = NULL }; return pe_eval_expr(rule, &rule_data, next_change); } /*! * \brief Evaluate one rule subelement (pass/fail) * * A rule element may contain another rule, a node attribute expression, or a * date expression. Given any one of those, evaluate it and return whether it * passed. * * \param[in,out] expr Rule subelement XML * \param[in] node_hash Node attributes to use when evaluating expression * \param[in] role Resource role to use when evaluating expression * \param[in] now Time to use when evaluating expression * \param[out] next_change If not NULL, set to when evaluation will change * \param[in] match_data If not NULL, resource back-references and params * * \return TRUE if expression is in effect under given conditions, else FALSE */ gboolean pe_test_expression(xmlNode *expr, GHashTable *node_hash, enum rsc_role_e role, crm_time_t *now, crm_time_t *next_change, pe_match_data_t *match_data) { pe_rule_eval_data_t rule_data = { .node_hash = node_hash, .role = role, .now = now, .match_data = match_data, .rsc_data = NULL, .op_data = NULL }; return pe_eval_subexpr(expr, &rule_data, next_change); } enum expression_type find_expression_type(xmlNode * expr) { const char *attr = NULL; attr = crm_element_value(expr, XML_EXPR_ATTR_ATTRIBUTE); if (pcmk__xe_is(expr, PCMK_XE_DATE_EXPRESSION)) { return pcmk__subexpr_datetime; } else if (pcmk__xe_is(expr, PCMK_XE_RSC_EXPRESSION)) { return pcmk__subexpr_resource; } else if (pcmk__xe_is(expr, PCMK_XE_OP_EXPRESSION)) { return pcmk__subexpr_operation; } else if (pcmk__xe_is(expr, XML_TAG_RULE)) { return pcmk__subexpr_rule; } else if (!pcmk__xe_is(expr, XML_TAG_EXPRESSION)) { return pcmk__subexpr_unknown; } else if (pcmk__str_any_of(attr, CRM_ATTR_UNAME, CRM_ATTR_KIND, CRM_ATTR_ID, NULL)) { return pcmk__subexpr_location; } else if (pcmk__str_eq(attr, CRM_ATTR_ROLE, pcmk__str_none)) { return pcmk__subexpr_role; } return pcmk__subexpr_attribute; } /* As per the nethack rules: * * moon period = 29.53058 days ~= 30, year = 365.2422 days * days moon phase advances on first day of year compared to preceding year * = 365.2422 - 12*29.53058 ~= 11 * years in Metonic cycle (time until same phases fall on the same days of * the month) = 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 * 177 ~= 8 reported phases * 22 * + 11/22 for rounding * * 0-7, with 0: new, 4: full */ static int phase_of_the_moon(const crm_time_t *now) { 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); } static int check_one(const xmlNode *cron_spec, const char *xml_field, uint32_t time_field) { int rc = pcmk_rc_undetermined; const char *value = crm_element_value(cron_spec, xml_field); long long low, high; if (value == NULL) { /* Return pe_date_result_undetermined if the field is missing. */ goto bail; } if (pcmk__parse_ll_range(value, &low, &high) != pcmk_rc_ok) { goto bail; } else if (low == high) { /* A single number was given, not a range. */ if (time_field < low) { rc = pcmk_rc_before_range; } else if (time_field > high) { rc = pcmk_rc_after_range; } else { rc = pcmk_rc_within_range; } } else if (low != -1 && high != -1) { /* This is a range with both bounds. */ if (time_field < low) { rc = pcmk_rc_before_range; } else if (time_field > high) { rc = pcmk_rc_after_range; } else { rc = pcmk_rc_within_range; } } else if (low == -1) { /* This is a range with no starting value. */ rc = time_field <= high ? pcmk_rc_within_range : pcmk_rc_after_range; } else if (high == -1) { /* This is a range with no ending value. */ rc = time_field >= low ? pcmk_rc_within_range : pcmk_rc_before_range; } bail: if (rc == pcmk_rc_within_range) { crm_debug("Condition '%s' in %s: passed", value, xml_field); } else { crm_debug("Condition '%s' in %s: failed", value, xml_field); } return rc; } static gboolean check_passes(int rc) { /* _within_range is obvious. _undetermined is a pass because * this is the return value if a field is not given. In this * case, we just want to ignore it and check other fields to * see if they place some restriction on what can pass. */ return rc == pcmk_rc_within_range || rc == pcmk_rc_undetermined; } #define CHECK_ONE(spec, name, var) do { \ int subpart_rc = check_one(spec, name, var); \ if (check_passes(subpart_rc) == FALSE) { \ return subpart_rc; \ } \ } while (0) int pe_cron_range_satisfied(const crm_time_t *now, const xmlNode *cron_spec) { uint32_t h, m, s, y, d, w; CRM_CHECK(now != NULL, return pcmk_rc_op_unsatisfied); crm_time_get_gregorian(now, &y, &m, &d); CHECK_ONE(cron_spec, "years", y); CHECK_ONE(cron_spec, "months", m); CHECK_ONE(cron_spec, "monthdays", d); crm_time_get_timeofday(now, &h, &m, &s); CHECK_ONE(cron_spec, "hours", h); CHECK_ONE(cron_spec, "minutes", m); CHECK_ONE(cron_spec, "seconds", s); crm_time_get_ordinal(now, &y, &d); CHECK_ONE(cron_spec, "yeardays", d); crm_time_get_isoweek(now, &y, &w, &d); CHECK_ONE(cron_spec, "weekyears", y); CHECK_ONE(cron_spec, "weeks", w); CHECK_ONE(cron_spec, "weekdays", d); CHECK_ONE(cron_spec, "moon", phase_of_the_moon(now)); if (crm_element_value(cron_spec, "moon") != NULL) { pcmk__config_warn("Support for 'moon' in date_spec elements " "(such as %s) is deprecated and will be removed " "in a future release of Pacemaker", ID(cron_spec)); } /* If we get here, either no fields were specified (which is success), or all * the fields that were specified had their conditions met (which is also a * success). Thus, the result is success. */ return pcmk_rc_ok; } static void update_field(crm_time_t *t, const xmlNode *xml, const char *attr, void (*time_fn)(crm_time_t *, int)) { long long value; if ((pcmk__scan_ll(crm_element_value(xml, attr), &value, 0LL) == pcmk_rc_ok) && (value != 0LL) && (value >= INT_MIN) && (value <= INT_MAX)) { time_fn(t, (int) value); } } static crm_time_t * parse_xml_duration(const crm_time_t *start, const xmlNode *duration_spec) { crm_time_t *end = pcmk_copy_time(start); update_field(end, duration_spec, "years", crm_time_add_years); update_field(end, duration_spec, "months", crm_time_add_months); update_field(end, duration_spec, "weeks", crm_time_add_weeks); update_field(end, duration_spec, "days", crm_time_add_days); update_field(end, duration_spec, "hours", crm_time_add_hours); update_field(end, duration_spec, "minutes", crm_time_add_minutes); update_field(end, duration_spec, "seconds", crm_time_add_seconds); return end; } // Set next_change to t if t is earlier static void crm_time_set_if_earlier(crm_time_t *next_change, crm_time_t *t) { if ((next_change != NULL) && (t != NULL)) { if (!crm_time_is_defined(next_change) || (crm_time_compare(t, next_change) < 0)) { crm_time_set(next_change, t); } } } // Information about a block of nvpair elements typedef struct sorted_set_s { int score; // This block's score for sorting const char *name; // This block's ID const char *special_name; // ID that should sort first xmlNode *attr_set; // This block gboolean overwrite; // Whether existing values will be overwritten } sorted_set_t; static gint sort_pairs(gconstpointer a, gconstpointer b) { const sorted_set_t *pair_a = a; const sorted_set_t *pair_b = b; if (a == NULL && b == NULL) { return 0; } else if (a == NULL) { return 1; } else if (b == NULL) { return -1; } if (pcmk__str_eq(pair_a->name, pair_a->special_name, pcmk__str_casei)) { return -1; } else if (pcmk__str_eq(pair_b->name, pair_a->special_name, pcmk__str_casei)) { return 1; } /* 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 (pair_a->score < pair_b->score) { return pair_a->overwrite? -1 : 1; } else if (pair_a->score > pair_b->score) { return pair_a->overwrite? 1 : -1; } return 0; } static void populate_hash(xmlNode * nvpair_list, GHashTable * hash, gboolean overwrite, xmlNode * top) { 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, XML_TAG_ATTRS)) { list = list->children; } for (an_attr = pcmk__xe_first_child(list); an_attr != NULL; an_attr = pcmk__xe_next(an_attr)) { if (pcmk__str_eq((const char *)an_attr->name, XML_CIB_TAG_NVPAIR, pcmk__str_none)) { xmlNode *ref_nvpair = expand_idref(an_attr, top); + if (ref_nvpair == NULL) { + continue; // Not possible with schema validation enabled + } + name = crm_element_value(an_attr, XML_NVPAIR_ATTR_NAME); if (name == NULL) { name = crm_element_value(ref_nvpair, XML_NVPAIR_ATTR_NAME); } value = crm_element_value(an_attr, XML_NVPAIR_ATTR_VALUE); if (value == NULL) { value = crm_element_value(ref_nvpair, XML_NVPAIR_ATTR_VALUE); } if (name == NULL || value == NULL) { continue; } old_value = g_hash_table_lookup(hash, name); if (pcmk__str_eq(value, "#default", pcmk__str_casei)) { 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); g_hash_table_insert(hash, strdup(name), strdup(value)); } else if (overwrite) { crm_trace("Setting %s=\"%s\" (overwriting old value \"%s\")", name, value, old_value); g_hash_table_replace(hash, strdup(name), strdup(value)); } } } } typedef struct unpack_data_s { gboolean overwrite; void *hash; crm_time_t *next_change; const pe_rule_eval_data_t *rule_data; xmlNode *top; } unpack_data_t; static void unpack_attr_set(gpointer data, gpointer user_data) { sorted_set_t *pair = data; unpack_data_t *unpack_data = user_data; if (!pe_eval_rules(pair->attr_set, unpack_data->rule_data, unpack_data->next_change)) { return; } crm_trace("Adding attributes from %s (score %d) %s overwrite", pair->name, pair->score, (unpack_data->overwrite? "with" : "without")); populate_hash(pair->attr_set, unpack_data->hash, unpack_data->overwrite, unpack_data->top); } /*! * \internal * \brief Create a sorted list of nvpair blocks * * \param[in,out] top XML document root (used to expand id-ref's) * \param[in] xml_obj XML element containing blocks of nvpair elements * \param[in] set_name If not NULL, only get blocks of this element * \param[in] always_first If not NULL, sort block with this ID as first * * \return List of sorted_set_t entries for nvpair blocks */ static GList * make_pairs(xmlNode *top, const xmlNode *xml_obj, const char *set_name, const char *always_first, gboolean overwrite) { GList *unsorted = NULL; if (xml_obj == NULL) { return NULL; } for (xmlNode *attr_set = pcmk__xe_first_child(xml_obj); attr_set != NULL; attr_set = pcmk__xe_next(attr_set)) { if (pcmk__str_eq(set_name, (const char *) attr_set->name, pcmk__str_null_matches)) { const char *score = NULL; sorted_set_t *pair = NULL; xmlNode *expanded_attr_set = expand_idref(attr_set, top); if (expanded_attr_set == NULL) { - // Schema (if not "none") prevents this - continue; + continue; // Not possible with schema validation enabled } pair = calloc(1, sizeof(sorted_set_t)); pair->name = ID(expanded_attr_set); pair->special_name = always_first; pair->attr_set = expanded_attr_set; pair->overwrite = overwrite; score = crm_element_value(expanded_attr_set, XML_RULE_ATTR_SCORE); pair->score = char2score(score); unsorted = g_list_prepend(unsorted, pair); } } return g_list_sort(unsorted, sort_pairs); } /*! * \brief Extract nvpair blocks contained by an XML element into a hash table * * \param[in,out] top XML document root (used to expand id-ref's) * \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(top, xml_obj, set_name, always_first, overwrite); if (pairs) { unpack_data_t data = { .hash = hash, .overwrite = overwrite, .next_change = next_change, .top = top, .rule_data = rule_data }; g_list_foreach(pairs, unpack_attr_set, &data); g_list_free_full(pairs, free); } } /*! * \brief Extract nvpair blocks contained by an XML element into a hash table * * \param[in,out] top XML document root (used to expand id-ref's) * \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, .role = pcmk_role_unknown, .now = now, .match_data = NULL, .rsc_data = NULL, .op_data = NULL }; pe_eval_nvpairs(top, xml_obj, set_name, &rule_data, hash, always_first, overwrite, next_change); } /*! * \brief Expand any regular expression submatches (%0-%9) in a string * * \param[in] string String possibly containing submatch variables * \param[in] match_data If not NULL, regular expression matches * * \return Newly allocated string identical to \p string with submatches * expanded, or NULL if there were no matches */ char * pe_expand_re_matches(const char *string, const pe_re_match_data_t *match_data) { size_t len = 0; int i; const char *p, *last_match_index; char *p_dst, *result = NULL; if (pcmk__str_empty(string) || !match_data) { return NULL; } p = last_match_index = string; while (*p) { if (*p == '%' && *(p + 1) && isdigit(*(p + 1))) { i = *(p + 1) - '0'; if (match_data->nregs >= i && match_data->pmatch[i].rm_so != -1 && match_data->pmatch[i].rm_eo > match_data->pmatch[i].rm_so) { len += p - last_match_index + (match_data->pmatch[i].rm_eo - match_data->pmatch[i].rm_so); last_match_index = p + 2; } p++; } p++; } len += p - last_match_index + 1; /* FIXME: Excessive? */ if (len - 1 <= 0) { return NULL; } p_dst = result = calloc(1, len); p = string; while (*p) { if (*p == '%' && *(p + 1) && isdigit(*(p + 1))) { i = *(p + 1) - '0'; if (match_data->nregs >= i && match_data->pmatch[i].rm_so != -1 && match_data->pmatch[i].rm_eo > match_data->pmatch[i].rm_so) { /* rm_eo can be equal to rm_so, but then there is nothing to do */ int match_len = match_data->pmatch[i].rm_eo - match_data->pmatch[i].rm_so; memcpy(p_dst, match_data->string + match_data->pmatch[i].rm_so, match_len); p_dst += match_len; } p++; } else { *(p_dst) = *(p); p_dst++; } p++; } return result; } /*! * \brief Evaluate rules * * \param[in,out] ruleset XML possibly containing rule sub-elements * \param[in] rule_data * \param[out] next_change If not NULL, set to when evaluation will change * * \return TRUE if there are no rules or */ gboolean pe_eval_rules(xmlNode *ruleset, const pe_rule_eval_data_t *rule_data, crm_time_t *next_change) { // If there are no rules, pass by default gboolean ruleset_default = TRUE; for (xmlNode *rule = first_named_child(ruleset, XML_TAG_RULE); rule != NULL; rule = crm_next_same_xml(rule)) { ruleset_default = FALSE; if (pe_eval_expr(rule, rule_data, next_change)) { /* Only the deprecated "lifetime" element of location constraints * may contain more than one rule at the top level -- the schema * limits a block of nvpairs to a single top-level rule. So, this * effectively means that a lifetime is active if any rule it * contains is active. */ return TRUE; } } return ruleset_default; } /*! * \brief Evaluate all of a rule's expressions * * \param[in,out] rule XML containing a rule definition or its id-ref * \param[in] rule_data Matching parameters to check against rule * \param[out] next_change If not NULL, set to when evaluation will change * * \return TRUE if \p rule_data passes \p rule, otherwise FALSE */ gboolean pe_eval_expr(xmlNode *rule, const pe_rule_eval_data_t *rule_data, crm_time_t *next_change) { xmlNode *expr = NULL; gboolean test = TRUE; gboolean empty = TRUE; gboolean passed = TRUE; gboolean do_and = TRUE; const char *value = NULL; rule = expand_idref(rule, NULL); + if (rule == NULL) { + return FALSE; // Not possible with schema validation enabled + } + value = crm_element_value(rule, XML_RULE_ATTR_BOOLEAN_OP); if (pcmk__str_eq(value, "or", pcmk__str_casei)) { do_and = FALSE; passed = FALSE; } crm_trace("Testing rule %s", ID(rule)); for (expr = pcmk__xe_first_child(rule); expr != NULL; expr = pcmk__xe_next(expr)) { test = pe_eval_subexpr(expr, rule_data, next_change); empty = FALSE; if (test && do_and == FALSE) { crm_trace("Expression %s/%s passed", ID(rule), ID(expr)); return TRUE; } else if (test == FALSE && do_and) { crm_trace("Expression %s/%s failed", ID(rule), ID(expr)); return FALSE; } } if (empty) { pcmk__config_err("Ignoring rule %s because it contains no expressions", ID(rule)); } crm_trace("Rule %s %s", ID(rule), passed ? "passed" : "failed"); return passed; } /*! * \brief Evaluate a single rule expression, including any subexpressions * * \param[in,out] expr XML containing a rule expression * \param[in] rule_data Matching parameters to check against expression * \param[out] next_change If not NULL, set to when evaluation will change * * \return TRUE if \p rule_data passes \p expr, otherwise FALSE */ gboolean pe_eval_subexpr(xmlNode *expr, const pe_rule_eval_data_t *rule_data, crm_time_t *next_change) { gboolean accept = FALSE; const char *uname = NULL; switch (find_expression_type(expr)) { case pcmk__subexpr_rule: accept = pe_eval_expr(expr, rule_data, next_change); break; case pcmk__subexpr_attribute: case pcmk__subexpr_location: /* these expressions can never succeed if there is * no node to compare with */ if (rule_data->node_hash != NULL) { accept = pe__eval_attr_expr(expr, rule_data); } break; case pcmk__subexpr_datetime: switch (pe__eval_date_expr(expr, rule_data, next_change)) { case pcmk_rc_within_range: case pcmk_rc_ok: accept = TRUE; break; default: accept = FALSE; break; } break; case pcmk__subexpr_role: accept = pe__eval_role_expr(expr, rule_data); break; case pcmk__subexpr_resource: accept = pe__eval_rsc_expr(expr, rule_data); break; case pcmk__subexpr_operation: accept = pe__eval_op_expr(expr, rule_data); break; default: CRM_CHECK(FALSE /* bad type */ , return FALSE); accept = FALSE; } if (rule_data->node_hash) { uname = g_hash_table_lookup(rule_data->node_hash, CRM_ATTR_UNAME); } crm_trace("Expression %s %s on %s", ID(expr), accept ? "passed" : "failed", uname ? uname : "all nodes"); return accept; } /*! * \internal * \brief Compare two values in a rule's node attribute expression * * \param[in] l_val Value on left-hand side of comparison * \param[in] r_val Value on right-hand side of comparison * \param[in] type How to interpret the values (allowed values: * \c "string", \c "integer", \c "number", * \c "version", \c NULL) * \param[in] op Type of comparison * * \return -1 if (l_val < r_val), * 0 if (l_val == r_val), * 1 if (l_val > r_val) */ static int compare_attr_expr_vals(const char *l_val, const char *r_val, const char *type, const char *op) { int cmp = 0; if (l_val != NULL && r_val != NULL) { if (type == NULL) { if (pcmk__strcase_any_of(op, "lt", "lte", "gt", "gte", NULL)) { if (pcmk__char_in_any_str('.', l_val, r_val, NULL)) { type = "number"; } else { type = "integer"; } } else { type = "string"; } crm_trace("Defaulting to %s based comparison for '%s' op", type, op); } if (pcmk__str_eq(type, "string", pcmk__str_casei)) { cmp = strcasecmp(l_val, r_val); } else if (pcmk__str_eq(type, "integer", pcmk__str_casei)) { long long l_val_num; int rc1 = pcmk__scan_ll(l_val, &l_val_num, 0LL); long long r_val_num; int rc2 = pcmk__scan_ll(r_val, &r_val_num, 0LL); if ((rc1 == pcmk_rc_ok) && (rc2 == pcmk_rc_ok)) { if (l_val_num < r_val_num) { cmp = -1; } else if (l_val_num > r_val_num) { cmp = 1; } else { cmp = 0; } } else { crm_debug("Integer parse error. Comparing %s and %s as strings", l_val, r_val); cmp = compare_attr_expr_vals(l_val, r_val, "string", op); } } else if (pcmk__str_eq(type, "number", pcmk__str_casei)) { double l_val_num; double r_val_num; int rc1 = pcmk__scan_double(l_val, &l_val_num, NULL, NULL); int rc2 = pcmk__scan_double(r_val, &r_val_num, NULL, NULL); if (rc1 == pcmk_rc_ok && rc2 == pcmk_rc_ok) { if (l_val_num < r_val_num) { cmp = -1; } else if (l_val_num > r_val_num) { cmp = 1; } else { cmp = 0; } } else { crm_debug("Floating-point parse error. Comparing %s and %s as " "strings", l_val, r_val); cmp = compare_attr_expr_vals(l_val, r_val, "string", op); } } else if (pcmk__str_eq(type, "version", pcmk__str_casei)) { cmp = compare_version(l_val, r_val); } } else if (l_val == NULL && r_val == NULL) { cmp = 0; } else if (r_val == NULL) { cmp = 1; } else { // l_val == NULL && r_val != NULL cmp = -1; } return cmp; } /*! * \internal * \brief Check whether an attribute expression evaluates to \c true * * \param[in] l_val Value on left-hand side of comparison * \param[in] r_val Value on right-hand side of comparison * \param[in] type How to interpret the values (allowed values: * \c "string", \c "integer", \c "number", * \c "version", \c NULL) * \param[in] op Type of comparison. * * \return \c true if expression evaluates to \c true, \c false * otherwise */ static bool accept_attr_expr(const char *l_val, const char *r_val, const char *type, const char *op) { int cmp; if (pcmk__str_eq(op, "defined", pcmk__str_casei)) { return (l_val != NULL); } else if (pcmk__str_eq(op, "not_defined", pcmk__str_casei)) { return (l_val == NULL); } cmp = compare_attr_expr_vals(l_val, r_val, type, op); if (pcmk__str_eq(op, "eq", pcmk__str_casei)) { return (cmp == 0); } else if (pcmk__str_eq(op, "ne", pcmk__str_casei)) { return (cmp != 0); } else if (l_val == NULL || r_val == NULL) { // The comparison is meaningless from this point on return false; } else if (pcmk__str_eq(op, "lt", pcmk__str_casei)) { return (cmp < 0); } else if (pcmk__str_eq(op, "lte", pcmk__str_casei)) { return (cmp <= 0); } else if (pcmk__str_eq(op, "gt", pcmk__str_casei)) { return (cmp > 0); } else if (pcmk__str_eq(op, "gte", pcmk__str_casei)) { return (cmp >= 0); } return false; // Should never reach this point } /*! * \internal * \brief Get correct value according to value-source * * \param[in] value value given in rule expression * \param[in] value_source value-source given in rule expressions * \param[in] match_data If not NULL, resource back-references and params */ static const char * expand_value_source(const char *value, const char *value_source, const pe_match_data_t *match_data) { GHashTable *table = NULL; if (pcmk__str_empty(value)) { return NULL; // value_source is irrelevant } else if (pcmk__str_eq(value_source, "param", pcmk__str_casei)) { table = match_data->params; } else if (pcmk__str_eq(value_source, "meta", pcmk__str_casei)) { table = match_data->meta; } else { // literal return value; } if (table == NULL) { return NULL; } return (const char *) g_hash_table_lookup(table, value); } /*! * \internal * \brief Evaluate a node attribute expression based on #uname, #id, #kind, * or a generic node attribute * * \param[in] expr XML of rule expression * \param[in] rule_data The match_data and node_hash members are used * * \return TRUE if rule_data satisfies the expression, FALSE otherwise */ gboolean pe__eval_attr_expr(const xmlNode *expr, const pe_rule_eval_data_t *rule_data) { gboolean attr_allocated = FALSE; const char *h_val = NULL; const char *op = NULL; const char *type = NULL; const char *attr = NULL; const char *value = NULL; const char *value_source = NULL; attr = crm_element_value(expr, XML_EXPR_ATTR_ATTRIBUTE); op = crm_element_value(expr, XML_EXPR_ATTR_OPERATION); value = crm_element_value(expr, XML_EXPR_ATTR_VALUE); type = crm_element_value(expr, XML_EXPR_ATTR_TYPE); value_source = crm_element_value(expr, XML_EXPR_ATTR_VALUE_SOURCE); if (attr == NULL) { pcmk__config_err("Expression %s invalid: " XML_EXPR_ATTR_ATTRIBUTE " not specified", pcmk__s(ID(expr), "without ID")); return FALSE; } else if (op == NULL) { pcmk__config_err("Expression %s invalid: " XML_EXPR_ATTR_OPERATION " not specified", pcmk__s(ID(expr), "without ID")); } if (rule_data->match_data != NULL) { // Expand any regular expression submatches (%0-%9) in attribute name if (rule_data->match_data->re != NULL) { char *resolved_attr = pe_expand_re_matches(attr, rule_data->match_data->re); if (resolved_attr != NULL) { attr = (const char *) resolved_attr; attr_allocated = TRUE; } } // Get value appropriate to value-source value = expand_value_source(value, value_source, rule_data->match_data); } if (rule_data->node_hash != NULL) { h_val = (const char *)g_hash_table_lookup(rule_data->node_hash, attr); } if (attr_allocated) { free((char *)attr); attr = NULL; } return accept_attr_expr(h_val, value, type, op); } /*! * \internal * \brief Evaluate a date_expression * * \param[in] expr XML of rule expression * \param[in] rule_data Only the now member is used * \param[out] next_change If not NULL, set to when evaluation will change * * \return Standard Pacemaker return code */ int pe__eval_date_expr(const xmlNode *expr, const pe_rule_eval_data_t *rule_data, crm_time_t *next_change) { crm_time_t *start = NULL; crm_time_t *end = NULL; const char *value = NULL; const char *op = crm_element_value(expr, "operation"); xmlNode *duration_spec = NULL; xmlNode *date_spec = NULL; // "undetermined" will also be returned for parsing errors int rc = pcmk_rc_undetermined; crm_trace("Testing expression: %s", ID(expr)); duration_spec = first_named_child(expr, "duration"); date_spec = first_named_child(expr, "date_spec"); value = crm_element_value(expr, "start"); if (value != NULL) { start = crm_time_new(value); } value = crm_element_value(expr, "end"); if (value != NULL) { end = crm_time_new(value); } if (start != NULL && end == NULL && duration_spec != NULL) { end = parse_xml_duration(start, duration_spec); } if (pcmk__str_eq(op, "in_range", pcmk__str_null_matches | pcmk__str_casei)) { if ((start == NULL) && (end == NULL)) { // in_range requires at least one of start or end } else if ((start != NULL) && (crm_time_compare(rule_data->now, start) < 0)) { rc = pcmk_rc_before_range; crm_time_set_if_earlier(next_change, start); } else if ((end != NULL) && (crm_time_compare(rule_data->now, end) > 0)) { rc = pcmk_rc_after_range; } else { rc = pcmk_rc_within_range; if (end && next_change) { // Evaluation doesn't change until second after end crm_time_add_seconds(end, 1); crm_time_set_if_earlier(next_change, end); } } } else if (pcmk__str_eq(op, "date_spec", pcmk__str_casei)) { rc = pe_cron_range_satisfied(rule_data->now, date_spec); // @TODO set next_change appropriately } else if (pcmk__str_eq(op, "gt", pcmk__str_casei)) { if (start == NULL) { // gt requires start } else if (crm_time_compare(rule_data->now, start) > 0) { rc = pcmk_rc_within_range; } else { rc = pcmk_rc_before_range; // Evaluation doesn't change until second after start crm_time_add_seconds(start, 1); crm_time_set_if_earlier(next_change, start); } } else if (pcmk__str_eq(op, "lt", pcmk__str_casei)) { if (end == NULL) { // lt requires end } else if (crm_time_compare(rule_data->now, end) < 0) { rc = pcmk_rc_within_range; crm_time_set_if_earlier(next_change, end); } else { rc = pcmk_rc_after_range; } } crm_time_free(start); crm_time_free(end); return rc; } gboolean pe__eval_op_expr(const xmlNode *expr, const pe_rule_eval_data_t *rule_data) { const char *name = crm_element_value(expr, XML_NVPAIR_ATTR_NAME); const char *interval_s = crm_element_value(expr, XML_LRM_ATTR_INTERVAL); guint interval; crm_trace("Testing op_defaults expression: %s", ID(expr)); if (rule_data->op_data == NULL) { crm_trace("No operations data provided"); return FALSE; } interval = crm_parse_interval_spec(interval_s); if (interval == 0 && errno != 0) { crm_trace("Could not parse interval: %s", interval_s); return FALSE; } if (interval_s != NULL && interval != rule_data->op_data->interval) { crm_trace("Interval doesn't match: %d != %d", interval, rule_data->op_data->interval); return FALSE; } if (!pcmk__str_eq(name, rule_data->op_data->op_name, pcmk__str_none)) { crm_trace("Name doesn't match: %s != %s", name, rule_data->op_data->op_name); return FALSE; } return TRUE; } /*! * \internal * \brief Check whether a resource role matches a rule role * * \param[in] expr XML of rule expression * \param[in] rule_data Only the role member is used * * \return true if role matches, otherwise false */ static bool role_matches(const xmlNode *expr, const pe_rule_eval_data_t *rule_data) { const char *value = crm_element_value(expr, XML_EXPR_ATTR_VALUE); enum rsc_role_e role = text2role(value); if (role == pcmk_role_unknown) { pcmk__config_err("Invalid role %s in rule expression", value); return false; } return role == rule_data->role; } /*! * \internal * \brief Evaluate a node attribute expression based on #role * * \param[in] expr XML of rule expression * \param[in] rule_data Only the role member is used * * \return TRUE if rule_data->role satisfies the expression, FALSE otherwise */ gboolean pe__eval_role_expr(const xmlNode *expr, const pe_rule_eval_data_t *rule_data) { const char *op = NULL; if (rule_data->role == pcmk_role_unknown) { // Skip role checks for now return FALSE; } op = crm_element_value(expr, XML_EXPR_ATTR_OPERATION); // @TODO Why are only Unpromoted/Promoted considered defined? if (pcmk__str_eq(op, "defined", pcmk__str_casei)) { if (rule_data->role > pcmk_role_started) { return TRUE; } } else if (pcmk__str_eq(op, "not_defined", pcmk__str_casei)) { if ((rule_data->role > pcmk_role_unknown) && (rule_data->role < pcmk_role_unpromoted)) { return TRUE; } } else if (pcmk__str_eq(op, "eq", pcmk__str_casei)) { return role_matches(expr, rule_data)? TRUE : FALSE; } else if (pcmk__str_eq(op, "ne", pcmk__str_casei) // Test "ne" only with promotable clone roles (@TODO Why?) && (rule_data->role >= pcmk_role_unpromoted)) { return role_matches(expr, rule_data)? FALSE : TRUE; } else { pcmk__config_err("Operation '%s' is not valid with " CRM_ATTR_ROLE " comparisons", op); } return FALSE; } gboolean pe__eval_rsc_expr(const xmlNode *expr, const pe_rule_eval_data_t *rule_data) { const char *class = crm_element_value(expr, XML_AGENT_ATTR_CLASS); const char *provider = crm_element_value(expr, XML_AGENT_ATTR_PROVIDER); const char *type = crm_element_value(expr, XML_EXPR_ATTR_TYPE); crm_trace("Testing rsc_defaults expression: %s", ID(expr)); if (rule_data->rsc_data == NULL) { crm_trace("No resource data provided"); return FALSE; } if (class != NULL && !pcmk__str_eq(class, rule_data->rsc_data->standard, pcmk__str_none)) { crm_trace("Class doesn't match: %s != %s", class, rule_data->rsc_data->standard); return FALSE; } if ((provider == NULL && rule_data->rsc_data->provider != NULL) || (provider != NULL && rule_data->rsc_data->provider == NULL) || !pcmk__str_eq(provider, rule_data->rsc_data->provider, pcmk__str_none)) { crm_trace("Provider doesn't match: %s != %s", provider, rule_data->rsc_data->provider); return FALSE; } if (type != NULL && !pcmk__str_eq(type, rule_data->rsc_data->agent, pcmk__str_none)) { crm_trace("Agent doesn't match: %s != %s", type, rule_data->rsc_data->agent); return FALSE; } return TRUE; } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include gboolean test_ruleset(xmlNode *ruleset, GHashTable *node_hash, crm_time_t *now) { return pe_evaluate_rules(ruleset, node_hash, now, NULL); } 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); } gboolean pe_test_rule_re(xmlNode * rule, GHashTable * node_hash, enum rsc_role_e role, crm_time_t * now, pe_re_match_data_t * re_match_data) { pe_match_data_t match_data = { .re = re_match_data, .params = NULL, .meta = NULL, }; return pe_test_rule(rule, node_hash, role, now, NULL, &match_data); } gboolean pe_test_rule_full(xmlNode *rule, GHashTable *node_hash, enum rsc_role_e role, crm_time_t *now, pe_match_data_t *match_data) { return pe_test_rule(rule, node_hash, role, now, NULL, match_data); } gboolean test_expression(xmlNode * expr, GHashTable * node_hash, enum rsc_role_e role, crm_time_t * now) { return pe_test_expression(expr, node_hash, role, now, NULL, NULL); } gboolean pe_test_expression_re(xmlNode * expr, GHashTable * node_hash, enum rsc_role_e role, crm_time_t * now, pe_re_match_data_t * re_match_data) { pe_match_data_t match_data = { .re = re_match_data, .params = NULL, .meta = NULL, }; return pe_test_expression(expr, node_hash, role, now, NULL, &match_data); } gboolean pe_test_expression_full(xmlNode *expr, GHashTable *node_hash, enum rsc_role_e role, crm_time_t *now, pe_match_data_t *match_data) { return pe_test_expression(expr, node_hash, role, now, NULL, match_data); } void unpack_instance_attributes(xmlNode *top, xmlNode *xml_obj, const char *set_name, GHashTable *node_hash, GHashTable *hash, const char *always_first, gboolean overwrite, crm_time_t *now) { pe_rule_eval_data_t rule_data = { .node_hash = node_hash, .role = pcmk_role_unknown, .now = now, .match_data = NULL, .rsc_data = NULL, .op_data = NULL }; pe_eval_nvpairs(top, xml_obj, set_name, &rule_data, hash, always_first, overwrite, NULL); } // LCOV_EXCL_STOP // End deprecated API