diff --git a/include/crm/common/output_internal.h b/include/crm/common/output_internal.h
index 4f92f4a6aa..37baba9f71 100644
--- a/include/crm/common/output_internal.h
+++ b/include/crm/common/output_internal.h
@@ -1,1019 +1,1036 @@
 /*
  * Copyright 2019-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__OUTPUT_INTERNAL__H
 #  define PCMK__OUTPUT_INTERNAL__H
 
 #  include <stdbool.h>
 #  include <stdint.h>
 #  include <stdio.h>
 #  include <libxml/tree.h>
 #  include <libxml/HTMLtree.h>
 
 #  include <glib.h>
 #  include <crm/common/results.h>
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 /**
  * \file
  * \brief Formatted output for pacemaker tools
  */
 
 #if defined(PCMK__WITH_ATTRIBUTE_OUTPUT_ARGS)
 #  define PCMK__OUTPUT_ARGS(ARGS...) __attribute__((output_args(ARGS)))
 #else
 #  define PCMK__OUTPUT_ARGS(ARGS...)
 #endif
 
 typedef struct pcmk__output_s pcmk__output_t;
 
 /*!
  * \internal
  * \brief The type of a function that creates a ::pcmk__output_t.
  *
  * Instances of this type are passed to pcmk__register_format(), stored in an
  * internal data structure, and later accessed by pcmk__output_new().  For 
  * examples, see pcmk__mk_xml_output() and pcmk__mk_text_output().
  *
  * \param[in] argv The list of command line arguments.
  */
 typedef pcmk__output_t * (*pcmk__output_factory_t)(char **argv);
 
 /*!
  * \internal
  * \brief The type of a custom message formatting function.
  *
  * These functions are defined by various libraries to support formatting of
  * types aside from the basic types provided by a ::pcmk__output_t.
  *
  * The meaning of the return value will be different for each message.
  * In general, however, 0 should be returned on success and a positive value
  * on error.
  *
  * \param[in,out] out   Output object to use to display message
  * \param[in,out] args  Message-specific arguments needed
  *
  * \note These functions must not call va_start or va_end - that is done
  *       automatically before the custom formatting function is called.
  */
 typedef int (*pcmk__message_fn_t)(pcmk__output_t *out, va_list args);
 
 /*!
  * \internal
  * \brief Internal type for tracking custom messages.
  *
  * Each library can register functions that format custom message types.  These
  * are commonly used to handle some library-specific type.  Registration is
  * done by first defining a table of ::pcmk__message_entry_t structures and
  * then passing that table to pcmk__register_messages().  Separate handlers
  * can be defined for the same message, but for different formats (xml vs.
  * text).  Unknown formats will be ignored.
  *
  * Additionally, a "default" value for fmt_table can be used.  In this case,
  * fn will be registered for all supported formats.  It is also possible to
  * register a default and then override that registration with a format-specific
  * function if necessary.
  *
  * \note The ::pcmk__message_entry_t table is processed in one pass, in order,
  * from top to bottom.  This means later entries with the same message_id will
  * override previous ones.  Thus, any default entry must come before any
  * format-specific entries for the same message_id.
  */
 typedef struct pcmk__message_entry_s {
     /*!
      * \brief The message to be handled.
      *
      * This must be the same ID that is passed to the message function of
      * a ::pcmk__output_t.  Unknown message IDs will be ignored.
      */
     const char *message_id;
 
     /*!
      * \brief The format type this handler is for.
      *
      * This name must match the fmt_name of the currently active formatter in
      * order for the registered function to be called.  It is valid to have
      * multiple entries for the same message_id but with different fmt_name
      * values.
      */
     const char *fmt_name;
 
     /*!
      * \brief The function to be called for message_id given a match on
      *        fmt_name.  See comments on ::pcmk__message_fn_t.
      */
     pcmk__message_fn_t fn;
 } pcmk__message_entry_t;
 
 /*!
  * \internal
  * \brief This structure contains everything needed to add support for a
  *        single output formatter to a command line program.
  */
 typedef struct pcmk__supported_format_s {
     /*!
      * \brief The name of this output formatter, which should match the
      *        fmt_name parameter in some ::pcmk__output_t structure.
      */
     const char *name;
 
     /*!
      * \brief A function that creates a ::pcmk__output_t.
      */
     pcmk__output_factory_t create;
 
     /*!
      * \brief Format-specific command line options.  This can be NULL if
      *        no command line options should be supported.
      */
     GOptionEntry *options;
 } pcmk__supported_format_t;
 
 /* The following three blocks need to be updated each time a new base formatter
  * is added.
  */
 
 extern GOptionEntry pcmk__html_output_entries[];
 extern GOptionEntry pcmk__text_output_entries[];
 
 pcmk__output_t *pcmk__mk_html_output(char **argv);
 pcmk__output_t *pcmk__mk_log_output(char **argv);
 pcmk__output_t *pcmk__mk_none_output(char **argv);
 pcmk__output_t *pcmk__mk_text_output(char **argv);
 pcmk__output_t *pcmk__mk_xml_output(char **argv);
 
 #define PCMK__SUPPORTED_FORMAT_HTML { "html", pcmk__mk_html_output, pcmk__html_output_entries }
 #define PCMK__SUPPORTED_FORMAT_LOG  { "log", pcmk__mk_log_output, NULL }
 #define PCMK__SUPPORTED_FORMAT_NONE { PCMK_VALUE_NONE, pcmk__mk_none_output, NULL }
 #define PCMK__SUPPORTED_FORMAT_TEXT { "text", pcmk__mk_text_output, pcmk__text_output_entries }
 #define PCMK__SUPPORTED_FORMAT_XML  { "xml", pcmk__mk_xml_output, NULL }
 
 /*!
  * \brief This structure contains everything that makes up a single output
  *        formatter.
  *
  * Instances of this structure may be created by calling pcmk__output_new()
  * with the name of the desired formatter.  They should later be freed with
  * pcmk__output_free().
  */
 struct pcmk__output_s {
     /*!
      * \brief The name of this output formatter.
      */
     const char *fmt_name;
 
     /*!
      * \brief Should this formatter supress most output?
      *
      * \note This setting is not respected by all formatters.  In general,
      *       machine-readable output formats will not support this while
      *       user-oriented formats will.  Callers should use is_quiet()
      *       to test whether to print or not.
      */
     bool quiet;
 
     /*!
      * \brief A copy of the request that generated this output.
      *
      * In the case of command line usage, this would be the command line
      * arguments.  For other use cases, it could be different.
      */
     gchar *request;
 
     /*!
      * \brief Where output should be written.
      *
      * This could be a file handle, or stdout or stderr.  This is really only
      * useful internally.
      */
     FILE *dest;
 
     /*!
      * \brief Custom messages that are currently registered on this formatter.
      *
      * Keys are the string message IDs, values are ::pcmk__message_fn_t function
      * pointers.
      */
     GHashTable *messages;
 
     /*!
      * \brief Implementation-specific private data.
      *
      * Each individual formatter may have some private data useful in its
      * implementation.  This points to that data.  Callers should not rely on
      * its contents or structure.
      */
     void *priv;
 
     /*!
      * \internal
      * \brief Take whatever actions are necessary to prepare out for use.  This is
      *        called by pcmk__output_new().  End users should not need to call this.
      *
      * \note For formatted output implementers - This function should be written in
      *       such a way that it can be called repeatedly on an already initialized
      *       object without causing problems, or on a previously finished object
      *       without crashing.
      *
      * \param[in,out] out The output functions structure.
      *
      * \return true on success, false on error.
      */
     bool (*init) (pcmk__output_t *out);
 
     /*!
      * \internal
      * \brief Free the private formatter-specific data.
      *
      * This is called from pcmk__output_free() and does not typically need to be
      * called directly.
      *
      * \param[in,out] out The output functions structure.
      */
     void (*free_priv) (pcmk__output_t *out);
 
     /*!
      * \internal
      * \brief Take whatever actions are necessary to end formatted output.
      *
      * This could include flushing output to a file, but does not include freeing
      * anything.  The finish method can potentially be fairly complicated, adding
      * additional information to the internal data structures or doing whatever
      * else.  It is therefore suggested that finish only be called once.
      *
      * \note The print parameter will only affect those formatters that do all
      *       their output at the end.  Console-oriented formatters typically print
      *       a line at a time as they go, so this parameter will not affect them.
      *       Structured formatters will honor it, however.
      *
      * \note The copy_dest parameter does not apply to all formatters.  Console-
      *       oriented formatters do not build up a structure as they go, and thus
      *       do not have anything to return.  Structured formatters will honor it,
      *       however.  Note that each type of formatter will return a different
      *       type of value in this parameter.  To use this parameter, call this
      *       function like so:
      *
      * \code
      * xmlNode *dest = NULL;
      * out->finish(out, exit_code, false, (void **) &dest);
      * \endcode
      *
      * \param[in,out] out         The output functions structure.
      * \param[in]     exit_status The exit value of the whole program.
      * \param[in]     print       Whether this function should write any output.
      * \param[out]    copy_dest   A destination to store a copy of the internal
      *                            data structure for this output, or NULL if no
      *                            copy is required.  The caller should free this
      *                            memory when done with it.
      */
     void (*finish) (pcmk__output_t *out, crm_exit_t exit_status, bool print,
                     void **copy_dest);
 
     /*!
      * \internal
      * \brief Finalize output and then immediately set back up to start a new set
      *        of output.
      *
      * This is conceptually the same as calling finish and then init, though in
      * practice more be happening behind the scenes.
      *
      * \note This function differs from finish in that no exit_status is added.
      *       The idea is that the program is not shutting down, so there is not
      *       yet a final exit code.  Call finish on the last time through if this
      *       is needed.
      *
      * \param[in,out] out The output functions structure.
      */
     void (*reset) (pcmk__output_t *out);
 
     /*!
      * \internal
      * \brief Register a custom message.
      *
      * \param[in,out] out        The output functions structure.
      * \param[in]     message_id The name of the message to register.  This name
      *                           will be used as the message_id parameter to the
      *                           message function in order to call the custom
      *                           format function.
      * \param[in]     fn         The custom format function to call for message_id.
      */
     void (*register_message) (pcmk__output_t *out, const char *message_id,
                               pcmk__message_fn_t fn);
 
     /*!
      * \internal
      * \brief Call a previously registered custom message.
      *
      * \param[in,out] out        The output functions structure.
      * \param[in]     message_id The name of the message to call.  This name must
      *                           be the same as the message_id parameter of some
      *                           previous call to register_message.
      * \param[in] ...            Arguments to be passed to the registered function.
      *
      * \return A standard Pacemaker return code.  Generally: 0 if a function was
      *         registered for the message, that function was called, and returned
      *         successfully; EINVAL if no function was registered; or pcmk_rc_no_output
      *         if a function was called but produced no output.
      */
     int (*message) (pcmk__output_t *out, const char *message_id, ...);
 
     /*!
      * \internal
      * \brief Format the output of a completed subprocess.
      *
      * \param[in,out] out         The output functions structure.
      * \param[in]     exit_status The exit value of the subprocess.
      * \param[in]     proc_stdout stdout from the completed subprocess.
      * \param[in]     proc_stderr stderr from the completed subprocess.
      */
     void (*subprocess_output) (pcmk__output_t *out, int exit_status,
                                const char *proc_stdout, const char *proc_stderr);
 
     /*!
      * \internal
      * \brief Format version information.  This is useful for the --version
      *        argument of command line tools.
      *
      * \param[in,out] out      The output functions structure.
      * \param[in]     extended Add additional version information.
      */
     void (*version) (pcmk__output_t *out, bool extended);
 
     /*!
      * \internal
      * \brief Format an informational message that should be shown to
      *        to an interactive user.  Not all formatters will do this.
      *
      * \note A newline will automatically be added to the end of the format
      *       string, so callers should not include a newline.
      *
      * \note It is possible for a formatter that supports this method to
      *       still not print anything out if is_quiet returns true.
      *
      * \param[in,out] out The output functions structure.
      * \param[in]     buf The message to be printed.
      * \param[in]     ... Arguments to be formatted.
      *
      * \return A standard Pacemaker return code.  Generally: pcmk_rc_ok
      *         if output was produced and pcmk_rc_no_output if it was not.
      *         As not all formatters implement this function, those that
      *         do not will always just return pcmk_rc_no_output.
      */
     int (*info) (pcmk__output_t *out, const char *format, ...) G_GNUC_PRINTF(2, 3);
 
     /*!
      * \internal
      * \brief Like \p info() but for messages that should appear only
      *        transiently. Not all formatters will do this.
      *
      * The originally envisioned use case is for console output, where a
      * transient status-related message may be quickly overwritten by a refresh.
      *
      * \param[in,out] out     The output functions structure.
      * \param[in]     format  The format string of the message to be printed.
      * \param[in]     ...     Arguments to be formatted.
      *
      * \return A standard Pacemaker return code. Generally: \p pcmk_rc_ok if
      *         output was produced and \p pcmk_rc_no_output if it was not. As
      *         not all formatters implement this function, those that do not
      *         will always just return \p pcmk_rc_no_output.
      */
     int (*transient) (pcmk__output_t *out, const char *format, ...)
         G_GNUC_PRINTF(2, 3);
 
     /*!
      * \internal
      * \brief Format an error message that should be shown to an interactive
      *        user.  Not all formatters will do this.
      *
      * \note A newline will automatically be added to the end of the format
      *       string, so callers should not include a newline.
      *
      * \note Formatters that support this method should always generate output,
      *       even if is_quiet returns true.
      *
      * \param[in,out] out The output functions structure.
      * \param[in]     buf The message to be printed.
      * \param[in]     ... Arguments to be formatted.
      */
     void (*err) (pcmk__output_t *out, const char *format, ...) G_GNUC_PRINTF(2, 3);
 
     /*!
      * \internal
      * \brief Format already formatted XML.
      *
      * \param[in,out] out  The output functions structure.
      * \param[in]     name A name to associate with the XML.
      * \param[in]     buf  The XML in a string.
      */
     void (*output_xml) (pcmk__output_t *out, const char *name, const char *buf);
 
     /*!
      * \internal
      * \brief Start a new list of items.
      *
      * \note For text output, this corresponds to another level of indentation.  For
      *       XML output, this corresponds to wrapping any following output in another
      *       layer of tags.
      *
      * \note If singular_noun and plural_noun are non-NULL, calling end_list will
      *       result in a summary being added.
      *
      * \param[in,out] out           The output functions structure.
      * \param[in]     singular_noun When outputting the summary for a list with
      *                              one item, the noun to use.
      * \param[in]     plural_noun   When outputting the summary for a list with
      *                              more than one item, the noun to use.
      * \param[in]     format        The format string.
      * \param[in]     ...           Arguments to be formatted.
      */
     void (*begin_list) (pcmk__output_t *out, const char *singular_noun,
                         const char *plural_noun, const char *format, ...)
                         G_GNUC_PRINTF(4, 5);
 
     /*!
      * \internal
      * \brief Format a single item in a list.
      *
      * \param[in,out] out     The output functions structure.
      * \param[in]     name    A name to associate with this item.
      * \param[in]     format  The format string.
      * \param[in]     ...     Arguments to be formatted.
      */
     void (*list_item) (pcmk__output_t *out, const char *name, const char *format, ...)
                       G_GNUC_PRINTF(3, 4);
 
     /*!
      * \internal
      * \brief Increment the internal counter of the current list's length.
      *
      * Typically, this counter is maintained behind the scenes as a side effect
      * of calling list_item().  However, custom functions that maintain lists
      * some other way will need to manage this counter manually.  This is
      * useful for implementing custom message functions and should not be
      * needed otherwise.
      *
      * \param[in,out] out The output functions structure.
      */
     void (*increment_list) (pcmk__output_t *out);
 
     /*!
      * \internal
      * \brief Conclude a list.
      *
      * \note If begin_list was called with non-NULL for both the singular_noun
      *       and plural_noun arguments, this function will output a summary.
      *       Otherwise, no summary will be added.
      *
      * \param[in,out] out The output functions structure.
      */
     void (*end_list) (pcmk__output_t *out);
 
     /*!
      * \internal
      * \brief Should anything be printed to the user?
      *
      * \note This takes into account both the \p quiet value as well as the
      *       current formatter.
      *
      * \param[in,out] out The output functions structure.
      *
      * \return true if output should be supressed, false otherwise.
      */
     bool (*is_quiet) (pcmk__output_t *out);
 
     /*!
      * \internal
      * \brief Output a spacer.  Not all formatters will do this.
      *
      * \param[in,out] out The output functions structure.
      */
     void (*spacer) (pcmk__output_t *out);
 
     /*!
      * \internal
      * \brief Output a progress indicator.  This is likely only useful for
      *        plain text, console based formatters.
      *
      * \param[in,out] out  The output functions structure
      * \param[in]     end  If true, output a newline afterwards (this should
      *                     only be used the last time this function is called)
      *
      */
     void (*progress) (pcmk__output_t *out, bool end);
 
     /*!
      * \internal
      * \brief Prompt the user for input.  Not all formatters will do this.
      *
      * \note This function is part of pcmk__output_t, but unlike all other
      *       function it does not take that as an argument.  In general, a
      *       prompt will go directly to the screen and therefore bypass any
      *       need to use the formatted output code to decide where and how
      *       to display.
      *
      * \param[in]  prompt The prompt to display.  This is required.
      * \param[in]  echo   If true, echo the user's input to the screen.  Set
      *                    to false for password entry.
      * \param[out] dest   Where to store the user's response.  This is
      *                    required.
      */
     void (*prompt) (const char *prompt, bool echo, char **dest);
 };
 
 /*!
  * \internal
  * \brief Call a formatting function for a previously registered message.
  *
  * \note This function is for implementing custom formatters.  It should not
  *       be called directly.  Instead, call out->message.
  *
  * \param[in,out] out        The output functions structure.
  * \param[in]     message_id The message to be handled.  Unknown messages
  *                           will be ignored.
  * \param[in]     ...        Arguments to be passed to the registered function.
  */
 int
 pcmk__call_message(pcmk__output_t *out, const char *message_id, ...);
 
 /*!
  * \internal
  * \brief Free a ::pcmk__output_t structure that was previously created by
  *        pcmk__output_new().
  *
  * \note While the create and finish functions are designed in such a way that
  *       they can be called repeatedly, this function will completely free the
  *       memory of the object.  Once this function has been called, producing
  *       more output requires starting over from pcmk__output_new().
  *
  * \param[in,out] out         The output structure.
  */
 void pcmk__output_free(pcmk__output_t *out);
 
 /*!
  * \internal
  * \brief Create a new ::pcmk__output_t structure.
  *
  * This also registers message functions from libcrmcommon.
  *
  * \param[in,out] out      The destination of the new ::pcmk__output_t.
  * \param[in]     fmt_name How should output be formatted?
  * \param[in]     filename Where should formatted output be written to?  This
  *                         can be a filename (which will be overwritten if it
  *                         already exists), or NULL or "-" for stdout.  For no
  *                         output, pass a filename of "/dev/null".
  * \param[in]     argv     The list of command line arguments.
  *
  * \return Standard Pacemaker return code
  */
 int pcmk__output_new(pcmk__output_t **out, const char *fmt_name,
                      const char *filename, char **argv);
 
 /*!
  * \internal
  * \brief Register a new output formatter, making it available for use
  *        the same as a base formatter.
  *
  * \param[in,out] group   A ::GOptionGroup that formatted output related command
  *                        line arguments should be added to.  This can be NULL
  *                        for use outside of command line programs.
  * \param[in]     name    The name of the format.  This will be used to select a
  *                        format from command line options and for displaying help.
  * \param[in]     create  A function that creates a ::pcmk__output_t.
  * \param[in]     options Format-specific command line options.  These will be
  *                        added to the context.  This argument can also be NULL.
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__register_format(GOptionGroup *group, const char *name,
                       pcmk__output_factory_t create,
                       const GOptionEntry *options);
 
 /*!
  * \internal
  * \brief Register an entire table of output formatters at once.
  *
  * \param[in,out] group A ::GOptionGroup that formatted output related command
  *                      line arguments should be added to.  This can be NULL
  *                      for use outside of command line programs.
  * \param[in]     table An array of ::pcmk__supported_format_t which should
  *                      all be registered.  This array must be NULL-terminated.
  *
  */
 void
 pcmk__register_formats(GOptionGroup *group,
                        const pcmk__supported_format_t *table);
 
 /*!
  * \internal
  * \brief Unregister a previously registered table of custom formatting
  *        functions and destroy the internal data structures associated with them.
  */
 void
 pcmk__unregister_formats(void);
 
 /*!
  * \internal
  * \brief Register a function to handle a custom message.
  *
  * \note This function is for implementing custom formatters.  It should not
  *       be called directly.  Instead, call out->register_message.
  *
  * \param[in,out] out        The output functions structure.
  * \param[in]     message_id The message to be handled.
  * \param[in]     fn         The custom format function to call for message_id.
  */
 void
 pcmk__register_message(pcmk__output_t *out, const char *message_id,
                        pcmk__message_fn_t fn);
 
 /*!
  * \internal
  * \brief Register an entire table of custom formatting functions at once.
  *
  * This table can contain multiple formatting functions for the same message ID
  * if they are for different format types.
  *
  * \param[in,out] out   The output functions structure.
  * \param[in]     table An array of ::pcmk__message_entry_t values which should
  *                      all be registered.  This array must be NULL-terminated.
  */
 void
 pcmk__register_messages(pcmk__output_t *out,
                         const pcmk__message_entry_t *table);
 
 /* Functions that are useful for implementing custom message formatters */
 
 void pcmk__output_text_set_fancy(pcmk__output_t *out, bool enabled);
 
 /*!
  * \internal
  * \brief A printf-like function.
  *
  * This function writes to out->dest and indents the text to the current level
  * of the text formatter's nesting.  This function should be used when implementing
  * custom message functions for the text output format.  It should not be used
  * for any other purpose.
  *
  * Typically, this function should be used instead of printf.
  *
  * \param[in,out] out    The output functions structure.
  * \param[in]     format The format string.
  * \param[in]     ...    Arguments to be passed to the format string.
  */
 void
 pcmk__indented_printf(pcmk__output_t *out, const char *format, ...) G_GNUC_PRINTF(2, 3);
 
 /*!
  * \internal
  * \brief A vprintf-like function.
  *
  * This function is like pcmk__indented_printf(), except it takes a va_list instead
  * of a list of arguments.  This function should be used when implementing custom
  * functions for the text output format.  It should not be used for any other purpose.
  *
  * Typically, this function should be used instead of vprintf.
  *
  * \param[in,out] out    The output functions structure.
  * \param[in]     format The format string.
  * \param[in]     args   A list of arguments to apply to the format string.
  */
 void
 pcmk__indented_vprintf(pcmk__output_t *out, const char *format, va_list args) G_GNUC_PRINTF(2, 0);
 
 
 /*!
  * \internal
  * \brief A printf-like function.
  *
  * This function writes to out->dest without indenting the text.  This function
  * should be used when implementing custom message functions for the text output
  * format.  It should not be used for any other purpose.
  *
  * \param[in,out] out    The output functions structure.
  * \param[in]     format The format string.
  * \param[in]     ...    Arguments to be passed to the format string.
  */
 void
 pcmk__formatted_printf(pcmk__output_t *out, const char *format, ...) G_GNUC_PRINTF(2, 3);
 
 /*!
  * \internal
  * \brief A vprintf-like function.
  *
  * This function is like pcmk__formatted_printf(), except it takes a va_list instead
  * of a list of arguments.  This function should be used when implementing custom
  * message functions for the text output format.  It should not be used for any
  * other purpose.
  *
  * \param[in,out] out    The output functions structure.
  * \param[in]     format The format string.
  * \param[in]     args   A list of arguments to apply to the format string.
  */
 void
 pcmk__formatted_vprintf(pcmk__output_t *out, const char *format, va_list args) G_GNUC_PRINTF(2, 0);
 
 /*!
  * \internal
  * \brief Prompt the user for input.
  *
  * \param[in]  prompt The prompt to display
  * \param[in]  echo   If true, echo the user's input to the screen.  Set
  *                    to false for password entry.
  * \param[out] dest   Where to store the user's response.
  */
 void
 pcmk__text_prompt(const char *prompt, bool echo, char **dest);
 
 /*!
  * \internal
  * \brief Get the log level used by the formatted output logger
  *
  * \param[in] out  Output object
  *
  * \return Log level used by \p out
  */
 uint8_t
 pcmk__output_get_log_level(const pcmk__output_t *out);
 
 /*!
  * \internal
  * \brief Set the log level used by the formatted output logger.
  *
  * \param[in,out] out       The output functions structure.
  * \param[in]     log_level The log level constant (LOG_INFO, LOG_ERR, etc.)
  *                          to use.
  *
  * \note By default, LOG_INFO is used.
  * \note Almost all formatted output messages will respect this setting.
  *       However, out->err will always log at LOG_ERR.
  */
 void
 pcmk__output_set_log_level(pcmk__output_t *out, uint8_t log_level);
 
 void pcmk__output_set_log_filter(pcmk__output_t *out, const char *file,
                                  const char *function, uint32_t line,
                                  uint32_t tags);
 
 
 /*!
  * \internal
  * \brief Create and return a new XML node with the given name, as a child of the
  *        current list parent.  The new node is then added as the new list parent,
  *        meaning all subsequent nodes will be its children.  This is used when
  *        implementing custom functions.
  *
  * \param[in,out] out  The output functions structure.
  * \param[in]     name The name of the node to be created.
  * \param[in]     ...     Name/value pairs to set as XML properties.
  */
 xmlNodePtr
 pcmk__output_xml_create_parent(pcmk__output_t *out, const char *name, ...)
 G_GNUC_NULL_TERMINATED;
 
 /*!
  * \internal
  * \brief Add a copy of the given node as a child of the current list parent.
  *        This is used when implementing custom message functions.
  *
  * \param[in,out] out  The output functions structure.
  * \param[in]     node An XML node to copy as a child.
  */
 void
 pcmk__output_xml_add_node_copy(pcmk__output_t *out, xmlNodePtr node);
 
 /*!
  * \internal
  * \brief Create and return a new XML node with the given name, as a child of the
  *        current list parent.  This is used when implementing custom functions.
  *
  * \param[in,out] out  The output functions structure.
  * \param[in]     name The name of the node to be created.
  * \param[in]     ...     Name/value pairs to set as XML properties.
  */
 xmlNodePtr
 pcmk__output_create_xml_node(pcmk__output_t *out, const char *name, ...)
 G_GNUC_NULL_TERMINATED;
 
 /*!
  * \internal
  * \brief Like pcmk__output_create_xml_node(), but add the given text content to the
  *        new node.
  *
  * \param[in,out] out     The output functions structure.
  * \param[in]     name    The name of the node to be created.
  * \param[in]     content The text content of the node.
  */
 xmlNodePtr
 pcmk__output_create_xml_text_node(pcmk__output_t *out, const char *name, const char *content);
 
 /*!
  * \internal
  * \brief Push a parent XML node onto the stack.  This is used when implementing
  *        custom message functions.
  *
  * The XML output formatter maintains an internal stack to keep track of which nodes
  * are parents in order to build up the tree structure.  This function can be used
  * to temporarily push a new node onto the stack.  After calling this function, any
  * other formatting functions will have their nodes added as children of this new
  * parent.
  *
  * \param[in,out] out     The output functions structure
  * \param[in]     parent  XML node to add
  */
 void
 pcmk__output_xml_push_parent(pcmk__output_t *out, xmlNodePtr parent);
 
 /*!
  * \internal
  * \brief Pop a parent XML node onto the stack.  This is used when implementing
  *        custom message functions.
  *
  * This function removes a parent node from the stack.  See pcmk__xml_push_parent()
  * for more details.
  *
  * \note Little checking is done with this function.  Be sure you only pop parents
  * that were previously pushed.  In general, it is best to keep the code between
  * push and pop simple.
  *
  * \param[in,out] out The output functions structure.
  */
 void
 pcmk__output_xml_pop_parent(pcmk__output_t *out);
 
 /*!
  * \internal
  * \brief Peek a parent XML node onto the stack.  This is used when implementing
  *        custom message functions.
  *
  * This function peeks a parent node on stack.  See pcmk__xml_push_parent()
  * for more details. It has no side-effect and can be called for an empty stack.
  *
  * \note Little checking is done with this function.
  *
  * \param[in,out] out The output functions structure.
  *
  * \return NULL if stack is empty, otherwise the parent of the stack.
  */
 xmlNodePtr
 pcmk__output_xml_peek_parent(pcmk__output_t *out);
 
 /*!
  * \internal
  * \brief Create a new XML node consisting of the provided text inside an HTML
  *        element node of the given name.
  *
  * \param[in,out] out          The output functions structure.
  * \param[in]     element_name The name of the new HTML element.
  * \param[in]     id           The CSS ID selector to apply to this element.
  *                             If NULL, no ID is added.
  * \param[in]     class_name   The CSS class selector to apply to this element.
  *                             If NULL, no class is added.
  * \param[in]     text         The text content of the node.
  */
 xmlNodePtr
 pcmk__output_create_html_node(pcmk__output_t *out, const char *element_name, const char *id,
                               const char *class_name, const char *text);
 
 /*!
  * \internal
  * \brief Add an HTML tag to the <head> section.
  *
  * The arguments after name are a NULL-terminated list of keys and values,
  * all of which will be added as attributes to the given tag.  For instance,
  * the following code would generate the tag
  * "<meta http-equiv='refresh' content='19'>":
  *
  * \code
  * pcmk__html_add_header(PCMK__XE_META,
  *                       PCMK__XA_HTTP_EQUIV, PCMK__VALUE_REFRESH,
  *                       PCMK__XA_CONTENT, "19",
  *                       NULL);
  * \endcode
  *
  * \param[in]     name   The HTML tag for the new node.
  * \param[in]     ...    A NULL-terminated key/value list of attributes.
  */
 void
 pcmk__html_add_header(const char *name, ...)
 G_GNUC_NULL_TERMINATED;
 
 /*!
  * \internal
  * \brief Handle end-of-program error reporting
  *
  * \param[in,out] error A GError object potentially containing some error.
  *                      If NULL, do nothing.
  * \param[in,out] out   The output functions structure.  If NULL, any errors
  *                      will simply be printed to stderr.
  */
 void pcmk__output_and_clear_error(GError **error, pcmk__output_t *out);
 
 int pcmk__xml_output_new(pcmk__output_t **out, xmlNodePtr *xml);
 void pcmk__xml_output_finish(pcmk__output_t *out, crm_exit_t exit_status, xmlNodePtr *xml);
 int pcmk__log_output_new(pcmk__output_t **out);
 int pcmk__text_output_new(pcmk__output_t **out, const char *filename);
 
 /*!
  * \internal
- * \brief Enable the older style XML output used by `crm_mon -X`
+ * \brief Check whether older style XML output is enabled
  *
- * \note This function should not be used anywhere except in crm_mon.
+ * The legacy flag should be used sparingly. Its meaning depends on the context
+ * in which it's used.
  *
- * @COMPAT This can be removed when `crm_mon -X` is removed
+ * \param[in] out  Output object
+ *
+ * \return \c true if the \c legacy_xml flag is enabled for \p out, or \c false
+ *         otherwise
+ */
+// @COMPAT This can be removed when `crm_mon -X` and daemon metadata are removed
+bool pcmk__output_get_legacy_xml(pcmk__output_t *out);
+
+/*!
+ * \internal
+ * \brief Enable older style XML output
+ *
+ * The legacy flag should be used sparingly. Its meaning depends on the context
+ * in which it's used.
+ *
+ * \param[in,out] out  Output object
  */
