Page Menu
Home
ClusterLabs Projects
Search
Configure Global Search
Log In
Files
F4623881
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
205 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/include/crm/common/xml_internal.h b/include/crm/common/xml_internal.h
index b17bb1e89b..f27206315e 100644
--- a/include/crm/common/xml_internal.h
+++ b/include/crm/common/xml_internal.h
@@ -1,594 +1,596 @@
/*
* Copyright 2017-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#ifndef PCMK__XML_INTERNAL__H
# define PCMK__XML_INTERNAL__H
/*
* Internal-only wrappers for and extensions to libxml2 (libxslt)
*/
# include <stdlib.h>
# include <stdint.h> // uint32_t
# include <stdio.h>
# include <string.h>
# include <crm/crm.h> /* transitively imports qblog.h */
# include <crm/common/output_internal.h>
# include <crm/common/xml_io_internal.h>
# include <crm/common/xml_names_internal.h> // PCMK__XE_PROMOTABLE_LEGACY
# include <libxml/relaxng.h>
/*!
* \brief Base for directing lib{xml2,xslt} log into standard libqb backend
*
* This macro implements the core of what can be needed for directing
* libxml2 or libxslt error messaging into standard, preconfigured
* libqb-backed log stream.
*
* It's a bit unfortunate that libxml2 (and more sparsely, also libxslt)
* emits a single message by chunks (location is emitted separatedly from
* the message itself), so we have to take the effort to combine these
* chunks back to single message. Whether to do this or not is driven
* with \p dechunk toggle.
*
* The form of a macro was chosen for implicit deriving of __FILE__, etc.
* and also because static dechunking buffer should be differentiated per
* library (here we assume different functions referring to this macro
* will not ever be using both at once), preferably also per-library
* context of use to avoid clashes altogether.
*
* Note that we cannot use qb_logt, because callsite data have to be known
* at the moment of compilation, which it is not always the case -- xml_log
* (and unfortunately there's no clear explanation of the fail to compile).
*
* Also note that there's no explicit guard against said libraries producing
* never-newline-terminated chunks (which would just keep consuming memory),
* as it's quite improbable. Termination of the program in between the
* same-message chunks will raise a flag with valgrind and the likes, though.
*
* And lastly, regarding how dechunking combines with other non-message
* parameters -- for \p priority, most important running specification
* wins (possibly elevated to LOG_ERR in case of nonconformance with the
* newline-termination "protocol"), \p dechunk is expected to always be
* on once it was at the start, and the rest (\p postemit and \p prefix)
* are picked directly from the last chunk entry finalizing the message
* (also reasonable to always have it the same with all related entries).
*
* \param[in] priority Syslog priority for the message to be logged
* \param[in] dechunk Whether to dechunk new-line terminated message
* \param[in] postemit Code to be executed once message is sent out
* \param[in] prefix How to prefix the message or NULL for raw passing
* \param[in] fmt Format string as with printf-like functions
* \param[in] ap Variable argument list to supplement \p fmt format string
*/
#define PCMK__XML_LOG_BASE(priority, dechunk, postemit, prefix, fmt, ap) \
do { \
if (!(dechunk) && (prefix) == NULL) { /* quick pass */ \
qb_log_from_external_source_va(__func__, __FILE__, (fmt), \
(priority), __LINE__, 0, (ap)); \
(void) (postemit); \
} else { \
int CXLB_len = 0; \
char *CXLB_buf = NULL; \
static int CXLB_buffer_len = 0; \
static char *CXLB_buffer = NULL; \
static uint8_t CXLB_priority = 0; \
\
CXLB_len = vasprintf(&CXLB_buf, (fmt), (ap)); \
\
if (CXLB_len <= 0 || CXLB_buf[CXLB_len - 1] == '\n' || !(dechunk)) { \
if (CXLB_len < 0) { \
CXLB_buf = (char *) "LOG CORRUPTION HAZARD"; /*we don't modify*/\
CXLB_priority = QB_MIN(CXLB_priority, LOG_ERR); \
} else if (CXLB_len > 0 /* && (dechunk) */ \
&& CXLB_buf[CXLB_len - 1] == '\n') { \
CXLB_buf[CXLB_len - 1] = '\0'; \
} \
if (CXLB_buffer) { \
qb_log_from_external_source(__func__, __FILE__, "%s%s%s", \
CXLB_priority, __LINE__, 0, \
(prefix) != NULL ? (prefix) : "", \
CXLB_buffer, CXLB_buf); \
free(CXLB_buffer); \
} else { \
qb_log_from_external_source(__func__, __FILE__, "%s%s", \
(priority), __LINE__, 0, \
(prefix) != NULL ? (prefix) : "", \
CXLB_buf); \
} \
if (CXLB_len < 0) { \
CXLB_buf = NULL; /* restore temporary override */ \
} \
CXLB_buffer = NULL; \
CXLB_buffer_len = 0; \
(void) (postemit); \
\
} else if (CXLB_buffer == NULL) { \
CXLB_buffer_len = CXLB_len; \
CXLB_buffer = CXLB_buf; \
CXLB_buf = NULL; \
CXLB_priority = (priority); /* remember as a running severest */ \
\
} else { \
CXLB_buffer = realloc(CXLB_buffer, 1 + CXLB_buffer_len + CXLB_len); \
memcpy(CXLB_buffer + CXLB_buffer_len, CXLB_buf, CXLB_len); \
CXLB_buffer_len += CXLB_len; \
CXLB_buffer[CXLB_buffer_len] = '\0'; \
CXLB_priority = QB_MIN(CXLB_priority, (priority)); /* severest? */ \
} \
free(CXLB_buf); \
} \
} while (0)
/*
* \enum pcmk__xml_fmt_options
* \brief Bit flags to control format in XML logs and dumps
*/
enum pcmk__xml_fmt_options {
//! Exclude certain XML attributes (for calculating digests)
pcmk__xml_fmt_filtered = (1 << 0),
//! Include indentation and newlines
pcmk__xml_fmt_pretty = (1 << 1),
//! Include 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),
// @COMPAT Remove when v1 patchsets are removed
//! Log a created XML subtree
pcmk__xml_fmt_diff_plus = (1 << 7),
// @COMPAT Remove when v1 patchsets are removed
//! Log a removed XML subtree
pcmk__xml_fmt_diff_minus = (1 << 8),
// @COMPAT Remove when v1 patchsets are removed
//! Log a minimal version of an XML diff (only showing the changes)
pcmk__xml_fmt_diff_short = (1 << 9),
};
+void pcmk__xml_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);
/* XML search strings for guest, remote and pacemaker_remote nodes */
/* search string to find CIB resources entries for cluster nodes */
#define PCMK__XP_MEMBER_NODE_CONFIG \
"//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_NODES \
"/" PCMK_XE_NODE \
"[not(@" PCMK_XA_TYPE ") or @" PCMK_XA_TYPE "='" PCMK_VALUE_MEMBER "']"
/* search string to find CIB resources entries for guest nodes */
#define PCMK__XP_GUEST_NODE_CONFIG \
"//" PCMK_XE_CIB "//" PCMK_XE_CONFIGURATION "//" PCMK_XE_PRIMITIVE \
"//" PCMK_XE_META_ATTRIBUTES "//" PCMK_XE_NVPAIR \
"[@" PCMK_XA_NAME "='" PCMK_META_REMOTE_NODE "']"
/* search string to find CIB resources entries for remote nodes */
#define PCMK__XP_REMOTE_NODE_CONFIG \
"//" PCMK_XE_CIB "//" PCMK_XE_CONFIGURATION "//" PCMK_XE_PRIMITIVE \
"[@" PCMK_XA_TYPE "='" PCMK_VALUE_REMOTE "']" \
"[@" PCMK_XA_PROVIDER "='pacemaker']"
/* search string to find CIB node status entries for pacemaker_remote nodes */
#define PCMK__XP_REMOTE_NODE_STATUS \
"//" PCMK_XE_CIB "//" PCMK_XE_STATUS "//" PCMK__XE_NODE_STATE \
"[@" PCMK_XA_REMOTE_NODE "='" PCMK_VALUE_TRUE "']"
/*!
* \internal
* \brief Serialize XML (using libxml) into provided descriptor
*
* \param[in] fd File descriptor to (piece-wise) write to
* \param[in] cur XML subtree to proceed
*
* \return a standard Pacemaker return code
*/
int pcmk__xml2fd(int fd, xmlNode *cur);
enum pcmk__xml_artefact_ns {
pcmk__xml_artefact_ns_legacy_rng = 1,
pcmk__xml_artefact_ns_legacy_xslt,
pcmk__xml_artefact_ns_base_rng,
pcmk__xml_artefact_ns_base_xslt,
};
void pcmk__strip_xml_text(xmlNode *xml);
const char *pcmk__xe_add_last_written(xmlNode *xe);
xmlNode *pcmk__xe_first_child(const xmlNode *parent, const char *node_name,
const char *attr_n, const char *attr_v);
void pcmk__xe_remove_attr(xmlNode *element, const char *name);
bool pcmk__xe_remove_attr_cb(xmlNode *xml, void *user_data);
void pcmk__xe_remove_matching_attrs(xmlNode *element,
bool (*match)(xmlAttrPtr, void *),
void *user_data);
int pcmk__xe_delete_match(xmlNode *xml, xmlNode *search);
int pcmk__xe_replace_match(xmlNode *xml, xmlNode *replace);
int pcmk__xe_update_match(xmlNode *xml, xmlNode *update, uint32_t flags);
GString *pcmk__element_xpath(const xmlNode *xml);
/*!
* \internal
* \enum pcmk__xml_escape_type
* \brief Indicators of which XML characters to escape
*
* XML allows the escaping of special characters by replacing them with entity
* references (for example, <tt>"""</tt>) or character references (for
* example, <tt>" "</tt>).
*
* The special characters <tt>'&'</tt> (except as the beginning of an entity
* reference) and <tt>'<'</tt> 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). <tt>'>'</tt> 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 Retrieve the value of the \c PCMK_XA_ID XML attribute
*
* \param[in] xml XML element to check
*
* \return Value of the \c PCMK_XA_ID attribute (may be \c NULL)
*/
static inline const char *
pcmk__xe_id(const xmlNode *xml)
{
return crm_element_value(xml, PCMK_XA_ID);
}
/*!
* \internal
* \brief Check whether an XML element is of a particular type
*
* \param[in] xml XML element to compare
* \param[in] name XML element name to compare
*
* \return \c true if \p xml is of type \p name, otherwise \c false
*/
static inline bool
pcmk__xe_is(const xmlNode *xml, const char *name)
{
return (xml != NULL) && (xml->name != NULL) && (name != NULL)
&& (strcmp((const char *) xml->name, name) == 0);
}
/*!
* \internal
* \brief Return first non-text child node of an XML node
*
* \param[in] parent XML node to check
*
* \return First non-text child node of \p parent (or NULL if none)
*/
static inline xmlNode *
pcmk__xml_first_child(const xmlNode *parent)
{
xmlNode *child = (parent? parent->children : NULL);
while (child && (child->type == XML_TEXT_NODE)) {
child = child->next;
}
return child;
}
/*!
* \internal
* \brief Return next non-text sibling node of an XML node
*
* \param[in] child XML node to check
*
* \return Next non-text sibling of \p child (or NULL if none)
*/
static inline xmlNode *
pcmk__xml_next(const xmlNode *child)
{
xmlNode *next = (child? child->next : NULL);
while (next && (next->type == XML_TEXT_NODE)) {
next = next->next;
}
return next;
}
/*!
* \internal
* \brief Return next non-text sibling element of an XML element
*
* \param[in] child XML element to check
*
* \return Next sibling element of \p child (or NULL if none)
*/
static inline xmlNode *
pcmk__xe_next(const xmlNode *child)
{
xmlNode *next = child? child->next : NULL;
while (next && (next->type != XML_ELEMENT_NODE)) {
next = next->next;
}
return next;
}
xmlNode *pcmk__xe_create(xmlNode *parent, const char *name);
xmlNode *pcmk__xml_copy(xmlNode *parent, xmlNode *src);
xmlNode *pcmk__xe_next_same(const xmlNode *node);
void pcmk__xe_set_content(xmlNode *node, const char *format, ...)
G_GNUC_PRINTF(2, 3);
/*!
* \internal
* \enum pcmk__xa_flags
* \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),
};
int pcmk__xe_copy_attrs(xmlNode *target, const xmlNode *src, uint32_t flags);
/*!
* \internal
* \brief Like pcmk__xe_set_props, but takes a va_list instead of
* arguments directly.
*
* \param[in,out] node XML to add attributes to
* \param[in] pairs NULL-terminated list of name/value pairs to add
*/
void
pcmk__xe_set_propv(xmlNodePtr node, va_list pairs);
/*!
* \internal
* \brief Add a NULL-terminated list of name/value pairs to the given
* XML node as properties.
*
* \param[in,out] node XML node to add properties to
* \param[in] ... NULL-terminated list of name/value pairs
*
* \note A NULL name terminates the arguments; a NULL value will be skipped.
*/
void
pcmk__xe_set_props(xmlNodePtr node, ...)
G_GNUC_NULL_TERMINATED;
/*!
* \internal
* \brief Get first attribute of an XML element
*
* \param[in] xe XML element to check
*
* \return First attribute of \p xe (or NULL if \p xe is NULL or has none)
*/
static inline xmlAttr *
pcmk__xe_first_attr(const xmlNode *xe)
{
return (xe == NULL)? NULL : xe->properties;
}
/*!
* \internal
* \brief Extract the ID attribute from an XML element
*
* \param[in] xpath String to search
* \param[in] node Node to get the ID for
*
* \return ID attribute of \p node in xpath string \p xpath
*/
char *
pcmk__xpath_node_id(const char *xpath, const char *node);
/*!
* \internal
* \brief Print an informational message if an xpath query returned multiple
* items with the same ID.
*
* \param[in,out] out The output object
* \param[in] search The xpath search result, most typically the result of
* calling cib->cmds->query().
* \param[in] name The name searched for
*/
void
pcmk__warn_multiple_name_matches(pcmk__output_t *out, xmlNode *search,
const char *name);
/* internal XML-related utilities */
enum xml_private_flags {
pcmk__xf_none = 0x0000,
pcmk__xf_dirty = 0x0001,
pcmk__xf_deleted = 0x0002,
pcmk__xf_created = 0x0004,
pcmk__xf_modified = 0x0008,
pcmk__xf_tracking = 0x0010,
pcmk__xf_processed = 0x0020,
pcmk__xf_skip = 0x0040,
pcmk__xf_moved = 0x0080,
pcmk__xf_acl_enabled = 0x0100,
pcmk__xf_acl_read = 0x0200,
pcmk__xf_acl_write = 0x0400,
pcmk__xf_acl_deny = 0x0800,
pcmk__xf_acl_create = 0x1000,
pcmk__xf_acl_denied = 0x2000,
pcmk__xf_lazy = 0x4000,
};
void pcmk__set_xml_doc_flag(xmlNode *xml, enum xml_private_flags flag);
/*!
* \internal
* \brief Iterate over child elements of \p xml
*
* This function iterates over the children of \p xml, performing the
* callback function \p handler on each node. If the callback returns
* a value other than pcmk_rc_ok, the iteration stops and the value is
* returned. It is therefore possible that not all children will be
* visited.
*
* \param[in,out] xml The starting XML node. Can be NULL.
* \param[in] child_element_name The name that the node must match in order
* for \p handler to be run. If NULL, all
* child elements will match.
* \param[in] handler The callback function.
* \param[in,out] userdata User data to pass to the callback function.
* Can be NULL.
*
* \return Standard Pacemaker return code
*/
int
pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name,
int (*handler)(xmlNode *xml, void *userdata),
void *userdata);
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;
}
// @COMPAT Remove when v1 patchsets are removed
xmlNode *pcmk__diff_v1_xml_object(xmlNode *left, xmlNode *right, bool suppress);
// @COMPAT Drop when PCMK__XE_PROMOTABLE_LEGACY is removed
static inline const char *
pcmk__map_element_name(const xmlNode *xml)
{
if (xml == NULL) {
return NULL;
} else if (pcmk__xe_is(xml, PCMK__XE_PROMOTABLE_LEGACY)) {
return PCMK_XE_CLONE;
} else {
return (const char *) xml->name;
}
}
#endif // PCMK__XML_INTERNAL__H
diff --git a/lib/common/logging.c b/lib/common/logging.c
index efdbac30b3..0555d527d5 100644
--- a/lib/common/logging.c
+++ b/lib/common/logging.c
@@ -1,1297 +1,1297 @@
/*
* 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 <crm_internal.h>
#include <sys/param.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/utsname.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <limits.h>
#include <ctype.h>
#include <pwd.h>
#include <grp.h>
#include <time.h>
#include <libgen.h>
#include <signal.h>
#include <bzlib.h>
#include <qb/qbdefs.h>
#include <crm/crm.h>
#include <crm/common/mainloop.h>
// Use high-resolution (millisecond) timestamps if libqb supports them
#ifdef QB_FEATURE_LOG_HIRES_TIMESTAMPS
#define TIMESTAMP_FORMAT_SPEC "%%T"
typedef struct timespec *log_time_t;
#else
#define TIMESTAMP_FORMAT_SPEC "%%t"
typedef time_t log_time_t;
#endif
unsigned int crm_log_level = LOG_INFO;
unsigned int crm_trace_nonlog = 0;
bool pcmk__is_daemon = false;
char *pcmk__our_nodename = NULL;
static unsigned int crm_log_priority = LOG_NOTICE;
static GLogFunc glib_log_default = NULL;
static pcmk__output_t *logger_out = NULL;
pcmk__config_error_func pcmk__config_error_handler = NULL;
pcmk__config_warning_func pcmk__config_warning_handler = NULL;
void *pcmk__config_error_context = NULL;
void *pcmk__config_warning_context = NULL;
static gboolean crm_tracing_enabled(void);
static void
crm_glib_handler(const gchar * log_domain, GLogLevelFlags flags, const gchar * message,
gpointer user_data)
{
int log_level = LOG_WARNING;
GLogLevelFlags msg_level = (flags & G_LOG_LEVEL_MASK);
static struct qb_log_callsite *glib_cs = NULL;
if (glib_cs == NULL) {
glib_cs = qb_log_callsite_get(__func__, __FILE__, "glib-handler",
LOG_DEBUG, __LINE__, crm_trace_nonlog);
}
switch (msg_level) {
case G_LOG_LEVEL_CRITICAL:
log_level = LOG_CRIT;
if (!crm_is_callsite_active(glib_cs, LOG_DEBUG, crm_trace_nonlog)) {
/* log and record how we got here */
crm_abort(__FILE__, __func__, __LINE__, message, TRUE, TRUE);
}
break;
case G_LOG_LEVEL_ERROR:
log_level = LOG_ERR;
break;
case G_LOG_LEVEL_MESSAGE:
log_level = LOG_NOTICE;
break;
case G_LOG_LEVEL_INFO:
log_level = LOG_INFO;
break;
case G_LOG_LEVEL_DEBUG:
log_level = LOG_DEBUG;
break;
case G_LOG_LEVEL_WARNING:
case G_LOG_FLAG_RECURSION:
case G_LOG_FLAG_FATAL:
case G_LOG_LEVEL_MASK:
log_level = LOG_WARNING;
break;
}
do_crm_log(log_level, "%s: %s", log_domain, message);
}
#ifndef NAME_MAX
# define NAME_MAX 256
#endif
/*!
* \internal
* \brief Write out a blackbox (enabling blackboxes if needed)
*
* \param[in] nsig Signal number that was received
*
* \note This is a true signal handler, and so must be async-safe.
*/
static void
crm_trigger_blackbox(int nsig)
{
if(nsig == SIGTRAP) {
/* Turn it on if it wasn't already */
crm_enable_blackbox(nsig);
}
crm_write_blackbox(nsig, NULL);
}
void
crm_log_deinit(void)
{
if (glib_log_default != NULL) {
g_log_set_default_handler(glib_log_default, NULL);
}
}
#define FMT_MAX 256
/*!
* \internal
* \brief Set the log format string based on the passed-in method
*
* \param[in] method The detail level of the log output
* \param[in] daemon The daemon ID included in error messages
* \param[in] use_pid Cached result of getpid() call, for efficiency
* \param[in] use_nodename Cached result of uname() call, for efficiency
*
*/
/* XXX __attribute__((nonnull)) for use_nodename parameter */
static void
set_format_string(int method, const char *daemon, pid_t use_pid,
const char *use_nodename)
{
if (method == QB_LOG_SYSLOG) {
// The system log gets a simplified, user-friendly format
crm_extended_logging(method, QB_FALSE);
qb_log_format_set(method, "%g %p: %b");
} else {
// Everything else gets more detail, for advanced troubleshooting
int offset = 0;
char fmt[FMT_MAX];
if (method > QB_LOG_STDERR) {
// If logging to file, prefix with timestamp, node name, daemon ID
offset += snprintf(fmt + offset, FMT_MAX - offset,
TIMESTAMP_FORMAT_SPEC " %s %-20s[%lu] ",
use_nodename, daemon, (unsigned long) use_pid);
}
// Add function name (in parentheses)
offset += snprintf(fmt + offset, FMT_MAX - offset, "(%%n");
if (crm_tracing_enabled()) {
// When tracing, add file and line number
offset += snprintf(fmt + offset, FMT_MAX - offset, "@%%f:%%l");
}
offset += snprintf(fmt + offset, FMT_MAX - offset, ")");
// Add tag (if any), severity, and actual message
offset += snprintf(fmt + offset, FMT_MAX - offset, " %%g\t%%p: %%b");
CRM_LOG_ASSERT(offset > 0);
qb_log_format_set(method, fmt);
}
}
#define DEFAULT_LOG_FILE CRM_LOG_DIR "/pacemaker.log"
static bool
logfile_disabled(const char *filename)
{
return pcmk__str_eq(filename, PCMK_VALUE_NONE, pcmk__str_casei)
|| pcmk__str_eq(filename, "/dev/null", pcmk__str_none);
}
/*!
* \internal
* \brief Fix log file ownership if group is wrong or doesn't have access
*
* \param[in] filename Log file name (for logging only)
* \param[in] logfd Log file descriptor
*
* \return Standard Pacemaker return code
*/
static int
chown_logfile(const char *filename, int logfd)
{
uid_t pcmk_uid = 0;
gid_t pcmk_gid = 0;
struct stat st;
int rc;
// Get the log file's current ownership and permissions
if (fstat(logfd, &st) < 0) {
return errno;
}
// Any other errors don't prevent file from being used as log
rc = pcmk_daemon_user(&pcmk_uid, &pcmk_gid);
if (rc != pcmk_ok) {
rc = pcmk_legacy2rc(rc);
crm_warn("Not changing '%s' ownership because user information "
"unavailable: %s", filename, pcmk_rc_str(rc));
return pcmk_rc_ok;
}
if ((st.st_gid == pcmk_gid)
&& ((st.st_mode & S_IRWXG) == (S_IRGRP|S_IWGRP))) {
return pcmk_rc_ok;
}
if (fchown(logfd, pcmk_uid, pcmk_gid) < 0) {
crm_warn("Couldn't change '%s' ownership to user %s gid %d: %s",
filename, CRM_DAEMON_USER, pcmk_gid, strerror(errno));
}
return pcmk_rc_ok;
}
// Reset log file permissions (using environment variable if set)
static void
chmod_logfile(const char *filename, int logfd)
{
const char *modestr = pcmk__env_option(PCMK__ENV_LOGFILE_MODE);
mode_t filemode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP;
if (modestr != NULL) {
long filemode_l = strtol(modestr, NULL, 8);
if ((filemode_l != LONG_MIN) && (filemode_l != LONG_MAX)) {
filemode = (mode_t) filemode_l;
}
}
if ((filemode != 0) && (fchmod(logfd, filemode) < 0)) {
crm_warn("Couldn't change '%s' mode to %04o: %s",
filename, filemode, strerror(errno));
}
}
// If we're root, correct a log file's permissions if needed
static int
set_logfile_permissions(const char *filename, FILE *logfile)
{
if (geteuid() == 0) {
int logfd = fileno(logfile);
int rc = chown_logfile(filename, logfd);
if (rc != pcmk_rc_ok) {
return rc;
}
chmod_logfile(filename, logfd);
}
return pcmk_rc_ok;
}
// Enable libqb logging to a new log file
static void
enable_logfile(int fd)
{
qb_log_ctl(fd, QB_LOG_CONF_ENABLED, QB_TRUE);
#if 0
qb_log_ctl(fd, QB_LOG_CONF_FILE_SYNC, 1); // Turn on synchronous writes
#endif
#ifdef HAVE_qb_log_conf_QB_LOG_CONF_MAX_LINE_LEN
// Longer than default, for logging long XML lines
qb_log_ctl(fd, QB_LOG_CONF_MAX_LINE_LEN, 800);
#endif
crm_update_callsites();
}
static inline void
disable_logfile(int fd)
{
qb_log_ctl(fd, QB_LOG_CONF_ENABLED, QB_FALSE);
}
static void
setenv_logfile(const char *filename)
{
// Some resource agents will log only if environment variable is set
if (pcmk__env_option(PCMK__ENV_LOGFILE) == NULL) {
pcmk__set_env_option(PCMK__ENV_LOGFILE, filename, true);
}
}
/*!
* \brief Add a file to be used as a Pacemaker detail log
*
* \param[in] filename Name of log file to use
*
* \return Standard Pacemaker return code
*/
int
pcmk__add_logfile(const char *filename)
{
/* No log messages from this function will be logged to the new log!
* If another target such as syslog has already been added, the messages
* should show up there.
*/
int fd = 0;
int rc = pcmk_rc_ok;
FILE *logfile = NULL;
bool is_default = false;
static int default_fd = -1;
static bool have_logfile = false;
// Use default if caller didn't specify (and we don't already have one)
if (filename == NULL) {
if (have_logfile) {
return pcmk_rc_ok;
}
filename = DEFAULT_LOG_FILE;
}
// If the user doesn't want logging, we're done
if (logfile_disabled(filename)) {
return pcmk_rc_ok;
}
// If the caller wants the default and we already have it, we're done
is_default = pcmk__str_eq(filename, DEFAULT_LOG_FILE, pcmk__str_none);
if (is_default && (default_fd >= 0)) {
return pcmk_rc_ok;
}
// Check whether we have write access to the file
logfile = fopen(filename, "a");
if (logfile == NULL) {
rc = errno;
crm_warn("Logging to '%s' is disabled: %s " CRM_XS " uid=%u gid=%u",
filename, strerror(rc), geteuid(), getegid());
return rc;
}
rc = set_logfile_permissions(filename, logfile);
if (rc != pcmk_rc_ok) {
crm_warn("Logging to '%s' is disabled: %s " CRM_XS " permissions",
filename, strerror(rc));
fclose(logfile);
return rc;
}
// Close and reopen as libqb logging target
fclose(logfile);
fd = qb_log_file_open(filename);
if (fd < 0) {
crm_warn("Logging to '%s' is disabled: %s " CRM_XS " qb_log_file_open",
filename, strerror(-fd));
return -fd; // == +errno
}
if (is_default) {
default_fd = fd;
setenv_logfile(filename);
} else if (default_fd >= 0) {
crm_notice("Switching logging to %s", filename);
disable_logfile(default_fd);
}
crm_notice("Additional logging available in %s", filename);
enable_logfile(fd);
have_logfile = true;
return pcmk_rc_ok;
}
/*!
* \brief Add multiple additional log files
*
* \param[in] log_files Array of log files to add
* \param[in] out Output object to use for error reporting
*
* \return Standard Pacemaker return code
*/
void
pcmk__add_logfiles(gchar **log_files, pcmk__output_t *out)
{
if (log_files == NULL) {
return;
}
for (gchar **fname = log_files; *fname != NULL; fname++) {
int rc = pcmk__add_logfile(*fname);
if (rc != pcmk_rc_ok) {
out->err(out, "Logging to %s is disabled: %s",
*fname, pcmk_rc_str(rc));
}
}
}
static int blackbox_trigger = 0;
static volatile char *blackbox_file_prefix = NULL;
static void
blackbox_logger(int32_t t, struct qb_log_callsite *cs, log_time_t timestamp,
const char *msg)
{
if(cs && cs->priority < LOG_ERR) {
crm_write_blackbox(SIGTRAP, cs); /* Bypass the over-dumping logic */
} else {
crm_write_blackbox(0, cs);
}
}
static void
crm_control_blackbox(int nsig, bool enable)
{
int lpc = 0;
if (blackbox_file_prefix == NULL) {
pid_t pid = getpid();
blackbox_file_prefix = crm_strdup_printf("%s/%s-%lu",
CRM_BLACKBOX_DIR,
crm_system_name,
(unsigned long) pid);
}
if (enable && qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) {
qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_SIZE, 5 * 1024 * 1024); /* Any size change drops existing entries */
qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_TRUE); /* Setting the size seems to disable it */
/* Enable synchronous logging */
for (lpc = QB_LOG_BLACKBOX; lpc < QB_LOG_TARGET_MAX; lpc++) {
qb_log_ctl(lpc, QB_LOG_CONF_FILE_SYNC, QB_TRUE);
}
crm_notice("Initiated blackbox recorder: %s", blackbox_file_prefix);
/* Save to disk on abnormal termination */
crm_signal_handler(SIGSEGV, crm_trigger_blackbox);
crm_signal_handler(SIGABRT, crm_trigger_blackbox);
crm_signal_handler(SIGILL, crm_trigger_blackbox);
crm_signal_handler(SIGBUS, crm_trigger_blackbox);
crm_signal_handler(SIGFPE, crm_trigger_blackbox);
crm_update_callsites();
blackbox_trigger = qb_log_custom_open(blackbox_logger, NULL, NULL, NULL);
qb_log_ctl(blackbox_trigger, QB_LOG_CONF_ENABLED, QB_TRUE);
crm_trace("Trigger: %d is %d %d", blackbox_trigger,
qb_log_ctl(blackbox_trigger, QB_LOG_CONF_STATE_GET, 0), QB_LOG_STATE_ENABLED);
crm_update_callsites();
} else if (!enable && qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_STATE_GET, 0) == QB_LOG_STATE_ENABLED) {
qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE);
/* Disable synchronous logging again when the blackbox is disabled */
for (lpc = QB_LOG_BLACKBOX; lpc < QB_LOG_TARGET_MAX; lpc++) {
qb_log_ctl(lpc, QB_LOG_CONF_FILE_SYNC, QB_FALSE);
}
}
}
void
crm_enable_blackbox(int nsig)
{
crm_control_blackbox(nsig, TRUE);
}
void
crm_disable_blackbox(int nsig)
{
crm_control_blackbox(nsig, FALSE);
}
/*!
* \internal
* \brief Write out a blackbox, if blackboxes are enabled
*
* \param[in] nsig Signal that was received
* \param[in] cs libqb callsite
*
* \note This may be called via a true signal handler and so must be async-safe.
* @TODO actually make this async-safe
*/
void
crm_write_blackbox(int nsig, const struct qb_log_callsite *cs)
{
static volatile int counter = 1;
static volatile time_t last = 0;
char buffer[NAME_MAX];
time_t now = time(NULL);
if (blackbox_file_prefix == NULL) {
return;
}
switch (nsig) {
case 0:
case SIGTRAP:
/* The graceful case - such as assertion failure or user request */
if (nsig == 0 && now == last) {
/* Prevent over-dumping */
return;
}
snprintf(buffer, NAME_MAX, "%s.%d", blackbox_file_prefix, counter++);
if (nsig == SIGTRAP) {
crm_notice("Blackbox dump requested, please see %s for contents", buffer);
} else if (cs) {
syslog(LOG_NOTICE,
"Problem detected at %s:%d (%s), please see %s for additional details",
cs->function, cs->lineno, cs->filename, buffer);
} else {
crm_notice("Problem detected, please see %s for additional details", buffer);
}
last = now;
qb_log_blackbox_write_to_file(buffer);
/* Flush the existing contents
* A size change would also work
*/
qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE);
qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_TRUE);
break;
default:
/* Do as little as possible, just try to get what we have out
* We logged the filename when the blackbox was enabled
*/
crm_signal_handler(nsig, SIG_DFL);
qb_log_blackbox_write_to_file((const char *)blackbox_file_prefix);
qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE);
raise(nsig);
break;
}
}
static const char *
crm_quark_to_string(uint32_t tag)
{
const char *text = g_quark_to_string(tag);
if (text) {
return text;
}
return "";
}
static void
crm_log_filter_source(int source, const char *trace_files, const char *trace_fns,
const char *trace_fmts, const char *trace_tags, const char *trace_blackbox,
struct qb_log_callsite *cs)
{
if (qb_log_ctl(source, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) {
return;
} else if (cs->tags != crm_trace_nonlog && source == QB_LOG_BLACKBOX) {
/* Blackbox gets everything if enabled */
qb_bit_set(cs->targets, source);
} else if (source == blackbox_trigger && blackbox_trigger > 0) {
/* Should this log message result in the blackbox being dumped */
if (cs->priority <= LOG_ERR) {
qb_bit_set(cs->targets, source);
} else if (trace_blackbox) {
char *key = crm_strdup_printf("%s:%d", cs->function, cs->lineno);
if (strstr(trace_blackbox, key) != NULL) {
qb_bit_set(cs->targets, source);
}
free(key);
}
} else if (source == QB_LOG_SYSLOG) { /* No tracing to syslog */
if (cs->priority <= crm_log_priority && cs->priority <= crm_log_level) {
qb_bit_set(cs->targets, source);
}
/* Log file tracing options... */
} else if (cs->priority <= crm_log_level) {
qb_bit_set(cs->targets, source);
} else if (trace_files && strstr(trace_files, cs->filename) != NULL) {
qb_bit_set(cs->targets, source);
} else if (trace_fns && strstr(trace_fns, cs->function) != NULL) {
qb_bit_set(cs->targets, source);
} else if (trace_fmts && strstr(trace_fmts, cs->format) != NULL) {
qb_bit_set(cs->targets, source);
} else if (trace_tags
&& cs->tags != 0
&& cs->tags != crm_trace_nonlog && g_quark_to_string(cs->tags) != NULL) {
qb_bit_set(cs->targets, source);
}
}
#ifndef HAVE_STRCHRNUL
/* strchrnul() is a GNU extension. If not present, use our own definition.
* The GNU version returns char*, but we only need it to be const char*.
*/
static const char *
strchrnul(const char *s, int c)
{
while ((*s != c) && (*s != '\0')) {
++s;
}
return s;
}
#endif
static void
crm_log_filter(struct qb_log_callsite *cs)
{
int lpc = 0;
static int need_init = 1;
static const char *trace_fns = NULL;
static const char *trace_tags = NULL;
static const char *trace_fmts = NULL;
static const char *trace_files = NULL;
static const char *trace_blackbox = NULL;
if (need_init) {
need_init = 0;
trace_fns = pcmk__env_option(PCMK__ENV_TRACE_FUNCTIONS);
trace_fmts = pcmk__env_option(PCMK__ENV_TRACE_FORMATS);
trace_tags = pcmk__env_option(PCMK__ENV_TRACE_TAGS);
trace_files = pcmk__env_option(PCMK__ENV_TRACE_FILES);
trace_blackbox = pcmk__env_option(PCMK__ENV_TRACE_BLACKBOX);
if (trace_tags != NULL) {
uint32_t tag;
char token[500];
const char *offset = NULL;
const char *next = trace_tags;
do {
offset = next;
next = strchrnul(offset, ',');
snprintf(token, sizeof(token), "%.*s", (int)(next - offset), offset);
tag = g_quark_from_string(token);
crm_info("Created GQuark %u from token '%s' in '%s'", tag, token, trace_tags);
if (next[0] != 0) {
next++;
}
} while (next != NULL && next[0] != 0);
}
}
cs->targets = 0; /* Reset then find targets to enable */
for (lpc = QB_LOG_SYSLOG; lpc < QB_LOG_TARGET_MAX; lpc++) {
crm_log_filter_source(lpc, trace_files, trace_fns, trace_fmts, trace_tags, trace_blackbox,
cs);
}
}
gboolean
crm_is_callsite_active(struct qb_log_callsite *cs, uint8_t level, uint32_t tags)
{
gboolean refilter = FALSE;
if (cs == NULL) {
return FALSE;
}
if (cs->priority != level) {
cs->priority = level;
refilter = TRUE;
}
if (cs->tags != tags) {
cs->tags = tags;
refilter = TRUE;
}
if (refilter) {
crm_log_filter(cs);
}
if (cs->targets == 0) {
return FALSE;
}
return TRUE;
}
void
crm_update_callsites(void)
{
static gboolean log = TRUE;
if (log) {
log = FALSE;
crm_debug
("Enabling callsites based on priority=%d, files=%s, functions=%s, formats=%s, tags=%s",
crm_log_level, pcmk__env_option(PCMK__ENV_TRACE_FILES),
pcmk__env_option(PCMK__ENV_TRACE_FUNCTIONS),
pcmk__env_option(PCMK__ENV_TRACE_FORMATS),
pcmk__env_option(PCMK__ENV_TRACE_TAGS));
}
qb_log_filter_fn_set(crm_log_filter);
}
static gboolean
crm_tracing_enabled(void)
{
return (crm_log_level == LOG_TRACE)
|| (pcmk__env_option(PCMK__ENV_TRACE_FILES) != NULL)
|| (pcmk__env_option(PCMK__ENV_TRACE_FUNCTIONS) != NULL)
|| (pcmk__env_option(PCMK__ENV_TRACE_FORMATS) != NULL)
|| (pcmk__env_option(PCMK__ENV_TRACE_TAGS) != NULL);
}
static int
crm_priority2int(const char *name)
{
struct syslog_names {
const char *name;
int priority;
};
static struct syslog_names p_names[] = {
{"emerg", LOG_EMERG},
{"alert", LOG_ALERT},
{"crit", LOG_CRIT},
{"error", LOG_ERR},
{"warning", LOG_WARNING},
{"notice", LOG_NOTICE},
{"info", LOG_INFO},
{"debug", LOG_DEBUG},
{NULL, -1}
};
int lpc;
for (lpc = 0; name != NULL && p_names[lpc].name != NULL; lpc++) {
if (pcmk__str_eq(p_names[lpc].name, name, pcmk__str_none)) {
return p_names[lpc].priority;
}
}
return crm_log_priority;
}
/*!
* \internal
* \brief Set the identifier for the current process
*
* If the identifier crm_system_name is not already set, then it is set as follows:
* - it is passed to the function via the "entity" parameter, or
* - it is derived from the executable name
*
* The identifier can be used in logs, IPC, and more.
*
* This method also sets the PCMK_service environment variable.
*
* \param[in] entity If not NULL, will be assigned to the identifier
* \param[in] argc The number of command line parameters
* \param[in] argv The command line parameter values
*/
static void
set_identity(const char *entity, int argc, char *const *argv)
{
if (crm_system_name != NULL) {
return; // Already set, don't overwrite
}
if (entity != NULL) {
crm_system_name = pcmk__str_copy(entity);
} else if ((argc > 0) && (argv != NULL)) {
char *mutable = strdup(argv[0]);
char *modified = basename(mutable);
if (strstr(modified, "lt-") == modified) {
modified += 3;
}
crm_system_name = pcmk__str_copy(modified);
free(mutable);
} else {
crm_system_name = pcmk__str_copy("Unknown");
}
// Used by fencing.py.py (in fence-agents)
pcmk__set_env_option(PCMK__ENV_SERVICE, crm_system_name, false);
}
void
crm_log_preinit(const char *entity, int argc, char *const *argv)
{
/* Configure libqb logging with nothing turned on */
struct utsname res;
int lpc = 0;
int32_t qb_facility = 0;
pid_t pid = getpid();
const char *nodename = "localhost";
static bool have_logging = false;
if (have_logging) {
return;
}
have_logging = true;
- crm_xml_init(); /* Sets buffer allocation strategy */
+ pcmk__xml_init();
if (crm_trace_nonlog == 0) {
crm_trace_nonlog = g_quark_from_static_string("Pacemaker non-logging tracepoint");
}
umask(S_IWGRP | S_IWOTH | S_IROTH);
/* Redirect messages from glib functions to our handler */
glib_log_default = g_log_set_default_handler(crm_glib_handler, NULL);
/* and for good measure... - this enum is a bit field (!) */
g_log_set_always_fatal((GLogLevelFlags) 0); /*value out of range */
/* Set crm_system_name, which is used as the logging name. It may also
* be used for other purposes such as an IPC client name.
*/
set_identity(entity, argc, argv);
qb_facility = qb_log_facility2int("local0");
qb_log_init(crm_system_name, qb_facility, LOG_ERR);
crm_log_level = LOG_CRIT;
/* Nuke any syslog activity until it's asked for */
qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_ENABLED, QB_FALSE);
#ifdef HAVE_qb_log_conf_QB_LOG_CONF_MAX_LINE_LEN
// Shorter than default, generous for what we *should* send to syslog
qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_MAX_LINE_LEN, 256);
#endif
if (uname(memset(&res, 0, sizeof(res))) == 0 && *res.nodename != '\0') {
nodename = res.nodename;
}
/* Set format strings and disable threading
* Pacemaker and threads do not mix well (due to the amount of forking)
*/
qb_log_tags_stringify_fn_set(crm_quark_to_string);
for (lpc = QB_LOG_SYSLOG; lpc < QB_LOG_TARGET_MAX; lpc++) {
qb_log_ctl(lpc, QB_LOG_CONF_THREADED, QB_FALSE);
#ifdef HAVE_qb_log_conf_QB_LOG_CONF_ELLIPSIS
// End truncated lines with '...'
qb_log_ctl(lpc, QB_LOG_CONF_ELLIPSIS, QB_TRUE);
#endif
set_format_string(lpc, crm_system_name, pid, nodename);
}
#ifdef ENABLE_NLS
/* Enable translations (experimental). Currently we only have a few
* proof-of-concept translations for some option help. The goal would be to
* offer translations for option help and man pages rather than logs or
* documentation, to reduce the burden of maintaining them.
*/
// Load locale information for the local host from the environment
setlocale(LC_ALL, "");
// Tell gettext where to find Pacemaker message catalogs
CRM_ASSERT(bindtextdomain(PACKAGE, PCMK__LOCALE_DIR) != NULL);
// Tell gettext to use the Pacemaker message catalogs
CRM_ASSERT(textdomain(PACKAGE) != NULL);
// Tell gettext that the translated strings are stored in UTF-8
bind_textdomain_codeset(PACKAGE, "UTF-8");
#endif
}
gboolean
crm_log_init(const char *entity, uint8_t level, gboolean daemon, gboolean to_stderr,
int argc, char **argv, gboolean quiet)
{
const char *syslog_priority = NULL;
const char *facility = pcmk__env_option(PCMK__ENV_LOGFACILITY);
const char *f_copy = facility;
pcmk__is_daemon = daemon;
crm_log_preinit(entity, argc, argv);
if (level > LOG_TRACE) {
level = LOG_TRACE;
}
if(level > crm_log_level) {
crm_log_level = level;
}
/* Should we log to syslog */
if (facility == NULL) {
if (pcmk__is_daemon) {
facility = "daemon";
} else {
facility = PCMK_VALUE_NONE;
}
pcmk__set_env_option(PCMK__ENV_LOGFACILITY, facility, true);
}
if (pcmk__str_eq(facility, PCMK_VALUE_NONE, pcmk__str_casei)) {
quiet = TRUE;
} else {
qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_FACILITY, qb_log_facility2int(facility));
}
if (pcmk__env_option_enabled(crm_system_name, PCMK__ENV_DEBUG)) {
/* Override the default setting */
crm_log_level = LOG_DEBUG;
}
/* What lower threshold do we have for sending to syslog */
syslog_priority = pcmk__env_option(PCMK__ENV_LOGPRIORITY);
if (syslog_priority) {
crm_log_priority = crm_priority2int(syslog_priority);
}
qb_log_filter_ctl(QB_LOG_SYSLOG, QB_LOG_FILTER_ADD, QB_LOG_FILTER_FILE, "*",
crm_log_priority);
// Log to syslog unless requested to be quiet
if (!quiet) {
qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_ENABLED, QB_TRUE);
}
/* Should we log to stderr */
if (pcmk__env_option_enabled(crm_system_name, PCMK__ENV_STDERR)) {
/* Override the default setting */
to_stderr = TRUE;
}
crm_enable_stderr(to_stderr);
// Log to a file if we're a daemon or user asked for one
{
const char *logfile = pcmk__env_option(PCMK__ENV_LOGFILE);
if (!pcmk__str_eq(PCMK_VALUE_NONE, logfile, pcmk__str_casei)
&& (pcmk__is_daemon || (logfile != NULL))) {
// Daemons always get a log file, unless explicitly set to "none"
pcmk__add_logfile(logfile);
}
}
if (pcmk__is_daemon
&& pcmk__env_option_enabled(crm_system_name, PCMK__ENV_BLACKBOX)) {
crm_enable_blackbox(0);
}
/* Summary */
crm_trace("Quiet: %d, facility %s", quiet, f_copy);
pcmk__env_option(PCMK__ENV_LOGFILE);
pcmk__env_option(PCMK__ENV_LOGFACILITY);
crm_update_callsites();
/* Ok, now we can start logging... */
// Disable daemon request if user isn't root or Pacemaker daemon user
if (pcmk__is_daemon) {
const char *user = getenv("USER");
if (user != NULL && !pcmk__strcase_any_of(user, "root", CRM_DAEMON_USER, NULL)) {
crm_trace("Not switching to corefile directory for %s", user);
pcmk__is_daemon = false;
}
}
if (pcmk__is_daemon) {
int user = getuid();
struct passwd *pwent = getpwuid(user);
if (pwent == NULL) {
crm_perror(LOG_ERR, "Cannot get name for uid: %d", user);
} else if (!pcmk__strcase_any_of(pwent->pw_name, "root", CRM_DAEMON_USER, NULL)) {
crm_trace("Don't change active directory for regular user: %s", pwent->pw_name);
} else if (chdir(CRM_CORE_DIR) < 0) {
crm_perror(LOG_INFO, "Cannot change active directory to " CRM_CORE_DIR);
} else {
crm_info("Changed active directory to " CRM_CORE_DIR);
}
/* Original meanings from signal(7)
*
* Signal Value Action Comment
* SIGTRAP 5 Core Trace/breakpoint trap
* SIGUSR1 30,10,16 Term User-defined signal 1
* SIGUSR2 31,12,17 Term User-defined signal 2
*
* Our usage is as similar as possible
*/
mainloop_add_signal(SIGUSR1, crm_enable_blackbox);
mainloop_add_signal(SIGUSR2, crm_disable_blackbox);
mainloop_add_signal(SIGTRAP, crm_trigger_blackbox);
} else if (!quiet) {
crm_log_args(argc, argv);
}
return TRUE;
}
/* returns the old value */
unsigned int
set_crm_log_level(unsigned int level)
{
unsigned int old = crm_log_level;
if (level > LOG_TRACE) {
level = LOG_TRACE;
}
crm_log_level = level;
crm_update_callsites();
crm_trace("New log level: %d", level);
return old;
}
void
crm_enable_stderr(int enable)
{
if (enable && qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) {
qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_ENABLED, QB_TRUE);
crm_update_callsites();
} else if (enable == FALSE) {
qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_ENABLED, QB_FALSE);
}
}
/*!
* \brief Make logging more verbose
*
* If logging to stderr is not already enabled when this function is called,
* enable it. Otherwise, increase the log level by 1.
*
* \param[in] argc Ignored
* \param[in] argv Ignored
*/
void
crm_bump_log_level(int argc, char **argv)
{
if (qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_STATE_GET, 0)
!= QB_LOG_STATE_ENABLED) {
crm_enable_stderr(TRUE);
} else {
set_crm_log_level(crm_log_level + 1);
}
}
unsigned int
get_crm_log_level(void)
{
return crm_log_level;
}
/*!
* \brief Log the command line (once)
*
* \param[in] Number of values in \p argv
* \param[in] Command-line arguments (including command name)
*
* \note This function will only log once, even if called with different
* arguments.
*/
void
crm_log_args(int argc, char **argv)
{
static bool logged = false;
gchar *arg_string = NULL;
if ((argc == 0) || (argv == NULL) || logged) {
return;
}
logged = true;
arg_string = g_strjoinv(" ", argv);
crm_notice("Invoked: %s", arg_string);
g_free(arg_string);
}
void
crm_log_output_fn(const char *file, const char *function, int line, int level, const char *prefix,
const char *output)
{
const char *next = NULL;
const char *offset = NULL;
if (level == LOG_NEVER) {
return;
}
if (output == NULL) {
if (level != LOG_STDOUT) {
level = LOG_TRACE;
}
output = "-- empty --";
}
next = output;
do {
offset = next;
next = strchrnul(offset, '\n');
do_crm_log_alias(level, file, function, line, "%s [ %.*s ]", prefix,
(int)(next - offset), offset);
if (next[0] != 0) {
next++;
}
} while (next != NULL && next[0] != 0);
}
void
pcmk__cli_init_logging(const char *name, unsigned int verbosity)
{
crm_log_init(name, LOG_ERR, FALSE, FALSE, 0, NULL, TRUE);
for (int i = 0; i < verbosity; i++) {
/* These arguments are ignored, so pass placeholders. */
crm_bump_log_level(0, NULL);
}
}
/*!
* \brief Log XML line-by-line in a formatted fashion
*
* \param[in] file File name to use for log filtering
* \param[in] function Function name to use for log filtering
* \param[in] line Line number to use for log filtering
* \param[in] tags Logging tags to use for log filtering
* \param[in] level Priority at which to log the messages
* \param[in] text Prefix for each line
* \param[in] xml XML to log
*
* \note This does nothing when \p level is \p LOG_STDOUT.
* \note Do not call this function directly. It should be called only from the
* \p do_crm_log_xml() macro.
*/
void
pcmk_log_xml_as(const char *file, const char *function, uint32_t line,
uint32_t tags, uint8_t level, const char *text, const xmlNode *xml)
{
if (xml == NULL) {
do_crm_log(level, "%s%sNo data to dump as XML",
pcmk__s(text, ""), pcmk__str_empty(text)? "" : " ");
} else {
if (logger_out == NULL) {
CRM_CHECK(pcmk__log_output_new(&logger_out) == pcmk_rc_ok, return);
}
pcmk__output_set_log_level(logger_out, level);
pcmk__output_set_log_filter(logger_out, file, function, line, tags);
pcmk__xml_show(logger_out, text, xml, 1,
pcmk__xml_fmt_pretty
|pcmk__xml_fmt_open
|pcmk__xml_fmt_children
|pcmk__xml_fmt_close);
pcmk__output_set_log_filter(logger_out, NULL, NULL, 0U, 0U);
}
}
/*!
* \internal
* \brief Log XML changes line-by-line in a formatted fashion
*
* \param[in] file File name to use for log filtering
* \param[in] function Function name to use for log filtering
* \param[in] line Line number to use for log filtering
* \param[in] tags Logging tags to use for log filtering
* \param[in] level Priority at which to log the messages
* \param[in] xml XML whose changes to log
*
* \note This does nothing when \p level is \c LOG_STDOUT.
*/
void
pcmk__log_xml_changes_as(const char *file, const char *function, uint32_t line,
uint32_t tags, uint8_t level, const xmlNode *xml)
{
if (xml == NULL) {
do_crm_log(level, "No XML to dump");
return;
}
if (logger_out == NULL) {
CRM_CHECK(pcmk__log_output_new(&logger_out) == pcmk_rc_ok, return);
}
pcmk__output_set_log_level(logger_out, level);
pcmk__output_set_log_filter(logger_out, file, function, line, tags);
pcmk__xml_show_changes(logger_out, xml);
pcmk__output_set_log_filter(logger_out, NULL, NULL, 0U, 0U);
}
/*!
* \internal
* \brief Log an XML patchset line-by-line in a formatted fashion
*
* \param[in] file File name to use for log filtering
* \param[in] function Function name to use for log filtering
* \param[in] line Line number to use for log filtering
* \param[in] tags Logging tags to use for log filtering
* \param[in] level Priority at which to log the messages
* \param[in] patchset XML patchset to log
*
* \note This does nothing when \p level is \c LOG_STDOUT.
*/
void
pcmk__log_xml_patchset_as(const char *file, const char *function, uint32_t line,
uint32_t tags, uint8_t level, const xmlNode *patchset)
{
if (patchset == NULL) {
do_crm_log(level, "No patchset to dump");
return;
}
if (logger_out == NULL) {
CRM_CHECK(pcmk__log_output_new(&logger_out) == pcmk_rc_ok, return);
}
pcmk__output_set_log_level(logger_out, level);
pcmk__output_set_log_filter(logger_out, file, function, line, tags);
logger_out->message(logger_out, "xml-patchset", patchset);
pcmk__output_set_log_filter(logger_out, NULL, NULL, 0U, 0U);
}
/*!
* \internal
* \brief Free the logging library's internal log output object
*/
void
pcmk__free_common_logger(void)
{
if (logger_out != NULL) {
logger_out->finish(logger_out, CRM_EX_OK, true, NULL);
pcmk__output_free(logger_out);
logger_out = NULL;
}
}
// Deprecated functions kept only for backward API compatibility
// LCOV_EXCL_START
#include <crm/common/logging_compat.h>
gboolean
crm_log_cli_init(const char *entity)
{
pcmk__cli_init_logging(entity, 0);
return TRUE;
}
gboolean
crm_add_logfile(const char *filename)
{
return pcmk__add_logfile(filename) == pcmk_rc_ok;
}
void
pcmk_log_xml_impl(uint8_t level, const char *text, const xmlNode *xml)
{
pcmk_log_xml_as(__FILE__, __func__, __LINE__, 0, level, text, xml);
}
// LCOV_EXCL_STOP
// End deprecated API
void pcmk__set_config_error_handler(pcmk__config_error_func error_handler, void *error_context)
{
pcmk__config_error_handler = error_handler;
pcmk__config_error_context = error_context;
}
void pcmk__set_config_warning_handler(pcmk__config_warning_func warning_handler, void *warning_context)
{
pcmk__config_warning_handler = warning_handler;
pcmk__config_warning_context = warning_context;
}
diff --git a/lib/common/tests/xml/Makefile.am b/lib/common/tests/xml/Makefile.am
index 9ed1620926..9afd05d6e4 100644
--- a/lib/common/tests/xml/Makefile.am
+++ b/lib/common/tests/xml/Makefile.am
@@ -1,22 +1,22 @@
#
# Copyright 2022-2024 the Pacemaker project contributors
#
# The version control history for this file may have further details.
#
# This source code is licensed under the GNU General Public License version 2
# or later (GPLv2+) WITHOUT ANY WARRANTY.
#
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 = crm_xml_init_test \
- pcmk__xe_copy_attrs_test \
+check_PROGRAMS = pcmk__xe_copy_attrs_test \
pcmk__xe_first_child_test \
pcmk__xe_foreach_child_test \
pcmk__xe_set_score_test \
pcmk__xml_escape_test \
+ pcmk__xml_init_test \
pcmk__xml_needs_escape_test
TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/xml/crm_xml_init_test.c b/lib/common/tests/xml/pcmk__xml_init_test.c
similarity index 99%
rename from lib/common/tests/xml/crm_xml_init_test.c
rename to lib/common/tests/xml/pcmk__xml_init_test.c
index 1f7872870b..d5e00b4f97 100644
--- a/lib/common/tests/xml/crm_xml_init_test.c
+++ b/lib/common/tests/xml/pcmk__xml_init_test.c
@@ -1,230 +1,230 @@
/*
* Copyright 2023-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <crm/common/xml.h>
#include <crm/common/unittest_internal.h>
#include <crm/common/xml_internal.h>
#include "crmcommon_private.h"
/* Copied from lib/common/xml.c */
#define XML_DOC_PRIVATE_MAGIC 0x81726354UL
#define XML_NODE_PRIVATE_MAGIC 0x54637281UL
static int
setup(void **state) {
- crm_xml_init();
+ pcmk__xml_init();
return 0;
}
static int
teardown(void **state) {
crm_xml_cleanup();
return 0;
}
static void
buffer_scheme_test(void **state) {
assert_int_equal(XML_BUFFER_ALLOC_DOUBLEIT, xmlGetBufferAllocationScheme());
}
/* These functions also serve as unit tests of the static new_private_data
* function. We can't test free_private_data because libxml will call that as
* part of freeing everything else. By the time we'd get back into a unit test
* where we could check that private members are NULL, the structure containing
* the private data would have been freed.
*
* This could probably be tested with a lot of function mocking, but that
* doesn't seem worth it.
*/
static void
create_document_node(void **state) {
xml_doc_private_t *docpriv = NULL;
xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
/* Double check things */
assert_non_null(doc);
assert_int_equal(doc->type, XML_DOCUMENT_NODE);
/* Check that the private data is initialized correctly */
docpriv = doc->_private;
assert_non_null(docpriv);
assert_int_equal(docpriv->check, XML_DOC_PRIVATE_MAGIC);
assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty|pcmk__xf_created));
/* Clean up */
xmlFreeDoc(doc);
}
static void
create_element_node(void **state) {
xml_doc_private_t *docpriv = NULL;
xml_node_private_t *priv = NULL;
xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
xmlNodePtr node = xmlNewDocNode(doc, NULL, (pcmkXmlStr) "test", NULL);
/* Adding a node to the document marks it as dirty */
docpriv = doc->_private;
assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
/* Double check things */
assert_non_null(node);
assert_int_equal(node->type, XML_ELEMENT_NODE);
/* Check that the private data is initialized correctly */
priv = node->_private;
assert_non_null(priv);
assert_int_equal(priv->check, XML_NODE_PRIVATE_MAGIC);
assert_true(pcmk_all_flags_set(priv->flags, pcmk__xf_dirty|pcmk__xf_created));
/* Clean up */
xmlFreeNode(node);
xmlFreeDoc(doc);
}
static void
create_attr_node(void **state) {
xml_doc_private_t *docpriv = NULL;
xml_node_private_t *priv = NULL;
xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
xmlNodePtr node = xmlNewDocNode(doc, NULL, (pcmkXmlStr) "test", NULL);
xmlAttrPtr attr = xmlNewProp(node, (pcmkXmlStr) PCMK_XA_NAME,
(pcmkXmlStr) "dummy-value");
/* Adding a node to the document marks it as dirty */
docpriv = doc->_private;
assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
/* Double check things */
assert_non_null(attr);
assert_int_equal(attr->type, XML_ATTRIBUTE_NODE);
/* Check that the private data is initialized correctly */
priv = attr->_private;
assert_non_null(priv);
assert_int_equal(priv->check, XML_NODE_PRIVATE_MAGIC);
assert_true(pcmk_all_flags_set(priv->flags, pcmk__xf_dirty|pcmk__xf_created));
/* Clean up */
xmlFreeNode(node);
xmlFreeDoc(doc);
}
static void
create_comment_node(void **state) {
xml_doc_private_t *docpriv = NULL;
xml_node_private_t *priv = NULL;
xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
xmlNodePtr node = xmlNewDocComment(doc, (pcmkXmlStr) "blahblah");
/* Adding a node to the document marks it as dirty */
docpriv = doc->_private;
assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
/* Double check things */
assert_non_null(node);
assert_int_equal(node->type, XML_COMMENT_NODE);
/* Check that the private data is initialized correctly */
priv = node->_private;
assert_non_null(priv);
assert_int_equal(priv->check, XML_NODE_PRIVATE_MAGIC);
assert_true(pcmk_all_flags_set(priv->flags, pcmk__xf_dirty|pcmk__xf_created));
/* Clean up */
xmlFreeNode(node);
xmlFreeDoc(doc);
}
static void
create_text_node(void **state) {
xml_doc_private_t *docpriv = NULL;
xml_node_private_t *priv = NULL;
xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
xmlNodePtr node = xmlNewDocText(doc, (pcmkXmlStr) "blahblah");
/* Adding a node to the document marks it as dirty */
docpriv = doc->_private;
assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
/* Double check things */
assert_non_null(node);
assert_int_equal(node->type, XML_TEXT_NODE);
/* Check that no private data was created */
priv = node->_private;
assert_null(priv);
/* Clean up */
xmlFreeNode(node);
xmlFreeDoc(doc);
}
static void
create_dtd_node(void **state) {
xml_doc_private_t *docpriv = NULL;
xml_node_private_t *priv = NULL;
xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
xmlDtdPtr dtd = xmlNewDtd(doc, (pcmkXmlStr) PCMK_XA_NAME,
(pcmkXmlStr) "externalId",
(pcmkXmlStr) "systemId");
/* Adding a node to the document marks it as dirty */
docpriv = doc->_private;
assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
/* Double check things */
assert_non_null(dtd);
assert_int_equal(dtd->type, XML_DTD_NODE);
/* Check that no private data was created */
priv = dtd->_private;
assert_null(priv);
/* Clean up */
/* If you call xmlFreeDtd before xmlFreeDoc, you get a segfault */
xmlFreeDoc(doc);
}
static void
create_cdata_node(void **state) {
xml_doc_private_t *docpriv = NULL;
xml_node_private_t *priv = NULL;
xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
xmlNodePtr node = xmlNewCDataBlock(doc, (pcmkXmlStr) "blahblah", 8);
/* Adding a node to the document marks it as dirty */
docpriv = doc->_private;
assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
/* Double check things */
assert_non_null(node);
assert_int_equal(node->type, XML_CDATA_SECTION_NODE);
/* Check that no private data was created */
priv = node->_private;
assert_null(priv);
/* Clean up */
xmlFreeNode(node);
xmlFreeDoc(doc);
}
PCMK__UNIT_TEST(setup, teardown,
cmocka_unit_test(buffer_scheme_test),
cmocka_unit_test(create_document_node),
cmocka_unit_test(create_element_node),
cmocka_unit_test(create_attr_node),
cmocka_unit_test(create_comment_node),
cmocka_unit_test(create_text_node),
cmocka_unit_test(create_dtd_node),
cmocka_unit_test(create_cdata_node));
diff --git a/lib/common/unittest.c b/lib/common/unittest.c
index 0e63915f7b..f4578ba0c4 100644
--- a/lib/common/unittest.c
+++ b/lib/common/unittest.c
@@ -1,128 +1,128 @@
/*
* Copyright 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 <crm_internal.h>
#include <crm/common/unittest_internal.h>
#include <stdlib.h>
#include <unistd.h>
// 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);
}
int
pcmk__xml_test_setup_group(void **state)
{
/* This needs to be run before we run unit tests that manipulate XML.
* Without it, document private data won't get created, which can cause
* segmentation faults or assertions in functions related to change
* tracking and ACLs. There's no harm in doing this before all tests.
*/
- crm_xml_init();
+ pcmk__xml_init();
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");
}
// LCOV_EXCL_STOP
diff --git a/lib/common/xml.c b/lib/common/xml.c
index 5e0fb355f0..58ed990dd1 100644
--- a/lib/common/xml.c
+++ b/lib/common/xml.c
@@ -1,2723 +1,2740 @@
/*
* 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 <crm_internal.h>
#include <stdarg.h>
#include <stdint.h> // uint32_t
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h> // stat(), S_ISREG, etc.
#include <sys/types.h>
#include <libxml/parser.h>
#include <libxml/tree.h>
#include <crm/crm.h>
#include <crm/common/xml.h>
#include <crm/common/xml_internal.h> // PCMK__XML_LOG_BASE, etc.
#include "crmcommon_private.h"
/*!
* \internal
* \brief Apply a function to each XML node in a tree (pre-order, depth-first)
*
* \param[in,out] xml XML tree to traverse
* \param[in,out] fn Function to call for each node (returns \c true to
* continue traversing the tree or \c false to stop)
* \param[in,out] user_data Argument to \p fn
*
* \return \c false if any \p fn call returned \c false, or \c true otherwise
*
* \note This function is recursive.
*/
bool
pcmk__xml_tree_foreach(xmlNode *xml, bool (*fn)(xmlNode *, void *),
void *user_data)
{
if (!fn(xml, user_data)) {
return false;
}
for (xml = pcmk__xml_first_child(xml); xml != NULL;
xml = pcmk__xml_next(xml)) {
if (!pcmk__xml_tree_foreach(xml, fn, user_data)) {
return false;
}
}
return true;
}
bool
pcmk__tracking_xml_changes(xmlNode *xml, bool lazy)
{
if(xml == NULL || xml->doc == NULL || xml->doc->_private == NULL) {
return FALSE;
} else if (!pcmk_is_set(((xml_doc_private_t *)xml->doc->_private)->flags,
pcmk__xf_tracking)) {
return FALSE;
} else if (lazy && !pcmk_is_set(((xml_doc_private_t *)xml->doc->_private)->flags,
pcmk__xf_lazy)) {
return FALSE;
}
return TRUE;
}
static inline void
set_parent_flag(xmlNode *xml, long flag)
{
for(; xml; xml = xml->parent) {
xml_node_private_t *nodepriv = xml->_private;
if (nodepriv == NULL) {
/* During calls to xmlDocCopyNode(), _private will be unset for parent nodes */
} else {
pcmk__set_xml_flags(nodepriv, flag);
}
}
}
void
pcmk__set_xml_doc_flag(xmlNode *xml, enum xml_private_flags flag)
{
if(xml && xml->doc && xml->doc->_private){
/* During calls to xmlDocCopyNode(), xml->doc may be unset */
xml_doc_private_t *docpriv = xml->doc->_private;
pcmk__set_xml_flags(docpriv, flag);
}
}
// Mark document, element, and all element's parents as changed
void
pcmk__mark_xml_node_dirty(xmlNode *xml)
{
pcmk__set_xml_doc_flag(xml, pcmk__xf_dirty);
set_parent_flag(xml, pcmk__xf_dirty);
}
/*!
* \internal
* \brief Clear flags on an XML node
*
* \param[in,out] xml XML node whose flags to reset
* \param[in,out] user_data Ignored
*
* \return \c true (to continue traversing the tree)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
static bool
reset_xml_node_flags(xmlNode *xml, void *user_data)
{
xml_node_private_t *nodepriv = xml->_private;
if (nodepriv != NULL) {
nodepriv->flags = pcmk__xf_none;
}
return true;
}
/*!
* \internal
* \brief Set the \c pcmk__xf_dirty and \c pcmk__xf_created flags on an XML node
*
* \param[in,out] xml Node whose flags to set
* \param[in] user_data Ignored
*
* \return \c true (to continue traversing the tree)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
static bool
mark_xml_dirty_created(xmlNode *xml, void *user_data)
{
xml_node_private_t *nodepriv = xml->_private;
if (nodepriv != NULL) {
pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created);
}
return true;
}
/*!
* \internal
* \brief Mark an XML tree as dirty and created, and mark its parents dirty
*
* Also mark the document dirty.
*
* \param[in,out] xml Tree to mark as dirty and created
*/
void
pcmk__xml_mark_created(xmlNode *xml)
{
CRM_ASSERT(xml != NULL);
if (!pcmk__tracking_xml_changes(xml, false)) {
// Tracking is disabled for entire document
return;
}
// Mark all parents and document dirty
pcmk__mark_xml_node_dirty(xml);
pcmk__xml_tree_foreach(xml, mark_xml_dirty_created, NULL);
}
#define XML_DOC_PRIVATE_MAGIC 0x81726354UL
#define XML_NODE_PRIVATE_MAGIC 0x54637281UL
// Free an XML object previously marked as deleted
static void
free_deleted_object(void *data)
{
if(data) {
pcmk__deleted_xml_t *deleted_obj = data;
g_free(deleted_obj->path);
free(deleted_obj);
}
}
// Free and NULL user, ACLs, and deleted objects in an XML node's private data
static void
reset_xml_private_data(xml_doc_private_t *docpriv)
{
if (docpriv != NULL) {
CRM_ASSERT(docpriv->check == XML_DOC_PRIVATE_MAGIC);
free(docpriv->user);
docpriv->user = NULL;
if (docpriv->acls != NULL) {
pcmk__free_acls(docpriv->acls);
docpriv->acls = NULL;
}
if(docpriv->deleted_objs) {
g_list_free_full(docpriv->deleted_objs, free_deleted_object);
docpriv->deleted_objs = NULL;
}
}
}
// Free all private data associated with an XML node
static void
free_private_data(xmlNode *node)
{
/* Note:
This function frees private data assosciated with an XML node,
unless the function is being called as a result of internal
XSLT cleanup.
That could happen through, for example, the following chain of
function calls:
xsltApplyStylesheetInternal
-> xsltFreeTransformContext
-> xsltFreeRVTs
-> xmlFreeDoc
And in that case, the node would fulfill three conditions:
1. It would be a standalone document (i.e. it wouldn't be
part of a document)
2. It would have a space-prefixed name (for reference, please
see xsltInternals.h: XSLT_MARK_RES_TREE_FRAG)
3. It would carry its own payload in the _private field.
We do not free data in this circumstance to avoid a failed
assertion on the XML_*_PRIVATE_MAGIC later.
*/
if (node->name == NULL || node->name[0] != ' ') {
if (node->_private) {
if (node->type == XML_DOCUMENT_NODE) {
reset_xml_private_data(node->_private);
} else {
CRM_ASSERT(((xml_node_private_t *) node->_private)->check
== XML_NODE_PRIVATE_MAGIC);
/* nothing dynamically allocated nested */
}
free(node->_private);
node->_private = NULL;
}
}
}
// Allocate and initialize private data for an XML node
static void
new_private_data(xmlNode *node)
{
switch (node->type) {
case XML_DOCUMENT_NODE: {
xml_doc_private_t *docpriv =
pcmk__assert_alloc(1, sizeof(xml_doc_private_t));
docpriv->check = XML_DOC_PRIVATE_MAGIC;
/* Flags will be reset if necessary when tracking is enabled */
pcmk__set_xml_flags(docpriv, pcmk__xf_dirty|pcmk__xf_created);
node->_private = docpriv;
break;
}
case XML_ELEMENT_NODE:
case XML_ATTRIBUTE_NODE:
case XML_COMMENT_NODE: {
xml_node_private_t *nodepriv =
pcmk__assert_alloc(1, sizeof(xml_node_private_t));
nodepriv->check = XML_NODE_PRIVATE_MAGIC;
/* Flags will be reset if necessary when tracking is enabled */
pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created);
node->_private = nodepriv;
if (pcmk__tracking_xml_changes(node, FALSE)) {
/* XML_ELEMENT_NODE doesn't get picked up here, node->doc is
* not hooked up at the point we are called
*/
pcmk__mark_xml_node_dirty(node);
}
break;
}
case XML_TEXT_NODE:
case XML_DTD_NODE:
case XML_CDATA_SECTION_NODE:
break;
default:
/* Ignore */
crm_trace("Ignoring %p %d", node, node->type);
CRM_LOG_ASSERT(node->type == XML_ELEMENT_NODE);
break;
}
}
void
xml_track_changes(xmlNode * xml, const char *user, xmlNode *acl_source, bool enforce_acls)
{
xml_accept_changes(xml);
crm_trace("Tracking changes%s to %p", enforce_acls?" with ACLs":"", xml);
pcmk__set_xml_doc_flag(xml, pcmk__xf_tracking);
if(enforce_acls) {
if(acl_source == NULL) {
acl_source = xml;
}
pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_enabled);
pcmk__unpack_acl(acl_source, xml, user);
pcmk__apply_acl(xml);
}
}
bool xml_tracking_changes(xmlNode * xml)
{
return (xml != NULL) && (xml->doc != NULL) && (xml->doc->_private != NULL)
&& pcmk_is_set(((xml_doc_private_t *)(xml->doc->_private))->flags,
pcmk__xf_tracking);
}
bool xml_document_dirty(xmlNode *xml)
{
return (xml != NULL) && (xml->doc != NULL) && (xml->doc->_private != NULL)
&& pcmk_is_set(((xml_doc_private_t *)(xml->doc->_private))->flags,
pcmk__xf_dirty);
}
/*!
* \internal
* \brief Return ordinal position of an XML node among its siblings
*
* \param[in] xml XML node to check
* \param[in] ignore_if_set Don't count siblings with this flag set
*
* \return Ordinal position of \p xml (starting with 0)
*/
int
pcmk__xml_position(const xmlNode *xml, enum xml_private_flags ignore_if_set)
{
int position = 0;
for (const xmlNode *cIter = xml; cIter->prev; cIter = cIter->prev) {
xml_node_private_t *nodepriv = ((xmlNode*)cIter->prev)->_private;
if (!pcmk_is_set(nodepriv->flags, ignore_if_set)) {
position++;
}
}
return position;
}
/*!
* \internal
* \brief Remove all attributes marked as deleted from an XML node
*
* \param[in,out] xml XML node whose deleted attributes to remove
* \param[in,out] user_data Ignored
*
* \return \c true (to continue traversing the tree)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
static bool
accept_attr_deletions(xmlNode *xml, void *user_data)
{
reset_xml_node_flags(xml, NULL);
pcmk__xe_remove_matching_attrs(xml, pcmk__marked_as_deleted, NULL);
return true;
}
/*!
* \internal
* \brief Find first child XML node matching another given XML node
*
* \param[in] haystack XML whose children should be checked
* \param[in] needle XML to match (comment content or element name and ID)
* \param[in] exact If true and needle is a comment, position must match
*/
xmlNode *
pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle, bool exact)
{
CRM_CHECK(needle != NULL, return NULL);
if (needle->type == XML_COMMENT_NODE) {
return pcmk__xc_match(haystack, needle, exact);
} else {
const char *id = pcmk__xe_id(needle);
const char *attr = (id == NULL)? NULL : PCMK_XA_ID;
return pcmk__xe_first_child(haystack, (const char *) needle->name, attr,
id);
}
}
void
xml_accept_changes(xmlNode * xml)
{
xmlNode *top = NULL;
xml_doc_private_t *docpriv = NULL;
if(xml == NULL) {
return;
}
crm_trace("Accepting changes to %p", xml);
docpriv = xml->doc->_private;
top = xmlDocGetRootElement(xml->doc);
reset_xml_private_data(xml->doc->_private);
if (!pcmk_is_set(docpriv->flags, pcmk__xf_dirty)) {
docpriv->flags = pcmk__xf_none;
return;
}
docpriv->flags = pcmk__xf_none;
pcmk__xml_tree_foreach(top, accept_attr_deletions, NULL);
}
/*!
* \internal
* \brief Find first XML child element matching given criteria
*
* \param[in] parent XML element to search (can be \c NULL)
* \param[in] node_name If not \c NULL, only match children of this type
* \param[in] attr_n If not \c NULL, only match children with an attribute
* of this name.
* \param[in] attr_v If \p attr_n and this are not NULL, only match children
* with an attribute named \p attr_n and this value
*
* \return Matching XML child element, or \c NULL if none found
*/
xmlNode *
pcmk__xe_first_child(const xmlNode *parent, const char *node_name,
const char *attr_n, const char *attr_v)
{
xmlNode *child = NULL;
const char *parent_name = "<null>";
CRM_CHECK((attr_v == NULL) || (attr_n != NULL), return NULL);
if (parent != NULL) {
child = parent->children;
while ((child != NULL) && (child->type != XML_ELEMENT_NODE)) {
child = child->next;
}
parent_name = (const char *) parent->name;
}
for (; child != NULL; child = pcmk__xe_next(child)) {
const char *value = NULL;
if ((node_name != NULL) && !pcmk__xe_is(child, node_name)) {
// Node name mismatch
continue;
}
if (attr_n == NULL) {
// No attribute match needed
return child;
}
value = crm_element_value(child, attr_n);
if ((attr_v == NULL) && (value != NULL)) {
// attr_v == NULL: Attribute attr_n must be set (to any value)
return child;
}
if ((attr_v != NULL) && (pcmk__str_eq(value, attr_v, pcmk__str_none))) {
// attr_v != NULL: Attribute attr_n must be set to value attr_v
return child;
}
}
if (node_name == NULL) {
node_name = "(any)"; // For logging
}
if (attr_n != NULL) {
crm_trace("XML child node <%s %s=%s> not found in %s",
node_name, attr_n, attr_v, parent_name);
} else {
crm_trace("XML child node <%s> not found in %s",
node_name, parent_name);
}
return NULL;
}
/*!
* \internal
* \brief Set an XML attribute, expanding \c ++ and \c += where appropriate
*
* If \p target already has an attribute named \p name set to an integer value
* and \p value is an addition assignment expression on \p name, then expand
* \p value to an integer and set attribute \p name to the expanded value in
* \p target.
*
* Otherwise, set attribute \p name on \p target using the literal \p value.
*
* The original attribute value in \p target and the number in an assignment
* expression in \p value are parsed and added as scores (that is, their values
* are capped at \c INFINITY and \c -INFINITY). For more details, refer to
* \c char2score().
*
* For example, suppose \p target has an attribute named \c "X" with value
* \c "5", and that \p name is \c "X".
* * If \p value is \c "X++", the new value of \c "X" in \p target is \c "6".
* * If \p value is \c "X+=3", the new value of \c "X" in \p target is \c "8".
* * If \p value is \c "val", the new value of \c "X" in \p target is \c "val".
* * If \p value is \c "Y++", the new value of \c "X" in \p target is \c "Y++".
*
* \param[in,out] target XML node whose attribute to set
* \param[in] name Name of the attribute to set
* \param[in] value New value of attribute to set
*
* \return Standard Pacemaker return code (specifically, \c EINVAL on invalid
* argument, or \c pcmk_rc_ok otherwise)
*/
int
pcmk__xe_set_score(xmlNode *target, const char *name, const char *value)
{
const char *old_value = NULL;
CRM_CHECK((target != NULL) && (name != NULL), return EINVAL);
if (value == NULL) {
return pcmk_rc_ok;
}
old_value = crm_element_value(target, name);
// If no previous value, skip to default case and set the value unexpanded.
if (old_value != NULL) {
const char *n = name;
const char *v = value;
// Stop at first character that differs between name and value
for (; (*n == *v) && (*n != '\0'); n++, v++);
// If value begins with name followed by a "++" or "+="
if ((*n == '\0')
&& (*v++ == '+')
&& ((*v == '+') || (*v == '='))) {
// If we're expanding ourselves, no previous value was set; use 0
int old_value_i = (old_value != value)? char2score(old_value) : 0;
/* value="X++": new value of X is old_value + 1
* value="X+=Y": new value of X is old_value + Y (for some number Y)
*/
int add = (*v == '+')? 1 : char2score(++v);
crm_xml_add_int(target, name, pcmk__add_scores(old_value_i, add));
return pcmk_rc_ok;
}
}
// Default case: set the attribute unexpanded (with value treated literally)
if (old_value != value) {
crm_xml_add(target, name, value);
}
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Copy XML attributes from a source element to a target element
*
* This is similar to \c xmlCopyPropList() except that attributes are marked
* as dirty for change tracking purposes.
*
* \param[in,out] target XML element to receive copied attributes from \p src
* \param[in] src XML element whose attributes to copy to \p target
* \param[in] flags Group of <tt>enum pcmk__xa_flags</tt>
*
* \return Standard Pacemaker return code
*/
int
pcmk__xe_copy_attrs(xmlNode *target, const xmlNode *src, uint32_t flags)
{
CRM_CHECK((src != NULL) && (target != NULL), return EINVAL);
for (xmlAttr *attr = pcmk__xe_first_attr(src); attr != NULL;
attr = attr->next) {
const char *name = (const char *) attr->name;
const char *value = pcmk__xml_attr_value(attr);
if (pcmk_is_set(flags, pcmk__xaf_no_overwrite)
&& (crm_element_value(target, name) != NULL)) {
continue;
}
if (pcmk_is_set(flags, pcmk__xaf_score_update)) {
pcmk__xe_set_score(target, name, value);
} else {
crm_xml_add(target, name, value);
}
}
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Remove an XML attribute from an element
*
* \param[in,out] element XML element that owns \p attr
* \param[in,out] attr XML attribute to remove from \p element
*
* \return Standard Pacemaker return code (\c EPERM if ACLs prevent removal of
* attributes from \p element, or \c pcmk_rc_ok otherwise)
*/
static int
remove_xe_attr(xmlNode *element, xmlAttr *attr)
{
if (attr == NULL) {
return pcmk_rc_ok;
}
if (!pcmk__check_acl(element, NULL, pcmk__xf_acl_write)) {
// ACLs apply to element, not to particular attributes
crm_trace("ACLs prevent removal of attributes from %s element",
(const char *) element->name);
return EPERM;
}
if (pcmk__tracking_xml_changes(element, false)) {
// Leave in place (marked for removal) until after diff is calculated
set_parent_flag(element, pcmk__xf_dirty);
pcmk__set_xml_flags((xml_node_private_t *) attr->_private,
pcmk__xf_deleted);
} else {
xmlRemoveProp(attr);
}
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Remove a named attribute from an XML element
*
* \param[in,out] element XML element to remove an attribute from
* \param[in] name Name of attribute to remove
*/
void
pcmk__xe_remove_attr(xmlNode *element, const char *name)
{
if (name != NULL) {
remove_xe_attr(element, xmlHasProp(element, (pcmkXmlStr) name));
}
}
/*!
* \internal
* \brief Remove a named attribute from an XML element
*
* This is a wrapper for \c pcmk__xe_remove_attr() for use with
* \c pcmk__xml_tree_foreach().
*
* \param[in,out] xml XML element to remove an attribute from
* \param[in] user_data Name of attribute to remove
*
* \return \c true (to continue traversing the tree)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
bool
pcmk__xe_remove_attr_cb(xmlNode *xml, void *user_data)
{
const char *name = user_data;
pcmk__xe_remove_attr(xml, name);
return true;
}
/*!
* \internal
* \brief Remove an XML element's attributes that match some criteria
*
* \param[in,out] element XML element to modify
* \param[in] match If not NULL, only remove attributes for which
* this function returns true
* \param[in,out] user_data Data to pass to \p match
*/
void
pcmk__xe_remove_matching_attrs(xmlNode *element,
bool (*match)(xmlAttrPtr, void *),
void *user_data)
{
xmlAttrPtr next = NULL;
for (xmlAttrPtr a = pcmk__xe_first_attr(element); a != NULL; a = next) {
next = a->next; // Grab now because attribute might get removed
if ((match == NULL) || match(a, user_data)) {
if (remove_xe_attr(element, a) != pcmk_rc_ok) {
return;
}
}
}
}
/*!
* \internal
* \brief Create a new XML element under a given parent
*
* \param[in,out] parent XML element that will be the new element's parent
* (\c NULL to create a new XML document with the new
* node as root)
* \param[in] name Name of new element
*
* \return Newly created XML element (guaranteed not to be \c NULL)
*/
xmlNode *
pcmk__xe_create(xmlNode *parent, const char *name)
{
xmlNode *node = NULL;
CRM_ASSERT(!pcmk__str_empty(name));
if (parent == NULL) {
xmlDoc *doc = xmlNewDoc(PCMK__XML_VERSION);
pcmk__mem_assert(doc);
node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL);
pcmk__mem_assert(node);
xmlDocSetRootElement(doc, node);
} else {
node = xmlNewChild(parent, NULL, (pcmkXmlStr) name, NULL);
pcmk__mem_assert(node);
}
pcmk__xml_mark_created(node);
return node;
}
/*!
* \internal
* \brief Set a formatted string as an XML node's content
*
* \param[in,out] node Node whose content to set
* \param[in] format <tt>printf(3)</tt>-style format string
* \param[in] ... Arguments for \p format
*
* \note This function escapes special characters. \c xmlNodeSetContent() does
* not.
*/
G_GNUC_PRINTF(2, 3)
void
pcmk__xe_set_content(xmlNode *node, const char *format, ...)
{
if (node != NULL) {
const char *content = NULL;
char *buf = NULL;
if (strchr(format, '%') == NULL) {
// Nothing to format
content = format;
} else {
va_list ap;
va_start(ap, format);
if (pcmk__str_eq(format, "%s", pcmk__str_none)) {
// No need to make a copy
content = va_arg(ap, const char *);
} else {
CRM_ASSERT(vasprintf(&buf, format, ap) >= 0);
content = buf;
}
va_end(ap);
}
xmlNodeSetContent(node, (pcmkXmlStr) content);
free(buf);
}
}
/*!
* Free an XML element and all of its children, removing it from its parent
*
* \param[in,out] xml XML element to free
*/
void
pcmk_free_xml_subtree(xmlNode *xml)
{
xmlUnlinkNode(xml); // Detaches from parent and siblings
xmlFreeNode(xml); // Frees
}
static void
free_xml_with_position(xmlNode *child, int position)
{
xmlDoc *doc = NULL;
xml_node_private_t *nodepriv = NULL;
if (child == NULL) {
return;
}
doc = child->doc;
nodepriv = child->_private;
if ((doc != NULL) && (xmlDocGetRootElement(doc) == child)) {
// Free everything
xmlFreeDoc(doc);
return;
}
if (!pcmk__check_acl(child, NULL, pcmk__xf_acl_write)) {
GString *xpath = NULL;
pcmk__if_tracing({}, return);
xpath = pcmk__element_xpath(child);
qb_log_from_external_source(__func__, __FILE__,
"Cannot remove %s %x", LOG_TRACE,
__LINE__, 0, xpath->str, nodepriv->flags);
g_string_free(xpath, TRUE);
return;
}
if ((doc != NULL) && pcmk__tracking_xml_changes(child, false)
&& !pcmk_is_set(nodepriv->flags, pcmk__xf_created)) {
xml_doc_private_t *docpriv = doc->_private;
GString *xpath = pcmk__element_xpath(child);
if (xpath != NULL) {
pcmk__deleted_xml_t *deleted_obj = NULL;
crm_trace("Deleting %s %p from %p", xpath->str, child, 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 (child->type == XML_COMMENT_NODE) {
if (position >= 0) {
deleted_obj->position = position;
} else {
deleted_obj->position = pcmk__xml_position(child,
pcmk__xf_skip);
}
}
docpriv->deleted_objs = g_list_append(docpriv->deleted_objs,
deleted_obj);
pcmk__set_xml_doc_flag(child, pcmk__xf_dirty);
}
}
pcmk_free_xml_subtree(child);
}
void
free_xml(xmlNode * child)
{
free_xml_with_position(child, -1);
}
/*!
* \internal
* \brief Make a deep copy of an XML node under a given parent
*
* \param[in,out] parent XML element that will be the copy's parent (\c NULL
* to create a new XML document with the copy as root)
* \param[in] src XML node to copy
*
* \return Deep copy of \p src, or \c NULL if \p src is \c NULL
*/
xmlNode *
pcmk__xml_copy(xmlNode *parent, xmlNode *src)
{
xmlNode *copy = NULL;
if (src == NULL) {
return NULL;
}
if (parent == NULL) {
xmlDoc *doc = NULL;
// The copy will be the root element of a new document
CRM_ASSERT(src->type == XML_ELEMENT_NODE);
doc = xmlNewDoc(PCMK__XML_VERSION);
pcmk__mem_assert(doc);
copy = xmlDocCopyNode(src, doc, 1);
pcmk__mem_assert(copy);
xmlDocSetRootElement(doc, copy);
} else {
copy = xmlDocCopyNode(src, parent->doc, 1);
pcmk__mem_assert(copy);
xmlAddChild(parent, copy);
}
pcmk__xml_mark_created(copy);
return copy;
}
/*!
* \internal
* \brief Remove XML text nodes from specified XML and all its children
*
* \param[in,out] xml XML to strip text from
*/
void
pcmk__strip_xml_text(xmlNode *xml)
{
xmlNode *iter = xml->children;
while (iter) {
xmlNode *next = iter->next;
switch (iter->type) {
case XML_TEXT_NODE:
/* Remove it */
pcmk_free_xml_subtree(iter);
break;
case XML_ELEMENT_NODE:
/* Search it */
pcmk__strip_xml_text(iter);
break;
default:
/* Leave it */
break;
}
iter = next;
}
}
/*!
* \internal
* \brief Add a "last written" attribute to an XML element, set to current time
*
* \param[in,out] xe XML element to add attribute to
*
* \return Value that was set, or NULL on error
*/
const char *
pcmk__xe_add_last_written(xmlNode *xe)
{
char *now_s = pcmk__epoch2str(NULL, 0);
const char *result = NULL;
result = crm_xml_add(xe, PCMK_XA_CIB_LAST_WRITTEN,
pcmk__s(now_s, "Could not determine current time"));
free(now_s);
return result;
}
/*!
* \brief Sanitize a string so it is usable as an XML ID
*
* \param[in,out] id String to sanitize
*/
void
crm_xml_sanitize_id(char *id)
{
char *c;
for (c = id; *c; ++c) {
/* @TODO Sanitize more comprehensively */
switch (*c) {
case ':':
case '#':
*c = '.';
}
}
}
/*!
* \brief Set the ID of an XML element using a format
*
* \param[in,out] xml XML element
* \param[in] fmt printf-style format
* \param[in] ... any arguments required by format
*/
void
crm_xml_set_id(xmlNode *xml, const char *format, ...)
{
va_list ap;
int len = 0;
char *id = NULL;
/* equivalent to crm_strdup_printf() */
va_start(ap, format);
len = vasprintf(&id, format, ap);
va_end(ap);
CRM_ASSERT(len > 0);
crm_xml_sanitize_id(id);
crm_xml_add(xml, PCMK_XA_ID, id);
free(id);
}
/*!
* \internal
* \brief Check whether a string has XML special characters that must be escaped
*
* See \c pcmk__xml_escape() and \c pcmk__xml_escape_type for more details.
*
* \param[in] text String to check
* \param[in] type Type of escaping
*
* \return \c true if \p text has special characters that need to be escaped, or
* \c false otherwise
*/
bool
pcmk__xml_needs_escape(const char *text, enum pcmk__xml_escape_type type)
{
if (text == NULL) {
return false;
}
while (*text != '\0') {
switch (type) {
case pcmk__xml_escape_text:
switch (*text) {
case '<':
case '>':
case '&':
return true;
case '\n':
case '\t':
break;
default:
if (g_ascii_iscntrl(*text)) {
return true;
}
break;
}
break;
case pcmk__xml_escape_attr:
switch (*text) {
case '<':
case '>':
case '&':
case '"':
return true;
default:
if (g_ascii_iscntrl(*text)) {
return true;
}
break;
}
break;
case pcmk__xml_escape_attr_pretty:
switch (*text) {
case '\n':
case '\r':
case '\t':
case '"':
return true;
default:
break;
}
break;
default: // Invalid enum value
CRM_ASSERT(false);
break;
}
text = g_utf8_next_char(text);
}
return false;
}
/*!
* \internal
* \brief Replace special characters with their XML escape sequences
*
* \param[in] text Text to escape
* \param[in] type Type of escaping
*
* \return Newly allocated string equivalent to \p text but with special
* characters replaced with XML escape sequences (or \c NULL if \p text
* is \c NULL). If \p text is not \c NULL, the return value is
* guaranteed not to be \c NULL.
*
* \note There are libxml functions that purport to do this:
* \c xmlEncodeEntitiesReentrant() and \c xmlEncodeSpecialChars().
* However, their escaping is incomplete. See:
* https://discourse.gnome.org/t/intended-use-of-xmlencodeentitiesreentrant-vs-xmlencodespecialchars/19252
* \note The caller is responsible for freeing the return value using
* \c g_free().
*/
gchar *
pcmk__xml_escape(const char *text, enum pcmk__xml_escape_type type)
{
GString *copy = NULL;
if (text == NULL) {
return NULL;
}
copy = g_string_sized_new(strlen(text));
while (*text != '\0') {
// Don't escape any non-ASCII characters
if ((*text & 0x80) != 0) {
size_t bytes = g_utf8_next_char(text) - text;
g_string_append_len(copy, text, bytes);
text += bytes;
continue;
}
switch (type) {
case pcmk__xml_escape_text:
switch (*text) {
case '<':
g_string_append(copy, PCMK__XML_ENTITY_LT);
break;
case '>':
g_string_append(copy, PCMK__XML_ENTITY_GT);
break;
case '&':
g_string_append(copy, PCMK__XML_ENTITY_AMP);
break;
case '\n':
case '\t':
g_string_append_c(copy, *text);
break;
default:
if (g_ascii_iscntrl(*text)) {
g_string_append_printf(copy, "&#x%.2X;", *text);
} else {
g_string_append_c(copy, *text);
}
break;
}
break;
case pcmk__xml_escape_attr:
switch (*text) {
case '<':
g_string_append(copy, PCMK__XML_ENTITY_LT);
break;
case '>':
g_string_append(copy, PCMK__XML_ENTITY_GT);
break;
case '&':
g_string_append(copy, PCMK__XML_ENTITY_AMP);
break;
case '"':
g_string_append(copy, PCMK__XML_ENTITY_QUOT);
break;
default:
if (g_ascii_iscntrl(*text)) {
g_string_append_printf(copy, "&#x%.2X;", *text);
} else {
g_string_append_c(copy, *text);
}
break;
}
break;
case pcmk__xml_escape_attr_pretty:
switch (*text) {
case '"':
g_string_append(copy, "\\\"");
break;
case '\n':
g_string_append(copy, "\\n");
break;
case '\r':
g_string_append(copy, "\\r");
break;
case '\t':
g_string_append(copy, "\\t");
break;
default:
g_string_append_c(copy, *text);
break;
}
break;
default: // Invalid enum value
CRM_ASSERT(false);
break;
}
text = g_utf8_next_char(text);
}
return g_string_free(copy, FALSE);
}
/*!
* \internal
* \brief Set a flag on all attributes of an XML element
*
* \param[in,out] xml XML node to set flags on
* \param[in] flag XML private flag to set
*/
static void
set_attrs_flag(xmlNode *xml, enum xml_private_flags flag)
{
for (xmlAttr *attr = pcmk__xe_first_attr(xml); attr; attr = attr->next) {
pcmk__set_xml_flags((xml_node_private_t *) (attr->_private), flag);
}
}
/*!
* \internal
* \brief Add an XML attribute to a node, marked as deleted
*
* When calculating XML changes, we need to know when an attribute has been
* deleted. Add the attribute back to the new XML, so that we can check the
* removal against ACLs, and mark it as deleted for later removal after
* differences have been calculated.
*
* \param[in,out] new_xml XML to modify
* \param[in] element Name of XML element that changed (for logging)
* \param[in] attr_name Name of attribute that was deleted
* \param[in] old_value Value of attribute that was deleted
*/
static void
mark_attr_deleted(xmlNode *new_xml, const char *element, const char *attr_name,
const char *old_value)
{
xml_doc_private_t *docpriv = new_xml->doc->_private;
xmlAttr *attr = NULL;
xml_node_private_t *nodepriv;
// Prevent the dirty flag being set recursively upwards
pcmk__clear_xml_flags(docpriv, pcmk__xf_tracking);
// Restore the old value (and the tracking flag)
attr = xmlSetProp(new_xml, (pcmkXmlStr) attr_name, (pcmkXmlStr) old_value);
pcmk__set_xml_flags(docpriv, pcmk__xf_tracking);
// Reset flags (so the attribute doesn't appear as newly created)
nodepriv = attr->_private;
nodepriv->flags = 0;
// Check ACLs and mark restored value for later removal
remove_xe_attr(new_xml, attr);
crm_trace("XML attribute %s=%s was removed from %s",
attr_name, old_value, element);
}
/*
* \internal
* \brief Check ACLs for a changed XML attribute
*/
static void
mark_attr_changed(xmlNode *new_xml, const char *element, const char *attr_name,
const char *old_value)
{
char *vcopy = crm_element_value_copy(new_xml, attr_name);
crm_trace("XML attribute %s was changed from '%s' to '%s' in %s",
attr_name, old_value, vcopy, element);
// Restore the original value
xmlSetProp(new_xml, (pcmkXmlStr) attr_name, (pcmkXmlStr) old_value);
// Change it back to the new value, to check ACLs
crm_xml_add(new_xml, attr_name, vcopy);
free(vcopy);
}
/*!
* \internal
* \brief Mark an XML attribute as having changed position
*
* \param[in,out] new_xml XML to modify
* \param[in] element Name of XML element that changed (for logging)
* \param[in,out] old_attr Attribute that moved, in original XML
* \param[in,out] new_attr Attribute that moved, in \p new_xml
* \param[in] p_old Ordinal position of \p old_attr in original XML
* \param[in] p_new Ordinal position of \p new_attr in \p new_xml
*/
static void
mark_attr_moved(xmlNode *new_xml, const char *element, xmlAttr *old_attr,
xmlAttr *new_attr, int p_old, int p_new)
{
xml_node_private_t *nodepriv = new_attr->_private;
crm_trace("XML attribute %s moved from position %d to %d in %s",
old_attr->name, p_old, p_new, element);
// Mark document, element, and all element's parents as changed
pcmk__mark_xml_node_dirty(new_xml);
// Mark attribute as changed
pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_moved);
nodepriv = (p_old > p_new)? old_attr->_private : new_attr->_private;
pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
}
/*!
* \internal
* \brief Calculate differences in all previously existing XML attributes
*
* \param[in,out] old_xml Original XML to compare
* \param[in,out] new_xml New XML to compare
*/
static void
xml_diff_old_attrs(xmlNode *old_xml, xmlNode *new_xml)
{
xmlAttr *attr_iter = pcmk__xe_first_attr(old_xml);
while (attr_iter != NULL) {
const char *name = (const char *) attr_iter->name;
xmlAttr *old_attr = attr_iter;
xmlAttr *new_attr = xmlHasProp(new_xml, attr_iter->name);
const char *old_value = pcmk__xml_attr_value(attr_iter);
attr_iter = attr_iter->next;
if (new_attr == NULL) {
mark_attr_deleted(new_xml, (const char *) old_xml->name, name,
old_value);
} else {
xml_node_private_t *nodepriv = new_attr->_private;
int new_pos = pcmk__xml_position((xmlNode*) new_attr,
pcmk__xf_skip);
int old_pos = pcmk__xml_position((xmlNode*) old_attr,
pcmk__xf_skip);
const char *new_value = crm_element_value(new_xml, name);
// This attribute isn't new
pcmk__clear_xml_flags(nodepriv, pcmk__xf_created);
if (strcmp(new_value, old_value) != 0) {
mark_attr_changed(new_xml, (const char *) old_xml->name, name,
old_value);
} else if ((old_pos != new_pos)
&& !pcmk__tracking_xml_changes(new_xml, TRUE)) {
mark_attr_moved(new_xml, (const char *) old_xml->name,
old_attr, new_attr, old_pos, new_pos);
}
}
}
}
/*!
* \internal
* \brief Check all attributes in new XML for creation
*
* For each of a given XML element's attributes marked as newly created, accept
* (and mark as dirty) or reject the creation according to ACLs.
*
* \param[in,out] new_xml XML to check
*/
static void
mark_created_attrs(xmlNode *new_xml)
{
xmlAttr *attr_iter = pcmk__xe_first_attr(new_xml);
while (attr_iter != NULL) {
xmlAttr *new_attr = attr_iter;
xml_node_private_t *nodepriv = attr_iter->_private;
attr_iter = attr_iter->next;
if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) {
const char *attr_name = (const char *) new_attr->name;
crm_trace("Created new attribute %s=%s in %s",
attr_name, pcmk__xml_attr_value(new_attr),
new_xml->name);
/* Check ACLs (we can't use the remove-then-create trick because it
* would modify the attribute position).
*/
if (pcmk__check_acl(new_xml, attr_name, pcmk__xf_acl_write)) {
pcmk__mark_xml_attr_dirty(new_attr);
} else {
// Creation was not allowed, so remove the attribute
xmlUnsetProp(new_xml, new_attr->name);
}
}
}
}
/*!
* \internal
* \brief Calculate differences in attributes between two XML nodes
*
* \param[in,out] old_xml Original XML to compare
* \param[in,out] new_xml New XML to compare
*/
static void
xml_diff_attrs(xmlNode *old_xml, xmlNode *new_xml)
{
set_attrs_flag(new_xml, pcmk__xf_created); // cleared later if not really new
xml_diff_old_attrs(old_xml, new_xml);
mark_created_attrs(new_xml);
}
/*!
* \internal
* \brief Add an XML child element to a node, marked as deleted
*
* When calculating XML changes, we need to know when a child element has been
* deleted. Add the child back to the new XML, so that we can check the removal
* against ACLs, and mark it as deleted for later removal after differences have
* been calculated.
*
* \param[in,out] old_child Child element from original XML
* \param[in,out] new_parent New XML to add marked copy to
*/
static void
mark_child_deleted(xmlNode *old_child, xmlNode *new_parent)
{
// Re-create the child element so we can check ACLs
xmlNode *candidate = pcmk__xml_copy(new_parent, old_child);
// Clear flags on new child and its children
pcmk__xml_tree_foreach(candidate, reset_xml_node_flags, NULL);
// Check whether ACLs allow the deletion
pcmk__apply_acl(xmlDocGetRootElement(candidate->doc));
// Remove the child again (which will track it in document's deleted_objs)
free_xml_with_position(candidate,
pcmk__xml_position(old_child, pcmk__xf_skip));
if (pcmk__xml_match(new_parent, old_child, true) == NULL) {
pcmk__set_xml_flags((xml_node_private_t *) (old_child->_private),
pcmk__xf_skip);
}
}
static void
mark_child_moved(xmlNode *old_child, xmlNode *new_parent, xmlNode *new_child,
int p_old, int p_new)
{
xml_node_private_t *nodepriv = new_child->_private;
crm_trace("Child element %s with "
PCMK_XA_ID "='%s' moved from position %d to %d under %s",
new_child->name, pcmk__s(pcmk__xe_id(new_child), "<no id>"),
p_old, p_new, new_parent->name);
pcmk__mark_xml_node_dirty(new_parent);
pcmk__set_xml_flags(nodepriv, pcmk__xf_moved);
if (p_old > p_new) {
nodepriv = old_child->_private;
} else {
nodepriv = new_child->_private;
}
pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
}
// Given original and new XML, mark new XML portions that have changed
static void
mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top)
{
xmlNode *old_child = NULL;
xmlNode *new_child = NULL;
xml_node_private_t *nodepriv = NULL;
CRM_CHECK(new_xml != NULL, return);
if (old_xml == NULL) {
pcmk__xml_mark_created(new_xml);
pcmk__apply_creation_acl(new_xml, check_top);
return;
}
nodepriv = new_xml->_private;
CRM_CHECK(nodepriv != NULL, return);
if(nodepriv->flags & pcmk__xf_processed) {
/* Avoid re-comparing nodes */
return;
}
pcmk__set_xml_flags(nodepriv, pcmk__xf_processed);
xml_diff_attrs(old_xml, new_xml);
// Check for differences in the original children
for (old_child = pcmk__xml_first_child(old_xml); old_child != NULL;
old_child = pcmk__xml_next(old_child)) {
new_child = pcmk__xml_match(new_xml, old_child, true);
if (new_child != NULL) {
mark_xml_changes(old_child, new_child, true);
} else {
mark_child_deleted(old_child, new_xml);
}
}
// Check for moved or created children
new_child = pcmk__xml_first_child(new_xml);
while (new_child != NULL) {
xmlNode *next = pcmk__xml_next(new_child);
old_child = pcmk__xml_match(old_xml, new_child, true);
if (old_child == NULL) {
// This is a newly created child
nodepriv = new_child->_private;
pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
// May free new_child
mark_xml_changes(old_child, new_child, true);
} else {
/* Check for movement, we already checked for differences */
int p_new = pcmk__xml_position(new_child, pcmk__xf_skip);
int p_old = pcmk__xml_position(old_child, pcmk__xf_skip);
if(p_old != p_new) {
mark_child_moved(old_child, new_xml, new_child, p_old, p_new);
}
}
new_child = next;
}
}
void
xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml)
{
pcmk__set_xml_doc_flag(new_xml, pcmk__xf_lazy);
xml_calculate_changes(old_xml, new_xml);
}
// Called functions may set the \p pcmk__xf_skip flag on parts of \p old_xml
void
xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml)
{
CRM_CHECK((old_xml != NULL) && (new_xml != NULL)
&& pcmk__xe_is(old_xml, (const char *) new_xml->name)
&& pcmk__str_eq(pcmk__xe_id(old_xml), pcmk__xe_id(new_xml),
pcmk__str_none),
return);
if(xml_tracking_changes(new_xml) == FALSE) {
xml_track_changes(new_xml, NULL, NULL, FALSE);
}
mark_xml_changes(old_xml, new_xml, FALSE);
}
/*!
* \internal
* \brief Find a comment with matching content in specified XML
*
* \param[in] root XML to search
* \param[in] search_comment Comment whose content should be searched for
* \param[in] exact If true, comment must also be at same position
*/
xmlNode *
pcmk__xc_match(const xmlNode *root, const xmlNode *search_comment, bool exact)
{
xmlNode *a_child = NULL;
int search_offset = pcmk__xml_position(search_comment, pcmk__xf_skip);
CRM_CHECK(search_comment->type == XML_COMMENT_NODE, return NULL);
for (a_child = pcmk__xml_first_child(root); a_child != NULL;
a_child = pcmk__xml_next(a_child)) {
if (exact) {
int offset = pcmk__xml_position(a_child, pcmk__xf_skip);
xml_node_private_t *nodepriv = a_child->_private;
if (offset < search_offset) {
continue;
} else if (offset > search_offset) {
return NULL;
}
if (pcmk_is_set(nodepriv->flags, pcmk__xf_skip)) {
continue;
}
}
if (a_child->type == XML_COMMENT_NODE
&& pcmk__str_eq((const char *)a_child->content, (const char *)search_comment->content, pcmk__str_casei)) {
return a_child;
} else if (exact) {
return NULL;
}
}
return NULL;
}
/*!
* \internal
* \brief Make one XML comment match another (in content)
*
* \param[in,out] parent If \p target is NULL and this is not, add or update
* comment child of this XML node that matches \p update
* \param[in,out] target If not NULL, update this XML comment node
* \param[in] update Make comment content match this (must not be NULL)
*
* \note At least one of \parent and \target must be non-NULL
*/
void
pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update)
{
CRM_CHECK(update != NULL, return);
CRM_CHECK(update->type == XML_COMMENT_NODE, return);
if (target == NULL) {
target = pcmk__xc_match(parent, update, false);
}
if (target == NULL) {
pcmk__xml_copy(parent, update);
} else if (!pcmk__str_eq((const char *)target->content, (const char *)update->content, pcmk__str_casei)) {
xmlFree(target->content);
target->content = xmlStrdup(update->content);
}
}
/*!
* \internal
* \brief Merge one XML tree into another
*
* Here, "merge" means:
* 1. Copy attribute values from \p update to the target, overwriting in case of
* conflict.
* 2. Descend through \p update and the target in parallel. At each level, for
* each child of \p update, look for a matching child of the target.
* a. For each child, if a match is found, go to step 1, recursively merging
* the child of \p update into the child of the target.
* b. Otherwise, copy the child of \p update as a child of the target.
*
* A match is defined as the first child of the same type within the target,
* with:
* * the \c PCMK_XA_ID attribute matching, if set in \p update; otherwise,
* * the \c PCMK_XA_ID_REF attribute matching, if set in \p update
*
* This function does not delete any elements or attributes from the target. It
* may add elements or overwrite attributes, as described above.
*
* \param[in,out] parent If \p target is NULL and this is not, add or update
* child of this XML node that matches \p update
* \param[in,out] target If not NULL, update this XML
* \param[in] update Make the desired XML match this (must not be \c NULL)
* \param[in] flags Group of <tt>enum pcmk__xa_flags</tt>
* \param[in] as_diff If \c true, preserve order of attributes (deprecated
* since 2.0.5)
*
* \note At least one of \p parent and \p target must be non-<tt>NULL</tt>.
* \note This function is recursive. For the top-level call, \p parent is
* \c NULL and \p target is not \c NULL. For recursive calls, \p target is
* \c NULL and \p parent is not \c NULL.
*/
void
pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update,
uint32_t flags, bool as_diff)
{
/* @COMPAT Refactor further and staticize after v1 patchset deprecation.
*
* @COMPAT Drop as_diff argument when apply_xml_diff() is dropped.
*/
const char *update_name = NULL;
const char *update_id_attr = NULL;
const char *update_id_val = NULL;
char *trace_s = NULL;
crm_log_xml_trace(update, "update");
crm_log_xml_trace(target, "target");
CRM_CHECK(update != NULL, goto done);
if (update->type == XML_COMMENT_NODE) {
pcmk__xc_update(parent, target, update);
goto done;
}
update_name = (const char *) update->name;
CRM_CHECK(update_name != NULL, goto done);
CRM_CHECK((target != NULL) || (parent != NULL), goto done);
update_id_val = pcmk__xe_id(update);
if (update_id_val != NULL) {
update_id_attr = PCMK_XA_ID;
} else {
update_id_val = crm_element_value(update, PCMK_XA_ID_REF);
if (update_id_val != NULL) {
update_id_attr = PCMK_XA_ID_REF;
}
}
pcmk__if_tracing(
{
if (update_id_attr != NULL) {
trace_s = crm_strdup_printf("<%s %s=%s/>",
update_name, update_id_attr,
update_id_val);
} else {
trace_s = crm_strdup_printf("<%s/>", update_name);
}
},
{}
);
if (target == NULL) {
// Recursive call
target = pcmk__xe_first_child(parent, update_name, update_id_attr,
update_id_val);
}
if (target == NULL) {
// Recursive call with no existing matching child
target = pcmk__xe_create(parent, update_name);
crm_trace("Added %s", pcmk__s(trace_s, update_name));
} else {
// Either recursive call with match, or top-level call
crm_trace("Found node %s to update", pcmk__s(trace_s, update_name));
}
CRM_CHECK(pcmk__xe_is(target, (const char *) update->name), return);
if (!as_diff) {
pcmk__xe_copy_attrs(target, update, flags);
} else {
// Preserve order of attributes. Don't use pcmk__xe_copy_attrs().
for (xmlAttrPtr a = pcmk__xe_first_attr(update); a != NULL;
a = a->next) {
const char *p_value = pcmk__xml_attr_value(a);
/* Remove it first so the ordering of the update is preserved */
xmlUnsetProp(target, a->name);
xmlSetProp(target, a->name, (pcmkXmlStr) p_value);
}
}
for (xmlNode *child = pcmk__xml_first_child(update); child != NULL;
child = pcmk__xml_next(child)) {
crm_trace("Updating child of %s", pcmk__s(trace_s, update_name));
pcmk__xml_update(target, NULL, child, flags, as_diff);
}
crm_trace("Finished with %s", pcmk__s(trace_s, update_name));
done:
free(trace_s);
}
/*!
* \internal
* \brief Delete an XML subtree if it matches a search element
*
* A match is defined as follows:
* * \p xml and \p user_data are both element nodes of the same type.
* * If \p user_data has attributes set, \p xml has those attributes set to the
* same values. (\p xml may have additional attributes set to arbitrary
* values.)
*
* \param[in,out] xml XML subtree to delete upon match
* \param[in] user_data Search element
*
* \return \c true to continue traversing the tree, or \c false to stop (because
* \p xml was deleted)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
static bool
delete_xe_if_matching(xmlNode *xml, void *user_data)
{
xmlNode *search = user_data;
if (!pcmk__xe_is(search, (const char *) xml->name)) {
// No match: either not both elements, or different element types
return true;
}
for (const xmlAttr *attr = pcmk__xe_first_attr(search); attr != NULL;
attr = attr->next) {
const char *search_val = pcmk__xml_attr_value(attr);
const char *xml_val = crm_element_value(xml, (const char *) attr->name);
if (!pcmk__str_eq(search_val, xml_val, pcmk__str_casei)) {
// No match: an attr in xml doesn't match the attr in search
return true;
}
}
crm_log_xml_trace(xml, "delete-match");
crm_log_xml_trace(search, "delete-search");
free_xml(xml);
// Found a match and deleted it; stop traversing tree
return false;
}
/*!
* \internal
* \brief Search an XML tree depth-first and delete the first matching element
*
* This function does not attempt to match the tree root (\p xml).
*
* A match with a node \c node is defined as follows:
* * \c node and \p search are both element nodes of the same type.
* * If \p search has attributes set, \c node has those attributes set to the
* same values. (\c node may have additional attributes set to arbitrary
* values.)
*
* \param[in,out] xml XML subtree to search
* \param[in] search Element to match against
*
* \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on
* successful deletion and an error code otherwise)
*/
int
pcmk__xe_delete_match(xmlNode *xml, xmlNode *search)
{
// See @COMPAT comment in pcmk__xe_replace_match()
CRM_CHECK((xml != NULL) && (search != NULL), return EINVAL);
for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL;
xml = pcmk__xe_next(xml)) {
if (!pcmk__xml_tree_foreach(xml, delete_xe_if_matching, search)) {
// Found and deleted an element
return pcmk_rc_ok;
}
}
// No match found in this subtree
return ENXIO;
}
/*!
* \internal
* \brief Replace one XML node with a copy of another XML node
*
* This function handles change tracking and applies ACLs.
*
* \param[in,out] old XML node to replace
* \param[in] new XML node to copy as replacement for \p old
*
* \note This frees \p old.
*/
static void
replace_node(xmlNode *old, xmlNode *new)
{
new = xmlCopyNode(new, 1);
pcmk__mem_assert(new);
// May be unnecessary but avoids slight changes to some test outputs
pcmk__xml_tree_foreach(new, reset_xml_node_flags, NULL);
old = xmlReplaceNode(old, new);
if (xml_tracking_changes(new)) {
// Replaced sections may have included relevant ACLs
pcmk__apply_acl(new);
}
xml_calculate_changes(old, new);
xmlFreeNode(old);
}
/*!
* \internal
* \brief Replace one XML subtree with a copy of another if the two match
*
* A match is defined as follows:
* * \p xml and \p user_data are both element nodes of the same type.
* * If \p user_data has the \c PCMK_XA_ID attribute set, then \p xml has
* \c PCMK_XA_ID set to the same value.
*
* \param[in,out] xml XML subtree to replace with \p user_data upon match
* \param[in] user_data XML to replace \p xml with a copy of upon match
*
* \return \c true to continue traversing the tree, or \c false to stop (because
* \p xml was replaced by \p user_data)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
static bool
replace_xe_if_matching(xmlNode *xml, void *user_data)
{
xmlNode *replace = user_data;
const char *xml_id = NULL;
const char *replace_id = NULL;
xml_id = pcmk__xe_id(xml);
replace_id = pcmk__xe_id(replace);
if (!pcmk__xe_is(replace, (const char *) xml->name)) {
// No match: either not both elements, or different element types
return true;
}
if ((replace_id != NULL)
&& !pcmk__str_eq(replace_id, xml_id, pcmk__str_none)) {
// No match: ID was provided in replace and doesn't match xml's ID
return true;
}
crm_log_xml_trace(xml, "replace-match");
crm_log_xml_trace(replace, "replace-with");
replace_node(xml, replace);
// Found a match and replaced it; stop traversing tree
return false;
}
/*!
* \internal
* \brief Search an XML tree depth-first and replace the first matching element
*
* This function does not attempt to match the tree root (\p xml).
*
* A match with a node \c node is defined as follows:
* * \c node and \p replace are both element nodes of the same type.
* * If \p replace has the \c PCMK_XA_ID attribute set, then \c node has
* \c PCMK_XA_ID set to the same value.
*
* \param[in,out] xml XML tree to search
* \param[in] replace XML to replace a matching element with a copy of
*
* \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on
* successful replacement and an error code otherwise)
*/
int
pcmk__xe_replace_match(xmlNode *xml, xmlNode *replace)
{
/* @COMPAT Some of this behavior (like not matching the tree root, which is
* allowed by pcmk__xe_update_match()) is questionable for general use but
* required for backward compatibility by cib_process_replace() and
* cib_process_delete(). Behavior can change at a major version release if
* desired.
*/
CRM_CHECK((xml != NULL) && (replace != NULL), return EINVAL);
for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL;
xml = pcmk__xe_next(xml)) {
if (!pcmk__xml_tree_foreach(xml, replace_xe_if_matching, replace)) {
// Found and replaced an element
return pcmk_rc_ok;
}
}
// No match found in this subtree
return ENXIO;
}
//! User data for \c update_xe_if_matching()
struct update_data {
xmlNode *update; //!< Update source
uint32_t flags; //!< Group of <tt>enum pcmk__xa_flags</tt>
};
/*!
* \internal
* \brief Update one XML subtree with another if the two match
*
* "Update" means to merge a source subtree into a target subtree (see
* \c pcmk__xml_update()).
*
* A match is defined as follows:
* * \p xml and \p user_data->update are both element nodes of the same type.
* * \p xml and \p user_data->update have the same \c PCMK_XA_ID attribute
* value, or \c PCMK_XA_ID is unset in both
*
* \param[in,out] xml XML subtree to update with \p user_data->update
* upon match
* \param[in] user_data <tt>struct update_data</tt> object
*
* \return \c true to continue traversing the tree, or \c false to stop (because
* \p xml was updated by \p user_data->update)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
static bool
update_xe_if_matching(xmlNode *xml, void *user_data)
{
struct update_data *data = user_data;
xmlNode *update = data->update;
if (!pcmk__xe_is(update, (const char *) xml->name)) {
// No match: either not both elements, or different element types
return true;
}
if (!pcmk__str_eq(pcmk__xe_id(xml), pcmk__xe_id(update), pcmk__str_none)) {
// No match: ID mismatch
return true;
}
crm_log_xml_trace(xml, "update-match");
crm_log_xml_trace(update, "update-with");
pcmk__xml_update(NULL, xml, update, data->flags, false);
// Found a match and replaced it; stop traversing tree
return false;
}
/*!
* \internal
* \brief Search an XML tree depth-first and update the first matching element
*
* "Update" means to merge a source subtree into a target subtree (see
* \c pcmk__xml_update()).
*
* A match with a node \c node is defined as follows:
* * \c node and \p update are both element nodes of the same type.
* * \c node and \p update have the same \c PCMK_XA_ID attribute value, or
* \c PCMK_XA_ID is unset in both
*
* \param[in,out] xml XML tree to search
* \param[in] update XML to update a matching element with
* \param[in] flags Group of <tt>enum pcmk__xa_flags</tt>
*
* \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on
* successful update and an error code otherwise)
*/
int
pcmk__xe_update_match(xmlNode *xml, xmlNode *update, uint32_t flags)
{
/* @COMPAT In pcmk__xe_delete_match() and pcmk__xe_replace_match(), we
* compare IDs only if the equivalent of the update argument has an ID.
* Here, we're stricter: we consider it a mismatch if only one element has
* an ID attribute, or if both elements have IDs but they don't match.
*
* Perhaps we should align the behavior at a major version release.
*/
struct update_data data = {
.update = update,
.flags = flags,
};
CRM_CHECK((xml != NULL) && (update != NULL), return EINVAL);
if (!pcmk__xml_tree_foreach(xml, update_xe_if_matching, &data)) {
// Found and updated an element
return pcmk_rc_ok;
}
// No match found in this subtree
return ENXIO;
}
xmlNode *
sorted_xml(xmlNode *input, xmlNode *parent, gboolean recursive)
{
xmlNode *child = NULL;
GSList *nvpairs = NULL;
xmlNode *result = NULL;
CRM_CHECK(input != NULL, return NULL);
result = pcmk__xe_create(parent, (const char *) input->name);
nvpairs = pcmk_xml_attrs2nvpairs(input);
nvpairs = pcmk_sort_nvpairs(nvpairs);
pcmk_nvpairs2xml_attrs(nvpairs, result);
pcmk_free_nvpairs(nvpairs);
for (child = pcmk__xe_first_child(input, NULL, NULL, NULL); child != NULL;
child = pcmk__xe_next(child)) {
if (recursive) {
sorted_xml(child, result, recursive);
} else {
pcmk__xml_copy(result, child);
}
}
return result;
}
/*!
* \internal
* \brief Get next sibling XML element with the same name as a given element
*
* \param[in] node XML element to start from
*
* \return Next sibling XML element with same name
*/
xmlNode *
pcmk__xe_next_same(const xmlNode *node)
{
for (xmlNode *match = pcmk__xe_next(node); match != NULL;
match = pcmk__xe_next(match)) {
if (pcmk__xe_is(match, (const char *) node->name)) {
return match;
}
}
return NULL;
}
+/*!
+ * \internal
+ * \brief Initialize the Pacemaker XML environment
+ *
+ * Set an XML buffer allocation scheme, set XML node create and destroy
+ * callbacks, and load schemas into the cache.
+ */
void
-crm_xml_init(void)
+pcmk__xml_init(void)
{
- static bool init = true;
+ // @TODO Try to find a better caller than crm_log_preinit()
+ static bool initialized = false;
+
+ if (!initialized) {
+ initialized = true;
- if(init) {
- init = false;
- /* The default allocator XML_BUFFER_ALLOC_EXACT does far too many
- * pcmk__realloc()s and it can take upwards of 18 seconds (yes, seconds)
- * to dump a 28kb tree which XML_BUFFER_ALLOC_DOUBLEIT can do in
- * less than 1 second.
+ /* Double the buffer size when the buffer needs to grow. The default
+ * allocator XML_BUFFER_ALLOC_EXACT was found to cause poor performance
+ * due to the number of reallocs.
*/
xmlSetBufferAllocationScheme(XML_BUFFER_ALLOC_DOUBLEIT);
- /* Populate and free the _private field when nodes are created and destroyed */
- xmlDeregisterNodeDefault(free_private_data);
+ // Initialize private data at node creation
xmlRegisterNodeDefault(new_private_data);
+ // Free private data at node destruction
+ xmlDeregisterNodeDefault(free_private_data);
+
+ // Load schemas into the cache
crm_schema_init();
}
}
+void
+crm_xml_init(void)
+{
+ pcmk__xml_init();
+}
+
void
crm_xml_cleanup(void)
{
crm_schema_cleanup();
xmlCleanupParser();
}
#define XPATH_MAX 512
xmlNode *
expand_idref(xmlNode * input, xmlNode * top)
{
char *xpath = NULL;
const char *ref = NULL;
xmlNode *result = NULL;
if (input == NULL) {
return NULL;
}
ref = crm_element_value(input, PCMK_XA_ID_REF);
if (ref == NULL) {
return input;
}
if (top == NULL) {
top = input;
}
xpath = crm_strdup_printf("//%s[@" PCMK_XA_ID "='%s']", input->name, ref);
result = get_xpath_object(xpath, top, LOG_DEBUG);
if (result == NULL) { // Not possible with schema validation enabled
pcmk__config_err("Ignoring invalid %s configuration: "
PCMK_XA_ID_REF " '%s' does not reference "
"a valid object " CRM_XS " xpath=%s",
input->name, ref, xpath);
}
free(xpath);
return result;
}
char *
pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns)
{
static const char *base = NULL;
char *ret = NULL;
if (base == NULL) {
base = pcmk__env_option(PCMK__ENV_SCHEMA_DIRECTORY);
}
if (pcmk__str_empty(base)) {
base = CRM_SCHEMA_DIRECTORY;
}
switch (ns) {
case pcmk__xml_artefact_ns_legacy_rng:
case pcmk__xml_artefact_ns_legacy_xslt:
ret = strdup(base);
break;
case pcmk__xml_artefact_ns_base_rng:
case pcmk__xml_artefact_ns_base_xslt:
ret = crm_strdup_printf("%s/base", base);
break;
default:
crm_err("XML artefact family specified as %u not recognized", ns);
}
return ret;
}
static char *
find_artefact(enum pcmk__xml_artefact_ns ns, const char *path, const char *filespec)
{
char *ret = NULL;
switch (ns) {
case pcmk__xml_artefact_ns_legacy_rng:
case pcmk__xml_artefact_ns_base_rng:
if (pcmk__ends_with(filespec, ".rng")) {
ret = crm_strdup_printf("%s/%s", path, filespec);
} else {
ret = crm_strdup_printf("%s/%s.rng", path, filespec);
}
break;
case pcmk__xml_artefact_ns_legacy_xslt:
case pcmk__xml_artefact_ns_base_xslt:
if (pcmk__ends_with(filespec, ".xsl")) {
ret = crm_strdup_printf("%s/%s", path, filespec);
} else {
ret = crm_strdup_printf("%s/%s.xsl", path, filespec);
}
break;
default:
crm_err("XML artefact family specified as %u not recognized", ns);
}
return ret;
}
char *
pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec)
{
struct stat sb;
char *base = pcmk__xml_artefact_root(ns);
char *ret = NULL;
ret = find_artefact(ns, base, filespec);
free(base);
if (stat(ret, &sb) != 0 || !S_ISREG(sb.st_mode)) {
const char *remote_schema_dir = pcmk__remote_schema_dir();
ret = find_artefact(ns, remote_schema_dir, filespec);
}
return ret;
}
void
pcmk__xe_set_propv(xmlNodePtr node, va_list pairs)
{
while (true) {
const char *name, *value;
name = va_arg(pairs, const char *);
if (name == NULL) {
return;
}
value = va_arg(pairs, const char *);
if (value != NULL) {
crm_xml_add(node, name, value);
}
}
}
void
pcmk__xe_set_props(xmlNodePtr node, ...)
{
va_list pairs;
va_start(pairs, node);
pcmk__xe_set_propv(node, pairs);
va_end(pairs);
}
int
pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name,
int (*handler)(xmlNode *xml, void *userdata),
void *userdata)
{
xmlNode *children = (xml? xml->children : NULL);
CRM_ASSERT(handler != NULL);
for (xmlNode *node = children; node != NULL; node = node->next) {
if ((node->type == XML_ELEMENT_NODE)
&& ((child_element_name == NULL)
|| pcmk__xe_is(node, child_element_name))) {
int rc = handler(node, userdata);
if (rc != pcmk_rc_ok) {
return rc;
}
}
}
return pcmk_rc_ok;
}
// Deprecated functions kept only for backward API compatibility
// LCOV_EXCL_START
#include <crm/common/xml_compat.h>
xmlNode *
find_entity(xmlNode *parent, const char *node_name, const char *id)
{
return pcmk__xe_first_child(parent, node_name,
((id == NULL)? id : PCMK_XA_ID), id);
}
void
crm_destroy_xml(gpointer data)
{
free_xml(data);
}
xmlDoc *
getDocPtr(xmlNode *node)
{
xmlDoc *doc = NULL;
CRM_CHECK(node != NULL, return NULL);
doc = node->doc;
if (doc == NULL) {
doc = xmlNewDoc(PCMK__XML_VERSION);
xmlDocSetRootElement(doc, node);
}
return doc;
}
xmlNode *
add_node_copy(xmlNode *parent, xmlNode *src_node)
{
xmlNode *child = NULL;
CRM_CHECK((parent != NULL) && (src_node != NULL), return NULL);
child = xmlDocCopyNode(src_node, parent->doc, 1);
if (child == NULL) {
return NULL;
}
xmlAddChild(parent, child);
pcmk__xml_mark_created(child);
return child;
}
int
add_node_nocopy(xmlNode *parent, const char *name, xmlNode *child)
{
add_node_copy(parent, child);
free_xml(child);
return 1;
}
gboolean
xml_has_children(const xmlNode * xml_root)
{
if (xml_root != NULL && xml_root->children != NULL) {
return TRUE;
}
return FALSE;
}
static char *
replace_text(char *text, size_t *index, size_t *length, const char *replace)
{
// We have space for 1 char already
size_t offset = strlen(replace) - 1;
if (offset > 0) {
*length += offset;
text = pcmk__realloc(text, *length + 1);
// Shift characters to the right to make room for the replacement string
for (size_t i = *length; i > (*index + offset); i--) {
text[i] = text[i - offset];
}
}
// Replace the character at index by the replacement string
memcpy(text + *index, replace, offset + 1);
// Reset index to the end of replacement string
*index += offset;
return text;
}
char *
crm_xml_escape(const char *text)
{
size_t length = 0;
char *copy = NULL;
if (text == NULL) {
return NULL;
}
length = strlen(text);
copy = pcmk__str_copy(text);
for (size_t index = 0; index <= length; index++) {
if(copy[index] & 0x80 && copy[index+1] & 0x80){
index++;
continue;
}
switch (copy[index]) {
case 0:
// Sanity only; loop should stop at the last non-null byte
break;
case '<':
copy = replace_text(copy, &index, &length, "<");
break;
case '>':
copy = replace_text(copy, &index, &length, ">");
break;
case '"':
copy = replace_text(copy, &index, &length, """);
break;
case '\'':
copy = replace_text(copy, &index, &length, "'");
break;
case '&':
copy = replace_text(copy, &index, &length, "&");
break;
case '\t':
/* Might as well just expand to a few spaces... */
copy = replace_text(copy, &index, &length, " ");
break;
case '\n':
copy = replace_text(copy, &index, &length, "\\n");
break;
case '\r':
copy = replace_text(copy, &index, &length, "\\r");
break;
default:
/* Check for and replace non-printing characters with their octal equivalent */
if(copy[index] < ' ' || copy[index] > '~') {
char *replace = crm_strdup_printf("\\%.3o", copy[index]);
copy = replace_text(copy, &index, &length, replace);
free(replace);
}
}
}
return copy;
}
xmlNode *
copy_xml(xmlNode *src)
{
xmlDoc *doc = xmlNewDoc(PCMK__XML_VERSION);
xmlNode *copy = NULL;
pcmk__mem_assert(doc);
copy = xmlDocCopyNode(src, doc, 1);
pcmk__mem_assert(copy);
xmlDocSetRootElement(doc, copy);
return copy;
}
xmlNode *
create_xml_node(xmlNode *parent, const char *name)
{
// Like pcmk__xe_create(), but returns NULL on failure
xmlNode *node = NULL;
CRM_CHECK(!pcmk__str_empty(name), return NULL);
if (parent == NULL) {
xmlDoc *doc = xmlNewDoc(PCMK__XML_VERSION);
if (doc == NULL) {
return NULL;
}
node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL);
if (node == NULL) {
xmlFreeDoc(doc);
return NULL;
}
xmlDocSetRootElement(doc, node);
} else {
node = xmlNewChild(parent, NULL, (pcmkXmlStr) name, NULL);
if (node == NULL) {
return NULL;
}
}
pcmk__xml_mark_created(node);
return node;
}
xmlNode *
pcmk_create_xml_text_node(xmlNode *parent, const char *name,
const char *content)
{
xmlNode *node = pcmk__xe_create(parent, name);
pcmk__xe_set_content(node, "%s", content);
return node;
}
xmlNode *
pcmk_create_html_node(xmlNode *parent, const char *element_name, const char *id,
const char *class_name, const char *text)
{
xmlNode *node = pcmk__html_create(parent, element_name, id, class_name);
pcmk__xe_set_content(node, "%s", text);
return node;
}
xmlNode *
first_named_child(const xmlNode *parent, const char *name)
{
return pcmk__xe_first_child(parent, name, NULL, NULL);
}
xmlNode *
find_xml_node(const xmlNode *root, const char *search_path, gboolean must_find)
{
xmlNode *result = NULL;
if (search_path == NULL) {
crm_warn("Will never find <NULL>");
return NULL;
}
result = pcmk__xe_first_child(root, search_path, NULL, NULL);
if (must_find && (result == NULL)) {
crm_warn("Could not find %s in %s",
search_path,
((root != NULL)? (const char *) root->name : "<NULL>"));
}
return result;
}
xmlNode *
crm_next_same_xml(const xmlNode *sibling)
{
return pcmk__xe_next_same(sibling);
}
void
xml_remove_prop(xmlNode * obj, const char *name)
{
pcmk__xe_remove_attr(obj, name);
}
gboolean
replace_xml_child(xmlNode * parent, xmlNode * child, xmlNode * update, gboolean delete_only)
{
bool is_match = false;
const char *child_id = NULL;
const char *update_id = NULL;
CRM_CHECK(child != NULL, return FALSE);
CRM_CHECK(update != NULL, return FALSE);
child_id = pcmk__xe_id(child);
update_id = pcmk__xe_id(update);
/* Match element name and (if provided in update XML) element ID. Don't
* match search root (child is search root if parent == NULL).
*/
is_match = (parent != NULL)
&& pcmk__xe_is(update, (const char *) child->name)
&& ((update_id == NULL)
|| pcmk__str_eq(update_id, child_id, pcmk__str_none));
/* For deletion, match all attributes provided in update. A matching node
* can have additional attributes, but values must match for provided ones.
*/
if (is_match && delete_only) {
for (xmlAttr *attr = pcmk__xe_first_attr(update); attr != NULL;
attr = attr->next) {
const char *name = (const char *) attr->name;
const char *update_val = pcmk__xml_attr_value(attr);
const char *child_val = crm_element_value(child, name);
if (!pcmk__str_eq(update_val, child_val, pcmk__str_casei)) {
is_match = false;
break;
}
}
}
if (is_match) {
if (delete_only) {
crm_log_xml_trace(child, "delete-match");
crm_log_xml_trace(update, "delete-search");
free_xml(child);
} else {
crm_log_xml_trace(child, "replace-match");
crm_log_xml_trace(update, "replace-with");
replace_node(child, update);
}
return TRUE;
}
// Current node not a match; search the rest of the subtree depth-first
parent = child;
for (child = pcmk__xml_first_child(parent); child != NULL;
child = pcmk__xml_next(child)) {
// Only delete/replace the first match
if (replace_xml_child(parent, child, update, delete_only)) {
return TRUE;
}
}
// No match found in this subtree
return FALSE;
}
gboolean
update_xml_child(xmlNode *child, xmlNode *to_update)
{
return pcmk__xe_update_match(child, to_update,
pcmk__xaf_score_update) == pcmk_rc_ok;
}
int
find_xml_children(xmlNode **children, xmlNode *root, const char *tag,
const char *field, const char *value, gboolean search_matches)
{
int match_found = 0;
CRM_CHECK(root != NULL, return FALSE);
CRM_CHECK(children != NULL, return FALSE);
if ((tag != NULL) && !pcmk__xe_is(root, tag)) {
} else if ((value != NULL)
&& !pcmk__str_eq(value, crm_element_value(root, field),
pcmk__str_casei)) {
} else {
if (*children == NULL) {
*children = pcmk__xe_create(NULL, __func__);
}
pcmk__xml_copy(*children, root);
match_found = 1;
}
if (search_matches || match_found == 0) {
xmlNode *child = NULL;
for (child = pcmk__xml_first_child(root); child != NULL;
child = pcmk__xml_next(child)) {
match_found += find_xml_children(children, child, tag, field, value,
search_matches);
}
}
return match_found;
}
void
fix_plus_plus_recursive(xmlNode *target)
{
/* TODO: Remove recursion and use xpath searches for value++ */
xmlNode *child = NULL;
for (xmlAttrPtr a = pcmk__xe_first_attr(target); a != NULL; a = a->next) {
const char *p_name = (const char *) a->name;
const char *p_value = pcmk__xml_attr_value(a);
expand_plus_plus(target, p_name, p_value);
}
for (child = pcmk__xe_first_child(target, NULL, NULL, NULL); child != NULL;
child = pcmk__xe_next(child)) {
fix_plus_plus_recursive(child);
}
}
void
copy_in_properties(xmlNode *target, const xmlNode *src)
{
if (src == NULL) {
crm_warn("No node to copy properties from");
} else if (target == NULL) {
crm_err("No node to copy properties into");
} else {
for (xmlAttrPtr a = pcmk__xe_first_attr(src); a != NULL; a = a->next) {
const char *p_name = (const char *) a->name;
const char *p_value = pcmk__xml_attr_value(a);
expand_plus_plus(target, p_name, p_value);
if (xml_acl_denied(target)) {
crm_trace("Cannot copy %s=%s to %s", p_name, p_value, target->name);
return;
}
}
}
}
void
expand_plus_plus(xmlNode * target, const char *name, const char *value)
{
pcmk__xe_set_score(target, name, value);
}
// LCOV_EXCL_STOP
// End deprecated API
diff --git a/lib/pengine/tests/native/native_find_rsc_test.c b/lib/pengine/tests/native/native_find_rsc_test.c
index ac1337d2b8..926473ebbd 100644
--- a/lib/pengine/tests/native/native_find_rsc_test.c
+++ b/lib/pengine/tests/native/native_find_rsc_test.c
@@ -1,917 +1,917 @@
/*
* Copyright 2022-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <crm/common/unittest_internal.h>
#include <crm/common/scheduler.h>
#include <crm/common/xml.h>
#include <crm/pengine/internal.h>
#include <crm/pengine/status.h>
xmlNode *input = NULL;
pcmk_scheduler_t *scheduler = NULL;
pcmk_node_t *cluster01, *cluster02, *httpd_bundle_0;
pcmk_resource_t *exim_group, *inactive_group;
pcmk_resource_t *promotable_clone, *inactive_clone;
pcmk_resource_t *httpd_bundle, *mysql_clone_group;
static int
setup(void **state) {
char *path = NULL;
- crm_xml_init();
+ pcmk__xml_init();
path = crm_strdup_printf("%s/crm_mon.xml", getenv("PCMK_CTS_CLI_DIR"));
input = pcmk__xml_read(path);
free(path);
if (input == NULL) {
return 1;
}
scheduler = pe_new_working_set();
if (scheduler == NULL) {
return 1;
}
pcmk__set_scheduler_flags(scheduler,
pcmk_sched_no_counts|pcmk_sched_no_compat);
scheduler->input = input;
cluster_status(scheduler);
/* Get references to the cluster nodes so we don't have to find them repeatedly. */
cluster01 = pcmk_find_node(scheduler, "cluster01");
cluster02 = pcmk_find_node(scheduler, "cluster02");
httpd_bundle_0 = pcmk_find_node(scheduler, "httpd-bundle-0");
/* Get references to several resources we use frequently. */
for (GList *iter = scheduler->resources; iter != NULL; iter = iter->next) {
pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data;
if (strcmp(rsc->id, "exim-group") == 0) {
exim_group = rsc;
} else if (strcmp(rsc->id, "httpd-bundle") == 0) {
httpd_bundle = rsc;
} else if (strcmp(rsc->id, "inactive-clone") == 0) {
inactive_clone = rsc;
} else if (strcmp(rsc->id, "inactive-group") == 0) {
inactive_group = rsc;
} else if (strcmp(rsc->id, "mysql-clone-group") == 0) {
mysql_clone_group = rsc;
} else if (strcmp(rsc->id, "promotable-clone") == 0) {
promotable_clone = rsc;
}
}
return 0;
}
static int
teardown(void **state) {
pe_free_working_set(scheduler);
return 0;
}
static void
bad_args(void **state) {
pcmk_resource_t *rsc = g_list_first(scheduler->resources)->data;
char *id = rsc->id;
char *name = NULL;
assert_non_null(rsc);
assert_null(native_find_rsc(NULL, "dummy", NULL, 0));
assert_null(native_find_rsc(rsc, NULL, NULL, 0));
/* No resources exist with these names. */
name = crm_strdup_printf("%sX", rsc->id);
assert_null(native_find_rsc(rsc, name, NULL, 0));
free(name);
name = crm_strdup_printf("x%s", rsc->id);
assert_null(native_find_rsc(rsc, name, NULL, 0));
free(name);
name = g_ascii_strup(rsc->id, -1);
assert_null(native_find_rsc(rsc, name, NULL, 0));
g_free(name);
/* Fails because resource ID is NULL. */
rsc->id = NULL;
assert_null(native_find_rsc(rsc, id, NULL, 0));
rsc->id = id;
}
static void
primitive_rsc(void **state) {
pcmk_resource_t *dummy = NULL;
/* Find the "dummy" resource, which is the only one with that ID in the set. */
for (GList *iter = scheduler->resources; iter != NULL; iter = iter->next) {
pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data;
if (strcmp(rsc->id, "dummy") == 0) {
dummy = rsc;
break;
}
}
assert_non_null(dummy);
/* Passes because NULL was passed for node, regardless of flags. */
assert_ptr_equal(dummy, native_find_rsc(dummy, "dummy", NULL, 0));
assert_ptr_equal(dummy,
native_find_rsc(dummy, "dummy", NULL,
pcmk_rsc_match_current_node));
/* Fails because resource is not a clone (nor cloned). */
assert_null(native_find_rsc(dummy, "dummy", NULL,
pcmk_rsc_match_clone_only));
assert_null(native_find_rsc(dummy, "dummy", cluster02,
pcmk_rsc_match_clone_only));
/* Fails because dummy is not running on cluster01, even with the right flags. */
assert_null(native_find_rsc(dummy, "dummy", cluster01,
pcmk_rsc_match_current_node));
// Fails because pcmk_rsc_match_current_node is required if a node is given
assert_null(native_find_rsc(dummy, "dummy", cluster02, 0));
/* Passes because dummy is running on cluster02. */
assert_ptr_equal(dummy,
native_find_rsc(dummy, "dummy", cluster02,
pcmk_rsc_match_current_node));
}
static void
group_rsc(void **state) {
assert_non_null(exim_group);
/* Passes because NULL was passed for node, regardless of flags. */
assert_ptr_equal(exim_group, native_find_rsc(exim_group, "exim-group", NULL, 0));
assert_ptr_equal(exim_group,
native_find_rsc(exim_group, "exim-group", NULL,
pcmk_rsc_match_current_node));
/* Fails because resource is not a clone (nor cloned). */
assert_null(native_find_rsc(exim_group, "exim-group", NULL,
pcmk_rsc_match_clone_only));
assert_null(native_find_rsc(exim_group, "exim-group", cluster01,
pcmk_rsc_match_clone_only));
/* Fails because none of exim-group's children are running on cluster01, even with the right flags. */
assert_null(native_find_rsc(exim_group, "exim-group", cluster01,
pcmk_rsc_match_current_node));
// Fails because pcmk_rsc_match_current_node is required if a node is given
assert_null(native_find_rsc(exim_group, "exim-group", cluster01, 0));
/* Passes because one of exim-group's children is running on cluster02. */
assert_ptr_equal(exim_group,
native_find_rsc(exim_group, "exim-group", cluster02,
pcmk_rsc_match_current_node));
}
static void
inactive_group_rsc(void **state) {
assert_non_null(inactive_group);
/* Passes because NULL was passed for node, regardless of flags. */
assert_ptr_equal(inactive_group, native_find_rsc(inactive_group, "inactive-group", NULL, 0));
assert_ptr_equal(inactive_group,
native_find_rsc(inactive_group, "inactive-group", NULL,
pcmk_rsc_match_current_node));
/* Fails because resource is not a clone (nor cloned). */
assert_null(native_find_rsc(inactive_group, "inactive-group", NULL,
pcmk_rsc_match_clone_only));
assert_null(native_find_rsc(inactive_group, "inactive-group", cluster01,
pcmk_rsc_match_clone_only));
/* Fails because none of inactive-group's children are running. */
assert_null(native_find_rsc(inactive_group, "inactive-group", cluster01,
pcmk_rsc_match_current_node));
assert_null(native_find_rsc(inactive_group, "inactive-group", cluster02,
pcmk_rsc_match_current_node));
}
static void
group_member_rsc(void **state) {
pcmk_resource_t *public_ip = NULL;
/* Find the "Public-IP" resource, a member of "exim-group". */
for (GList *iter = exim_group->children; iter != NULL; iter = iter->next) {
pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data;
if (strcmp(rsc->id, "Public-IP") == 0) {
public_ip = rsc;
break;
}
}
assert_non_null(public_ip);
/* Passes because NULL was passed for node, regardless of flags. */
assert_ptr_equal(public_ip, native_find_rsc(public_ip, "Public-IP", NULL, 0));
assert_ptr_equal(public_ip,
native_find_rsc(public_ip, "Public-IP", NULL,
pcmk_rsc_match_current_node));
/* Fails because resource is not a clone (nor cloned). */
assert_null(native_find_rsc(public_ip, "Public-IP", NULL,
pcmk_rsc_match_clone_only));
assert_null(native_find_rsc(public_ip, "Public-IP", cluster02,
pcmk_rsc_match_clone_only));
/* Fails because Public-IP is not running on cluster01, even with the right flags. */
assert_null(native_find_rsc(public_ip, "Public-IP", cluster01,
pcmk_rsc_match_current_node));
// Fails because pcmk_rsc_match_current_node is required if a node is given
assert_null(native_find_rsc(public_ip, "Public-IP", cluster02, 0));
/* Passes because Public-IP is running on cluster02. */
assert_ptr_equal(public_ip,
native_find_rsc(public_ip, "Public-IP", cluster02,
pcmk_rsc_match_current_node));
}
static void
inactive_group_member_rsc(void **state) {
pcmk_resource_t *inactive_dummy_1 = NULL;
/* Find the "inactive-dummy-1" resource, a member of "inactive-group". */
for (GList *iter = inactive_group->children; iter != NULL; iter = iter->next) {
pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data;
if (strcmp(rsc->id, "inactive-dummy-1") == 0) {
inactive_dummy_1 = rsc;
break;
}
}
assert_non_null(inactive_dummy_1);
/* Passes because NULL was passed for node, regardless of flags. */
assert_ptr_equal(inactive_dummy_1, native_find_rsc(inactive_dummy_1, "inactive-dummy-1", NULL, 0));
assert_ptr_equal(inactive_dummy_1,
native_find_rsc(inactive_dummy_1, "inactive-dummy-1", NULL,
pcmk_rsc_match_current_node));
/* Fails because resource is not a clone (nor cloned). */
assert_null(native_find_rsc(inactive_dummy_1, "inactive-dummy-1", NULL,
pcmk_rsc_match_clone_only));
assert_null(native_find_rsc(inactive_dummy_1, "inactive-dummy-1", cluster01,
pcmk_rsc_match_clone_only));
/* Fails because inactive-dummy-1 is not running. */
assert_null(native_find_rsc(inactive_dummy_1, "inactive-dummy-1", cluster01,
pcmk_rsc_match_current_node));
assert_null(native_find_rsc(inactive_dummy_1, "inactive-dummy-1", cluster02,
pcmk_rsc_match_current_node));
}
static void
clone_rsc(void **state) {
assert_non_null(promotable_clone);
/* Passes because NULL was passed for node, regardless of flags. */
assert_ptr_equal(promotable_clone, native_find_rsc(promotable_clone, "promotable-clone", NULL, 0));
assert_ptr_equal(promotable_clone,
native_find_rsc(promotable_clone, "promotable-clone", NULL,
pcmk_rsc_match_current_node));
assert_ptr_equal(promotable_clone,
native_find_rsc(promotable_clone, "promotable-clone", NULL,
pcmk_rsc_match_clone_only));
// Fails because pcmk_rsc_match_current_node is required if a node is given
assert_null(native_find_rsc(promotable_clone, "promotable-clone", cluster01, 0));
/* Passes because one of ping-clone's children is running on cluster01. */
assert_ptr_equal(promotable_clone,
native_find_rsc(promotable_clone, "promotable-clone",
cluster01, pcmk_rsc_match_current_node));
// Fails because pcmk_rsc_match_current_node is required if a node is given
assert_null(native_find_rsc(promotable_clone, "promotable-clone", cluster02, 0));
/* Passes because one of ping_clone's children is running on cluster02. */
assert_ptr_equal(promotable_clone,
native_find_rsc(promotable_clone, "promotable-clone",
cluster02, pcmk_rsc_match_current_node));
// Passes for previous reasons, plus includes pcmk_rsc_match_clone_only
assert_ptr_equal(promotable_clone,
native_find_rsc(promotable_clone, "promotable-clone",
cluster01,
pcmk_rsc_match_clone_only
|pcmk_rsc_match_current_node));
assert_ptr_equal(promotable_clone,
native_find_rsc(promotable_clone, "promotable-clone",
cluster02,
pcmk_rsc_match_clone_only
|pcmk_rsc_match_current_node));
}
static void
inactive_clone_rsc(void **state) {
assert_non_null(inactive_clone);
/* Passes because NULL was passed for node, regardless of flags. */
assert_ptr_equal(inactive_clone, native_find_rsc(inactive_clone, "inactive-clone", NULL, 0));
assert_ptr_equal(inactive_clone,
native_find_rsc(inactive_clone, "inactive-clone", NULL,
pcmk_rsc_match_current_node));
assert_ptr_equal(inactive_clone,
native_find_rsc(inactive_clone, "inactive-clone", NULL,
pcmk_rsc_match_clone_only));
/* Fails because none of inactive-clone's children are running. */
assert_null(native_find_rsc(inactive_clone, "inactive-clone", cluster01,
pcmk_rsc_match_current_node
|pcmk_rsc_match_clone_only));
assert_null(native_find_rsc(inactive_clone, "inactive-clone", cluster02,
pcmk_rsc_match_current_node
|pcmk_rsc_match_clone_only));
}
static void
clone_instance_rsc(void **state) {
pcmk_resource_t *promotable_0 = NULL;
pcmk_resource_t *promotable_1 = NULL;
/* Find the "promotable-rsc:0" and "promotable-rsc:1" resources, members of "promotable-clone". */
for (GList *iter = promotable_clone->children; iter != NULL; iter = iter->next) {
pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data;
if (strcmp(rsc->id, "promotable-rsc:0") == 0) {
promotable_0 = rsc;
} else if (strcmp(rsc->id, "promotable-rsc:1") == 0) {
promotable_1 = rsc;
}
}
assert_non_null(promotable_0);
assert_non_null(promotable_1);
/* Passes because NULL was passed for node, regardless of flags. */
assert_ptr_equal(promotable_0, native_find_rsc(promotable_0, "promotable-rsc:0", NULL, 0));
assert_ptr_equal(promotable_0,
native_find_rsc(promotable_0, "promotable-rsc:0", NULL,
pcmk_rsc_match_current_node));
assert_ptr_equal(promotable_1, native_find_rsc(promotable_1, "promotable-rsc:1", NULL, 0));
assert_ptr_equal(promotable_1,
native_find_rsc(promotable_1, "promotable-rsc:1", NULL,
pcmk_rsc_match_current_node));
// Fails because pcmk_rsc_match_current_node is required if a node is given
assert_null(native_find_rsc(promotable_0, "promotable-rsc:0", cluster02, 0));
assert_null(native_find_rsc(promotable_1, "promotable-rsc:1", cluster01, 0));
/* Check that the resource is running on the node we expect. */
assert_ptr_equal(promotable_0,
native_find_rsc(promotable_0, "promotable-rsc:0",
cluster02, pcmk_rsc_match_current_node));
assert_null(native_find_rsc(promotable_0, "promotable-rsc:0", cluster01,
pcmk_rsc_match_current_node));
assert_ptr_equal(promotable_1,
native_find_rsc(promotable_1, "promotable-rsc:1",
cluster01, pcmk_rsc_match_current_node));
assert_null(native_find_rsc(promotable_1, "promotable-rsc:1", cluster02,
pcmk_rsc_match_current_node));
/* Passes because NULL was passed for node and primitive name was given, with correct flags. */
assert_ptr_equal(promotable_0,
native_find_rsc(promotable_0, "promotable-rsc", NULL,
pcmk_rsc_match_clone_only));
// Passes because pcmk_rsc_match_basename matches any instance's base name
assert_ptr_equal(promotable_0,
native_find_rsc(promotable_0, "promotable-rsc", NULL,
pcmk_rsc_match_basename));
assert_ptr_equal(promotable_1,
native_find_rsc(promotable_1, "promotable-rsc", NULL,
pcmk_rsc_match_basename));
// Passes because pcmk_rsc_match_anon_basename matches
assert_ptr_equal(promotable_0,
native_find_rsc(promotable_0, "promotable-rsc", NULL,
pcmk_rsc_match_anon_basename));
assert_ptr_equal(promotable_1,
native_find_rsc(promotable_1, "promotable-rsc", NULL,
pcmk_rsc_match_anon_basename));
/* Check that the resource is running on the node we expect. */
assert_ptr_equal(promotable_0,
native_find_rsc(promotable_0, "promotable-rsc", cluster02,
pcmk_rsc_match_basename
|pcmk_rsc_match_current_node));
assert_ptr_equal(promotable_0,
native_find_rsc(promotable_0, "promotable-rsc", cluster02,
pcmk_rsc_match_anon_basename
|pcmk_rsc_match_current_node));
assert_null(native_find_rsc(promotable_0, "promotable-rsc", cluster01,
pcmk_rsc_match_basename
|pcmk_rsc_match_current_node));
assert_null(native_find_rsc(promotable_0, "promotable-rsc", cluster01,
pcmk_rsc_match_anon_basename
|pcmk_rsc_match_current_node));
assert_ptr_equal(promotable_1,
native_find_rsc(promotable_1, "promotable-rsc", cluster01,
pcmk_rsc_match_basename
|pcmk_rsc_match_current_node));
assert_ptr_equal(promotable_1,
native_find_rsc(promotable_1, "promotable-rsc", cluster01,
pcmk_rsc_match_anon_basename
|pcmk_rsc_match_current_node));
assert_null(native_find_rsc(promotable_1, "promotable-rsc", cluster02,
pcmk_rsc_match_basename
|pcmk_rsc_match_current_node));
assert_null(native_find_rsc(promotable_1, "promotable-rsc", cluster02,
pcmk_rsc_match_anon_basename
|pcmk_rsc_match_current_node));
/* Fails because incorrect flags were given along with primitive name. */
assert_null(native_find_rsc(promotable_0, "promotable-rsc", NULL,
pcmk_rsc_match_current_node));
assert_null(native_find_rsc(promotable_1, "promotable-rsc", NULL,
pcmk_rsc_match_current_node));
/* And then we check failure possibilities again, except passing promotable_clone
* instead of promotable_X as the first argument to native_find_rsc.
*/
// Fails because pcmk_rsc_match_current_node is required if a node is given
assert_null(native_find_rsc(promotable_clone, "promotable-rsc:0", cluster02, 0));
assert_null(native_find_rsc(promotable_clone, "promotable-rsc:1", cluster01, 0));
/* Check that the resource is running on the node we expect. */
assert_ptr_equal(promotable_0,
native_find_rsc(promotable_clone, "promotable-rsc:0",
cluster02, pcmk_rsc_match_current_node));
assert_ptr_equal(promotable_0,
native_find_rsc(promotable_clone, "promotable-rsc",
cluster02,
pcmk_rsc_match_basename
|pcmk_rsc_match_current_node));
assert_ptr_equal(promotable_0,
native_find_rsc(promotable_clone, "promotable-rsc",
cluster02,
pcmk_rsc_match_anon_basename
|pcmk_rsc_match_current_node));
assert_ptr_equal(promotable_1,
native_find_rsc(promotable_clone, "promotable-rsc:1",
cluster01, pcmk_rsc_match_current_node));
assert_ptr_equal(promotable_1,
native_find_rsc(promotable_clone, "promotable-rsc",
cluster01,
pcmk_rsc_match_basename
|pcmk_rsc_match_current_node));
assert_ptr_equal(promotable_1,
native_find_rsc(promotable_clone, "promotable-rsc",
cluster01,
pcmk_rsc_match_anon_basename
|pcmk_rsc_match_current_node));
}
static void
renamed_rsc(void **state) {
pcmk_resource_t *promotable_0 = NULL;
pcmk_resource_t *promotable_1 = NULL;
/* Find the "promotable-rsc:0" and "promotable-rsc:1" resources, members of "promotable-clone". */
for (GList *iter = promotable_clone->children; iter != NULL; iter = iter->next) {
pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data;
if (strcmp(rsc->id, "promotable-rsc:0") == 0) {
promotable_0 = rsc;
} else if (strcmp(rsc->id, "promotable-rsc:1") == 0) {
promotable_1 = rsc;
}
}
assert_non_null(promotable_0);
assert_non_null(promotable_1);
// Passes because pcmk_rsc_match_history means base name matches clone_name
assert_ptr_equal(promotable_0,
native_find_rsc(promotable_0, "promotable-rsc", NULL,
pcmk_rsc_match_history));
assert_ptr_equal(promotable_1,
native_find_rsc(promotable_1, "promotable-rsc", NULL,
pcmk_rsc_match_history));
}
static void
bundle_rsc(void **state) {
assert_non_null(httpd_bundle);
/* Passes because NULL was passed for node, regardless of flags. */
assert_ptr_equal(httpd_bundle, native_find_rsc(httpd_bundle, "httpd-bundle", NULL, 0));
assert_ptr_equal(httpd_bundle,
native_find_rsc(httpd_bundle, "httpd-bundle", NULL,
pcmk_rsc_match_current_node));
/* Fails because resource is not a clone (nor cloned). */
assert_null(native_find_rsc(httpd_bundle, "httpd-bundle", NULL,
pcmk_rsc_match_clone_only));
assert_null(native_find_rsc(httpd_bundle, "httpd-bundle", cluster01,
pcmk_rsc_match_clone_only));
// Fails because pcmk_rsc_match_current_node is required if a node is given
assert_null(native_find_rsc(httpd_bundle, "httpd-bundle", cluster01, 0));
/* Passes because one of httpd_bundle's children is running on cluster01. */
assert_ptr_equal(httpd_bundle,
native_find_rsc(httpd_bundle, "httpd-bundle", cluster01,
pcmk_rsc_match_current_node));
}
static bool
bundle_first_replica(pcmk__bundle_replica_t *replica, void *user_data)
{
pcmk_resource_t *ip_0 = replica->ip;
pcmk_resource_t *child_0 = replica->child;
pcmk_resource_t *container_0 = replica->container;
pcmk_resource_t *remote_0 = replica->remote;
assert_non_null(ip_0);
assert_non_null(child_0);
assert_non_null(container_0);
assert_non_null(remote_0);
/* Passes because NULL was passed for node, regardless of flags. */
assert_ptr_equal(ip_0, native_find_rsc(ip_0, "httpd-bundle-ip-192.168.122.131", NULL, 0));
assert_ptr_equal(child_0, native_find_rsc(child_0, "httpd:0", NULL, 0));
assert_ptr_equal(container_0, native_find_rsc(container_0, "httpd-bundle-docker-0", NULL, 0));
assert_ptr_equal(remote_0, native_find_rsc(remote_0, "httpd-bundle-0", NULL, 0));
// Fails because pcmk_rsc_match_current_node is required if a node is given
assert_null(native_find_rsc(ip_0, "httpd-bundle-ip-192.168.122.131", cluster01, 0));
assert_null(native_find_rsc(child_0, "httpd:0", httpd_bundle_0, 0));
assert_null(native_find_rsc(container_0, "httpd-bundle-docker-0", cluster01, 0));
assert_null(native_find_rsc(remote_0, "httpd-bundle-0", cluster01, 0));
/* Check that the resource is running on the node we expect. */
assert_ptr_equal(ip_0,
native_find_rsc(ip_0, "httpd-bundle-ip-192.168.122.131",
cluster01, pcmk_rsc_match_current_node));
assert_null(native_find_rsc(ip_0, "httpd-bundle-ip-192.168.122.131",
cluster02, pcmk_rsc_match_current_node));
assert_null(native_find_rsc(ip_0, "httpd-bundle-ip-192.168.122.131",
httpd_bundle_0, pcmk_rsc_match_current_node));
assert_ptr_equal(child_0,
native_find_rsc(child_0, "httpd:0", httpd_bundle_0,
pcmk_rsc_match_current_node));
assert_null(native_find_rsc(child_0, "httpd:0", cluster01,
pcmk_rsc_match_current_node));
assert_null(native_find_rsc(child_0, "httpd:0", cluster02,
pcmk_rsc_match_current_node));
assert_ptr_equal(container_0,
native_find_rsc(container_0, "httpd-bundle-docker-0",
cluster01, pcmk_rsc_match_current_node));
assert_null(native_find_rsc(container_0, "httpd-bundle-docker-0", cluster02,
pcmk_rsc_match_current_node));
assert_null(native_find_rsc(container_0, "httpd-bundle-docker-0",
httpd_bundle_0, pcmk_rsc_match_current_node));
assert_ptr_equal(remote_0,
native_find_rsc(remote_0, "httpd-bundle-0", cluster01,
pcmk_rsc_match_current_node));
assert_null(native_find_rsc(remote_0, "httpd-bundle-0", cluster02,
pcmk_rsc_match_current_node));
assert_null(native_find_rsc(remote_0, "httpd-bundle-0", httpd_bundle_0,
pcmk_rsc_match_current_node));
// Passes because pcmk_rsc_match_basename matches any replica's base name
assert_ptr_equal(child_0,
native_find_rsc(child_0, "httpd", NULL,
pcmk_rsc_match_basename));
// Passes because pcmk_rsc_match_anon_basename matches
assert_ptr_equal(child_0,
native_find_rsc(child_0, "httpd", NULL,
pcmk_rsc_match_anon_basename));
/* Check that the resource is running on the node we expect. */
assert_ptr_equal(child_0,
native_find_rsc(child_0, "httpd", httpd_bundle_0,
pcmk_rsc_match_basename
|pcmk_rsc_match_current_node));
assert_ptr_equal(child_0,
native_find_rsc(child_0, "httpd", httpd_bundle_0,
pcmk_rsc_match_anon_basename
|pcmk_rsc_match_current_node));
assert_null(native_find_rsc(child_0, "httpd", cluster01,
pcmk_rsc_match_basename
|pcmk_rsc_match_current_node));
assert_null(native_find_rsc(child_0, "httpd", cluster01,
pcmk_rsc_match_anon_basename
|pcmk_rsc_match_current_node));
assert_null(native_find_rsc(child_0, "httpd", cluster02,
pcmk_rsc_match_basename
|pcmk_rsc_match_current_node));
assert_null(native_find_rsc(child_0, "httpd", cluster02,
pcmk_rsc_match_anon_basename
|pcmk_rsc_match_current_node));
/* Fails because incorrect flags were given along with base name. */
assert_null(native_find_rsc(child_0, "httpd", NULL,
pcmk_rsc_match_current_node));
/* And then we check failure possibilities again, except passing httpd-bundle
* instead of X_0 as the first argument to native_find_rsc.
*/
// Fails because pcmk_rsc_match_current_node is required if a node is given
assert_null(native_find_rsc(httpd_bundle, "httpd-bundle-ip-192.168.122.131", cluster01, 0));
assert_null(native_find_rsc(httpd_bundle, "httpd:0", httpd_bundle_0, 0));
assert_null(native_find_rsc(httpd_bundle, "httpd-bundle-docker-0", cluster01, 0));
assert_null(native_find_rsc(httpd_bundle, "httpd-bundle-0", cluster01, 0));
/* Check that the resource is running on the node we expect. */
assert_ptr_equal(ip_0,
native_find_rsc(httpd_bundle,
"httpd-bundle-ip-192.168.122.131",
cluster01, pcmk_rsc_match_current_node));
assert_ptr_equal(child_0,
native_find_rsc(httpd_bundle, "httpd:0", httpd_bundle_0,
pcmk_rsc_match_current_node));
assert_ptr_equal(container_0,
native_find_rsc(httpd_bundle, "httpd-bundle-docker-0",
cluster01, pcmk_rsc_match_current_node));
assert_ptr_equal(remote_0,
native_find_rsc(httpd_bundle, "httpd-bundle-0", cluster01,
pcmk_rsc_match_current_node));
return false; // Do not iterate through any further replicas
}
static void
bundle_replica_rsc(void **state)
{
pe__foreach_bundle_replica(httpd_bundle, bundle_first_replica, NULL);
}
static void
clone_group_rsc(void **rsc) {
assert_non_null(mysql_clone_group);
/* Passes because NULL was passed for node, regardless of flags. */
assert_ptr_equal(mysql_clone_group, native_find_rsc(mysql_clone_group, "mysql-clone-group", NULL, 0));
assert_ptr_equal(mysql_clone_group,
native_find_rsc(mysql_clone_group, "mysql-clone-group",
NULL, pcmk_rsc_match_current_node));
assert_ptr_equal(mysql_clone_group,
native_find_rsc(mysql_clone_group, "mysql-clone-group",
NULL, pcmk_rsc_match_clone_only));
// Fails because pcmk_rsc_match_current_node is required if a node is given
assert_null(native_find_rsc(mysql_clone_group, "mysql-clone-group", cluster01, 0));
/* Passes because one of mysql-clone-group's children is running on cluster01. */
assert_ptr_equal(mysql_clone_group,
native_find_rsc(mysql_clone_group, "mysql-clone-group",
cluster01, pcmk_rsc_match_current_node));
// Fails because pcmk_rsc_match_current_node is required if a node is given
assert_null(native_find_rsc(mysql_clone_group, "mysql-clone-group", cluster02, 0));
/* Passes because one of mysql-clone-group's children is running on cluster02. */
assert_ptr_equal(mysql_clone_group,
native_find_rsc(mysql_clone_group, "mysql-clone-group",
cluster02, pcmk_rsc_match_current_node));
// Passes for previous reasons, plus includes pcmk_rsc_match_clone_only
assert_ptr_equal(mysql_clone_group,
native_find_rsc(mysql_clone_group, "mysql-clone-group",
cluster01,
pcmk_rsc_match_clone_only
|pcmk_rsc_match_current_node));
assert_ptr_equal(mysql_clone_group,
native_find_rsc(mysql_clone_group, "mysql-clone-group",
cluster02,
pcmk_rsc_match_clone_only
|pcmk_rsc_match_current_node));
}
static void
clone_group_instance_rsc(void **rsc) {
pcmk_resource_t *mysql_group_0 = NULL;
pcmk_resource_t *mysql_group_1 = NULL;
/* Find the "mysql-group:0" and "mysql-group:1" resources, members of "mysql-clone-group". */
for (GList *iter = mysql_clone_group->children; iter != NULL; iter = iter->next) {
pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data;
if (strcmp(rsc->id, "mysql-group:0") == 0) {
mysql_group_0 = rsc;
} else if (strcmp(rsc->id, "mysql-group:1") == 0) {
mysql_group_1 = rsc;
}
}
assert_non_null(mysql_group_0);
assert_non_null(mysql_group_1);
/* Passes because NULL was passed for node, regardless of flags. */
assert_ptr_equal(mysql_group_0, native_find_rsc(mysql_group_0, "mysql-group:0", NULL, 0));
assert_ptr_equal(mysql_group_0,
native_find_rsc(mysql_group_0, "mysql-group:0", NULL,
pcmk_rsc_match_current_node));
assert_ptr_equal(mysql_group_1, native_find_rsc(mysql_group_1, "mysql-group:1", NULL, 0));
assert_ptr_equal(mysql_group_1,
native_find_rsc(mysql_group_1, "mysql-group:1", NULL,
pcmk_rsc_match_current_node));
// Fails because pcmk_rsc_match_current_node is required if a node is given
assert_null(native_find_rsc(mysql_group_0, "mysql-group:0", cluster02, 0));
assert_null(native_find_rsc(mysql_group_1, "mysql-group:1", cluster01, 0));
/* Check that the resource is running on the node we expect. */
assert_ptr_equal(mysql_group_0,
native_find_rsc(mysql_group_0, "mysql-group:0", cluster02,
pcmk_rsc_match_current_node));
assert_null(native_find_rsc(mysql_group_0, "mysql-group:0", cluster01,
pcmk_rsc_match_current_node));
assert_ptr_equal(mysql_group_1,
native_find_rsc(mysql_group_1, "mysql-group:1", cluster01,
pcmk_rsc_match_current_node));
assert_null(native_find_rsc(mysql_group_1, "mysql-group:1", cluster02,
pcmk_rsc_match_current_node));
/* Passes because NULL was passed for node and base name was given, with correct flags. */
assert_ptr_equal(mysql_group_0,
native_find_rsc(mysql_group_0, "mysql-group" , NULL,
pcmk_rsc_match_clone_only));
// Passes because pcmk_rsc_match_basename matches any base name
assert_ptr_equal(mysql_group_0,
native_find_rsc(mysql_group_0, "mysql-group" , NULL,
pcmk_rsc_match_basename));
assert_ptr_equal(mysql_group_1,
native_find_rsc(mysql_group_1, "mysql-group" , NULL,
pcmk_rsc_match_basename));
// Passes because pcmk_rsc_match_anon_basename matches
assert_ptr_equal(mysql_group_0,
native_find_rsc(mysql_group_0, "mysql-group" , NULL,
pcmk_rsc_match_anon_basename));
assert_ptr_equal(mysql_group_1,
native_find_rsc(mysql_group_1, "mysql-group" , NULL,
pcmk_rsc_match_anon_basename));
/* Check that the resource is running on the node we expect. */
assert_ptr_equal(mysql_group_0,
native_find_rsc(mysql_group_0, "mysql-group", cluster02,
pcmk_rsc_match_basename
|pcmk_rsc_match_current_node));
assert_ptr_equal(mysql_group_0,
native_find_rsc(mysql_group_0, "mysql-group", cluster02,
pcmk_rsc_match_anon_basename
|pcmk_rsc_match_current_node));
assert_null(native_find_rsc(mysql_group_0, "mysql-group", cluster01,
pcmk_rsc_match_basename
|pcmk_rsc_match_current_node));
assert_null(native_find_rsc(mysql_group_0, "mysql-group", cluster01,
pcmk_rsc_match_anon_basename
|pcmk_rsc_match_current_node));
assert_ptr_equal(mysql_group_1,
native_find_rsc(mysql_group_1, "mysql-group", cluster01,
pcmk_rsc_match_basename
|pcmk_rsc_match_current_node));
assert_ptr_equal(mysql_group_1,
native_find_rsc(mysql_group_1, "mysql-group", cluster01,
pcmk_rsc_match_anon_basename
|pcmk_rsc_match_current_node));
assert_null(native_find_rsc(mysql_group_1, "mysql-group", cluster02,
pcmk_rsc_match_basename
|pcmk_rsc_match_current_node));
assert_null(native_find_rsc(mysql_group_1, "mysql-group", cluster02,
pcmk_rsc_match_anon_basename
|pcmk_rsc_match_current_node));
/* Fails because incorrect flags were given along with base name. */
assert_null(native_find_rsc(mysql_group_0, "mysql-group", NULL,
pcmk_rsc_match_current_node));
assert_null(native_find_rsc(mysql_group_1, "mysql-group", NULL,
pcmk_rsc_match_current_node));
/* And then we check failure possibilities again, except passing mysql_clone_group
* instead of mysql_group_X as the first argument to native_find_rsc.
*/
// Fails because pcmk_rsc_match_current_node is required if a node is given
assert_null(native_find_rsc(mysql_clone_group, "mysql-group:0", cluster02, 0));
assert_null(native_find_rsc(mysql_clone_group, "mysql-group:1", cluster01, 0));
/* Check that the resource is running on the node we expect. */
assert_ptr_equal(mysql_group_0,
native_find_rsc(mysql_clone_group, "mysql-group:0",
cluster02, pcmk_rsc_match_current_node));
assert_ptr_equal(mysql_group_0,
native_find_rsc(mysql_clone_group, "mysql-group",
cluster02,
pcmk_rsc_match_basename
|pcmk_rsc_match_current_node));
assert_ptr_equal(mysql_group_0,
native_find_rsc(mysql_clone_group, "mysql-group",
cluster02,
pcmk_rsc_match_anon_basename
|pcmk_rsc_match_current_node));
assert_ptr_equal(mysql_group_1,
native_find_rsc(mysql_clone_group, "mysql-group:1",
cluster01, pcmk_rsc_match_current_node));
assert_ptr_equal(mysql_group_1,
native_find_rsc(mysql_clone_group, "mysql-group",
cluster01,
pcmk_rsc_match_basename
|pcmk_rsc_match_current_node));
assert_ptr_equal(mysql_group_1,
native_find_rsc(mysql_clone_group, "mysql-group",
cluster01,
pcmk_rsc_match_anon_basename
|pcmk_rsc_match_current_node));
}
static void
clone_group_member_rsc(void **state) {
pcmk_resource_t *mysql_proxy = NULL;
/* Find the "mysql-proxy" resource, a member of "mysql-group". */
for (GList *iter = mysql_clone_group->children; iter != NULL; iter = iter->next) {
pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data;
if (strcmp(rsc->id, "mysql-group:0") == 0) {
for (GList *iter2 = rsc->children; iter2 != NULL; iter2 = iter2->next) {
pcmk_resource_t *child = (pcmk_resource_t *) iter2->data;
if (strcmp(child->id, "mysql-proxy:0") == 0) {
mysql_proxy = child;
break;
}
}
break;
}
}
assert_non_null(mysql_proxy);
/* Passes because NULL was passed for node, regardless of flags. */
assert_ptr_equal(mysql_proxy, native_find_rsc(mysql_proxy, "mysql-proxy:0", NULL, 0));
assert_ptr_equal(mysql_proxy,
native_find_rsc(mysql_proxy, "mysql-proxy:0", NULL,
pcmk_rsc_match_current_node));
/* Passes because resource's parent is a clone. */
assert_ptr_equal(mysql_proxy,
native_find_rsc(mysql_proxy, "mysql-proxy:0", NULL,
pcmk_rsc_match_clone_only));
assert_ptr_equal(mysql_proxy,
native_find_rsc(mysql_proxy, "mysql-proxy:0", cluster02,
pcmk_rsc_match_clone_only
|pcmk_rsc_match_current_node));
/* Fails because mysql-proxy:0 is not running on cluster01, even with the right flags. */
assert_null(native_find_rsc(mysql_proxy, "mysql-proxy:0", cluster01,
pcmk_rsc_match_current_node));
// Fails because pcmk_rsc_match_current_node is required if a node is given
assert_null(native_find_rsc(mysql_proxy, "mysql-proxy:0", cluster02, 0));
/* Passes because mysql-proxy:0 is running on cluster02. */
assert_ptr_equal(mysql_proxy,
native_find_rsc(mysql_proxy, "mysql-proxy:0", cluster02,
pcmk_rsc_match_current_node));
}
/* TODO: Add tests for finding on assigned node (passing a node without
* pcmk_rsc_match_current_node, after scheduling, for a resource that is
* starting/stopping/moving.
*/
PCMK__UNIT_TEST(setup, teardown,
cmocka_unit_test(bad_args),
cmocka_unit_test(primitive_rsc),
cmocka_unit_test(group_rsc),
cmocka_unit_test(inactive_group_rsc),
cmocka_unit_test(group_member_rsc),
cmocka_unit_test(inactive_group_member_rsc),
cmocka_unit_test(clone_rsc),
cmocka_unit_test(inactive_clone_rsc),
cmocka_unit_test(clone_instance_rsc),
cmocka_unit_test(renamed_rsc),
cmocka_unit_test(bundle_rsc),
cmocka_unit_test(bundle_replica_rsc),
cmocka_unit_test(clone_group_rsc),
cmocka_unit_test(clone_group_instance_rsc),
cmocka_unit_test(clone_group_member_rsc))
diff --git a/lib/pengine/tests/native/pe_base_name_eq_test.c b/lib/pengine/tests/native/pe_base_name_eq_test.c
index 68112bb4d0..0bdbbb7438 100644
--- a/lib/pengine/tests/native/pe_base_name_eq_test.c
+++ b/lib/pengine/tests/native/pe_base_name_eq_test.c
@@ -1,150 +1,150 @@
/*
* Copyright 2022-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <crm/common/unittest_internal.h>
#include <crm/common/xml.h>
#include <crm/common/scheduler.h>
#include <crm/pengine/internal.h>
#include <crm/pengine/status.h>
xmlNode *input = NULL;
pcmk_scheduler_t *scheduler = NULL;
pcmk_resource_t *exim_group, *promotable_0, *promotable_1, *dummy;
pcmk_resource_t *httpd_bundle, *mysql_group_0, *mysql_group_1;
static int
setup(void **state) {
char *path = NULL;
- crm_xml_init();
+ pcmk__xml_init();
path = crm_strdup_printf("%s/crm_mon.xml", getenv("PCMK_CTS_CLI_DIR"));
input = pcmk__xml_read(path);
free(path);
if (input == NULL) {
return 1;
}
scheduler = pe_new_working_set();
if (scheduler == NULL) {
return 1;
}
pcmk__set_scheduler_flags(scheduler,
pcmk_sched_no_counts|pcmk_sched_no_compat);
scheduler->input = input;
cluster_status(scheduler);
/* Get references to several resources we use frequently. */
for (GList *iter = scheduler->resources; iter != NULL; iter = iter->next) {
pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data;
if (strcmp(rsc->id, "dummy") == 0) {
dummy = rsc;
} else if (strcmp(rsc->id, "exim-group") == 0) {
exim_group = rsc;
} else if (strcmp(rsc->id, "httpd-bundle") == 0) {
httpd_bundle = rsc;
} else if (strcmp(rsc->id, "mysql-clone-group") == 0) {
for (GList *iter = rsc->children; iter != NULL; iter = iter->next) {
pcmk_resource_t *child = (pcmk_resource_t *) iter->data;
if (strcmp(child->id, "mysql-group:0") == 0) {
mysql_group_0 = child;
} else if (strcmp(child->id, "mysql-group:1") == 0) {
mysql_group_1 = child;
}
}
} else if (strcmp(rsc->id, "promotable-clone") == 0) {
for (GList *iter = rsc->children; iter != NULL; iter = iter->next) {
pcmk_resource_t *child = (pcmk_resource_t *) iter->data;
if (strcmp(child->id, "promotable-rsc:0") == 0) {
promotable_0 = child;
} else if (strcmp(child->id, "promotable-rsc:1") == 0) {
promotable_1 = child;
}
}
}
}
return 0;
}
static int
teardown(void **state) {
pe_free_working_set(scheduler);
return 0;
}
static void
bad_args(void **state) {
char *id = dummy->id;
assert_false(pe_base_name_eq(NULL, "dummy"));
assert_false(pe_base_name_eq(dummy, NULL));
dummy->id = NULL;
assert_false(pe_base_name_eq(dummy, "dummy"));
dummy->id = id;
}
static void
primitive_rsc(void **state) {
assert_true(pe_base_name_eq(dummy, "dummy"));
assert_false(pe_base_name_eq(dummy, "DUMMY"));
assert_false(pe_base_name_eq(dummy, "dUmMy"));
assert_false(pe_base_name_eq(dummy, "dummy0"));
assert_false(pe_base_name_eq(dummy, "dummy:0"));
}
static void
group_rsc(void **state) {
assert_true(pe_base_name_eq(exim_group, "exim-group"));
assert_false(pe_base_name_eq(exim_group, "EXIM-GROUP"));
assert_false(pe_base_name_eq(exim_group, "exim-group0"));
assert_false(pe_base_name_eq(exim_group, "exim-group:0"));
assert_false(pe_base_name_eq(exim_group, "Public-IP"));
}
static void
clone_rsc(void **state) {
assert_true(pe_base_name_eq(promotable_0, "promotable-rsc"));
assert_true(pe_base_name_eq(promotable_1, "promotable-rsc"));
assert_false(pe_base_name_eq(promotable_0, "promotable-rsc:0"));
assert_false(pe_base_name_eq(promotable_1, "promotable-rsc:1"));
assert_false(pe_base_name_eq(promotable_0, "PROMOTABLE-RSC"));
assert_false(pe_base_name_eq(promotable_1, "PROMOTABLE-RSC"));
assert_false(pe_base_name_eq(promotable_0, "Promotable-rsc"));
assert_false(pe_base_name_eq(promotable_1, "Promotable-rsc"));
}
static void
bundle_rsc(void **state) {
assert_true(pe_base_name_eq(httpd_bundle, "httpd-bundle"));
assert_false(pe_base_name_eq(httpd_bundle, "HTTPD-BUNDLE"));
assert_false(pe_base_name_eq(httpd_bundle, "httpd"));
assert_false(pe_base_name_eq(httpd_bundle, "httpd-docker-0"));
}
PCMK__UNIT_TEST(setup, teardown,
cmocka_unit_test(bad_args),
cmocka_unit_test(primitive_rsc),
cmocka_unit_test(group_rsc),
cmocka_unit_test(clone_rsc),
cmocka_unit_test(bundle_rsc))
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Tue, Jul 8, 5:06 PM (1 d, 15 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2002330
Default Alt Text
(205 KB)
Attached To
Mode
rP Pacemaker
Attached
Detach File
Event Timeline
Log In to Comment