diff --git a/include/crm/common/Makefile.am b/include/crm/common/Makefile.am index 4d2055f5d7..a70711b9c1 100644 --- a/include/crm/common/Makefile.am +++ b/include/crm/common/Makefile.am @@ -1,52 +1,51 @@ # # Copyright 2004-2024 the Pacemaker project contributors # # The version control history for this file may have further details. # # This source code is licensed under the GNU General Public License version 2 # or later (GPLv2+) WITHOUT ANY WARRANTY. # MAINTAINERCLEANFILES = Makefile.in headerdir=$(pkgincludedir)/crm/common header_HEADERS = acl.h \ actions.h \ agents.h \ agents_compat.h \ cib.h \ ipc.h \ ipc_controld.h \ ipc_pacemakerd.h \ ipc_schedulerd.h \ iso8601.h \ logging.h \ - logging_compat.h \ mainloop.h \ mainloop_compat.h \ nodes.h \ nvpair.h \ options.h \ output.h \ resources.h \ results.h \ results_compat.h \ roles.h \ rules.h \ scheduler.h \ scheduler_types.h \ schemas.h \ scores.h \ scores_compat.h \ tags.h \ tickets.h \ util.h \ util_compat.h \ xml.h \ xml_compat.h \ xml_io.h \ xml_io_compat.h \ xml_names.h noinst_HEADERS = $(wildcard *internal.h) diff --git a/include/crm/common/logging.h b/include/crm/common/logging.h index abc2fe85c0..37983a8415 100644 --- a/include/crm/common/logging.h +++ b/include/crm/common/logging.h @@ -1,432 +1,428 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__CRM_COMMON_LOGGING__H #define PCMK__CRM_COMMON_LOGGING__H #include #include // uint8_t, uint32_t #include #include #include #ifdef __cplusplus extern "C" { #endif /** * \file * \brief Wrappers for and extensions to libqb logging * \ingroup core */ /* Define custom log priorities. * * syslog(3) uses int for priorities, but libqb's struct qb_log_callsite uses * uint8_t, so make sure they fit in the latter. */ // Define something even less desired than debug #ifndef LOG_TRACE #define LOG_TRACE (LOG_DEBUG+1) #endif // Print message to stdout instead of logging it #ifndef LOG_STDOUT #define LOG_STDOUT 254 #endif // Don't send message anywhere #ifndef LOG_NEVER #define LOG_NEVER 255 #endif /* "Extended information" logging support */ #ifdef QB_XS #define CRM_XS QB_XS #define crm_extended_logging(t, e) qb_log_ctl((t), QB_LOG_CONF_EXTENDED, (e)) #else #define CRM_XS "|" /* A caller might want to check the return value, so we can't define this as a * no-op, and we can't simply define it to be 0 because gcc will then complain * when the value isn't checked. */ static inline int crm_extended_logging(int t, int e) { return 0; } #endif // @COMPAT Make internal when we can break API backward compatibility //! \deprecated Do not use extern unsigned int crm_log_level; // @COMPAT Make internal when we can break API backward compatibility //! \deprecated Do not use extern unsigned int crm_trace_nonlog; /*! \deprecated Pacemaker library functions set this when a configuration * error is found, which turns on extra messages at the end of * processing. It should not be used directly and will be removed * from the public C API in a future release. */ extern gboolean crm_config_error; /*! \deprecated Pacemaker library functions set this when a configuration * warning is found, which turns on extra messages at the end of * processing. It should not be used directly and will be removed * from the public C API in a future release. */ extern gboolean crm_config_warning; void crm_enable_blackbox(int nsig); void crm_disable_blackbox(int nsig); void crm_write_blackbox(int nsig, const struct qb_log_callsite *callsite); void crm_update_callsites(void); void crm_log_deinit(void); /*! * \brief Initializes the logging system and defaults to the least verbose output level * * \param[in] entity If not NULL, will be used as the identity for logging purposes * \param[in] argc The number of command line parameters * \param[in] argv The command line parameter values */ void crm_log_preinit(const char *entity, int argc, char *const *argv); gboolean crm_log_init(const char *entity, uint8_t level, gboolean daemon, gboolean to_stderr, int argc, char **argv, gboolean quiet); void crm_log_args(int argc, char **argv); void crm_log_output_fn(const char *file, const char *function, int line, int level, const char *prefix, const char *output); // Log a block of text line by line #define crm_log_output(level, prefix, output) \ crm_log_output_fn(__FILE__, __func__, __LINE__, level, prefix, output) void crm_bump_log_level(int argc, char **argv); void crm_enable_stderr(int enable); gboolean crm_is_callsite_active(struct qb_log_callsite *cs, uint8_t level, uint32_t tags); // NOTE: sbd (as of at least 1.5.2) uses this /* returns the old value */ unsigned int set_crm_log_level(unsigned int level); unsigned int get_crm_log_level(void); void pcmk_log_xml_as(const char *file, const char *function, uint32_t line, uint32_t tags, uint8_t level, const char *text, const xmlNode *xml); /* * Throughout the macros below, note the leading, pre-comma, space in the * various ' , ##args' occurrences to aid portability across versions of 'gcc'. * https://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html#Variadic-Macros */ #if defined(__clang__) #define CRM_TRACE_INIT_DATA(name) #else #include // required by QB_LOG_INIT_DATA() macro #define CRM_TRACE_INIT_DATA(name) QB_LOG_INIT_DATA(name) #endif /*! * \internal * \brief Clip log_level to \p uint8_t range * * \param[in] level Log level to clip * * \return 0 if \p level is less than 0, \p UINT8_MAX if \p level is greater * than \p UINT8_MAX, or \p level otherwise */ /* @COMPAT: Make this function internal at a compatibility break. It's used in * public macros for now. */ static inline uint8_t pcmk__clip_log_level(int level) { if (level <= 0) { return 0; } if (level >= UINT8_MAX) { return UINT8_MAX; } return level; } /* Using "switch" instead of "if" in these macro definitions keeps * static analysis from complaining about constant evaluations */ /*! * \brief Log a message * * \param[in] level Priority at which to log the message * \param[in] fmt printf-style format string literal for message * \param[in] args Any arguments needed by format string */ #define do_crm_log(level, fmt, args...) do { \ uint8_t _level = pcmk__clip_log_level(level); \ \ switch (_level) { \ case LOG_STDOUT: \ printf(fmt "\n" , ##args); \ break; \ case LOG_NEVER: \ break; \ default: \ qb_log_from_external_source(__func__, __FILE__, fmt, \ _level, __LINE__, 0 , ##args); \ break; \ } \ } while (0) /*! * \brief Log a message that is likely to be filtered out * * \param[in] level Priority at which to log the message * \param[in] fmt printf-style format string for message * \param[in] args Any arguments needed by format string * * \note This does nothing when level is \p LOG_STDOUT. */ #define do_crm_log_unlikely(level, fmt, args...) do { \ uint8_t _level = pcmk__clip_log_level(level); \ \ switch (_level) { \ case LOG_STDOUT: case LOG_NEVER: \ break; \ default: { \ static struct qb_log_callsite *trace_cs = NULL; \ if (trace_cs == NULL) { \ trace_cs = qb_log_callsite_get(__func__, __FILE__, fmt, \ _level, __LINE__, 0); \ } \ if (crm_is_callsite_active(trace_cs, _level, 0)) { \ qb_log_from_external_source(__func__, __FILE__, fmt, \ _level, __LINE__, 0 , \ ##args); \ } \ } \ break; \ } \ } while (0) #define CRM_LOG_ASSERT(expr) do { \ if (!(expr)) { \ static struct qb_log_callsite *core_cs = NULL; \ if(core_cs == NULL) { \ core_cs = qb_log_callsite_get(__func__, __FILE__, \ "log-assert", LOG_TRACE, \ __LINE__, 0); \ } \ crm_abort(__FILE__, __func__, __LINE__, #expr, \ core_cs?core_cs->targets:FALSE, TRUE); \ } \ } while(0) // NOTE: sbd (as of at least 1.5.2) uses this /* 'failure_action' MUST NOT be 'continue' as it will apply to the * macro's do-while loop */ #define CRM_CHECK(expr, failure_action) do { \ if (!(expr)) { \ static struct qb_log_callsite *core_cs = NULL; \ if (core_cs == NULL) { \ core_cs = qb_log_callsite_get(__func__, __FILE__, \ "check-assert", \ LOG_TRACE, __LINE__, 0); \ } \ crm_abort(__FILE__, __func__, __LINE__, #expr, \ (core_cs? core_cs->targets: FALSE), TRUE); \ failure_action; \ } \ } while(0) /*! * \brief Log XML line-by-line in a formatted fashion * * \param[in] level Priority at which to log the messages * \param[in] text Prefix for each line * \param[in] xml XML to log * * \note This does nothing when \p level is \p LOG_STDOUT. */ #define do_crm_log_xml(level, text, xml) do { \ uint8_t _level = pcmk__clip_log_level(level); \ static struct qb_log_callsite *xml_cs = NULL; \ \ switch (_level) { \ case LOG_STDOUT: \ case LOG_NEVER: \ break; \ default: \ if (xml_cs == NULL) { \ xml_cs = qb_log_callsite_get(__func__, __FILE__, \ "xml-blob", _level, \ __LINE__, 0); \ } \ if (crm_is_callsite_active(xml_cs, _level, 0)) { \ pcmk_log_xml_as(__FILE__, __func__, __LINE__, 0, \ _level, text, (xml)); \ } \ break; \ } \ } while(0) /*! * \brief Log a message as if it came from a different code location * * \param[in] level Priority at which to log the message * \param[in] file Source file name to use instead of __FILE__ * \param[in] function Source function name to use instead of __func__ * \param[in] line Source line number to use instead of __line__ * \param[in] fmt printf-style format string literal for message * \param[in] args Any arguments needed by format string */ #define do_crm_log_alias(level, file, function, line, fmt, args...) do { \ uint8_t _level = pcmk__clip_log_level(level); \ \ switch (_level) { \ case LOG_STDOUT: \ printf(fmt "\n" , ##args); \ break; \ case LOG_NEVER: \ break; \ default: \ qb_log_from_external_source(function, file, fmt, _level, \ line, 0 , ##args); \ break; \ } \ } while (0) // NOTE: sbd (as of at least 1.5.2) uses this /*! * \brief Send a system error message to both the log and stderr * * \param[in] level Priority at which to log the message * \param[in] fmt printf-style format string for message * \param[in] args Any arguments needed by format string * * \deprecated One of the other logging functions should be used with * pcmk_strerror() instead. * \note This is a macro, and \p level may be evaluated more than once. * \note Because crm_perror() adds the system error message and error number * onto the end of fmt, that information will become extended information * if CRM_XS is used inside fmt and will not show up in syslog. */ #define crm_perror(level, fmt, args...) do { \ uint8_t _level = pcmk__clip_log_level(level); \ \ switch (_level) { \ case LOG_NEVER: \ break; \ default: { \ const char *err = strerror(errno); \ if (_level <= crm_log_level) { \ fprintf(stderr, fmt ": %s (%d)\n" , ##args, err, \ errno); \ } \ /* Pass original level arg since do_crm_log() also declares \ * _level \ */ \ do_crm_log((level), fmt ": %s (%d)" , ##args, err, errno); \ } \ break; \ } \ } while (0) /*! * \brief Log a message with a tag (for use with PCMK_trace_tags) * * \param[in] level Priority at which to log the message * \param[in] tag String to tag message with * \param[in] fmt printf-style format string for message * \param[in] args Any arguments needed by format string * * \note This does nothing when level is LOG_STDOUT. */ #define crm_log_tag(level, tag, fmt, args...) do { \ uint8_t _level = pcmk__clip_log_level(level); \ \ switch (_level) { \ case LOG_STDOUT: case LOG_NEVER: \ break; \ default: { \ static struct qb_log_callsite *trace_tag_cs = NULL; \ int converted_tag = g_quark_try_string(tag); \ if (trace_tag_cs == NULL) { \ trace_tag_cs = qb_log_callsite_get(__func__, __FILE__, \ fmt, _level, \ __LINE__, \ converted_tag); \ } \ if (crm_is_callsite_active(trace_tag_cs, _level, \ converted_tag)) { \ qb_log_from_external_source(__func__, __FILE__, fmt, \ _level, __LINE__, \ converted_tag , ##args); \ } \ } \ } \ } while (0) #define crm_emerg(fmt, args...) qb_log(LOG_EMERG, fmt , ##args) #define crm_crit(fmt, args...) qb_logt(LOG_CRIT, 0, fmt , ##args) // NOTE: sbd (as of at least 1.5.2) uses this #define crm_err(fmt, args...) qb_logt(LOG_ERR, 0, fmt , ##args) // NOTE: sbd (as of at least 1.5.2) uses this #define crm_warn(fmt, args...) qb_logt(LOG_WARNING, 0, fmt , ##args) // NOTE: sbd (as of at least 1.5.2) uses this #define crm_notice(fmt, args...) qb_logt(LOG_NOTICE, 0, fmt , ##args) #define crm_info(fmt, args...) qb_logt(LOG_INFO, 0, fmt , ##args) // // NOTE: sbd (as of at least 1.5.2) uses this #define crm_debug(fmt, args...) do_crm_log_unlikely(LOG_DEBUG, fmt , ##args) #define crm_trace(fmt, args...) do_crm_log_unlikely(LOG_TRACE, fmt , ##args) #define crm_log_xml_crit(xml, text) do_crm_log_xml(LOG_CRIT, text, xml) #define crm_log_xml_err(xml, text) do_crm_log_xml(LOG_ERR, text, xml) #define crm_log_xml_warn(xml, text) do_crm_log_xml(LOG_WARNING, text, xml) #define crm_log_xml_notice(xml, text) do_crm_log_xml(LOG_NOTICE, text, xml) #define crm_log_xml_info(xml, text) do_crm_log_xml(LOG_INFO, text, xml) #define crm_log_xml_debug(xml, text) do_crm_log_xml(LOG_DEBUG, text, xml) #define crm_log_xml_trace(xml, text) do_crm_log_xml(LOG_TRACE, text, xml) #define crm_log_xml_explicit(xml, text) do { \ static struct qb_log_callsite *digest_cs = NULL; \ digest_cs = qb_log_callsite_get( \ __func__, __FILE__, text, LOG_TRACE, __LINE__, \ crm_trace_nonlog); \ if (digest_cs && digest_cs->targets) { \ do_crm_log_xml(LOG_TRACE, text, xml); \ } \ } while(0) -#if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1) -#include -#endif - #ifdef __cplusplus } #endif #endif diff --git a/include/crm/common/logging_compat.h b/include/crm/common/logging_compat.h deleted file mode 100644 index f6dec36838..0000000000 --- a/include/crm/common/logging_compat.h +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2004-2024 the Pacemaker project contributors - * - * The version control history for this file may have further details. - * - * This source code is licensed under the GNU General Public License version 2 - * or later (GPLv2+) WITHOUT ANY WARRANTY. - */ - -#ifndef PCMK__CRM_COMMON_LOGGING_COMPAT__H -#define PCMK__CRM_COMMON_LOGGING_COMPAT__H - -#include // uint8_t -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * \file - * \brief Deprecated Pacemaker logging API - * \ingroup core - * \deprecated Do not include this header directly. Do not use Pacemaker - * libraries for general-purpose logging; libqb's logging API is a - * suitable replacement. The logging APIs in this header, and the - * header itself, will be removed in a future release. - */ - -//! \deprecated This enum will be removed in a future release -enum xml_log_options { - xml_log_option_filtered = 0x0001, - xml_log_option_formatted = 0x0002, - xml_log_option_text = 0x0004, - xml_log_option_full_fledged = 0x0008, - xml_log_option_diff_plus = 0x0010, - xml_log_option_diff_minus = 0x0020, - xml_log_option_diff_short = 0x0040, - xml_log_option_diff_all = 0x0100, - xml_log_option_dirty_add = 0x1000, - xml_log_option_open = 0x2000, - xml_log_option_children = 0x4000, - xml_log_option_close = 0x8000, -}; - -/*! - * \brief Log a message using constant priority - * - * \param[in] level Priority at which to log the message - * \param[in] fmt printf-style format string literal for message - * \param[in] args Any arguments needed by format string - * - * \deprecated Do not use Pacemaker for general-purpose logging - * \note This is a macro, and \p level may be evaluated more than once. - * This does nothing when level is LOG_STDOUT. - */ -#define do_crm_log_always(level, fmt, args...) do { \ - switch (level) { \ - case LOG_STDOUT: case LOG_NEVER: \ - break; \ - default: \ - qb_log((level), fmt , ##args); \ - break; \ - } \ - } while (0) - -//! \deprecated Do not use Pacemaker for general-purpose string handling -#define crm_str(x) (const char *) ((x)? (x) : "") - -//! \deprecated Do not use Pacemaker for general-purpose logging -gboolean crm_log_cli_init(const char *entity); - -//! \deprecated Do not use Pacemaker for general-purpose logging -gboolean crm_add_logfile(const char *filename); - -//! \deprecated Do not use Pacemaker for general-purpose logging -void log_data_element(int log_level, const char *file, const char *function, - int line, const char *prefix, const xmlNode *data, - int depth, int legacy_options); - -//! \deprecated Do not use Pacemaker for general-purpose logging -void pcmk_log_xml_impl(uint8_t level, const char *text, const xmlNode *xml); - -#ifdef __cplusplus -} -#endif - -#endif // PCMK__CRM_COMMON_LOGGING_COMPAT__H diff --git a/include/crm/common/xml_compat.h b/include/crm/common/xml_compat.h index 12ca631afe..54423eb3f6 100644 --- a/include/crm/common/xml_compat.h +++ b/include/crm/common/xml_compat.h @@ -1,216 +1,69 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__CRM_COMMON_XML_COMPAT__H #define PCMK__CRM_COMMON_XML_COMPAT__H #include // gboolean #include // xmlNode #include // crm_xml_add() #include // PCMK_XE_CLONE #ifdef __cplusplus extern "C" { #endif /** * \file * \brief Deprecated Pacemaker XML API * \ingroup core * \deprecated Do not include this header directly. The XML APIs in this * header, and the header itself, will be removed in a future * release. */ -//! \deprecated Do not use (will be removed in a future release) -#define XML_PARANOIA_CHECKS 0 - -//! \deprecated This function will be removed in a future release -xmlDoc *getDocPtr(xmlNode *node); - -//! \deprecated This function will be removed in a future release -int add_node_nocopy(xmlNode * parent, const char *name, xmlNode * child); - -//! \deprecated This function will be removed in a future release -xmlNode *find_entity(xmlNode *parent, const char *node_name, const char *id); - -//! \deprecated This function will be removed in a future release -char *xml_get_path(const xmlNode *xml); - -//! \deprecated This function will be removed in a future release -void xml_log_changes(uint8_t level, const char *function, const xmlNode *xml); - -//! \deprecated This function will be removed in a future release -void xml_log_patchset(uint8_t level, const char *function, const xmlNode *xml); - -//! \deprecated Use xml_apply_patchset() instead -gboolean apply_xml_diff(xmlNode *old_xml, xmlNode *diff, xmlNode **new_xml); - -//! \deprecated Do not use (will be removed in a future release) -void crm_destroy_xml(gpointer data); - -//! \deprecated Check children member directly -gboolean xml_has_children(const xmlNode *root); - -//! \deprecated Use crm_xml_add() with "true" or "false" instead -static inline const char * -crm_xml_add_boolean(xmlNode *node, const char *name, gboolean value) -{ - return crm_xml_add(node, name, (value? "true" : "false")); -} - // NOTE: sbd (as of at least 1.5.2) uses this //! \deprecated Use name member directly static inline const char * crm_element_name(const xmlNode *xml) { return (xml == NULL)? NULL : (const char *) xml->name; } -//! \deprecated Do not use -char *crm_xml_escape(const char *text); - // NOTE: sbd (as of at least 1.5.2) uses this //! \deprecated Do not use Pacemaker for general-purpose XML manipulation xmlNode *copy_xml(xmlNode *src_node); -//! \deprecated Do not use Pacemaker for general-purpose XML manipulation -xmlNode *add_node_copy(xmlNode *new_parent, xmlNode *xml_node); - -//! \deprecated Do not use -void purge_diff_markers(xmlNode *a_node); - -//! \deprecated Do not use -xmlNode *diff_xml_object(xmlNode *left, xmlNode *right, gboolean suppress); - -//! \deprecated Do not use -xmlNode *subtract_xml_object(xmlNode *parent, xmlNode *left, xmlNode *right, - gboolean full, gboolean *changed, - const char *marker); - -//! \deprecated Do not use -gboolean can_prune_leaf(xmlNode *xml_node); - -//! \deprecated Do not use Pacemaker for general-purpose XML manipulation -xmlNode *create_xml_node(xmlNode *parent, const char *name); - -//! \deprecated Do not use Pacemaker for general-purpose XML manipulation -xmlNode *pcmk_create_xml_text_node(xmlNode *parent, const char *name, - const char *content); - -//! \deprecated Do not use Pacemaker for general-purpose XML manipulation -xmlNode *pcmk_create_html_node(xmlNode *parent, const char *element_name, - const char *id, const char *class_name, - const char *text); - -//! \deprecated Do not use Pacemaker for general-purpose XML manipulation -xmlNode *first_named_child(const xmlNode *parent, const char *name); - -//! \deprecated Do not use Pacemaker for general-purpose XML manipulation -xmlNode *find_xml_node(const xmlNode *root, const char *search_path, - gboolean must_find); - -//! \deprecated Do not use Pacemaker for general-purpose XML manipulation -xmlNode *crm_next_same_xml(const xmlNode *sibling); - -//! \deprecated Do not use Pacemaker for general-purpose XML manipulation -void xml_remove_prop(xmlNode *obj, const char *name); - -//! \deprecated Do not use Pacemaker for general-purpose XML manipulation -gboolean replace_xml_child(xmlNode *parent, xmlNode *child, xmlNode *update, - gboolean delete_only); - -//! \deprecated Do not use Pacemaker for general-purpose XML manipulation -gboolean update_xml_child(xmlNode *child, xmlNode *to_update); - -//! \deprecated Do not use Pacemaker for general-purpose XML manipulation -int find_xml_children(xmlNode **children, xmlNode *root, const char *tag, - const char *field, const char *value, - gboolean search_matches); - -//! \deprecated Do not use Pacemaker for general-purpose XML manipulation -xmlNode *get_xpath_object_relative(const char *xpath, xmlNode *xml_obj, - int error_level); - -//! \deprecated Do not use -void fix_plus_plus_recursive(xmlNode *target); - -//! \deprecated Do not use Pacemaker for general-purpose XML manipulation -gboolean add_message_xml(xmlNode *msg, const char *field, xmlNode *xml); - -//! \deprecated Do not use Pacemaker for general-purpose XML manipulation -xmlNode *get_message_xml(const xmlNode *msg, const char *field); - -//! \deprecated Do not use -const char *xml_latest_schema(void); - -//! \deprecated Do not use -const char *get_schema_name(int version); - -//! \deprecated Do not use -int get_schema_version(const char *name); - -//! \deprecated Do not use -int update_validation(xmlNode **xml_blob, int *best, int max, - gboolean transform, gboolean to_logs); - -//! \deprecated Do not use -gboolean validate_xml(xmlNode *xml_blob, const char *validation, - gboolean to_logs); - -//! \deprecated Do not use -gboolean validate_xml_verbose(const xmlNode *xml_blob); - // NOTE: sbd (as of at least 1.5.2) uses this //! \deprecated Do not use gboolean cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs); -//! \deprecated Do not use -static inline const char * -crm_map_element_name(const xmlNode *xml) -{ - if (xml == NULL) { - return NULL; - } else if (strcmp((const char *) xml->name, "master") == 0) { - // Can't use PCMK__XE_PROMOTABLE_LEGACY because it's internal - return PCMK_XE_CLONE; - } else { - return (const char *) xml->name; - } -} - -//! \deprecated Do not use -void copy_in_properties(xmlNode *target, const xmlNode *src); - -//! \deprecated Do not use -void expand_plus_plus(xmlNode * target, const char *name, const char *value); - // NOTE: sbd (as of at least 1.5.2) uses this //! \deprecated Call \c crm_log_init() or \c crm_log_cli_init() instead void crm_xml_init(void); //! \deprecated Exit with \c crm_exit() instead void crm_xml_cleanup(void); //! \deprecated Do not use Pacemaker for general-purpose XML manipulation void pcmk_free_xml_subtree(xmlNode *xml); // NOTE: sbd (as of at least 1.5.2) uses this //! \deprecated Do not use Pacemaker for general-purpose XML manipulation void free_xml(xmlNode *child); //! \deprecated Do not use Pacemaker for general-purpose XML manipulation xmlNode *expand_idref(xmlNode *input, xmlNode *top); #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_XML_COMPAT__H diff --git a/lib/common/logging.c b/lib/common/logging.c index 0555d527d5..444f49365e 100644 --- a/lib/common/logging.c +++ b/lib/common/logging.c @@ -1,1297 +1,1270 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Use high-resolution (millisecond) timestamps if libqb supports them #ifdef QB_FEATURE_LOG_HIRES_TIMESTAMPS #define TIMESTAMP_FORMAT_SPEC "%%T" typedef struct timespec *log_time_t; #else #define TIMESTAMP_FORMAT_SPEC "%%t" typedef time_t log_time_t; #endif unsigned int crm_log_level = LOG_INFO; unsigned int crm_trace_nonlog = 0; bool pcmk__is_daemon = false; char *pcmk__our_nodename = NULL; static unsigned int crm_log_priority = LOG_NOTICE; static GLogFunc glib_log_default = NULL; static pcmk__output_t *logger_out = NULL; pcmk__config_error_func pcmk__config_error_handler = NULL; pcmk__config_warning_func pcmk__config_warning_handler = NULL; void *pcmk__config_error_context = NULL; void *pcmk__config_warning_context = NULL; static gboolean crm_tracing_enabled(void); static void crm_glib_handler(const gchar * log_domain, GLogLevelFlags flags, const gchar * message, gpointer user_data) { int log_level = LOG_WARNING; GLogLevelFlags msg_level = (flags & G_LOG_LEVEL_MASK); static struct qb_log_callsite *glib_cs = NULL; if (glib_cs == NULL) { glib_cs = qb_log_callsite_get(__func__, __FILE__, "glib-handler", LOG_DEBUG, __LINE__, crm_trace_nonlog); } switch (msg_level) { case G_LOG_LEVEL_CRITICAL: log_level = LOG_CRIT; if (!crm_is_callsite_active(glib_cs, LOG_DEBUG, crm_trace_nonlog)) { /* log and record how we got here */ crm_abort(__FILE__, __func__, __LINE__, message, TRUE, TRUE); } break; case G_LOG_LEVEL_ERROR: log_level = LOG_ERR; break; case G_LOG_LEVEL_MESSAGE: log_level = LOG_NOTICE; break; case G_LOG_LEVEL_INFO: log_level = LOG_INFO; break; case G_LOG_LEVEL_DEBUG: log_level = LOG_DEBUG; break; case G_LOG_LEVEL_WARNING: case G_LOG_FLAG_RECURSION: case G_LOG_FLAG_FATAL: case G_LOG_LEVEL_MASK: log_level = LOG_WARNING; break; } do_crm_log(log_level, "%s: %s", log_domain, message); } #ifndef NAME_MAX # define NAME_MAX 256 #endif /*! * \internal * \brief Write out a blackbox (enabling blackboxes if needed) * * \param[in] nsig Signal number that was received * * \note This is a true signal handler, and so must be async-safe. */ static void crm_trigger_blackbox(int nsig) { if(nsig == SIGTRAP) { /* Turn it on if it wasn't already */ crm_enable_blackbox(nsig); } crm_write_blackbox(nsig, NULL); } void crm_log_deinit(void) { if (glib_log_default != NULL) { g_log_set_default_handler(glib_log_default, NULL); } } #define FMT_MAX 256 /*! * \internal * \brief Set the log format string based on the passed-in method * * \param[in] method The detail level of the log output * \param[in] daemon The daemon ID included in error messages * \param[in] use_pid Cached result of getpid() call, for efficiency * \param[in] use_nodename Cached result of uname() call, for efficiency * */ /* XXX __attribute__((nonnull)) for use_nodename parameter */ static void set_format_string(int method, const char *daemon, pid_t use_pid, const char *use_nodename) { if (method == QB_LOG_SYSLOG) { // The system log gets a simplified, user-friendly format crm_extended_logging(method, QB_FALSE); qb_log_format_set(method, "%g %p: %b"); } else { // Everything else gets more detail, for advanced troubleshooting int offset = 0; char fmt[FMT_MAX]; if (method > QB_LOG_STDERR) { // If logging to file, prefix with timestamp, node name, daemon ID offset += snprintf(fmt + offset, FMT_MAX - offset, TIMESTAMP_FORMAT_SPEC " %s %-20s[%lu] ", use_nodename, daemon, (unsigned long) use_pid); } // Add function name (in parentheses) offset += snprintf(fmt + offset, FMT_MAX - offset, "(%%n"); if (crm_tracing_enabled()) { // When tracing, add file and line number offset += snprintf(fmt + offset, FMT_MAX - offset, "@%%f:%%l"); } offset += snprintf(fmt + offset, FMT_MAX - offset, ")"); // Add tag (if any), severity, and actual message offset += snprintf(fmt + offset, FMT_MAX - offset, " %%g\t%%p: %%b"); CRM_LOG_ASSERT(offset > 0); qb_log_format_set(method, fmt); } } #define DEFAULT_LOG_FILE CRM_LOG_DIR "/pacemaker.log" static bool logfile_disabled(const char *filename) { return pcmk__str_eq(filename, PCMK_VALUE_NONE, pcmk__str_casei) || pcmk__str_eq(filename, "/dev/null", pcmk__str_none); } /*! * \internal * \brief Fix log file ownership if group is wrong or doesn't have access * * \param[in] filename Log file name (for logging only) * \param[in] logfd Log file descriptor * * \return Standard Pacemaker return code */ static int chown_logfile(const char *filename, int logfd) { uid_t pcmk_uid = 0; gid_t pcmk_gid = 0; struct stat st; int rc; // Get the log file's current ownership and permissions if (fstat(logfd, &st) < 0) { return errno; } // Any other errors don't prevent file from being used as log rc = pcmk_daemon_user(&pcmk_uid, &pcmk_gid); if (rc != pcmk_ok) { rc = pcmk_legacy2rc(rc); crm_warn("Not changing '%s' ownership because user information " "unavailable: %s", filename, pcmk_rc_str(rc)); return pcmk_rc_ok; } if ((st.st_gid == pcmk_gid) && ((st.st_mode & S_IRWXG) == (S_IRGRP|S_IWGRP))) { return pcmk_rc_ok; } if (fchown(logfd, pcmk_uid, pcmk_gid) < 0) { crm_warn("Couldn't change '%s' ownership to user %s gid %d: %s", filename, CRM_DAEMON_USER, pcmk_gid, strerror(errno)); } return pcmk_rc_ok; } // Reset log file permissions (using environment variable if set) static void chmod_logfile(const char *filename, int logfd) { const char *modestr = pcmk__env_option(PCMK__ENV_LOGFILE_MODE); mode_t filemode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP; if (modestr != NULL) { long filemode_l = strtol(modestr, NULL, 8); if ((filemode_l != LONG_MIN) && (filemode_l != LONG_MAX)) { filemode = (mode_t) filemode_l; } } if ((filemode != 0) && (fchmod(logfd, filemode) < 0)) { crm_warn("Couldn't change '%s' mode to %04o: %s", filename, filemode, strerror(errno)); } } // If we're root, correct a log file's permissions if needed static int set_logfile_permissions(const char *filename, FILE *logfile) { if (geteuid() == 0) { int logfd = fileno(logfile); int rc = chown_logfile(filename, logfd); if (rc != pcmk_rc_ok) { return rc; } chmod_logfile(filename, logfd); } return pcmk_rc_ok; } // Enable libqb logging to a new log file static void enable_logfile(int fd) { qb_log_ctl(fd, QB_LOG_CONF_ENABLED, QB_TRUE); #if 0 qb_log_ctl(fd, QB_LOG_CONF_FILE_SYNC, 1); // Turn on synchronous writes #endif #ifdef HAVE_qb_log_conf_QB_LOG_CONF_MAX_LINE_LEN // Longer than default, for logging long XML lines qb_log_ctl(fd, QB_LOG_CONF_MAX_LINE_LEN, 800); #endif crm_update_callsites(); } static inline void disable_logfile(int fd) { qb_log_ctl(fd, QB_LOG_CONF_ENABLED, QB_FALSE); } static void setenv_logfile(const char *filename) { // Some resource agents will log only if environment variable is set if (pcmk__env_option(PCMK__ENV_LOGFILE) == NULL) { pcmk__set_env_option(PCMK__ENV_LOGFILE, filename, true); } } /*! * \brief Add a file to be used as a Pacemaker detail log * * \param[in] filename Name of log file to use * * \return Standard Pacemaker return code */ int pcmk__add_logfile(const char *filename) { /* No log messages from this function will be logged to the new log! * If another target such as syslog has already been added, the messages * should show up there. */ int fd = 0; int rc = pcmk_rc_ok; FILE *logfile = NULL; bool is_default = false; static int default_fd = -1; static bool have_logfile = false; // Use default if caller didn't specify (and we don't already have one) if (filename == NULL) { if (have_logfile) { return pcmk_rc_ok; } filename = DEFAULT_LOG_FILE; } // If the user doesn't want logging, we're done if (logfile_disabled(filename)) { return pcmk_rc_ok; } // If the caller wants the default and we already have it, we're done is_default = pcmk__str_eq(filename, DEFAULT_LOG_FILE, pcmk__str_none); if (is_default && (default_fd >= 0)) { return pcmk_rc_ok; } // Check whether we have write access to the file logfile = fopen(filename, "a"); if (logfile == NULL) { rc = errno; crm_warn("Logging to '%s' is disabled: %s " CRM_XS " uid=%u gid=%u", filename, strerror(rc), geteuid(), getegid()); return rc; } rc = set_logfile_permissions(filename, logfile); if (rc != pcmk_rc_ok) { crm_warn("Logging to '%s' is disabled: %s " CRM_XS " permissions", filename, strerror(rc)); fclose(logfile); return rc; } // Close and reopen as libqb logging target fclose(logfile); fd = qb_log_file_open(filename); if (fd < 0) { crm_warn("Logging to '%s' is disabled: %s " CRM_XS " qb_log_file_open", filename, strerror(-fd)); return -fd; // == +errno } if (is_default) { default_fd = fd; setenv_logfile(filename); } else if (default_fd >= 0) { crm_notice("Switching logging to %s", filename); disable_logfile(default_fd); } crm_notice("Additional logging available in %s", filename); enable_logfile(fd); have_logfile = true; return pcmk_rc_ok; } /*! * \brief Add multiple additional log files * * \param[in] log_files Array of log files to add * \param[in] out Output object to use for error reporting * * \return Standard Pacemaker return code */ void pcmk__add_logfiles(gchar **log_files, pcmk__output_t *out) { if (log_files == NULL) { return; } for (gchar **fname = log_files; *fname != NULL; fname++) { int rc = pcmk__add_logfile(*fname); if (rc != pcmk_rc_ok) { out->err(out, "Logging to %s is disabled: %s", *fname, pcmk_rc_str(rc)); } } } static int blackbox_trigger = 0; static volatile char *blackbox_file_prefix = NULL; static void blackbox_logger(int32_t t, struct qb_log_callsite *cs, log_time_t timestamp, const char *msg) { if(cs && cs->priority < LOG_ERR) { crm_write_blackbox(SIGTRAP, cs); /* Bypass the over-dumping logic */ } else { crm_write_blackbox(0, cs); } } static void crm_control_blackbox(int nsig, bool enable) { int lpc = 0; if (blackbox_file_prefix == NULL) { pid_t pid = getpid(); blackbox_file_prefix = crm_strdup_printf("%s/%s-%lu", CRM_BLACKBOX_DIR, crm_system_name, (unsigned long) pid); } if (enable && qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) { qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_SIZE, 5 * 1024 * 1024); /* Any size change drops existing entries */ qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_TRUE); /* Setting the size seems to disable it */ /* Enable synchronous logging */ for (lpc = QB_LOG_BLACKBOX; lpc < QB_LOG_TARGET_MAX; lpc++) { qb_log_ctl(lpc, QB_LOG_CONF_FILE_SYNC, QB_TRUE); } crm_notice("Initiated blackbox recorder: %s", blackbox_file_prefix); /* Save to disk on abnormal termination */ crm_signal_handler(SIGSEGV, crm_trigger_blackbox); crm_signal_handler(SIGABRT, crm_trigger_blackbox); crm_signal_handler(SIGILL, crm_trigger_blackbox); crm_signal_handler(SIGBUS, crm_trigger_blackbox); crm_signal_handler(SIGFPE, crm_trigger_blackbox); crm_update_callsites(); blackbox_trigger = qb_log_custom_open(blackbox_logger, NULL, NULL, NULL); qb_log_ctl(blackbox_trigger, QB_LOG_CONF_ENABLED, QB_TRUE); crm_trace("Trigger: %d is %d %d", blackbox_trigger, qb_log_ctl(blackbox_trigger, QB_LOG_CONF_STATE_GET, 0), QB_LOG_STATE_ENABLED); crm_update_callsites(); } else if (!enable && qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_STATE_GET, 0) == QB_LOG_STATE_ENABLED) { qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE); /* Disable synchronous logging again when the blackbox is disabled */ for (lpc = QB_LOG_BLACKBOX; lpc < QB_LOG_TARGET_MAX; lpc++) { qb_log_ctl(lpc, QB_LOG_CONF_FILE_SYNC, QB_FALSE); } } } void crm_enable_blackbox(int nsig) { crm_control_blackbox(nsig, TRUE); } void crm_disable_blackbox(int nsig) { crm_control_blackbox(nsig, FALSE); } /*! * \internal * \brief Write out a blackbox, if blackboxes are enabled * * \param[in] nsig Signal that was received * \param[in] cs libqb callsite * * \note This may be called via a true signal handler and so must be async-safe. * @TODO actually make this async-safe */ void crm_write_blackbox(int nsig, const struct qb_log_callsite *cs) { static volatile int counter = 1; static volatile time_t last = 0; char buffer[NAME_MAX]; time_t now = time(NULL); if (blackbox_file_prefix == NULL) { return; } switch (nsig) { case 0: case SIGTRAP: /* The graceful case - such as assertion failure or user request */ if (nsig == 0 && now == last) { /* Prevent over-dumping */ return; } snprintf(buffer, NAME_MAX, "%s.%d", blackbox_file_prefix, counter++); if (nsig == SIGTRAP) { crm_notice("Blackbox dump requested, please see %s for contents", buffer); } else if (cs) { syslog(LOG_NOTICE, "Problem detected at %s:%d (%s), please see %s for additional details", cs->function, cs->lineno, cs->filename, buffer); } else { crm_notice("Problem detected, please see %s for additional details", buffer); } last = now; qb_log_blackbox_write_to_file(buffer); /* Flush the existing contents * A size change would also work */ qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE); qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_TRUE); break; default: /* Do as little as possible, just try to get what we have out * We logged the filename when the blackbox was enabled */ crm_signal_handler(nsig, SIG_DFL); qb_log_blackbox_write_to_file((const char *)blackbox_file_prefix); qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE); raise(nsig); break; } } static const char * crm_quark_to_string(uint32_t tag) { const char *text = g_quark_to_string(tag); if (text) { return text; } return ""; } static void crm_log_filter_source(int source, const char *trace_files, const char *trace_fns, const char *trace_fmts, const char *trace_tags, const char *trace_blackbox, struct qb_log_callsite *cs) { if (qb_log_ctl(source, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) { return; } else if (cs->tags != crm_trace_nonlog && source == QB_LOG_BLACKBOX) { /* Blackbox gets everything if enabled */ qb_bit_set(cs->targets, source); } else if (source == blackbox_trigger && blackbox_trigger > 0) { /* Should this log message result in the blackbox being dumped */ if (cs->priority <= LOG_ERR) { qb_bit_set(cs->targets, source); } else if (trace_blackbox) { char *key = crm_strdup_printf("%s:%d", cs->function, cs->lineno); if (strstr(trace_blackbox, key) != NULL) { qb_bit_set(cs->targets, source); } free(key); } } else if (source == QB_LOG_SYSLOG) { /* No tracing to syslog */ if (cs->priority <= crm_log_priority && cs->priority <= crm_log_level) { qb_bit_set(cs->targets, source); } /* Log file tracing options... */ } else if (cs->priority <= crm_log_level) { qb_bit_set(cs->targets, source); } else if (trace_files && strstr(trace_files, cs->filename) != NULL) { qb_bit_set(cs->targets, source); } else if (trace_fns && strstr(trace_fns, cs->function) != NULL) { qb_bit_set(cs->targets, source); } else if (trace_fmts && strstr(trace_fmts, cs->format) != NULL) { qb_bit_set(cs->targets, source); } else if (trace_tags && cs->tags != 0 && cs->tags != crm_trace_nonlog && g_quark_to_string(cs->tags) != NULL) { qb_bit_set(cs->targets, source); } } #ifndef HAVE_STRCHRNUL /* strchrnul() is a GNU extension. If not present, use our own definition. * The GNU version returns char*, but we only need it to be const char*. */ static const char * strchrnul(const char *s, int c) { while ((*s != c) && (*s != '\0')) { ++s; } return s; } #endif static void crm_log_filter(struct qb_log_callsite *cs) { int lpc = 0; static int need_init = 1; static const char *trace_fns = NULL; static const char *trace_tags = NULL; static const char *trace_fmts = NULL; static const char *trace_files = NULL; static const char *trace_blackbox = NULL; if (need_init) { need_init = 0; trace_fns = pcmk__env_option(PCMK__ENV_TRACE_FUNCTIONS); trace_fmts = pcmk__env_option(PCMK__ENV_TRACE_FORMATS); trace_tags = pcmk__env_option(PCMK__ENV_TRACE_TAGS); trace_files = pcmk__env_option(PCMK__ENV_TRACE_FILES); trace_blackbox = pcmk__env_option(PCMK__ENV_TRACE_BLACKBOX); if (trace_tags != NULL) { uint32_t tag; char token[500]; const char *offset = NULL; const char *next = trace_tags; do { offset = next; next = strchrnul(offset, ','); snprintf(token, sizeof(token), "%.*s", (int)(next - offset), offset); tag = g_quark_from_string(token); crm_info("Created GQuark %u from token '%s' in '%s'", tag, token, trace_tags); if (next[0] != 0) { next++; } } while (next != NULL && next[0] != 0); } } cs->targets = 0; /* Reset then find targets to enable */ for (lpc = QB_LOG_SYSLOG; lpc < QB_LOG_TARGET_MAX; lpc++) { crm_log_filter_source(lpc, trace_files, trace_fns, trace_fmts, trace_tags, trace_blackbox, cs); } } gboolean crm_is_callsite_active(struct qb_log_callsite *cs, uint8_t level, uint32_t tags) { gboolean refilter = FALSE; if (cs == NULL) { return FALSE; } if (cs->priority != level) { cs->priority = level; refilter = TRUE; } if (cs->tags != tags) { cs->tags = tags; refilter = TRUE; } if (refilter) { crm_log_filter(cs); } if (cs->targets == 0) { return FALSE; } return TRUE; } void crm_update_callsites(void) { static gboolean log = TRUE; if (log) { log = FALSE; crm_debug ("Enabling callsites based on priority=%d, files=%s, functions=%s, formats=%s, tags=%s", crm_log_level, pcmk__env_option(PCMK__ENV_TRACE_FILES), pcmk__env_option(PCMK__ENV_TRACE_FUNCTIONS), pcmk__env_option(PCMK__ENV_TRACE_FORMATS), pcmk__env_option(PCMK__ENV_TRACE_TAGS)); } qb_log_filter_fn_set(crm_log_filter); } static gboolean crm_tracing_enabled(void) { return (crm_log_level == LOG_TRACE) || (pcmk__env_option(PCMK__ENV_TRACE_FILES) != NULL) || (pcmk__env_option(PCMK__ENV_TRACE_FUNCTIONS) != NULL) || (pcmk__env_option(PCMK__ENV_TRACE_FORMATS) != NULL) || (pcmk__env_option(PCMK__ENV_TRACE_TAGS) != NULL); } static int crm_priority2int(const char *name) { struct syslog_names { const char *name; int priority; }; static struct syslog_names p_names[] = { {"emerg", LOG_EMERG}, {"alert", LOG_ALERT}, {"crit", LOG_CRIT}, {"error", LOG_ERR}, {"warning", LOG_WARNING}, {"notice", LOG_NOTICE}, {"info", LOG_INFO}, {"debug", LOG_DEBUG}, {NULL, -1} }; int lpc; for (lpc = 0; name != NULL && p_names[lpc].name != NULL; lpc++) { if (pcmk__str_eq(p_names[lpc].name, name, pcmk__str_none)) { return p_names[lpc].priority; } } return crm_log_priority; } /*! * \internal * \brief Set the identifier for the current process * * If the identifier crm_system_name is not already set, then it is set as follows: * - it is passed to the function via the "entity" parameter, or * - it is derived from the executable name * * The identifier can be used in logs, IPC, and more. * * This method also sets the PCMK_service environment variable. * * \param[in] entity If not NULL, will be assigned to the identifier * \param[in] argc The number of command line parameters * \param[in] argv The command line parameter values */ static void set_identity(const char *entity, int argc, char *const *argv) { if (crm_system_name != NULL) { return; // Already set, don't overwrite } if (entity != NULL) { crm_system_name = pcmk__str_copy(entity); } else if ((argc > 0) && (argv != NULL)) { char *mutable = strdup(argv[0]); char *modified = basename(mutable); if (strstr(modified, "lt-") == modified) { modified += 3; } crm_system_name = pcmk__str_copy(modified); free(mutable); } else { crm_system_name = pcmk__str_copy("Unknown"); } // Used by fencing.py.py (in fence-agents) pcmk__set_env_option(PCMK__ENV_SERVICE, crm_system_name, false); } void crm_log_preinit(const char *entity, int argc, char *const *argv) { /* Configure libqb logging with nothing turned on */ struct utsname res; int lpc = 0; int32_t qb_facility = 0; pid_t pid = getpid(); const char *nodename = "localhost"; static bool have_logging = false; if (have_logging) { return; } have_logging = true; pcmk__xml_init(); if (crm_trace_nonlog == 0) { crm_trace_nonlog = g_quark_from_static_string("Pacemaker non-logging tracepoint"); } umask(S_IWGRP | S_IWOTH | S_IROTH); /* Redirect messages from glib functions to our handler */ glib_log_default = g_log_set_default_handler(crm_glib_handler, NULL); /* and for good measure... - this enum is a bit field (!) */ g_log_set_always_fatal((GLogLevelFlags) 0); /*value out of range */ /* Set crm_system_name, which is used as the logging name. It may also * be used for other purposes such as an IPC client name. */ set_identity(entity, argc, argv); qb_facility = qb_log_facility2int("local0"); qb_log_init(crm_system_name, qb_facility, LOG_ERR); crm_log_level = LOG_CRIT; /* Nuke any syslog activity until it's asked for */ qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_ENABLED, QB_FALSE); #ifdef HAVE_qb_log_conf_QB_LOG_CONF_MAX_LINE_LEN // Shorter than default, generous for what we *should* send to syslog qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_MAX_LINE_LEN, 256); #endif if (uname(memset(&res, 0, sizeof(res))) == 0 && *res.nodename != '\0') { nodename = res.nodename; } /* Set format strings and disable threading * Pacemaker and threads do not mix well (due to the amount of forking) */ qb_log_tags_stringify_fn_set(crm_quark_to_string); for (lpc = QB_LOG_SYSLOG; lpc < QB_LOG_TARGET_MAX; lpc++) { qb_log_ctl(lpc, QB_LOG_CONF_THREADED, QB_FALSE); #ifdef HAVE_qb_log_conf_QB_LOG_CONF_ELLIPSIS // End truncated lines with '...' qb_log_ctl(lpc, QB_LOG_CONF_ELLIPSIS, QB_TRUE); #endif set_format_string(lpc, crm_system_name, pid, nodename); } #ifdef ENABLE_NLS /* Enable translations (experimental). Currently we only have a few * proof-of-concept translations for some option help. The goal would be to * offer translations for option help and man pages rather than logs or * documentation, to reduce the burden of maintaining them. */ // Load locale information for the local host from the environment setlocale(LC_ALL, ""); // Tell gettext where to find Pacemaker message catalogs CRM_ASSERT(bindtextdomain(PACKAGE, PCMK__LOCALE_DIR) != NULL); // Tell gettext to use the Pacemaker message catalogs CRM_ASSERT(textdomain(PACKAGE) != NULL); // Tell gettext that the translated strings are stored in UTF-8 bind_textdomain_codeset(PACKAGE, "UTF-8"); #endif } gboolean crm_log_init(const char *entity, uint8_t level, gboolean daemon, gboolean to_stderr, int argc, char **argv, gboolean quiet) { const char *syslog_priority = NULL; const char *facility = pcmk__env_option(PCMK__ENV_LOGFACILITY); const char *f_copy = facility; pcmk__is_daemon = daemon; crm_log_preinit(entity, argc, argv); if (level > LOG_TRACE) { level = LOG_TRACE; } if(level > crm_log_level) { crm_log_level = level; } /* Should we log to syslog */ if (facility == NULL) { if (pcmk__is_daemon) { facility = "daemon"; } else { facility = PCMK_VALUE_NONE; } pcmk__set_env_option(PCMK__ENV_LOGFACILITY, facility, true); } if (pcmk__str_eq(facility, PCMK_VALUE_NONE, pcmk__str_casei)) { quiet = TRUE; } else { qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_FACILITY, qb_log_facility2int(facility)); } if (pcmk__env_option_enabled(crm_system_name, PCMK__ENV_DEBUG)) { /* Override the default setting */ crm_log_level = LOG_DEBUG; } /* What lower threshold do we have for sending to syslog */ syslog_priority = pcmk__env_option(PCMK__ENV_LOGPRIORITY); if (syslog_priority) { crm_log_priority = crm_priority2int(syslog_priority); } qb_log_filter_ctl(QB_LOG_SYSLOG, QB_LOG_FILTER_ADD, QB_LOG_FILTER_FILE, "*", crm_log_priority); // Log to syslog unless requested to be quiet if (!quiet) { qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_ENABLED, QB_TRUE); } /* Should we log to stderr */ if (pcmk__env_option_enabled(crm_system_name, PCMK__ENV_STDERR)) { /* Override the default setting */ to_stderr = TRUE; } crm_enable_stderr(to_stderr); // Log to a file if we're a daemon or user asked for one { const char *logfile = pcmk__env_option(PCMK__ENV_LOGFILE); if (!pcmk__str_eq(PCMK_VALUE_NONE, logfile, pcmk__str_casei) && (pcmk__is_daemon || (logfile != NULL))) { // Daemons always get a log file, unless explicitly set to "none" pcmk__add_logfile(logfile); } } if (pcmk__is_daemon && pcmk__env_option_enabled(crm_system_name, PCMK__ENV_BLACKBOX)) { crm_enable_blackbox(0); } /* Summary */ crm_trace("Quiet: %d, facility %s", quiet, f_copy); pcmk__env_option(PCMK__ENV_LOGFILE); pcmk__env_option(PCMK__ENV_LOGFACILITY); crm_update_callsites(); /* Ok, now we can start logging... */ // Disable daemon request if user isn't root or Pacemaker daemon user if (pcmk__is_daemon) { const char *user = getenv("USER"); if (user != NULL && !pcmk__strcase_any_of(user, "root", CRM_DAEMON_USER, NULL)) { crm_trace("Not switching to corefile directory for %s", user); pcmk__is_daemon = false; } } if (pcmk__is_daemon) { int user = getuid(); struct passwd *pwent = getpwuid(user); if (pwent == NULL) { crm_perror(LOG_ERR, "Cannot get name for uid: %d", user); } else if (!pcmk__strcase_any_of(pwent->pw_name, "root", CRM_DAEMON_USER, NULL)) { crm_trace("Don't change active directory for regular user: %s", pwent->pw_name); } else if (chdir(CRM_CORE_DIR) < 0) { crm_perror(LOG_INFO, "Cannot change active directory to " CRM_CORE_DIR); } else { crm_info("Changed active directory to " CRM_CORE_DIR); } /* Original meanings from signal(7) * * Signal Value Action Comment * SIGTRAP 5 Core Trace/breakpoint trap * SIGUSR1 30,10,16 Term User-defined signal 1 * SIGUSR2 31,12,17 Term User-defined signal 2 * * Our usage is as similar as possible */ mainloop_add_signal(SIGUSR1, crm_enable_blackbox); mainloop_add_signal(SIGUSR2, crm_disable_blackbox); mainloop_add_signal(SIGTRAP, crm_trigger_blackbox); } else if (!quiet) { crm_log_args(argc, argv); } return TRUE; } /* returns the old value */ unsigned int set_crm_log_level(unsigned int level) { unsigned int old = crm_log_level; if (level > LOG_TRACE) { level = LOG_TRACE; } crm_log_level = level; crm_update_callsites(); crm_trace("New log level: %d", level); return old; } void crm_enable_stderr(int enable) { if (enable && qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) { qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_ENABLED, QB_TRUE); crm_update_callsites(); } else if (enable == FALSE) { qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_ENABLED, QB_FALSE); } } /*! * \brief Make logging more verbose * * If logging to stderr is not already enabled when this function is called, * enable it. Otherwise, increase the log level by 1. * * \param[in] argc Ignored * \param[in] argv Ignored */ void crm_bump_log_level(int argc, char **argv) { if (qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) { crm_enable_stderr(TRUE); } else { set_crm_log_level(crm_log_level + 1); } } unsigned int get_crm_log_level(void) { return crm_log_level; } /*! * \brief Log the command line (once) * * \param[in] Number of values in \p argv * \param[in] Command-line arguments (including command name) * * \note This function will only log once, even if called with different * arguments. */ void crm_log_args(int argc, char **argv) { static bool logged = false; gchar *arg_string = NULL; if ((argc == 0) || (argv == NULL) || logged) { return; } logged = true; arg_string = g_strjoinv(" ", argv); crm_notice("Invoked: %s", arg_string); g_free(arg_string); } void crm_log_output_fn(const char *file, const char *function, int line, int level, const char *prefix, const char *output) { const char *next = NULL; const char *offset = NULL; if (level == LOG_NEVER) { return; } if (output == NULL) { if (level != LOG_STDOUT) { level = LOG_TRACE; } output = "-- empty --"; } next = output; do { offset = next; next = strchrnul(offset, '\n'); do_crm_log_alias(level, file, function, line, "%s [ %.*s ]", prefix, (int)(next - offset), offset); if (next[0] != 0) { next++; } } while (next != NULL && next[0] != 0); } void pcmk__cli_init_logging(const char *name, unsigned int verbosity) { crm_log_init(name, LOG_ERR, FALSE, FALSE, 0, NULL, TRUE); for (int i = 0; i < verbosity; i++) { /* These arguments are ignored, so pass placeholders. */ crm_bump_log_level(0, NULL); } } /*! * \brief Log XML line-by-line in a formatted fashion * * \param[in] file File name to use for log filtering * \param[in] function Function name to use for log filtering * \param[in] line Line number to use for log filtering * \param[in] tags Logging tags to use for log filtering * \param[in] level Priority at which to log the messages * \param[in] text Prefix for each line * \param[in] xml XML to log * * \note This does nothing when \p level is \p LOG_STDOUT. * \note Do not call this function directly. It should be called only from the * \p do_crm_log_xml() macro. */ void pcmk_log_xml_as(const char *file, const char *function, uint32_t line, uint32_t tags, uint8_t level, const char *text, const xmlNode *xml) { if (xml == NULL) { do_crm_log(level, "%s%sNo data to dump as XML", pcmk__s(text, ""), pcmk__str_empty(text)? "" : " "); } else { if (logger_out == NULL) { CRM_CHECK(pcmk__log_output_new(&logger_out) == pcmk_rc_ok, return); } pcmk__output_set_log_level(logger_out, level); pcmk__output_set_log_filter(logger_out, file, function, line, tags); pcmk__xml_show(logger_out, text, xml, 1, pcmk__xml_fmt_pretty |pcmk__xml_fmt_open |pcmk__xml_fmt_children |pcmk__xml_fmt_close); pcmk__output_set_log_filter(logger_out, NULL, NULL, 0U, 0U); } } /*! * \internal * \brief Log XML changes line-by-line in a formatted fashion * * \param[in] file File name to use for log filtering * \param[in] function Function name to use for log filtering * \param[in] line Line number to use for log filtering * \param[in] tags Logging tags to use for log filtering * \param[in] level Priority at which to log the messages * \param[in] xml XML whose changes to log * * \note This does nothing when \p level is \c LOG_STDOUT. */ void pcmk__log_xml_changes_as(const char *file, const char *function, uint32_t line, uint32_t tags, uint8_t level, const xmlNode *xml) { if (xml == NULL) { do_crm_log(level, "No XML to dump"); return; } if (logger_out == NULL) { CRM_CHECK(pcmk__log_output_new(&logger_out) == pcmk_rc_ok, return); } pcmk__output_set_log_level(logger_out, level); pcmk__output_set_log_filter(logger_out, file, function, line, tags); pcmk__xml_show_changes(logger_out, xml); pcmk__output_set_log_filter(logger_out, NULL, NULL, 0U, 0U); } /*! * \internal * \brief Log an XML patchset line-by-line in a formatted fashion * * \param[in] file File name to use for log filtering * \param[in] function Function name to use for log filtering * \param[in] line Line number to use for log filtering * \param[in] tags Logging tags to use for log filtering * \param[in] level Priority at which to log the messages * \param[in] patchset XML patchset to log * * \note This does nothing when \p level is \c LOG_STDOUT. */ void pcmk__log_xml_patchset_as(const char *file, const char *function, uint32_t line, uint32_t tags, uint8_t level, const xmlNode *patchset) { if (patchset == NULL) { do_crm_log(level, "No patchset to dump"); return; } if (logger_out == NULL) { CRM_CHECK(pcmk__log_output_new(&logger_out) == pcmk_rc_ok, return); } pcmk__output_set_log_level(logger_out, level); pcmk__output_set_log_filter(logger_out, file, function, line, tags); logger_out->message(logger_out, "xml-patchset", patchset); pcmk__output_set_log_filter(logger_out, NULL, NULL, 0U, 0U); } /*! * \internal * \brief Free the logging library's internal log output object */ void pcmk__free_common_logger(void) { if (logger_out != NULL) { logger_out->finish(logger_out, CRM_EX_OK, true, NULL); pcmk__output_free(logger_out); logger_out = NULL; } } -// Deprecated functions kept only for backward API compatibility -// LCOV_EXCL_START - -#include - -gboolean -crm_log_cli_init(const char *entity) -{ - pcmk__cli_init_logging(entity, 0); - return TRUE; -} - -gboolean -crm_add_logfile(const char *filename) -{ - return pcmk__add_logfile(filename) == pcmk_rc_ok; -} - -void -pcmk_log_xml_impl(uint8_t level, const char *text, const xmlNode *xml) -{ - pcmk_log_xml_as(__FILE__, __func__, __LINE__, 0, level, text, xml); -} - -// LCOV_EXCL_STOP -// End deprecated API - void pcmk__set_config_error_handler(pcmk__config_error_func error_handler, void *error_context) { pcmk__config_error_handler = error_handler; pcmk__config_error_context = error_context; } void pcmk__set_config_warning_handler(pcmk__config_warning_func warning_handler, void *warning_context) { pcmk__config_warning_handler = warning_handler; pcmk__config_warning_context = warning_context; } diff --git a/lib/common/messages.c b/lib/common/messages.c index c619aacc0e..2e78f8bcde 100644 --- a/lib/common/messages.c +++ b/lib/common/messages.c @@ -1,309 +1,284 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include /*! * \brief Create a Pacemaker request (for IPC or cluster layer) * * \param[in] task What to set as the request's task * \param[in] msg_data What to add as the request's data contents * \param[in] host_to What to set as the request's destination host * \param[in] sys_to What to set as the request's destination system * \param[in] sys_from If not NULL, set as request's origin system * \param[in] uuid_from If not NULL, use in request's origin system * \param[in] origin Name of function that called this one * * \return XML of new request * * \note One of sys_from or uuid_from must be non-NULL * \note This function should not be called directly, but via the * create_request() wrapper. * \note The caller is responsible for freeing the return value using * \c pcmk__xml_free(). */ xmlNode * create_request_adv(const char *task, xmlNode *msg_data, const char *host_to, const char *sys_to, const char *sys_from, const char *uuid_from, const char *origin) { static uint ref_counter = 0; char *true_from = NULL; xmlNode *request = NULL; char *reference = crm_strdup_printf("%s-%s-%lld-%u", (task? task : "_empty_"), (sys_from? sys_from : "_empty_"), (long long) time(NULL), ref_counter++); if (uuid_from != NULL) { true_from = crm_strdup_printf("%s_%s", uuid_from, (sys_from? sys_from : "none")); } else if (sys_from != NULL) { true_from = strdup(sys_from); } else { crm_err("Cannot create IPC request: No originating system specified"); } // host_from will get set for us if necessary by the controller when routed request = pcmk__xe_create(NULL, __func__); crm_xml_add(request, PCMK_XA_ORIGIN, origin); crm_xml_add(request, PCMK__XA_T, PCMK__VALUE_CRMD); crm_xml_add(request, PCMK_XA_VERSION, CRM_FEATURE_SET); crm_xml_add(request, PCMK__XA_SUBT, PCMK__VALUE_REQUEST); crm_xml_add(request, PCMK_XA_REFERENCE, reference); crm_xml_add(request, PCMK__XA_CRM_TASK, task); crm_xml_add(request, PCMK__XA_CRM_SYS_TO, sys_to); crm_xml_add(request, PCMK__XA_CRM_SYS_FROM, true_from); /* HOSTTO will be ignored if it is to the DC anyway. */ if (host_to != NULL && strlen(host_to) > 0) { crm_xml_add(request, PCMK__XA_CRM_HOST_TO, host_to); } if (msg_data != NULL) { xmlNode *wrapper = pcmk__xe_create(request, PCMK__XE_CRM_XML); pcmk__xml_copy(wrapper, msg_data); } free(reference); free(true_from); return request; } /*! * \brief Create a Pacemaker reply (for IPC or cluster layer) * * \param[in] original_request XML of request this is a reply to * \param[in] xml_response_data XML to copy as data section of reply * \param[in] origin Name of function that called this one * * \return XML of new reply * * \note This function should not be called directly, but via the * create_reply() wrapper. * \note The caller is responsible for freeing the return value using * \c pcmk__xml_free(). */ xmlNode * create_reply_adv(const xmlNode *original_request, xmlNode *xml_response_data, const char *origin) { xmlNode *reply = NULL; const char *host_from = crm_element_value(original_request, PCMK__XA_SRC); const char *sys_from = crm_element_value(original_request, PCMK__XA_CRM_SYS_FROM); const char *sys_to = crm_element_value(original_request, PCMK__XA_CRM_SYS_TO); const char *type = crm_element_value(original_request, PCMK__XA_SUBT); const char *operation = crm_element_value(original_request, PCMK__XA_CRM_TASK); const char *crm_msg_reference = crm_element_value(original_request, PCMK_XA_REFERENCE); if (type == NULL) { crm_err("Cannot create new_message, no message type in original message"); CRM_ASSERT(type != NULL); return NULL; } if (strcmp(type, PCMK__VALUE_REQUEST) != 0) { /* Replies should only be generated for request messages, but it's possible * we expect replies to other messages right now so this can't be enforced. */ crm_trace("Creating a reply for a non-request original message"); } reply = pcmk__xe_create(NULL, __func__); crm_xml_add(reply, PCMK_XA_ORIGIN, origin); crm_xml_add(reply, PCMK__XA_T, PCMK__VALUE_CRMD); crm_xml_add(reply, PCMK_XA_VERSION, CRM_FEATURE_SET); crm_xml_add(reply, PCMK__XA_SUBT, PCMK__VALUE_RESPONSE); crm_xml_add(reply, PCMK_XA_REFERENCE, crm_msg_reference); crm_xml_add(reply, PCMK__XA_CRM_TASK, operation); /* since this is a reply, we reverse the from and to */ crm_xml_add(reply, PCMK__XA_CRM_SYS_TO, sys_from); crm_xml_add(reply, PCMK__XA_CRM_SYS_FROM, sys_to); /* HOSTTO will be ignored if it is to the DC anyway. */ if (host_from != NULL && strlen(host_from) > 0) { crm_xml_add(reply, PCMK__XA_CRM_HOST_TO, host_from); } if (xml_response_data != NULL) { xmlNode *wrapper = pcmk__xe_create(reply, PCMK__XE_CRM_XML); pcmk__xml_copy(wrapper, xml_response_data); } return reply; } /*! * \brief Get name to be used as identifier for cluster messages * * \param[in] name Actual system name to check * * \return Non-NULL cluster message identifier corresponding to name * * \note The Pacemaker daemons were renamed in version 2.0.0, but the old names * must continue to be used as the identifier for cluster messages, so * that mixed-version clusters are possible during a rolling upgrade. */ const char * pcmk__message_name(const char *name) { if (name == NULL) { return "unknown"; } else if (!strcmp(name, "pacemaker-attrd")) { return "attrd"; } else if (!strcmp(name, "pacemaker-based")) { return CRM_SYSTEM_CIB; } else if (!strcmp(name, "pacemaker-controld")) { return CRM_SYSTEM_CRMD; } else if (!strcmp(name, "pacemaker-execd")) { return CRM_SYSTEM_LRMD; } else if (!strcmp(name, "pacemaker-fenced")) { return "stonith-ng"; } else if (!strcmp(name, "pacemaker-schedulerd")) { return CRM_SYSTEM_PENGINE; } else { return name; } } /*! * \internal * \brief Register handlers for server commands * * \param[in] handlers Array of handler functions for supported server commands * (the final entry must have a NULL command name, and if * it has a handler it will be used as the default handler * for unrecognized commands) * * \return Newly created hash table with commands and handlers * \note The caller is responsible for freeing the return value with * g_hash_table_destroy(). */ GHashTable * pcmk__register_handlers(const pcmk__server_command_t handlers[]) { GHashTable *commands = g_hash_table_new(g_str_hash, g_str_equal); if (handlers != NULL) { int i; for (i = 0; handlers[i].command != NULL; ++i) { g_hash_table_insert(commands, (gpointer) handlers[i].command, handlers[i].handler); } if (handlers[i].handler != NULL) { // g_str_hash() can't handle NULL, so use empty string for default g_hash_table_insert(commands, (gpointer) "", handlers[i].handler); } } return commands; } /*! * \internal * \brief Process an incoming request * * \param[in,out] request Request to process * \param[in] handlers Command table created by pcmk__register_handlers() * * \return XML to send as reply (or NULL if no reply is needed) */ xmlNode * pcmk__process_request(pcmk__request_t *request, GHashTable *handlers) { xmlNode *(*handler)(pcmk__request_t *request) = NULL; CRM_CHECK((request != NULL) && (request->op != NULL) && (handlers != NULL), return NULL); if (pcmk_is_set(request->flags, pcmk__request_sync) && (request->ipc_client != NULL)) { CRM_CHECK(request->ipc_client->request_id == request->ipc_id, return NULL); } handler = g_hash_table_lookup(handlers, request->op); if (handler == NULL) { handler = g_hash_table_lookup(handlers, ""); // Default handler if (handler == NULL) { crm_info("Ignoring %s request from %s %s with no handler", request->op, pcmk__request_origin_type(request), pcmk__request_origin(request)); return NULL; } } return (*handler)(request); } /*! * \internal * \brief Free memory used within a request (but not the request itself) * * \param[in,out] request Request to reset */ void pcmk__reset_request(pcmk__request_t *request) { free(request->op); request->op = NULL; pcmk__reset_result(&(request->result)); } - -// Deprecated functions kept only for backward API compatibility -// LCOV_EXCL_START - -#include - -gboolean -add_message_xml(xmlNode *msg, const char *field, xmlNode *xml) -{ - xmlNode *holder = pcmk__xe_create(msg, field); - - pcmk__xml_copy(holder, xml); - return TRUE; -} - -xmlNode * -get_message_xml(const xmlNode *msg, const char *field) -{ - xmlNode *child = pcmk__xe_first_child(msg, field, NULL, NULL); - - return pcmk__xe_first_child(child, NULL, NULL, NULL); -} - -// LCOV_EXCL_STOP -// End deprecated API diff --git a/lib/common/patchset.c b/lib/common/patchset.c index 312bdd2af7..9c155674d2 100644 --- a/lib/common/patchset.c +++ b/lib/common/patchset.c @@ -1,1596 +1,1473 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include #include #include // CRM_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; } // @COMPAT Remove when v1 patchsets are removed static void xml_repair_v1_diff(xmlNode *last, xmlNode *next, xmlNode *local_diff, gboolean changed) { int lpc = 0; xmlNode *cib = NULL; xmlNode *diff_child = NULL; const char *tag = NULL; const char *vfields[] = { PCMK_XA_ADMIN_EPOCH, PCMK_XA_EPOCH, PCMK_XA_NUM_UPDATES, }; if (local_diff == NULL) { crm_trace("Nothing to do"); return; } tag = PCMK__XE_DIFF_REMOVED; diff_child = pcmk__xe_first_child(local_diff, tag, NULL, NULL); if (diff_child == NULL) { diff_child = pcmk__xe_create(local_diff, tag); } tag = PCMK_XE_CIB; cib = pcmk__xe_first_child(diff_child, tag, NULL, NULL); if (cib == NULL) { cib = pcmk__xe_create(diff_child, tag); } for (lpc = 0; (last != NULL) && (lpc < PCMK__NELEM(vfields)); lpc++) { const char *value = crm_element_value(last, vfields[lpc]); crm_xml_add(diff_child, vfields[lpc], value); if (changed || lpc == 2) { crm_xml_add(cib, vfields[lpc], value); } } tag = PCMK__XE_DIFF_ADDED; diff_child = pcmk__xe_first_child(local_diff, tag, NULL, NULL); if (diff_child == NULL) { diff_child = pcmk__xe_create(local_diff, tag); } tag = PCMK_XE_CIB; cib = pcmk__xe_first_child(diff_child, tag, NULL, NULL); if (cib == NULL) { cib = pcmk__xe_create(diff_child, tag); } for (lpc = 0; next && lpc < PCMK__NELEM(vfields); lpc++) { const char *value = crm_element_value(next, vfields[lpc]); crm_xml_add(diff_child, vfields[lpc], value); } for (xmlAttrPtr a = pcmk__xe_first_attr(next); a != NULL; a = a->next) { const char *p_value = pcmk__xml_attr_value(a); xmlSetProp(cib, a->name, (pcmkXmlStr) p_value); } crm_log_xml_explicit(local_diff, "Repaired-diff"); } // @COMPAT Remove when v1 patchsets are removed static xmlNode * xml_create_patchset_v1(xmlNode *source, xmlNode *target, bool config, bool suppress) { xmlNode *patchset = pcmk__diff_v1_xml_object(source, target, suppress); if (patchset) { CRM_LOG_ASSERT(xml_document_dirty(target)); xml_repair_v1_diff(source, target, patchset, config); crm_xml_add(patchset, PCMK_XA_FORMAT, "1"); } return patchset; } 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, }; CRM_ASSERT(target); if (!xml_document_dirty(target)) { return NULL; } CRM_ASSERT(target->doc); 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) { int counter = 0; bool config = FALSE; xmlNode *patch = NULL; const char *version = crm_element_value(source, PCMK_XA_CRM_FEATURE_SET); xml_acl_disable(target); if (!xml_document_dirty(target)) { crm_trace("No change %d", format); return NULL; /* No change */ } config = is_config_change(target); if (config_changed) { *config_changed = config; } if (manage_version && config) { crm_trace("Config changed %d", format); 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 if (manage_version) { crm_element_value_int(target, PCMK_XA_NUM_UPDATES, &counter); crm_trace("Status changed %d - %d %s", format, counter, crm_element_value(source, PCMK_XA_NUM_UPDATES)); crm_xml_add_int(target, PCMK_XA_NUM_UPDATES, (counter + 1)); } if (format == 0) { if (compare_version("3.0.8", version) < 0) { format = 2; } else { format = 1; } crm_trace("Using patch format %d for version: %s", format, version); } switch (format) { case 1: // @COMPAT Remove when v1 patchsets are removed patch = xml_create_patchset_v1(source, target, config, FALSE); break; case 2: patch = xml_create_patchset_v2(source, target); break; default: crm_err("Unknown patch format: %d", format); return NULL; } return patch; } void patchset_process_digest(xmlNode *patch, xmlNode *source, xmlNode *target, bool with_digest) { int format = 1; const char *version = NULL; char *digest = NULL; if ((patch == NULL) || (source == NULL) || (target == NULL)) { 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(!xml_document_dirty(target)); crm_element_value_int(patch, PCMK_XA_FORMAT, &format); if ((format > 1) && !with_digest) { return; } version = crm_element_value(source, PCMK_XA_CRM_FEATURE_SET); digest = calculate_xml_versioned_digest(target, FALSE, TRUE, version); crm_xml_add(patch, PCMK__XA_DIGEST, digest); free(digest); return; } // @COMPAT Remove when v1 patchsets are removed static xmlNode * subtract_v1_xml_comment(xmlNode *parent, xmlNode *left, xmlNode *right, gboolean *changed) { CRM_CHECK(left != NULL, return NULL); CRM_CHECK(left->type == XML_COMMENT_NODE, return NULL); if ((right == NULL) || !pcmk__str_eq((const char *)left->content, (const char *)right->content, pcmk__str_casei)) { xmlNode *deleted = NULL; deleted = pcmk__xml_copy(parent, left); *changed = TRUE; return deleted; } return NULL; } // @COMPAT Remove when v1 patchsets are removed static xmlNode * subtract_v1_xml_object(xmlNode *parent, xmlNode *left, xmlNode *right, bool full, gboolean *changed, const char *marker) { gboolean dummy = FALSE; xmlNode *diff = NULL; xmlNode *right_child = NULL; xmlNode *left_child = NULL; xmlAttrPtr xIter = NULL; const char *id = NULL; const char *name = NULL; const char *value = NULL; const char *right_val = NULL; if (changed == NULL) { changed = &dummy; } if (left == NULL) { return NULL; } if (left->type == XML_COMMENT_NODE) { return subtract_v1_xml_comment(parent, left, right, changed); } id = pcmk__xe_id(left); name = (const char *) left->name; if (right == NULL) { xmlNode *deleted = NULL; crm_trace("Processing <%s " PCMK_XA_ID "=%s> (complete copy)", name, id); deleted = pcmk__xml_copy(parent, left); crm_xml_add(deleted, PCMK__XA_CRM_DIFF_MARKER, marker); *changed = TRUE; return deleted; } CRM_CHECK(name != NULL, return NULL); CRM_CHECK(pcmk__xe_is(left, (const char *) right->name), return NULL); // Check for PCMK__XA_CRM_DIFF_MARKER in a child value = crm_element_value(right, PCMK__XA_CRM_DIFF_MARKER); if ((value != NULL) && (strcmp(value, "removed:top") == 0)) { crm_trace("We are the root of the deletion: %s.id=%s", name, id); *changed = TRUE; return NULL; } // @TODO Avoiding creating the full hierarchy would save work here diff = pcmk__xe_create(parent, name); // Changes to child objects for (left_child = pcmk__xml_first_child(left); left_child != NULL; left_child = pcmk__xml_next(left_child)) { gboolean child_changed = FALSE; right_child = pcmk__xml_match(right, left_child, false); subtract_v1_xml_object(diff, left_child, right_child, full, &child_changed, marker); if (child_changed) { *changed = TRUE; } } if (!*changed) { /* Nothing to do */ } else if (full) { xmlAttrPtr pIter = NULL; for (pIter = pcmk__xe_first_attr(left); pIter != NULL; pIter = pIter->next) { const char *p_name = (const char *)pIter->name; const char *p_value = pcmk__xml_attr_value(pIter); xmlSetProp(diff, (pcmkXmlStr) p_name, (pcmkXmlStr) p_value); } // We have everything we need goto done; } // Changes to name/value pairs for (xIter = pcmk__xe_first_attr(left); xIter != NULL; xIter = xIter->next) { const char *prop_name = (const char *) xIter->name; xmlAttrPtr right_attr = NULL; xml_node_private_t *nodepriv = NULL; if (strcmp(prop_name, PCMK_XA_ID) == 0) { // id already obtained when present ~ this case, so just reuse xmlSetProp(diff, (pcmkXmlStr) PCMK_XA_ID, (pcmkXmlStr) id); continue; } if (pcmk__xa_filterable(prop_name)) { continue; } right_attr = xmlHasProp(right, (pcmkXmlStr) prop_name); if (right_attr) { nodepriv = right_attr->_private; } right_val = crm_element_value(right, prop_name); if ((right_val == NULL) || (nodepriv && pcmk_is_set(nodepriv->flags, pcmk__xf_deleted))) { /* new */ *changed = TRUE; if (full) { xmlAttrPtr pIter = NULL; for (pIter = pcmk__xe_first_attr(left); pIter != NULL; pIter = pIter->next) { const char *p_name = (const char *) pIter->name; const char *p_value = pcmk__xml_attr_value(pIter); xmlSetProp(diff, (pcmkXmlStr) p_name, (pcmkXmlStr) p_value); } break; } else { const char *left_value = pcmk__xml_attr_value(xIter); xmlSetProp(diff, (pcmkXmlStr) prop_name, (pcmkXmlStr) value); crm_xml_add(diff, prop_name, left_value); } } else { /* Only now do we need the left value */ const char *left_value = pcmk__xml_attr_value(xIter); if (strcmp(left_value, right_val) == 0) { /* unchanged */ } else { *changed = TRUE; if (full) { xmlAttrPtr pIter = NULL; crm_trace("Changes detected to %s in " "<%s " PCMK_XA_ID "=%s>", prop_name, name, id); for (pIter = pcmk__xe_first_attr(left); pIter != NULL; pIter = pIter->next) { const char *p_name = (const char *) pIter->name; const char *p_value = pcmk__xml_attr_value(pIter); xmlSetProp(diff, (pcmkXmlStr) p_name, (pcmkXmlStr) p_value); } break; } else { crm_trace("Changes detected to %s (%s -> %s) in " "<%s " PCMK_XA_ID "=%s>", prop_name, left_value, right_val, name, id); crm_xml_add(diff, prop_name, left_value); } } } } if (!*changed) { pcmk__xml_free(diff); return NULL; } else if (!full && (id != NULL)) { crm_xml_add(diff, PCMK_XA_ID, id); } done: return diff; } /* @COMPAT Remove when v1 patchsets are removed. * * Return true if attribute name is not \c PCMK_XML_ID. */ static bool not_id(xmlAttrPtr attr, void *user_data) { return strcmp((const char *) attr->name, PCMK_XA_ID) != 0; } /* @COMPAT Remove when v1 patchsets are removed. * * Apply the removals section of a v1 patchset to an XML node. */ static void process_v1_removals(xmlNode *target, xmlNode *patch) { xmlNode *patch_child = NULL; xmlNode *cIter = NULL; char *id = NULL; const char *value = NULL; if ((target == NULL) || (patch == NULL)) { return; } if (target->type == XML_COMMENT_NODE) { gboolean dummy; subtract_v1_xml_comment(target->parent, target, patch, &dummy); } CRM_CHECK(pcmk__xe_is(target, (const char *) patch->name), return); CRM_CHECK(pcmk__str_eq(pcmk__xe_id(target), pcmk__xe_id(patch), pcmk__str_none), return); // Check for PCMK__XA_CRM_DIFF_MARKER in a child id = crm_element_value_copy(target, PCMK_XA_ID); value = crm_element_value(patch, PCMK__XA_CRM_DIFF_MARKER); if ((value != NULL) && (strcmp(value, "removed:top") == 0)) { crm_trace("We are the root of the deletion: %s.id=%s", target->name, id); pcmk__xml_free(target); free(id); return; } // Removing then restoring id would change ordering of properties pcmk__xe_remove_matching_attrs(patch, not_id, NULL); // Changes to child objects cIter = pcmk__xml_first_child(target); while (cIter) { xmlNode *target_child = cIter; cIter = pcmk__xml_next(cIter); patch_child = pcmk__xml_match(patch, target_child, false); process_v1_removals(target_child, patch_child); } free(id); } /* @COMPAT Remove when v1 patchsets are removed. * * Apply the additions section of a v1 patchset to an XML node. */ static void process_v1_additions(xmlNode *parent, xmlNode *target, xmlNode *patch) { xmlNode *patch_child = NULL; xmlNode *target_child = NULL; xmlAttrPtr xIter = NULL; const char *id = NULL; const char *name = NULL; const char *value = NULL; if (patch == NULL) { return; } else if ((parent == NULL) && (target == NULL)) { return; } // Check for PCMK__XA_CRM_DIFF_MARKER in a child name = (const char *) patch->name; value = crm_element_value(patch, PCMK__XA_CRM_DIFF_MARKER); if ((target == NULL) && (value != NULL) && (strcmp(value, "added:top") == 0)) { id = pcmk__xe_id(patch); crm_trace("We are the root of the addition: %s.id=%s", name, id); pcmk__xml_copy(parent, patch); return; } else if (target == NULL) { id = pcmk__xe_id(patch); crm_err("Could not locate: %s.id=%s", name, id); return; } if (target->type == XML_COMMENT_NODE) { pcmk__xc_update(parent, target, patch); } CRM_CHECK(pcmk__xe_is(target, name), return); CRM_CHECK(pcmk__str_eq(pcmk__xe_id(target), pcmk__xe_id(patch), pcmk__str_none), return); for (xIter = pcmk__xe_first_attr(patch); xIter != NULL; xIter = xIter->next) { const char *p_name = (const char *) xIter->name; const char *p_value = pcmk__xml_attr_value(xIter); pcmk__xe_remove_attr(target, p_name); // Preserve patch order crm_xml_add(target, p_name, p_value); } // Changes to child objects for (patch_child = pcmk__xml_first_child(patch); patch_child != NULL; patch_child = pcmk__xml_next(patch_child)) { target_child = pcmk__xml_match(target, patch_child, false); process_v1_additions(target, target_child, patch_child); } } /*! * \internal * \brief Find additions or removals in a patch set * * \param[in] patchset XML of patch * \param[in] format Patch version * \param[in] added TRUE if looking for additions, FALSE if removals * \param[in,out] patch_node Will be set to node if found * * \return TRUE if format is valid, FALSE if invalid */ static bool find_patch_xml_node(const xmlNode *patchset, int format, bool added, xmlNode **patch_node) { xmlNode *cib_node; const char *label; switch (format) { case 1: // @COMPAT Remove when v1 patchsets are removed label = added? PCMK__XE_DIFF_ADDED : PCMK__XE_DIFF_REMOVED; *patch_node = pcmk__xe_first_child(patchset, label, NULL, NULL); cib_node = pcmk__xe_first_child(*patch_node, PCMK_XE_CIB, NULL, NULL); if (cib_node != NULL) { *patch_node = cib_node; } break; case 2: label = added? PCMK_XE_TARGET : PCMK_XE_SOURCE; *patch_node = pcmk__xe_first_child(patchset, PCMK_XE_VERSION, NULL, NULL); *patch_node = pcmk__xe_first_child(*patch_node, label, NULL, NULL); break; default: crm_warn("Unknown patch format: %d", format); *patch_node = NULL; return FALSE; } return TRUE; } // Get CIB versions used for additions and deletions in a patchset bool xml_patch_versions(const xmlNode *patchset, int add[3], int del[3]) { int lpc = 0; int format = 1; xmlNode *tmp = NULL; const char *vfields[] = { PCMK_XA_ADMIN_EPOCH, PCMK_XA_EPOCH, PCMK_XA_NUM_UPDATES, }; crm_element_value_int(patchset, PCMK_XA_FORMAT, &format); /* Process removals */ if (!find_patch_xml_node(patchset, format, FALSE, &tmp)) { return -EINVAL; } if (tmp != NULL) { for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { crm_element_value_int(tmp, vfields[lpc], &(del[lpc])); crm_trace("Got %d for del[%s]", del[lpc], vfields[lpc]); } } /* Process additions */ if (!find_patch_xml_node(patchset, format, TRUE, &tmp)) { return -EINVAL; } if (tmp != NULL) { for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { crm_element_value_int(tmp, vfields[lpc], &(add[lpc])); crm_trace("Got %d for add[%s]", add[lpc], vfields[lpc]); } } 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; } // @COMPAT Remove when v1 patchsets are removed static void purge_v1_diff_markers(xmlNode *node) { xmlNode *child = NULL; CRM_CHECK(node != NULL, return); pcmk__xe_remove_attr(node, PCMK__XA_CRM_DIFF_MARKER); for (child = pcmk__xml_first_child(node); child != NULL; child = pcmk__xml_next(child)) { purge_v1_diff_markers(child); } } // @COMPAT Remove when v1 patchsets are removed /*! * \internal * \brief Apply a version 1 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_v1_patchset(xmlNode *xml, const xmlNode *patchset) { int rc = pcmk_rc_ok; int root_nodes_seen = 0; xmlNode *child_diff = NULL; xmlNode *added = pcmk__xe_first_child(patchset, PCMK__XE_DIFF_ADDED, NULL, NULL); xmlNode *removed = pcmk__xe_first_child(patchset, PCMK__XE_DIFF_REMOVED, NULL, NULL); xmlNode *old = pcmk__xml_copy(NULL, xml); crm_trace("Subtraction Phase"); for (child_diff = pcmk__xml_first_child(removed); child_diff != NULL; child_diff = pcmk__xml_next(child_diff)) { CRM_CHECK(root_nodes_seen == 0, rc = FALSE); if (root_nodes_seen == 0) { process_v1_removals(xml, child_diff); } root_nodes_seen++; } if (root_nodes_seen > 1) { crm_err("(-) Diffs cannot contain more than one change set... saw %d", root_nodes_seen); rc = ENOTUNIQ; } root_nodes_seen = 0; crm_trace("Addition Phase"); if (rc == pcmk_rc_ok) { xmlNode *child_diff = NULL; for (child_diff = pcmk__xml_first_child(added); child_diff != NULL; child_diff = pcmk__xml_next(child_diff)) { CRM_CHECK(root_nodes_seen == 0, rc = FALSE); if (root_nodes_seen == 0) { process_v1_additions(NULL, xml, child_diff); } root_nodes_seen++; } } if (root_nodes_seen > 1) { crm_err("(+) Diffs cannot contain more than one change set... saw %d", root_nodes_seen); rc = ENOTUNIQ; } purge_v1_diff_markers(xml); // Purge prior to checking digest pcmk__xml_free(old); return rc; } // 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 get_xpath_object() * * \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: // @COMPAT Remove when v1 patchsets are removed 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 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 = xmlDocCopyNode(change->children, match->doc, 1); if (child == NULL) { return ENOMEM; } if (match_child) { crm_trace("Adding %s at position %d", child->name, position); xmlAddPrevSibling(match_child, child); } else if (match->last) { crm_trace("Adding %s at position %d (end)", child->name, position); xmlAddNextSibling(match->last, child); } else { crm_trace("Adding %s at position %d (first)", child->name, position); CRM_LOG_ASSERT(position == 0); xmlAddChild(match, child); } pcmk__xml_mark_created(child); } 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 } CRM_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 { CRM_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); switch (format) { case 1: // @COMPAT Remove when v1 patchsets are removed rc = pcmk_rc2legacy(apply_v1_patchset(xml, patchset)); break; case 2: rc = pcmk_rc2legacy(apply_v2_patchset(xml, patchset)); break; default: crm_err("Unknown patch format: %d", format); rc = -EINVAL; } } if ((rc == pcmk_ok) && (digest != NULL)) { char *new_digest = NULL; char *version = crm_element_value_copy(xml, PCMK_XA_CRM_FEATURE_SET); new_digest = calculate_xml_versioned_digest(xml, FALSE, TRUE, version); 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); free(version); } pcmk__xml_free(old); return rc; } // @COMPAT Remove when v1 patchsets are removed static bool can_prune_leaf_v1(xmlNode *node) { xmlNode *cIter = NULL; bool can_prune = true; CRM_CHECK(node != NULL, return false); /* @COMPAT PCMK__XE_ROLE_REF was deprecated in Pacemaker 1.1.12 (needed for * rolling upgrades) */ if (pcmk__strcase_any_of((const char *) node->name, PCMK_XE_RESOURCE_REF, PCMK_XE_OBJ_REF, PCMK_XE_ROLE, PCMK__XE_ROLE_REF, NULL)) { return false; } for (xmlAttrPtr a = pcmk__xe_first_attr(node); a != NULL; a = a->next) { const char *p_name = (const char *) a->name; if (strcmp(p_name, PCMK_XA_ID) == 0) { continue; } can_prune = false; } cIter = pcmk__xml_first_child(node); while (cIter) { xmlNode *child = cIter; cIter = pcmk__xml_next(cIter); if (can_prune_leaf_v1(child)) { pcmk__xml_free(child); } else { can_prune = false; } } return can_prune; } // @COMPAT Remove when v1 patchsets are removed xmlNode * pcmk__diff_v1_xml_object(xmlNode *old, xmlNode *new, bool suppress) { xmlNode *tmp1 = NULL; xmlNode *diff = pcmk__xe_create(NULL, PCMK_XE_DIFF); xmlNode *removed = pcmk__xe_create(diff, PCMK__XE_DIFF_REMOVED); xmlNode *added = pcmk__xe_create(diff, PCMK__XE_DIFF_ADDED); crm_xml_add(diff, PCMK_XA_CRM_FEATURE_SET, CRM_FEATURE_SET); tmp1 = subtract_v1_xml_object(removed, old, new, false, NULL, "removed:top"); if (suppress && (tmp1 != NULL) && can_prune_leaf_v1(tmp1)) { pcmk__xml_free(tmp1); } tmp1 = subtract_v1_xml_object(added, new, old, true, NULL, "added:top"); if (suppress && (tmp1 != NULL) && can_prune_leaf_v1(tmp1)) { pcmk__xml_free(tmp1); } if ((added->children == NULL) && (removed->children == NULL)) { pcmk__xml_free(diff); diff = NULL; } return diff; } - -// Deprecated functions kept only for backward API compatibility -// LCOV_EXCL_START - -#include - -gboolean -apply_xml_diff(xmlNode *old_xml, xmlNode *diff, xmlNode **new_xml) -{ - gboolean result = TRUE; - int root_nodes_seen = 0; - const char *digest = crm_element_value(diff, PCMK__XA_DIGEST); - const char *version = crm_element_value(diff, PCMK_XA_CRM_FEATURE_SET); - - xmlNode *child_diff = NULL; - xmlNode *added = pcmk__xe_first_child(diff, PCMK__XE_DIFF_ADDED, NULL, - NULL); - xmlNode *removed = pcmk__xe_first_child(diff, PCMK__XE_DIFF_REMOVED, NULL, - NULL); - - CRM_CHECK(new_xml != NULL, return FALSE); - - crm_trace("Subtraction Phase"); - for (child_diff = pcmk__xml_first_child(removed); child_diff != NULL; - child_diff = pcmk__xml_next(child_diff)) { - CRM_CHECK(root_nodes_seen == 0, result = FALSE); - if (root_nodes_seen == 0) { - *new_xml = subtract_v1_xml_object(NULL, old_xml, child_diff, false, - NULL, NULL); - } - root_nodes_seen++; - } - - if (root_nodes_seen == 0) { - *new_xml = pcmk__xml_copy(NULL, old_xml); - - } else if (root_nodes_seen > 1) { - crm_err("(-) Diffs cannot contain more than one change set... saw %d", - root_nodes_seen); - result = FALSE; - } - - root_nodes_seen = 0; - crm_trace("Addition Phase"); - if (result) { - xmlNode *child_diff = NULL; - - for (child_diff = pcmk__xml_first_child(added); child_diff != NULL; - child_diff = pcmk__xml_next(child_diff)) { - CRM_CHECK(root_nodes_seen == 0, result = FALSE); - if (root_nodes_seen == 0) { - pcmk__xml_update(NULL, *new_xml, child_diff, pcmk__xaf_none, - true); - } - root_nodes_seen++; - } - } - - if (root_nodes_seen > 1) { - crm_err("(+) Diffs cannot contain more than one change set... saw %d", - root_nodes_seen); - result = FALSE; - - } else if (result && (digest != NULL)) { - char *new_digest = NULL; - - purge_v1_diff_markers(*new_xml); // Purge now so diff is ok - new_digest = calculate_xml_versioned_digest(*new_xml, FALSE, TRUE, - version); - if (!pcmk__str_eq(new_digest, digest, pcmk__str_casei)) { - crm_info("Digest mis-match: expected %s, calculated %s", - digest, new_digest); - result = FALSE; - - pcmk__if_tracing( - { - save_xml_to_file(old_xml, "diff:original", NULL); - save_xml_to_file(diff, "diff:input", NULL); - save_xml_to_file(*new_xml, "diff:new", NULL); - }, - {} - ); - - } else { - crm_trace("Digest matched: expected %s, calculated %s", - digest, new_digest); - } - free(new_digest); - - } else if (result) { - purge_v1_diff_markers(*new_xml); // Purge now so diff is ok - } - - return result; -} - -void -purge_diff_markers(xmlNode *a_node) -{ - purge_v1_diff_markers(a_node); -} - -xmlNode * -diff_xml_object(xmlNode *old, xmlNode *new, gboolean suppress) -{ - return pcmk__diff_v1_xml_object(old, new, suppress); -} - -xmlNode * -subtract_xml_object(xmlNode *parent, xmlNode *left, xmlNode *right, - gboolean full, gboolean *changed, const char *marker) -{ - return subtract_v1_xml_object(parent, left, right, full, changed, marker); -} - -gboolean -can_prune_leaf(xmlNode *xml_node) -{ - return can_prune_leaf_v1(xml_node); -} - -// LCOV_EXCL_STOP -// End deprecated API diff --git a/lib/common/patchset_display.c b/lib/common/patchset_display.c index 1351c86071..0855c6e829 100644 --- a/lib/common/patchset_display.c +++ b/lib/common/patchset_display.c @@ -1,530 +1,452 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include "crmcommon_private.h" /*! * \internal * \brief Output an XML patchset header * * This function parses a header from an XML patchset (an \p XML_ATTR_DIFF * element and its children). * * All header lines contain three integers separated by dots, of the form * {0}.{1}.{2}: * * \p {0}: \c PCMK_XA_ADMIN_EPOCH * * \p {1}: \c PCMK_XA_EPOCH * * \p {2}: \c PCMK_XA_NUM_UPDATES * * Lines containing \p "---" describe removals and end with the patch format * number. Lines containing \p "+++" describe additions and end with the patch * digest. * * \param[in,out] out Output object * \param[in] patchset XML patchset to output * * \return Standard Pacemaker return code * * \note This function produces output only for text-like formats. */ static int xml_show_patchset_header(pcmk__output_t *out, const xmlNode *patchset) { int rc = pcmk_rc_no_output; int add[] = { 0, 0, 0 }; int del[] = { 0, 0, 0 }; xml_patch_versions(patchset, add, del); if ((add[0] != del[0]) || (add[1] != del[1]) || (add[2] != del[2])) { const char *fmt = crm_element_value(patchset, PCMK_XA_FORMAT); const char *digest = crm_element_value(patchset, PCMK__XA_DIGEST); out->info(out, "Diff: --- %d.%d.%d %s", del[0], del[1], del[2], fmt); rc = out->info(out, "Diff: +++ %d.%d.%d %s", add[0], add[1], add[2], digest); } else if ((add[0] != 0) || (add[1] != 0) || (add[2] != 0)) { rc = out->info(out, "Local-only Change: %d.%d.%d", add[0], add[1], add[2]); } return rc; } /*! * \internal * \brief Output a user-friendly form of XML additions or removals * * \param[in,out] out Output object * \param[in] prefix String to prepend to every line of output * \param[in] data XML node to output * \param[in] depth Current indentation level * \param[in] options Group of \p pcmk__xml_fmt_options flags * * \return Standard Pacemaker return code * * \note This function produces output only for text-like formats. */ static int xml_show_patchset_v1_recursive(pcmk__output_t *out, const char *prefix, const xmlNode *data, int depth, uint32_t options) { if ((data->children == NULL) || (crm_element_value(data, PCMK__XA_CRM_DIFF_MARKER) != NULL)) { // Found a change; clear the pcmk__xml_fmt_diff_short option if set options &= ~pcmk__xml_fmt_diff_short; if (pcmk_is_set(options, pcmk__xml_fmt_diff_plus)) { prefix = PCMK__XML_PREFIX_CREATED; } else { // pcmk_is_set(options, pcmk__xml_fmt_diff_minus) prefix = PCMK__XML_PREFIX_DELETED; } } if (pcmk_is_set(options, pcmk__xml_fmt_diff_short)) { int rc = pcmk_rc_no_output; // Keep looking for the actual change for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL; child = pcmk__xml_next(child)) { int temp_rc = xml_show_patchset_v1_recursive(out, prefix, child, depth + 1, options); rc = pcmk__output_select_rc(rc, temp_rc); } return rc; } return pcmk__xml_show(out, prefix, data, depth, options |pcmk__xml_fmt_open |pcmk__xml_fmt_children |pcmk__xml_fmt_close); } /*! * \internal * \brief Output a user-friendly form of an XML patchset (format 1) * * This function parses an XML patchset (an \p XML_ATTR_DIFF element and its * children) into a user-friendly combined diff output. * * \param[in,out] out Output object * \param[in] patchset XML patchset to output * \param[in] options Group of \p pcmk__xml_fmt_options flags * * \return Standard Pacemaker return code * * \note This function produces output only for text-like formats. */ static int xml_show_patchset_v1(pcmk__output_t *out, const xmlNode *patchset, uint32_t options) { const xmlNode *removed = NULL; const xmlNode *added = NULL; const xmlNode *child = NULL; bool is_first = true; int rc = xml_show_patchset_header(out, patchset); /* It's not clear whether "- " or "+ " ever does *not* get overridden by * PCMK__XML_PREFIX_DELETED or PCMK__XML_PREFIX_CREATED in practice. * However, v1 patchsets can only exist during rolling upgrades from * Pacemaker 1.1.11, so not worth worrying about. */ removed = pcmk__xe_first_child(patchset, PCMK__XE_DIFF_REMOVED, NULL, NULL); for (child = pcmk__xml_first_child(removed); child != NULL; child = pcmk__xml_next(child)) { int temp_rc = xml_show_patchset_v1_recursive(out, "- ", child, 0, options |pcmk__xml_fmt_diff_minus); rc = pcmk__output_select_rc(rc, temp_rc); if (is_first) { is_first = false; } else { rc = pcmk__output_select_rc(rc, out->info(out, " --- ")); } } is_first = true; added = pcmk__xe_first_child(patchset, PCMK__XE_DIFF_ADDED, NULL, NULL); for (child = pcmk__xml_first_child(added); child != NULL; child = pcmk__xml_next(child)) { int temp_rc = xml_show_patchset_v1_recursive(out, "+ ", child, 0, options |pcmk__xml_fmt_diff_plus); rc = pcmk__output_select_rc(rc, temp_rc); if (is_first) { is_first = false; } else { rc = pcmk__output_select_rc(rc, out->info(out, " +++ ")); } } return rc; } /*! * \internal * \brief Output a user-friendly form of an XML patchset (format 2) * * This function parses an XML patchset (an \p XML_ATTR_DIFF element and its * children) into a user-friendly combined diff output. * * \param[in,out] out Output object * \param[in] patchset XML patchset to output * * \return Standard Pacemaker return code * * \note This function produces output only for text-like formats. */ static int xml_show_patchset_v2(pcmk__output_t *out, const xmlNode *patchset) { int rc = xml_show_patchset_header(out, patchset); int temp_rc = pcmk_rc_no_output; for (const xmlNode *change = pcmk__xe_first_child(patchset, NULL, NULL, NULL); change != NULL; change = pcmk__xe_next(change)) { const char *op = crm_element_value(change, PCMK_XA_OPERATION); const char *xpath = crm_element_value(change, PCMK_XA_PATH); if (op == NULL) { continue; } if (strcmp(op, PCMK_VALUE_CREATE) == 0) { char *prefix = crm_strdup_printf(PCMK__XML_PREFIX_CREATED " %s: ", xpath); temp_rc = pcmk__xml_show(out, prefix, change->children, 0, pcmk__xml_fmt_pretty|pcmk__xml_fmt_open); rc = pcmk__output_select_rc(rc, temp_rc); // Overwrite all except the first two characters with spaces for (char *ch = prefix + 2; *ch != '\0'; ch++) { *ch = ' '; } temp_rc = pcmk__xml_show(out, prefix, change->children, 0, pcmk__xml_fmt_pretty |pcmk__xml_fmt_children |pcmk__xml_fmt_close); rc = pcmk__output_select_rc(rc, temp_rc); free(prefix); } else if (strcmp(op, PCMK_VALUE_MOVE) == 0) { const char *position = crm_element_value(change, PCMK_XE_POSITION); temp_rc = out->info(out, PCMK__XML_PREFIX_MOVED " %s moved to offset %s", xpath, position); rc = pcmk__output_select_rc(rc, temp_rc); } else if (strcmp(op, PCMK_VALUE_MODIFY) == 0) { xmlNode *clist = pcmk__xe_first_child(change, PCMK_XE_CHANGE_LIST, NULL, NULL); GString *buffer_set = NULL; GString *buffer_unset = NULL; for (const xmlNode *child = pcmk__xe_first_child(clist, NULL, NULL, NULL); child != NULL; child = pcmk__xe_next(child)) { const char *name = crm_element_value(child, PCMK_XA_NAME); op = crm_element_value(child, PCMK_XA_OPERATION); if (op == NULL) { continue; } if (strcmp(op, "set") == 0) { const char *value = crm_element_value(child, PCMK_XA_VALUE); pcmk__add_separated_word(&buffer_set, 256, "@", ", "); pcmk__g_strcat(buffer_set, name, "=", value, NULL); } else if (strcmp(op, "unset") == 0) { pcmk__add_separated_word(&buffer_unset, 256, "@", ", "); g_string_append(buffer_unset, name); } } if (buffer_set != NULL) { temp_rc = out->info(out, "+ %s: %s", xpath, buffer_set->str); rc = pcmk__output_select_rc(rc, temp_rc); g_string_free(buffer_set, TRUE); } if (buffer_unset != NULL) { temp_rc = out->info(out, "-- %s: %s", xpath, buffer_unset->str); rc = pcmk__output_select_rc(rc, temp_rc); g_string_free(buffer_unset, TRUE); } } else if (strcmp(op, PCMK_VALUE_DELETE) == 0) { int position = -1; crm_element_value_int(change, PCMK_XE_POSITION, &position); if (position >= 0) { temp_rc = out->info(out, "-- %s (%d)", xpath, position); } else { temp_rc = out->info(out, "-- %s", xpath); } rc = pcmk__output_select_rc(rc, temp_rc); } } return rc; } /*! * \internal * \brief Output a user-friendly form of an XML patchset * * This function parses an XML patchset (an \p XML_ATTR_DIFF element and its * children) into a user-friendly combined diff output. * * \param[in,out] out Output object * \param[in] args Message-specific arguments * * \return Standard Pacemaker return code * * \note \p args should contain the following: * -# XML patchset */ PCMK__OUTPUT_ARGS("xml-patchset", "const xmlNode *") static int xml_patchset_default(pcmk__output_t *out, va_list args) { const xmlNode *patchset = va_arg(args, const xmlNode *); int format = 1; if (patchset == NULL) { crm_trace("Empty patch"); return pcmk_rc_no_output; } crm_element_value_int(patchset, PCMK_XA_FORMAT, &format); switch (format) { case 1: return xml_show_patchset_v1(out, patchset, pcmk__xml_fmt_pretty); case 2: return xml_show_patchset_v2(out, patchset); default: crm_err("Unknown patch format: %d", format); return pcmk_rc_bad_xml_patch; } } /*! * \internal * \brief Output a user-friendly form of an XML patchset * * This function parses an XML patchset (an \p XML_ATTR_DIFF element and its * children) into a user-friendly combined diff output. * * \param[in,out] out Output object * \param[in] args Message-specific arguments * * \return Standard Pacemaker return code * * \note \p args should contain the following: * -# XML patchset */ PCMK__OUTPUT_ARGS("xml-patchset", "const xmlNode *") static int xml_patchset_log(pcmk__output_t *out, va_list args) { static struct qb_log_callsite *patchset_cs = NULL; const xmlNode *patchset = va_arg(args, const xmlNode *); uint8_t log_level = pcmk__output_get_log_level(out); int format = 1; if (log_level == LOG_NEVER) { return pcmk_rc_no_output; } if (patchset == NULL) { crm_trace("Empty patch"); return pcmk_rc_no_output; } if (patchset_cs == NULL) { patchset_cs = qb_log_callsite_get(__func__, __FILE__, "xml-patchset", log_level, __LINE__, crm_trace_nonlog); } if (!crm_is_callsite_active(patchset_cs, log_level, crm_trace_nonlog)) { // Nothing would be logged, so skip all the work return pcmk_rc_no_output; } crm_element_value_int(patchset, PCMK_XA_FORMAT, &format); switch (format) { case 1: if (log_level < LOG_DEBUG) { return xml_show_patchset_v1(out, patchset, pcmk__xml_fmt_pretty |pcmk__xml_fmt_diff_short); } return xml_show_patchset_v1(out, patchset, pcmk__xml_fmt_pretty); case 2: return xml_show_patchset_v2(out, patchset); default: crm_err("Unknown patch format: %d", format); return pcmk_rc_bad_xml_patch; } } /*! * \internal * \brief Output an XML patchset * * This function outputs an XML patchset (an \p XML_ATTR_DIFF element and its * children) without modification, as a CDATA block. * * \param[in,out] out Output object * \param[in] args Message-specific arguments * * \return Standard Pacemaker return code * * \note \p args should contain the following: * -# XML patchset */ PCMK__OUTPUT_ARGS("xml-patchset", "const xmlNode *") static int xml_patchset_xml(pcmk__output_t *out, va_list args) { const xmlNode *patchset = va_arg(args, const xmlNode *); if (patchset != NULL) { GString *buf = g_string_sized_new(1024); pcmk__xml_string(patchset, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, buf, 0); out->output_xml(out, PCMK_XE_XML_PATCHSET, buf->str); g_string_free(buf, TRUE); return pcmk_rc_ok; } crm_trace("Empty patch"); return pcmk_rc_no_output; } static pcmk__message_entry_t fmt_functions[] = { { "xml-patchset", "default", xml_patchset_default }, { "xml-patchset", "log", xml_patchset_log }, { "xml-patchset", "xml", xml_patchset_xml }, { NULL, NULL, NULL } }; /*! * \internal * \brief Register the formatting functions for XML patchsets * * \param[in,out] out Output object */ void pcmk__register_patchset_messages(pcmk__output_t *out) { pcmk__register_messages(out, fmt_functions); } - -// Deprecated functions kept only for backward API compatibility -// LCOV_EXCL_START - -#include - -void -xml_log_patchset(uint8_t log_level, const char *function, - const xmlNode *patchset) -{ - /* This function has some duplication relative to the message functions. - * This way, we can maintain the const xmlNode * in the signature. The - * message functions must be non-const. They have to support XML output - * objects, which must make a copy of a the patchset, requiring a non-const - * function call. - * - * In contrast, this legacy function doesn't need to support XML output. - */ - static struct qb_log_callsite *patchset_cs = NULL; - - pcmk__output_t *out = NULL; - int format = 1; - int rc = pcmk_rc_no_output; - - switch (log_level) { - case LOG_NEVER: - return; - case LOG_STDOUT: - CRM_CHECK(pcmk__text_output_new(&out, NULL) == pcmk_rc_ok, return); - break; - default: - if (patchset_cs == NULL) { - patchset_cs = qb_log_callsite_get(__func__, __FILE__, - "xml-patchset", log_level, - __LINE__, crm_trace_nonlog); - } - if (!crm_is_callsite_active(patchset_cs, log_level, - crm_trace_nonlog)) { - return; - } - CRM_CHECK(pcmk__log_output_new(&out) == pcmk_rc_ok, return); - pcmk__output_set_log_level(out, log_level); - break; - } - - if (patchset == NULL) { - // Should come after the LOG_NEVER check - crm_trace("Empty patch"); - goto done; - } - - crm_element_value_int(patchset, PCMK_XA_FORMAT, &format); - switch (format) { - case 1: - if (log_level < LOG_DEBUG) { - rc = xml_show_patchset_v1(out, patchset, - pcmk__xml_fmt_pretty - |pcmk__xml_fmt_diff_short); - } else { // Note: LOG_STDOUT > LOG_DEBUG - rc = xml_show_patchset_v1(out, patchset, pcmk__xml_fmt_pretty); - } - break; - case 2: - rc = xml_show_patchset_v2(out, patchset); - break; - default: - crm_err("Unknown patch format: %d", format); - rc = pcmk_rc_bad_xml_patch; - break; - } - -done: - out->finish(out, pcmk_rc2exitc(rc), true, NULL); - pcmk__output_free(out); -} - -// LCOV_EXCL_STOP -// End deprecated API diff --git a/lib/common/schemas.c b/lib/common/schemas.c index 65345c46b3..a123085048 100644 --- a/lib/common/schemas.c +++ b/lib/common/schemas.c @@ -1,1744 +1,1621 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* PCMK__XML_LOG_BASE */ #include "crmcommon_private.h" #define SCHEMA_ZERO { .v = { 0, 0 } } #define schema_strdup_printf(prefix, version, suffix) \ crm_strdup_printf(prefix "%u.%u" suffix, (version).v[0], (version).v[1]) typedef struct { xmlRelaxNGPtr rng; xmlRelaxNGValidCtxtPtr valid; xmlRelaxNGParserCtxtPtr parser; } relaxng_ctx_cache_t; static GList *known_schemas = NULL; static bool initialized = false; static bool silent_logging = FALSE; static void G_GNUC_PRINTF(2, 3) xml_log(int priority, const char *fmt, ...) { va_list ap; va_start(ap, fmt); if (silent_logging == FALSE) { /* XXX should not this enable dechunking as well? */ PCMK__XML_LOG_BASE(priority, FALSE, 0, NULL, fmt, ap); } va_end(ap); } static int xml_latest_schema_index(void) { /* This function assumes that pcmk__schema_init() has been called * beforehand, so we have at least three schemas (one real schema, the * "pacemaker-next" schema, and the "none" schema). * * @COMPAT: pacemaker-next is deprecated since 2.1.5 and none since 2.1.8. * Update this when we drop those. */ return g_list_length(known_schemas) - 3; } /*! * \internal * \brief Return the schema entry of the highest-versioned schema * * \return Schema entry of highest-versioned schema (or NULL on error) */ static GList * get_highest_schema(void) { /* The highest numerically versioned schema is the one before pacemaker-next * * @COMPAT pacemaker-next is deprecated since 2.1.5 */ GList *entry = pcmk__get_schema("pacemaker-next"); CRM_ASSERT((entry != NULL) && (entry->prev != NULL)); return entry->prev; } /*! * \internal * \brief Return the name of the highest-versioned schema * * \return Name of highest-versioned schema (or NULL on error) */ const char * pcmk__highest_schema_name(void) { GList *entry = get_highest_schema(); return ((pcmk__schema_t *)(entry->data))->name; } /*! * \internal * \brief Find first entry of highest major schema version series * * \return Schema entry of first schema with highest major version */ GList * pcmk__find_x_0_schema(void) { #if defined(PCMK__UNIT_TESTING) /* If we're unit testing, this can't be static because it'll stick * around from one test run to the next. It needs to be cleared out * every time. */ GList *x_0_entry = NULL; #else static GList *x_0_entry = NULL; #endif pcmk__schema_t *highest_schema = NULL; if (x_0_entry != NULL) { return x_0_entry; } x_0_entry = get_highest_schema(); highest_schema = x_0_entry->data; for (GList *iter = x_0_entry->prev; iter != NULL; iter = iter->prev) { pcmk__schema_t *schema = iter->data; /* We've found a schema in an older major version series. Return * the index of the first one in the same major version series as * the highest schema. */ if (schema->version.v[0] < highest_schema->version.v[0]) { x_0_entry = iter->next; break; } /* We're out of list to examine. This probably means there was only * one major version series, so return the first schema entry. */ if (iter->prev == NULL) { x_0_entry = known_schemas->data; break; } } return x_0_entry; } static inline bool version_from_filename(const char *filename, pcmk__schema_version_t *version) { if (pcmk__ends_with(filename, ".rng")) { return sscanf(filename, "pacemaker-%hhu.%hhu.rng", &(version->v[0]), &(version->v[1])) == 2; } else { return sscanf(filename, "pacemaker-%hhu.%hhu", &(version->v[0]), &(version->v[1])) == 2; } } static int schema_filter(const struct dirent *a) { int rc = 0; pcmk__schema_version_t version = SCHEMA_ZERO; if (strstr(a->d_name, "pacemaker-") != a->d_name) { /* crm_trace("%s - wrong prefix", a->d_name); */ } else if (!pcmk__ends_with_ext(a->d_name, ".rng")) { /* crm_trace("%s - wrong suffix", a->d_name); */ } else if (!version_from_filename(a->d_name, &version)) { /* crm_trace("%s - wrong format", a->d_name); */ } else { /* crm_debug("%s - candidate", a->d_name); */ rc = 1; } return rc; } static int schema_cmp(pcmk__schema_version_t a_version, pcmk__schema_version_t b_version) { for (int i = 0; i < 2; ++i) { if (a_version.v[i] < b_version.v[i]) { return -1; } else if (a_version.v[i] > b_version.v[i]) { return 1; } } return 0; } static int schema_cmp_directory(const struct dirent **a, const struct dirent **b) { pcmk__schema_version_t a_version = SCHEMA_ZERO; pcmk__schema_version_t b_version = SCHEMA_ZERO; if (!version_from_filename(a[0]->d_name, &a_version) || !version_from_filename(b[0]->d_name, &b_version)) { // Shouldn't be possible, but makes static analysis happy return 0; } return schema_cmp(a_version, b_version); } /*! * \internal * \brief Add given schema + auxiliary data to internal bookkeeping. * * \note When providing \p version, should not be called directly but * through \c add_schema_by_version. */ static void add_schema(enum pcmk__schema_validator validator, const pcmk__schema_version_t *version, const char *name, const char *transform, const char *transform_enter, bool transform_onleave) { pcmk__schema_t *schema = NULL; schema = pcmk__assert_alloc(1, sizeof(pcmk__schema_t)); schema->validator = validator; schema->version.v[0] = version->v[0]; schema->version.v[1] = version->v[1]; schema->transform_onleave = transform_onleave; // schema->schema_index is set after all schemas are loaded and sorted if (version->v[0] || version->v[1]) { schema->name = schema_strdup_printf("pacemaker-", *version, ""); } else { schema->name = pcmk__str_copy(name); } if (transform) { schema->transform = pcmk__str_copy(transform); } if (transform_enter) { schema->transform_enter = pcmk__str_copy(transform_enter); } known_schemas = g_list_prepend(known_schemas, schema); } /*! * \internal * \brief Add version-specified schema + auxiliary data to internal bookkeeping. * \return Standard Pacemaker return value (the only possible values are * \c ENOENT when no upgrade schema is associated, or \c pcmk_rc_ok otherwise. * * \note There's no reliance on the particular order of schemas entering here. * * \par A bit of theory * We track 3 XSLT stylesheets that differ per usage: * - "upgrade": * . sparsely spread over the sequence of all available schemas, * as they are only relevant when major version of the schema * is getting bumped -- in that case, it MUST be set * . name convention: upgrade-X.Y.xsl * - "upgrade-enter": * . may only accompany "upgrade" occurrence, but doesn't need to * be present anytime such one is, i.e., it MAY not be set when * "upgrade" is * . name convention: upgrade-X.Y-enter.xsl, * when not present: upgrade-enter.xsl * - "upgrade-leave": * . like "upgrade-enter", but SHOULD be present whenever * "upgrade-enter" is (and vice versa, but that's only * to prevent confusion based on observing the files, * it would get ignored regardless) * . name convention: (see "upgrade-enter") */ static int add_schema_by_version(const pcmk__schema_version_t *version, bool transform_expected) { bool transform_onleave = FALSE; int rc = pcmk_rc_ok; struct stat s; char *xslt = NULL, *transform_upgrade = NULL, *transform_enter = NULL; /* prologue for further transform_expected handling */ if (transform_expected) { /* check if there's suitable "upgrade" stylesheet */ transform_upgrade = schema_strdup_printf("upgrade-", *version, ); xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, transform_upgrade); } if (!transform_expected) { /* jump directly to the end */ } else if (stat(xslt, &s) == 0) { /* perhaps there's also a targeted "upgrade-enter" stylesheet */ transform_enter = schema_strdup_printf("upgrade-", *version, "-enter"); free(xslt); xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, transform_enter); if (stat(xslt, &s) != 0) { /* or initially, at least a generic one */ crm_debug("Upgrade-enter transform %s.xsl not found", xslt); free(xslt); free(transform_enter); transform_enter = strdup("upgrade-enter"); xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, transform_enter); if (stat(xslt, &s) != 0) { crm_debug("Upgrade-enter transform %s.xsl not found, either", xslt); free(xslt); xslt = NULL; } } /* xslt contains full path to "upgrade-enter" stylesheet */ if (xslt != NULL) { /* then there should be "upgrade-leave" counterpart (enter->leave) */ memcpy(strrchr(xslt, '-') + 1, "leave", sizeof("leave") - 1); transform_onleave = (stat(xslt, &s) == 0); free(xslt); } else { free(transform_enter); transform_enter = NULL; } } else { crm_err("Upgrade transform %s not found", xslt); free(xslt); free(transform_upgrade); transform_upgrade = NULL; rc = ENOENT; } add_schema(pcmk__schema_validator_rng, version, NULL, transform_upgrade, transform_enter, transform_onleave); free(transform_upgrade); free(transform_enter); return rc; } static void wrap_libxslt(bool finalize) { static xsltSecurityPrefsPtr secprefs; int ret = 0; /* security framework preferences */ if (!finalize) { CRM_ASSERT(secprefs == NULL); secprefs = xsltNewSecurityPrefs(); ret = xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_WRITE_FILE, xsltSecurityForbid) | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_CREATE_DIRECTORY, xsltSecurityForbid) | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_READ_NETWORK, xsltSecurityForbid) | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_WRITE_NETWORK, xsltSecurityForbid); if (ret != 0) { return; } } else { xsltFreeSecurityPrefs(secprefs); secprefs = NULL; } /* cleanup only */ if (finalize) { xsltCleanupGlobals(); } } void pcmk__load_schemas_from_dir(const char *dir) { int lpc, max; struct dirent **namelist = NULL; max = scandir(dir, &namelist, schema_filter, schema_cmp_directory); if (max < 0) { crm_warn("Could not load schemas from %s: %s", dir, strerror(errno)); return; } for (lpc = 0; lpc < max; lpc++) { bool transform_expected = false; pcmk__schema_version_t version = SCHEMA_ZERO; if (!version_from_filename(namelist[lpc]->d_name, &version)) { // Shouldn't be possible, but makes static analysis happy crm_warn("Skipping schema '%s': could not parse version", namelist[lpc]->d_name); continue; } if ((lpc + 1) < max) { pcmk__schema_version_t next_version = SCHEMA_ZERO; if (version_from_filename(namelist[lpc+1]->d_name, &next_version) && (version.v[0] < next_version.v[0])) { transform_expected = true; } } if (add_schema_by_version(&version, transform_expected) != pcmk_rc_ok) { break; } } for (lpc = 0; lpc < max; lpc++) { free(namelist[lpc]); } free(namelist); } static gint schema_sort_GCompareFunc(gconstpointer a, gconstpointer b) { const pcmk__schema_t *schema_a = a; const pcmk__schema_t *schema_b = b; // @COMPAT pacemaker-next is deprecated since 2.1.5 and none since 2.1.8 if (pcmk__str_eq(schema_a->name, "pacemaker-next", pcmk__str_none)) { if (pcmk__str_eq(schema_b->name, PCMK_VALUE_NONE, pcmk__str_none)) { return -1; } else { return 1; } } else if (pcmk__str_eq(schema_a->name, PCMK_VALUE_NONE, pcmk__str_none)) { return 1; } else if (pcmk__str_eq(schema_b->name, "pacemaker-next", pcmk__str_none)) { return -1; } else { return schema_cmp(schema_a->version, schema_b->version); } } /*! * \internal * \brief Sort the list of known schemas such that all pacemaker-X.Y are in * version order, then pacemaker-next, then none * * This function should be called whenever additional schemas are loaded using * \c pcmk__load_schemas_from_dir(), after the initial sets in * \c pcmk__schema_init(). */ void pcmk__sort_schemas(void) { known_schemas = g_list_sort(known_schemas, schema_sort_GCompareFunc); } /*! * \internal * \brief Load pacemaker schemas into cache * * \note This currently also serves as an entry point for the * generic initialization of the libxslt library. */ void pcmk__schema_init(void) { if (!initialized) { const char *remote_schema_dir = pcmk__remote_schema_dir(); char *base = pcmk__xml_artefact_root(pcmk__xml_artefact_ns_legacy_rng); const pcmk__schema_version_t zero = SCHEMA_ZERO; int schema_index = 0; initialized = true; wrap_libxslt(false); pcmk__load_schemas_from_dir(base); pcmk__load_schemas_from_dir(remote_schema_dir); free(base); // @COMPAT: Deprecated since 2.1.5 add_schema(pcmk__schema_validator_rng, &zero, "pacemaker-next", NULL, NULL, FALSE); // @COMPAT Deprecated since 2.1.8 add_schema(pcmk__schema_validator_none, &zero, PCMK_VALUE_NONE, NULL, NULL, FALSE); /* add_schema() prepends items to the list, so in the simple case, this * just reverses the list. However if there were any remote schemas, * sorting is necessary. */ pcmk__sort_schemas(); // Now set the schema indexes and log the final result for (GList *iter = known_schemas; iter != NULL; iter = iter->next) { pcmk__schema_t *schema = iter->data; if (schema->transform == NULL) { crm_debug("Loaded schema %d: %s", schema_index, schema->name); } else { crm_debug("Loaded schema %d: %s (upgrades with %s.xsl)", schema_index, schema->name, schema->transform); } schema->schema_index = schema_index++; } } } static bool validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler, void *error_handler_context, const char *relaxng_file, relaxng_ctx_cache_t **cached_ctx) { int rc = 0; bool valid = true; relaxng_ctx_cache_t *ctx = NULL; CRM_CHECK(doc != NULL, return false); CRM_CHECK(relaxng_file != NULL, return false); if (cached_ctx && *cached_ctx) { ctx = *cached_ctx; } else { crm_debug("Creating RNG parser context"); ctx = pcmk__assert_alloc(1, sizeof(relaxng_ctx_cache_t)); ctx->parser = xmlRelaxNGNewParserCtxt(relaxng_file); CRM_CHECK(ctx->parser != NULL, goto cleanup); if (error_handler) { xmlRelaxNGSetParserErrors(ctx->parser, (xmlRelaxNGValidityErrorFunc) error_handler, (xmlRelaxNGValidityWarningFunc) error_handler, error_handler_context); } else { xmlRelaxNGSetParserErrors(ctx->parser, (xmlRelaxNGValidityErrorFunc) fprintf, (xmlRelaxNGValidityWarningFunc) fprintf, stderr); } ctx->rng = xmlRelaxNGParse(ctx->parser); CRM_CHECK(ctx->rng != NULL, crm_err("Could not find/parse %s", relaxng_file); goto cleanup); ctx->valid = xmlRelaxNGNewValidCtxt(ctx->rng); CRM_CHECK(ctx->valid != NULL, goto cleanup); if (error_handler) { xmlRelaxNGSetValidErrors(ctx->valid, (xmlRelaxNGValidityErrorFunc) error_handler, (xmlRelaxNGValidityWarningFunc) error_handler, error_handler_context); } else { xmlRelaxNGSetValidErrors(ctx->valid, (xmlRelaxNGValidityErrorFunc) fprintf, (xmlRelaxNGValidityWarningFunc) fprintf, stderr); } } rc = xmlRelaxNGValidateDoc(ctx->valid, doc); if (rc > 0) { valid = false; } else if (rc < 0) { crm_err("Internal libxml error during validation"); } cleanup: if (cached_ctx) { *cached_ctx = ctx; } else { if (ctx->parser != NULL) { xmlRelaxNGFreeParserCtxt(ctx->parser); } if (ctx->valid != NULL) { xmlRelaxNGFreeValidCtxt(ctx->valid); } if (ctx->rng != NULL) { xmlRelaxNGFree(ctx->rng); } free(ctx); } return valid; } static void free_schema(gpointer data) { pcmk__schema_t *schema = data; relaxng_ctx_cache_t *ctx = NULL; switch (schema->validator) { case pcmk__schema_validator_none: // not cached break; case pcmk__schema_validator_rng: // cached ctx = (relaxng_ctx_cache_t *) schema->cache; if (ctx == NULL) { break; } if (ctx->parser != NULL) { xmlRelaxNGFreeParserCtxt(ctx->parser); } if (ctx->valid != NULL) { xmlRelaxNGFreeValidCtxt(ctx->valid); } if (ctx->rng != NULL) { xmlRelaxNGFree(ctx->rng); } free(ctx); schema->cache = NULL; break; } free(schema->name); free(schema->transform); free(schema->transform_enter); free(schema); } /*! * \internal * \brief Clean up global memory associated with XML schemas */ void pcmk__schema_cleanup(void) { if (known_schemas != NULL) { g_list_free_full(known_schemas, free_schema); known_schemas = NULL; } initialized = false; wrap_libxslt(true); } /*! * \internal * \brief Get schema list entry corresponding to a schema name * * \param[in] name Name of schema to get * * \return Schema list entry corresponding to \p name, or NULL if unknown */ GList * pcmk__get_schema(const char *name) { // @COMPAT Not specifying a schema name is deprecated since 2.1.8 if (name == NULL) { name = PCMK_VALUE_NONE; } for (GList *iter = known_schemas; iter != NULL; iter = iter->next) { pcmk__schema_t *schema = iter->data; if (pcmk__str_eq(name, schema->name, pcmk__str_casei)) { return iter; } } return NULL; } /*! * \internal * \brief Compare two schema version numbers given the schema names * * \param[in] schema1 Name of first schema to compare * \param[in] schema2 Name of second schema to compare * * \return Standard comparison result (negative integer if \p schema1 has the * lower version number, positive integer if \p schema1 has the higher * version number, of 0 if the version numbers are equal) */ int pcmk__cmp_schemas_by_name(const char *schema1_name, const char *schema2_name) { GList *entry1 = pcmk__get_schema(schema1_name); GList *entry2 = pcmk__get_schema(schema2_name); if (entry1 == NULL) { return (entry2 == NULL)? 0 : -1; } else if (entry2 == NULL) { return 1; } else { pcmk__schema_t *schema1 = entry1->data; pcmk__schema_t *schema2 = entry2->data; return schema1->schema_index - schema2->schema_index; } } static bool validate_with(xmlNode *xml, pcmk__schema_t *schema, xmlRelaxNGValidityErrorFunc error_handler, void *error_handler_context) { bool valid = false; char *file = NULL; relaxng_ctx_cache_t **cache = NULL; if (schema == NULL) { return false; } if (schema->validator == pcmk__schema_validator_none) { return true; } file = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng, schema->name); crm_trace("Validating with %s (type=%d)", pcmk__s(file, "missing schema"), schema->validator); switch (schema->validator) { case pcmk__schema_validator_rng: cache = (relaxng_ctx_cache_t **) &(schema->cache); valid = validate_with_relaxng(xml->doc, error_handler, error_handler_context, file, cache); break; default: crm_err("Unknown validator type: %d", schema->validator); break; } free(file); return valid; } static bool validate_with_silent(xmlNode *xml, pcmk__schema_t *schema) { bool rc, sl_backup = silent_logging; silent_logging = TRUE; rc = validate_with(xml, schema, (xmlRelaxNGValidityErrorFunc) xml_log, GUINT_TO_POINTER(LOG_ERR)); silent_logging = sl_backup; return rc; } bool pcmk__validate_xml(xmlNode *xml_blob, const char *validation, xmlRelaxNGValidityErrorFunc error_handler, void *error_handler_context) { GList *entry = NULL; pcmk__schema_t *schema = NULL; CRM_CHECK((xml_blob != NULL) && (xml_blob->doc != NULL), return false); if (validation == NULL) { validation = crm_element_value(xml_blob, PCMK_XA_VALIDATE_WITH); } pcmk__warn_if_schema_deprecated(validation); // @COMPAT Not specifying a schema name is deprecated since 2.1.8 if (validation == NULL) { bool valid = false; for (entry = known_schemas; entry != NULL; entry = entry->next) { schema = entry->data; if (validate_with(xml_blob, schema, NULL, NULL)) { valid = true; crm_xml_add(xml_blob, PCMK_XA_VALIDATE_WITH, schema->name); crm_info("XML validated against %s", schema->name); } } return valid; } entry = pcmk__get_schema(validation); if (entry == NULL) { pcmk__config_err("Cannot validate CIB with " PCMK_XA_VALIDATE_WITH " set to an unknown schema such as '%s' (manually" " edit to use a known schema)", validation); return false; } schema = entry->data; return validate_with(xml_blob, schema, error_handler, error_handler_context); } /*! * \internal * \brief Validate XML using its configured schema (and send errors to logs) * * \param[in] xml XML to validate * * \return true if XML validates, otherwise false */ bool pcmk__configured_schema_validates(xmlNode *xml) { return pcmk__validate_xml(xml, NULL, (xmlRelaxNGValidityErrorFunc) xml_log, GUINT_TO_POINTER(LOG_ERR)); } /* With this arrangement, an attempt to identify the message severity as explicitly signalled directly from XSLT is performed in rather a smart way (no reliance on formatting string + arguments being always specified as ["%s", purposeful_string], as it can also be ["%s: %s", some_prefix, purposeful_string] etc. so every argument pertaining %s specifier is investigated), and if such a mark found, the respective level is determined and, when the messages are to go to the native logs, the mark itself gets dropped (by the means of string shift). NOTE: whether the native logging is the right sink is decided per the ctx parameter -- NULL denotes this case, otherwise it carries a pointer to the numeric expression of the desired target logging level (messages with higher level will be suppressed) NOTE: on some architectures, this string shift may not have any effect, but that's an acceptable tradeoff The logging level for not explicitly designated messages (suspicious, likely internal errors or some runaways) is LOG_WARNING. */ static void G_GNUC_PRINTF(2, 3) cib_upgrade_err(void *ctx, const char *fmt, ...) { va_list ap, aq; char *arg_cur; bool found = FALSE; const char *fmt_iter = fmt; uint8_t msg_log_level = LOG_WARNING; /* default for runaway messages */ const unsigned * log_level = (const unsigned *) ctx; enum { escan_seennothing, escan_seenpercent, } scan_state = escan_seennothing; va_start(ap, fmt); va_copy(aq, ap); while (!found && *fmt_iter != '\0') { /* while casing schema borrowed from libqb:qb_vsnprintf_serialize */ switch (*fmt_iter++) { case '%': if (scan_state == escan_seennothing) { scan_state = escan_seenpercent; } else if (scan_state == escan_seenpercent) { scan_state = escan_seennothing; } break; case 's': if (scan_state == escan_seenpercent) { scan_state = escan_seennothing; arg_cur = va_arg(aq, char *); if (arg_cur != NULL) { switch (arg_cur[0]) { case 'W': if (!strncmp(arg_cur, "WARNING: ", sizeof("WARNING: ") - 1)) { msg_log_level = LOG_WARNING; } if (ctx == NULL) { memmove(arg_cur, arg_cur + sizeof("WARNING: ") - 1, strlen(arg_cur + sizeof("WARNING: ") - 1) + 1); } found = TRUE; break; case 'I': if (!strncmp(arg_cur, "INFO: ", sizeof("INFO: ") - 1)) { msg_log_level = LOG_INFO; } if (ctx == NULL) { memmove(arg_cur, arg_cur + sizeof("INFO: ") - 1, strlen(arg_cur + sizeof("INFO: ") - 1) + 1); } found = TRUE; break; case 'D': if (!strncmp(arg_cur, "DEBUG: ", sizeof("DEBUG: ") - 1)) { msg_log_level = LOG_DEBUG; } if (ctx == NULL) { memmove(arg_cur, arg_cur + sizeof("DEBUG: ") - 1, strlen(arg_cur + sizeof("DEBUG: ") - 1) + 1); } found = TRUE; break; } } } break; case '#': case '-': case ' ': case '+': case '\'': case 'I': case '.': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '*': break; case 'l': case 'z': case 't': case 'j': case 'd': case 'i': case 'o': case 'u': case 'x': case 'X': case 'e': case 'E': case 'f': case 'F': case 'g': case 'G': case 'a': case 'A': case 'c': case 'p': if (scan_state == escan_seenpercent) { (void) va_arg(aq, void *); /* skip forward */ scan_state = escan_seennothing; } break; default: scan_state = escan_seennothing; break; } } if (log_level != NULL) { /* intention of the following offset is: cibadmin -V -> start showing INFO labelled messages */ if (*log_level + 4 >= msg_log_level) { vfprintf(stderr, fmt, ap); } } else { PCMK__XML_LOG_BASE(msg_log_level, TRUE, 0, "CIB upgrade: ", fmt, ap); } va_end(aq); va_end(ap); } /*! * \internal * \brief Apply a single XSL transformation to given XML * * \param[in] xml XML to transform * \param[in] transform XSL name * \param[in] to_logs If false, certain validation errors will be sent to * stderr rather than logged * * \return Transformed XML on success, otherwise NULL */ static xmlNode * apply_transformation(const xmlNode *xml, const char *transform, gboolean to_logs) { char *xform = NULL; xmlNode *out = NULL; xmlDocPtr res = NULL; xsltStylesheet *xslt = NULL; xform = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, transform); /* for capturing, e.g., what's emitted via */ if (to_logs) { xsltSetGenericErrorFunc(NULL, cib_upgrade_err); } else { xsltSetGenericErrorFunc(&crm_log_level, cib_upgrade_err); } xslt = xsltParseStylesheetFile((pcmkXmlStr) xform); CRM_CHECK(xslt != NULL, goto cleanup); res = xsltApplyStylesheet(xslt, xml->doc, NULL); CRM_CHECK(res != NULL, goto cleanup); xsltSetGenericErrorFunc(NULL, NULL); /* restore default one */ out = xmlDocGetRootElement(res); cleanup: if (xslt) { xsltFreeStylesheet(xslt); } free(xform); return out; } /*! * \internal * \brief Perform all transformations needed to upgrade XML to next schema * * A schema upgrade can require up to three XSL transformations: an "enter" * transform, the main upgrade transform, and a "leave" transform. Perform * all needed transforms to upgrade given XML to the next schema. * * \param[in] original_xml XML to transform * \param[in] schema_index Index of schema that successfully validates * \p original_xml * \param[in] to_logs If false, certain validation errors will be sent to * stderr rather than logged * * \return XML result of schema transforms if successful, otherwise NULL */ static xmlNode * apply_upgrade(const xmlNode *original_xml, int schema_index, gboolean to_logs) { pcmk__schema_t *schema = g_list_nth_data(known_schemas, schema_index); pcmk__schema_t *upgraded_schema = g_list_nth_data(known_schemas, schema_index + 1); bool transform_onleave = false; char *transform_leave; const xmlNode *xml = original_xml; xmlNode *upgrade = NULL; xmlNode *final = NULL; xmlRelaxNGValidityErrorFunc error_handler = NULL; CRM_ASSERT((schema != NULL) && (upgraded_schema != NULL)); if (to_logs) { error_handler = (xmlRelaxNGValidityErrorFunc) xml_log; } transform_onleave = schema->transform_onleave; if (schema->transform_enter != NULL) { crm_debug("Upgrading schema from %s to %s: " "applying pre-upgrade XSL transform %s", schema->name, upgraded_schema->name, schema->transform_enter); upgrade = apply_transformation(xml, schema->transform_enter, to_logs); if (upgrade == NULL) { crm_warn("Pre-upgrade XSL transform %s failed, " "will skip post-upgrade transform", schema->transform_enter); transform_onleave = FALSE; } else { xml = upgrade; } } crm_debug("Upgrading schema from %s to %s: " "applying upgrade XSL transform %s", schema->name, upgraded_schema->name, schema->transform); final = apply_transformation(xml, schema->transform, to_logs); if (upgrade != xml) { pcmk__xml_free(upgrade); upgrade = NULL; } if ((final != NULL) && transform_onleave) { upgrade = final; /* following condition ensured in add_schema_by_version */ CRM_ASSERT(schema->transform_enter != NULL); transform_leave = strdup(schema->transform_enter); /* enter -> leave */ memcpy(strrchr(transform_leave, '-') + 1, "leave", sizeof("leave") - 1); crm_debug("Upgrading schema from %s to %s: " "applying post-upgrade XSL transform %s", schema->name, upgraded_schema->name, transform_leave); final = apply_transformation(upgrade, transform_leave, to_logs); if (final == NULL) { crm_warn("Ignoring failure of post-upgrade XSL transform %s", transform_leave); final = upgrade; } else { pcmk__xml_free(upgrade); } free(transform_leave); } if (final == NULL) { return NULL; } // Ensure result validates with its new schema if (!validate_with(final, upgraded_schema, error_handler, GUINT_TO_POINTER(LOG_ERR))) { crm_err("Schema upgrade from %s to %s failed: " "XSL transform %s produced an invalid configuration", schema->name, upgraded_schema->name, schema->transform); crm_log_xml_debug(final, "bad-transform-result"); pcmk__xml_free(final); return NULL; } crm_info("Schema upgrade from %s to %s succeeded", schema->name, upgraded_schema->name); return final; } /*! * \internal * \brief Get the schema list entry corresponding to XML configuration * * \param[in] xml CIB XML to check * * \return List entry of schema configured in \p xml */ static GList * get_configured_schema(const xmlNode *xml) { const char *schema_name = crm_element_value(xml, PCMK_XA_VALIDATE_WITH); pcmk__warn_if_schema_deprecated(schema_name); if (schema_name == NULL) { return NULL; } return pcmk__get_schema(schema_name); } /*! * \brief Update CIB XML to latest schema that validates it * * \param[in,out] xml XML to update (may be freed and replaced * after being transformed) * \param[in] max_schema_name If not NULL, do not update \p xml to any * schema later than this one * \param[in] transform If false, do not update \p xml to any schema * that requires an XSL transform * \param[in] to_logs If false, certain validation errors will be * sent to stderr rather than logged * * \return Standard Pacemaker return code */ int pcmk__update_schema(xmlNode **xml, const char *max_schema_name, bool transform, bool to_logs) { int max_stable_schemas = xml_latest_schema_index(); int max_schema_index = 0; int rc = pcmk_rc_ok; GList *entry = NULL; pcmk__schema_t *best_schema = NULL; pcmk__schema_t *original_schema = NULL; xmlRelaxNGValidityErrorFunc error_handler = to_logs ? (xmlRelaxNGValidityErrorFunc) xml_log : NULL; CRM_CHECK((xml != NULL) && (*xml != NULL) && ((*xml)->doc != NULL), return EINVAL); if (max_schema_name != NULL) { GList *max_entry = pcmk__get_schema(max_schema_name); if (max_entry != NULL) { pcmk__schema_t *max_schema = max_entry->data; max_schema_index = max_schema->schema_index; } } if ((max_schema_index < 1) || (max_schema_index > max_stable_schemas)) { max_schema_index = max_stable_schemas; } entry = get_configured_schema(*xml); if (entry == NULL) { // @COMPAT Not specifying a schema name is deprecated since 2.1.8 entry = known_schemas; } else { original_schema = entry->data; if (original_schema->schema_index >= max_schema_index) { return pcmk_rc_ok; } } for (; entry != NULL; entry = entry->next) { pcmk__schema_t *current_schema = entry->data; xmlNode *upgrade = NULL; if (current_schema->schema_index > max_schema_index) { break; } if (!validate_with(*xml, current_schema, error_handler, GUINT_TO_POINTER(LOG_ERR))) { crm_debug("Schema %s does not validate", current_schema->name); if (best_schema != NULL) { /* we've satisfied the validation, no need to check further */ break; } rc = pcmk_rc_schema_validation; continue; // Try again with the next higher schema } crm_debug("Schema %s validates", current_schema->name); rc = pcmk_rc_ok; best_schema = current_schema; if (current_schema->schema_index == max_schema_index) { break; // No further transformations possible } if (!transform || (current_schema->transform == NULL) || validate_with_silent(*xml, entry->next->data)) { /* The next schema either doesn't require a transform or validates * successfully even without the transform. Skip the transform and * try the next schema with the same XML. */ continue; } upgrade = apply_upgrade(*xml, current_schema->schema_index, to_logs); if (upgrade == NULL) { /* The transform failed, so this schema can't be used. Later * schemas are unlikely to validate, but try anyway until we * run out of options. */ rc = pcmk_rc_transform_failed; } else { best_schema = current_schema; pcmk__xml_free(*xml); *xml = upgrade; } } if (best_schema != NULL) { if ((original_schema == NULL) || (best_schema->schema_index > original_schema->schema_index)) { crm_info("%s the configuration schema to %s", (transform? "Transformed" : "Upgraded"), best_schema->name); crm_xml_add(*xml, PCMK_XA_VALIDATE_WITH, best_schema->name); } } return rc; } /*! * \brief Update XML from its configured schema to the latest major series * * \param[in,out] xml XML to update * \param[in] to_logs If false, certain validation errors will be * sent to stderr rather than logged * * \return Standard Pacemaker return code */ int pcmk_update_configured_schema(xmlNode **xml, bool to_logs) { int rc = pcmk_rc_ok; char *original_schema_name = NULL; // @COMPAT Not specifying a schema name is deprecated since 2.1.8 const char *effective_original_name = "the first"; int orig_version = -1; pcmk__schema_t *x_0_schema = pcmk__find_x_0_schema()->data; GList *entry = NULL; CRM_CHECK(xml != NULL, return EINVAL); original_schema_name = crm_element_value_copy(*xml, PCMK_XA_VALIDATE_WITH); pcmk__warn_if_schema_deprecated(original_schema_name); entry = pcmk__get_schema(original_schema_name); if (entry != NULL) { pcmk__schema_t *original_schema = entry->data; effective_original_name = original_schema->name; orig_version = original_schema->schema_index; } if (orig_version < x_0_schema->schema_index) { // Current configuration schema is not acceptable, try to update xmlNode *converted = NULL; const char *new_schema_name = NULL; pcmk__schema_t *schema = NULL; entry = NULL; converted = pcmk__xml_copy(NULL, *xml); if (pcmk__update_schema(&converted, NULL, true, to_logs) == pcmk_rc_ok) { new_schema_name = crm_element_value(converted, PCMK_XA_VALIDATE_WITH); entry = pcmk__get_schema(new_schema_name); } schema = (entry == NULL)? NULL : entry->data; if ((schema == NULL) || (schema->schema_index < x_0_schema->schema_index)) { // Updated configuration schema is still not acceptable if ((orig_version == -1) || (schema == NULL) || (schema->schema_index < orig_version)) { // We couldn't validate any schema at all if (to_logs) { pcmk__config_err("Cannot upgrade configuration (claiming " "%s schema) to at least %s because it " "does not validate with any schema from " "%s to the latest", pcmk__s(original_schema_name, "no"), x_0_schema->name, effective_original_name); } else { fprintf(stderr, "Cannot upgrade configuration (claiming " "%s schema) to at least %s because it " "does not validate with any schema from " "%s to the latest\n", pcmk__s(original_schema_name, "no"), x_0_schema->name, effective_original_name); } } else { // We updated configuration successfully, but still too low if (to_logs) { pcmk__config_err("Cannot upgrade configuration (claiming " "%s schema) to at least %s because it " "would not upgrade past %s", pcmk__s(original_schema_name, "no"), x_0_schema->name, pcmk__s(new_schema_name, "unspecified version")); } else { fprintf(stderr, "Cannot upgrade configuration (claiming " "%s schema) to at least %s because it " "would not upgrade past %s\n", pcmk__s(original_schema_name, "no"), x_0_schema->name, pcmk__s(new_schema_name, "unspecified version")); } } pcmk__xml_free(converted); converted = NULL; rc = pcmk_rc_transform_failed; } else { // Updated configuration schema is acceptable pcmk__xml_free(*xml); *xml = converted; if (schema->schema_index < xml_latest_schema_index()) { if (to_logs) { pcmk__config_warn("Configuration with %s schema was " "internally upgraded to acceptable (but " "not most recent) %s", pcmk__s(original_schema_name, "no"), schema->name); } } else if (to_logs) { crm_info("Configuration with %s schema was internally " "upgraded to latest version %s", pcmk__s(original_schema_name, "no"), schema->name); } } } else { // @COMPAT the none schema is deprecated since 2.1.8 pcmk__schema_t *none_schema = NULL; entry = pcmk__get_schema(PCMK_VALUE_NONE); CRM_ASSERT((entry != NULL) && (entry->data != NULL)); none_schema = entry->data; if (!to_logs && (orig_version >= none_schema->schema_index)) { fprintf(stderr, "Schema validation of configuration is " "disabled (support for " PCMK_XA_VALIDATE_WITH " set to \"" PCMK_VALUE_NONE "\" is deprecated" " and will be removed in a future release)\n"); } } free(original_schema_name); return rc; } /*! * \internal * \brief Return a list of all schema files and any associated XSLT files * later than the given one * \brief Return a list of all schema versions later than the given one * * \param[in] schema The schema to compare against (for example, * "pacemaker-3.1.rng" or "pacemaker-3.1") * * \note The caller is responsible for freeing both the returned list and * the elements of the list */ GList * pcmk__schema_files_later_than(const char *name) { GList *lst = NULL; pcmk__schema_version_t ver; if (!version_from_filename(name, &ver)) { return lst; } for (GList *iter = g_list_nth(known_schemas, xml_latest_schema_index()); iter != NULL; iter = iter->prev) { pcmk__schema_t *schema = iter->data; char *s = NULL; if (schema_cmp(ver, schema->version) != -1) { continue; } s = crm_strdup_printf("%s.rng", schema->name); lst = g_list_prepend(lst, s); if (schema->transform != NULL) { char *xform = crm_strdup_printf("%s.xsl", schema->transform); lst = g_list_prepend(lst, xform); } if (schema->transform_enter != NULL) { char *enter = crm_strdup_printf("%s.xsl", schema->transform_enter); lst = g_list_prepend(lst, enter); if (schema->transform_onleave) { int last_dash = strrchr(enter, '-') - enter; char *leave = crm_strdup_printf("%.*s-leave.xsl", last_dash, enter); lst = g_list_prepend(lst, leave); } } } return lst; } static void append_href(xmlNode *xml, void *user_data) { GList **list = user_data; char *href = crm_element_value_copy(xml, "href"); if (href == NULL) { return; } *list = g_list_prepend(*list, href); } static void external_refs_in_schema(GList **list, const char *contents) { /* local-name()= is needed to ignore the xmlns= setting at the top of * the XML file. Otherwise, the xpath query will always return nothing. */ const char *search = "//*[local-name()='externalRef'] | //*[local-name()='include']"; xmlNode *xml = pcmk__xml_parse(contents); crm_foreach_xpath_result(xml, search, append_href, list); pcmk__xml_free(xml); } static int read_file_contents(const char *file, char **contents) { int rc = pcmk_rc_ok; char *path = NULL; if (pcmk__ends_with(file, ".rng")) { path = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng, file); } else { path = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, file); } rc = pcmk__file_contents(path, contents); free(path); return rc; } static void add_schema_file_to_xml(xmlNode *parent, const char *file, GList **already_included) { char *contents = NULL; char *path = NULL; xmlNode *file_node = NULL; GList *includes = NULL; int rc = pcmk_rc_ok; /* If we already included this file, don't do so again. */ if (g_list_find_custom(*already_included, file, (GCompareFunc) strcmp) != NULL) { return; } /* Ensure whatever file we were given has a suffix we know about. If not, * just assume it's an RNG file. */ if (!pcmk__ends_with(file, ".rng") && !pcmk__ends_with(file, ".xsl")) { path = crm_strdup_printf("%s.rng", file); } else { path = pcmk__str_copy(file); } rc = read_file_contents(path, &contents); if (rc != pcmk_rc_ok || contents == NULL) { crm_warn("Could not read schema file %s: %s", file, pcmk_rc_str(rc)); free(path); return; } /* Create a new node with the contents of the file * as a CDATA block underneath it. */ file_node = pcmk__xe_create(parent, PCMK_XA_FILE); crm_xml_add(file_node, PCMK_XA_PATH, path); *already_included = g_list_prepend(*already_included, path); xmlAddChild(file_node, xmlNewCDataBlock(parent->doc, (pcmkXmlStr) contents, strlen(contents))); /* Scan the file for any or nodes and build up * a list of the files they reference. */ external_refs_in_schema(&includes, contents); /* For each referenced file, recurse to add it (and potentially anything it * references, ...) to the XML. */ for (GList *iter = includes; iter != NULL; iter = iter->next) { add_schema_file_to_xml(parent, iter->data, already_included); } free(contents); g_list_free_full(includes, free); } /*! * \internal * \brief Add an XML schema file and all the files it references as children * of a given XML node * * \param[in,out] parent The parent XML node * \param[in] name The schema version to compare against * (for example, "pacemaker-3.1" or "pacemaker-3.1.rng") * \param[in,out] already_included A list of names that have already been added * to the parent node. * * \note The caller is responsible for freeing both the returned list and * the elements of the list */ void pcmk__build_schema_xml_node(xmlNode *parent, const char *name, GList **already_included) { xmlNode *schema_node = pcmk__xe_create(parent, PCMK__XA_SCHEMA); crm_xml_add(schema_node, PCMK_XA_VERSION, name); add_schema_file_to_xml(schema_node, name, already_included); if (schema_node->children == NULL) { // Not needed if empty. May happen if name was invalid, for example. pcmk__xml_free(schema_node); } } /*! * \internal * \brief Return the directory containing any extra schema files that a * Pacemaker Remote node fetched from the cluster */ const char * pcmk__remote_schema_dir(void) { const char *dir = pcmk__env_option(PCMK__ENV_REMOTE_SCHEMA_DIRECTORY); if (pcmk__str_empty(dir)) { return PCMK__REMOTE_SCHEMA_DIR; } return dir; } /*! * \internal * \brief Warn if a given validation schema is deprecated * * \param[in] Schema name to check */ void pcmk__warn_if_schema_deprecated(const char *schema) { if ((schema == NULL) || pcmk__strcase_any_of(schema, "pacemaker-next", PCMK_VALUE_NONE, NULL)) { pcmk__config_warn("Support for " PCMK_XA_VALIDATE_WITH "='%s' is " "deprecated and will be removed in a future release " "without the possibility of upgrades (manually edit " "to use a supported schema)", pcmk__s(schema, "")); } } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include -const char * -xml_latest_schema(void) -{ - return pcmk__highest_schema_name(); -} - -const char * -get_schema_name(int version) -{ - pcmk__schema_t *schema = g_list_nth_data(known_schemas, version); - - return (schema != NULL)? schema->name : "unknown"; -} - -int -get_schema_version(const char *name) -{ - int lpc = 0; - - if (name == NULL) { - name = PCMK_VALUE_NONE; - } - - for (GList *iter = known_schemas; iter != NULL; iter = iter->next) { - pcmk__schema_t *schema = iter->data; - - if (pcmk__str_eq(name, schema->name, pcmk__str_casei)) { - return lpc; - } - - lpc++; - } - - return -1; -} - -int -update_validation(xmlNode **xml, int *best, int max, gboolean transform, - gboolean to_logs) -{ - int rc = pcmk__update_schema(xml, get_schema_name(max), transform, to_logs); - - if ((best != NULL) && (xml != NULL) && (rc == pcmk_rc_ok)) { - const char *schema_name = crm_element_value(*xml, - PCMK_XA_VALIDATE_WITH); - GList *schema_entry = pcmk__get_schema(schema_name); - - if (schema_entry != NULL) { - *best = ((pcmk__schema_t *)(schema_entry->data))->schema_index; - } - } - - return pcmk_rc2legacy(rc); -} - -gboolean -validate_xml(xmlNode *xml_blob, const char *validation, gboolean to_logs) -{ - bool rc = pcmk__validate_xml(xml_blob, validation, - to_logs? (xmlRelaxNGValidityErrorFunc) xml_log : NULL, - GUINT_TO_POINTER(LOG_ERR)); - return rc? TRUE : FALSE; -} - -static void -dump_file(const char *filename) -{ - - FILE *fp = NULL; - int ch, line = 0; - - CRM_CHECK(filename != NULL, return); - - fp = fopen(filename, "r"); - if (fp == NULL) { - crm_perror(LOG_ERR, "Could not open %s for reading", filename); - return; - } - - fprintf(stderr, "%4d ", ++line); - do { - ch = getc(fp); - if (ch == EOF) { - putc('\n', stderr); - break; - } else if (ch == '\n') { - fprintf(stderr, "\n%4d ", ++line); - } else { - putc(ch, stderr); - } - } while (1); - - fclose(fp); -} - -gboolean -validate_xml_verbose(const xmlNode *xml_blob) -{ - int fd = 0; - xmlDoc *doc = NULL; - xmlNode *xml = NULL; - gboolean rc = FALSE; - char *filename = NULL; - - filename = crm_strdup_printf("%s/cib-invalid.XXXXXX", pcmk__get_tmpdir()); - - umask(S_IWGRP | S_IWOTH | S_IROTH); - fd = mkstemp(filename); - pcmk__xml_write_fd(xml_blob, filename, fd, false, NULL); - - dump_file(filename); - - doc = xmlReadFile(filename, NULL, 0); - xml = xmlDocGetRootElement(doc); - rc = pcmk__validate_xml(xml, NULL, NULL, NULL); - pcmk__xml_free(xml); - - unlink(filename); - free(filename); - - return rc? TRUE : FALSE; -} - gboolean cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs) { int rc = pcmk_update_configured_schema(xml, to_logs); if (best_version != NULL) { const char *name = crm_element_value(*xml, PCMK_XA_VALIDATE_WITH); if (name == NULL) { *best_version = -1; } else { GList *entry = pcmk__get_schema(name); pcmk__schema_t *schema = (entry == NULL)? NULL : entry->data; *best_version = (schema == NULL)? -1 : schema->schema_index; } } return (rc == pcmk_rc_ok)? TRUE: FALSE; } // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/common/xml.c b/lib/common/xml.c index ea9621f79d..b0ca4d4230 100644 --- a/lib/common/xml.c +++ b/lib/common/xml.c @@ -1,2793 +1,2400 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include // uint32_t #include #include #include #include // stat(), S_ISREG, etc. #include #include #include #include #include #include // PCMK__XML_LOG_BASE, etc. #include "crmcommon_private.h" /*! * \internal * \brief Apply a function to each XML node in a tree (pre-order, depth-first) * * \param[in,out] xml XML tree to traverse * \param[in,out] fn Function to call for each node (returns \c true to * continue traversing the tree or \c false to stop) * \param[in,out] user_data Argument to \p fn * * \return \c false if any \p fn call returned \c false, or \c true otherwise * * \note This function is recursive. */ bool pcmk__xml_tree_foreach(xmlNode *xml, bool (*fn)(xmlNode *, void *), void *user_data) { if (!fn(xml, user_data)) { return false; } for (xml = pcmk__xml_first_child(xml); xml != NULL; xml = pcmk__xml_next(xml)) { if (!pcmk__xml_tree_foreach(xml, fn, user_data)) { return false; } } return true; } bool pcmk__tracking_xml_changes(xmlNode *xml, bool lazy) { if(xml == NULL || xml->doc == NULL || xml->doc->_private == NULL) { return FALSE; } else if (!pcmk_is_set(((xml_doc_private_t *)xml->doc->_private)->flags, pcmk__xf_tracking)) { return FALSE; } else if (lazy && !pcmk_is_set(((xml_doc_private_t *)xml->doc->_private)->flags, pcmk__xf_lazy)) { return FALSE; } return TRUE; } static inline void set_parent_flag(xmlNode *xml, long flag) { for(; xml; xml = xml->parent) { xml_node_private_t *nodepriv = xml->_private; if (nodepriv == NULL) { /* During calls to xmlDocCopyNode(), _private will be unset for parent nodes */ } else { pcmk__set_xml_flags(nodepriv, flag); } } } void pcmk__set_xml_doc_flag(xmlNode *xml, enum xml_private_flags flag) { if(xml && xml->doc && xml->doc->_private){ /* During calls to xmlDocCopyNode(), xml->doc may be unset */ xml_doc_private_t *docpriv = xml->doc->_private; pcmk__set_xml_flags(docpriv, flag); } } // Mark document, element, and all element's parents as changed void pcmk__mark_xml_node_dirty(xmlNode *xml) { pcmk__set_xml_doc_flag(xml, pcmk__xf_dirty); set_parent_flag(xml, pcmk__xf_dirty); } /*! * \internal * \brief Clear flags on an XML node * * \param[in,out] xml XML node whose flags to reset * \param[in,out] user_data Ignored * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool reset_xml_node_flags(xmlNode *xml, void *user_data) { xml_node_private_t *nodepriv = xml->_private; if (nodepriv != NULL) { nodepriv->flags = pcmk__xf_none; } return true; } /*! * \internal * \brief Set the \c pcmk__xf_dirty and \c pcmk__xf_created flags on an XML node * * \param[in,out] xml Node whose flags to set * \param[in] user_data Ignored * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool mark_xml_dirty_created(xmlNode *xml, void *user_data) { xml_node_private_t *nodepriv = xml->_private; if (nodepriv != NULL) { pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created); } return true; } /*! * \internal * \brief Mark an XML tree as dirty and created, and mark its parents dirty * * Also mark the document dirty. * * \param[in,out] xml Tree to mark as dirty and created */ void pcmk__xml_mark_created(xmlNode *xml) { CRM_ASSERT(xml != NULL); if (!pcmk__tracking_xml_changes(xml, false)) { // Tracking is disabled for entire document return; } // Mark all parents and document dirty pcmk__mark_xml_node_dirty(xml); pcmk__xml_tree_foreach(xml, mark_xml_dirty_created, NULL); } #define XML_DOC_PRIVATE_MAGIC 0x81726354UL #define XML_NODE_PRIVATE_MAGIC 0x54637281UL // Free an XML object previously marked as deleted static void free_deleted_object(void *data) { if(data) { pcmk__deleted_xml_t *deleted_obj = data; g_free(deleted_obj->path); free(deleted_obj); } } // Free and NULL user, ACLs, and deleted objects in an XML node's private data static void reset_xml_private_data(xml_doc_private_t *docpriv) { if (docpriv != NULL) { CRM_ASSERT(docpriv->check == XML_DOC_PRIVATE_MAGIC); free(docpriv->user); docpriv->user = NULL; if (docpriv->acls != NULL) { pcmk__free_acls(docpriv->acls); docpriv->acls = NULL; } if(docpriv->deleted_objs) { g_list_free_full(docpriv->deleted_objs, free_deleted_object); docpriv->deleted_objs = NULL; } } } // Free all private data associated with an XML node static void free_private_data(xmlNode *node) { /* Note: This function frees private data assosciated with an XML node, unless the function is being called as a result of internal XSLT cleanup. That could happen through, for example, the following chain of function calls: xsltApplyStylesheetInternal -> xsltFreeTransformContext -> xsltFreeRVTs -> xmlFreeDoc And in that case, the node would fulfill three conditions: 1. It would be a standalone document (i.e. it wouldn't be part of a document) 2. It would have a space-prefixed name (for reference, please see xsltInternals.h: XSLT_MARK_RES_TREE_FRAG) 3. It would carry its own payload in the _private field. We do not free data in this circumstance to avoid a failed assertion on the XML_*_PRIVATE_MAGIC later. */ if (node->name == NULL || node->name[0] != ' ') { if (node->_private) { if (node->type == XML_DOCUMENT_NODE) { reset_xml_private_data(node->_private); } else { CRM_ASSERT(((xml_node_private_t *) node->_private)->check == XML_NODE_PRIVATE_MAGIC); /* nothing dynamically allocated nested */ } free(node->_private); node->_private = NULL; } } } // Allocate and initialize private data for an XML node static void new_private_data(xmlNode *node) { switch (node->type) { case XML_DOCUMENT_NODE: { xml_doc_private_t *docpriv = pcmk__assert_alloc(1, sizeof(xml_doc_private_t)); docpriv->check = XML_DOC_PRIVATE_MAGIC; /* Flags will be reset if necessary when tracking is enabled */ pcmk__set_xml_flags(docpriv, pcmk__xf_dirty|pcmk__xf_created); node->_private = docpriv; break; } case XML_ELEMENT_NODE: case XML_ATTRIBUTE_NODE: case XML_COMMENT_NODE: { xml_node_private_t *nodepriv = pcmk__assert_alloc(1, sizeof(xml_node_private_t)); nodepriv->check = XML_NODE_PRIVATE_MAGIC; /* Flags will be reset if necessary when tracking is enabled */ pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created); node->_private = nodepriv; if (pcmk__tracking_xml_changes(node, FALSE)) { /* XML_ELEMENT_NODE doesn't get picked up here, node->doc is * not hooked up at the point we are called */ pcmk__mark_xml_node_dirty(node); } break; } case XML_TEXT_NODE: case XML_DTD_NODE: case XML_CDATA_SECTION_NODE: break; default: /* Ignore */ crm_trace("Ignoring %p %d", node, node->type); CRM_LOG_ASSERT(node->type == XML_ELEMENT_NODE); break; } } void xml_track_changes(xmlNode * xml, const char *user, xmlNode *acl_source, bool enforce_acls) { xml_accept_changes(xml); crm_trace("Tracking changes%s to %p", enforce_acls?" with ACLs":"", xml); pcmk__set_xml_doc_flag(xml, pcmk__xf_tracking); if(enforce_acls) { if(acl_source == NULL) { acl_source = xml; } pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_enabled); pcmk__unpack_acl(acl_source, xml, user); pcmk__apply_acl(xml); } } bool xml_tracking_changes(xmlNode * xml) { return (xml != NULL) && (xml->doc != NULL) && (xml->doc->_private != NULL) && pcmk_is_set(((xml_doc_private_t *)(xml->doc->_private))->flags, pcmk__xf_tracking); } bool xml_document_dirty(xmlNode *xml) { return (xml != NULL) && (xml->doc != NULL) && (xml->doc->_private != NULL) && pcmk_is_set(((xml_doc_private_t *)(xml->doc->_private))->flags, pcmk__xf_dirty); } /*! * \internal * \brief Return ordinal position of an XML node among its siblings * * \param[in] xml XML node to check * \param[in] ignore_if_set Don't count siblings with this flag set * * \return Ordinal position of \p xml (starting with 0) */ int pcmk__xml_position(const xmlNode *xml, enum xml_private_flags ignore_if_set) { int position = 0; for (const xmlNode *cIter = xml; cIter->prev; cIter = cIter->prev) { xml_node_private_t *nodepriv = ((xmlNode*)cIter->prev)->_private; if (!pcmk_is_set(nodepriv->flags, ignore_if_set)) { position++; } } return position; } /*! * \internal * \brief Remove all attributes marked as deleted from an XML node * * \param[in,out] xml XML node whose deleted attributes to remove * \param[in,out] user_data Ignored * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool accept_attr_deletions(xmlNode *xml, void *user_data) { reset_xml_node_flags(xml, NULL); pcmk__xe_remove_matching_attrs(xml, pcmk__marked_as_deleted, NULL); return true; } /*! * \internal * \brief Find first child XML node matching another given XML node * * \param[in] haystack XML whose children should be checked * \param[in] needle XML to match (comment content or element name and ID) * \param[in] exact If true and needle is a comment, position must match */ xmlNode * pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle, bool exact) { CRM_CHECK(needle != NULL, return NULL); if (needle->type == XML_COMMENT_NODE) { return pcmk__xc_match(haystack, needle, exact); } else { const char *id = pcmk__xe_id(needle); const char *attr = (id == NULL)? NULL : PCMK_XA_ID; return pcmk__xe_first_child(haystack, (const char *) needle->name, attr, id); } } void xml_accept_changes(xmlNode * xml) { xmlNode *top = NULL; xml_doc_private_t *docpriv = NULL; if(xml == NULL) { return; } crm_trace("Accepting changes to %p", xml); docpriv = xml->doc->_private; top = xmlDocGetRootElement(xml->doc); reset_xml_private_data(xml->doc->_private); if (!pcmk_is_set(docpriv->flags, pcmk__xf_dirty)) { docpriv->flags = pcmk__xf_none; return; } docpriv->flags = pcmk__xf_none; pcmk__xml_tree_foreach(top, accept_attr_deletions, NULL); } /*! * \internal * \brief Find first XML child element matching given criteria * * \param[in] parent XML element to search (can be \c NULL) * \param[in] node_name If not \c NULL, only match children of this type * \param[in] attr_n If not \c NULL, only match children with an attribute * of this name. * \param[in] attr_v If \p attr_n and this are not NULL, only match children * with an attribute named \p attr_n and this value * * \return Matching XML child element, or \c NULL if none found */ xmlNode * pcmk__xe_first_child(const xmlNode *parent, const char *node_name, const char *attr_n, const char *attr_v) { xmlNode *child = NULL; const char *parent_name = ""; CRM_CHECK((attr_v == NULL) || (attr_n != NULL), return NULL); if (parent != NULL) { child = parent->children; while ((child != NULL) && (child->type != XML_ELEMENT_NODE)) { child = child->next; } parent_name = (const char *) parent->name; } for (; child != NULL; child = pcmk__xe_next(child)) { const char *value = NULL; if ((node_name != NULL) && !pcmk__xe_is(child, node_name)) { // Node name mismatch continue; } if (attr_n == NULL) { // No attribute match needed return child; } value = crm_element_value(child, attr_n); if ((attr_v == NULL) && (value != NULL)) { // attr_v == NULL: Attribute attr_n must be set (to any value) return child; } if ((attr_v != NULL) && (pcmk__str_eq(value, attr_v, pcmk__str_none))) { // attr_v != NULL: Attribute attr_n must be set to value attr_v return child; } } if (node_name == NULL) { node_name = "(any)"; // For logging } if (attr_n != NULL) { crm_trace("XML child node <%s %s=%s> not found in %s", node_name, attr_n, attr_v, parent_name); } else { crm_trace("XML child node <%s> not found in %s", node_name, parent_name); } return NULL; } /*! * \internal * \brief Set an XML attribute, expanding \c ++ and \c += where appropriate * * If \p target already has an attribute named \p name set to an integer value * and \p value is an addition assignment expression on \p name, then expand * \p value to an integer and set attribute \p name to the expanded value in * \p target. * * Otherwise, set attribute \p name on \p target using the literal \p value. * * The original attribute value in \p target and the number in an assignment * expression in \p value are parsed and added as scores (that is, their values * are capped at \c INFINITY and \c -INFINITY). For more details, refer to * \c char2score(). * * For example, suppose \p target has an attribute named \c "X" with value * \c "5", and that \p name is \c "X". * * If \p value is \c "X++", the new value of \c "X" in \p target is \c "6". * * If \p value is \c "X+=3", the new value of \c "X" in \p target is \c "8". * * If \p value is \c "val", the new value of \c "X" in \p target is \c "val". * * If \p value is \c "Y++", the new value of \c "X" in \p target is \c "Y++". * * \param[in,out] target XML node whose attribute to set * \param[in] name Name of the attribute to set * \param[in] value New value of attribute to set * * \return Standard Pacemaker return code (specifically, \c EINVAL on invalid * argument, or \c pcmk_rc_ok otherwise) */ int pcmk__xe_set_score(xmlNode *target, const char *name, const char *value) { const char *old_value = NULL; CRM_CHECK((target != NULL) && (name != NULL), return EINVAL); if (value == NULL) { return pcmk_rc_ok; } old_value = crm_element_value(target, name); // If no previous value, skip to default case and set the value unexpanded. if (old_value != NULL) { const char *n = name; const char *v = value; // Stop at first character that differs between name and value for (; (*n == *v) && (*n != '\0'); n++, v++); // If value begins with name followed by a "++" or "+=" if ((*n == '\0') && (*v++ == '+') && ((*v == '+') || (*v == '='))) { // If we're expanding ourselves, no previous value was set; use 0 int old_value_i = (old_value != value)? char2score(old_value) : 0; /* value="X++": new value of X is old_value + 1 * value="X+=Y": new value of X is old_value + Y (for some number Y) */ int add = (*v == '+')? 1 : char2score(++v); crm_xml_add_int(target, name, pcmk__add_scores(old_value_i, add)); return pcmk_rc_ok; } } // Default case: set the attribute unexpanded (with value treated literally) if (old_value != value) { crm_xml_add(target, name, value); } return pcmk_rc_ok; } /*! * \internal * \brief Copy XML attributes from a source element to a target element * * This is similar to \c xmlCopyPropList() except that attributes are marked * as dirty for change tracking purposes. * * \param[in,out] target XML element to receive copied attributes from \p src * \param[in] src XML element whose attributes to copy to \p target * \param[in] flags Group of enum pcmk__xa_flags * * \return Standard Pacemaker return code */ int pcmk__xe_copy_attrs(xmlNode *target, const xmlNode *src, uint32_t flags) { CRM_CHECK((src != NULL) && (target != NULL), return EINVAL); for (xmlAttr *attr = pcmk__xe_first_attr(src); attr != NULL; attr = attr->next) { const char *name = (const char *) attr->name; const char *value = pcmk__xml_attr_value(attr); if (pcmk_is_set(flags, pcmk__xaf_no_overwrite) && (crm_element_value(target, name) != NULL)) { continue; } if (pcmk_is_set(flags, pcmk__xaf_score_update)) { pcmk__xe_set_score(target, name, value); } else { crm_xml_add(target, name, value); } } return pcmk_rc_ok; } /*! * \internal * \brief Remove an XML attribute from an element * * \param[in,out] element XML element that owns \p attr * \param[in,out] attr XML attribute to remove from \p element * * \return Standard Pacemaker return code (\c EPERM if ACLs prevent removal of * attributes from \p element, or \c pcmk_rc_ok otherwise) */ static int remove_xe_attr(xmlNode *element, xmlAttr *attr) { if (attr == NULL) { return pcmk_rc_ok; } if (!pcmk__check_acl(element, NULL, pcmk__xf_acl_write)) { // ACLs apply to element, not to particular attributes crm_trace("ACLs prevent removal of attributes from %s element", (const char *) element->name); return EPERM; } if (pcmk__tracking_xml_changes(element, false)) { // Leave in place (marked for removal) until after diff is calculated set_parent_flag(element, pcmk__xf_dirty); pcmk__set_xml_flags((xml_node_private_t *) attr->_private, pcmk__xf_deleted); } else { xmlRemoveProp(attr); } return pcmk_rc_ok; } /*! * \internal * \brief Remove a named attribute from an XML element * * \param[in,out] element XML element to remove an attribute from * \param[in] name Name of attribute to remove */ void pcmk__xe_remove_attr(xmlNode *element, const char *name) { if (name != NULL) { remove_xe_attr(element, xmlHasProp(element, (pcmkXmlStr) name)); } } /*! * \internal * \brief Remove a named attribute from an XML element * * This is a wrapper for \c pcmk__xe_remove_attr() for use with * \c pcmk__xml_tree_foreach(). * * \param[in,out] xml XML element to remove an attribute from * \param[in] user_data Name of attribute to remove * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ bool pcmk__xe_remove_attr_cb(xmlNode *xml, void *user_data) { const char *name = user_data; pcmk__xe_remove_attr(xml, name); return true; } /*! * \internal * \brief Remove an XML element's attributes that match some criteria * * \param[in,out] element XML element to modify * \param[in] match If not NULL, only remove attributes for which * this function returns true * \param[in,out] user_data Data to pass to \p match */ void pcmk__xe_remove_matching_attrs(xmlNode *element, bool (*match)(xmlAttrPtr, void *), void *user_data) { xmlAttrPtr next = NULL; for (xmlAttrPtr a = pcmk__xe_first_attr(element); a != NULL; a = next) { next = a->next; // Grab now because attribute might get removed if ((match == NULL) || match(a, user_data)) { if (remove_xe_attr(element, a) != pcmk_rc_ok) { return; } } } } /*! * \internal * \brief Create a new XML element under a given parent * * \param[in,out] parent XML element that will be the new element's parent * (\c NULL to create a new XML document with the new * node as root) * \param[in] name Name of new element * * \return Newly created XML element (guaranteed not to be \c NULL) */ xmlNode * pcmk__xe_create(xmlNode *parent, const char *name) { xmlNode *node = NULL; CRM_ASSERT(!pcmk__str_empty(name)); if (parent == NULL) { xmlDoc *doc = xmlNewDoc(PCMK__XML_VERSION); pcmk__mem_assert(doc); node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL); pcmk__mem_assert(node); xmlDocSetRootElement(doc, node); } else { node = xmlNewChild(parent, NULL, (pcmkXmlStr) name, NULL); pcmk__mem_assert(node); } pcmk__xml_mark_created(node); return node; } /*! * \internal * \brief Set a formatted string as an XML node's content * * \param[in,out] node Node whose content to set * \param[in] format printf(3)-style format string * \param[in] ... Arguments for \p format * * \note This function escapes special characters. \c xmlNodeSetContent() does * not. */ G_GNUC_PRINTF(2, 3) void pcmk__xe_set_content(xmlNode *node, const char *format, ...) { if (node != NULL) { const char *content = NULL; char *buf = NULL; if (strchr(format, '%') == NULL) { // Nothing to format content = format; } else { va_list ap; va_start(ap, format); if (pcmk__str_eq(format, "%s", pcmk__str_none)) { // No need to make a copy content = va_arg(ap, const char *); } else { CRM_ASSERT(vasprintf(&buf, format, ap) >= 0); content = buf; } va_end(ap); } xmlNodeSetContent(node, (pcmkXmlStr) content); free(buf); } } /*! * \internal * \brief Free an XML tree if ACLs allow; track deletion if tracking is enabled * * If \p node is the root of its document, free the entire document. * * \param[in,out] node XML node to free * \param[in] position Position of \p node among its siblings for change * tracking (negative to calculate automatically if * needed) */ static void free_xml_with_position(xmlNode *node, int position) { xmlDoc *doc = NULL; xml_node_private_t *nodepriv = NULL; if (node == NULL) { return; } doc = node->doc; nodepriv = node->_private; if ((doc != NULL) && (xmlDocGetRootElement(doc) == node)) { /* @TODO Should we check ACLs first? Otherwise it seems like we could * free the root element without write permission. */ xmlFreeDoc(doc); return; } if (!pcmk__check_acl(node, NULL, pcmk__xf_acl_write)) { GString *xpath = NULL; pcmk__if_tracing({}, return); xpath = pcmk__element_xpath(node); qb_log_from_external_source(__func__, __FILE__, "Cannot remove %s %x", LOG_TRACE, __LINE__, 0, xpath->str, nodepriv->flags); g_string_free(xpath, TRUE); return; } if ((doc != NULL) && pcmk__tracking_xml_changes(node, false) && !pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { xml_doc_private_t *docpriv = doc->_private; GString *xpath = pcmk__element_xpath(node); if (xpath != NULL) { pcmk__deleted_xml_t *deleted_obj = NULL; crm_trace("Deleting %s %p from %p", xpath->str, node, doc); deleted_obj = pcmk__assert_alloc(1, sizeof(pcmk__deleted_xml_t)); deleted_obj->path = g_string_free(xpath, FALSE); deleted_obj->position = -1; // Record the position only for XML comments for now if (node->type == XML_COMMENT_NODE) { if (position >= 0) { deleted_obj->position = position; } else { deleted_obj->position = pcmk__xml_position(node, pcmk__xf_skip); } } docpriv->deleted_objs = g_list_append(docpriv->deleted_objs, deleted_obj); pcmk__set_xml_doc_flag(node, pcmk__xf_dirty); } } xmlUnlinkNode(node); xmlFreeNode(node); } /*! * \internal * \brief Free an XML tree if ACLs allow; track deletion if tracking is enabled * * If \p xml is the root of its document, free the entire document. * * \param[in,out] xml XML node to free */ void pcmk__xml_free(xmlNode *xml) { free_xml_with_position(xml, -1); } /*! * \internal * \brief Make a deep copy of an XML node under a given parent * * \param[in,out] parent XML element that will be the copy's parent (\c NULL * to create a new XML document with the copy as root) * \param[in] src XML node to copy * * \return Deep copy of \p src, or \c NULL if \p src is \c NULL */ xmlNode * pcmk__xml_copy(xmlNode *parent, xmlNode *src) { xmlNode *copy = NULL; if (src == NULL) { return NULL; } if (parent == NULL) { xmlDoc *doc = NULL; // The copy will be the root element of a new document CRM_ASSERT(src->type == XML_ELEMENT_NODE); doc = xmlNewDoc(PCMK__XML_VERSION); pcmk__mem_assert(doc); copy = xmlDocCopyNode(src, doc, 1); pcmk__mem_assert(copy); xmlDocSetRootElement(doc, copy); } else { copy = xmlDocCopyNode(src, parent->doc, 1); pcmk__mem_assert(copy); xmlAddChild(parent, copy); } pcmk__xml_mark_created(copy); return copy; } /*! * \internal * \brief Remove XML text nodes from specified XML and all its children * * \param[in,out] xml XML to strip text from */ void pcmk__strip_xml_text(xmlNode *xml) { xmlNode *iter = xml->children; while (iter) { xmlNode *next = iter->next; switch (iter->type) { case XML_TEXT_NODE: xmlUnlinkNode(iter); xmlFreeNode(iter); break; case XML_ELEMENT_NODE: /* Search it */ pcmk__strip_xml_text(iter); break; default: /* Leave it */ break; } iter = next; } } /*! * \internal * \brief Add a "last written" attribute to an XML element, set to current time * * \param[in,out] xe XML element to add attribute to * * \return Value that was set, or NULL on error */ const char * pcmk__xe_add_last_written(xmlNode *xe) { char *now_s = pcmk__epoch2str(NULL, 0); const char *result = NULL; result = crm_xml_add(xe, PCMK_XA_CIB_LAST_WRITTEN, pcmk__s(now_s, "Could not determine current time")); free(now_s); return result; } /*! * \brief Sanitize a string so it is usable as an XML ID * * \param[in,out] id String to sanitize */ void crm_xml_sanitize_id(char *id) { char *c; for (c = id; *c; ++c) { /* @TODO Sanitize more comprehensively */ switch (*c) { case ':': case '#': *c = '.'; } } } /*! * \brief Set the ID of an XML element using a format * * \param[in,out] xml XML element * \param[in] fmt printf-style format * \param[in] ... any arguments required by format */ void crm_xml_set_id(xmlNode *xml, const char *format, ...) { va_list ap; int len = 0; char *id = NULL; /* equivalent to crm_strdup_printf() */ va_start(ap, format); len = vasprintf(&id, format, ap); va_end(ap); CRM_ASSERT(len > 0); crm_xml_sanitize_id(id); crm_xml_add(xml, PCMK_XA_ID, id); free(id); } /*! * \internal * \brief Check whether a string has XML special characters that must be escaped * * See \c pcmk__xml_escape() and \c pcmk__xml_escape_type for more details. * * \param[in] text String to check * \param[in] type Type of escaping * * \return \c true if \p text has special characters that need to be escaped, or * \c false otherwise */ bool pcmk__xml_needs_escape(const char *text, enum pcmk__xml_escape_type type) { if (text == NULL) { return false; } while (*text != '\0') { switch (type) { case pcmk__xml_escape_text: switch (*text) { case '<': case '>': case '&': return true; case '\n': case '\t': break; default: if (g_ascii_iscntrl(*text)) { return true; } break; } break; case pcmk__xml_escape_attr: switch (*text) { case '<': case '>': case '&': case '"': return true; default: if (g_ascii_iscntrl(*text)) { return true; } break; } break; case pcmk__xml_escape_attr_pretty: switch (*text) { case '\n': case '\r': case '\t': case '"': return true; default: break; } break; default: // Invalid enum value CRM_ASSERT(false); break; } text = g_utf8_next_char(text); } return false; } /*! * \internal * \brief Replace special characters with their XML escape sequences * * \param[in] text Text to escape * \param[in] type Type of escaping * * \return Newly allocated string equivalent to \p text but with special * characters replaced with XML escape sequences (or \c NULL if \p text * is \c NULL). If \p text is not \c NULL, the return value is * guaranteed not to be \c NULL. * * \note There are libxml functions that purport to do this: * \c xmlEncodeEntitiesReentrant() and \c xmlEncodeSpecialChars(). * However, their escaping is incomplete. See: * https://discourse.gnome.org/t/intended-use-of-xmlencodeentitiesreentrant-vs-xmlencodespecialchars/19252 * \note The caller is responsible for freeing the return value using * \c g_free(). */ gchar * pcmk__xml_escape(const char *text, enum pcmk__xml_escape_type type) { GString *copy = NULL; if (text == NULL) { return NULL; } copy = g_string_sized_new(strlen(text)); while (*text != '\0') { // Don't escape any non-ASCII characters if ((*text & 0x80) != 0) { size_t bytes = g_utf8_next_char(text) - text; g_string_append_len(copy, text, bytes); text += bytes; continue; } switch (type) { case pcmk__xml_escape_text: switch (*text) { case '<': g_string_append(copy, PCMK__XML_ENTITY_LT); break; case '>': g_string_append(copy, PCMK__XML_ENTITY_GT); break; case '&': g_string_append(copy, PCMK__XML_ENTITY_AMP); break; case '\n': case '\t': g_string_append_c(copy, *text); break; default: if (g_ascii_iscntrl(*text)) { g_string_append_printf(copy, "&#x%.2X;", *text); } else { g_string_append_c(copy, *text); } break; } break; case pcmk__xml_escape_attr: switch (*text) { case '<': g_string_append(copy, PCMK__XML_ENTITY_LT); break; case '>': g_string_append(copy, PCMK__XML_ENTITY_GT); break; case '&': g_string_append(copy, PCMK__XML_ENTITY_AMP); break; case '"': g_string_append(copy, PCMK__XML_ENTITY_QUOT); break; default: if (g_ascii_iscntrl(*text)) { g_string_append_printf(copy, "&#x%.2X;", *text); } else { g_string_append_c(copy, *text); } break; } break; case pcmk__xml_escape_attr_pretty: switch (*text) { case '"': g_string_append(copy, "\\\""); break; case '\n': g_string_append(copy, "\\n"); break; case '\r': g_string_append(copy, "\\r"); break; case '\t': g_string_append(copy, "\\t"); break; default: g_string_append_c(copy, *text); break; } break; default: // Invalid enum value CRM_ASSERT(false); break; } text = g_utf8_next_char(text); } return g_string_free(copy, FALSE); } /*! * \internal * \brief Set a flag on all attributes of an XML element * * \param[in,out] xml XML node to set flags on * \param[in] flag XML private flag to set */ static void set_attrs_flag(xmlNode *xml, enum xml_private_flags flag) { for (xmlAttr *attr = pcmk__xe_first_attr(xml); attr; attr = attr->next) { pcmk__set_xml_flags((xml_node_private_t *) (attr->_private), flag); } } /*! * \internal * \brief Add an XML attribute to a node, marked as deleted * * When calculating XML changes, we need to know when an attribute has been * deleted. Add the attribute back to the new XML, so that we can check the * removal against ACLs, and mark it as deleted for later removal after * differences have been calculated. * * \param[in,out] new_xml XML to modify * \param[in] element Name of XML element that changed (for logging) * \param[in] attr_name Name of attribute that was deleted * \param[in] old_value Value of attribute that was deleted */ static void mark_attr_deleted(xmlNode *new_xml, const char *element, const char *attr_name, const char *old_value) { xml_doc_private_t *docpriv = new_xml->doc->_private; xmlAttr *attr = NULL; xml_node_private_t *nodepriv; // Prevent the dirty flag being set recursively upwards pcmk__clear_xml_flags(docpriv, pcmk__xf_tracking); // Restore the old value (and the tracking flag) attr = xmlSetProp(new_xml, (pcmkXmlStr) attr_name, (pcmkXmlStr) old_value); pcmk__set_xml_flags(docpriv, pcmk__xf_tracking); // Reset flags (so the attribute doesn't appear as newly created) nodepriv = attr->_private; nodepriv->flags = 0; // Check ACLs and mark restored value for later removal remove_xe_attr(new_xml, attr); crm_trace("XML attribute %s=%s was removed from %s", attr_name, old_value, element); } /* * \internal * \brief Check ACLs for a changed XML attribute */ static void mark_attr_changed(xmlNode *new_xml, const char *element, const char *attr_name, const char *old_value) { char *vcopy = crm_element_value_copy(new_xml, attr_name); crm_trace("XML attribute %s was changed from '%s' to '%s' in %s", attr_name, old_value, vcopy, element); // Restore the original value xmlSetProp(new_xml, (pcmkXmlStr) attr_name, (pcmkXmlStr) old_value); // Change it back to the new value, to check ACLs crm_xml_add(new_xml, attr_name, vcopy); free(vcopy); } /*! * \internal * \brief Mark an XML attribute as having changed position * * \param[in,out] new_xml XML to modify * \param[in] element Name of XML element that changed (for logging) * \param[in,out] old_attr Attribute that moved, in original XML * \param[in,out] new_attr Attribute that moved, in \p new_xml * \param[in] p_old Ordinal position of \p old_attr in original XML * \param[in] p_new Ordinal position of \p new_attr in \p new_xml */ static void mark_attr_moved(xmlNode *new_xml, const char *element, xmlAttr *old_attr, xmlAttr *new_attr, int p_old, int p_new) { xml_node_private_t *nodepriv = new_attr->_private; crm_trace("XML attribute %s moved from position %d to %d in %s", old_attr->name, p_old, p_new, element); // Mark document, element, and all element's parents as changed pcmk__mark_xml_node_dirty(new_xml); // Mark attribute as changed pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_moved); nodepriv = (p_old > p_new)? old_attr->_private : new_attr->_private; pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); } /*! * \internal * \brief Calculate differences in all previously existing XML attributes * * \param[in,out] old_xml Original XML to compare * \param[in,out] new_xml New XML to compare */ static void xml_diff_old_attrs(xmlNode *old_xml, xmlNode *new_xml) { xmlAttr *attr_iter = pcmk__xe_first_attr(old_xml); while (attr_iter != NULL) { const char *name = (const char *) attr_iter->name; xmlAttr *old_attr = attr_iter; xmlAttr *new_attr = xmlHasProp(new_xml, attr_iter->name); const char *old_value = pcmk__xml_attr_value(attr_iter); attr_iter = attr_iter->next; if (new_attr == NULL) { mark_attr_deleted(new_xml, (const char *) old_xml->name, name, old_value); } else { xml_node_private_t *nodepriv = new_attr->_private; int new_pos = pcmk__xml_position((xmlNode*) new_attr, pcmk__xf_skip); int old_pos = pcmk__xml_position((xmlNode*) old_attr, pcmk__xf_skip); const char *new_value = crm_element_value(new_xml, name); // This attribute isn't new pcmk__clear_xml_flags(nodepriv, pcmk__xf_created); if (strcmp(new_value, old_value) != 0) { mark_attr_changed(new_xml, (const char *) old_xml->name, name, old_value); } else if ((old_pos != new_pos) && !pcmk__tracking_xml_changes(new_xml, TRUE)) { mark_attr_moved(new_xml, (const char *) old_xml->name, old_attr, new_attr, old_pos, new_pos); } } } } /*! * \internal * \brief Check all attributes in new XML for creation * * For each of a given XML element's attributes marked as newly created, accept * (and mark as dirty) or reject the creation according to ACLs. * * \param[in,out] new_xml XML to check */ static void mark_created_attrs(xmlNode *new_xml) { xmlAttr *attr_iter = pcmk__xe_first_attr(new_xml); while (attr_iter != NULL) { xmlAttr *new_attr = attr_iter; xml_node_private_t *nodepriv = attr_iter->_private; attr_iter = attr_iter->next; if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { const char *attr_name = (const char *) new_attr->name; crm_trace("Created new attribute %s=%s in %s", attr_name, pcmk__xml_attr_value(new_attr), new_xml->name); /* Check ACLs (we can't use the remove-then-create trick because it * would modify the attribute position). */ if (pcmk__check_acl(new_xml, attr_name, pcmk__xf_acl_write)) { pcmk__mark_xml_attr_dirty(new_attr); } else { // Creation was not allowed, so remove the attribute xmlUnsetProp(new_xml, new_attr->name); } } } } /*! * \internal * \brief Calculate differences in attributes between two XML nodes * * \param[in,out] old_xml Original XML to compare * \param[in,out] new_xml New XML to compare */ static void xml_diff_attrs(xmlNode *old_xml, xmlNode *new_xml) { set_attrs_flag(new_xml, pcmk__xf_created); // cleared later if not really new xml_diff_old_attrs(old_xml, new_xml); mark_created_attrs(new_xml); } /*! * \internal * \brief Add an XML child element to a node, marked as deleted * * When calculating XML changes, we need to know when a child element has been * deleted. Add the child back to the new XML, so that we can check the removal * against ACLs, and mark it as deleted for later removal after differences have * been calculated. * * \param[in,out] old_child Child element from original XML * \param[in,out] new_parent New XML to add marked copy to */ static void mark_child_deleted(xmlNode *old_child, xmlNode *new_parent) { // Re-create the child element so we can check ACLs xmlNode *candidate = pcmk__xml_copy(new_parent, old_child); // Clear flags on new child and its children pcmk__xml_tree_foreach(candidate, reset_xml_node_flags, NULL); // Check whether ACLs allow the deletion pcmk__apply_acl(xmlDocGetRootElement(candidate->doc)); // Remove the child again (which will track it in document's deleted_objs) free_xml_with_position(candidate, pcmk__xml_position(old_child, pcmk__xf_skip)); if (pcmk__xml_match(new_parent, old_child, true) == NULL) { pcmk__set_xml_flags((xml_node_private_t *) (old_child->_private), pcmk__xf_skip); } } static void mark_child_moved(xmlNode *old_child, xmlNode *new_parent, xmlNode *new_child, int p_old, int p_new) { xml_node_private_t *nodepriv = new_child->_private; crm_trace("Child element %s with " PCMK_XA_ID "='%s' moved from position %d to %d under %s", new_child->name, pcmk__s(pcmk__xe_id(new_child), ""), p_old, p_new, new_parent->name); pcmk__mark_xml_node_dirty(new_parent); pcmk__set_xml_flags(nodepriv, pcmk__xf_moved); if (p_old > p_new) { nodepriv = old_child->_private; } else { nodepriv = new_child->_private; } pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); } // Given original and new XML, mark new XML portions that have changed static void mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top) { xmlNode *old_child = NULL; xmlNode *new_child = NULL; xml_node_private_t *nodepriv = NULL; CRM_CHECK(new_xml != NULL, return); if (old_xml == NULL) { pcmk__xml_mark_created(new_xml); pcmk__apply_creation_acl(new_xml, check_top); return; } nodepriv = new_xml->_private; CRM_CHECK(nodepriv != NULL, return); if(nodepriv->flags & pcmk__xf_processed) { /* Avoid re-comparing nodes */ return; } pcmk__set_xml_flags(nodepriv, pcmk__xf_processed); xml_diff_attrs(old_xml, new_xml); // Check for differences in the original children for (old_child = pcmk__xml_first_child(old_xml); old_child != NULL; old_child = pcmk__xml_next(old_child)) { new_child = pcmk__xml_match(new_xml, old_child, true); if (new_child != NULL) { mark_xml_changes(old_child, new_child, true); } else { mark_child_deleted(old_child, new_xml); } } // Check for moved or created children new_child = pcmk__xml_first_child(new_xml); while (new_child != NULL) { xmlNode *next = pcmk__xml_next(new_child); old_child = pcmk__xml_match(old_xml, new_child, true); if (old_child == NULL) { // This is a newly created child nodepriv = new_child->_private; pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); // May free new_child mark_xml_changes(old_child, new_child, true); } else { /* Check for movement, we already checked for differences */ int p_new = pcmk__xml_position(new_child, pcmk__xf_skip); int p_old = pcmk__xml_position(old_child, pcmk__xf_skip); if(p_old != p_new) { mark_child_moved(old_child, new_xml, new_child, p_old, p_new); } } new_child = next; } } void xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml) { pcmk__set_xml_doc_flag(new_xml, pcmk__xf_lazy); xml_calculate_changes(old_xml, new_xml); } // Called functions may set the \p pcmk__xf_skip flag on parts of \p old_xml void xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml) { CRM_CHECK((old_xml != NULL) && (new_xml != NULL) && pcmk__xe_is(old_xml, (const char *) new_xml->name) && pcmk__str_eq(pcmk__xe_id(old_xml), pcmk__xe_id(new_xml), pcmk__str_none), return); if(xml_tracking_changes(new_xml) == FALSE) { xml_track_changes(new_xml, NULL, NULL, FALSE); } mark_xml_changes(old_xml, new_xml, FALSE); } /*! * \internal * \brief Find a comment with matching content in specified XML * * \param[in] root XML to search * \param[in] search_comment Comment whose content should be searched for * \param[in] exact If true, comment must also be at same position */ xmlNode * pcmk__xc_match(const xmlNode *root, const xmlNode *search_comment, bool exact) { xmlNode *a_child = NULL; int search_offset = pcmk__xml_position(search_comment, pcmk__xf_skip); CRM_CHECK(search_comment->type == XML_COMMENT_NODE, return NULL); for (a_child = pcmk__xml_first_child(root); a_child != NULL; a_child = pcmk__xml_next(a_child)) { if (exact) { int offset = pcmk__xml_position(a_child, pcmk__xf_skip); xml_node_private_t *nodepriv = a_child->_private; if (offset < search_offset) { continue; } else if (offset > search_offset) { return NULL; } if (pcmk_is_set(nodepriv->flags, pcmk__xf_skip)) { continue; } } if (a_child->type == XML_COMMENT_NODE && pcmk__str_eq((const char *)a_child->content, (const char *)search_comment->content, pcmk__str_casei)) { return a_child; } else if (exact) { return NULL; } } return NULL; } /*! * \internal * \brief Make one XML comment match another (in content) * * \param[in,out] parent If \p target is NULL and this is not, add or update * comment child of this XML node that matches \p update * \param[in,out] target If not NULL, update this XML comment node * \param[in] update Make comment content match this (must not be NULL) * * \note At least one of \parent and \target must be non-NULL */ void pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update) { CRM_CHECK(update != NULL, return); CRM_CHECK(update->type == XML_COMMENT_NODE, return); if (target == NULL) { target = pcmk__xc_match(parent, update, false); } if (target == NULL) { pcmk__xml_copy(parent, update); } else if (!pcmk__str_eq((const char *)target->content, (const char *)update->content, pcmk__str_casei)) { xmlFree(target->content); target->content = xmlStrdup(update->content); } } /*! * \internal * \brief Merge one XML tree into another * * Here, "merge" means: * 1. Copy attribute values from \p update to the target, overwriting in case of * conflict. * 2. Descend through \p update and the target in parallel. At each level, for * each child of \p update, look for a matching child of the target. * a. For each child, if a match is found, go to step 1, recursively merging * the child of \p update into the child of the target. * b. Otherwise, copy the child of \p update as a child of the target. * * A match is defined as the first child of the same type within the target, * with: * * the \c PCMK_XA_ID attribute matching, if set in \p update; otherwise, * * the \c PCMK_XA_ID_REF attribute matching, if set in \p update * * This function does not delete any elements or attributes from the target. It * may add elements or overwrite attributes, as described above. * * \param[in,out] parent If \p target is NULL and this is not, add or update * child of this XML node that matches \p update * \param[in,out] target If not NULL, update this XML * \param[in] update Make the desired XML match this (must not be \c NULL) * \param[in] flags Group of enum pcmk__xa_flags * \param[in] as_diff If \c true, preserve order of attributes (deprecated * since 2.0.5) * * \note At least one of \p parent and \p target must be non-NULL. * \note This function is recursive. For the top-level call, \p parent is * \c NULL and \p target is not \c NULL. For recursive calls, \p target is * \c NULL and \p parent is not \c NULL. */ void pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update, uint32_t flags, bool as_diff) { /* @COMPAT Refactor further and staticize after v1 patchset deprecation. * * @COMPAT Drop as_diff argument when apply_xml_diff() is dropped. */ const char *update_name = NULL; const char *update_id_attr = NULL; const char *update_id_val = NULL; char *trace_s = NULL; crm_log_xml_trace(update, "update"); crm_log_xml_trace(target, "target"); CRM_CHECK(update != NULL, goto done); if (update->type == XML_COMMENT_NODE) { pcmk__xc_update(parent, target, update); goto done; } update_name = (const char *) update->name; CRM_CHECK(update_name != NULL, goto done); CRM_CHECK((target != NULL) || (parent != NULL), goto done); update_id_val = pcmk__xe_id(update); if (update_id_val != NULL) { update_id_attr = PCMK_XA_ID; } else { update_id_val = crm_element_value(update, PCMK_XA_ID_REF); if (update_id_val != NULL) { update_id_attr = PCMK_XA_ID_REF; } } pcmk__if_tracing( { if (update_id_attr != NULL) { trace_s = crm_strdup_printf("<%s %s=%s/>", update_name, update_id_attr, update_id_val); } else { trace_s = crm_strdup_printf("<%s/>", update_name); } }, {} ); if (target == NULL) { // Recursive call target = pcmk__xe_first_child(parent, update_name, update_id_attr, update_id_val); } if (target == NULL) { // Recursive call with no existing matching child target = pcmk__xe_create(parent, update_name); crm_trace("Added %s", pcmk__s(trace_s, update_name)); } else { // Either recursive call with match, or top-level call crm_trace("Found node %s to update", pcmk__s(trace_s, update_name)); } CRM_CHECK(pcmk__xe_is(target, (const char *) update->name), return); if (!as_diff) { pcmk__xe_copy_attrs(target, update, flags); } else { // Preserve order of attributes. Don't use pcmk__xe_copy_attrs(). for (xmlAttrPtr a = pcmk__xe_first_attr(update); a != NULL; a = a->next) { const char *p_value = pcmk__xml_attr_value(a); /* Remove it first so the ordering of the update is preserved */ xmlUnsetProp(target, a->name); xmlSetProp(target, a->name, (pcmkXmlStr) p_value); } } for (xmlNode *child = pcmk__xml_first_child(update); child != NULL; child = pcmk__xml_next(child)) { crm_trace("Updating child of %s", pcmk__s(trace_s, update_name)); pcmk__xml_update(target, NULL, child, flags, as_diff); } crm_trace("Finished with %s", pcmk__s(trace_s, update_name)); done: free(trace_s); } /*! * \internal * \brief Delete an XML subtree if it matches a search element * * A match is defined as follows: * * \p xml and \p user_data are both element nodes of the same type. * * If \p user_data has attributes set, \p xml has those attributes set to the * same values. (\p xml may have additional attributes set to arbitrary * values.) * * \param[in,out] xml XML subtree to delete upon match * \param[in] user_data Search element * * \return \c true to continue traversing the tree, or \c false to stop (because * \p xml was deleted) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool delete_xe_if_matching(xmlNode *xml, void *user_data) { xmlNode *search = user_data; if (!pcmk__xe_is(search, (const char *) xml->name)) { // No match: either not both elements, or different element types return true; } for (const xmlAttr *attr = pcmk__xe_first_attr(search); attr != NULL; attr = attr->next) { const char *search_val = pcmk__xml_attr_value(attr); const char *xml_val = crm_element_value(xml, (const char *) attr->name); if (!pcmk__str_eq(search_val, xml_val, pcmk__str_casei)) { // No match: an attr in xml doesn't match the attr in search return true; } } crm_log_xml_trace(xml, "delete-match"); crm_log_xml_trace(search, "delete-search"); pcmk__xml_free(xml); // Found a match and deleted it; stop traversing tree return false; } /*! * \internal * \brief Search an XML tree depth-first and delete the first matching element * * This function does not attempt to match the tree root (\p xml). * * A match with a node \c node is defined as follows: * * \c node and \p search are both element nodes of the same type. * * If \p search has attributes set, \c node has those attributes set to the * same values. (\c node may have additional attributes set to arbitrary * values.) * * \param[in,out] xml XML subtree to search * \param[in] search Element to match against * * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on * successful deletion and an error code otherwise) */ int pcmk__xe_delete_match(xmlNode *xml, xmlNode *search) { // See @COMPAT comment in pcmk__xe_replace_match() CRM_CHECK((xml != NULL) && (search != NULL), return EINVAL); for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL; xml = pcmk__xe_next(xml)) { if (!pcmk__xml_tree_foreach(xml, delete_xe_if_matching, search)) { // Found and deleted an element return pcmk_rc_ok; } } // No match found in this subtree return ENXIO; } /*! * \internal * \brief Replace one XML node with a copy of another XML node * * This function handles change tracking and applies ACLs. * * \param[in,out] old XML node to replace * \param[in] new XML node to copy as replacement for \p old * * \note This frees \p old. */ static void replace_node(xmlNode *old, xmlNode *new) { new = xmlCopyNode(new, 1); pcmk__mem_assert(new); // May be unnecessary but avoids slight changes to some test outputs pcmk__xml_tree_foreach(new, reset_xml_node_flags, NULL); old = xmlReplaceNode(old, new); if (xml_tracking_changes(new)) { // Replaced sections may have included relevant ACLs pcmk__apply_acl(new); } xml_calculate_changes(old, new); xmlFreeNode(old); } /*! * \internal * \brief Replace one XML subtree with a copy of another if the two match * * A match is defined as follows: * * \p xml and \p user_data are both element nodes of the same type. * * If \p user_data has the \c PCMK_XA_ID attribute set, then \p xml has * \c PCMK_XA_ID set to the same value. * * \param[in,out] xml XML subtree to replace with \p user_data upon match * \param[in] user_data XML to replace \p xml with a copy of upon match * * \return \c true to continue traversing the tree, or \c false to stop (because * \p xml was replaced by \p user_data) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool replace_xe_if_matching(xmlNode *xml, void *user_data) { xmlNode *replace = user_data; const char *xml_id = NULL; const char *replace_id = NULL; xml_id = pcmk__xe_id(xml); replace_id = pcmk__xe_id(replace); if (!pcmk__xe_is(replace, (const char *) xml->name)) { // No match: either not both elements, or different element types return true; } if ((replace_id != NULL) && !pcmk__str_eq(replace_id, xml_id, pcmk__str_none)) { // No match: ID was provided in replace and doesn't match xml's ID return true; } crm_log_xml_trace(xml, "replace-match"); crm_log_xml_trace(replace, "replace-with"); replace_node(xml, replace); // Found a match and replaced it; stop traversing tree return false; } /*! * \internal * \brief Search an XML tree depth-first and replace the first matching element * * This function does not attempt to match the tree root (\p xml). * * A match with a node \c node is defined as follows: * * \c node and \p replace are both element nodes of the same type. * * If \p replace has the \c PCMK_XA_ID attribute set, then \c node has * \c PCMK_XA_ID set to the same value. * * \param[in,out] xml XML tree to search * \param[in] replace XML to replace a matching element with a copy of * * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on * successful replacement and an error code otherwise) */ int pcmk__xe_replace_match(xmlNode *xml, xmlNode *replace) { /* @COMPAT Some of this behavior (like not matching the tree root, which is * allowed by pcmk__xe_update_match()) is questionable for general use but * required for backward compatibility by cib_process_replace() and * cib_process_delete(). Behavior can change at a major version release if * desired. */ CRM_CHECK((xml != NULL) && (replace != NULL), return EINVAL); for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL; xml = pcmk__xe_next(xml)) { if (!pcmk__xml_tree_foreach(xml, replace_xe_if_matching, replace)) { // Found and replaced an element return pcmk_rc_ok; } } // No match found in this subtree return ENXIO; } //! User data for \c update_xe_if_matching() struct update_data { xmlNode *update; //!< Update source uint32_t flags; //!< Group of enum pcmk__xa_flags }; /*! * \internal * \brief Update one XML subtree with another if the two match * * "Update" means to merge a source subtree into a target subtree (see * \c pcmk__xml_update()). * * A match is defined as follows: * * \p xml and \p user_data->update are both element nodes of the same type. * * \p xml and \p user_data->update have the same \c PCMK_XA_ID attribute * value, or \c PCMK_XA_ID is unset in both * * \param[in,out] xml XML subtree to update with \p user_data->update * upon match * \param[in] user_data struct update_data object * * \return \c true to continue traversing the tree, or \c false to stop (because * \p xml was updated by \p user_data->update) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool update_xe_if_matching(xmlNode *xml, void *user_data) { struct update_data *data = user_data; xmlNode *update = data->update; if (!pcmk__xe_is(update, (const char *) xml->name)) { // No match: either not both elements, or different element types return true; } if (!pcmk__str_eq(pcmk__xe_id(xml), pcmk__xe_id(update), pcmk__str_none)) { // No match: ID mismatch return true; } crm_log_xml_trace(xml, "update-match"); crm_log_xml_trace(update, "update-with"); pcmk__xml_update(NULL, xml, update, data->flags, false); // Found a match and replaced it; stop traversing tree return false; } /*! * \internal * \brief Search an XML tree depth-first and update the first matching element * * "Update" means to merge a source subtree into a target subtree (see * \c pcmk__xml_update()). * * A match with a node \c node is defined as follows: * * \c node and \p update are both element nodes of the same type. * * \c node and \p update have the same \c PCMK_XA_ID attribute value, or * \c PCMK_XA_ID is unset in both * * \param[in,out] xml XML tree to search * \param[in] update XML to update a matching element with * \param[in] flags Group of enum pcmk__xa_flags * * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on * successful update and an error code otherwise) */ int pcmk__xe_update_match(xmlNode *xml, xmlNode *update, uint32_t flags) { /* @COMPAT In pcmk__xe_delete_match() and pcmk__xe_replace_match(), we * compare IDs only if the equivalent of the update argument has an ID. * Here, we're stricter: we consider it a mismatch if only one element has * an ID attribute, or if both elements have IDs but they don't match. * * Perhaps we should align the behavior at a major version release. */ struct update_data data = { .update = update, .flags = flags, }; CRM_CHECK((xml != NULL) && (update != NULL), return EINVAL); if (!pcmk__xml_tree_foreach(xml, update_xe_if_matching, &data)) { // Found and updated an element return pcmk_rc_ok; } // No match found in this subtree return ENXIO; } xmlNode * sorted_xml(xmlNode *input, xmlNode *parent, gboolean recursive) { xmlNode *child = NULL; GSList *nvpairs = NULL; xmlNode *result = NULL; CRM_CHECK(input != NULL, return NULL); result = pcmk__xe_create(parent, (const char *) input->name); nvpairs = pcmk_xml_attrs2nvpairs(input); nvpairs = pcmk_sort_nvpairs(nvpairs); pcmk_nvpairs2xml_attrs(nvpairs, result); pcmk_free_nvpairs(nvpairs); for (child = pcmk__xe_first_child(input, NULL, NULL, NULL); child != NULL; child = pcmk__xe_next(child)) { if (recursive) { sorted_xml(child, result, recursive); } else { pcmk__xml_copy(result, child); } } return result; } /*! * \internal * \brief Get next sibling XML element with the same name as a given element * * \param[in] node XML element to start from * * \return Next sibling XML element with same name */ xmlNode * pcmk__xe_next_same(const xmlNode *node) { for (xmlNode *match = pcmk__xe_next(node); match != NULL; match = pcmk__xe_next(match)) { if (pcmk__xe_is(match, (const char *) node->name)) { return match; } } return NULL; } /*! * \internal * \brief Initialize the Pacemaker XML environment * * Set an XML buffer allocation scheme, set XML node create and destroy * callbacks, and load schemas into the cache. */ void pcmk__xml_init(void) { // @TODO Try to find a better caller than crm_log_preinit() static bool initialized = false; if (!initialized) { initialized = true; /* Double the buffer size when the buffer needs to grow. The default * allocator XML_BUFFER_ALLOC_EXACT was found to cause poor performance * due to the number of reallocs. */ xmlSetBufferAllocationScheme(XML_BUFFER_ALLOC_DOUBLEIT); // Initialize private data at node creation xmlRegisterNodeDefault(new_private_data); // Free private data at node destruction xmlDeregisterNodeDefault(free_private_data); // Load schemas into the cache 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(); } /*! * \internal * \brief Get the XML element whose \c PCMK_XA_ID matches an \c PCMK_XA_ID_REF * * \param[in] xml Element whose \c PCMK_XA_ID_REF attribute to check * \param[in] search Node whose document to search for node with matching * \c PCMK_XA_ID (\c NULL to use \p xml) * * \return If \p xml has a \c PCMK_XA_ID_REF attribute, node in * search's document whose \c PCMK_XA_ID attribute matches; * otherwise, \p xml */ xmlNode * pcmk__xe_resolve_idref(xmlNode *xml, xmlNode *search) { char *xpath = NULL; const char *ref = NULL; xmlNode *result = NULL; if (xml == NULL) { return NULL; } ref = crm_element_value(xml, PCMK_XA_ID_REF); if (ref == NULL) { return xml; } if (search == NULL) { search = xml; } xpath = crm_strdup_printf("//%s[@" PCMK_XA_ID "='%s']", xml->name, ref); result = get_xpath_object(xpath, search, LOG_DEBUG); if (result == NULL) { // Not possible with schema validation enabled pcmk__config_err("Ignoring invalid %s configuration: " PCMK_XA_ID_REF " '%s' does not reference " "a valid object " CRM_XS " xpath=%s", xml->name, ref, xpath); } free(xpath); return result; } char * pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns) { static const char *base = NULL; char *ret = NULL; if (base == NULL) { base = pcmk__env_option(PCMK__ENV_SCHEMA_DIRECTORY); } if (pcmk__str_empty(base)) { base = CRM_SCHEMA_DIRECTORY; } switch (ns) { case pcmk__xml_artefact_ns_legacy_rng: case pcmk__xml_artefact_ns_legacy_xslt: ret = strdup(base); break; case pcmk__xml_artefact_ns_base_rng: case pcmk__xml_artefact_ns_base_xslt: ret = crm_strdup_printf("%s/base", base); break; default: crm_err("XML artefact family specified as %u not recognized", ns); } return ret; } static char * find_artefact(enum pcmk__xml_artefact_ns ns, const char *path, const char *filespec) { char *ret = NULL; switch (ns) { case pcmk__xml_artefact_ns_legacy_rng: case pcmk__xml_artefact_ns_base_rng: if (pcmk__ends_with(filespec, ".rng")) { ret = crm_strdup_printf("%s/%s", path, filespec); } else { ret = crm_strdup_printf("%s/%s.rng", path, filespec); } break; case pcmk__xml_artefact_ns_legacy_xslt: case pcmk__xml_artefact_ns_base_xslt: if (pcmk__ends_with(filespec, ".xsl")) { ret = crm_strdup_printf("%s/%s", path, filespec); } else { ret = crm_strdup_printf("%s/%s.xsl", path, filespec); } break; default: crm_err("XML artefact family specified as %u not recognized", ns); } return ret; } char * pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec) { struct stat sb; char *base = pcmk__xml_artefact_root(ns); char *ret = NULL; ret = find_artefact(ns, base, filespec); free(base); if (stat(ret, &sb) != 0 || !S_ISREG(sb.st_mode)) { const char *remote_schema_dir = pcmk__remote_schema_dir(); free(ret); ret = find_artefact(ns, remote_schema_dir, filespec); } return ret; } void pcmk__xe_set_propv(xmlNodePtr node, va_list pairs) { while (true) { const char *name, *value; name = va_arg(pairs, const char *); if (name == NULL) { return; } value = va_arg(pairs, const char *); if (value != NULL) { crm_xml_add(node, name, value); } } } void pcmk__xe_set_props(xmlNodePtr node, ...) { va_list pairs; va_start(pairs, node); pcmk__xe_set_propv(node, pairs); va_end(pairs); } int pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name, int (*handler)(xmlNode *xml, void *userdata), void *userdata) { xmlNode *children = (xml? xml->children : NULL); CRM_ASSERT(handler != NULL); for (xmlNode *node = children; node != NULL; node = node->next) { if ((node->type == XML_ELEMENT_NODE) && ((child_element_name == NULL) || pcmk__xe_is(node, child_element_name))) { int rc = handler(node, userdata); if (rc != pcmk_rc_ok) { return rc; } } } return pcmk_rc_ok; } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include -xmlNode * -find_entity(xmlNode *parent, const char *node_name, const char *id) -{ - return pcmk__xe_first_child(parent, node_name, - ((id == NULL)? id : PCMK_XA_ID), id); -} - -void -crm_destroy_xml(gpointer data) -{ - pcmk__xml_free(data); -} - -xmlDoc * -getDocPtr(xmlNode *node) -{ - xmlDoc *doc = NULL; - - CRM_CHECK(node != NULL, return NULL); - - doc = node->doc; - if (doc == NULL) { - doc = xmlNewDoc(PCMK__XML_VERSION); - xmlDocSetRootElement(doc, node); - } - return doc; -} - -xmlNode * -add_node_copy(xmlNode *parent, xmlNode *src_node) -{ - xmlNode *child = NULL; - - CRM_CHECK((parent != NULL) && (src_node != NULL), return NULL); - - child = xmlDocCopyNode(src_node, parent->doc, 1); - if (child == NULL) { - return NULL; - } - xmlAddChild(parent, child); - pcmk__xml_mark_created(child); - return child; -} - -int -add_node_nocopy(xmlNode *parent, const char *name, xmlNode *child) -{ - add_node_copy(parent, child); - pcmk__xml_free(child); - return 1; -} - -gboolean -xml_has_children(const xmlNode * xml_root) -{ - if (xml_root != NULL && xml_root->children != NULL) { - return TRUE; - } - return FALSE; -} - -static char * -replace_text(char *text, size_t *index, size_t *length, const char *replace) -{ - // We have space for 1 char already - size_t offset = strlen(replace) - 1; - - if (offset > 0) { - *length += offset; - text = pcmk__realloc(text, *length + 1); - - // Shift characters to the right to make room for the replacement string - for (size_t i = *length; i > (*index + offset); i--) { - text[i] = text[i - offset]; - } - } - - // Replace the character at index by the replacement string - memcpy(text + *index, replace, offset + 1); - - // Reset index to the end of replacement string - *index += offset; - return text; -} - -char * -crm_xml_escape(const char *text) -{ - size_t length = 0; - char *copy = NULL; - - if (text == NULL) { - return NULL; - } - - length = strlen(text); - copy = pcmk__str_copy(text); - for (size_t index = 0; index <= length; index++) { - if(copy[index] & 0x80 && copy[index+1] & 0x80){ - index++; - continue; - } - switch (copy[index]) { - case 0: - // Sanity only; loop should stop at the last non-null byte - break; - case '<': - copy = replace_text(copy, &index, &length, "<"); - break; - case '>': - copy = replace_text(copy, &index, &length, ">"); - break; - case '"': - copy = replace_text(copy, &index, &length, """); - break; - case '\'': - copy = replace_text(copy, &index, &length, "'"); - break; - case '&': - copy = replace_text(copy, &index, &length, "&"); - break; - case '\t': - /* Might as well just expand to a few spaces... */ - copy = replace_text(copy, &index, &length, " "); - break; - case '\n': - copy = replace_text(copy, &index, &length, "\\n"); - break; - case '\r': - copy = replace_text(copy, &index, &length, "\\r"); - break; - default: - /* Check for and replace non-printing characters with their octal equivalent */ - if(copy[index] < ' ' || copy[index] > '~') { - char *replace = crm_strdup_printf("\\%.3o", copy[index]); - - copy = replace_text(copy, &index, &length, replace); - free(replace); - } - } - } - return copy; -} - xmlNode * copy_xml(xmlNode *src) { xmlDoc *doc = xmlNewDoc(PCMK__XML_VERSION); xmlNode *copy = NULL; pcmk__mem_assert(doc); copy = xmlDocCopyNode(src, doc, 1); pcmk__mem_assert(copy); xmlDocSetRootElement(doc, copy); return copy; } -xmlNode * -create_xml_node(xmlNode *parent, const char *name) -{ - // Like pcmk__xe_create(), but returns NULL on failure - xmlNode *node = NULL; - - CRM_CHECK(!pcmk__str_empty(name), return NULL); - - if (parent == NULL) { - xmlDoc *doc = xmlNewDoc(PCMK__XML_VERSION); - - if (doc == NULL) { - return NULL; - } - - node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL); - if (node == NULL) { - xmlFreeDoc(doc); - return NULL; - } - xmlDocSetRootElement(doc, node); - - } else { - node = xmlNewChild(parent, NULL, (pcmkXmlStr) name, NULL); - if (node == NULL) { - return NULL; - } - } - pcmk__xml_mark_created(node); - return node; -} - -xmlNode * -pcmk_create_xml_text_node(xmlNode *parent, const char *name, - const char *content) -{ - xmlNode *node = pcmk__xe_create(parent, name); - - pcmk__xe_set_content(node, "%s", content); - return node; -} - -xmlNode * -pcmk_create_html_node(xmlNode *parent, const char *element_name, const char *id, - const char *class_name, const char *text) -{ - xmlNode *node = pcmk__html_create(parent, element_name, id, class_name); - - pcmk__xe_set_content(node, "%s", text); - return node; -} - -xmlNode * -first_named_child(const xmlNode *parent, const char *name) -{ - return pcmk__xe_first_child(parent, name, NULL, NULL); -} - -xmlNode * -find_xml_node(const xmlNode *root, const char *search_path, gboolean must_find) -{ - xmlNode *result = NULL; - - if (search_path == NULL) { - crm_warn("Will never find "); - return NULL; - } - - result = pcmk__xe_first_child(root, search_path, NULL, NULL); - - if (must_find && (result == NULL)) { - crm_warn("Could not find %s in %s", - search_path, - ((root != NULL)? (const char *) root->name : "")); - } - - return result; -} - -xmlNode * -crm_next_same_xml(const xmlNode *sibling) -{ - return pcmk__xe_next_same(sibling); -} - -void -xml_remove_prop(xmlNode * obj, const char *name) -{ - pcmk__xe_remove_attr(obj, name); -} - -gboolean -replace_xml_child(xmlNode * parent, xmlNode * child, xmlNode * update, gboolean delete_only) -{ - bool is_match = false; - const char *child_id = NULL; - const char *update_id = NULL; - - CRM_CHECK(child != NULL, return FALSE); - CRM_CHECK(update != NULL, return FALSE); - - child_id = pcmk__xe_id(child); - update_id = pcmk__xe_id(update); - - /* Match element name and (if provided in update XML) element ID. Don't - * match search root (child is search root if parent == NULL). - */ - is_match = (parent != NULL) - && pcmk__xe_is(update, (const char *) child->name) - && ((update_id == NULL) - || pcmk__str_eq(update_id, child_id, pcmk__str_none)); - - /* For deletion, match all attributes provided in update. A matching node - * can have additional attributes, but values must match for provided ones. - */ - if (is_match && delete_only) { - for (xmlAttr *attr = pcmk__xe_first_attr(update); attr != NULL; - attr = attr->next) { - const char *name = (const char *) attr->name; - const char *update_val = pcmk__xml_attr_value(attr); - const char *child_val = crm_element_value(child, name); - - if (!pcmk__str_eq(update_val, child_val, pcmk__str_casei)) { - is_match = false; - break; - } - } - } - - if (is_match) { - if (delete_only) { - crm_log_xml_trace(child, "delete-match"); - crm_log_xml_trace(update, "delete-search"); - pcmk__xml_free(child); - - } else { - crm_log_xml_trace(child, "replace-match"); - crm_log_xml_trace(update, "replace-with"); - replace_node(child, update); - } - return TRUE; - } - - // Current node not a match; search the rest of the subtree depth-first - parent = child; - for (child = pcmk__xml_first_child(parent); child != NULL; - child = pcmk__xml_next(child)) { - - // Only delete/replace the first match - if (replace_xml_child(parent, child, update, delete_only)) { - return TRUE; - } - } - - // No match found in this subtree - return FALSE; -} - -gboolean -update_xml_child(xmlNode *child, xmlNode *to_update) -{ - return pcmk__xe_update_match(child, to_update, - pcmk__xaf_score_update) == pcmk_rc_ok; -} - -int -find_xml_children(xmlNode **children, xmlNode *root, const char *tag, - const char *field, const char *value, gboolean search_matches) -{ - int match_found = 0; - - CRM_CHECK(root != NULL, return FALSE); - CRM_CHECK(children != NULL, return FALSE); - - if ((tag != NULL) && !pcmk__xe_is(root, tag)) { - - } else if ((value != NULL) - && !pcmk__str_eq(value, crm_element_value(root, field), - pcmk__str_casei)) { - - } else { - if (*children == NULL) { - *children = pcmk__xe_create(NULL, __func__); - } - pcmk__xml_copy(*children, root); - match_found = 1; - } - - if (search_matches || match_found == 0) { - xmlNode *child = NULL; - - for (child = pcmk__xml_first_child(root); child != NULL; - child = pcmk__xml_next(child)) { - match_found += find_xml_children(children, child, tag, field, value, - search_matches); - } - } - - return match_found; -} - -void -fix_plus_plus_recursive(xmlNode *target) -{ - /* TODO: Remove recursion and use xpath searches for value++ */ - xmlNode *child = NULL; - - for (xmlAttrPtr a = pcmk__xe_first_attr(target); a != NULL; a = a->next) { - const char *p_name = (const char *) a->name; - const char *p_value = pcmk__xml_attr_value(a); - - expand_plus_plus(target, p_name, p_value); - } - for (child = pcmk__xe_first_child(target, NULL, NULL, NULL); child != NULL; - child = pcmk__xe_next(child)) { - - fix_plus_plus_recursive(child); - } -} - -void -copy_in_properties(xmlNode *target, const xmlNode *src) -{ - if (src == NULL) { - crm_warn("No node to copy properties from"); - - } else if (target == NULL) { - crm_err("No node to copy properties into"); - - } else { - for (xmlAttrPtr a = pcmk__xe_first_attr(src); a != NULL; a = a->next) { - const char *p_name = (const char *) a->name; - const char *p_value = pcmk__xml_attr_value(a); - - expand_plus_plus(target, p_name, p_value); - if (xml_acl_denied(target)) { - crm_trace("Cannot copy %s=%s to %s", p_name, p_value, target->name); - return; - } - } - } -} - -void -expand_plus_plus(xmlNode * target, const char *name, const char *value) -{ - pcmk__xe_set_score(target, name, value); -} - void crm_xml_init(void) { pcmk__xml_init(); } void crm_xml_cleanup(void) { pcmk__xml_cleanup(); } void pcmk_free_xml_subtree(xmlNode *xml) { xmlUnlinkNode(xml); // Detaches from parent and siblings xmlFreeNode(xml); // Frees } void free_xml(xmlNode *child) { pcmk__xml_free(child); } xmlNode * expand_idref(xmlNode *input, xmlNode *top) { return pcmk__xe_resolve_idref(input, top); } // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/common/xml_display.c b/lib/common/xml_display.c index b563d3a6dd..502051ea8c 100644 --- a/lib/common/xml_display.c +++ b/lib/common/xml_display.c @@ -1,546 +1,401 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include // PCMK__XML_LOG_BASE, etc. #include "crmcommon_private.h" static int show_xml_node(pcmk__output_t *out, GString *buffer, const char *prefix, const xmlNode *data, int depth, uint32_t options); // Log an XML library error void pcmk__log_xmllib_err(void *ctx, const char *fmt, ...) { va_list ap; va_start(ap, fmt); pcmk__if_tracing( { PCMK__XML_LOG_BASE(LOG_ERR, TRUE, crm_abort(__FILE__, __PRETTY_FUNCTION__, __LINE__, "xml library error", TRUE, TRUE), "XML Error: ", fmt, ap); }, { PCMK__XML_LOG_BASE(LOG_ERR, TRUE, 0, "XML Error: ", fmt, ap); } ); va_end(ap); } /*! * \internal * \brief Output an XML comment with depth-based indentation * * \param[in,out] out Output object * \param[in] data XML node to output * \param[in] depth Current indentation level * \param[in] options Group of \p pcmk__xml_fmt_options flags * * \return Standard Pacemaker return code * * \note This currently produces output only for text-like output objects. */ static int show_xml_comment(pcmk__output_t *out, const xmlNode *data, int depth, uint32_t options) { if (pcmk_is_set(options, pcmk__xml_fmt_open)) { int width = pcmk_is_set(options, pcmk__xml_fmt_pretty)? (2 * depth) : 0; return out->info(out, "%*s", width, "", (const char *) data->content); } return pcmk_rc_no_output; } /*! * \internal * \brief Output an XML element in a formatted way * * \param[in,out] out Output object * \param[in,out] buffer Where to build output strings * \param[in] prefix String to prepend to every line of output * \param[in] data XML node to output * \param[in] depth Current indentation level * \param[in] options Group of \p pcmk__xml_fmt_options flags * * \return Standard Pacemaker return code * * \note This is a recursive helper function for \p show_xml_node(). * \note This currently produces output only for text-like output objects. * \note \p buffer may be overwritten many times. The caller is responsible for * freeing it using \p g_string_free() but should not rely on its * contents. */ static int show_xml_element(pcmk__output_t *out, GString *buffer, const char *prefix, const xmlNode *data, int depth, uint32_t options) { int spaces = pcmk_is_set(options, pcmk__xml_fmt_pretty)? (2 * depth) : 0; int rc = pcmk_rc_no_output; if (pcmk_is_set(options, pcmk__xml_fmt_open)) { const char *hidden = crm_element_value(data, PCMK__XA_HIDDEN); g_string_truncate(buffer, 0); for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } pcmk__g_strcat(buffer, "<", data->name, NULL); for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL; attr = attr->next) { xml_node_private_t *nodepriv = attr->_private; const char *p_name = (const char *) attr->name; const char *p_value = pcmk__xml_attr_value(attr); gchar *p_copy = NULL; if (pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) { continue; } // @COMPAT Remove when v1 patchsets are removed if (pcmk_any_flags_set(options, pcmk__xml_fmt_diff_plus |pcmk__xml_fmt_diff_minus) && (strcmp(PCMK__XA_CRM_DIFF_MARKER, p_name) == 0)) { continue; } if ((hidden != NULL) && (p_name[0] != '\0') && (strstr(hidden, p_name) != NULL)) { p_value = "*****"; } else { p_copy = pcmk__xml_escape(p_value, true); p_value = p_copy; } pcmk__g_strcat(buffer, " ", p_name, "=\"", pcmk__s(p_value, ""), "\"", NULL); g_free(p_copy); } if ((data->children != NULL) && pcmk_is_set(options, pcmk__xml_fmt_children)) { g_string_append_c(buffer, '>'); } else { g_string_append(buffer, "/>"); } rc = out->info(out, "%s%s%s", pcmk__s(prefix, ""), pcmk__str_empty(prefix)? "" : " ", buffer->str); } if (data->children == NULL) { return rc; } if (pcmk_is_set(options, pcmk__xml_fmt_children)) { for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL; child = pcmk__xml_next(child)) { int temp_rc = show_xml_node(out, buffer, prefix, child, depth + 1, options |pcmk__xml_fmt_open |pcmk__xml_fmt_close); rc = pcmk__output_select_rc(rc, temp_rc); } } if (pcmk_is_set(options, pcmk__xml_fmt_close)) { int temp_rc = out->info(out, "%s%s%*s", pcmk__s(prefix, ""), pcmk__str_empty(prefix)? "" : " ", spaces, "", data->name); rc = pcmk__output_select_rc(rc, temp_rc); } return rc; } /*! * \internal * \brief Output an XML element or comment in a formatted way * * \param[in,out] out Output object * \param[in,out] buffer Where to build output strings * \param[in] prefix String to prepend to every line of output * \param[in] data XML node to log * \param[in] depth Current indentation level * \param[in] options Group of \p pcmk__xml_fmt_options flags * * \return Standard Pacemaker return code * * \note This is a recursive helper function for \p pcmk__xml_show(). * \note This currently produces output only for text-like output objects. * \note \p buffer may be overwritten many times. The caller is responsible for * freeing it using \p g_string_free() but should not rely on its * contents. */ static int show_xml_node(pcmk__output_t *out, GString *buffer, const char *prefix, const xmlNode *data, int depth, uint32_t options) { switch (data->type) { case XML_COMMENT_NODE: return show_xml_comment(out, data, depth, options); case XML_ELEMENT_NODE: return show_xml_element(out, buffer, prefix, data, depth, options); default: return pcmk_rc_no_output; } } /*! * \internal * \brief Output an XML element or comment in a formatted way * * \param[in,out] out Output object * \param[in] prefix String to prepend to every line of output * \param[in] data XML node to output * \param[in] depth Current nesting level * \param[in] options Group of \p pcmk__xml_fmt_options flags * * \return Standard Pacemaker return code * * \note This currently produces output only for text-like output objects. */ int pcmk__xml_show(pcmk__output_t *out, const char *prefix, const xmlNode *data, int depth, uint32_t options) { int rc = pcmk_rc_no_output; GString *buffer = NULL; CRM_ASSERT(out != NULL); CRM_CHECK(depth >= 0, depth = 0); if (data == NULL) { return rc; } /* Allocate a buffer once, for show_xml_node() to truncate and reuse in * recursive calls */ buffer = g_string_sized_new(1024); rc = show_xml_node(out, buffer, prefix, data, depth, options); g_string_free(buffer, TRUE); return rc; } /*! * \internal * \brief Output XML portions that have been marked as changed * * \param[in,out] out Output object * \param[in] data XML node to output * \param[in] depth Current indentation level * \param[in] options Group of \p pcmk__xml_fmt_options flags * * \note This is a recursive helper for \p pcmk__xml_show_changes(), showing * changes to \p data and its children. * \note This currently produces output only for text-like output objects. */ static int show_xml_changes_recursive(pcmk__output_t *out, const xmlNode *data, int depth, uint32_t options) { /* @COMPAT: When log_data_element() is removed, we can remove the options * argument here and instead hard-code pcmk__xml_log_pretty. */ xml_node_private_t *nodepriv = (xml_node_private_t *) data->_private; int rc = pcmk_rc_no_output; int temp_rc = pcmk_rc_no_output; if (pcmk_all_flags_set(nodepriv->flags, pcmk__xf_dirty|pcmk__xf_created)) { // Newly created return pcmk__xml_show(out, PCMK__XML_PREFIX_CREATED, data, depth, options |pcmk__xml_fmt_open |pcmk__xml_fmt_children |pcmk__xml_fmt_close); } if (pcmk_is_set(nodepriv->flags, pcmk__xf_dirty)) { // Modified or moved bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); int spaces = pretty? (2 * depth) : 0; const char *prefix = PCMK__XML_PREFIX_MODIFIED; if (pcmk_is_set(nodepriv->flags, pcmk__xf_moved)) { prefix = PCMK__XML_PREFIX_MOVED; } // Log opening tag rc = pcmk__xml_show(out, prefix, data, depth, options|pcmk__xml_fmt_open); // Log changes to attributes for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL; attr = attr->next) { const char *name = (const char *) attr->name; nodepriv = attr->_private; if (pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) { const char *value = pcmk__xml_attr_value(attr); temp_rc = out->info(out, "%s %*s @%s=%s", PCMK__XML_PREFIX_DELETED, spaces, "", name, value); } else if (pcmk_is_set(nodepriv->flags, pcmk__xf_dirty)) { const char *value = pcmk__xml_attr_value(attr); if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { prefix = PCMK__XML_PREFIX_CREATED; } else if (pcmk_is_set(nodepriv->flags, pcmk__xf_modified)) { prefix = PCMK__XML_PREFIX_MODIFIED; } else if (pcmk_is_set(nodepriv->flags, pcmk__xf_moved)) { prefix = PCMK__XML_PREFIX_MOVED; } else { prefix = PCMK__XML_PREFIX_MODIFIED; } temp_rc = out->info(out, "%s %*s @%s=%s", prefix, spaces, "", name, value); } rc = pcmk__output_select_rc(rc, temp_rc); } // Log changes to children for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL; child = pcmk__xml_next(child)) { temp_rc = show_xml_changes_recursive(out, child, depth + 1, options); rc = pcmk__output_select_rc(rc, temp_rc); } // Log closing tag temp_rc = pcmk__xml_show(out, PCMK__XML_PREFIX_MODIFIED, data, depth, options|pcmk__xml_fmt_close); return pcmk__output_select_rc(rc, temp_rc); } // This node hasn't changed, but check its children for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL; child = pcmk__xml_next(child)) { temp_rc = show_xml_changes_recursive(out, child, depth + 1, options); rc = pcmk__output_select_rc(rc, temp_rc); } return rc; } /*! * \internal * \brief Output changes to an XML node and any children * * \param[in,out] out Output object * \param[in] xml XML node to output * * \return Standard Pacemaker return code * * \note This currently produces output only for text-like output objects. */ int pcmk__xml_show_changes(pcmk__output_t *out, const xmlNode *xml) { xml_doc_private_t *docpriv = NULL; int rc = pcmk_rc_no_output; int temp_rc = pcmk_rc_no_output; CRM_ASSERT(out != NULL); CRM_ASSERT(xml != NULL); CRM_ASSERT(xml->doc != NULL); docpriv = xml->doc->_private; if (!pcmk_is_set(docpriv->flags, pcmk__xf_dirty)) { return rc; } for (const GList *iter = docpriv->deleted_objs; iter != NULL; iter = iter->next) { const pcmk__deleted_xml_t *deleted_obj = iter->data; if (deleted_obj->position >= 0) { temp_rc = out->info(out, PCMK__XML_PREFIX_DELETED " %s (%d)", deleted_obj->path, deleted_obj->position); } else { temp_rc = out->info(out, PCMK__XML_PREFIX_DELETED " %s", deleted_obj->path); } rc = pcmk__output_select_rc(rc, temp_rc); } temp_rc = show_xml_changes_recursive(out, xml, 0, pcmk__xml_fmt_pretty); return pcmk__output_select_rc(rc, temp_rc); } - -// Deprecated functions kept only for backward API compatibility -// LCOV_EXCL_START - -#include -#include - -void -log_data_element(int log_level, const char *file, const char *function, - int line, const char *prefix, const xmlNode *data, int depth, - int legacy_options) -{ - uint32_t options = 0; - pcmk__output_t *out = NULL; - - // Confine log_level to uint8_t range - log_level = pcmk__clip_log_level(log_level); - - if (data == NULL) { - do_crm_log(log_level, "%s%sNo data to dump as XML", - pcmk__s(prefix, ""), pcmk__str_empty(prefix)? "" : " "); - return; - } - - switch (log_level) { - case LOG_NEVER: - return; - case LOG_STDOUT: - CRM_CHECK(pcmk__text_output_new(&out, NULL) == pcmk_rc_ok, return); - break; - default: - CRM_CHECK(pcmk__log_output_new(&out) == pcmk_rc_ok, return); - pcmk__output_set_log_level(out, log_level); - break; - } - - /* Map xml_log_options to pcmk__xml_fmt_options so that we can go ahead and - * start using the pcmk__xml_fmt_options in all the internal functions. - * - * xml_log_option_dirty_add and xml_log_option_diff_all are ignored by - * internal code and only used here, so they don't need to be addressed. - */ - if (pcmk_is_set(legacy_options, xml_log_option_filtered)) { - options |= pcmk__xml_fmt_filtered; - } - if (pcmk_is_set(legacy_options, xml_log_option_formatted)) { - options |= pcmk__xml_fmt_pretty; - } - if (pcmk_is_set(legacy_options, xml_log_option_open)) { - options |= pcmk__xml_fmt_open; - } - if (pcmk_is_set(legacy_options, xml_log_option_children)) { - options |= pcmk__xml_fmt_children; - } - if (pcmk_is_set(legacy_options, xml_log_option_close)) { - options |= pcmk__xml_fmt_close; - } - if (pcmk_is_set(legacy_options, xml_log_option_text)) { - options |= pcmk__xml_fmt_text; - } - if (pcmk_is_set(legacy_options, xml_log_option_diff_plus)) { - options |= pcmk__xml_fmt_diff_plus; - } - if (pcmk_is_set(legacy_options, xml_log_option_diff_minus)) { - options |= pcmk__xml_fmt_diff_minus; - } - if (pcmk_is_set(legacy_options, xml_log_option_diff_short)) { - options |= pcmk__xml_fmt_diff_short; - } - - // Log element based on options - if (pcmk_is_set(legacy_options, xml_log_option_dirty_add)) { - CRM_CHECK(depth >= 0, depth = 0); - show_xml_changes_recursive(out, data, depth, options); - goto done; - } - - if (pcmk_is_set(options, pcmk__xml_fmt_pretty) - && ((data->children == NULL) - || (crm_element_value(data, PCMK__XA_CRM_DIFF_MARKER) != NULL))) { - - if (pcmk_is_set(options, pcmk__xml_fmt_diff_plus)) { - legacy_options |= xml_log_option_diff_all; - prefix = PCMK__XML_PREFIX_CREATED; - - } else if (pcmk_is_set(options, pcmk__xml_fmt_diff_minus)) { - legacy_options |= xml_log_option_diff_all; - prefix = PCMK__XML_PREFIX_DELETED; - } - } - - if (pcmk_is_set(options, pcmk__xml_fmt_diff_short) - && !pcmk_is_set(legacy_options, xml_log_option_diff_all)) { - - if (!pcmk_any_flags_set(options, - pcmk__xml_fmt_diff_plus - |pcmk__xml_fmt_diff_minus)) { - // Nothing will ever be logged - goto done; - } - - // Keep looking for the actual change - for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL; - child = pcmk__xml_next(child)) { - log_data_element(log_level, file, function, line, prefix, child, - depth + 1, options); - } - - } else { - pcmk__xml_show(out, prefix, data, depth, - options - |pcmk__xml_fmt_open - |pcmk__xml_fmt_children - |pcmk__xml_fmt_close); - } - -done: - out->finish(out, CRM_EX_OK, true, NULL); - pcmk__output_free(out); -} - -void -xml_log_changes(uint8_t log_level, const char *function, const xmlNode *xml) -{ - pcmk__output_t *out = NULL; - int rc = pcmk_rc_ok; - - switch (log_level) { - case LOG_NEVER: - return; - case LOG_STDOUT: - CRM_CHECK(pcmk__text_output_new(&out, NULL) == pcmk_rc_ok, return); - break; - default: - CRM_CHECK(pcmk__log_output_new(&out) == pcmk_rc_ok, return); - pcmk__output_set_log_level(out, log_level); - break; - } - rc = pcmk__xml_show_changes(out, xml); - out->finish(out, pcmk_rc2exitc(rc), true, NULL); - pcmk__output_free(out); -} - -// LCOV_EXCL_STOP -// End deprecated API diff --git a/lib/common/xml_io.c b/lib/common/xml_io.c index f88e0b523e..b84395233a 100644 --- a/lib/common/xml_io.c +++ b/lib/common/xml_io.c @@ -1,840 +1,840 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include // xmlOutputBuffer* #include #include #include #include "crmcommon_private.h" /* @COMPAT XML_PARSE_RECOVER allows some XML errors to be silently worked around * by libxml2, which is potentially ambiguous and dangerous. We should drop it * when we can break backward compatibility with configurations that might be * relying on it (i.e. pacemaker 3.0.0). */ #define PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER (XML_PARSE_NOBLANKS) #define PCMK__XML_PARSE_OPTS_WITH_RECOVER (XML_PARSE_NOBLANKS \ |XML_PARSE_RECOVER) /*! * \internal * \brief Read from \c stdin until EOF or error * * \return Newly allocated string containing the bytes read from \c stdin, or * \c NULL on error * * \note The caller is responsible for freeing the return value using \c free(). */ static char * read_stdin(void) { char *buf = NULL; size_t length = 0; do { buf = pcmk__realloc(buf, length + PCMK__BUFFER_SIZE + 1); length += fread(buf + length, 1, PCMK__BUFFER_SIZE, stdin); } while ((feof(stdin) == 0) && (ferror(stdin) == 0)); if (ferror(stdin) != 0) { crm_err("Error reading input from stdin"); free(buf); buf = NULL; } else { buf[length] = '\0'; } clearerr(stdin); return buf; } /*! * \internal * \brief Decompress a bzip2-compressed file into a string buffer * * \param[in] filename Name of file to decompress * * \return Newly allocated string with the decompressed contents of \p filename, * or \c NULL on error. * * \note The caller is responsible for freeing the return value using \c free(). */ static char * decompress_file(const char *filename) { char *buffer = NULL; int rc = pcmk_rc_ok; size_t length = 0; BZFILE *bz_file = NULL; FILE *input = fopen(filename, "r"); if (input == NULL) { crm_perror(LOG_ERR, "Could not open %s for reading", filename); return NULL; } bz_file = BZ2_bzReadOpen(&rc, input, 0, 0, NULL, 0); rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { crm_err("Could not prepare to read compressed %s: %s " CRM_XS " rc=%d", filename, pcmk_rc_str(rc), rc); goto done; } // cppcheck seems not to understand the abort-logic in pcmk__realloc // cppcheck-suppress memleak do { int read_len = 0; buffer = pcmk__realloc(buffer, length + PCMK__BUFFER_SIZE + 1); read_len = BZ2_bzRead(&rc, bz_file, buffer + length, PCMK__BUFFER_SIZE); if ((rc == BZ_OK) || (rc == BZ_STREAM_END)) { crm_trace("Read %ld bytes from file: %d", (long) read_len, rc); length += read_len; } } while (rc == BZ_OK); rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { rc = pcmk__bzlib2rc(rc); crm_err("Could not read compressed %s: %s " CRM_XS " rc=%d", filename, pcmk_rc_str(rc), rc); free(buffer); buffer = NULL; } else { buffer[length] = '\0'; } done: BZ2_bzReadClose(&rc, bz_file); fclose(input); return buffer; } // @COMPAT Remove macro at 3.0.0 when we drop XML_PARSE_RECOVER /*! * \internal * \brief Try to parse XML first without and then with recovery enabled * * \param[out] result Where to store the resulting XML doc (xmlDoc **) * \param[in] fn XML parser function * \param[in] ... All arguments for \p fn except the final one (an * \c xmlParserOption group) */ #define parse_xml_recover(result, fn, ...) do { \ *result = fn(__VA_ARGS__, PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER); \ if (*result == NULL) { \ *result = fn(__VA_ARGS__, PCMK__XML_PARSE_OPTS_WITH_RECOVER); \ \ if (*result != NULL) { \ crm_warn("Successfully recovered from XML errors " \ "(note: a future release will treat this as a " \ "fatal failure)"); \ } \ } \ } while (0); /*! * \internal * \brief Parse XML from a file * * \param[in] filename Name of file containing XML (\c NULL or \c "-" for * \c stdin); if \p filename ends in \c ".bz2", the file * will be decompressed using \c bzip2 * * \return XML tree parsed from the given file; may be \c NULL or only partial * on error */ xmlNode * pcmk__xml_read(const char *filename) { bool use_stdin = pcmk__str_eq(filename, "-", pcmk__str_null_matches); xmlNode *xml = NULL; xmlDoc *output = NULL; xmlParserCtxt *ctxt = NULL; const xmlError *last_error = NULL; // Create a parser context ctxt = xmlNewParserCtxt(); CRM_CHECK(ctxt != NULL, return NULL); xmlCtxtResetLastError(ctxt); xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err); if (use_stdin) { /* @COMPAT After dropping XML_PARSE_RECOVER, we can avoid capturing * stdin into a buffer and instead call * xmlCtxtReadFd(ctxt, STDIN_FILENO, NULL, NULL, XML_PARSE_NOBLANKS); * * For now we have to save the input so that we can use it twice. */ char *input = read_stdin(); if (input != NULL) { parse_xml_recover(&output, xmlCtxtReadDoc, ctxt, (pcmkXmlStr) input, NULL, NULL); free(input); } } else if (pcmk__ends_with_ext(filename, ".bz2")) { char *input = decompress_file(filename); if (input != NULL) { parse_xml_recover(&output, xmlCtxtReadDoc, ctxt, (pcmkXmlStr) input, NULL, NULL); free(input); } } else { parse_xml_recover(&output, xmlCtxtReadFile, ctxt, filename, NULL); } if (output != NULL) { xml = xmlDocGetRootElement(output); if (xml != NULL) { /* @TODO Should we really be stripping out text? This seems like an * overly broad way to get rid of whitespace, if that's the goal. * Text nodes may be invalid in most or all Pacemaker inputs, but * stripping them in a generic "parse XML from file" function may * not be the best way to ignore them. */ pcmk__strip_xml_text(xml); } } // @COMPAT At 3.0.0, free xml and return NULL if xml != NULL on error last_error = xmlCtxtGetLastError(ctxt); if (last_error != NULL) { if (xml != NULL) { crm_log_xml_info(xml, "Partial"); } } xmlFreeParserCtxt(ctxt); return xml; } /*! * \internal * \brief Parse XML from a string * * \param[in] input String to parse * * \return XML tree parsed from the given string; may be \c NULL or only partial * on error */ xmlNode * pcmk__xml_parse(const char *input) { xmlNode *xml = NULL; xmlDoc *output = NULL; xmlParserCtxt *ctxt = NULL; const xmlError *last_error = NULL; if (input == NULL) { return NULL; } ctxt = xmlNewParserCtxt(); if (ctxt == NULL) { return NULL; } xmlCtxtResetLastError(ctxt); xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err); parse_xml_recover(&output, xmlCtxtReadDoc, ctxt, (pcmkXmlStr) input, NULL, NULL); if (output != NULL) { xml = xmlDocGetRootElement(output); } // @COMPAT At 3.0.0, free xml and return NULL if xml != NULL; update doxygen last_error = xmlCtxtGetLastError(ctxt); if (last_error != NULL) { if (xml != NULL) { crm_log_xml_info(xml, "Partial"); } } xmlFreeParserCtxt(ctxt); return xml; } /*! * \internal * \brief Append a string representation of an XML element to a buffer * * \param[in] data XML whose representation to append * \param[in] options Group of \p pcmk__xml_fmt_options flags * \param[in,out] buffer Where to append the content (must not be \p NULL) * \param[in] depth Current indentation level */ static void dump_xml_element(const xmlNode *data, uint32_t options, GString *buffer, int depth) { bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); bool filtered = pcmk_is_set(options, pcmk__xml_fmt_filtered); int spaces = pretty? (2 * depth) : 0; for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } pcmk__g_strcat(buffer, "<", data->name, NULL); for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL; attr = attr->next) { if (!filtered || !pcmk__xa_filterable((const char *) (attr->name))) { pcmk__dump_xml_attr(attr, buffer); } } if (data->children == NULL) { g_string_append(buffer, "/>"); } else { g_string_append_c(buffer, '>'); } if (pretty) { g_string_append_c(buffer, '\n'); } if (data->children) { for (const xmlNode *child = data->children; child != NULL; child = child->next) { pcmk__xml_string(child, options, buffer, depth + 1); } for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } pcmk__g_strcat(buffer, "name, ">", NULL); if (pretty) { g_string_append_c(buffer, '\n'); } } } /*! * \internal * \brief Append XML text content to a buffer * * \param[in] data XML whose content to append - * \param[in] options Group of \p xml_log_options flags + * \param[in] options Group of enum pcmk__xml_fmt_options * \param[in,out] buffer Where to append the content (must not be \p NULL) * \param[in] depth Current indentation level */ static void dump_xml_text(const xmlNode *data, uint32_t options, GString *buffer, int depth) { bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); int spaces = pretty? (2 * depth) : 0; const char *content = (const char *) data->content; gchar *content_esc = NULL; if (pcmk__xml_needs_escape(content, pcmk__xml_escape_text)) { content_esc = pcmk__xml_escape(content, pcmk__xml_escape_text); content = content_esc; } for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } g_string_append(buffer, content); if (pretty) { g_string_append_c(buffer, '\n'); } g_free(content_esc); } /*! * \internal * \brief Append XML CDATA content to a buffer * * \param[in] data XML whose content to append * \param[in] options Group of \p pcmk__xml_fmt_options flags * \param[in,out] buffer Where to append the content (must not be \p NULL) * \param[in] depth Current indentation level */ static void dump_xml_cdata(const xmlNode *data, uint32_t options, GString *buffer, int depth) { bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); int spaces = pretty? (2 * depth) : 0; for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } pcmk__g_strcat(buffer, "content, "]]>", NULL); if (pretty) { g_string_append_c(buffer, '\n'); } } /*! * \internal * \brief Append an XML comment to a buffer * * \param[in] data XML whose content to append * \param[in] options Group of \p pcmk__xml_fmt_options flags * \param[in,out] buffer Where to append the content (must not be \p NULL) * \param[in] depth Current indentation level */ static void dump_xml_comment(const xmlNode *data, uint32_t options, GString *buffer, int depth) { bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); int spaces = pretty? (2 * depth) : 0; for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } pcmk__g_strcat(buffer, "", NULL); if (pretty) { g_string_append_c(buffer, '\n'); } } /*! * \internal * \brief Get a string representation of an XML element type * * \param[in] type XML element type * * \return String representation of \p type */ static const char * xml_element_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", }; if ((type < 0) || (type >= PCMK__NELEM(element_type_names))) { return "unrecognized type"; } return element_type_names[type]; } /*! * \internal * \brief Create a string representation of an XML object * * libxml2's \c xmlNodeDumpOutput() doesn't allow filtering, doesn't escape * special characters thoroughly, and doesn't allow a const argument. * * \param[in] data XML to convert * \param[in] options Group of \p pcmk__xml_fmt_options flags * \param[in,out] buffer Where to store the text (must not be \p NULL) * \param[in] depth Current indentation level * * \todo Create a wrapper that doesn't require \p depth. Only used with * recursive calls currently. */ void pcmk__xml_string(const xmlNode *data, uint32_t options, GString *buffer, int depth) { if (data == NULL) { crm_trace("Nothing to dump"); return; } CRM_ASSERT(buffer != NULL); CRM_CHECK(depth >= 0, depth = 0); switch(data->type) { case XML_ELEMENT_NODE: /* Handle below */ dump_xml_element(data, options, buffer, depth); break; case XML_TEXT_NODE: if (pcmk_is_set(options, pcmk__xml_fmt_text)) { dump_xml_text(data, options, buffer, depth); } break; case XML_COMMENT_NODE: dump_xml_comment(data, options, buffer, depth); break; case XML_CDATA_SECTION_NODE: dump_xml_cdata(data, options, buffer, depth); break; default: crm_warn("Cannot convert XML %s node to text " CRM_XS " type=%d", xml_element_type_text(data->type), data->type); break; } } /*! * \internal * \brief Write a string to a file stream, compressed using \c bzip2 * * \param[in] text String to write * \param[in] filename Name of file being written (for logging only) * \param[in,out] stream Open file stream to write to * \param[out] bytes_out Number of bytes written (valid only on success) * * \return Standard Pacemaker return code */ static int write_compressed_stream(char *text, const char *filename, FILE *stream, unsigned int *bytes_out) { unsigned int bytes_in = 0; int rc = pcmk_rc_ok; // (5, 0, 0): (intermediate block size, silent, default workFactor) BZFILE *bz_file = BZ2_bzWriteOpen(&rc, stream, 5, 0, 0); rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { crm_warn("Not compressing %s: could not prepare file stream: %s " CRM_XS " rc=%d", filename, pcmk_rc_str(rc), rc); goto done; } BZ2_bzWrite(&rc, bz_file, text, strlen(text)); rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { crm_warn("Not compressing %s: could not compress data: %s " CRM_XS " rc=%d errno=%d", filename, pcmk_rc_str(rc), rc, errno); goto done; } BZ2_bzWriteClose(&rc, bz_file, 0, &bytes_in, bytes_out); bz_file = NULL; rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { crm_warn("Not compressing %s: could not write compressed data: %s " CRM_XS " rc=%d errno=%d", filename, pcmk_rc_str(rc), rc, errno); goto done; } crm_trace("Compressed XML for %s from %u bytes to %u", filename, bytes_in, *bytes_out); done: if (bz_file != NULL) { BZ2_bzWriteClose(&rc, bz_file, 0, NULL, NULL); } return rc; } /*! * \internal * \brief Write XML to a file stream * * \param[in] xml XML to write * \param[in] filename Name of file being written (for logging only) * \param[in,out] stream Open file stream corresponding to filename (closed * when this function returns) * \param[in] compress Whether to compress XML before writing * \param[out] nbytes Number of bytes written * * \return Standard Pacemaker return code */ static int write_xml_stream(const xmlNode *xml, const char *filename, FILE *stream, bool compress, unsigned int *nbytes) { // @COMPAT Drop nbytes as arg when we drop write_xml_fd()/write_xml_file() GString *buffer = g_string_sized_new(1024); unsigned int bytes_out = 0; int rc = pcmk_rc_ok; pcmk__xml_string(xml, pcmk__xml_fmt_pretty, buffer, 0); CRM_CHECK(!pcmk__str_empty(buffer->str), crm_log_xml_info(xml, "dump-failed"); rc = pcmk_rc_error; goto done); crm_log_xml_trace(xml, "writing"); if (compress && (write_compressed_stream(buffer->str, filename, stream, &bytes_out) == pcmk_rc_ok)) { goto done; } rc = fprintf(stream, "%s", buffer->str); if (rc < 0) { rc = EIO; crm_perror(LOG_ERR, "writing %s", filename); goto done; } bytes_out = (unsigned int) rc; rc = pcmk_rc_ok; done: if (fflush(stream) != 0) { rc = errno; crm_perror(LOG_ERR, "flushing %s", filename); } // Don't report error if the file does not support synchronization if ((fsync(fileno(stream)) < 0) && (errno != EROFS) && (errno != EINVAL)) { rc = errno; crm_perror(LOG_ERR, "synchronizing %s", filename); } fclose(stream); crm_trace("Saved %u bytes to %s as XML", bytes_out, filename); if (nbytes != NULL) { *nbytes = bytes_out; } g_string_free(buffer, TRUE); return rc; } /*! * \internal * \brief Write XML to a file descriptor * * \param[in] xml XML to write * \param[in] filename Name of file being written (for logging only) * \param[in] fd Open file descriptor corresponding to \p filename * \param[in] compress If \c true, compress XML before writing * \param[out] nbytes Number of bytes written (can be \c NULL) * * \return Standard Pacemaker return code */ int pcmk__xml_write_fd(const xmlNode *xml, const char *filename, int fd, bool compress, unsigned int *nbytes) { // @COMPAT Drop compress and nbytes arguments when we drop write_xml_fd() FILE *stream = NULL; CRM_CHECK((xml != NULL) && (fd > 0), return EINVAL); stream = fdopen(fd, "w"); if (stream == NULL) { return errno; } return write_xml_stream(xml, pcmk__s(filename, "unnamed file"), stream, compress, nbytes); } /*! * \internal * \brief Write XML to a file * * \param[in] xml XML to write * \param[in] filename Name of file to write * \param[in] compress If \c true, compress XML before writing * \param[out] nbytes Number of bytes written (can be \c NULL) * * \return Standard Pacemaker return code */ int pcmk__xml_write_file(const xmlNode *xml, const char *filename, bool compress, unsigned int *nbytes) { // @COMPAT Drop nbytes argument when we drop write_xml_fd() FILE *stream = NULL; CRM_CHECK((xml != NULL) && (filename != NULL), return EINVAL); stream = fopen(filename, "w"); if (stream == NULL) { return errno; } return write_xml_stream(xml, filename, stream, compress, nbytes); } /*! * \internal * \brief Serialize XML (using libxml) into provided descriptor * * \param[in] fd File descriptor to (piece-wise) write to * \param[in] cur XML subtree to proceed * * \return a standard Pacemaker return code */ int pcmk__xml2fd(int fd, xmlNode *cur) { bool success; xmlOutputBuffer *fd_out = xmlOutputBufferCreateFd(fd, NULL); pcmk__mem_assert(fd_out); xmlNodeDumpOutput(fd_out, cur->doc, cur, 0, pcmk__xml_fmt_pretty, NULL); success = xmlOutputBufferWrite(fd_out, sizeof("\n") - 1, "\n") != -1; success = xmlOutputBufferClose(fd_out) != -1 && success; if (!success) { return EIO; } fsync(fd); return pcmk_rc_ok; } void save_xml_to_file(const xmlNode *xml, const char *desc, const char *filename) { char *f = NULL; if (filename == NULL) { char *uuid = crm_generate_uuid(); f = crm_strdup_printf("%s/%s", pcmk__get_tmpdir(), uuid); filename = f; free(uuid); } crm_info("Saving %s to %s", desc, filename); pcmk__xml_write_file(xml, filename, false, NULL); free(f); } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include xmlNode * filename2xml(const char *filename) { return pcmk__xml_read(filename); } xmlNode * stdin2xml(void) { return pcmk__xml_read(NULL); } xmlNode * string2xml(const char *input) { return pcmk__xml_parse(input); } char * dump_xml_formatted(const xmlNode *xml) { char *str = NULL; GString *buffer = g_string_sized_new(1024); pcmk__xml_string(xml, pcmk__xml_fmt_pretty, buffer, 0); str = pcmk__str_copy(buffer->str); g_string_free(buffer, TRUE); return str; } char * dump_xml_formatted_with_text(const xmlNode *xml) { char *str = NULL; GString *buffer = g_string_sized_new(1024); pcmk__xml_string(xml, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, buffer, 0); str = pcmk__str_copy(buffer->str); g_string_free(buffer, TRUE); return str; } char * dump_xml_unformatted(const xmlNode *xml) { char *str = NULL; GString *buffer = g_string_sized_new(1024); pcmk__xml_string(xml, 0, buffer, 0); str = pcmk__str_copy(buffer->str); g_string_free(buffer, TRUE); return str; } int write_xml_fd(const xmlNode *xml, const char *filename, int fd, gboolean compress) { unsigned int nbytes = 0; int rc = pcmk__xml_write_fd(xml, filename, fd, compress, &nbytes); if (rc != pcmk_rc_ok) { return pcmk_rc2legacy(rc); } return (int) nbytes; } int write_xml_file(const xmlNode *xml, const char *filename, gboolean compress) { unsigned int nbytes = 0; int rc = pcmk__xml_write_file(xml, filename, compress, &nbytes); if (rc != pcmk_rc_ok) { return pcmk_rc2legacy(rc); } return (int) nbytes; } // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/common/xpath.c b/lib/common/xpath.c index 9fc95c5a5f..1cb47b5604 100644 --- a/lib/common/xpath.c +++ b/lib/common/xpath.c @@ -1,396 +1,342 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include "crmcommon_private.h" /* * From xpath2.c * * All the elements returned by an XPath query are pointers to * elements from the tree *except* namespace nodes where the XPath * semantic is different from the implementation in libxml2 tree. * As a result when a returned node set is freed when * xmlXPathFreeObject() is called, that routine must check the * element type. But node from the returned set may have been removed * by xmlNodeSetContent() resulting in access to freed data. * * This can be exercised by running * valgrind xpath2 test3.xml '//discarded' discarded * * There is 2 ways around it: * - make a copy of the pointers to the nodes from the result set * then call xmlXPathFreeObject() and then modify the nodes * or * - remove the references from the node set, if they are not namespace nodes, before calling xmlXPathFreeObject(). */ void freeXpathObject(xmlXPathObjectPtr xpathObj) { int lpc, max = numXpathResults(xpathObj); if (xpathObj == NULL) { return; } for (lpc = 0; lpc < max; lpc++) { if (xpathObj->nodesetval->nodeTab[lpc] && xpathObj->nodesetval->nodeTab[lpc]->type != XML_NAMESPACE_DECL) { xpathObj->nodesetval->nodeTab[lpc] = NULL; } } /* _Now_ it's safe to free it */ xmlXPathFreeObject(xpathObj); } xmlNode * getXpathResult(xmlXPathObjectPtr xpathObj, int index) { xmlNode *match = NULL; int max = numXpathResults(xpathObj); CRM_CHECK(index >= 0, return NULL); CRM_CHECK(xpathObj != NULL, return NULL); if (index >= max) { crm_err("Requested index %d of only %d items", index, max); return NULL; } else if(xpathObj->nodesetval->nodeTab[index] == NULL) { /* Previously requested */ return NULL; } match = xpathObj->nodesetval->nodeTab[index]; CRM_CHECK(match != NULL, return NULL); if (xpathObj->nodesetval->nodeTab[index]->type != XML_NAMESPACE_DECL) { /* See the comment for freeXpathObject() */ xpathObj->nodesetval->nodeTab[index] = NULL; } if (match->type == XML_DOCUMENT_NODE) { /* Will happen if section = '/' */ match = match->children; } else if (match->type != XML_ELEMENT_NODE && match->parent && match->parent->type == XML_ELEMENT_NODE) { /* Return the parent instead */ match = match->parent; } else if (match->type != XML_ELEMENT_NODE) { /* We only support searching nodes */ crm_err("We only support %d not %d", XML_ELEMENT_NODE, match->type); match = NULL; } return match; } void dedupXpathResults(xmlXPathObjectPtr xpathObj) { int lpc, max = numXpathResults(xpathObj); if (xpathObj == NULL) { return; } for (lpc = 0; lpc < max; lpc++) { xmlNode *xml = NULL; gboolean dedup = FALSE; if (xpathObj->nodesetval->nodeTab[lpc] == NULL) { continue; } xml = xpathObj->nodesetval->nodeTab[lpc]->parent; for (; xml; xml = xml->parent) { int lpc2 = 0; for (lpc2 = 0; lpc2 < max; lpc2++) { if (xpathObj->nodesetval->nodeTab[lpc2] == xml) { xpathObj->nodesetval->nodeTab[lpc] = NULL; dedup = TRUE; break; } } if (dedup) { break; } } } } /* the caller needs to check if the result contains a xmlDocPtr or xmlNodePtr */ xmlXPathObjectPtr xpath_search(const xmlNode *xml_top, const char *path) { xmlXPathObjectPtr xpathObj = NULL; xmlXPathContextPtr xpathCtx = NULL; const xmlChar *xpathExpr = (pcmkXmlStr) path; CRM_CHECK(path != NULL, return NULL); CRM_CHECK(xml_top != NULL, return NULL); CRM_CHECK(strlen(path) > 0, return NULL); xpathCtx = xmlXPathNewContext(xml_top->doc); pcmk__mem_assert(xpathCtx); xpathObj = xmlXPathEvalExpression(xpathExpr, xpathCtx); xmlXPathFreeContext(xpathCtx); return xpathObj; } /*! * \brief Run a supplied function for each result of an xpath search * * \param[in,out] xml XML to search * \param[in] xpath XPath search string * \param[in] helper Function to call for each result * \param[in,out] user_data Data to pass to supplied function * * \note The helper function will be passed the XML node of the result, * and the supplied user_data. This function does not otherwise * use user_data. */ void crm_foreach_xpath_result(xmlNode *xml, const char *xpath, void (*helper)(xmlNode*, void*), void *user_data) { xmlXPathObjectPtr xpathObj = xpath_search(xml, xpath); int nresults = numXpathResults(xpathObj); int i; for (i = 0; i < nresults; i++) { xmlNode *result = getXpathResult(xpathObj, i); CRM_LOG_ASSERT(result != NULL); if (result) { (*helper)(result, user_data); } } freeXpathObject(xpathObj); } xmlNode * get_xpath_object(const char *xpath, xmlNode * xml_obj, int error_level) { int max; xmlNode *result = NULL; xmlXPathObjectPtr xpathObj = NULL; char *nodePath = NULL; char *matchNodePath = NULL; if (xpath == NULL) { return xml_obj; /* or return NULL? */ } xpathObj = xpath_search(xml_obj, xpath); nodePath = (char *)xmlGetNodePath(xml_obj); max = numXpathResults(xpathObj); if (max < 1) { if (error_level < LOG_NEVER) { do_crm_log(error_level, "No match for %s in %s", xpath, pcmk__s(nodePath, "unknown path")); crm_log_xml_explicit(xml_obj, "Unexpected Input"); } } else if (max > 1) { if (error_level < LOG_NEVER) { int lpc = 0; do_crm_log(error_level, "Too many matches for %s in %s", xpath, pcmk__s(nodePath, "unknown path")); for (lpc = 0; lpc < max; lpc++) { xmlNode *match = getXpathResult(xpathObj, lpc); CRM_LOG_ASSERT(match != NULL); if (match != NULL) { matchNodePath = (char *) xmlGetNodePath(match); do_crm_log(error_level, "%s[%d] = %s", xpath, lpc, pcmk__s(matchNodePath, "unrecognizable match")); free(matchNodePath); } } crm_log_xml_explicit(xml_obj, "Bad Input"); } } else { result = getXpathResult(xpathObj, 0); } freeXpathObject(xpathObj); free(nodePath); return result; } /*! * \internal * \brief Get an XPath string that matches an XML element as closely as possible * * \param[in] xml The XML element for which to build an XPath string * * \return A \p GString that matches \p xml, or \p NULL if \p xml is \p NULL. * * \note The caller is responsible for freeing the string using * \p g_string_free(). */ GString * pcmk__element_xpath(const xmlNode *xml) { const xmlNode *parent = NULL; GString *xpath = NULL; const char *id = NULL; if (xml == NULL) { return NULL; } parent = xml->parent; xpath = pcmk__element_xpath(parent); if (xpath == NULL) { xpath = g_string_sized_new(256); } // Build xpath like "/" -> "/cib" -> "/cib/configuration" if (parent == NULL) { g_string_append_c(xpath, '/'); } else if (parent->parent == NULL) { g_string_append(xpath, (const gchar *) xml->name); } else { pcmk__g_strcat(xpath, "/", (const char *) xml->name, NULL); } id = pcmk__xe_id(xml); if (id != NULL) { pcmk__g_strcat(xpath, "[@" PCMK_XA_ID "='", id, "']", NULL); } return xpath; } char * pcmk__xpath_node_id(const char *xpath, const char *node) { char *retval = NULL; char *patt = NULL; char *start = NULL; char *end = NULL; if (node == NULL || xpath == NULL) { return retval; } patt = crm_strdup_printf("/%s[@" PCMK_XA_ID "=", node); start = strstr(xpath, patt); if (!start) { free(patt); return retval; } start += strlen(patt); start++; end = strstr(start, "\'"); CRM_ASSERT(end); retval = strndup(start, end-start); free(patt); return retval; } static int output_attr_child(xmlNode *child, void *userdata) { pcmk__output_t *out = userdata; out->info(out, " Value: %s \t(id=%s)", crm_element_value(child, PCMK_XA_VALUE), pcmk__s(pcmk__xe_id(child), "")); return pcmk_rc_ok; } void pcmk__warn_multiple_name_matches(pcmk__output_t *out, xmlNode *search, const char *name) { if (out == NULL || name == NULL || search == NULL || search->children == NULL) { return; } out->info(out, "Multiple attributes match " PCMK_XA_NAME "=%s", name); pcmk__xe_foreach_child(search, NULL, output_attr_child, out); } - -// Deprecated functions kept only for backward API compatibility -// LCOV_EXCL_START - -#include - -/*! - * \deprecated This function will be removed in a future release - * \brief Get an XPath string that matches an XML element as closely as possible - * - * \param[in] xml The XML element for which to build an XPath string - * - * \return A string that matches \p xml, or \p NULL if \p xml is \p NULL. - * - * \note The caller is responsible for freeing the string using free(). - */ -char * -xml_get_path(const xmlNode *xml) -{ - char *path = NULL; - GString *g_path = pcmk__element_xpath(xml); - - if (g_path == NULL) { - return NULL; - } - path = pcmk__str_copy(g_path->str); - g_string_free(g_path, TRUE); - return path; -} - -xmlNode * -get_xpath_object_relative(const char *xpath, xmlNode *xml_obj, int error_level) -{ - xmlNode *result = NULL; - char *xpath_full = NULL; - char *xpath_prefix = NULL; - - if (xml_obj == NULL || xpath == NULL) { - return NULL; - } - - xpath_prefix = (char *)xmlGetNodePath(xml_obj); - - xpath_full = crm_strdup_printf("%s%s", xpath_prefix, xpath); - - result = get_xpath_object(xpath_full, xml_obj, error_level); - - free(xpath_prefix); - free(xpath_full); - return result; -} - -// LCOV_EXCL_STOP -// End deprecated API