diff --git a/include/crm/common/output_internal.h b/include/crm/common/output_internal.h
index ab84f73cc6..c46d55ae65 100644
--- a/include/crm/common/output_internal.h
+++ b/include/crm/common/output_internal.h
@@ -1,821 +1,832 @@
 /*
  * Copyright 2019-2020 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 CRM_OUTPUT__H
 #  define CRM_OUTPUT__H
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 /**
  * \file
  * \brief Formatted output for pacemaker tools
  */
 
 #  include <stdbool.h>
 #  include <stdio.h>
 #  include <libxml/tree.h>
 #  include <libxml/HTMLtree.h>
 
 #  include <glib.h>
 #  include <crm/common/results.h>
 
 #  define PCMK__API_VERSION "2.3"
 
 #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.
  *
  * \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__log_output_entries[];
 extern GOptionEntry pcmk__none_output_entries[];
 extern GOptionEntry pcmk__text_output_entries[];
 extern GOptionEntry pcmk__xml_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, pcmk__log_output_entries }
 #define PCMK__SUPPORTED_FORMAT_NONE { "none", pcmk__mk_none_output, pcmk__none_output_entries }
 #define PCMK__SUPPORTED_FORMAT_TEXT { "text", pcmk__mk_text_output, pcmk__text_output_entries }
 #define PCMK__SUPPORTED_FORMAT_XML  { "xml", pcmk__mk_xml_output, pcmk__xml_output_entries }
 
 /*!
  * \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.
      *
      * \param[in,out] out The output functions structure.
      * \param[in]     buf The message to be printed.
      * \param[in]     ... Arguments to be formatted.
      */
     void (*info) (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.
      *
      * \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 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 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 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.
  *
  * \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 0 on success or an error code on error.
  */
 int
 pcmk__register_format(GOptionGroup *group, const char *name,
                       pcmk__output_factory_t create, 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, 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, pcmk__message_entry_t *table);
 
 /* Functions that are useful for implementing custom message formatters */
 
 /*!
  * \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 should be used when implementing
  * custom message functions instead of printf.
  *
  * \param[in,out] out The output functions structure.
  */
 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 should be used when implementing custom message
  * functions 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 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 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 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 be added as a child.
  */
 void
 pcmk__output_xml_add_node(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]     node The node to be added/
  */
 void
 pcmk__output_xml_push_parent(pcmk__output_t *out, xmlNodePtr node);
 
 /*!
  * \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("meta", "http-equiv", "refresh", "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   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);
+
 #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.c b/lib/common/output.c
index 1c7ee7f44a..4a3c2fa556 100644
--- a/lib/common/output.c
+++ b/lib/common/output.c
@@ -1,153 +1,169 @@
 /*
- * Copyright 2019-2020 the Pacemaker project contributors
+ * Copyright 2019-2021 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 <libxml/tree.h>
 
 #include <crm/common/util.h>
 #include <crm/common/xml.h>
 #include <crm/common/internal.h>
 #include <crm/common/output_internal.h>
 #include <crm/common/strings_internal.h>
 #include <libxml/tree.h>
 
 static GHashTable *formatters = NULL;
 
 void
 pcmk__output_free(pcmk__output_t *out) {
     out->free_priv(out);
 
     if (out->messages != NULL) {
         g_hash_table_destroy(out->messages);
     }
 
     g_free(out->request);
     free(out);
 }
 
 int
 pcmk__output_new(pcmk__output_t **out, const char *fmt_name, const char *filename,
                  char **argv) {
     pcmk__output_factory_t create = NULL;;
 
     if (formatters == NULL) {
         return EINVAL;
     }
 
     /* If no name was given, just try "text".  It's up to each tool to register
      * what it supports so this also may not be valid.
      */
     if (fmt_name == NULL) {
         create = g_hash_table_lookup(formatters, "text");
     } else {
         create = g_hash_table_lookup(formatters, fmt_name);
     }
 
     if (create == NULL) {
         return pcmk_rc_unknown_format;
     }
 
     *out = create(argv);
     if (*out == NULL) {
         return ENOMEM;
     }
 
     if (pcmk__str_eq(filename, "-", pcmk__str_null_matches)) {
         (*out)->dest = stdout;
     } else {
         (*out)->dest = fopen(filename, "w");
         if ((*out)->dest == NULL) {
             return errno;
         }
     }
 
     (*out)->quiet = false;
 
     (*out)->messages = g_hash_table_new_full(crm_str_hash, g_str_equal, free, NULL);
 
     if ((*out)->init(*out) == false) {
         pcmk__output_free(*out);
         return ENOMEM;
     }
 
     return pcmk_rc_ok;
 }
 
 int
 pcmk__register_format(GOptionGroup *group, const char *name,
                       pcmk__output_factory_t create, GOptionEntry *options) {
     if (create == NULL) {
         return -EINVAL;
     }
 
     if (formatters == NULL) {
         formatters = g_hash_table_new_full(crm_str_hash, g_str_equal, free, NULL);
     }
 
     if (options != NULL && group != NULL) {
         g_option_group_add_entries(group, options);
     }
 
     g_hash_table_insert(formatters, strdup(name), create);
     return 0;
 }
 
 void
 pcmk__register_formats(GOptionGroup *group, pcmk__supported_format_t *formats) {
     pcmk__supported_format_t *entry = NULL;
 
     if (formats == NULL) {
         return;
     }
 
     for (entry = formats; entry->name != NULL; entry++) {
         pcmk__register_format(group, entry->name, entry->create, entry->options);
     }
 }
 
 void
 pcmk__unregister_formats() {
     if (formatters != NULL) {
         g_hash_table_destroy(formatters);
     }
 }
 
 int
 pcmk__call_message(pcmk__output_t *out, const char *message_id, ...) {
     va_list args;
     int rc = pcmk_rc_ok;
     pcmk__message_fn_t fn;
 
     fn = g_hash_table_lookup(out->messages, message_id);
     if (fn == NULL) {
         return EINVAL;
     }
 
     va_start(args, message_id);
     rc = fn(out, args);
     va_end(args);
 
     return rc;
 }
 
 void
 pcmk__register_message(pcmk__output_t *out, const char *message_id,
                        pcmk__message_fn_t fn) {
     g_hash_table_replace(out->messages, strdup(message_id), fn);
 }
 
 void
 pcmk__register_messages(pcmk__output_t *out, pcmk__message_entry_t *table) {
     pcmk__message_entry_t *entry;
 
     for (entry = table; entry->message_id != NULL; entry++) {
         if (pcmk__strcase_any_of(entry->fmt_name, "default", out->fmt_name, NULL)) {
             pcmk__register_message(out, entry->message_id, entry->fn);
         }
     }
 }
+
+void
+pcmk__output_and_clear_error(GError *error, pcmk__output_t *out)
+{
+    if (error == NULL) {
+        return;
+    }
+
+    if (out != NULL) {
+        out->err(out, "%s: %s", g_get_prgname(), error->message);
+    } else {
+        fprintf(stderr, "%s: %s\n", g_get_prgname(), error->message);
+    }
+
+    g_clear_error(&error);
+}
diff --git a/tools/crm_diff.c b/tools/crm_diff.c
index 646847d2a7..234f56e2c7 100644
--- a/tools/crm_diff.c
+++ b/tools/crm_diff.c
@@ -1,390 +1,387 @@
 /*
  * Copyright 2005-2021 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 <stdio.h>
 #include <unistd.h>
 #include <stdlib.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <sys/param.h>
 #include <sys/types.h>
 
 #include <crm/crm.h>
 #include <crm/msg_xml.h>
 #include <crm/common/cmdline_internal.h>
+#include <crm/common/output_internal.h>
 #include <crm/common/xml.h>
 #include <crm/common/ipc.h>
 #include <crm/cib.h>
 
 #define SUMMARY "Compare two Pacemaker configurations (in XML format) to produce a custom diff-like output, " \
                 "or apply such an output as a patch"
 
 struct {
     gboolean apply;
     gboolean as_cib;
     gboolean no_version;
     gboolean raw_1;
     gboolean raw_2;
     gboolean use_stdin;
     char *xml_file_1;
     char *xml_file_2;
 } options;
 
 gboolean new_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean original_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean patch_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 
 static GOptionEntry original_xml_entries[] = {
     { "original", 'o', 0, G_OPTION_ARG_STRING, &options.xml_file_1,
       "XML is contained in the named file",
       "FILE" },
     { "original-string", 'O', 0, G_OPTION_ARG_CALLBACK, original_string_cb,
       "XML is contained in the supplied string",
       "STRING" },
 
     { NULL }
 };
 
 static GOptionEntry operation_entries[] = {
     { "new", 'n', 0, G_OPTION_ARG_STRING, &options.xml_file_2,
       "Compare the original XML to the contents of the named file",
       "FILE" },
     { "new-string", 'N', 0, G_OPTION_ARG_CALLBACK, new_string_cb,
       "Compare the original XML with the contents of the supplied string",
       "STRING" },
     { "patch", 'p', 0, G_OPTION_ARG_CALLBACK, patch_cb,
       "Patch the original XML with the contents of the named file",
       "FILE" },
 
     { NULL }
 };
 
 static GOptionEntry addl_entries[] = {
     { "cib", 'c', 0, G_OPTION_ARG_NONE, &options.as_cib,
       "Compare/patch the inputs as a CIB (includes versions details)",
       NULL },
     { "stdin", 's', 0, G_OPTION_ARG_NONE, &options.use_stdin,
       "",
       NULL },
     { "no-version", 'u', 0, G_OPTION_ARG_NONE, &options.no_version,
       "Generate the difference without versions details",
       NULL },
 
     { NULL }
 };
 
 gboolean
 new_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.raw_2 = TRUE;
 
     if (options.xml_file_2 != NULL) {
         free(options.xml_file_2);
     }
 
     options.xml_file_2 = strdup(optarg);
     return TRUE;
 }
 
 gboolean
 original_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.raw_1 = TRUE;
 
     if (options.xml_file_1 != NULL) {
         free(options.xml_file_1);
     }
 
     options.xml_file_1 = strdup(optarg);
     return TRUE;
 }
 
 gboolean
 patch_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.apply = TRUE;
 
     if (options.xml_file_2 != NULL) {
         free(options.xml_file_2);
     }
 
     options.xml_file_2 = strdup(optarg);
     return TRUE;
 }
 
 static void
 print_patch(xmlNode *patch)
 {
     char *buffer = dump_xml_formatted(patch);
 
     printf("%s\n", crm_str(buffer));
     free(buffer);
     fflush(stdout);
 }
 
 static int
 apply_patch(xmlNode *input, xmlNode *patch, gboolean as_cib)
 {
     int rc;
     xmlNode *output = copy_xml(input);
 
     rc = xml_apply_patchset(output, patch, as_cib);
     if (rc != pcmk_ok) {
         fprintf(stderr, "Could not apply patch: %s\n", pcmk_strerror(rc));
         free_xml(output);
         return rc;
     }
 
     if (output != NULL) {
         const char *version;
         char *buffer;
 
         print_patch(output);
 
         version = crm_element_value(output, XML_ATTR_CRM_VERSION);
         buffer = calculate_xml_versioned_digest(output, FALSE, TRUE, version);
         crm_trace("Digest: %s\n", crm_str(buffer));
         free(buffer);
         free_xml(output);
     }
     return pcmk_ok;
 }
 
 static void
 log_patch_cib_versions(xmlNode *patch)
 {
     int add[] = { 0, 0, 0 };
     int del[] = { 0, 0, 0 };
 
     const char *fmt = NULL;
     const char *digest = NULL;
 
     xml_patch_versions(patch, add, del);
     fmt = crm_element_value(patch, "format");
     digest = crm_element_value(patch, XML_ATTR_DIGEST);
 
     if (add[2] != del[2] || add[1] != del[1] || add[0] != del[0]) {
         crm_info("Patch: --- %d.%d.%d %s", del[0], del[1], del[2], fmt);
         crm_info("Patch: +++ %d.%d.%d %s", add[0], add[1], add[2], digest);
     }
 }
 
 static void
 strip_patch_cib_version(xmlNode *patch, const char **vfields, size_t nvfields)
 {
     int format = 1;
 
     crm_element_value_int(patch, "format", &format);
     if (format == 2) {
         xmlNode *version_xml = find_xml_node(patch, "version", FALSE);
 
         if (version_xml) {
             free_xml(version_xml);
         }
 
     } else {
         int i = 0;
 
         const char *tags[] = {
             XML_TAG_DIFF_REMOVED,
             XML_TAG_DIFF_ADDED,
         };
 
         for (i = 0; i < DIMOF(tags); i++) {
             xmlNode *tmp = NULL;
             int lpc;
 
             tmp = find_xml_node(patch, tags[i], FALSE);
             if (tmp) {
                 for (lpc = 0; lpc < nvfields; lpc++) {
                     xml_remove_prop(tmp, vfields[lpc]);
                 }
 
                 tmp = find_xml_node(tmp, XML_TAG_CIB, FALSE);
                 if (tmp) {
                     for (lpc = 0; lpc < nvfields; lpc++) {
                         xml_remove_prop(tmp, vfields[lpc]);
                     }
                 }
             }
         }
     }
 }
 
 static int
 generate_patch(xmlNode *object_1, xmlNode *object_2, const char *xml_file_2,
                gboolean as_cib, gboolean no_version)
 {
     xmlNode *output = NULL;
 
     const char *vfields[] = {
         XML_ATTR_GENERATION_ADMIN,
         XML_ATTR_GENERATION,
         XML_ATTR_NUMUPDATES,
     };
 
     /* If we're ignoring the version, make the version information
      * identical, so it isn't detected as a change. */
     if (no_version) {
         int lpc;
 
         for (lpc = 0; lpc < DIMOF(vfields); lpc++) {
             crm_copy_xml_element(object_1, object_2, vfields[lpc]);
         }
     }
 
     xml_track_changes(object_2, NULL, object_2, FALSE);
     if(as_cib) {
         xml_calculate_significant_changes(object_1, object_2);
     } else {
         xml_calculate_changes(object_1, object_2);
     }
     crm_log_xml_debug(object_2, (xml_file_2? xml_file_2: "target"));
 
     output = xml_create_patchset(0, object_1, object_2, NULL, FALSE);
 
     xml_log_changes(LOG_INFO, __func__, object_2);
     xml_accept_changes(object_2);
 
     if (output == NULL) {
         return pcmk_ok;
     }
 
     patchset_process_digest(output, object_1, object_2, as_cib);
 
     if (as_cib) {
         log_patch_cib_versions(output);
 
     } else if (no_version) {
         strip_patch_cib_version(output, vfields, DIMOF(vfields));
     }
 
     xml_log_patchset(LOG_NOTICE, __func__, output);
     print_patch(output);
     free_xml(output);
     return -pcmk_err_generic;
 }
 
 static GOptionContext *
 build_arg_context(pcmk__common_args_t *args) {
     GOptionContext *context = NULL;
 
     const char *description = "Examples:\n\n"
                               "Obtain the two different configuration files by running cibadmin on the two cluster setups to compare:\n\n"
                               "\t# cibadmin --query > cib-old.xml\n\n"
                               "\t# cibadmin --query > cib-new.xml\n\n"
                               "Calculate and save the difference between the two files:\n\n"
                               "\t# crm_diff --original cib-old.xml --new cib-new.xml > patch.xml\n\n"
                               "Apply the patch to the original file:\n\n"
                               "\t# crm_diff --original cib-old.xml --patch patch.xml > updated.xml\n\n"
                               "Apply the patch to the running cluster:\n\n"
                               "\t# cibadmin --patch -x patch.xml\n";
 
     context = pcmk__build_arg_context(args, NULL, NULL, NULL);
     g_option_context_set_description(context, description);
 
     pcmk__add_arg_group(context, "xml", "Original XML:",
                         "Show original XML options", original_xml_entries);
     pcmk__add_arg_group(context, "operation", "Operation:",
                         "Show operation options", operation_entries);
     pcmk__add_arg_group(context, "additional", "Additional Options:",
                         "Show additional options", addl_entries);
     return context;
 }
 
 int
 main(int argc, char **argv)
 {
     xmlNode *object_1 = NULL;
     xmlNode *object_2 = NULL;
 
     crm_exit_t exit_code = CRM_EX_OK;
     GError *error = NULL;
 
     pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
     gchar **processed_args = pcmk__cmdline_preproc(argv, "nopNO");
     GOptionContext *context = build_arg_context(args);
 
     if (!g_option_context_parse_strv(context, &processed_args, &error)) {
         exit_code = CRM_EX_USAGE;
         goto done;
     }
 
     pcmk__cli_init_logging("crm_diff", args->verbosity);
 
     if (args->version) {
         g_strfreev(processed_args);
         pcmk__free_arg_context(context);
         /* FIXME:  When crm_diff is converted to use formatted output, this can go. */
         pcmk__cli_help('v', CRM_EX_USAGE);
     }
 
     if (options.apply && options.no_version) {
         fprintf(stderr, "warning: -u/--no-version ignored with -p/--patch\n");
     } else if (options.as_cib && options.no_version) {
         fprintf(stderr, "error: -u/--no-version incompatible with -c/--cib\n");
         exit_code = CRM_EX_USAGE;
         goto done;
     }
 
     if (options.raw_1) {
         object_1 = string2xml(options.xml_file_1);
 
     } else if (options.use_stdin) {
         fprintf(stderr, "Input first XML fragment:");
         object_1 = stdin2xml();
 
     } else if (options.xml_file_1 != NULL) {
         object_1 = filename2xml(options.xml_file_1);
     }
 
     if (options.raw_2) {
         object_2 = string2xml(options.xml_file_2);
 
     } else if (options.use_stdin) {
         fprintf(stderr, "Input second XML fragment:");
         object_2 = stdin2xml();
 
     } else if (options.xml_file_2 != NULL) {
         object_2 = filename2xml(options.xml_file_2);
     }
 
     if (object_1 == NULL) {
         fprintf(stderr, "Could not parse the first XML fragment\n");
         exit_code = CRM_EX_DATAERR;
         goto done;
     }
     if (object_2 == NULL) {
         fprintf(stderr, "Could not parse the second XML fragment\n");
         exit_code = CRM_EX_DATAERR;
         goto done;
     }
 
     if (options.apply) {
         int ret = apply_patch(object_1, object_2, options.as_cib);
         exit_code = crm_errno2exit(ret);
     } else {
         int ret = generate_patch(object_1, object_2, options.xml_file_2, options.as_cib, options.no_version);
         exit_code = crm_errno2exit(ret);
     }
 
 done:
     g_strfreev(processed_args);
     pcmk__free_arg_context(context);
     free(options.xml_file_1);
     free(options.xml_file_2);
     free_xml(object_1);
     free_xml(object_2);
 
-    if (error != NULL) {
-        fprintf(stderr, "%s: %s\n", g_get_prgname(), error->message);
-        g_clear_error(&error);
-    }
-
+    pcmk__output_and_clear_error(error, NULL);
     return exit_code;
 }
diff --git a/tools/crm_error.c b/tools/crm_error.c
index 974edf671d..2c1b78f583 100644
--- a/tools/crm_error.c
+++ b/tools/crm_error.c
@@ -1,158 +1,155 @@
 /*
  * Copyright 2012-2021 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/cmdline_internal.h>
+#include <crm/common/output_internal.h>
 #include <crm/common/strings_internal.h>
 
 #include <crm/crm.h>
 
 #define SUMMARY "crm_error - display name or description of a Pacemaker error code"
 
 struct {
     gboolean as_exit_code;
     gboolean as_rc;
     gboolean with_name;
     gboolean do_list;
 } options;
 
 static GOptionEntry entries[] = {
     { "name", 'n', 0, G_OPTION_ARG_NONE, &options.with_name,
       "Show error's name with its description (useful for looking for sources "
       "of the error in source code)",
        NULL },
     { "list", 'l', 0, G_OPTION_ARG_NONE, &options.do_list,
       "Show all known errors",
       NULL },
     { "exit", 'X', 0, G_OPTION_ARG_NONE, &options.as_exit_code,
       "Interpret as exit code rather than legacy function return value",
       NULL },
     { "rc", 'r', 0, G_OPTION_ARG_NONE, &options.as_rc,
       "Interpret as return code rather than legacy function return value",
       NULL },
 
     { NULL }
 };
 
 static void
 get_strings(int rc, const char **name, const char **str)
 {
     if (options.as_exit_code) {
         *str = crm_exit_str((crm_exit_t) rc);
         *name = crm_exit_name(rc);
     } else if (options.as_rc) {
         *str = pcmk_rc_str(rc);
         *name = pcmk_rc_name(rc);
     } else {
         *str = pcmk_strerror(rc);
         *name = pcmk_errorname(rc);
     }
 }
 
 
 static GOptionContext *
 build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) {
     GOptionContext *context = NULL;
 
     context = pcmk__build_arg_context(args, NULL, group, "-- <rc> [...]");
     pcmk__add_main_args(context, entries);
     return context;
 }
 
 int
 main(int argc, char **argv)
 {
     crm_exit_t exit_code = CRM_EX_OK;
     int rc = pcmk_rc_ok;
     int lpc;
     const char *name = NULL;
     const char *desc = NULL;
 
     GError *error = NULL;
 
     GOptionGroup *output_group = NULL;
     pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
     gchar **processed_args = pcmk__cmdline_preproc(argv, "lrnX");
     GOptionContext *context = build_arg_context(args, &output_group);
 
     if (!g_option_context_parse_strv(context, &processed_args, &error)) {
         exit_code = CRM_EX_USAGE;
         goto done;
     }
 
     pcmk__cli_init_logging("crm_error", args->verbosity);
 
     if (args->version) {
         g_strfreev(processed_args);
         pcmk__free_arg_context(context);
         /* FIXME:  When crm_error is converted to use formatted output, this can go. */
         pcmk__cli_help('v', CRM_EX_USAGE);
     }
 
     if (options.do_list) {
         int start, end, width;
 
         // 256 is a hacky magic number that "should" be enough
         if (options.as_rc) {
             start = pcmk_rc_error - 256;
             end = PCMK_CUSTOM_OFFSET;
             width = 4;
         } else {
             start = 0;
             end = 256;
             width = 3;
         }
 
         for (rc = start; rc < end; rc++) {
             if (rc == (pcmk_rc_error + 1)) {
                 // Values in between are reserved for callers, no use iterating
                 rc = pcmk_rc_ok;
             }
             get_strings(rc, &name, &desc);
             if (pcmk__str_eq(name, "Unknown", pcmk__str_null_matches) || !strcmp(name, "CRM_EX_UNKNOWN")) {
                 // Undefined
             } else if(options.with_name) {
                 printf("% .*d: %-26s  %s\n", width, rc, name, desc);
             } else {
                 printf("% .*d: %s\n", width, rc, desc);
             }
         }
 
     } else {
         if (g_strv_length(processed_args) < 2) {
             char *help = g_option_context_get_help(context, TRUE, NULL);
             fprintf(stderr, "%s", help);
             g_free(help);
             exit_code = CRM_EX_USAGE;
             goto done;
         }
 
         /* Skip #1 because that's the program name. */
         for (lpc = 1; processed_args[lpc] != NULL; lpc++) {
             rc = crm_atoi(processed_args[lpc], NULL);
             get_strings(rc, &name, &desc);
             if (options.with_name) {
                 printf("%s - %s\n", name, desc);
             } else {
                 printf("%s\n", desc);
             }
         }
     }
 
  done:
     g_strfreev(processed_args);
     pcmk__free_arg_context(context);
 
-    if (error != NULL) {
-        fprintf(stderr, "%s: %s\n", g_get_prgname(), error->message);
-        g_clear_error(&error);
-    }
-
+    pcmk__output_and_clear_error(error, NULL);
     return exit_code;
 }
