diff --git a/include/crm/common/output.h b/include/crm/common/output.h new file mode 100644 index 0000000000..f31b78430f --- /dev/null +++ b/include/crm/common/output.h @@ -0,0 +1,513 @@ +/* + * Copyright 2019 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 +# include + +# include +# include + +# define PCMK__API_VERSION "1.0" + +/* Add to the long_options block in each tool to get the formatted output + * command line options added. Then call pcmk__parse_output_args to handle + * them. + */ +# define PCMK__OUTPUT_OPTIONS(fmts) \ + { "output-as", required_argument, NULL, 0, \ + "Specify the format for output, one of: " fmts \ + }, \ + { "output-to", required_argument, NULL, 0, \ + "Specify the destination for formatted output, \"-\" for stdout or a filename" \ + } + +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. + */ +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; + +/* Basic formatters everything supports. This block needs to be updated every + * time a new base formatter is added. + */ +pcmk__output_t *pcmk__mk_text_output(char **argv); +pcmk__output_t *pcmk__mk_xml_output(char **argv); + +/*! + * \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. + */ + char *fmt_name; + + /*! + * \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. + */ + char *request; + + /*! + * \brief Does this formatter support a special quiet mode? + * + * In this mode, most output can be supressed but some information is still + * displayed to an interactive user. In general, machine-readable output + * formats will not support this while user-oriented formats will. + */ + bool supports_quiet; + + /*! + * \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. Note that pcmk__output_free() will automatically call this + * function, so there is typically no need to do so manually. + * + * \note For formatted output implementers - This function should be written in + * such a way that it can be called repeatedly on a previously finished + * object without crashing. + * + * \param[in,out] out The output functions structure. + * \param[in] exit_status The exit value of the whole program. + */ + void (*finish) (pcmk__output_t *out, crm_exit_t exit_status); + + /*! + * \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 0 if a function was registered for the message, that function was + * called, and returned successfully. A negative value is returned if + * no function was registered. A positive value is returned if the + * function was called but encountered an error. + */ + 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 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 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] name A descriptive, user-facing name for this list. + * \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. + */ + void (*begin_list) (pcmk__output_t *out, const char *name, + const char *singular_noun, const char *plural_noun); + + /*! + * \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] content The item to be formatted. + */ + void (*list_item) (pcmk__output_t *out, const char *name, const char *content); + + /*! + * \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 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(). This will first call the finish function. + * + * \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. + * \param[in] exit_status The exit value of the whole program. + */ +void pcmk__output_free(pcmk__output_t *out, crm_exit_t exit_status); + +/*! + * \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 0 on success or an error code on error. + */ +int pcmk__output_new(pcmk__output_t **out, const char *fmt_name, + const char *filename, char **argv); + +/*! + * \internal + * \brief Process formatted output related command line options. This should + * be called wherever other long options are handled. + * + * \param[in] argname The long command line argument to process. + * \param[in] argvalue The value of the command line argument. + * \param[out] output_ty How should output be formatted? ("text", "xml", etc.) + * \param[out] output_dest Where should formatted output be written to? This is + * typically a filename, but could be NULL or "-". + * + * \return true if longname was handled, false otherwise. + */ +bool +pcmk__parse_output_args(const char *argname, char *argvalue, char **output_ty, + char **output_dest); + +/*! + * \internal + * \brief Register a new output formatter, making it available for use + * the same as a base formatter. + * + * \param[in] fmt The new output formatter to register. + * + * \return 0 on success or an error code on error. + */ +int +pcmk__register_format(const char *fmt_name, pcmk__output_factory_t create); + + +/*! + * \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 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__xml_add_node(pcmk__output_t *out, xmlNodePtr node); + +/*! + * \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__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__xml_pop_parent(pcmk__output_t *out); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/lib/common/Makefile.am b/lib/common/Makefile.am index 78f8d4e38b..f8bebd7acd 100644 --- a/lib/common/Makefile.am +++ b/lib/common/Makefile.am @@ -1,43 +1,44 @@ # # Copyright 2004-2019 Andrew Beekhof # # This source code is licensed under the GNU General Public License version 2 # or later (GPLv2+) WITHOUT ANY WARRANTY. # include $(top_srcdir)/Makefile.common AM_CPPFLAGS += -I$(top_builddir)/lib/gnu -I$(top_srcdir)/lib/gnu -DPCMK_SCHEMAS_EMERGENCY_XSLT=0 ## libraries lib_LTLIBRARIES = libcrmcommon.la # Disable -Wcast-qual if used, because we do some hacky casting, # and because libxml2 has some signatures that should be const but aren't # for backward compatibility reasons. # s390 needs -fPIC # s390-suse-linux/bin/ld: .libs/ipc.o: relocation R_390_PC32DBL against `__stack_chk_fail@@GLIBC_2.4' can not be used when making a shared object; recompile with -fPIC CFLAGS = $(CFLAGS_COPY:-Wcast-qual=) -fPIC noinst_HEADERS = crmcommon_private.h libcrmcommon_la_LDFLAGS = -version-info 35:0:1 libcrmcommon_la_CFLAGS = $(CFLAGS_HARDENED_LIB) libcrmcommon_la_LDFLAGS += $(LDFLAGS_HARDENED_LIB) libcrmcommon_la_LIBADD = @LIBADD_DL@ $(GNUTLSLIBS) libcrmcommon_la_SOURCES = compat.c digest.c ipc.c io.c procfs.c utils.c xml.c \ iso8601.c remote.c mainloop.c logging.c watchdog.c \ schemas.c strings.c xpath.c attrd_client.c alerts.c \ - operations.c pid.c results.c acl.c agents.c nvpair.c + operations.c pid.c results.c acl.c agents.c nvpair.c \ + output.c output_text.c output_xml.c if BUILD_CIBSECRETS libcrmcommon_la_SOURCES += cib_secrets.c endif #libcrmcommon_la_SOURCES += $(top_builddir)/lib/gnu/md5.c libcrmcommon_la_SOURCES += ../gnu/md5.c clean-generic: rm -f *.log *.debug *.xml *~ diff --git a/lib/common/output.c b/lib/common/output.c new file mode 100644 index 0000000000..534a377c03 --- /dev/null +++ b/lib/common/output.c @@ -0,0 +1,149 @@ +/* + * Copyright 2019 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include +#include +#include +#include +#include + +static GHashTable *formatters = NULL; + +void +pcmk__output_free(pcmk__output_t *out, crm_exit_t exit_status) { + pcmk__output_factory_t fn = g_hash_table_lookup(formatters, out->fmt_name); + CRM_ASSERT(fn != NULL); + + out->finish(out, exit_status); + out->free_priv(out); + + g_hash_table_destroy(out->messages); + free(out->fmt_name); + 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_err_unknown_format; + } + + *out = create(argv); + if (*out == NULL) { + return ENOMEM; + } + + if (fmt_name == NULL) { + (*out)->fmt_name = strdup("text"); + } else { + (*out)->fmt_name = strdup(fmt_name); + } + + if (filename == NULL || safe_str_eq(filename, "-")) { + (*out)->dest = stdout; + } else { + (*out)->dest = fopen(filename, "w"); + if ((*out)->dest == NULL) { + return errno; + } + } + + (*out)->messages = g_hash_table_new_full(crm_str_hash, g_str_equal, free, NULL); + + if ((*out)->init(*out) == false) { + pcmk__output_free(*out, 0); + return ENOMEM; + } + + return 0; +} + +bool +pcmk__parse_output_args(const char *argname, char *argvalue, char **output_ty, char **output_dest) { + if (safe_str_eq("output-as", argname)) { + *output_ty = argvalue; + return true; + } else if (safe_str_eq("output-to", argname)) { + if (safe_str_eq(argvalue, "-")) { + *output_dest = NULL; + } else { + *output_dest = argvalue; + } + + return true; + } + + return false; +} + +int +pcmk__register_format(const char *fmt_name, pcmk__output_factory_t create) { + if (create == NULL) { + return -EINVAL; + } + + if (formatters == NULL) { + formatters = g_hash_table_new_full(crm_str_hash, g_str_equal, NULL, NULL); + } + + g_hash_table_insert(formatters, strdup(fmt_name), create); + return 0; +} + +int +pcmk__call_message(pcmk__output_t *out, const char *message_id, ...) { + va_list args; + int rc = 0; + 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 (safe_str_eq(out->fmt_name, entry->fmt_name)) { + pcmk__register_message(out, entry->message_id, entry->fn); + } + } +} diff --git a/lib/common/output_text.c b/lib/common/output_text.c new file mode 100644 index 0000000000..76ae4ca4e1 --- /dev/null +++ b/lib/common/output_text.c @@ -0,0 +1,223 @@ +/* + * Copyright 2019 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include +#include +#include + +/* Disabled for the moment, but we can enable it (or remove it entirely) + * when we make a decision on whether this is preferred output. + */ +#define FANCY_TEXT_OUTPUT 0 + +typedef struct text_list_data_s { + unsigned int len; + char *singular_noun; + char *plural_noun; +} text_list_data_t; + +typedef struct text_private_s { + GQueue *parent_q; +} text_private_t; + +static void +text_free_priv(pcmk__output_t *out) { + text_private_t *priv = out->priv; + + if (priv == NULL) { + return; + } + + g_queue_free(priv->parent_q); + free(priv); +} + +static bool +text_init(pcmk__output_t *out) { + text_private_t *priv = NULL; + + /* If text_init was previously called on this output struct, just return. */ + if (out->priv != NULL) { + return true; + } else { + out->priv = calloc(1, sizeof(text_private_t)); + if (out->priv == NULL) { + return false; + } + + priv = out->priv; + } + + priv->parent_q = g_queue_new(); + return true; +} + +static void +text_finish(pcmk__output_t *out, crm_exit_t exit_status) { + /* This function intentionally left blank */ +} + +static void +text_reset(pcmk__output_t *out) { + CRM_ASSERT(out->priv != NULL); + + text_free_priv(out); + text_init(out); +} + +static void +text_subprocess_output(pcmk__output_t *out, int exit_status, + const char *proc_stdout, const char *proc_stderr) { + if (proc_stdout != NULL) { + fprintf(out->dest, "%s\n", proc_stdout); + } + + if (proc_stderr != NULL) { + fprintf(out->dest, "%s\n", proc_stderr); + } +} + +G_GNUC_PRINTF(2, 3) +static void +text_info(pcmk__output_t *out, const char *format, ...) { + va_list ap; + int len = 0; + + va_start(ap, format); + + /* Informational output does not get indented, to separate it from other + * potentially indented list output. + */ + len = vfprintf(out->dest, format, ap); + CRM_ASSERT(len > 0); + va_end(ap); + + /* Add a newline. */ + fprintf(out->dest, "\n"); +} + +static void +text_output_xml(pcmk__output_t *out, const char *name, const char *buf) { + text_private_t *priv = out->priv; + + CRM_ASSERT(priv != NULL); + pcmk__indented_printf(out, "%s", buf); +} + +static void +text_begin_list(pcmk__output_t *out, const char *name, const char *singular_noun, + const char *plural_noun) { + text_private_t *priv = out->priv; + text_list_data_t *new_list = NULL; + + CRM_ASSERT(priv != NULL); + +#if FANCY_TEXT_OUTPUT > 0 + pcmk__indented_printf(out, "%s:\n", name); +#endif + + new_list = calloc(1, sizeof(text_list_data_t)); + new_list->len = 0; + new_list->singular_noun = strdup(singular_noun); + new_list->plural_noun = strdup(plural_noun); + + g_queue_push_tail(priv->parent_q, new_list); +} + +static void +text_list_item(pcmk__output_t *out, const char *id, const char *content) { + text_private_t *priv = out->priv; + + CRM_ASSERT(priv != NULL); + +#if FANCY_TEXT_OUTPUT > 0 + if (id != NULL) { + pcmk__indented_printf(out, "* %s: %s\n", id, content); + } else { + pcmk__indented_printf(out, "* %s\n", content); + } +#else + fprintf(out->dest, "%s\n", content); +#endif + + ((text_list_data_t *) g_queue_peek_tail(priv->parent_q))->len++; +} + +static void +text_end_list(pcmk__output_t *out) { + text_private_t *priv = out->priv; + text_list_data_t *node = NULL; + + CRM_ASSERT(priv != NULL); + node = g_queue_pop_tail(priv->parent_q); + + if (node->singular_noun != NULL && node->plural_noun != NULL) { + if (node->len == 1) { + pcmk__indented_printf(out, "%d %s found\n", node->len, node->singular_noun); + } else { + pcmk__indented_printf(out, "%d %s found\n", node->len, node->plural_noun); + } + } + + free(node); +} + +pcmk__output_t * +pcmk__mk_text_output(char **argv) { + pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t)); + + if (retval == NULL) { + return NULL; + } + + retval->request = g_strjoinv(" ", argv); + retval->supports_quiet = true; + + retval->init = text_init; + retval->free_priv = text_free_priv; + retval->finish = text_finish; + retval->reset = text_reset; + + retval->register_message = pcmk__register_message; + retval->message = pcmk__call_message; + + retval->subprocess_output = text_subprocess_output; + retval->info = text_info; + retval->output_xml = text_output_xml; + + retval->begin_list = text_begin_list; + retval->list_item = text_list_item; + retval->end_list = text_end_list; + + return retval; +} + +G_GNUC_PRINTF(2, 3) +void +pcmk__indented_printf(pcmk__output_t *out, const char *format, ...) { + va_list ap; + int len = 0; +#if FANCY_TEXT_OUTPUT > 0 + int level = 0; + text_private_t *priv = out->priv; + + CRM_ASSERT(priv != NULL); + + level = g_queue_get_length(priv->parent_q); + + for (int i = 0; i < level; i++) { + putc('\t', out->dest); + } +#endif + + va_start(ap, format); + len = vfprintf(out->dest, format, ap); + CRM_ASSERT(len > 0); + va_end(ap); +} diff --git a/lib/common/output_xml.c b/lib/common/output_xml.c new file mode 100644 index 0000000000..c93d66ce4e --- /dev/null +++ b/lib/common/output_xml.c @@ -0,0 +1,257 @@ +/* + * Copyright 2019 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 _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include +#include + +typedef struct xml_private_s { + xmlNode *root; + GQueue *parent_q; +} xml_private_t; + +static void +xml_free_priv(pcmk__output_t *out) { + xml_private_t *priv = out->priv; + + if (priv == NULL) { + return; + } + + xmlFreeNode(priv->root); + g_queue_free(priv->parent_q); + free(priv); +} + +static bool +xml_init(pcmk__output_t *out) { + xml_private_t *priv = NULL; + + /* If xml_init was previously called on this output struct, just return. */ + if (out->priv != NULL) { + return true; + } else { + out->priv = calloc(1, sizeof(xml_private_t)); + if (out->priv == NULL) { + return false; + } + + priv = out->priv; + } + + priv->root = create_xml_node(NULL, "pacemaker-result"); + xmlSetProp(priv->root, (pcmkXmlStr) "api-version", (pcmkXmlStr) PCMK__API_VERSION); + + if (out->request != NULL) { + xmlSetProp(priv->root, (pcmkXmlStr) "request", (pcmkXmlStr) out->request); + } + + priv->parent_q = g_queue_new(); + g_queue_push_tail(priv->parent_q, priv->root); + + return true; +} + +static void +xml_finish(pcmk__output_t *out, crm_exit_t exit_status) { + xmlNodePtr node; + char *rc_as_str = NULL; + char *buf = NULL; + xml_private_t *priv = out->priv; + + /* If root is NULL, xml_init failed and we are being called from pcmk__output_free + * in the pcmk__output_new path. + */ + if (priv->root == NULL) { + return; + } + + rc_as_str = crm_itoa(exit_status); + + node = xmlNewTextChild(priv->root, NULL, (pcmkXmlStr) "status", + (pcmkXmlStr) crm_exit_str(exit_status)); + xmlSetProp(node, (pcmkXmlStr) "code", (pcmkXmlStr) rc_as_str); + + buf = dump_xml_formatted_with_text(priv->root); + fprintf(out->dest, "%s", buf); + + free(rc_as_str); + free(buf); +} + +static void +xml_reset(pcmk__output_t *out) { + char *buf = NULL; + xml_private_t *priv = out->priv; + + CRM_ASSERT(priv != NULL); + + buf = dump_xml_formatted_with_text(priv->root); + fprintf(out->dest, "%s", buf); + + free(buf); + xml_free_priv(out); + xml_init(out); +} + +static void +xml_subprocess_output(pcmk__output_t *out, int exit_status, + const char *proc_stdout, const char *proc_stderr) { + xmlNodePtr node, child_node; + char *rc_as_str = NULL; + xml_private_t *priv = out->priv; + CRM_ASSERT(priv != NULL); + + rc_as_str = crm_itoa(exit_status); + + node = xmlNewNode(g_queue_peek_tail(priv->parent_q), (pcmkXmlStr) "command"); + xmlSetProp(node, (pcmkXmlStr) "code", (pcmkXmlStr) rc_as_str); + + if (proc_stdout != NULL) { + child_node = xmlNewTextChild(node, NULL, (pcmkXmlStr) "output", + (pcmkXmlStr) proc_stdout); + xmlSetProp(child_node, (pcmkXmlStr) "source", (pcmkXmlStr) "stdout"); + } + + if (proc_stderr != NULL) { + child_node = xmlNewTextChild(node, NULL, (pcmkXmlStr) "output", + (pcmkXmlStr) proc_stderr); + xmlSetProp(node, (pcmkXmlStr) "source", (pcmkXmlStr) "stderr"); + } + + pcmk__xml_add_node(out, node); + free(rc_as_str); +} + +G_GNUC_PRINTF(2, 3) +static void +xml_info(pcmk__output_t *out, const char *format, ...) { + /* This function intentially left blank */ +} + +static void +xml_output_xml(pcmk__output_t *out, const char *name, const char *buf) { + xmlNodePtr parent = NULL; + xmlNodePtr cdata_node = NULL; + xml_private_t *priv = out->priv; + + CRM_ASSERT(priv != NULL); + + parent = xmlNewChild(g_queue_peek_tail(priv->parent_q), NULL, + (pcmkXmlStr) name, NULL); + cdata_node = xmlNewCDataBlock(getDocPtr(parent), (pcmkXmlStr) buf, strlen(buf)); + xmlAddChild(parent, cdata_node); +} + +static void +xml_begin_list(pcmk__output_t *out, const char *name, + const char *singular_noun, const char *plural_noun) { + xmlNodePtr list_node = NULL; + xml_private_t *priv = out->priv; + + CRM_ASSERT(priv != NULL); + + list_node = create_xml_node(g_queue_peek_tail(priv->parent_q), "list"); + xmlSetProp(list_node, (pcmkXmlStr) "name", (pcmkXmlStr) name); + g_queue_push_tail(priv->parent_q, list_node); +} + +static void +xml_list_item(pcmk__output_t *out, const char *name, const char *content) { + xml_private_t *priv = out->priv; + xmlNodePtr item_node = NULL; + + CRM_ASSERT(priv != NULL); + + item_node = xmlNewChild(g_queue_peek_tail(priv->parent_q), NULL, + (pcmkXmlStr) "item", (pcmkXmlStr) content); + xmlSetProp(item_node, (pcmkXmlStr) "name", (pcmkXmlStr) name); +} + +static void +xml_end_list(pcmk__output_t *out) { + char *buf = NULL; + xml_private_t *priv = out->priv; + xmlNodePtr node; + + CRM_ASSERT(priv != NULL); + + node = g_queue_pop_tail(priv->parent_q); + buf = crm_strdup_printf("%lu", xmlChildElementCount(node)); + xmlSetProp(node, (pcmkXmlStr) "count", (pcmkXmlStr) buf); + free(buf); +} + +pcmk__output_t * +pcmk__mk_xml_output(char **argv) { + pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t)); + + if (retval == NULL) { + return NULL; + } + + retval->request = g_strjoinv(" ", argv); + retval->supports_quiet = false; + + retval->init = xml_init; + retval->free_priv = xml_free_priv; + retval->finish = xml_finish; + retval->reset = xml_reset; + + retval->register_message = pcmk__register_message; + retval->message = pcmk__call_message; + + retval->subprocess_output = xml_subprocess_output; + retval->info = xml_info; + retval->output_xml = xml_output_xml; + + retval->begin_list = xml_begin_list; + retval->list_item = xml_list_item; + retval->end_list = xml_end_list; + + return retval; +} + +void +pcmk__xml_add_node(pcmk__output_t *out, xmlNodePtr node) { + xml_private_t *priv = out->priv; + + CRM_ASSERT(priv != NULL); + CRM_ASSERT(node != NULL); + + xmlAddChild(g_queue_peek_tail(priv->parent_q), node); +} + +void +pcmk__xml_push_parent(pcmk__output_t *out, xmlNodePtr parent) { + xml_private_t *priv = out->priv; + + CRM_ASSERT(priv != NULL); + CRM_ASSERT(parent != NULL); + + g_queue_push_tail(priv->parent_q, parent); +} + +void +pcmk__xml_pop_parent(pcmk__output_t *out) { + xml_private_t *priv = out->priv; + + CRM_ASSERT(priv != NULL); + CRM_ASSERT(g_queue_get_length(priv->parent_q) > 0); + + g_queue_pop_tail(priv->parent_q); +}