diff options
Diffstat (limited to 'lib/common/xml_display.c')
-rw-r--r-- | lib/common/xml_display.c | 549 |
1 files changed, 549 insertions, 0 deletions
diff --git a/lib/common/xml_display.c b/lib/common/xml_display.c new file mode 100644 index 0000000..e2d46ce --- /dev/null +++ b/lib/common/xml_display.c @@ -0,0 +1,549 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <libxml/tree.h> + +#include <crm/crm.h> +#include <crm/msg_xml.h> +#include <crm/common/xml.h> +#include <crm/common/xml_internal.h> // PCMK__XML_LOG_BASE, etc. +#include "crmcommon_private.h" + +static int show_xml_node(pcmk__output_t *out, GString *buffer, + const char *prefix, const xmlNode *data, int depth, + uint32_t options); + +// Log an XML library error +void +pcmk__log_xmllib_err(void *ctx, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + pcmk__if_tracing( + { + PCMK__XML_LOG_BASE(LOG_ERR, TRUE, + crm_abort(__FILE__, __PRETTY_FUNCTION__, + __LINE__, "xml library error", TRUE, + TRUE), + "XML Error: ", fmt, ap); + }, + { + PCMK__XML_LOG_BASE(LOG_ERR, TRUE, 0, "XML Error: ", fmt, ap); + } + ); + va_end(ap); +} + +/*! + * \internal + * \brief Output an XML comment with depth-based indentation + * + * \param[in,out] out Output object + * \param[in] data XML node to output + * \param[in] depth Current indentation level + * \param[in] options Group of \p pcmk__xml_fmt_options flags + * + * \return Standard Pacemaker return code + * + * \note This currently produces output only for text-like output objects. + */ +static int +show_xml_comment(pcmk__output_t *out, const xmlNode *data, int depth, + uint32_t options) +{ + if (pcmk_is_set(options, pcmk__xml_fmt_open)) { + int width = pcmk_is_set(options, pcmk__xml_fmt_pretty)? (2 * depth) : 0; + + return out->info(out, "%*s<!--%s-->", + width, "", (const char *) data->content); + } + return pcmk_rc_no_output; +} + +/*! + * \internal + * \brief Output an XML element in a formatted way + * + * \param[in,out] out Output object + * \param[in,out] buffer Where to build output strings + * \param[in] prefix String to prepend to every line of output + * \param[in] data XML node to output + * \param[in] depth Current indentation level + * \param[in] options Group of \p pcmk__xml_fmt_options flags + * + * \return Standard Pacemaker return code + * + * \note This is a recursive helper function for \p show_xml_node(). + * \note This currently produces output only for text-like output objects. + * \note \p buffer may be overwritten many times. The caller is responsible for + * freeing it using \p g_string_free() but should not rely on its + * contents. + */ +static int +show_xml_element(pcmk__output_t *out, GString *buffer, const char *prefix, + const xmlNode *data, int depth, uint32_t options) +{ + const char *name = crm_element_name(data); + int spaces = pcmk_is_set(options, pcmk__xml_fmt_pretty)? (2 * depth) : 0; + int rc = pcmk_rc_no_output; + + if (pcmk_is_set(options, pcmk__xml_fmt_open)) { + const char *hidden = crm_element_value(data, "hidden"); + + g_string_truncate(buffer, 0); + + for (int lpc = 0; lpc < spaces; lpc++) { + g_string_append_c(buffer, ' '); + } + pcmk__g_strcat(buffer, "<", name, NULL); + + for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL; + attr = attr->next) { + xml_node_private_t *nodepriv = attr->_private; + const char *p_name = (const char *) attr->name; + const char *p_value = pcmk__xml_attr_value(attr); + char *p_copy = NULL; + + if (pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) { + continue; + } + + // @COMPAT Remove when v1 patchsets are removed + if (pcmk_any_flags_set(options, + pcmk__xml_fmt_diff_plus + |pcmk__xml_fmt_diff_minus) + && (strcmp(XML_DIFF_MARKER, p_name) == 0)) { + continue; + } + + if ((hidden != NULL) && (p_name[0] != '\0') + && (strstr(hidden, p_name) != NULL)) { + pcmk__str_update(&p_copy, "*****"); + + } else { + p_copy = crm_xml_escape(p_value); + } + + pcmk__g_strcat(buffer, " ", p_name, "=\"", + pcmk__s(p_copy, "<null>"), "\"", NULL); + free(p_copy); + } + + if (xml_has_children(data) + && pcmk_is_set(options, pcmk__xml_fmt_children)) { + g_string_append_c(buffer, '>'); + + } else { + g_string_append(buffer, "/>"); + } + + rc = out->info(out, "%s%s%s", + pcmk__s(prefix, ""), pcmk__str_empty(prefix)? "" : " ", + buffer->str); + } + + if (!xml_has_children(data)) { + return rc; + } + + if (pcmk_is_set(options, pcmk__xml_fmt_children)) { + for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL; + child = pcmk__xml_next(child)) { + + int temp_rc = show_xml_node(out, buffer, prefix, child, depth + 1, + options + |pcmk__xml_fmt_open + |pcmk__xml_fmt_close); + rc = pcmk__output_select_rc(rc, temp_rc); + } + } + + if (pcmk_is_set(options, pcmk__xml_fmt_close)) { + int temp_rc = out->info(out, "%s%s%*s</%s>", + pcmk__s(prefix, ""), + pcmk__str_empty(prefix)? "" : " ", + spaces, "", name); + rc = pcmk__output_select_rc(rc, temp_rc); + } + + return rc; +} + +/*! + * \internal + * \brief Output an XML element or comment in a formatted way + * + * \param[in,out] out Output object + * \param[in,out] buffer Where to build output strings + * \param[in] prefix String to prepend to every line of output + * \param[in] data XML node to log + * \param[in] depth Current indentation level + * \param[in] options Group of \p pcmk__xml_fmt_options flags + * + * \return Standard Pacemaker return code + * + * \note This is a recursive helper function for \p pcmk__xml_show(). + * \note This currently produces output only for text-like output objects. + * \note \p buffer may be overwritten many times. The caller is responsible for + * freeing it using \p g_string_free() but should not rely on its + * contents. + */ +static int +show_xml_node(pcmk__output_t *out, GString *buffer, const char *prefix, + const xmlNode *data, int depth, uint32_t options) +{ + switch (data->type) { + case XML_COMMENT_NODE: + return show_xml_comment(out, data, depth, options); + case XML_ELEMENT_NODE: + return show_xml_element(out, buffer, prefix, data, depth, options); + default: + return pcmk_rc_no_output; + } +} + +/*! + * \internal + * \brief Output an XML element or comment in a formatted way + * + * \param[in,out] out Output object + * \param[in] prefix String to prepend to every line of output + * \param[in] data XML node to output + * \param[in] depth Current nesting level + * \param[in] options Group of \p pcmk__xml_fmt_options flags + * + * \return Standard Pacemaker return code + * + * \note This currently produces output only for text-like output objects. + */ +int +pcmk__xml_show(pcmk__output_t *out, const char *prefix, const xmlNode *data, + int depth, uint32_t options) +{ + int rc = pcmk_rc_no_output; + GString *buffer = NULL; + + CRM_ASSERT(out != NULL); + CRM_CHECK(depth >= 0, depth = 0); + + if (data == NULL) { + return rc; + } + + /* Allocate a buffer once, for show_xml_node() to truncate and reuse in + * recursive calls + */ + buffer = g_string_sized_new(1024); + rc = show_xml_node(out, buffer, prefix, data, depth, options); + g_string_free(buffer, TRUE); + + return rc; +} + +/*! + * \internal + * \brief Output XML portions that have been marked as changed + * + * \param[in,out] out Output object + * \param[in] data XML node to output + * \param[in] depth Current indentation level + * \param[in] options Group of \p pcmk__xml_fmt_options flags + * + * \note This is a recursive helper for \p pcmk__xml_show_changes(), showing + * changes to \p data and its children. + * \note This currently produces output only for text-like output objects. + */ +static int +show_xml_changes_recursive(pcmk__output_t *out, const xmlNode *data, int depth, + uint32_t options) +{ + /* @COMPAT: When log_data_element() is removed, we can remove the options + * argument here and instead hard-code pcmk__xml_log_pretty. + */ + xml_node_private_t *nodepriv = (xml_node_private_t *) data->_private; + int rc = pcmk_rc_no_output; + int temp_rc = pcmk_rc_no_output; + + if (pcmk_all_flags_set(nodepriv->flags, pcmk__xf_dirty|pcmk__xf_created)) { + // Newly created + return pcmk__xml_show(out, PCMK__XML_PREFIX_CREATED, data, depth, + options + |pcmk__xml_fmt_open + |pcmk__xml_fmt_children + |pcmk__xml_fmt_close); + } + + if (pcmk_is_set(nodepriv->flags, pcmk__xf_dirty)) { + // Modified or moved + bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); + int spaces = pretty? (2 * depth) : 0; + const char *prefix = PCMK__XML_PREFIX_MODIFIED; + + if (pcmk_is_set(nodepriv->flags, pcmk__xf_moved)) { + prefix = PCMK__XML_PREFIX_MOVED; + } + + // Log opening tag + rc = pcmk__xml_show(out, prefix, data, depth, + options|pcmk__xml_fmt_open); + + // Log changes to attributes + for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL; + attr = attr->next) { + const char *name = (const char *) attr->name; + + nodepriv = attr->_private; + + if (pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) { + const char *value = crm_element_value(data, name); + + temp_rc = out->info(out, "%s %*s @%s=%s", + PCMK__XML_PREFIX_DELETED, spaces, "", name, + value); + + } else if (pcmk_is_set(nodepriv->flags, pcmk__xf_dirty)) { + const char *value = crm_element_value(data, name); + + if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { + prefix = PCMK__XML_PREFIX_CREATED; + + } else if (pcmk_is_set(nodepriv->flags, pcmk__xf_modified)) { + prefix = PCMK__XML_PREFIX_MODIFIED; + + } else if (pcmk_is_set(nodepriv->flags, pcmk__xf_moved)) { + prefix = PCMK__XML_PREFIX_MOVED; + + } else { + prefix = PCMK__XML_PREFIX_MODIFIED; + } + + temp_rc = out->info(out, "%s %*s @%s=%s", + prefix, spaces, "", name, value); + } + rc = pcmk__output_select_rc(rc, temp_rc); + } + + // Log changes to children + for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL; + child = pcmk__xml_next(child)) { + temp_rc = show_xml_changes_recursive(out, child, depth + 1, + options); + rc = pcmk__output_select_rc(rc, temp_rc); + } + + // Log closing tag + temp_rc = pcmk__xml_show(out, PCMK__XML_PREFIX_MODIFIED, data, depth, + options|pcmk__xml_fmt_close); + return pcmk__output_select_rc(rc, temp_rc); + } + + // This node hasn't changed, but check its children + for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL; + child = pcmk__xml_next(child)) { + temp_rc = show_xml_changes_recursive(out, child, depth + 1, options); + rc = pcmk__output_select_rc(rc, temp_rc); + } + return rc; +} + +/*! + * \internal + * \brief Output changes to an XML node and any children + * + * \param[in,out] out Output object + * \param[in] xml XML node to output + * + * \return Standard Pacemaker return code + * + * \note This currently produces output only for text-like output objects. + */ +int +pcmk__xml_show_changes(pcmk__output_t *out, const xmlNode *xml) +{ + xml_doc_private_t *docpriv = NULL; + int rc = pcmk_rc_no_output; + int temp_rc = pcmk_rc_no_output; + + CRM_ASSERT(out != NULL); + CRM_ASSERT(xml != NULL); + CRM_ASSERT(xml->doc != NULL); + + docpriv = xml->doc->_private; + if (!pcmk_is_set(docpriv->flags, pcmk__xf_dirty)) { + return rc; + } + + for (const GList *iter = docpriv->deleted_objs; iter != NULL; + iter = iter->next) { + const pcmk__deleted_xml_t *deleted_obj = iter->data; + + if (deleted_obj->position >= 0) { + temp_rc = out->info(out, PCMK__XML_PREFIX_DELETED " %s (%d)", + deleted_obj->path, deleted_obj->position); + } else { + temp_rc = out->info(out, PCMK__XML_PREFIX_DELETED " %s", + deleted_obj->path); + } + rc = pcmk__output_select_rc(rc, temp_rc); + } + + temp_rc = show_xml_changes_recursive(out, xml, 0, pcmk__xml_fmt_pretty); + return pcmk__output_select_rc(rc, temp_rc); +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include <crm/common/logging_compat.h> +#include <crm/common/xml_compat.h> + +void +log_data_element(int log_level, const char *file, const char *function, + int line, const char *prefix, const xmlNode *data, int depth, + int legacy_options) +{ + uint32_t options = 0; + pcmk__output_t *out = NULL; + + // Confine log_level to uint8_t range + log_level = pcmk__clip_log_level(log_level); + + if (data == NULL) { + do_crm_log(log_level, "%s%sNo data to dump as XML", + pcmk__s(prefix, ""), pcmk__str_empty(prefix)? "" : " "); + return; + } + + switch (log_level) { + case LOG_NEVER: + return; + case LOG_STDOUT: + CRM_CHECK(pcmk__text_output_new(&out, NULL) == pcmk_rc_ok, return); + break; + default: + CRM_CHECK(pcmk__log_output_new(&out) == pcmk_rc_ok, return); + pcmk__output_set_log_level(out, log_level); + break; + } + + /* Map xml_log_options to pcmk__xml_fmt_options so that we can go ahead and + * start using the pcmk__xml_fmt_options in all the internal functions. + * + * xml_log_option_dirty_add and xml_log_option_diff_all are ignored by + * internal code and only used here, so they don't need to be addressed. + */ + if (pcmk_is_set(legacy_options, xml_log_option_filtered)) { + options |= pcmk__xml_fmt_filtered; + } + if (pcmk_is_set(legacy_options, xml_log_option_formatted)) { + options |= pcmk__xml_fmt_pretty; + } + if (pcmk_is_set(legacy_options, xml_log_option_full_fledged)) { + options |= pcmk__xml_fmt_full; + } + if (pcmk_is_set(legacy_options, xml_log_option_open)) { + options |= pcmk__xml_fmt_open; + } + if (pcmk_is_set(legacy_options, xml_log_option_children)) { + options |= pcmk__xml_fmt_children; + } + if (pcmk_is_set(legacy_options, xml_log_option_close)) { + options |= pcmk__xml_fmt_close; + } + if (pcmk_is_set(legacy_options, xml_log_option_text)) { + options |= pcmk__xml_fmt_text; + } + if (pcmk_is_set(legacy_options, xml_log_option_diff_plus)) { + options |= pcmk__xml_fmt_diff_plus; + } + if (pcmk_is_set(legacy_options, xml_log_option_diff_minus)) { + options |= pcmk__xml_fmt_diff_minus; + } + if (pcmk_is_set(legacy_options, xml_log_option_diff_short)) { + options |= pcmk__xml_fmt_diff_short; + } + + // Log element based on options + if (pcmk_is_set(legacy_options, xml_log_option_dirty_add)) { + CRM_CHECK(depth >= 0, depth = 0); + show_xml_changes_recursive(out, data, depth, options); + goto done; + } + + if (pcmk_is_set(options, pcmk__xml_fmt_pretty) + && (!xml_has_children(data) + || (crm_element_value(data, XML_DIFF_MARKER) != NULL))) { + + if (pcmk_is_set(options, pcmk__xml_fmt_diff_plus)) { + legacy_options |= xml_log_option_diff_all; + prefix = PCMK__XML_PREFIX_CREATED; + + } else if (pcmk_is_set(options, pcmk__xml_fmt_diff_minus)) { + legacy_options |= xml_log_option_diff_all; + prefix = PCMK__XML_PREFIX_DELETED; + } + } + + if (pcmk_is_set(options, pcmk__xml_fmt_diff_short) + && !pcmk_is_set(legacy_options, xml_log_option_diff_all)) { + + if (!pcmk_any_flags_set(options, + pcmk__xml_fmt_diff_plus + |pcmk__xml_fmt_diff_minus)) { + // Nothing will ever be logged + goto done; + } + + // Keep looking for the actual change + for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL; + child = pcmk__xml_next(child)) { + log_data_element(log_level, file, function, line, prefix, child, + depth + 1, options); + } + + } else { + pcmk__xml_show(out, prefix, data, depth, + options + |pcmk__xml_fmt_open + |pcmk__xml_fmt_children + |pcmk__xml_fmt_close); + } + +done: + out->finish(out, CRM_EX_OK, true, NULL); + pcmk__output_free(out); +} + +void +xml_log_changes(uint8_t log_level, const char *function, const xmlNode *xml) +{ + pcmk__output_t *out = NULL; + int rc = pcmk_rc_ok; + + switch (log_level) { + case LOG_NEVER: + return; + case LOG_STDOUT: + CRM_CHECK(pcmk__text_output_new(&out, NULL) == pcmk_rc_ok, return); + break; + default: + CRM_CHECK(pcmk__log_output_new(&out) == pcmk_rc_ok, return); + pcmk__output_set_log_level(out, log_level); + break; + } + rc = pcmk__xml_show_changes(out, xml); + out->finish(out, pcmk_rc2exitc(rc), true, NULL); + pcmk__output_free(out); +} + +// LCOV_EXCL_STOP +// End deprecated API |