diff --git a/tools/crm_node.c b/tools/crm_node.c
index cce6425f65..b53d810f5f 100644
--- a/tools/crm_node.c
+++ b/tools/crm_node.c
@@ -1,601 +1,598 @@
 /*
  * Copyright 2004-2021 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 <stdio.h>
 #include <stdlib.h>
 #include <errno.h>
 #include <sys/types.h>
 
 #include <crm/crm.h>
 #include <crm/common/cmdline_internal.h>
+#include <crm/common/output_internal.h>
 #include <crm/common/mainloop.h>
 #include <crm/msg_xml.h>
 #include <crm/cib.h>
 #include <crm/common/ipc_controld.h>
 #include <crm/common/attrd_internal.h>
 
 #define SUMMARY "crm_node - Tool for displaying low-level node information"
 
 struct {
     gboolean corosync;
     gboolean dangerous_cmd;
     gboolean force_flag;
     char command;
     int nodeid;
     char *target_uname;
 } options = {
     .command = '\0',
     .force_flag = FALSE
 };
 
 gboolean command_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean name_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean remove_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 
 static GMainLoop *mainloop = NULL;
 static crm_exit_t exit_code = CRM_EX_OK;
 
 #define INDENT "                           "
 
 static GOptionEntry command_entries[] = {
     { "cluster-id", 'i', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Display this node's cluster id",
       NULL },
     { "list", 'l', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Display all known members (past and present) of this cluster",
       NULL },
     { "name", 'n', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Display the name used by the cluster for this node",
       NULL },
     { "partition", 'p', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Display the members of this partition",
       NULL },
     { "quorum", 'q', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Display a 1 if our partition has quorum, 0 if not",
       NULL },
     { "name-for-id", 'N', 0, G_OPTION_ARG_CALLBACK, name_cb,
       "Display the name used by the cluster for the node with the specified ID",
       "ID" },
     { "remove", 'R', 0, G_OPTION_ARG_CALLBACK, remove_cb,
       "(Advanced) Remove the (stopped) node with the specified name from Pacemaker's\n"
       INDENT "configuration and caches (the node must already have been removed from\n"
       INDENT "the underlying cluster stack configuration",
       "NAME" },
 
     { NULL }
 };
 
 static GOptionEntry addl_entries[] = {
     { "force", 'f', 0, G_OPTION_ARG_NONE, &options.force_flag,
       NULL,
       NULL },
 #if SUPPORT_COROSYNC
     /* Unused and deprecated */
     { "corosync", 'C', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.corosync,
       NULL,
       NULL },
 #endif
 
     // @TODO add timeout option for when IPC replies are needed
 
     { NULL }
 };
 
 gboolean
 command_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     if (pcmk__str_eq("-i", option_name, pcmk__str_casei) || pcmk__str_eq("--cluster-id", option_name, pcmk__str_casei)) {
         options.command = 'i';
     } else if (pcmk__str_eq("-l", option_name, pcmk__str_casei) || pcmk__str_eq("--list", option_name, pcmk__str_casei)) {
         options.command = 'l';
     } else if (pcmk__str_eq("-n", option_name, pcmk__str_casei) || pcmk__str_eq("--name", option_name, pcmk__str_casei)) {
         options.command = 'n';
     } else if (pcmk__str_eq("-p", option_name, pcmk__str_casei) || pcmk__str_eq("--partition", option_name, pcmk__str_casei)) {
         options.command = 'p';
     } else if (pcmk__str_eq("-q", option_name, pcmk__str_casei) || pcmk__str_eq("--quorum", option_name, pcmk__str_casei)) {
         options.command = 'q';
     } else {
         g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_INVALID_PARAM, "Unknown param passed to command_cb: %s\n", option_name);
         return FALSE;
     }
 
     return TRUE;
 }
 
 gboolean
 name_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.command = 'N';
     options.nodeid = crm_parse_int(optarg, NULL);
     return TRUE;
 }
 
 gboolean
 remove_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     if (optarg == NULL) {
         crm_err("-R option requires an argument");
         g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_INVALID_PARAM, "-R option requires an argument");
         return FALSE;
     }
 
     options.command = 'R';
     options.dangerous_cmd = TRUE;
     options.target_uname = strdup(optarg);
     return TRUE;
 }
 
 static gint
 sort_node(gconstpointer a, gconstpointer b)
 {
     const pcmk_controld_api_node_t *node_a = a;
     const pcmk_controld_api_node_t *node_b = b;
 
     return pcmk_numeric_strcasecmp((node_a->uname? node_a->uname : ""),
                                    (node_b->uname? node_b->uname : ""));
 }
 
 static void
 controller_event_cb(pcmk_ipc_api_t *controld_api,
                     enum pcmk_ipc_event event_type, crm_exit_t status,
                     void *event_data, void *user_data)
 {
     pcmk_controld_api_reply_t *reply = event_data;
 
     switch (event_type) {
         case pcmk_ipc_event_disconnect:
             if (exit_code == CRM_EX_DISCONNECT) { // Unexpected
                 fprintf(stderr, "error: Lost connection to controller\n");
             }
             goto done;
             break;
 
         case pcmk_ipc_event_reply:
             break;
 
         default:
             return;
     }
 
     if (status != CRM_EX_OK) {
         fprintf(stderr, "error: Bad reply from controller: %s\n",
                 crm_exit_str(status));
         goto done;
     }
 
     // Parse desired info from reply and display to user
     switch (options.command) {
         case 'i':
             if (reply->reply_type != pcmk_controld_reply_info) {
                 fprintf(stderr,
                         "error: Unknown reply type %d from controller\n",
                         reply->reply_type);
                 goto done;
             }
             if (reply->data.node_info.id == 0) {
                 fprintf(stderr,
                         "error: Controller reply did not contain node ID\n");
                 exit_code = CRM_EX_PROTOCOL;
                 goto done;
             }
             printf("%d\n", reply->data.node_info.id);
             break;
 
         case 'n':
         case 'N':
             if (reply->reply_type != pcmk_controld_reply_info) {
                 fprintf(stderr,
                         "error: Unknown reply type %d from controller\n",
                         reply->reply_type);
                 goto done;
             }
             if (reply->data.node_info.uname == NULL) {
                 fprintf(stderr, "Node is not known to cluster\n");
                 exit_code = CRM_EX_NOHOST;
                 goto done;
             }
             printf("%s\n", reply->data.node_info.uname);
             break;
 
         case 'q':
             if (reply->reply_type != pcmk_controld_reply_info) {
                 fprintf(stderr,
                         "error: Unknown reply type %d from controller\n",
                         reply->reply_type);
                 goto done;
             }
             printf("%d\n", reply->data.node_info.have_quorum);
             if (!(reply->data.node_info.have_quorum)) {
                 exit_code = CRM_EX_QUORUM;
                 goto done;
             }
             break;
 
         case 'l':
         case 'p':
             if (reply->reply_type != pcmk_controld_reply_nodes) {
                 fprintf(stderr,
                         "error: Unknown reply type %d from controller\n",
                         reply->reply_type);
                 goto done;
             }
             reply->data.nodes = g_list_sort(reply->data.nodes, sort_node);
             for (GList *node_iter = reply->data.nodes;
                  node_iter != NULL; node_iter = node_iter->next) {
 
                 pcmk_controld_api_node_t *node = node_iter->data;
                 const char *uname = (node->uname? node->uname : "");
                 const char *state = (node->state? node->state : "");
 
                 if (options.command == 'l') {
                     printf("%lu %s %s\n",
                            (unsigned long) node->id, uname, state);
 
                 // i.e. CRM_NODE_MEMBER, but we don't want to include cluster.h
                 } else if (!strcmp(state, "member")) {
                     printf("%s ", uname);
                 }
             }
             if (options.command == 'p') {
                 printf("\n");
             }
             break;
 
         default:
             fprintf(stderr, "internal error: Controller reply not expected\n");
             exit_code = CRM_EX_SOFTWARE;
             goto done;
     }
 
     // Success
     exit_code = CRM_EX_OK;
 done:
     pcmk_disconnect_ipc(controld_api);
     pcmk_quit_main_loop(mainloop, 10);
 }
 
 static void
 run_controller_mainloop(uint32_t nodeid, bool list_nodes)
 {
     pcmk_ipc_api_t *controld_api = NULL;
     int rc;
 
     // Set disconnect exit code to handle unexpected disconnects
     exit_code = CRM_EX_DISCONNECT;
 
     // Create controller IPC object
     rc = pcmk_new_ipc_api(&controld_api, pcmk_ipc_controld);
     if (rc != pcmk_rc_ok) {
         fprintf(stderr, "error: Could not connect to controller: %s\n",
                 pcmk_rc_str(rc));
         return;
     }
     pcmk_register_ipc_callback(controld_api, controller_event_cb, NULL);
 
     // Connect to controller
     rc = pcmk_connect_ipc(controld_api, pcmk_ipc_dispatch_main);
     if (rc != pcmk_rc_ok) {
         fprintf(stderr, "error: Could not connect to controller: %s\n",
                 pcmk_rc_str(rc));
         exit_code = pcmk_rc2exitc(rc);
         return;
     }
 
     if (list_nodes) {
         rc = pcmk_controld_api_list_nodes(controld_api);
     } else {
         rc = pcmk_controld_api_node_info(controld_api, nodeid);
     }
     if (rc != pcmk_rc_ok) {
         fprintf(stderr, "error: Could not ping controller: %s\n",
                 pcmk_rc_str(rc));
         pcmk_disconnect_ipc(controld_api);
         exit_code = pcmk_rc2exitc(rc);
         return;
     }
 
     // Run main loop to get controller reply via controller_event_cb()
     mainloop = g_main_loop_new(NULL, FALSE);
     g_main_loop_run(mainloop);
     g_main_loop_unref(mainloop);
     mainloop = NULL;
     pcmk_free_ipc_api(controld_api);
 }
 
 static void
 print_node_name(void)
 {
     // Check environment first (i.e. when called by resource agent)
     const char *name = getenv("OCF_RESKEY_" CRM_META "_" XML_LRM_ATTR_TARGET);
 
     if (name != NULL) {
         printf("%s\n", name);
         exit_code = CRM_EX_OK;
         return;
 
     } else {
         // Otherwise ask the controller
         run_controller_mainloop(0, false);
     }
 }
 
 static int
 cib_remove_node(long id, const char *name)
 {
     int rc;
     cib_t *cib = NULL;
     xmlNode *node = NULL;
     xmlNode *node_state = NULL;
 
     crm_trace("Removing %s from the CIB", name);
 
     if(name == NULL && id == 0) {
         return -ENOTUNIQ;
     }
 
     node = create_xml_node(NULL, XML_CIB_TAG_NODE);
     node_state = create_xml_node(NULL, XML_CIB_TAG_STATE);
 
     crm_xml_add(node, XML_ATTR_UNAME, name);
     crm_xml_add(node_state, XML_ATTR_UNAME, name);
     if (id > 0) {
         crm_xml_set_id(node, "%ld", id);
         crm_xml_add(node_state, XML_ATTR_ID, ID(node));
     }
 
     cib = cib_new();
     cib->cmds->signon(cib, crm_system_name, cib_command);
 
     rc = cib->cmds->remove(cib, XML_CIB_TAG_NODES, node, cib_sync_call);
     if (rc != pcmk_ok) {
         printf("Could not remove %s[%ld] from " XML_CIB_TAG_NODES ": %s",
                 name, id, pcmk_strerror(rc));
     }
     rc = cib->cmds->remove(cib, XML_CIB_TAG_STATUS, node_state, cib_sync_call);
     if (rc != pcmk_ok) {
         printf("Could not remove %s[%ld] from " XML_CIB_TAG_STATUS ": %s",
                 name, id, pcmk_strerror(rc));
     }
 
     cib->cmds->signoff(cib);
     cib_delete(cib);
     return rc;
 }
 
 static int
 controller_remove_node(const char *node_name, long nodeid)
 {
     pcmk_ipc_api_t *controld_api = NULL;
     int rc;
 
     // Create controller IPC object
     rc = pcmk_new_ipc_api(&controld_api, pcmk_ipc_controld);
     if (rc != pcmk_rc_ok) {
         fprintf(stderr, "error: Could not connect to controller: %s\n",
                 pcmk_rc_str(rc));
         return ENOTCONN;
     }
 
     // Connect to controller (without main loop)
     rc = pcmk_connect_ipc(controld_api, pcmk_ipc_dispatch_sync);
     if (rc != pcmk_rc_ok) {
         fprintf(stderr, "error: Could not connect to controller: %s\n",
                 pcmk_rc_str(rc));
         pcmk_free_ipc_api(controld_api);
         return rc;
     }
 
     rc = pcmk_ipc_purge_node(controld_api, node_name, nodeid);
     if (rc != pcmk_rc_ok) {
         fprintf(stderr,
                 "error: Could not clear node from controller's cache: %s\n",
                 pcmk_rc_str(rc));
     }
 
     pcmk_free_ipc_api(controld_api);
     return pcmk_rc_ok;
 }
 
 static int
 tools_remove_node_cache(const char *node_name, long nodeid, const char *target)
 {
     int rc = -1;
     crm_ipc_t *conn = NULL;
     xmlNode *cmd = NULL;
 
     conn = crm_ipc_new(target, 0);
     if (!conn) {
         return -ENOTCONN;
     }
     if (!crm_ipc_connect(conn)) {
         crm_perror(LOG_ERR, "Connection to %s failed", target);
         crm_ipc_destroy(conn);
         return -ENOTCONN;
     }
 
     crm_trace("Removing %s[%ld] from the %s membership cache",
               node_name, nodeid, target);
 
     if(pcmk__str_eq(target, T_ATTRD, pcmk__str_casei)) {
         cmd = create_xml_node(NULL, __func__);
 
         crm_xml_add(cmd, F_TYPE, T_ATTRD);
         crm_xml_add(cmd, F_ORIG, crm_system_name);
 
         crm_xml_add(cmd, PCMK__XA_TASK, PCMK__ATTRD_CMD_PEER_REMOVE);
         crm_xml_add(cmd, PCMK__XA_ATTR_NODE_NAME, node_name);
 
         if (nodeid > 0) {
             crm_xml_add_int(cmd, PCMK__XA_ATTR_NODE_ID, (int) nodeid);
         }
 
     } else { // Fencer or pacemakerd
         cmd = create_request(CRM_OP_RM_NODE_CACHE, NULL, NULL, target,
                              crm_system_name, NULL);
         if (nodeid > 0) {
             crm_xml_set_id(cmd, "%ld", nodeid);
         }
         crm_xml_add(cmd, XML_ATTR_UNAME, node_name);
     }
 
     rc = crm_ipc_send(conn, cmd, 0, 0, NULL);
     crm_debug("%s peer cache cleanup for %s (%ld): %d",
               target, node_name, nodeid, rc);
 
     if (rc > 0) {
         // @TODO Should this be done just once after all the rest?
         rc = cib_remove_node(nodeid, node_name);
     }
 
     if (conn) {
         crm_ipc_close(conn);
         crm_ipc_destroy(conn);
     }
     free_xml(cmd);
     return rc > 0 ? 0 : rc;
 }
 
 static void
 remove_node(const char *target_uname)
 {
     int rc;
     int d = 0;
     long nodeid = 0;
     const char *node_name = NULL;
     char *endptr = NULL;
     const char *daemons[] = {
         "stonith-ng",
         T_ATTRD,
         CRM_SYSTEM_MCP,
     };
 
     // Check whether node was specified by name or numeric ID
     errno = 0;
     nodeid = strtol(target_uname, &endptr, 10);
     if ((errno != 0) || (endptr == target_uname) || (*endptr != '\0')
         || (nodeid <= 0)) {
         // It's not a positive integer, so assume it's a node name
         nodeid = 0;
         node_name = target_uname;
     }
 
     rc = controller_remove_node(node_name, nodeid);
     if (rc != pcmk_rc_ok) {
         exit_code = pcmk_rc2exitc(rc);
         return;
     }
 
     for (d = 0; d < DIMOF(daemons); d++) {
         if (tools_remove_node_cache(node_name, nodeid, daemons[d])) {
             crm_err("Failed to connect to %s to remove node '%s'",
                     daemons[d], target_uname);
             exit_code = CRM_EX_ERROR;
             return;
         }
     }
     exit_code = CRM_EX_OK;
 }
 
 static GOptionContext *
 build_arg_context(pcmk__common_args_t *args, GOptionGroup *group) {
     GOptionContext *context = NULL;
 
     GOptionEntry extra_prog_entries[] = {
         { "quiet", 'Q', 0, G_OPTION_ARG_NONE, &(args->quiet),
           "Be less descriptive in output.",
           NULL },
 
         { NULL }
     };
 
     context = pcmk__build_arg_context(args, NULL, &group, NULL);
 
     /* Add the -q option, which cannot be part of the globally supported options
      * because some tools use that flag for something else.
      */
     pcmk__add_main_args(context, extra_prog_entries);
 
     pcmk__add_arg_group(context, "commands", "Commands:",
                         "Show command help", command_entries);
     pcmk__add_arg_group(context, "additional", "Additional Options:",
                         "Show additional options", addl_entries);
     return context;
 }
 
 int
 main(int argc, char **argv)
 {
     GError *error = NULL;
 
     GOptionGroup *output_group = NULL;
     pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
     gchar **processed_args = pcmk__cmdline_preproc(argv, "NR");
     GOptionContext *context = build_arg_context(args, output_group);
 
     if (!g_option_context_parse_strv(context, &processed_args, &error)) {
         exit_code = CRM_EX_USAGE;
         goto done;
     }
 
     pcmk__cli_init_logging("crm_node", args->verbosity);
 
     if (args->version) {
         g_strfreev(processed_args);
         pcmk__free_arg_context(context);
         /* FIXME:  When crm_node is converted to use formatted output, this can go. */
         pcmk__cli_help('v', CRM_EX_USAGE);
     }
 
     if (options.command == 0) {
         char *help = g_option_context_get_help(context, TRUE, NULL);
 
         fprintf(stderr, "%s", help);
         g_free(help);
         exit_code = CRM_EX_USAGE;
         goto done;
     }
 
     if (options.dangerous_cmd && options.force_flag == FALSE) {
         fprintf(stderr, "The supplied command is considered dangerous."
                 "  To prevent accidental destruction of the cluster,"
                 " the --force flag is required in order to proceed.\n");
         exit_code = CRM_EX_USAGE;
         goto done;
     }
 
     switch (options.command) {
         case 'n':
             print_node_name();
             break;
         case 'R':
             remove_node(options.target_uname);
             break;
         case 'i':
         case 'q':
         case 'N':
             run_controller_mainloop(options.nodeid, false);
             break;
         case 'l':
         case 'p':
             run_controller_mainloop(0, true);
             break;
         default:
             break;
     }
 
 done:
     g_strfreev(processed_args);
     pcmk__free_arg_context(context);
 
-    if (error != NULL) {
-        fprintf(stderr, "%s: %s\n", g_get_prgname(), error->message);
-        g_clear_error(&error);
-    }
-
+    pcmk__output_and_clear_error(error, NULL);
     return crm_exit(exit_code);
 }