+// @COMPAT This can be removed when `crm_mon -X` and daemon metadata are removed
 void pcmk__output_set_legacy_xml(pcmk__output_t *out);
 
 /*!
  * \internal
  * \brief Enable using the <list> element for lists
  *
  * \note This function is only used in limited places and should not be
  * used anywhere new.  We are trying to discourage and ultimately remove
  * uses of this style of list.
  *
  * @COMPAT This can be removed when the stonith_admin and crm_resource
  * schemas can be changed
  */
 void pcmk__output_enable_list_element(pcmk__output_t *out);
 
 /*!
  * \internal
  * \brief Select an updated return code for an operation on a \p pcmk__output_t
  *
  * This function helps to keep an up-to-date record of the most relevant return
  * code from a series of operations on a \p pcmk__output_t object. For example,
  * suppose the object has already produced some output, and we've saved a
  * \p pcmk_rc_ok return code. A new operation did not produce any output and
  * returned \p pcmk_rc_no_output. We can ignore the new \p pcmk_rc_no_output
  * return code and keep the previous \p pcmk_rc_ok return code.
  *
  * It prioritizes return codes as follows (from highest to lowest priority):
  * 1. Other return codes (unexpected errors)
  * 2. \p pcmk_rc_ok
  * 3. \p pcmk_rc_no_output
  *
  * \param[in] old_rc  Saved return code from \p pcmk__output_t operations
  * \param[in] new_rc  New return code from a \p pcmk__output_t operation
  *
  * \retval \p old_rc  \p new_rc is \p pcmk_rc_no_output, or \p new_rc is
  *                    \p pcmk_rc_ok and \p old_rc is not \p pcmk_rc_no_output
  * \retval \p new_rc  Otherwise
  */
 static inline int
 pcmk__output_select_rc(int old_rc, int new_rc)
 {
     switch (new_rc) {
         case pcmk_rc_no_output:
             return old_rc;
         case pcmk_rc_ok:
             switch (old_rc) {
                 case pcmk_rc_no_output:
                     return new_rc;
                 default:
                     return old_rc;
             }
         default:
             return new_rc;
     }
 }
 
 #if defined(PCMK__UNIT_TESTING)
 /* If we are building libcrmcommon_test.a, add this accessor function so we can
  * inspect the internal formatters hash table.
  */
 GHashTable *pcmk__output_formatters(void);
 #endif
 
 #define PCMK__OUTPUT_SPACER_IF(out_obj, cond)   \
     if (cond) {                                 \
         out->spacer(out);                       \
     }
 
 #define PCMK__OUTPUT_LIST_HEADER(out_obj, cond, retcode, title...)  \
     if (retcode == pcmk_rc_no_output) {                             \
         PCMK__OUTPUT_SPACER_IF(out_obj, cond);                      \
         retcode = pcmk_rc_ok;                                       \
         out_obj->begin_list(out_obj, NULL, NULL, title);            \
     }
 
 #define PCMK__OUTPUT_LIST_FOOTER(out_obj, retcode)  \
     if (retcode == pcmk_rc_ok) {                    \
         out_obj->end_list(out_obj);                 \
     }
 
 #ifdef __cplusplus
 }
 #endif
 
 #endif
