diff --git a/include/crm/common/xml_internal.h b/include/crm/common/xml_internal.h index 3a4f6e96c7..e0bf139476 100644 --- a/include/crm/common/xml_internal.h +++ b/include/crm/common/xml_internal.h @@ -1,456 +1,454 @@ /* * Copyright 2017-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__CRM_COMMON_XML_INTERNAL__H #define PCMK__CRM_COMMON_XML_INTERNAL__H /* * Internal-only wrappers for and extensions to libxml2 (libxslt) */ #include #include // uint32_t #include #include /* transitively imports qblog.h */ #include #include // PCMK_XA_ID, PCMK_XE_CLONE // This file is a wrapper for other {xml_*,xpath}_internal.h headers #include #include #include #include #include #include #include #ifdef __cplusplus extern "C" { #endif /*! * \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) /*! * \internal * \brief Bit flags to control format in XML logs and dumps */ enum pcmk__xml_fmt_options { //! Exclude certain XML attributes (for calculating digests) pcmk__xml_fmt_filtered = (1 << 0), //! Include indentation and newlines pcmk__xml_fmt_pretty = (1 << 1), //! Include the opening tag of an XML element, and include XML comments pcmk__xml_fmt_open = (1 << 3), //! Include the children of an XML element pcmk__xml_fmt_children = (1 << 4), //! Include the closing tag of an XML element pcmk__xml_fmt_close = (1 << 5), // @COMPAT Can we start including text nodes unconditionally? //! Include XML text nodes pcmk__xml_fmt_text = (1 << 6), }; -void pcmk__xml_init(void); - int pcmk__xml_show(pcmk__output_t *out, const char *prefix, const xmlNode *data, int depth, uint32_t options); int pcmk__xml_show_changes(pcmk__output_t *out, const xmlNode *xml); 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); /*! * \internal * \brief Indicators of which XML characters to escape * * XML allows the escaping of special characters by replacing them with entity * references (for example, """) or character references (for * example, " "). * * The special characters '&' (except as the beginning of an entity * reference) and '<' are not allowed in their literal forms in XML * character data. Character data is non-markup text (for example, the content * of a text node). '>' is allowed under most circumstances; we escape * it for safety and symmetry. * * For more details, see the "Character Data and Markup" section of the XML * spec, currently section 2.4: * https://www.w3.org/TR/xml/#dt-markup * * Attribute values are handled specially. * * If an attribute value is delimited by single quotes, then single quotes * must be escaped within the value. * * Similarly, if an attribute value is delimited by double quotes, then double * quotes must be escaped within the value. * * A conformant XML processor replaces a literal whitespace character (tab, * newline, carriage return, space) in an attribute value with a space * (\c '#x20') character. However, a reference to a whitespace character (for * example, \c " " for \c '\n') does not get replaced. * * For more details, see the "Attribute-Value Normalization" section of the * XML spec, currently section 3.3.3. Note that the default attribute type * is CDATA; we don't deal with NMTOKENS, etc.: * https://www.w3.org/TR/xml/#AVNormalize * * Pacemaker always delimits attribute values with double quotes, so there's no * need to escape single quotes. * * Newlines and tabs should be escaped in attribute values when XML is * serialized to text, so that future parsing preserves them rather than * normalizing them to spaces. * * We always escape carriage returns, so that they're not converted to spaces * during attribute-value normalization and because displaying them as literals * is messy. */ enum pcmk__xml_escape_type { /*! * For text nodes. * * Escape \c '<', \c '>', and \c '&' using entity references. * * Do not escape \c '\n' and \c '\t'. * * Escape other non-printing characters using character references. */ pcmk__xml_escape_text, /*! * For attribute values. * * Escape \c '<', \c '>', \c '&', and \c '"' using entity references. * * Escape \c '\n', \c '\t', and other non-printing characters using * character references. */ pcmk__xml_escape_attr, /* @COMPAT Drop escaping of at least '\n' and '\t' for * pcmk__xml_escape_attr_pretty when openstack-info, openstack-floating-ip, * and openstack-virtual-ip resource agents no longer depend on it. * * At time of writing, openstack-info may set a multiline value for the * openstack_ports node attribute. The other two agents query the value and * require it to be on one line with no spaces. */ /*! * For attribute values displayed in text output delimited by double quotes. * * Escape \c '\n' as \c "\\n" * * Escape \c '\r' as \c "\\r" * * Escape \c '\t' as \c "\\t" * * Escape \c '"' as \c "\\"" */ pcmk__xml_escape_attr_pretty, }; bool pcmk__xml_needs_escape(const char *text, enum pcmk__xml_escape_type type); char *pcmk__xml_escape(const char *text, enum pcmk__xml_escape_type type); /*! * \internal * \brief Get the root directory to scan XML artefacts of given kind for * * \param[in] ns governs the hierarchy nesting against the inherent root dir * * \return root directory to scan XML artefacts of given kind for */ char * pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns); /*! * \internal * \brief Get the fully unwrapped path to particular XML artifact (RNG/XSLT) * * \param[in] ns denotes path forming details (parent dir, suffix) * \param[in] filespec symbolic file specification to be combined with * #artefact_ns to form the final path * \return unwrapped path to particular XML artifact (RNG/XSLT) */ char *pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec); /*! * \internal * \brief 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; } void pcmk__xml_free(xmlNode *xml); void pcmk__xml_free_doc(xmlDoc *doc); xmlNode *pcmk__xml_copy(xmlNode *parent, xmlNode *src); /*! * \internal * \brief Flags for operations affecting XML attributes */ enum pcmk__xa_flags { //! Flag has no effect pcmk__xaf_none = 0U, //! Don't overwrite existing values pcmk__xaf_no_overwrite = (1U << 0), /*! * Treat values as score updates where possible (see * \c pcmk__xe_set_score()) */ pcmk__xaf_score_update = (1U << 1), }; void pcmk__xml_sanitize_id(char *id); /* internal XML-related utilities */ /*! * \internal * \brief Flags related to XML change tracking and ACLs */ enum pcmk__xml_flags { //! This flag has no effect pcmk__xf_none = UINT32_C(0), /*! * Node was created or modified, or one of its descendants was created, * modified, moved, or deleted. */ pcmk__xf_dirty = (UINT32_C(1) << 0), //! Node was deleted (set for attribute only) pcmk__xf_deleted = (UINT32_C(1) << 1), //! Node was created pcmk__xf_created = (UINT32_C(1) << 2), //! Node was modified pcmk__xf_modified = (UINT32_C(1) << 3), /*! * \brief Tracking is enabled (set for document only) * * Call \c pcmk__xml_commit_changes() before setting this flag if a clean * start for tracking is needed. */ pcmk__xf_tracking = (UINT32_C(1) << 4), //! Skip counting this node when getting a node's position among siblings pcmk__xf_skip = (UINT32_C(1) << 6), //! Node was moved pcmk__xf_moved = (UINT32_C(1) << 7), //! ACLs are enabled (set for document only) pcmk__xf_acl_enabled = (UINT32_C(1) << 8), /* @TODO Consider splitting the ACL permission flags (pcmk__xf_acl_read, * pcmk__xf_acl_write, pcmk__xf_acl_write, and pcmk__xf_acl_create) into a * separate enum and reserving this enum for tracking-related flags. * * The ACL permission flags have various meanings in different contexts (for * example, what permission an ACL grants or denies; what permissions the * current ACL user has for a given XML node; and possibly others). And * for xml_acl_t objects, they're used in exclusive mode (exactly one is * set), rather than as flags. */ //! ACL read permission pcmk__xf_acl_read = (UINT32_C(1) << 9), //! ACL write permission (implies read permission in most or all contexts) pcmk__xf_acl_write = (UINT32_C(1) << 10), //! ACL deny permission (that is, no permission) pcmk__xf_acl_deny = (UINT32_C(1) << 11), /*! * ACL create permission for attributes (if attribute exists, this is mapped * to \c pcmk__xf_acl_write) */ pcmk__xf_acl_create = (UINT32_C(1) << 12), //! ACLs deny the user access (set for document only) pcmk__xf_acl_denied = (UINT32_C(1) << 13), //! Ignore attribute moves within an element (set for document only) pcmk__xf_ignore_attr_pos = (UINT32_C(1) << 14), }; void pcmk__xml_doc_set_flags(xmlDoc *doc, uint32_t flags); bool pcmk__xml_doc_all_flags_set(const xmlDoc *xml, uint32_t flags); void pcmk__xml_commit_changes(xmlDoc *doc); void pcmk__xml_mark_changes(xmlNode *old_xml, xmlNode *new_xml); bool pcmk__xml_tree_foreach(xmlNode *xml, bool (*fn)(xmlNode *, void *), void *user_data); static inline const char * pcmk__xml_attr_value(const xmlAttr *attr) { return ((attr == NULL) || (attr->children == NULL))? NULL : (const char *) attr->children->content; } /*! * \internal * \brief Check whether a given CIB element was modified in a CIB patchset * * \param[in] patchset CIB XML patchset * \param[in] element XML tag of CIB element to check (\c NULL is equivalent * to \c PCMK_XE_CIB). Supported values include any CIB * element supported by \c pcmk__cib_abs_xpath_for(). * * \return \c true if \p element was modified, or \c false otherwise */ bool pcmk__cib_element_in_patchset(const xmlNode *patchset, const char *element); #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_XML_INTERNAL__H diff --git a/lib/common/logging.c b/lib/common/logging.c index 022a4770be..2053fcc3bc 100644 --- a/lib/common/logging.c +++ b/lib/common/logging.c @@ -1,1295 +1,1298 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Use high-resolution (millisecond) timestamps if libqb supports them #ifdef QB_FEATURE_LOG_HIRES_TIMESTAMPS #define TIMESTAMP_FORMAT_SPEC "%%T" typedef struct timespec *log_time_t; #else #define TIMESTAMP_FORMAT_SPEC "%%t" typedef time_t log_time_t; #endif unsigned int crm_log_level = LOG_INFO; unsigned int crm_trace_nonlog = 0; bool pcmk__is_daemon = false; static unsigned int crm_log_priority = LOG_NOTICE; static guint pcmk__log_id = 0; static guint pcmk__glib_log_id = 0; static guint pcmk__gio_log_id = 0; static guint pcmk__gmodule_log_id = 0; static guint pcmk__gthread_log_id = 0; static pcmk__output_t *logger_out = NULL; pcmk__config_error_func pcmk__config_error_handler = NULL; pcmk__config_warning_func pcmk__config_warning_handler = NULL; void *pcmk__config_error_context = NULL; void *pcmk__config_warning_context = NULL; static gboolean crm_tracing_enabled(void); static void crm_glib_handler(const gchar * log_domain, GLogLevelFlags flags, const gchar * message, gpointer user_data) { int log_level = LOG_WARNING; GLogLevelFlags msg_level = (flags & G_LOG_LEVEL_MASK); static struct qb_log_callsite *glib_cs = NULL; if (glib_cs == NULL) { glib_cs = qb_log_callsite_get(__func__, __FILE__, "glib-handler", LOG_DEBUG, __LINE__, crm_trace_nonlog); } switch (msg_level) { case G_LOG_LEVEL_CRITICAL: log_level = LOG_CRIT; if (!crm_is_callsite_active(glib_cs, LOG_DEBUG, crm_trace_nonlog)) { /* log and record how we got here */ crm_abort(__FILE__, __func__, __LINE__, message, TRUE, TRUE); } break; case G_LOG_LEVEL_ERROR: log_level = LOG_ERR; break; case G_LOG_LEVEL_MESSAGE: log_level = LOG_NOTICE; break; case G_LOG_LEVEL_INFO: log_level = LOG_INFO; break; case G_LOG_LEVEL_DEBUG: log_level = LOG_DEBUG; break; case G_LOG_LEVEL_WARNING: case G_LOG_FLAG_RECURSION: case G_LOG_FLAG_FATAL: case G_LOG_LEVEL_MASK: log_level = LOG_WARNING; break; } do_crm_log(log_level, "%s: %s", log_domain, message); } #ifndef NAME_MAX # define NAME_MAX 256 #endif /*! * \internal * \brief Write out a blackbox (enabling blackboxes if needed) * * \param[in] nsig Signal number that was received * * \note This is a true signal handler, and so must be async-safe. */ static void crm_trigger_blackbox(int nsig) { if(nsig == SIGTRAP) { /* Turn it on if it wasn't already */ crm_enable_blackbox(nsig); } crm_write_blackbox(nsig, NULL); } void crm_log_deinit(void) { if (pcmk__log_id == 0) { return; } g_log_remove_handler(G_LOG_DOMAIN, pcmk__log_id); pcmk__log_id = 0; g_log_remove_handler("GLib", pcmk__glib_log_id); pcmk__glib_log_id = 0; g_log_remove_handler("GLib-GIO", pcmk__gio_log_id); pcmk__gio_log_id = 0; g_log_remove_handler("GModule", pcmk__gmodule_log_id); pcmk__gmodule_log_id = 0; g_log_remove_handler("GThread", pcmk__gthread_log_id); pcmk__gthread_log_id = 0; } #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 qb_log_ctl(method, QB_LOG_CONF_EXTENDED, QB_FALSE); qb_log_format_set(method, "%g %p: %b"); } else { // Everything else gets more detail, for advanced troubleshooting int offset = 0; char fmt[FMT_MAX]; if (method > QB_LOG_STDERR) { // If logging to file, prefix with timestamp, node name, daemon ID offset += snprintf(fmt + offset, FMT_MAX - offset, TIMESTAMP_FORMAT_SPEC " %s %-20s[%lu] ", use_nodename, daemon, (unsigned long) use_pid); } // Add function name (in parentheses) offset += snprintf(fmt + offset, FMT_MAX - offset, "(%%n"); if (crm_tracing_enabled()) { // When tracing, add file and line number offset += snprintf(fmt + offset, FMT_MAX - offset, "@%%f:%%l"); } offset += snprintf(fmt + offset, FMT_MAX - offset, ")"); // Add tag (if any), severity, and actual message offset += snprintf(fmt + offset, FMT_MAX - offset, " %%g\t%%p: %%b"); CRM_LOG_ASSERT(offset > 0); qb_log_format_set(method, fmt); } } #define DEFAULT_LOG_FILE CRM_LOG_DIR "/pacemaker.log" static bool logfile_disabled(const char *filename) { return pcmk__str_eq(filename, PCMK_VALUE_NONE, pcmk__str_casei) || pcmk__str_eq(filename, "/dev/null", pcmk__str_none); } /*! * \internal * \brief Fix log file ownership if group is wrong or doesn't have access * * \param[in] filename Log file name (for logging only) * \param[in] logfd Log file descriptor * * \return Standard Pacemaker return code */ static int chown_logfile(const char *filename, int logfd) { uid_t pcmk_uid = 0; gid_t pcmk_gid = 0; struct stat st; int rc; // Get the log file's current ownership and permissions if (fstat(logfd, &st) < 0) { return errno; } // Any other errors don't prevent file from being used as log rc = pcmk_daemon_user(&pcmk_uid, &pcmk_gid); if (rc != pcmk_ok) { rc = pcmk_legacy2rc(rc); crm_warn("Not changing '%s' ownership because user information " "unavailable: %s", filename, pcmk_rc_str(rc)); return pcmk_rc_ok; } if ((st.st_gid == pcmk_gid) && ((st.st_mode & S_IRWXG) == (S_IRGRP|S_IWGRP))) { return pcmk_rc_ok; } if (fchown(logfd, pcmk_uid, pcmk_gid) < 0) { crm_warn("Couldn't change '%s' ownership to user %s gid %d: %s", filename, CRM_DAEMON_USER, pcmk_gid, strerror(errno)); } return pcmk_rc_ok; } // Reset log file permissions (using environment variable if set) static void chmod_logfile(const char *filename, int logfd) { const char *modestr = pcmk__env_option(PCMK__ENV_LOGFILE_MODE); mode_t filemode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP; if (modestr != NULL) { long filemode_l = strtol(modestr, NULL, 8); if ((filemode_l != LONG_MIN) && (filemode_l != LONG_MAX)) { filemode = (mode_t) filemode_l; } } if ((filemode != 0) && (fchmod(logfd, filemode) < 0)) { crm_warn("Couldn't change '%s' mode to %04o: %s", filename, filemode, strerror(errno)); } } // If we're root, correct a log file's permissions if needed static int set_logfile_permissions(const char *filename, FILE *logfile) { if (geteuid() == 0) { int logfd = fileno(logfile); int rc = chown_logfile(filename, logfd); if (rc != pcmk_rc_ok) { return rc; } chmod_logfile(filename, logfd); } return pcmk_rc_ok; } // Enable libqb logging to a new log file static void enable_logfile(int fd) { qb_log_ctl(fd, QB_LOG_CONF_ENABLED, QB_TRUE); #if 0 qb_log_ctl(fd, QB_LOG_CONF_FILE_SYNC, 1); // Turn on synchronous writes #endif #ifdef HAVE_qb_log_conf_QB_LOG_CONF_MAX_LINE_LEN // Longer than default, for logging long XML lines qb_log_ctl(fd, QB_LOG_CONF_MAX_LINE_LEN, 800); #endif crm_update_callsites(); } static inline void disable_logfile(int fd) { qb_log_ctl(fd, QB_LOG_CONF_ENABLED, QB_FALSE); } static void setenv_logfile(const char *filename) { // Some resource agents will log only if environment variable is set if (pcmk__env_option(PCMK__ENV_LOGFILE) == NULL) { pcmk__set_env_option(PCMK__ENV_LOGFILE, filename, true); } } /*! * \brief Add a file to be used as a Pacemaker detail log * * \param[in] filename Name of log file to use * * \return Standard Pacemaker return code */ int pcmk__add_logfile(const char *filename) { /* No log messages from this function will be logged to the new log! * If another target such as syslog has already been added, the messages * should show up there. */ int fd = 0; int rc = pcmk_rc_ok; FILE *logfile = NULL; bool is_default = false; static int default_fd = -1; static bool have_logfile = false; // Use default if caller didn't specify (and we don't already have one) if (filename == NULL) { if (have_logfile) { return pcmk_rc_ok; } filename = DEFAULT_LOG_FILE; } // If the user doesn't want logging, we're done if (logfile_disabled(filename)) { return pcmk_rc_ok; } // If the caller wants the default and we already have it, we're done is_default = pcmk__str_eq(filename, DEFAULT_LOG_FILE, pcmk__str_none); if (is_default && (default_fd >= 0)) { return pcmk_rc_ok; } // Check whether we have write access to the file logfile = fopen(filename, "a"); if (logfile == NULL) { rc = errno; crm_warn("Logging to '%s' is disabled: %s " QB_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 " QB_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 " QB_XS " qb_log_file_open", filename, strerror(-fd)); return -fd; // == +errno } if (is_default) { default_fd = fd; setenv_logfile(filename); } else if (default_fd >= 0) { crm_notice("Switching logging to %s", filename); disable_logfile(default_fd); } crm_notice("Additional logging available in %s", filename); enable_logfile(fd); have_logfile = true; return pcmk_rc_ok; } /*! * \brief Add multiple additional log files * * \param[in] log_files Array of log files to add * \param[in] out Output object to use for error reporting * * \return Standard Pacemaker return code */ void pcmk__add_logfiles(gchar **log_files, pcmk__output_t *out) { if (log_files == NULL) { return; } for (gchar **fname = log_files; *fname != NULL; fname++) { int rc = pcmk__add_logfile(*fname); if (rc != pcmk_rc_ok) { out->err(out, "Logging to %s is disabled: %s", *fname, pcmk_rc_str(rc)); } } } static int blackbox_trigger = 0; static volatile char *blackbox_file_prefix = NULL; static void blackbox_logger(int32_t t, struct qb_log_callsite *cs, log_time_t timestamp, const char *msg) { if(cs && cs->priority < LOG_ERR) { crm_write_blackbox(SIGTRAP, cs); /* Bypass the over-dumping logic */ } else { crm_write_blackbox(0, cs); } } static void crm_control_blackbox(int nsig, bool enable) { int lpc = 0; if (blackbox_file_prefix == NULL) { pid_t pid = getpid(); blackbox_file_prefix = crm_strdup_printf("%s/%s-%lu", CRM_BLACKBOX_DIR, crm_system_name, (unsigned long) pid); } if (enable && qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) { qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_SIZE, 5 * 1024 * 1024); /* Any size change drops existing entries */ qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_TRUE); /* Setting the size seems to disable it */ /* Enable synchronous logging */ for (lpc = QB_LOG_BLACKBOX; lpc < QB_LOG_TARGET_MAX; lpc++) { qb_log_ctl(lpc, QB_LOG_CONF_FILE_SYNC, QB_TRUE); } crm_notice("Initiated blackbox recorder: %s", blackbox_file_prefix); /* Save to disk on abnormal termination */ crm_signal_handler(SIGSEGV, crm_trigger_blackbox); crm_signal_handler(SIGABRT, crm_trigger_blackbox); crm_signal_handler(SIGILL, crm_trigger_blackbox); crm_signal_handler(SIGBUS, crm_trigger_blackbox); crm_signal_handler(SIGFPE, crm_trigger_blackbox); crm_update_callsites(); blackbox_trigger = qb_log_custom_open(blackbox_logger, NULL, NULL, NULL); qb_log_ctl(blackbox_trigger, QB_LOG_CONF_ENABLED, QB_TRUE); crm_trace("Trigger: %d is %d %d", blackbox_trigger, qb_log_ctl(blackbox_trigger, QB_LOG_CONF_STATE_GET, 0), QB_LOG_STATE_ENABLED); crm_update_callsites(); } else if (!enable && qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_STATE_GET, 0) == QB_LOG_STATE_ENABLED) { qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE); /* Disable synchronous logging again when the blackbox is disabled */ for (lpc = QB_LOG_BLACKBOX; lpc < QB_LOG_TARGET_MAX; lpc++) { qb_log_ctl(lpc, QB_LOG_CONF_FILE_SYNC, QB_FALSE); } } } void crm_enable_blackbox(int nsig) { crm_control_blackbox(nsig, TRUE); } void crm_disable_blackbox(int nsig) { crm_control_blackbox(nsig, FALSE); } /*! * \internal * \brief Write out a blackbox, if blackboxes are enabled * * \param[in] nsig Signal that was received * \param[in] cs libqb callsite * * \note This may be called via a true signal handler and so must be async-safe. * @TODO actually make this async-safe */ void crm_write_blackbox(int nsig, const struct qb_log_callsite *cs) { static volatile int counter = 1; static volatile time_t last = 0; char buffer[NAME_MAX]; time_t now = time(NULL); if (blackbox_file_prefix == NULL) { return; } switch (nsig) { case 0: case SIGTRAP: /* The graceful case - such as assertion failure or user request */ if (nsig == 0 && now == last) { /* Prevent over-dumping */ return; } snprintf(buffer, NAME_MAX, "%s.%d", blackbox_file_prefix, counter++); if (nsig == SIGTRAP) { crm_notice("Blackbox dump requested, please see %s for contents", buffer); } else if (cs) { syslog(LOG_NOTICE, "Problem detected at %s:%d (%s), please see %s for additional details", cs->function, cs->lineno, cs->filename, buffer); } else { crm_notice("Problem detected, please see %s for additional details", buffer); } last = now; qb_log_blackbox_write_to_file(buffer); /* Flush the existing contents * A size change would also work */ qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE); qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_TRUE); break; default: /* Do as little as possible, just try to get what we have out * We logged the filename when the blackbox was enabled */ crm_signal_handler(nsig, SIG_DFL); qb_log_blackbox_write_to_file((const char *)blackbox_file_prefix); qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE); raise(nsig); break; } } static const char * crm_quark_to_string(uint32_t tag) { const char *text = g_quark_to_string(tag); if (text) { return text; } return ""; } static void crm_log_filter_source(int source, const char *trace_files, const char *trace_fns, const char *trace_fmts, const char *trace_tags, const char *trace_blackbox, struct qb_log_callsite *cs) { if (qb_log_ctl(source, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) { return; } else if (cs->tags != crm_trace_nonlog && source == QB_LOG_BLACKBOX) { /* Blackbox gets everything if enabled */ qb_bit_set(cs->targets, source); } else if (source == blackbox_trigger && blackbox_trigger > 0) { /* Should this log message result in the blackbox being dumped */ if (cs->priority <= LOG_ERR) { qb_bit_set(cs->targets, source); } else if (trace_blackbox) { char *key = crm_strdup_printf("%s:%d", cs->function, cs->lineno); if (strstr(trace_blackbox, key) != NULL) { qb_bit_set(cs->targets, source); } free(key); } } else if (source == QB_LOG_SYSLOG) { /* No tracing to syslog */ if (cs->priority <= crm_log_priority && cs->priority <= crm_log_level) { qb_bit_set(cs->targets, source); } /* Log file tracing options... */ } else if (cs->priority <= crm_log_level) { qb_bit_set(cs->targets, source); } else if (trace_files && strstr(trace_files, cs->filename) != NULL) { qb_bit_set(cs->targets, source); } else if (trace_fns && strstr(trace_fns, cs->function) != NULL) { qb_bit_set(cs->targets, source); } else if (trace_fmts && strstr(trace_fmts, cs->format) != NULL) { qb_bit_set(cs->targets, source); } else if (trace_tags && cs->tags != 0 && cs->tags != crm_trace_nonlog && g_quark_to_string(cs->tags) != NULL) { qb_bit_set(cs->targets, source); } } #ifndef HAVE_STRCHRNUL /* strchrnul() is a GNU extension. If not present, use our own definition. * The GNU version returns char*, but we only need it to be const char*. */ static const char * strchrnul(const char *s, int c) { while ((*s != c) && (*s != '\0')) { ++s; } return s; } #endif static void crm_log_filter(struct qb_log_callsite *cs) { int lpc = 0; static int need_init = 1; static const char *trace_fns = NULL; static const char *trace_tags = NULL; static const char *trace_fmts = NULL; static const char *trace_files = NULL; static const char *trace_blackbox = NULL; if (need_init) { need_init = 0; trace_fns = pcmk__env_option(PCMK__ENV_TRACE_FUNCTIONS); trace_fmts = pcmk__env_option(PCMK__ENV_TRACE_FORMATS); trace_tags = pcmk__env_option(PCMK__ENV_TRACE_TAGS); trace_files = pcmk__env_option(PCMK__ENV_TRACE_FILES); trace_blackbox = pcmk__env_option(PCMK__ENV_TRACE_BLACKBOX); if (trace_tags != NULL) { uint32_t tag; char token[500]; const char *offset = NULL; const char *next = trace_tags; do { offset = next; next = strchrnul(offset, ','); snprintf(token, sizeof(token), "%.*s", (int)(next - offset), offset); tag = g_quark_from_string(token); crm_info("Created GQuark %u from token '%s' in '%s'", tag, token, trace_tags); if (next[0] != 0) { next++; } } while (next != NULL && next[0] != 0); } } cs->targets = 0; /* Reset then find targets to enable */ for (lpc = QB_LOG_SYSLOG; lpc < QB_LOG_TARGET_MAX; lpc++) { crm_log_filter_source(lpc, trace_files, trace_fns, trace_fmts, trace_tags, trace_blackbox, cs); } } gboolean crm_is_callsite_active(struct qb_log_callsite *cs, uint8_t level, uint32_t tags) { gboolean refilter = FALSE; if (cs == NULL) { return FALSE; } if (cs->priority != level) { cs->priority = level; refilter = TRUE; } if (cs->tags != tags) { cs->tags = tags; refilter = TRUE; } if (refilter) { crm_log_filter(cs); } if (cs->targets == 0) { return FALSE; } return TRUE; } void crm_update_callsites(void) { static gboolean log = TRUE; if (log) { log = FALSE; crm_debug ("Enabling callsites based on priority=%d, files=%s, functions=%s, formats=%s, tags=%s", crm_log_level, pcmk__env_option(PCMK__ENV_TRACE_FILES), pcmk__env_option(PCMK__ENV_TRACE_FUNCTIONS), pcmk__env_option(PCMK__ENV_TRACE_FORMATS), pcmk__env_option(PCMK__ENV_TRACE_TAGS)); } qb_log_filter_fn_set(crm_log_filter); } static gboolean crm_tracing_enabled(void) { return (crm_log_level == LOG_TRACE) || (pcmk__env_option(PCMK__ENV_TRACE_FILES) != NULL) || (pcmk__env_option(PCMK__ENV_TRACE_FUNCTIONS) != NULL) || (pcmk__env_option(PCMK__ENV_TRACE_FORMATS) != NULL) || (pcmk__env_option(PCMK__ENV_TRACE_TAGS) != NULL); } static int crm_priority2int(const char *name) { struct syslog_names { const char *name; int priority; }; static struct syslog_names p_names[] = { {"emerg", LOG_EMERG}, {"alert", LOG_ALERT}, {"crit", LOG_CRIT}, {"error", LOG_ERR}, {"warning", LOG_WARNING}, {"notice", LOG_NOTICE}, {"info", LOG_INFO}, {"debug", LOG_DEBUG}, {NULL, -1} }; int lpc; for (lpc = 0; name != NULL && p_names[lpc].name != NULL; lpc++) { if (pcmk__str_eq(p_names[lpc].name, name, pcmk__str_none)) { return p_names[lpc].priority; } } return crm_log_priority; } /*! * \internal * \brief Set the identifier for the current process * * If the identifier crm_system_name is not already set, then it is set as follows: * - it is passed to the function via the "entity" parameter, or * - it is derived from the executable name * * The identifier can be used in logs, IPC, and more. * * This method also sets the PCMK_service environment variable. * * \param[in] entity If not NULL, will be assigned to the identifier * \param[in] argc The number of command line parameters * \param[in] argv The command line parameter values */ static void set_identity(const char *entity, int argc, char *const *argv) { if (crm_system_name != NULL) { return; // Already set, don't overwrite } if (entity != NULL) { crm_system_name = pcmk__str_copy(entity); } else if ((argc > 0) && (argv != NULL)) { char *mutable = strdup(argv[0]); char *modified = basename(mutable); if (strstr(modified, "lt-") == modified) { modified += 3; } crm_system_name = pcmk__str_copy(modified); free(mutable); } else { crm_system_name = pcmk__str_copy("Unknown"); } // Used by fencing.py.py (in fence-agents) pcmk__set_env_option(PCMK__ENV_SERVICE, crm_system_name, false); } void crm_log_preinit(const char *entity, int argc, char *const *argv) { /* Configure libqb logging with nothing turned on */ struct utsname res; int lpc = 0; int32_t qb_facility = 0; pid_t pid = getpid(); const char *nodename = "localhost"; static bool have_logging = false; GLogLevelFlags log_levels; if (have_logging) { return; } have_logging = true; - pcmk__xml_init(); + /* @TODO Try to create a more obvious "global Pacemaker initializer" + * function than crm_log_preinit(), and call pcmk__schema_init() there + */ + pcmk__schema_init(); if (crm_trace_nonlog == 0) { crm_trace_nonlog = g_quark_from_static_string("Pacemaker non-logging tracepoint"); } umask(S_IWGRP | S_IWOTH | S_IROTH); /* Add a log handler for messages from our log domain at any log level. */ log_levels = G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION; pcmk__log_id = g_log_set_handler(G_LOG_DOMAIN, log_levels, crm_glib_handler, NULL); /* Add a log handler for messages from the GLib domains at any log level. */ pcmk__glib_log_id = g_log_set_handler("GLib", log_levels, crm_glib_handler, NULL); pcmk__gio_log_id = g_log_set_handler("GLib-GIO", log_levels, crm_glib_handler, NULL); pcmk__gmodule_log_id = g_log_set_handler("GModule", log_levels, crm_glib_handler, NULL); pcmk__gthread_log_id = g_log_set_handler("GThread", log_levels, crm_glib_handler, NULL); /* glib should not abort for any messages from the Pacemaker domain, but * other domains are still free to specify their own behavior. However, * note that G_LOG_LEVEL_ERROR is always fatal regardless of what we do * here. */ g_log_set_fatal_mask(G_LOG_DOMAIN, 0); /* 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 pcmk__assert(bindtextdomain(PACKAGE, PCMK__LOCALE_DIR) != NULL); // Tell gettext to use the Pacemaker message catalogs pcmk__assert(textdomain(PACKAGE) != NULL); // Tell gettext that the translated strings are stored in UTF-8 bind_textdomain_codeset(PACKAGE, "UTF-8"); #endif } gboolean crm_log_init(const char *entity, uint8_t level, gboolean daemon, gboolean to_stderr, int argc, char **argv, gboolean quiet) { const char *syslog_priority = NULL; const char *facility = pcmk__env_option(PCMK__ENV_LOGFACILITY); const char *f_copy = facility; pcmk__is_daemon = daemon; crm_log_preinit(entity, argc, argv); if (level > LOG_TRACE) { level = LOG_TRACE; } if(level > crm_log_level) { crm_log_level = level; } /* Should we log to syslog */ if (facility == NULL) { if (pcmk__is_daemon) { facility = "daemon"; } else { facility = PCMK_VALUE_NONE; } pcmk__set_env_option(PCMK__ENV_LOGFACILITY, facility, true); } if (pcmk__str_eq(facility, PCMK_VALUE_NONE, pcmk__str_casei)) { quiet = TRUE; } else { qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_FACILITY, qb_log_facility2int(facility)); } if (pcmk__env_option_enabled(crm_system_name, PCMK__ENV_DEBUG)) { /* Override the default setting */ crm_log_level = LOG_DEBUG; } /* What lower threshold do we have for sending to syslog */ syslog_priority = pcmk__env_option(PCMK__ENV_LOGPRIORITY); if (syslog_priority) { crm_log_priority = crm_priority2int(syslog_priority); } qb_log_filter_ctl(QB_LOG_SYSLOG, QB_LOG_FILTER_ADD, QB_LOG_FILTER_FILE, "*", crm_log_priority); // Log to syslog unless requested to be quiet if (!quiet) { qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_ENABLED, QB_TRUE); } /* Should we log to stderr */ if (pcmk__env_option_enabled(crm_system_name, PCMK__ENV_STDERR)) { /* Override the default setting */ to_stderr = TRUE; } crm_enable_stderr(to_stderr); // Log to a file if we're a daemon or user asked for one { const char *logfile = pcmk__env_option(PCMK__ENV_LOGFILE); if (!pcmk__str_eq(PCMK_VALUE_NONE, logfile, pcmk__str_casei) && (pcmk__is_daemon || (logfile != NULL))) { // Daemons always get a log file, unless explicitly set to "none" pcmk__add_logfile(logfile); } } if (pcmk__is_daemon && pcmk__env_option_enabled(crm_system_name, PCMK__ENV_BLACKBOX)) { crm_enable_blackbox(0); } /* Summary */ crm_trace("Quiet: %d, facility %s", quiet, f_copy); pcmk__env_option(PCMK__ENV_LOGFILE); pcmk__env_option(PCMK__ENV_LOGFACILITY); crm_update_callsites(); /* Ok, now we can start logging... */ // Disable daemon request if user isn't root or Pacemaker daemon user if (pcmk__is_daemon) { const char *user = getenv("USER"); if (user != NULL && !pcmk__strcase_any_of(user, "root", CRM_DAEMON_USER, NULL)) { crm_trace("Not switching to corefile directory for %s", user); pcmk__is_daemon = false; } } if (pcmk__is_daemon) { int user = getuid(); struct passwd *pwent = getpwuid(user); if (pwent == NULL) { crm_perror(LOG_ERR, "Cannot get name for uid: %d", user); } else if (!pcmk__strcase_any_of(pwent->pw_name, "root", CRM_DAEMON_USER, NULL)) { crm_trace("Don't change active directory for regular user: %s", pwent->pw_name); } else if (chdir(CRM_CORE_DIR) < 0) { crm_perror(LOG_INFO, "Cannot change active directory to " CRM_CORE_DIR); } else { crm_info("Changed active directory to " CRM_CORE_DIR); } /* Original meanings from signal(7) * * Signal Value Action Comment * SIGTRAP 5 Core Trace/breakpoint trap * SIGUSR1 30,10,16 Term User-defined signal 1 * SIGUSR2 31,12,17 Term User-defined signal 2 * * Our usage is as similar as possible */ mainloop_add_signal(SIGUSR1, crm_enable_blackbox); mainloop_add_signal(SIGUSR2, crm_disable_blackbox); mainloop_add_signal(SIGTRAP, crm_trigger_blackbox); } else if (!quiet) { crm_log_args(argc, argv); } return TRUE; } /* returns the old value */ unsigned int set_crm_log_level(unsigned int level) { unsigned int old = crm_log_level; if (level > LOG_TRACE) { level = LOG_TRACE; } crm_log_level = level; crm_update_callsites(); crm_trace("New log level: %d", level); return old; } void crm_enable_stderr(int enable) { if (enable && qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) { qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_ENABLED, QB_TRUE); crm_update_callsites(); } else if (enable == FALSE) { qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_ENABLED, QB_FALSE); } } /*! * \brief Make logging more verbose * * If logging to stderr is not already enabled when this function is called, * enable it. Otherwise, increase the log level by 1. * * \param[in] argc Ignored * \param[in] argv Ignored */ void crm_bump_log_level(int argc, char **argv) { if (qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) { crm_enable_stderr(TRUE); } else { set_crm_log_level(crm_log_level + 1); } } unsigned int get_crm_log_level(void) { return crm_log_level; } /*! * \brief Log the command line (once) * * \param[in] Number of values in \p argv * \param[in] Command-line arguments (including command name) * * \note This function will only log once, even if called with different * arguments. */ void crm_log_args(int argc, char **argv) { static bool logged = false; gchar *arg_string = NULL; if ((argc == 0) || (argv == NULL) || logged) { return; } logged = true; arg_string = g_strjoinv(" ", argv); crm_notice("Invoked: %s", arg_string); g_free(arg_string); } void crm_log_output_fn(const char *file, const char *function, int line, int level, const char *prefix, const char *output) { const char *next = NULL; const char *offset = NULL; if (level == LOG_NEVER) { return; } if (output == NULL) { if (level != LOG_STDOUT) { level = LOG_TRACE; } output = "-- empty --"; } next = output; do { offset = next; next = strchrnul(offset, '\n'); do_crm_log_alias(level, file, function, line, "%s [ %.*s ]", prefix, (int)(next - offset), offset); if (next[0] != 0) { next++; } } while (next != NULL && next[0] != 0); } void pcmk__cli_init_logging(const char *name, unsigned int verbosity) { crm_log_init(name, LOG_ERR, FALSE, FALSE, 0, NULL, TRUE); for (int i = 0; i < verbosity; i++) { /* These arguments are ignored, so pass placeholders. */ crm_bump_log_level(0, NULL); } } /*! * \brief Log XML line-by-line in a formatted fashion * * \param[in] file File name to use for log filtering * \param[in] function Function name to use for log filtering * \param[in] line Line number to use for log filtering * \param[in] tags Logging tags to use for log filtering * \param[in] level Priority at which to log the messages * \param[in] text Prefix for each line * \param[in] xml XML to log * * \note This does nothing when \p level is \p LOG_STDOUT. * \note Do not call this function directly. It should be called only from the * \p do_crm_log_xml() macro. */ void pcmk_log_xml_as(const char *file, const char *function, uint32_t line, uint32_t tags, uint8_t level, const char *text, const xmlNode *xml) { if (xml == NULL) { do_crm_log(level, "%s%sNo data to dump as XML", pcmk__s(text, ""), pcmk__str_empty(text)? "" : " "); } else { if (logger_out == NULL) { CRM_CHECK(pcmk__log_output_new(&logger_out) == pcmk_rc_ok, return); } pcmk__output_set_log_level(logger_out, level); pcmk__output_set_log_filter(logger_out, file, function, line, tags); pcmk__xml_show(logger_out, text, xml, 1, pcmk__xml_fmt_pretty |pcmk__xml_fmt_open |pcmk__xml_fmt_children |pcmk__xml_fmt_close); pcmk__output_set_log_filter(logger_out, NULL, NULL, 0U, 0U); } } /*! * \internal * \brief Log XML changes line-by-line in a formatted fashion * * \param[in] file File name to use for log filtering * \param[in] function Function name to use for log filtering * \param[in] line Line number to use for log filtering * \param[in] tags Logging tags to use for log filtering * \param[in] level Priority at which to log the messages * \param[in] xml XML whose changes to log * * \note This does nothing when \p level is \c LOG_STDOUT. */ void pcmk__log_xml_changes_as(const char *file, const char *function, uint32_t line, uint32_t tags, uint8_t level, const xmlNode *xml) { if (xml == NULL) { do_crm_log(level, "No XML to dump"); return; } if (logger_out == NULL) { CRM_CHECK(pcmk__log_output_new(&logger_out) == pcmk_rc_ok, return); } pcmk__output_set_log_level(logger_out, level); pcmk__output_set_log_filter(logger_out, file, function, line, tags); pcmk__xml_show_changes(logger_out, xml); pcmk__output_set_log_filter(logger_out, NULL, NULL, 0U, 0U); } /*! * \internal * \brief Log an XML patchset line-by-line in a formatted fashion * * \param[in] file File name to use for log filtering * \param[in] function Function name to use for log filtering * \param[in] line Line number to use for log filtering * \param[in] tags Logging tags to use for log filtering * \param[in] level Priority at which to log the messages * \param[in] patchset XML patchset to log * * \note This does nothing when \p level is \c LOG_STDOUT. */ void pcmk__log_xml_patchset_as(const char *file, const char *function, uint32_t line, uint32_t tags, uint8_t level, const xmlNode *patchset) { if (patchset == NULL) { do_crm_log(level, "No patchset to dump"); return; } if (logger_out == NULL) { CRM_CHECK(pcmk__log_output_new(&logger_out) == pcmk_rc_ok, return); } pcmk__output_set_log_level(logger_out, level); pcmk__output_set_log_filter(logger_out, file, function, line, tags); logger_out->message(logger_out, "xml-patchset", patchset); pcmk__output_set_log_filter(logger_out, NULL, NULL, 0U, 0U); } /*! * \internal * \brief Free the logging library's internal log output object */ void pcmk__free_common_logger(void) { if (logger_out != NULL) { logger_out->finish(logger_out, CRM_EX_OK, true, NULL); pcmk__output_free(logger_out); logger_out = NULL; } } void pcmk__set_config_error_handler(pcmk__config_error_func error_handler, void *error_context) { pcmk__config_error_handler = error_handler; pcmk__config_error_context = error_context; } void pcmk__set_config_warning_handler(pcmk__config_warning_func warning_handler, void *warning_context) { pcmk__config_warning_handler = warning_handler; pcmk__config_warning_context = warning_context; } diff --git a/lib/common/tests/xml/Makefile.am b/lib/common/tests/xml/Makefile.am index 89586faadc..bc180f36e3 100644 --- a/lib/common/tests/xml/Makefile.am +++ b/lib/common/tests/xml/Makefile.am @@ -1,24 +1,23 @@ # -# Copyright 2022-2024 the Pacemaker project contributors +# Copyright 2022-2025 the Pacemaker project contributors # # The version control history for this file may have further details. # # This source code is licensed under the GNU General Public License version 2 # or later (GPLv2+) WITHOUT ANY WARRANTY. # include $(top_srcdir)/mk/common.mk include $(top_srcdir)/mk/tap.mk include $(top_srcdir)/mk/unittest.mk # Add "_test" to the end of all test program names to simplify .gitignore. check_PROGRAMS = \ pcmk__xml_escape_test \ - pcmk__xml_init_test \ pcmk__xml_is_name_char_test \ pcmk__xml_is_name_start_char_test \ pcmk__xml_needs_escape_test \ pcmk__xml_new_doc_test \ pcmk__xml_sanitize_id_test TESTS = $(check_PROGRAMS) diff --git a/lib/common/unittest.c b/lib/common/unittest.c index 40bce7377a..bd5ee2520b 100644 --- a/lib/common/unittest.c +++ b/lib/common/unittest.c @@ -1,174 +1,172 @@ /* * Copyright 2024-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include // LCOV_EXCL_START void pcmk__assert_validates(xmlNode *xml) { const char *schema_dir = NULL; char *cmd = NULL; gchar *out = NULL; gchar *err = NULL; gint status; GError *gerr = NULL; char *xmllint_input = crm_strdup_printf("%s/test-xmllint.XXXXXX", pcmk__get_tmpdir()); int fd; int rc; fd = mkstemp(xmllint_input); if (fd < 0) { fail_msg("Could not create temp file: %s", strerror(errno)); } rc = pcmk__xml2fd(fd, xml); if (rc != pcmk_rc_ok) { unlink(xmllint_input); fail_msg("Could not write temp file: %s", pcmk_rc_str(rc)); } close(fd); /* This should be set as part of AM_TESTS_ENVIRONMENT in Makefile.am. */ schema_dir = getenv("PCMK_schema_directory"); if (schema_dir == NULL) { unlink(xmllint_input); fail_msg("PCMK_schema_directory is not set in test environment"); } cmd = crm_strdup_printf("xmllint --relaxng %s/api/api-result.rng %s", schema_dir, xmllint_input); if (!g_spawn_command_line_sync(cmd, &out, &err, &status, &gerr)) { unlink(xmllint_input); fail_msg("Error occurred when performing validation: %s", gerr->message); } if (WIFEXITED(status) && WEXITSTATUS(status) != 0) { unlink(xmllint_input); fail_msg("XML validation failed: %s\n%s\n", out, err); } free(cmd); g_free(out); g_free(err); unlink(xmllint_input); free(xmllint_input); } /*! * \internal * \brief Perform setup for a group of unit tests that manipulate XML * * This function is suitable for being passed as the first argument to the * \c PCMK__UNIT_TEST macro. * * \param[in] state Ignored * * \return 0 */ int pcmk__xml_test_setup_group(void **state) { - // Load schemas - pcmk__xml_init(); + pcmk__schema_init(); return 0; } /*! * \internal * \brief Perform teardown for a group of unit tests that manipulate XML * * This function is suitable for being passed as the second argument to the * \c PCMK__UNIT_TEST macro. * * \param[in] state Ignored * * \return 0 */ int pcmk__xml_test_teardown_group(void **state) { - // Clean up schemas and libxml2 global memory pcmk__schema_cleanup(); xmlCleanupParser(); return 0; } char * pcmk__cib_test_copy_cib(const char *in_file) { char *in_path = crm_strdup_printf("%s/%s", getenv("PCMK_CTS_CLI_DIR"), in_file); char *out_path = NULL; char *contents = NULL; int fd; /* Copy the CIB over to a temp location so we can modify it. */ out_path = crm_strdup_printf("%s/test-cib.XXXXXX", pcmk__get_tmpdir()); fd = mkstemp(out_path); if (fd < 0) { free(out_path); return NULL; } if (pcmk__file_contents(in_path, &contents) != pcmk_rc_ok) { free(out_path); close(fd); return NULL; } if (pcmk__write_sync(fd, contents) != pcmk_rc_ok) { free(out_path); free(in_path); free(contents); close(fd); return NULL; } setenv("CIB_file", out_path, 1); return out_path; } void pcmk__cib_test_cleanup(char *out_path) { unlink(out_path); free(out_path); unsetenv("CIB_file"); } /*! * \internal * \brief Initialize logging for unit testing purposes * * \param[in] name What to use as system name for logging * \param[in] filename If not NULL, enable debug logs to this file (intended * for debugging during development rather than committed * unit tests) */ void pcmk__test_init_logging(const char *name, const char *filename) { pcmk__cli_init_logging(name, 0); if (filename != NULL) { pcmk__add_logfile(filename); set_crm_log_level(LOG_DEBUG); } } // LCOV_EXCL_STOP diff --git a/lib/common/xml.c b/lib/common/xml.c index 939c87efbb..90f4dfc5d6 100644 --- a/lib/common/xml.c +++ b/lib/common/xml.c @@ -1,1884 +1,1864 @@ /* * Copyright 2004-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include // uint32_t #include #include #include #include // stat(), S_ISREG, etc. #include #include // gboolean, GString #include // xmlNode, etc. #include // xmlChar, xmlGetUTF8Char() #include #include #include // PCMK__XML_LOG_BASE, etc. #include "crmcommon_private.h" //! libxml2 supports only XML version 1.0, at least as of libxml2-2.12.5 #define XML_VERSION ((const xmlChar *) "1.0") /*! * \internal * \brief Get a string representation of an XML element type for logging * * \param[in] type XML element type * * \return String representation of \p type */ const char * pcmk__xml_element_type_text(xmlElementType type) { static const char *const element_type_names[] = { [XML_ELEMENT_NODE] = "element", [XML_ATTRIBUTE_NODE] = "attribute", [XML_TEXT_NODE] = "text", [XML_CDATA_SECTION_NODE] = "CDATA section", [XML_ENTITY_REF_NODE] = "entity reference", [XML_ENTITY_NODE] = "entity", [XML_PI_NODE] = "PI", [XML_COMMENT_NODE] = "comment", [XML_DOCUMENT_NODE] = "document", [XML_DOCUMENT_TYPE_NODE] = "document type", [XML_DOCUMENT_FRAG_NODE] = "document fragment", [XML_NOTATION_NODE] = "notation", [XML_HTML_DOCUMENT_NODE] = "HTML document", [XML_DTD_NODE] = "DTD", [XML_ELEMENT_DECL] = "element declaration", [XML_ATTRIBUTE_DECL] = "attribute declaration", [XML_ENTITY_DECL] = "entity declaration", [XML_NAMESPACE_DECL] = "namespace declaration", [XML_XINCLUDE_START] = "XInclude start", [XML_XINCLUDE_END] = "XInclude end", }; // Assumes the numeric values of the indices are in ascending order if ((type < XML_ELEMENT_NODE) || (type > XML_XINCLUDE_END)) { return "unrecognized type"; } return element_type_names[type]; } /*! * \internal * \brief Apply a function to each XML node in a tree (pre-order, depth-first) * * \param[in,out] xml XML tree to traverse * \param[in,out] fn Function to call for each node (returns \c true to * continue traversing the tree or \c false to stop) * \param[in,out] user_data Argument to \p fn * * \return \c false if any \p fn call returned \c false, or \c true otherwise * * \note This function is recursive. */ bool pcmk__xml_tree_foreach(xmlNode *xml, bool (*fn)(xmlNode *, void *), void *user_data) { if (xml == NULL) { return true; } if (!fn(xml, user_data)) { return false; } for (xml = pcmk__xml_first_child(xml); xml != NULL; xml = pcmk__xml_next(xml)) { if (!pcmk__xml_tree_foreach(xml, fn, user_data)) { return false; } } return true; } void pcmk__xml_set_parent_flags(xmlNode *xml, uint64_t flags) { for (; xml != NULL; xml = xml->parent) { xml_node_private_t *nodepriv = xml->_private; if (nodepriv != NULL) { pcmk__set_xml_flags(nodepriv, flags); } } } /*! * \internal * \brief Set flags for an XML document * * \param[in,out] doc XML document * \param[in] flags Group of enum pcmk__xml_flags */ void pcmk__xml_doc_set_flags(xmlDoc *doc, uint32_t flags) { if (doc != NULL) { xml_doc_private_t *docpriv = doc->_private; pcmk__set_xml_flags(docpriv, flags); } } /*! * \internal * \brief Check whether the given flags are set for an XML document * * \param[in] doc XML document to check * \param[in] flags Group of enum pcmk__xml_flags * * \return \c true if all of \p flags are set for \p doc, or \c false otherwise */ bool pcmk__xml_doc_all_flags_set(const xmlDoc *doc, uint32_t flags) { if (doc != NULL) { xml_doc_private_t *docpriv = doc->_private; return (docpriv != NULL) && pcmk_all_flags_set(docpriv->flags, flags); } return false; } // Mark document, element, and all element's parents as changed void pcmk__mark_xml_node_dirty(xmlNode *xml) { if (xml == NULL) { return; } pcmk__xml_doc_set_flags(xml->doc, pcmk__xf_dirty); pcmk__xml_set_parent_flags(xml, pcmk__xf_dirty); } /*! * \internal * \brief Clear flags on an XML node * * \param[in,out] xml XML node whose flags to reset * \param[in,out] user_data Ignored * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ bool pcmk__xml_reset_node_flags(xmlNode *xml, void *user_data) { xml_node_private_t *nodepriv = xml->_private; if (nodepriv != NULL) { nodepriv->flags = pcmk__xf_none; } return true; } /*! * \internal * \brief Set the \c pcmk__xf_dirty and \c pcmk__xf_created flags on an XML node * * \param[in,out] xml Node whose flags to set * \param[in] user_data Ignored * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool mark_xml_dirty_created(xmlNode *xml, void *user_data) { xml_node_private_t *nodepriv = xml->_private; if (nodepriv != NULL) { pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created); } return true; } /*! * \internal * \brief Mark an XML tree as dirty and created, and mark its parents dirty * * Also mark the document dirty. * * \param[in,out] xml Tree to mark as dirty and created */ static void mark_xml_tree_dirty_created(xmlNode *xml) { pcmk__assert(xml != NULL); if (!pcmk__xml_doc_all_flags_set(xml->doc, pcmk__xf_tracking)) { // Tracking is disabled for entire document return; } // Mark all parents and document dirty pcmk__mark_xml_node_dirty(xml); pcmk__xml_tree_foreach(xml, mark_xml_dirty_created, NULL); } // Free an XML object previously marked as deleted static void free_deleted_object(void *data) { if(data) { pcmk__deleted_xml_t *deleted_obj = data; g_free(deleted_obj->path); free(deleted_obj); } } // Free and NULL user, ACLs, and deleted objects in an XML node's private data static void reset_xml_private_data(xml_doc_private_t *docpriv) { if (docpriv != NULL) { pcmk__assert(docpriv->check == PCMK__XML_DOC_PRIVATE_MAGIC); pcmk__str_update(&(docpriv->acl_user), NULL); if (docpriv->acls != NULL) { pcmk__free_acls(docpriv->acls); docpriv->acls = NULL; } if(docpriv->deleted_objs) { g_list_free_full(docpriv->deleted_objs, free_deleted_object); docpriv->deleted_objs = NULL; } } } /*! * \internal * \brief Allocate and initialize private data for an XML node * * \param[in,out] node XML node whose private data to initialize * \param[in] user_data Ignored * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool new_private_data(xmlNode *node, void *user_data) { bool tracking = false; CRM_CHECK(node != NULL, return true); if (node->_private != NULL) { return true; } tracking = pcmk__xml_doc_all_flags_set(node->doc, pcmk__xf_tracking); switch (node->type) { case XML_DOCUMENT_NODE: { xml_doc_private_t *docpriv = pcmk__assert_alloc(1, sizeof(xml_doc_private_t)); docpriv->check = PCMK__XML_DOC_PRIVATE_MAGIC; node->_private = docpriv; } break; case XML_ELEMENT_NODE: case XML_ATTRIBUTE_NODE: case XML_COMMENT_NODE: { xml_node_private_t *nodepriv = pcmk__assert_alloc(1, sizeof(xml_node_private_t)); nodepriv->check = PCMK__XML_NODE_PRIVATE_MAGIC; node->_private = nodepriv; if (tracking) { pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created); } for (xmlAttr *iter = pcmk__xe_first_attr(node); iter != NULL; iter = iter->next) { new_private_data((xmlNode *) iter, user_data); } } break; case XML_TEXT_NODE: case XML_DTD_NODE: case XML_CDATA_SECTION_NODE: return true; default: CRM_LOG_ASSERT(node->type == XML_ELEMENT_NODE); return true; } if (tracking) { pcmk__mark_xml_node_dirty(node); } return true; } /*! * \internal * \brief Free private data for an XML node * * \param[in,out] node XML node whose private data to free * \param[in] user_data Ignored * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool free_private_data(xmlNode *node, void *user_data) { CRM_CHECK(node != NULL, return true); if (node->_private == NULL) { return true; } if (node->type == XML_DOCUMENT_NODE) { reset_xml_private_data((xml_doc_private_t *) node->_private); } else { xml_node_private_t *nodepriv = node->_private; pcmk__assert(nodepriv->check == PCMK__XML_NODE_PRIVATE_MAGIC); for (xmlAttr *iter = pcmk__xe_first_attr(node); iter != NULL; iter = iter->next) { free_private_data((xmlNode *) iter, user_data); } } free(node->_private); node->_private = NULL; return true; } /*! * \internal * \brief Allocate and initialize private data recursively for an XML tree * * \param[in,out] node XML node whose private data to initialize */ void pcmk__xml_new_private_data(xmlNode *xml) { pcmk__xml_tree_foreach(xml, new_private_data, NULL); } /*! * \internal * \brief Free private data recursively for an XML tree * * \param[in,out] node XML node whose private data to free */ void pcmk__xml_free_private_data(xmlNode *xml) { pcmk__xml_tree_foreach(xml, free_private_data, NULL); } /*! * \internal * \brief Return ordinal position of an XML node among its siblings * * \param[in] xml XML node to check * \param[in] ignore_if_set Don't count siblings with this flag set * * \return Ordinal position of \p xml (starting with 0) */ int pcmk__xml_position(const xmlNode *xml, enum pcmk__xml_flags ignore_if_set) { int position = 0; for (const xmlNode *cIter = xml; cIter->prev; cIter = cIter->prev) { xml_node_private_t *nodepriv = ((xmlNode*)cIter->prev)->_private; if (!pcmk_is_set(nodepriv->flags, ignore_if_set)) { position++; } } return position; } /*! * \internal * \brief Remove all attributes marked as deleted from an XML node * * \param[in,out] xml XML node whose deleted attributes to remove * \param[in,out] user_data Ignored * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool commit_attr_deletions(xmlNode *xml, void *user_data) { pcmk__xml_reset_node_flags(xml, NULL); pcmk__xe_remove_matching_attrs(xml, true, pcmk__marked_as_deleted, NULL); return true; } /*! * \internal * \brief Finalize all pending changes to an XML document and reset private data * * Clear the ACL user and all flags, unpacked ACLs, and deleted node records for * the document; clear all flags on each node in the tree; and delete any * attributes that are marked for deletion. * * \param[in,out] doc XML document * * \note When change tracking is enabled, "deleting" an attribute simply marks * it for deletion (using \c pcmk__xf_deleted) until changes are * committed. Freeing a node (using \c pcmk__xml_free()) adds a deleted * node record (\c pcmk__deleted_xml_t) to the node's document before * freeing it. * \note This function clears all flags, not just flags that indicate changes. * In particular, note that it clears the \c pcmk__xf_tracking flag, thus * disabling tracking. */ void pcmk__xml_commit_changes(xmlDoc *doc) { xml_doc_private_t *docpriv = NULL; if (doc == NULL) { return; } docpriv = doc->_private; if (docpriv == NULL) { return; } if (pcmk_is_set(docpriv->flags, pcmk__xf_dirty)) { pcmk__xml_tree_foreach(xmlDocGetRootElement(doc), commit_attr_deletions, NULL); } reset_xml_private_data(docpriv); docpriv->flags = pcmk__xf_none; } /*! * \internal * \brief Create a new XML document * * \return Newly allocated XML document (guaranteed not to be \c NULL) * * \note The caller is responsible for freeing the return value using * \c pcmk__xml_free_doc(). */ xmlDoc * pcmk__xml_new_doc(void) { xmlDoc *doc = xmlNewDoc(XML_VERSION); pcmk__mem_assert(doc); pcmk__xml_new_private_data((xmlNode *) doc); return doc; } /*! * \internal * \brief Free a new XML document * * \param[in,out] doc XML document to free */ void pcmk__xml_free_doc(xmlDoc *doc) { if (doc != NULL) { pcmk__xml_free_private_data((xmlNode *) doc); xmlFreeDoc(doc); } } /*! * \internal * \brief Check whether the first character of a string is an XML NameStartChar * * See https://www.w3.org/TR/xml/#NT-NameStartChar. * * This is almost identical to libxml2's \c xmlIsDocNameStartChar(), but they * don't expose it as part of the public API. * * \param[in] utf8 UTF-8 encoded string * \param[out] len If not \c NULL, where to store size in bytes of first * character in \p utf8 * * \return \c true if \p utf8 begins with a valid XML NameStartChar, or \c false * otherwise */ bool pcmk__xml_is_name_start_char(const char *utf8, int *len) { int c = 0; int local_len = 0; if (len == NULL) { len = &local_len; } /* xmlGetUTF8Char() abuses the len argument. At call time, it must be set to * "the minimum number of bytes present in the sequence... to assure the * next character is completely contained within the sequence." It's similar * to the "n" in the strn*() functions. However, this doesn't make any sense * for null-terminated strings, and there's no value that indicates "keep * going until '\0'." So we set it to 4, the max number of bytes in a UTF-8 * character. * * At return, it's set to the actual number of bytes in the char, or 0 on * error. */ *len = 4; // Note: xmlGetUTF8Char() assumes a 32-bit int c = xmlGetUTF8Char((const xmlChar *) utf8, len); if (c < 0) { GString *buf = g_string_sized_new(32); for (int i = 0; (i < 4) && (utf8[i] != '\0'); i++) { g_string_append_printf(buf, " 0x%.2X", utf8[i]); } crm_info("Invalid UTF-8 character (bytes:%s)", (pcmk__str_empty(buf->str)? " " : buf->str)); g_string_free(buf, TRUE); return false; } return (c == '_') || (c == ':') || ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')) || ((c >= 0xC0) && (c <= 0xD6)) || ((c >= 0xD8) && (c <= 0xF6)) || ((c >= 0xF8) && (c <= 0x2FF)) || ((c >= 0x370) && (c <= 0x37D)) || ((c >= 0x37F) && (c <= 0x1FFF)) || ((c >= 0x200C) && (c <= 0x200D)) || ((c >= 0x2070) && (c <= 0x218F)) || ((c >= 0x2C00) && (c <= 0x2FEF)) || ((c >= 0x3001) && (c <= 0xD7FF)) || ((c >= 0xF900) && (c <= 0xFDCF)) || ((c >= 0xFDF0) && (c <= 0xFFFD)) || ((c >= 0x10000) && (c <= 0xEFFFF)); } /*! * \internal * \brief Check whether the first character of a string is an XML NameChar * * See https://www.w3.org/TR/xml/#NT-NameChar. * * This is almost identical to libxml2's \c xmlIsDocNameChar(), but they don't * expose it as part of the public API. * * \param[in] utf8 UTF-8 encoded string * \param[out] len If not \c NULL, where to store size in bytes of first * character in \p utf8 * * \return \c true if \p utf8 begins with a valid XML NameChar, or \c false * otherwise */ bool pcmk__xml_is_name_char(const char *utf8, int *len) { int c = 0; int local_len = 0; if (len == NULL) { len = &local_len; } // See comment regarding len in pcmk__xml_is_name_start_char() *len = 4; // Note: xmlGetUTF8Char() assumes a 32-bit int c = xmlGetUTF8Char((const xmlChar *) utf8, len); if (c < 0) { GString *buf = g_string_sized_new(32); for (int i = 0; (i < 4) && (utf8[i] != '\0'); i++) { g_string_append_printf(buf, " 0x%.2X", utf8[i]); } crm_info("Invalid UTF-8 character (bytes:%s)", (pcmk__str_empty(buf->str)? " " : buf->str)); g_string_free(buf, TRUE); return false; } return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')) || ((c >= '0') && (c <= '9')) || (c == '_') || (c == ':') || (c == '-') || (c == '.') || (c == 0xB7) || ((c >= 0xC0) && (c <= 0xD6)) || ((c >= 0xD8) && (c <= 0xF6)) || ((c >= 0xF8) && (c <= 0x2FF)) || ((c >= 0x300) && (c <= 0x36F)) || ((c >= 0x370) && (c <= 0x37D)) || ((c >= 0x37F) && (c <= 0x1FFF)) || ((c >= 0x200C) && (c <= 0x200D)) || ((c >= 0x203F) && (c <= 0x2040)) || ((c >= 0x2070) && (c <= 0x218F)) || ((c >= 0x2C00) && (c <= 0x2FEF)) || ((c >= 0x3001) && (c <= 0xD7FF)) || ((c >= 0xF900) && (c <= 0xFDCF)) || ((c >= 0xFDF0) && (c <= 0xFFFD)) || ((c >= 0x10000) && (c <= 0xEFFFF)); } /*! * \internal * \brief Sanitize a string so it is usable as an XML ID * * An ID must match the Name production as defined here: * https://www.w3.org/TR/xml/#NT-Name. * * Convert an invalid start character to \c '_'. Convert an invalid character * after the start character to \c '.'. * * \param[in,out] id String to sanitize */ void pcmk__xml_sanitize_id(char *id) { bool valid = true; int len = 0; // If id is empty or NULL, there's no way to make it a valid XML ID pcmk__assert(!pcmk__str_empty(id)); /* @TODO Suppose there are two strings and each has an invalid ID character * in the same position. The strings are otherwise identical. Both strings * will be sanitized to the same valid ID, which is incorrect. * * The caller is responsible for ensuring the sanitized ID does not already * exist in a given XML document before using it, if uniqueness is desired. */ valid = pcmk__xml_is_name_start_char(id, &len); CRM_CHECK(len > 0, return); // UTF-8 encoding error if (!valid) { *id = '_'; for (int i = 1; i < len; i++) { id[i] = '.'; } } for (id += len; *id != '\0'; id += len) { valid = pcmk__xml_is_name_char(id, &len); CRM_CHECK(len > 0, return); // UTF-8 encoding error if (!valid) { for (int i = 0; i < len; i++) { id[i] = '.'; } } } } /*! * \internal * \brief Free an XML tree without ACL checks or change tracking * * \param[in,out] xml XML node to free */ void pcmk__xml_free_node(xmlNode *xml) { pcmk__xml_free_private_data(xml); xmlUnlinkNode(xml); xmlFreeNode(xml); } /*! * \internal * \brief Free an XML tree if ACLs allow; track deletion if tracking is enabled * * If \p node is the root of its document, free the entire document. * * \param[in,out] node XML node to free * \param[in] position Position of \p node among its siblings for change * tracking (negative to calculate automatically if * needed) * * \return Standard Pacemaker return code */ static int free_xml_with_position(xmlNode *node, int position) { xmlDoc *doc = NULL; xml_node_private_t *nodepriv = NULL; if (node == NULL) { return pcmk_rc_ok; } doc = node->doc; nodepriv = node->_private; if ((doc != NULL) && (xmlDocGetRootElement(doc) == node)) { /* @TODO Should we check ACLs first? Otherwise it seems like we could * free the root element without write permission. */ pcmk__xml_free_doc(doc); return pcmk_rc_ok; } if (!pcmk__check_acl(node, NULL, pcmk__xf_acl_write)) { pcmk__if_tracing( { GString *xpath = pcmk__element_xpath(node); qb_log_from_external_source(__func__, __FILE__, "Cannot remove %s %x", LOG_TRACE, __LINE__, 0, xpath->str, nodepriv->flags); g_string_free(xpath, TRUE); }, {} ); return EACCES; } if (pcmk__xml_doc_all_flags_set(node->doc, pcmk__xf_tracking) && !pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { xml_doc_private_t *docpriv = doc->_private; GString *xpath = pcmk__element_xpath(node); if (xpath != NULL) { pcmk__deleted_xml_t *deleted_obj = NULL; crm_trace("Deleting %s %p from %p", xpath->str, node, doc); deleted_obj = pcmk__assert_alloc(1, sizeof(pcmk__deleted_xml_t)); deleted_obj->path = g_string_free(xpath, FALSE); deleted_obj->position = -1; // Record the position only for XML comments for now if (node->type == XML_COMMENT_NODE) { if (position >= 0) { deleted_obj->position = position; } else { deleted_obj->position = pcmk__xml_position(node, pcmk__xf_skip); } } docpriv->deleted_objs = g_list_append(docpriv->deleted_objs, deleted_obj); pcmk__xml_doc_set_flags(node->doc, pcmk__xf_dirty); } } pcmk__xml_free_node(node); return pcmk_rc_ok; } /*! * \internal * \brief Free an XML tree if ACLs allow; track deletion if tracking is enabled * * If \p xml is the root of its document, free the entire document. * * \param[in,out] xml XML node to free */ void pcmk__xml_free(xmlNode *xml) { free_xml_with_position(xml, -1); } /*! * \internal * \brief Make a deep copy of an XML node under a given parent * * \param[in,out] parent XML element that will be the copy's parent (\c NULL * to create a new XML document with the copy as root) * \param[in] src XML node to copy * * \return Deep copy of \p src, or \c NULL if \p src is \c NULL */ xmlNode * pcmk__xml_copy(xmlNode *parent, xmlNode *src) { xmlNode *copy = NULL; if (src == NULL) { return NULL; } if (parent == NULL) { xmlDoc *doc = NULL; // The copy will be the root element of a new document pcmk__assert(src->type == XML_ELEMENT_NODE); doc = pcmk__xml_new_doc(); copy = xmlDocCopyNode(src, doc, 1); pcmk__mem_assert(copy); xmlDocSetRootElement(doc, copy); } else { copy = xmlDocCopyNode(src, parent->doc, 1); pcmk__mem_assert(copy); xmlAddChild(parent, copy); } pcmk__xml_new_private_data(copy); return copy; } /*! * \internal * \brief Remove XML text nodes from specified XML and all its children * * \param[in,out] xml XML to strip text from */ void pcmk__strip_xml_text(xmlNode *xml) { xmlNode *iter = xml->children; while (iter) { xmlNode *next = iter->next; switch (iter->type) { case XML_TEXT_NODE: pcmk__xml_free_node(iter); break; case XML_ELEMENT_NODE: /* Search it */ pcmk__strip_xml_text(iter); break; default: /* Leave it */ break; } iter = next; } } /*! * \internal * \brief Check whether a string has XML special characters that must be escaped * * See \c pcmk__xml_escape() and \c pcmk__xml_escape_type for more details. * * \param[in] text String to check * \param[in] type Type of escaping * * \return \c true if \p text has special characters that need to be escaped, or * \c false otherwise */ bool pcmk__xml_needs_escape(const char *text, enum pcmk__xml_escape_type type) { if (text == NULL) { return false; } while (*text != '\0') { switch (type) { case pcmk__xml_escape_text: switch (*text) { case '<': case '>': case '&': return true; case '\n': case '\t': break; default: if (g_ascii_iscntrl(*text)) { return true; } break; } break; case pcmk__xml_escape_attr: switch (*text) { case '<': case '>': case '&': case '"': return true; default: if (g_ascii_iscntrl(*text)) { return true; } break; } break; case pcmk__xml_escape_attr_pretty: switch (*text) { case '\n': case '\r': case '\t': case '"': return true; default: break; } break; default: // Invalid enum value pcmk__assert(false); break; } text = g_utf8_next_char(text); } return false; } /*! * \internal * \brief Replace special characters with their XML escape sequences * * \param[in] text Text to escape * \param[in] type Type of escaping * * \return Newly allocated string equivalent to \p text but with special * characters replaced with XML escape sequences (or \c NULL if \p text * is \c NULL). If \p text is not \c NULL, the return value is * guaranteed not to be \c NULL. * * \note There are libxml functions that purport to do this: * \c xmlEncodeEntitiesReentrant() and \c xmlEncodeSpecialChars(). * However, their escaping is incomplete. See: * https://discourse.gnome.org/t/intended-use-of-xmlencodeentitiesreentrant-vs-xmlencodespecialchars/19252 * \note The caller is responsible for freeing the return value using * \c g_free(). */ gchar * pcmk__xml_escape(const char *text, enum pcmk__xml_escape_type type) { GString *copy = NULL; if (text == NULL) { return NULL; } copy = g_string_sized_new(strlen(text)); while (*text != '\0') { // Don't escape any non-ASCII characters if ((*text & 0x80) != 0) { size_t bytes = g_utf8_next_char(text) - text; g_string_append_len(copy, text, bytes); text += bytes; continue; } switch (type) { case pcmk__xml_escape_text: switch (*text) { case '<': g_string_append(copy, PCMK__XML_ENTITY_LT); break; case '>': g_string_append(copy, PCMK__XML_ENTITY_GT); break; case '&': g_string_append(copy, PCMK__XML_ENTITY_AMP); break; case '\n': case '\t': g_string_append_c(copy, *text); break; default: if (g_ascii_iscntrl(*text)) { g_string_append_printf(copy, "&#x%.2X;", *text); } else { g_string_append_c(copy, *text); } break; } break; case pcmk__xml_escape_attr: switch (*text) { case '<': g_string_append(copy, PCMK__XML_ENTITY_LT); break; case '>': g_string_append(copy, PCMK__XML_ENTITY_GT); break; case '&': g_string_append(copy, PCMK__XML_ENTITY_AMP); break; case '"': g_string_append(copy, PCMK__XML_ENTITY_QUOT); break; default: if (g_ascii_iscntrl(*text)) { g_string_append_printf(copy, "&#x%.2X;", *text); } else { g_string_append_c(copy, *text); } break; } break; case pcmk__xml_escape_attr_pretty: switch (*text) { case '"': g_string_append(copy, "\\\""); break; case '\n': g_string_append(copy, "\\n"); break; case '\r': g_string_append(copy, "\\r"); break; case '\t': g_string_append(copy, "\\t"); break; default: g_string_append_c(copy, *text); break; } break; default: // Invalid enum value pcmk__assert(false); break; } text = g_utf8_next_char(text); } return g_string_free(copy, FALSE); } /*! * \internal * \brief Add an XML attribute to a node, marked as deleted * * When calculating XML changes, we need to know when an attribute has been * deleted. Add the attribute back to the new XML, so that we can check the * removal against ACLs, and mark it as deleted for later removal after * differences have been calculated. * * \param[in,out] new_xml XML to modify * \param[in] element Name of XML element that changed (for logging) * \param[in] attr_name Name of attribute that was deleted * \param[in] old_value Value of attribute that was deleted */ static void mark_attr_deleted(xmlNode *new_xml, const char *element, const char *attr_name, const char *old_value) { xml_doc_private_t *docpriv = new_xml->doc->_private; xmlAttr *attr = NULL; xml_node_private_t *nodepriv; /* Restore the old value (without setting dirty flag recursively upwards or * checking ACLs) */ pcmk__clear_xml_flags(docpriv, pcmk__xf_tracking); crm_xml_add(new_xml, attr_name, old_value); pcmk__set_xml_flags(docpriv, pcmk__xf_tracking); // Reset flags (so the attribute doesn't appear as newly created) attr = xmlHasProp(new_xml, (const xmlChar *) attr_name); nodepriv = attr->_private; nodepriv->flags = 0; // Check ACLs and mark restored value for later removal pcmk__xa_remove(attr, false); crm_trace("XML attribute %s=%s was removed from %s", attr_name, old_value, element); } /* * \internal * \brief Check ACLs for a changed XML attribute */ static void mark_attr_changed(xmlNode *new_xml, const char *element, const char *attr_name, const char *old_value) { xml_doc_private_t *docpriv = new_xml->doc->_private; char *vcopy = crm_element_value_copy(new_xml, attr_name); crm_trace("XML attribute %s was changed from '%s' to '%s' in %s", attr_name, old_value, vcopy, element); // Restore the original value (without checking ACLs) pcmk__clear_xml_flags(docpriv, pcmk__xf_tracking); crm_xml_add(new_xml, attr_name, old_value); pcmk__set_xml_flags(docpriv, pcmk__xf_tracking); // Change it back to the new value, to check ACLs crm_xml_add(new_xml, attr_name, vcopy); free(vcopy); } /*! * \internal * \brief Mark an XML attribute as having changed position * * \param[in,out] new_xml XML to modify * \param[in] element Name of XML element that changed (for logging) * \param[in,out] old_attr Attribute that moved, in original XML * \param[in,out] new_attr Attribute that moved, in \p new_xml * \param[in] p_old Ordinal position of \p old_attr in original XML * \param[in] p_new Ordinal position of \p new_attr in \p new_xml */ static void mark_attr_moved(xmlNode *new_xml, const char *element, xmlAttr *old_attr, xmlAttr *new_attr, int p_old, int p_new) { xml_node_private_t *nodepriv = new_attr->_private; crm_trace("XML attribute %s moved from position %d to %d in %s", old_attr->name, p_old, p_new, element); // Mark document, element, and all element's parents as changed pcmk__mark_xml_node_dirty(new_xml); // Mark attribute as changed pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_moved); nodepriv = (p_old > p_new)? old_attr->_private : new_attr->_private; pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); } /*! * \internal * \brief Calculate differences in all previously existing XML attributes * * \param[in,out] old_xml Original XML to compare * \param[in,out] new_xml New XML to compare */ static void xml_diff_old_attrs(xmlNode *old_xml, xmlNode *new_xml) { xmlAttr *attr_iter = pcmk__xe_first_attr(old_xml); while (attr_iter != NULL) { const char *name = (const char *) attr_iter->name; xmlAttr *old_attr = attr_iter; xmlAttr *new_attr = xmlHasProp(new_xml, attr_iter->name); const char *old_value = pcmk__xml_attr_value(attr_iter); attr_iter = attr_iter->next; if (new_attr == NULL) { mark_attr_deleted(new_xml, (const char *) old_xml->name, name, old_value); } else { xml_node_private_t *nodepriv = new_attr->_private; int new_pos = pcmk__xml_position((xmlNode*) new_attr, pcmk__xf_skip); int old_pos = pcmk__xml_position((xmlNode*) old_attr, pcmk__xf_skip); const char *new_value = crm_element_value(new_xml, name); // This attribute isn't new pcmk__clear_xml_flags(nodepriv, pcmk__xf_created); if (strcmp(new_value, old_value) != 0) { mark_attr_changed(new_xml, (const char *) old_xml->name, name, old_value); } else if ((old_pos != new_pos) && !pcmk__xml_doc_all_flags_set(new_xml->doc, pcmk__xf_ignore_attr_pos |pcmk__xf_tracking)) { /* pcmk__xf_tracking is always set by pcmk__xml_mark_changes() * before this function is called, so only the * pcmk__xf_ignore_attr_pos check is truly relevant. */ mark_attr_moved(new_xml, (const char *) old_xml->name, old_attr, new_attr, old_pos, new_pos); } } } } /*! * \internal * \brief Check all attributes in new XML for creation * * For each of a given XML element's attributes marked as newly created, accept * (and mark as dirty) or reject the creation according to ACLs. * * \param[in,out] new_xml XML to check */ static void mark_created_attrs(xmlNode *new_xml) { xmlAttr *attr_iter = pcmk__xe_first_attr(new_xml); while (attr_iter != NULL) { xmlAttr *new_attr = attr_iter; xml_node_private_t *nodepriv = attr_iter->_private; attr_iter = attr_iter->next; if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { const char *attr_name = (const char *) new_attr->name; crm_trace("Created new attribute %s=%s in %s", attr_name, pcmk__xml_attr_value(new_attr), new_xml->name); /* Check ACLs (we can't use the remove-then-create trick because it * would modify the attribute position). */ if (pcmk__check_acl(new_xml, attr_name, pcmk__xf_acl_write)) { pcmk__mark_xml_attr_dirty(new_attr); } else { // Creation was not allowed, so remove the attribute pcmk__xa_remove(new_attr, true); } } } } /*! * \internal * \brief Calculate differences in attributes between two XML nodes * * \param[in,out] old_xml Original XML to compare * \param[in,out] new_xml New XML to compare */ static void xml_diff_attrs(xmlNode *old_xml, xmlNode *new_xml) { // Cleared later if attributes are not really new for (xmlAttr *attr = pcmk__xe_first_attr(new_xml); attr != NULL; attr = attr->next) { xml_node_private_t *nodepriv = attr->_private; pcmk__set_xml_flags(nodepriv, pcmk__xf_created); } xml_diff_old_attrs(old_xml, new_xml); mark_created_attrs(new_xml); } /*! * \internal * \brief Add a deleted object record for an old XML child if ACLs allow * * This is intended to be called for a child of an old XML element that is not * present as a child of a new XML element. * * Add a temporary copy of the old child to the new XML. Then check whether ACLs * would have allowed the deletion of that element. If so, add a deleted object * record for it to the new XML's document, and set the \c pcmk__xf_skip flag on * the old child. * * The temporary copy is removed before returning. The new XML and all of its * ancestors will have the \c pcmk__xf_dirty flag set because of the creation, * however. * * \param[in,out] old_child Child of old XML * \param[in,out] new_parent New XML that does not contain \p old_child */ static void mark_child_deleted(xmlNode *old_child, xmlNode *new_parent) { int pos = pcmk__xml_position(old_child, pcmk__xf_skip); // Re-create the child element so we can check ACLs xmlNode *candidate = pcmk__xml_copy(new_parent, old_child); // Clear flags on new child and its children pcmk__xml_tree_foreach(candidate, pcmk__xml_reset_node_flags, NULL); // free_xml_with_position() will check whether ACLs allow the deletion pcmk__apply_acl(xmlDocGetRootElement(candidate->doc)); /* Try to remove the child again (which will track it in document's * deleted_objs on success) */ if (free_xml_with_position(candidate, pos) != pcmk_rc_ok) { // ACLs denied deletion in free_xml_with_position. Free candidate here. pcmk__xml_free_node(candidate); } pcmk__set_xml_flags((xml_node_private_t *) old_child->_private, pcmk__xf_skip); } /*! * \internal * \brief Mark a new child as moved and set \c pcmk__xf_skip as appropriate * * \param[in,out] old_child Child of old XML * \param[in,out] new_child Child of new XML that matches \p old_child * \param[in] old_pos Position of \p old_child among its siblings * \param[in] new_pos Position of \p new_child among its siblings */ static void mark_child_moved(xmlNode *old_child, xmlNode *new_child, int old_pos, int new_pos) { const char *id_s = pcmk__s(pcmk__xe_id(new_child), ""); xmlNode *new_parent = new_child->parent; xml_node_private_t *nodepriv = new_child->_private; crm_trace("Child element %s with " PCMK_XA_ID "='%s' moved from position " "%d to %d under %s", new_child->name, id_s, old_pos, new_pos, new_parent->name); pcmk__mark_xml_node_dirty(new_parent); pcmk__set_xml_flags(nodepriv, pcmk__xf_moved); /* @TODO Figure out and document why we skip the old child in future * position calculations if the old position is higher, and skip the new * child in future position calculations if the new position is higher. This * goes back to d028b52, and there's no explanation in the commit message. */ if (old_pos > new_pos) { nodepriv = old_child->_private; } pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); } /*! * \internal * \brief Check whether a new XML child comment matches an old XML child comment * * Two comments match if they have the same position among their siblings and * the same contents. * * If \p new_comment has the \c pcmk__xf_skip flag set, then it is automatically * considered not to match. * * \param[in] old_comment Old XML child element * \param[in] new_comment New XML child element * * \retval \c true if \p new_comment matches \p old_comment * \retval \c false otherwise */ static bool new_comment_matches(const xmlNode *old_comment, const xmlNode *new_comment) { xml_node_private_t *nodepriv = new_comment->_private; if (pcmk_is_set(nodepriv->flags, pcmk__xf_skip)) { /* @TODO Should we also return false if old_comment has pcmk__xf_skip * set? This preserves existing behavior at time of writing. */ return false; } if (pcmk__xml_position(old_comment, pcmk__xf_skip) != pcmk__xml_position(new_comment, pcmk__xf_skip)) { return false; } return pcmk__xc_matches(old_comment, new_comment); } /*! * \internal * \brief Check whether a new XML child element matches an old XML child element * * Two elements match if they have the same name and, if \p match_ids is * \c true, the same ID. (Both IDs can be \c NULL in this case.) * * \param[in] old_element Old XML child element * \param[in] new_element New XML child element * \param[in] match_ids If \c true, require IDs to match (or both to be * \c NULL) * * \retval \c true if \p new_element matches \p old_element * \retval \c false otherwise */ static bool new_element_matches(const xmlNode *old_element, const xmlNode *new_element, bool match_ids) { if (!pcmk__xe_is(new_element, (const char *) old_element->name)) { return false; } return !match_ids || pcmk__str_eq(pcmk__xe_id(old_element), pcmk__xe_id(new_element), pcmk__str_none); } /*! * \internal * \brief Check whether a new XML child node matches an old XML child node * * Node types must be the same in order to match. * * For comments, a match is a comment at the same position with the same * content. * * For elements, a match is an element with the same name and, if required, the * same ID. (Both IDs can be \c NULL in this case.) * * For other node types, there is no match. * * \param[in] old_child Child of old XML * \param[in] new_child Child of new XML * \param[in] match_ids If \c true, require element IDs to match (or both to be * \c NULL) * * \retval \c true if \p new_child matches \p old_child * \retval \c false otherwise */ static bool new_child_matches(const xmlNode *old_child, const xmlNode *new_child, bool match_ids) { if (old_child->type != new_child->type) { return false; } switch (old_child->type) { case XML_COMMENT_NODE: return new_comment_matches(old_child, new_child); case XML_ELEMENT_NODE: return new_element_matches(old_child, new_child, match_ids); default: return false; } } /*! * \internal * \brief Find matching XML node pairs between old and new XML's children * * A node that is part of a matching pair has its _private:match member * set to the matching node. * * \param[in,out] old_xml Old XML * \param[in,out] new_xml New XML * \param[in] comments_ids If \c true, match comments and require element * IDs to match; otherwise, skip comments and match * elements by name only */ static void find_matching_children(xmlNode *old_xml, xmlNode *new_xml, bool comments_ids) { for (xmlNode *old_child = pcmk__xml_first_child(old_xml); old_child != NULL; old_child = pcmk__xml_next(old_child)) { xml_node_private_t *old_nodepriv = old_child->_private; if ((old_nodepriv == NULL) || (old_nodepriv->match != NULL)) { // Can't process, or we already found a match for this old child continue; } if (!comments_ids && (old_child->type != XML_ELEMENT_NODE)) { /* We only match comments and elements, and we're not matching * comments during this call */ continue; } for (xmlNode *new_child = pcmk__xml_first_child(new_xml); new_child != NULL; new_child = pcmk__xml_next(new_child)) { xml_node_private_t *new_nodepriv = new_child->_private; if ((new_nodepriv == NULL) || (new_nodepriv->match != NULL)) { /* Can't process, or this new child already matched some old * child */ continue; } if (new_child_matches(old_child, new_child, comments_ids)) { old_nodepriv->match = new_child; new_nodepriv->match = old_child; break; } } } } /*! * \internal * \brief Mark changes between two XML trees * * Set flags in a new XML tree to indicate changes relative to an old XML tree. * * \param[in,out] old_xml XML before changes * \param[in,out] new_xml XML after changes * * \note This may set \c pcmk__xf_skip on parts of \p old_xml. */ void pcmk__xml_mark_changes(xmlNode *old_xml, xmlNode *new_xml) { /* This function may set the xml_node_private_t:match member on children of * old_xml and new_xml, but it clears that member before returning. * * @TODO Ensure we handle (for example, by copying) or reject user-created * XML that is missing xml_node_private_t at top level or in any children. * Similarly, check handling of node types for which we don't create private * data. For now, we'll skip them in the loops below. */ CRM_CHECK((old_xml != NULL) && (new_xml != NULL), return); if ((old_xml->_private == NULL) || (new_xml->_private == NULL)) { return; } pcmk__xml_doc_set_flags(new_xml->doc, pcmk__xf_tracking); xml_diff_attrs(old_xml, new_xml); find_matching_children(old_xml, new_xml, true); find_matching_children(old_xml, new_xml, false); // Process matches (changed children) and deletions for (xmlNode *old_child = pcmk__xml_first_child(old_xml); old_child != NULL; old_child = pcmk__xml_next(old_child)) { xml_node_private_t *nodepriv = old_child->_private; xmlNode *new_child = NULL; if (nodepriv == NULL) { continue; } if (nodepriv->match == NULL) { // No match in new XML means the old child was deleted mark_child_deleted(old_child, new_xml); continue; } /* Fetch the match and clear old_child->_private's match member. * new_child->_private's match member is handled in the new_xml loop. */ new_child = nodepriv->match; nodepriv->match = NULL; pcmk__assert(old_child->type == new_child->type); if (old_child->type == XML_COMMENT_NODE) { // Comments match only if their positions and contents match continue; } pcmk__xml_mark_changes(old_child, new_child); } /* Mark unmatched new children as created, and mark matched new children as * moved if their positions changed. Grab the next new child in advance, * since new_child may get freed in the loop body. */ for (xmlNode *new_child = pcmk__xml_first_child(new_xml), *next = pcmk__xml_next(new_child); new_child != NULL; new_child = next, next = pcmk__xml_next(new_child)) { xml_node_private_t *nodepriv = new_child->_private; if (nodepriv == NULL) { continue; } if (nodepriv->match != NULL) { /* Fetch the match and clear new_child->_private's match member. Any * changes were marked in the old_xml loop. Mark the move. * * We might be able to mark the move earlier, when we mark changes * for matches in the old_xml loop, consolidating both actions. We'd * have to think about whether the timing of setting the * pcmk__xf_skip flag makes any difference. */ xmlNode *old_child = nodepriv->match; int old_pos = pcmk__xml_position(old_child, pcmk__xf_skip); int new_pos = pcmk__xml_position(new_child, pcmk__xf_skip); if (old_pos != new_pos) { mark_child_moved(old_child, new_child, old_pos, new_pos); } nodepriv->match = NULL; continue; } // No match in old XML means the new child is newly created pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); mark_xml_tree_dirty_created(new_child); // Check whether creation was allowed (may free new_child) pcmk__apply_creation_acl(new_child, true); } } -/*! - * \internal - * \brief Initialize the Pacemaker XML environment - * - * Currently this only loads schemas into the cache. It used to do more. - */ -void -pcmk__xml_init(void) -{ - // @TODO Try to find a better caller than crm_log_preinit() - static bool initialized = false; - - if (!initialized) { - initialized = true; - - // Load schemas into the cache - pcmk__schema_init(); - } -} - char * pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns) { static const char *base = NULL; char *ret = NULL; if (base == NULL) { base = pcmk__env_option(PCMK__ENV_SCHEMA_DIRECTORY); } if (pcmk__str_empty(base)) { base = PCMK_SCHEMA_DIR; } switch (ns) { case pcmk__xml_artefact_ns_legacy_rng: case pcmk__xml_artefact_ns_legacy_xslt: ret = strdup(base); break; case pcmk__xml_artefact_ns_base_rng: case pcmk__xml_artefact_ns_base_xslt: ret = crm_strdup_printf("%s/base", base); break; default: crm_err("XML artefact family specified as %u not recognized", ns); } return ret; } static char * find_artefact(enum pcmk__xml_artefact_ns ns, const char *path, const char *filespec) { char *ret = NULL; switch (ns) { case pcmk__xml_artefact_ns_legacy_rng: case pcmk__xml_artefact_ns_base_rng: if (pcmk__ends_with(filespec, ".rng")) { ret = crm_strdup_printf("%s/%s", path, filespec); } else { ret = crm_strdup_printf("%s/%s.rng", path, filespec); } break; case pcmk__xml_artefact_ns_legacy_xslt: case pcmk__xml_artefact_ns_base_xslt: if (pcmk__ends_with(filespec, ".xsl")) { ret = crm_strdup_printf("%s/%s", path, filespec); } else { ret = crm_strdup_printf("%s/%s.xsl", path, filespec); } break; default: crm_err("XML artefact family specified as %u not recognized", ns); } return ret; } char * pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec) { struct stat sb; char *base = pcmk__xml_artefact_root(ns); char *ret = NULL; ret = find_artefact(ns, base, filespec); free(base); if (stat(ret, &sb) != 0 || !S_ISREG(sb.st_mode)) { const char *remote_schema_dir = pcmk__remote_schema_dir(); free(ret); ret = find_artefact(ns, remote_schema_dir, filespec); } return ret; } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include xmlNode * copy_xml(xmlNode *src) { xmlDoc *doc = pcmk__xml_new_doc(); xmlNode *copy = NULL; copy = xmlDocCopyNode(src, doc, 1); pcmk__mem_assert(copy); xmlDocSetRootElement(doc, copy); pcmk__xml_new_private_data(copy); return copy; } void crm_xml_init(void) { - pcmk__xml_init(); + pcmk__schema_init(); } void crm_xml_cleanup(void) { pcmk__schema_cleanup(); xmlCleanupParser(); } void pcmk_free_xml_subtree(xmlNode *xml) { pcmk__xml_free_node(xml); } void free_xml(xmlNode *child) { pcmk__xml_free(child); } void crm_xml_sanitize_id(char *id) { char *c; for (c = id; *c; ++c) { switch (*c) { case ':': case '#': *c = '.'; } } } bool xml_tracking_changes(xmlNode *xml) { return (xml != NULL) && pcmk__xml_doc_all_flags_set(xml->doc, pcmk__xf_tracking); } bool xml_document_dirty(xmlNode *xml) { return (xml != NULL) && pcmk__xml_doc_all_flags_set(xml->doc, pcmk__xf_dirty); } void xml_accept_changes(xmlNode *xml) { if (xml != NULL) { pcmk__xml_commit_changes(xml->doc); } } void xml_track_changes(xmlNode *xml, const char *user, xmlNode *acl_source, bool enforce_acls) { if (xml == NULL) { return; } pcmk__xml_commit_changes(xml->doc); crm_trace("Tracking changes%s to %p", (enforce_acls? " with ACLs" : ""), xml); pcmk__xml_doc_set_flags(xml->doc, pcmk__xf_tracking); if (enforce_acls) { if (acl_source == NULL) { acl_source = xml; } pcmk__xml_doc_set_flags(xml->doc, pcmk__xf_acl_enabled); pcmk__unpack_acl(acl_source, xml, user); pcmk__apply_acl(xml); } } void xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml) { CRM_CHECK((old_xml != NULL) && (new_xml != NULL) && pcmk__xe_is(old_xml, (const char *) new_xml->name) && pcmk__str_eq(pcmk__xe_id(old_xml), pcmk__xe_id(new_xml), pcmk__str_none), return); if (!pcmk__xml_doc_all_flags_set(new_xml->doc, pcmk__xf_tracking)) { // Ensure tracking has a clean start (pcmk__xml_mark_changes() enables) pcmk__xml_commit_changes(new_xml->doc); } pcmk__xml_mark_changes(old_xml, new_xml); } void xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml) { CRM_CHECK((old_xml != NULL) && (new_xml != NULL) && pcmk__xe_is(old_xml, (const char *) new_xml->name) && pcmk__str_eq(pcmk__xe_id(old_xml), pcmk__xe_id(new_xml), pcmk__str_none), return); /* BUG: If pcmk__xf_tracking is not set for new_xml when this function is * called, then we unset pcmk__xf_ignore_attr_pos via * pcmk__xml_commit_changes(). Since this function is about to be * deprecated, it's not worth fixing this and changing the user-facing * behavior. */ pcmk__xml_doc_set_flags(new_xml->doc, pcmk__xf_ignore_attr_pos); if (!pcmk__xml_doc_all_flags_set(new_xml->doc, pcmk__xf_tracking)) { // Ensure tracking has a clean start (pcmk__xml_mark_changes() enables) pcmk__xml_commit_changes(new_xml->doc); } pcmk__xml_mark_changes(old_xml, new_xml); } // LCOV_EXCL_STOP // End deprecated API