diff --git a/tools/crm_resource.c b/tools/crm_resource.c
index a4486002f5..564600ea29 100644
--- a/tools/crm_resource.c
+++ b/tools/crm_resource.c
@@ -1,2098 +1,2091 @@
 /*
  * Copyright 2004-2021 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_resource.h>
 #include <crm/lrmd_internal.h>
 #include <crm/common/cmdline_internal.h>
+#include <crm/common/output_internal.h>
 #include <crm/common/lists_internal.h>
 #include <pacemaker-internal.h>
 
 #include <sys/param.h>
 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 #include <stdlib.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <libgen.h>
 #include <time.h>
 
 #include <crm/crm.h>
 #include <crm/stonith-ng.h>
 #include <crm/common/ipc_controld.h>
 #include <crm/cib/internal.h>
 
 #define SUMMARY "crm_resource - perform tasks related to Pacemaker cluster resources"
 
 enum rsc_command {
     cmd_none = 0,           // No command option given (yet)
     cmd_ban,
     cmd_cleanup,
     cmd_clear,
     cmd_colocations,
     cmd_colocations_deep,
     cmd_cts,
     cmd_delete,
     cmd_delete_param,
     cmd_digests,
     cmd_execute_agent,
     cmd_fail,
     cmd_get_param,
     cmd_get_property,
     cmd_list_active_ops,
     cmd_list_agents,
     cmd_list_all_ops,
     cmd_list_alternatives,
     cmd_list_instances,
     cmd_list_providers,
     cmd_list_resources,
     cmd_list_standards,
     cmd_locate,
     cmd_metadata,
     cmd_move,
     cmd_query_raw_xml,
     cmd_query_xml,
     cmd_refresh,
     cmd_restart,
     cmd_set_param,
     cmd_set_property,
     cmd_wait,
     cmd_why,
 };
 
 struct {
     enum rsc_command rsc_cmd;     // crm_resource command to perform
 
     // Infrastructure that given command needs to work
     gboolean require_cib;         // Whether command requires CIB IPC
     int cib_options;              // Options to use with CIB IPC calls
     gboolean require_crmd;        // Whether command requires controller IPC
     gboolean require_dataset;     // Whether command requires populated data set
     gboolean require_resource;    // Whether command requires resource specified
     gboolean require_node;        // Whether command requires node specified
     int find_flags;               // Flags to use when searching for resource
 
     // Command-line option values
     gchar *rsc_id;                // Value of --resource
     gchar *rsc_type;              // Value of --resource-type
     gboolean force;               // --force was given
     gboolean clear_expired;       // --expired was given
     gboolean recursive;           // --recursive was given
     gboolean promoted_role_only;  // --master was given
     gchar *host_uname;            // Value of --node
     gchar *interval_spec;         // Value of --interval
     gchar *move_lifetime;         // Value of --lifetime
     gchar *operation;             // Value of --operation
     const char *attr_set_type;    // Instance, meta, or utilization attribute
     gchar *prop_id;               // --nvpair (attribute XML ID)
     char *prop_name;              // Attribute name
     gchar *prop_set;              // --set-name (attribute block XML ID)
     gchar *prop_value;            // --parameter-value (attribute value)
     int timeout_ms;               // Parsed from --timeout value
     char *agent_spec;             // Standard and/or provider and/or agent
     gchar *xml_file;              // Value of (deprecated) --xml-file
 
     // Resource configuration specified via command-line arguments
     gboolean cmdline_config;      // Resource configuration was via arguments
     char *v_agent;                // Value of --agent
     char *v_class;                // Value of --class
     char *v_provider;             // Value of --provider
     GHashTable *cmdline_params;   // Resource parameters specified
 
     // Positional command-line arguments
     gchar **remainder;            // Positional arguments as given
     GHashTable *override_params;  // Resource parameter values that override config
 } options = {
     .attr_set_type = XML_TAG_ATTR_SETS,
     .cib_options = cib_sync_call,
     .require_cib = TRUE,
     .require_dataset = TRUE,
     .require_resource = TRUE,
 };
 
 #if 0
 // @COMPAT @TODO enable this at next backward compatibility break
 #define SET_COMMAND(cmd) do {                                               \
         if (options.rsc_cmd != cmd_none) {                                  \
             g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_USAGE,             \
                         "Only one command option may be specified");        \
             return FALSE;                                                   \
         }                                                                   \
         options.rsc_cmd = (cmd);                                            \
     } while (0)
 #else
 #define SET_COMMAND(cmd) do { options.rsc_cmd = (cmd); } while (0)
 #endif
 
 gboolean agent_provider_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean attr_set_type_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean class_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean cleanup_refresh_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean delete_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean expired_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean list_agents_cb(const gchar *option_name, const gchar *optarg,
                         gpointer data, GError **error);
 gboolean list_providers_cb(const gchar *option_name, const gchar *optarg,
                            gpointer data, GError **error);
 gboolean list_standards_cb(const gchar *option_name, const gchar *optarg,
                            gpointer data, GError **error);
 gboolean list_alternatives_cb(const gchar *option_name, const gchar *optarg,
                               gpointer data, GError **error);
 gboolean metadata_cb(const gchar *option_name, const gchar *optarg,
                      gpointer data, GError **error);
 gboolean option_cb(const gchar *option_name, const gchar *optarg,
                    gpointer data, GError **error);
 gboolean fail_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean flag_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean get_param_prop_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean list_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean set_delete_param_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean set_prop_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean timeout_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean validate_or_force_cb(const gchar *option_name, const gchar *optarg,
                               gpointer data, GError **error);
 gboolean restart_cb(const gchar *option_name, const gchar *optarg,
                     gpointer data, GError **error);
 gboolean digests_cb(const gchar *option_name, const gchar *optarg,
                     gpointer data, GError **error);
 gboolean wait_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean why_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 
 static crm_exit_t exit_code = CRM_EX_OK;
 static pcmk__output_t *out = NULL;
 
 // Things that should be cleaned up on exit
 static GError *error = NULL;
 static GMainLoop *mainloop = NULL;
 static cib_t *cib_conn = NULL;
 static pcmk_ipc_api_t *controld_api = NULL;
 static pe_working_set_t *data_set = NULL;
 
 #define MESSAGE_TIMEOUT_S 60
 
 #define INDENT "                                    "
 
 static pcmk__supported_format_t formats[] = {
     PCMK__SUPPORTED_FORMAT_NONE,
     PCMK__SUPPORTED_FORMAT_TEXT,
     PCMK__SUPPORTED_FORMAT_XML,
     { NULL, NULL, NULL }
 };
 
 // Clean up and exit
 static crm_exit_t
 bye(crm_exit_t ec)
 {
-    if (error != NULL) {
-        if (out != NULL) {
-            out->err(out, "%s: %s", g_get_prgname(), error->message);
-        } else {
-            fprintf(stderr, "%s: %s\n", g_get_prgname(), error->message);
-        }
-
-        g_clear_error(&error);
-    }
+    pcmk__output_and_clear_error(error, out);
 
     if (out != NULL) {
         out->finish(out, ec, true, NULL);
         pcmk__output_free(out);
     }
 
     if (cib_conn != NULL) {
         cib_t *save_cib_conn = cib_conn;
 
         cib_conn = NULL; // Ensure we can't free this twice
         save_cib_conn->cmds->signoff(save_cib_conn);
         cib_delete(save_cib_conn);
     }
     if (controld_api != NULL) {
         pcmk_ipc_api_t *save_controld_api = controld_api;
 
         controld_api = NULL; // Ensure we can't free this twice
         pcmk_free_ipc_api(save_controld_api);
     }
     if (mainloop != NULL) {
         g_main_loop_unref(mainloop);
         mainloop = NULL;
     }
     pe_free_working_set(data_set);
     data_set = NULL;
     crm_exit(ec);
     return ec;
 }
 
 static void
 quit_main_loop(crm_exit_t ec)
 {
     exit_code = ec;
     if (mainloop != NULL) {
         GMainLoop *mloop = mainloop;
 
         mainloop = NULL; // Don't re-enter this block
         pcmk_quit_main_loop(mloop, 10);
         g_main_loop_unref(mloop);
     }
 }
 
 static gboolean
 resource_ipc_timeout(gpointer data)
 {
     // Start with newline because "Waiting for ..." message doesn't have one
     if (error != NULL) {
         g_clear_error(&error);
     }
 
     g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_TIMEOUT,
                 "Aborting because no messages received in %d seconds", MESSAGE_TIMEOUT_S);
 
     quit_main_loop(CRM_EX_TIMEOUT);
     return FALSE;
 }
 
 static void
 controller_event_callback(pcmk_ipc_api_t *api, enum pcmk_ipc_event event_type,
                           crm_exit_t status, void *event_data, void *user_data)
 {
     switch (event_type) {
         case pcmk_ipc_event_disconnect:
             if (exit_code == CRM_EX_DISCONNECT) { // Unexpected
                 crm_info("Connection to controller was terminated");
             }
             quit_main_loop(exit_code);
             break;
 
         case pcmk_ipc_event_reply:
             if (status != CRM_EX_OK) {
                 out->err(out, "Error: bad reply from controller: %s",
                          crm_exit_str(status));
                 pcmk_disconnect_ipc(api);
                 quit_main_loop(status);
             } else {
                 if ((pcmk_controld_api_replies_expected(api) == 0)
                     && mainloop && g_main_loop_is_running(mainloop)) {
                     out->info(out, "... got reply (done)");
                     crm_debug("Got all the replies we expected");
                     pcmk_disconnect_ipc(api);
                     quit_main_loop(CRM_EX_OK);
                 } else {
                     out->info(out, "... got reply");
                 }
             }
             break;
 
         default:
             break;
     }
 }
 
 static void
 start_mainloop(pcmk_ipc_api_t *capi)
 {
     unsigned int count = pcmk_controld_api_replies_expected(capi);
 
     if (count > 0) {
         out->info(out, "Waiting for %d %s from the controller",
                   count, pcmk__plural_alt(count, "reply", "replies"));
         exit_code = CRM_EX_DISCONNECT; // For unexpected disconnects
         mainloop = g_main_loop_new(NULL, FALSE);
         g_timeout_add(MESSAGE_TIMEOUT_S * 1000, resource_ipc_timeout, NULL);
         g_main_loop_run(mainloop);
     }
 }
 
 static int
 compare_id(gconstpointer a, gconstpointer b)
 {
     return strcmp((const char *)a, (const char *)b);
 }
 
 static GListPtr
 build_constraint_list(xmlNode *root)
 {
     GListPtr retval = NULL;
     xmlNode *cib_constraints = NULL;
     xmlXPathObjectPtr xpathObj = NULL;
     int ndx = 0;
 
     cib_constraints = get_object_root(XML_CIB_TAG_CONSTRAINTS, root);
     xpathObj = xpath_search(cib_constraints, "//" XML_CONS_TAG_RSC_LOCATION);
 
     for (ndx = 0; ndx < numXpathResults(xpathObj); ndx++) {
         xmlNode *match = getXpathResult(xpathObj, ndx);
         retval = g_list_insert_sorted(retval, (gpointer) ID(match), compare_id);
     }
 
     freeXpathObject(xpathObj);
     return retval;
 }
 
 /* short option letters still available: eEJkKXyYZ */
 
 static GOptionEntry query_entries[] = {
     { "list", 'L', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, list_cb,
       "List all cluster resources with status",
       NULL },
     { "list-raw", 'l', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, list_cb,
       "List IDs of all instantiated resources (individual members\n"
       INDENT "rather than groups etc.)",
       NULL },
     { "list-cts", 'c', G_OPTION_FLAG_HIDDEN|G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, list_cb,
       NULL,
       NULL },
     { "list-operations", 'O', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, list_cb,
       "List active resource operations, optionally filtered by\n"
       INDENT "--resource and/or --node",
       NULL },
     { "list-all-operations", 'o', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, list_cb,
       "List all resource operations, optionally filtered by\n"
       INDENT "--resource and/or --node",
       NULL },
     { "list-standards", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
       list_standards_cb,
       "List supported standards",
       NULL },
     { "list-ocf-providers", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
       list_providers_cb,
       "List all available OCF providers",
       NULL },
     { "list-agents", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK,
       list_agents_cb,
       "List all agents available for the named standard and/or provider",
       "STD/PROV" },
     { "list-ocf-alternatives", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK,
       list_alternatives_cb,
       "List all available providers for the named OCF agent",
       "AGENT" },
     { "show-metadata", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK,
       metadata_cb,
       "Show the metadata for the named class:provider:agent",
       "SPEC" },
     { "query-xml", 'q', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, flag_cb,
       "Show XML configuration of resource (after any template expansion)",
       NULL },
     { "query-xml-raw", 'w', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, flag_cb,
       "Show XML configuration of resource (before any template expansion)",
       NULL },
     { "get-parameter", 'g', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, get_param_prop_cb,
       "Display named parameter for resource (use instance attribute\n"
       INDENT "unless --meta or --utilization is specified)",
       "PARAM" },
     { "get-property", 'G', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_CALLBACK, get_param_prop_cb,
       "Display named property of resource ('class', 'type', or 'provider') "
       "(requires --resource)",
       "PROPERTY" },
     { "locate", 'W', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, flag_cb,
       "Show node(s) currently running resource",
       NULL },
     { "stack", 'A', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, flag_cb,
       "Display the (co)location constraints that apply to a resource\n"
       INDENT "and the resources is it colocated with",
       NULL },
     { "constraints", 'a', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, flag_cb,
       "Display the (co)location constraints that apply to a resource",
       NULL },
     { "why", 'Y', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, why_cb,
       "Show why resources are not running, optionally filtered by\n"
       INDENT "--resource and/or --node",
       NULL },
 
     { NULL }
 };
 
 static GOptionEntry command_entries[] = {
     { "validate", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
       validate_or_force_cb,
       "Validate resource configuration by calling agent's validate-all\n"
       INDENT "action. The configuration may be specified either by giving an\n"
       INDENT "existing resource name with -r, or by specifying --class,\n"
       INDENT "--agent, and --provider arguments, along with any number of\n"
       INDENT "--option arguments.",
       NULL },
     { "cleanup", 'C', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, cleanup_refresh_cb,
       "If resource has any past failures, clear its history and fail\n"
       INDENT "count. Optionally filtered by --resource, --node, --operation\n"
       INDENT "and --interval (otherwise all). --operation and --interval\n"
       INDENT "apply to fail counts, but entire history is always clear, to\n"
       INDENT "allow current state to be rechecked. If the named resource is\n"
       INDENT "part of a group, or one numbered instance of a clone or bundled\n"
       INDENT "resource, the clean-up applies to the whole collective resource\n"
       INDENT "unless --force is given.",
       NULL },
     { "refresh", 'R', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, cleanup_refresh_cb,
       "Delete resource's history (including failures) so its current state\n"
       INDENT "is rechecked. Optionally filtered by --resource and --node\n"
       INDENT "(otherwise all). If the named resource is part of a group, or one\n"
       INDENT "numbered instance of a clone or bundled resource, the refresh\n"
       INDENT "applies to the whole collective resource unless --force is given.",
       NULL },
     { "set-parameter", 'p', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, set_delete_param_cb,
       "Set named parameter for resource (requires -v). Use instance\n"
       INDENT "attribute unless --meta or --utilization is specified.",
       "PARAM" },
     { "delete-parameter", 'd', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, set_delete_param_cb,
       "Delete named parameter for resource. Use instance attribute\n"
       INDENT "unless --meta or --utilization is specified.",
       "PARAM" },
     { "set-property", 'S', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_CALLBACK, set_prop_cb,
       "Set named property of resource ('class', 'type', or 'provider') "
       "(requires -r, -t, -v)",
       "PROPERTY" },
 
     { NULL }
 };
 
 static GOptionEntry location_entries[] = {
     { "move", 'M', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, flag_cb,
       "Create a constraint to move resource. If --node is specified,\n"
       INDENT "the constraint will be to move to that node, otherwise it\n"
       INDENT "will be to ban the current node. Unless --force is specified\n"
       INDENT "this will return an error if the resource is already running\n"
       INDENT "on the specified node. If --force is specified, this will\n"
       INDENT "always ban the current node.\n"
       INDENT "Optional: --lifetime, --master. NOTE: This may prevent the\n"
       INDENT "resource from running on its previous location until the\n"
       INDENT "implicit constraint expires or is removed with --clear.",
       NULL },
     { "ban", 'B', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, flag_cb,
       "Create a constraint to keep resource off a node.\n"
       INDENT "Optional: --node, --lifetime, --master.\n"
       INDENT "NOTE: This will prevent the resource from running on the\n"
       INDENT "affected node until the implicit constraint expires or is\n"
       INDENT "removed with --clear. If --node is not specified, it defaults\n"
       INDENT "to the node currently running the resource for primitives\n"
       INDENT "and groups, or the master for promotable clones with\n"
       INDENT "promoted-max=1 (all other situations result in an error as\n"
       INDENT "there is no sane default).",
       NULL },
     { "clear", 'U', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, flag_cb,
       "Remove all constraints created by the --ban and/or --move\n"
       INDENT "commands. Requires: --resource. Optional: --node, --master,\n"
       INDENT "--expired. If --node is not specified, all constraints created\n"
       INDENT "by --ban and --move will be removed for the named resource. If\n"
       INDENT "--node and --force are specified, any constraint created by\n"
       INDENT "--move will be cleared, even if it is not for the specified\n"
       INDENT "node. If --expired is specified, only those constraints whose\n"
       INDENT "lifetimes have expired will be removed.",
       NULL },
     { "expired", 'e', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, expired_cb,
       "Modifies the --clear argument to remove constraints with\n"
       INDENT "expired lifetimes.",
       NULL },
     { "lifetime", 'u', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.move_lifetime,
       "Lifespan (as ISO 8601 duration) of created constraints (with\n"
       INDENT "-B, -M) see https://en.wikipedia.org/wiki/ISO_8601#Durations)",
       "TIMESPEC" },
     { "master", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.promoted_role_only,
       "Limit scope of command to Master role (with -B, -M, -U). For\n"
       INDENT "-B and -M the previous master may remain active in the Slave role.",
       NULL },
 
     { NULL }
 };
 
 static GOptionEntry advanced_entries[] = {
     { "delete", 'D', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, delete_cb,
       "(Advanced) Delete a resource from the CIB. Required: -t",
       NULL },
     { "fail", 'F', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, fail_cb,
       "(Advanced) Tell the cluster this resource has failed",
       NULL },
     { "restart", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, restart_cb,
       "(Advanced) Tell the cluster to restart this resource and\n"
       INDENT "anything that depends on it",
       NULL },
     { "wait", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, wait_cb,
       "(Advanced) Wait until the cluster settles into a stable state",
       NULL },
     { "digests", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, digests_cb,
       "(Advanced) Show parameter hashes that Pacemaker uses to detect\n"
       INDENT "configuration changes (only accurate if there is resource\n"
       INDENT "history on the specified node). Required: --resource, --node.\n"
       INDENT "Optional: any NAME=VALUE parameters will be used to override\n"
       INDENT "the configuration (to see what the hash would be with those\n"
       INDENT "changes).",
       NULL },
     { "force-demote", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
       validate_or_force_cb,
       "(Advanced) Bypass the cluster and demote a resource on the local\n"
       INDENT "node. Unless --force is specified, this will refuse to do so if\n"
       INDENT "the cluster believes the resource is a clone instance already\n"
       INDENT "running on the local node.",
       NULL },
     { "force-stop", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
       validate_or_force_cb,
       "(Advanced) Bypass the cluster and stop a resource on the local node",
       NULL },
     { "force-start", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
       validate_or_force_cb,
       "(Advanced) Bypass the cluster and start a resource on the local\n"
       INDENT "node. Unless --force is specified, this will refuse to do so if\n"
       INDENT "the cluster believes the resource is a clone instance already\n"
       INDENT "running on the local node.",
       NULL },
     { "force-promote", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
       validate_or_force_cb,
       "(Advanced) Bypass the cluster and promote a resource on the local\n"
       INDENT "node. Unless --force is specified, this will refuse to do so if\n"
       INDENT "the cluster believes the resource is a clone instance already\n"
       INDENT "running on the local node.",
       NULL },
     { "force-check", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
       validate_or_force_cb,
       "(Advanced) Bypass the cluster and check the state of a resource on\n"
       INDENT "the local node",
       NULL },
 
     { NULL }
 };
 
 static GOptionEntry addl_entries[] = {
     { "node", 'N', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.host_uname,
       "Node name",
       "NAME" },
     { "recursive", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.recursive,
       "Follow colocation chains when using --set-parameter",
       NULL },
     { "resource-type", 't', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.rsc_type,
       "Resource XML element (primitive, group, etc.) (with -D)",
       "ELEMENT" },
     { "parameter-value", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.prop_value,
       "Value to use with -p",
       "PARAM" },
     { "meta", 'm', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, attr_set_type_cb,
       "Use resource meta-attribute instead of instance attribute\n"
       INDENT "(with -p, -g, -d)",
       NULL },
     { "utilization", 'z', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, attr_set_type_cb,
       "Use resource utilization attribute instead of instance attribute\n"
       INDENT "(with -p, -g, -d)",
       NULL },
     { "operation", 'n', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.operation,
       "Operation to clear instead of all (with -C -r)",
       "OPERATION" },
     { "interval", 'I', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.interval_spec,
       "Interval of operation to clear (default 0) (with -C -r -n)",
       "N" },
     { "class", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, class_cb,
       "The standard the resource agent conforms to (for example, ocf).\n"
       INDENT "Use with --agent, --provider, --option, and --validate.",
       "CLASS" },
     { "agent", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, agent_provider_cb,
       "The agent to use (for example, IPaddr). Use with --class,\n"
       INDENT "--provider, --option, and --validate.",
       "AGENT" },
     { "provider", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, agent_provider_cb,
       "The vendor that supplies the resource agent (for example,\n"
       INDENT "heartbeat). Use with --class, --agent, --option, and --validate.",
       "PROVIDER" },
     { "option", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, option_cb,
       "Specify a device configuration parameter as NAME=VALUE (may be\n"
       INDENT "specified multiple times). Use with --validate and without the\n"
       INDENT "-r option.",
       "PARAM" },
     { "set-name", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.prop_set,
       "(Advanced) XML ID of attributes element to use (with -p, -d)",
       "ID" },
     { "nvpair", 'i', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.prop_id,
       "(Advanced) XML ID of nvpair element to use (with -p, -d)",
       "ID" },
     { "timeout", 'T', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, timeout_cb,
       "(Advanced) Abort if command does not finish in this time (with\n"
       INDENT "--restart, --wait, --force-*)",
       "N" },
     { "force", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.force,
       "If making CIB changes, do so regardless of quorum. See help for\n"
       INDENT "individual commands for additional behavior.",
       NULL },
     { "xml-file", 'x', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_FILENAME, &options.xml_file,
       NULL,
       "FILE" },
     { "host-uname", 'H', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &options.host_uname,
       NULL,
       "HOST" },
 
     { NULL }
 };
 
 gboolean
 agent_provider_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.cmdline_config = TRUE;
     options.require_resource = FALSE;
 
     if (pcmk__str_eq(option_name, "--provider", pcmk__str_casei)) {
         if (options.v_provider) {
            free(options.v_provider);
         }
         options.v_provider = strdup(optarg);
     } else {
         if (options.v_agent) {
            free(options.v_agent);
         }
         options.v_agent = strdup(optarg);
     }
 
     return TRUE;
 }
 
 gboolean
 attr_set_type_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     if (pcmk__str_any_of(option_name, "-m", "--meta", NULL)) {
         options.attr_set_type = XML_TAG_META_SETS;
     } else if (pcmk__str_any_of(option_name, "-z", "--utilization", NULL)) {
         options.attr_set_type = XML_TAG_UTILIZATION;
     }
 
     return TRUE;
 }
 
 gboolean
 class_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     if (!(pcmk_get_ra_caps(optarg) & pcmk_ra_cap_params)) {
         if (!out->is_quiet(out)) {
             g_set_error(error, G_OPTION_ERROR, CRM_EX_INVALID_PARAM,
                         "Standard %s does not support parameters\n", optarg);
         }
         return FALSE;
 
     } else {
         if (options.v_class != NULL) {
             free(options.v_class);
         }
 
         options.v_class = strdup(optarg);
     }
 
     options.cmdline_config = TRUE;
     options.require_resource = FALSE;
     return TRUE;
 }
 
 gboolean
 cleanup_refresh_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     if (pcmk__str_any_of(option_name, "-C", "--cleanup", NULL)) {
         SET_COMMAND(cmd_cleanup);
     } else {
         SET_COMMAND(cmd_refresh);
     }
 
     options.require_resource = FALSE;
     if (getenv("CIB_file") == NULL) {
         options.require_crmd = TRUE;
     }
     options.find_flags = pe_find_renamed|pe_find_anon;
     return TRUE;
 }
 
 gboolean
 delete_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.require_dataset = FALSE;
     SET_COMMAND(cmd_delete);
     options.find_flags = pe_find_renamed|pe_find_any;
     return TRUE;
 }
 
 gboolean
 expired_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.clear_expired = TRUE;
     options.require_resource = FALSE;
     return TRUE;
 }
 
 static void
 get_agent_spec(const gchar *optarg)
 {
     options.require_cib = FALSE;
     options.require_dataset = FALSE;
     options.require_resource = FALSE;
     if (options.agent_spec != NULL) {
         free(options.agent_spec);
         options.agent_spec = NULL;
     }
     if (optarg != NULL) {
         options.agent_spec = strdup(optarg);
     }
 }
 
 gboolean
 list_agents_cb(const gchar *option_name, const gchar *optarg, gpointer data,
                GError **error)
 {
     SET_COMMAND(cmd_list_agents);
     get_agent_spec(optarg);
     return TRUE;
 }
 
 gboolean
 list_providers_cb(const gchar *option_name, const gchar *optarg, gpointer data,
                   GError **error)
 {
     SET_COMMAND(cmd_list_providers);
     get_agent_spec(optarg);
     return TRUE;
 }
 
 gboolean
 list_standards_cb(const gchar *option_name, const gchar *optarg, gpointer data,
                   GError **error)
 {
     SET_COMMAND(cmd_list_standards);
     options.require_cib = FALSE;
     options.require_dataset = FALSE;
     options.require_resource = FALSE;
     return TRUE;
 }
 
 gboolean
 list_alternatives_cb(const gchar *option_name, const gchar *optarg,
                      gpointer data, GError **error)
 {
     SET_COMMAND(cmd_list_alternatives);
     get_agent_spec(optarg);
     return TRUE;
 }
 
 gboolean
 metadata_cb(const gchar *option_name, const gchar *optarg, gpointer data,
             GError **error)
 {
     SET_COMMAND(cmd_metadata);
     get_agent_spec(optarg);
     return TRUE;
 }
 
 gboolean
 option_cb(const gchar *option_name, const gchar *optarg, gpointer data,
           GError **error)
 {
     char *name = NULL;
     char *value = NULL;
 
     if (pcmk_scan_nvpair(optarg, &name, &value) != 2) {
         return FALSE;
     }
     if (options.cmdline_params == NULL) {
         options.cmdline_params = crm_str_table_new();
     }
     g_hash_table_replace(options.cmdline_params, name, value);
     return TRUE;
 }
 
 gboolean
 fail_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.require_crmd = TRUE;
     options.require_node = TRUE;
     SET_COMMAND(cmd_fail);
     return TRUE;
 }
 
 gboolean
 flag_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     if (pcmk__str_any_of(option_name, "-U", "--clear", NULL)) {
         options.find_flags = pe_find_renamed|pe_find_anon;
         SET_COMMAND(cmd_clear);
     } else if (pcmk__str_any_of(option_name, "-B", "--ban", NULL)) {
         options.find_flags = pe_find_renamed|pe_find_anon;
         SET_COMMAND(cmd_ban);
     } else if (pcmk__str_any_of(option_name, "-M", "--move", NULL)) {
         options.find_flags = pe_find_renamed|pe_find_anon;
         SET_COMMAND(cmd_move);
     } else if (pcmk__str_any_of(option_name, "-q", "--query-xml", NULL)) {
         options.find_flags = pe_find_renamed|pe_find_any;
         SET_COMMAND(cmd_query_xml);
     } else if (pcmk__str_any_of(option_name, "-w", "--query-xml-raw", NULL)) {
         options.find_flags = pe_find_renamed|pe_find_any;
         SET_COMMAND(cmd_query_raw_xml);
     } else if (pcmk__str_any_of(option_name, "-W", "--locate", NULL)) {
         options.find_flags = pe_find_renamed|pe_find_anon;
         SET_COMMAND(cmd_locate);
     } else if (pcmk__str_any_of(option_name, "-A", "--stack", NULL)) {
         options.find_flags = pe_find_renamed|pe_find_anon;
         SET_COMMAND(cmd_colocations_deep);
     } else {
         options.find_flags = pe_find_renamed|pe_find_anon;
         SET_COMMAND(cmd_colocations);
     }
 
     return TRUE;
 }
 
 gboolean
 get_param_prop_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     if (pcmk__str_any_of(option_name, "-g", "--get-parameter", NULL)) {
         SET_COMMAND(cmd_get_param);
     } else {
         SET_COMMAND(cmd_get_property);
     }
 
     if (options.prop_name) {
         free(options.prop_name);
     }
 
     options.prop_name = strdup(optarg);
     options.find_flags = pe_find_renamed|pe_find_any;
     return TRUE;
 }
 
 gboolean
 list_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     if (pcmk__str_any_of(option_name, "-c", "--list-cts", NULL)) {
         SET_COMMAND(cmd_cts);
     } else if (pcmk__str_any_of(option_name, "-L", "--list", NULL)) {
         SET_COMMAND(cmd_list_resources);
     } else if (pcmk__str_any_of(option_name, "-l", "--list-raw", NULL)) {
         SET_COMMAND(cmd_list_instances);
     } else if (pcmk__str_any_of(option_name, "-O", "--list-operations", NULL)) {
         SET_COMMAND(cmd_list_active_ops);
     } else {
         SET_COMMAND(cmd_list_all_ops);
     }
 
     options.require_resource = FALSE;
     return TRUE;
 }
 
 gboolean
 set_delete_param_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     if (pcmk__str_any_of(option_name, "-p", "--set-parameter", NULL)) {
         SET_COMMAND(cmd_set_param);
     } else {
         SET_COMMAND(cmd_delete_param);
     }
 
     if (options.prop_name) {
         free(options.prop_name);
     }
 
     options.prop_name = strdup(optarg);
     options.find_flags = pe_find_renamed|pe_find_any;
     return TRUE;
 }
 
 gboolean
 set_prop_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.require_dataset = FALSE;
 
     if (options.prop_name) {
         free(options.prop_name);
     }
 
     options.prop_name = strdup(optarg);
     SET_COMMAND(cmd_set_property);
     options.find_flags = pe_find_renamed|pe_find_any;
     return TRUE;
 }
 
 gboolean
 timeout_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.timeout_ms = crm_get_msec(optarg);
     return TRUE;
 }
 
 gboolean
 validate_or_force_cb(const gchar *option_name, const gchar *optarg,
                      gpointer data, GError **error)
 {
     SET_COMMAND(cmd_execute_agent);
     if (options.operation) {
         g_free(options.operation);
     }
     options.operation = g_strdup(option_name + 2); // skip "--"
     options.find_flags = pe_find_renamed|pe_find_anon;
     if (options.override_params == NULL) {
         options.override_params = crm_str_table_new();
     }
     return TRUE;
 }
 
 gboolean
 restart_cb(const gchar *option_name, const gchar *optarg, gpointer data,
            GError **error)
 {
     SET_COMMAND(cmd_restart);
     options.find_flags = pe_find_renamed|pe_find_anon;
     return TRUE;
 }
 
 gboolean
 digests_cb(const gchar *option_name, const gchar *optarg, gpointer data,
            GError **error)
 {
     SET_COMMAND(cmd_digests);
     options.find_flags = pe_find_renamed|pe_find_anon;
     if (options.override_params == NULL) {
         options.override_params = crm_str_table_new();
     }
     options.require_node = TRUE;
     options.require_dataset = TRUE;
     return TRUE;
 }
 
 gboolean
 wait_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     SET_COMMAND(cmd_wait);
     options.require_resource = FALSE;
     options.require_dataset = FALSE;
     return TRUE;
 }
 
 gboolean
 why_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.require_resource = FALSE;
     SET_COMMAND(cmd_why);
     options.find_flags = pe_find_renamed|pe_find_anon;
     return TRUE;
 }
 
 static int
 ban_or_move(pcmk__output_t *out, pe_resource_t *rsc, const char *move_lifetime,
             crm_exit_t *exit_code)
 {
     int rc = pcmk_rc_ok;
     pe_node_t *current = NULL;
     unsigned int nactive = 0;
 
     CRM_CHECK(rsc != NULL, return EINVAL);
 
     current = pe__find_active_requires(rsc, &nactive);
 
     if (nactive == 1) {
         rc = cli_resource_ban(out, options.rsc_id, current->details->uname, move_lifetime, NULL,
                               cib_conn, options.cib_options, options.promoted_role_only);
 
     } else if (pcmk_is_set(rsc->flags, pe_rsc_promotable)) {
         int count = 0;
         GListPtr iter = NULL;
 
         current = NULL;
         for(iter = rsc->children; iter; iter = iter->next) {
             pe_resource_t *child = (pe_resource_t *)iter->data;
             enum rsc_role_e child_role = child->fns->state(child, TRUE);
 
             if(child_role == RSC_ROLE_MASTER) {
                 count++;
                 current = pe__current_node(child);
             }
         }
 
         if(count == 1 && current) {
             rc = cli_resource_ban(out, options.rsc_id, current->details->uname, move_lifetime, NULL,
                                   cib_conn, options.cib_options, options.promoted_role_only);
 
         } else {
             rc = EINVAL;
             *exit_code = CRM_EX_USAGE;
             g_set_error(&error, PCMK__EXITC_ERROR, *exit_code,
                         "Resource '%s' not moved: active in %d locations (promoted in %d).\n"
                         "To prevent '%s' from running on a specific location, "
                         "specify a node."
                         "To prevent '%s' from being promoted at a specific "
                         "location, specify a node and the master option.",
                         options.rsc_id, nactive, count, options.rsc_id, options.rsc_id);
         }
 
     } else {
         rc = EINVAL;
         *exit_code = CRM_EX_USAGE;
         g_set_error(&error, PCMK__EXITC_ERROR, *exit_code,
                     "Resource '%s' not moved: active in %d locations.\n"
                     "To prevent '%s' from running on a specific location, "
                     "specify a node.",
                     options.rsc_id, nactive, options.rsc_id);
     }
 
     return rc;
 }
 
 static void
 cleanup(pcmk__output_t *out, pe_resource_t *rsc)
 {
     int rc = pcmk_rc_ok;
 
     if (options.force == FALSE) {
         rsc = uber_parent(rsc);
     }
 
     crm_debug("Erasing failures of %s (%s requested) on %s",
               rsc->id, options.rsc_id, (options.host_uname? options.host_uname: "all nodes"));
     rc = cli_resource_delete(out, controld_api, options.host_uname, rsc, options.operation,
                              options.interval_spec, TRUE, data_set, options.force);
 
     if ((rc == pcmk_rc_ok) && !out->is_quiet(out)) {
         // Show any reasons why resource might stay stopped
         cli_resource_check(out, cib_conn, rsc);
     }
 
     if (rc == pcmk_rc_ok) {
         start_mainloop(controld_api);
     }
 }
 
 static int
 clear_constraints(pcmk__output_t *out, xmlNodePtr *cib_xml_copy)
 {
     GListPtr before = NULL;
     GListPtr after = NULL;
     GListPtr remaining = NULL;
     GListPtr ele = NULL;
     pe_node_t *dest = NULL;
     int rc = pcmk_rc_ok;
 
     if (!out->is_quiet(out)) {
         before = build_constraint_list(data_set->input);
     }
 
     if (options.clear_expired) {
         rc = cli_resource_clear_all_expired(data_set->input, cib_conn, options.cib_options,
                                             options.rsc_id, options.host_uname,
                                             options.promoted_role_only);
 
     } else if (options.host_uname) {
         dest = pe_find_node(data_set->nodes, options.host_uname);
         if (dest == NULL) {
             rc = pcmk_rc_node_unknown;
             if (!out->is_quiet(out)) {
                 g_list_free(before);
             }
             return rc;
         }
         rc = cli_resource_clear(options.rsc_id, dest->details->uname, NULL,
                                 cib_conn, options.cib_options, TRUE, options.force);
 
     } else {
         rc = cli_resource_clear(options.rsc_id, NULL, data_set->nodes,
                                 cib_conn, options.cib_options, TRUE, options.force);
     }
 
     if (!out->is_quiet(out)) {
         rc = cib_conn->cmds->query(cib_conn, NULL, cib_xml_copy, cib_scope_local | cib_sync_call);
         rc = pcmk_legacy2rc(rc);
 
         if (rc != pcmk_rc_ok) {
             g_set_error(&error, PCMK__RC_ERROR, rc,
                         "Could not get modified CIB: %s\n", pcmk_strerror(rc));
             g_list_free(before);
             return rc;
         }
 
         data_set->input = *cib_xml_copy;
         cluster_status(data_set);
 
         after = build_constraint_list(data_set->input);
         remaining = pcmk__subtract_lists(before, after, (GCompareFunc) strcmp);
 
         for (ele = remaining; ele != NULL; ele = ele->next) {
             out->info(out, "Removing constraint: %s", (char *) ele->data);
         }
 
         g_list_free(before);
         g_list_free(after);
         g_list_free(remaining);
     }
 
     return rc;
 }
 
 static int
 delete(void)
 {
     int rc = pcmk_rc_ok;
     xmlNode *msg_data = NULL;
 
     if (options.rsc_type == NULL) {
         rc = ENXIO;
         g_set_error(&error, PCMK__RC_ERROR, rc,
                     "You need to specify a resource type with -t");
         return rc;
     }
 
     msg_data = create_xml_node(NULL, options.rsc_type);
     crm_xml_add(msg_data, XML_ATTR_ID, options.rsc_id);
 
     rc = cib_conn->cmds->remove(cib_conn, XML_CIB_TAG_RESOURCES, msg_data,
                                 options.cib_options);
     rc = pcmk_legacy2rc(rc);
     free_xml(msg_data);
     return rc;
 }
 
 static int
 list_agents(pcmk__output_t *out, const char *agent_spec, crm_exit_t *exit_code)
 {
     int rc = pcmk_rc_ok;
     char *provider = strchr(agent_spec, ':');
     lrmd_t *lrmd_conn = lrmd_api_new();
     lrmd_list_t *list = NULL;
 
     if (provider) {
         *provider++ = 0;
     }
 
     rc = lrmd_conn->cmds->list_agents(lrmd_conn, &list, agent_spec, provider);
 
     if (rc > 0) {
         rc = out->message(out, "agents-list", list, agent_spec, provider);
     } else {
         rc = pcmk_rc_error;
     }
 
     if (rc != pcmk_rc_ok) {
         *exit_code = CRM_EX_NOSUCH;
         if (provider == NULL) {
             g_set_error(&error, PCMK__EXITC_ERROR, *exit_code,
                         "No agents found for standard '%s'", agent_spec);
         } else {
             g_set_error(&error, PCMK__EXITC_ERROR, *exit_code,
                         "No agents found for standard '%s' and provider '%s'",
                         agent_spec, provider);
         }
     }
 
     lrmd_api_delete(lrmd_conn);
     return rc;
 }
 
 static int
 list_providers(pcmk__output_t *out, const char *agent_spec, crm_exit_t *exit_code)
 {
     int rc;
     const char *text = NULL;
     lrmd_t *lrmd_conn = lrmd_api_new();
     lrmd_list_t *list = NULL;
 
     switch (options.rsc_cmd) {
         case cmd_list_alternatives:
             rc = lrmd_conn->cmds->list_ocf_providers(lrmd_conn, agent_spec, &list);
 
             if (rc > 0) {
                 rc = out->message(out, "alternatives-list", list, agent_spec);
             } else {
                 rc = pcmk_rc_error;
             }
 
             text = "OCF providers";
             break;
         case cmd_list_standards:
             rc = lrmd_conn->cmds->list_standards(lrmd_conn, &list);
 
             if (rc > 0) {
                 rc = out->message(out, "standards-list", list);
             } else {
                 rc = pcmk_rc_error;
             }
 
             text = "standards";
             break;
         case cmd_list_providers:
             rc = lrmd_conn->cmds->list_ocf_providers(lrmd_conn, agent_spec, &list);
 
             if (rc > 0) {
                 rc = out->message(out, "providers-list", list, agent_spec);
             } else {
                 rc = pcmk_rc_error;
             }
 
             text = "OCF providers";
             break;
         default:
             *exit_code = CRM_EX_SOFTWARE;
             g_set_error(&error, PCMK__EXITC_ERROR, *exit_code, "Bug");
             lrmd_api_delete(lrmd_conn);
             return pcmk_rc_error;
     }
 
     if (rc != pcmk_rc_ok) {
         if (agent_spec != NULL) {
             *exit_code = CRM_EX_NOSUCH;
             rc = pcmk_rc_error;
             g_set_error(&error, PCMK__EXITC_ERROR, *exit_code,
                         "No %s found for %s", text, agent_spec);
 
         } else {
             *exit_code = CRM_EX_NOSUCH;
             rc = pcmk_rc_error;
             g_set_error(&error, PCMK__EXITC_ERROR, *exit_code,
                         "No %s found", text);
         }
     }
 
     lrmd_api_delete(lrmd_conn);
     return rc;
 }
 
 static int
 populate_working_set(xmlNodePtr *cib_xml_copy)
 {
     int rc = pcmk_rc_ok;
 
     if (options.xml_file != NULL) {
         *cib_xml_copy = filename2xml(options.xml_file);
     } else {
         rc = cib_conn->cmds->query(cib_conn, NULL, cib_xml_copy, cib_scope_local | cib_sync_call);
         rc = pcmk_legacy2rc(rc);
     }
 
     if(rc != pcmk_rc_ok) {
         return rc;
     }
 
     /* Populate the working set instance */
     data_set = pe_new_working_set();
     if (data_set == NULL) {
         rc = ENOMEM;
         return rc;
     }
 
     pe__set_working_set_flags(data_set, pe_flag_no_counts|pe_flag_no_compat);
     rc = update_working_set_xml(data_set, cib_xml_copy);
     if (rc == pcmk_rc_ok) {
         cluster_status(data_set);
     }
     return rc;
 }
 
 static int
 refresh(pcmk__output_t *out)
 {
     int rc = pcmk_rc_ok;
     const char *router_node = options.host_uname;
     int attr_options = pcmk__node_attr_none;
 
     if (options.host_uname) {
         pe_node_t *node = pe_find_node(data_set->nodes, options.host_uname);
 
         if (pe__is_guest_or_remote_node(node)) {
             node = pe__current_node(node->details->remote_rsc);
             if (node == NULL) {
                 rc = ENXIO;
                 g_set_error(&error, PCMK__RC_ERROR, rc,
                             "No cluster connection to Pacemaker Remote node %s detected",
                             options.host_uname);
                 return rc;
             }
             router_node = node->details->uname;
             attr_options |= pcmk__node_attr_remote;
         }
     }
 
     if (controld_api == NULL) {
         out->info(out, "Dry run: skipping clean-up of %s due to CIB_file",
                   options.host_uname? options.host_uname : "all nodes");
         rc = pcmk_rc_ok;
         return rc;
     }
 
     crm_debug("Re-checking the state of all resources on %s", options.host_uname?options.host_uname:"all nodes");
 
     rc = pcmk__node_attr_request_clear(NULL, options.host_uname,
                                        NULL, NULL, NULL,
                                        NULL, attr_options);
 
     if (pcmk_controld_api_reprobe(controld_api, options.host_uname,
                                   router_node) == pcmk_rc_ok) {
         start_mainloop(controld_api);
     }
 
     return rc;
 }
 
 static void
 refresh_resource(pcmk__output_t *out, pe_resource_t *rsc)
 {
     int rc = pcmk_rc_ok;
 
     if (options.force == FALSE) {
         rsc = uber_parent(rsc);
     }
 
     crm_debug("Re-checking the state of %s (%s requested) on %s",
               rsc->id, options.rsc_id, (options.host_uname? options.host_uname: "all nodes"));
     rc = cli_resource_delete(out, controld_api, options.host_uname, rsc, NULL,
                              0, FALSE, data_set, options.force);
 
     if ((rc == pcmk_rc_ok) && !out->is_quiet(out)) {
         // Show any reasons why resource might stay stopped
         cli_resource_check(out, cib_conn, rsc);
     }
 
     if (rc == pcmk_rc_ok) {
         start_mainloop(controld_api);
     }
 }
 
 static int
 set_property(void)
 {
     int rc = pcmk_rc_ok;
     xmlNode *msg_data = NULL;
 
     if (pcmk__str_empty(options.rsc_type)) {
         g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
                     "Must specify -t with resource type");
         rc = ENXIO;
         return rc;
 
     } else if (pcmk__str_empty(options.prop_value)) {
         g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
                     "Must supply -v with new value");
         rc = EINVAL;
         return rc;
     }
 
     CRM_LOG_ASSERT(options.prop_name != NULL);
 
     msg_data = create_xml_node(NULL, options.rsc_type);
     crm_xml_add(msg_data, XML_ATTR_ID, options.rsc_id);
     crm_xml_add(msg_data, options.prop_name, options.prop_value);
 
     rc = cib_conn->cmds->modify(cib_conn, XML_CIB_TAG_RESOURCES, msg_data,
                                 options.cib_options);
     rc = pcmk_legacy2rc(rc);
     free_xml(msg_data);
 
     return rc;
 }
 
 static int
 show_metadata(pcmk__output_t *out, const char *agent_spec, crm_exit_t *exit_code)
 {
     int rc = pcmk_rc_ok;
     char *standard = NULL;
     char *provider = NULL;
     char *type = NULL;
     char *metadata = NULL;
     lrmd_t *lrmd_conn = lrmd_api_new();
 
     rc = crm_parse_agent_spec(agent_spec, &standard, &provider, &type);
     rc = pcmk_legacy2rc(rc);
 
     if (rc == pcmk_rc_ok) {
         rc = lrmd_conn->cmds->get_metadata(lrmd_conn, standard,
                                            provider, type,
                                            &metadata, 0);
         rc = pcmk_legacy2rc(rc);
 
         if (metadata) {
             out->output_xml(out, "metadata", metadata);
         } else {
             *exit_code = crm_errno2exit(rc);
             g_set_error(&error, PCMK__EXITC_ERROR, *exit_code,
                         "Metadata query for %s failed: %s",
                         agent_spec, pcmk_rc_str(rc));
         }
     } else {
         rc = ENXIO;
         g_set_error(&error, PCMK__RC_ERROR, rc,
                     "'%s' is not a valid agent specification", agent_spec);
     }
 
     lrmd_api_delete(lrmd_conn);
     return rc;
 }
 
 static void
 validate_cmdline_config(void)
 {
     // Cannot use both --resource and command-line resource configuration
     if (options.rsc_id != NULL) {
         g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
                     "--resource cannot be used with --class, --agent, and --provider");
 
     // Not all commands support command-line resource configuration
     } else if (options.rsc_cmd != cmd_execute_agent) {
         g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
                     "--class, --agent, and --provider can only be used with "
                     "--validate");
 
     // Not all of --class, --agent, and --provider need to be given.  Not all
     // classes support the concept of a provider.  Check that what we were given
     // is valid.
     } else if (pcmk__str_eq(options.v_class, "stonith", pcmk__str_none)) {
         if (options.v_provider != NULL) {
             g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
                         "stonith does not support providers");
 
         } else if (stonith_agent_exists(options.v_agent, 0) == FALSE) {
             g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
                         "%s is not a known stonith agent", options.v_agent ? options.v_agent : "");
         }
 
     } else if (resources_agent_exists(options.v_class, options.v_provider, options.v_agent) == FALSE) {
         g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
                     "%s:%s:%s is not a known resource",
                     options.v_class ? options.v_class : "",
                     options.v_provider ? options.v_provider : "",
                     options.v_agent ? options.v_agent : "");
     }
 
     if (error != NULL) {
         return;
     }
 
     if (options.cmdline_params == NULL) {
         options.cmdline_params = crm_str_table_new();
     }
     options.require_resource = FALSE;
     options.require_dataset = FALSE;
     options.require_cib = FALSE;
 }
 
 static GOptionContext *
 build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) {
     GOptionContext *context = NULL;
 
     GOptionEntry extra_prog_entries[] = {
         { "quiet", 'Q', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &(args->quiet),
           "Be less descriptive in output.",
           NULL },
         { "resource", 'r', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.rsc_id,
           "Resource ID",
           "ID" },
         { G_OPTION_REMAINING, 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING_ARRAY, &options.remainder,
           NULL,
           NULL },
 
         { NULL }
     };
 
     const char *description = "Examples:\n\n"
                               "List the available OCF agents:\n\n"
                               "\t# crm_resource --list-agents ocf\n\n"
                               "List the available OCF agents from the linux-ha project:\n\n"
                               "\t# crm_resource --list-agents ocf:heartbeat\n\n"
                               "Move 'myResource' to a specific node:\n\n"
                               "\t# crm_resource --resource myResource --move --node altNode\n\n"
                               "Allow (but not force) 'myResource' to move back to its original "
                               "location:\n\n"
                               "\t# crm_resource --resource myResource --clear\n\n"
                               "Stop 'myResource' (and anything that depends on it):\n\n"
                               "\t# crm_resource --resource myResource --set-parameter target-role "
                               "--meta --parameter-value Stopped\n\n"
                               "Tell the cluster not to manage 'myResource' (the cluster will not "
                               "attempt to start or stop the\n"
                               "resource under any circumstances; useful when performing maintenance "
                               "tasks on a resource):\n\n"
                               "\t# crm_resource --resource myResource --set-parameter is-managed "
                               "--meta --parameter-value false\n\n"
                               "Erase the operation history of 'myResource' on 'aNode' (the cluster "
                               "will 'forget' the existing\n"
                               "resource state, including any errors, and attempt to recover the"
                               "resource; useful when a resource\n"
                               "had failed permanently and has been repaired by an administrator):\n\n"
                               "\t# crm_resource --resource myResource --cleanup --node aNode\n\n";
 
     context = pcmk__build_arg_context(args, "text (default), xml", group, NULL);
     g_option_context_set_description(context, description);
 
     /* Add the -Q option, which cannot be part of the globally supported options
      * because some tools use that flag for something else.
      */
     pcmk__add_main_args(context, extra_prog_entries);
 
     pcmk__add_arg_group(context, "queries", "Queries:",
                         "Show query help", query_entries);
     pcmk__add_arg_group(context, "commands", "Commands:",
                         "Show command help", command_entries);
     pcmk__add_arg_group(context, "locations", "Locations:",
                         "Show location help", location_entries);
     pcmk__add_arg_group(context, "advanced", "Advanced:",
                         "Show advanced option help", advanced_entries);
     pcmk__add_arg_group(context, "additional", "Additional Options:",
                         "Show additional options", addl_entries);
     return context;
 }
 
 int
 main(int argc, char **argv)
 {
     xmlNode *cib_xml_copy = NULL;
     pe_resource_t *rsc = NULL;
     pe_node_t *node = NULL;
     int rc = pcmk_rc_ok;
 
     /*
      * Parse command line arguments
      */
 
     GOptionGroup *output_group = NULL;
     pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
     gchar **processed_args = pcmk__cmdline_preproc(argv, "GINSTdginpstuv");
     GOptionContext *context = build_arg_context(args, &output_group);
 
     pcmk__register_formats(output_group, formats);
     if (!g_option_context_parse_strv(context, &processed_args, &error)) {
         exit_code = CRM_EX_USAGE;
         goto done;
     }
 
     pcmk__cli_init_logging("crm_resource", args->verbosity);
 
     rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
     if (rc != pcmk_rc_ok) {
         exit_code = CRM_EX_ERROR;
         g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Error creating output format %s: %s",
                     args->output_ty, pcmk_rc_str(rc));
         goto done;
     }
 
     pe__register_messages(out);
     crm_resource_register_messages(out);
     lrmd__register_messages(out);
     pcmk__register_lib_messages(out);
 
     out->quiet = args->quiet;
 
     crm_log_args(argc, argv);
 
     /*
      * Validate option combinations
      */
 
     // If the user didn't explicitly specify a command, list resources
     if (options.rsc_cmd == cmd_none) {
         options.rsc_cmd = cmd_list_resources;
         options.require_resource = FALSE;
     }
 
     // --expired without --clear/-U doesn't make sense
     if (options.clear_expired && (options.rsc_cmd != cmd_clear)) {
         g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE, "--expired requires --clear or -U");
         goto done;
     }
 
     if ((options.remainder != NULL) && (options.override_params != NULL)) {
         // Commands that use positional arguments will create override_params
         for (gchar **s = options.remainder; *s; s++) {
             char *name = calloc(1, strlen(*s));
             char *value = calloc(1, strlen(*s));
             int rc = sscanf(*s, "%[^=]=%s", name, value);
 
             if (rc == 2) {
                 g_hash_table_replace(options.override_params, name, value);
 
             } else {
                 g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
                             "Error parsing '%s' as a name=value pair",
                             argv[optind]);
                 free(value);
                 free(name);
                 goto done;
             }
         }
 
     } else if (options.remainder != NULL) {
         gchar **strv = NULL;
         gchar *msg = NULL;
         int i = 1;
         int len = 0;
 
         for (gchar **s = options.remainder; *s; s++) {
             len++;
         }
 
         CRM_ASSERT(len > 0);
 
         strv = calloc(len, sizeof(char *));
         strv[0] = strdup("non-option ARGV-elements:");
 
         for (gchar **s = options.remainder; *s; s++) {
             strv[i] = crm_strdup_printf("[%d of %d] %s\n", i, len, *s);
             i++;
         }
 
         msg = g_strjoinv("", strv);
         g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE, "%s", msg);
         g_free(msg);
 
         for(i = 0; i < len; i++) {
             free(strv[i]);
         }
         free(strv);
 
         goto done;
     }
 
     if (pcmk__str_eq(args->output_ty, "xml", pcmk__str_none)) {
         /* Kind of a hack to display XML lists using a real tag instead of <list>.  This just
          * saves from having to write custom messages to build the lists around all these things
          */
         switch (options.rsc_cmd) {
             case cmd_list_resources:
             case cmd_query_xml:
             case cmd_query_raw_xml:
             case cmd_list_active_ops:
             case cmd_list_all_ops:
             case cmd_colocations:
             case cmd_colocations_deep:
                 pcmk__force_args(context, &error, "%s --xml-simple-list --xml-substitute", g_get_prgname());
                 break;
 
             default:
                 pcmk__force_args(context, &error, "%s --xml-substitute", g_get_prgname());
                 break;
         }
     } else if (pcmk__str_eq(args->output_ty, "text", pcmk__str_null_matches)) {
         if (options.rsc_cmd == cmd_colocations || options.rsc_cmd == cmd_colocations_deep ||
             options.rsc_cmd == cmd_list_resources) {
             pcmk__force_args(context, &error, "%s --text-fancy", g_get_prgname());
         }
     }
 
     if (args->version) {
         out->version(out, false);
         goto done;
     }
 
     if (options.cmdline_config) {
         /* A resource configuration was given on the command line. Sanity-check
          * the values and set error if they don't make sense.
          */
         validate_cmdline_config();
         if (error != NULL) {
             exit_code = CRM_EX_USAGE;
             goto done;
         }
 
     } else if (options.cmdline_params != NULL) {
         // @COMPAT @TODO error out here when we can break backward compatibility
         g_hash_table_destroy(options.cmdline_params);
         options.cmdline_params = NULL;
     }
 
     if (options.require_resource && (options.rsc_id == NULL)) {
         rc = ENXIO;
         exit_code = CRM_EX_USAGE;
         g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                     "Must supply a resource id with -r");
         goto done;
     }
     if (options.require_node && (options.host_uname == NULL)) {
         rc = ENXIO;
         exit_code = CRM_EX_USAGE;
         g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
                     "Must supply a node name with -N");
         goto done;
     }
 
     /*
      * Set up necessary connections
      */
 
     if (options.force) {
         crm_debug("Forcing...");
         cib__set_call_options(options.cib_options, crm_system_name,
                               cib_quorum_override);
     }
 
     if (options.find_flags && options.rsc_id) {
         options.require_dataset = TRUE;
     }
 
     // Establish a connection to the CIB if needed
     if (options.require_cib) {
         cib_conn = cib_new();
         if ((cib_conn == NULL) || (cib_conn->cmds == NULL)) {
             rc = pcmk_rc_error;
             g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_DISCONNECT,
                         "Could not create CIB connection");
             goto done;
         }
         rc = cib_conn->cmds->signon(cib_conn, crm_system_name, cib_command);
         rc = pcmk_legacy2rc(rc);
         if (rc != pcmk_rc_ok) {
             g_set_error(&error, PCMK__RC_ERROR, rc,
                         "Could not connect to the CIB: %s", pcmk_rc_str(rc));
             goto done;
         }
     }
 
     /* Populate working set from XML file if specified or CIB query otherwise */
     if (options.require_dataset) {
         rc = populate_working_set(&cib_xml_copy);
         if (rc != pcmk_rc_ok) {
             goto done;
         }
     }
 
     // If command requires that resource exist if specified, find it
     if (options.find_flags && options.rsc_id) {
         rsc = pe_find_resource_with_flags(data_set->resources, options.rsc_id,
                                           options.find_flags);
         if (rsc == NULL) {
             rc = ENXIO;
             g_set_error(&error, PCMK__RC_ERROR, rc,
                         "Resource '%s' not found", options.rsc_id);
             goto done;
         }
     }
 
     // If user supplied a node name, check whether it exists
     if ((options.host_uname != NULL) && (data_set != NULL)) {
         node = pe_find_node(data_set->nodes, options.host_uname);
     }
 
     // Establish a connection to the controller if needed
     if (options.require_crmd) {
         rc = pcmk_new_ipc_api(&controld_api, pcmk_ipc_controld);
         if (rc != pcmk_rc_ok) {
             g_set_error(&error, PCMK__RC_ERROR, rc,
                         "Error connecting to the controller: %s", pcmk_rc_str(rc));
             goto done;
         }
         pcmk_register_ipc_callback(controld_api, controller_event_callback,
                                    NULL);
         rc = pcmk_connect_ipc(controld_api, pcmk_ipc_dispatch_main);
         if (rc != pcmk_rc_ok) {
             g_set_error(&error, PCMK__RC_ERROR, rc,
                         "Error connecting to the controller: %s", pcmk_rc_str(rc));
             goto done;
         }
     }
 
     /*
      * Handle requested command
      */
 
     switch (options.rsc_cmd) {
         case cmd_list_resources: {
             GListPtr all = NULL;
             all = g_list_prepend(all, strdup("*"));
             rc = out->message(out, "resource-list", data_set,
                               pe_print_rsconly | pe_print_pending,
                               FALSE, TRUE, FALSE, TRUE, all, all, FALSE);
             g_list_free_full(all, free);
 
             if (rc == pcmk_rc_no_output) {
                 rc = ENXIO;
             }
             break;
         }
 
         case cmd_list_instances:
             rc = out->message(out, "resource-names-list", data_set->resources);
 
             if (rc != pcmk_rc_ok) {
                 rc = ENXIO;
             }
 
             break;
 
         case cmd_list_standards:
         case cmd_list_providers:
         case cmd_list_alternatives:
             rc = list_providers(out, options.agent_spec, &exit_code);
             break;
 
         case cmd_list_agents:
             rc = list_agents(out, options.agent_spec, &exit_code);
             break;
 
         case cmd_metadata:
             rc = show_metadata(out, options.agent_spec, &exit_code);
             break;
 
         case cmd_restart:
             /* We don't pass data_set because rsc needs to stay valid for the
              * entire lifetime of cli_resource_restart(), but it will reset and
              * update the working set multiple times, so it needs to use its own
              * copy.
              */
             rc = cli_resource_restart(out, rsc, options.host_uname,
                                       options.move_lifetime, options.timeout_ms,
                                       cib_conn, options.cib_options,
                                       options.promoted_role_only,
                                       options.force);
             break;
 
         case cmd_wait:
             rc = wait_till_stable(out, options.timeout_ms, cib_conn);
             break;
 
         case cmd_execute_agent:
             if (options.cmdline_config) {
                 exit_code = cli_resource_execute_from_params(out, "test",
                     options.v_class, options.v_provider, options.v_agent,
                     "validate-all", options.cmdline_params,
                     options.override_params, options.timeout_ms,
                     args->verbosity, options.force);
             } else {
                 exit_code = cli_resource_execute(out, rsc, options.rsc_id,
                     options.operation, options.override_params,
                     options.timeout_ms, cib_conn, data_set,
                     args->verbosity, options.force);
             }
             break;
 
         case cmd_digests:
             node = pe_find_node(data_set->nodes, options.host_uname);
             if (node == NULL) {
                 rc = pcmk_rc_node_unknown;
             } else {
                 rc = pcmk__resource_digests(out, rsc, node,
                                             options.override_params, data_set);
             }
             break;
 
         case cmd_colocations:
             rc = out->message(out, "stacks-constraints", rsc, data_set, false);
             break;
 
         case cmd_colocations_deep:
             rc = out->message(out, "stacks-constraints", rsc, data_set, true);
             break;
 
         case cmd_cts:
             rc = pcmk_rc_ok;
 
             for (GList *lpc = data_set->resources; lpc != NULL;
                  lpc = lpc->next) {
 
                 rsc = (pe_resource_t *) lpc->data;
                 cli_resource_print_cts(out, rsc);
             }
 
             cli_resource_print_cts_constraints(out, data_set);
             break;
 
         case cmd_fail:
             rc = cli_resource_fail(out, controld_api, options.host_uname,
                                    options.rsc_id, data_set);
             if (rc == pcmk_rc_ok) {
                 start_mainloop(controld_api);
             }
             break;
 
         case cmd_list_active_ops:
             rc = cli_resource_print_operations(out, options.rsc_id,
                                                options.host_uname, TRUE,
                                                data_set);
             break;
 
         case cmd_list_all_ops:
             rc = cli_resource_print_operations(out, options.rsc_id,
                                                options.host_uname, FALSE,
                                                data_set);
             break;
 
         case cmd_locate: {
             GListPtr resources = cli_resource_search(out, rsc, options.rsc_id, data_set);
             rc = out->message(out, "resource-search-list", resources, rsc, options.rsc_id);
             break;
         }
 
         case cmd_query_xml:
             rc = cli_resource_print(out, rsc, data_set, TRUE);
             break;
 
         case cmd_query_raw_xml:
             rc = cli_resource_print(out, rsc, data_set, FALSE);
             break;
 
         case cmd_why:
             if ((options.host_uname != NULL) && (node == NULL)) {
                 rc = pcmk_rc_node_unknown;
             } else {
                 rc = out->message(out, "resource-reasons-list", cib_conn,
                                   data_set->resources, rsc, node);
             }
             break;
 
         case cmd_clear:
             rc = clear_constraints(out, &cib_xml_copy);
             break;
 
         case cmd_move:
             if (options.host_uname == NULL) {
                 rc = ban_or_move(out, rsc, options.move_lifetime, &exit_code);
             } else {
                 rc = cli_resource_move(out, rsc, options.rsc_id, options.host_uname,
                                        options.move_lifetime, cib_conn,
                                        options.cib_options, data_set,
                                        options.promoted_role_only,
                                        options.force);
             }
             break;
 
         case cmd_ban:
             if (options.host_uname == NULL) {
                 rc = ban_or_move(out, rsc, options.move_lifetime, &exit_code);
             } else if (node == NULL) {
                 rc = pcmk_rc_node_unknown;
             } else {
                 rc = cli_resource_ban(out, options.rsc_id, node->details->uname,
                                       options.move_lifetime, NULL, cib_conn,
                                       options.cib_options,
                                       options.promoted_role_only);
             }
             break;
 
         case cmd_get_property:
             rc = out->message(out, "property-list", rsc, options.prop_name);
             if (rc == pcmk_rc_no_output) {
                 rc = ENXIO;
             }
 
             break;
 
         case cmd_set_property:
             rc = set_property();
             break;
 
         case cmd_get_param: {
             unsigned int count = 0;
             GHashTable *params = NULL;
             pe_node_t *current = pe__find_active_on(rsc, &count, NULL);
             bool free_params = true;
 
             if (count > 1) {
                 out->err(out, "%s is active on more than one node,"
                          " returning the default value for %s", rsc->id, crm_str(options.prop_name));
                 current = NULL;
             }
 
             crm_debug("Looking up %s in %s", options.prop_name, rsc->id);
 
             if (pcmk__str_eq(options.attr_set_type, XML_TAG_ATTR_SETS, pcmk__str_casei)) {
                 params = pe_rsc_params(rsc, current, data_set);
                 free_params = false;
 
             } else if (pcmk__str_eq(options.attr_set_type, XML_TAG_META_SETS, pcmk__str_casei)) {
                 params = crm_str_table_new();
                 get_meta_attributes(params, rsc, current, data_set);
 
             } else {
                 params = crm_str_table_new();
                 pe__unpack_dataset_nvpairs(rsc->xml, XML_TAG_UTILIZATION, NULL, params,
                                            NULL, FALSE, data_set);
             }
 
             rc = out->message(out, "attribute-list", rsc, options.prop_name, params);
             if (free_params) {
                 g_hash_table_destroy(params);
             }
             break;
         }
 
         case cmd_set_param:
             if (pcmk__str_empty(options.prop_value)) {
                 g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
                             "You need to supply a value with the -v option");
                 rc = EINVAL;
                 goto done;
             }
 
             /* coverity[var_deref_model] False positive */
             rc = cli_resource_update_attribute(out, rsc, options.rsc_id,
                                                options.prop_set,
                                                options.attr_set_type,
                                                options.prop_id,
                                                options.prop_name,
                                                options.prop_value,
                                                options.recursive, cib_conn,
                                                options.cib_options, data_set,
                                                options.force);
             break;
 
         case cmd_delete_param:
             /* coverity[var_deref_model] False positive */
             rc = cli_resource_delete_attribute(out, rsc, options.rsc_id,
                                                options.prop_set,
                                                options.attr_set_type,
                                                options.prop_id,
                                                options.prop_name, cib_conn,
                                                options.cib_options, data_set,
                                                options.force);
             break;
 
         case cmd_cleanup:
             if (rsc == NULL) {
                 rc = cli_cleanup_all(out, controld_api, options.host_uname,
                                      options.operation, options.interval_spec,
                                      data_set);
                 if (rc == pcmk_rc_ok) {
                     start_mainloop(controld_api);
                 }
             } else {
                 cleanup(out, rsc);
             }
             break;
 
         case cmd_refresh:
             if (rsc == NULL) {
                 rc = refresh(out);
             } else {
                 refresh_resource(out, rsc);
             }
             break;
 
         case cmd_delete:
             rc = delete();
             break;
 
         default:
             g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_SOFTWARE,
                         "Unimplemented command: %d", (int) options.rsc_cmd);
             break;
     }
 
     /*
      * Clean up and exit
      */
 
 done:
     if (rc != pcmk_rc_ok || exit_code != CRM_EX_OK) {
         if (rc == pcmk_rc_no_quorum) {
             g_prefix_error(&error, "To ignore quorum, use the force option.\n");
         }
 
         if (error != NULL) {
             char *msg = crm_strdup_printf("%s\nError performing operation: %s",
                                           error->message, pcmk_rc_str(rc));
             g_clear_error(&error);
             g_set_error(&error, PCMK__RC_ERROR, rc, "%s", msg);
             free(msg);
         } else {
             g_set_error(&error, PCMK__RC_ERROR, rc,
                         "Error performing operation: %s", pcmk_rc_str(rc));
         }
 
         if (exit_code == CRM_EX_OK) {
             exit_code = pcmk_rc2exitc(rc);
         }
     }
 
     g_free(options.host_uname);
     g_free(options.interval_spec);
     g_free(options.move_lifetime);
     g_free(options.operation);
     g_free(options.prop_id);
     free(options.prop_name);
     g_free(options.prop_set);
     g_free(options.prop_value);
     g_free(options.rsc_id);
     g_free(options.rsc_type);
     free(options.agent_spec);
     free(options.v_agent);
     free(options.v_class);
     free(options.v_provider);
     g_free(options.xml_file);
     g_strfreev(options.remainder);
 
     if (options.override_params != NULL) {
         g_hash_table_destroy(options.override_params);
     }
 
     /* options.cmdline_params does not need to be destroyed here.  See the
      * comments in cli_resource_execute_from_params.
      */
 
     g_strfreev(processed_args);
     g_option_context_free(context);
 
     return bye(exit_code);
 }