diff --git a/lib/common/output_xml.c b/lib/common/output_xml.c
index b6ff6e57db..41d70011ef 100644
--- a/lib/common/output_xml.c
+++ b/lib/common/output_xml.c
@@ -1,624 +1,641 @@
 /*
  * Copyright 2019-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 <ctype.h>
 #include <stdarg.h>
 #include <stdlib.h>
 #include <stdio.h>
 #include <crm/crm.h>
 #include <glib.h>
 
 #include <crm/common/cmdline_internal.h>
 #include <crm/common/output.h>
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>    // pcmk__xml2fd
 
 typedef struct subst_s {
     const char *from;
     const char *to;
 } subst_t;
 
 static const subst_t substitutions[] = {
     { "Active Resources",
       PCMK_XE_RESOURCES, },
     { "Assignment Scores",
       PCMK_XE_ALLOCATIONS, },
     { "Assignment Scores and Utilization Information",
       PCMK_XE_ALLOCATIONS_UTILIZATIONS, },
     { "Cluster Summary",
       PCMK_XE_SUMMARY, },
     { "Current cluster status",
       PCMK_XE_CLUSTER_STATUS, },
     { "Executing Cluster Transition",
       PCMK_XE_TRANSITION, },
     { "Failed Resource Actions",
       PCMK_XE_FAILURES, },
     { "Fencing History",
       PCMK_XE_FENCE_HISTORY, },
     { "Full List of Resources",
       PCMK_XE_RESOURCES, },
     { "Inactive Resources",
       PCMK_XE_RESOURCES, },
     { "Migration Summary",
       PCMK_XE_NODE_HISTORY, },
     { "Negative Location Constraints",
       PCMK_XE_BANS, },
     { "Node Attributes",
       PCMK_XE_NODE_ATTRIBUTES, },
     { "Operations",
       PCMK_XE_NODE_HISTORY, },
     { "Resource Config",
       PCMK_XE_RESOURCE_CONFIG, },
     { "Resource Operations",
       PCMK_XE_OPERATIONS, },
     { "Revised Cluster Status",
       PCMK_XE_REVISED_CLUSTER_STATUS, },
     { "Timings",
       PCMK_XE_TIMINGS, },
     { "Transition Summary",
       PCMK_XE_ACTIONS, },
     { "Utilization Information",
       PCMK_XE_UTILIZATIONS, },
 
     { NULL, NULL }
 };
 
 /* The first several elements of this struct must be the same as the first
  * several elements of private_data_s in lib/common/output_html.c.  That
  * struct gets passed to a bunch of the pcmk__output_xml_* functions which
  * assume an XML private_data_s.  Keeping them laid out the same means this
  * still works.
  */
 typedef struct private_data_s {
     /* Begin members that must match the HTML version */
     xmlNode *root;
     GQueue *parent_q;
     GSList *errors;
     /* End members that must match the HTML version */
     bool legacy_xml;
     bool list_element;
 } private_data_t;
 
 static bool
 has_root_node(pcmk__output_t *out)
 {
     private_data_t *priv = NULL;
 
     CRM_ASSERT(out != NULL);
 
     priv = out->priv;
     return priv != NULL && priv->root != NULL;
 }
 
 static void
 add_root_node(pcmk__output_t *out)
 {
     private_data_t *priv = NULL;
 
     /* has_root_node will assert if out is NULL, so no need to do it here */
     if (has_root_node(out)) {
         return;
     }
 
     priv = out->priv;
 
     if (priv->legacy_xml) {
         priv->root = create_xml_node(NULL, PCMK_XE_CRM_MON);
         crm_xml_add(priv->root, PCMK_XA_VERSION, PACEMAKER_VERSION);
     } else {
         priv->root = create_xml_node(NULL, PCMK_XE_PACEMAKER_RESULT);
         crm_xml_add(priv->root, PCMK_XA_API_VERSION, PCMK__API_VERSION);
         crm_xml_add(priv->root, PCMK_XA_REQUEST,
                     pcmk__s(out->request, "libpacemaker"));
     }
 
     priv->parent_q = g_queue_new();
     g_queue_push_tail(priv->parent_q, priv->root);
 }
 
 static void
 xml_free_priv(pcmk__output_t *out) {
     private_data_t *priv = NULL;
 
     if (out == NULL || out->priv == NULL) {
         return;
     }
 
     priv = out->priv;
 
     if (has_root_node(out)) {
         free_xml(priv->root);
         /* The elements of parent_q are xmlNodes that are a part of the
          * priv->root document, so the above line already frees them.  Don't
          * call g_queue_free_full here.
          */
         g_queue_free(priv->parent_q);
     }
 
     g_slist_free_full(priv->errors, free);
     free(priv);
     out->priv = NULL;
 }
 
 static bool
 xml_init(pcmk__output_t *out) {
     private_data_t *priv = NULL;
 
     CRM_ASSERT(out != NULL);
 
     /* If xml_init was previously called on this output struct, just return. */
     if (out->priv != NULL) {
         return true;
     } else {
         out->priv = calloc(1, sizeof(private_data_t));
         if (out->priv == NULL) {
             return false;
         }
 
         priv = out->priv;
     }
 
     priv->errors = NULL;
 
     return true;
 }
 
 static void
 add_error_node(gpointer data, gpointer user_data) {
     char *str = (char *) data;
     xmlNodePtr node = (xmlNodePtr) user_data;
     pcmk_create_xml_text_node(node, PCMK_XE_ERROR, str);
 }
 
 static void
 xml_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_dest) {
     private_data_t *priv = NULL;
     xmlNodePtr node;
 
     CRM_ASSERT(out != NULL);
     priv = out->priv;
 
     if (priv == NULL) {
         return;
     }
 
     add_root_node(out);
 
     if (priv->legacy_xml) {
         GSList *node = priv->errors;
 
         if (exit_status != CRM_EX_OK) {
             fprintf(stderr, "%s\n", crm_exit_str(exit_status));
         }
 
         while (node != NULL) {
             fprintf(stderr, "%s\n", (char *) node->data);
             node = node->next;
         }
     } else {
         char *rc_as_str = pcmk__itoa(exit_status);
 
         node = create_xml_node(priv->root, PCMK_XE_STATUS);
         pcmk__xe_set_props(node,
                            PCMK_XA_CODE, rc_as_str,
                            PCMK_XA_MESSAGE, crm_exit_str(exit_status),
                            NULL);
 
         if (g_slist_length(priv->errors) > 0) {
             xmlNodePtr errors_node = create_xml_node(node, PCMK_XE_ERRORS);
             g_slist_foreach(priv->errors, add_error_node, (gpointer) errors_node);
         }
 
         free(rc_as_str);
     }
 
     if (print) {
         pcmk__xml2fd(fileno(out->dest), priv->root);
     }
 
     if (copy_dest != NULL) {
         *copy_dest = pcmk__xml_copy(NULL, priv->root);
     }
 }
 
 static void
 xml_reset(pcmk__output_t *out) {
     CRM_ASSERT(out != NULL);
 
     out->dest = freopen(NULL, "w", out->dest);
     CRM_ASSERT(out->dest != NULL);
 
     xml_free_priv(out);
     xml_init(out);
 }
 
 static void
 xml_subprocess_output(pcmk__output_t *out, int exit_status,
                       const char *proc_stdout, const char *proc_stderr) {
     xmlNodePtr node, child_node;
     char *rc_as_str = NULL;
 
     CRM_ASSERT(out != NULL);
 
     rc_as_str = pcmk__itoa(exit_status);
 
     node = pcmk__output_xml_create_parent(out, PCMK_XE_COMMAND,
                                           PCMK_XA_CODE, rc_as_str,
                                           NULL);
 
     if (proc_stdout != NULL) {
         child_node = pcmk_create_xml_text_node(node, PCMK_XE_OUTPUT,
                                                proc_stdout);
         crm_xml_add(child_node, PCMK_XA_SOURCE, "stdout");
     }
 
     if (proc_stderr != NULL) {
         child_node = pcmk_create_xml_text_node(node, PCMK_XE_OUTPUT,
                                                proc_stderr);
         crm_xml_add(child_node, PCMK_XA_SOURCE, "stderr");
     }
 
     free(rc_as_str);
 }
 
 static void
 xml_version(pcmk__output_t *out, bool extended) {
     const char *author = "Andrew Beekhof and the Pacemaker project "
                          "contributors";
     CRM_ASSERT(out != NULL);
 
     pcmk__output_create_xml_node(out, PCMK_XE_VERSION,
                                  PCMK_XA_PROGRAM, "Pacemaker",
                                  PCMK_XA_VERSION, PACEMAKER_VERSION,
                                  PCMK_XA_AUTHOR, author,
                                  PCMK_XA_BUILD, BUILD_VERSION,
                                  PCMK_XA_FEATURES, CRM_FEATURES,
                                  NULL);
 }
 
 G_GNUC_PRINTF(2, 3)
 static void
 xml_err(pcmk__output_t *out, const char *format, ...) {
     private_data_t *priv = NULL;
     int len = 0;
     char *buf = NULL;
     va_list ap;
 
     CRM_ASSERT(out != NULL && out->priv != NULL);
     priv = out->priv;
 
     add_root_node(out);
 
     va_start(ap, format);
     len = vasprintf(&buf, format, ap);
     CRM_ASSERT(len > 0);
     va_end(ap);
 
     priv->errors = g_slist_append(priv->errors, buf);
 }
 
 G_GNUC_PRINTF(2, 3)
 static int
 xml_info(pcmk__output_t *out, const char *format, ...) {
     return pcmk_rc_no_output;
 }
 
 static void
 xml_output_xml(pcmk__output_t *out, const char *name, const char *buf) {
     xmlNodePtr parent = NULL;
     xmlNodePtr cdata_node = NULL;
 
     CRM_ASSERT(out != NULL);
 
     parent = pcmk__output_create_xml_node(out, name, NULL);
     if (parent == NULL) {
         return;
     }
     cdata_node = xmlNewCDataBlock(parent->doc, (pcmkXmlStr) buf, strlen(buf));
     xmlAddChild(parent, cdata_node);
 }
 
 G_GNUC_PRINTF(4, 5)
 static void
 xml_begin_list(pcmk__output_t *out, const char *singular_noun, const char *plural_noun,
                const char *format, ...) {
     va_list ap;
     char *name = NULL;
     char *buf = NULL;
     int len;
     private_data_t *priv = NULL;
 
     CRM_ASSERT(out != NULL && out->priv != NULL);
     priv = out->priv;
 
     va_start(ap, format);
     len = vasprintf(&buf, format, ap);
     CRM_ASSERT(len >= 0);
     va_end(ap);
 
     for (const subst_t *s = substitutions; s->from != NULL; s++) {
         if (strcmp(s->from, buf) == 0) {
             name = g_strdup(s->to);
             break;
         }
     }
 
     if (name == NULL) {
         name = g_ascii_strdown(buf, -1);
     }
 
     if (priv->list_element) {
         pcmk__output_xml_create_parent(out, PCMK_XE_LIST,
                                        PCMK_XA_NAME, name,
                                        NULL);
     } else {
         pcmk__output_xml_create_parent(out, name, NULL);
     }
 
     g_free(name);
     free(buf);
 }
 
 G_GNUC_PRINTF(3, 4)
 static void
 xml_list_item(pcmk__output_t *out, const char *name, const char *format, ...) {
     xmlNodePtr item_node = NULL;
     va_list ap;
     char *buf = NULL;
     int len;
 
     CRM_ASSERT(out != NULL);
 
     va_start(ap, format);
     len = vasprintf(&buf, format, ap);
     CRM_ASSERT(len >= 0);
     va_end(ap);
 
     item_node = pcmk__output_create_xml_text_node(out, PCMK_XE_ITEM, buf);
 
     if (name != NULL) {
         crm_xml_add(item_node, PCMK_XA_NAME, name);
     }
 
     free(buf);
 }
 
 static void
 xml_increment_list(pcmk__output_t *out) {
     /* This function intentially left blank */
 }
 
 static void
 xml_end_list(pcmk__output_t *out) {
     private_data_t *priv = NULL;
 
     CRM_ASSERT(out != NULL && out->priv != NULL);
     priv = out->priv;
 
     if (priv->list_element) {
         char *buf = NULL;
         xmlNodePtr node;
 
         /* Do not free node here - it's still part of the document */
         node = g_queue_pop_tail(priv->parent_q);
         buf = crm_strdup_printf("%lu", xmlChildElementCount(node));
         crm_xml_add(node, PCMK_XA_COUNT, buf);
         free(buf);
     } else {
         /* Do not free this result - it's still part of the document */
         g_queue_pop_tail(priv->parent_q);
     }
 }
 
 static bool
 xml_is_quiet(pcmk__output_t *out) {
     return false;
 }
 
 static void
 xml_spacer(pcmk__output_t *out) {
     /* This function intentionally left blank */
 }
 
 static void
 xml_progress(pcmk__output_t *out, bool end) {
     /* This function intentionally left blank */
 }
 
 pcmk__output_t *
 pcmk__mk_xml_output(char **argv) {
     pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t));
 
     if (retval == NULL) {
         return NULL;
     }
 
     retval->fmt_name = "xml";
     retval->request = pcmk__quote_cmdline(argv);
 
     retval->init = xml_init;
     retval->free_priv = xml_free_priv;
     retval->finish = xml_finish;
     retval->reset = xml_reset;
 
     retval->register_message = pcmk__register_message;
     retval->message = pcmk__call_message;
 
     retval->subprocess_output = xml_subprocess_output;
     retval->version = xml_version;
     retval->info = xml_info;
     retval->transient = xml_info;
     retval->err = xml_err;
     retval->output_xml = xml_output_xml;
 
     retval->begin_list = xml_begin_list;
     retval->list_item = xml_list_item;
     retval->increment_list = xml_increment_list;
     retval->end_list = xml_end_list;
 
     retval->is_quiet = xml_is_quiet;
     retval->spacer = xml_spacer;
     retval->progress = xml_progress;
     retval->prompt = pcmk__text_prompt;
 
     return retval;
 }
 
 xmlNodePtr
 pcmk__output_xml_create_parent(pcmk__output_t *out, const char *name, ...) {
     va_list args;
     xmlNodePtr node = NULL;
 
     CRM_ASSERT(out != NULL);
     CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL);
 
     node = pcmk__output_create_xml_node(out, name, NULL);
 
     va_start(args, name);
     pcmk__xe_set_propv(node, args);
     va_end(args);
 
     pcmk__output_xml_push_parent(out, node);
     return node;
 }
 
 void
 pcmk__output_xml_add_node_copy(pcmk__output_t *out, xmlNodePtr node) {
     private_data_t *priv = NULL;
     xmlNodePtr parent = NULL;
 
     CRM_ASSERT(out != NULL && out->priv != NULL);
     CRM_ASSERT(node != NULL);
     CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return);
 
     add_root_node(out);
 
     priv = out->priv;
     parent = g_queue_peek_tail(priv->parent_q);
 
     // Shouldn't happen unless the caller popped priv->root
     CRM_CHECK(parent != NULL, return);
 
     pcmk__xml_copy(parent, node);
 }
 
 xmlNodePtr
 pcmk__output_create_xml_node(pcmk__output_t *out, const char *name, ...) {
     xmlNodePtr node = NULL;
     private_data_t *priv = NULL;
     va_list args;
 
     CRM_ASSERT(out != NULL && out->priv != NULL);
     CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL);
 
     add_root_node(out);
 
     priv = out->priv;
 
     node = create_xml_node(g_queue_peek_tail(priv->parent_q), name);
     va_start(args, name);
     pcmk__xe_set_propv(node, args);
     va_end(args);
 
     return node;
 }
 
 xmlNodePtr
 pcmk__output_create_xml_text_node(pcmk__output_t *out, const char *name, const char *content) {
     xmlNodePtr node = NULL;
 
     CRM_ASSERT(out != NULL);
     CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL);
 
     node = pcmk__output_create_xml_node(out, name, NULL);
     pcmk__xe_set_content(node, content);
     return node;
 }
 
 void
 pcmk__output_xml_push_parent(pcmk__output_t *out, xmlNodePtr parent) {
     private_data_t *priv = NULL;
 
     CRM_ASSERT(out != NULL && out->priv != NULL);
     CRM_ASSERT(parent != NULL);
     CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return);
 
     add_root_node(out);
 
     priv = out->priv;
 
     g_queue_push_tail(priv->parent_q, parent);
 }
 
 void
 pcmk__output_xml_pop_parent(pcmk__output_t *out) {
     private_data_t *priv = NULL;
 
     CRM_ASSERT(out != NULL && out->priv != NULL);
     CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return);
 
     add_root_node(out);
 
     priv = out->priv;
 
     CRM_ASSERT(g_queue_get_length(priv->parent_q) > 0);
     /* Do not free this result - it's still part of the document */
     g_queue_pop_tail(priv->parent_q);
 }
 
 xmlNodePtr
 pcmk__output_xml_peek_parent(pcmk__output_t *out) {
     private_data_t *priv = NULL;
 
     CRM_ASSERT(out != NULL && out->priv != NULL);
     CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL);
 
     add_root_node(out);
 
     priv = out->priv;
 
     /* If queue is empty NULL will be returned */
     return g_queue_peek_tail(priv->parent_q);
 }
 
