diff --git a/daemons/attrd/attrd_messages.c b/daemons/attrd/attrd_messages.c index e1038a820b..44312ae8bc 100644 --- a/daemons/attrd/attrd_messages.c +++ b/daemons/attrd/attrd_messages.c @@ -1,348 +1,348 @@ /* - * Copyright 2022-2024 the Pacemaker project contributors + * Copyright 2022-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include // PRIu32 #include #include #include // pcmk__get_node() #include #include "pacemaker-attrd.h" int minimum_protocol_version = -1; static GHashTable *attrd_handlers = NULL; static bool is_sync_point_attr(xmlAttrPtr attr, void *data) { return pcmk__str_eq((const char *) attr->name, PCMK__XA_ATTR_SYNC_POINT, pcmk__str_none); } static int remove_sync_point_attribute(xmlNode *xml, void *data) { - pcmk__xe_remove_matching_attrs(xml, is_sync_point_attr, NULL); + pcmk__xe_remove_matching_attrs(xml, false, is_sync_point_attr, NULL); pcmk__xe_foreach_child(xml, PCMK_XE_OP, remove_sync_point_attribute, NULL); return pcmk_rc_ok; } /* Sync points on a multi-update IPC message to an attrd too old to support * multi-update messages won't work. Strip the sync point attribute off here * so we don't pretend to support this situation and instead ACK the client * immediately. */ static void remove_unsupported_sync_points(pcmk__request_t *request) { if (request->xml->children != NULL && !ATTRD_SUPPORTS_MULTI_MESSAGE(minimum_protocol_version) && attrd_request_has_sync_point(request->xml)) { crm_warn("Ignoring sync point in request from %s because not all nodes support it", pcmk__request_origin(request)); remove_sync_point_attribute(request->xml, NULL); } } static xmlNode * handle_unknown_request(pcmk__request_t *request) { crm_err("Unknown IPC request %s from %s %s", request->op, pcmk__request_origin_type(request), pcmk__request_origin(request)); pcmk__format_result(&request->result, CRM_EX_PROTOCOL, PCMK_EXEC_INVALID, "Unknown request type '%s' (bug?)", request->op); return NULL; } static xmlNode * handle_clear_failure_request(pcmk__request_t *request) { if (request->peer != NULL) { /* It is not currently possible to receive this as a peer command, * but will be, if we one day enable propagating this operation. */ attrd_peer_clear_failure(request); pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); return NULL; } else { remove_unsupported_sync_points(request); if (attrd_request_has_sync_point(request->xml)) { /* If this client supplied a sync point it wants to wait for, add it to * the wait list. Clients on this list will not receive an ACK until * their sync point is hit which will result in the client stalled there * until it receives a response. * * All other clients will receive the expected response as normal. */ attrd_add_client_to_waitlist(request); } else { /* If the client doesn't want to wait for a sync point, go ahead and send * the ACK immediately. Otherwise, we'll send the ACK when the appropriate * sync point is reached. */ attrd_send_ack(request->ipc_client, request->ipc_id, request->ipc_flags); } return attrd_client_clear_failure(request); } } static xmlNode * handle_confirm_request(pcmk__request_t *request) { if (request->peer != NULL) { int callid; crm_debug("Received confirmation from %s", request->peer); if (crm_element_value_int(request->xml, PCMK__XA_CALL_ID, &callid) == -1) { pcmk__set_result(&request->result, CRM_EX_PROTOCOL, PCMK_EXEC_INVALID, "Could not get callid from XML"); } else { attrd_handle_confirmation(callid, request->peer); } pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); return NULL; } else { return handle_unknown_request(request); } } static xmlNode * handle_query_request(pcmk__request_t *request) { if (request->peer != NULL) { return handle_unknown_request(request); } else { return attrd_client_query(request); } } static xmlNode * handle_remove_request(pcmk__request_t *request) { if (request->peer != NULL) { const char *host = crm_element_value(request->xml, PCMK__XA_ATTR_HOST); bool reap = false; if (pcmk__xe_get_bool_attr(request->xml, PCMK__XA_REAP, &reap) != pcmk_rc_ok) { reap = true; // Default to true for backward compatibility } attrd_peer_remove(host, reap, request->peer); pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); return NULL; } else { return attrd_client_peer_remove(request); } } static xmlNode * handle_refresh_request(pcmk__request_t *request) { if (request->peer != NULL) { return handle_unknown_request(request); } else { return attrd_client_refresh(request); } } static xmlNode * handle_sync_response_request(pcmk__request_t *request) { if (request->ipc_client != NULL) { return handle_unknown_request(request); } else { if (request->peer != NULL) { pcmk__node_status_t *peer = pcmk__get_node(0, request->peer, NULL, pcmk__node_search_cluster_member); bool peer_won = attrd_check_for_new_writer(peer, request->xml); if (!pcmk__str_eq(peer->name, attrd_cluster->priv->node_name, pcmk__str_casei)) { attrd_peer_sync_response(peer, peer_won, request->xml); } } pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); return NULL; } } static xmlNode * handle_update_request(pcmk__request_t *request) { if (request->peer != NULL) { const char *host = crm_element_value(request->xml, PCMK__XA_ATTR_HOST); pcmk__node_status_t *peer = pcmk__get_node(0, request->peer, NULL, pcmk__node_search_cluster_member); attrd_peer_update(peer, request->xml, host, false); pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); return NULL; } else { remove_unsupported_sync_points(request); if (attrd_request_has_sync_point(request->xml)) { /* If this client supplied a sync point it wants to wait for, add it to * the wait list. Clients on this list will not receive an ACK until * their sync point is hit which will result in the client stalled there * until it receives a response. * * All other clients will receive the expected response as normal. */ attrd_add_client_to_waitlist(request); } else { /* If the client doesn't want to wait for a sync point, go ahead and send * the ACK immediately. Otherwise, we'll send the ACK when the appropriate * sync point is reached. * * In the normal case, attrd_client_update can be called recursively which * makes where to send the ACK tricky. Doing it here ensures the client * only ever receives one. */ attrd_send_ack(request->ipc_client, request->ipc_id, request->flags|crm_ipc_client_response); } return attrd_client_update(request); } } static void attrd_register_handlers(void) { pcmk__server_command_t handlers[] = { { PCMK__ATTRD_CMD_CLEAR_FAILURE, handle_clear_failure_request }, { PCMK__ATTRD_CMD_CONFIRM, handle_confirm_request }, { PCMK__ATTRD_CMD_PEER_REMOVE, handle_remove_request }, { PCMK__ATTRD_CMD_QUERY, handle_query_request }, { PCMK__ATTRD_CMD_REFRESH, handle_refresh_request }, { PCMK__ATTRD_CMD_SYNC_RESPONSE, handle_sync_response_request }, { PCMK__ATTRD_CMD_UPDATE, handle_update_request }, { PCMK__ATTRD_CMD_UPDATE_DELAY, handle_update_request }, { PCMK__ATTRD_CMD_UPDATE_BOTH, handle_update_request }, { NULL, handle_unknown_request }, }; attrd_handlers = pcmk__register_handlers(handlers); } void attrd_unregister_handlers(void) { if (attrd_handlers != NULL) { g_hash_table_destroy(attrd_handlers); attrd_handlers = NULL; } } void attrd_handle_request(pcmk__request_t *request) { xmlNode *reply = NULL; char *log_msg = NULL; const char *reason = NULL; if (attrd_handlers == NULL) { attrd_register_handlers(); } reply = pcmk__process_request(request, attrd_handlers); if (reply != NULL) { crm_log_xml_trace(reply, "Reply"); if (request->ipc_client != NULL) { pcmk__ipc_send_xml(request->ipc_client, request->ipc_id, reply, request->ipc_flags); } else { crm_err("Not sending CPG reply to client"); } pcmk__xml_free(reply); } reason = request->result.exit_reason; log_msg = crm_strdup_printf("Processed %s request from %s %s: %s%s%s%s", request->op, pcmk__request_origin_type(request), pcmk__request_origin(request), pcmk_exec_status_str(request->result.execution_status), (reason == NULL)? "" : " (", pcmk__s(reason, ""), (reason == NULL)? "" : ")"); if (!pcmk__result_ok(&request->result)) { crm_warn("%s", log_msg); } else { crm_debug("%s", log_msg); } free(log_msg); pcmk__reset_request(request); } /*! \internal \brief Broadcast private attribute for local node with protocol version */ void attrd_broadcast_protocol(void) { xmlNode *attrd_op = pcmk__xe_create(NULL, __func__); crm_xml_add(attrd_op, PCMK__XA_T, PCMK__VALUE_ATTRD); crm_xml_add(attrd_op, PCMK__XA_SRC, crm_system_name); crm_xml_add(attrd_op, PCMK_XA_TASK, PCMK__ATTRD_CMD_UPDATE); crm_xml_add(attrd_op, PCMK__XA_ATTR_NAME, CRM_ATTR_PROTOCOL); crm_xml_add(attrd_op, PCMK__XA_ATTR_VALUE, ATTRD_PROTOCOL_VERSION); crm_xml_add_int(attrd_op, PCMK__XA_ATTR_IS_PRIVATE, 1); crm_xml_add(attrd_op, PCMK__XA_ATTR_HOST, attrd_cluster->priv->node_name); crm_xml_add(attrd_op, PCMK__XA_ATTR_HOST_ID, attrd_cluster->priv->node_xml_id); crm_debug("Broadcasting attrd protocol version %s for node %s", ATTRD_PROTOCOL_VERSION, attrd_cluster->priv->node_name); attrd_send_message(NULL, attrd_op, false); /* ends up at attrd_peer_message() */ pcmk__xml_free(attrd_op); } gboolean attrd_send_message(pcmk__node_status_t *node, xmlNode *data, bool confirm) { const char *op = crm_element_value(data, PCMK_XA_TASK); crm_xml_add(data, PCMK__XA_T, PCMK__VALUE_ATTRD); crm_xml_add(data, PCMK__XA_ATTR_VERSION, ATTRD_PROTOCOL_VERSION); /* Request a confirmation from the destination peer node (which could * be all if node is NULL) that the message has been received and * acted upon. */ if (!pcmk__str_eq(op, PCMK__ATTRD_CMD_CONFIRM, pcmk__str_none)) { pcmk__xe_set_bool_attr(data, PCMK__XA_CONFIRM, confirm); } attrd_xml_add_writer(data); return pcmk__cluster_send_message(node, pcmk_ipc_attrd, data); } diff --git a/include/crm/common/xml_element_internal.h b/include/crm/common/xml_element_internal.h index 939f4f7f58..0420212952 100644 --- a/include/crm/common/xml_element_internal.h +++ b/include/crm/common/xml_element_internal.h @@ -1,168 +1,168 @@ /* - * Copyright 2017-2024 the Pacemaker project contributors + * Copyright 2017-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__CRM_COMMON_XML_ELEMENT_INTERNAL__H #define PCMK__CRM_COMMON_XML_ELEMENT_INTERNAL__H /* * Internal-only wrappers for and extensions to libxml2 for processing XML * elements */ #include // bool #include // uint32_t #include // NULL #include // strcmp() #include // xmlNode, etc. #include // crm_time_t #include // crm_element_value() #include // PCMK_XA_ID #ifdef __cplusplus extern "C" { #endif const char *pcmk__xe_add_last_written(xmlNode *xe); xmlNode *pcmk__xe_first_child(const xmlNode *parent, const char *node_name, const char *attr_n, const char *attr_v); void pcmk__xe_remove_attr(xmlNode *element, const char *name); bool pcmk__xe_remove_attr_cb(xmlNode *xml, void *user_data); -void pcmk__xe_remove_matching_attrs(xmlNode *element, +void pcmk__xe_remove_matching_attrs(xmlNode *element, bool force, bool (*match)(xmlAttrPtr, void *), void *user_data); int pcmk__xe_delete_match(xmlNode *xml, xmlNode *search); int pcmk__xe_replace_match(xmlNode *xml, xmlNode *replace); int pcmk__xe_update_match(xmlNode *xml, xmlNode *update, uint32_t flags); /*! * \internal * \brief Retrieve the value of the \c PCMK_XA_ID XML attribute * * \param[in] xml XML element to check * * \return Value of the \c PCMK_XA_ID attribute (may be \c NULL) */ static inline const char * pcmk__xe_id(const xmlNode *xml) { return crm_element_value(xml, PCMK_XA_ID); } /*! * \internal * \brief Check whether an XML element is of a particular type * * \param[in] xml XML element to compare * \param[in] name XML element name to compare * * \return \c true if \p xml is of type \p name, otherwise \c false */ static inline bool pcmk__xe_is(const xmlNode *xml, const char *name) { return (xml != NULL) && (xml->name != NULL) && (name != NULL) && (strcmp((const char *) xml->name, name) == 0); } xmlNode *pcmk__xe_create(xmlNode *parent, const char *name); xmlNode *pcmk__xe_next(const xmlNode *node, const char *element_name); void pcmk__xe_set_content(xmlNode *node, const char *format, ...) G_GNUC_PRINTF(2, 3); int pcmk__xe_get_score(const xmlNode *xml, const char *name, int *score, int default_score); int pcmk__xe_copy_attrs(xmlNode *target, const xmlNode *src, uint32_t flags); void pcmk__xe_sort_attrs(xmlNode *xml); void pcmk__xe_set_id(xmlNode *xml, const char *format, ...) G_GNUC_PRINTF(2, 3); /*! * \internal * \brief Like pcmk__xe_set_props, but takes a va_list instead of * arguments directly. * * \param[in,out] node XML to add attributes to * \param[in] pairs NULL-terminated list of name/value pairs to add */ void pcmk__xe_set_propv(xmlNodePtr node, va_list pairs); /*! * \internal * \brief Add a NULL-terminated list of name/value pairs to the given * XML node as properties. * * \param[in,out] node XML node to add properties to * \param[in] ... NULL-terminated list of name/value pairs * * \note A NULL name terminates the arguments; a NULL value will be skipped. */ void pcmk__xe_set_props(xmlNodePtr node, ...) G_GNUC_NULL_TERMINATED; /*! * \internal * \brief Get first attribute of an XML element * * \param[in] xe XML element to check * * \return First attribute of \p xe (or NULL if \p xe is NULL or has none) */ static inline xmlAttr * pcmk__xe_first_attr(const xmlNode *xe) { return (xe == NULL)? NULL : xe->properties; } /*! * \internal * \brief Iterate over child elements of \p xml * * This function iterates over the children of \p xml, performing the * callback function \p handler on each node. If the callback returns * a value other than pcmk_rc_ok, the iteration stops and the value is * returned. It is therefore possible that not all children will be * visited. * * \param[in,out] xml The starting XML node. Can be NULL. * \param[in] child_element_name The name that the node must match in order * for \p handler to be run. If NULL, all * child elements will match. * \param[in] handler The callback function. * \param[in,out] userdata User data to pass to the callback function. * Can be NULL. * * \return Standard Pacemaker return code */ int pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name, int (*handler)(xmlNode *xml, void *userdata), void *userdata); int pcmk__xe_get_datetime(const xmlNode *xml, const char *attr, crm_time_t **t); int pcmk__xe_get_flags(const xmlNode *xml, const char *name, uint32_t *dest, uint32_t default_value); void pcmk__xe_set_bool_attr(xmlNodePtr node, const char *name, bool value); int pcmk__xe_get_bool_attr(const xmlNode *node, const char *name, bool *value); bool pcmk__xe_attr_is_true(const xmlNode *node, const char *name); #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_XML_ELEMENT_INTERNAL__H diff --git a/lib/common/digest.c b/lib/common/digest.c index 0884f5642b..e7d23f7088 100644 --- a/lib/common/digest.c +++ b/lib/common/digest.c @@ -1,383 +1,384 @@ /* - * Copyright 2015-2024 the Pacemaker project contributors + * Copyright 2015-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include // GString, etc. #include // gnutls_hash_fast(), gnutls_hash_get_len() #include // gnutls_strerror() #include #include #include "crmcommon_private.h" #define BEST_EFFORT_STATUS 0 /* * Pacemaker uses digests (MD5 hashes) of stringified XML to detect changes in * the CIB as a whole, a particular resource's agent parameters, and the device * parameters last used to unfence a particular node. * * "v2" digests hash pcmk__xml_string() directly, while less efficient "v1" * digests do the same with a prefixed space, suffixed newline, and optional * pre-sorting. * * On-disk CIB digests use v1 without sorting. * * Operation digests use v1 with sorting, and are stored in a resource's * operation history in the CIB status section. They come in three flavors: * - a digest of (nearly) all resource parameters and options, used to detect * any resource configuration change; * - a digest of resource parameters marked as nonreloadable, used to decide * whether a reload or full restart is needed after a configuration change; * - and a digest of resource parameters not marked as private, used in * simulations where private parameters have been removed from the input. * * Unfencing digests are set as node attributes, and are used to require * that nodes be unfenced again after a device's configuration changes. */ /*! * \internal * \brief Dump XML in a format used with v1 digests * * \param[in] xml Root of XML to dump * * \return Newly allocated buffer containing dumped XML */ static GString * dump_xml_for_digest(xmlNodePtr xml) { GString *buffer = g_string_sized_new(1024); /* for compatibility with the old result which is used for v1 digests */ g_string_append_c(buffer, ' '); pcmk__xml_string(xml, 0, buffer, 0); g_string_append_c(buffer, '\n'); return buffer; } /*! * \internal * \brief Calculate and return v1 digest of XML tree * * \param[in] input Root of XML to digest * * \return Newly allocated string containing digest * * \note Example return value: "c048eae664dba840e1d2060f00299e9d" */ static char * calculate_xml_digest_v1(xmlNode *input) { GString *buffer = dump_xml_for_digest(input); char *digest = NULL; // buffer->len > 2 for initial space and trailing newline CRM_CHECK(buffer->len > 2, g_string_free(buffer, TRUE); return NULL); digest = crm_md5sum((const char *) buffer->str); crm_log_xml_trace(input, "digest:source"); g_string_free(buffer, TRUE); return digest; } /*! * \internal * \brief Calculate and return the digest of a CIB, suitable for storing on disk * * \param[in] input Root of XML to digest * * \return Newly allocated string containing digest */ char * pcmk__digest_on_disk_cib(xmlNode *input) { /* Always use the v1 format for on-disk digests. * * Switching to v2 affects even full-restart upgrades, so it would be a * compatibility nightmare. * * We only use this once at startup. All other invocations are in a * separate child process. */ return calculate_xml_digest_v1(input); } /*! * \internal * \brief Calculate and return digest of an operation XML element * * The digest is invariant to changes in the order of XML attributes, provided * that \p input has no children. * * \param[in] input Root of XML to digest * * \return Newly allocated string containing digest */ char * pcmk__digest_operation(xmlNode *input) { /* Switching to v2 digests would likely cause restarts during rolling * upgrades. * * @TODO Confirm this. Switch to v2 if safe, or drop this TODO otherwise. */ xmlNode *sorted = pcmk__xml_copy(NULL, input); char *digest = NULL; pcmk__xe_sort_attrs(sorted); digest = calculate_xml_digest_v1(sorted); pcmk__xml_free(sorted); return digest; } /*! * \internal * \brief Calculate and return the digest of an XML tree * * \param[in] xml XML tree to digest * \param[in] filter Whether to filter certain XML attributes * * \return Newly allocated string containing digest */ char * pcmk__digest_xml(xmlNode *xml, bool filter) { /* @TODO Filtering accounts for significant CPU usage. Consider removing if * possible. */ char *digest = NULL; GString *buf = g_string_sized_new(1024); pcmk__xml_string(xml, (filter? pcmk__xml_fmt_filtered : 0), buf, 0); digest = crm_md5sum(buf->str); pcmk__if_tracing( { char *trace_file = crm_strdup_printf("%s/digest-%s", pcmk__get_tmpdir(), digest); crm_trace("Saving %s.%s.%s to %s", crm_element_value(xml, PCMK_XA_ADMIN_EPOCH), crm_element_value(xml, PCMK_XA_EPOCH), crm_element_value(xml, PCMK_XA_NUM_UPDATES), trace_file); save_xml_to_file(xml, "digest input", trace_file); free(trace_file); }, {} ); g_string_free(buf, TRUE); return digest; } /*! * \internal * \brief Check whether calculated digest of given XML matches expected digest * * \param[in] input Root of XML tree to digest * \param[in] expected Expected digest in on-disk format * * \return true if digests match, false on mismatch or error */ bool pcmk__verify_digest(xmlNode *input, const char *expected) { char *calculated = NULL; bool passed; if (input != NULL) { calculated = pcmk__digest_on_disk_cib(input); if (calculated == NULL) { crm_perror(LOG_ERR, "Could not calculate digest for comparison"); return false; } } passed = pcmk__str_eq(expected, calculated, pcmk__str_casei); if (passed) { crm_trace("Digest comparison passed: %s", calculated); } else { crm_err("Digest comparison failed: expected %s, calculated %s", expected, calculated); } free(calculated); return passed; } /*! * \internal * \brief Check whether an XML attribute should be excluded from CIB digests * * \param[in] name XML attribute name * * \return true if XML attribute should be excluded from CIB digest calculation */ bool pcmk__xa_filterable(const char *name) { static const char *filter[] = { PCMK_XA_CRM_DEBUG_ORIGIN, PCMK_XA_CIB_LAST_WRITTEN, PCMK_XA_UPDATE_ORIGIN, PCMK_XA_UPDATE_CLIENT, PCMK_XA_UPDATE_USER, }; for (int i = 0; i < PCMK__NELEM(filter); i++) { if (strcmp(name, filter[i]) == 0) { return true; } } return false; } char * crm_md5sum(const char *buffer) { char *digest = NULL; gchar *raw_digest = NULL; if (buffer == NULL) { return NULL; } raw_digest = g_compute_checksum_for_string(G_CHECKSUM_MD5, buffer, -1); if (raw_digest == NULL) { crm_err("Failed to calculate hash"); return NULL; } digest = pcmk__str_copy(raw_digest); g_free(raw_digest); crm_trace("Digest %s.", digest); return digest; } // Return true if a is an attribute that should be filtered static bool should_filter_for_digest(xmlAttrPtr a, void *user_data) { if (strncmp((const char *) a->name, CRM_META "_", sizeof(CRM_META " ") - 1) == 0) { return true; } return pcmk__str_any_of((const char *) a->name, PCMK_XA_ID, PCMK_XA_CRM_FEATURE_SET, PCMK__XA_OP_DIGEST, PCMK__META_ON_NODE, PCMK__META_ON_NODE_UUID, "pcmk_external_ip", NULL); } /*! * \internal * \brief Remove XML attributes not needed for operation digest * * \param[in,out] param_set XML with operation parameters */ void pcmk__filter_op_for_digest(xmlNode *param_set) { char *key = NULL; char *timeout = NULL; guint interval_ms = 0; if (param_set == NULL) { return; } /* Timeout is useful for recurring operation digests, so grab it before * removing meta-attributes */ key = crm_meta_name(PCMK_META_INTERVAL); if (crm_element_value_ms(param_set, key, &interval_ms) != pcmk_ok) { interval_ms = 0; } free(key); key = NULL; if (interval_ms != 0) { key = crm_meta_name(PCMK_META_TIMEOUT); timeout = crm_element_value_copy(param_set, key); } // Remove all CRM_meta_* attributes and certain other attributes - pcmk__xe_remove_matching_attrs(param_set, should_filter_for_digest, NULL); + pcmk__xe_remove_matching_attrs(param_set, false, should_filter_for_digest, + NULL); // Add timeout back for recurring operation digests if (timeout != NULL) { crm_xml_add(param_set, key, timeout); } free(timeout); free(key); } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include #include char * calculate_on_disk_digest(xmlNode *input) { return calculate_xml_digest_v1(input); } char * calculate_operation_digest(xmlNode *input, const char *version) { xmlNode *sorted = sorted_xml(input, NULL, true); char *digest = calculate_xml_digest_v1(sorted); pcmk__xml_free(sorted); return digest; } char * calculate_xml_versioned_digest(xmlNode *input, gboolean sort, gboolean do_filter, const char *version) { if ((version == NULL) || (compare_version("3.0.5", version) > 0)) { xmlNode *sorted = NULL; char *digest = NULL; if (sort) { xmlNode *sorted = sorted_xml(input, NULL, true); input = sorted; } crm_trace("Using v1 digest algorithm for %s", pcmk__s(version, "unknown feature set")); digest = calculate_xml_digest_v1(input); pcmk__xml_free(sorted); return digest; } crm_trace("Using v2 digest algorithm for %s", version); return pcmk__digest_xml(input, do_filter); } // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/common/patchset.c b/lib/common/patchset.c index 91a16aae2e..4f1fcbadf7 100644 --- a/lib/common/patchset.c +++ b/lib/common/patchset.c @@ -1,882 +1,884 @@ /* * Copyright 2004-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include // xmlNode #include #include #include #include // CRM_XML_LOG_BASE, etc. #include "crmcommon_private.h" /* Add changes for specified XML to patchset. * For patchset format, refer to diff schema. */ static void add_xml_changes_to_patchset(xmlNode *xml, xmlNode *patchset) { xmlNode *cIter = NULL; xmlAttr *pIter = NULL; xmlNode *change = NULL; xml_node_private_t *nodepriv = xml->_private; const char *value = NULL; if (nodepriv == NULL) { /* Elements that shouldn't occur in a CIB don't have _private set. They * should be stripped out, ignored, or have an error thrown by any code * that processes their parent, so we ignore any changes to them. */ return; } // If this XML node is new, just report that if (patchset && pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { GString *xpath = pcmk__element_xpath(xml->parent); if (xpath != NULL) { int position = pcmk__xml_position(xml, pcmk__xf_deleted); change = pcmk__xe_create(patchset, PCMK_XE_CHANGE); crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_CREATE); crm_xml_add(change, PCMK_XA_PATH, (const char *) xpath->str); crm_xml_add_int(change, PCMK_XE_POSITION, position); pcmk__xml_copy(change, xml); g_string_free(xpath, TRUE); } return; } // Check each of the XML node's attributes for changes for (pIter = pcmk__xe_first_attr(xml); pIter != NULL; pIter = pIter->next) { xmlNode *attr = NULL; nodepriv = pIter->_private; if (!pcmk_any_flags_set(nodepriv->flags, pcmk__xf_deleted|pcmk__xf_dirty)) { continue; } if (change == NULL) { GString *xpath = pcmk__element_xpath(xml); if (xpath != NULL) { change = pcmk__xe_create(patchset, PCMK_XE_CHANGE); crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_MODIFY); crm_xml_add(change, PCMK_XA_PATH, (const char *) xpath->str); change = pcmk__xe_create(change, PCMK_XE_CHANGE_LIST); g_string_free(xpath, TRUE); } } attr = pcmk__xe_create(change, PCMK_XE_CHANGE_ATTR); crm_xml_add(attr, PCMK_XA_NAME, (const char *) pIter->name); if (nodepriv->flags & pcmk__xf_deleted) { crm_xml_add(attr, PCMK_XA_OPERATION, "unset"); } else { crm_xml_add(attr, PCMK_XA_OPERATION, "set"); value = pcmk__xml_attr_value(pIter); crm_xml_add(attr, PCMK_XA_VALUE, value); } } if (change) { xmlNode *result = NULL; change = pcmk__xe_create(change->parent, PCMK_XE_CHANGE_RESULT); result = pcmk__xe_create(change, (const char *)xml->name); for (pIter = pcmk__xe_first_attr(xml); pIter != NULL; pIter = pIter->next) { nodepriv = pIter->_private; if (!pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) { value = crm_element_value(xml, (const char *) pIter->name); crm_xml_add(result, (const char *)pIter->name, value); } } } // Now recursively do the same for each child node of this node for (cIter = pcmk__xml_first_child(xml); cIter != NULL; cIter = pcmk__xml_next(cIter)) { add_xml_changes_to_patchset(cIter, patchset); } nodepriv = xml->_private; if (patchset && pcmk_is_set(nodepriv->flags, pcmk__xf_moved)) { GString *xpath = pcmk__element_xpath(xml); crm_trace("%s.%s moved to position %d", xml->name, pcmk__xe_id(xml), pcmk__xml_position(xml, pcmk__xf_skip)); if (xpath != NULL) { change = pcmk__xe_create(patchset, PCMK_XE_CHANGE); crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_MOVE); crm_xml_add(change, PCMK_XA_PATH, (const char *) xpath->str); crm_xml_add_int(change, PCMK_XE_POSITION, pcmk__xml_position(xml, pcmk__xf_deleted)); g_string_free(xpath, TRUE); } } } static bool is_config_change(xmlNode *xml) { GList *gIter = NULL; xml_node_private_t *nodepriv = NULL; xml_doc_private_t *docpriv; xmlNode *config = pcmk__xe_first_child(xml, PCMK_XE_CONFIGURATION, NULL, NULL); if (config) { nodepriv = config->_private; } if ((nodepriv != NULL) && pcmk_is_set(nodepriv->flags, pcmk__xf_dirty)) { return TRUE; } if ((xml->doc != NULL) && (xml->doc->_private != NULL)) { docpriv = xml->doc->_private; for (gIter = docpriv->deleted_objs; gIter; gIter = gIter->next) { pcmk__deleted_xml_t *deleted_obj = gIter->data; if (strstr(deleted_obj->path, "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION) != NULL) { return TRUE; } } } return FALSE; } static xmlNode * xml_create_patchset_v2(xmlNode *source, xmlNode *target) { int lpc = 0; GList *gIter = NULL; xml_doc_private_t *docpriv; xmlNode *v = NULL; xmlNode *version = NULL; xmlNode *patchset = NULL; const char *vfields[] = { PCMK_XA_ADMIN_EPOCH, PCMK_XA_EPOCH, PCMK_XA_NUM_UPDATES, }; pcmk__assert(target != NULL); if (!pcmk__xml_doc_all_flags_set(target->doc, pcmk__xf_dirty)) { return NULL; } pcmk__assert(target->doc != NULL); docpriv = target->doc->_private; patchset = pcmk__xe_create(NULL, PCMK_XE_DIFF); crm_xml_add_int(patchset, PCMK_XA_FORMAT, 2); version = pcmk__xe_create(patchset, PCMK_XE_VERSION); v = pcmk__xe_create(version, PCMK_XE_SOURCE); for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { const char *value = crm_element_value(source, vfields[lpc]); if (value == NULL) { value = "1"; } crm_xml_add(v, vfields[lpc], value); } v = pcmk__xe_create(version, PCMK_XE_TARGET); for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { const char *value = crm_element_value(target, vfields[lpc]); if (value == NULL) { value = "1"; } crm_xml_add(v, vfields[lpc], value); } for (gIter = docpriv->deleted_objs; gIter; gIter = gIter->next) { pcmk__deleted_xml_t *deleted_obj = gIter->data; xmlNode *change = pcmk__xe_create(patchset, PCMK_XE_CHANGE); crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_DELETE); crm_xml_add(change, PCMK_XA_PATH, deleted_obj->path); if (deleted_obj->position >= 0) { crm_xml_add_int(change, PCMK_XE_POSITION, deleted_obj->position); } } add_xml_changes_to_patchset(target, patchset); return patchset; } xmlNode * xml_create_patchset(int format, xmlNode *source, xmlNode *target, bool *config_changed, bool manage_version) { bool local_config_changed = false; if (format == 0) { format = 2; } if (format != 2) { crm_err("Unknown patch format: %d", format); return NULL; } xml_acl_disable(target); if ((target == NULL) || !pcmk__xml_doc_all_flags_set(target->doc, pcmk__xf_dirty)) { crm_trace("No change %d", format); return NULL; } if (config_changed == NULL) { config_changed = &local_config_changed; } *config_changed = is_config_change(target); if (manage_version) { int counter = 0; if (*config_changed) { crm_xml_add(target, PCMK_XA_NUM_UPDATES, "0"); crm_element_value_int(target, PCMK_XA_EPOCH, &counter); crm_xml_add_int(target, PCMK_XA_EPOCH, counter + 1); } else { crm_element_value_int(target, PCMK_XA_NUM_UPDATES, &counter); crm_xml_add_int(target, PCMK_XA_NUM_UPDATES, counter + 1); } } return xml_create_patchset_v2(source, target); } void patchset_process_digest(xmlNode *patch, xmlNode *source, xmlNode *target, bool with_digest) { char *digest = NULL; if ((patch == NULL) || (source == NULL) || (target == NULL) || !with_digest) { return; } /* We should always call xml_accept_changes() before calculating a digest. * Otherwise, with an on-tracking dirty target, we could get a wrong digest. */ CRM_LOG_ASSERT(!pcmk__xml_doc_all_flags_set(target->doc, pcmk__xf_dirty)); digest = pcmk__digest_xml(target, true); crm_xml_add(patch, PCMK__XA_DIGEST, digest); free(digest); return; } // Get CIB versions used for additions and deletions in a patchset bool xml_patch_versions(const xmlNode *patchset, int add[3], int del[3]) { static const char *const vfields[] = { PCMK_XA_ADMIN_EPOCH, PCMK_XA_EPOCH, PCMK_XA_NUM_UPDATES, }; const xmlNode *version = pcmk__xe_first_child(patchset, PCMK_XE_VERSION, NULL, NULL); const xmlNode *source = pcmk__xe_first_child(version, PCMK_XE_SOURCE, NULL, NULL); const xmlNode *target = pcmk__xe_first_child(version, PCMK_XE_TARGET, NULL, NULL); int format = 1; crm_element_value_int(patchset, PCMK_XA_FORMAT, &format); if (format != 2) { crm_err("Unknown patch format: %d", format); return -EINVAL; } if (source != NULL) { for (int i = 0; i < PCMK__NELEM(vfields); i++) { crm_element_value_int(source, vfields[i], &(del[i])); crm_trace("Got %d for del[%s]", del[i], vfields[i]); } } if (target != NULL) { for (int i = 0; i < PCMK__NELEM(vfields); i++) { crm_element_value_int(target, vfields[i], &(add[i])); crm_trace("Got %d for add[%s]", add[i], vfields[i]); } } return pcmk_ok; } /*! * \internal * \brief Check whether patchset can be applied to current CIB * * \param[in] xml Root of current CIB * \param[in] patchset Patchset to check * * \return Standard Pacemaker return code */ static int xml_patch_version_check(const xmlNode *xml, const xmlNode *patchset) { int lpc = 0; bool changed = FALSE; int this[] = { 0, 0, 0 }; int add[] = { 0, 0, 0 }; int del[] = { 0, 0, 0 }; const char *vfields[] = { PCMK_XA_ADMIN_EPOCH, PCMK_XA_EPOCH, PCMK_XA_NUM_UPDATES, }; for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { crm_element_value_int(xml, vfields[lpc], &(this[lpc])); crm_trace("Got %d for this[%s]", this[lpc], vfields[lpc]); if (this[lpc] < 0) { this[lpc] = 0; } } /* Set some defaults in case nothing is present */ add[0] = this[0]; add[1] = this[1]; add[2] = this[2] + 1; for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { del[lpc] = this[lpc]; } xml_patch_versions(patchset, add, del); for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { if (this[lpc] < del[lpc]) { crm_debug("Current %s is too low (%d.%d.%d < %d.%d.%d --> %d.%d.%d)", vfields[lpc], this[0], this[1], this[2], del[0], del[1], del[2], add[0], add[1], add[2]); return pcmk_rc_diff_resync; } else if (this[lpc] > del[lpc]) { crm_info("Current %s is too high (%d.%d.%d > %d.%d.%d --> %d.%d.%d) %p", vfields[lpc], this[0], this[1], this[2], del[0], del[1], del[2], add[0], add[1], add[2], patchset); crm_log_xml_info(patchset, "OldPatch"); return pcmk_rc_old_data; } } for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { if (add[lpc] > del[lpc]) { changed = TRUE; } } if (!changed) { crm_notice("Versions did not change in patch %d.%d.%d", add[0], add[1], add[2]); return pcmk_rc_old_data; } crm_debug("Can apply patch %d.%d.%d to %d.%d.%d", add[0], add[1], add[2], this[0], this[1], this[2]); return pcmk_rc_ok; } // Return first child matching element name and optionally id or position static xmlNode * first_matching_xml_child(const xmlNode *parent, const char *name, const char *id, int position) { xmlNode *cIter = NULL; for (cIter = pcmk__xml_first_child(parent); cIter != NULL; cIter = pcmk__xml_next(cIter)) { if (strcmp((const char *) cIter->name, name) != 0) { continue; } else if (id) { const char *cid = pcmk__xe_id(cIter); if ((cid == NULL) || (strcmp(cid, id) != 0)) { continue; } } // "position" makes sense only for XML comments for now if ((cIter->type == XML_COMMENT_NODE) && (position >= 0) && (pcmk__xml_position(cIter, pcmk__xf_skip) != position)) { continue; } return cIter; } return NULL; } /*! * \internal * \brief Simplified, more efficient alternative to pcmk__xpath_find_one() * * \param[in] top Root of XML to search * \param[in] key Search xpath * \param[in] target_position If deleting, where to delete * * \return XML child matching xpath if found, NULL otherwise * * \note This only works on simplified xpaths found in v2 patchset diffs, * i.e. the only allowed search predicate is [@id='XXX']. */ static xmlNode * search_v2_xpath(const xmlNode *top, const char *key, int target_position) { xmlNode *target = (xmlNode *) top->doc; const char *current = key; char *section; char *remainder; char *id; char *tag; char *path = NULL; int rc; size_t key_len; CRM_CHECK(key != NULL, return NULL); key_len = strlen(key); /* These are scanned from key after a slash, so they can't be bigger * than key_len - 1 characters plus a null terminator. */ remainder = pcmk__assert_alloc(key_len, sizeof(char)); section = pcmk__assert_alloc(key_len, sizeof(char)); id = pcmk__assert_alloc(key_len, sizeof(char)); tag = pcmk__assert_alloc(key_len, sizeof(char)); do { // Look for /NEXT_COMPONENT/REMAINING_COMPONENTS rc = sscanf(current, "/%[^/]%s", section, remainder); if (rc > 0) { // Separate FIRST_COMPONENT into TAG[@id='ID'] int f = sscanf(section, "%[^[][@" PCMK_XA_ID "='%[^']", tag, id); int current_position = -1; /* The target position is for the final component tag, so only use * it if there is nothing left to search after this component. */ if ((rc == 1) && (target_position >= 0)) { current_position = target_position; } switch (f) { case 1: target = first_matching_xml_child(target, tag, NULL, current_position); break; case 2: target = first_matching_xml_child(target, tag, id, current_position); break; default: // This should not be possible target = NULL; break; } current = remainder; } // Continue if something remains to search, and we've matched so far } while ((rc == 2) && target); if (target) { crm_trace("Found %s for %s", (path = (char *) xmlGetNodePath(target)), key); free(path); } else { crm_debug("No match for %s", key); } free(remainder); free(section); free(tag); free(id); return target; } typedef struct xml_change_obj_s { const xmlNode *change; xmlNode *match; } xml_change_obj_t; static gint sort_change_obj_by_position(gconstpointer a, gconstpointer b) { const xml_change_obj_t *change_obj_a = a; const xml_change_obj_t *change_obj_b = b; int position_a = -1; int position_b = -1; crm_element_value_int(change_obj_a->change, PCMK_XE_POSITION, &position_a); crm_element_value_int(change_obj_b->change, PCMK_XE_POSITION, &position_b); if (position_a < position_b) { return -1; } else if (position_a > position_b) { return 1; } return 0; } /*! * \internal * \brief Apply a version 2 patchset to an XML node * * \param[in,out] xml XML to apply patchset to * \param[in] patchset Patchset to apply * * \return Standard Pacemaker return code */ static int apply_v2_patchset(xmlNode *xml, const xmlNode *patchset) { int rc = pcmk_rc_ok; const xmlNode *change = NULL; GList *change_objs = NULL; GList *gIter = NULL; for (change = pcmk__xml_first_child(patchset); change != NULL; change = pcmk__xml_next(change)) { xmlNode *match = NULL; const char *op = crm_element_value(change, PCMK_XA_OPERATION); const char *xpath = crm_element_value(change, PCMK_XA_PATH); int position = -1; if (op == NULL) { continue; } crm_trace("Processing %s %s", change->name, op); /* PCMK_VALUE_DELETE changes for XML comments are generated with * PCMK_XE_POSITION */ if (strcmp(op, PCMK_VALUE_DELETE) == 0) { crm_element_value_int(change, PCMK_XE_POSITION, &position); } match = search_v2_xpath(xml, xpath, position); crm_trace("Performing %s on %s with %p", op, xpath, match); if ((match == NULL) && (strcmp(op, PCMK_VALUE_DELETE) == 0)) { crm_debug("No %s match for %s in %p", op, xpath, xml->doc); continue; } else if (match == NULL) { crm_err("No %s match for %s in %p", op, xpath, xml->doc); rc = pcmk_rc_diff_failed; continue; } else if (pcmk__str_any_of(op, PCMK_VALUE_CREATE, PCMK_VALUE_MOVE, NULL)) { // Delay the adding of a PCMK_VALUE_CREATE object xml_change_obj_t *change_obj = pcmk__assert_alloc(1, sizeof(xml_change_obj_t)); change_obj->change = change; change_obj->match = match; change_objs = g_list_append(change_objs, change_obj); if (strcmp(op, PCMK_VALUE_MOVE) == 0) { // Temporarily put the PCMK_VALUE_MOVE object after the last sibling if ((match->parent != NULL) && (match->parent->last != NULL)) { xmlAddNextSibling(match->parent->last, match); } } } else if (strcmp(op, PCMK_VALUE_DELETE) == 0) { pcmk__xml_free(match); } else if (strcmp(op, PCMK_VALUE_MODIFY) == 0) { const xmlNode *child = pcmk__xe_first_child(change, PCMK_XE_CHANGE_RESULT, NULL, NULL); const xmlNode *attrs = pcmk__xml_first_child(child); if (attrs == NULL) { rc = ENOMSG; continue; } - pcmk__xe_remove_matching_attrs(match, NULL, NULL); // Remove all + + // Remove all attributes + pcmk__xe_remove_matching_attrs(match, false, NULL, NULL); for (xmlAttrPtr pIter = pcmk__xe_first_attr(attrs); pIter != NULL; pIter = pIter->next) { const char *name = (const char *) pIter->name; const char *value = pcmk__xml_attr_value(pIter); crm_xml_add(match, name, value); } } else { crm_err("Unknown operation: %s", op); rc = pcmk_rc_diff_failed; } } // Changes should be generated in the right order. Double checking. change_objs = g_list_sort(change_objs, sort_change_obj_by_position); for (gIter = change_objs; gIter; gIter = gIter->next) { xml_change_obj_t *change_obj = gIter->data; xmlNode *match = change_obj->match; const char *op = NULL; const char *xpath = NULL; change = change_obj->change; op = crm_element_value(change, PCMK_XA_OPERATION); xpath = crm_element_value(change, PCMK_XA_PATH); crm_trace("Continue performing %s on %s with %p", op, xpath, match); if (strcmp(op, PCMK_VALUE_CREATE) == 0) { int position = 0; xmlNode *child = NULL; xmlNode *match_child = NULL; match_child = match->children; crm_element_value_int(change, PCMK_XE_POSITION, &position); while ((match_child != NULL) && (position != pcmk__xml_position(match_child, pcmk__xf_skip))) { match_child = match_child->next; } child = pcmk__xml_copy(match, change->children); if (match_child != NULL) { crm_trace("Adding %s at position %d", child->name, position); xmlAddPrevSibling(match_child, child); } else { crm_trace("Adding %s at position %d (end)", child->name, position); } } else if (strcmp(op, PCMK_VALUE_MOVE) == 0) { int position = 0; crm_element_value_int(change, PCMK_XE_POSITION, &position); if (position != pcmk__xml_position(match, pcmk__xf_skip)) { xmlNode *match_child = NULL; int p = position; if (p > pcmk__xml_position(match, pcmk__xf_skip)) { p++; // Skip ourselves } pcmk__assert(match->parent != NULL); match_child = match->parent->children; while ((match_child != NULL) && (p != pcmk__xml_position(match_child, pcmk__xf_skip))) { match_child = match_child->next; } crm_trace("Moving %s to position %d (was %d, prev %p, %s %p)", match->name, position, pcmk__xml_position(match, pcmk__xf_skip), match->prev, (match_child? "next":"last"), (match_child? match_child : match->parent->last)); if (match_child) { xmlAddPrevSibling(match_child, match); } else { pcmk__assert(match->parent->last != NULL); xmlAddNextSibling(match->parent->last, match); } } else { crm_trace("%s is already in position %d", match->name, position); } if (position != pcmk__xml_position(match, pcmk__xf_skip)) { crm_err("Moved %s.%s to position %d instead of %d (%p)", match->name, pcmk__xe_id(match), pcmk__xml_position(match, pcmk__xf_skip), position, match->prev); rc = pcmk_rc_diff_failed; } } } g_list_free_full(change_objs, free); return rc; } int xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version) { int format = 1; int rc = pcmk_ok; xmlNode *old = NULL; const char *digest = NULL; if (patchset == NULL) { return rc; } pcmk__log_xml_patchset(LOG_TRACE, patchset); if (check_version) { rc = pcmk_rc2legacy(xml_patch_version_check(xml, patchset)); if (rc != pcmk_ok) { return rc; } } digest = crm_element_value(patchset, PCMK__XA_DIGEST); if (digest != NULL) { /* Make original XML available for logging in case result doesn't have * expected digest */ pcmk__if_tracing(old = pcmk__xml_copy(NULL, xml), {}); } if (rc == pcmk_ok) { crm_element_value_int(patchset, PCMK_XA_FORMAT, &format); if (format != 2) { crm_err("Unknown patch format: %d", format); rc = -EINVAL; } else { rc = pcmk_rc2legacy(apply_v2_patchset(xml, patchset)); } } if ((rc == pcmk_ok) && (digest != NULL)) { char *new_digest = NULL; new_digest = pcmk__digest_xml(xml, true); if (!pcmk__str_eq(new_digest, digest, pcmk__str_casei)) { crm_info("v%d digest mis-match: expected %s, calculated %s", format, digest, new_digest); rc = -pcmk_err_diff_failed; pcmk__if_tracing( { save_xml_to_file(old, "PatchDigest:input", NULL); save_xml_to_file(xml, "PatchDigest:result", NULL); save_xml_to_file(patchset, "PatchDigest:diff", NULL); }, {} ); } else { crm_trace("v%d digest matched: expected %s, calculated %s", format, digest, new_digest); } free(new_digest); } pcmk__xml_free(old); return rc; } bool pcmk__cib_element_in_patchset(const xmlNode *patchset, const char *element) { const char *element_xpath = pcmk__cib_abs_xpath_for(element); const char *parent_xpath = pcmk_cib_parent_name_for(element); char *element_regex = NULL; bool rc = false; int format = 1; pcmk__assert(patchset != NULL); crm_element_value_int(patchset, PCMK_XA_FORMAT, &format); if (format != 2) { crm_warn("Unknown patch format: %d", format); return false; } CRM_CHECK(element_xpath != NULL, return false); // Unsupported element /* Matches if and only if element_xpath is part of a changed path * (supported values for element never contain XML IDs with schema * validation enabled) * * @TODO Use POSIX word boundary instead of (/|$), if it works: * https://www.regular-expressions.info/wordboundaries.html. */ element_regex = crm_strdup_printf("^%s(/|$)", element_xpath); for (const xmlNode *change = pcmk__xe_first_child(patchset, PCMK_XE_CHANGE, NULL, NULL); change != NULL; change = pcmk__xe_next(change, PCMK_XE_CHANGE)) { const char *op = crm_element_value(change, PCMK_XA_OPERATION); const char *diff_xpath = crm_element_value(change, PCMK_XA_PATH); if (pcmk__str_eq(diff_xpath, element_regex, pcmk__str_regex)) { // Change to an existing element rc = true; break; } if (pcmk__str_eq(op, PCMK_VALUE_CREATE, pcmk__str_none) && pcmk__str_eq(diff_xpath, parent_xpath, pcmk__str_none) && pcmk__xe_is(pcmk__xe_first_child(change, NULL, NULL, NULL), element)) { // Newly added element rc = true; break; } } free(element_regex); return rc; } diff --git a/lib/common/xml.c b/lib/common/xml.c index fe3eae7269..74bf14aa84 100644 --- a/lib/common/xml.c +++ b/lib/common/xml.c @@ -1,1664 +1,1660 @@ /* * Copyright 2004-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include // uint32_t #include #include #include #include // stat(), S_ISREG, etc. #include #include // gboolean, GString #include // xmlCleanupParser() #include // xmlNode, etc. #include // xmlChar, xmlGetUTF8Char() #include #include #include // PCMK__XML_LOG_BASE, etc. #include "crmcommon_private.h" //! libxml2 supports only XML version 1.0, at least as of libxml2-2.12.5 #define XML_VERSION ((const xmlChar *) "1.0") /*! * \internal * \brief Get a string representation of an XML element type for logging * * \param[in] type XML element type * * \return String representation of \p type */ const char * pcmk__xml_element_type_text(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", }; // Assumes the numeric values of the indices are in ascending order if ((type < XML_ELEMENT_NODE) || (type > XML_XINCLUDE_END)) { return "unrecognized type"; } return element_type_names[type]; } /*! * \internal * \brief Apply a function to each XML node in a tree (pre-order, depth-first) * * \param[in,out] xml XML tree to traverse * \param[in,out] fn Function to call for each node (returns \c true to * continue traversing the tree or \c false to stop) * \param[in,out] user_data Argument to \p fn * * \return \c false if any \p fn call returned \c false, or \c true otherwise * * \note This function is recursive. */ bool pcmk__xml_tree_foreach(xmlNode *xml, bool (*fn)(xmlNode *, void *), void *user_data) { if (xml == NULL) { return true; } if (!fn(xml, user_data)) { return false; } for (xml = pcmk__xml_first_child(xml); xml != NULL; xml = pcmk__xml_next(xml)) { if (!pcmk__xml_tree_foreach(xml, fn, user_data)) { return false; } } return true; } void pcmk__xml_set_parent_flags(xmlNode *xml, uint64_t flags) { for (; xml != NULL; xml = xml->parent) { xml_node_private_t *nodepriv = xml->_private; if (nodepriv != NULL) { pcmk__set_xml_flags(nodepriv, flags); } } } /*! * \internal * \brief Set flags for an XML document * * \param[in,out] doc XML document * \param[in] flags Group of enum xml_private_flags */ void pcmk__xml_doc_set_flags(xmlDoc *doc, uint32_t flags) { if (doc != NULL) { xml_doc_private_t *docpriv = doc->_private; pcmk__set_xml_flags(docpriv, flags); } } /*! * \internal * \brief Check whether the given flags are set for an XML document * * \param[in] doc XML document to check * \param[in] flags Group of enum xml_private_flags * * \return \c true if all of \p flags are set for \p doc, or \c false otherwise */ bool pcmk__xml_doc_all_flags_set(const xmlDoc *doc, uint32_t flags) { if (doc != NULL) { xml_doc_private_t *docpriv = doc->_private; return (docpriv != NULL) && pcmk_all_flags_set(docpriv->flags, flags); } return false; } // Mark document, element, and all element's parents as changed void pcmk__mark_xml_node_dirty(xmlNode *xml) { if (xml == NULL) { return; } pcmk__xml_doc_set_flags(xml->doc, pcmk__xf_dirty); pcmk__xml_set_parent_flags(xml, pcmk__xf_dirty); } /*! * \internal * \brief Clear flags on an XML node * * \param[in,out] xml XML node whose flags to reset * \param[in,out] user_data Ignored * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ bool pcmk__xml_reset_node_flags(xmlNode *xml, void *user_data) { xml_node_private_t *nodepriv = xml->_private; if (nodepriv != NULL) { nodepriv->flags = pcmk__xf_none; } return true; } /*! * \internal * \brief Set the \c pcmk__xf_dirty and \c pcmk__xf_created flags on an XML node * * \param[in,out] xml Node whose flags to set * \param[in] user_data Ignored * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool mark_xml_dirty_created(xmlNode *xml, void *user_data) { xml_node_private_t *nodepriv = xml->_private; if (nodepriv != NULL) { pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created); } return true; } /*! * \internal * \brief Mark an XML tree as dirty and created, and mark its parents dirty * * Also mark the document dirty. * * \param[in,out] xml Tree to mark as dirty and created */ static void mark_xml_tree_dirty_created(xmlNode *xml) { pcmk__assert(xml != NULL); if (!pcmk__xml_doc_all_flags_set(xml->doc, pcmk__xf_tracking)) { // Tracking is disabled for entire document return; } // Mark all parents and document dirty pcmk__mark_xml_node_dirty(xml); pcmk__xml_tree_foreach(xml, mark_xml_dirty_created, NULL); } // Free an XML object previously marked as deleted static void free_deleted_object(void *data) { if(data) { pcmk__deleted_xml_t *deleted_obj = data; g_free(deleted_obj->path); free(deleted_obj); } } // Free and NULL user, ACLs, and deleted objects in an XML node's private data static void reset_xml_private_data(xml_doc_private_t *docpriv) { if (docpriv != NULL) { pcmk__assert(docpriv->check == PCMK__XML_DOC_PRIVATE_MAGIC); pcmk__str_update(&(docpriv->acl_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; } } } /*! * \internal * \brief Allocate and initialize private data for an XML node * * \param[in,out] node XML node whose private data to initialize * \param[in] user_data Ignored * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool new_private_data(xmlNode *node, void *user_data) { CRM_CHECK(node != NULL, return true); if (node->_private != NULL) { return true; } switch (node->type) { case XML_DOCUMENT_NODE: { xml_doc_private_t *docpriv = pcmk__assert_alloc(1, sizeof(xml_doc_private_t)); docpriv->check = PCMK__XML_DOC_PRIVATE_MAGIC; node->_private = docpriv; pcmk__set_xml_flags(docpriv, pcmk__xf_dirty|pcmk__xf_created); } break; case XML_ELEMENT_NODE: case XML_ATTRIBUTE_NODE: case XML_COMMENT_NODE: { xml_node_private_t *nodepriv = pcmk__assert_alloc(1, sizeof(xml_node_private_t)); nodepriv->check = PCMK__XML_NODE_PRIVATE_MAGIC; node->_private = nodepriv; pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created); for (xmlAttr *iter = pcmk__xe_first_attr(node); iter != NULL; iter = iter->next) { new_private_data((xmlNode *) iter, user_data); } } break; case XML_TEXT_NODE: case XML_DTD_NODE: case XML_CDATA_SECTION_NODE: return true; default: CRM_LOG_ASSERT(node->type == XML_ELEMENT_NODE); return true; } if (pcmk__xml_doc_all_flags_set(node->doc, pcmk__xf_tracking)) { pcmk__mark_xml_node_dirty(node); } return true; } /*! * \internal * \brief Free private data for an XML node * * \param[in,out] node XML node whose private data to free * \param[in] user_data Ignored * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool free_private_data(xmlNode *node, void *user_data) { CRM_CHECK(node != NULL, return true); if (node->_private == NULL) { return true; } if (node->type == XML_DOCUMENT_NODE) { reset_xml_private_data((xml_doc_private_t *) node->_private); } else { xml_node_private_t *nodepriv = node->_private; pcmk__assert(nodepriv->check == PCMK__XML_NODE_PRIVATE_MAGIC); for (xmlAttr *iter = pcmk__xe_first_attr(node); iter != NULL; iter = iter->next) { free_private_data((xmlNode *) iter, user_data); } } free(node->_private); node->_private = NULL; return true; } /*! * \internal * \brief Allocate and initialize private data recursively for an XML tree * * \param[in,out] node XML node whose private data to initialize */ void pcmk__xml_new_private_data(xmlNode *xml) { pcmk__xml_tree_foreach(xml, new_private_data, NULL); } /*! * \internal * \brief Free private data recursively for an XML tree * * \param[in,out] node XML node whose private data to free */ void pcmk__xml_free_private_data(xmlNode *xml) { pcmk__xml_tree_foreach(xml, free_private_data, NULL); } void xml_track_changes(xmlNode * xml, const char *user, xmlNode *acl_source, bool enforce_acls) { if (xml == NULL) { return; } xml_accept_changes(xml); crm_trace("Tracking changes%s to %p", enforce_acls?" with ACLs":"", xml); pcmk__xml_doc_set_flags(xml->doc, pcmk__xf_tracking); if(enforce_acls) { if(acl_source == NULL) { acl_source = xml; } pcmk__xml_doc_set_flags(xml->doc, pcmk__xf_acl_enabled); pcmk__unpack_acl(acl_source, xml, user); pcmk__apply_acl(xml); } } /*! * \internal * \brief Return ordinal position of an XML node among its siblings * * \param[in] xml XML node to check * \param[in] ignore_if_set Don't count siblings with this flag set * * \return Ordinal position of \p xml (starting with 0) */ int pcmk__xml_position(const xmlNode *xml, enum xml_private_flags ignore_if_set) { int position = 0; for (const xmlNode *cIter = xml; cIter->prev; cIter = cIter->prev) { xml_node_private_t *nodepriv = ((xmlNode*)cIter->prev)->_private; if (!pcmk_is_set(nodepriv->flags, ignore_if_set)) { position++; } } return position; } /*! * \internal * \brief Remove all attributes marked as deleted from an XML node * * \param[in,out] xml XML node whose deleted attributes to remove * \param[in,out] user_data Ignored * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool accept_attr_deletions(xmlNode *xml, void *user_data) { pcmk__xml_reset_node_flags(xml, NULL); - pcmk__xe_remove_matching_attrs(xml, pcmk__marked_as_deleted, NULL); + pcmk__xe_remove_matching_attrs(xml, true, pcmk__marked_as_deleted, NULL); return true; } /*! * \internal * \brief Find first child XML node matching another given XML node * * \param[in] haystack XML whose children should be checked * \param[in] needle XML to match (comment content or element name and ID) * \param[in] exact If true and needle is a comment, position must match */ xmlNode * pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle, bool exact) { CRM_CHECK(needle != NULL, return NULL); if (needle->type == XML_COMMENT_NODE) { return pcmk__xc_match(haystack, needle, exact); } else { const char *id = pcmk__xe_id(needle); const char *attr = (id == NULL)? NULL : PCMK_XA_ID; return pcmk__xe_first_child(haystack, (const char *) needle->name, attr, id); } } void xml_accept_changes(xmlNode * xml) { xmlNode *top = NULL; xml_doc_private_t *docpriv = NULL; if(xml == NULL) { return; } crm_trace("Accepting changes to %p", xml); docpriv = xml->doc->_private; top = xmlDocGetRootElement(xml->doc); - reset_xml_private_data(xml->doc->_private); - - if (!pcmk_is_set(docpriv->flags, pcmk__xf_dirty)) { - docpriv->flags = pcmk__xf_none; - return; + if (pcmk_is_set(docpriv->flags, pcmk__xf_dirty)) { + pcmk__xml_tree_foreach(top, accept_attr_deletions, NULL); } - + reset_xml_private_data(xml->doc->_private); docpriv->flags = pcmk__xf_none; - pcmk__xml_tree_foreach(top, accept_attr_deletions, NULL); } /*! * \internal * \brief Create a new XML document * * \return Newly allocated XML document (guaranteed not to be \c NULL) * * \note The caller is responsible for freeing the return value using * \c pcmk__xml_free_doc(). */ xmlDoc * pcmk__xml_new_doc(void) { xmlDoc *doc = xmlNewDoc(XML_VERSION); pcmk__mem_assert(doc); pcmk__xml_new_private_data((xmlNode *) doc); return doc; } /*! * \internal * \brief Free a new XML document * * \param[in,out] doc XML document to free */ void pcmk__xml_free_doc(xmlDoc *doc) { if (doc != NULL) { pcmk__xml_free_private_data((xmlNode *) doc); xmlFreeDoc(doc); } } /*! * \internal * \brief Check whether the first character of a string is an XML NameStartChar * * See https://www.w3.org/TR/xml/#NT-NameStartChar. * * This is almost identical to libxml2's \c xmlIsDocNameStartChar(), but they * don't expose it as part of the public API. * * \param[in] utf8 UTF-8 encoded string * \param[out] len If not \c NULL, where to store size in bytes of first * character in \p utf8 * * \return \c true if \p utf8 begins with a valid XML NameStartChar, or \c false * otherwise */ bool pcmk__xml_is_name_start_char(const char *utf8, int *len) { int c = 0; int local_len = 0; if (len == NULL) { len = &local_len; } /* xmlGetUTF8Char() abuses the len argument. At call time, it must be set to * "the minimum number of bytes present in the sequence... to assure the * next character is completely contained within the sequence." It's similar * to the "n" in the strn*() functions. However, this doesn't make any sense * for null-terminated strings, and there's no value that indicates "keep * going until '\0'." So we set it to 4, the max number of bytes in a UTF-8 * character. * * At return, it's set to the actual number of bytes in the char, or 0 on * error. */ *len = 4; // Note: xmlGetUTF8Char() assumes a 32-bit int c = xmlGetUTF8Char((const xmlChar *) utf8, len); if (c < 0) { GString *buf = g_string_sized_new(32); for (int i = 0; (i < 4) && (utf8[i] != '\0'); i++) { g_string_append_printf(buf, " 0x%.2X", utf8[i]); } crm_info("Invalid UTF-8 character (bytes:%s)", (pcmk__str_empty(buf->str)? " " : buf->str)); g_string_free(buf, TRUE); return false; } return (c == '_') || (c == ':') || ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')) || ((c >= 0xC0) && (c <= 0xD6)) || ((c >= 0xD8) && (c <= 0xF6)) || ((c >= 0xF8) && (c <= 0x2FF)) || ((c >= 0x370) && (c <= 0x37D)) || ((c >= 0x37F) && (c <= 0x1FFF)) || ((c >= 0x200C) && (c <= 0x200D)) || ((c >= 0x2070) && (c <= 0x218F)) || ((c >= 0x2C00) && (c <= 0x2FEF)) || ((c >= 0x3001) && (c <= 0xD7FF)) || ((c >= 0xF900) && (c <= 0xFDCF)) || ((c >= 0xFDF0) && (c <= 0xFFFD)) || ((c >= 0x10000) && (c <= 0xEFFFF)); } /*! * \internal * \brief Check whether the first character of a string is an XML NameChar * * See https://www.w3.org/TR/xml/#NT-NameChar. * * This is almost identical to libxml2's \c xmlIsDocNameChar(), but they don't * expose it as part of the public API. * * \param[in] utf8 UTF-8 encoded string * \param[out] len If not \c NULL, where to store size in bytes of first * character in \p utf8 * * \return \c true if \p utf8 begins with a valid XML NameChar, or \c false * otherwise */ bool pcmk__xml_is_name_char(const char *utf8, int *len) { int c = 0; int local_len = 0; if (len == NULL) { len = &local_len; } // See comment regarding len in pcmk__xml_is_name_start_char() *len = 4; // Note: xmlGetUTF8Char() assumes a 32-bit int c = xmlGetUTF8Char((const xmlChar *) utf8, len); if (c < 0) { GString *buf = g_string_sized_new(32); for (int i = 0; (i < 4) && (utf8[i] != '\0'); i++) { g_string_append_printf(buf, " 0x%.2X", utf8[i]); } crm_info("Invalid UTF-8 character (bytes:%s)", (pcmk__str_empty(buf->str)? " " : buf->str)); g_string_free(buf, TRUE); return false; } return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')) || ((c >= '0') && (c <= '9')) || (c == '_') || (c == ':') || (c == '-') || (c == '.') || (c == 0xB7) || ((c >= 0xC0) && (c <= 0xD6)) || ((c >= 0xD8) && (c <= 0xF6)) || ((c >= 0xF8) && (c <= 0x2FF)) || ((c >= 0x300) && (c <= 0x36F)) || ((c >= 0x370) && (c <= 0x37D)) || ((c >= 0x37F) && (c <= 0x1FFF)) || ((c >= 0x200C) && (c <= 0x200D)) || ((c >= 0x203F) && (c <= 0x2040)) || ((c >= 0x2070) && (c <= 0x218F)) || ((c >= 0x2C00) && (c <= 0x2FEF)) || ((c >= 0x3001) && (c <= 0xD7FF)) || ((c >= 0xF900) && (c <= 0xFDCF)) || ((c >= 0xFDF0) && (c <= 0xFFFD)) || ((c >= 0x10000) && (c <= 0xEFFFF)); } /*! * \internal * \brief Sanitize a string so it is usable as an XML ID * * An ID must match the Name production as defined here: * https://www.w3.org/TR/xml/#NT-Name. * * Convert an invalid start character to \c '_'. Convert an invalid character * after the start character to \c '.'. * * \param[in,out] id String to sanitize */ void pcmk__xml_sanitize_id(char *id) { bool valid = true; int len = 0; // If id is empty or NULL, there's no way to make it a valid XML ID pcmk__assert(!pcmk__str_empty(id)); /* @TODO Suppose there are two strings and each has an invalid ID character * in the same position. The strings are otherwise identical. Both strings * will be sanitized to the same valid ID, which is incorrect. * * The caller is responsible for ensuring the sanitized ID does not already * exist in a given XML document before using it, if uniqueness is desired. */ valid = pcmk__xml_is_name_start_char(id, &len); CRM_CHECK(len > 0, return); // UTF-8 encoding error if (!valid) { *id = '_'; for (int i = 1; i < len; i++) { id[i] = '.'; } } for (id += len; *id != '\0'; id += len) { valid = pcmk__xml_is_name_char(id, &len); CRM_CHECK(len > 0, return); // UTF-8 encoding error if (!valid) { for (int i = 0; i < len; i++) { id[i] = '.'; } } } } /*! * \internal * \brief Free an XML tree without ACL checks or change tracking * * \param[in,out] xml XML node to free */ void pcmk__xml_free_node(xmlNode *xml) { pcmk__xml_free_private_data(xml); xmlUnlinkNode(xml); xmlFreeNode(xml); } /*! * \internal * \brief Free an XML tree if ACLs allow; track deletion if tracking is enabled * * If \p node is the root of its document, free the entire document. * * \param[in,out] node XML node to free * \param[in] position Position of \p node among its siblings for change * tracking (negative to calculate automatically if * needed) */ static void free_xml_with_position(xmlNode *node, int position) { xmlDoc *doc = NULL; xml_node_private_t *nodepriv = NULL; if (node == NULL) { return; } doc = node->doc; nodepriv = node->_private; if ((doc != NULL) && (xmlDocGetRootElement(doc) == node)) { /* @TODO Should we check ACLs first? Otherwise it seems like we could * free the root element without write permission. */ pcmk__xml_free_doc(doc); return; } if (!pcmk__check_acl(node, NULL, pcmk__xf_acl_write)) { GString *xpath = NULL; pcmk__if_tracing({}, return); xpath = pcmk__element_xpath(node); qb_log_from_external_source(__func__, __FILE__, "Cannot remove %s %x", LOG_TRACE, __LINE__, 0, xpath->str, nodepriv->flags); g_string_free(xpath, TRUE); return; } if (pcmk__xml_doc_all_flags_set(node->doc, pcmk__xf_tracking) && !pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { xml_doc_private_t *docpriv = doc->_private; GString *xpath = pcmk__element_xpath(node); if (xpath != NULL) { pcmk__deleted_xml_t *deleted_obj = NULL; crm_trace("Deleting %s %p from %p", xpath->str, node, doc); deleted_obj = pcmk__assert_alloc(1, sizeof(pcmk__deleted_xml_t)); deleted_obj->path = g_string_free(xpath, FALSE); deleted_obj->position = -1; // Record the position only for XML comments for now if (node->type == XML_COMMENT_NODE) { if (position >= 0) { deleted_obj->position = position; } else { deleted_obj->position = pcmk__xml_position(node, pcmk__xf_skip); } } docpriv->deleted_objs = g_list_append(docpriv->deleted_objs, deleted_obj); pcmk__xml_doc_set_flags(node->doc, pcmk__xf_dirty); } } pcmk__xml_free_node(node); } /*! * \internal * \brief Free an XML tree if ACLs allow; track deletion if tracking is enabled * * If \p xml is the root of its document, free the entire document. * * \param[in,out] xml XML node to free */ void pcmk__xml_free(xmlNode *xml) { free_xml_with_position(xml, -1); } /*! * \internal * \brief Make a deep copy of an XML node under a given parent * * \param[in,out] parent XML element that will be the copy's parent (\c NULL * to create a new XML document with the copy as root) * \param[in] src XML node to copy * * \return Deep copy of \p src, or \c NULL if \p src is \c NULL */ xmlNode * pcmk__xml_copy(xmlNode *parent, xmlNode *src) { xmlNode *copy = NULL; if (src == NULL) { return NULL; } if (parent == NULL) { xmlDoc *doc = NULL; // The copy will be the root element of a new document pcmk__assert(src->type == XML_ELEMENT_NODE); doc = pcmk__xml_new_doc(); copy = xmlDocCopyNode(src, doc, 1); pcmk__mem_assert(copy); xmlDocSetRootElement(doc, copy); } else { copy = xmlDocCopyNode(src, parent->doc, 1); pcmk__mem_assert(copy); xmlAddChild(parent, copy); } pcmk__xml_new_private_data(copy); return copy; } /*! * \internal * \brief Remove XML text nodes from specified XML and all its children * * \param[in,out] xml XML to strip text from */ void pcmk__strip_xml_text(xmlNode *xml) { xmlNode *iter = xml->children; while (iter) { xmlNode *next = iter->next; switch (iter->type) { case XML_TEXT_NODE: pcmk__xml_free_node(iter); break; case XML_ELEMENT_NODE: /* Search it */ pcmk__strip_xml_text(iter); break; default: /* Leave it */ break; } iter = next; } } /*! * \internal * \brief Check whether a string has XML special characters that must be escaped * * See \c pcmk__xml_escape() and \c pcmk__xml_escape_type for more details. * * \param[in] text String to check * \param[in] type Type of escaping * * \return \c true if \p text has special characters that need to be escaped, or * \c false otherwise */ bool pcmk__xml_needs_escape(const char *text, enum pcmk__xml_escape_type type) { if (text == NULL) { return false; } while (*text != '\0') { switch (type) { case pcmk__xml_escape_text: switch (*text) { case '<': case '>': case '&': return true; case '\n': case '\t': break; default: if (g_ascii_iscntrl(*text)) { return true; } break; } break; case pcmk__xml_escape_attr: switch (*text) { case '<': case '>': case '&': case '"': return true; default: if (g_ascii_iscntrl(*text)) { return true; } break; } break; case pcmk__xml_escape_attr_pretty: switch (*text) { case '\n': case '\r': case '\t': case '"': return true; default: break; } break; default: // Invalid enum value pcmk__assert(false); break; } text = g_utf8_next_char(text); } return false; } /*! * \internal * \brief Replace special characters with their XML escape sequences * * \param[in] text Text to escape * \param[in] type Type of escaping * * \return Newly allocated string equivalent to \p text but with special * characters replaced with XML escape sequences (or \c NULL if \p text * is \c NULL). If \p text is not \c NULL, the return value is * guaranteed not to be \c NULL. * * \note There are libxml functions that purport to do this: * \c xmlEncodeEntitiesReentrant() and \c xmlEncodeSpecialChars(). * However, their escaping is incomplete. See: * https://discourse.gnome.org/t/intended-use-of-xmlencodeentitiesreentrant-vs-xmlencodespecialchars/19252 * \note The caller is responsible for freeing the return value using * \c g_free(). */ gchar * pcmk__xml_escape(const char *text, enum pcmk__xml_escape_type type) { GString *copy = NULL; if (text == NULL) { return NULL; } copy = g_string_sized_new(strlen(text)); while (*text != '\0') { // Don't escape any non-ASCII characters if ((*text & 0x80) != 0) { size_t bytes = g_utf8_next_char(text) - text; g_string_append_len(copy, text, bytes); text += bytes; continue; } switch (type) { case pcmk__xml_escape_text: switch (*text) { case '<': g_string_append(copy, PCMK__XML_ENTITY_LT); break; case '>': g_string_append(copy, PCMK__XML_ENTITY_GT); break; case '&': g_string_append(copy, PCMK__XML_ENTITY_AMP); break; case '\n': case '\t': g_string_append_c(copy, *text); break; default: if (g_ascii_iscntrl(*text)) { g_string_append_printf(copy, "&#x%.2X;", *text); } else { g_string_append_c(copy, *text); } break; } break; case pcmk__xml_escape_attr: switch (*text) { case '<': g_string_append(copy, PCMK__XML_ENTITY_LT); break; case '>': g_string_append(copy, PCMK__XML_ENTITY_GT); break; case '&': g_string_append(copy, PCMK__XML_ENTITY_AMP); break; case '"': g_string_append(copy, PCMK__XML_ENTITY_QUOT); break; default: if (g_ascii_iscntrl(*text)) { g_string_append_printf(copy, "&#x%.2X;", *text); } else { g_string_append_c(copy, *text); } break; } break; case pcmk__xml_escape_attr_pretty: switch (*text) { case '"': g_string_append(copy, "\\\""); break; case '\n': g_string_append(copy, "\\n"); break; case '\r': g_string_append(copy, "\\r"); break; case '\t': g_string_append(copy, "\\t"); break; default: g_string_append_c(copy, *text); break; } break; default: // Invalid enum value pcmk__assert(false); break; } text = g_utf8_next_char(text); } return g_string_free(copy, FALSE); } /*! * \internal * \brief Set a flag on all attributes of an XML element * * \param[in,out] xml XML node to set flags on * \param[in] flag XML private flag to set */ static void set_attrs_flag(xmlNode *xml, enum xml_private_flags flag) { for (xmlAttr *attr = pcmk__xe_first_attr(xml); attr; attr = attr->next) { pcmk__set_xml_flags((xml_node_private_t *) (attr->_private), flag); } } /*! * \internal * \brief Add an XML attribute to a node, marked as deleted * * When calculating XML changes, we need to know when an attribute has been * deleted. Add the attribute back to the new XML, so that we can check the * removal against ACLs, and mark it as deleted for later removal after * differences have been calculated. * * \param[in,out] new_xml XML to modify * \param[in] element Name of XML element that changed (for logging) * \param[in] attr_name Name of attribute that was deleted * \param[in] old_value Value of attribute that was deleted */ static void mark_attr_deleted(xmlNode *new_xml, const char *element, const char *attr_name, const char *old_value) { xml_doc_private_t *docpriv = new_xml->doc->_private; xmlAttr *attr = NULL; xml_node_private_t *nodepriv; /* Restore the old value (without setting dirty flag recursively upwards or * checking ACLs) */ pcmk__clear_xml_flags(docpriv, pcmk__xf_tracking); crm_xml_add(new_xml, attr_name, old_value); pcmk__set_xml_flags(docpriv, pcmk__xf_tracking); // Reset flags (so the attribute doesn't appear as newly created) attr = xmlHasProp(new_xml, (const xmlChar *) attr_name); nodepriv = attr->_private; nodepriv->flags = 0; // Check ACLs and mark restored value for later removal pcmk__xa_remove(attr, false); 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) { xml_doc_private_t *docpriv = new_xml->doc->_private; 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 (without checking ACLs) pcmk__clear_xml_flags(docpriv, pcmk__xf_tracking); crm_xml_add(new_xml, attr_name, old_value); pcmk__set_xml_flags(docpriv, pcmk__xf_tracking); // 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__xml_doc_all_flags_set(new_xml->doc, pcmk__xf_lazy |pcmk__xf_tracking)) { /* pcmk__xf_tracking is always set by xml_calculate_changes() * before this function is called, so only the pcmk__xf_lazy * check is truly relevant. */ 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 pcmk__xa_remove(new_attr, true); } } } } /*! * \internal * \brief Calculate differences in attributes between two XML nodes * * \param[in,out] old_xml Original XML to compare * \param[in,out] new_xml New XML to compare */ static void xml_diff_attrs(xmlNode *old_xml, xmlNode *new_xml) { set_attrs_flag(new_xml, pcmk__xf_created); // cleared later if not really new xml_diff_old_attrs(old_xml, new_xml); mark_created_attrs(new_xml); } /*! * \internal * \brief Add an XML child element to a node, marked as deleted * * When calculating XML changes, we need to know when a child element has been * deleted. Add the child back to the new XML, so that we can check the removal * against ACLs, and mark it as deleted for later removal after differences have * been calculated. * * \param[in,out] old_child Child element from original XML * \param[in,out] new_parent New XML to add marked copy to */ static void mark_child_deleted(xmlNode *old_child, xmlNode *new_parent) { // Re-create the child element so we can check ACLs xmlNode *candidate = pcmk__xml_copy(new_parent, old_child); // Clear flags on new child and its children pcmk__xml_tree_foreach(candidate, pcmk__xml_reset_node_flags, NULL); // Check whether ACLs allow the deletion pcmk__apply_acl(xmlDocGetRootElement(candidate->doc)); // Remove the child again (which will track it in document's deleted_objs) free_xml_with_position(candidate, pcmk__xml_position(old_child, pcmk__xf_skip)); if (pcmk__xml_match(new_parent, old_child, true) == NULL) { pcmk__set_xml_flags((xml_node_private_t *) (old_child->_private), pcmk__xf_skip); } } static void mark_child_moved(xmlNode *old_child, xmlNode *new_parent, xmlNode *new_child, int p_old, int p_new) { xml_node_private_t *nodepriv = new_child->_private; crm_trace("Child element %s with " PCMK_XA_ID "='%s' moved from position %d to %d under %s", new_child->name, pcmk__s(pcmk__xe_id(new_child), ""), p_old, p_new, new_parent->name); pcmk__mark_xml_node_dirty(new_parent); pcmk__set_xml_flags(nodepriv, pcmk__xf_moved); if (p_old > p_new) { nodepriv = old_child->_private; } else { nodepriv = new_child->_private; } pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); } // Given original and new XML, mark new XML portions that have changed static void mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top) { xmlNode *old_child = NULL; xmlNode *new_child = NULL; xml_node_private_t *nodepriv = NULL; CRM_CHECK(new_xml != NULL, return); if (old_xml == NULL) { mark_xml_tree_dirty_created(new_xml); pcmk__apply_creation_acl(new_xml, check_top); return; } nodepriv = new_xml->_private; CRM_CHECK(nodepriv != NULL, return); if(nodepriv->flags & pcmk__xf_processed) { /* Avoid re-comparing nodes */ return; } pcmk__set_xml_flags(nodepriv, pcmk__xf_processed); xml_diff_attrs(old_xml, new_xml); // Check for differences in the original children for (old_child = pcmk__xml_first_child(old_xml); old_child != NULL; old_child = pcmk__xml_next(old_child)) { new_child = pcmk__xml_match(new_xml, old_child, true); if (new_child != NULL) { mark_xml_changes(old_child, new_child, true); } else { mark_child_deleted(old_child, new_xml); } } // Check for moved or created children new_child = pcmk__xml_first_child(new_xml); while (new_child != NULL) { xmlNode *next = pcmk__xml_next(new_child); old_child = pcmk__xml_match(old_xml, new_child, true); if (old_child == NULL) { // This is a newly created child nodepriv = new_child->_private; pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); // May free new_child mark_xml_changes(old_child, new_child, true); } else { /* Check for movement, we already checked for differences */ int p_new = pcmk__xml_position(new_child, pcmk__xf_skip); int p_old = pcmk__xml_position(old_child, pcmk__xf_skip); if(p_old != p_new) { mark_child_moved(old_child, new_xml, new_child, p_old, p_new); } } new_child = next; } } void xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml) { if (new_xml != NULL) { pcmk__xml_doc_set_flags(new_xml->doc, pcmk__xf_lazy); } xml_calculate_changes(old_xml, new_xml); } // Called functions may set the \p pcmk__xf_skip flag on parts of \p old_xml void xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml) { CRM_CHECK((old_xml != NULL) && (new_xml != NULL) && pcmk__xe_is(old_xml, (const char *) new_xml->name) && pcmk__str_eq(pcmk__xe_id(old_xml), pcmk__xe_id(new_xml), pcmk__str_none), return); if (!pcmk__xml_doc_all_flags_set(new_xml->doc, pcmk__xf_tracking)) { xml_track_changes(new_xml, NULL, NULL, FALSE); } mark_xml_changes(old_xml, new_xml, FALSE); } /*! * \internal * \brief Initialize the Pacemaker XML environment * * Set an XML buffer allocation scheme, set XML node create and destroy * callbacks, and load schemas into the cache. */ void pcmk__xml_init(void) { // @TODO Try to find a better caller than crm_log_preinit() static bool initialized = false; if (!initialized) { initialized = true; /* Double the buffer size when the buffer needs to grow. The default * allocator XML_BUFFER_ALLOC_EXACT was found to cause poor performance * due to the number of reallocs. */ xmlSetBufferAllocationScheme(XML_BUFFER_ALLOC_DOUBLEIT); // Load schemas into the cache pcmk__schema_init(); } } /*! * \internal * \brief Tear down the Pacemaker XML environment * * Destroy schema cache and clean up memory allocated by libxml2. */ void pcmk__xml_cleanup(void) { pcmk__schema_cleanup(); xmlCleanupParser(); } 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 = PCMK_SCHEMA_DIR; } switch (ns) { case pcmk__xml_artefact_ns_legacy_rng: case pcmk__xml_artefact_ns_legacy_xslt: ret = strdup(base); break; case pcmk__xml_artefact_ns_base_rng: case pcmk__xml_artefact_ns_base_xslt: ret = crm_strdup_printf("%s/base", base); break; default: crm_err("XML artefact family specified as %u not recognized", ns); } return ret; } static char * find_artefact(enum pcmk__xml_artefact_ns ns, const char *path, const char *filespec) { char *ret = NULL; switch (ns) { case pcmk__xml_artefact_ns_legacy_rng: case pcmk__xml_artefact_ns_base_rng: if (pcmk__ends_with(filespec, ".rng")) { ret = crm_strdup_printf("%s/%s", path, filespec); } else { ret = crm_strdup_printf("%s/%s.rng", path, filespec); } break; case pcmk__xml_artefact_ns_legacy_xslt: case pcmk__xml_artefact_ns_base_xslt: if (pcmk__ends_with(filespec, ".xsl")) { ret = crm_strdup_printf("%s/%s", path, filespec); } else { ret = crm_strdup_printf("%s/%s.xsl", path, filespec); } break; default: crm_err("XML artefact family specified as %u not recognized", ns); } return ret; } char * pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec) { struct stat sb; char *base = pcmk__xml_artefact_root(ns); char *ret = NULL; ret = find_artefact(ns, base, filespec); free(base); if (stat(ret, &sb) != 0 || !S_ISREG(sb.st_mode)) { const char *remote_schema_dir = pcmk__remote_schema_dir(); free(ret); ret = find_artefact(ns, remote_schema_dir, filespec); } return ret; } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include xmlNode * copy_xml(xmlNode *src) { xmlDoc *doc = pcmk__xml_new_doc(); xmlNode *copy = NULL; copy = xmlDocCopyNode(src, doc, 1); pcmk__mem_assert(copy); xmlDocSetRootElement(doc, copy); pcmk__xml_new_private_data(copy); return copy; } void crm_xml_init(void) { pcmk__xml_init(); } void crm_xml_cleanup(void) { pcmk__xml_cleanup(); } void pcmk_free_xml_subtree(xmlNode *xml) { pcmk__xml_free_node(xml); } void free_xml(xmlNode *child) { pcmk__xml_free(child); } void crm_xml_sanitize_id(char *id) { char *c; for (c = id; *c; ++c) { switch (*c) { case ':': case '#': *c = '.'; } } } bool xml_tracking_changes(xmlNode *xml) { return (xml != NULL) && pcmk__xml_doc_all_flags_set(xml->doc, pcmk__xf_tracking); } bool xml_document_dirty(xmlNode *xml) { return (xml != NULL) && pcmk__xml_doc_all_flags_set(xml->doc, pcmk__xf_dirty); } // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/common/xml_element.c b/lib/common/xml_element.c index 0f04ace466..2361f8ff64 100644 --- a/lib/common/xml_element.c +++ b/lib/common/xml_element.c @@ -1,1607 +1,1609 @@ /* * Copyright 2004-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include // va_start(), etc. #include // uint32_t #include // NULL, etc. #include // free(), etc. #include // strchr(), etc. #include // time_t, etc. #include // xmlNode, etc. #include // xmlValidateNameValue() #include // xmlChar #include #include // crm_xml_add(), etc. #include // pcmk_rc_ok, etc. #include #include "crmcommon_private.h" /*! * \internal * \brief Find first XML child element matching given criteria * * \param[in] parent XML element to search (can be \c NULL) * \param[in] node_name If not \c NULL, only match children of this type * \param[in] attr_n If not \c NULL, only match children with an attribute * of this name. * \param[in] attr_v If \p attr_n and this are not NULL, only match children * with an attribute named \p attr_n and this value * * \return Matching XML child element, or \c NULL if none found */ xmlNode * pcmk__xe_first_child(const xmlNode *parent, const char *node_name, const char *attr_n, const char *attr_v) { xmlNode *child = NULL; CRM_CHECK((attr_v == NULL) || (attr_n != NULL), return NULL); if (parent == NULL) { return NULL; } child = parent->children; while ((child != NULL) && (child->type != XML_ELEMENT_NODE)) { child = child->next; } for (; child != NULL; child = pcmk__xe_next(child, NULL)) { const char *value = NULL; if ((node_name != NULL) && !pcmk__xe_is(child, node_name)) { // Node name mismatch continue; } if (attr_n == NULL) { // No attribute match needed return child; } value = crm_element_value(child, attr_n); if ((attr_v == NULL) && (value != NULL)) { // attr_v == NULL: Attribute attr_n must be set (to any value) return child; } if ((attr_v != NULL) && (pcmk__str_eq(value, attr_v, pcmk__str_none))) { // attr_v != NULL: Attribute attr_n must be set to value attr_v return child; } } if (attr_n == NULL) { crm_trace("%s XML has no child element of %s type", (const char *) parent->name, pcmk__s(node_name, "any")); } else { crm_trace("%s XML has no child element of %s type with %s='%s'", (const char *) parent->name, pcmk__s(node_name, "any"), attr_n, attr_v); } return NULL; } /*! * \internal * \brief Return next sibling element of an XML element * * \param[in] xml XML element to check * \param[in] element_name If not NULL, get next sibling with this element name * * \return Next desired sibling of \p xml (or NULL if none) */ xmlNode * pcmk__xe_next(const xmlNode *xml, const char *element_name) { for (xmlNode *next = (xml == NULL)? NULL : xml->next; next != NULL; next = next->next) { if ((next->type == XML_ELEMENT_NODE) && ((element_name == NULL) || pcmk__xe_is(next, element_name))) { return next; } } return NULL; } /*! * \internal * \brief Parse an integer score from an XML attribute * * \param[in] xml XML element with attribute to parse * \param[in] name Name of attribute to parse * \param[out] score Where to store parsed score (can be NULL to * just validate) * \param[in] default_score What to return if the attribute value is not * present or invalid * * \return Standard Pacemaker return code */ int pcmk__xe_get_score(const xmlNode *xml, const char *name, int *score, int default_score) { const char *value = NULL; CRM_CHECK((xml != NULL) && (name != NULL), return EINVAL); value = crm_element_value(xml, name); return pcmk_parse_score(value, score, default_score); } /*! * \internal * \brief Set an XML attribute, expanding \c ++ and \c += where appropriate * * If \p target already has an attribute named \p name set to an integer value * and \p value is an addition assignment expression on \p name, then expand * \p value to an integer and set attribute \p name to the expanded value in * \p target. * * Otherwise, set attribute \p name on \p target using the literal \p value. * * The original attribute value in \p target and the number in an assignment * expression in \p value are parsed and added as scores (that is, their values * are capped at \c INFINITY and \c -INFINITY). For more details, refer to * \c pcmk_parse_score(). * * For example, suppose \p target has an attribute named \c "X" with value * \c "5", and that \p name is \c "X". * * If \p value is \c "X++", the new value of \c "X" in \p target is \c "6". * * If \p value is \c "X+=3", the new value of \c "X" in \p target is \c "8". * * If \p value is \c "val", the new value of \c "X" in \p target is \c "val". * * If \p value is \c "Y++", the new value of \c "X" in \p target is \c "Y++". * * \param[in,out] target XML node whose attribute to set * \param[in] name Name of the attribute to set * \param[in] value New value of attribute to set (if NULL, initial value * will be left unchanged) * * \return Standard Pacemaker return code (specifically, \c EINVAL on invalid * argument, or \c pcmk_rc_ok otherwise) */ int pcmk__xe_set_score(xmlNode *target, const char *name, const char *value) { const char *old_value = NULL; CRM_CHECK((target != NULL) && (name != NULL), return EINVAL); if (value == NULL) { // @TODO Maybe instead delete the attribute or set it to 0 return pcmk_rc_ok; } old_value = crm_element_value(target, name); // If no previous value, skip to default case and set the value unexpanded. if (old_value != NULL) { const char *n = name; const char *v = value; // Stop at first character that differs between name and value for (; (*n == *v) && (*n != '\0'); n++, v++); // If value begins with name followed by a "++" or "+=" if ((*n == '\0') && (*v++ == '+') && ((*v == '+') || (*v == '='))) { int add = 1; int old_value_i = 0; int rc = pcmk_rc_ok; // If we're expanding ourselves, no previous value was set; use 0 if (old_value != value) { rc = pcmk_parse_score(old_value, &old_value_i, 0); if (rc != pcmk_rc_ok) { // @TODO This is inconsistent with old_value==NULL crm_trace("Using 0 before incrementing %s because '%s' " "is not a score", name, old_value); } } /* value="X++": new value of X is old_value + 1 * value="X+=Y": new value of X is old_value + Y (for some number Y) */ if (*v != '+') { rc = pcmk_parse_score(++v, &add, 0); if (rc != pcmk_rc_ok) { // @TODO We should probably skip expansion instead crm_trace("Not incrementing %s because '%s' does not have " "a valid increment", name, value); } } crm_xml_add_int(target, name, pcmk__add_scores(old_value_i, add)); return pcmk_rc_ok; } } // Default case: set the attribute unexpanded (with value treated literally) if (old_value != value) { crm_xml_add(target, name, value); } return pcmk_rc_ok; } /*! * \internal * \brief Copy XML attributes from a source element to a target element * * This is similar to \c xmlCopyPropList() except that attributes are marked * as dirty for change tracking purposes. * * \param[in,out] target XML element to receive copied attributes from \p src * \param[in] src XML element whose attributes to copy to \p target * \param[in] flags Group of enum pcmk__xa_flags * * \return Standard Pacemaker return code */ int pcmk__xe_copy_attrs(xmlNode *target, const xmlNode *src, uint32_t flags) { CRM_CHECK((src != NULL) && (target != NULL), return EINVAL); for (xmlAttr *attr = pcmk__xe_first_attr(src); attr != NULL; attr = attr->next) { const char *name = (const char *) attr->name; const char *value = pcmk__xml_attr_value(attr); if (pcmk_is_set(flags, pcmk__xaf_no_overwrite) && (crm_element_value(target, name) != NULL)) { continue; } if (pcmk_is_set(flags, pcmk__xaf_score_update)) { pcmk__xe_set_score(target, name, value); } else { crm_xml_add(target, name, value); } } return pcmk_rc_ok; } /*! * \internal * \brief Compare two XML attributes by name * * \param[in] a First XML attribute to compare * \param[in] b Second XML attribute to compare * * \retval negative \c a->name is \c NULL or comes before \c b->name * lexicographically * \retval 0 \c a->name and \c b->name are equal * \retval positive \c b->name is \c NULL or comes before \c a->name * lexicographically */ static gint compare_xml_attr(gconstpointer a, gconstpointer b) { const xmlAttr *attr_a = a; const xmlAttr *attr_b = b; return pcmk__strcmp((const char *) attr_a->name, (const char *) attr_b->name, pcmk__str_none); } /*! * \internal * \brief Sort an XML element's attributes by name * * This does not consider ACLs and does not mark the attributes as deleted or * dirty. Upon return, all attributes still exist and are set to the same values * as before the call. The only thing that may change is the order of the * attribute list. * * \param[in,out] xml XML element whose attributes to sort */ void pcmk__xe_sort_attrs(xmlNode *xml) { GSList *attr_list = NULL; for (xmlAttr *iter = pcmk__xe_first_attr(xml); iter != NULL; iter = iter->next) { attr_list = g_slist_prepend(attr_list, iter); } attr_list = g_slist_sort(attr_list, compare_xml_attr); for (GSList *iter = attr_list; iter != NULL; iter = iter->next) { xmlNode *attr = iter->data; xmlUnlinkNode(attr); xmlAddChild(xml, attr); } g_slist_free(attr_list); } /*! * \internal * \brief Remove a named attribute from an XML element * * \param[in,out] element XML element to remove an attribute from * \param[in] name Name of attribute to remove */ void pcmk__xe_remove_attr(xmlNode *element, const char *name) { if (name != NULL) { pcmk__xa_remove(xmlHasProp(element, (const xmlChar *) name), false); } } /*! * \internal * \brief Remove a named attribute from an XML element * * This is a wrapper for \c pcmk__xe_remove_attr() for use with * \c pcmk__xml_tree_foreach(). * * \param[in,out] xml XML element to remove an attribute from * \param[in] user_data Name of attribute to remove * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ bool pcmk__xe_remove_attr_cb(xmlNode *xml, void *user_data) { const char *name = user_data; pcmk__xe_remove_attr(xml, name); return true; } /*! * \internal * \brief Remove an XML element's attributes that match some criteria * * \param[in,out] element XML element to modify + * \param[in] force If \c true, remove matching attributes immediately, + * ignoring ACLs and change tracking * \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, +pcmk__xe_remove_matching_attrs(xmlNode *element, bool force, 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__xa_remove(a, false) != pcmk_rc_ok) { + if (pcmk__xa_remove(a, force) != pcmk_rc_ok) { return; } } } } /*! * \internal * \brief Create a new XML element under a given parent * * \param[in,out] parent XML element that will be the new element's parent * (\c NULL to create a new XML document with the new * node as root) * \param[in] name Name of new element * * \return Newly created XML element (guaranteed not to be \c NULL) */ xmlNode * pcmk__xe_create(xmlNode *parent, const char *name) { xmlNode *node = NULL; pcmk__assert(!pcmk__str_empty(name)); if (parent == NULL) { xmlDoc *doc = pcmk__xml_new_doc(); node = xmlNewDocRawNode(doc, NULL, (const xmlChar *) name, NULL); pcmk__mem_assert(node); xmlDocSetRootElement(doc, node); } else { node = xmlNewChild(parent, NULL, (const xmlChar *) name, NULL); pcmk__mem_assert(node); } pcmk__xml_new_private_data(node); return node; } /*! * \internal * \brief Set a formatted string as an XML node's content * * \param[in,out] node Node whose content to set * \param[in] format printf(3)-style format string * \param[in] ... Arguments for \p format * * \note This function escapes special characters. \c xmlNodeSetContent() does * not. */ G_GNUC_PRINTF(2, 3) void pcmk__xe_set_content(xmlNode *node, const char *format, ...) { if (node != NULL) { const char *content = NULL; char *buf = NULL; /* xmlNodeSetContent() frees node->children and replaces it with new * text. If this function is called for a node that already has a non- * text child, it's a bug. */ CRM_CHECK((node->children == NULL) || (node->children->type == XML_TEXT_NODE), return); if (strchr(format, '%') == NULL) { // Nothing to format content = format; } else { va_list ap; va_start(ap, format); if (pcmk__str_eq(format, "%s", pcmk__str_none)) { // No need to make a copy content = va_arg(ap, const char *); } else { pcmk__assert(vasprintf(&buf, format, ap) >= 0); content = buf; } va_end(ap); } xmlNodeSetContent(node, (const xmlChar *) content); free(buf); } } /*! * \internal * \brief Set a formatted string as an XML element's ID * * If the formatted string would not be a valid ID, it's first sanitized by * \c pcmk__xml_sanitize_id(). * * \param[in,out] node Node whose ID to set * \param[in] format printf(3)-style format string * \param[in] ... Arguments for \p format */ G_GNUC_PRINTF(2, 3) void pcmk__xe_set_id(xmlNode *node, const char *format, ...) { char *id = NULL; va_list ap; pcmk__assert(!pcmk__str_empty(format)); if (node == NULL) { return; } va_start(ap, format); pcmk__assert(vasprintf(&id, format, ap) >= 0); va_end(ap); if (!xmlValidateNameValue((const xmlChar *) id)) { pcmk__xml_sanitize_id(id); } crm_xml_add(node, PCMK_XA_ID, id); free(id); } /*! * \internal * \brief Add a "last written" attribute to an XML element, set to current time * * \param[in,out] xe XML element to add attribute to * * \return Value that was set, or NULL on error */ const char * pcmk__xe_add_last_written(xmlNode *xe) { char *now_s = pcmk__epoch2str(NULL, 0); const char *result = NULL; result = crm_xml_add(xe, PCMK_XA_CIB_LAST_WRITTEN, pcmk__s(now_s, "Could not determine current time")); free(now_s); return result; } /*! * \internal * \brief Merge one XML tree into another * * Here, "merge" means: * 1. Copy attribute values from \p update to the target, overwriting in case of * conflict. * 2. Descend through \p update and the target in parallel. At each level, for * each child of \p update, look for a matching child of the target. * a. For each child, if a match is found, go to step 1, recursively merging * the child of \p update into the child of the target. * b. Otherwise, copy the child of \p update as a child of the target. * * A match is defined as the first child of the same type within the target, * with: * * the \c PCMK_XA_ID attribute matching, if set in \p update; otherwise, * * the \c PCMK_XA_ID_REF attribute matching, if set in \p update * * This function does not delete any elements or attributes from the target. It * may add elements or overwrite attributes, as described above. * * \param[in,out] parent If \p target is NULL and this is not, add or update * child of this XML node that matches \p update * \param[in,out] target If not NULL, update this XML * \param[in] update Make the desired XML match this (must not be \c NULL) * \param[in] flags Group of enum pcmk__xa_flags * * \note At least one of \p parent and \p target must be non-NULL. * \note This function is recursive. For the top-level call, \p parent is * \c NULL and \p target is not \c NULL. For recursive calls, \p target is * \c NULL and \p parent is not \c NULL. */ static void update_xe(xmlNode *parent, xmlNode *target, xmlNode *update, uint32_t flags) { // @TODO Try to refactor further, possibly using pcmk__xml_tree_foreach() const char *update_name = NULL; const char *update_id_attr = NULL; const char *update_id_val = NULL; char *trace_s = NULL; crm_log_xml_trace(update, "update"); crm_log_xml_trace(target, "target"); CRM_CHECK(update != NULL, goto done); if (update->type == XML_COMMENT_NODE) { pcmk__xc_update(parent, target, update); goto done; } update_name = (const char *) update->name; CRM_CHECK(update_name != NULL, goto done); CRM_CHECK((target != NULL) || (parent != NULL), goto done); update_id_val = pcmk__xe_id(update); if (update_id_val != NULL) { update_id_attr = PCMK_XA_ID; } else { update_id_val = crm_element_value(update, PCMK_XA_ID_REF); if (update_id_val != NULL) { update_id_attr = PCMK_XA_ID_REF; } } pcmk__if_tracing( { if (update_id_attr != NULL) { trace_s = crm_strdup_printf("<%s %s=%s/>", update_name, update_id_attr, update_id_val); } else { trace_s = crm_strdup_printf("<%s/>", update_name); } }, {} ); if (target == NULL) { // Recursive call target = pcmk__xe_first_child(parent, update_name, update_id_attr, update_id_val); } if (target == NULL) { // Recursive call with no existing matching child target = pcmk__xe_create(parent, update_name); crm_trace("Added %s", pcmk__s(trace_s, update_name)); } else { // Either recursive call with match, or top-level call crm_trace("Found node %s to update", pcmk__s(trace_s, update_name)); } CRM_CHECK(pcmk__xe_is(target, (const char *) update->name), return); pcmk__xe_copy_attrs(target, update, flags); for (xmlNode *child = pcmk__xml_first_child(update); child != NULL; child = pcmk__xml_next(child)) { crm_trace("Updating child of %s", pcmk__s(trace_s, update_name)); update_xe(target, NULL, child, flags); } crm_trace("Finished with %s", pcmk__s(trace_s, update_name)); done: free(trace_s); } /*! * \internal * \brief Delete an XML subtree if it matches a search element * * A match is defined as follows: * * \p xml and \p user_data are both element nodes of the same type. * * If \p user_data has attributes set, \p xml has those attributes set to the * same values. (\p xml may have additional attributes set to arbitrary * values.) * * \param[in,out] xml XML subtree to delete upon match * \param[in] user_data Search element * * \return \c true to continue traversing the tree, or \c false to stop (because * \p xml was deleted) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool delete_xe_if_matching(xmlNode *xml, void *user_data) { xmlNode *search = user_data; if (!pcmk__xe_is(search, (const char *) xml->name)) { // No match: either not both elements, or different element types return true; } for (const xmlAttr *attr = pcmk__xe_first_attr(search); attr != NULL; attr = attr->next) { const char *search_val = pcmk__xml_attr_value(attr); const char *xml_val = crm_element_value(xml, (const char *) attr->name); if (!pcmk__str_eq(search_val, xml_val, pcmk__str_casei)) { // No match: an attr in xml doesn't match the attr in search return true; } } crm_log_xml_trace(xml, "delete-match"); crm_log_xml_trace(search, "delete-search"); pcmk__xml_free(xml); // Found a match and deleted it; stop traversing tree return false; } /*! * \internal * \brief Search an XML tree depth-first and delete the first matching element * * This function does not attempt to match the tree root (\p xml). * * A match with a node \c node is defined as follows: * * \c node and \p search are both element nodes of the same type. * * If \p search has attributes set, \c node has those attributes set to the * same values. (\c node may have additional attributes set to arbitrary * values.) * * \param[in,out] xml XML subtree to search * \param[in] search Element to match against * * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on * successful deletion and an error code otherwise) */ int pcmk__xe_delete_match(xmlNode *xml, xmlNode *search) { // See @COMPAT comment in pcmk__xe_replace_match() CRM_CHECK((xml != NULL) && (search != NULL), return EINVAL); for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL; xml = pcmk__xe_next(xml, NULL)) { if (!pcmk__xml_tree_foreach(xml, delete_xe_if_matching, search)) { // Found and deleted an element return pcmk_rc_ok; } } // No match found in this subtree return ENXIO; } /*! * \internal * \brief Replace one XML node with a copy of another XML node * * This function handles change tracking and applies ACLs. * * \param[in,out] old XML node to replace * \param[in] new XML node to copy as replacement for \p old * * \note This frees \p old. */ static void replace_node(xmlNode *old, xmlNode *new) { // Pass old for its doc; it won't remain the parent of new new = pcmk__xml_copy(old, new); old = xmlReplaceNode(old, new); // old == NULL means memory allocation error pcmk__assert(old != NULL); // May be unnecessary but avoids slight changes to some test outputs pcmk__xml_tree_foreach(new, pcmk__xml_reset_node_flags, NULL); if (pcmk__xml_doc_all_flags_set(new->doc, pcmk__xf_tracking)) { // Replaced sections may have included relevant ACLs pcmk__apply_acl(new); } xml_calculate_changes(old, new); pcmk__xml_free_node(old); } /*! * \internal * \brief Replace one XML subtree with a copy of another if the two match * * A match is defined as follows: * * \p xml and \p user_data are both element nodes of the same type. * * If \p user_data has the \c PCMK_XA_ID attribute set, then \p xml has * \c PCMK_XA_ID set to the same value. * * \param[in,out] xml XML subtree to replace with \p user_data upon match * \param[in] user_data XML to replace \p xml with a copy of upon match * * \return \c true to continue traversing the tree, or \c false to stop (because * \p xml was replaced by \p user_data) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool replace_xe_if_matching(xmlNode *xml, void *user_data) { xmlNode *replace = user_data; const char *xml_id = NULL; const char *replace_id = NULL; xml_id = pcmk__xe_id(xml); replace_id = pcmk__xe_id(replace); if (!pcmk__xe_is(replace, (const char *) xml->name)) { // No match: either not both elements, or different element types return true; } if ((replace_id != NULL) && !pcmk__str_eq(replace_id, xml_id, pcmk__str_none)) { // No match: ID was provided in replace and doesn't match xml's ID return true; } crm_log_xml_trace(xml, "replace-match"); crm_log_xml_trace(replace, "replace-with"); replace_node(xml, replace); // Found a match and replaced it; stop traversing tree return false; } /*! * \internal * \brief Search an XML tree depth-first and replace the first matching element * * This function does not attempt to match the tree root (\p xml). * * A match with a node \c node is defined as follows: * * \c node and \p replace are both element nodes of the same type. * * If \p replace has the \c PCMK_XA_ID attribute set, then \c node has * \c PCMK_XA_ID set to the same value. * * \param[in,out] xml XML tree to search * \param[in] replace XML to replace a matching element with a copy of * * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on * successful replacement and an error code otherwise) */ int pcmk__xe_replace_match(xmlNode *xml, xmlNode *replace) { /* @COMPAT Some of this behavior (like not matching the tree root, which is * allowed by pcmk__xe_update_match()) is questionable for general use but * required for backward compatibility by cib_process_replace() and * cib_process_delete(). Behavior can change at a major version release if * desired. */ CRM_CHECK((xml != NULL) && (replace != NULL), return EINVAL); for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL; xml = pcmk__xe_next(xml, NULL)) { if (!pcmk__xml_tree_foreach(xml, replace_xe_if_matching, replace)) { // Found and replaced an element return pcmk_rc_ok; } } // No match found in this subtree return ENXIO; } //! User data for \c update_xe_if_matching() struct update_data { xmlNode *update; //!< Update source uint32_t flags; //!< Group of enum pcmk__xa_flags }; /*! * \internal * \brief Update one XML subtree with another if the two match * * "Update" means to merge a source subtree into a target subtree (see * \c update_xe()). * * A match is defined as follows: * * \p xml and \p user_data->update are both element nodes of the same type. * * \p xml and \p user_data->update have the same \c PCMK_XA_ID attribute * value, or \c PCMK_XA_ID is unset in both * * \param[in,out] xml XML subtree to update with \p user_data->update * upon match * \param[in] user_data struct update_data object * * \return \c true to continue traversing the tree, or \c false to stop (because * \p xml was updated by \p user_data->update) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool update_xe_if_matching(xmlNode *xml, void *user_data) { struct update_data *data = user_data; xmlNode *update = data->update; if (!pcmk__xe_is(update, (const char *) xml->name)) { // No match: either not both elements, or different element types return true; } if (!pcmk__str_eq(pcmk__xe_id(xml), pcmk__xe_id(update), pcmk__str_none)) { // No match: ID mismatch return true; } crm_log_xml_trace(xml, "update-match"); crm_log_xml_trace(update, "update-with"); update_xe(NULL, xml, update, data->flags); // Found a match and replaced it; stop traversing tree return false; } /*! * \internal * \brief Search an XML tree depth-first and update the first matching element * * "Update" means to merge a source subtree into a target subtree (see * \c update_xe()). * * A match with a node \c node is defined as follows: * * \c node and \p update are both element nodes of the same type. * * \c node and \p update have the same \c PCMK_XA_ID attribute value, or * \c PCMK_XA_ID is unset in both * * \param[in,out] xml XML tree to search * \param[in] update XML to update a matching element with * \param[in] flags Group of enum pcmk__xa_flags * * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on * successful update and an error code otherwise) */ int pcmk__xe_update_match(xmlNode *xml, xmlNode *update, uint32_t flags) { /* @COMPAT In pcmk__xe_delete_match() and pcmk__xe_replace_match(), we * compare IDs only if the equivalent of the update argument has an ID. * Here, we're stricter: we consider it a mismatch if only one element has * an ID attribute, or if both elements have IDs but they don't match. * * Perhaps we should align the behavior at a major version release. */ struct update_data data = { .update = update, .flags = flags, }; CRM_CHECK((xml != NULL) && (update != NULL), return EINVAL); if (!pcmk__xml_tree_foreach(xml, update_xe_if_matching, &data)) { // Found and updated an element return pcmk_rc_ok; } // No match found in this subtree return ENXIO; } 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); pcmk__assert(handler != NULL); for (xmlNode *node = children; node != NULL; node = node->next) { if ((node->type == XML_ELEMENT_NODE) && ((child_element_name == NULL) || pcmk__xe_is(node, child_element_name))) { int rc = handler(node, userdata); if (rc != pcmk_rc_ok) { return rc; } } } return pcmk_rc_ok; } // XML attribute handling /*! * \brief Create an XML attribute with specified name and value * * \param[in,out] node XML node to modify * \param[in] name Attribute name to set * \param[in] value Attribute value to set * * \return New value on success, \c NULL otherwise * \note This does nothing if node, name, or value are \c NULL or empty. */ const char * crm_xml_add(xmlNode *node, const char *name, const char *value) { // @TODO Replace with internal function that returns the new attribute bool dirty = FALSE; xmlAttr *attr = NULL; CRM_CHECK(node != NULL, return NULL); CRM_CHECK(name != NULL, return NULL); if (value == NULL) { return NULL; } if (pcmk__xml_doc_all_flags_set(node->doc, pcmk__xf_tracking)) { const char *old = crm_element_value(node, name); if (old == NULL || value == NULL || strcmp(old, value) != 0) { dirty = TRUE; } } if (dirty && (pcmk__check_acl(node, name, pcmk__xf_acl_create) == FALSE)) { crm_trace("Cannot add %s=%s to %s", name, value, node->name); return NULL; } attr = xmlSetProp(node, (const xmlChar *) name, (const xmlChar *) value); /* If the attribute already exists, this does nothing. Attribute values * don't get private data. */ pcmk__xml_new_private_data((xmlNode *) attr); if (dirty) { pcmk__mark_xml_attr_dirty(attr); } CRM_CHECK(attr && attr->children && attr->children->content, return NULL); return (char *)attr->children->content; } /*! * \brief Create an XML attribute with specified name and integer value * * This is like \c crm_xml_add() but taking an integer value. * * \param[in,out] node XML node to modify * \param[in] name Attribute name to set * \param[in] value Attribute value to set * * \return New value as string on success, \c NULL otherwise * \note This does nothing if node or name are \c NULL or empty. */ const char * crm_xml_add_int(xmlNode *node, const char *name, int value) { char *number = pcmk__itoa(value); const char *added = crm_xml_add(node, name, number); free(number); return added; } /*! * \brief Create an XML attribute with specified name and unsigned value * * This is like \c crm_xml_add() but taking a guint value. * * \param[in,out] node XML node to modify * \param[in] name Attribute name to set * \param[in] ms Attribute value to set * * \return New value as string on success, \c NULL otherwise * \note This does nothing if node or name are \c NULL or empty. */ const char * crm_xml_add_ms(xmlNode *node, const char *name, guint ms) { char *number = crm_strdup_printf("%u", ms); const char *added = crm_xml_add(node, name, number); free(number); return added; } // Maximum size of null-terminated string representation of 64-bit integer // -9223372036854775808 #define LLSTRSIZE 21 /*! * \brief Create an XML attribute with specified name and long long int value * * This is like \c crm_xml_add() but taking a long long int value. It is a * useful equivalent for defined types like time_t, etc. * * \param[in,out] xml XML node to modify * \param[in] name Attribute name to set * \param[in] value Attribute value to set * * \return New value as string on success, \c NULL otherwise * \note This does nothing if xml or name are \c NULL or empty. * This does not support greater than 64-bit values. */ const char * crm_xml_add_ll(xmlNode *xml, const char *name, long long value) { char s[LLSTRSIZE] = { '\0', }; if (snprintf(s, LLSTRSIZE, "%lld", (long long) value) == LLSTRSIZE) { return NULL; } return crm_xml_add(xml, name, s); } /*! * \brief Create XML attributes for seconds and microseconds * * This is like \c crm_xml_add() but taking a struct timeval. * * \param[in,out] xml XML node to modify * \param[in] name_sec Name of XML attribute for seconds * \param[in] name_usec Name of XML attribute for microseconds (or NULL) * \param[in] value Time value to set * * \return New seconds value as string on success, \c NULL otherwise * \note This does nothing if xml, name_sec, or value is \c NULL. */ const char * crm_xml_add_timeval(xmlNode *xml, const char *name_sec, const char *name_usec, const struct timeval *value) { const char *added = NULL; if (xml && name_sec && value) { added = crm_xml_add_ll(xml, name_sec, (long long) value->tv_sec); if (added && name_usec) { // Any error is ignored (we successfully added seconds) crm_xml_add_ll(xml, name_usec, (long long) value->tv_usec); } } return added; } /*! * \brief Retrieve the value of an XML attribute * * \param[in] data XML node to check * \param[in] name Attribute name to check * * \return Value of specified attribute (may be \c NULL) */ const char * crm_element_value(const xmlNode *data, const char *name) { xmlAttr *attr = NULL; if (data == NULL) { crm_err("Couldn't find %s in NULL", name ? name : ""); CRM_LOG_ASSERT(data != NULL); return NULL; } else if (name == NULL) { crm_err("Couldn't find NULL in %s", data->name); return NULL; } attr = xmlHasProp(data, (const xmlChar *) name); if (!attr || !attr->children) { return NULL; } return (const char *) attr->children->content; } /*! * \brief Retrieve the integer value of an XML attribute * * This is like \c crm_element_value() but getting the value as an integer. * * \param[in] data XML node to check * \param[in] name Attribute name to check * \param[out] dest Where to store element value * * \return 0 on success, -1 otherwise */ int crm_element_value_int(const xmlNode *data, const char *name, int *dest) { const char *value = NULL; CRM_CHECK(dest != NULL, return -1); value = crm_element_value(data, name); if (value) { long long value_ll; int rc = pcmk__scan_ll(value, &value_ll, 0LL); *dest = PCMK__PARSE_INT_DEFAULT; if (rc != pcmk_rc_ok) { crm_warn("Using default for %s " "because '%s' is not a valid integer: %s", name, value, pcmk_rc_str(rc)); } else if ((value_ll < INT_MIN) || (value_ll > INT_MAX)) { crm_warn("Using default for %s because '%s' is out of range", name, value); } else { *dest = (int) value_ll; return 0; } } return -1; } /*! * \internal * \brief Retrieve a flag group from an XML attribute value * * This is like \c crm_element_value() except getting the value as a 32-bit * unsigned integer. * * \param[in] xml XML node to check * \param[in] name Attribute name to check (must not be NULL) * \param[out] dest Where to store flags (may be NULL to just * validate type) * \param[in] default_value What to use for missing or invalid value * * \return Standard Pacemaker return code */ int pcmk__xe_get_flags(const xmlNode *xml, const char *name, uint32_t *dest, uint32_t default_value) { const char *value = NULL; long long value_ll = 0LL; int rc = pcmk_rc_ok; if (dest != NULL) { *dest = default_value; } if (name == NULL) { return EINVAL; } if (xml == NULL) { return pcmk_rc_ok; } value = crm_element_value(xml, name); if (value == NULL) { return pcmk_rc_ok; } rc = pcmk__scan_ll(value, &value_ll, default_value); if ((value_ll < 0) || (value_ll > UINT32_MAX)) { value_ll = default_value; if (rc == pcmk_rc_ok) { rc = pcmk_rc_bad_input; } } if (dest != NULL) { *dest = (uint32_t) value_ll; } return rc; } /*! * \brief Retrieve the long long integer value of an XML attribute * * This is like \c crm_element_value() but getting the value as a long long int. * * \param[in] data XML node to check * \param[in] name Attribute name to check * \param[out] dest Where to store element value * * \return 0 on success, -1 otherwise */ int crm_element_value_ll(const xmlNode *data, const char *name, long long *dest) { const char *value = NULL; CRM_CHECK(dest != NULL, return -1); value = crm_element_value(data, name); if (value != NULL) { int rc = pcmk__scan_ll(value, dest, PCMK__PARSE_INT_DEFAULT); if (rc == pcmk_rc_ok) { return 0; } crm_warn("Using default for %s " "because '%s' is not a valid integer: %s", name, value, pcmk_rc_str(rc)); } return -1; } /*! * \brief Retrieve the millisecond value of an XML attribute * * This is like \c crm_element_value() but returning the value as a guint. * * \param[in] data XML node to check * \param[in] name Attribute name to check * \param[out] dest Where to store attribute value * * \return \c pcmk_ok on success, -1 otherwise */ int crm_element_value_ms(const xmlNode *data, const char *name, guint *dest) { const char *value = NULL; long long value_ll; int rc = pcmk_rc_ok; CRM_CHECK(dest != NULL, return -1); *dest = 0; value = crm_element_value(data, name); rc = pcmk__scan_ll(value, &value_ll, 0LL); if (rc != pcmk_rc_ok) { crm_warn("Using default for %s " "because '%s' is not valid milliseconds: %s", name, value, pcmk_rc_str(rc)); return -1; } if ((value_ll < 0) || (value_ll > G_MAXUINT)) { crm_warn("Using default for %s because '%s' is out of range", name, value); return -1; } *dest = (guint) value_ll; return pcmk_ok; } /*! * \brief Retrieve the seconds-since-epoch value of an XML attribute * * This is like \c crm_element_value() but returning the value as a time_t. * * \param[in] xml XML node to check * \param[in] name Attribute name to check * \param[out] dest Where to store attribute value * * \return \c pcmk_ok on success, -1 otherwise */ int crm_element_value_epoch(const xmlNode *xml, const char *name, time_t *dest) { long long value_ll = 0; if (crm_element_value_ll(xml, name, &value_ll) < 0) { return -1; } /* Unfortunately, we can't do any bounds checking, since time_t has neither * standardized bounds nor constants defined for them. */ *dest = (time_t) value_ll; return pcmk_ok; } /*! * \brief Retrieve the value of XML second/microsecond attributes as time * * This is like \c crm_element_value() but returning value as a struct timeval. * * \param[in] xml XML to parse * \param[in] name_sec Name of XML attribute for seconds * \param[in] name_usec Name of XML attribute for microseconds * \param[out] dest Where to store result * * \return \c pcmk_ok on success, -errno on error * \note Values default to 0 if XML or XML attribute does not exist */ int crm_element_value_timeval(const xmlNode *xml, const char *name_sec, const char *name_usec, struct timeval *dest) { long long value_i = 0; CRM_CHECK(dest != NULL, return -EINVAL); dest->tv_sec = 0; dest->tv_usec = 0; if (xml == NULL) { return pcmk_ok; } /* Unfortunately, we can't do any bounds checking, since there are no * constants provided for the bounds of time_t and suseconds_t, and * calculating them isn't worth the effort. If there are XML values * beyond the native sizes, there will probably be worse problems anyway. */ // Parse seconds errno = 0; if (crm_element_value_ll(xml, name_sec, &value_i) < 0) { return -errno; } dest->tv_sec = (time_t) value_i; // Parse microseconds if (crm_element_value_ll(xml, name_usec, &value_i) < 0) { return -errno; } dest->tv_usec = (suseconds_t) value_i; return pcmk_ok; } /*! * \internal * \brief Get a date/time object from an XML attribute value * * \param[in] xml XML with attribute to parse (from CIB) * \param[in] attr Name of attribute to parse * \param[out] t Where to create date/time object * (\p *t must be NULL initially) * * \return Standard Pacemaker return code * \note The caller is responsible for freeing \p *t using crm_time_free(). */ int pcmk__xe_get_datetime(const xmlNode *xml, const char *attr, crm_time_t **t) { const char *value = NULL; if ((t == NULL) || (*t != NULL) || (xml == NULL) || (attr == NULL)) { return EINVAL; } value = crm_element_value(xml, attr); if (value != NULL) { *t = crm_time_new(value); if (*t == NULL) { return pcmk_rc_unpack_error; } } return pcmk_rc_ok; } /*! * \brief Retrieve a copy of the value of an XML attribute * * This is like \c crm_element_value() but allocating new memory for the result. * * \param[in] data XML node to check * \param[in] name Attribute name to check * * \return Value of specified attribute (may be \c NULL) * \note The caller is responsible for freeing the result. */ char * crm_element_value_copy(const xmlNode *data, const char *name) { return pcmk__str_copy(crm_element_value(data, name)); } /*! * \internal * \brief Add a boolean attribute to an XML node. * * \param[in,out] node XML node to add attributes to * \param[in] name XML attribute to create * \param[in] value Value to give to the attribute */ void pcmk__xe_set_bool_attr(xmlNodePtr node, const char *name, bool value) { crm_xml_add(node, name, pcmk__btoa(value)); } /*! * \internal * \brief Extract a boolean attribute's value from an XML element, with * error checking * * \param[in] node XML node to get attribute from * \param[in] name XML attribute to get * \param[out] value Destination for the value of the attribute * * \return EINVAL if \p name or \p value are NULL, ENODATA if \p node is * NULL or the attribute does not exist, pcmk_rc_unknown_format * if the attribute is not a boolean, and pcmk_rc_ok otherwise. * * \note \p value only has any meaning if the return value is pcmk_rc_ok. */ int pcmk__xe_get_bool_attr(const xmlNode *node, const char *name, bool *value) { const char *xml_value = NULL; int ret, rc; if (node == NULL) { return ENODATA; } else if (name == NULL || value == NULL) { return EINVAL; } xml_value = crm_element_value(node, name); if (xml_value == NULL) { return ENODATA; } rc = crm_str_to_boolean(xml_value, &ret); if (rc == 1) { *value = ret; return pcmk_rc_ok; } else { return pcmk_rc_bad_input; } } /*! * \internal * \brief Extract a boolean attribute's value from an XML element * * \param[in] node XML node to get attribute from * \param[in] name XML attribute to get * * \return True if the given \p name is an attribute on \p node and has * the value \c PCMK_VALUE_TRUE, False in all other cases */ bool pcmk__xe_attr_is_true(const xmlNode *node, const char *name) { bool value = false; int rc; rc = pcmk__xe_get_bool_attr(node, name, &value); return rc == pcmk_rc_ok && value == true; } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include // gboolean, GSList #include // pcmk_xml_attrs2nvpairs(), etc. #include // crm_xml_sanitize_id() #include xmlNode * expand_idref(xmlNode *input, xmlNode *top) { return pcmk__xe_resolve_idref(input, top); } 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); pcmk__assert(len > 0); crm_xml_sanitize_id(id); crm_xml_add(xml, PCMK_XA_ID, id); free(id); } xmlNode * sorted_xml(xmlNode *input, xmlNode *parent, gboolean recursive) { xmlNode *child = NULL; GSList *nvpairs = NULL; xmlNode *result = NULL; CRM_CHECK(input != NULL, return NULL); result = pcmk__xe_create(parent, (const char *) input->name); nvpairs = pcmk_xml_attrs2nvpairs(input); nvpairs = pcmk_sort_nvpairs(nvpairs); pcmk_nvpairs2xml_attrs(nvpairs, result); pcmk_free_nvpairs(nvpairs); for (child = pcmk__xe_first_child(input, NULL, NULL, NULL); child != NULL; child = pcmk__xe_next(child, NULL)) { if (recursive) { sorted_xml(child, result, recursive); } else { pcmk__xml_copy(result, child); } } return result; } // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/pengine/pe_digest.c b/lib/pengine/pe_digest.c index 2979835942..ff59a5aa74 100644 --- a/lib/pengine/pe_digest.c +++ b/lib/pengine/pe_digest.c @@ -1,615 +1,615 @@ /* - * Copyright 2004-2024 the Pacemaker project contributors + * Copyright 2004-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include "pe_status_private.h" extern bool pcmk__is_daemon; /*! * \internal * \brief Free an operation digest cache entry * * \param[in,out] ptr Pointer to cache entry to free * * \note The argument is a gpointer so this can be used as a hash table * free function. */ void pe__free_digests(gpointer ptr) { pcmk__op_digest_t *data = ptr; if (data != NULL) { pcmk__xml_free(data->params_all); pcmk__xml_free(data->params_secure); pcmk__xml_free(data->params_restart); free(data->digest_all_calc); free(data->digest_restart_calc); free(data->digest_secure_calc); free(data); } } // Return true if XML attribute name is not substring of a given string static bool attr_not_in_string(xmlAttrPtr a, void *user_data) { bool filter = false; char *name = crm_strdup_printf(" %s ", (const char *) a->name); if (strstr((const char *) user_data, name) == NULL) { crm_trace("Filtering %s (not found in '%s')", (const char *) a->name, (const char *) user_data); filter = true; } free(name); return filter; } // Return true if XML attribute name is substring of a given string static bool attr_in_string(xmlAttrPtr a, void *user_data) { bool filter = false; char *name = crm_strdup_printf(" %s ", (const char *) a->name); if (strstr((const char *) user_data, name) != NULL) { crm_trace("Filtering %s (found in '%s')", (const char *) a->name, (const char *) user_data); filter = true; } free(name); return filter; } /*! * \internal * \brief Add digest of all parameters to a digest cache entry * * \param[out] data Digest cache entry to modify * \param[in,out] rsc Resource that action was for * \param[in] node Node action was performed on * \param[in] params Resource parameters evaluated for node * \param[in] task Name of action performed * \param[in,out] interval_ms Action's interval (will be reset if in overrides) * \param[in] xml_op Unused * \param[in] op_version CRM feature set to use for digest calculation * \param[in] overrides Key/value table to override resource parameters * \param[in,out] scheduler Scheduler data */ static void calculate_main_digest(pcmk__op_digest_t *data, pcmk_resource_t *rsc, const pcmk_node_t *node, GHashTable *params, const char *task, guint *interval_ms, const xmlNode *xml_op, const char *op_version, GHashTable *overrides, pcmk_scheduler_t *scheduler) { xmlNode *action_config = NULL; data->params_all = pcmk__xe_create(NULL, PCMK_XE_PARAMETERS); /* REMOTE_CONTAINER_HACK: Allow Pacemaker Remote nodes to run containers * that themselves are Pacemaker Remote nodes */ (void) pe__add_bundle_remote_name(rsc, data->params_all, PCMK_REMOTE_RA_ADDR); if (overrides != NULL) { // If interval was overridden, reset it const char *meta_name = CRM_META "_" PCMK_META_INTERVAL; const char *interval_s = g_hash_table_lookup(overrides, meta_name); if (interval_s != NULL) { long long value_ll; if ((pcmk__scan_ll(interval_s, &value_ll, 0LL) == pcmk_rc_ok) && (value_ll >= 0) && (value_ll <= G_MAXUINT)) { *interval_ms = (guint) value_ll; } } // Add overrides to list of all parameters g_hash_table_foreach(overrides, hash2field, data->params_all); } // Add provided instance parameters g_hash_table_foreach(params, hash2field, data->params_all); // Find action configuration XML in CIB action_config = pcmk__find_action_config(rsc, task, *interval_ms, true); /* Add action-specific resource instance attributes to the digest list. * * If this is a one-time action with action-specific instance attributes, * enforce a restart instead of reload-agent in case the main digest doesn't * match, even if the restart digest does. This ensures any changes of the * action-specific parameters get applied for this specific action, and * digests calculated for the resulting history will be correct. Default the * result to RSC_DIGEST_RESTART for the case where the main digest doesn't * match. */ params = pcmk__unpack_action_rsc_params(action_config, node->priv->attrs, scheduler); if ((*interval_ms == 0) && (g_hash_table_size(params) > 0)) { data->rc = pcmk__digest_restart; } g_hash_table_foreach(params, hash2field, data->params_all); g_hash_table_destroy(params); // Add action meta-attributes params = pcmk__unpack_action_meta(rsc, node, task, *interval_ms, action_config); g_hash_table_foreach(params, hash2metafield, data->params_all); g_hash_table_destroy(params); pcmk__filter_op_for_digest(data->params_all); data->digest_all_calc = pcmk__digest_operation(data->params_all); } // Return true if XML attribute name is a Pacemaker-defined fencing parameter static bool is_fence_param(xmlAttrPtr attr, void *user_data) { return pcmk_stonith_param((const char *) attr->name); } /*! * \internal * \brief Add secure digest to a digest cache entry * * \param[out] data Digest cache entry to modify * \param[in] rsc Resource that action was for * \param[in] params Resource parameters evaluated for node * \param[in] xml_op XML of operation in CIB status (if available) * \param[in] op_version CRM feature set to use for digest calculation * \param[in] overrides Key/value hash table to override resource parameters */ static void calculate_secure_digest(pcmk__op_digest_t *data, const pcmk_resource_t *rsc, GHashTable *params, const xmlNode *xml_op, const char *op_version, GHashTable *overrides) { const char *class = crm_element_value(rsc->priv->xml, PCMK_XA_CLASS); const char *secure_list = NULL; bool old_version = (compare_version(op_version, "3.16.0") < 0); if (xml_op == NULL) { secure_list = " passwd password user "; } else { secure_list = crm_element_value(xml_op, PCMK__XA_OP_SECURE_PARAMS); } if (old_version) { data->params_secure = pcmk__xe_create(NULL, PCMK_XE_PARAMETERS); if (overrides != NULL) { g_hash_table_foreach(overrides, hash2field, data->params_secure); } g_hash_table_foreach(params, hash2field, data->params_secure); } else { // Start with a copy of all parameters data->params_secure = pcmk__xml_copy(NULL, data->params_all); } if (secure_list != NULL) { - pcmk__xe_remove_matching_attrs(data->params_secure, attr_in_string, - (void *) secure_list); + pcmk__xe_remove_matching_attrs(data->params_secure, false, + attr_in_string, (void *) secure_list); } if (old_version && pcmk_is_set(pcmk_get_ra_caps(class), pcmk_ra_cap_fence_params)) { /* For stonith resources, Pacemaker adds special parameters, * but these are not listed in fence agent meta-data, so with older * versions of DC, the controller will not hash them. That means we have * to filter them out before calculating our hash for comparison. */ - pcmk__xe_remove_matching_attrs(data->params_secure, is_fence_param, - NULL); + pcmk__xe_remove_matching_attrs(data->params_secure, false, + is_fence_param, NULL); } pcmk__filter_op_for_digest(data->params_secure); /* CRM_meta_timeout *should* be part of a digest for recurring operations. * However, with older versions of DC, the controller does not add timeout * to secure digests, because it only includes parameters declared by the * resource agent. * Remove any timeout that made it this far, to match. */ if (old_version) { pcmk__xe_remove_attr(data->params_secure, CRM_META "_" PCMK_META_TIMEOUT); } data->digest_secure_calc = pcmk__digest_operation(data->params_secure); } /*! * \internal * \brief Add restart digest to a digest cache entry * * \param[out] data Digest cache entry to modify * \param[in] xml_op XML of operation in CIB status (if available) * \param[in] op_version CRM feature set to use for digest calculation * * \note This function doesn't need to handle overrides because it starts with * data->params_all, which already has overrides applied. */ static void calculate_restart_digest(pcmk__op_digest_t *data, const xmlNode *xml_op, const char *op_version) { const char *value = NULL; // We must have XML of resource operation history if (xml_op == NULL) { return; } // And the history must have a restart digest to compare against if (crm_element_value(xml_op, PCMK__XA_OP_RESTART_DIGEST) == NULL) { return; } // Start with a copy of all parameters data->params_restart = pcmk__xml_copy(NULL, data->params_all); // Then filter out reloadable parameters, if any value = crm_element_value(xml_op, PCMK__XA_OP_FORCE_RESTART); if (value != NULL) { - pcmk__xe_remove_matching_attrs(data->params_restart, attr_not_in_string, - (void *) value); + pcmk__xe_remove_matching_attrs(data->params_restart, false, + attr_not_in_string, (void *) value); } value = crm_element_value(xml_op, PCMK_XA_CRM_FEATURE_SET); data->digest_restart_calc = pcmk__digest_operation(data->params_restart); } /*! * \internal * \brief Create a new digest cache entry with calculated digests * * \param[in,out] rsc Resource that action was for * \param[in] task Name of action performed * \param[in,out] interval_ms Action's interval (will be reset if in overrides) * \param[in] node Node action was performed on * \param[in] xml_op XML of operation in CIB status (if available) * \param[in] overrides Key/value table to override resource parameters * \param[in] calc_secure Whether to calculate secure digest * \param[in,out] scheduler Scheduler data * * \return Pointer to new digest cache entry (or NULL on memory error) * \note It is the caller's responsibility to free the result using * pe__free_digests(). */ pcmk__op_digest_t * pe__calculate_digests(pcmk_resource_t *rsc, const char *task, guint *interval_ms, const pcmk_node_t *node, const xmlNode *xml_op, GHashTable *overrides, bool calc_secure, pcmk_scheduler_t *scheduler) { pcmk__op_digest_t *data = NULL; const char *op_version = NULL; GHashTable *params = NULL; CRM_CHECK(scheduler != NULL, return NULL); data = calloc(1, sizeof(pcmk__op_digest_t)); if (data == NULL) { pcmk__sched_err(scheduler, "Could not allocate memory for operation digest"); return NULL; } data->rc = pcmk__digest_match; if (xml_op != NULL) { op_version = crm_element_value(xml_op, PCMK_XA_CRM_FEATURE_SET); } if ((op_version == NULL) && (scheduler->input != NULL)) { op_version = crm_element_value(scheduler->input, PCMK_XA_CRM_FEATURE_SET); } if (op_version == NULL) { op_version = CRM_FEATURE_SET; } params = pe_rsc_params(rsc, node, scheduler); calculate_main_digest(data, rsc, node, params, task, interval_ms, xml_op, op_version, overrides, scheduler); if (calc_secure) { calculate_secure_digest(data, rsc, params, xml_op, op_version, overrides); } calculate_restart_digest(data, xml_op, op_version); return data; } /*! * \internal * \brief Calculate action digests and store in node's digest cache * * \param[in,out] rsc Resource that action was for * \param[in] task Name of action performed * \param[in] interval_ms Action's interval * \param[in,out] node Node action was performed on * \param[in] xml_op XML of operation in CIB status (if available) * \param[in] calc_secure Whether to calculate secure digest * \param[in,out] scheduler Scheduler data * * \return Pointer to node's digest cache entry */ static pcmk__op_digest_t * rsc_action_digest(pcmk_resource_t *rsc, const char *task, guint interval_ms, pcmk_node_t *node, const xmlNode *xml_op, bool calc_secure, pcmk_scheduler_t *scheduler) { pcmk__op_digest_t *data = NULL; char *key = pcmk__op_key(rsc->id, task, interval_ms); data = g_hash_table_lookup(node->priv->digest_cache, key); if (data == NULL) { data = pe__calculate_digests(rsc, task, &interval_ms, node, xml_op, NULL, calc_secure, scheduler); pcmk__assert(data != NULL); g_hash_table_insert(node->priv->digest_cache, strdup(key), data); } free(key); return data; } /*! * \internal * \brief Calculate operation digests and compare against an XML history entry * * \param[in,out] rsc Resource to check * \param[in] xml_op Resource history XML * \param[in,out] node Node to use for digest calculation * \param[in,out] scheduler Scheduler data * * \return Pointer to node's digest cache entry, with comparison result set */ pcmk__op_digest_t * rsc_action_digest_cmp(pcmk_resource_t *rsc, const xmlNode *xml_op, pcmk_node_t *node, pcmk_scheduler_t *scheduler) { pcmk__op_digest_t *data = NULL; guint interval_ms = 0; const char *op_version; const char *task = crm_element_value(xml_op, PCMK_XA_OPERATION); const char *digest_all; const char *digest_restart; pcmk__assert(node != NULL); op_version = crm_element_value(xml_op, PCMK_XA_CRM_FEATURE_SET); digest_all = crm_element_value(xml_op, PCMK__XA_OP_DIGEST); digest_restart = crm_element_value(xml_op, PCMK__XA_OP_RESTART_DIGEST); crm_element_value_ms(xml_op, PCMK_META_INTERVAL, &interval_ms); data = rsc_action_digest(rsc, task, interval_ms, node, xml_op, pcmk_is_set(scheduler->flags, pcmk__sched_sanitized), scheduler); if (!pcmk__str_eq(data->digest_restart_calc, digest_restart, pcmk__str_null_matches)) { pcmk__rsc_info(rsc, "Parameter change for %s-interval %s of %s on %s " "requires restart (hash now %s vs. %s " "with op feature set %s for transition %s)", pcmk__readable_interval(interval_ms), task, rsc->id, pcmk__node_name(node), data->digest_restart_calc, pcmk__s(digest_restart, "missing"), op_version, crm_element_value(xml_op, PCMK__XA_TRANSITION_MAGIC)); data->rc = pcmk__digest_restart; } else if (digest_all == NULL) { /* it is unknown what the previous op digest was */ data->rc = pcmk__digest_unknown; } else if (strcmp(digest_all, data->digest_all_calc) != 0) { /* Given a non-recurring operation with extra parameters configured, * in case that the main digest doesn't match, even if the restart * digest matches, enforce a restart rather than a reload-agent anyway. * So that it ensures any changes of the extra parameters get applied * for this specific operation, and the digests calculated for the * resulting PCMK__XE_LRM_RSC_OP will be correct. * Preserve the implied rc pcmk__digest_restart for the case that the * main digest doesn't match. */ if ((interval_ms == 0) && (data->rc == pcmk__digest_restart)) { pcmk__rsc_info(rsc, "Parameters containing extra ones to %ums-interval" " %s action for %s on %s " "changed: hash was %s vs. now %s (restart:%s) %s", interval_ms, task, rsc->id, pcmk__node_name(node), pcmk__s(digest_all, "missing"), data->digest_all_calc, op_version, crm_element_value(xml_op, PCMK__XA_TRANSITION_MAGIC)); } else { pcmk__rsc_info(rsc, "Parameters to %ums-interval %s action for %s on %s " "changed: hash was %s vs. now %s (%s:%s) %s", interval_ms, task, rsc->id, pcmk__node_name(node), pcmk__s(digest_all, "missing"), data->digest_all_calc, (interval_ms > 0)? "reschedule" : "reload", op_version, crm_element_value(xml_op, PCMK__XA_TRANSITION_MAGIC)); data->rc = pcmk__digest_mismatch; } } else { data->rc = pcmk__digest_match; } return data; } /*! * \internal * \brief Create an unfencing summary for use in special node attribute * * Create a string combining a fence device's resource ID, agent type, and * parameter digest (whether for all parameters or just non-private parameters). * This can be stored in a special node attribute, allowing us to detect changes * in either the agent type or parameters, to know whether unfencing must be * redone or can be safely skipped when the device's history is cleaned. * * \param[in] rsc_id Fence device resource ID * \param[in] agent_type Fence device agent * \param[in] param_digest Fence device parameter digest * * \return Newly allocated string with unfencing digest * \note The caller is responsible for freeing the result. */ static inline char * create_unfencing_summary(const char *rsc_id, const char *agent_type, const char *param_digest) { return crm_strdup_printf("%s:%s:%s", rsc_id, agent_type, param_digest); } /*! * \internal * \brief Check whether a node can skip unfencing * * Check whether a fence device's current definition matches a node's * stored summary of when it was last unfenced by the device. * * \param[in] rsc_id Fence device's resource ID * \param[in] agent Fence device's agent type * \param[in] digest_calc Fence device's current parameter digest * \param[in] node_summary Value of node's special unfencing node attribute * (a comma-separated list of unfencing summaries for * all devices that have unfenced this node) * * \return TRUE if digest matches, FALSE otherwise */ static bool unfencing_digest_matches(const char *rsc_id, const char *agent, const char *digest_calc, const char *node_summary) { bool matches = FALSE; if (rsc_id && agent && digest_calc && node_summary) { char *search_secure = create_unfencing_summary(rsc_id, agent, digest_calc); /* The digest was calculated including the device ID and agent, * so there is no risk of collision using strstr(). */ matches = (strstr(node_summary, search_secure) != NULL); crm_trace("Calculated unfencing digest '%s' %sfound in '%s'", search_secure, matches? "" : "not ", node_summary); free(search_secure); } return matches; } /* Magic string to use as action name for digest cache entries used for * unfencing checks. This is not a real action name (i.e. "on"), so * pcmk__check_action_config() won't confuse these entries with real actions. */ #define STONITH_DIGEST_TASK "stonith-on" /*! * \internal * \brief Calculate fence device digests and digest comparison result * * \param[in,out] rsc Fence device resource * \param[in] agent Fence device's agent type * \param[in,out] node Node with digest cache to use * \param[in,out] scheduler Scheduler data * * \return Node's digest cache entry */ pcmk__op_digest_t * pe__compare_fencing_digest(pcmk_resource_t *rsc, const char *agent, pcmk_node_t *node, pcmk_scheduler_t *scheduler) { const char *node_summary = NULL; // Calculate device's current parameter digests pcmk__op_digest_t *data = rsc_action_digest(rsc, STONITH_DIGEST_TASK, 0U, node, NULL, TRUE, scheduler); // Check whether node has special unfencing summary node attribute node_summary = pcmk__node_attr(node, CRM_ATTR_DIGESTS_ALL, NULL, pcmk__rsc_node_current); if (node_summary == NULL) { data->rc = pcmk__digest_unknown; return data; } // Check whether full parameter digest matches if (unfencing_digest_matches(rsc->id, agent, data->digest_all_calc, node_summary)) { data->rc = pcmk__digest_match; return data; } // Check whether secure parameter digest matches node_summary = pcmk__node_attr(node, CRM_ATTR_DIGESTS_SECURE, NULL, pcmk__rsc_node_current); if (unfencing_digest_matches(rsc->id, agent, data->digest_secure_calc, node_summary)) { data->rc = pcmk__digest_match; if (!pcmk__is_daemon && (scheduler->priv->out != NULL)) { pcmk__output_t *out = scheduler->priv->out; out->info(out, "Only 'private' parameters to %s " "for unfencing %s changed", rsc->id, pcmk__node_name(node)); } return data; } // Parameters don't match data->rc = pcmk__digest_mismatch; if (pcmk_is_set(scheduler->flags, pcmk__sched_sanitized) && (data->digest_secure_calc != NULL)) { if (scheduler->priv->out != NULL) { pcmk__output_t *out = scheduler->priv->out; char *digest = create_unfencing_summary(rsc->id, agent, data->digest_secure_calc); out->info(out, "Parameters to %s for unfencing " "%s changed, try '%s'", rsc->id, pcmk__node_name(node), digest); free(digest); } else if (!pcmk__is_daemon) { char *digest = create_unfencing_summary(rsc->id, agent, data->digest_secure_calc); printf("Parameters to %s for unfencing %s changed, try '%s'\n", rsc->id, pcmk__node_name(node), digest); free(digest); } } return data; }