diff --git a/tools/crm_rule.c b/tools/crm_rule.c
index 0b132ed147..8b19bcd1be 100644
--- a/tools/crm_rule.c
+++ b/tools/crm_rule.c
@@ -1,374 +1,371 @@
 /*
  * Copyright 2019-2021 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/cib.h>
 #include <crm/common/cmdline_internal.h>
+#include <crm/common/output_internal.h>
 #include <crm/common/iso8601.h>
 #include <crm/msg_xml.h>
 #include <crm/pengine/rules_internal.h>
 #include <crm/pengine/status.h>
 #include <pacemaker-internal.h>
 
 #include <sys/stat.h>
 
 #define SUMMARY "evaluate rules from the Pacemaker configuration"
 
 enum crm_rule_mode {
     crm_rule_mode_none,
     crm_rule_mode_check
 };
 
 struct {
     char *date;
     char *input_xml;
     enum crm_rule_mode mode;
     char *rule;
 } options = {
     .mode = crm_rule_mode_none
 };
 
 static int crm_rule_check(pe_working_set_t *data_set, const char *rule_id, crm_time_t *effective_date);
 
 static gboolean mode_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 
 static GOptionEntry mode_entries[] = {
     { "check", 'c', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, mode_cb,
       "Check whether a rule is in effect",
       NULL },
 
     { NULL }
 };
 
 static GOptionEntry data_entries[] = {
     { "xml-text", 'X', 0, G_OPTION_ARG_STRING, &options.input_xml,
       "Use argument for XML (or stdin if '-')",
       NULL },
 
     { NULL }
 };
 
 static GOptionEntry addl_entries[] = {
     { "date", 'd', 0, G_OPTION_ARG_STRING, &options.date,
       "Whether the rule is in effect on a given date",
       NULL },
     { "rule", 'r', 0, G_OPTION_ARG_STRING, &options.rule,
       "The ID of the rule to check",
       NULL },
 
     { NULL }
 };
 
 static gboolean
 mode_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     if (strcmp(option_name, "c")) {
         options.mode = crm_rule_mode_check;
     }
 
     return TRUE;
 }
 
 /*!
  * \internal
  * \brief Evaluate a date expression for a specific time
  *
  * \param[in]  time_expr    date_expression XML
  * \param[in]  now          Time for which to evaluate expression
  * \param[out] next_change  If not NULL, set to when evaluation will change
  *
  * \return Standard Pacemaker return code
  */
 static int
 eval_date_expression(xmlNode *expr, crm_time_t *now, crm_time_t *next_change)
 {
     pe_rule_eval_data_t rule_data = {
         .node_hash = NULL,
         .role = RSC_ROLE_UNKNOWN,
         .now = now,
         .match_data = NULL,
         .rsc_data = NULL,
         .op_data = NULL
     };
 
     return pe__eval_date_expr(expr, &rule_data, next_change);
 }
 
 static int
 crm_rule_check(pe_working_set_t *data_set, const char *rule_id, crm_time_t *effective_date)
 {
     xmlNode *cib_constraints = NULL;
     xmlNode *match = NULL;
     xmlXPathObjectPtr xpathObj = NULL;
     char *xpath = NULL;
     int rc = pcmk_rc_ok;
     int max = 0;
 
     /* Rules are under the constraints node in the XML, so first find that. */
     cib_constraints = get_object_root(XML_CIB_TAG_CONSTRAINTS, data_set->input);
 
     /* Get all rules matching the given ID which are also simple enough for us to check.
      * For the moment, these rules must only have a single date_expression child and:
      * - Do not have a date_spec operation, or
      * - Have a date_spec operation that contains years= but does not contain moon=.
      *
      * We do this in steps to provide better error messages.  First, check that there's
      * any rule with the given ID.
      */
     xpath = crm_strdup_printf("//rule[@id='%s']", rule_id);
     xpathObj = xpath_search(cib_constraints, xpath);
     max = numXpathResults(xpathObj);
 
     if (max == 0) {
         CMD_ERR("No rule found with ID=%s", rule_id);
         rc = ENXIO;
         goto done;
     } else if (max > 1) {
         CMD_ERR("More than one rule with ID=%s found", rule_id);
         rc = ENXIO;
         goto done;
     }
 
     free(xpath);
     freeXpathObject(xpathObj);
 
     /* Next, make sure it has exactly one date_expression. */
     xpath = crm_strdup_printf("//rule[@id='%s']//date_expression", rule_id);
     xpathObj = xpath_search(cib_constraints, xpath);
     max = numXpathResults(xpathObj);
 
     if (max != 1) {
         CMD_ERR("Can't check rule %s because it does not have exactly one date_expression", rule_id);
         rc = EOPNOTSUPP;
         goto done;
     }
 
     free(xpath);
     freeXpathObject(xpathObj);
 
     /* Then, check that it's something we actually support. */
     xpath = crm_strdup_printf("//rule[@id='%s']//date_expression[@operation!='date_spec']", rule_id);
     xpathObj = xpath_search(cib_constraints, xpath);
     max = numXpathResults(xpathObj);
 
     if (max == 0) {
         free(xpath);
         freeXpathObject(xpathObj);
 
         xpath = crm_strdup_printf("//rule[@id='%s']//date_expression[@operation='date_spec' and date_spec/@years and not(date_spec/@moon)]",
                                   rule_id);
         xpathObj = xpath_search(cib_constraints, xpath);
         max = numXpathResults(xpathObj);
 
         if (max == 0) {
             CMD_ERR("Rule either must not use date_spec, or use date_spec with years= but not moon=");
             rc = ENXIO;
             goto done;
         }
     }
 
     match = getXpathResult(xpathObj, 0);
 
     /* We should have ensured both of these pass with the xpath query above, but
      * double checking can't hurt.
      */
     CRM_ASSERT(match != NULL);
     CRM_ASSERT(find_expression_type(match) == time_expr);
 
     rc = eval_date_expression(match, effective_date, NULL);
 
     if (rc == pcmk_rc_within_range) {
         printf("Rule %s is still in effect\n", rule_id);
         rc = pcmk_rc_ok;
     } else if (rc == pcmk_rc_ok) {
         printf("Rule %s satisfies conditions\n", rule_id);
     } else if (rc == pcmk_rc_after_range) {
         printf("Rule %s is expired\n", rule_id);
     } else if (rc == pcmk_rc_before_range) {
         printf("Rule %s has not yet taken effect\n", rule_id);
     } else if (rc == pcmk_rc_op_unsatisfied) {
         printf("Rule %s does not satisfy conditions\n", rule_id);
     } else {
         printf("Could not determine whether rule %s is expired\n", rule_id);
     }
 
 done:
     free(xpath);
     freeXpathObject(xpathObj);
     return rc;
 }
 
 static GOptionContext *
 build_arg_context(pcmk__common_args_t *args) {
     GOptionContext *context = NULL;
 
     const char *description = "This tool is currently experimental.\n"
                               "The interface, behavior, and output may change with any version of pacemaker.";
 
     context = pcmk__build_arg_context(args, NULL, NULL, NULL);
     g_option_context_set_description(context, description);
 
     pcmk__add_arg_group(context, "modes", "Modes (mutually exclusive):",
                         "Show modes of operation", mode_entries);
     pcmk__add_arg_group(context, "data", "Data:",
                         "Show data options", data_entries);
     pcmk__add_arg_group(context, "additional", "Additional Options:",
                         "Show additional options", addl_entries);
     return context;
 }
 
 int
 main(int argc, char **argv)
 {
     cib_t *cib_conn = NULL;
     pe_working_set_t *data_set = NULL;
 
     crm_time_t *rule_date = NULL;
     xmlNode *input = NULL;
 
     int rc = pcmk_ok;
     crm_exit_t exit_code = CRM_EX_OK;
     GError *error = NULL;
 
     pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
     GOptionContext *context = build_arg_context(args);
     gchar **processed_args = pcmk__cmdline_preproc(argv, "nopNO");
 
     if (!g_option_context_parse_strv(context, &processed_args, &error)) {
         exit_code = CRM_EX_USAGE;
         goto done;
     }
 
     pcmk__cli_init_logging("crm_rule", args->verbosity);
 
     if (args->version) {
         g_strfreev(processed_args);
         pcmk__free_arg_context(context);
         /* FIXME:  When crm_rule is converted to use formatted output, this can go. */
         pcmk__cli_help('v', CRM_EX_USAGE);
     }
 
     /* Check command line arguments before opening a connection to
      * the CIB manager or doing anything else important.
      */
     switch(options.mode) {
         case crm_rule_mode_check:
             if (options.rule == NULL) {
                 CMD_ERR("--check requires use of --rule=");
                 exit_code = CRM_EX_USAGE;
                 goto done;
             }
 
             break;
 
         default:
             CMD_ERR("No mode operation given");
             exit_code = CRM_EX_USAGE;
             goto done;
             break;
     }
 
     /* Set up some defaults. */
     rule_date = crm_time_new(options.date);
     if (rule_date == NULL) {
         CMD_ERR("No --date given and can't determine current date");
         exit_code = CRM_EX_DATAERR;
         goto done;
     }
 
     /* Where does the XML come from?  If one of various command line options were
      * given, use those.  Otherwise, connect to the CIB and use that.
      */
     if (pcmk__str_eq(options.input_xml, "-", pcmk__str_casei)) {
         input = stdin2xml();
 
         if (input == NULL) {
             CMD_ERR("Couldn't parse input from STDIN\n");
             exit_code = CRM_EX_DATAERR;
             goto done;
         }
     } else if (options.input_xml != NULL) {
         input = string2xml(options.input_xml);
 
         if (input == NULL) {
             CMD_ERR("Couldn't parse input string: %s\n", options.input_xml);
 
             exit_code = CRM_EX_DATAERR;
             goto done;
         }
     } else {
         // Establish a connection to the CIB
         cib_conn = cib_new();
         rc = cib_conn->cmds->signon(cib_conn, crm_system_name, cib_command);
         if (rc != pcmk_ok) {
             CMD_ERR("Could not connect to CIB: %s", pcmk_strerror(rc));
             exit_code = crm_errno2exit(rc);
             goto done;
         }
     }
 
     /* Populate working set from CIB query */
     if (input == NULL) {
         rc = cib_conn->cmds->query(cib_conn, NULL, &input, cib_scope_local | cib_sync_call);
         if (rc != pcmk_ok) {
             exit_code = crm_errno2exit(rc);
             goto done;
         }
     }
 
     /* Populate the working set instance */
     data_set = pe_new_working_set();
     if (data_set == NULL) {
         exit_code = crm_errno2exit(ENOMEM);
         goto done;
     }
     pe__set_working_set_flags(data_set, pe_flag_no_counts|pe_flag_no_compat);
 
     data_set->input = input;
     data_set->now = rule_date;
 
     /* Unpack everything. */
     cluster_status(data_set);
 
     /* Now do whichever operation mode was asked for.  There's only one at the
      * moment so this looks a little silly, but I expect there will be more
      * modes in the future.
      */
     switch(options.mode) {
         case crm_rule_mode_check:
             rc = crm_rule_check(data_set, options.rule, rule_date);
 
             if (rc > 0) {
                 CMD_ERR("Error checking rule: %s", pcmk_rc_str(rc));
             }
 
             exit_code = pcmk_rc2exitc(rc);
             break;
 
         default:
             break;
     }
 
 done:
     if (cib_conn != NULL) {
         cib_conn->cmds->signoff(cib_conn);
         cib_delete(cib_conn);
     }
 
     g_strfreev(processed_args);
     pcmk__free_arg_context(context);
     pe_free_working_set(data_set);
 
-    if (error != NULL) {
-        fprintf(stderr, "%s: %s\n", g_get_prgname(), error->message);
-        g_clear_error(&error);
-    }
-
+    pcmk__output_and_clear_error(error, NULL);
     crm_exit(exit_code);
 }
