diff --git a/include/crm/common/xml_internal.h b/include/crm/common/xml_internal.h index b163650fc7..e8ae5729f9 100644 --- a/include/crm/common/xml_internal.h +++ b/include/crm/common/xml_internal.h @@ -1,414 +1,415 @@ /* * Copyright 2017-2023 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__XML_INTERNAL__H # define PCMK__XML_INTERNAL__H /* * Internal-only wrappers for and extensions to libxml2 (libxslt) */ # include # include # include # include /* transitively imports qblog.h */ +# include /*! * \brief Base for directing lib{xml2,xslt} log into standard libqb backend * * This macro implements the core of what can be needed for directing * libxml2 or libxslt error messaging into standard, preconfigured * libqb-backed log stream. * * It's a bit unfortunate that libxml2 (and more sparsely, also libxslt) * emits a single message by chunks (location is emitted separatedly from * the message itself), so we have to take the effort to combine these * chunks back to single message. Whether to do this or not is driven * with \p dechunk toggle. * * The form of a macro was chosen for implicit deriving of __FILE__, etc. * and also because static dechunking buffer should be differentiated per * library (here we assume different functions referring to this macro * will not ever be using both at once), preferably also per-library * context of use to avoid clashes altogether. * * Note that we cannot use qb_logt, because callsite data have to be known * at the moment of compilation, which it is not always the case -- xml_log * (and unfortunately there's no clear explanation of the fail to compile). * * Also note that there's no explicit guard against said libraries producing * never-newline-terminated chunks (which would just keep consuming memory), * as it's quite improbable. Termination of the program in between the * same-message chunks will raise a flag with valgrind and the likes, though. * * And lastly, regarding how dechunking combines with other non-message * parameters -- for \p priority, most important running specification * wins (possibly elevated to LOG_ERR in case of nonconformance with the * newline-termination "protocol"), \p dechunk is expected to always be * on once it was at the start, and the rest (\p postemit and \p prefix) * are picked directly from the last chunk entry finalizing the message * (also reasonable to always have it the same with all related entries). * * \param[in] priority Syslog priority for the message to be logged * \param[in] dechunk Whether to dechunk new-line terminated message * \param[in] postemit Code to be executed once message is sent out * \param[in] prefix How to prefix the message or NULL for raw passing * \param[in] fmt Format string as with printf-like functions * \param[in] ap Variable argument list to supplement \p fmt format string */ #define PCMK__XML_LOG_BASE(priority, dechunk, postemit, prefix, fmt, ap) \ do { \ if (!(dechunk) && (prefix) == NULL) { /* quick pass */ \ qb_log_from_external_source_va(__func__, __FILE__, (fmt), \ (priority), __LINE__, 0, (ap)); \ (void) (postemit); \ } else { \ int CXLB_len = 0; \ char *CXLB_buf = NULL; \ static int CXLB_buffer_len = 0; \ static char *CXLB_buffer = NULL; \ static uint8_t CXLB_priority = 0; \ \ CXLB_len = vasprintf(&CXLB_buf, (fmt), (ap)); \ \ if (CXLB_len <= 0 || CXLB_buf[CXLB_len - 1] == '\n' || !(dechunk)) { \ if (CXLB_len < 0) { \ CXLB_buf = (char *) "LOG CORRUPTION HAZARD"; /*we don't modify*/\ CXLB_priority = QB_MIN(CXLB_priority, LOG_ERR); \ } else if (CXLB_len > 0 /* && (dechunk) */ \ && CXLB_buf[CXLB_len - 1] == '\n') { \ CXLB_buf[CXLB_len - 1] = '\0'; \ } \ if (CXLB_buffer) { \ qb_log_from_external_source(__func__, __FILE__, "%s%s%s", \ CXLB_priority, __LINE__, 0, \ (prefix) != NULL ? (prefix) : "", \ CXLB_buffer, CXLB_buf); \ free(CXLB_buffer); \ } else { \ qb_log_from_external_source(__func__, __FILE__, "%s%s", \ (priority), __LINE__, 0, \ (prefix) != NULL ? (prefix) : "", \ CXLB_buf); \ } \ if (CXLB_len < 0) { \ CXLB_buf = NULL; /* restore temporary override */ \ } \ CXLB_buffer = NULL; \ CXLB_buffer_len = 0; \ (void) (postemit); \ \ } else if (CXLB_buffer == NULL) { \ CXLB_buffer_len = CXLB_len; \ CXLB_buffer = CXLB_buf; \ CXLB_buf = NULL; \ CXLB_priority = (priority); /* remember as a running severest */ \ \ } else { \ CXLB_buffer = realloc(CXLB_buffer, 1 + CXLB_buffer_len + CXLB_len); \ memcpy(CXLB_buffer + CXLB_buffer_len, CXLB_buf, CXLB_len); \ CXLB_buffer_len += CXLB_len; \ CXLB_buffer[CXLB_buffer_len] = '\0'; \ CXLB_priority = QB_MIN(CXLB_priority, (priority)); /* severest? */ \ } \ free(CXLB_buf); \ } \ } while (0) /* * \enum pcmk__xml_fmt_options * \brief Bit flags to control format in XML logs and dumps */ enum pcmk__xml_fmt_options { //! Exclude certain XML attributes (for calculating digests) pcmk__xml_fmt_filtered = (1 << 0), //! Include indentation and newlines pcmk__xml_fmt_pretty = (1 << 1), //! Include full XML subtree (with any text), using libxml serialization pcmk__xml_fmt_full = (1 << 2), //! Include the opening tag of an XML element, and include XML comments pcmk__xml_fmt_open = (1 << 3), //! Include the children of an XML element pcmk__xml_fmt_children = (1 << 4), //! Include the closing tag of an XML element pcmk__xml_fmt_close = (1 << 5), // @COMPAT Remove when log_data_element() is removed //! Include XML text nodes pcmk__xml_fmt_text = (1 << 6), // @COMPAT Remove when v1 patchsets are removed //! Log a created XML subtree pcmk__xml_fmt_diff_plus = (1 << 7), // @COMPAT Remove when v1 patchsets are removed //! Log a removed XML subtree pcmk__xml_fmt_diff_minus = (1 << 8), // @COMPAT Remove when v1 patchsets are removed //! Log a minimal version of an XML diff (only showing the changes) pcmk__xml_fmt_diff_short = (1 << 9), }; -void pcmk__xml_log(int log_level, const char *prefix, const xmlNode *data, - int depth, uint32_t options); +void pcmk__xml_show(pcmk__output_t *out, int log_level, const char *prefix, + const xmlNode *data, int depth, uint32_t options); void pcmk__xml_log_changes(uint8_t log_level, const xmlNode *xml); void pcmk__xml_log_patchset(uint8_t log_level, const xmlNode *patchset); /* XML search strings for guest, remote and pacemaker_remote nodes */ /* search string to find CIB resources entries for cluster nodes */ #define PCMK__XP_MEMBER_NODE_CONFIG \ "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_NODES \ "/" XML_CIB_TAG_NODE "[not(@type) or @type='member']" /* search string to find CIB resources entries for guest nodes */ #define PCMK__XP_GUEST_NODE_CONFIG \ "//" XML_TAG_CIB "//" XML_CIB_TAG_CONFIGURATION "//" XML_CIB_TAG_RESOURCE \ "//" XML_TAG_META_SETS "//" XML_CIB_TAG_NVPAIR \ "[@name='" XML_RSC_ATTR_REMOTE_NODE "']" /* search string to find CIB resources entries for remote nodes */ #define PCMK__XP_REMOTE_NODE_CONFIG \ "//" XML_TAG_CIB "//" XML_CIB_TAG_CONFIGURATION "//" XML_CIB_TAG_RESOURCE \ "[@type='remote'][@provider='pacemaker']" /* search string to find CIB node status entries for pacemaker_remote nodes */ #define PCMK__XP_REMOTE_NODE_STATUS \ "//" XML_TAG_CIB "//" XML_CIB_TAG_STATUS "//" XML_CIB_TAG_STATE \ "[@" XML_NODE_IS_REMOTE "='true']" enum pcmk__xml_artefact_ns { pcmk__xml_artefact_ns_legacy_rng = 1, pcmk__xml_artefact_ns_legacy_xslt, pcmk__xml_artefact_ns_base_rng, pcmk__xml_artefact_ns_base_xslt, }; void pcmk__strip_xml_text(xmlNode *xml); const char *pcmk__xe_add_last_written(xmlNode *xe); xmlNode *pcmk__xe_match(const xmlNode *parent, const char *node_name, const char *attr_n, const char *attr_v); void pcmk__xe_remove_matching_attrs(xmlNode *element, bool (*match)(xmlAttrPtr, void *), void *user_data); GString *pcmk__element_xpath(const xmlNode *xml); /*! * \internal * \brief Get the root directory to scan XML artefacts of given kind for * * \param[in] ns governs the hierarchy nesting against the inherent root dir * * \return root directory to scan XML artefacts of given kind for */ char * pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns); /*! * \internal * \brief Get the fully unwrapped path to particular XML artifact (RNG/XSLT) * * \param[in] ns denotes path forming details (parent dir, suffix) * \param[in] filespec symbolic file specification to be combined with * #artefact_ns to form the final path * \return unwrapped path to particular XML artifact (RNG/XSLT) */ char *pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec); /*! * \internal * \brief Return first non-text child node of an XML node * * \param[in] parent XML node to check * * \return First non-text child node of \p parent (or NULL if none) */ static inline xmlNode * pcmk__xml_first_child(const xmlNode *parent) { xmlNode *child = (parent? parent->children : NULL); while (child && (child->type == XML_TEXT_NODE)) { child = child->next; } return child; } /*! * \internal * \brief Return next non-text sibling node of an XML node * * \param[in] child XML node to check * * \return Next non-text sibling of \p child (or NULL if none) */ static inline xmlNode * pcmk__xml_next(const xmlNode *child) { xmlNode *next = (child? child->next : NULL); while (next && (next->type == XML_TEXT_NODE)) { next = next->next; } return next; } /*! * \internal * \brief Return first non-text child element of an XML node * * \param[in] parent XML node to check * * \return First child element of \p parent (or NULL if none) */ static inline xmlNode * pcmk__xe_first_child(const xmlNode *parent) { xmlNode *child = (parent? parent->children : NULL); while (child && (child->type != XML_ELEMENT_NODE)) { child = child->next; } return child; } /*! * \internal * \brief Return next non-text sibling element of an XML element * * \param[in] child XML element to check * * \return Next sibling element of \p child (or NULL if none) */ static inline xmlNode * pcmk__xe_next(const xmlNode *child) { xmlNode *next = child? child->next : NULL; while (next && (next->type != XML_ELEMENT_NODE)) { next = next->next; } return next; } /*! * \internal * \brief Like pcmk__xe_set_props, but takes a va_list instead of * arguments directly. * * \param[in,out] node XML to add attributes to * \param[in] pairs NULL-terminated list of name/value pairs to add */ void pcmk__xe_set_propv(xmlNodePtr node, va_list pairs); /*! * \internal * \brief Add a NULL-terminated list of name/value pairs to the given * XML node as properties. * * \param[in,out] node XML node to add properties to * \param[in] ... NULL-terminated list of name/value pairs * * \note A NULL name terminates the arguments; a NULL value will be skipped. */ void pcmk__xe_set_props(xmlNodePtr node, ...) G_GNUC_NULL_TERMINATED; /*! * \internal * \brief Get first attribute of an XML element * * \param[in] xe XML element to check * * \return First attribute of \p xe (or NULL if \p xe is NULL or has none) */ static inline xmlAttr * pcmk__xe_first_attr(const xmlNode *xe) { return (xe == NULL)? NULL : xe->properties; } /*! * \internal * \brief Extract the ID attribute from an XML element * * \param[in] xpath String to search * \param[in] node Node to get the ID for * * \return ID attribute of \p node in xpath string \p xpath */ char * pcmk__xpath_node_id(const char *xpath, const char *node); /* internal XML-related utilities */ enum xml_private_flags { pcmk__xf_none = 0x0000, pcmk__xf_dirty = 0x0001, pcmk__xf_deleted = 0x0002, pcmk__xf_created = 0x0004, pcmk__xf_modified = 0x0008, pcmk__xf_tracking = 0x0010, pcmk__xf_processed = 0x0020, pcmk__xf_skip = 0x0040, pcmk__xf_moved = 0x0080, pcmk__xf_acl_enabled = 0x0100, pcmk__xf_acl_read = 0x0200, pcmk__xf_acl_write = 0x0400, pcmk__xf_acl_deny = 0x0800, pcmk__xf_acl_create = 0x1000, pcmk__xf_acl_denied = 0x2000, pcmk__xf_lazy = 0x4000, }; void pcmk__set_xml_doc_flag(xmlNode *xml, enum xml_private_flags flag); /*! * \internal * \brief Iterate over child elements of \p xml * * This function iterates over the children of \p xml, performing the * callback function \p handler on each node. If the callback returns * a value other than pcmk_rc_ok, the iteration stops and the value is * returned. It is therefore possible that not all children will be * visited. * * \param[in,out] xml The starting XML node. Can be NULL. * \param[in] child_element_name The name that the node must match in order * for \p handler to be run. If NULL, all * child elements will match. * \param[in] handler The callback function. * \param[in,out] userdata User data to pass to the callback function. * Can be NULL. * * \return Standard Pacemaker return code */ int pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name, int (*handler)(xmlNode *xml, void *userdata), void *userdata); #endif // PCMK__XML_INTERNAL__H diff --git a/lib/common/logging.c b/lib/common/logging.c index 1754801215..e42c097ed3 100644 --- a/lib/common/logging.c +++ b/lib/common/logging.c @@ -1,1147 +1,1147 @@ /* * Copyright 2004-2023 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #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 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, 0) == FALSE) { /* 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 = getenv("PCMK_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); } } /*! * \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; } 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); } } 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 = getenv("PCMK_trace_functions"); trace_fmts = getenv("PCMK_trace_formats"); trace_tags = getenv("PCMK_trace_tags"); trace_files = getenv("PCMK_trace_files"); trace_blackbox = getenv("PCMK_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, getenv("PCMK_trace_files"), getenv("PCMK_trace_functions"), getenv("PCMK_trace_formats"), getenv("PCMK_trace_tags")); } qb_log_filter_fn_set(crm_log_filter); } static gboolean crm_tracing_enabled(void) { if (crm_log_level == LOG_TRACE) { return TRUE; } else if (getenv("PCMK_trace_files") || getenv("PCMK_trace_functions") || getenv("PCMK_trace_formats") || getenv("PCMK_trace_tags")) { return TRUE; } return FALSE; } 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 = strdup(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 = strdup(modified); free(mutable); } else { crm_system_name = strdup("Unknown"); } CRM_ASSERT(crm_system_name != NULL); setenv("PCMK_service", crm_system_name, 1); } 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; crm_xml_init(); /* Sets buffer allocation strategy */ 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); } 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] 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_impl(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 { - pcmk__xml_log(level, text, xml, 1, - pcmk__xml_fmt_pretty - |pcmk__xml_fmt_open - |pcmk__xml_fmt_children - |pcmk__xml_fmt_close); + pcmk__xml_show(NULL, level, text, xml, 1, + pcmk__xml_fmt_pretty + |pcmk__xml_fmt_open + |pcmk__xml_fmt_children + |pcmk__xml_fmt_close); } } // 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; } // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/common/patchset.c b/lib/common/patchset.c index dd481da685..9c5df565ec 100644 --- a/lib/common/patchset.c +++ b/lib/common/patchset.c @@ -1,1857 +1,1857 @@ /* * Copyright 2004-2023 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include #include /* xmlAllocOutputBuffer */ #include #include #include #include // CRM_XML_LOG_BASE, etc. #include "crmcommon_private.h" static xmlNode *subtract_xml_comment(xmlNode *parent, xmlNode *left, xmlNode *right, gboolean *changed); /* */ // Add changes for specified XML to patchset 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 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 = create_xml_node(patchset, XML_DIFF_CHANGE); crm_xml_add(change, XML_DIFF_OP, "create"); crm_xml_add(change, XML_DIFF_PATH, (const char *) xpath->str); crm_xml_add_int(change, XML_DIFF_POSITION, position); add_node_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 = create_xml_node(patchset, XML_DIFF_CHANGE); crm_xml_add(change, XML_DIFF_OP, "modify"); crm_xml_add(change, XML_DIFF_PATH, (const char *) xpath->str); change = create_xml_node(change, XML_DIFF_LIST); g_string_free(xpath, TRUE); } } attr = create_xml_node(change, XML_DIFF_ATTR); crm_xml_add(attr, XML_NVPAIR_ATTR_NAME, (const char *)pIter->name); if (nodepriv->flags & pcmk__xf_deleted) { crm_xml_add(attr, XML_DIFF_OP, "unset"); } else { crm_xml_add(attr, XML_DIFF_OP, "set"); value = crm_element_value(xml, (const char *) pIter->name); crm_xml_add(attr, XML_NVPAIR_ATTR_VALUE, value); } } if (change) { xmlNode *result = NULL; change = create_xml_node(change->parent, XML_DIFF_RESULT); result = create_xml_node(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, ID(xml), pcmk__xml_position(xml, pcmk__xf_skip)); if (xpath != NULL) { change = create_xml_node(patchset, XML_DIFF_CHANGE); crm_xml_add(change, XML_DIFF_OP, "move"); crm_xml_add(change, XML_DIFF_PATH, (const char *) xpath->str); crm_xml_add_int(change, XML_DIFF_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 = first_named_child(xml, XML_CIB_TAG_CONFIGURATION); 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, "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION) != NULL) { return TRUE; } } } return FALSE; } 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[] = { XML_ATTR_GENERATION_ADMIN, XML_ATTR_GENERATION, XML_ATTR_NUMUPDATES, }; if (local_diff == NULL) { crm_trace("Nothing to do"); return; } tag = "diff-removed"; diff_child = find_xml_node(local_diff, tag, FALSE); if (diff_child == NULL) { diff_child = create_xml_node(local_diff, tag); } tag = XML_TAG_CIB; cib = find_xml_node(diff_child, tag, FALSE); if (cib == NULL) { cib = create_xml_node(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 = "diff-added"; diff_child = find_xml_node(local_diff, tag, FALSE); if (diff_child == NULL) { diff_child = create_xml_node(local_diff, tag); } tag = XML_TAG_CIB; cib = find_xml_node(diff_child, tag, FALSE); if (cib == NULL) { cib = create_xml_node(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 = crm_element_value(next, (const char *) a->name); xmlSetProp(cib, a->name, (pcmkXmlStr) p_value); } crm_log_xml_explicit(local_diff, "Repaired-diff"); } static xmlNode * xml_create_patchset_v1(xmlNode *source, xmlNode *target, bool config, bool suppress) { xmlNode *patchset = diff_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, "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[] = { XML_ATTR_GENERATION_ADMIN, XML_ATTR_GENERATION, XML_ATTR_NUMUPDATES, }; CRM_ASSERT(target); if (!xml_document_dirty(target)) { return NULL; } CRM_ASSERT(target->doc); docpriv = target->doc->_private; patchset = create_xml_node(NULL, XML_TAG_DIFF); crm_xml_add_int(patchset, "format", 2); version = create_xml_node(patchset, XML_DIFF_VERSION); v = create_xml_node(version, XML_DIFF_VSOURCE); 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 = create_xml_node(version, XML_DIFF_VTARGET); 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 = create_xml_node(patchset, XML_DIFF_CHANGE); crm_xml_add(change, XML_DIFF_OP, "delete"); crm_xml_add(change, XML_DIFF_PATH, deleted_obj->path); if (deleted_obj->position >= 0) { crm_xml_add_int(change, XML_DIFF_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, XML_ATTR_CRM_VERSION); 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, XML_ATTR_NUMUPDATES, "0"); crm_element_value_int(target, XML_ATTR_GENERATION, &counter); crm_xml_add_int(target, XML_ATTR_GENERATION, counter+1); } else if (manage_version) { crm_element_value_int(target, XML_ATTR_NUMUPDATES, &counter); crm_trace("Status changed %d - %d %s", format, counter, crm_element_value(source, XML_ATTR_NUMUPDATES)); crm_xml_add_int(target, XML_ATTR_NUMUPDATES, (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: 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, "format", &format); if ((format > 1) && !with_digest) { return; } version = crm_element_value(source, XML_ATTR_CRM_VERSION); digest = calculate_xml_versioned_digest(target, FALSE, TRUE, version); crm_xml_add(patch, XML_ATTR_DIGEST, digest); free(digest); return; } /*! * \internal * \brief Log an XML patchset header * * This function parses a header from an XML patchset (an \p XML_ATTR_DIFF * element and its children). Depending on the value of \p log_level, the output * may be written to \p stdout or to a log file. * * All header lines contain three integers separated by dots, of the form * {0}.{1}.{2}: * * \p {0}: \p XML_ATTR_GENERATION_ADMIN * * \p {1}: \p XML_ATTR_GENERATION * * \p {2}: \p XML_ATTR_NUMUPDATES * * 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] log_level Priority at which to log the messages * \param[in] patchset XML patchset to log */ static void xml_log_patchset_header(uint8_t log_level, const xmlNode *patchset) { 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, "format"); const char *digest = crm_element_value(patchset, XML_ATTR_DIGEST); do_crm_log(log_level, "Diff: --- %d.%d.%d %s", del[0], del[1], del[2], fmt); do_crm_log(log_level, "Diff: +++ %d.%d.%d %s", add[0], add[1], add[2], digest); } else if ((add[0] != 0) || (add[1] != 0) || (add[2] != 0)) { do_crm_log(log_level, "Local-only Change: %d.%d.%d", add[0], add[1], add[2]); } } /*! * \internal * \brief Log a user-friendly form of XML additions or removals * * \param[in] log_level Priority at which to log the messages * \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 */ static void xml_log_patchset_v1_recursive(uint8_t log_level, const char *prefix, const xmlNode *data, int depth, uint32_t options) { if (!xml_has_children(data) || (crm_element_value(data, XML_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)) { // Keep looking for the actual change for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL; child = pcmk__xml_next(child)) { xml_log_patchset_v1_recursive(log_level, prefix, child, depth + 1, options); } } else { - pcmk__xml_log(log_level, prefix, data, depth, - options - |pcmk__xml_fmt_open - |pcmk__xml_fmt_children - |pcmk__xml_fmt_close); + pcmk__xml_show(NULL, log_level, prefix, data, depth, + options + |pcmk__xml_fmt_open + |pcmk__xml_fmt_children + |pcmk__xml_fmt_close); } } /*! * \internal * \brief Log 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. Depending on the value * of \p log_level, the output may be written to \p stdout or to a log file. * * \param[in] log_level Priority at which to log the messages * \param[in] patchset XML patchset to log */ static void xml_log_patchset_v1(uint8_t log_level, const xmlNode *patchset) { uint32_t options = pcmk__xml_fmt_pretty; const xmlNode *removed = NULL; const xmlNode *added = NULL; const xmlNode *child = NULL; bool is_first = true; if (log_level < LOG_DEBUG) { options |= pcmk__xml_fmt_diff_short; } xml_log_patchset_header(log_level, 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 = find_xml_node(patchset, "diff-removed", FALSE); for (child = pcmk__xml_first_child(removed); child != NULL; child = pcmk__xml_next(child)) { xml_log_patchset_v1_recursive(log_level, "- ", child, 0, options|pcmk__xml_fmt_diff_minus); if (is_first) { is_first = false; } else { do_crm_log(log_level, " --- "); } } is_first = true; added = find_xml_node(patchset, "diff-added", FALSE); for (child = pcmk__xml_first_child(added); child != NULL; child = pcmk__xml_next(child)) { xml_log_patchset_v1_recursive(log_level, "+ ", child, 0, options|pcmk__xml_fmt_diff_plus); if (is_first) { is_first = false; } else { do_crm_log(log_level, " +++ "); } } } /*! * \internal * \brief Log 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. Depending on the value * of \p log_level, the output may be written to \p stdout or to a log file. * * \param[in] log_level Priority at which to log the messages * \param[in] patchset XML patchset to log */ static void xml_log_patchset_v2(uint8_t log_level, const xmlNode *patchset) { xml_log_patchset_header(log_level, patchset); for (const xmlNode *change = pcmk__xml_first_child(patchset); change != NULL; change = pcmk__xml_next(change)) { const char *op = crm_element_value(change, XML_DIFF_OP); const char *xpath = crm_element_value(change, XML_DIFF_PATH); if (op == NULL) { continue; } if (strcmp(op, "create") == 0) { char *prefix = crm_strdup_printf(PCMK__XML_PREFIX_CREATED " %s: ", xpath); - pcmk__xml_log(log_level, prefix, change->children, 0, - pcmk__xml_fmt_pretty|pcmk__xml_fmt_open); + pcmk__xml_show(NULL, log_level, prefix, change->children, 0, + pcmk__xml_fmt_pretty|pcmk__xml_fmt_open); // Overwrite all except the first two characters with spaces for (char *ch = prefix + 2; *ch != '\0'; ch++) { *ch = ' '; } - pcmk__xml_log(log_level, prefix, change->children, 0, - pcmk__xml_fmt_pretty - |pcmk__xml_fmt_children - |pcmk__xml_fmt_close); + pcmk__xml_show(NULL, log_level, prefix, change->children, 0, + pcmk__xml_fmt_pretty + |pcmk__xml_fmt_children + |pcmk__xml_fmt_close); free(prefix); } else if (strcmp(op, "move") == 0) { const char *position = crm_element_value(change, XML_DIFF_POSITION); do_crm_log(log_level, PCMK__XML_PREFIX_MOVED " %s moved to offset %s", xpath, position); } else if (strcmp(op, "modify") == 0) { xmlNode *clist = first_named_child(change, XML_DIFF_LIST); GString *buffer_set = NULL; GString *buffer_unset = NULL; for (const xmlNode *child = pcmk__xml_first_child(clist); child != NULL; child = pcmk__xml_next(child)) { const char *name = crm_element_value(child, "name"); op = crm_element_value(child, XML_DIFF_OP); if (op == NULL) { continue; } if (strcmp(op, "set") == 0) { const char *value = crm_element_value(child, "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) { do_crm_log(log_level, "+ %s: %s", xpath, buffer_set->str); g_string_free(buffer_set, TRUE); } if (buffer_unset != NULL) { do_crm_log(log_level, "-- %s: %s", xpath, buffer_unset->str); g_string_free(buffer_unset, TRUE); } } else if (strcmp(op, "delete") == 0) { int position = -1; crm_element_value_int(change, XML_DIFF_POSITION, &position); if (position >= 0) { do_crm_log(log_level, "-- %s (%d)", xpath, position); } else { do_crm_log(log_level, "-- %s", xpath); } } } } /*! * \internal * \brief Log 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. Depending on the value * of \p log_level, the output may be written to \p stdout or to a log file. * * \param[in] log_level Priority at which to log the messages * \param[in] patchset XML patchset to log */ void pcmk__xml_log_patchset(uint8_t log_level, const xmlNode *patchset) { int format = 1; if (log_level == LOG_NEVER) { return; } if (patchset == NULL) { crm_trace("Empty patch"); return; } if (log_level != LOG_STDOUT) { static struct qb_log_callsite *patchset_cs = NULL; if (patchset_cs == NULL) { patchset_cs = qb_log_callsite_get(__func__, __FILE__, "xml-patchset", log_level, __LINE__, 0); } if (!crm_is_callsite_active(patchset_cs, log_level, 0)) { return; } } crm_element_value_int(patchset, "format", &format); switch (format) { case 1: xml_log_patchset_v1(log_level, patchset); break; case 2: xml_log_patchset_v2(log_level, patchset); break; default: crm_err("Unknown patch format: %d", format); break; } } // Return true if attribute name is not "id" static bool not_id(xmlAttrPtr attr, void *user_data) { return strcmp((const char *) attr->name, XML_ATTR_ID) != 0; } // Apply the removals section of an 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 *name = NULL; const char *value = NULL; if ((target == NULL) || (patch == NULL)) { return; } if (target->type == XML_COMMENT_NODE) { gboolean dummy; subtract_xml_comment(target->parent, target, patch, &dummy); } name = crm_element_name(target); CRM_CHECK(name != NULL, return); CRM_CHECK(pcmk__str_eq(crm_element_name(target), crm_element_name(patch), pcmk__str_casei), return); CRM_CHECK(pcmk__str_eq(ID(target), ID(patch), pcmk__str_casei), return); // Check for XML_DIFF_MARKER in a child id = crm_element_value_copy(target, XML_ATTR_ID); value = crm_element_value(patch, XML_DIFF_MARKER); if ((value != NULL) && (strcmp(value, "removed:top") == 0)) { crm_trace("We are the root of the deletion: %s.id=%s", name, id); free_xml(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); } // Apply the additions section of an 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 XML_DIFF_MARKER in a child value = crm_element_value(patch, XML_DIFF_MARKER); if ((target == NULL) && (value != NULL) && (strcmp(value, "added:top") == 0)) { id = ID(patch); name = crm_element_name(patch); crm_trace("We are the root of the addition: %s.id=%s", name, id); add_node_copy(parent, patch); return; } else if (target == NULL) { id = ID(patch); name = crm_element_name(patch); crm_err("Could not locate: %s.id=%s", name, id); return; } if (target->type == XML_COMMENT_NODE) { pcmk__xc_update(parent, target, patch); } name = crm_element_name(target); CRM_CHECK(name != NULL, return); CRM_CHECK(pcmk__str_eq(crm_element_name(target), crm_element_name(patch), pcmk__str_casei), return); CRM_CHECK(pcmk__str_eq(ID(target), ID(patch), pcmk__str_casei), 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 = crm_element_value(patch, p_name); xml_remove_prop(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: label = added? "diff-added" : "diff-removed"; *patch_node = find_xml_node(patchset, label, FALSE); cib_node = find_xml_node(*patch_node, "cib", FALSE); if (cib_node != NULL) { *patch_node = cib_node; } break; case 2: label = added? "target" : "source"; *patch_node = find_xml_node(patchset, "version", FALSE); *patch_node = find_xml_node(*patch_node, label, FALSE); 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[] = { XML_ATTR_GENERATION_ADMIN, XML_ATTR_GENERATION, XML_ATTR_NUMUPDATES, }; crm_element_value_int(patchset, "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 * \param[in] format Patchset version * * \return Standard Pacemaker return code */ static int xml_patch_version_check(const xmlNode *xml, const xmlNode *patchset, int format) { int lpc = 0; bool changed = FALSE; int this[] = { 0, 0, 0 }; int add[] = { 0, 0, 0 }; int del[] = { 0, 0, 0 }; const char *vfields[] = { XML_ATTR_GENERATION_ADMIN, XML_ATTR_GENERATION, XML_ATTR_NUMUPDATES, }; 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; } /*! * \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 = find_xml_node(patchset, "diff-added", FALSE); xmlNode *removed = find_xml_node(patchset, "diff-removed", FALSE); xmlNode *old = copy_xml(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_diff_markers(xml); // Purge prior to checking digest free_xml(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 = 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 = calloc(key_len, sizeof(char)); CRM_ASSERT(remainder != NULL); section = calloc(key_len, sizeof(char)); CRM_ASSERT(section != NULL); id = calloc(key_len, sizeof(char)); CRM_ASSERT(id != NULL); tag = calloc(key_len, sizeof(char)); CRM_ASSERT(tag != NULL); 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, "%[^[][@" XML_ATTR_ID "='%[^']", tag, id); int current_position = -1; /* The target position is for the final component tag, so only use * it if there is nothing left to search after this component. */ if ((rc == 1) && (target_position >= 0)) { current_position = target_position; } switch (f) { case 1: target = first_matching_xml_child(target, tag, NULL, current_position); break; case 2: target = first_matching_xml_child(target, tag, id, current_position); break; default: // This should not be possible target = NULL; break; } current = remainder; } // Continue if something remains to search, and we've matched so far } while ((rc == 2) && target); if (target) { crm_trace("Found %s for %s", (path = (char *) xmlGetNodePath(target)), key); free(path); } else { crm_debug("No match for %s", key); } free(remainder); free(section); free(tag); free(id); return target; } typedef struct xml_change_obj_s { const xmlNode *change; xmlNode *match; } xml_change_obj_t; static gint sort_change_obj_by_position(gconstpointer a, gconstpointer b) { const xml_change_obj_t *change_obj_a = a; const xml_change_obj_t *change_obj_b = b; int position_a = -1; int position_b = -1; crm_element_value_int(change_obj_a->change, XML_DIFF_POSITION, &position_a); crm_element_value_int(change_obj_b->change, XML_DIFF_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, XML_DIFF_OP); const char *xpath = crm_element_value(change, XML_DIFF_PATH); int position = -1; if (op == NULL) { continue; } crm_trace("Processing %s %s", change->name, op); // "delete" changes for XML comments are generated with "position" if (strcmp(op, "delete") == 0) { crm_element_value_int(change, XML_DIFF_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, "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 ((strcmp(op, "create") == 0) || (strcmp(op, "move") == 0)) { // Delay the adding of a "create" object xml_change_obj_t *change_obj = calloc(1, sizeof(xml_change_obj_t)); CRM_ASSERT(change_obj != NULL); change_obj->change = change; change_obj->match = match; change_objs = g_list_append(change_objs, change_obj); if (strcmp(op, "move") == 0) { // Temporarily put the "move" object after the last sibling if ((match->parent != NULL) && (match->parent->last != NULL)) { xmlAddNextSibling(match->parent->last, match); } } } else if (strcmp(op, "delete") == 0) { free_xml(match); } else if (strcmp(op, "modify") == 0) { xmlNode *attrs = NULL; attrs = pcmk__xml_first_child(first_named_child(change, XML_DIFF_RESULT)); 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 = crm_element_value(attrs, name); 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, XML_DIFF_OP); xpath = crm_element_value(change, XML_DIFF_PATH); crm_trace("Continue performing %s on %s with %p", op, xpath, match); if (strcmp(op, "create") == 0) { int position = 0; xmlNode *child = NULL; xmlNode *match_child = NULL; match_child = match->children; crm_element_value_int(change, XML_DIFF_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 (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__mark_xml_created(child); } else if (strcmp(op, "move") == 0) { int position = 0; crm_element_value_int(change, XML_DIFF_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, 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 = crm_element_value(patchset, XML_ATTR_DIGEST); if (patchset == NULL) { return rc; } pcmk__xml_log_patchset(LOG_TRACE, patchset); crm_element_value_int(patchset, "format", &format); if (check_version) { rc = pcmk_rc2legacy(xml_patch_version_check(xml, patchset, format)); if (rc != pcmk_ok) { return rc; } } if (digest) { // Make it available for logging if result doesn't have expected digest old = copy_xml(xml); } if (rc == pcmk_ok) { switch (format) { case 1: 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, XML_ATTR_CRM_VERSION); 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); } free_xml(old); return rc; } void purge_diff_markers(xmlNode *a_node) { xmlNode *child = NULL; CRM_CHECK(a_node != NULL, return); xml_remove_prop(a_node, XML_DIFF_MARKER); for (child = pcmk__xml_first_child(a_node); child != NULL; child = pcmk__xml_next(child)) { purge_diff_markers(child); } } xmlNode * diff_xml_object(xmlNode *old, xmlNode *new, gboolean suppress) { xmlNode *tmp1 = NULL; xmlNode *diff = create_xml_node(NULL, "diff"); xmlNode *removed = create_xml_node(diff, "diff-removed"); xmlNode *added = create_xml_node(diff, "diff-added"); crm_xml_add(diff, XML_ATTR_CRM_VERSION, CRM_FEATURE_SET); tmp1 = subtract_xml_object(removed, old, new, FALSE, NULL, "removed:top"); if (suppress && (tmp1 != NULL) && can_prune_leaf(tmp1)) { free_xml(tmp1); } tmp1 = subtract_xml_object(added, new, old, TRUE, NULL, "added:top"); if (suppress && (tmp1 != NULL) && can_prune_leaf(tmp1)) { free_xml(tmp1); } if ((added->children == NULL) && (removed->children == NULL)) { free_xml(diff); diff = NULL; } return diff; } static xmlNode * subtract_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 = add_node_copy(parent, left); *changed = TRUE; return deleted; } return NULL; } xmlNode * subtract_xml_object(xmlNode *parent, xmlNode *left, xmlNode *right, gboolean 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_xml_comment(parent, left, right, changed); } id = ID(left); if (right == NULL) { xmlNode *deleted = NULL; crm_trace("Processing <%s " XML_ATTR_ID "=%s> (complete copy)", crm_element_name(left), id); deleted = add_node_copy(parent, left); crm_xml_add(deleted, XML_DIFF_MARKER, marker); *changed = TRUE; return deleted; } name = crm_element_name(left); CRM_CHECK(name != NULL, return NULL); CRM_CHECK(pcmk__str_eq(crm_element_name(left), crm_element_name(right), pcmk__str_casei), return NULL); // Check for XML_DIFF_MARKER in a child value = crm_element_value(right, XML_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 = create_xml_node(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_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, XML_ATTR_ID) == 0) { // id already obtained when present ~ this case, so just reuse xmlSetProp(diff, (pcmkXmlStr) XML_ATTR_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 = crm_element_value(left, prop_name); 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 = crm_element_value(left, prop_name); if (strcmp(left_value, right_val) == 0) { /* unchanged */ } else { *changed = TRUE; if (full) { xmlAttrPtr pIter = NULL; crm_trace("Changes detected to %s in " "<%s " XML_ATTR_ID "=%s>", prop_name, crm_element_name(left), 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 " XML_ATTR_ID "=%s>", prop_name, left_value, right_val, crm_element_name(left), id); crm_xml_add(diff, prop_name, left_value); } } } } if (!*changed) { free_xml(diff); return NULL; } else if (!full && (id != NULL)) { crm_xml_add(diff, XML_ATTR_ID, id); } done: 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; static struct qb_log_callsite *digest_cs = NULL; const char *digest = crm_element_value(diff, XML_ATTR_DIGEST); const char *version = crm_element_value(diff, XML_ATTR_CRM_VERSION); xmlNode *child_diff = NULL; xmlNode *added = find_xml_node(diff, "diff-added", FALSE); xmlNode *removed = find_xml_node(diff, "diff-removed", FALSE); CRM_CHECK(new_xml != NULL, return FALSE); if (digest_cs == NULL) { digest_cs = qb_log_callsite_get(__func__, __FILE__, "diff-digest", LOG_TRACE, __LINE__, crm_trace_nonlog); } 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_xml_object(NULL, old_xml, child_diff, FALSE, NULL, NULL); } root_nodes_seen++; } if (root_nodes_seen == 0) { *new_xml = copy_xml(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, 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_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; crm_trace("%p %.6x", digest_cs, digest_cs ? digest_cs->targets : 0); if ((digest_cs != NULL) && digest_cs->targets) { 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_diff_markers(*new_xml); // Purge now so diff is ok } return result; } void xml_log_patchset(uint8_t log_level, const char *function, const xmlNode *patchset) { pcmk__xml_log_patchset(log_level, patchset); } // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/common/xml_display.c b/lib/common/xml_display.c index 15c1440568..932200d92d 100644 --- a/lib/common/xml_display.c +++ b/lib/common/xml_display.c @@ -1,492 +1,515 @@ /* * Copyright 2023 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include // PCMK__XML_LOG_BASE, etc. #include "crmcommon_private.h" -static void log_xml_node(GString *buffer, int log_level, const char *prefix, - const xmlNode *data, int depth, uint32_t options); +static void 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 Log an XML comment with depth-based indentation + * \brief Output an XML comment with depth-based indentation * - * Depending on the value of \p log_level, the output may be written to - * \p stdout or to a log file. + * \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 * - * \param[in] log_level Priority at which to log the message - * \param[in] data XML node to log - * \param[in] depth Current indentation level - * \param[in] options Group of \p pcmk__xml_fmt_options flags + * \note This currently produces output only for text-like output objects. */ static void -log_xml_comment(int log_level, const xmlNode *data, int depth, uint32_t options) +show_xml_comment(pcmk__output_t *out, const xmlNode *data, int depth, + uint32_t options) { if (pcmk_is_set(options, pcmk__xml_fmt_open)) { - do_crm_log(log_level, "%*s", - pcmk_is_set(options, pcmk__xml_fmt_pretty)? (2 * depth) : 0, - "", (const char *) data->content); + out->info(out, "%*s", + pcmk_is_set(options, pcmk__xml_fmt_pretty)? (2 * depth) : 0, + "", (const char *) data->content); } } /*! * \internal - * \brief Log an XML element in a formatted way - * - * Depending on the value of \p log_level, the output may be written to - * \p stdout or to a log file. + * \brief Output an XML element in a formatted way * - * \param[in,out] buffer Where to build output strings - * \param[in] log_level Priority at which to log the messages - * \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 + * \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 * - * \note This is a recursive helper function for \p log_xml_node(). + * \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 void -log_xml_element(GString *buffer, int log_level, const char *prefix, - const xmlNode *data, int depth, uint32_t options) +show_xml_element(pcmk__output_t *out, GString *buffer, const char *prefix, + const xmlNode *data, int depth, uint32_t options) { const char *name = crm_element_name(data); int spaces = pcmk_is_set(options, pcmk__xml_fmt_pretty)? (2 * depth) : 0; if (pcmk_is_set(options, pcmk__xml_fmt_open)) { const char *hidden = crm_element_value(data, "hidden"); g_string_truncate(buffer, 0); for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } pcmk__g_strcat(buffer, "<", 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); char *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(XML_DIFF_MARKER, p_name) == 0)) { continue; } if ((hidden != NULL) && (p_name[0] != '\0') && (strstr(hidden, p_name) != NULL)) { pcmk__str_update(&p_copy, "*****"); } else { p_copy = crm_xml_escape(p_value); } pcmk__g_strcat(buffer, " ", p_name, "=\"", pcmk__s(p_copy, ""), "\"", NULL); free(p_copy); } if (xml_has_children(data) && pcmk_is_set(options, pcmk__xml_fmt_children)) { g_string_append_c(buffer, '>'); } else { g_string_append(buffer, "/>"); } - do_crm_log(log_level, "%s%s%s", - pcmk__s(prefix, ""), pcmk__str_empty(prefix)? "" : " ", - buffer->str); + out->info(out, "%s%s%s", + pcmk__s(prefix, ""), pcmk__str_empty(prefix)? "" : " ", + buffer->str); } if (!xml_has_children(data)) { return; } 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)) { - log_xml_node(buffer, log_level, prefix, child, depth + 1, - options|pcmk__xml_fmt_open|pcmk__xml_fmt_close); + show_xml_node(out, buffer, prefix, child, depth + 1, + options|pcmk__xml_fmt_open|pcmk__xml_fmt_close); } } if (pcmk_is_set(options, pcmk__xml_fmt_close)) { - do_crm_log(log_level, "%s%s%*s", - pcmk__s(prefix, ""), pcmk__str_empty(prefix)? "" : " ", - spaces, "", name); + out->info(out, "%s%s%*s", + pcmk__s(prefix, ""), pcmk__str_empty(prefix)? "" : " ", + spaces, "", name); } } /*! * \internal - * \brief Log an XML element or comment in a formatted way - * - * Depending on the value of \p log_level, the output may be written to - * \p stdout or to a log file. + * \brief Output an XML element or comment in a formatted way * - * \param[in,out] buffer Where to build output strings - * \param[in] log_level Priority at which to log the messages - * \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 + * \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 * - * \note This is a recursive helper function for \p pcmk__xml_log(). + * \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 void -log_xml_node(GString *buffer, int log_level, const char *prefix, - const xmlNode *data, int depth, uint32_t options) +show_xml_node(pcmk__output_t *out, GString *buffer, const char *prefix, + const xmlNode *data, int depth, uint32_t options) { - if ((data == NULL) || (log_level == LOG_NEVER)) { + if (data == NULL) { return; } switch (data->type) { case XML_COMMENT_NODE: - log_xml_comment(log_level, data, depth, options); + show_xml_comment(out, data, depth, options); break; case XML_ELEMENT_NODE: - log_xml_element(buffer, log_level, prefix, data, depth, options); + show_xml_element(out, buffer, prefix, data, depth, options); break; default: break; } } /*! * \internal - * \brief Log an XML element or comment in a formatted way + * \brief Output an XML element or comment in a formatted way * - * Depending on the value of \p log_level, the output may be written to - * \p stdout or to a log file. + * \param[in,out] out Output object + * \param[in] log_level Priority at which to log the messages (ignored if + * \p out is not \p NULL) + * \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 * - * \param[in] log_level Priority at which to log the messages - * \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 + * \note This currently produces output only for text-like output objects. */ void -pcmk__xml_log(int log_level, const char *prefix, const xmlNode *data, int depth, - uint32_t options) +pcmk__xml_show(pcmk__output_t *out, int log_level, const char *prefix, + const xmlNode *data, int depth, uint32_t options) { - /* Allocate a buffer once, for log_xml_node() to truncate and reuse in + GString *buffer = NULL; + bool need_output_free = false; + + if (out == NULL) { + 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; + } + + need_output_free = true; + } + + CRM_CHECK(depth >= 0, depth = 0); + + /* Allocate a buffer once, for show_xml_node() to truncate and reuse in * recursive calls */ - GString *buffer = g_string_sized_new(1024); + buffer = g_string_sized_new(1024); - CRM_CHECK(depth >= 0, depth = 0); + show_xml_node(out, buffer, prefix, data, depth, options); - log_xml_node(buffer, log_level, prefix, data, depth, options); g_string_free(buffer, TRUE); + if (need_output_free) { + out->finish(out, CRM_EX_OK, true, NULL); + pcmk__output_free(out); + } } /*! * \internal * \brief Log XML portions that have been marked as changed * * \param[in] log_level Priority at which to log the messages * \param[in] data XML node to log * \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_log_changes(), logging * changes to \p data and its children. */ static void log_xml_changes_recursive(int log_level, 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 = NULL; if ((data == NULL) || (log_level == LOG_NEVER)) { return; } nodepriv = data->_private; if (pcmk_all_flags_set(nodepriv->flags, pcmk__xf_dirty|pcmk__xf_created)) { // Newly created - pcmk__xml_log(log_level, PCMK__XML_PREFIX_CREATED, data, depth, - options - |pcmk__xml_fmt_open - |pcmk__xml_fmt_children - |pcmk__xml_fmt_close); + pcmk__xml_show(NULL, log_level, PCMK__XML_PREFIX_CREATED, data, depth, + options + |pcmk__xml_fmt_open + |pcmk__xml_fmt_children + |pcmk__xml_fmt_close); return; } 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 - pcmk__xml_log(log_level, prefix, data, depth, - options|pcmk__xml_fmt_open); + pcmk__xml_show(NULL, log_level, 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 = crm_element_value(data, name); do_crm_log(log_level, "%s %*s @%s=%s", PCMK__XML_PREFIX_DELETED, spaces, "", name, value); } else if (pcmk_is_set(nodepriv->flags, pcmk__xf_dirty)) { const char *value = crm_element_value(data, name); 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; } do_crm_log(log_level, "%s %*s @%s=%s", prefix, spaces, "", name, value); } } // Log changes to children for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL; child = pcmk__xml_next(child)) { log_xml_changes_recursive(log_level, child, depth + 1, options); } // Log closing tag - pcmk__xml_log(log_level, PCMK__XML_PREFIX_MODIFIED, data, depth, - options|pcmk__xml_fmt_close); + pcmk__xml_show(NULL, log_level, PCMK__XML_PREFIX_MODIFIED, data, depth, + options|pcmk__xml_fmt_close); } else { // 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)) { log_xml_changes_recursive(log_level, child, depth + 1, options); } } } /*! * \internal * \brief Log changes to an XML node and any children * * \param[in] log_level Priority at which to log the message * \param[in] xml XML node to log */ void pcmk__xml_log_changes(uint8_t log_level, const xmlNode *xml) { xml_doc_private_t *docpriv = NULL; if (log_level == LOG_NEVER) { return; } CRM_ASSERT(xml != NULL); CRM_ASSERT(xml->doc != NULL); docpriv = xml->doc->_private; if (!pcmk_is_set(docpriv->flags, pcmk__xf_dirty)) { return; } 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) { do_crm_log(log_level, PCMK__XML_PREFIX_DELETED " %s (%d)", deleted_obj->path, deleted_obj->position); } else { do_crm_log(log_level, PCMK__XML_PREFIX_DELETED " %s", deleted_obj->path); } } log_xml_changes_recursive(log_level, xml, 0, pcmk__xml_fmt_pretty); } // 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; if (log_level == LOG_NEVER) { return; } if (data == NULL) { do_crm_log(log_level, "%s%sNo data to dump as XML", pcmk__s(prefix, ""), pcmk__str_empty(prefix)? "" : " "); return; } /* 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_full_fledged)) { options |= pcmk__xml_fmt_full; } 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; } if (pcmk_is_set(legacy_options, xml_log_option_dirty_add)) { CRM_CHECK(depth >= 0, depth = 0); log_xml_changes_recursive(log_level, data, depth, options); return; } if (pcmk_is_set(options, pcmk__xml_fmt_pretty) && (!xml_has_children(data) || (crm_element_value(data, XML_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 return; } // 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_log(log_level, prefix, data, depth, - options - |pcmk__xml_fmt_open - |pcmk__xml_fmt_children - |pcmk__xml_fmt_close); + pcmk__xml_show(NULL, log_level, prefix, data, depth, + options + |pcmk__xml_fmt_open + |pcmk__xml_fmt_children + |pcmk__xml_fmt_close); } } void xml_log_changes(uint8_t log_level, const char *function, const xmlNode *xml) { pcmk__xml_log_changes(log_level, xml); } // LCOV_EXCL_STOP // End deprecated API