+bool
+pcmk__output_get_legacy_xml(pcmk__output_t *out)
+{
+    private_data_t *priv = NULL;
+
+    CRM_ASSERT(out != NULL);
+
+    if (!pcmk__str_eq(out->fmt_name, "xml", pcmk__str_none)) {
+        return false;
+    }
+
+    CRM_ASSERT(out->priv != NULL);
+
+    priv = out->priv;
+    return priv->legacy_xml;
+}
+
 void
 pcmk__output_set_legacy_xml(pcmk__output_t *out)
 {
     private_data_t *priv = NULL;
 
     CRM_ASSERT(out != NULL);
 
     if (!pcmk__str_eq(out->fmt_name, "xml", pcmk__str_none)) {
         return;
     }
 
     CRM_ASSERT(out->priv != NULL);
 
     priv = out->priv;
     priv->legacy_xml = true;
 }
 
 void
 pcmk__output_enable_list_element(pcmk__output_t *out)
 {
     private_data_t *priv = NULL;
 
     CRM_ASSERT(out != NULL);
 
     if (!pcmk__str_eq(out->fmt_name, "xml", pcmk__str_none)) {
         return;
     }
 
     CRM_ASSERT(out->priv != NULL);
 
     priv = out->priv;
     priv->list_element = true;
 }