diff --git a/tools/crm_simulate.c b/tools/crm_simulate.c
index 8115ffda6a..e3e9463d6e 100644
--- a/tools/crm_simulate.c
+++ b/tools/crm_simulate.c
@@ -1,1139 +1,1137 @@
 /*
  * Copyright 2009-2021 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 <stdio.h>
 #include <unistd.h>
 #include <stdlib.h>
 #include <time.h>
 
 #include <sys/stat.h>
 #include <sys/param.h>
 #include <sys/types.h>
 #include <dirent.h>
 
 #include <crm/crm.h>
 #include <crm/cib.h>
 #include <crm/common/cmdline_internal.h>
+#include <crm/common/output_internal.h>
 #include <crm/common/util.h>
 #include <crm/common/iso8601.h>
 #include <crm/pengine/status.h>
 #include <pacemaker-internal.h>
 
 #define SUMMARY "crm_simulate - simulate a Pacemaker cluster's response to events"
 
 /* show_scores and show_utilization can't be added to this struct.  They
  * actually come from include/pcmki/pcmki_scheduler.h where they are
  * defined as extern.
  */
 struct {
     gboolean all_actions;
     char *dot_file;
     char *graph_file;
     gchar *input_file;
     guint modified;
     GListPtr node_up;
     GListPtr node_down;
     GListPtr node_fail;
     GListPtr op_fail;
     GListPtr op_inject;
     gchar *output_file;
     gboolean print_pending;
     gboolean process;
     char *quorum;
     long long repeat;
     gboolean simulate;
     gboolean store;
     gchar *test_dir;
     GListPtr ticket_grant;
     GListPtr ticket_revoke;
     GListPtr ticket_standby;
     GListPtr ticket_activate;
     char *use_date;
     char *watchdog;
     char *xml_file;
 } options = {
     .print_pending = TRUE,
     .repeat = 1
 };
 
 cib_t *global_cib = NULL;
 bool action_numbers = FALSE;
 gboolean quiet = FALSE;
 char *temp_shadow = NULL;
 extern gboolean bringing_nodes_online;
 
 #define quiet_log(fmt, args...) do {		\
 	if(quiet == FALSE) {			\
 	    printf(fmt , ##args);		\
 	}					\
     } while(0)
 
 #define INDENT "                                   "
 
 static gboolean
 in_place_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.store = TRUE;
     options.process = TRUE;
     options.simulate = TRUE;
     return TRUE;
 }
 
 static gboolean
 live_check_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     if (options.xml_file) {
         free(options.xml_file);
     }
 
     options.xml_file = NULL;
     return TRUE;
 }
 
 static gboolean
 node_down_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.modified++;
     options.node_down = g_list_append(options.node_down, (gchar *) g_strdup(optarg));
     return TRUE;
 }
 
 static gboolean
 node_fail_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.modified++;
     options.node_fail = g_list_append(options.node_fail, (gchar *) g_strdup(optarg));
     return TRUE;
 }
 
 static gboolean
 node_up_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.modified++;
     bringing_nodes_online = TRUE;
     options.node_up = g_list_append(options.node_up, (gchar *) g_strdup(optarg));
     return TRUE;
 }
 
 static gboolean
 op_fail_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.process = TRUE;
     options.simulate = TRUE;
     options.op_fail = g_list_append(options.op_fail, (gchar *) g_strdup(optarg));
     return TRUE;
 }
 
 static gboolean
 op_inject_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.modified++;
     options.op_inject = g_list_append(options.op_inject, (gchar *) g_strdup(optarg));
     return TRUE;
 }
 
 static gboolean
 quorum_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     if (options.quorum) {
         free(options.quorum);
     }
 
     options.modified++;
     options.quorum = strdup(optarg);
     return TRUE;
 }
 
 static gboolean
 save_dotfile_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     if (options.dot_file) {
         free(options.dot_file);
     }
 
     options.process = TRUE;
     options.dot_file = strdup(optarg);
     return TRUE;
 }
 
 static gboolean
 save_graph_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     if (options.graph_file) {
         free(options.graph_file);
     }
 
     options.process = TRUE;
     options.graph_file = strdup(optarg);
     return TRUE;
 }
 
 static gboolean
 show_scores_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.process = TRUE;
     show_scores = TRUE;
     return TRUE;
 }
 
 static gboolean
 simulate_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.process = TRUE;
     options.simulate = TRUE;
     return TRUE;
 }
 
 static gboolean
 ticket_activate_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.modified++;
     options.ticket_activate = g_list_append(options.ticket_activate, (gchar *) g_strdup(optarg));
     return TRUE;
 }
 
 static gboolean
 ticket_grant_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.modified++;
     options.ticket_grant = g_list_append(options.ticket_grant, (gchar *) g_strdup(optarg));
     return TRUE;
 }
 
 static gboolean
 ticket_revoke_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.modified++;
     options.ticket_revoke = g_list_append(options.ticket_revoke, (gchar *) g_strdup(optarg));
     return TRUE;
 }
 
 static gboolean
 ticket_standby_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.modified++;
     options.ticket_standby = g_list_append(options.ticket_standby, (gchar *) g_strdup(optarg));
     return TRUE;
 }
 
 static gboolean
 utilization_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.process = TRUE;
     show_utilization = TRUE;
     return TRUE;
 }
 
 static gboolean
 watchdog_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     if (options.watchdog) {
         free(options.watchdog);
     }
 
     options.modified++;
     options.watchdog = strdup(optarg);
     return TRUE;
 }
 
 static gboolean
 xml_file_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     if (options.xml_file) {
         free(options.xml_file);
     }
 
     options.xml_file = strdup(optarg);
     return TRUE;
 }
 
 static gboolean
 xml_pipe_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     if (options.xml_file) {
         free(options.xml_file);
     }
 
     options.xml_file = strdup("-");
     return TRUE;
 }
 
 static GOptionEntry operation_entries[] = {
     { "run", 'R', 0, G_OPTION_ARG_NONE, &options.process,
       "Determine cluster's response to the given configuration and status",
       NULL },
     { "simulate", 'S', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, simulate_cb,
       "Simulate transition's execution and display resulting cluster status",
       NULL },
     { "in-place", 'X', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, in_place_cb,
       "Simulate transition's execution and store result back to input file",
       NULL },
     { "show-scores", 's', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, show_scores_cb,
       "Show allocation scores",
       NULL },
     { "show-utilization", 'U', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, utilization_cb,
       "Show utilization information",
       NULL },
     { "profile", 'P', 0, G_OPTION_ARG_FILENAME, &options.test_dir,
       "Run all tests in the named directory to create profiling data",
       NULL },
     { "repeat", 'N', 0, G_OPTION_ARG_INT, &options.repeat,
       "With --profile, repeat each test N times and print timings",
       "N" },
     { "pending", 'j', 0, G_OPTION_ARG_NONE, &options.print_pending,
       "Display pending state if 'record-pending' is enabled",
       NULL },
 
     { NULL }
 };
 
 static GOptionEntry synthetic_entries[] = {
     { "node-up", 'u', 0, G_OPTION_ARG_CALLBACK, node_up_cb,
       "Bring a node online",
       "NODE" },
     { "node-down", 'd', 0, G_OPTION_ARG_CALLBACK, node_down_cb,
       "Take a node offline",
       "NODE" },
     { "node-fail", 'f', 0, G_OPTION_ARG_CALLBACK, node_fail_cb,
       "Mark a node as failed",
       "NODE" },
     { "op-inject", 'i', 0, G_OPTION_ARG_CALLBACK, op_inject_cb,
       "Generate a failure for the cluster to react to in the simulation.\n"
       INDENT "See `Operation Specification` help for more information.",
       "OPSPEC" },
     { "op-fail", 'F', 0, G_OPTION_ARG_CALLBACK, op_fail_cb,
       "If the specified task occurs during the simulation, have it fail with return code ${rc}.\n"
       INDENT "The transition will normally stop at the failed action.\n"
       INDENT "Save the result with --save-output and re-run with --xml-file.\n"
       INDENT "See `Operation Specification` help for more information.",
       "OPSPEC" },
     { "set-datetime", 't', 0, G_OPTION_ARG_STRING, &options.use_date,
       "Set date/time (ISO 8601 format, see https://en.wikipedia.org/wiki/ISO_8601)",
       "DATETIME" },
     { "quorum", 'q', 0, G_OPTION_ARG_CALLBACK, quorum_cb,
       "Specify a value for quorum",
       "QUORUM" },
     { "watchdog", 'w', 0, G_OPTION_ARG_CALLBACK, watchdog_cb,
       "Assume a watchdog device is active",
       "DEVICE" },
     { "ticket-grant", 'g', 0, G_OPTION_ARG_CALLBACK, ticket_grant_cb,
       "Grant a ticket",
       "TICKET" },
     { "ticket-revoke", 'r', 0, G_OPTION_ARG_CALLBACK, ticket_revoke_cb,
       "Revoke a ticket",
       "TICKET" },
     { "ticket-standby", 'b', 0, G_OPTION_ARG_CALLBACK, ticket_standby_cb,
       "Make a ticket standby",
       "TICKET" },
     { "ticket-activate", 'e', 0, G_OPTION_ARG_CALLBACK, ticket_activate_cb,
       "Activate a ticket",
       "TICKET" },
 
     { NULL }
 };
 
 static GOptionEntry output_entries[] = {
     { "save-input", 'I', 0, G_OPTION_ARG_FILENAME, &options.input_file,
       "Save the input configuration to the named file",
       "FILE" },
     { "save-output", 'O', 0, G_OPTION_ARG_FILENAME, &options.output_file,
       "Save the output configuration to the named file",
       "FILE" },
     { "save-graph", 'G', 0, G_OPTION_ARG_CALLBACK, save_graph_cb,
       "Save the transition graph (XML format) to the named file",
       "FILE" },
     { "save-dotfile", 'D', 0, G_OPTION_ARG_CALLBACK, save_dotfile_cb,
       "Save the transition graph (DOT format) to the named file",
       "FILE" },
     { "all-actions", 'a', 0, G_OPTION_ARG_NONE, &options.all_actions,
       "Display all possible actions in DOT graph (even if not part of transition)",
       NULL },
 
     { NULL }
 };
 
 static GOptionEntry source_entries[] = {
     { "live-check", 'L', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, live_check_cb,
       "Connect to CIB manager and use the current CIB contents as input",
       NULL },
     { "xml-file", 'x', 0, G_OPTION_ARG_CALLBACK, xml_file_cb,
       "Retrieve XML from the named file",
       "FILE" },
     { "xml-pipe", 'p', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, xml_pipe_cb,
       "Retrieve XML from stdin",
       NULL },
 
     { NULL }
 };
 
 static void
 get_date(pe_working_set_t *data_set, bool print_original, char *use_date)
 {
     time_t original_date = 0;
 
     crm_element_value_epoch(data_set->input, "execution-date", &original_date);
 
     if (use_date) {
         data_set->now = crm_time_new(use_date);
         quiet_log(" + Setting effective cluster time: %s", use_date);
         crm_time_log(LOG_NOTICE, "Pretending 'now' is", data_set->now,
                      crm_time_log_date | crm_time_log_timeofday);
 
 
     } else if (original_date) {
 
         data_set->now = crm_time_new(NULL);
         crm_time_set_timet(data_set->now, &original_date);
 
         if (print_original) {
             char *when = crm_time_as_string(data_set->now,
                             crm_time_log_date|crm_time_log_timeofday);
 
             printf("Using the original execution date of: %s\n", when);
             free(when);
         }
     }
 }
 
 static void
 print_cluster_status(pe_working_set_t * data_set, long options)
 {
     char *online_nodes = NULL;
     char *online_remote_nodes = NULL;
     char *online_guest_nodes = NULL;
     char *offline_nodes = NULL;
     char *offline_remote_nodes = NULL;
 
     size_t online_nodes_len = 0;
     size_t online_remote_nodes_len = 0;
     size_t online_guest_nodes_len = 0;
     size_t offline_nodes_len = 0;
     size_t offline_remote_nodes_len = 0;
 
     GListPtr gIter = NULL;
 
     for (gIter = data_set->nodes; gIter != NULL; gIter = gIter->next) {
         pe_node_t *node = (pe_node_t *) gIter->data;
         const char *node_mode = NULL;
         char *node_name = NULL;
 
         if (pe__is_guest_node(node)) {
             node_name = crm_strdup_printf("%s:%s", node->details->uname, node->details->remote_rsc->container->id);
         } else {
             node_name = crm_strdup_printf("%s", node->details->uname);
         }
 
         if (node->details->unclean) {
             if (node->details->online && node->details->unclean) {
                 node_mode = "UNCLEAN (online)";
 
             } else if (node->details->pending) {
                 node_mode = "UNCLEAN (pending)";
 
             } else {
                 node_mode = "UNCLEAN (offline)";
             }
 
         } else if (node->details->pending) {
             node_mode = "pending";
 
         } else if (node->details->standby_onfail && node->details->online) {
             node_mode = "standby (on-fail)";
 
         } else if (node->details->standby) {
             if (node->details->online) {
                 node_mode = "standby";
             } else {
                 node_mode = "OFFLINE (standby)";
             }
 
         } else if (node->details->maintenance) {
             if (node->details->online) {
                 node_mode = "maintenance";
             } else {
                 node_mode = "OFFLINE (maintenance)";
             }
 
         } else if (node->details->online) {
             if (pe__is_guest_node(node)) {
                 pcmk__add_word(&online_guest_nodes, &online_guest_nodes_len,
                                node_name);
             } else if (pe__is_remote_node(node)) {
                 pcmk__add_word(&online_remote_nodes, &online_remote_nodes_len,
                                node_name);
             } else {
                 pcmk__add_word(&online_nodes, &online_nodes_len, node_name);
             }
             free(node_name);
             continue;
 
         } else {
             if (pe__is_remote_node(node)) {
                 pcmk__add_word(&offline_remote_nodes, &offline_remote_nodes_len,
                                node_name);
             } else if (pe__is_guest_node(node)) {
                 /* ignore offline container nodes */
             } else {
                 pcmk__add_word(&offline_nodes, &offline_nodes_len, node_name);
             }
             free(node_name);
             continue;
         }
 
         if (pe__is_guest_node(node)) {
             printf("GuestNode %s: %s\n", node_name, node_mode);
         } else if (pe__is_remote_node(node)) {
             printf("RemoteNode %s: %s\n", node_name, node_mode);
         } else if (pcmk__str_eq(node->details->uname, node->details->id, pcmk__str_casei)) {
             printf("Node %s: %s\n", node_name, node_mode);
         } else {
             printf("Node %s (%s): %s\n", node_name, node->details->id, node_mode);
         }
 
         free(node_name);
     }
 
     if (online_nodes) {
         printf("Online: [ %s ]\n", online_nodes);
         free(online_nodes);
     }
     if (offline_nodes) {
         printf("OFFLINE: [ %s ]\n", offline_nodes);
         free(offline_nodes);
     }
     if (online_remote_nodes) {
         printf("RemoteOnline: [ %s ]\n", online_remote_nodes);
         free(online_remote_nodes);
     }
     if (offline_remote_nodes) {
         printf("RemoteOFFLINE: [ %s ]\n", offline_remote_nodes);
         free(offline_remote_nodes);
     }
     if (online_guest_nodes) {
         printf("GuestOnline: [ %s ]\n", online_guest_nodes);
         free(online_guest_nodes);
     }
 
     fprintf(stdout, "\n");
     for (gIter = data_set->resources; gIter != NULL; gIter = gIter->next) {
         pe_resource_t *rsc = (pe_resource_t *) gIter->data;
 
         if (pcmk_is_set(rsc->flags, pe_rsc_orphan)
             && rsc->role == RSC_ROLE_STOPPED) {
             continue;
         }
         rsc->fns->print(rsc, NULL, pe_print_printf | options, stdout);
     }
     fprintf(stdout, "\n");
 }
 
 static char *
 create_action_name(pe_action_t *action)
 {
     char *action_name = NULL;
     const char *prefix = "";
     const char *action_host = NULL;
     const char *clone_name = NULL;
     const char *task = action->task;
 
     if (action->node) {
         action_host = action->node->details->uname;
     } else if (!pcmk_is_set(action->flags, pe_action_pseudo)) {
         action_host = "<none>";
     }
 
     if (pcmk__str_eq(action->task, RSC_CANCEL, pcmk__str_casei)) {
         prefix = "Cancel ";
         task = action->cancel_task;
     }
 
     if (action->rsc && action->rsc->clone_name) {
         clone_name = action->rsc->clone_name;
     }
 
     if (clone_name) {
         char *key = NULL;
         guint interval_ms = 0;
 
         if (pcmk__guint_from_hash(action->meta,
                                   XML_LRM_ATTR_INTERVAL_MS, 0,
                                   &interval_ms) != pcmk_rc_ok) {
             interval_ms = 0;
         }
 
         if (pcmk__strcase_any_of(action->task, RSC_NOTIFY, RSC_NOTIFIED, NULL)) {
             const char *n_type = g_hash_table_lookup(action->meta, "notify_key_type");
             const char *n_task = g_hash_table_lookup(action->meta, "notify_key_operation");
 
             CRM_ASSERT(n_type != NULL);
             CRM_ASSERT(n_task != NULL);
             key = pcmk__notify_key(clone_name, n_type, n_task);
 
         } else {
             key = pcmk__op_key(clone_name, task, interval_ms);
         }
 
         if (action_host) {
             action_name = crm_strdup_printf("%s%s %s", prefix, key, action_host);
         } else {
             action_name = crm_strdup_printf("%s%s", prefix, key);
         }
         free(key);
 
     } else if (pcmk__str_eq(action->task, CRM_OP_FENCE, pcmk__str_casei)) {
         const char *op = g_hash_table_lookup(action->meta, "stonith_action");
 
         action_name = crm_strdup_printf("%s%s '%s' %s", prefix, action->task, op, action_host);
 
     } else if (action->rsc && action_host) {
         action_name = crm_strdup_printf("%s%s %s", prefix, action->uuid, action_host);
 
     } else if (action_host) {
         action_name = crm_strdup_printf("%s%s %s", prefix, action->task, action_host);
 
     } else {
         action_name = crm_strdup_printf("%s", action->uuid);
     }
 
     if (action_numbers) { // i.e. verbose
         char *with_id = crm_strdup_printf("%s (%d)", action_name, action->id);
 
         free(action_name);
         action_name = with_id;
     }
     return action_name;
 }
 
 static bool
 create_dotfile(pe_working_set_t * data_set, const char *dot_file, gboolean all_actions,
                GError **error)
 {
     GListPtr gIter = NULL;
     FILE *dot_strm = fopen(dot_file, "w");
 
     if (dot_strm == NULL) {
         g_set_error(error, PCMK__RC_ERROR, errno,
                     "Could not open %s for writing: %s", dot_file,
                     pcmk_rc_str(errno));
         return false;
     }
 
     fprintf(dot_strm, " digraph \"g\" {\n");
     for (gIter = data_set->actions; gIter != NULL; gIter = gIter->next) {
         pe_action_t *action = (pe_action_t *) gIter->data;
         const char *style = "dashed";
         const char *font = "black";
         const char *color = "black";
         char *action_name = create_action_name(action);
 
         crm_trace("Action %d: %s %s %p", action->id, action_name, action->uuid, action);
 
         if (pcmk_is_set(action->flags, pe_action_pseudo)) {
             font = "orange";
         }
 
         if (pcmk_is_set(action->flags, pe_action_dumped)) {
             style = "bold";
             color = "green";
 
         } else if ((action->rsc != NULL)
                    && !pcmk_is_set(action->rsc->flags, pe_rsc_managed)) {
             color = "red";
             font = "purple";
             if (all_actions == FALSE) {
                 goto do_not_write;
             }
 
         } else if (pcmk_is_set(action->flags, pe_action_optional)) {
             color = "blue";
             if (all_actions == FALSE) {
                 goto do_not_write;
             }
 
         } else {
             color = "red";
             CRM_CHECK(!pcmk_is_set(action->flags, pe_action_runnable), ;);
         }
 
         pe__set_action_flags(action, pe_action_dumped);
         crm_trace("\"%s\" [ style=%s color=\"%s\" fontcolor=\"%s\"]",
                 action_name, style, color, font);
         fprintf(dot_strm, "\"%s\" [ style=%s color=\"%s\" fontcolor=\"%s\"]\n",
                 action_name, style, color, font);
   do_not_write:
         free(action_name);
     }
 
     for (gIter = data_set->actions; gIter != NULL; gIter = gIter->next) {
         pe_action_t *action = (pe_action_t *) gIter->data;
 
         GListPtr gIter2 = NULL;
 
         for (gIter2 = action->actions_before; gIter2 != NULL; gIter2 = gIter2->next) {
             pe_action_wrapper_t *before = (pe_action_wrapper_t *) gIter2->data;
 
             char *before_name = NULL;
             char *after_name = NULL;
             const char *style = "dashed";
             gboolean optional = TRUE;
 
             if (before->state == pe_link_dumped) {
                 optional = FALSE;
                 style = "bold";
             } else if (pcmk_is_set(action->flags, pe_action_pseudo)
                        && (before->type & pe_order_stonith_stop)) {
                 continue;
             } else if (before->type == pe_order_none) {
                 continue;
             } else if (pcmk_is_set(before->action->flags, pe_action_dumped)
                        && pcmk_is_set(action->flags, pe_action_dumped)
                        && before->type != pe_order_load) {
                 optional = FALSE;
             }
 
             if (all_actions || optional == FALSE) {
                 before_name = create_action_name(before->action);
                 after_name = create_action_name(action);
                 crm_trace("\"%s\" -> \"%s\" [ style = %s]",
                         before_name, after_name, style);
                 fprintf(dot_strm, "\"%s\" -> \"%s\" [ style = %s]\n",
                         before_name, after_name, style);
                 free(before_name);
                 free(after_name);
             }
         }
     }
 
     fprintf(dot_strm, "}\n");
     fflush(dot_strm);
     fclose(dot_strm);
     return true;
 }
 
 static int
 setup_input(const char *input, const char *output, GError **error)
 {
     int rc = pcmk_rc_ok;
     cib_t *cib_conn = NULL;
     xmlNode *cib_object = NULL;
     char *local_output = NULL;
 
     if (input == NULL) {
         /* Use live CIB */
         cib_conn = cib_new();
         rc = cib_conn->cmds->signon(cib_conn, crm_system_name, cib_command);
         rc = pcmk_legacy2rc(rc);
 
         if (rc == pcmk_rc_ok) {
             rc = cib_conn->cmds->query(cib_conn, NULL, &cib_object, cib_scope_local | cib_sync_call);
         }
 
         cib_conn->cmds->signoff(cib_conn);
         cib_delete(cib_conn);
         cib_conn = NULL;
 
         if (rc != pcmk_rc_ok) {
             rc = pcmk_legacy2rc(rc);
             g_set_error(error, PCMK__RC_ERROR, rc,
                         "Live CIB query failed: %s (%d)", pcmk_rc_str(rc), rc);
             return rc;
 
         } else if (cib_object == NULL) {
             g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_NOINPUT,
                         "Live CIB query failed: empty result");
             return pcmk_rc_no_input;
         }
 
     } else if (pcmk__str_eq(input, "-", pcmk__str_casei)) {
         cib_object = filename2xml(NULL);
 
     } else {
         cib_object = filename2xml(input);
     }
 
     if (get_object_root(XML_CIB_TAG_STATUS, cib_object) == NULL) {
         create_xml_node(cib_object, XML_CIB_TAG_STATUS);
     }
 
     if (cli_config_update(&cib_object, NULL, FALSE) == FALSE) {
         free_xml(cib_object);
         return pcmk_rc_transform_failed;
     }
 
     if (validate_xml(cib_object, NULL, FALSE) != TRUE) {
         free_xml(cib_object);
         return pcmk_rc_schema_validation;
     }
 
     if (output == NULL) {
         char *pid = pcmk__getpid_s();
 
         local_output = get_shadow_file(pid);
         temp_shadow = strdup(local_output);
         output = local_output;
         free(pid);
     }
 
     rc = write_xml_file(cib_object, output, FALSE);
     free_xml(cib_object);
     cib_object = NULL;
 
     if (rc < 0) {
         rc = pcmk_legacy2rc(rc);
         g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_CANTCREAT,
                     "Could not create '%s': %s", output, pcmk_rc_str(rc));
         return rc;
     } else {
         setenv("CIB_file", output, 1);
         free(local_output);
         return pcmk_rc_ok;
     }
 }
 
 static void
 profile_one(const char *xml_file, long long repeat, pe_working_set_t *data_set, char *use_date)
 {
     xmlNode *cib_object = NULL;
     clock_t start = 0;
 
     printf("* Testing %s ...", xml_file);
     fflush(stdout);
 
     cib_object = filename2xml(xml_file);
     start = clock();
 
     if (get_object_root(XML_CIB_TAG_STATUS, cib_object) == NULL) {
         create_xml_node(cib_object, XML_CIB_TAG_STATUS);
     }
 
 
     if (cli_config_update(&cib_object, NULL, FALSE) == FALSE) {
         free_xml(cib_object);
         return;
     }
 
     if (validate_xml(cib_object, NULL, FALSE) != TRUE) {
         free_xml(cib_object);
         return;
     }
 
     for (int i = 0; i < repeat; ++i) {
         xmlNode *input = (repeat == 1)? cib_object : copy_xml(cib_object);
 
         data_set->input = input;
         get_date(data_set, false, use_date);
         pcmk__schedule_actions(data_set, input, NULL);
         pe_reset_working_set(data_set);
     }
     printf(" %.2f secs\n", (clock() - start) / (float) CLOCKS_PER_SEC);
 }
 
 #ifndef FILENAME_MAX
 #  define FILENAME_MAX 512
 #endif
 
 static void
 profile_all(const char *dir, long long repeat, pe_working_set_t *data_set, char *use_date)
 {
     struct dirent **namelist;
 
     int file_num = scandir(dir, &namelist, 0, alphasort);
 
     if (file_num > 0) {
         struct stat prop;
         char buffer[FILENAME_MAX];
 
         while (file_num--) {
             if ('.' == namelist[file_num]->d_name[0]) {
                 free(namelist[file_num]);
                 continue;
 
             } else if (!pcmk__ends_with_ext(namelist[file_num]->d_name,
                                             ".xml")) {
                 free(namelist[file_num]);
                 continue;
             }
             snprintf(buffer, sizeof(buffer), "%s/%s", dir, namelist[file_num]->d_name);
             if (stat(buffer, &prop) == 0 && S_ISREG(prop.st_mode)) {
                 profile_one(buffer, repeat, data_set, use_date);
             }
             free(namelist[file_num]);
         }
         free(namelist);
     }
 }
 
 static GOptionContext *
 build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) {
     GOptionContext *context = NULL;
 
     GOptionEntry extra_prog_entries[] = {
         { "quiet", 'Q', 0, G_OPTION_ARG_NONE, &(args->quiet),
           "Display only essential output",
           NULL },
 
         { NULL }
     };
 
     const char *description = "Operation Specification:\n\n"
                               "The OPSPEC in any command line option is of the form\n"
                               "${resource}_${task}_${interval_in_ms}@${node}=${rc}\n"
                               "(memcached_monitor_20000@bart.example.com=7, for example).\n"
                               "${rc} is an OCF return code.  For more information on these\n"
                               "return codes, refer to https://clusterlabs.org/pacemaker/doc/en-US/Pacemaker/2.0/html/Pacemaker_Administration/s-ocf-return-codes.html\n\n"
                               "Examples:\n\n"
                               "Pretend a recurring monitor action found memcached stopped on node\n"
                               "fred.example.com and, during recovery, that the memcached stop\n"
                               "action failed:\n\n"
                               "\tcrm_simulate -LS --op-inject memcached:0_monitor_20000@bart.example.com=7 "
                               "--op-fail memcached:0_stop_0@fred.example.com=1 --save-output /tmp/memcached-test.xml\n\n"
                               "Now see what the reaction to the stop failed would be:\n\n"
                               "\tcrm_simulate -S --xml-file /tmp/memcached-test.xml\n\n";
 
     context = pcmk__build_arg_context(args, NULL, group, NULL);
     pcmk__add_main_args(context, extra_prog_entries);
     g_option_context_set_description(context, description);
 
     pcmk__add_arg_group(context, "operations", "Operations:",
                         "Show operations options", operation_entries);
     pcmk__add_arg_group(context, "synthetic", "Synthetic Cluster Events:",
                         "Show synthetic cluster event options", synthetic_entries);
     pcmk__add_arg_group(context, "output", "Output Options:",
                         "Show output options", output_entries);
     pcmk__add_arg_group(context, "source", "Data Source:",
                         "Show data source options", source_entries);
 
     return context;
 }
 
 int
 main(int argc, char **argv)
 {
     int rc = pcmk_rc_ok;
     pe_working_set_t *data_set = NULL;
     xmlNode *input = NULL;
 
     GError *error = NULL;
 
     GOptionGroup *output_group = NULL;
     pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
     gchar **processed_args = pcmk__cmdline_preproc(argv, "bdefgiqrtuwxDFGINO");
     GOptionContext *context = build_arg_context(args, &output_group);
 
     /* This must come before g_option_context_parse_strv. */
     options.xml_file = strdup("-");
 
     if (!g_option_context_parse_strv(context, &processed_args, &error)) {
         goto done;
     }
 
     pcmk__cli_init_logging("crm_simulate", args->verbosity);
 
     if (args->version) {
         g_strfreev(processed_args);
         pcmk__free_arg_context(context);
         /* FIXME:  When crm_simulate is converted to use formatted output, this can go. */
         pcmk__cli_help('v', CRM_EX_USAGE);
     }
 
     if (args->verbosity > 0) {
         /* Redirect stderr to stdout so we can grep the output */
         close(STDERR_FILENO);
         dup2(STDOUT_FILENO, STDERR_FILENO);
         action_numbers = TRUE;
     }
 
     if (args->quiet) {
         quiet = TRUE;
     }
 
     data_set = pe_new_working_set();
     if (data_set == NULL) {
         rc = ENOMEM;
         g_set_error(&error, PCMK__RC_ERROR, rc, "Could not allocate working set");
         goto done;
     }
     pe__set_working_set_flags(data_set, pe_flag_no_compat);
 
     if (options.test_dir != NULL) {
         profile_all(options.test_dir, options.repeat, data_set, options.use_date);
         rc = pcmk_rc_ok;
         goto done;
     }
 
     rc = setup_input(options.xml_file, options.store ? options.xml_file : options.output_file, &error);
     if (rc != pcmk_rc_ok) {
         goto done;
     }
 
     global_cib = cib_new();
     rc = global_cib->cmds->signon(global_cib, crm_system_name, cib_command);
     if (rc != pcmk_rc_ok) {
         rc = pcmk_legacy2rc(rc);
         g_set_error(&error, PCMK__RC_ERROR, rc,
                     "Could not connect to the CIB: %s", pcmk_rc_str(rc));
         goto done;
     }
 
     rc = global_cib->cmds->query(global_cib, NULL, &input, cib_sync_call | cib_scope_local);
     if (rc != pcmk_rc_ok) {
         rc = pcmk_legacy2rc(rc);
         g_set_error(&error, PCMK__RC_ERROR, rc,
                     "Could not get local CIB: %s", pcmk_rc_str(rc));
         goto done;
     }
 
     data_set->input = input;
     get_date(data_set, true, options.use_date);
     if(options.xml_file) {
         pe__set_working_set_flags(data_set, pe_flag_sanitized);
     }
     pe__set_working_set_flags(data_set, pe_flag_stdout);
     cluster_status(data_set);
 
     if (quiet == FALSE) {
         int opts = options.print_pending ? pe_print_pending : 0;
 
         if (pcmk_is_set(data_set->flags, pe_flag_maintenance_mode)) {
             quiet_log("\n              *** Resource management is DISABLED ***");
             quiet_log("\n  The cluster will not attempt to start, stop or recover services");
             quiet_log("\n");
         }
 
         if (data_set->disabled_resources || data_set->blocked_resources) {
             quiet_log("%d of %d resource instances DISABLED and %d BLOCKED "
                       "from further action due to failure\n",
                       data_set->disabled_resources, data_set->ninstances,
                       data_set->blocked_resources);
         }
 
         quiet_log("\nCurrent cluster status:\n");
         print_cluster_status(data_set, opts);
     }
 
     if (options.modified) {
         quiet_log("Performing requested modifications\n");
         modify_configuration(data_set, global_cib, options.quorum, options.watchdog, options.node_up,
                              options.node_down, options.node_fail, options.op_inject,
                              options.ticket_grant, options.ticket_revoke, options.ticket_standby,
                              options.ticket_activate);
 
         rc = global_cib->cmds->query(global_cib, NULL, &input, cib_sync_call);
         if (rc != pcmk_rc_ok) {
             rc = pcmk_legacy2rc(rc);
             g_set_error(&error, PCMK__RC_ERROR, rc,
                         "Could not get modified CIB: %s", pcmk_rc_str(rc));
             goto done;
         }
 
         cleanup_calculations(data_set);
         data_set->input = input;
         get_date(data_set, true, options.use_date);
 
         if(options.xml_file) {
             pe__set_working_set_flags(data_set, pe_flag_sanitized);
         }
         pe__set_working_set_flags(data_set, pe_flag_stdout);
         cluster_status(data_set);
     }
 
     if (options.input_file != NULL) {
         rc = write_xml_file(input, options.input_file, FALSE);
         if (rc < 0) {
             rc = pcmk_legacy2rc(rc);
             g_set_error(&error, PCMK__RC_ERROR, rc,
                         "Could not create '%s': %s", options.input_file, pcmk_rc_str(rc));
             goto done;
         }
     }
 
     if (options.process || options.simulate) {
         crm_time_t *local_date = NULL;
 
         if (show_scores && show_utilization) {
             printf("Allocation scores and utilization information:\n");
         } else if (show_scores) {
             fprintf(stdout, "Allocation scores:\n");
         } else if (show_utilization) {
             printf("Utilization information:\n");
         }
 
         pcmk__schedule_actions(data_set, input, local_date);
         input = NULL;           /* Don't try and free it twice */
 
         if (options.graph_file != NULL) {
             write_xml_file(data_set->graph, options.graph_file, FALSE);
         }
 
         if (options.dot_file != NULL) {
             if (!create_dotfile(data_set, options.dot_file, options.all_actions, &error)) {
                 goto done;
             }
         }
 
         if (quiet == FALSE) {
             GListPtr gIter = NULL;
 
             quiet_log("%sTransition Summary:\n", show_scores || show_utilization
                       || options.modified ? "\n" : "");
             fflush(stdout);
 
             LogNodeActions(data_set, TRUE);
             for (gIter = data_set->resources; gIter != NULL; gIter = gIter->next) {
                 pe_resource_t *rsc = (pe_resource_t *) gIter->data;
 
                 LogActions(rsc, data_set, TRUE);
             }
         }
     }
 
     rc = pcmk_rc_ok;
 
     if (options.simulate) {
         if (run_simulation(data_set, global_cib, options.op_fail, quiet) != pcmk_rc_ok) {
             rc = pcmk_rc_error;
         }
         if(quiet == FALSE) {
             get_date(data_set, true, options.use_date);
 
             quiet_log("\nRevised cluster status:\n");
             pe__set_working_set_flags(data_set, pe_flag_stdout);
             cluster_status(data_set);
             print_cluster_status(data_set, 0);
         }
     }
 
   done:
-    if (error != NULL) {
-        fprintf(stderr, "%s: %s\n", g_get_prgname(), error->message);
-        g_clear_error(&error);
-    }
+    pcmk__output_and_clear_error(error, NULL);
 
     /* There sure is a lot to free in options. */
     free(options.dot_file);
     free(options.graph_file);
     g_free(options.input_file);
     g_list_free_full(options.node_up, g_free);
     g_list_free_full(options.node_down, g_free);
     g_list_free_full(options.node_fail, g_free);
     g_list_free_full(options.op_fail, g_free);
     g_list_free_full(options.op_inject, g_free);
     g_free(options.output_file);
     free(options.quorum);
     g_free(options.test_dir);
     g_list_free_full(options.ticket_grant, g_free);
     g_list_free_full(options.ticket_revoke, g_free);
     g_list_free_full(options.ticket_standby, g_free);
     g_list_free_full(options.ticket_activate, g_free);
     free(options.use_date);
     free(options.watchdog);
     free(options.xml_file);
 
     pcmk__free_arg_context(context);
     g_strfreev(processed_args);
 
     if (data_set) {
         pe_free_working_set(data_set);
     }
 
     if (global_cib) {
         global_cib->cmds->signoff(global_cib);
         cib_delete(global_cib);
     }
 
     fflush(stderr);
 
     if (temp_shadow) {
         unlink(temp_shadow);
         free(temp_shadow);
     }
     crm_exit(pcmk_rc2exitc(rc));
 }
diff --git a/tools/crmadmin.c b/tools/crmadmin.c
index 1c4bfef562..04d35f41d9 100644
--- a/tools/crmadmin.c
+++ b/tools/crmadmin.c
@@ -1,297 +1,289 @@
 /*
  * Copyright 2004-2021 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 <stdio.h>
 #include <stdbool.h>
 #include <stdlib.h>             // atoi()
 
 #include <glib.h>               // gboolean, GMainLoop, etc.
 #include <libxml/tree.h>        // xmlNode
 
 #include <pacemaker-internal.h>
 
 #include <crm/common/cmdline_internal.h>
 #include <crm/common/output_internal.h>
 
 #define SUMMARY "query and manage the Pacemaker controller"
 
 static enum {
     cmd_none,
     cmd_shutdown,
     cmd_health,
     cmd_elect_dc,
     cmd_whois_dc,
     cmd_list_nodes,
     cmd_pacemakerd_health,
 } command = cmd_none;
 
 struct {
     gboolean health;
     gint timeout;
     char *optarg;
     char *ipc_name;
     gboolean BASH_EXPORT;
 } options = {
     .optarg = NULL,
     .ipc_name = NULL,
     .BASH_EXPORT = FALSE
 };
 
 gboolean command_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 
 static GOptionEntry command_options[] = {
     { "status", 'S', 0, G_OPTION_ARG_CALLBACK, command_cb,
       "Display the status of the specified node."
       "\n                          Result is state of node's internal finite state"
       "\n                          machine, which can be useful for debugging",
       NULL
     },
     { "pacemakerd", 'P', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Display the status of local pacemakerd."
       "\n                          Result is the state of the sub-daemons watched"
       "\n                          by pacemakerd.",
       NULL
     },
     { "dc_lookup", 'D', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Display the uname of the node co-ordinating the cluster."
       "\n                          This is an internal detail rarely useful to"
       "\n                          administrators except when deciding on which"
       "\n                          node to examine the logs.",
       NULL
     },
     { "nodes", 'N', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "Display the uname of all member nodes [optionally filtered by type (comma-separated)]"
       "\n                          Types: all (default), cluster, guest, remote",
       "TYPE"
     },
     { "election", 'E', G_OPTION_FLAG_HIDDEN|G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
       "(Advanced) Start an election for the cluster co-ordinator",
       NULL
     },
     { "kill", 'K', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_CALLBACK, command_cb,
       "(Advanced) Stop controller (not rest of cluster stack) on specified node",
       NULL
     },
     { "health", 'H', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.health,
       NULL,
       NULL
     },
 
     { NULL }
 };
 
 static GOptionEntry additional_options[] = {
     { "timeout", 't', 0, G_OPTION_ARG_INT, &options.timeout,
       "Time (in milliseconds) to wait before declaring the"
       "\n                          operation failed",
       NULL
     },
     { "bash-export", 'B', 0, G_OPTION_ARG_NONE, &options.BASH_EXPORT,
       "Display nodes as shell commands of the form 'export uname=uuid'"
       "\n                          (valid with -N/--nodes)",
     },
     { "ipc-name", 'i', 0, G_OPTION_ARG_STRING, &options.ipc_name,
       "Name to use for ipc instead of 'crmadmin' (with -P/--pacemakerd).",
       NULL
     },
 
     { NULL }
 };
 
 gboolean
 command_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error)
 {
     if (!strcmp(option_name, "--status") || !strcmp(option_name, "-S")) {
         command = cmd_health;
         crm_trace("Option %c => %s", 'S', optarg);
     }
 
     if (!strcmp(option_name, "--pacemakerd") || !strcmp(option_name, "-P")) {
         command = cmd_pacemakerd_health;
     }
 
     if (!strcmp(option_name, "--dc_lookup") || !strcmp(option_name, "-D")) {
         command = cmd_whois_dc;
     }
 
     if (!strcmp(option_name, "--nodes") || !strcmp(option_name, "-N")) {
         command = cmd_list_nodes;
     }
 
     if (!strcmp(option_name, "--election") || !strcmp(option_name, "-E")) {
         command = cmd_elect_dc;
     }
 
     if (!strcmp(option_name, "--kill") || !strcmp(option_name, "-K")) {
         command = cmd_shutdown;
         crm_trace("Option %c => %s", 'K', optarg);
     }
 
     if (optarg) {
         if (options.optarg != NULL) {
             free(options.optarg);
         }
         options.optarg = strdup(optarg);
     }
 
     return TRUE;
 }
 
 static pcmk__supported_format_t formats[] = {
     PCMK__SUPPORTED_FORMAT_TEXT,
     PCMK__SUPPORTED_FORMAT_XML,
     { NULL, NULL, NULL }
 };
 
 static GOptionContext *
 build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) {
     GOptionContext *context = NULL;
 
     const char *description = "Report bugs to users@clusterlabs.org";
 
     GOptionEntry extra_prog_entries[] = {
         { "quiet", 'q', 0, G_OPTION_ARG_NONE, &(args->quiet),
           "Display only the essential query information",
           NULL },
 
         { NULL }
     };
 
     context = pcmk__build_arg_context(args, "text (default), xml", group, NULL);
     g_option_context_set_description(context, description);
 
     /* Add the -q option, which cannot be part of the globally supported options
      * because some tools use that flag for something else.
      */
     pcmk__add_main_args(context, extra_prog_entries);
 
     pcmk__add_arg_group(context, "command", "Commands:",
                         "Show command options", command_options);
     pcmk__add_arg_group(context, "additional", "Additional Options:",
                         "Show additional options", additional_options);
     return context;
 }
 
 int
 main(int argc, char **argv)
 {
     crm_exit_t exit_code = CRM_EX_OK;
     int rc;
     int argerr = 0;
 
     GError *error = NULL;
 
     pcmk__output_t *out = NULL;
 
     GOptionGroup *output_group = NULL;
     pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
     gchar **processed_args = pcmk__cmdline_preproc(argv, "itBDEHKNPS");
     GOptionContext *context = build_arg_context(args, &output_group);
 
     pcmk__register_formats(output_group, formats);
     if (!g_option_context_parse_strv(context, &processed_args, &error)) {
         exit_code = CRM_EX_USAGE;
         goto done;
     }
 
     pcmk__cli_init_logging("crmadmin", args->verbosity);
 
     rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
     if (rc != pcmk_rc_ok) {
         exit_code = CRM_EX_ERROR;
         g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Error creating output format %s: %s",
                     args->output_ty, pcmk_rc_str(rc));
         goto done;
     }
 
     pcmk__register_lib_messages(out);
 
     out->quiet = args->quiet;
 
     if (!pcmk__force_args(context, &error, "%s --xml-simple-list --xml-substitute", g_get_prgname())) {
         goto done;
     }
 
     if (args->version) {
         out->version(out, false);
         goto done;
     }
 
     if (options.health) {
         out->err(out, "Cluster-wide health option not supported");
         ++argerr;
     }
 
     if (command == cmd_none) {
         out->err(out, "error: Must specify a command option");
         ++argerr;
     }
 
     if (argerr) {
         char *help = g_option_context_get_help(context, TRUE, NULL);
 
         out->err(out, "%s", help);
         g_free(help);
         exit_code = CRM_EX_USAGE;
         goto done;
     }
 
     switch (command) {
         case cmd_health:
             rc = pcmk__controller_status(out, options.optarg, options.timeout);
             break;
         case cmd_pacemakerd_health:
             rc = pcmk__pacemakerd_status(out, options.ipc_name, options.timeout);
             break;
         case cmd_list_nodes:
             rc = pcmk__list_nodes(out, options.optarg, options.BASH_EXPORT);
             break;
         case cmd_whois_dc:
             rc = pcmk__designated_controller(out, options.timeout);
             break;
         case cmd_shutdown:
             rc = pcmk__shutdown_controller(out, options.optarg);
             break;
         case cmd_elect_dc:
             rc = pcmk__start_election(out);
             break;
         case cmd_none:
             rc = pcmk_rc_error;
             break;
     }
 
     if (rc != pcmk_rc_ok) {
         out->err(out, "error: Command failed: %s", pcmk_rc_str(rc));
         exit_code = pcmk_rc2exitc(rc);
     }
 
 done:
 
     g_strfreev(processed_args);
     pcmk__free_arg_context(context);
 
-    if (error != NULL) {
-        if (out != NULL) {
-            out->err(out, "%s: %s", g_get_prgname(), error->message);
-        } else {
-            fprintf(stderr, "%s: %s\n", g_get_prgname(), error->message);
-        }
-
-        g_clear_error(&error);
-    }
+    pcmk__output_and_clear_error(error, out);
 
     if (out != NULL) {
         out->finish(out, exit_code, true, NULL);
         pcmk__output_free(out);
     }
     return crm_exit(exit_code);
 
 }
diff --git a/tools/stonith_admin.c b/tools/stonith_admin.c
index 0c84d56f0b..cc2668ef23 100644
--- a/tools/stonith_admin.c
+++ b/tools/stonith_admin.c
@@ -1,632 +1,624 @@
 /*
  * Copyright 2009-2021 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 <sys/param.h>
 #include <stdio.h>
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <unistd.h>
 #include <sys/utsname.h>
 
 #include <errno.h>
 #include <fcntl.h>
 #include <stdbool.h>
 #include <stdlib.h>
 #include <string.h>
 
 #include <crm/crm.h>
 #include <crm/msg_xml.h>
 #include <crm/common/ipc.h>
 #include <crm/cluster/internal.h>
 #include <crm/common/cmdline_internal.h>
 #include <crm/common/output_internal.h>
 
 #include <crm/stonith-ng.h>
 #include <crm/fencing/internal.h>
 #include <crm/cib.h>
 #include <crm/pengine/status.h>
 
 #include <crm/common/xml.h>
 #include <pacemaker-internal.h>
 
 #define SUMMARY "stonith_admin - Access the Pacemaker fencing API"
 
 char action = 0;
 
 struct {
     gboolean as_nodeid;
     gboolean broadcast;
     gboolean cleanup;
     gboolean installed;
     gboolean metadata;
     gboolean registered;
     gboolean validate_cfg;
     stonith_key_value_t *devices;
     stonith_key_value_t *params;
     int fence_level;
     int timeout ;
     int tolerance;
     int delay;
     char *agent;
     char *confirm_host;
     char *fence_host;
     char *history;
     char *last_fenced;
     char *query;
     char *reboot_host;
     char *register_dev;
     char *register_level;
     char *targets;
     char *terminate;
     char *unfence_host;
     char *unregister_dev;
     char *unregister_level;
 } options = {
     .timeout = 120,
     .delay = 0
 };
 
 gboolean add_env_params(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean add_stonith_device(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean add_stonith_params(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean add_tolerance(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 gboolean set_tag(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
 
 #define INDENT "                                    "
 
 /* *INDENT-OFF* */
 static GOptionEntry defn_entries[] = {
     { "register", 'R', 0, G_OPTION_ARG_STRING, &options.register_dev,
       "Register the named stonith device. Requires: --agent.\n"
       INDENT "Optional: --option, --env-option.",
       "DEVICE" },
     { "deregister", 'D', 0, G_OPTION_ARG_STRING, &options.unregister_dev,
       "De-register the named stonith device.",
       "DEVICE" },
     { "register-level", 'r', 0, G_OPTION_ARG_STRING, &options.register_level,
       "Register a stonith level for the named target,\n"
       INDENT "specified as one of NAME, @PATTERN, or ATTR=VALUE.\n"
       INDENT "Requires: --index and one or more --device entries.",
       "TARGET" },
     { "deregister-level", 'd', 0, G_OPTION_ARG_STRING, &options.unregister_level,
       "Unregister a stonith level for the named target,\n"
       INDENT "specified as for --register-level. Requires: --index",
       "TARGET" },
 
     { NULL }
 };
 
 static GOptionEntry query_entries[] = {
     { "list", 'l', 0, G_OPTION_ARG_STRING, &options.terminate,
       "List devices that can terminate the specified host.\n"
       INDENT "Optional: --timeout",
       "HOST" },
     { "list-registered", 'L', 0, G_OPTION_ARG_NONE, &options.registered,
       "List all registered devices. Optional: --timeout.",
       NULL },
     { "list-installed", 'I', 0, G_OPTION_ARG_NONE, &options.installed,
       "List all installed devices. Optional: --timeout.",
       NULL },
     { "list-targets", 's', 0, G_OPTION_ARG_STRING, &options.targets,
       "List the targets that can be fenced by the\n"
       INDENT "named device. Optional: --timeout.",
       "DEVICE" },
     { "metadata", 'M', 0, G_OPTION_ARG_NONE, &options.metadata,
       "Show agent metadata. Requires: --agent.\n"
       INDENT "Optional: --timeout.",
       NULL },
     { "query", 'Q', 0, G_OPTION_ARG_STRING, &options.query,
       "Check the named device's status. Optional: --timeout.",
       "DEVICE" },
     { "history", 'H', 0, G_OPTION_ARG_STRING, &options.history,
       "Show last successful fencing operation for named node\n"
       INDENT "(or '*' for all nodes). Optional: --timeout, --cleanup,\n"
       INDENT "--quiet (show only the operation's epoch timestamp),\n"
       INDENT "--verbose (show all recorded and pending operations),\n"
       INDENT "--broadcast (update history from all nodes available).",
       "NODE" },
     { "last", 'h', 0, G_OPTION_ARG_STRING, &options.last_fenced,
       "Indicate when the named node was last fenced.\n"
       INDENT "Optional: --as-node-id.",
       "NODE" },
     { "validate", 'K', 0, G_OPTION_ARG_NONE, &options.validate_cfg,
       "Validate a fence device configuration.\n"
       INDENT "Requires: --agent. Optional: --option, --env-option,\n"
       INDENT "--quiet (print no output, only return status).",
       NULL },
 
     { NULL }
 };
 
 static GOptionEntry fence_entries[] = {
     { "fence", 'F', 0, G_OPTION_ARG_STRING, &options.fence_host,
       "Fence named host. Optional: --timeout, --tolerance, --delay.",
       "HOST" },
     { "unfence", 'U', 0, G_OPTION_ARG_STRING, &options.unfence_host,
       "Unfence named host. Optional: --timeout, --tolerance, --delay.",
       "HOST" },
     { "reboot", 'B', 0, G_OPTION_ARG_STRING, &options.reboot_host,
       "Reboot named host. Optional: --timeout, --tolerance, --delay.",
       "HOST" },
     { "confirm", 'C', 0, G_OPTION_ARG_STRING, &options.confirm_host,
       "Tell cluster that named host is now safely down.",
       "HOST", },
 
     { NULL }
 };
 
 static GOptionEntry addl_entries[] = {
     { "cleanup", 'c', 0, G_OPTION_ARG_NONE, &options.cleanup,
       "Cleanup wherever appropriate. Requires --history.",
       NULL },
     { "broadcast", 'b', 0, G_OPTION_ARG_NONE, &options.broadcast,
       "Broadcast wherever appropriate.",
       NULL },
     { "agent", 'a', 0, G_OPTION_ARG_STRING, &options.agent,
       "The agent to use (for example, fence_xvm;\n"
       INDENT "with --register, --metadata, --validate).",
       "AGENT" },
     { "option", 'o', 0, G_OPTION_ARG_CALLBACK, add_stonith_params,
       "Specify a device configuration parameter as NAME=VALUE\n"
       INDENT "(may be specified multiple times; with --register,\n"
       INDENT "--validate).",
       "PARAM" },
     { "env-option", 'e', 0, G_OPTION_ARG_CALLBACK, add_env_params,
       "Specify a device configuration parameter with the\n"
       INDENT "specified name, using the value of the\n"
       INDENT "environment variable of the same name prefixed with\n"
       INDENT "OCF_RESKEY_ (may be specified multiple times;\n"
       INDENT "with --register, --validate).",
       "PARAM" },
     { "tag", 'T', 0, G_OPTION_ARG_CALLBACK, set_tag,
       "Identify fencing operations in logs with the specified\n"
       INDENT "tag; useful when multiple entities might invoke\n"
       INDENT "stonith_admin (used with most commands).",
       "TAG" },
     { "device", 'v', 0, G_OPTION_ARG_CALLBACK, add_stonith_device,
       "Device ID (with --register-level, device to associate with\n"
       INDENT "a given host and level; may be specified multiple times)"
 #if SUPPORT_CIBSECRETS
       "\n" INDENT "(with --validate, name to use to load CIB secrets)"
 #endif
       ".",
       "DEVICE" },
     { "index", 'i', 0, G_OPTION_ARG_INT, &options.fence_level,
       "The stonith level (1-9) (with --register-level,\n"
       INDENT "--deregister-level).",
       "LEVEL" },
     { "timeout", 't', 0, G_OPTION_ARG_INT, &options.timeout,
       "Operation timeout in seconds (default 120;\n"
       INDENT "used with most commands).",
       "SECONDS" },
     { "delay", 'y', 0, G_OPTION_ARG_INT, &options.delay,
       "Apply a fencing delay in seconds. Any static/random delays from\n"
       INDENT "pcmk_delay_base/max will be added, otherwise all\n"
       INDENT "disabled with the value -1\n"
       INDENT "(default 0; with --fence, --reboot, --unfence).",
       "SECONDS" },
     { "as-node-id", 'n', 0, G_OPTION_ARG_NONE, &options.as_nodeid,
       "(Advanced) The supplied node is the corosync node ID\n"
       INDENT "(with --last).",
       NULL },
     { "tolerance", 0, 0, G_OPTION_ARG_CALLBACK, add_tolerance,
       "(Advanced) Do nothing if an equivalent --fence request\n"
       INDENT "succeeded less than this many seconds earlier\n"
       INDENT "(with --fence, --unfence, --reboot).",
       "SECONDS" },
 
     { NULL }
 };
 /* *INDENT-ON* */
 
 static pcmk__supported_format_t formats[] = {
     PCMK__SUPPORTED_FORMAT_HTML,
     PCMK__SUPPORTED_FORMAT_TEXT,
     PCMK__SUPPORTED_FORMAT_XML,
     { NULL, NULL, NULL }
 };
 
 static const int st_opts = st_opt_sync_call | st_opt_allow_suicide;
 
 static char *name = NULL;
 
 gboolean
 add_env_params(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     char *key = crm_strdup_printf("OCF_RESKEY_%s", optarg);
     const char *env = getenv(key);
     gboolean retval = TRUE;
 
     if (env == NULL) {
         g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_INVALID_PARAM, "Invalid option: -e %s", optarg);
         retval = FALSE;
     } else {
         crm_info("Got: '%s'='%s'", optarg, env);
         options.params = stonith_key_value_add(options.params, optarg, env);
     }
 
     free(key);
     return retval;
 }
 
 gboolean
 add_stonith_device(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.devices = stonith_key_value_add(options.devices, NULL, optarg);
     return TRUE;
 }
 
 gboolean
 add_tolerance(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     options.tolerance = crm_get_msec(optarg) / 1000;
     return TRUE;
 }
 
 gboolean
 add_stonith_params(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     char *name = NULL;
     char *value = NULL;
     int rc = 0;
     gboolean retval = TRUE;
 
     crm_info("Scanning: -o %s", optarg);
 
     rc = pcmk_scan_nvpair(optarg, &name, &value);
 
     if (rc != 2) {
         rc = pcmk_legacy2rc(rc);
         g_set_error(error, PCMK__RC_ERROR, rc, "Invalid option: -o %s: %s", optarg, pcmk_rc_str(rc));
         retval = FALSE;
     } else {
         crm_info("Got: '%s'='%s'", name, value);
         options.params = stonith_key_value_add(options.params, name, value);
     }
 
     free(name);
     free(value);
     return retval;
 }
 
 gboolean
 set_tag(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     free(name);
     name = crm_strdup_printf("%s.%s", crm_system_name, optarg);
     return TRUE;
 }
 
 static GOptionContext *
 build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) {
     GOptionContext *context = NULL;
 
     GOptionEntry extra_prog_entries[] = {
         { "quiet", 'q', 0, G_OPTION_ARG_NONE, &(args->quiet),
           "Be less descriptive in output.",
           NULL },
 
         { NULL }
     };
 
     context = pcmk__build_arg_context(args, "text (default), html, xml", group, NULL);
 
     /* Add the -q option, which cannot be part of the globally supported options
      * because some tools use that flag for something else.
      */
     pcmk__add_main_args(context, extra_prog_entries);
 
     pcmk__add_arg_group(context, "definition", "Device Definition Commands:",
                         "Show device definition help", defn_entries);
     pcmk__add_arg_group(context, "queries", "Queries:",
                         "Show query help", query_entries);
     pcmk__add_arg_group(context, "fence", "Fencing Commands:",
                         "Show fence help", fence_entries);
     pcmk__add_arg_group(context, "additional", "Additional Options:",
                         "Show additional options", addl_entries);
     return context;
 }
 
 int
 main(int argc, char **argv)
 {
     int rc = 0;
     crm_exit_t exit_code = CRM_EX_OK;
     bool no_connect = false;
     bool required_agent = false;
 
     char *target = NULL;
     const char *device = NULL;
     stonith_t *st = NULL;
 
     GError *error = NULL;
 
     pcmk__output_t *out = NULL;
 
     GOptionGroup *output_group = NULL;
     pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
     gchar **processed_args = pcmk__cmdline_preproc(argv, "adehilorstvBCDFHQRTU");
     GOptionContext *context = build_arg_context(args, &output_group);
 
     pcmk__register_formats(output_group, formats);
     if (!g_option_context_parse_strv(context, &processed_args, &error)) {
         exit_code = CRM_EX_USAGE;
         goto done;
     }
 
     pcmk__cli_init_logging("stonith_admin", args->verbosity);
 
     name = strdup(crm_system_name);
 
     rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
     if (rc != pcmk_rc_ok) {
         exit_code = CRM_EX_ERROR;
         g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Error creating output format %s: %s",
                     args->output_ty, pcmk_rc_str(rc));
         goto done;
     }
 
     stonith__register_messages(out);
 
     if (args->version) {
         out->version(out, false);
         goto done;
     }
 
     if (options.validate_cfg) {
         required_agent = true;
         no_connect = true;
         action = 'K';
     }
 
     if (options.installed) {
         no_connect = true;
         action = 'I';
     }
 
     if (options.registered) {
         action = 'L';
     }
 
     if (options.register_dev != NULL) {
         required_agent = true;
         action = 'R';
         device = options.register_dev;
     }
 
     if (options.query != NULL) {
         action = 'Q';
         device = options.query;
     }
 
     if (options.unregister_dev != NULL) {
         action = 'D';
         device = options.unregister_dev;
     }
 
     if (options.targets != NULL) {
         action = 's';
         device = options.targets;
     }
 
     if (options.terminate != NULL) {
         action = 'L';
         target = options.terminate;
     }
 
     if (options.metadata) {
         no_connect = true;
         required_agent = true;
         action = 'M';
     }
 
     if (options.reboot_host != NULL) {
         no_connect = true;
         action = 'B';
         target = options.reboot_host;
         crm_log_args(argc, argv);
     }
 
     if (options.fence_host != NULL) {
         no_connect = true;
         action = 'F';
         target = options.fence_host;
         crm_log_args(argc, argv);
     }
 
     if (options.unfence_host != NULL) {
         no_connect = true;
         action = 'U';
         target = options.unfence_host;
         crm_log_args(argc, argv);
     }
 
     if (options.confirm_host != NULL) {
         action = 'C';
         target = options.confirm_host;
         crm_log_args(argc, argv);
     }
 
     if (options.last_fenced != NULL) {
         action = 'h';
         target = options.last_fenced;
     }
 
     if (options.history != NULL) {
         action = 'H';
         target = options.history;
     }
 
     if (options.register_level != NULL) {
         action = 'r';
         target = options.register_level;
     }
 
     if (options.unregister_level != NULL) {
         action = 'd';
         target = options.unregister_level;
     }
 
     if (action == 0) {
         char *help = g_option_context_get_help(context, TRUE, NULL);
 
         out->err(out, "%s", help);
         g_free(help);
         exit_code = CRM_EX_USAGE;
         goto done;
     }
 
     if (required_agent && options.agent == NULL) {
         char *help = g_option_context_get_help(context, TRUE, NULL);
 
         out->err(out, "Please specify an agent to query using -a,--agent [value]");
         out->err(out, "%s", help);
         g_free(help);
         exit_code = CRM_EX_USAGE;
         goto done;
     }
 
     out->quiet = args->quiet;
 
     st = stonith_api_new();
     if (st == NULL) {
         rc = -ENOMEM;
     } else if (!no_connect) {
         rc = st->cmds->connect(st, name, NULL);
     }
     if (rc < 0) {
         out->err(out, "Could not connect to fencer: %s", pcmk_strerror(rc));
         exit_code = CRM_EX_DISCONNECT;
         goto done;
     }
 
     switch (action) {
         case 'I':
             rc = pcmk__fence_installed(out, st, options.timeout*1000);
             if (rc != pcmk_rc_ok) {
                 out->err(out, "Failed to list installed devices: %s", pcmk_strerror(rc));
             }
 
             break;
 
         case 'L':
             rc = pcmk__fence_registered(out, st, target, options.timeout*1000);
             if (rc != pcmk_rc_ok) {
                 out->err(out, "Failed to list registered devices: %s", pcmk_strerror(rc));
             }
 
             break;
 
         case 'Q':
             rc = st->cmds->monitor(st, st_opts, device, options.timeout);
             if (rc != pcmk_rc_ok) {
                 rc = st->cmds->list(st, st_opts, device, NULL, options.timeout);
             }
             rc = pcmk_legacy2rc(rc);
             break;
 
         case 's':
             rc = pcmk__fence_list_targets(out, st, device, options.timeout*1000);
             if (rc != pcmk_rc_ok) {
                 out->err(out, "Couldn't list targets: %s", pcmk_strerror(rc));
             }
 
             break;
 
         case 'R':
             rc = st->cmds->register_device(st, st_opts, device, NULL, options.agent,
                                            options.params);
             rc = pcmk_legacy2rc(rc);
             break;
 
         case 'D':
             rc = st->cmds->remove_device(st, st_opts, device);
             rc = pcmk_legacy2rc(rc);
             break;
 
         case 'd':
             rc = pcmk__fence_unregister_level(st, target, options.fence_level);
             break;
 
         case 'r':
             rc = pcmk__fence_register_level(st, target, options.fence_level, options.devices);
             break;
 
         case 'M':
             rc = pcmk__fence_metadata(out, st, options.agent, options.timeout*1000);
             if (rc != pcmk_rc_ok) {
                 out->err(out, "Can't get fence agent meta-data: %s", pcmk_strerror(rc));
             }
 
             break;
 
         case 'C':
             rc = st->cmds->confirm(st, st_opts, target);
             rc = pcmk_legacy2rc(rc);
             break;
 
         case 'B':
             rc = pcmk__fence_action(st, target, "reboot", name, options.timeout*1000,
                                     options.tolerance*1000, options.delay);
             break;
 
         case 'F':
             rc = pcmk__fence_action(st, target, "off", name, options.timeout*1000,
                                     options.tolerance*1000, options.delay);
             break;
 
         case 'U':
             rc = pcmk__fence_action(st, target, "on", name, options.timeout*1000,
                                     options.tolerance*1000, options.delay);
             break;
 
         case 'h':
             rc = pcmk__fence_last(out, target, options.as_nodeid);
             break;
 
         case 'H':
             rc = pcmk__fence_history(out, st, target, options.timeout*1000, args->verbosity,
                                      options.broadcast, options.cleanup);
             break;
 
         case 'K':
             device = options.devices ? options.devices->key : NULL;
             rc = pcmk__fence_validate(out, st, options.agent, device, options.params,
                                         options.timeout*1000);
             break;
     }
 
     crm_info("Command returned: %s (%d)", pcmk_rc_str(rc), rc);
     exit_code = pcmk_rc2exitc(rc);
 
   done:
     g_strfreev(processed_args);
     pcmk__free_arg_context(context);
 
-    if (error != NULL) {
-        if (out != NULL) {
-            out->err(out, "%s: %s", g_get_prgname(), error->message);
-        } else {
-            fprintf(stderr, "%s: %s\n", g_get_prgname(), error->message);
-        }
-
-        g_clear_error(&error);
-    }
+    pcmk__output_and_clear_error(error, out);
 
     if (out != NULL) {
         out->finish(out, exit_code, true, NULL);
         pcmk__output_free(out);
     }
     free(name);
     stonith_key_value_freeall(options.params, 1, 1);
 
     if (st != NULL) {
         st->cmds->disconnect(st);
         stonith_api_delete(st);
     }
 
     return exit_code;
 }